Skip to content

Commit 336d332

Browse files
committed
Setup og image generation
1. It adds endpoint for dynamic og images generation 2. It adds necessary og: & twitter: meta tags to the Head component
1 parent 024fab9 commit 336d332

File tree

4 files changed

+113
-2
lines changed

4 files changed

+113
-2
lines changed

astro.config.mjs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import vercel from "@astrojs/vercel";
99
import remarkMath from "remark-math";
1010
import rehypeKatex from "rehype-katex";
1111
import { loadEnv } from "vite";
12+
import { getEnvsSchema } from "./src/lib/og-image/schema.mjs";
1213

1314
const env = loadEnv(process.env.NODE_ENV || "development", process.cwd(), "");
1415
const ALGOLIA_APP_ID = env.ALGOLIA_APP_ID;
@@ -118,6 +119,7 @@ export default defineConfig({
118119
customCss: ["./src/globals.css", "katex/dist/katex.min.css"],
119120
}),
120121
],
122+
output: "server",
121123
adapter: vercel(),
122124
vite: {
123125
plugins: [tailwindcss()],
@@ -128,12 +130,23 @@ export default defineConfig({
128130
},
129131
env: {
130132
schema: {
131-
ALGOLIA_APP_ID: envField.string({ context: "client", access: "public" }),
133+
...getEnvsSchema(),
134+
ALGOLIA_APP_ID: envField.string({
135+
context: "client",
136+
access: "public",
137+
optional: !hasAlgoliaConfig,
138+
}),
132139
ALGOLIA_SEARCH_API_KEY: envField.string({
133140
context: "client",
134141
access: "public",
142+
optional: !hasAlgoliaConfig,
143+
}),
144+
ALGOLIA_INDEX_NAME: envField.string({
145+
context: "client",
146+
access: "public",
147+
optional: !hasAlgoliaConfig,
135148
}),
136-
ALGOLIA_INDEX_NAME: envField.string({ context: "client", access: "public" }),
137149
},
150+
validateSecrets: true,
138151
},
139152
});

public/fonts/Satoshi-Medium.otf

49.2 KB
Binary file not shown.

src/pages/api/og-image.svg.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import fs from "node:fs/promises";
2+
import path from "node:path";
3+
import type { APIRoute } from "astro";
4+
import { z } from "astro:schema";
5+
import { generateImage } from "~/lib/og-image/generateImage";
6+
import { parseTokenOptions } from "~/lib/og-image/parseTokenOptions";
7+
8+
const CACHE_CONTROL_TTL = 24 * 60 * 60; // 24 hours
9+
const OPTIONS_SCHEMA = z.object({ title: z.string() });
10+
const FONTS = [
11+
{
12+
name: "Satoshi",
13+
data: await fs.readFile(path.resolve("public/fonts/Satoshi-Medium.otf")),
14+
weight: 600,
15+
style: "normal",
16+
} as const,
17+
];
18+
19+
export const prerender = false;
20+
export const GET: APIRoute = async ({ request }) => {
21+
const url = new URL(request.url);
22+
const { title } = parseTokenOptions(url.searchParams.get("token"), OPTIONS_SCHEMA);
23+
const image = await generateImage({
24+
width: 1200,
25+
height: 630,
26+
fonts: FONTS,
27+
embedFont: true,
28+
})`
29+
<div style="
30+
height: 100%;
31+
width: 100%;
32+
padding: 80px;
33+
display: flex;
34+
flex-direction: column;
35+
align-items: flex-start;
36+
justify-content: space-between;
37+
gap: 40px;
38+
background-color: #051419;
39+
font-family: Satoshi;
40+
font-weight: 600;
41+
color: white;
42+
">
43+
<h1 style="
44+
flex-grow: 1;
45+
display: flex;
46+
align-items: center;
47+
font-size: 90px;
48+
line-height: 1.1;
49+
letter-spacing: -2px;
50+
background-image: linear-gradient(90deg, #fff 20%, #ccc);
51+
text-shadow: 0 2px 30px #000;
52+
background-clip: text;
53+
color: transparent;
54+
">
55+
${title} | Aptos docs
56+
</h1>
57+
<div style="
58+
display: flex;
59+
align-items: center;
60+
gap: 15px;
61+
font-size: 40px;
62+
letter-spacing: -1px;
63+
">
64+
<svg width="40" height="40" viewBox="0 0 64 65" fill="none" xmlns="http://www.w3.org/2000/svg">
65+
<path d="M49.5678 22.5137H43.9056C43.2471 22.5137 42.6189 22.2516 42.1808 21.7961L39.8857 19.4017C39.544 19.0455 39.0508 18.8394 38.5356 18.8394C38.0203 18.8394 37.5271 19.0429 37.1855 19.4017L35.2154 21.4577C34.5707 22.1294 33.6449 22.5162 32.6723 22.5162H1.68624C0.804545 24.8394 0.228689 27.2923 0 29.8394H29.253C29.7682 29.8394 30.2586 29.646 30.6141 29.3025L33.3391 26.679C33.678 26.3508 34.1491 26.1651 34.6396 26.1651H34.7525C35.2678 26.1651 35.761 26.3686 36.1026 26.7274L38.3978 29.1218C38.8359 29.5773 39.4613 29.8394 40.1226 29.8394H64C63.7713 27.2923 63.1954 24.8394 62.3137 22.5162H49.5678V22.5137Z" fill="white"/>
66+
<path d="M17.9561 46.8366C18.4622 46.8366 18.944 46.627 19.2931 46.2547L21.9698 43.4112C22.3027 43.0554 22.7655 42.8541 23.2473 42.8541H23.3582C23.8643 42.8541 24.3488 43.0747 24.6844 43.4636L26.9389 46.0588C27.3692 46.5525 27.9836 46.8366 28.6332 46.8366H60.418C61.6089 44.3296 62.4804 41.6378 63 38.8219H32.4926C31.8457 38.8219 31.2287 38.5378 30.7983 38.0441L28.5438 35.4489C28.2082 35.0628 27.7238 34.8394 27.2177 34.8394C26.7115 34.8394 26.2271 35.06 25.8915 35.4489L23.9563 37.6773C23.323 38.4054 22.4137 38.8246 21.4583 38.8246H1C1.51964 41.6433 2.39114 44.3323 3.58199 46.8394H17.9561V46.8366Z" fill="white"/>
67+
<path d="M40.0967 13.8394C40.6083 13.8394 41.0953 13.6272 41.4483 13.2502L44.1542 10.3716C44.4908 10.0114 44.9586 9.80757 45.4457 9.80757H45.5578C46.0695 9.80757 46.5592 10.0309 46.8985 10.4246L49.1776 13.052C49.6127 13.5518 50.2338 13.8394 50.8904 13.8394H57C51.1804 5.9461 41.9244 0.839355 31.5 0.839355C21.0757 0.839355 11.8196 5.9461 6 13.8394H40.0994H40.0967Z" fill="white"/>
68+
<path d="M27.7966 54.7633H19.375C18.7159 54.7633 18.0872 54.4834 17.6487 53.997L15.3517 51.4399C15.0097 51.0595 14.5161 50.8394 14.0005 50.8394C13.4848 50.8394 12.9912 51.0567 12.6493 51.4399L10.6776 53.6356C10.0323 54.3529 9.1058 54.766 8.13237 54.766H8C13.8681 60.9616 22.2208 64.8394 31.5 64.8394C40.7792 64.8394 49.1319 60.9616 55 54.766H27.7966V54.7633Z" fill="white"/>
69+
</svg>
70+
Developers
71+
</span>
72+
</div>
73+
`;
74+
75+
return new Response(image, {
76+
headers: {
77+
"Content-Type": "image/svg+xml",
78+
"Cache-Control": `public, max-age=${String(CACHE_CONTROL_TTL)}`,
79+
},
80+
});
81+
};

src/starlight-overrides/Head.astro

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,32 @@ import Default from "@astrojs/starlight/components/Head.astro";
44
import AnalyticsComponent from "@vercel/analytics/astro";
55
import type { AnalyticsProps } from "@vercel/analytics";
66
import type { JSX } from "astro/jsx-runtime";
7+
import { getImageUrl } from "~/lib/og-image/getImageUrl";
78
const { lang } = Astro.props;
89
910
// This is workaround for wrongly typed Analytics component from @vercel/analytics/astro
1011
const Analytics = AnalyticsComponent as unknown as (
1112
props: Omit<AnalyticsProps, "framework" | "beforeSend">,
1213
) => JSX.Element;
14+
15+
const entryData = Astro.props.entry.data;
16+
const ogImageUrl = getImageUrl(new URL("/api/og-image.svg", Astro.site), {
17+
title: entryData.title,
18+
});
1319
---
1420

1521
<Default {...Astro.props}><slot /></Default>
1622
<Analytics />
1723
<!-- Algolia docsearch language facet -->
1824
<meta name="docsearch:language" content={lang} />
25+
<!-- Open Graph / Facebook -->
26+
<meta property="og:type" content="website" />
27+
<meta property="og:url" content={Astro.request.url} />
28+
<meta property="og:title" content={entryData.title} />
29+
{entryData.description && <meta property="og:description" content={entryData.description} />}
30+
<meta property="og:image" content={ogImageUrl} />
31+
<!-- Twitter -->
32+
<meta name="twitter:image" content={ogImageUrl} />
33+
<meta name="twitter:card" content="summary_large_image" />
34+
<meta name="twitter:title" content={entryData.title} />
35+
{entryData.description && <meta name="twitter:description" content={entryData.description} />}

0 commit comments

Comments
 (0)