@portablecore/seo-audit
v0.1.0
Published
Platform-agnostic SEO and AEO auditing: polite sitemap-seeded crawler, structured page observations, and severity-ranked findings written for non-technical readers
Maintainers
Readme
@portablecore/seo-audit
Platform-agnostic SEO and AEO auditing. Crawls a site from what it advertises about itself, records structured observations, and turns those into severity-ranked findings written for a non-technical reader.
Companion to @portablecore/seo. That package is how a Portable property gets SEO right by construction. This one is how any property, including ones we do not control, gets measured.
Usage
npx seo-audit https://example.org
npx seo-audit https://example.org --out data --delay 3000 --cooldown 180000
npx seo-audit --analyze data/crawl.jsonimport { auditSite, renderTextReport } from "@portablecore/seo-audit";
const report = await auditSite("https://example.org");
console.log(renderTextReport(report));Crawling and analysis are separable on purpose. A polite pass over a rate-limited site takes twenty minutes, so the raw observations are written to crawl.json and the rules can be re-run against them for free with --analyze.
Design notes
Only crawls what the site advertises. Discovery reads robots.txt, follows whatever Sitemap: it declares, and falls back to the conventional locations. Following links instead would measure something other than what the site claims about itself.
Refuses to report on a contaminated crawl. A bot-check interstitial returns HTTP 200 and carries its own noindex. Fed to the rules unrecognized, it manufactures a critical finding that the entire site is deindexed. analyzeCrawl throws ContaminatedCrawlError when interstitials reached the page set or when coverage falls below 90%, rather than publishing confident nonsense.
This is not hypothetical. It is what happened auditing a Cloudflare-fronted WordPress site in July 2026, and the first pass produced 59 false criticals before detection was added.
Polite by default. Three seconds between requests, three-minute cooldown on a challenge, retrying the same URL rather than moving on. A crawl is a favor the site is doing you: being slow costs minutes, being rude costs a penalty window that invalidates the whole run.
Remedies are platform-aware. "Add Person schema" is a plugin setting on WordPress, a template edit in Next.js, and sometimes not possible at all on a hosted builder. The crawler detects the stack and the rules tailor the fix text accordingly.
Zero runtime dependencies. HTML extraction is anchored regex rather than a DOM parser, so the package runs anywhere without a lockfile negotiation.
What it checks
| Concern | Rules |
| --- | --- |
| Crawlability | site-wide disallow, high Crawl-delay, blocked AI crawlers |
| Indexability | noindex in sitemap, canonical mismatch, non-200 and redirecting sitemap URLs |
| Metadata | missing or duplicate H1, brand repeated in title, over-long titles, missing descriptions, duplicate titles, missing lang |
| Structured data and AEO | missing Organization schema, missing Person schema, missing Event schema, unparseable JSON-LD, missing llms.txt |
| Content | archive bloat, thin pages |
| Performance | heavy HTML, missing alt attributes, slow responses |
Rule titles are noun phrases rather than sentences so they read correctly at any count, including one. The output is meant to be pasted into a document a client will read.
Configuration
await auditSite("https://example.org", {
config: {
brand: "Example School", // inferred from title templates when omitted
peopleSections: ["directory"], // sections wanting Person schema
archiveSections: ["category"], // sections that should not be in a sitemap
eventSections: ["events"], // sections wanting Event schema
thinWordCount: 300,
},
fetch: { delayMs: 3000, cooldownMs: 180_000 },
analyze: { minCoverage: 0.9 },
});Sections come from the sitemap-index child that listed a URL (page-sitemap.xml becomes page), falling back to the first path segment for flat sitemaps.
