---
name: spam-update-audit
description: Audit a website's source against the Google spam update — deceptive freshness, hidden text, stale hardcoded claims, templated near-duplicates, dead pages — then fix what fails. Use when a site loses Google or AI Overviews traffic, or before a large content push.
---

# Spam-update audit and fix

Run the checks in order, report findings as a table, then work the fix queue.
Nothing is deleted or rewritten without asking first.

Three policies cause most damage. Ranked by how often the cause is automation
rather than a person:

| Policy | Trigger |
|---|---|
| Deceptive freshness / hidden text | A date bumped with no real change; text served only to crawlers |
| Scaled content abuse | Many pages on one template that add little of their own |
| Site reputation abuse | Unrelated topics hosted on a trusted domain for ranking reasons |

Not spam signals, and commonly confused for them: losing AI Overview citations
(a separate retrieval system), a year in the slug, or a page simply being old.

## Step 0 — orient

Find where pages live and how many there are. Adjust the paths in every command
below to match.

    # framework guesses, in order of likelihood
    ls src/app src/pages content posts _posts 2>/dev/null
    find . -name "page.jsx" -o -name "page.tsx" -o -name "*.mdx" | grep -v node_modules | wc -l

Record the page count. Percentages matter more than raw numbers from here on.

## Check 1 — deceptive freshness

The single most common automated offence. Look for dates clustered on a handful
of recent days.

    grep -rhoE "dateModified: '[0-9-]+'" --include=page.jsx src \
      | sort | uniq -c | sort -rn | head -20

    # frontmatter variants
    grep -rhoE "^(date|updated|lastmod|modifiedTime):.*" content posts \
      | sort | uniq -c | sort -rn | head -20

**Red flag:** more than 20% of pages sharing a date inside any 7-day window, or
any single date covering more than 10% of the site.

Confirm the cause before concluding anything. Find the commit that set them:

    git log --oneline -S"dateModified: '<the-clustered-date>'" -- src | head -5
    git show --stat <commit> | tail -3
    git show <commit> -- src | grep -E "^[+-]" | grep -v "^[+-][+-]" | head -40

**Verdict rule:** if the diff for a page contains only a date string, a
timestamp field, or a sentence like "Updated <Month> <Year>", that page was not
updated. Count how many of the commit's files match that shape. Also check for a
scheduler that does this on a cadence:

    ls .github/workflows/ && grep -rl "schedule:" .github/workflows/
    grep -rn "dateModified\|modifiedTime\|lastmod" .github/workflows/ scripts/ | head

## Check 2 — hidden text

Text present for crawlers and invisible to readers.

    grep -rl 'aria-hidden="false"' src | wc -l
    grep -rn 'sr-only\|visually-hidden\|screen-reader-text' src | head -20
    grep -rn 'display:\s*none\|visibility:\s*hidden\|font-size:\s*0\|text-indent:\s*-9999' src | head -20

Then test whether the block is real accessibility markup or a keyword payload.
Pull one instance and read it. Ask three questions:

1. Does it repeat near-verbatim across many pages? Count it:
   `grep -rl '<a distinctive sentence from the block>' src | wc -l`
2. Does it contain marketing claims, statistics, or brand names rather than
   navigational help ("skip to content", a chart's text alternative)?
3. Would you be comfortable showing it to a reader as written?

**Red flag:** yes, yes, no. Legitimate `sr-only` use is short, per-page, and
describes an interface element. A repeated paragraph of company facts is not.

## Check 3 — stale hardcoded claims

    grep -rhoE '\$[0-9,]+(\.[0-9]{2})?(/mo|/month|/yr| per month)?' src \
      | sort | uniq -c | sort -rn | head -20
    grep -rhoE '[0-9,]+\+? (customers|users|clients|marketers|reviews)' src \
      | sort | uniq -c | sort -rn | head -20

**Red flag:** any figure repeated on more than ~50 pages, because one change
makes it wrong everywhere at once. Compounds badly with check 1: a page that
claims a fresh timestamp while quoting a stale price is asserting a reliability
it does not have.

For each repeated figure, verify the current value against the source before
touching anything. Report figures you could not verify separately from ones you
confirmed wrong — do not guess.

## Check 4 — templated families

Cluster the slugs first:

    ls src/app/\(site\)/blog | sed -E 's/^(.*)-(alternatives|pricing|review|vs-.*)$/\2/' \
      | sort | uniq -c | sort -rn | head

Structure repeating is normal. Duplicated prose is not — so measure the prose,
never judge by the slug pattern:

    python3 - <<'PY'
    import re, os, itertools, sys
    base = sys.argv[1] if len(sys.argv) > 1 else "."
    def sents(d):
        txt = ""
        for f in os.listdir(d):
            if f.endswith((".jsx", ".js", ".mdx", ".md")):
                txt += open(os.path.join(d, f), errors="ignore").read()
        return set(s.strip() for s in re.findall(r'["\'>]([^"\'<]{40,})["\'<]', txt))
    dirs = [os.path.join(base, d) for d in sorted(os.listdir(base))
            if os.path.isdir(os.path.join(base, d))]
    for a, b in itertools.combinations(dirs[:40], 2):
        A, B = sents(a), sents(b)
        if not A or not B: continue
        o = len(A & B) / min(len(A), len(B))
        if o > 0.30:
            print(f"{o:.0%}  {os.path.basename(a)} vs {os.path.basename(b)}")
    PY

**Verdict rule:** under 10% overlap means the pages are genuinely distinct —
leave them alone. 30%+ across many pairs is real duplication; consolidate into
one strong page and 301 the rest to it. Between the two, read a pair yourself
before deciding.

Do not mass-delete a templated family on pattern alone. This is the most common
and most expensive mistake in a post-update panic.

## Check 5 — dead weight

Pages older than 30 days with effectively no traffic and no AI-assistant
sessions. This needs analytics, not grep — pull Search Console clicks and
impressions per URL over 90 days, plus referral sessions from ChatGPT,
Perplexity, Claude, Gemini and Copilot.

**Red flag:** 30+ days old, ≤3 clicks in 90 days, zero AI sessions. Protect any
page that earns AI citations even when clicks are near zero — those are two
different kinds of value.

## Report first

Before changing anything, output one table:

| Check | Pages affected | % of site | Red flag? | Cause |
|---|---|---|---|---|

Then state plainly which findings are risk signals versus proven damage. A grep
result is a risk signal. Proven damage needs a step change in Search Console on
a specific date, with positions holding while clicks fall. Positions flat and
impressions falling is a demand drop, not a penalty — say so rather than letting
someone rewrite a site that was never hit.

## Fix queue, in order

Work top to bottom. The first two stop new signal from being created; the rest
clean up what exists.

1. **Turn off any scheduled job that bumps dates.** Disable the workflow. Nothing
   else matters while it is still running nightly.
2. **Gate the date bump on a real diff.** If the only change is a date string,
   skip the page and log it. Keep the staleness ceiling — pages that genuinely
   go stale get rewritten, not restamped.
3. **Reset the false timestamps.** For each page whose only recent change was the
   date, restore `dateModified` from the last commit that touched visible
   content: `git log --format=%cI --name-only -- <path>`, skipping the refresh
   commits. Regenerate the sitemap afterwards so `lastmod` agrees.
4. **Surface or delete the hidden block.** Either promote it to a visible summary
   at the top of the page, or remove it and let JSON-LD carry the entity facts.
   Drop `aria-hidden="false"` either way.
5. **De-duplicate boilerplate.** Keep the claim where it is evidence — pricing,
   about, case studies. Remove it from articles where it is filler.
6. **Correct the hardcoded figures.** This is a genuine content change, so these
   pages earn a new `dateModified` honestly.
7. **Consolidate confirmed duplicates.** Lift anything unique out of the loser
   into the keeper first, then 301. Redirect to a topically matching target —
   a mismatched redirect is treated as a soft 404 and the equity is discarded.
8. **Prune dead weight.** Only after 1–3, so freshness noise is not polluting the
   traffic data.
9. **Write the rule down** in the repo's contributor guide: a date bump requires
   a content change, and hidden text is not an AI-visibility tactic.

## Rules for the agent

- Ask before deleting, redirecting, or bulk-rewriting. Show the list first.
- Never fix a freshness problem by editing dates in bulk — that is the offence.
- Never add hidden text to satisfy a check.
- Change one category at a time and commit separately, so a regression is
  traceable to a single pass.
- Report what you could not verify. An unverified figure stays unverified in the
  report; it does not become a confident claim.
