feat(seo): add metadata, Open Graph tags, and canonical URLs - #65
Merged
Conversation
- Export a root `metadata: Metadata` object covering `metadataBase`, the `%s · Lychee Docs` title template with a default title, the canonical value-proposition description, and Open Graph defaults (title, description, image, url, siteName, type). - Point `openGraph.images` at `/og-image.png`, with a comment noting the placeholder can be swapped for final artwork without code changes.
- Export an absolute `title` on the landing page so the root layout's docs title template does not append `· Lychee Docs` to it. - Set `description` to the canonical value proposition, mirror it in `openGraph`, and set `alternates.canonical` to the site root.
- Export a plain string `title: 'Documentation'` so it picks up the root layout's title template. - Set a short description, a `/docs` canonical URL, and an Open Graph image reference.
- Replace the generateMetadata stub in the doc route with a real implementation that resolves the requested doc via getDocBySlug and derives title, description, Open Graph fields, and a slug-specific canonical URL from it. - Restate the combined title explicitly in openGraph.title, since Open Graph fields don't pass through the root layout's title template.
- Commit a real 1200x630 PNG at web/public/og-image.png using the site's background and gradient tokens plus the Lychee wordmark, so og:image resolves correctly on every route. - Final artwork can replace this file at the same path with no code changes required.
- Assert the root layout's metadataBase, default title, title template, description, and Open Graph defaults. - Assert the landing page's absolute title (verifying it opts out of the docs title template), description, canonical, and Open Graph mirror fields. - Assert the docs index's plain string title, description, canonical, and Open Graph image reference. - Mock `next/font/google` so the root layout module can be imported outside of the Next.js build pipeline.
- Mock @/lib/docs so generateMetadata assertions don't depend on real doc content, matching the mocking convention already used by the docs index page tests. - Assert generateMetadata calls getDocBySlug with the requested slug, carries its title/description through, restates the combined title in openGraph.title, and builds a slug-specific canonical URL.
- Smoke: confirm every route's metadata resolves without throwing and that og-image.png is committed as a non-zero-byte, real 1200x630 PNG (verified by reading its PNG signature and IHDR dimensions). - Sanity: pin metadataBase to the deployed domain, confirm every route references /og-image.png, and sweep all descriptions for banned marketing words and the exact value-proposition sentence. - Regression: snapshot the serialized metadata for the landing page, docs index, and a sample doc page to guard against drift. - Mark Component, Accessibility, End-to-end, and Responsive/Mobile as explicitly not applicable, with the reason each is covered instead.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This adds title tags, meta descriptions, Open Graph tags, and canonical URLs to every route on the site: the landing page, the docs index, and each individual doc page. None of that exists today, so shared links produce blank or generic previews in Slack, Twitter, and LinkedIn, and search engines have no signal for which URL is canonical when the same content is reachable through more than one path.
What this fixes
/docs/configurationposted in Slack currently renders with no preview text and no image./docs/configurationas the authoritative one.Approach
Metadata is set with the Next.js App Router
MetadataAPI rather than hand-written<head>tags, so everything resolves at build time and the site stays fully static. No new environment variables and no runtime data fetches.The root layout now exports base metadata: a
metadataBasepointing athttps://lychee.vercel.appso relative canonical and image paths resolve correctly, a title template (%s · Lychee Docs) that child pages inherit unless they opt out, the default description, and Open Graph defaults referencing a committed share image.The landing page sets an absolute title so the layout's template doesn't append the docs suffix to it, the value-proposition sentence as its description, and a canonical pointing at the site root.
The docs index sets a plain string title, which does pick up the
· Lychee Docssuffix from the template, plus a short description and a canonical for/docs.Individual doc pages implement
generateMetadata, which was previously a stub returning nothing. Each of the 11 doc routes now resolves its own title, description, canonical, and Open Graph tags fromgetDocBySlug. Open Graph titles are built explicitly rather than through the layout's template, since Open Graph fields don't pass through it.web/public/og-image.pngis a real, committed 1200x630 PNG built from the site's own background and gradient tokens plus the Lychee wordmark. It's a placeholder: swapping in final artwork later is a file replacement, not a code change.Testing
web/app/seo.test.tsx: asserts the shape of every exported metadata object, checksgenerateMetadata's behavior withgetDocBySlugmocked, reads the PNG's signature and IHDR dimensions directly, sweeps every description for banned marketing language, and snapshots the serialized metadata for the landing page, docs index, and a sample doc page.tsc --noEmitpasses clean on every file this PR touches.vitest runpasses for all new and existing tests, aside from three pre-existing failures inCodeBlock.test.tsxandlib/highlight.test.tsfrom a shiki version mismatch. Those failures are already present onmainand untouched by this change.Out of scope
<title>/canonical tags on built pages. The metadata objects are covered directly instead.