seo-meta-onpage (M7)
SkillSearchAudit and generate the document head — title (50-60 chars), meta description (150-160 chars), viewport, charset, <html lang>, and robots-meta sanity — flagging missing/duplicate/over-long values and producing length-bounded, keyword-aware replacements. Module M7. Feeds the Search SEO score.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the seo-meta-onpage (M7) skill
What this skill tells your AI
The instructions your AI receives, as published by hainrixz/claude-seo-ai in skills/seo-meta-onpage/SKILL.md and read by ahel’s review.
Head hygiene is cheap, deterministic, and the first thing every crawler reads. Reference: references/schema-tier1.md for the structured-data layer that sits alongside the head (owned by M5).
Inputs
Work from the PageSnapshot named in your dispatch envelope: read parsed from <run_dir>/pages/<slug>.json (title, metas[], robots_meta[], canonicals[], word_count) plus headers (content-language, x-robots-tag); Grep pages/<slug>.html for verbatim evidence; site artifacts live in <run_dir>/site/{robots.json,sitemaps.json,discovery.json}. Deterministic findings already emitted by audit.mjs are listed in <run_dir>/findings.deterministic.json — do not re-emit those ids; add model-judged findings only. If invoked directly with a URL/path and no snapshot exists, first run node "${CLAUDE_PLUGIN_ROOT}/scripts/snapshot.mjs" <target> --out "${CLAUDE_PLUGIN_DATA}/runs" and use the printed snapshot path.
Audits
Working from the PageSnapshot (parsed_rendered when render.used is not none, else parsed):
- Title — exactly one
<title>. Length is judged against the shared bands inscripts/lib/bands.mjs(the single source;parse-html.mjs, the on-page checks and this skill all read it): pass 30-60 characters, recommended 50-60. Inside 50-60 the verdict isrecommended; inside 30-60 but outside that,pass; below 30short, above 60long— both arewarn, neverfail. Fail only when the title is missing or empty, or duplicated site-wide across distinct URLs. Not all caps; not a pure keyword list. - Meta description — present, judged against the same shared bands: pass 70-160 characters, recommended 150-160. Outside the pass band is
warn, neverfail— Google rewrites descriptions freely. Not duplicated site-wide; describes this page, not boilerplate. - Viewport —
<meta name="viewport" content="width=device-width, initial-scale=1">present (fail if missing — mobile rendering/indexing depends on it). - Charset —
<meta charset="utf-8">present and in the first 1024 bytes of<head>. - Language —
<html lang="…">present and a valid BCP-47 tag. - Robots meta — sanity-check
<meta name="robots">: flag an accidentalnoindex/nofollowon a page meant to rank. Full indexability/canonical logic (including the canonical tag) is owned by seo-indexability (M2) — here only note presence of<link rel="canonical">, do not adjudicate it.
Fixes
- AUTO — add missing
viewport,charset(utf-8), and<html lang>(inferred fromContent-Languageheader or page text; leave a TODO if ambiguous). These are deterministic, additive, verifiable writes → diff forfix. - PROPOSED — generate/trim a
<title>andmeta descriptionfrom<h1>, lead paragraph, and primary topic: length-bounded, keyword-aware but not stuffed, brand suffix only if the site uses one. Each is a draft requiring per-item accept (humans own messaging). - ADVISORY — removing an intentional
noindexis never auto-written; surface it and let the user decide. - Never fabricate a title, description, or
lang— when content is too thin to derive a value, leave a clearly-markedTODO:placeholder for the user to fill.
Verification
- Method
dom_assert:node "${CLAUDE_PLUGIN_ROOT}/scripts/parse-html.mjs" --snapshot <pages/<slug>.json>(or--url <u>/--file <path>) — returnstitle(value/length/ok),meta_description(value/length/ok),robots_meta,canonical, andhead_hygiene(viewport/charset/lang) so each head element's presence and length is asserted directly against the snapshot. - Duplicate-title/description checks need the full crawl set; when only a single page is available the cross-URL assertion is reported
needs_api, never a falsepass.
Findings
Findings conform to schema/finding.schema.json. Severity policy: 5 is reserved for catastrophic, eligibility-killing facts at site/template scope; 4 major · 3 moderate · 2 minor · 1 cosmetic · 0 informational — and only an established severity-5 fail in an active category can cap a score (references/scoring-model.md). Head hygiene never reaches 5. Examples:
M7.title.missing— no<title>(fail, severity 3,fixable: proposed, axissearch, confidenceestablished).M7.title.length_out_of_band— title 78 chars, outside the 30-60 pass band and likely truncated in the SERP (warn, severity 3,fixable: proposed, axissearch, confidencedirectional). Quote the length and the band that produced the verdict.M7.description.length_out_of_band— description outside the 70-160 pass band (warn, severity 2,fixable: proposed, axissearch, confidencedirectional).M7.viewport.missing— no responsive viewport (fail, severity 3,fixable: auto, axissearch, confidenceestablished).M7.title.duplicate_across_urls— the same<title>is served from several sampled URLs (warn, severity 3,fixable: proposed, axissearch, confidencedirectional). One finding per duplicate cluster, quoting the shared title and the URLs.M7.description.duplicate_across_urls— the same meta description is served from several sampled URLs (warn, severity 2,fixable: proposed, axissearch, confidencedirectional). Both duplicate checks are cross-URL: with fewer than two content pages in the run they are reported once asM7.title.duplicate_across_urlswith statusneeds_api(severity 0, unscored), never as a pass. Platform-conditional ids. This module also emits 6 ids that fire only whenprofile.jsonnames the platform (shopify,wordpress,nextjs,gatsby,jekyll,react-router). They are indexed inreferences/routing.md§ Platform-conditional finding ids and specified inreferences/platforms/<id>.md§10.
Each finding: evidence.observed quotes the head verbatim (e.g. the exact title string + its length); verification.reproduce is the runnable parse-html.mjs command above; expected_impact is banded + confidence-tagged (no naked %).
Honesty
- The "meta keywords" tag is dead — Google ignores it; do not add or recommend it.
- Title/description length bands are SERP-display heuristics (Google truncates on pixel width, not characters), not ranking factors — out-of-band values are
warn, neverfail, and impact isdirectionalat most. The pass band is deliberately wider than the recommended band so a 35-character title is not reported as a defect; both bands come fromscripts/lib/bands.mjsso the script, the report, and this skill can never disagree. - Google frequently rewrites the displayed title/description from on-page content; a perfect tag is not guaranteed to appear, so never promise a SERP snippet as a ranking gain.
Signals
- GitHub stars
- 59
- Forks
- 5
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
seo-meta-onpage- Source
- github.com/hainrixz/claude-seo-ai