Rasterize content

Slack has no Mermaid and no LaTeX. slackmark-node renders both through a local Chromium and uploads the resulting PNG assets through the configured uploader. It returns Slack file-backed image blocks; your application posts the message payload. This is the message the quickstart below produces:

```mermaid
flowchart LR
  Agent --> Slack
```

$$
x^2 + y^2 = z^2
$$
The quickstart program's markdown, converted. Recorded from a real slackmark-node run rather than produced live, so the pictures are the renderer's own PNGs.
Block Kit JSON

Chromium cannot run in a browser, so this figure replays a committed recording of a real createNodeConverter() run (slackmark-node with mermaid 11.16.0, KaTeX 0.18.1, Chromium via puppeteer 25.3.0). Regenerate it with pnpm docs:raster-examples. The pictures are painted from PNG bytes committed to this repo (528×140, 5 kB, renderer bytes; 324×126, 4 kB, renderer bytes), substituted for the payload's image reference because a browser cannot fetch it. A capturing stub stood in for the Slack uploader, so the slack_file IDs below are synthetic; a real upload returns workspace file IDs. Nothing else was touched: the JSON below is what convert() returned.

[
  {
    "type": "image",
    "alt_text": "mermaid diagram",
    "slack_file": {
      "id": "F09DOCS0001"
    }
  },
  {
    "type": "image",
    "alt_text": "display math",
    "slack_file": {
      "id": "F09DOCS0002"
    }
  }
]

Install

pnpm add slackmark slackmark-node

Requires Node.js 22.12+. This complete flow uses standard fetch; install a Slack SDK only if your app otherwise needs it.

Create a bot with chat:write and files:write, invite it to the channel, save the first program below as quickstart.mjs, then run:

SLACK_BOT_TOKEN=xoxb-… SLACK_CHANNEL_ID=C… \
  node quickstart.mjs

Quickstart

Happy path only, with no retry or crash recovery:

quickstart.mjs
import { FetchSlackUploader } from "slackmark/slack";
import { createNodeConverter } from "slackmark-node";

const token = process.env["SLACK_BOT_TOKEN"];
const channel = process.env["SLACK_CHANNEL_ID"];
if (!token || !channel) throw new Error("SLACK_BOT_TOKEN and SLACK_CHANNEL_ID required");

const markdown = `\`\`\`mermaid
flowchart LR
  Agent --> Slack
\`\`\`

$$
x^2 + y^2 = z^2
$$`;

const converter = createNodeConverter({ uploader: new FetchSlackUploader({ token }) });
try {
  const { blocks, text } = await converter.convert(markdown, { timeoutMs: 30_000 });
  const response = await fetch("https://slack.com/api/chat.postMessage", {
    method: "POST",
    headers: {
      authorization: `Bearer ${token}`,
      "content-type": "application/json; charset=utf-8",
    },
    body: JSON.stringify({ channel, blocks, text }),
  });
  const body = await response.json();
  if (!response.ok || body.ok !== true) throw new Error("Slack rejected the post");
} finally {
  await converter.close();
}

Shut down cleanly

For production, the complete program below separates a definite rejection from an ambiguous timeout or lost response, and preserves a conversion error and a close error together. That last part is the shape worth copying — converter is the one built above, and postPrepared is the prepared-envelope call the full file carries:

post-rendered-markdown.ts · excerptView on GitHub
let primaryError: unknown;
try {
  const result = await converter.convert(markdown, { timeoutMs: CONVERT_TIMEOUT_MS });
  await postPrepared(token, {
    channel,
    blocks: result.blocks,
    text: result.text,
    receipts: result.receipts,
  });
} catch (error) {
  primaryError = error;
  throw error;
} finally {
  try {
    await converter.close({ behavior: "drain", timeoutMs: CLOSE_TIMEOUT_MS });
  } catch (closeError) {
    if (primaryError !== undefined) {
      throw new AggregateError([primaryError, closeError], "Operation and close both failed");
    }
    throw closeError;
  }
}

That is the same prepared-envelope pattern as Getting started, and it is why a close failure can never quietly replace the error that actually broke the run.

On post failure, the thrown error retains the exact prepared envelope. Upload to Slack covers retry ambiguity, optional crash-recovery persistence, and sensitive receipts without repeating that policy here.

Next: Custom renderers, Upload to Slack, and Production.