Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6f4a200
Initial prototype
WarningImHack3r Aug 24, 2025
c1be525
Fix stuff, more HTML
WarningImHack3r Aug 24, 2025
f0b9059
Merge branch 'main' into dynamic-og-images
WarningImHack3r Sep 5, 2025
0f0929d
Upgrade satori
WarningImHack3r Sep 5, 2025
c3a7f04
Merge branch 'main' into dynamic-og-images
WarningImHack3r Sep 7, 2025
9dab72f
Merge branch 'main' into dynamic-og-images
WarningImHack3r Sep 14, 2025
dc70a6c
Merge branch 'main' into dynamic-og-images
WarningImHack3r Sep 21, 2025
9df5abb
fix lockfile
WarningImHack3r Sep 21, 2025
5571591
update satori
WarningImHack3r Sep 21, 2025
2135546
feat(server): use GH app instead of a raw token
WarningImHack3r Sep 21, 2025
94550a4
Merge branch 'main' into dynamic-og-images
WarningImHack3r Sep 21, 2025
7d6ba4d
Update README.md
WarningImHack3r Sep 21, 2025
c2b60f9
Merge branch 'main' into dynamic-og-images
WarningImHack3r Sep 21, 2025
9aa0dde
Merge branch 'main' into dynamic-og-images
WarningImHack3r Sep 23, 2025
428f6c1
Merge branch 'main' into dynamic-og-images
WarningImHack3r Sep 30, 2025
5c1e2a4
finalize design, fix CSS issues
WarningImHack3r Oct 5, 2025
afa84b8
Merge branch 'main' into dynamic-og-images
WarningImHack3r Oct 7, 2025
df219cb
add another Svelte logo in the background
WarningImHack3r Oct 7, 2025
7f05142
implement og for all pages
WarningImHack3r Oct 7, 2025
effcb7c
fix hf link
WarningImHack3r Oct 7, 2025
6693382
remove unused url param
WarningImHack3r Oct 7, 2025
9d0ce3f
add cache control header
WarningImHack3r Oct 7, 2025
0246597
fix case
WarningImHack3r Oct 7, 2025
90a3f41
fix lint
WarningImHack3r Oct 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@neoconfetti/svelte": "^2.2.2",
"@octokit/graphql-schema": "^15.26.0",
"@prgm/sveltekit-progress-bar": "^3.0.2",
"@resvg/resvg-js": "^2.6.2",
"@shikijs/langs": "^3.13.0",
"@shikijs/rehype": "^3.13.0",
"@shikijs/themes": "^3.13.0",
Expand Down Expand Up @@ -57,6 +58,8 @@
"remark-gemoji": "^8.0.0",
"remark-github": "^12.0.0",
"runed": "^0.34.0",
"satori": "^0.18.3",
"satori-html": "^0.3.2",
"semver": "^7.7.2",
"shiki": "^3.13.0",
"svelte": "^5.39.7",
Expand Down
289 changes: 289 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

19 changes: 17 additions & 2 deletions src/routes/[pid=pid]/[org]/[repo]/[id=number]/+page.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import type { MetaTagsProps } from "svelte-meta-tags";

export function load({ data }) {
export function load({ data, url }) {
return {
...data,
pageMetaTags: Object.freeze({
title: `Detail of ${data.itemMetadata.org}/${data.itemMetadata.repo}#${data.itemMetadata.id}`
title: `Detail of ${data.itemMetadata.org}/${data.itemMetadata.repo}#${data.itemMetadata.id}`,
openGraph: {
images: [
{
get url() {
const ogUrl = new URL("og", url.origin);
ogUrl.searchParams.set("title", data.item.info.title);
ogUrl.searchParams.set(
"description",
`${data.itemMetadata.org}/${data.itemMetadata.repo}#${data.itemMetadata.id}`
);
return ogUrl.href;
}
}
]
}
}) satisfies MetaTagsProps
};
}
14 changes: 11 additions & 3 deletions src/routes/devlog/v2/+page.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import type { MetaTagsProps } from "svelte-meta-tags";

export function load() {
export function load({ url }) {
return {
pageMetaTags: Object.freeze({
title: "v2 • Devlog",
description: "The development blog of Svelte Changelog",
twitter: {
description: "The development blog of Svelte Changelog"
openGraph: {
images: [
{
get url() {
const ogUrl = new URL("og", url.origin);
ogUrl.searchParams.set("title", "v2 • Devlog");
return ogUrl.href;
}
}
]
}
}) satisfies MetaTagsProps
};
Expand Down
78 changes: 78 additions & 0 deletions src/routes/og/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { render } from "svelte/server";
import { read } from "$app/server";
import DMSerifDisplay from "@fontsource/dm-serif-display/files/dm-serif-display-latin-400-normal.woff";
import Pretendard from "@fontsource/pretendard/files/pretendard-latin-400-normal.woff";
import PretendardSemibold from "@fontsource/pretendard/files/pretendard-latin-600-normal.woff";
import { Resvg } from "@resvg/resvg-js";
import satori from "satori";
import { html } from "satori-html";
import type { RequestHandler } from "./$types";
import Thumbnail from "./Thumbnail.svelte";
import { OG_HEIGHT, OG_WIDTH } from "./constants";

const sansFont = read(Pretendard).arrayBuffer();
const sansFontSemibold = read(PretendardSemibold).arrayBuffer();
const displayFont = read(DMSerifDisplay).arrayBuffer();

// Sources: https://github.com/huggingface/chat-ui/blob/ebeff50ac0ac4367a8e1a32b46dcc5ac2e8fc43f/src/routes/assistant/%5BassistantId%5D/thumbnail.png/%2Bserver.ts#L44-L82
// https://geoffrich.net/posts/svelte-social-image/
export const GET: RequestHandler = async ({ url }) => {
const renderedComponent = render(Thumbnail, {
props: {
title: url.searchParams.get("title") ?? "",
description: url.searchParams.get("description") ?? undefined
}
});

const reactLike = html(`<style>${renderedComponent.head}</style>${renderedComponent.body}`);

const svg = await satori(reactLike, {
width: OG_WIDTH,
height: OG_HEIGHT,
fonts: [
{
name: "SansFont",
data: await sansFont,
weight: 400,
style: "normal"
},
{
name: "SansFontSemibold",
data: await sansFontSemibold,
weight: 600,
style: "normal"
},
{
name: "DisplayFont",
data: await displayFont,
weight: 400,
style: "normal"
}
]
});

const png = new Resvg(svg, {
fitTo: {
mode: "original"
}
})
.render()
.asPng();

// `png` is not usable directly inside `new Response()` for some reason, TS says
let bodyData;
if (png instanceof ArrayBuffer) {
bodyData = png;
} else if (png.buffer instanceof ArrayBuffer) {
bodyData = png.buffer;
} else {
bodyData = new Uint8Array(png);
}

return new Response(bodyData, {
headers: {
"Content-Type": "image/png",
"Cache-Control": "public, max-age=31536000, immutable"
}
});
};
36 changes: 36 additions & 0 deletions src/routes/og/Thumbnail.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script lang="ts">
type Props = {
title: string;
description?: string;
};
let { title, description }: Props = $props();
</script>

<div class="flex h-full w-full border-b border-orange-600" style:border-bottom-width="32px">
<img
src="https://raw.githubusercontent.com/sveltejs/branding/master/svelte-logo.svg"
alt="Svelte"
class="absolute -top-40 -right-40 opacity-20"
style:height="150%"
/>
<div class="flex flex-col p-8">
<div class="flex items-center" style:gap="8">
<img
src="https://raw.githubusercontent.com/sveltejs/branding/master/svelte-logo.svg"
alt="Svelte"
class="w-16"
/>
<span class="flex text-4xl" style:gap="6">
<span style:font-family="DisplayFont">Svelte</span>
<span class="text-orange-600" style:font-family="SansFontSemibold">Changelog</span>
</span>
</div>
<div class="my-auto flex flex-col justify-center">
<p class="text-7xl" style:font-family="DisplayFont">{title}</p>
{#if description}
<p class="text-4xl text-gray-600" style:font-family="SansFont">{description}</p>
{/if}
</div>
</div>
</div>
4 changes: 4 additions & 0 deletions src/routes/og/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Source: https://vercel.com/docs/og-image-generation#technical-details

export const OG_WIDTH = 1_200;
export const OG_HEIGHT = 630;
19 changes: 17 additions & 2 deletions src/routes/package/[...package]/+page.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import type { MetaTagsProps } from "svelte-meta-tags";

export function load({ data }) {
export function load({ data, url }) {
return {
...data,
pageMetaTags: Object.freeze({
title: data.currentPackage.pkg.name
title: data.currentPackage.pkg.name,
openGraph: {
images: [
{
get url() {
const ogUrl = new URL("og", url.origin);
ogUrl.searchParams.set("title", data.currentPackage.pkg.name);
ogUrl.searchParams.set(
"description",
`${data.currentPackage.repoOwner}/${data.currentPackage.repoName}`
);
return ogUrl.href;
}
}
]
}
}) satisfies MetaTagsProps
};
}
15 changes: 13 additions & 2 deletions src/routes/packages/+page.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import type { MetaTagsProps } from "svelte-meta-tags";

export function load({ data }) {
export function load({ data, url }) {
return {
...data,
pageMetaTags: Object.freeze({
title: "All Packages"
title: "All Packages",
openGraph: {
images: [
{
get url() {
const ogUrl = new URL("og", url.origin);
ogUrl.searchParams.set("title", "All Packages");
return ogUrl.href;
}
}
]
}
}) satisfies MetaTagsProps
};
}
15 changes: 13 additions & 2 deletions src/routes/tracker/[org]/[repo]/+page.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import type { MetaTagsProps } from "svelte-meta-tags";

export function load({ data, params }) {
export function load({ data, params, url }) {
return {
...data,
pageMetaTags: Object.freeze({
title: `Tracker for ${params.org}/${params.repo}`
title: `Tracker for ${params.org}/${params.repo}`,
openGraph: {
images: [
{
get url() {
const ogUrl = new URL("og", url.origin);
ogUrl.searchParams.set("title", `Tracker • ${params.org}/${params.repo}`);
return ogUrl.href;
}
}
]
}
}) satisfies MetaTagsProps
};
}
Loading