# SEO Run Book — 410 SEO actions
Generated 05 September 2026 from https://seorunbook.com
Tick them off as you go. Every action is also readable, free and without an account,
at https://seorunbook.com/playbook/
---
## Before you start — please read
1. This site is not affiliated with, endorsed by or connected to Google LLC or any tool vendor named in the actions. Product names are used descriptively to identify the products an action refers to.
2. No ranking, traffic figure or commercial outcome is promised. Nobody outside Google controls the ranking algorithm.
3. Several actions change how a site is served — robots.txt, redirects, canonical tags, headers, indexation controls. Applied to the wrong page they can remove a site from search results. Back up first, apply one change at a time, and run the verification step given with each action.
4. This is general information, not advice tailored to your site, and no client relationship is created by reading it. The content is provided as it is, without warranty, and to the fullest extent the law allows the publisher accepts no liability for loss arising from its use.
Full legal notice: https://seorunbook.com/legal/
---
## A · Crawl and indexing
_20 actions, #1–20_
Nothing else on this list matters if Google cannot fetch and index the page. These actions come first because they are the gate: a page that is blocked, redirected in a loop, or buried behind a parameter never gets to compete, no matter how good it is.
- [ ] **1.** Generate an XML sitemap, split at 50,000 URLs / 50MB per file, index them in a sitemap_index.xml
- How: Decide your sitemap source. WordPress: use Yoast or Rank Math . Next.js: next-sitemap . Static (Hugo/Astro/Jekyll): build-step generator. Custom: write a script that pulls from your CMS database. · Generate sitemap files. Split at 50,000 URLs or 50MB uncompressed, whichever comes first. Most sites never hit this — 10K-100K URL sites use one file. · Generate a sitemap_index.xml that lists all your sitemap files. Example: https://yoursite.com/sitemap-1.xml2026-08-16 · Place sitemap files at your site root: /sitemap.xml and /sitemap_index.xml . Both must be served with Content-Type: application/xml . · Reference in robots.txt: Sitemap: https://yoursite.com/sitemap_index.xml · Submit the sitemap_index URL in GSC → Sitemaps. Check back in 24-48 hours for the "Discovered URLs" count.
- Avoid: Including /admin/ , /cart/ , search results, paginated pages, or ?utm_source= variants · Generating one massive 200K-URL file and not splitting it (Google will only process the first 50K) · Returning the sitemap with Content-Type: text/html instead of application/xml · Forgetting to update the sitemap on publish (most CMS plugins handle this; custom builds often don't)
- Verify: Open https://yoursite.com/sitemap.xml in a browser — it should display as raw XML, not HTML · Validate with xml-sitemaps.com validator or xmllint --noout sitemap.xml · Check GSC → Sitemaps 24-48 hours after submission: "Discovered URLs" should be > 0 and growing
- [ ] **2.** Set in the sitemap to the real last-content-change date (not build time); Google uses it as a freshness/crawl signal
- How: Identify the field in your CMS that holds the real last-edit timestamp. WordPress: post_modified (not post_date ). Sanity: _updatedAt . Contentful: sys.updatedAt . Static sites with frontmatter: updated: field. · Wire your sitemap generator to that field. Example in Node.js: const pages = await db.query('SELECT slug, updated_at FROM posts WHERE status = ?', ['published']); for (const p of pages) { sitemap.push({ loc: `https://yoursite.com/${p.slug}`, lastmod: p.updated_at.toISOString().split('T')[0] // YYYY-MM-DD }); } · Update only on meaningful content changes. Don't update it for comment additions, sidebar widget changes, or trivial copy edits. Google devalues that's updated on every build. · For pages with no meaningful edits in years, set to the publish date and don't change it. Stable is more useful than a constant "today's date" pattern.
- Avoid: Using build time: every deploy updates all values to the same timestamp, which Google devalues within weeks · Using publish date only: a heavily-edited page with a 2020 publish date tells Google it's stale · Using future dates: this doesn't help and may trigger quality flags · Forgetting to update on real edits: the signal becomes meaningless
- Verify: Pick 5 URLs across different content ages. Check their in your sitemap. · Edit one of them. Re-generate the sitemap. Check the updated to the new edit date. · Don't expect every to be the same date — if they are, you're using build time, not content time.
- [ ] **3.** List only 200-status, canonical, indexable URLs in the sitemap — no redirects, 404s, noindex, or param URLs
- How: Before generating the sitemap, filter your URL list. Exclude: URLs with · URLs with an HTTP header X-Robots-Tag: noindex · URLs whose canonical points to a different URL · URLs that return 3xx, 4xx, or 5xx · URLs with parameter strings ( ?id= , ?utm_source= ) unless they're indexable canonical versions · Pagination URLs ( /blog?page=2 ) — use a single canonical URL per content piece · · For WordPress, use a plugin like Yoast's "Exclude" settings. For custom builds, filter in your generator: const indexable = pages.filter(p => p.status === 200 && !p.hasNoindex && p.canonical_url === p.url && !p.hasQueryParams ); · After generating, manually review 10 random sitemap URLs. Each should be a real, indexable, canonical page.
- Avoid: Including all URLs from your CMS database, including drafts, private posts, internal search results · Including parameter URLs that all canonical to a single base URL (canonicalizes to one, not the others) · Including 404 pages that still return 200 (soft-404s) · Including paginated views of the same content (each ?page=N should be excluded or canonicalized)
- Verify: Take 20 random URLs from your sitemap. curl -I each. All should return 200. · Open 5 in GSC URL Inspection. None should report "Excluded by noindex" or "Redirected." · If GSC reports "Discovered URLs < Submitted URLs" with a large gap, your sitemap is including non-indexable URLs.
- [ ] **4.** Submit the sitemap index in GSC → Sitemaps and reference it in robots.txt via Sitemap:
- How: Open GSC → Sitemaps (left sidebar). Paste the URL of your sitemap index (e.g., https://yoursite.com/sitemap_index.xml ). Click Submit. · Add a Sitemap: line to your robots.txt, at the bottom of the file: User-agent: * Allow: / Sitemap: https://yoursite.com/sitemap_index.xml · Submit sitemaps per property if you have regional or sub-property GSC accounts. · For Bing, also submit at Bing Webmaster Tools → Sitemaps . · Check back in GSC in 24-48 hours. The "Status" should be "Success" and "Discovered URLs" should grow over the next week.
- Avoid: Submitting only the parent sitemap_index and not the individual sitemap files (GSC will discover the children from the index, but some engines don't) · Forgetting to re-submit after a sitemap URL change (GSC only re-crawls submitted sitemaps periodically) · Submitting every individual sitemap AND the index, double-counting in GSC's reported count
- Verify: Open GSC → Sitemaps. Status should be "Success." Last download time should be recent. · Check robots.txt: curl https://yoursite.com/robots.txt | grep Sitemap should return the line. · After a week, GSC's "Discovered URLs" should be roughly 80-100% of your actual indexable URL count.
- [ ] **5.** Serve separate image and video sitemaps (or / extensions)
- How: Choose your approach. Either: Option A: separate sitemap-images.xml and sitemap-videos.xml files · Option B: add and entries to your main blocks in your existing sitemap · Option B is simpler if your main sitemap is already small. Option A is cleaner for image-heavy or video-heavy sites. · For images, each needs (the image URL) and optionally , , . · For videos, each needs , , , (or ). · Reference in sitemap_index.xml and submit to GSC.
- Avoid: Including images that aren't on the page (don't list images from a global image library if they don't appear on the listed URL) · Missing the thumbnail — Google won't index a video without a valid thumbnail image · Pointing video:content_loc at a private URL that requires auth to play · Adding image sitemaps for stock photos that have no SEO value
- Verify: Submit your image/video sitemap in GSC. Check for "Discovered" count. · Open Google Images, search for a unique alt text from one of your images. Verify the page ranks. · Use Google's Rich Results Test to validate video schema.
- [ ] **6.** In robots.txt: Allow all indexable paths, Disallow faceted/param/search/cart/admin paths
- How: Start from a permissive baseline: User-agent: * Allow: / Disallow: /search Disallow: /admin/ Disallow: /cart Disallow: /account/ Disallow: /*?* Sitemap: https://yoursite.com/sitemap_index.xml · Identify paths that should never be indexed: Search results ( /search?q= ) · Internal admin ( /admin/ , /wp-admin/ ) · User account pages ( /account/ , /login ) · Faceted navigation URLs ( /products?color=red&size=large ) · Internal search result pages · Cart and checkout · · Use Disallow: /path (no trailing slash) to block a path and everything under it. Disallow: /path/ (trailing slash) is identical functionally but signals "directory." · Don't block CSS/JS files. Modern Google needs them to render your page correctly. · Test with Google's robots.txt Tester in GSC.
- Avoid: Blocking /wp-admin/ without checking if your theme uses it for AJAX endpoints (blocks real features) · Disallowing /? to block query params, which can block legitimate parameterized URLs · Blocking CSS or JS files (Google can't render the page properly) · Forgetting to allow the sitemap file itself (some templates accidentally disallow it)
- Verify: Open GSC → robots.txt Tester. Verify the rules parse correctly. · Test a few URLs: a real indexable page should say "Allowed," an admin URL should say "Blocked." · After 2-3 weeks, check GSC → Pages → "Crawled - currently not indexed." A spike suggests robots.txt is over-blocking.
- [ ] **7.** Audit index coverage in GSC → Pages weekly; investigate "Crawled - not indexed" and "Discovered - not indexed"
- How: Open GSC → Pages (or Index → Pages in older GSC). · Filter by "Why pages aren't indexed." Look at the top three reasons by URL count. · For "Crawled - currently not indexed": Export the URL list (top 1000) · Cross-reference with your analytics: are these URLs getting traffic? If yes, they're being held back by quality signals · Check the actual pages. Are they near-duplicates of other pages? Are they thin? · · For "Discovered - currently not indexed": These are URLs Google knows about (from sitemaps or links) but hasn't crawled yet · Normal for new sites; concerning for established sites · Fix by improving internal linking to these pages, ensuring they're in the sitemap, and reducing overall crawl budget waste · · Repeat this audit weekly. Track the counts.
- Avoid: Ignoring the report because the count is "low" (every unindexed URL is lost potential traffic) · Forcing indexing via URL Inspection on every page (GSC has a quota) · Not investigating the why — the report tells you which URLs are affected but you have to diagnose the cause
- Verify: Pick 5 URLs from the "Crawled - not indexed" list. Open them in a private browser. Are they useful? Are they distinct from other pages on your site? · Check GSC URL Inspection for one of them. What does Google say about the rendered version? · Re-check the count weekly. It should be stable or declining.
- [ ] **8.** Remove accidental noindex (meta robots + X-Robots-Tag HTTP header) from pages you want ranked
- How: Audit your most important pages. Use a tool like Screaming Frog in list mode, or write a quick script: const pages = ['/', '/about', '/product-1', ...]; for (const url of pages) { const res = await fetch(url); const meta = await res.text(); const noindex = meta.match(//); const xRobots = res.headers.get('X-Robots-Tag'); if (noindex?.[1].includes('noindex') || xRobots?.includes('noindex')) { console.log('NOINDEX:', url); } } · For each affected page, find the source. Common causes: CMS has a "hide from search engines" checkbox that got toggled · Theme has a staging-mode check that adds noindex to all pages in non-production environments (and the check is wrong in production) · Reverse proxy or CDN adds X-Robots-Tag: noindex for some path patterns · .htaccess or nginx config has a noindex rule on a specific path · · Fix the source. Don't just remove the meta tag — fix the config so it doesn't come back. · Re-crawl with URL Inspection to confirm. Google should re-crawl within days.
- Avoid: Fixing only the meta tag without finding the source config (it'll come back) · Forgetting to check the X-Robots-Tag HTTP header (a common overlooked source) · Not checking staging environments (if you have one, it should have a reverse noindex)
- Verify: After fixing, re-run the audit. All important pages should now return "index, follow." · Use GSC URL Inspection → "Request Indexing" on 5 of the most important pages. · Check 7 days later: the affected URLs should appear in GSC's "Indexed" count.
- [ ] **9.** Crawl the full site with Screaming Frog / Sitebulk; fix every 4xx and 5xx
- How: Get a tool. Screaming Frog SEO Spider (free up to 500 URLs) or Sitebulb (paid, better reporting). · Configure: User-agent: Googlebot (Smartphone) · Respect robots.txt: ON · Follow internal "nofollow": OFF (you want to see them) · Limit crawl depth or URL count based on site size · · Run the crawl. Export "Response Codes" and "Client Errors." · For every 4xx and 5xx: 4xx (client error): either fix the page, set up a 301 redirect, or remove the link pointing to it · 5xx (server error): fix the server issue. Google deprioritizes sites with 5xx patterns · · Run this crawl monthly. Track the error count over time.
- Avoid: Only running once and assuming the site is clean (errors creep in) · Not checking 5xx — these are the most damaging because they suggest site instability · Ignoring 3xx chains — chains waste crawl budget (see related action #10) · Missing soft-404s (pages returning 200 with "page not found" content)
- Verify: Run the crawl. Confirm 4xx and 5xx count is 0 (or close to it). · For every "soft-404" detected, either return a real 404/410 or add real content · After fixing, re-crawl. The count should drop. Set up a scheduled crawl (monthly at minimum)
- [ ] **10.** Replace all redirect chains/loops with a single 301 hop
- How: Crawl your site with Screaming Frog or Sitebulb. Export "Redirect Chains" and "Redirect Loops." · For each chain (A → B → C), decide the right fix: If A and C are both legitimate URLs, point A directly to C, skipping B · If B was an old URL, redirect A directly to C and either remove B or redirect B to C too · Each URL should have exactly one 301 hop to its final destination · · For loops (A → B → A), break the cycle by: Identifying which URL is the "real" one · Redirecting the other directly to the real one · Removing any code that re-creates the loop (e.g., a CMS rule that adds trailing slashes, then another that removes them) · · Re-crawl to confirm chains are now single hops.
- Avoid: Adding a new redirect on top of an existing chain (chain grows longer) · Using 302 (temporary) redirects for permanent moves (use 301) · Creating meta-refresh or JS redirects when a 301 would work (Google follows them but they're slower) · Leaving loops in place because the URLs "still work" (Google may not index them)
- Verify: Re-crawl. The "Redirect Chains" report should be empty. · Pick 5 URLs that previously chained. Use curl -I -L and count the number of 3xx responses — should be exactly 1.
- [ ] **11.** Keep response headers clean: one canonical, correct Content-Type, 200 for indexables
- How: Test your key pages. Use curl -I https://yoursite.com/ and inspect: HTTP/2 200 — good status · Content-Type: text/html; charset=utf-8 — correct type for HTML · Cache-Control — should allow caching for static assets, appropriate values for HTML · No X-Robots-Tag: noindex (unless intentional) · · For indexable pages, return 200 OK . For redirects, 301 Moved Permanently . For not-found, 404 Not Found (or 410 Gone for permanently removed content). · Don't return 200 for paginated, faceted, or filtered URLs. Use canonical, noindex, or block via robots.txt. · Check your CDN or reverse proxy doesn't add conflicting headers.
- Avoid: Returning 200 for soft-404 pages (a 404 page that says "we couldn't find it" but returns 200 status) · Setting Content-Type: text/plain for HTML pages (some CDNs do this for static files served from wrong origin) · Adding no-cache headers that prevent Google from caching your HTML (wastes crawl budget)
- Verify: curl -I against 10 important pages. Verify 200, correct content type, no stray X-Robots-Tag. · Run a Screaming Frog crawl with the "Response Codes" filter showing 200 — review for soft-404 patterns.
- [ ] **12.** Confirm Googlebot renders JS content: GSC → URL Inspection → View Rendered HTML; ensure main content is in the DOM
- How: Open GSC → URL Inspection. Paste a URL that has JS-rendered content. Click "View Rendered HTML." · Verify that your main content (h1, body text, internal links) is present in the rendered HTML, not just in the unprocessed HTML. · If content is missing: Server-side render the content instead of client-side rendering · Or pre-render with a tool like Puppeteer/Playwright at build time · Or use a dynamic rendering service for crawlers (less recommended) · · For Next.js: use getServerSideProps or static generation. Avoid client-only useEffect data fetching for content. · Test with Google's Mobile-Friendly Test (deprecated but similar tools exist).
- Avoid: Showing the first 200ms of HTML to Google and only loading the real content via JS (Google may not wait) · Blocking Googlebot from CSS/JS files in robots.txt (Google can't render without them) · Using heavy client-side state for content (use server-side rendering for indexable content)
- Verify: For 5 important pages, check "View Rendered HTML" in GSC URL Inspection. Content should be present. · Open Chrome DevTools → Disable JavaScript → Reload the page. The content you see is what Google sees in the first wave.
- [ ] **13.** Server-side-render or pre-render primary content; don't rely on click/scroll to inject indexable text
- How: Decide between SSG, SSR, or ISR: SSG (static): best for content that changes rarely (blog posts, docs, marketing pages). Build at deploy time. Examples: Next.js with getStaticProps , Gatsby, Astro, Hugo, Jekyll. · SSR (server-rendered): for content that must be fresh at request time (user dashboards, real-time data). Examples: Next.js with getServerSideProps . · ISR (incremental static regeneration): SSG with on-demand revalidation. Best of both for large content sites. Next.js supports this natively. · · For SSG, pre-render at build time. Make sure your build pipeline generates a real HTML file for each URL. · For SSR, render the page on the server with the real data. Don't serve a shell that JS fills in. · Verify: curl https://yoursite.com/page | grep "your main content" should return matches.
- Avoid: Pretending to SSR but only delivering a shell with and no content · Using CSR for SEO-critical content (homepage, blog posts, product pages) · Not handling hydration errors (page renders differently after JS loads) · Assuming CSR works for Google (it can, but it's slower to index and easier to break)
- Verify: View source on your key pages. The main content should be in the HTML, not just in a script tag or loaded after. · Use curl from a server (no JS) — if you see the content, you're SSR/SSG. · Check GSC → URL Inspection → Rendered HTML for a few pages.
- [ ] **14.** Keep URLs short, lowercase, hyphen-separated, static, keyword-bearing; no ?id=, session IDs, or uppercase
- How: Set URL rules for your site: Lowercase only — configure your server to lowercase URLs (or 301 uppercase to lowercase) · Hyphen-separate words, not underscores: /blue-cotton-tshirt not /blue_cotton_tshirt · Avoid session IDs, user IDs, or internal parameters in URLs · Keep total URL under 75-100 characters when possible · Include a primary keyword: /seo/schema-markup not /articles/12345 · · For existing URLs with bad patterns, set up 301 redirects to the clean version. Don't break the URL on existing indexed pages without a redirect. · For WordPress, go to Settings → Permalinks → Post name. Configure the system to enforce these rules. · For custom apps, add URL validation in your routing layer that rejects bad patterns with 404.
- Avoid: Using underscores: Google treats them as part of a single word, not separators · Including the date in URLs for evergreen content (forces you to either redirect later or commit to the date) · Making URLs case-sensitive — /Page and /page are different URLs to Google · Using ?id=12345 for everything (worst case for SEO and sharing)
- Verify: Sample 20 URLs from your site. Check: lowercase? Hyphen-separated? Short? No parameters? · Test a URL with mixed case: https://yoursite.com/Page-Name should 301 to /page-name · Look in GSC for URLs with parameters in the top indexed list — these are usually worth cleaning up
- [ ] **15.** Optimize crawl budget: return 304 Not Modified on unchanged pages; keep TTFB low so Googlebot crawls more
- How: Check if crawl budget is even a problem. In GSC → Settings → Crawl stats. Look at: Requests per day (your crawl rate) · Time spent downloading a page (lower is better — under 200ms is great) · Total bytes downloaded per day · "Crawled - not indexed" rate (high rate = wasted budget) · · Optimize if needed: Return 304 Not Modified for unchanged pages. Configure your server to support If-Modified-Since headers and respond with 304 when appropriate · Reduce TTFB (Time To First Byte) — faster responses let Googlebot crawl more URLs per second · Remove soft-404s and thin content (these waste budget on URLs that won't rank) · Block infinite-space URLs via robots.txt (calendars, internal search, faceted nav) · · Re-check GSC crawl stats monthly.
- Avoid: Optimizing crawl budget on a small site where it's not an issue (wasted effort) · Blocking Googlebot from CSS/JS (Google needs them to render) · Adding 304 headers but having them always return 200 anyway (server bug)
- Verify: Check GSC → Settings → Crawl stats. Look at the trend over the last 90 days. · Use curl -I -H "If-Modified-Since: [recent date]" on a few pages. Should return 304 if unchanged. · Test TTFB with WebPageTest or GTmetrix . Aim for under 200ms.
- [ ] **16.** Add -driven priority; ping IndexNow (Bing) and use GSC URL Inspection → Request Indexing for new/updated key pages
- How: For a new or substantially-updated key page: Open GSC → URL Inspection → paste the URL → "Request Indexing" · Google will queue it for fast re-crawl (hours to days, depending on authority) · · For IndexNow: Generate an API key once: openssl rand -hex 16 · Host a {key}.txt file at https://yoursite.com/{key}.txt containing the key · On new/updated URLs, submit: curl "https://api.indexnow.org/indexnow?url=https://yoursite.com/new-page&key=YOUR_KEY" · · Don't abuse Request Indexing. GSC has a per-property quota. Use it for your top 5-10 pages per week, not every page. · Verify: after a few hours, the URL should show as "Crawled" in URL Inspection.
- Avoid: Requesting indexing for every page on the site (Google will rate-limit you) · Requesting indexing for pages that aren't really new (Google tracks this and may slow down future requests) · Forgetting to wire to real content dates (see action #2) — without this, the request indexing has no freshness signal to act on
- Verify: After Request Indexing, the URL should show "Crawled" in URL Inspection within hours for high-authority pages · Watch GSC → Pages over the next week: the requested URL should appear in the "Indexed" count · For IndexNow, check the response of the curl command — 200 means the URL was accepted
- [ ] **17.** Fix soft-404s (thin/empty pages returning 200); return real 404/410 for gone content
- How: Identify soft-404s. Run a crawl with Screaming Frog or use GSC → Pages filtered by "Soft 404." · For each soft-404, decide the right fix: Page is truly gone: return real 404 Not Found (or 410 Gone if permanently removed). Configure your server or CMS to do this. · Page exists but is empty/thin: add real, unique content. The URL has a purpose — fulfill it. · Page was a search result or filtered view: block via robots.txt or noindex, don't 404 it (it might be a real filter URL) · · For WordPress: don't use a "404 page" plugin that returns 200 with a 404 template. Use a real 404. · For custom apps: configure your error handler to return the right status code. 404 template = 404 status.
- Avoid: Returning 200 with a "Page not found" template (the most common soft-404 mistake) · Returning 404 for URLs that should be real (search results, filtered views, paginated indexes) · Using 404 instead of 410 for permanently removed content (410 is more explicit and slightly faster for Google to deindex) · Not checking the response status of your 404 page itself (it should be 404, not 200)
- Verify: Visit a few obviously missing URLs. Confirm they return 404 (or 410). · Check GSC → Pages → Soft 404. The count should drop to 0 over the next crawl cycle.
- [ ] **18.** Block infinite crawl traps (calendars, filters) with robots.txt + rel="nofollow" on those internal links
- How: Identify common crawl-trap patterns on your site: /calendar/2026/08/16/page/3 — every date + every page = millions of URLs · /search?q=red&filter=size&filter=color&page=2 — every filter combination = infinite URLs · /products?sort=price&dir=asc&page=1 — sort + direction + page = many URLs of the same content · · Block these in robots.txt: Disallow: /calendar/ Disallow: /search Disallow: /*?sort=* Disallow: /*&sort=* · Add rel="nofollow" on internal links to these URLs. Don't pass PageRank to them. · Use canonical tags pointing parameter URLs to the base URL. · Verify: run Screaming Frog with "Respect robots.txt: ON." It should not crawl the blocked patterns.
- Avoid: Blocking /calendar/ when your content is calendar-based (blocks real pages) · Not using nofollow on the internal links (PageRank still flows to the blocked URLs, which is wasted) · Blocking one pattern but missing a similar one (e.g., blocking /search but not /find )
- Verify: After robots.txt update, check GSC → Pages over 2-4 weeks: the count of crawled URLs from your trap patterns should drop to 0 · Run Screaming Frog with robots.txt respected. The trap URLs should be excluded from the crawl
- [ ] **19.** Set a logical URL folder taxonomy (/topic/subtopic/page) mirroring the site hierarchy
- How: Map your site's information architecture. Group your content into: Top-level categories (3-7 main topics) · Subcategories (specific topics within each) · Content pieces (the actual pages) · Example for a marketing site: /seo/ — all SEO content · /seo/schema/ — schema-specific pages · /seo/schema/markup-guide — a specific article · · Configure your CMS or framework to enforce this structure. WordPress: use categories and proper permalinks. Custom: enforce in your routing layer. · For existing sites with messy URLs, set up 301 redirects to the new clean URLs. Update internal links to point to the new URLs. · Maintain it. When adding new content, place it in the right taxonomy. If a new category emerges, add it explicitly.
- Avoid: Deep nesting (more than 3 levels deep) makes URLs long and signals that the page is "deep" in your site · Changing the taxonomy without redirects (loses all the equity built up in old URLs) · Adding categories ad-hoc (every new article gets its own top-level category, defeating the purpose of hierarchy)
- Verify: Pick 10 random URLs. Each should fit cleanly into your documented taxonomy. · Check for orphans: pages that don't link to anything in your site and aren't linked from anything. Use a tool like Screaming Frog's "Orphan Pages" filter.
- [ ] **20.** Verify all property variants (http/https, www/non-www) in GSC; 301 all variants to one canonical host
- How: Decide your canonical host and protocol. Common choices: https://yoursite.com (no www) — most common in 2026 · https://www.yoursite.com (with www) — also fine · Always https, never http · · Add 301 redirects from every variant to the canonical. In nginx: server { server_name www.yoursite.com yoursite.com; return 301 https://yoursite.com$request_uri; } server { listen 80; return 301 https://yoursite.com$request_uri; } · In GSC, add and verify ALL four properties: http://yoursite.com · http://www.yoursite.com · https://yoursite.com · https://www.yoursite.com · In each non-canonical property, set the canonical as the preferred domain.
- Avoid: Only redirecting http → https but not www → non-www (or vice versa) · Only verifying the canonical property in GSC (you need to verify all variants) · Using canonical tags instead of redirects (canonical tags are a hint, not a directive — use 301 for actual consolidation) · Allowing https://www to keep being indexed because you didn't add it to GSC
- Verify: Test each variant: curl -I http://yoursite.com , curl -I https://www.yoursite.com . All should 301 to the canonical. · In GSC, confirm all four properties are verified and the canonical is set as preferred.
## B · Canonicalization and duplicates
_12 actions, #21–32_
The same content reachable at four URLs splits its own signals four ways. These actions make one URL the answer and every other variant point at it — trailing slashes, parameters, protocol, www, pagination, syndication.
- [ ] **21.** Add a self-referencing absolute rel="canonical" on every page
- How: Add this tag inside the of every indexable page: The href must be an absolute URL (not relative). · For most CMSes, this is a one-line config: WordPress: Yoast or Rank Math add this automatically. Verify on a few pages. · Next.js: use metadata.alternates.canonical in the page or layout. · Astro: use Astro.site + your slug. · · For dynamic pages, generate the canonical from the request URL. Example in Express: app.use((req, res, next) => { res.locals.canonical = `${req.protocol}://${req.get('host')}${req.originalUrl.split('?')[0]}`; next(); }); · Verify: view source on 10 random pages. Each should have exactly one canonical, pointing to itself.
- Avoid: Pointing the canonical to a different URL than the page itself (use a separate page if you really want to canonicalize to another URL) · Using relative URLs in the canonical href (must be absolute) · Having multiple canonical tags on the same page (Google ignores both) · Forgetting to canonicalize paginated views (page 2, 3, etc. should canonical to page 1, or use a "view-all" page)
- Verify: Curl 10 random pages. Each should have exactly one in the head. · Each canonical's href should match the page's own URL (after stripping query params). · In GSC URL Inspection, the "Canonical" should match "User-declared canonical" should match the inspected URL.
- [ ] **22.** Point cross-domain/syndicated duplicates' canonical to the original URL
- How: Identify the relationship. Page A on domain1.com and page B on domain2.com have substantially the same content. The master is domain1.com/page. · On the page at domain2.com/page, add: · On the master page at domain1.com/page, the canonical points to itself (self-referencing). · Optionally, also add hreflang tags if the pages serve different markets/regions. The canonical and hreflang are independent signals — they don't conflict. · Verify by running GSC URL Inspection on the duplicate URL. The "Canonical" should show the master URL.
- Avoid: Cross-domain canonicalizing a page to a different page (the duplicate must contain substantially the same content) · Forgetting to handle the noindex conflict — if you have a cross-domain canonical, do NOT also noindex the duplicate (Google will treat that as a contradiction) · Using 301 redirects instead of canonical tags when you want to keep both URLs accessible (use 301 when you want users to land on the master, canonical when you want to keep the duplicate URL accessible but consolidate signals)
- Verify: On the duplicate page, view source. Confirm . · GSC URL Inspection on the duplicate URL. Google should report the master as the canonical. · After 2-4 weeks, search site:duplicate-domain.com/page in Google. The result should point to the master.
- [ ] **23.** Ensure canonical, hreflang, sitemap, and internal links all use the SAME URL string (protocol/slash/case)
- How: Identify which parameters matter and which don't: utm_source , utm_medium , utm_campaign — tracking. Don't index. · id , pageid — content identifier. Index the canonical version. · sort , dir — view order. Don't index. · q , query , search — search within site. Don't index. · · For each parameter type, add a canonical tag to the URL that points to the parameterless version. Example: /products?id=123 should have . · In GSC → URL Parameters (legacy tool), declare which parameters Google should ignore. (This tool is deprecated for new users; canonical tags are the recommended approach.) · For unknown bots that don't respect canonicals, block parameter URLs in robots.txt: Disallow: /*?*
- Avoid: Blocking all parameter URLs (some are legitimate, like search results on a faceted navigation site) · Canonicalizing paginated parameters to page 1 (page 1 canonicalizes to itself; pages 2, 3 canonical to page 1; or use view-all-page canonical to a single URL) · Not handling tracking parameters — without explicit handling, ?utm_source= variants get indexed as duplicates
- Verify: Add a tracking parameter to a URL: yoursite.com/about?utm_source=test . The page should have a canonical pointing to yoursite.com/about . · GSC → URL Inspection on the parameterized URL should report the canonical as the unparameterized version. · After a month, GSC's Pages report should show no parameter URLs as separate indexed pages.
- [ ] **24.** Consolidate example.com/, /index.html, trailing-slash, and uppercase variants with 301s
- How: Pick your preferred hostname. www.yoursite.com or yoursite.com. The choice doesn't matter for SEO; pick what's easier to brand. · Set up 301 redirects from the non-preferred to the preferred. In nginx: server { server_name www.yoursite.com; return 301 https://yoursite.com$request_uri; } · Set the canonical tags throughout your site to the preferred version. · In GSC, add and verify BOTH the www and non-www properties. In the non-preferred, set the preferred as the canonical domain. · Update all internal links to use the preferred version (use a search-and-replace across the codebase).
- Avoid: Setting 301 redirects but forgetting to update internal links (the 301 still works but the signal is weaker) · Verifying only one property in GSC (you need both, so GSC can show the right canonical preference) · Using canonical tags instead of 301 redirects — canonical is a hint, not a directive; use 301 to actually consolidate
- Verify: Test both: curl -I https://www.yoursite.com/ and curl -I https://yoursite.com/ . Both should 301 to the same preferred version. · In GSC, both properties should be verified, with one set as preferred.
- [ ] **25.** De-index paginated ?page=2+ bodies' thin value: keep them indexable but ensure each has unique content, canonical to self (not to page 1)
- How: Pick your preference. Most modern sites use no trailing slash ( /about ). Static file servers tend to use trailing slash ( /about/ ). · Configure your server or framework to enforce it. In Next.js, trailingSlash: false in next.config.js. In Apache with .htaccess: RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.+)/$ /$1 [L,R=301] · For internal links, use the preferred version. Audit and fix. · For canonicals, use the preferred version.
- Avoid: Setting the redirect but not updating internal links (signal still splits between the two) · Allowing the CMS to add a trailing slash on some pages and not others (inconsistent) · Not handling the index.html / index.php case (also creates duplicates — /about/ vs /about/index.html )
- Verify: Test variants: curl -I https://yoursite.com/about/ should 301 to https://yoursite.com/about (or vice versa). · Sample 10 pages, check for trailing-slash consistency in canonicals and internal links.
- [ ] **26.** Parameterize sort/filter URLs with noindex or robots-block; keep one clean canonical per product/category
- How: If you have AMP pages: On the AMP page, add: · On the regular page, add: · · Validate the canonical chain: the regular page should reference the AMP, the AMP should reference back. · If you're not using AMP, just don't create AMP pages. The advice in 2026: skip AMP entirely. The performance benefits aren't worth the complexity unless you have a news publishing use case.
- Avoid: Implementing AMP without the bidirectional canonicals (Google may pick the wrong version) · Using AMP for everything (only use it for top-funnel content where speed matters most) · Assuming AMP is required for Top Stories (Google deprecated the AMP requirement in 2021)
- Verify: If you have AMP pages, view source on both the regular and AMP versions. Confirm the bidirectional link tags. · If you don't need AMP, ensure you're not creating AMP pages by default (check your CMS or template).
- [ ] **27.** Merge near-duplicate pages (same intent, <20% unique) into one stronger URL + 301 the rest
- How: Every non-production environment must be noindex. The simplest way: add a meta robots tag via your staging environment config: · Block the entire staging domain in robots.txt: User-agent: * Disallow: / · Add the staging domain to GSC as a separate property and verify ownership. If Google ever does index it, you'll see it here and can act. · Add a password (HTTP Basic Auth) to staging for defense in depth. Stops both bots and curious humans. · Never link from production to staging (no "staging.example.com" anchor tags in your site).
- Avoid: Relying on robots.txt alone (some bots ignore it; a meta noindex is more reliable) · Forgetting to add the staging URL to GSC until it's already indexed (preventive monitoring is better) · Using a "noindex" tag on production by accident (a config error in your staging banner that bled into production)
- Verify: Visit a few URLs on your staging domain. Confirm the noindex tag is present in the HTML source. · Test that robots.txt on staging disallows everything. · Run a GSC URL Inspection on a known staging URL: Google should report it as excluded.
- [ ] **28.** Remove boilerplate-only pages that Google clusters as duplicates (leak: IndexingDupsLocalized clustering)
- How: For paginated content (like a blog category page), pick an approach: Canonical to self: each page (page 1, 2, 3) canonicals to itself. This works but doesn't consolidate signals. · Canonical to page 1: all pages canonical to the first page. But this means Google won't index pages 2+, which may be what you want for thin paginated content. · Noindex pages 2+: use noindex on paginated views. Only page 1 is indexable. · View-all page: a single page that shows all items, canonical referenced by the paginated pages. · For most modern sites, noindex pages 2+ is the right call. Users get the value of the listing page, but Google only sees page 1 as the canonical version. · For paginated articles (like a 5-part series), don't use pagination for SEO. Use a single page or proper series linking.
- Avoid: Indexing every paginated page (each page becomes a thin version of page 1, dilutes signals) · Using rel="next/prev" (deprecated by Google in 2019, no longer a signal) · Forgetting that pagination URLs also have parameter variants ( ?page=2 ) that need handling
- Verify: Curl page 2 of a paginated section. Confirm noindex meta tag OR canonical to page 1. · Run a Screaming Frog crawl. The "Noindex" filter should include your paginated pages.
- [ ] **29.** Give every page a unique and meta description (dup titles trigger duplicate clustering)
- How: Audit your URLs. Look for patterns like ?sid= , ?phpsessid= , ;jsessionid= . If they exist, fix at the source. · Configure your server or framework to not include session IDs in URLs. In PHP: // php.ini session.use_only_cookies = 1 session.use_trans_sid = 0 · In Java/Servlet containers (Tomcat etc.), disable URL rewriting for sessions: // Disable in web.xml true · Add canonical tags to all URLs (should not include session IDs in the canonical href). · Block session ID patterns in robots.txt as a fallback: Disallow: /*?sid=
- Avoid: Adding the canonical but keeping the session ID in the canonical href (defeats the purpose) · Blocking the pattern in robots.txt but not fixing the source (other crawlers may ignore robots.txt) · Not realizing the Java web framework defaults to URL session rewriting (this is a real production gotcha)
- Verify: Browse your site as an unauthenticated visitor. Inspect URL bar. No session IDs. · Run a Screaming Frog crawl. No URLs should contain session parameters.
- [ ] **30.** Set a preferred domain and enforce HTTPS canonical; redirect HTTP→HTTPS site-wide
- How: Get a valid SSL certificate. Let's Encrypt is free and automated. · Configure your server to redirect all HTTP to HTTPS. In nginx: server { listen 80; server_name yoursite.com www.yoursite.com; return 301 https://yoursite.com$request_uri; } · Update all internal links to use HTTPS. Find and replace across your codebase. · Add HSTS to lock HTTPS: add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; · Add the HTTPS property to GSC. Set the HTTPS version as preferred.
- Avoid: Allowing mixed content (HTTPS page loading HTTP images, scripts, or iframes — browsers block these) · Forgetting to update external integrations (CDN, third-party scripts, webhooks) to use HTTPS · Not setting HSTS (allows SSL stripping attacks) · Forgetting to renew the SSL cert (Let's Encrypt is 90 days; expired certs are demoted)
- Verify: curl -I http://yoursite.com/ should 301 to https://yoursite.com/ . · View source on every page type. No mixed-content warnings in Chrome DevTools. · Test with SSL Labs — aim for A+.
- [ ] **31.** Handle UTM/tracking params with canonical to the clean URL
- How: For your product/category pages, identify which facets are useful for users vs which just create duplicates: Useful (indexable): primary category, brand, primary use case · Less useful (noindex or block): color, size, price range, secondary filters · · For non-indexable facets: Add rel="nofollow" to the internal links pointing to them · Add noindex meta on the resulting URL (or canonical to the base URL) · Block via robots.txt as a backup · · For indexable facets, generate a real canonical URL with descriptive content. E.g., /shoes/running has its own title, description, and intro — not just a filtered list. · Use a single canonical for filtered URLs that points back to the base. Example: /shoes?color=red canonicals to /shoes .
- Avoid: Indexing every facet combination ("red Nike running shoes size 10" gets 5 different indexable URLs) · Using JavaScript-only filter changes (creates URLs Google can't see but users see as different) · Not having unique content on indexable facet pages (Google's "thin content" filter will hit them)
- Verify: Apply several facet combinations. Verify each combination either has a canonical to the base OR is blocked by robots.txt. · Check GSC → Pages over 4 weeks. The count of "faceted" URLs in the index should be 0 or near 0.
- [ ] **32.** Localize duplicates correctly: same-language regional variants need hreflang, not canonical, between them
- How: Decide the original. Your site is the canonical source. The syndication partner republishes but signals you as canonical. · On the syndicated version, add: · On your original, no change needed. The syndicated version canonicals back to you. · For Medium: when you import, Medium automatically adds a canonical to your original. Verify this happened. · For LinkedIn: LinkedIn doesn't support rel=canonical in articles. Use a "Originally published at [link]" link at the top, and don't syndicate the same content to multiple partners (each adds noise).
- Avoid: Letting the syndication partner rank for your article (because they didn't add the canonical, or added it incorrectly) · Syndicating to multiple partners without checking each canonicalizes properly (Google may pick the wrong one) · Not linking back to your original from the syndicated version (helps users and signals authority)
- Verify: Find a syndicated article. View source. Confirm the canonical points to your original. · Search the article's title in Google. Your site should rank, the syndicated version should not (or rank below you).
## C · Architecture and internal linking
_16 actions, #33–48_
Internal links are the only ranking factor you control completely. They tell Google which pages matter, and they carry authority from the pages that have it to the pages that need it.
- [ ] **33.** Keep every important page ≤3 clicks from the homepage
- How: Map your content. Group every page into 3-7 main topics (e.g., for a marketing site: SEO, GEO/AI, Local, Schema, Content). · For each topic, have: A pillar page (broad overview, links to all subtopics) · Cluster pages (specific deep-dives, link back to the pillar) · Detail pages (specific items, link up to the cluster) · · Establish a URL hierarchy: /topic/ (pillar) /topic/subtopic/ (cluster) /topic/subtopic/item/ (detail) · Internal links should reinforce the structure: cluster pages link to the pillar, detail pages link to both. · Aim for "3 clicks from anywhere." If a page is deeper than 3 clicks, it may be an orphan or buried too deep.
- Avoid: Going 5+ levels deep (URLs get long, signal weakens at each level) · Cross-linking everything to everything (drowns the actual structure in noise) · Reorganizing without 301 redirects (loses the equity built up in old URLs) · Having a flat structure (no topics) — Google sees the site as a soup of pages with no topical authority
- Verify: Draw your site structure on paper. Each page should have a clear parent topic. · Run Screaming Frog → "Site Structure" visualization. Look for orphan pages and overly deep pages. · Check: from your homepage, can you reach any page in 3 clicks?
- [ ] **34.** Build one pillar page per core topic; link every cluster article up to its pillar and back down
- How: Identify a topic with 5+ related subtopic pages. That's your hub candidate. · Create (or repurpose an existing page as) the hub. The hub should: Cover the topic at a high level (~1500-2500 words) · Link to every subtopic page in the cluster · Have a clear table of contents or section structure · Be the most authoritative single page on the topic · · From each subtopic page, add a link back to the hub ("Back to: [Hub title]"). · Update the site's main navigation to include hubs in their respective sections. · Promote the hub from your homepage or main topic page.
- Avoid: Hub page is too short (under 800 words) — doesn't have enough authority to concentrate signals · Hub page is a list of links only (no real content above the links) · Hub page doesn't link to all the cluster pages (some signals get stranded) · Multiple hub pages for the same topic (splits the authority)
- Verify: From the hub, count the links to subtopic cluster pages. Should be at least 5, ideally all of them. · From each subtopic, count the back-links to the hub. Should be 1+ each. · Run an internal-link audit with Screaming Frog. The hub should be one of the most linked-to pages on the site.
- [ ] **35.** Add 3–10 contextual in-body internal links per page (not just nav/footer)
- How: For each new or existing content piece, when you mention a concept that's covered elsewhere on your site, link to that page. Example: in an article about sitemap.xml, link to your article about . · Aim for 3-10 contextual links per page. More than 10 starts to look spammy. Less than 3 means you're missing linking opportunities. · Use descriptive anchor text. "Learn more about XML sitemaps" not "click here". · Link to pages that are relevant to the surrounding content, not pages you're trying to rank for the sake of it. Relevance > volume. · For old content, audit and add new internal links as you create new content. New pages should link back to old ones in the same topic area.
- Avoid: Using "click here" or "read more" as anchor text (wastes the relevance signal) · Linking to the same page multiple times from the same page (consolidate into one link) · Adding 30+ links per page (dilutes the signal, looks spammy) · Linking to the same destination from every page (Google ignores the duplicate)
- Verify: On a 1500-word page, count contextual links. Should be 3-10. · Sample 10 pages. Check the anchor text of internal links. Is it descriptive, not generic? · Use Screaming Frog to map internal links. Are there pages with very few internal links pointing to them (orphans or under-linked)?
- [ ] **36.** Use descriptive 2–5 word anchor text on internal links; never "click here"/"read more" for key targets
- How: For any important destination URL, use a mix of anchor texts: Exact match: "XML sitemaps" (link to /xml-sitemaps) · Partial match: "how to set up sitemaps" (still relevant) · Branded: "Acme's sitemap guide" (safe) · Generic: "read this guide" (least SEO value but natural) · URL: "acme.com/xml-sitemaps" (works but ugly) · · Don't engineer the exact-match ratio. Aim for natural variation — usually 30-40% exact, 30% partial, 20% branded, 10% other. · Avoid: 100% exact-match anchor text (a clear manipulation pattern).
- Avoid: Same anchor text on every link to the same page (Google ignores most of them) · Using off-topic anchor text (links "cheap shoes" to a page about XML sitemaps) · Over-optimizing exact-match anchors (looks like manipulation)
- Verify: For an important URL, sample all internal links pointing to it. Note the anchor texts. · Are they varied? Are they all on-topic? · Check external links too (with a tool like Ahrefs or Majestic). External anchor diversity matters more than internal.
- [ ] **37.** Link every new post from ≥3 topically related existing pages within 24h of publishing
- How: Run Screaming Frog. Filter by "Orphan Pages." This shows every page that has zero internal links pointing to it. · For each orphan page, decide: Important content: add internal links from relevant pages · Outdated content: redirect to a current page or noindex it · Low-value content: noindex it or delete it · · Common causes of orphans: Page exists in CMS but no one links to it (forgotten content) · URL changed but internal links not updated · New category added but no nav link to its children · · For new content, add it to relevant hub pages, related articles, and the site's nav or footer as appropriate.
- Avoid: Adding new content and forgetting to link to it from existing pages · Having a "recent posts" widget as the only place a page is linked (Google can find it but it's still a weak signal) · Not auditing for orphans regularly (the count grows as content ages)
- Verify: Run Screaming Frog → Orphan Pages filter. Aim for 0 (or only pages you intentionally want to be hard to find). · Add the orphan check to your monthly audit.
- [ ] **38.** Find and fix orphan pages (Screaming Frog → Orphan URLs vs sitemap) by adding internal links
- How: Implement structured breadcrumbs with schema.org markup. JSON-LD: { "@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [ {"@type": "ListItem", "position": 1, "name": "Home", "item": "https://yoursite.com/"}, {"@type": "ListItem", "position": 2, "name": "SEO", "item": "https://yoursite.com/seo/"}, {"@type": "ListItem", "position": 3, "name": "Schema markup", "item": "https://yoursite.com/seo/schema/"} ] } · Render visible breadcrumbs in your template. The path should match the URL structure. · Each breadcrumb should be a clickable link (except the current page, which is plain text). · Place breadcrumbs near the top of the page, before the main content.
- Avoid: Breadcrumbs that don't match the URL structure (inconsistency confuses users and Google) · Missing schema markup (Google may not display breadcrumbs in SERPs) · Breadcrumb links that all go to the homepage (no actual hierarchy) · Breadcrumbs in a tiny font at the very bottom of the page (low visibility)
- Verify: Open a deep page on your site. Confirm breadcrumbs are visible and reflect the URL structure. · Validate the BreadcrumbList schema with Schema.org validator . · After Google recrawls, check GSC → Enhancements → Breadcrumbs for any errors.
- [ ] **39.** Push link equity to money pages: link them from the homepage and high-authority posts
- How: Identify the most important destinations for users and search. These belong in the footer: Main topic/category pages (3-7) · About, Contact, Blog · Privacy, Terms · Resources (llms.txt, sitemap, RSS) · · Keep the footer to 4-5 columns of links, 5-8 links per column. Total: 20-40 footer links. · Order columns by importance: most important navigation first. · Use real anchor text, not icons. (Google can't read most icons without alt text.)
- Avoid: Stuffing the footer with 200+ links (looks manipulative, dilutes the value of each) · Using the same footer across the whole site without thought (it's a global nav, treat it that way) · Having footer links to pages that don't exist or 404 (use GSC to verify)
- Verify: Count your footer links. Should be 20-40. · Check: does every footer link resolve to a 200 page? · Run Screaming Frog → "All" → filter by "Inlinks > 0." Every page in your footer should be linked to by every other page.
- [ ] **40.** Add breadcrumb navigation site-wide + BreadcrumbList schema
- How: Use algorithmic related-posts (not just "most recent"). Look at: Same category/tag · Same keywords in title or first paragraph · High overlap in content (TF-IDF or simple keyword matching) · · For WordPress, plugins like JARVIZ or Related Posts for WP do this. · For custom sites, compute relatedness on the server. Use Jaccard similarity on tokenized content, or cosine similarity on TF-IDF vectors. · Display 4-6 related posts. Below the article, not in a sidebar (above the fold on mobile is wasted on related content).
- Avoid: Showing the same related posts on every page (algorithmic bug, suggests no actual relevance calculation) · Showing 20+ related posts (overwhelming, dilutes CTR on each) · Showing off-topic content (recommendation algorithm is broken) · Auto-linking to your most-trafficked pages regardless of relevance (Google may see it as PageRank sculpting)
- Verify: On 5 different content pages, check the related posts. Are they actually related? · Click through to a related post. Does the relevance hold across the click?
- [ ] **41.** Keep a flat-ish depth: avoid pages buried >4 folders deep with no internal links
- How: For every internal link on a page, check the anchor text: Is it descriptive? (Names the destination topic) · Is it relevant? (Matches the surrounding sentence) · Is it not generic? (Avoids "click here", "this article", "read more") · · When you can't find a good natural fit, rephrase the surrounding sentence. Don't force "click here" to link to your XML sitemap guide. · For images used as links, the alt text serves as the anchor text. Make it descriptive too.
- Avoid: Using "Read more" as the entire anchor (no information, no value) · Using the URL as the anchor (ugly, no context) · Using "this article" or "this post" as the anchor (vague) · Using image alt text as the only descriptive link element (works but text is better)
- Verify: Sample 20 internal links across your site. Note the anchor text. Is it descriptive? · Run Screaming Frog → "All" → "Anchor Text" filter. Look for common low-value anchors ("click here", "read more").
- [ ] **42.** Add HTML sitemap / hub pages for large sections to distribute crawl and equity
- How: For every facet link in your UI (color filter, size filter, sort options), decide: Is the resulting URL a useful, indexable page? (Like /running-shoes vs /shoes?type=running) — then nofollow isn't needed · Is it a near-duplicate of the base? (Like /shoes?color=red) — then nofollow · · For non-indexable facet links, add rel="nofollow" : Red · For JavaScript-rendered facets, use the API-level nofollow equivalent (don't render the link, or render it as a button that triggers a state change). · Block the patterns in robots.txt as a backup.
- Avoid: Nofollowing links that lead to real indexable content (wastes PageRank flow) · Forgetting nofollow on JavaScript-rendered facet changes (they still create URLs) · Nofollowing without canonical or robots.txt protection (Google may still discover the URLs from external links)
- Verify: Apply 5 facet combinations in your UI. Check the resulting URLs and their canonical tags. · Each facet URL should be either noindex, canonical-to-base, or blocked by robots.txt.
- [ ] **43.** Limit on-page links to a reasonable count; avoid diluting equity with hundreds of footer links
- How: You can't directly ask Google for sitelinks, but you can increase your chances: Have a clear site structure with prominent nav links to important pages · Use descriptive, distinct anchor text for those nav links ("About us" not just "About") · Use a clear, text-based navigation (not just images or JavaScript) · Have unique titles and meta descriptions for each subpage · · Avoid using nositelinkssearchbox meta tag (which would actively prevent Google from showing them). · Wait. Google generates sitelinks algorithmically. They're not guaranteed, and Google may add or remove them over time.
- Avoid: Looking for ways to force sitelinks (you can't, and trying to game them often backfires) · Using nofollow on your main nav (wastes the signal that helps Google identify these as important pages) · Having nav links to low-quality pages (Google may show those instead of the ones you want)
- Verify: Search site:yoursite.com in Google. Check if sitelinks appear under your homepage. · If not: check that your main nav is text-based, well-structured, and the most important pages are prominent. · If they appear: don't make sudden structural changes (Google may update sitelinks accordingly).
- [ ] **44.** Use rel="nofollow"/sponsored/ugc correctly on paid, user-generated, and untrusted links
- How: For paginated archive views (category pages, blog index, search results): Page 1: indexable, canonical to itself · Pages 2+: noindex, OR canonical to page 1 · · For paginated content (an article split across pages, like a 5-part guide): Don't paginate. Use a single page with anchor links to sections. · If you must paginate: each page is indexable with self-referencing canonicals. Use rel="next" and rel="prev" (deprecated as a signal but still works as a hint). · · For WordPress: paginated archive views are usually noindex by default in modern themes. Verify.
- Avoid: Having page 2, 3, 4... all as separate indexable URLs with no canonical (creates thin duplicate content) · Using rel="next/prev" thinking it's a current signal (Google deprecated this in 2019) · Not handling page 1 specifically (it should be the canonical version of the archive, not a paginated view)
- Verify: View source on /blog?page=2 and /blog?page=3 . Confirm noindex meta OR canonical to page 1. · Use Screaming Frog to crawl your site. Check the "Noindex" filter includes your paginated views.
- [ ] **45.** Audit internal anchor-text distribution; ensure target pages get keyword-relevant (not generic) anchors
- How: Identify claims in your content that benefit from external citations: Statistics: link to the original study · Industry terms: link to the canonical source (Wikipedia for general topics, official docs for technical) · Quotes: link to the original source · · Use 3-5 outbound links per long-form content piece. Not every sentence needs a link, but claims should be backed. · Open external links in a new tab ( target="_blank" ) to keep users on your site. · Add rel="noopener" to target="_blank" links (security best practice).
- Avoid: Linking to low-quality external sites (Google may use this as a quality signal about your page) · Linking to direct competitors (they may rank instead of you for the topic) · Forgetting to open external links in a new tab (users navigate away and don't return) · Adding 30+ outbound links per page (overwhelming, dilutes value)
- Verify: Sample 5 long-form pages. Count external links. Should be 3-5 per page. · Check: do the external links open in a new tab? Do they have rel="noopener" ?
- [ ] **46.** Link related/next-step content at the end of articles to reduce pogo-sticking back to SERP
- How: Configure your server or CMS to return 404 for non-existent URLs (not 200 with a "not found" page — that's a soft-404). · Make the 404 page useful: Clear "page not found" message · Search box (let users find what they were looking for) · Top categories or popular pages (so they can navigate) · Link to your sitemap · · Don't noindex your 404 page. It's not in the index anyway, but for safety don't add an explicit noindex. · Track 404s in GSC → Pages. If you see legitimate content hitting 404, set up a 301 to the right destination.
- Avoid: Returning 200 with a "page not found" page (soft-404, confuses Google) · Showing only a blank error message (missed opportunity to keep the user) · Noindexing the 404 page explicitly (redundant, but signals confusion) · Not checking GSC for patterns (legit 404s from broken links should be redirected)
- Verify: Visit a non-existent URL on your site. Confirm 404 status code and helpful page content. · Check GSC → Pages for 404 patterns. If a deleted page is getting traffic, 301 it to a replacement.
- [ ] **47.** Ensure primary nav is crawlable (not JS onclick / button-only)
- How: Decide if you need one. Most modern sites don't — XML sitemaps + good internal linking is enough. HTML sitemaps add value for: Very large sites (10,000+ pages) where internal linking can't reach everything · Directories or link-collection sites · User-facing "site directory" experiences · · If you build one, organize by topic. Use a real URL structure that mirrors your site's architecture. · Don't auto-generate from your database (will create thousands of links). Curate it. · Add a nofollow attribute to keep the link equity from spilling across the entire site (if your footer already lists everything, the HTML sitemap may be redundant).
- Avoid: Adding an HTML sitemap with 5,000 links (looks spammy, overwhelms users) · Not organizing the HTML sitemap by topic (one flat list of every URL is useless) · Forgetting to update it as new content is added (becomes stale fast) · Confusing the HTML sitemap with the XML sitemap (different things, both valid)
- Verify: If you have one, check that it loads in a reasonable time and is organized by topic. · Count the links. Should be 50-500 (curated, not auto-generated). · Check that the most important pages are linked from the top of the sitemap.
- [ ] **48.** Fix redirected/404 internal links so equity isn't lost in hops
- How: Use real words, not IDs or session parameters. · Include the primary keyword in the URL when natural. /xml-sitemap-guide not /post-12345. · Keep it short. Under 75 characters is good. Over 100 starts to look spammy. · Use hyphens, not underscores. Google treats hyphens as word separators, underscores as part of words. · Lowercase only. Mixed case creates duplicate URLs. · Don't change URLs once published. Each change requires 301 redirects and loses some signal. If you must change, set up permanent redirects from the old URL.
- Avoid: Including the date in the URL (forces a redirect or staleness when the date changes) · Using underscores (Google sees blue_widgets as one word) · Changing URL structure after the site is established (each change loses signal) · Including category hierarchy in URLs that you'll need to change (like /category/subcategory/post-title)
- Verify: Sample 20 URLs. Each should be human-readable, under 75 chars, with hyphens separating words. · Check for stability: don't change URLs unless absolutely necessary.
## D · Speed and Core Web Vitals
_30 actions, #49–78_
Speed is a measured signal, not an opinion: Google reads real Chrome data from real visitors. These actions target the three metrics that count, and the render path that produces them.
- [ ] **49.** Re-encode photos to AVIF (avifenc --min 20 --max 30), UI/graphics/screenshots to WebP (cwebp -q 80)
- How: Understand each metric: LCP — when the largest visible element renders. Good: under 2.5s. · INP — how long the page takes to respond to user interaction. Good: under 200ms. · CLS — how much the layout shifts during load. Good: under 0.1. · · Check your current scores in GSC → Experience → Core Web Vitals . This shows real-user data from Chrome users. · Cross-reference with PageSpeed Insights for the same pages. PSI gives you both lab data (controlled test) and field data (real users). · For each "Poor" or "Needs Improvement" page, identify the dominant issue (usually one metric). Focus your fix on that.
- Avoid: Optimizing for lab scores (PSI) and ignoring field data (GSC) — Google ranks on field · Optimizing for LCP only and ignoring INP/CLS — all three matter · Optimizing the homepage and assuming the rest of the site is fine (Google reports per-URL)
- Verify: Check GSC → Core Web Vitals monthly. Track the percentage of "Good" URLs. · For URLs in "Poor" category, run PageSpeed Insights. The lab test reproduces the field issue.
- [ ] **50.** Serve images via with AVIF/WebP + a JPEG/PNG fallback
- How: Generate three versions of each image: AVIF, WebP, JPEG. Use cavif for AVIF, cwebp for WebP, and your standard JPEG export. · In your HTML, use with multiple sources: · Modern browsers (Chrome, Firefox, Safari 16+) will use AVIF. Older browsers fall through to WebP or JPEG. · For WordPress, use a plugin like ShortPixel or Imagify. For Next.js, use the next/image component with AVIF in config. · Verify: open the page, check Network tab. The image request should be .avif or .webp.
- Avoid: Using AVIF without a fallback (Safari < 16 and old Android won't show the image) · Forgetting to set width/height on the (causes CLS) · Generating AVIF in lower quality than the source JPEG (no point in modern format if it looks bad)
- Verify: Open the page in Chrome DevTools → Network. Find image requests. The format column should show avif or webp for modern browsers. · Compare file sizes: an AVIF should be ~30% smaller than a comparable-quality WebP, which is ~30% smaller than JPEG.
- [ ] **51.** Cap in-content images at ≤1600px wide / ≤100KB; hero ≤200KB
- How: For body images, export at 1600px max width. For most desktop displays, this is full-width on a 1920px screen. · For hero images, export at 2000px max width. · Run your images through a compressor. Squoosh or cavif -Q 60 (AVIF) usually gets to target size. · For a static site, automate this in your build pipeline. Image optimization should run on every build. · For WordPress, use ShortPixel or Imagify with the right quality settings.
- Avoid: Exporting 4000px+ images "for retina" (modern displays don't need more than 2x of the rendered size) · Using uncompressed PNG for photos (use JPEG/WebP/AVIF instead) · Keeping the original full-res image in your CMS even after generating optimized versions (waste of disk)
- Verify: Sample 10 images on a page. Each should be under 100KB (body) or 250KB (hero). · Right-click any image → "Open in new tab" → check the actual width/height in pixels.
- [ ] **52.** Add responsive srcset + sizes so mobile downloads small variants
- How: Generate multiple sizes of each image: 400w, 800w, 1200w, 1600w (and 2000w for hero). · Use the srcset attribute on : · The sizes attribute tells the browser how wide the image will be displayed at different viewport widths. The browser picks the closest source. · For WordPress, this is automatic since 4.4. For custom sites, implement it manually or use a library.
- Avoid: Using srcset without sizes (browser assumes 100vw, downloads the largest) · Wrong sizes values (claiming 50vw when it's really 25vw downloads an unnecessarily large image) · Not generating enough size variants (a 4-size srcset is usually enough)
- Verify: On a 360px phone, open a page with images. The image request should be the 400w or 800w variant, not the 1600w. · DevTools → Network → check the actual size downloaded for each image.
- [ ] **53.** Set explicit width/height (or CSS aspect-ratio) on every image, iframe, and ad slot to kill CLS
- How: For images: include width and height in the HTML: · For iframes: same.