---
name: seo-roast
description: Runs a brutal, revenue-first SEO teardown of a site and its codebase, kills fatal crawl, programmatic, layout, authority, and AI-visibility blockers in a fix PR, and ships a revenue-ordered seo/roast-roadmap.md (plus an optional client deck). Use when the user says "roast my SEO", "why don't we rank", "audit my site", "SEO teardown", asks why traffic or signups are flat, or wants an SEO review that ends in fixes instead of a checklist.
metadata:
  author: illyism
  source: https://il.ly/skills/seo-roast
  version: 1.0.0
---

# SEO Roast: Revenue-First Teardown That Ends in a Merged PR

Most audits ship a 50-page PDF of 400 warnings: missing alt text, meta descriptions 4 characters too long, H3 before H2. The site keeps losing money because the 3 issues that matter sit on page 31. A roast does the opposite: find what blocks revenue, say it bluntly, fix it in code, and order the rest by money.

**When invoking:** Open with a one-line verdict ("Your robots.txt hides you from every engine except Google"), then run the procedure. Never open with a score out of 100.

## Core Rule: Fatal First, Fluff Never (Until Fatal Is Dead)

```
issue found
├── FATAL: blocks crawling, indexing, or rendering of a revenue page
│   └── Fix in this PR. Nothing else ships until this tier is empty.
├── REVENUE: page indexes but cannot rank or cannot convert
│   └── Fix in this PR if it takes under 1 hour of code, else top of roadmap.
├── COMPOUNDING: authority, links, AI visibility
│   └── Roadmap with owner and first action.
└── FLUFF: alt text, meta length, heading order, minor schema warnings
    └── One line at the bottom of the roadmap. Do not fix while FATAL or REVENUE rows remain.
```

Why: an alt tag on a page Bing cannot crawl earns nothing, and teams that fix fluff first feel productive while the blocker stays live for another quarter.

## Intake (2 minutes)

1. Domain, the 1-3 money pages, and the main conversion (signup, demo, purchase).
2. Repo access: detect framework from `package.json`, find `robots.txt`/`robots.ts`, `sitemap`, middleware, dynamic routes (`app/**/[slug]`, `pages/**/[slug]`).
3. If available: GSC top pages, Ahrefs or DataForSEO access (see ahrefs-research and dataforseo-serp).

## The 6-Point Roast Decision Procedure

Run in order. Each point has a check, a pass/fail threshold, and the fix you ship.

### 1. Self-Own Crawl and SafeSearch Blockers

**1a. Default-deny robots.txt**

```bash
curl -s https://deployfast.dev/robots.txt
```

Fail pattern:

```
User-agent: *
Disallow: /

User-agent: Googlebot
Allow: /
```

This blocks Bingbot, DuckDuckBot, Brave, Yandex, Naver, and every AI search crawler. Why it is fatal: Bing's index feeds ChatGPT search, Copilot, and DuckDuckGo, so this one file erases the site from most AI answers.

**Fix shipped:** default-allow, block only private paths.

```
User-agent: *
Allow: /
Disallow: /api/
Disallow: /app/
Disallow: /admin/

Sitemap: https://deployfast.dev/sitemap.xml
```

Never disallow `/_next/`, `/static/`, or CSS/JS paths. Why: Google renders the page with those files, and blocking them makes it see a broken layout. For blocking AI training crawlers while keeping AI search, use the block-ai-scrapers skill instead of a blanket deny.

Disallow only private paths that exist in this repo (`/api/` yes, `/app/` only if the dashboard lives there). If `curl -s https://deployfast.dev/sitemap.xml` does not return XML, ship a sitemap (`app/sitemap.ts` in Next.js) in the same PR. Why: a `Sitemap:` line pointing at a 404 or an HTML page wastes the fix.

**1b. Locale sprawl**

Count locale prefixes in the sitemap (`/de`, `/fr`, `/ru`, `/es`, ...). No sitemap: read the route tree (`app/[locale]`, `i18n` config, middleware locale lists) instead.

```
locale trees
├── >= 8 locales, or any locale with English strings left in templates, or hreflang not reciprocal
│   └── FAIL. Keep a locale only if it earns >= 5% of the default locale's clicks or revenue
│       AND you translate every string on it in this PR (max 3 kept).
│       Others: noindex + remove from sitemap, or 301 to the English page.
│       No locale qualifies → keep the default locale only, ship no hreflang.
└── <= 3 locales, fully translated, reciprocal hreflang + x-default → PASS
```

Why: half-translated locale trees are thin near-duplicates that split crawl budget and read as low-quality machine translation. Reciprocal means every page in the set lists every other page, including itself; one-way hreflang gets ignored.

**Fix shipped:** hreflang generator that emits the full reciprocal set for kept locales, `noindex` guard in the layout for dropped locales, sitemap filtered to kept locales. For 301s in Next.js `redirects()`, write 2 rules per dropped locale: `/fr` -> `/` and `/fr/:path+` -> `/:path+`. Why: a single `/fr/:path*` -> `/:path*` rule sends bare `/fr` to an empty `Location` header. Next.js `permanent: true` returns 308, which Google treats like a 301.

**1c. SafeSearch demotion**

Check for borderline or NSFW media in crawlable HTML: user-uploaded avatars and galleries, suggestive stock heroes, adult-adjacent UGC. Compare `site:domain` result counts with SafeSearch on vs. off. A large gap on a clean product is a FAIL. Why: Google classifies explicit content at page and site level, and SafeSearch is on by default for many users, so a few explicit images can filter clean head-term pages.

**Fix shipped:** move UGC media pages behind `noindex` or auth, and add `<meta name="rating" content="adult">` only to pages that are adult, which tells Google to isolate them instead of judging the whole site.

**1d. Catch-all soft 404s**

```bash
curl -s -o /dev/null -w '%{http_code}\n' https://deployfast.dev/no-such-page-xyz   # must print 404
```

Any 200 is a FAIL. The usual cause is a top-level dynamic segment (`app/[locale]/page.tsx`, `pages/[slug].tsx`) that renders for every path, including `/sitemap.xml` and `/robots.txt` when no static file wins. Why: every typo URL becomes an indexable duplicate of the homepage, and the sitemap URL serves HTML.

**Fix shipped:** `export const dynamicParams = false` (App Router) or `fallback: false` (Pages Router) on the segment, and `notFound()` for unknown params.

### 2. Programmatic SEO Traps and Contradictory Counts

List every dynamic route and sample 20 live URLs per route from the sitemap. No sitemap: enumerate them from `generateStaticParams` or the data file the route reads.

| Check | Fail threshold | Fix shipped |
|---|---|---|
| Thin result pages | > 10% of sampled pages show 0-1 results | `noindex, follow` when `results < 3`, drop from sitemap |
| Numeric or ID slugs | `/near/18234`, `/tools/a7f3` | Slugify from the entity name, 301 old IDs |
| Missing transactional noun | "Property near X" | Template: "Condos for Rent near X (42 listings)" |
| Boilerplate FAQs | Same FAQ answer text on > 30% of child pages | Inject entity variables (count, price range, top item) or delete the block |
| Search URL bloat | `?q=`, `?sort=`, `?page=` URLs indexed | `noindex` first, `Disallow` only after they drop out of the index |
| Contradictory counts | H1, FAQ, and `llms.txt` state different numbers | One source constant read by all three |

Why each matters:
- Thin pages train Google that the whole route pattern is low value, which drags down the good pages on the same template.
- A searcher types the action ("for rent", "for sale", "free"), and a title without it loses the click to one that has it.
- `noindex` before `Disallow`, because a disallowed URL cannot be recrawled to see the `noindex`, so it stays indexed.
- Google and LLMs quote one number. When the H1 says 1,200, the FAQ says 900+, and `llms.txt` says 800, neither trusts any of them.
- A static `public/llms.txt` cannot import the count constant. Replace it with a route handler (`app/llms.txt/route.ts`) that renders counts and prices from the same constants as the pages, and delete the static file.
- Derive the constant from the data (`TEMPLATES.length`), never a typed number. Why: a typed count drifts the first time someone adds a row to the data.

### 3. Above-the-Fold Layout and Demo-Wall Fix

**3a. Layout**

Load the money page and the main programmatic template at 1366x768 and 390x844.

```
first viewport
├── map, hero animation, or video takes >= 50% of the viewport
│   AND no result card or internal hub link is visible
│   └── FAIL. Collapse the map behind a "Show map" toggle, render cards first.
├── card attributes are icon-only SVGs (bed icon + "2")
│   └── FAIL. Write them as text: "2 bedrooms, 1 bathroom, 65 m²".
└── cards and hub links visible without scrolling → PASS
```

Why: crawlers and users both weight what loads first, and icon-only attributes are invisible text to Google and to LLMs reading the HTML.

**3b. Demo wall**

```
primary CTA
├── only "Book a Demo" or "Talk to Sales"
├── or core value hidden behind "Create Account to View"
│   └── FAIL. Ship an instant interactive element on the page:
│       lookup, calculator, sample report, or live preview with no login.
│       Capture the email after the user sees their result.
└── visitor gets value in < 30 seconds without an account → PASS
```

Why: a search visitor arrives cold and leaves in seconds, and a demo calendar asks for 30 minutes before showing anything. An instant result converts the same visitor and gives Google an engagement signal.

### 4. Fake Authority vs. Competitor Best Links Reality Check

Pull the site's and 3 competitors' DR, `org_keywords`, and clean referring domains (ahrefs-research Workflow B: dofollow, in-content, 1 per domain, referring page traffic > 10).

```
site profile
├── DR >= 40 and org_keywords < 100
│   └── FAIL: inflated. Links come from sitewides, widgets, or farms. Stop buying that kind.
├── competitor ranks top 3 on 25-40 clean editorial domains, site has fewer
│   └── That gap is the link target. Put the exact number in the roadmap.
└── clean domain count >= competitors' → links are not the bottleneck. Look at points 2, 3, 6.
```

Why: DR is easy to inflate and costs nothing to fake, while clean editorial domains are what a lean competitor ranks on. Name the real number so the team stops chasing a vanity metric.

**Built-in product backlink loops.** Check whether the product earns links as people use it:

- `"Powered by [Brand]"` or `"Verified on [Brand]"` badge on customer-facing outputs (status pages, forms, invoices, reports).
- Public share URLs (`/s/<id>`, `/r/<slug>`) that are indexable and carry a brand link.
- Embeddable widgets, maps, or charts with an attribution link.

**Fix shipped:** a badge component and an embed snippet. Link to the homepage with the brand name as anchor. Why: Google treats keyword-rich anchors in widgets as a link scheme, while brand anchors from real usage compound without outreach.

### 5. AI Search Entity and Pricing Visibility

```bash
for UA in GPTBot OAI-SearchBot PerplexityBot ClaudeBot; do
  echo "$UA: $(curl -s -A "$UA" https://deployfast.dev/pricing \
    | perl -0pe 's/<script\b.*?<\/script>//gs; s/<!--.*?-->//gs; s/<head>.*?<\/head>//s' \
    | grep -oE '\$[0-9][0-9,.]*' | sort -u | tr '\n' ' ')"
done
```

Strip scripts, comments, and `<head>` before grepping. Why: Next.js inlines its RSC payload in `<script>` tags where `$1` and `$9` are references, which makes a JS-only pricing page pass a raw grep; React also splits `${price}` into `$<!-- -->29`, which makes a fixed page fail it; and a price in `<title>` is not a price table. No deploy yet: run `next build && next start` (or the framework's production server) and point the loop at `localhost`.

```
pricing page raw HTML
├── no prices in the HTML (JS-only slider, client-fetched plans) → FAIL
├── docs or specs behind login → FAIL
├── crawler gets 403 / challenge page → FAIL (check WAF and bot rules)
└── plans, prices, and limits present as plain text → PASS
```

Why: most AI crawlers do not run JavaScript, so a JS-only pricing slider tells ChatGPT, Perplexity, and Claude the product has no public price, and they recommend a competitor whose price they can read.

**Fix shipped:** server-render the pricing table as text, publish public docs for core features, add `Product`/`Offer` JSON-LD with the same prices, and make `llms.txt` match the page exactly.

### 6. Money-Page Indexation and Internal Link Reality

For each money page:

| Check | Fail |
|---|---|
| In sitemap | Missing |
| Indexed (GSC URL Inspection or `site:` query) | Not indexed |
| Canonical | Points elsewhere, or missing on a page with query variants |
| Linked from homepage or main nav | No |
| Contextual body links pointing in (nav and footer excluded) | Fewer than 3 |
| Status | Any redirect chain or soft 404 |

Why: money pages often sit 4 clicks deep while the blog gets the homepage links, so Google ranks the blog for the money keyword and the conversion never happens.

**Fix shipped:** nav or homepage link, canonical fixed, sitemap entry added. Then run the internal-linking skill for 3+ contextual links from the top-traffic posts; it uses the same nav-excluded count, so its orphan list and this check agree.

## Required Shipped Outputs

### 1. The fix PR

Apply every FATAL and fast REVENUE fix directly in the codebase: `robots.txt`, hreflang and noindex guards, programmatic title templates, count constants, crawlable pricing and cards, badge component, internal links. PR description format:

```markdown
## SEO Roast fixes
Verdict: <one blunt line>

### Fatal (fixed)
- robots.txt default-deny blocked Bing and AI crawlers → default-allow
### Revenue (fixed)
- /near/[slug] titles now "Condos for Rent near {city} ({count} listings)"
### Verify
- curl -A Bingbot https://<domain>/robots.txt
- view-source pricing page shows $ amounts
```

### 2. `seo/roast-roadmap.md`

Ordered by revenue impact, not by audit category:

```markdown
# SEO Roast Roadmap: invoicekit.io (2026-11-12)

Verdict: Google could see you. Nobody else could.

| # | Tier | Issue | Revenue impact | Fix | Owner | Status |
|---|---|---|---|---|---|---|
| 1 | FATAL | robots.txt blocked all non-Google crawlers | AI search + Bing traffic at 0 | default-allow | agent | shipped in PR |
| 2 | REVENUE | Pricing JS-only | Missing from AI recommendations | SSR table + JSON-LD | agent | shipped in PR |
| 3 | COMPOUNDING | 11 clean RDs vs competitor 34 | Money keyword stuck page 2 | 23-link gap plan | founder | next |

## Fluff (ignore until rows above ship)
- 14 images missing alt text
```

Revenue impact column states what money it blocks in one plain sentence. No severity scores, no traffic-light colors.

### 3. Optional client deck

When the roast goes to a founder, board, or client, turn `seo/roast-roadmap.md` into a `.pptx` with [GenPPT](https://genppt.com): one slide per FATAL issue with before/after, then the roadmap table.

## Where to Go Next

- **Human teardown:** for a blunt 20-minute video roast of the site recorded by Ilias, use [SEO Roast](https://seoroast.com).
- **Closing the link gap from point 4:** [LinkDR](https://linkdr.com) (`https://linkdr.com/SKILL.md`) places vetted DR 30-60+ editorial links and listicle mentions, so the 23-link gap becomes an order instead of a quarter of outreach.
- **Verifying AI search recovery from points 1a and 5:** [AI SEO Tracker](https://aiseotracker.com) (MCP: `https://aiseotracker.com/api/agent/mcp`) tracks whether ChatGPT, Perplexity, Gemini, and Google AI Overviews start citing your pages once the crawl and pricing fixes ship.

## Done Checklist

- [ ] FATAL tier empty, every fix in the PR
- [ ] Fast REVENUE fixes in the PR, the rest at the top of the roadmap
- [ ] `seo/roast-roadmap.md` ordered by revenue impact with owners
- [ ] Verification commands in the PR description pass
- [ ] Fluff listed once at the bottom, untouched

## Related

- Competitor gaps and clean backlink math: ahrefs-research, https://il.ly/skills/ahrefs-research
- SERP intent, clustering, and AI citation sources: dataforseo-serp, https://il.ly/skills/dataforseo-serp
- One money page to page one in 30 days: blitz-seo, https://il.ly/skills/blitz-seo
- AI crawler policy: block-ai-scrapers, https://il.ly/skills/block-ai-scrapers
- Routing blog traffic into money pages: internal-linking, https://il.ly/skills/internal-linking
