Skip to content

feat(seo): use branded og images per section - #480

Merged
TheMeinerLP merged 2 commits into
mainfrom
feat/og-images
Oct 10, 2026
Merged

TheMeinerLP merged 2 commits into
mainfrom
feat/og-images

Conversation

@TheMeinerLP

Copy link
Copy Markdown
Collaborator

Summary

Section pages (home, blog, projects, community, community POI, events, team, about, BlueMap) now share a branded 1200x630 social card per locale instead of falling back to /images/og-default.png. Project detail pages without a logo use the projects card.

The cards are rendered once by scripts/render-og-images.mjs from the real components/OgImage/NuxtSeo.satori.vue template (no prerender pass) and uploaded to the image bucket. The template itself is rebranded: OneLiteFeather logo (240 px, about 2x the previous size), "OneLiteFeather.net" wordmark, MD3 dark-scheme colours, title and description props only.

Problem

With ogImage.zeroRuntime: true and no nitro.prerender, no OG images are generated. Every page without its own image shares og-default.png, and the ejected template still showed the nuxt-og-image "Nuxt SEO" logo.

Decision: fixed images per section, no prerender

Prerendering was tried first (recipe in references/og-images.md, crawlLinks from /en and /de) and rejected:

  1. Prerendered HTML becomes static. The crawl rendered 269 routes, and 101 HTML pages were written to .output/public. The generated wrangler.json has no run_worker_first, so Workers Assets would serve those pages ahead of the Worker. That bypasses SSR, the time-dependent event phase, and the A/B flag.
  2. Runtime pages cannot reference the generated files. defineOgImage returns [] outside prerender (zero-runtime.js), so only the prerendered HTML carried the generated og:image. Deriving the URL at runtime would mean replicating the module's /_og/s/<encoded params> scheme.
  3. Two further build issues. The native @resvg/resvg-js binding is not installed (fixed with the already-installed WASM build, but only relevant if prerender is used). TeamMember cards fail because the module blocks the remote mc-heads.net avatar. Both are moot for this approach.

This PR does not add nitro.prerender, does not touch zeroRuntime, and does not add prerendered or static HTML.

Uploaded keys

All 18 keys were uploaded with rclone copyto --s3-no-check-bucket --immutable, and only after every upload had finished were they requested. Each was checked with curl -sI https://img.onelitefeather.net/images/og/<key>:

key status content-type
images/og/home-de.png, images/og/home-en.png 200 image/png
images/og/blog-de.png, images/og/blog-en.png 200 image/png
images/og/projects-de.png, images/og/projects-en.png 200 image/png
images/og/community-de.png, images/og/community-en.png 200 image/png
images/og/community-poi-de.png, images/og/community-poi-en.png 200 image/png
images/og/events-de.png, images/og/events-en.png 200 image/png
images/og/team-de.png, images/og/team-en.png 200 image/png
images/og/about-de.png, images/og/about-en.png 200 image/png
images/og/bluemap-de.png, images/og/bluemap-en.png 200 image/png

Re-rendering reproduces the uploaded bytes: the MD5 of a fresh home-de.png and home-en.png equals the bucket ETags.

Wiring

  • The page passes image: sectionOgImage('<section>', locale.value) to usePageSeo, or to useHomeSeo on the home page. sectionOgImage lives in layers/content-core/utils/sectionOgImage.ts.
  • Paths resolve through the same img() transform as other page images (fit: cover, 1200x630). A quick check of …/cdn-cgi/image/w=1200,h=630,fit=cover,f=webp,q=80/images/og/home-de.png returned a 1200x630 PNG, so there is no recrop.
  • Project detail: image: project.value?.logo || sectionOgImage('projects', locale.value).
  • Events, POI, team profiles and blog posts keep their own images.
  • Pages outside this list still use og-default.png.

Regenerating the cards

pnpm build                                  # copies the Inter fonts into .output/public/_og-static-fonts
node scripts/render-og-images.mjs           # 18 PNGs in node_modules/.cache/og-section-cards/png/
# then run the printed rclone copyto commands, then curl -sI the keys

Re-run after changing the template or a section's i18n title or description. Its SECTIONS list must match OG_SECTIONS in sectionOgImage.ts; tests/seo/section-og-images.spec.ts enforces this.

Rendered card

The card is 1200x630 on the dark MD3 surface (#121319). The feather logo is on the left at 240 px, with the "OneLiteFeather.net" wordmark in the primary colour above the page title (68 px, up to two lines). The description sits below in the on-surface-variant colour, and a gradient bar in the primary, secondary and tertiary colours runs along the bottom.

Verification

  • pnpm test: 166 files, 1313 tests pass. New specs: tests/seo/og-image-template.spec.ts (6) and tests/seo/section-og-images.spec.ts (16).
  • pnpm quality: ESLint errors 186, warnings 17, TypeScript errors 3, all at baseline.
  • pnpm build: passes. .output/public/_og is absent, and the only prerendered routes are the existing content SQL dumps.
  • Dev server (NUXT_IMAGE_PROVIDER=cloudflare): not a usable production check. In dev the module's defineOgImage is active and overrides og:image with /_og/d/... URLs. The production value comes from usePageSeo's socialImage, which the spec and build cover.

Not in this PR

  • TeamMember.satori.vue is unchanged. Its remote avatar fetch is blocked at render time, so it is out of scope.
  • The composables keep their existing defineOgImage('NuxtSeo.satori', …) call, which is a no-op at runtime under zeroRuntime and was left as is.

https://claude.ai/code/session_019zws4ceJu2jctk3XT48fMX

Section pages share one fixed 1200x630 card per locale, rendered from the
ejected NuxtSeo.satori template by scripts/render-og-images.mjs and uploaded
to images/og/<section>-<locale>.png. Home, blog, projects, community,
community-poi, events, team, about and bluemap pass their card to usePageSeo;
project detail pages without a logo fall back to the projects card.

Claude-Session: https://claude.ai/code/session_019zws4ceJu2jctk3XT48fMX
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
launchpad 693a3fa Commit Preview URL

Branch Preview URL
Oct 10 2026, 10:54 PM

Section og images live behind the image proxy, so the local check now
decides by whether the file exists in public/ instead of a fixed list
of routes.

Claude-Session: https://claude.ai/code/session_019zws4ceJu2jctk3XT48fMX
@TheMeinerLP
TheMeinerLP merged commit 142e70e into main Oct 10, 2026
8 checks passed
@TheMeinerLP
TheMeinerLP deleted the feat/og-images branch October 10, 2026 22:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant