Gate CI with pagegraph check
Fail CI on structural SEO violations with pagegraph check, and verify the server-rendered HTML of every declared page with the --site flag.
pagegraph check reads the declared graph and exits 1 when it finds a structural violation. This guide runs it in CI and extends it to rendered pages with --site. It assumes the pagegraph.config.ts from the quick start.
Run the declared-graph check
pagegraph check # CI gate — exit 1 on structural violations
Structural violations fail the command. Editorial findings are reported and do not fail it. A passing check without --site says nothing about served HTML.
Check rendered content
The declared graph cannot see what a page renders. Headings come from components, and the <title>, JSON-LD and visible text are composed at render time. pagegraph check --site <url> fetches every sitemap-eligible page, and every concrete page that declares noindex, from a running server. It checks the server-rendered HTML, which is what a crawler receives before any script runs. A server error or failed request is retried once, because a dev server's first render of a route can fail while it compiles.
pagegraph check --site http://localhost:3000 --allow-private
pagegraph check --site https://staging.example.com --json | jq '.violations'
The check rules reference lists every rendered-content rule and its severity.
Google requires structured data to describe content the page visibly shows. A FAQ answer that exists only in JSON-LD or in hydration data, such as an accordion that mounts answers on open, is a structural violation. Comparisons ignore punctuation, case and markup. Prices match as numbers: 19 matches $19/mo and 19.00, not $199. Canonicals compare by path. Robots are read from the meta tag only, so a local or preview host that sends X-Robots-Tag: noindex everywhere can still run the check.
Set word-count floors
Word-count floors are project policy, declared per path glob. A page that matches several rules must meet the highest.
export default defineSeoConfig({
// ...
content: {
minWords: [
{ path: "/blog/*", minWords: 500 },
{ path: "/compare/*", minWords: 800 },
],
},
});
Add a freshness policy
Declare a freshness policy to turn old pages into editorial stale-page findings, and list the refresh queue with pagegraph stale:
export default defineSeoConfig({
// ...
freshness: { maxAgeDays: 90 },
});
pagegraph stale # stale pages, oldest first; undated pages counted
pagegraph stale --max-age-days 180 --json
Only sitemap-eligible pages are judged. A page without a date is reported as undated rather than stale. Give content its modifiedAt, or a route its staticData.seo.modifiedAt, to bring it into the report and the sitemap's <lastmod>.
Test the same checks
Everything the CLI checks is a pure function you can call from a test:
import {
checkGraph,
checkPageContent,
extractPageContent,
hasStructuralViolations,
inspectHtml,
} from "pagegraph";
import { evaluateAppGraph } from "pagegraph/tanstack-start";
// the graph the plugin ships, evaluated through your Vite config
const { graph } = await evaluateAppGraph({ root: appRoot, mode: "production" });
expect(hasStructuralViolations(checkGraph(graph))).toBe(false);
// render a page however you like, then assert the head it actually ships
const report = inspectHtml(url, 200, html);
expect(report.issues).toEqual([]);
// …and the content it ships: one H1, visible FAQ answers, shown prices
expect(checkPageContent(extractPageContent(html))).toEqual([]);
pagegraph audit also runs the heading and structured-data rules on every audited HTML page. The robots, canonical and word-count rules need the declared graph, so they stay in check. See Audit any website.
Read next
- Check rulesEvery PageGraph check rule by name and severity, covering graph rules, rendered-content rules, Open Graph images, robots conflicts and stale pages.
- CLI commandsEvery pagegraph CLI command with a one-line description, the agentic workflow command table, and the stdout and stderr convention.