@cartiqo/discord-transcript
v0.1.1
Published
Generate beautiful, self-contained HTML transcripts of Discord channels, rendered with Discord's own message components.
Maintainers
Readme
@cartiqo/discord-transcript
Generate beautiful, self-contained HTML transcripts of Discord channels —
rendered with Discord's own message components via
@skyra/discord-components-core.
Give it plain message data (author, content, timestamp, attachments, embeds,
reactions) and get back a single HTML string you can save to a file, upload as an
attachment, or serve. No headless browser, no runtime dependencies to bundle —
the output is one portable .html document.
Install
npm install @cartiqo/discord-transcriptUsage
import { renderTranscript } from "@cartiqo/discord-transcript";
const html = renderTranscript({
guild: { name: "My Server", iconURL: "https://…/icon.png" },
channel: { name: "ticket-0042", topic: "Support ticket for @ada" },
messages: [
{
author: { name: "Ada", avatarURL: "https://…/ada.png", color: "#7c5cff" },
content: "Hey, I need help with **billing** — here's a screenshot.",
timestamp: new Date(),
attachments: [{ url: "https://…/shot.png", contentType: "image/png" }],
},
{
author: { name: "Support", bot: true, verified: true },
content: "On it! Give me a moment.",
timestamp: new Date(),
reply: { author: "Ada", content: "I need help with billing" },
},
],
});
// html is a complete <!doctype html> document.From discord.js
The package is framework-agnostic — it takes plain data — so you map your messages once:
import { renderTranscript, type TranscriptMessage } from "@cartiqo/discord-transcript";
const collection = await channel.messages.fetch({ limit: 100 });
const messages: TranscriptMessage[] = [...collection.values()]
.reverse()
.map((m) => ({
id: m.id,
author: {
id: m.author.id,
name: m.member?.displayName ?? m.author.username,
avatarURL: m.author.displayAvatarURL(),
bot: m.author.bot,
color: m.member?.displayHexColor,
},
content: m.content,
timestamp: m.createdAt,
edited: Boolean(m.editedAt),
attachments: [...m.attachments.values()].map((a) => ({
url: a.url,
name: a.name ?? undefined,
contentType: a.contentType ?? undefined,
width: a.width ?? undefined,
height: a.height ?? undefined,
size: a.size,
})),
embeds: m.embeds.map((e) => ({
title: e.title ?? undefined,
description: e.description ?? undefined,
url: e.url ?? undefined,
color: e.hexColor ?? undefined,
authorName: e.author?.name,
imageURL: e.image?.url,
thumbnailURL: e.thumbnail?.url,
footerText: e.footer?.text,
fields: e.fields.map((f) => ({ name: f.name, value: f.value, inline: f.inline })),
timestamp: e.timestamp ?? undefined,
})),
reactions: m.reactions.cache.map((r) => ({
emoji: r.emoji.name ?? "❓",
imageURL: r.emoji.imageURL() ?? undefined,
count: r.count,
})),
reply: m.reference
? (() => {
const ref = collection.get(m.reference!.messageId!);
return ref ? { author: ref.author.username, content: ref.content } : undefined;
})()
: undefined,
}));
const html = renderTranscript({ channel: { name: channel.name }, messages });Options
renderTranscript(options) accepts:
| Option | Type | Default | Description |
| ------------------- | ------------------- | ------------------ | -------------------------------------------------------- |
| messages | TranscriptMessage[] | — | The messages, oldest first. |
| channel | TranscriptChannel | — | Channel name / topic shown in the header. |
| guild | TranscriptGuild | — | Server name / icon shown in the header. |
| generatedAt | Date | new Date() | Export timestamp. |
| title | string | channel name | The document <title>. |
| light | boolean | false | Use the light theme. |
| footerCredit | boolean | true | Show a small footer credit. |
| componentsVersion | string | current | Pin the components version loaded from the CDN. |
Markdown
Message content and embed descriptions support a safe subset of Discord markdown: fenced and inline code, bold, italics, underline, ~~strikethrough~~, spoilers, block quotes, and autolinked URLs. All input is HTML-escaped first, so raw message text can never inject markup.
Rendering
The output loads @skyra/discord-components-core from a CDN (jsDelivr) to render
the message components, so opening the transcript needs a network connection the
first time. The surrounding page chrome (header, code blocks, links) is styled
inline and works offline.
