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 @@ + + + + +
+ Svelte Machine + +
+ + + Docs • {breadcrumbs.map(({ title }) => title).join(' • ')} + +

{title}

+
+
+ +