Self-Hosting CDN
Configure Streamdown to self-host the CDN for offline, on-premise, or air-gapped environments.
Streamdown uses a hybrid approach for syntax highlighting: common languages and themes are bundled locally, while additional ones load on-demand from a CDN. By default, Streamdown loads assets from https://streamdown.ai/cdn. This guide shows you how to configure Streamdown for environments without internet access.
How CDN Loading Works
Languages
Streamdown includes 15 common languages bundled with the library for instant syntax highlighting:
- Web: JavaScript, TypeScript, JSX, TSX, HTML, CSS
- Data: JSON, YAML, TOML
- Shell: Bash, Shell Script
- Backend: Python, Go
- Markup: Markdown, SQL
For other languages (like Rust, Java, Ruby, Elixir), Streamdown loads syntax grammars on-demand from a CDN proxy.
Themes
Streamdown includes 2 themes bundled with the library:
github-light(default light theme)github-dark(default dark theme)
For other themes (like dracula, nord, one-dark-pro), Streamdown loads theme definitions on-demand from a CDN proxy.
Mermaid Diagrams
Mermaid is loaded entirely from a CDN when a diagram is rendered. If no mermaid diagrams are used, no mermaid code is downloaded.
In environments where you can't connect to the Streamdown CDN, CDN loading fails and code blocks fall back to plain text (for languages) or the default GitHub themes (for themes). Mermaid diagrams will show an error if the library cannot be loaded.
Configure Self-Hosted Assets
To self-host the CDN, you need to:
- Host Shiki and Mermaid files locally
- Configure your application to serve them
- Tell Streamdown where to find them
Step 1: Download Required Files
Install Shiki and Mermaid as dependencies:
npm install shiki mermaidThe files you need to serve are located in:
node_modules/shiki/dist/langs/- Language grammar filesnode_modules/shiki/dist/themes/- Theme definition filesnode_modules/mermaid/dist/mermaid.esm.min.mjs- Mermaid library
Step 2: Serve Files
Configure your framework to serve the Shiki and Mermaid files as static assets.
Next.js (App Router or Pages Router)
Add rewrites to your next.config.ts to serve files from node_modules:
import type { NextConfig } from "next";
const config: NextConfig = {
async rewrites() {
return [
{
source: "/cdn/shiki/:version/langs/:path*",
destination: "/node_modules/shiki/dist/langs/:path*",
},
{
source: "/cdn/shiki/:version/themes/:path*",
destination: "/node_modules/shiki/dist/themes/:path*",
},
{
source: "/cdn/mermaid/:version/:path*",
destination: "/node_modules/mermaid/dist/:path*",
},
];
},
};
export default config;Alternatively, copy the files to your public directory during build:
import { copyFileSync, mkdirSync, readdirSync } from "node:fs";
import { join } from "node:path";
const config: NextConfig = {
webpack: (config, { isServer }) => {
if (isServer) {
// Copy Shiki languages to public directory
const shikiLangsSource = join(
process.cwd(),
"node_modules/shiki/dist/langs"
);
const shikiLangsDest = join(process.cwd(), "public/shiki/langs");
mkdirSync(shikiLangsDest, { recursive: true });
const langFiles = readdirSync(shikiLangsSource).filter((f) =>
f.endsWith(".mjs")
);
for (const file of langFiles) {
copyFileSync(join(shikiLangsSource, file), join(shikiLangsDest, file));
}
// Copy Shiki themes to public directory
const shikiThemesSource = join(
process.cwd(),
"node_modules/shiki/dist/themes"
);
const shikiThemesDest = join(process.cwd(), "public/shiki/themes");
mkdirSync(shikiThemesDest, { recursive: true });
const themeFiles = readdirSync(shikiThemesSource).filter((f) =>
f.endsWith(".mjs")
);
for (const file of themeFiles) {
copyFileSync(
join(shikiThemesSource, file),
join(shikiThemesDest, file)
);
}
// Copy Mermaid to public directory
const mermaidSource = join(process.cwd(), "node_modules/mermaid/dist");
const mermaidDest = join(process.cwd(), "public/mermaid");
mkdirSync(mermaidDest, { recursive: true });
copyFileSync(
join(mermaidSource, "mermaid.esm.min.mjs"),
join(mermaidDest, "mermaid.esm.min.mjs")
);
}
return config;
},
};
export default config;Then use the cdnUrl prop to point to the local files:
import { Streamdown } from "streamdown";
export default function Page() {
return <Streamdown cdnUrl="/cdn">{markdown}</Streamdown>;
}Vite / Remix
Copy the Shiki files to your public directory and use the cdnUrl prop:
import { Streamdown } from "streamdown";
export default function Index() {
return <Streamdown cdnUrl="/cdn">{markdown}</Streamdown>;
}Express / Node.js Server
Serve the Shiki and Mermaid files as static assets:
const express = require("express");
const path = require("path");
const app = express();
// Serve Shiki language files from node_modules
app.use(
"/cdn/shiki/:version/langs",
express.static(path.join(__dirname, "node_modules/shiki/dist/langs"))
);
// Serve Shiki theme files from node_modules
app.use(
"/cdn/shiki/:version/themes",
express.static(path.join(__dirname, "node_modules/shiki/dist/themes"))
);
// Serve Mermaid files from node_modules
app.use(
"/cdn/mermaid/:version",
express.static(path.join(__dirname, "node_modules/mermaid/dist"))
);
app.listen(3000);Step 3: Configure Streamdown CDN Path
Pass the cdnUrl prop to the Streamdown component to specify the base path for all CDN assets:
import { Streamdown } from "streamdown";
const markdown = `
\`\`\`rust
fn main() {
println!("Hello, world!");
}
\`\`\`
`;
export default function Page() {
return <Streamdown cdnUrl="/cdn">{markdown}</Streamdown>;
}The cdnUrl prop sets the base path. Streamdown will automatically construct full paths:
- Shiki languages:
{cdnUrl}/shiki/{version}/langs/{language}.mjs - Shiki themes:
{cdnUrl}/shiki/{version}/themes/{theme}.mjs - Mermaid:
{cdnUrl}/mermaid/{version}/mermaid.esm.min.mjs - KaTeX CSS:
{cdnUrl}/katex/{version}/katex.min.css
KaTeX CSS for Math Rendering
If you use math rendering with KaTeX, Streamdown automatically loads the KaTeX CSS from:
{cdnUrl}/katex/{version}/katex.min.cssWhen you configure a custom cdnUrl, KaTeX CSS will be loaded from the same base path. Ensure your server serves the KaTeX CSS files at the expected path.
Self-Host KaTeX CSS
Configure your framework to serve KaTeX CSS alongside other CDN assets:
Next.js
const config: NextConfig = {
async rewrites() {
return [
{
source: "/cdn/shiki/:version/langs/:path*",
destination: "/node_modules/shiki/dist/langs/:path*",
},
{
source: "/cdn/shiki/:version/themes/:path*",
destination: "/node_modules/shiki/dist/themes/:path*",
},
{
source: "/cdn/katex/:version/:path*",
destination: "/node_modules/katex/dist/:path*",
},
{
source: "/cdn/mermaid/:version/:path*",
destination: "/node_modules/mermaid/dist/:path*",
},
];
},
};Copy to Public Directory
Alternatively, copy the KaTeX CSS to your public directory:
mkdir -p public/cdn/katex/0.16.22
cp node_modules/katex/dist/katex.min.css public/cdn/katex/0.16.22/katex.min.cssDisable CDN Loading
To completely disable CDN loading and only use bundled languages, pass cdnUrl={null}:
import { Streamdown } from "streamdown";
export default function Page() {
return <Streamdown cdnUrl={null}>{markdown}</Streamdown>;
}With this configuration:
- Only the 15 bundled languages will work (others fall back to plain text)
- Only the 2 bundled themes will work (
github-light,github-dark) - Mermaid diagrams will not render (Mermaid requires CDN loading)
- KaTeX CSS will not be loaded (math may render without proper styling)
Verify Configuration
To verify your self-hosted CDN configuration works:
- Disconnect from the internet
- Render a code block with a non-bundled language (like Rust or Ruby)
- Check the browser console for any CDN loading errors
- Verify the syntax highlighting appears correctly
Example test code:
import { Streamdown } from "streamdown";
const markdown = `
\`\`\`rust
fn main() {
println!("Hello, world!");
}
\`\`\`
`;
export default function Page() {
return <Streamdown>{markdown}</Streamdown>;
}If configured correctly, the Rust code should have syntax highlighting even without internet access.
Best Practices
Bundle Additional Languages
If you consistently use specific non-bundled languages, consider creating a custom build that includes them. This provides instant loading without CDN requests.
You can fork Streamdown and modify packages/streamdown/lib/code-block/bundled-languages.ts to include additional languages.
Use a Local CDN Mirror
For large deployments, consider setting up a local CDN mirror that serves assets from an internal server:
<Streamdown cdnUrl="https://internal-cdn.company.com/cdn">
{markdown}
</Streamdown>The internal server should mirror the same path structure as the default CDN.
Monitor Failed Loads
Check browser console for warnings about failed language loads:
[Streamdown] Failed to load language "rust" from CDN: Network errorThese warnings indicate which languages need to be self-hosted for your use case.
Troubleshooting
Language Files Not Loading
- Verify the CDN path points to the correct directory
- Check that
.mjsfiles are being served with the correct MIME type - Ensure the file permissions allow reading
- Check browser network tab for 404 errors
MIME Type Errors
Some servers don't recognize .mjs files. Configure your server to serve them as JavaScript:
Express:
express.static.mime.define({ "application/javascript": ["mjs"] });Nginx:
types {
application/javascript mjs;
}Cached Failed Requests
If you previously tried loading a language before setting up self-hosting, clear the cache:
// Reload the page after setting up self-hosting
if (typeof window !== "undefined") {
window.location.reload();
}Or clear browser cache manually.