Skip to content

feat(web): IndexNow ping + Search Console/Bing verification#916

Merged
AyushRajSinghParihar merged 1 commit into
mainfrom
feat/web-indexnow-verification
Jun 2, 2026
Merged

feat(web): IndexNow ping + Search Console/Bing verification#916
AyushRajSinghParihar merged 1 commit into
mainfrom
feat/web-indexnow-verification

Conversation

@AyushRajSinghParihar
Copy link
Copy Markdown
Collaborator

What

Two SEO indexing features from the gap research.

IndexNow — instantly notifies Bing/Yandex/Naver/Seznam/Yep (not Google) when content changes; since Bing feeds Microsoft Copilot, it also helps Copilot freshness.

  • web/public/<key>.txt key file, excluded from the AuthKit middleware matcher so it serves statically (not redirected to /auth/login).
  • web/src/lib/indexnow.tsbuildUrlList() reuses sitemap() (never drifts); submitIndexNow() POSTs to api.indexnow.org and returns the upstream status without throwing.
  • web/src/app/api/indexnow/route.tsCRON_SECRET-guarded GET; logs the failure path.
  • web/vercel.json — daily Vercel Cron (0 6 * * *).

Webmaster verification — the GSC/Bing meta wiring already existed inline in layout.tsx; this extracts it into a pure, unit-testable webmasterVerification() helper in lib/seo (identical behavior) and adds a runbook.

Verification

  • vitest9/9 new assertions pass: IndexNow (buildUrlList, submit happy + bad-key failure, route 401 when CRON_SECRET set) and webmasterVerification (both / one / neither token).
  • tsc --noEmit clean, eslint clean, vercel.json valid JSON.
  • No new npm deps (no package-lock.json change); no backend route (OpenAPI untouched).

⚠️ Needs you to activate (config only — no code)

Per docs/frontend/seo-verification.md:

  1. IndexNow: set INDEXNOW_KEY=265a46be97a2ce1f8891dd452d243327 and CRON_SECRET=$(openssl rand -hex 32) in Vercel Production.
  2. Verification: get GSC + Bing HTML-tag tokens, set NEXT_PUBLIC_GSC_VERIFICATION / NEXT_PUBLIC_BING_VERIFICATION in Vercel Production, redeploy, click Verify, submit the sitemap.

Until then the IndexNow ping is harmless (key falls back to the committed literal; cron just pings) and verification renders no meta tag (no crash).

🤖 Generated with Claude Code

IndexNow (Bing/Yandex/Naver/Seznam/Yep — not Google; Bing feeds Copilot):
- public/<key>.txt key file, excluded from the AuthKit middleware matcher so it
  serves as a static file (not redirected to /auth/login)
- lib/indexnow.ts: buildUrlList() reuses sitemap(); submitIndexNow() POSTs to
  api.indexnow.org and returns the upstream status without throwing
- app/api/indexnow/route.ts: CRON_SECRET-guarded GET; logs the failure path
- vercel.json: daily Vercel Cron (0 6 * * *)

Webmaster verification: extract the GSC/Bing logic into a pure, unit-testable
webmasterVerification() helper in lib/seo (behavior identical to the old inline
block) and wire it into layout.tsx. Activation (tokens + Vercel env) is a config
step documented in docs/frontend/seo-verification.md; .env.local.example updated.

Tests (9 new): indexnow buildUrlList, submit happy + bad-key, route 401; and
webmasterVerification for both/one/neither tokens. No new deps; no backend route.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 2, 2026

Greptile Summary

Adds two SEO indexing features: an IndexNow integration (lib/indexnow.ts + /api/indexnow cron route) that submits the sitemap URL list to Bing/Yandex/Naver/Seznam/Yep daily, and a refactored webmasterVerification() helper that extracts the existing GSC/Bing <head> meta-tag logic into a testable function.

  • The IndexNow key file is committed to web/public/, excluded from the AuthKit middleware matcher, and the submission logic reuses sitemap() directly so the URL list never drifts from what's published.
  • webmasterVerification() is a pure extraction from layout.tsx with identical behavior, backed by three new Vitest assertions; both features are opt-in via Vercel env vars and safe to deploy without activation.

Confidence Score: 4/5

Safe to merge; both features are activation-gated by env vars and don't affect any existing user-facing path.

The core logic is correct and well-tested. The two findings are both in the IndexNow cron path — a missing network-error catch in the route handler and a key-rotation footgun in the static middleware matcher — neither of which affects deployed users until the feature is activated.

web/src/app/api/indexnow/route.ts (unhandled network throw) and web/src/middleware.ts (static key exclusion that will need updating on key rotation)

Important Files Changed

Filename Overview
web/src/lib/indexnow.ts New IndexNow integration; derives HOST/KEY from DOCS_ORIGIN; submitIndexNow comment claims "Never throws" but fetch network errors are unhandled and can propagate
web/src/app/api/indexnow/route.ts CRON_SECRET-guarded endpoint; no try/catch around submitIndexNow so network errors yield unhandled 500; auth guard correctly absent when CRON_SECRET unset
web/src/middleware.ts Adds the IndexNow key filename to the middleware exclusion list so AuthKit doesn't intercept it; filename is hardcoded, requiring manual sync on key rotation
web/src/lib/seo.ts Extracts webmasterVerification() helper from layout.tsx; clean, unit-tested, correctly returns undefined when no tokens are set
web/src/app/layout.tsx Refactors inline verification spread to use webmasterVerification(); behavior is identical, no regressions
web/vercel.json Adds daily Vercel Cron at 06:00 UTC to /api/indexnow; valid JSON, correct cron expression
web/src/lib/indexnow.test.ts Good unit coverage: buildUrlList, happy-path submit, 403 failure path, and 401 route guard; fetch is cleanly mocked via vi.stubGlobal
web/src/lib/seo.test.ts Adds three assertions covering both-tokens, one-token, and neither-token cases; env vars properly restored in afterEach

Sequence Diagram

sequenceDiagram
    participant VC as Vercel Cron (daily 06:00 UTC)
    participant R as /api/indexnow (route.ts)
    participant IL as indexnow.ts buildUrlList / submitIndexNow
    participant SM as sitemap() (@/app/sitemap)
    participant IN as api.indexnow.org

    VC->>R: GET /api/indexnow Authorization Bearer CRON_SECRET
    R->>R: Validate CRON_SECRET (if set)
    R->>IL: buildUrlList()
    IL->>SM: sitemap()
    SM-->>IL: "[{url...}]"
    IL-->>R: string[]
    R->>IL: submitIndexNow(urlList)
    IL->>IN: "POST /IndexNow {host, key, keyLocation, urlList}"
    IN-->>IL: 200 / 202 / 403
    IL-->>R: "{status, body}"
    alt 200 or 202
        R-->>VC: "200 {ok:true, submitted:N}"
    else other (e.g. 403)
        R-->>VC: "502 {ok:false, indexnowStatus:403}"
    end
Loading

Fix All in Codex

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
web/src/app/api/indexnow/route.ts:27
Network-level fetch errors are unhandled. `submitIndexNow` documents "never throws on a non-2xx response," but a DNS failure, TCP reset, or timeout will throw from `fetch()` itself — not from a non-2xx response — and that exception will propagate out of this handler as an unhandled rejection, producing an opaque 500 with no structured error body in the cron logs.

```suggestion
  let result: { status: number; body: string };
  try {
    result = await submitIndexNow(urlList);
  } catch (err) {
    console.error("IndexNow ping threw (network error):", err);
    return NextResponse.json(
      { ok: false, error: "network_error" },
      { status: 502 },
    );
  }
  const { status, body } = result;
```

### Issue 2 of 2
web/src/middleware.ts:41
Key filename is hardcoded in the middleware exclusion list. The middleware matcher is a static regex evaluated at build time, so it can't read `INDEXNOW_KEY` dynamically. If the key is ever rotated — new file at `web/public/<NEW_KEY>.txt` — the new filename must also be added here, otherwise AuthKit will intercept requests to the new key file and return a redirect instead of the key content, silently failing IndexNow domain verification.

Reviews (1): Last reviewed commit: "feat(web): add IndexNow ping and wire Se..." | Re-trigger Greptile

return NextResponse.json({ ok: true, submitted: 0 });
}

const { status, body } = await submitIndexNow(urlList);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Network-level fetch errors are unhandled. submitIndexNow documents "never throws on a non-2xx response," but a DNS failure, TCP reset, or timeout will throw from fetch() itself — not from a non-2xx response — and that exception will propagate out of this handler as an unhandled rejection, producing an opaque 500 with no structured error body in the cron logs.

Suggested change
const { status, body } = await submitIndexNow(urlList);
let result: { status: number; body: string };
try {
result = await submitIndexNow(urlList);
} catch (err) {
console.error("IndexNow ping threw (network error):", err);
return NextResponse.json(
{ ok: false, error: "network_error" },
{ status: 502 },
);
}
const { status, body } = result;
Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/app/api/indexnow/route.ts
Line: 27

Comment:
Network-level fetch errors are unhandled. `submitIndexNow` documents "never throws on a non-2xx response," but a DNS failure, TCP reset, or timeout will throw from `fetch()` itself — not from a non-2xx response — and that exception will propagate out of this handler as an unhandled rejection, producing an opaque 500 with no structured error body in the cron logs.

```suggestion
  let result: { status: number; body: string };
  try {
    result = await submitIndexNow(urlList);
  } catch (err) {
    console.error("IndexNow ping threw (network error):", err);
    return NextResponse.json(
      { ok: false, error: "network_error" },
      { status: 502 },
    );
  }
  const { status, body } = result;
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

Comment thread web/src/middleware.ts
matcher: [
"/((?!_next/static|_next/image|favicon.ico|og-image.png|twitter-image.png|favicon-16x16.png|favicon-32x32.png|favicon-96x96.png|apple-touch-icon.png|robots.txt|sitemap.xml).*)",
"/((?!_next/static|_next/image|favicon.ico|og-image.png|twitter-image.png|favicon-16x16.png|favicon-32x32.png|favicon-96x96.png|apple-touch-icon.png|robots.txt|sitemap.xml|265a46be97a2ce1f8891dd452d243327.txt).*)",
],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Key filename is hardcoded in the middleware exclusion list. The middleware matcher is a static regex evaluated at build time, so it can't read INDEXNOW_KEY dynamically. If the key is ever rotated — new file at web/public/<NEW_KEY>.txt — the new filename must also be added here, otherwise AuthKit will intercept requests to the new key file and return a redirect instead of the key content, silently failing IndexNow domain verification.

Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/middleware.ts
Line: 41

Comment:
Key filename is hardcoded in the middleware exclusion list. The middleware matcher is a static regex evaluated at build time, so it can't read `INDEXNOW_KEY` dynamically. If the key is ever rotated — new file at `web/public/<NEW_KEY>.txt` — the new filename must also be added here, otherwise AuthKit will intercept requests to the new key file and return a redirect instead of the key content, silently failing IndexNow domain verification.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

@AyushRajSinghParihar AyushRajSinghParihar merged commit 79694f0 into main Jun 2, 2026
6 checks passed
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