An exporter must own its renderer
TLDR
The industry default for rendering math in HTML is a CDN link to KaTeX or MathJax. That is the right call for an always-online web page and the wrong call for a tool whose output is a standalone HTML file. A document exporter must vendor its renderer and inline its fonts as data URIs, or every exported file silently depends on a network it was supposed to outlive. The extra bundle weight is the honest price of portability.The position
If your product emits HTML files that are supposed to work forever, anywhere, offline, then rendering math with a CDN link is a defect. Vendor the renderer, and inline the fonts at build time.
What the industry does and why
The standard pattern is a <link> tag to a public CDN, or a <script> tag that loads a renderer at runtime. It is cached across millions of pages, it is free bandwidth, and it is one line of markup. For a blog, a docs site or an app with a server, that is genuinely the best trade. The CDN is as reliable as the page that uses it, because both need the network anyway.
Why it fails for this problem class
An extracted document is not a web page, it is an artifact. The whole point of the export button is a file that opens anywhere: emailed, archived, opened from a USB stick, rendered by a CI job in a sandbox with no egress. A CDN link converts that artifact into a page that is only complete while the network is reachable. Fonts fail first and fail silently: the browser falls back to a system font and the math renders with wrong metrics, which in math rendering is worse than not rendering at all, because it looks almost right.
CSP is the second failure. Standalone files are opened in contexts with policies the exporter does not control, and a policy that blocks third-party fonts breaks the math with a console error the user never reads.
The better approach
Vendor the renderer in the build. Emit the math as HTML at assembly time, so the export needs no runtime script at all. Then generate the stylesheet at build time with every font inlined as a base64 data URI, woff2 only, the format that carries the best metrics per byte.
// Generated at build time: every font is a data URI, no network path survives
const css = readFile('katex.min.css');
const inlined = css.replace(/url\(fonts\/([^)]+\.woff2)\)/g, (m, f) => {
return url(data:font/woff2;base64,${readFile(f).toString('base64')});
});
Two details make this survive contact. The inlined stylesheet is emitted only when the export actually contains math, so a prose-only document pays nothing. And the generation is a build-time step, not a manual artifact, so the inlined fonts can never drift out of sync with the vendored renderer version.
What you give up
Bundle weight. The woff2 set is a few hundred kilobytes, and inlined base64 inflates it by a third. Cache sharing disappears: every exported file carries its own fonts instead of one shared CDN copy. A CDN also updates KaTeX for free across every consumer, where a vendored copy updates only when you bump it deliberately, which is exactly why the build-time generation step matters.
When the common pattern is right
When the page is always online, when the CDN budget is real, when the audience has never seen a broken font fallback. A SaaS dashboard, a documentation site, a blog. There, the CDN link is not a shortcut, it is the correct engineering. The rule is not "never use a CDN", it is "the exporter must own everything the export needs to render", because the export has no runtime to apologize for it.