@misar/seo-guards (0.1.0)
Installation
@misar:registry=npm install @misar/seo-guards@0.1.0"@misar/seo-guards": "0.1.0"About this package
@misar/seo-guards
Advisory-first SEO / AEO / GEO organic-readiness validators for the Misar AI
monorepo. Zero runtime dependencies (plain Node.js ESM, Node built-ins only).
Same config schema, guard shape, CLI contract and Forgejo-annotation output as
@misar/guards — this is the organic-growth sibling of the
convention linter.
It validates the substrate that lets search engines and AI answer engines
(Google AI Overview / Gemini, Perplexity, ChatGPT search, Bing) index and cite
Misar pages. It does not write content, earn authority, or enforce
per-request bot behavior — see docs/DEVOPS/organic-readiness.md for the honest
3-layer model and what CI does / does not guarantee.
Install / run
Runs via plain node — no install step required in CI:
# from a repo root that has a seo-guards.config.json
pnpm dlx @misar/seo-guards --advisory
# or, in the monorepo
node packages/seo-guards/bin/misar-seo-guards.mjs --advisory
CLI
misar-seo-guards [options]
--advisory Emit ::warning annotations and ALWAYS exit 0 (default posture).
--blocking Force blocking mode even if the config is advisory.
--only=<ids> Run only these guard ids (comma-separated).
--json Machine-readable JSON ({ ran, counts, advisory, findings }).
--config=<path> Path to a config file (default: ./seo-guards.config.json).
--list List all guard ids and exit.
-h, --help Show help.
Exit: 0 no findings (or advisory) · 1 blocking findings · 2 usage/config error.
Advisory-first: unlike @misar/guards, advisory defaults to true here
(absent config → advisory). Findings default to warning severity. A repo only
starts failing CI on SEO regressions after it deliberately sets "advisory": false
(or passes --blocking). This is the advisory → blocking ramp.
Config — seo-guards.config.json
Identical schema to @misar/guards:
{
"advisory": true,
"ignore": ["packages/seo-guards", "apps/template"],
"guards": {
"json-ld-presence": true,
"json-ld-validity": true,
"metadata-completeness": { "multiLocale": true },
"hreflang-correctness": true,
"answer-block-extractability": { "answerMaxWords": 80, "wallWords": 400 },
"robots-sitemap-llms-drift": {
"appSitemap": "src/app/sitemap.ts",
"appRobots": "src/app/robots.ts",
"landingRobots": "src/landing/src/app/robots.txt/route.ts"
}
}
}
- A guard listed
true/{opts}is enabled;falsedisables it. Not listed → on iff default-on (all six SEO guards are default-on). ignoreentries are repo-relative path prefixes (also substring-matched).- See
seo-guards.config.example.json.
Guards
| id | what it flags (all advisory) |
|---|---|
| json-ld-presence | App root emits no WebSite/Organization JSON-LD anywhere; a content route (blog/article/post/docs/faq/[slug]) renders a page but references no structured data. Import of a shared <StructuredData/> counts as presence. |
| json-ld-validity | Statically-parseable <script type="application/ld+json"> blocks that fail JSON.parse (error severity), declare a @type missing its schema.org required props, or use a deprecated @type. JSON.stringify(var) blocks are skipped (not statically knowable). |
| metadata-completeness | A declared Next.js metadata / generateMetadata scope with title but no description; openGraph/twitter present without the other (card parity); page-level metadata with no alternates.canonical; multi-locale apps missing alternates.languages. Only fires on files that already declare metadata (inheritance-safe). |
| hreflang-correctness | alternates.languages maps with invalid BCP-47 codes, no x-default, or (when the app's locale set is statically known and the map is literal) omitted declared locales. Dynamically-generated maps are validated only for their literal keys. |
| answer-block-extractability | AEO/GEO heuristic on published content (.mdx; .md under /content/ or /articles/): a question-form heading not followed by an answer-first paragraph, a hedged opener, or a wall-of-text with zero question headings. |
| robots-sitemap-llms-drift | Ports MisarReach's sitemap/robots drift check and extends it: llms.txt/llms-full.txt presence, robots ↔ sitemap ↔ llms cross-reference, and host agreement across the three. Each sub-check is a no-op if its inputs are absent. |
The spec behind the AEO/GEO guards is captured in the geo-aio-citation-surfaces
and paa-people-also-ask notes: the citation unit is a passage (question
heading + concise answer-first block), WebSite.name drives the AI-Overview
site-name chip, and Organization anchors the brand entity.
Design guarantees
- Low false positives. Guards trigger on structure (headings, declared metadata scopes, parseable JSON-LD), never on prose quality, and each check no-ops when its inputs are absent.
- Zero dependencies. Runs under any Node ≥18 with no install.
- Additive. Never modifies app behavior; it only reads and reports.
Tests
node tests/test.mjs # fixtures written to a temp dir at runtime, never committed