Skip to content

Commit 2040726

Browse files
feat(seo): dynamic OG images (#101)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 6961efd commit 2040726

File tree

10 files changed

+481
-11
lines changed

10 files changed

+481
-11
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"@neoconfetti/svelte": "^2.2.2",
2323
"@octokit/graphql-schema": "^15.26.0",
2424
"@prgm/sveltekit-progress-bar": "^3.0.2",
25+
"@resvg/resvg-js": "^2.6.2",
2526
"@shikijs/langs": "^3.13.0",
2627
"@shikijs/rehype": "^3.13.0",
2728
"@shikijs/themes": "^3.13.0",
@@ -57,6 +58,8 @@
5758
"remark-gemoji": "^8.0.0",
5859
"remark-github": "^12.0.0",
5960
"runed": "^0.34.0",
61+
"satori": "^0.18.3",
62+
"satori-html": "^0.3.2",
6063
"semver": "^7.7.2",
6164
"shiki": "^3.13.0",
6265
"svelte": "^5.39.7",

pnpm-lock.yaml

Lines changed: 289 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,25 @@
11
import type { MetaTagsProps } from "svelte-meta-tags";
22

3-
export function load({ data }) {
3+
export function load({ data, url }) {
44
return {
55
...data,
66
pageMetaTags: Object.freeze({
7-
title: `Detail of ${data.itemMetadata.org}/${data.itemMetadata.repo}#${data.itemMetadata.id}`
7+
title: `Detail of ${data.itemMetadata.org}/${data.itemMetadata.repo}#${data.itemMetadata.id}`,
8+
openGraph: {
9+
images: [
10+
{
11+
get url() {
12+
const ogUrl = new URL("og", url.origin);
13+
ogUrl.searchParams.set("title", data.item.info.title);
14+
ogUrl.searchParams.set(
15+
"description",
16+
`${data.itemMetadata.org}/${data.itemMetadata.repo}#${data.itemMetadata.id}`
17+
);
18+
return ogUrl.href;
19+
}
20+
}
21+
]
22+
}
823
}) satisfies MetaTagsProps
924
};
1025
}

src/routes/devlog/v2/+page.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
import type { MetaTagsProps } from "svelte-meta-tags";
22

3-
export function load() {
3+
export function load({ url }) {
44
return {
55
pageMetaTags: Object.freeze({
66
title: "v2 • Devlog",
77
description: "The development blog of Svelte Changelog",
8-
twitter: {
9-
description: "The development blog of Svelte Changelog"
8+
openGraph: {
9+
images: [
10+
{
11+
get url() {
12+
const ogUrl = new URL("og", url.origin);
13+
ogUrl.searchParams.set("title", "v2 • Devlog");
14+
return ogUrl.href;
15+
}
16+
}
17+
]
1018
}
1119
}) satisfies MetaTagsProps
1220
};

src/routes/og/+server.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { render } from "svelte/server";
2+
import { read } from "$app/server";
3+
import DMSerifDisplay from "@fontsource/dm-serif-display/files/dm-serif-display-latin-400-normal.woff";
4+
import Pretendard from "@fontsource/pretendard/files/pretendard-latin-400-normal.woff";
5+
import PretendardSemibold from "@fontsource/pretendard/files/pretendard-latin-600-normal.woff";
6+
import { Resvg } from "@resvg/resvg-js";
7+
import satori from "satori";
8+
import { html } from "satori-html";
9+
import type { RequestHandler } from "./$types";
10+
import Thumbnail from "./Thumbnail.svelte";
11+
import { OG_HEIGHT, OG_WIDTH } from "./constants";
12+
13+
const sansFont = read(Pretendard).arrayBuffer();
14+
const sansFontSemibold = read(PretendardSemibold).arrayBuffer();
15+
const displayFont = read(DMSerifDisplay).arrayBuffer();
16+
17+
// Sources: https://github.com/huggingface/chat-ui/blob/ebeff50ac0ac4367a8e1a32b46dcc5ac2e8fc43f/src/routes/assistant/%5BassistantId%5D/thumbnail.png/%2Bserver.ts#L44-L82
18+
// https://geoffrich.net/posts/svelte-social-image/
19+
export const GET: RequestHandler = async ({ url }) => {
20+
const renderedComponent = render(Thumbnail, {
21+
props: {
22+
title: url.searchParams.get("title") ?? "",
23+
description: url.searchParams.get("description") ?? undefined
24+
}
25+
});
26+
27+
const reactLike = html(`<style>${renderedComponent.head}</style>${renderedComponent.body}`);
28+
29+
const svg = await satori(reactLike, {
30+
width: OG_WIDTH,
31+
height: OG_HEIGHT,
32+
fonts: [
33+
{
34+
name: "SansFont",
35+
data: await sansFont,
36+
weight: 400,
37+
style: "normal"
38+
},
39+
{
40+
name: "SansFontSemibold",
41+
data: await sansFontSemibold,
42+
weight: 600,
43+
style: "normal"
44+
},
45+
{
46+
name: "DisplayFont",
47+
data: await displayFont,
48+
weight: 400,
49+
style: "normal"
50+
}
51+
]
52+
});
53+
54+
const png = new Resvg(svg, {
55+
fitTo: {
56+
mode: "original"
57+
}
58+
})
59+
.render()
60+
.asPng();
61+
62+
// `png` is not usable directly inside `new Response()` for some reason, TS says
63+
let bodyData;
64+
if (png instanceof ArrayBuffer) {
65+
bodyData = png;
66+
} else if (png.buffer instanceof ArrayBuffer) {
67+
bodyData = png.buffer;
68+
} else {
69+
bodyData = new Uint8Array(png);
70+
}
71+
72+
return new Response(bodyData, {
73+
headers: {
74+
"Content-Type": "image/png",
75+
"Cache-Control": "public, max-age=31536000, immutable"
76+
}
77+
});
78+
};

src/routes/og/Thumbnail.svelte

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<script lang="ts">
2+
type Props = {
3+
title: string;
4+
description?: string;
5+
};
6+
7+
let { title, description }: Props = $props();
8+
</script>
9+
10+
<div class="flex h-full w-full border-b border-orange-600" style:border-bottom-width="32px">
11+
<img
12+
src="https://raw.githubusercontent.com/sveltejs/branding/master/svelte-logo.svg"
13+
alt="Svelte"
14+
class="absolute -top-40 -right-40 opacity-20"
15+
style:height="150%"
16+
/>
17+
<div class="flex flex-col p-8">
18+
<div class="flex items-center" style:gap="8">
19+
<img
20+
src="https://raw.githubusercontent.com/sveltejs/branding/master/svelte-logo.svg"
21+
alt="Svelte"
22+
class="w-16"
23+
/>
24+
<span class="flex text-4xl" style:gap="6">
25+
<span style:font-family="DisplayFont">Svelte</span>
26+
<span class="text-orange-600" style:font-family="SansFontSemibold">Changelog</span>
27+
</span>
28+
</div>
29+
<div class="my-auto flex flex-col justify-center">
30+
<p class="text-7xl" style:font-family="DisplayFont">{title}</p>
31+
{#if description}
32+
<p class="text-4xl text-gray-600" style:font-family="SansFont">{description}</p>
33+
{/if}
34+
</div>
35+
</div>
36+
</div>

src/routes/og/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Source: https://vercel.com/docs/og-image-generation#technical-details
2+
3+
export const OG_WIDTH = 1_200;
4+
export const OG_HEIGHT = 630;
Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,25 @@
11
import type { MetaTagsProps } from "svelte-meta-tags";
22

3-
export function load({ data }) {
3+
export function load({ data, url }) {
44
return {
55
...data,
66
pageMetaTags: Object.freeze({
7-
title: data.currentPackage.pkg.name
7+
title: data.currentPackage.pkg.name,
8+
openGraph: {
9+
images: [
10+
{
11+
get url() {
12+
const ogUrl = new URL("og", url.origin);
13+
ogUrl.searchParams.set("title", data.currentPackage.pkg.name);
14+
ogUrl.searchParams.set(
15+
"description",
16+
`${data.currentPackage.repoOwner}/${data.currentPackage.repoName}`
17+
);
18+
return ogUrl.href;
19+
}
20+
}
21+
]
22+
}
823
}) satisfies MetaTagsProps
924
};
1025
}

src/routes/packages/+page.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
import type { MetaTagsProps } from "svelte-meta-tags";
22

3-
export function load({ data }) {
3+
export function load({ data, url }) {
44
return {
55
...data,
66
pageMetaTags: Object.freeze({
7-
title: "All Packages"
7+
title: "All Packages",
8+
openGraph: {
9+
images: [
10+
{
11+
get url() {
12+
const ogUrl = new URL("og", url.origin);
13+
ogUrl.searchParams.set("title", "All Packages");
14+
return ogUrl.href;
15+
}
16+
}
17+
]
18+
}
819
}) satisfies MetaTagsProps
920
};
1021
}
Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
import type { MetaTagsProps } from "svelte-meta-tags";
22

3-
export function load({ data, params }) {
3+
export function load({ data, params, url }) {
44
return {
55
...data,
66
pageMetaTags: Object.freeze({
7-
title: `Tracker for ${params.org}/${params.repo}`
7+
title: `Tracker for ${params.org}/${params.repo}`,
8+
openGraph: {
9+
images: [
10+
{
11+
get url() {
12+
const ogUrl = new URL("og", url.origin);
13+
ogUrl.searchParams.set("title", `Tracker • ${params.org}/${params.repo}`);
14+
return ogUrl.href;
15+
}
16+
}
17+
]
18+
}
819
}) satisfies MetaTagsProps
920
};
1021
}

0 commit comments

Comments
 (0)