LLM-generated markdown differs from human-written markdown in five specific ways that break conventional converters. Chrome's own developer documentation warns that naively parsing LLM markdown with a parser that allows HTML and assigning it to innerHTML lets prompt-injected payloads execute β an XSS vector that turns a report generator into a security hole [1]. The guidance specifies that LLM output must be treated as user-generated content and sanitized with tools like DOMPurify or sanitize-html, with chunks sanitized in combination rather than isolation because dangerous code can be split across chunks [1].
A long JSON configuration or Python script overflows print layouts or gets sliced mid-block, leaving the bottom half of a function on one page and the remainder on the next without context. Emoji become empty rectangles when the renderer lacks proper font stacks. GitHub Flavored Markdown alerts, YAML front matter, and math blocks sit outside CommonMark and break layouts entirely. An open-source markdown renderer maintainer documented in October 2026 that LLM-generated YAML front matter renders as a stray horizontal rule followed by a paragraph of raw key/value text [2]. GitHub alerts β the > [!NOTE], > [!WARNING] blocks that LLMs use constantly β render as plain blockquotes with the literal [!NOTE] marker still visible [2]. Inconsistent heading depth from one LLM response to the next wrecks table of contents generation and document navigation.
Convert: Anything to PDF is a free, account-free browser extension that processes everything locally, with no API and no template system. markdown2pdf.ai converts via LaTeX rather than HTML-to-PDF, charges per PDF through Bitcoin Lightning, and targets AI agents rather than end-user applications.
The standard developer response is md-to-pdf, a CLI tool that converts Markdown to PDF using Marked for markdown-to-HTML and Puppeteer for HTML-to-PDF, with highlight.js for code highlighting. It has about 2,000 GitHub stars and its whole source is only ~250 lines of JS, ~500 lines of TypeScript, and ~100 lines of CSS β too thin to handle the edge cases that LLM output creates.
The tool's own documentation warns that user-provided markdown content must be sanitized before processing.
Puppeteer header and footer templates default to 1pt font-size and require sufficient page margin or the header/footer can be overlaid by content. On serverless deployments, Puppeteer brings cold starts, font bundling complexity, and CORS failures that break asset loading [3].
Math blocks with currency amounts like $5 and $10 get misidentified as math delimiters unless your parser implements the GitHub/Pandoc rule that no whitespace may appear directly inside delimiters [2]. Every one of these issues requires pre-processing that md-to-pdf does not provide.
Sanitize LLM markdown as user-generated content, not trusted input. Strip or escape raw HTML, remove script tags and event handlers, and validate that what remains is safe markdown rather than HTML-in-markdown.
Normalize heading depth and strip or convert front matter before sending. Flatten heading hierarchy to a consistent baseline. Remove YAML front matter entirely or convert it to a styled header block. Transform GitHub alerts into styled divs or blockquotes with proper icons and color coding. Handle math delimiters with currency-aware parsing.
Use a hosted API that handles markdown, custom CSS, and reusable templates. MarkupGo's /pdf endpoint accepts source.type: 'markdown' [4] alongside html, template, and url source types, with options including headers, footers, page properties, and metadata [4]. The markdown source supports padding, custom CSS, and a dark theme option [4]. Setting singlePage to true generates a single-page PDF with automatic height adjustment [4]. PDF headers and footers are supplied as complete HTML documents and support injected values like pageNumber and totalPages [4]. Generated PDFs can be set to auto-delete via an expiration field, with expiresInSeconds ranging from 60 seconds to 90 days [4]. Reusable templates can be invoked by template ID with a context object, and template options in the request override dashboard-configured options [4].
Authentication uses an x-api-key header with POST to /api/v1/pdf [4].
const response = await fetch('/api/v1/pdf', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.MARKUPGO_API_KEY
},
body: JSON.stringify({
source: {
type: 'markdown',
data: {
markdown: sanitizedLlmOutput,
padding: 24,
css: 'body { font-family: system-ui, -apple-system, sans-serif; } code { font-size: 13px; }',
dark: false
}
},
options: {
properties: {
size: { width: 210, height: 297 },
margins: { top: 15, bottom: 15, left: 15, right: 15 }
},
header: '<html><head><style>body { font-size: 10px; margin: auto 20px; color: #666; }</style></head><body><p>Acme Corp β Confidential</p></body></html>',
footer: '<html><head><style>body { font-size: 10px; margin: auto 20px; }</style></head><body><p><span class="pageNumber"></span> of <span class="totalPages"></span></p></body></html>'
},
expiration: { expiresInSeconds: 86400 }
})
});
const { url } = await response.json();
// url contains the rendered PDF, auto-deleted after 24 hours
For brand consistency across all generated reports, move the CSS and header/footer HTML into a reusable template in the MarkupGo dashboard, then reference it by ID:
source: {
type: 'template',
data: {
id: 'YOUR_TEMPLATE_ID',
context: {
reportTitle: 'Q4 Security Audit',
generatedAt: new Date().toISOString()
}
}
}
Keep PDF generation in-house only if you need offline-only operation or heavy custom Chromium tuning that no hosted API exposes. This is rare for LLM-powered applications, which are already network-dependent.
Hand it off when you have variable LLM output, need brand consistency across thousands of generated documents, or run on serverless infrastructure where Puppeteer cold starts and font bundling become operational burdens [3].
If your tool handles fewer than three of these automatically, you are already doing the work yourself.