Skip to content

fix(seo): submit only changed URLs to IndexNow, and fix the workflow - #2590

Open
innolope-dev wants to merge 2 commits into
mainfrom
fix/indexnow-delta-pings
Open

fix(seo): submit only changed URLs to IndexNow, and fix the workflow#2590
innolope-dev wants to merge 2 commits into
mainfrom
fix/indexnow-delta-pings

Conversation

@innolope-dev

@innolope-dev innolope-dev commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

The workflow has never run successfully

.github/workflows/indexnow.yml resolves Node from node-version-file: '.node-version', but that file does not exist in this repo. Every production deploy since the workflow landed in February has failed at actions/setup-node, before pnpm install:

The specified node version file at: .../.node-version does not exist

0 successes out of 2750 runs (348 failures, the rest correctly skipped preview deploys). IndexNow has never actually been pinged from CI. The key at public/054e10e6239a45cb2d06e92d669f5b6f.txt is in place, so nothing else was wrong — the job just never got that far.

Behind that, it was pinging far too much

deployment_status fires on every successful Production deploy (~14/day), and the script rebuilt and submitted the entire URL list each time, unconditionally. IndexNow is for URLs that were added, updated or deleted — resubmitting an unchanged site burns the daily quota and is the pattern that gets a host's submissions deprioritised. Nothing changes on /en/argentina because a mobile jank fix merged.

And the URL list had drifted from the sitemap

scripts/ping-indexnow.ts kept its own copy of the page list. It had fallen behind sitemap.ts by 205 URLs — missing blog posts, stories, use-cases, withdraw, deposit rails, pricing, supported-networks and the content hub. Those are exactly the new-article pages worth submitting.

Changes

  • node-version: '20' (matching tests.yml) so the job runs at all.
  • URLs come from sitemap.ts instead of a second, drifting copy — 916 → 1121 URLs, and no way to drift again.
  • Delta by default. The sitemap's URL set is diffed against the set submitted last time (kept in the Actions cache), and content files changed since then are mapped back to the pages they render. Nothing to submit → no API call.
  • Skip the install when nothing feeding the sitemap changed (content submodule, src/data/seo, sitemap.ts, src/i18n, src/lib/{content,blog}.ts).
  • workflow_dispatch gains a full input for a deliberate full resubmission.
  • Sitemap URLs are rewritten onto the production origin — BASE_URL is env-dependent, and a stray NEXT_PUBLIC_BASE_URL would otherwise submit preview URLs under peanut.me.
  • concurrency: indexnow serialises overlapping deploys so they can't race on the cache.

Effect

deploy before after
no content change 916 URLs none — job skips before install
one blog post added 916 URLs (post not even included) 5 (one per locale)
help article + corridor edited 916 URLs 10
first run / cache evicted / manual full 916 URLs 1121 (full, deliberate)

Verification

Ran locally against a mock IndexNow endpoint, and the workflow's resolve step against real git history:

  • bootstrap with no cached state → full 1121
  • immediately after → Nothing changed — skipping IndexNow submission.
  • 5 blog URLs removed from cached state → submits exactly those 5
  • content/help/passkeys/*.md + content/send-to/argentina/from/spain/en.md changed → submits exactly /{locale}/help/passkeys and /{locale}/send-money-from/spain/to/argentina, 10 URLs
  • singleton content/help/en.md matches /{locale}/help only, not all 37 help articles
  • resolve step returns run=false when nothing changed, and run=true for commit 032683ba9 ("register Faster Payments + SPEI as deposit rails") — a data-only SEO change that adds URLs without moving the content submodule
  • pnpm typecheck clean; prettier --check clean

The first run after merge will submit the full sitemap once (no cached state), which is correct — it's also the first ping this site has ever sent.

Summary by CodeRabbit

  • New Features

    • IndexNow submissions now support full-site mode or changed-content-only mode.
    • Added a manual option to submit every sitemap URL.
    • Automatically detects newly added and updated pages for targeted notifications.
  • Bug Fixes

    • Prevented overlapping IndexNow runs from causing submission-state conflicts.
    • Improved behavior when prior submission history is unavailable.
    • Failed request batches no longer abort the entire run; errors are reported after processing.
  • Performance / Reliability

    • Skips submissions when no sitemap URLs have changed and batches requests for more dependable delivery.
  • Chores

    • Updated ignore rules for persisted IndexNow submission state.

The workflow has never succeeded: it resolves Node from a `.node-version`
file that does not exist in this repo, so every production deploy since
February failed at setup-node before reaching the script. 0 of 2750 runs
sent a ping.

Behind that, the script resubmitted all ~900 URLs on every successful
production deploy (~14/day). IndexNow is for URLs that were added, updated
or deleted; resubmitting an unchanged site burns the daily quota and gets
the host deprioritised.

- Pin node-version: '20' so the job runs at all.
- Take URLs from sitemap.ts instead of a second, drifted copy of the page
  list — the copy was missing blog posts, stories, use-cases, withdraw,
  deposit rails, pricing, supported-networks and the content hub, i.e.
  exactly the new-article pages worth submitting (916 vs 1121 URLs).
- Default to a delta: diff the sitemap's URL set against the set submitted
  last time (cached between runs) and map content files changed since then
  back to the pages they render. A typical content deploy now submits ~15
  URLs instead of ~1100, and a deploy that touches no content submits none.
- Skip install entirely when nothing that feeds the sitemap has changed.
- workflow_dispatch gains a `full` input for a deliberate full resubmission.

Rewrite sitemap URLs onto the production origin, since BASE_URL is
env-dependent and a stray NEXT_PUBLIC_BASE_URL would otherwise submit
preview URLs under peanut.me.
@innolope-dev innolope-dev self-assigned this Jul 30, 2026
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview Jul 30, 2026 5:02pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3dc16fa0-4bca-43f6-9e2a-3473978d1868

📥 Commits

Reviewing files that changed from the base of the PR and between d992db2 and c7d84d6.

📒 Files selected for processing (2)
  • .github/workflows/indexnow.yml
  • scripts/ping-indexnow.ts

📝 Walkthrough

Walkthrough

The IndexNow workflow supports manual full and automatic delta submissions, detects sitemap-relevant content changes, restores and saves cached state, and serializes runs. The ping script derives URLs from the sitemap, submits batches with per-batch failure handling, and persists submission metadata.

Changes

IndexNow delta submission

Layer / File(s) Summary
Sitemap state and delta computation
scripts/ping-indexnow.ts, .gitignore
The script derives and deduplicates sitemap URLs, compares them with persisted state, maps changed content slugs to affected URLs, supports full and delta branches, and writes updated state metadata.
Submission modes and batching
scripts/ping-indexnow.ts
CLI path submissions and sitemap submissions use batched IndexNow requests that continue after transport failures and report failed batches.
Workflow change detection and cache orchestration
.github/workflows/indexnow.yml
The workflow restores cached state, detects relevant changes, supports manual and forced full submissions, gates execution, serializes runs, passes change metadata, and saves state after submission.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GitHubActions
  participant StateCache
  participant ChangeDetector
  participant ping-indexnow.ts
  participant IndexNowAPI
  GitHubActions->>StateCache: restore submission state
  GitHubActions->>ChangeDetector: detect sitemap-relevant changes
  ChangeDetector-->>GitHubActions: return run decision and submission metadata
  GitHubActions->>ping-indexnow.ts: pass full mode and changed files
  ping-indexnow.ts->>IndexNowAPI: submit URL batches
  GitHubActions->>StateCache: save updated submission state
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: delta IndexNow submissions plus workflow fixes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/indexnow-delta-pings

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: dependency version conflict. Check your lock file or package.json.


Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 6350.19 → 6350.19 (0)
Findings: 0 net (+0 new, -0 resolved)

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 2294 ran, 0 failed, 0 skipped, 36.9s

📊 Coverage (unit)

metric %
statements 62.5%
branches 45.9%
functions 52.2%
lines 62.9%
⏱ 10 slowest test cases
time test
3.3s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.1s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.4s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.4s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
0.3s src/utils/__tests__/demo-balance.test.ts › starts at the full balance on a fresh install and stamps a timestamp
0.2s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › keeps stickers off the username pill (final pass respects the keep-out)
0.2s src/utils/__tests__/demo-balance.test.ts › auto-refills a wallet older than the TTL on cold start
0.2s src/utils/__tests__/demo-balance.test.ts › debits and floors at zero
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
scripts/ping-indexnow.ts (1)

119-153: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

submit() doesn't catch fetch failures — a network error/timeout crashes the whole run instead of being counted as a batch failure.

fetch() is only guarded for HTTP status codes ≥400; a rejected promise (DNS failure, connection reset, or the 30s AbortController timeout firing) propagates as an unhandled rejection. That aborts main() mid-loop, skips any remaining batches, and — critically — skips the writeState(current) call that follows submit(urls) in every caller in main(), so a single flaky batch loses all progress for the run instead of failing gracefully and reporting which batches actually failed.

🛠️ Proposed fix: treat network/timeout errors the same as HTTP failures
         const controller = new AbortController()
         const timeout = setTimeout(() => controller.abort(), 30_000)
-        const res = await fetch(INDEXNOW_ENDPOINT, {
-            method: 'POST',
-            headers: { 'Content-Type': 'application/json; charset=utf-8' },
-            body: JSON.stringify({
-                host: 'peanut.me',
-                key: KEY,
-                keyLocation: `${PRODUCTION_ORIGIN}/${KEY}.txt`,
-                urlList: batch,
-            }),
-            signal: controller.signal,
-        }).finally(() => clearTimeout(timeout))
-
-        console.log(
-            `Batch ${Math.floor(i / MAX_URLS_PER_REQUEST) + 1}: ${res.status} ${res.statusText} (${batch.length} URLs)`
-        )
-
-        if (res.status >= 400) {
-            console.error('  Error:', await res.text())
-            failures++
-        }
+        try {
+            const res = await fetch(INDEXNOW_ENDPOINT, {
+                method: 'POST',
+                headers: { 'Content-Type': 'application/json; charset=utf-8' },
+                body: JSON.stringify({
+                    host: 'peanut.me',
+                    key: KEY,
+                    keyLocation: `${PRODUCTION_ORIGIN}/${KEY}.txt`,
+                    urlList: batch,
+                }),
+                signal: controller.signal,
+            })
+            console.log(
+                `Batch ${Math.floor(i / MAX_URLS_PER_REQUEST) + 1}: ${res.status} ${res.statusText} (${batch.length} URLs)`
+            )
+            if (res.status >= 400) {
+                console.error('  Error:', await res.text())
+                failures++
+            }
+        } catch (err) {
+            console.error(`  Batch ${Math.floor(i / MAX_URLS_PER_REQUEST) + 1} failed:`, err)
+            failures++
+        } finally {
+            clearTimeout(timeout)
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/ping-indexnow.ts` around lines 119 - 153, Update submit() to catch
fetch and timeout/network errors for each batch, count them in failures, and
continue processing remaining batches. Report the affected batch and error
details, while preserving the existing HTTP failure handling and final nonzero
exit when any batch fails.
🧹 Nitpick comments (1)
scripts/ping-indexnow.ts (1)

184-203: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Delta submission only ever grows the tracked URL set — removed pages are never resubmitted for deindexing.

added/updated are both derived by filtering current, so a URL that existed in previous.urls but no longer appears in current (a deleted page, retired corridor, etc.) is simply dropped from tracking and never gets IndexNow-notified. IndexNow's purpose per the file's own header comment is signalling "added, updated or deleted" URLs — deleted ones currently get no signal, leaving stale pages indexed longer than necessary.

Consider also computing removed = previous.urls.filter((url) => !current.includes(url)) and including those in the submission batch (IndexNow will re-crawl and see the 404, prompting deindexing).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/ping-indexnow.ts` around lines 184 - 203, Update the delta
computation around added and updated to also derive removed URLs from
previous.urls entries absent from current. Include removed in the urls
submission batch and report its count in the summary, while preserving the
existing no-change handling and state update flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/indexnow.yml:
- Around line 59-71: Update the fallback branch around changed_files in the
workflow to emit an unknown/force_full signal when prev_content_sha is
unavailable, rather than only leaving changed empty. Propagate that signal into
the existing INDEXNOW_FULL calculation by OR-ing it with the current full-run
conditions, matching the sources_changed fallback behavior and ensuring
changedSlugSets() processes all URLs.

---

Outside diff comments:
In `@scripts/ping-indexnow.ts`:
- Around line 119-153: Update submit() to catch fetch and timeout/network errors
for each batch, count them in failures, and continue processing remaining
batches. Report the affected batch and error details, while preserving the
existing HTTP failure handling and final nonzero exit when any batch fails.

---

Nitpick comments:
In `@scripts/ping-indexnow.ts`:
- Around line 184-203: Update the delta computation around added and updated to
also derive removed URLs from previous.urls entries absent from current. Include
removed in the urls submission batch and report its count in the summary, while
preserving the existing no-change handling and state update flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 707cca41-f0dd-4057-9251-6c32fdba2a4e

📥 Commits

Reviewing files that changed from the base of the PR and between 4b1b482 and d992db2.

📒 Files selected for processing (3)
  • .github/workflows/indexnow.yml
  • .gitignore
  • scripts/ping-indexnow.ts

Comment thread .github/workflows/indexnow.yml
…le state

Both from CodeRabbit review on #2590.

A rejected fetch — DNS failure, connection reset, or the 30s abort firing —
propagated out of submit() and aborted the run mid-loop, skipping the
remaining batches with a stack trace instead of a counted failure. Catch it,
count it like an HTTP error, and keep going; the non-zero exit still leaves
the state file untouched so the next run retries.

When the content commit from the last submission could not be resolved, the
diff came back empty and edits to existing pages were silently dropped (new
pages were still caught by the sitemap diff). Unshallow the content submodule
so the commit is almost always reachable, and fall back to a full submission
when it still is not, rather than skipping.
@innolope-dev

Copy link
Copy Markdown
Collaborator Author

Both CodeRabbit findings addressed in c7d84d6.

submit() didn't catch fetch rejections — real. A transport error or the 30s abort firing escaped the loop, skipping remaining batches with a stack trace. Now caught and counted like an HTTP failure. Kept the non-zero exit deliberately: it leaves the state file unwritten, so the next run retries those URLs rather than recording them as submitted.

Unresolvable previous content commit — real, though narrower than described: the sitemap diff still catches new pages, so what was lost was re-pings for edited existing pages in that window. Fixed at the root as well as the symptom:

  • actions/checkout clones submodules at the superproject's depth, so the commit we last submitted for could fall outside it. The resolve step now unshallows src/content first, which makes the diff resolvable in practice.
  • If it still can't resolve, force_full=true now ORs into INDEXNOW_FULL and we resubmit everything instead of skipping — as suggested.

Verified against real git history: unresolvable contentShaforce_full=true run=true; content moved 5 commits → force_full=false run=true with 33 changed files; nothing changed → run=false. Transport failure → Batch 1: request failed, exit 1, no state written. Delta behaviour unchanged (bootstrap 1121, no-op 0, new page + two edits 15, forced full 1121). typecheck and prettier --check clean.

The eslint failure is the pre-existing red baseline on main — no hits for ping-indexnow anywhere in that log.

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