Skip to content

chore: reduce Contentful load from bot-driven ISR cache misses - #25

Open
gsteenkamp89 wants to merge 1 commit into
mainfrom
claude/contentful-blog-protect
Open

chore: reduce Contentful load from bot-driven ISR cache misses#25
gsteenkamp89 wants to merge 1 commit into
mainfrom
claude/contentful-blog-protect

Conversation

@gsteenkamp89

Copy link
Copy Markdown
Collaborator

Summary

UMA blog turned out to be ~22% of total Contentful CDA volume on the shared space — confirmed by SDK fingerprint (contentful 10.9.2 + Node 20) and by being the only consumer of content_type=blogPost, the highest-frequency single query in support's report. Most of that volume is ISR cache misses from crawlers hitting parameterized listing URLs.

This PR closes the same three gaps we just fixed in across.to#135 and the dapp PR:

What changed

  • robots.txt → robots.ts. The static app/robots.txt was empty (Disallow:) and pointed at the wrong host (blog.uma.xyz while SITE_BASE_URL is uma.blog.xyz). New app/robots.ts disallows /? so crawlers can't probe /?search=*, /?tag=*, /?product=*, /?page=*, /?utm_*, or anything else with query strings on the root. Articles at /articles/[slug] and the canonical / remain crawlable, and the dynamic sitemap continues to give Google a deterministic discovery path.
  • Listing page param hardening. app/page.tsx now whitelists page, search, tag, product, limit — anything else triggers a redirect to the canonical URL. Out-of-range ?page=N redirects to the first page. Together these prevent bots from minting unbounded ISR cache entries.
  • Per-request dedup on getBlogPostBySlug. Wrapped in React cache(). The contentful SDK at ^10.9.2 uses axios, so Next.js's automatic fetch dedup doesn't apply — both generateMetadata and the page component were independently hitting Contentful for the same slug. Cuts article-page renders from 3 Contentful calls to 2.

Why now

Contentful support flagged limit pressure on the shared space; the across.to fix is in flight. The CSV showed content_type=blogPost&fields.content[exists]=true&limit=10&order=-fields.publishDate (164 calls — exact match for getBlogEntries with default args) as the single most-requested query, plus 1774 calls overall for this app. With ISR already in place (revalidate = 1800 on /), the leak is unique URL variants forcing cache misses, not the canonical render itself.

Test plan

  • pnpm build succeeds locally (verified — /robots.txt and /sitemap.xml show as static routes in the build output).
  • curl https://<deploy>/robots.txt shows Disallow: /? and the correct Sitemap: reference.
  • Visiting /?utm_source=foo (or any unknown param) 307-redirects to /.
  • Visiting /?page=99999 redirects to / (or to the canonical filtered URL if other filters are set).
  • /articles/<slug> still renders correctly with metadata, image, and related articles.
  • Sitemap still serves from the dynamic app/sitemap.ts and lists every published article.
  • Contentful CDA call rate drops within a couple of revalidate cycles after deploy.

Related

🤖 Generated with Claude Code

Three small changes to keep crawler traffic from spamming Contentful via
ISR cache-key inflation, and to dedupe per-request fetches.

- robots.txt → robots.ts: blocks `/?` query variants (search, tag,
  product, page, utm_*, anything). The canonical `/` and every
  /articles/[slug] stay crawlable. Also fixes a host typo by routing
  through SITE_BASE_URL.
- Listing page (app/page.tsx): redirects unknown query params to the
  canonical URL and clamps out-of-range `?page=N` to the first page,
  so bots can't mint unbounded ISR entries with `?cb=12345` or
  `?page=99999` style URLs.
- getBlogPostBySlug wrapped in React `cache()`: contentful 10.9.2 uses
  axios, so Next.js's automatic fetch dedup doesn't apply — cache()
  collapses the duplicate call between generateMetadata and the page
  render down to one Contentful request.
@gsteenkamp89
gsteenkamp89 requested a review from 0xjorgen as a code owner May 4, 2026 11:08
@vercel

vercel Bot commented May 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
uma-blog Ready Ready Preview, Comment May 4, 2026 11:09am

Request Review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6954edf6c9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/page.tsx
Comment on lines +69 to +74
const hasUnknownParams = Object.keys(searchParams).some(
(key) =>
!ALLOWED_SEARCH_PARAMS.includes(
key as (typeof ALLOWED_SEARCH_PARAMS)[number],
),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exempt Next.js internal _rsc param from canonical redirect

The whitelist redirect treats every unknown query key as invalid, but Next.js App Router issues internal requests with an _rsc query parameter for Flight/RSC payloads. With the current check, those framework requests are redirected to a URL without _rsc, which can cause extra 307 hops on every client navigation/prefetch and, in some deployments, broken route transitions or redirect loops when the server/CDN expects _rsc to be preserved. This regression is introduced by the new unknown-param canonicalization logic and should explicitly allow _rsc (or skip redirecting framework-internal requests).

Useful? React with 👍 / 👎.

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.

2 participants