From 631659e1496499154d30d1733eb5061483468cb4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Apr 2026 08:28:56 +0000 Subject: [PATCH 1/3] feat: add og images to docs pages Agent-Logs-Url: https://github.com/100gle/bub/sessions/cb0595f3-5b41-4359-8656-9cc275c7b397 Co-authored-by: 100gle <36526527+100gle@users.noreply.github.com> --- website/astro.config.mjs | 1 + website/src/lib/og.ts | 25 +++++++++++++++++++++++-- website/src/routeData.ts | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 website/src/routeData.ts diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 407cb23e..7be12193 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -44,6 +44,7 @@ export default defineConfig({ starlight({ title: 'Bub', description: 'A common shape for agents that live alongside people.', + routeMiddleware: './src/routeData.ts', expressiveCode: false, logo: { light: './src/assets/bub-logo.png', diff --git a/website/src/lib/og.ts b/website/src/lib/og.ts index 5e4b2f15..07b2f72d 100644 --- a/website/src/lib/og.ts +++ b/website/src/lib/og.ts @@ -32,6 +32,9 @@ export interface FontsResult { fontFamily: string; } +const SITE_DESCRIPTION = 'A common shape for agents that live alongside people.'; +const ZH_CN_SITE_DESCRIPTION = '与 Human 同在的轻量级 Agent 运行时。'; + /* ─── Font loading (@fontsource packages) ─── */ const OUTFIT_DIR = 'node_modules/@fontsource/outfit/files'; @@ -129,8 +132,18 @@ export function loadFonts(allText: string): FontsResult { export async function collectPages(): Promise> { const posts = await getCollection('posts'); + const docs = await getCollection('docs'); const pages: Record = {}; + const normalizeRoute = (route: string) => + route + .replace(/\.(md|mdx)$/, '') + .replace(/(?:^|\/)index$/, '') + .replace(/^\/+|\/+$/g, '') || 'index'; + + const defaultDescription = (route: string) => + route.startsWith('zh-cn/') ? ZH_CN_SITE_DESCRIPTION : SITE_DESCRIPTION; + for (const post of posts) { const route = `posts/${post.id.replace(/\.md$/, '')}`; pages[route] = { @@ -139,14 +152,22 @@ export async function collectPages(): Promise> { }; } + for (const doc of docs) { + const route = normalizeRoute(doc.id); + pages[route] = { + title: doc.data.title, + description: doc.data.description ?? defaultDescription(route), + }; + } + pages['index'] = { title: 'Bub', - description: 'A common shape for agents that live alongside people.', + description: SITE_DESCRIPTION, }; pages['zh-cn/index'] = { title: 'Bub', - description: '与 Human 同在的轻量级 Agent 运行时。', + description: ZH_CN_SITE_DESCRIPTION, }; return pages; diff --git a/website/src/routeData.ts b/website/src/routeData.ts new file mode 100644 index 00000000..b7053e93 --- /dev/null +++ b/website/src/routeData.ts @@ -0,0 +1,38 @@ +import { defineRouteMiddleware } from '@astrojs/starlight/route-data'; + +const DEFAULT_SITE_URL = 'https://bub.build'; + +function normalizeRoute(pathname: string): string { + return pathname.replace(/^\/+|\/+$/g, '') || 'index'; +} + +function upsertMetaTag( + head: Array<{ tag: string; attrs?: Record }>, + key: 'property' | 'name', + value: string, + content: string, +) { + const existingTag = head.find((entry) => entry.tag === 'meta' && entry.attrs?.[key] === value); + + if (existingTag) { + existingTag.attrs = { ...existingTag.attrs, content }; + return; + } + + head.push({ + tag: 'meta', + attrs: { [key]: value, content }, + }); +} + +export const onRequest = defineRouteMiddleware((context) => { + const route = normalizeRoute(context.url.pathname); + const site = context.site ?? new URL(DEFAULT_SITE_URL); + const imageUrl = new URL(`/og/${route}.png`, site).toString(); + const { head } = context.locals.starlightRoute; + + upsertMetaTag(head, 'property', 'og:image', imageUrl); + upsertMetaTag(head, 'property', 'og:image:width', '1200'); + upsertMetaTag(head, 'property', 'og:image:height', '630'); + upsertMetaTag(head, 'name', 'twitter:image', imageUrl); +}); From a54bfaf8ff7fc5a1863efc484ae877deb788c570 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Apr 2026 08:31:15 +0000 Subject: [PATCH 2/3] refactor: share og route normalization Agent-Logs-Url: https://github.com/100gle/bub/sessions/cb0595f3-5b41-4359-8656-9cc275c7b397 Co-authored-by: 100gle <36526527+100gle@users.noreply.github.com> --- website/src/lib/og-routes.ts | 16 ++++++++++++++++ website/src/lib/og.ts | 9 ++------- website/src/routeData.ts | 16 +++++++--------- 3 files changed, 25 insertions(+), 16 deletions(-) create mode 100644 website/src/lib/og-routes.ts diff --git a/website/src/lib/og-routes.ts b/website/src/lib/og-routes.ts new file mode 100644 index 00000000..f6f55708 --- /dev/null +++ b/website/src/lib/og-routes.ts @@ -0,0 +1,16 @@ +function trimRoute(route: string): string { + return route.replace(/^\/+|\/+$/g, ''); +} + +export function normalizeOgRouteFromPathname(pathname: string): string { + return trimRoute(pathname) || 'index'; +} + +export function normalizeOgRouteFromEntryId(id: string): string { + const route = id.replace(/\.(md|mdx)$/, '').replace(/(?:^|\/)index$/, ''); + return normalizeOgRouteFromPathname(route); +} + +export function createOgImageUrl(route: string, site: URL): string { + return new URL(`/og/${route}.png`, site).toString(); +} diff --git a/website/src/lib/og.ts b/website/src/lib/og.ts index 07b2f72d..55539064 100644 --- a/website/src/lib/og.ts +++ b/website/src/lib/og.ts @@ -11,6 +11,7 @@ import sharp from 'sharp'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { getCollection } from 'astro:content'; +import { normalizeOgRouteFromEntryId } from '@/lib/og-routes'; /* ─── Types ─── */ @@ -135,12 +136,6 @@ export async function collectPages(): Promise> { const docs = await getCollection('docs'); const pages: Record = {}; - const normalizeRoute = (route: string) => - route - .replace(/\.(md|mdx)$/, '') - .replace(/(?:^|\/)index$/, '') - .replace(/^\/+|\/+$/g, '') || 'index'; - const defaultDescription = (route: string) => route.startsWith('zh-cn/') ? ZH_CN_SITE_DESCRIPTION : SITE_DESCRIPTION; @@ -153,7 +148,7 @@ export async function collectPages(): Promise> { } for (const doc of docs) { - const route = normalizeRoute(doc.id); + const route = normalizeOgRouteFromEntryId(doc.id); pages[route] = { title: doc.data.title, description: doc.data.description ?? defaultDescription(route), diff --git a/website/src/routeData.ts b/website/src/routeData.ts index b7053e93..818022a1 100644 --- a/website/src/routeData.ts +++ b/website/src/routeData.ts @@ -1,10 +1,5 @@ import { defineRouteMiddleware } from '@astrojs/starlight/route-data'; - -const DEFAULT_SITE_URL = 'https://bub.build'; - -function normalizeRoute(pathname: string): string { - return pathname.replace(/^\/+|\/+$/g, '') || 'index'; -} +import { createOgImageUrl, normalizeOgRouteFromPathname } from '@/lib/og-routes'; function upsertMetaTag( head: Array<{ tag: string; attrs?: Record }>, @@ -26,9 +21,12 @@ function upsertMetaTag( } export const onRequest = defineRouteMiddleware((context) => { - const route = normalizeRoute(context.url.pathname); - const site = context.site ?? new URL(DEFAULT_SITE_URL); - const imageUrl = new URL(`/og/${route}.png`, site).toString(); + if (!context.site) { + throw new Error('Astro site URL must be configured for docs OG image metadata.'); + } + + const route = normalizeOgRouteFromPathname(context.url.pathname); + const imageUrl = createOgImageUrl(route, context.site); const { head } = context.locals.starlightRoute; upsertMetaTag(head, 'property', 'og:image', imageUrl); From 2c09d4dced26602d9ab90ec043f4fce20e37b4f8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Apr 2026 11:32:05 +0000 Subject: [PATCH 3/3] feat: support docs cover og images Agent-Logs-Url: https://github.com/100gle/bub/sessions/b1b2fae2-02f5-476c-8f16-70213ebb5c95 Co-authored-by: 100gle <36526527+100gle@users.noreply.github.com> --- website/src/content.config.ts | 10 ++++++- website/src/lib/og-routes.ts | 6 +++- website/src/routeData.ts | 52 +++++++++++++++++++++++++++++++---- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/website/src/content.config.ts b/website/src/content.config.ts index 5c1b4d38..88d3ffce 100644 --- a/website/src/content.config.ts +++ b/website/src/content.config.ts @@ -27,7 +27,15 @@ const userwall = defineCollection({ }); export const collections = { - docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), + docs: defineCollection({ + loader: docsLoader(), + schema: docsSchema({ + extend: ({ image }) => + z.object({ + cover: image().optional(), + }), + }), + }), i18n: defineCollection({ loader: i18nLoader(), schema: i18nSchema() }), posts, userwall, diff --git a/website/src/lib/og-routes.ts b/website/src/lib/og-routes.ts index f6f55708..4c57370c 100644 --- a/website/src/lib/og-routes.ts +++ b/website/src/lib/og-routes.ts @@ -1,3 +1,5 @@ +const INDEX_ROUTE_PATTERN = /(?:^|\/)index$/; + function trimRoute(route: string): string { return route.replace(/^\/+|\/+$/g, ''); } @@ -7,7 +9,9 @@ export function normalizeOgRouteFromPathname(pathname: string): string { } export function normalizeOgRouteFromEntryId(id: string): string { - const route = id.replace(/\.(md|mdx)$/, '').replace(/(?:^|\/)index$/, ''); + // Strip the source file extension and collapse trailing `/index` docs files + // onto their directory route so content entry IDs match page URLs. + const route = id.replace(/\.(md|mdx)$/, '').replace(INDEX_ROUTE_PATTERN, ''); return normalizeOgRouteFromPathname(route); } diff --git a/website/src/routeData.ts b/website/src/routeData.ts index 818022a1..d0e05b31 100644 --- a/website/src/routeData.ts +++ b/website/src/routeData.ts @@ -1,6 +1,24 @@ import { defineRouteMiddleware } from '@astrojs/starlight/route-data'; import { createOgImageUrl, normalizeOgRouteFromPathname } from '@/lib/og-routes'; +interface OgImageMeta { + url: string; + width?: string; + height?: string; +} + +interface CoverImage { + src: string; + width?: number | string; + height?: number | string; +} + +interface RouteDataContext { + locals: { starlightRoute: { entry: { data: { cover?: CoverImage } } } }; + site: URL; + url: URL; +} + function upsertMetaTag( head: Array<{ tag: string; attrs?: Record }>, key: 'property' | 'name', @@ -20,17 +38,39 @@ function upsertMetaTag( }); } +function normalizeDimension(value: unknown): string | undefined { + return typeof value === 'number' || typeof value === 'string' ? String(value) : undefined; +} + +function getDocOgImage(context: RouteDataContext): OgImageMeta { + const cover = context.locals.starlightRoute.entry.data.cover; + + if (cover) { + return { + url: new URL(cover.src, context.site).toString(), + width: normalizeDimension(cover.width), + height: normalizeDimension(cover.height), + }; + } + + const route = normalizeOgRouteFromPathname(context.url.pathname); + return { + url: createOgImageUrl(route, context.site), + width: '1200', + height: '630', + }; +} + export const onRequest = defineRouteMiddleware((context) => { if (!context.site) { throw new Error('Astro site URL must be configured for docs OG image metadata.'); } - const route = normalizeOgRouteFromPathname(context.url.pathname); - const imageUrl = createOgImageUrl(route, context.site); + const image = getDocOgImage(context); const { head } = context.locals.starlightRoute; - upsertMetaTag(head, 'property', 'og:image', imageUrl); - upsertMetaTag(head, 'property', 'og:image:width', '1200'); - upsertMetaTag(head, 'property', 'og:image:height', '630'); - upsertMetaTag(head, 'name', 'twitter:image', imageUrl); + upsertMetaTag(head, 'property', 'og:image', image.url); + if (image.width) upsertMetaTag(head, 'property', 'og:image:width', image.width); + if (image.height) upsertMetaTag(head, 'property', 'og:image:height', image.height); + upsertMetaTag(head, 'name', 'twitter:image', image.url); });