Skip to content

Commit ee12ec8

Browse files
committed
Resolves #297
1 parent 6bd211d commit ee12ec8

File tree

8 files changed

+502
-26
lines changed

8 files changed

+502
-26
lines changed

.changeset/good-moons-happen.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"streamdown": patch
3+
---
4+
5+
Add support for CDN offline mode

apps/website/content/docs/code-blocks.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ All other languages are loaded on-demand when first used, including:
4646

4747
Languages are cached after the first load, so subsequent uses are instant.
4848

49+
For offline or air-gapped environments, see [Offline Mode](/docs/offline-mode) to learn how to self-host language files.
50+
4951
### Language Examples
5052

5153
#### TypeScript
Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
---
2+
title: Offline Mode
3+
description: Configure Streamdown for offline, on-premise, or air-gapped environments.
4+
---
5+
6+
Streamdown uses a hybrid approach for syntax highlighting: common languages are bundled locally, while additional languages load on-demand from a CDN. This guide shows you how to configure Streamdown for environments without internet access.
7+
8+
## How Language Loading Works
9+
10+
Streamdown includes 15 common languages bundled with the library for instant syntax highlighting:
11+
12+
- **Web**: JavaScript, TypeScript, JSX, TSX, HTML, CSS
13+
- **Data**: JSON, YAML, TOML
14+
- **Shell**: Bash, Shell Script
15+
- **Backend**: Python, Go
16+
- **Markup**: Markdown, SQL
17+
18+
For other languages (like Rust, Java, Ruby, Elixir), Streamdown loads syntax grammars on-demand from jsDelivr through a CDN proxy. In offline environments, this loading fails and code blocks fall back to plain text.
19+
20+
## Configure Self-Hosted Assets
21+
22+
To run Streamdown offline, you need to:
23+
24+
1. Host Shiki language files locally
25+
2. Configure your application to serve them
26+
3. Tell Streamdown where to find them
27+
28+
### Step 1: Download Shiki Language Files
29+
30+
Install Shiki as a dependency (if not already installed):
31+
32+
```package-install
33+
npm install shiki
34+
```
35+
36+
The language grammar files are located in `node_modules/shiki/dist/langs/`. You need to serve these files from your application.
37+
38+
### Step 2: Serve Language Files
39+
40+
Configure your framework to serve the Shiki language files as static assets.
41+
42+
#### Next.js (App Router or Pages Router)
43+
44+
Add a rewrite to your `next.config.ts` to serve files from `node_modules`:
45+
46+
```tsx title="next.config.ts"
47+
import type { NextConfig } from 'next';
48+
49+
const config: NextConfig = {
50+
async rewrites() {
51+
return [
52+
{
53+
source: '/cdn/shiki/:version/langs/:path*',
54+
destination: '/node_modules/shiki/dist/langs/:path*',
55+
},
56+
];
57+
},
58+
};
59+
60+
export default config;
61+
```
62+
63+
Alternatively, copy the files to your `public` directory during build:
64+
65+
```tsx title="next.config.ts"
66+
import { copyFileSync, mkdirSync } from 'node:fs';
67+
import { join } from 'node:path';
68+
69+
const config: NextConfig = {
70+
webpack: (config, { isServer }) => {
71+
if (isServer) {
72+
// Copy Shiki languages to public directory
73+
const shikiLangsSource = join(process.cwd(), 'node_modules/shiki/dist/langs');
74+
const shikiLangsDest = join(process.cwd(), 'public/shiki/langs');
75+
76+
mkdirSync(shikiLangsDest, { recursive: true });
77+
78+
// Copy all .mjs files
79+
const { readdirSync } = require('node:fs');
80+
const files = readdirSync(shikiLangsSource).filter(f => f.endsWith('.mjs'));
81+
82+
for (const file of files) {
83+
copyFileSync(
84+
join(shikiLangsSource, file),
85+
join(shikiLangsDest, file)
86+
);
87+
}
88+
}
89+
return config;
90+
},
91+
};
92+
93+
export default config;
94+
```
95+
96+
Then use the `cdnUrl` prop to point to the local files:
97+
98+
```tsx title="app/page.tsx"
99+
import { Streamdown } from 'streamdown';
100+
101+
export default function Page() {
102+
return (
103+
<Streamdown cdnUrl="/shiki/langs">
104+
{markdown}
105+
</Streamdown>
106+
);
107+
}
108+
```
109+
110+
#### Vite / Remix
111+
112+
Copy the Shiki language files to your `public` directory and use the `cdnUrl` prop:
113+
114+
```tsx title="app/routes/_index.tsx"
115+
import { Streamdown } from 'streamdown';
116+
117+
export default function Index() {
118+
return (
119+
<Streamdown cdnUrl="/shiki/langs">
120+
{markdown}
121+
</Streamdown>
122+
);
123+
}
124+
```
125+
126+
#### Express / Node.js Server
127+
128+
Serve the Shiki language files as static assets:
129+
130+
```javascript title="server.js"
131+
const express = require('express');
132+
const path = require('path');
133+
134+
const app = express();
135+
136+
// Serve Shiki language files from node_modules
137+
app.use(
138+
'/cdn/shiki/:version/langs',
139+
express.static(path.join(__dirname, 'node_modules/shiki/dist/langs'))
140+
);
141+
142+
app.listen(3000);
143+
```
144+
145+
### Step 3: Configure Streamdown CDN Path
146+
147+
Pass the `cdnUrl` prop to the Streamdown component to specify where language files should be loaded from:
148+
149+
```tsx title="app/page.tsx"
150+
import { Streamdown } from 'streamdown';
151+
152+
const markdown = `
153+
\`\`\`rust
154+
fn main() {
155+
println!("Hello, world!");
156+
}
157+
\`\`\`
158+
`;
159+
160+
export default function Page() {
161+
return (
162+
<Streamdown cdnUrl="/shiki/langs">
163+
{markdown}
164+
</Streamdown>
165+
);
166+
}
167+
```
168+
169+
The CDN path should point to the directory containing the `.mjs` language files. Streamdown will append the language name and `.mjs` extension automatically.
170+
171+
## KaTeX CSS for Math Rendering
172+
173+
If you use math rendering with KaTeX, you also need to self-host the KaTeX CSS file. By default, Streamdown loads it from:
174+
175+
```
176+
/cdn/katex/{version}/katex.min.css
177+
```
178+
179+
### Self-Host KaTeX CSS
180+
181+
Configure your framework to serve KaTeX CSS:
182+
183+
#### Next.js
184+
185+
```tsx title="next.config.ts"
186+
const config: NextConfig = {
187+
async rewrites() {
188+
return [
189+
{
190+
source: '/cdn/shiki/:version/langs/:path*',
191+
destination: '/node_modules/shiki/dist/langs/:path*',
192+
},
193+
{
194+
source: '/cdn/katex/:version/:path*',
195+
destination: '/node_modules/katex/dist/:path*',
196+
},
197+
];
198+
},
199+
};
200+
```
201+
202+
#### Copy to Public Directory
203+
204+
Alternatively, copy the KaTeX CSS to your `public` directory:
205+
206+
```bash
207+
cp node_modules/katex/dist/katex.min.css public/katex/katex.min.css
208+
```
209+
210+
And update the CDN path in the Streamdown source or create a custom fork.
211+
212+
## Disable CDN Loading
213+
214+
To completely disable CDN loading and only use bundled languages, pass `cdnUrl={null}`:
215+
216+
```tsx title="app/page.tsx"
217+
import { Streamdown } from 'streamdown';
218+
219+
export default function Page() {
220+
return (
221+
<Streamdown cdnUrl={null}>
222+
{markdown}
223+
</Streamdown>
224+
);
225+
}
226+
```
227+
228+
With this configuration, only the 15 bundled languages will work. All other languages will fall back to plain text rendering.
229+
230+
## Verify Configuration
231+
232+
To verify your offline configuration works:
233+
234+
1. Disconnect from the internet
235+
2. Render a code block with a non-bundled language (like Rust or Ruby)
236+
3. Check the browser console for any CDN loading errors
237+
4. Verify the syntax highlighting appears correctly
238+
239+
Example test code:
240+
241+
```tsx title="app/page.tsx"
242+
import { Streamdown } from 'streamdown';
243+
244+
const markdown = `
245+
\`\`\`rust
246+
fn main() {
247+
println!("Hello, world!");
248+
}
249+
\`\`\`
250+
`;
251+
252+
export default function Page() {
253+
return <Streamdown>{markdown}</Streamdown>;
254+
}
255+
```
256+
257+
If configured correctly, the Rust code should have syntax highlighting even without internet access.
258+
259+
## Best Practices
260+
261+
### Bundle Additional Languages
262+
263+
If you consistently use specific non-bundled languages, consider creating a custom build that includes them. This provides instant loading without CDN requests.
264+
265+
You can fork Streamdown and modify `packages/streamdown/lib/code-block/bundled-languages.ts` to include additional languages.
266+
267+
### Use a Local CDN Mirror
268+
269+
For large deployments, consider setting up a local CDN mirror that serves Shiki language files from an internal server:
270+
271+
```tsx title="app/page.tsx"
272+
<Streamdown cdnUrl="https://internal-cdn.company.com/shiki/langs">
273+
{markdown}
274+
</Streamdown>
275+
```
276+
277+
### Monitor Failed Loads
278+
279+
Check browser console for warnings about failed language loads:
280+
281+
```
282+
[Streamdown] Failed to load language "rust" from CDN: Network error
283+
```
284+
285+
These warnings indicate which languages need to be self-hosted for your use case.
286+
287+
## Troubleshooting
288+
289+
### Language Files Not Loading
290+
291+
- Verify the CDN path points to the correct directory
292+
- Check that `.mjs` files are being served with the correct MIME type
293+
- Ensure the file permissions allow reading
294+
- Check browser network tab for 404 errors
295+
296+
### MIME Type Errors
297+
298+
Some servers don't recognize `.mjs` files. Configure your server to serve them as JavaScript:
299+
300+
**Express:**
301+
```javascript
302+
express.static.mime.define({'application/javascript': ['mjs']});
303+
```
304+
305+
**Nginx:**
306+
```nginx
307+
types {
308+
application/javascript mjs;
309+
}
310+
```
311+
312+
### Cached Failed Requests
313+
314+
If you previously tried loading a language before configuring offline mode, clear the cache:
315+
316+
```tsx
317+
// Reload the page after configuring CDN
318+
if (typeof window !== 'undefined') {
319+
window.location.reload();
320+
}
321+
```
322+
323+
Or clear browser cache manually.

0 commit comments

Comments
 (0)