For: WDS Agents performing Agentic Development Purpose: Verify SEO implementation against specification before presenting to user When: After a public page is built and previewable (browser or deployed)
Every public page must pass SEO validation before approval.
The agent verifies all measurable SEO criteria using browser tools (Puppeteer, MCP browser-tools, or manual inspection). SEO failures caught during development cost minutes to fix. SEO failures caught after deployment cost rankings and traffic.
| Trigger | Action |
|---|---|
| Public page section complete (4c/4d) | Run SEO checks before presenting |
| Full page implementation complete | Run complete SEO audit |
| Pre-deployment review | Full validation against spec + project brief |
| Post-deployment verification | Validate live URL matches specification |
Before running validation, gather:
../../data/agent-guides/saga/seo-strategy-guide.mdThese are the top errors found in real-world SEO audits. Failing any of these blocks approval.
Verify:
- Title tag exists and is not empty
- Length ≤ 60 characters (check each language)
- Contains primary keyword
- Contains brand name
- Is unique (different from other pages)
- Matches specification
Report:
"Page title is 'Bilservice på Öland | Källa Fordonservice' (51 chars)
— contains keyword 'bilservice', includes brand. ✓ Passes"
"Page title is 'Home' (4 chars)
— too short, no keyword, no brand. ✗ Fails"
Verify:
- Meta description tag exists and is not empty
- Length 150-160 characters
- Contains primary keyword
- Contains call-to-action
- Matches specification
Report:
"Meta description is 156 chars, contains 'bilservice Öland', ends with
'Ring oss idag!' ✓ Passes"
"Meta description is missing. ✗ Fails — 80% of audited sites miss this"
Verify:
- Exactly ONE <h1> tag on the page
- Contains primary keyword (natural, not stuffed)
- Is visible (not hidden)
- Matches specification
Report:
"Found 1 <h1>: 'Bilservice och reparationer på Öland'
— contains keyword 'bilservice'. ✓ Passes"
"Found 0 <h1> tags. ✗ Fails — 75% of audited sites have H1 issues"
"Found 3 <h1> tags. ✗ Fails — only one H1 allowed per page"
Verify:
- Headings follow logical order (H1 → H2 → H3)
- No skipped levels (H1 → H3 without H2)
- No duplicate H1
Report:
"Heading hierarchy: H1 → H2 → H3 → H2 → H3 ✓ Logical flow"
"Heading hierarchy: H1 → H3 (skipped H2) ✗ Fix: Change H3 to H2"
Verify:
- ALL images have alt attributes
- Alt text is descriptive (not empty, not "image")
- Alt text exists in all required languages
- Decorative images have alt="" (empty, not missing)
Report:
"Found 8 images:
hero-image: alt='Källa Fordonservice verkstad...' ✓
service-ac: alt='AC-service på personbil' ✓
icon-phone: alt='' (decorative) ✓
team-photo: alt attribute MISSING ✗
Result: 7/8 images pass. 1 missing alt text."
Verify:
- og:title tag present
- og:description tag present
- og:image tag present (valid URL, image exists)
- og:type tag present
- twitter:card tag present
Report:
"Social sharing tags:
og:title: 'Bilservice Öland — Källa Fordonservice' ✓
og:description: present (148 chars) ✓
og:image: '/images/social/hem-social.jpg' ✓ (file exists)
og:type: 'website' ✓
twitter:card: 'summary_large_image' ✓
All social tags present."
"Missing: og:image ✗ — 70% of audited sites miss social tags"
Verify:
- JSON-LD script tag exists
- Schema type matches project brief plan
- Required properties present (name, address, phone for LocalBusiness)
- JSON is valid (parseable)
Report:
"Structured data found:
@type: 'AutoRepair' ✓
name: 'Källa Fordonservice' ✓
address: complete ✓
telephone: '+46485-27070' ✓
openingHours: present ✓
JSON-LD validates. ✓ Passes"
"No structured data found. ✗ Fails — spec requires LocalBusiness schema"
Verify:
- Page has at least 2 internal links to other pages
- Links have descriptive anchor text (not "click here", "read more")
- No broken internal links (404s)
- No redirect chains (link → 301 → 301 → page)
Report:
"Internal links found: 5
'Läs mer om AC-service' → /ac-service ✓ Descriptive
'Ring oss' → tel:+46485-27070 ✓ CTA
'Klicka här' → /kontakt ✗ Non-descriptive anchor text
Result: 4/5 links pass."
Verify:
- URL slug matches specification
- Slug is lowercase
- Uses hyphens (not underscores or spaces)
- No special characters (ä, ö, å)
- Keyword present in slug
Report:
"URL slug: /ac-service ✓ Matches spec, lowercase, keyword present"
"URL slug: /Sida?id=42 ✗ Not descriptive, no keyword"
Verify:
- <link rel="canonical"> tag present
- Points to the correct URL (self-referencing)
- One canonical per page
Report:
"Canonical: <link rel='canonical' href='https://kallafordon.se/ac-service'> ✓"
"Canonical tag missing. ✗ Fails"
These checks apply to the deployed/preview site, not the prototype.
Verify:
- Total page weight < 3MB
- Largest image < 400KB (hero) / < 200KB (other)
- Time to First Byte (TTFB) < 1.5s
- No uncompressed images (should be WebP or compressed JPEG)
Report:
"Page weight: 1.8MB ✓ (target < 3MB)
hero.jpg: 380KB ✓ (target < 400KB)
team.jpg: 1.2MB ✗ (target < 200KB — compress!)
icon.svg: 3KB ✓
TTFB: 0.8s ✓ (target < 1.5s)"
Verify:
- robots.txt exists (not 404)
- Allows crawling of public pages
- References sitemap
- Blocks admin/private pages
Report:
"robots.txt: exists ✓
Sitemap reference: present ✓
Public pages: allowed ✓
/wp-admin/: blocked ✓"
Verify:
- Sitemap exists at /sitemap.xml (or referenced location)
- Contains all public pages
- All URLs return 200 (no broken links)
- Includes all language versions (if multilingual)
Report:
"Sitemap: 32 URLs, all return 200 ✓
Includes /en/ versions ✓
Includes /de/ versions ✓"
Verify:
- Each page declares all language alternates
- x-default points to primary language
- Tags are reciprocal (EN page links to SE, SE page links to EN)
Report:
"hreflang tags on /ac-service:
sv: /ac-service ✓
en: /en/ac-service ✓
de: /de/ac-service ✓
x-default: /ac-service ✓
All reciprocal. ✓ Passes"
Verify:
- HSTS present
- X-Content-Type-Options present
- X-Frame-Options present
- Referrer-Policy present
Report:
"Security headers: 2/6 present ✗
HSTS: missing
CSP: missing
X-Content-Type-Options: 'nosniff' ✓
X-Frame-Options: 'DENY' ✓
Referrer-Policy: missing
Permissions-Policy: missing
Note: 95% of audited sites fail security headers."
Verify:
- Favicon exists (check <link rel="icon">)
- Multiple sizes available (16x16, 32x32, 180x180)
Report:
"Favicon: present ✓
16x16: ✓
32x32: ✓
apple-touch-icon (180x180): ✓"
// Navigate to page
await page.goto(pageUrl, { waitUntil: 'networkidle0' });
// 1. Title tag
const title = await page.title();
console.log(`Title: "${title}" (${title.length} chars)`);
// 2. Meta description
const metaDesc = await page.$eval(
'meta[name="description"]',
el => el.content
).catch(() => null);
console.log(`Meta desc: "${metaDesc}" (${metaDesc?.length || 0} chars)`);
// 3. H1 count and content
const h1s = await page.$$eval('h1', els => els.map(el => el.textContent.trim()));
console.log(`H1 tags: ${h1s.length} — "${h1s.join('", "')}"`);
// 4. Heading hierarchy
const headings = await page.$$eval('h1,h2,h3,h4,h5,h6', els =>
els.map(el => ({ tag: el.tagName, text: el.textContent.trim().substring(0, 50) }))
);
console.log('Heading hierarchy:', headings.map(h => h.tag).join(' → '));
// 5. Images without alt
const imagesNoAlt = await page.$$eval('img', els =>
els.filter(el => !el.hasAttribute('alt')).map(el => el.src)
);
console.log(`Images without alt: ${imagesNoAlt.length}`);
// 6. Open Graph tags
const ogTags = await page.$$eval('meta[property^="og:"]', els =>
els.map(el => ({ property: el.getAttribute('property'), content: el.content }))
);
console.log(`OG tags: ${ogTags.length}`, ogTags);
// 7. Structured data
const jsonLd = await page.$$eval('script[type="application/ld+json"]', els =>
els.map(el => JSON.parse(el.textContent))
).catch(() => []);
console.log(`Structured data: ${jsonLd.length} blocks`, jsonLd.map(j => j['@type']));
// 8. Canonical
const canonical = await page.$eval('link[rel="canonical"]', el => el.href).catch(() => null);
console.log(`Canonical: ${canonical || 'MISSING'}`);
// 9. Internal links
const links = await page.$$eval('a[href]', els =>
els.filter(el => el.href.startsWith(window.location.origin))
.map(el => ({ text: el.textContent.trim().substring(0, 40), href: el.href }))
);
console.log(`Internal links: ${links.length}`);
Group results by severity and narrate clearly:
## SEO Validation Report: {Page Name}
### Critical ✓/✗
Title tag: "Bilservice Öland | Källa Fordonservice" (51 chars) ✓
Meta description: "Komplett bilverkstad..." (156 chars) ✓
H1: 1 found — "Bilservice och reparationer på Öland" ✓
Heading hierarchy: H1 → H2 → H3 → H2 → H3 ✓
Image alt text: 7/8 images have alt ✗ (team-photo missing)
### Important ✓/✗
Open Graph: 5/5 tags present ✓
Structured data: AutoRepair schema valid ✓
Internal links: 5 found, 4/5 descriptive ✗ (1 "Klicka här")
URL slug: /ac-service ✓
Canonical: present, self-referencing ✓
### Technical (deployment only)
Page weight: 1.8MB ✓
Image sizes: 1 oversized (team.jpg 1.2MB) ✗
Security headers: 2/6 ✗
### Summary
Critical: 4/5 pass
Important: 4/5 pass
Technical: 1/3 pass
Action needed: Fix 1 missing alt text, 1 non-descriptive link,
1 oversized image, 4 security headers.
4a: Announce & Gather
4b: Create Story File
4c: Implement Section
↓
Agent runs Puppeteer verification (INLINE-TESTING-GUIDE)
Agent runs SEO validation (THIS GUIDE) — for public pages only
↓
All pass? ── No ──→ Agent fixes, re-verifies (loop)
│
Yes
↓
4d: Present for Testing
Add SEO criteria to the story file's Agent-Verifiable section:
### SEO Criteria (Public Pages)
| # | Criterion | Expected | How to Verify |
|---|-----------|----------|---------------|
| S1 | Title tag | "Bilservice Öland \| Källa" ≤60 chars | Read document.title |
| S2 | Meta description | 150-160 chars, keyword present | Read meta[name=description] |
| S3 | H1 count | Exactly 1 | Count h1 elements |
| S4 | H1 keyword | Contains "bilservice" | Read h1 textContent |
| S5 | Heading hierarchy | H1→H2→H3, no skips | Scan all headings |
| S6 | Image alt coverage | 100% images have alt | Check img elements |
| S7 | OG tags | og:title, og:description, og:image | Check meta[property^=og:] |
| S8 | Internal links | ≥ 2, descriptive text | Count and check a[href] |
When creating test scenarios (Phase 4 [H] Handover / Phase 5 [T] Acceptance Testing), include SEO as a test category:
seo_checks:
- id: 'SEO-001'
name: 'Page title correct'
verify:
- 'Title tag matches specification'
- 'Title ≤ 60 characters'
- 'Contains primary keyword'
- id: 'SEO-002'
name: 'Meta description correct'
verify:
- 'Meta description matches specification'
- 'Length 150-160 characters'
- 'Contains CTA'
- id: 'SEO-003'
name: 'Heading structure valid'
verify:
- 'Exactly one H1'
- 'No skipped heading levels'
- id: 'SEO-004'
name: 'Image alt text complete'
verify:
- 'All content images have alt text'
- 'Alt text in correct language'
- id: 'SEO-005'
name: 'Structured data valid'
verify:
- 'JSON-LD present and parseable'
- 'Schema type matches plan'
- 'Required properties present'
| Issue | Frequency | Fix Time | Fix |
|---|---|---|---|
| Missing alt text | 85% | 1 min/image | Add descriptive alt attribute |
| Missing meta description | 80% | 2 min/page | Add meta tag from spec |
| H1 missing or wrong | 75% | 1 min | Add/fix h1 tag |
| Missing OG tags | 70% | 3 min/page | Add og: meta tags from spec |
| Missing structured data | 65% | 5 min/page | Add JSON-LD script |
| Oversized images | 65% | 2 min/image | Compress + convert to WebP |
| Non-descriptive links | 30% | 1 min/link | Rewrite anchor text |
| Missing canonical | 40% | 1 min | Add link rel=canonical |
Total estimated fix time for a typical page: 15-20 minutes These are all preventable by validating during development.
INLINE-TESTING-GUIDE.md — General Puppeteer verification../../data/agent-guides/saga/seo-strategy-guide.md — SEO reference../../wds-4-ux-design/templates/instructions/seo-content.instructions.md — Spec-level SEO../../data/agent-guides/freya/specification-quality.md — Quality checklist../../data/agent-guides/freya/meta-content-guide.md — Meta tag detailsSEO validation during development = zero SEO issues at launch. Validate as you build.