Next.js technical SEO checklist: 22 checks you can actually run
Crawlability, content structure, structured data, performance and internationalisation — 22 checks with the exact curl commands, pass criteria and fix direction. All from real projects.
This checklist comes from real work: a 126-page site rebuild, then a bilingual restructure. Every item earned its place by breaking something.
Work through it in order. Don't skip the first eight — if the basics are wrong, everything after them is decoration.
1. Crawlability (8 checks)
1. Does the initial HTML contain body text?
The most common failure.
curl -s https://your-site.com/page | python3 -c "
import sys, re, html
raw = sys.stdin.read()
m = re.search(r'<main[^>]*>(.*?)</main>', raw, flags=re.S|re.I)
seg = m.group(1) if m else raw
t = re.sub(r'<(script|style)[^>]*>.*?</\1>', ' ', seg, flags=re.S|re.I)
t = re.sub(r'<[^>]+>', ' ', t)
print('body length:', len(re.sub(r'\s+', ' ', html.unescape(t)).strip()))
"
Pass: over 500 characters. I've seen 46 — the entire content was Loading….
Cause: client-side rendering. Crawlers don't execute JS on the first pass.
2. Is robots.txt blocking anything by accident?
curl -s https://your-site.com/robots.txt
Pass: no Disallow: /, sitemap URL correct.
Trap: a middleware allowlist that misses .txt — which silently tells AI crawlers the site is gone.
3. Is the sitemap complete and reachable?
curl -s -o /dev/null -w "%{http_code} %{size_download}\n" https://your-site.com/sitemap.xml
curl -s https://your-site.com/sitemap.xml | grep -c "<loc>"
Pass: 200, count matches actual pages.
Trap: lastmod set to new Date(). Every deploy marks all URLs as updated today; Google eventually distrusts the field and ignores it.
4. Are status codes correct?
for u in / /about /nonexistent-page; do
printf "%-20s " "$u"
curl -s -o /dev/null -w "%{http_code}\n" "https://your-site.com$u"
done
Pass: 200 for real pages, 404 for missing ones.
Trap: returning 410 for every unknown path. 410 means "permanently deleted" — a typo shouldn't tell search engines that, and you lose error monitoring signal.
5. Is canonical self-referencing?
curl -s https://your-site.com/page | grep -o '<link rel="canonical"[^>]*>'
Pass: points at itself, at the final URL (not a redirect source).
Trap: canonical says /services/en while that URL 308s to /en.
6. Any accidental noindex?
curl -s https://your-site.com/page | grep -o '<meta name="robots"[^>]*>'
curl -sI https://your-site.com/page | grep -i x-robots-tag
Pass: index, follow on content pages.
7. Single-hop redirects only
curl -sIL https://your-site.com/old-page | grep -E "^HTTP|^location"
Pass: no A → B → C chains.
8. Are static assets blocked?
curl -s -o /dev/null -w "%{http_code}\n" https://your-site.com/_next/static/chunks/main.js
Pass: 200. An over-broad middleware matcher that catches /_next/static produces a blank page.
2. Content structure (4 checks)
9. Exactly one H1
curl -s https://your-site.com/page | grep -c "<h1"
Pass: 1.
In a sample of 13 US small-business sites I audited, only 7 had exactly one — three had none at all.
10. Are H2s used for sections?
Pass: long pages broken into H2 sections.
Trap: <div class="text-2xl font-bold"> pretending to be a heading. Looks right, reads as nothing.
11. Unique titles
curl -s https://your-site.com/sitemap.xml | grep -o '<loc>[^<]*' | sed 's|<loc>||' | while read u; do
echo "$(curl -s "$u" | grep -o '<title>[^<]*' | head -1) | $u"
done
Pass: no duplicates.
12. Meta description length
Pass: 60–155 characters. Enforce it in code with description.slice(0, 155).
3. Structured data (4 checks)
13. Is JSON-LD present?
curl -s https://your-site.com/page | grep -o '"@type":"[A-Za-z]*"' | sort | uniq -c
| Page type | Expected types |
|---|---|
| Homepage | Organization, WebSite |
| Service page | Service, FAQPage |
| Article | TechArticle, BreadcrumbList |
| Product | Product, Offer |
14. Are FAQs marked up?
A visible FAQ block without FAQPage schema is invisible to extraction. Add mainEntity with Question/Answer pairs.
15. Does Service reference provider by @id?
{ "@type": "Service", "provider": { "@id": "https://your-site.com/#organization" } }
Reference, don't redeclare — one entity, many mentions.
16. Are prices in the schema?
"offers": [{ "@type": "Offer", "name": "Starter", "price": "1200", "priceCurrency": "USD" }]
Pages with structured pricing get cited more readily when AI answers "how much does this cost".
4. Performance (4 checks)
17. Core Web Vitals
npx lighthouse https://your-site.com --output=json --output-path=lh.json --chrome-flags="--headless=new"
| Metric | Target |
|---|---|
| LCP | < 2.5s |
| CLS | < 0.1 |
| TBT | < 200ms |
18. Modern image formats
UI screenshots should be WebP. Measured: four PNGs at 955 KB became 105 KB as WebP — 11%, and 6% on flat-colour screenshots.
19. Blocking scripts
A <head> script that rewrote a lang attribute pushed TBT from 60ms to 240ms. Removing it (static markup instead) brought it back to 60ms.
20. Fonts
An 8–10 MB CJK webfont destroys LCP. Use a system font stack or load a subset.
5. Internationalisation (2 checks)
21. Does html lang match the page language?
for u in / /en; do
printf "%-6s " "$u"
curl -s "https://your-site.com$u" | grep -o 'lang="[a-zA-Z-]*"' | head -1
done
Pass: / is zh-CN, /en is en.
This is the hardest one. In App Router, <html lang> can only be set once by the root layout. Per-route switching requires route groups:
app/(zh)/layout.tsx <html lang="zh-CN">
app/(en)/layout.tsx <html lang="en">
Patching it client-side doesn't work — crawlers see the wrong value.
22. Is hreflang bidirectional with x-default?
curl -s https://your-site.com/en | grep -o 'hrefLang="[^"]*"'
Pass: every language version lists all languages including itself, plus x-default.
Trap: page-level metadata replaces the layout's entire alternates object rather than merging. That's how /en lost its hreflang after a restructure.
Five-minute version
If you only have five minutes:
- Body length on the homepage (check 1)
/robots.txt(check 2)sitemap.xmlcount (check 3)html lang(check 21)
If any of those fail, the rest doesn't matter.
Automate the basics
Checks 1, 2, 3, 9, 13 and 18 can be automated: free site audit tool — domain in, results in 30 seconds, no signup.
If it surfaces more than you want to fix yourself, the technical SEO service covers the full process and pricing.
Does your site have these problems?
Run the free audit tool for a 30-second report, or just write to me and describe the situation — I will tell you whether it is worth fixing.