Repository navigation
Roadmap
Status: pre-alpha. Dates are not promised. Each milestone ships when its acceptance criteria are met. Tracked in GitHub milestones, issues and the project board.
Scope freeze (firm, 2026-10-14): no new features are added to any milestone before v0.1 ships. v0.1 is intentionally small so it can ship fast. Issues #57–#61 are the last additions before v0.1; any further feature request goes to Considered, not scheduled below or to a discussion, and is triaged only after
0.1.0is published. Bug fixes, docs corrections and v0.1 acceptance criteria are not features.Amendment (2026-10-14): the maintainer approved the SEO/AEO review. Its additions are scheduled in v0.2–v0.5 (issues #45–#56). The only v0.1 change is the API shape of the AI-crawler option in #11 (plus docs fixes in #10), because it must be right before it ships. See AEO stance and ADR 0012.
Amendment 2 (2026-10-14): the maintainer approved five features, scheduled after v0.1 with no change to v0.1: SEO preview panel (#57), redirect manager (#58), pagination and faceted URLs (#59) in v0.3; image SEO (#60) and
herald check --diff(#61) in v0.4. The scope freeze above applies from here on.
Constraints for every milestone: zero dependencies beyond the next and react peers (ADR 0002, #3); no request APIs in library code (ADR 0006, #7); Next.js 16 App Router only (ADR 0003).
Goal: Ship a small, useful alpha fast: everything a typical site needs in layout.tsx, page.tsx, sitemap.ts and robots.ts, with the zero-dependency and Cache Components guarantees enforced in CI from day one.
Scope: Config, metadata builders, <JsonLd> + 7 types, basic sitemap/robots, CI with dependency budget and canary. Small on purpose.
Issues and acceptance criteria
-
#1 Repo scaffolding: pnpm workspace, TypeScript, Vitest, lint, CI, changesets
-
packages/next-heraldwith ESM build,exportsmap for every entry point in docs/README.md,sideEffects: false. - CI runs lint, typecheck, unit tests on Node 20.9 and 22 LTS.
- Changesets configured; npm provenance planned for publish (no publish in this issue).
-
-
#2 defineHerald config with validation
-
siteUrlvalidated (absolute, https or http://localhost, no path/query/hash). -
title.templatemust contain%s; errors name the option path. -
indexing: 'auto'resolves fromHERALD_ENV/VERCEL_ENVat build time. - Config object is frozen; unit tests cover every option.
-
-
#3 CI dependency budget check: no deps beyond next/react peers
- CI job fails if the published package.json has any
dependencies,optionalDependenciesorbundleDependencies, orpeerDependenciesother thannextandreact. - The check runs on the packed tarball (
npm pack), not just the workspace file. - A test asserts source files import only
node:built-ins,next,next/*,reactand package-internal modules. - Failing output names the offending dependency and links the ADR.
- CI job fails if the published package.json has any
-
#4 Supported Next versions matrix and nightly next@canary job
- E2E runs on latest Next 16 minor and the oldest supported minor, Turbopack (default) and
--webpack. - Nightly canary job opens/updates an issue labelled
upstreamon failure. - Peer range
next >=16.0.0 <17,react >=19documented and tested.
- E2E runs on latest Next 16 minor and the oldest supported minor, Turbopack (default) and
-
#5 Metadata builders: createMetadata and pageMetadata
-
createMetadatasetsmetadataBase, title template, OG/Twitter defaults androbotsfromindexing. -
pageMetadatasets absolute canonical andog:urlfrompath, normalizes trailing slash, copies title/description into OG and Twitter. - Type tests:
pathrequired;type: 'article'enables article fields. - Rendered via a real Next app in e2e: tags match expectations.
-
-
#6 mergeMetadata: deep merge of openGraph, twitter, robots, alternates
- Deep merges
openGraph,twitter,robots,alternates,other; arrays replaced. -
undefinednever overrides a defined value;nullexplicitly clears. - Unit tests for every merge case in the docs table.
- Deep merges
-
#7 Cache Components safety: request-API guard and cacheComponents example
- CI fails if
src/importsheaders,cookies,connectionordraftModefromnext/headers/next/server. -
examples/cache-components(cacheComponents: true) builds; every route is reported static/partially static in the build output. - Runtime entry points contain no
node:imports (Edge-compatible) — test.
- CI fails if
-
#8
<JsonLd>component and serializeJsonLd- Escapes
<,>,&, U+2028, U+2029;JSON.parse(output)deep-equals input. - Throws in dev on functions, symbols, bigint, cycles, non-finite numbers; strips
undefined. - Adds
@contextwhen missing; supports arrays and@graph;nonceprop. - Server Component; zero client JS (asserted in e2e).
- Escapes
-
#9 JSON-LD XSS corpus in CI
- Corpus includes
</script>case/whitespace variants,<!--,<![CDATA[, U+2028/2029, nested strings in keys. - For each payload, the rendered page's DOM contains exactly one script element and no injected nodes (Playwright).
- Corpus documented and extendable via PR.
- Corpus includes
-
#10 Schema types and builders batch 1
- Builders: organization, webSite (+searchAction), webPage, breadcrumbList (+breadcrumbsFromPath), article/blogPosting, product+offer+aggregateRating+review, faqPage.
- Google-required properties are required in types (type tests).
- schema-dts
WithContext<T>values are accepted by<JsonLd>(type test with schema-dts as devDependency only). - Each builder's output validated against fixtures captured from the Schema.org validator.
-
faqPagedocumented without a rich-result promise (FAQ rich results limited to government/health sites since 2023); no HowTo builder. -
article/organizationaccept{ '@id' }refs forauthor/publisher(forward-compatible with #49).
-
#11 defineSitemap (explicit routes) and defineRobots
- Returns native
MetadataRoute.Sitemap; absolute, deduped, sorted URLs;excludeglobs. - Dynamic patterns without a
paramsenumerator are a type error. -
defineRobotsadds sitemap URLs, locks down whenindexingis off, validates paths per RFC 9309. - Native
images/videosfields passed through and rendered correctly by Next (e2e). - AI crawlers split by purpose:
ai: { training, search?, userFetch? }replacesaiCrawlers;search/userFetchdefault'allow'; dated UA table with source per entry (Robots). - Robots docs say Google-Extended does not control AI Overviews/AI Mode; point to
nosnippet/max-snippet/noindex.
- Returns native
-
#12 examples/basic and e2e harness
-
examples/basicuses every v0.1 API. - Playwright e2e builds with
next buildand asserts head tags, JSON-LD,/sitemap.xml,/robots.txt. - Runs in CI under 5 minutes.
-
-
#13 Docs-as-tests: type-check every code sample in docs/
- Script extracts ts/tsx code blocks from
docs/and README and type-checks them against the package. - Blocks can opt out with an explicit
<!-- notest -->marker and reason. - Runs in CI; wiki mirror regenerated from
docs/.
- Script extracts ts/tsx code blocks from
Exit criteria: examples/basic and examples/cache-components pass e2e on the latest Next 16 and canary; dependency budget green; docs samples type-check. Then publish 0.1.0 (manual maintainer step).
Goal: The differentiator: a dependency-free herald check that catches the most common technical SEO mistakes in CI.
Scope: Zero-dep crawler and head scanner, title/description/canonical/noindex/sitemap/robots rules, no-JS visibility, internal links, Lighthouse-equivalent basics, AI-crawler robots rules, herald routes, entity graph by default.
Issues and acceptance criteria
-
#14 herald CLI skeleton and config loading
-
node:utilparseArgs;--helpfor every command. - Loads
herald.config.tsvia Node type stripping (≥ 22.18), else.mjs/.jsonwith a clear message. - No dependencies (enforced by #3).
-
-
#15 Head scanner (own tolerant HTML tokenizer) with fuzz corpus
- Extracts title, meta, link, html lang, JSON-LD scripts and same-origin
<a href>. - Corpus of real Next 16 output plus malformed HTML; results match a reference parser (devDependency in tests only) on ≥ 99% of corpus fields.
- Fuzzing finds no crash or hang (time limit per document).
- Extracts title, meta, link, html lang, JSON-LD scripts and same-origin
-
#16 Crawler for --url: robots, sitemap seeding, concurrency, limits
- Seeds from
/, robots.txt sitemaps (incl. indexes),--seed; follows same-origin links. -
--max-pages,--concurrency,--timeout, response size caps; manual redirect tracking. - Honours robots.txt for its UA unless
--ignore-robots. -
--buildrunsnext build+next starton a free port and shuts it down.
- Seeds from
-
#17 herald check --dir for static export output
- Maps URLs to
out/files (trailingSlashboth ways). - Same rule results as
--urlagainstnpx serve outon the fixture.
- Maps URLs to
-
#18 Rules: title and description
- Implements title-missing, title-duplicate, title-length, description-missing, description-duplicate, description-length.
- Duplicates grouped by canonical; noindex pages excluded.
- Fixture tests per rule (positive and negative).
-
#19 Rules: canonicals
- Implements canonical-missing, canonical-multiple, canonical-relative, canonical-broken, canonical-host, canonical-noindex.
-
--site-urllets a preview crawl compare against the production host.
-
#20 Rules: noindex leaks and sitemap/robots consistency
- Implements noindex-leak (meta + X-Robots-Tag), sitemap-noindex, sitemap-url-status, sitemap-non-canonical, sitemap-missing, sitemap-invalid, sitemap-orphan, robots-missing, robots-blocks-sitemap-url, robots-blocks-noindex, robots-sitemap-host.
- robots.txt matching per RFC 9309 (longest match,
*,$) with the RFC examples as tests.
-
#21 Reporters: pretty, json, github annotations; exit codes
- Exit 1 on any error, 2 on crawl failure, else 0.
-
--format githubemits workflow annotations;jsonschema documented and versioned. - Severity overrides via config and
--rule.
-
#22 herald routes: scan app/ and generate typed routes
- Handles route groups,
@slot, intercepting routes,_private,src/app,pageExtensions. - Writes
herald-routes.gen.ts;defineSitemapinfers requiredparamskeys from it (type tests). -
--watchusesfs.watch; no orphaned watchers after exit.
- Handles route groups,
-
#45 Rules: AI crawler robots
- Implements robots-blocks-ai-search, robots-ai-inconsistent, robots-ai-list-stale (180-day table age).
- Uses the RFC 9309 matcher from #20; fixtures per rule.
-
#46 Rules: no-JS visibility
- Scanner extracts visible
<main>/<article>text (HTTP only, no browser). - Implements content-js-only (default < 50 words), content-bot-parity (bot vs browser UA), metadata-in-body.
- Fixtures per rule.
- Scanner extracts visible
-
#47 Rules: internal links
- Implements link-broken (4xx/5xx) and link-redirect; targets fetched once.
-
#48 Rules: Lighthouse-equivalent page basics
- Implements http-status-indexable, viewport-missing, anchor-not-crawlable; each mapped to the Lighthouse audit it mirrors.
-
#49 Entity graph by default (moved from v0.4)
-
defineHerald({ organization | person, sameAs })validated; stable@ids (/#organization,/#person,/#website,#webpage). -
jsonLd.siteGraph,jsonLd.ref,graph()with dangling-id dev errors;personbuilder. - Same config → identical entity nodes on every page (snapshot).
-
Exit criteria: herald check --build on the examples reports zero errors, and each rule has a failing fixture that it catches.
Goal: Sites with >50k URLs, news publishers and multilingual sites.
Scope: Sitemap index splitting, news sitemaps, hreflang, next-tongue adapter, hreflang rules, static export, accurate lastmod, dev-only SEO preview panel, redirect manager, pagination and faceted URL helpers.
Issues and acceptance criteria
-
#23 Sitemap index splitting at 50k URLs / 50 MB
-
splitSitemap+sitemapIndexproduce valid chunks and index for a 120k-URL fixture. - Byte limit enforced (splits early if a chunk would exceed 50 MB).
- Only one page of the source is loaded per chunk request.
- sitemap-size and sitemap-index-broken rules implemented.
-
-
#24 News sitemap route and image/video helpers
-
newsSitemapdrops items older than 2 days and caps at 1,000 URLs. -
sitemapVideoenforces required video fields; output validated against Google's documented examples.
-
-
#25 hreflang alternates from I18nSource
-
pageMetadata({ locale, locales })anddefineSitemapemit identical alternate sets including self andx-default. - Invalid codes rejected at config load.
- Reciprocity holds for every page of
examples/i18n(checked by #27).
-
-
#26 fromTongue adapter and next-intl/Paraglide recipes
-
fromTonguehas no import of next-tongue (structural types). - Contract test against next-tongue once it publishes (skipped until then).
- Recipes for next-intl and Paraglide verified in a scratch app and noted with versions.
-
-
#27 Rules: hreflang and html lang
- Implements hreflang-code, hreflang-self, hreflang-reciprocal, hreflang-target-status, hreflang-canonical, hreflang-x-default, html-lang.
- Sitemap alternates and HTML alternates are both checked and compared.
-
#28 Static export compatibility e2e
-
examples/static-exportbuilds withoutput: 'export'including sitemap index viaforce-staticroute handlers, or the fallbackherald build --emit-xmlis implemented and documented. -
herald check --dir outpasses.
-
-
#50 Accurate lastmod and lastmod rules
-
herald routes --git-lastmodwrites commit dates at build time (node:child_process, no dep); clear error on shallow clones. - Implements lastmod-future, lastmod-uniform (≥ 90% identical), lastmod-mismatch (> 1 day vs JSON-LD/OG).
- Never defaults to build time.
-
-
#57 Dev-only SEO preview panel: Google snippet, OG/X cards, chat link preview, live check warnings
-
<HeraldPreview />fromnext-herald/preview(Client Component) rendersnullunlessprocess.env.NODE_ENV === 'development'; the production bundle ofexamples/basiccontains no preview code (asserted by a bundle grep in e2e) and client JS added in production stays 0. - Reads the rendered
document.head/ DOM on the client only; noheaders(),cookies()or request APIs anywhere (guard #7); pages stay static undercacheComponents: true. - Shows a Google result snippet (title, URL breadcrumb, description, truncated at documented pixel/char widths), OG card (
summary_large_imageandsummary), X card and a generic chat link preview (title, description, image, site name). - Live warnings run the page-level rules from
checkHtml(next-herald/testing) against the current DOM and update on client navigation; each warning links to its rule on the CLI page. - Docs state previews are approximations of each platform, not pixel-exact; no dependencies beyond
next/react(#3).
-
-
#58 Redirect manager: typed rules for next.config redirects or proxy, and redirect-chain/loop/temporary rules
-
defineRedirects(config, rules)fromnext-herald/redirectsvalidates rules at load (source starts with/, no duplicate sources, destination absolute or root-relative,permanentdefaults totrue) with errors naming the rule index. -
.toNextConfig()returns the exactRedirect[]shape ofnext.configredirects();.match(pathname)is a pure function usable from the user's ownproxy.ts(the library never reads headers or cookies). - Build-time validation reports chains (A→B→C) and loops within the rule set before deploy;
herald buildfails on loops. - Implements
redirect-chain(warn),redirect-loop(error) andredirect-temporary(warn) inherald check, with failing fixtures. - Docs cover static export (
output: 'export'has no redirects: emit a host config recipe instead) and zero dependencies (#3).
-
-
#59 Pagination and faceted URL helpers with a params-duplicate-content rule
-
listingMetadata(config, { path, params, indexable, pageParam?, drop? })innext-herald/metadatareturns a self-referencing canonical for page N (never canonicalises page 2+ to page 1), stripsdropparams (sort,utm_*), orders kept params stably, and setsnoindex, followfor non-indexable facet combinations. - Takes params as a plain object passed by the caller; the library never reads
searchParamsor headers itself. Docs explain that readingsearchParamsmakes a route dynamic and show the static[[...facets]]path-segment alternative. -
listingHref(path, params, opts)builds the same normalised URLs for links, so internal links match canonicals. - Implements
params-duplicate-content(warn) andpagination-canonical-first(warn) with fixtures. - No
rel=prev/nextpromise (Google no longer uses it); zero dependencies (#3).
-
Goal: Social previews and structured data validation.
Scope: OG templates, OG and JSON-LD rules, citation-friendly structure and entity rules, schema types batch 2, SARIF, image SEO, herald check --diff.
Issues and acceptance criteria
-
#29 OG image templates and ogImage
-
SimpleCard,ArticleCard,ProductCardwith typed props; only Satori-supported CSS. - No fonts bundled;
fontsoption documented. - Works in Node and Edge runtimes.
-
-
#30 OG template PNG snapshot tests
- Snapshots per template for LTR, long titles, and a CJK font.
- Pixel diff threshold documented; runs in CI.
-
#31 Rules: OG images
- Implements og-image-missing, og-image-status, og-image-dimensions, og-image-bytes, og-image-type, og-image-alt.
- PNG/JPEG/WebP/GIF header readers with fixtures; Range requests where supported.
-
#32 Rules: JSON-LD validity
- Implements jsonld-parse, jsonld-type, jsonld-required, jsonld-recommended for all built-in types.
- Docs state these are a subset of Google's checks.
-
#33 Schema types and builders batch 2
- newsArticle, localBusiness, event, videoObject, softwareApplication, recipe, jobPosting with required-field type tests.
-
andgraph()with@idreferencesperson: moved to #49 (v0.2).
-
#34 SARIF output and GitHub Action recipe
-
--format sarifvalidates against SARIF 2.1.0 schema in tests. - Docs recipe uploads to GitHub code scanning.
-
-
#51 Rules: citation-friendly structure
- Implements h1-missing, h1-multiple, heading-order, img-alt-missing, jsonld-date-missing, jsonld-author, jsonld-visible-mismatch.
- Docs: consistency checks, not ranking or citation factors.
-
#52 Rules: entity consistency
- Implements jsonld-entity-id, sameas-inconsistent, jsonld-deprecated-type.
-
#60 Image SEO: image sitemap entries from next/image usage and route data, plus image rules
-
herald routes --imagesstatically scansapp/fornext/image<Image>with literal or statically importedsrcand writes them toroutes[i].imagesinherald-routes.gen.ts(own tokenizer, Node built-ins only); dynamicsrcvalues come fromentry()route data. -
defineSitemapmerges both sources into nativeimagesentries: absolute, deduped, at most 1,000 per URL (Google's limit),/_next/imageURLs resolved to the original source. - Missing alt reuses
img-alt-missing(#51), raised from info to warn for images listed in the image sitemap; addsimg-dimensions-missing(warn),img-format-legacy(info) andsitemap-image-status(error) with fixtures. - Dimensions and type read from file headers with the #31 readers; no image decoding library; zero dependencies (#3).
-
-
#61 herald check --diff : report only SEO changes against a base build or branch, markdown/SARIF for PRs
-
herald check --snapshot <file>writes a deterministic JSON snapshot (per URL: status, title, description, canonical, robots, hreflang, JSON-LD types; sitemap URL set). -
--diff <base>accepts a snapshot file or a git ref; for a ref it builds the base in a temporarygit worktree(node:child_process, no dependency) and cleans it up. - Reports only changes:
diff-noindex-added(error),diff-canonical-changed(warn),diff-sitemap-url-removed(warn),diff-title-changed(info),diff-description-changed(info),diff-status-changed(warn). -
--format markdownproduces a PR-comment-ready summary (collapsed per-URL details, capped length);--format sarifreuses #34 and validates against SARIF 2.1.0. - Exit code 1 only when a diff finding is
error; GitHub Actions recipe documented; zero dependencies (#3).
-
Goal: Make adoption easy and back the README with published numbers.
Scope: herald init, herald indexnow, verified migration guides, published size and perf report, AI referral docs; experimental markdown alternates and llms.txt.
Issues and acceptance criteria
-
#35 herald init scaffolder
- Creates config, sitemap, robots;
--largeand--i18nvariants; never overwrites without--force. - Scaffolded app passes
herald check --build.
- Creates config, sitemap, robots;
-
#36 Verified migration guide from next-seo
- Guide applied to a sample next-seo 7 App Router app;
herald checkbefore/after shows no regressions. - Every mapping row verified.
- Guide applied to a sample next-seo 7 App Router app;
-
#37 Verified migration guide from next-sitemap
- Guide applied to a sample next-sitemap app; URL sets identical (scripted diff).
- Every mapping row verified.
-
#38 Published size and check performance report
- Script measures client JS added (expected 0) and server bundle size per entry point.
-
herald checktime and memory on a 10k-page fixture published with hardware notes. - Reproducible via one command.
-
#53 herald indexnow CLI
- Diffs current vs previous sitemap snapshot; POSTs to api.indexnow.org in ≤ 10,000-URL batches with global
fetch. - Static key file (
--init-key→public/<key>.txt); indexnow-key-missing rule; mock-endpoint tests.
- Diffs current vs previous sitemap snapshot; POSTs to api.indexnow.org in ≤ 10,000-URL batches with global
-
#54 Experimental: markdown alternates
-
markdownRoute(force-static, params only) andmarkdownAlternate; md-alternate-broken rule. - Accept negotiation only as a
proxy.tsrecipe;examples/cache-componentsstays static.
-
-
#55 Experimental: llms.txt / llms-full.txt
-
defineLlmsTxt/defineLlmsFullTxt, off by default; docs state no proven visibility benefit. - llms-txt-invalid rule (absence never reported).
-
-
#56 Docs: AI referral and citation measurement
- Referrer/UTM recipe; Search Console "Web" note; Bing AI Performance report; log-based AI bot counts.
Goal: Freeze the API and rule IDs; harden.
Scope: API and rule-ID freeze, security review, upstream issue audit.
Issues and acceptance criteria
-
#39 API and rule-ID freeze review
- Every public export reviewed and documented; deprecated names removed.
- Rule IDs and default severities frozen and documented as stable.
-
#40 Security review against the threat model
- Each threat row has a linked test.
- Crawler SSRF limits (same-origin, size caps, off-origin redirects) tested.
-
#41 Audit upstream Next.js metadata/sitemap issues
- Each listed issue re-checked on the latest Next; docs and troubleshooting updated.
- Workarounds removed when fixed upstream.
Goal: Stable release.
Scope: Semver guarantees, complete docs.
Issues and acceptance criteria
-
#42 v1.0 release
- All v0.x acceptance criteria met; docs complete; semver policy published.
- npm publish with provenance (separate, explicit maintainer action).
Goal: Automated migration.
Scope: next-seo and next-sitemap codemods.
Issues and acceptance criteria
-
#43 Codemod: next-seo to next-herald
- Converts JSON-LD components and
DefaultSeoconfig; reports what it can't convert. - Zero dependencies (own TSX transform scope documented) or shipped as a separate package — decided by ADR first.
- Converts JSON-LD components and
-
#44 Codemod: next-sitemap config to next-herald
- Converts
next-sitemap.config.jsintoherald.config.ts,app/sitemap.ts,app/robots.ts. - Removes the postbuild script with a confirmation prompt.
- Converts
See rejected ideas: head components, postbuild files in public/, headless browser crawling, request-derived URLs, Pages Router. AEO hype rejected: research (GEO scores, speakable, HowTo, Indexing API for ordinary pages, Accept negotiation in library code).
Considered, not scheduled:
- Content-Signal / Content-Usage passthrough in robots.txt (AEO stance).
- Search Console / Bing Webmaster API reports (pulling indexing, coverage or performance data into
herald checkor a report). Needs OAuth credentials and network calls to third-party APIs, which do not fit a dependency-free CI check; revisit after v1.0.
Guides
- Getting Started
- Configuration
- Metadata
- JSON LD
- Sitemaps
- Robots
- hreflang and i18n
- OG Images
- CLI and Check Rules
- Cache Components and PPR
- Static Export
- Testing
- AEO Stance
- SEO Preview Panel
- Redirects
- Pagination and Facets
- Image SEO
- Check Diff Mode
- Migration from next seo
- Migration from next sitemap
Reference