Skip to content

feat(seo): add metadata, Open Graph tags, and canonical URLs - #65

Merged
aruneem-bhowmick merged 8 commits into
mainfrom
feat/site-seo-metadata
Jul 2, 2026
Merged

feat(seo): add metadata, Open Graph tags, and canonical URLs#65
aruneem-bhowmick merged 8 commits into
mainfrom
feat/site-seo-metadata

Conversation

@aruneem-bhowmick

Copy link
Copy Markdown
Owner

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

  • A link to /docs/configuration posted in Slack currently renders with no preview text and no image.
  • Search engines index pages under whatever URL variant they crawl first, since nothing marks /docs/configuration as the authoritative one.
  • Every page's browser tab and search-result title show the Next.js default instead of anything specific to that page.

Approach

Metadata is set with the Next.js App Router Metadata API 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 metadataBase pointing at https://lychee.vercel.app so 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 Docs suffix 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 from getDocBySlug. 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.png is 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

  • Added web/app/seo.test.tsx: asserts the shape of every exported metadata object, checks generateMetadata's behavior with getDocBySlug mocked, 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 --noEmit passes clean on every file this PR touches.
  • vitest run passes for all new and existing tests, aside from three pre-existing failures in CodeBlock.test.tsx and lib/highlight.test.ts from a shiki version mismatch. Those failures are already present on main and untouched by this change.

Out of scope

  • Final share-image artwork. The current PNG is a placeholder built to the 1200x630 spec with on-brand colors.
  • Sitemap and robots.txt.
  • A Playwright check of rendered <title>/canonical tags on built pages. The metadata objects are covered directly instead.

- 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.
@aruneem-bhowmick
aruneem-bhowmick merged commit d138e11 into main Jul 2, 2026
4 checks passed
@aruneem-bhowmick
aruneem-bhowmick deleted the feat/site-seo-metadata branch July 2, 2026 02:19
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