Create secure, responsive, standalone HTML transcripts from Discord channels or already loaded
discord.js messages. The package targets discord.js 14.27.x and renders classic interactive
message components as well as Components V2 display components.
The generated document contains its CSS and requires no JavaScript, framework, CDN stylesheet, or web font. It supports light, dark, automatic, mobile, and print layouts.
Repository note: the repository, homepage, and issue URLs in
package.jsonuse the visibleexample.invalidplaceholder because no canonical public repository URL was available when this package was created. Replace those metadata values before the first publication.
- Node.js 20 or newer
discord.js^14.27.0ViewChannelandReadMessageHistorypermissions when fetching a channel
npm install @fmfl-devteam/discord-html-transcripts discord.jspnpm add @fmfl-devteam/discord-html-transcripts discord.jsyarn add @fmfl-devteam/discord-html-transcripts discord.jsimport { createTranscript } from "@fmfl-devteam/discord-html-transcripts";
const transcript = await createTranscript(channel, {
filename: `transcript-${channel.id}.html`,
theme: "dark",
includeComponents: true,
includeAttachments: true,
});
await targetChannel.send({
files: [transcript.attachment],
});createTranscript() fetches up to 100 messages per request, follows the oldest message ID with
before, removes duplicate IDs, and produces a stable oldest-to-newest transcript. The default
maximum is 10,000 messages. Discord API and permission failures are wrapped in a
TranscriptFetchError with channel and permission context.
import { generateFromMessages } from "@fmfl-devteam/discord-html-transcripts";
const transcript = await generateFromMessages(messages, {
title: "Ticket Transcript",
theme: "auto",
});The input may be a Collection<string, Message> or a readonly Message[]. Input order does not
matter; messages are sorted chronologically. A limit keeps the newest N messages.
import { writeFile } from "node:fs/promises";
import { createTranscript } from "@fmfl-devteam/discord-html-transcripts";
const transcript = await createTranscript(channel, {
title: "Audit Transcript",
poweredBy: false,
});
await writeFile(transcript.filename, transcript.html, "utf8");interface TranscriptOptions {
limit?: number;
filename?: string;
title?: string;
description?: string;
includeSystemMessages?: boolean;
includeAttachments?: boolean;
includeComponents?: boolean;
includeEmbeds?: boolean;
includeReactions?: boolean;
includeAvatars?: boolean;
includeUserIds?: boolean;
includeChannelId?: boolean;
poweredBy?: boolean;
saveImages?: boolean;
maxInlineMediaBytes?: number;
maxSingleMediaBytes?: number;
theme?: "light" | "dark" | "auto";
timezone?: string;
locale?: string;
generatedAt?: Date;
}| Option | Default | Description |
|---|---|---|
limit |
10000 |
Newest messages to fetch or retain; integer from 1 through 10,000. |
filename |
transcript.html |
Attachment filename. Unsafe filesystem characters are replaced and .html is appended. |
title |
Channel-based title | Transcript heading and document title. |
description |
none | Plain-text description in the document header. |
includeSystemMessages |
true |
Include Discord system event messages. |
includeAttachments |
true |
Render image, video, audio, text, and generic file attachments. |
includeComponents |
true |
Render classic components and Components V2. |
includeEmbeds |
true |
Render rich embeds. |
includeReactions |
true |
Render reaction emoji and counts. |
includeAvatars |
true |
Render author avatars; initials are used otherwise. |
includeUserIds |
false |
Add author IDs as data-author-id metadata. |
includeChannelId |
false |
Include the channel ID in header metadata. |
poweredBy |
true |
Include the package attribution footer. |
saveImages |
false |
Inline supported Discord-CDN raster images as data URLs. |
maxInlineMediaBytes |
26214400 |
Total inlined-image budget, at most 100 MiB. |
maxSingleMediaBytes |
8388608 |
Per-image budget, at most 100 MiB. |
theme |
auto |
Light, dark, or operating-system color scheme. |
timezone |
UTC |
IANA timezone used for message and embed timestamps. |
locale |
en-US |
Intl locale used for dates and numbers. |
generatedAt |
current time | Explicit archive creation time, useful for deterministic exports. |
The result contains the complete html, the sanitized filename, and a ready-to-send discord.js
AttachmentBuilder.
Message components are not assumed to be Action Rows. discord.js structures are converted through
their toJSON() representation and normalized into a recursive discriminated union.
Supported message component types from discord.js 14.27.x and Discord API v10:
- Action Rows with Primary, Secondary, Success, Danger, Link, and Premium buttons
- String, User, Role, Mentionable, and Channel Select menus
- Containers with accent color, spoiler state, and ordered nested content
- Text Displays with the same safe Discord Markdown renderer as message content
- Sections containing Text Displays with a Button or Thumbnail accessory
- Separators with Small or Large spacing and visible or invisible dividers
- responsive Media Galleries with item descriptions and per-item spoilers
- File display components, including
attachment://filenameresolution - Thumbnails with descriptions, spoilers, and unavailable-media fallbacks
Modal-only Text Inputs, Labels, File Uploads, Radio Groups, and Checkboxes are not treated as message components. Unknown future message component types render a small static fallback and never abort the transcript.
Messages include author and display names, bot/webhook badges, optional IDs, avatars, timestamps, edits, pins, TTS, system events, threads, replies, unavailable references, embeds, attachments, and reactions. Replies are resolved when the referenced message is present in the provided/fetched set.
The Markdown renderer supports bold, italic, underline, strikethrough, spoilers, inline and fenced code, headings, lists, blockquotes, masked and plain links, Discord timestamps, user/role/channel mentions, Unicode emoji, and custom Discord emoji.
All Discord-controlled strings are HTML-escaped before markup is produced. The renderer never
passes message HTML through, does not emit scripts, and adds a restrictive Content Security Policy.
URLs are parsed centrally and allow only HTTPS for embedded media; ordinary links additionally
allow HTTP. javascript:, arbitrary data:, file:, malformed attachment references, inline styles
from message data, and SVG/data payloads supplied by messages are rejected. Generated links use
noopener, noreferrer, and nofollow.
saveImages is deliberately constrained:
- only HTTPS hosts in Discord's
discordapp.comanddiscordapp.netmedia domains are fetched; - redirects are rejected;
- only PNG, JPEG, GIF, WebP, and AVIF response types are accepted;
- response streams stop at the configured per-item and total byte budgets;
- failures leave the original secure URL in place and do not fail transcript generation.
Inlining makes the HTML larger. The generated file can still reference external HTTPS media when inlining is disabled, a file exceeds its budget, or a CDN request fails.
- Discord CDN links can expire and externally hosted media can disappear.
- Only referenced messages included in the input set can be expanded; missing messages use a safe unavailable-reference marker and no extra API request is made.
- Discord Markdown edge cases can evolve independently of this package. The renderer intentionally favors predictable, escaped output over executing arbitrary HTML or matching every client quirk.
- Media fallbacks are structural. A standalone HTML file cannot detect a later image load failure without JavaScript.
saveImagesdoes not inline videos, audio, SVG, arbitrary hosts, or generic files.
npm install
npm run lint
npm run typecheck
npm test
npm run build
npm pack --dry-runnpm test builds first so the test suite also validates the scoped package-root import. See
CONTRIBUTING.md for fixture and pull-request expectations. A complete discord.js bot example is in
examples/basic.ts. Biome performs formatting, import organization, and lint checks through
npm run format and npm run lint.
This package is configured as the public scoped package
@fmfl-devteam/discord-html-transcripts. Tagged releases are published by
.github/workflows/release.yml. A tag must be exactly v followed by the version in package.json.
The workflow runs the complete prepublish check, publishes the public npm package, and creates a
GitHub Release with generated release notes.
Configure npm Trusted Publishing for this package with these values:
- Provider: GitHub Actions
- Organization:
fmfl-devteam - Repository:
discord-html-transcripts - Workflow filename:
release.yml - Allowed action:
npm publish
Trusted Publishing uses short-lived OIDC credentials and requires no permanent npm token. The
workflow uses a GitHub-hosted Node.js 24 runner and has the required id-token: write permission.
If the package has not been published before and npm does not yet allow configuring its Trusted
Publisher, add a granular publishing token temporarily as the NPM_TOKEN repository secret for the
initial release. Configure Trusted Publishing afterward and delete that secret.
Create a release by updating the version and pushing its generated tag:
npm version patch
git push origin main --follow-tagsUse minor or major instead of patch when appropriate. prepublishOnly runs Biome, strict
typechecking, tests, and the dual ESM/CommonJS build. Workflow retries are safe: an existing npm
version and an existing GitHub Release are detected and skipped. Never commit npm tokens or put them
in workflow files.
Contributions are welcome under the process in CONTRIBUTING.md. Changes to Discord structures
should use payloads from Discord API v10 and types/classes exported by the supported discord.js
version, with security and snapshot coverage for renderer changes.
MIT © Fmfl-Devteam. See LICENSE.