01 · Diagnose the problem

Turning Markdown into a file that renders the same everywhere

Markdown is not a document, it is instructions for making one. The moment the file leaves your process - into an email client, an archive, a ticket, another team's viewer - the renderer changes, and with it the tables, the code blocks and the task lists.

4 min readPublished Updated
What you actually see

The release notes that looked right in the app arrive with tables collapsed into runs of pipes, or as unstyled black-on-white text that nobody wants to read.

Reproduce it

  1. 01Render the same Markdown in two viewers - a code host and a mail client - and compare the table and the task list, which are the two extensions that most often go missing.
  2. 02Save the rendered page and reopen the saved file offline: a stylesheet loaded from a URL leaves an unstyled document behind.
  3. 03Forward the file to someone outside your network and see which assets fail to load for them.
02 / Do it yourself

Inline the stylesheet before the file leaves your process

Parse the Markdown with a spec-compliant parser, put the result inside a full document, and inline the stylesheet so nothing is fetched at open time.

standalone.mts
import { readFile, writeFile } from "node:fs/promises";
import { marked } from "marked";

const body = marked.parse(await readFile("notes.md", "utf8"), { gfm: true });
const css = await readFile("document.css", "utf8");

await writeFile(
  "notes.html",
  `<!doctype html><html lang="en"><head><meta charset="utf-8">
<style>${css}</style></head><body><main>${body}</main></body></html>`
);

// Nothing is fetched when this file is opened: no font URL, no stylesheet,
// no image host. That is what makes it survive being forwarded.
03 / Where it breaks

Where a single file stops being enough

The call above is small. Everything around it is the system — and the system is what you would be signing up to own.

The Markdown links remote images

Inlining the stylesheet does not inline the pictures. An image referenced by URL still fails for a reader who is offline or behind a different network.

The parser is not the recipient's parser

Tables and task lists are GitHub extensions, not CommonMark. Rendering them yourself is precisely what stops the recipient's viewer from deciding whether they exist.

Untrusted Markdown carries HTML

Markdown allows raw HTML through, so content from a user is a script injection surface unless it is sanitized before it becomes a document.

The artifact needs to be paginated

A single HTML file has no pages, headers or footers. When the recipient will print it or archive it, PDF is the format that carries that structure.

Keep the DIY version when

  • The Markdown is yours, the audience opens it in a browser, and a build step already produces the file.
  • The document is short-lived - a preview inside your own app - and never leaves the environment that rendered it.
04 / The recipe

Turn Markdown into a self-contained HTML file

Send the Markdown and take the document back. The template is deterministic and versioned, so the same source always produces the same file, and the stylesheet is already inside it.

Capture settings
Enter the source, choose your options, then run the capture.
Result
Your capture will appear here and stay in view.
waiting

No result yet

Complete the settings and run the tool. Images, PDFs, text, and video all preview in this panel.

Send this exact request
cURL, TypeScript and Python are generated from the same configuration as the demo.
curl --fail-with-body "https://api.pagecapture.dev/v1/take" \
  -H "X-Access-Key: $PAGECAPTURE_KEY" \
  -H "Content-Type: application/json" \
  --data '{"markdown":"# Release notes\n\n**v2.4** ships the report generator.\n\n| Change | Impact |\n| --- | --- |\n| Batch capture | Fewer round trips |\n\n- [x] Docs updated\n- [ ] Blog post","format":"html"}' \
  -o capture.html
05 / Output, limits, cost, failures

What a self-contained document contains

Output

One HTML document with the template stylesheet inlined: CommonMark plus GitHub tables and task lists, serialized from the rendered DOM.

Cost

One successful, non-cached capture costs one credit. Failures, including platform failures, cost zero; cache hits cost zero.

1 creditper successful non-cached capture

Limits

  • The template is fixed so the output stays reproducible; your own CSS goes in through the styles parameter rather than by editing it.
  • Images referenced by URL stay references - the document is self-contained in styling, not in media.
  • The same source and the same template version always produce the same file; a template change moves the engine version with it.

Common errors

The Markdown carries large embedded data URLs and the request body passes the limit.

HTTP 413 · terminal

timeout_errornever billed

A remote image or font referenced by the content never resolves and the capture budget runs out.

HTTP 500 · retryable

06 / Take it for a run

Try it against your own page

Swap the example for a page you care about. The runner keeps every option from this guide, and carries the whole configuration into the playground — no retyping, no starting over.

Return to the configured runner
07 / Verification
Tested and reviewed

Written by PageCapture Engineering. Reviewed by PageCapture render maintainers. Verified 2026-08-26 by running the published code against a fixture.

The recipe was normalized through the public contract and its Markdown rendered through the template used by the engine; the CI fixture run has not executed this piece yet.

engine-crunknown-pwunknown-f2026-07-1-b2026-08-1StatusInline Markdown input referenceEditorial method

Sources and verification basis

Sources support the browser and API behaviors named above. PageCapture-specific limits and billing are taken from the public contract; external sources are used for the underlying browser behavior.

  1. Defines the base syntax the template parses.

  2. Defines the table and task list extensions cited as the ones that go missing.