Skip to content

seo hygiene P0: cache headers + robots dedup + case redirects + og:image - #1473

Merged
Flotapponnier merged 1 commit into
devfrom
feat/seo-hygiene-p0
Jul 26, 2026
Merged

seo hygiene P0: cache headers + robots dedup + case redirects + og:image#1473
Flotapponnier merged 1 commit into
devfrom
feat/seo-hygiene-p0

Conversation

@Flotapponnier

Copy link
Copy Markdown
Collaborator

Summary

Four verified crawl / SEO issues confirmed live on prod (2026-07-26):

  1. Sitemap + /api/citable have no s-maxage — every crawler hit re-runs the loader chain (x-vercel-cache: MISS observed). Add explicit Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400 via next.config.ts headers().

  2. /404 emitted 2 identical <meta name="robots" content="noindex"> tags — not-found.tsx set { index: false } and Next merged instead of replacing the layout's robots shape. Give not-found the full IS_STAGING-style shape so Next replaces.

  3. Mixed-case URLs served 200 instead of 308-lowercase — /products/Alchemy + /Benchmarks/foo both 200. Add case-normalising 308 at the top of middleware; broaden matcher to cover every HTML route.

  4. Bench pages with chain variants emitted 4-5 og:image tags — aggregator-head-lag has 5, perp-fees has 4. Social scrapers pick nondeterministically. Chain-scoped OG cards were never actually rendered on the /[chain] pages anyway (they inherit site-wide OG), so the fan-out gave no upside. Return only the default entry from generateImageMetadata.

Test plan

  • pnpm typecheck
  • After merge → dev preview: verify /sitemap.xml headers include s-maxage, /does-not-exist has single robots tag, /Products/Alchemy returns 308, /benchmarks/aggregator-head-lag has 1 og:image
  • After cherry-pick → main: same 4 checks on openchainbench.com

Four verified crawl / SEO issues confirmed on prod (2026-07-26):

1. Sitemap + /api/citable served no s-maxage, every crawl re-ran the
   loader chain (x-vercel-cache: MISS observed). Add explicit
   Cache-Control on both via next.config.ts headers().

2. /404 emitted two identical <meta name="robots" content="noindex">
   tags because not-found.tsx set { index: false } and Next merged
   instead of replacing the layout's robots shape. Give not-found the
   full IS_STAGING-style shape so Next replaces.

3. Mixed-case URLs served 200 instead of 308-lowercase
   (/products/Alchemy, /Benchmarks/foo). Add case-normalising 308 at
   the top of middleware; broaden matcher to cover every HTML route.

4. Bench pages with chain variants (aggregator-head-lag, perp-fees)
   emitted 4-5 <meta property="og:image"> tags via generateImageMetadata
   fanning out per-chain entries. Social scrapers picked
   nondeterministically. Chain-scoped OG cards were never rendered on
   the actual /[chain] pages anyway (they inherit the site-wide OG),
   so the fan-out gave no upside. Return only the default entry.
@Flotapponnier
Flotapponnier merged commit 4fa780c into dev Jul 26, 2026
1 check failed
Flotapponnier added a commit that referenced this pull request Jul 26, 2026
…age (#1473) (#1474)

Four verified crawl / SEO issues confirmed on prod (2026-07-26):

1. Sitemap + /api/citable served no s-maxage, every crawl re-ran the
   loader chain (x-vercel-cache: MISS observed). Add explicit
   Cache-Control on both via next.config.ts headers().

2. /404 emitted two identical <meta name="robots" content="noindex">
   tags because not-found.tsx set { index: false } and Next merged
   instead of replacing the layout's robots shape. Give not-found the
   full IS_STAGING-style shape so Next replaces.

3. Mixed-case URLs served 200 instead of 308-lowercase
   (/products/Alchemy, /Benchmarks/foo). Add case-normalising 308 at
   the top of middleware; broaden matcher to cover every HTML route.

4. Bench pages with chain variants (aggregator-head-lag, perp-fees)
   emitted 4-5 <meta property="og:image"> tags via generateImageMetadata
   fanning out per-chain entries. Social scrapers picked
   nondeterministically. Chain-scoped OG cards were never rendered on
   the actual /[chain] pages anyway (they inherit the site-wide OG),
   so the fan-out gave no upside. Return only the default entry.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
Flotapponnier added a commit that referenced this pull request Jul 26, 2026
Next Metadata routes ignore next.config.ts headers() — the previous
sitemap.ts left Next in charge of the response headers and hard-coded
Cache-Control: public, max-age=0, must-revalidate on force-dynamic
sitemaps. Every crawler hit re-ran the full loader chain (x-vercel-cache:
MISS observed on prod 2026-07-26 after PR #1473 landed the config-level
attempt). Confirmed by curl: my next.config /sitemap.xml rule was
silently dropped.

Move the builder logic to src/lib/sitemap-builder.ts and add a
Route Handler at src/app/sitemap.xml/route.ts that returns a raw
Response with:
  Content-Type: application/xml
  Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400

Same builder logic, same buildFullSitemap + buildStaticFallback,
just serialized to XML in the route and shipped with a real edge
cache header. Also drop the now-redundant /sitemap.xml rule from
next.config headers().

Co-authored-by: Florent Tapponnier <contact@mobula.io>
Flotapponnier added a commit that referenced this pull request Jul 26, 2026
Next Metadata routes ignore next.config.ts headers() — the previous
sitemap.ts left Next in charge of the response headers and hard-coded
Cache-Control: public, max-age=0, must-revalidate on force-dynamic
sitemaps. Every crawler hit re-ran the full loader chain (x-vercel-cache:
MISS observed on prod 2026-07-26 after PR #1473 landed the config-level
attempt). Confirmed by curl: my next.config /sitemap.xml rule was
silently dropped.

Move the builder logic to src/lib/sitemap-builder.ts and add a
Route Handler at src/app/sitemap.xml/route.ts that returns a raw
Response with:
  Content-Type: application/xml
  Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400

Same builder logic, same buildFullSitemap + buildStaticFallback,
just serialized to XML in the route and shipped with a real edge
cache header. Also drop the now-redundant /sitemap.xml rule from
next.config headers().

Co-authored-by: Florent Tapponnier <contact@mobula.io>
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