diff --git a/apps/svelte.dev/src/routes/blog/[slug]/card.png/DMSerifDisplay-Regular.ttf b/apps/svelte.dev/src/lib/fonts/DMSerifDisplay-Regular.ttf
similarity index 100%
rename from apps/svelte.dev/src/routes/blog/[slug]/card.png/DMSerifDisplay-Regular.ttf
rename to apps/svelte.dev/src/lib/fonts/DMSerifDisplay-Regular.ttf
diff --git a/apps/svelte.dev/src/routes/blog/[slug]/card.png/FiraSans-Regular.ttf b/apps/svelte.dev/src/lib/fonts/FiraSans-Regular.ttf
similarity index 100%
rename from apps/svelte.dev/src/routes/blog/[slug]/card.png/FiraSans-Regular.ttf
rename to apps/svelte.dev/src/lib/fonts/FiraSans-Regular.ttf
diff --git a/apps/svelte.dev/src/routes/+layout.svelte b/apps/svelte.dev/src/routes/+layout.svelte
index 54a2d4c8fb..4a73f6c202 100644
--- a/apps/svelte.dev/src/routes/+layout.svelte
+++ b/apps/svelte.dev/src/routes/+layout.svelte
@@ -39,7 +39,7 @@
- {#if !page.route.id?.startsWith('/blog/')}
+ {#if !page.route.id || !page.route.id.startsWith('/blog/') || !/^\/docs\/[^\/]+\/[^\/]+$/.test(page.route.id)}
diff --git a/apps/svelte.dev/src/routes/blog/[slug]/card.png/+server.ts b/apps/svelte.dev/src/routes/blog/[slug]/card.png/+server.ts
index de03a5e3cd..7d215e3f85 100644
--- a/apps/svelte.dev/src/routes/blog/[slug]/card.png/+server.ts
+++ b/apps/svelte.dev/src/routes/blog/[slug]/card.png/+server.ts
@@ -5,8 +5,8 @@ import { read } from '$app/server';
import satori from 'satori';
import { html as toReactNode } from 'satori-html';
import Card from './Card.svelte';
-import DMSerifDisplay from './DMSerifDisplay-Regular.ttf?url';
-import FiraSans from './FiraSans-Regular.ttf?url';
+import DMSerifDisplay from '$lib/fonts/DMSerifDisplay-Regular.ttf?url';
+import FiraSans from '$lib/fonts/FiraSans-Regular.ttf?url';
import { blog_posts } from '$lib/server/content';
import type { ServerlessConfig } from '@sveltejs/adapter-vercel';
diff --git a/apps/svelte.dev/src/routes/docs/[topic]/[...path]/+page.svelte b/apps/svelte.dev/src/routes/docs/[topic]/[...path]/+page.svelte
index e3d566d732..749d474cc3 100644
--- a/apps/svelte.dev/src/routes/docs/[topic]/[...path]/+page.svelte
+++ b/apps/svelte.dev/src/routes/docs/[topic]/[...path]/+page.svelte
@@ -8,6 +8,7 @@
import PageControls from '$lib/components/PageControls.svelte';
import { goto } from '$app/navigation';
import { escape_html } from '$lib/utils/escape';
+ import { page } from '$app/state';
let { data } = $props();
@@ -64,7 +65,16 @@
name="twitter:description"
content="{data.document.metadata.title} • Svelte documentation"
/>
+
+
+
diff --git a/apps/svelte.dev/src/routes/docs/[topic]/[...path]/card.png/+server.ts b/apps/svelte.dev/src/routes/docs/[topic]/[...path]/card.png/+server.ts
new file mode 100644
index 0000000000..4b7ffe3e9e
--- /dev/null
+++ b/apps/svelte.dev/src/routes/docs/[topic]/[...path]/card.png/+server.ts
@@ -0,0 +1,79 @@
+import { render } from 'svelte/server';
+import { Resvg } from '@resvg/resvg-js';
+import { error } from '@sveltejs/kit';
+import { read } from '$app/server';
+import satori from 'satori';
+import { html as toReactNode } from 'satori-html';
+import Card from './Card.svelte';
+import DMSerifDisplay from '$lib/fonts/DMSerifDisplay-Regular.ttf?url';
+import FiraSans from '$lib/fonts/FiraSans-Regular.ttf?url';
+import { docs } from '$lib/server/content';
+import type { ServerlessConfig } from '@sveltejs/adapter-vercel';
+
+export const config: ServerlessConfig = {
+ isr: {
+ expiration: false
+ }
+};
+
+export function entries() {
+ return Object.keys(docs.pages).map((doc) => {
+ const full = doc.slice(5); // removes 'docs/' prefix
+ const [topic, ...path] = full.split('/');
+ return {
+ topic,
+ path: path.join('/')
+ };
+ });
+}
+
+const height = 630;
+const width = 1200;
+const dm_serif_display = await read(DMSerifDisplay).arrayBuffer();
+const fira_sans = await read(FiraSans).arrayBuffer();
+
+export async function GET({ params }) {
+ const document = docs.pages[`docs/${params.topic}/${params.path}`];
+
+ if (!document) error(404);
+
+ const result = render(Card, {
+ props: { title: document.metadata.title, breadcrumbs: document.breadcrumbs.slice(1) }
+ });
+ const element = toReactNode(`${result.head}${result.body}`);
+
+ const svg = await satori(element, {
+ fonts: [
+ {
+ name: 'DMSerif Display',
+ data: dm_serif_display,
+ style: 'normal',
+ weight: 400
+ },
+ {
+ name: 'Fira Sans',
+ data: fira_sans,
+ style: 'normal',
+ weight: 400
+ }
+ ],
+ height,
+ width
+ });
+
+ const resvg = new Resvg(svg, {
+ fitTo: {
+ mode: 'width',
+ value: width
+ }
+ });
+
+ const image = resvg.render();
+
+ return new Response(image.asPng(), {
+ headers: {
+ 'content-type': 'image/png',
+ 'cache-control': 'public, max-age=600' // cache for 10 minutes
+ }
+ });
+}
diff --git a/apps/svelte.dev/src/routes/docs/[topic]/[...path]/card.png/Card.svelte b/apps/svelte.dev/src/routes/docs/[topic]/[...path]/card.png/Card.svelte
new file mode 100644
index 0000000000..bf44ed6ae2
--- /dev/null
+++ b/apps/svelte.dev/src/routes/docs/[topic]/[...path]/card.png/Card.svelte
@@ -0,0 +1,64 @@
+
+
+
+
+
+

+
+
+
+
+ Docs • {breadcrumbs.map(({ title }) => title).join(' • ')}
+
+
{title}
+
+
+
+