# SEO Validation Guide
**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)
---
## Core Principle
**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.
---
## When to Run SEO Validation
| 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 |
---
## Reference Documents
Before running validation, gather:
1. **Page specification** — SEO & Search section (keywords, URL, headings, meta)
2. **Project brief** — SEO Strategy section (page-keyword map, structured data plan)
3. **SEO Strategy Guide** — `../../data/agent-guides/saga/seo-strategy-guide.md`
---
## SEO Validation Checklist
### Level 1: Critical (Must Pass)
These are the top errors found in real-world SEO audits. Failing any of these blocks approval.
#### 1.1 Page Title Tag
```
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"
```
#### 1.2 Meta Description
```
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"
```
#### 1.3 H1 Heading
```
Verify:
- Exactly ONE
tag on the page
- Contains primary keyword (natural, not stuffed)
- Is visible (not hidden)
- Matches specification
Report:
"Found 1
: 'Bilservice och reparationer på Öland'
— contains keyword 'bilservice'. ✓ Passes"
"Found 0
tags. ✗ Fails — 75% of audited sites have H1 issues"
"Found 3
tags. ✗ Fails — only one H1 allowed per page"
```
#### 1.4 Heading Hierarchy
```
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"
```
#### 1.5 Image Alt Text
```
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."
```
---
### Level 2: Important (Should Pass)
#### 2.1 Open Graph / Social Sharing
```
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"
```
#### 2.2 Structured Data (Schema.org)
```
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"
```
#### 2.3 Internal Links
```
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."
```
#### 2.4 URL / Slug
```
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"
```
#### 2.5 Canonical URL
```
Verify:
- tag present
- Points to the correct URL (self-referencing)
- One canonical per page
Report:
"Canonical: ✓"
"Canonical tag missing. ✗ Fails"
```
---
### Level 3: Technical (Verify on Deployment)
These checks apply to the deployed/preview site, not the prototype.
#### 3.1 Performance
```
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)"
```
#### 3.2 robots.txt
```
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 ✓"
```
#### 3.3 XML Sitemap
```
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 ✓"
```
#### 3.4 hreflang Tags (Multilingual)
```
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"
```
#### 3.5 Security Headers
```
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."
```
#### 3.6 Favicon
```
Verify:
- Favicon exists (check )
- Multiple sizes available (16x16, 32x32, 180x180)
Report:
"Favicon: present ✓
16x16: ✓
32x32: ✓
apple-touch-icon (180x180): ✓"
```
---
## Verification with Puppeteer
### Automated SEO Check Script Pattern
```javascript
// 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}`);
```
---
## Narration Pattern
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.
```
---
## Integration with Phase 5 Flow
```
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
```
### Story File Addition
Add SEO criteria to the story file's Agent-Verifiable section:
```markdown
### 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] |
```
---
## Integration with Acceptance Testing
When creating test scenarios (Phase 4 [H] Handover / Phase 5 [T] Acceptance Testing), include SEO as a test category:
```yaml
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'
```
---
## Anti-Patterns
- **Never skip SEO validation on public pages** — It's not optional
- **Never approve a page with missing alt text** — 85% of real sites fail this
- **Never use "click here" or "read more" as link text** — Describe the destination
- **Never have more than one H1** — One per page, always
- **Never deploy without meta description** — 80% of sites miss this
- **Never assume SEO "can be added later"** — It's specification, not decoration
---
## Common Fixes (From 44 Real-World Audits)
| 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.
---
## Related Resources
- **Inline Testing Guide:** `INLINE-TESTING-GUIDE.md` — General Puppeteer verification
- **SEO Strategy Guide:** `../../data/agent-guides/saga/seo-strategy-guide.md` — SEO reference
- **SEO Content Instructions:** `../../4-ux-design/templates/instructions/seo-content.instructions.md` — Spec-level SEO
- **Specification Quality:** `../../data/agent-guides/freya/specification-quality.md` — Quality checklist
- **Meta Content Guide:** `../../data/agent-guides/freya/meta-content-guide.md` — Meta tag details
---
*SEO validation during development = zero SEO issues at launch. Validate as you build.*