@jdevalk/astro-seo-graph 1.4.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,96 @@
1
+ import { createHash } from 'node:crypto';
2
+ /** Identifier for the default hash, recorded in the serialized manifest. */
3
+ export const DEFAULT_HASH_ALGORITHM = 'sha256-16';
4
+ /** Current serialized-manifest format version. */
5
+ export const MANIFEST_VERSION = 1;
6
+ /** SHA-256 of `content` as hex, truncated to 16 chars (64 bits of collision space). */
7
+ export function hashContent(content) {
8
+ return createHash('sha256').update(content, 'utf8').digest('hex').slice(0, 16);
9
+ }
10
+ /**
11
+ * Build a `{ url: hash }` manifest from the eligible page entries.
12
+ *
13
+ * Pass a custom `hash` to normalize volatile markup (per-build nonces,
14
+ * timestamps, build ids) before hashing, so unchanged content doesn't read as
15
+ * "updated". The default hashes the raw content.
16
+ */
17
+ export function buildUrlManifest(entries, hash = (content) => hashContent(content)) {
18
+ const manifest = {};
19
+ for (const entry of entries) {
20
+ manifest[entry.url] = hash(entry.content, entry.url);
21
+ }
22
+ return manifest;
23
+ }
24
+ /**
25
+ * Diff a previous manifest against the current one. Result arrays are sorted
26
+ * for stable logs and deterministic output.
27
+ */
28
+ export function diffManifests(prev, curr) {
29
+ const added = [];
30
+ const updated = [];
31
+ const deleted = [];
32
+ for (const url of Object.keys(curr)) {
33
+ if (!(url in prev))
34
+ added.push(url);
35
+ else if (prev[url] !== curr[url])
36
+ updated.push(url);
37
+ }
38
+ for (const url of Object.keys(prev)) {
39
+ if (!(url in curr))
40
+ deleted.push(url);
41
+ }
42
+ added.sort();
43
+ updated.sort();
44
+ deleted.sort();
45
+ return { added, updated, deleted };
46
+ }
47
+ /**
48
+ * Flatten a diff into the URL set to submit to IndexNow: added + updated +
49
+ * deleted. IndexNow accepts removed URLs (the engine recrawls and drops the
50
+ * 404/410), so deletions are included.
51
+ */
52
+ export function changedUrls(diff) {
53
+ return [...diff.added, ...diff.updated, ...diff.deleted];
54
+ }
55
+ /**
56
+ * Serialize a manifest to stable JSON with sorted keys (so the published file
57
+ * diffs cleanly between deploys).
58
+ */
59
+ export function serializeManifest(urls, algorithm = DEFAULT_HASH_ALGORITHM) {
60
+ const sorted = {};
61
+ for (const url of Object.keys(urls).sort()) {
62
+ const hash = urls[url];
63
+ if (hash !== undefined)
64
+ sorted[url] = hash;
65
+ }
66
+ const payload = { version: MANIFEST_VERSION, algorithm, urls: sorted };
67
+ return JSON.stringify(payload, null, 2);
68
+ }
69
+ /**
70
+ * Parse a previously published manifest. Returns the `{ url: hash }` map, or
71
+ * `null` when the input is missing or not a recognizable manifest — the caller
72
+ * decides whether that means "first run" or "fetch failed, skip submission".
73
+ */
74
+ export function parseManifest(text) {
75
+ if (!text)
76
+ return null;
77
+ let data;
78
+ try {
79
+ data = JSON.parse(text);
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ if (typeof data !== 'object' || data === null)
85
+ return null;
86
+ const urls = data.urls;
87
+ if (typeof urls !== 'object' || urls === null)
88
+ return null;
89
+ const out = {};
90
+ for (const [url, hash] of Object.entries(urls)) {
91
+ if (typeof hash === 'string')
92
+ out[url] = hash;
93
+ }
94
+ return out;
95
+ }
96
+ //# sourceMappingURL=indexnow-manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"indexnow-manifest.js","sourceRoot":"","sources":["../src/indexnow-manifest.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\n\n/**\n * Incremental-IndexNow manifest helpers.\n *\n * The integration submits the whole site to IndexNow on every build, which\n * the IndexNow spec discourages (submit only added/updated/deleted URLs) and\n * which can trip per-host rate limits. These pure helpers let a caller compute\n * a content-hash manifest of the built pages, diff it against the previously\n * published manifest, and submit only the URLs that actually changed.\n *\n * The helpers are runtime-agnostic and IO-free: the caller decides how to\n * read the built pages, where to fetch the previous manifest from, and where\n * to publish the new one. The integration wires them to the Astro build\n * output and the live site (see `seoGraph({ indexNow: { incremental } })`).\n */\n\n/** Mapping of each eligible page URL to a short content hash. */\nexport type UrlManifest = Record<string, string>;\n\n/** A single page's URL plus the content to hash (typically built HTML). */\nexport interface ManifestEntry {\n url: string;\n content: string;\n}\n\n/** Identifier for the default hash, recorded in the serialized manifest. */\nexport const DEFAULT_HASH_ALGORITHM = 'sha256-16';\n\n/** Current serialized-manifest format version. */\nexport const MANIFEST_VERSION = 1;\n\n/** The on-disk / over-the-wire manifest shape. */\nexport interface SerializedManifest {\n version: number;\n algorithm: string;\n urls: UrlManifest;\n}\n\n/** SHA-256 of `content` as hex, truncated to 16 chars (64 bits of collision space). */\nexport function hashContent(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex').slice(0, 16);\n}\n\n/**\n * Build a `{ url: hash }` manifest from the eligible page entries.\n *\n * Pass a custom `hash` to normalize volatile markup (per-build nonces,\n * timestamps, build ids) before hashing, so unchanged content doesn't read as\n * \"updated\". The default hashes the raw content.\n */\nexport function buildUrlManifest(\n entries: readonly ManifestEntry[],\n hash: (content: string, url: string) => string = (content) => hashContent(content),\n): UrlManifest {\n const manifest: UrlManifest = {};\n for (const entry of entries) {\n manifest[entry.url] = hash(entry.content, entry.url);\n }\n return manifest;\n}\n\n/** The three change sets produced by comparing two manifests. */\nexport interface ManifestDiff {\n /** URLs present now but not in the previous manifest. */\n added: string[];\n /** URLs present in both, with a different hash. */\n updated: string[];\n /** URLs present previously but gone now. */\n deleted: string[];\n}\n\n/**\n * Diff a previous manifest against the current one. Result arrays are sorted\n * for stable logs and deterministic output.\n */\nexport function diffManifests(prev: UrlManifest, curr: UrlManifest): ManifestDiff {\n const added: string[] = [];\n const updated: string[] = [];\n const deleted: string[] = [];\n\n for (const url of Object.keys(curr)) {\n if (!(url in prev)) added.push(url);\n else if (prev[url] !== curr[url]) updated.push(url);\n }\n for (const url of Object.keys(prev)) {\n if (!(url in curr)) deleted.push(url);\n }\n\n added.sort();\n updated.sort();\n deleted.sort();\n return { added, updated, deleted };\n}\n\n/**\n * Flatten a diff into the URL set to submit to IndexNow: added + updated +\n * deleted. IndexNow accepts removed URLs (the engine recrawls and drops the\n * 404/410), so deletions are included.\n */\nexport function changedUrls(diff: ManifestDiff): string[] {\n return [...diff.added, ...diff.updated, ...diff.deleted];\n}\n\n/**\n * Serialize a manifest to stable JSON with sorted keys (so the published file\n * diffs cleanly between deploys).\n */\nexport function serializeManifest(\n urls: UrlManifest,\n algorithm: string = DEFAULT_HASH_ALGORITHM,\n): string {\n const sorted: UrlManifest = {};\n for (const url of Object.keys(urls).sort()) {\n const hash = urls[url];\n if (hash !== undefined) sorted[url] = hash;\n }\n const payload: SerializedManifest = { version: MANIFEST_VERSION, algorithm, urls: sorted };\n return JSON.stringify(payload, null, 2);\n}\n\n/**\n * Parse a previously published manifest. Returns the `{ url: hash }` map, or\n * `null` when the input is missing or not a recognizable manifest — the caller\n * decides whether that means \"first run\" or \"fetch failed, skip submission\".\n */\nexport function parseManifest(text: string | null | undefined): UrlManifest | null {\n if (!text) return null;\n let data: unknown;\n try {\n data = JSON.parse(text);\n } catch {\n return null;\n }\n if (typeof data !== 'object' || data === null) return null;\n const urls = (data as Partial<SerializedManifest>).urls;\n if (typeof urls !== 'object' || urls === null) return null;\n const out: UrlManifest = {};\n for (const [url, hash] of Object.entries(urls)) {\n if (typeof hash === 'string') out[url] = hash;\n }\n return out;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"indexnow.js","sourceRoot":"","sources":["../src/indexnow.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AASpE;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,sBAAsB,CAAC,OAAgC;IACnE,MAAM,IAAI,GAAG,yBAAyB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACpD,MAAM,YAAY,GACd,OAAO,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC;IAExF,OAAO,KAAK,IAAI,EAAE;QACd,MAAM,OAAO,GAA2B;YACpC,cAAc,EAAE,2BAA2B;SAC9C,CAAC;QACF,IAAI,YAAY,KAAK,IAAI;YAAE,OAAO,CAAC,eAAe,CAAC,GAAG,YAAY,CAAC;QACnE,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;IAC3C,CAAC,CAAC;AACN,CAAC;AAED,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"indexnow.js","sourceRoot":"","sources":["../src/indexnow.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AASpE;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,sBAAsB,CAAC,OAAgC;IACnE,MAAM,IAAI,GAAG,yBAAyB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACpD,MAAM,YAAY,GACd,OAAO,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC;IAExF,OAAO,KAAK,IAAI,EAAE;QACd,MAAM,OAAO,GAA2B;YACpC,cAAc,EAAE,2BAA2B;SAC9C,CAAC;QACF,IAAI,YAAY,KAAK,IAAI;YAAE,OAAO,CAAC,eAAe,CAAC,GAAG,YAAY,CAAC;QACnE,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;IAC3C,CAAC,CAAC;AACN,CAAC;AAED,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC","sourcesContent":["import type { APIRoute } from 'astro';\nimport { getIndexNowKeyFileContent } from '@jdevalk/seo-graph-core';\n\nexport interface IndexNowKeyRouteOptions {\n /** IndexNow key (8–128 characters from `[A-Za-z0-9-]`). */\n key: string;\n /** Defaults to `public, max-age=86400`. Pass `null` to omit. */\n cacheControl?: string | null;\n}\n\n/**\n * Returns an Astro `APIRoute` that serves the IndexNow key verification\n * file. Place this at `src/pages/[key].txt.ts` or `src/pages/<key>.txt.ts`\n * so it resolves to `/<key>.txt` on the deployed site.\n *\n * @example\n * ```ts\n * // src/pages/your-key-here.txt.ts\n * import { createIndexNowKeyRoute } from '@jdevalk/astro-seo-graph';\n *\n * export const GET = createIndexNowKeyRoute({ key: 'your-key-here' });\n * ```\n */\nexport function createIndexNowKeyRoute(options: IndexNowKeyRouteOptions): APIRoute {\n const body = getIndexNowKeyFileContent(options.key);\n const cacheControl =\n options.cacheControl === undefined ? 'public, max-age=86400' : options.cacheControl;\n\n return async () => {\n const headers: Record<string, string> = {\n 'Content-Type': 'text/plain; charset=utf-8',\n };\n if (cacheControl !== null) headers['Cache-Control'] = cacheControl;\n return new Response(body, { headers });\n };\n}\n\nexport { submitToIndexNow, validateIndexNowKey } from '@jdevalk/seo-graph-core';\nexport type { IndexNowSubmitResult } from '@jdevalk/seo-graph-core';\n"]}
@@ -62,6 +62,54 @@ export interface IndexNowIntegrationOptions {
62
62
  * ```
63
63
  */
64
64
  filter?: (url: string) => boolean;
65
+ /**
66
+ * Submit only URLs that changed since the last build, instead of the
67
+ * whole site every time (the IndexNow spec asks for added/updated/
68
+ * deleted URLs only, and full resubmits can trip per-host rate limits).
69
+ *
70
+ * Enabled with `true` (defaults) or an options object. On each build the
71
+ * integration hashes every eligible page into a manifest, fetches the
72
+ * previously published manifest from the live site, diffs them, and
73
+ * submits only the difference — then writes the new manifest into the
74
+ * build output so it ships with this deploy and becomes the next baseline.
75
+ *
76
+ * The previous state lives on the live site (not local disk or a
77
+ * key/value store), so this works the same whether you build locally or
78
+ * in CI, and adds no infrastructure. Default is off (full submit).
79
+ */
80
+ incremental?: boolean | IndexNowIncrementalOptions;
81
+ }
82
+ /**
83
+ * Options for incremental IndexNow submission (`indexNow.incremental`).
84
+ */
85
+ export interface IndexNowIncrementalOptions {
86
+ /**
87
+ * Build-output path the manifest is written to, and the URL path it is
88
+ * served from. Defaults to `indexnow-manifest.json` at the site root.
89
+ */
90
+ manifestPath?: string;
91
+ /**
92
+ * Absolute URL to fetch the previous manifest from. Defaults to
93
+ * `<siteUrl>/<manifestPath>`. Override if the manifest is served from a
94
+ * different host or path than where it's written.
95
+ */
96
+ manifestUrl?: string;
97
+ /**
98
+ * Normalize a page's HTML before hashing, to strip per-build volatile
99
+ * markup (CSP nonces, timestamps, build ids) that would otherwise make
100
+ * unchanged pages look modified. Receives the raw HTML and the page URL.
101
+ */
102
+ normalize?: (html: string, url: string) => string;
103
+ /**
104
+ * What to do when the previous manifest can't be fetched or parsed for a
105
+ * reason other than a clean 404 (network error, 5xx, malformed body). A
106
+ * 404 is always treated as "first run" (submit everything once).
107
+ *
108
+ * - `'skip'` (default): submit nothing this build, so a transient fetch
109
+ * failure can't trigger an accidental full resubmit.
110
+ * - `'full'`: fall back to submitting every eligible URL.
111
+ */
112
+ onError?: 'skip' | 'full';
65
113
  }
66
114
  /**
67
115
  * Pathnames that `indexNow` excludes by default, regardless of any
@@ -74,6 +122,40 @@ export interface IndexNowIntegrationOptions {
74
122
  * when the build emits `404/index.html`).
75
123
  */
76
124
  export declare function isDefaultExcludedFromIndexNow(url: string): boolean;
125
+ /**
126
+ * Return `options` only when `branch` matches `productionBranch` (default
127
+ * `"main"`), otherwise return `undefined` so that `seoGraph()` skips
128
+ * IndexNow submission entirely.
129
+ *
130
+ * This is useful on CI/CD platforms that expose the current branch as an
131
+ * environment variable (e.g. `CF_PAGES_BRANCH`, `VERCEL_GIT_COMMIT_REF`,
132
+ * `BRANCH`) so that preview deployments never submit URLs to IndexNow.
133
+ *
134
+ * @example
135
+ * ```js
136
+ * // astro.config.mjs
137
+ * import { seoGraph, indexNowOnBranch } from '@jdevalk/astro-seo-graph/integration';
138
+ *
139
+ * export default defineConfig({
140
+ * integrations: [
141
+ * seoGraph({
142
+ * indexNow: indexNowOnBranch(process.env.CF_PAGES_BRANCH ?? '', {
143
+ * key: 'your-key-here',
144
+ * host: 'example.com',
145
+ * siteUrl: 'https://example.com',
146
+ * }),
147
+ * }),
148
+ * ],
149
+ * });
150
+ * ```
151
+ */
152
+ export declare function indexNowOnBranch(
153
+ /** Current branch name, e.g. process.env.WORKERS_CI_BRANCH or process.env.VERCEL_GIT_COMMIT_REF or process.env.BRANCH, etc. */
154
+ branch: string,
155
+ /** Options for IndexNow submission. */
156
+ options: IndexNowIntegrationOptions,
157
+ /** Branch name that triggers production submission, defaults to `"main"` */
158
+ productionBranch?: string): IndexNowIntegrationOptions | undefined;
77
159
  export interface LlmsTxtIntegrationOptions {
78
160
  /** H1 of the generated `llms.txt`. */
79
161
  title: string;
@@ -353,7 +435,10 @@ export declare function classifyInternalLink(href: string, builtPaths: ReadonlyS
353
435
  * Two WCAG-sanctioned ways to mark an image as decorative are respected
354
436
  * and NOT flagged:
355
437
  *
356
- * - `alt=""` — the canonical decorative-image pattern.
438
+ * - `alt=""` — the canonical decorative-image pattern. Also recognized
439
+ * in HTML5 boolean-attribute form `<img ... alt>` (no `=`), which is
440
+ * spec-equivalent to `alt=""` and is what Vite's HTML minifier (used
441
+ * by Astro) emits when given `alt=""`.
357
442
  * - `role="presentation"` (or `role="none"`) — removes the image from
358
443
  * the accessibility tree; typically paired with `alt=""` but some
359
444
  * codebases use it alone.
@@ -1 +1 @@
1
- {"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../src/integration.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../src/integration.ts"],"names":[],"mappings":"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"}
@@ -3,6 +3,7 @@ import { join, relative } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { submitToIndexNow } from '@jdevalk/seo-graph-core';
5
5
  import { renderLlmsTxt } from './llms-txt.js';
6
+ import { buildUrlManifest, changedUrls, diffManifests, hashContent, parseManifest, serializeManifest, } from './indexnow-manifest.js';
6
7
  /**
7
8
  * Pathnames that `indexNow` excludes by default, regardless of any
8
9
  * caller-supplied `filter`. The 404 page is the only universal case —
@@ -23,6 +24,42 @@ export function isDefaultExcludedFromIndexNow(url) {
23
24
  }
24
25
  return pathname === '/404' || pathname === '/404/';
25
26
  }
27
+ /**
28
+ * Return `options` only when `branch` matches `productionBranch` (default
29
+ * `"main"`), otherwise return `undefined` so that `seoGraph()` skips
30
+ * IndexNow submission entirely.
31
+ *
32
+ * This is useful on CI/CD platforms that expose the current branch as an
33
+ * environment variable (e.g. `CF_PAGES_BRANCH`, `VERCEL_GIT_COMMIT_REF`,
34
+ * `BRANCH`) so that preview deployments never submit URLs to IndexNow.
35
+ *
36
+ * @example
37
+ * ```js
38
+ * // astro.config.mjs
39
+ * import { seoGraph, indexNowOnBranch } from '@jdevalk/astro-seo-graph/integration';
40
+ *
41
+ * export default defineConfig({
42
+ * integrations: [
43
+ * seoGraph({
44
+ * indexNow: indexNowOnBranch(process.env.CF_PAGES_BRANCH ?? '', {
45
+ * key: 'your-key-here',
46
+ * host: 'example.com',
47
+ * siteUrl: 'https://example.com',
48
+ * }),
49
+ * }),
50
+ * ],
51
+ * });
52
+ * ```
53
+ */
54
+ export function indexNowOnBranch(
55
+ /** Current branch name, e.g. process.env.WORKERS_CI_BRANCH or process.env.VERCEL_GIT_COMMIT_REF or process.env.BRANCH, etc. */
56
+ branch,
57
+ /** Options for IndexNow submission. */
58
+ options,
59
+ /** Branch name that triggers production submission, defaults to `"main"` */
60
+ productionBranch = 'main') {
61
+ return branch === productionBranch ? options : undefined;
62
+ }
26
63
  /** Defaults applied when `validateMetadataLength: true`. */
27
64
  export const DEFAULT_METADATA_LENGTH_BOUNDS = {
28
65
  title: { min: 30, max: 65 },
@@ -258,7 +295,10 @@ export function classifyInternalLink(href, builtPaths, origin) {
258
295
  * Two WCAG-sanctioned ways to mark an image as decorative are respected
259
296
  * and NOT flagged:
260
297
  *
261
- * - `alt=""` — the canonical decorative-image pattern.
298
+ * - `alt=""` — the canonical decorative-image pattern. Also recognized
299
+ * in HTML5 boolean-attribute form `<img ... alt>` (no `=`), which is
300
+ * spec-equivalent to `alt=""` and is what Vite's HTML minifier (used
301
+ * by Astro) emits when given `alt=""`.
262
302
  * - `role="presentation"` (or `role="none"`) — removes the image from
263
303
  * the accessibility tree; typically paired with `alt=""` but some
264
304
  * codebases use it alone.
@@ -270,7 +310,11 @@ export function findImagesWithoutAlt(html) {
270
310
  const results = [];
271
311
  for (const match of html.matchAll(/<img\b[^>]*>/gi)) {
272
312
  const tag = match[0];
273
- if (/\balt\s*=/i.test(tag))
313
+ // Match `alt` whether written as `alt="..."`, bare-boolean `alt`,
314
+ // or self-closed `alt/`. Leading `\s` anchors `alt` to an
315
+ // attribute slot inside the tag, avoiding false matches on
316
+ // attribute names like `data-alt` or `data-altitude`.
317
+ if (/\salt(?:\s*=|\s|\/?>|$)/i.test(tag))
274
318
  continue;
275
319
  if (/\brole\s*=\s*["'](?:presentation|none)["']/i.test(tag))
276
320
  continue;
@@ -729,14 +773,15 @@ export default function seoGraph(options = {}) {
729
773
  }
730
774
  }
731
775
  if (indexNow) {
732
- const urls = htmlFiles
733
- .map((f) => htmlFileToUrl(f, indexNow.siteUrl))
734
- .filter((u) => !isDefaultExcludedFromIndexNow(u))
735
- .filter((u) => (indexNow.filter ? indexNow.filter(u) : true));
736
- if (urls.length === 0) {
737
- logger.info('IndexNow: no URLs to submit.');
738
- }
739
- else {
776
+ const eligible = htmlFiles
777
+ .map((f) => ({ file: f, url: htmlFileToUrl(f, indexNow.siteUrl) }))
778
+ .filter((e) => !isDefaultExcludedFromIndexNow(e.url))
779
+ .filter((e) => (indexNow.filter ? indexNow.filter(e.url) : true));
780
+ const submit = async (urls) => {
781
+ if (urls.length === 0) {
782
+ logger.info('IndexNow: no URLs to submit.');
783
+ return;
784
+ }
740
785
  const results = await submitToIndexNow({
741
786
  host: indexNow.host,
742
787
  key: indexNow.key,
@@ -752,6 +797,60 @@ export default function seoGraph(options = {}) {
752
797
  logger.warn(`IndexNow: submission failed (status ${r.status}): ${r.message}`);
753
798
  }
754
799
  }
800
+ };
801
+ if (!indexNow.incremental) {
802
+ await submit(eligible.map((e) => e.url));
803
+ }
804
+ else {
805
+ const opts = indexNow.incremental === true ? {} : indexNow.incremental;
806
+ const manifestRelPath = (opts.manifestPath ?? 'indexnow-manifest.json').replace(/^\//, '');
807
+ const siteBase = indexNow.siteUrl.replace(/\/$/, '');
808
+ const manifestUrl = opts.manifestUrl ?? `${siteBase}/${manifestRelPath}`;
809
+ const onError = opts.onError ?? 'skip';
810
+ const normalize = opts.normalize;
811
+ // Current manifest: hash every eligible page's built HTML.
812
+ const entries = [];
813
+ for (const e of eligible) {
814
+ const html = await readFile(join(buildDir, e.file), 'utf8');
815
+ entries.push({ url: e.url, content: html });
816
+ }
817
+ const current = buildUrlManifest(entries, normalize
818
+ ? (content, url) => hashContent(normalize(content, url))
819
+ : undefined);
820
+ // Previous state lives on the live site. A clean 404 means
821
+ // "first run" (baseline). Any other failure leaves `previous`
822
+ // null so we never accidentally full-resubmit on a transient
823
+ // network blip.
824
+ let previous = null;
825
+ try {
826
+ const res = await fetch(manifestUrl);
827
+ if (res.status === 404) {
828
+ previous = {};
829
+ }
830
+ else if (res.ok) {
831
+ previous = parseManifest(await res.text());
832
+ }
833
+ }
834
+ catch {
835
+ previous = null;
836
+ }
837
+ // Always publish the new manifest so it ships with this
838
+ // deploy and becomes the next build's baseline.
839
+ await writeFile(join(buildDir, manifestRelPath), serializeManifest(current), 'utf8');
840
+ if (previous === null) {
841
+ if (onError === 'full') {
842
+ logger.warn(`IndexNow: could not read previous manifest at ${manifestUrl}; submitting all URLs.`);
843
+ await submit(Object.keys(current));
844
+ }
845
+ else {
846
+ logger.warn(`IndexNow: could not read previous manifest at ${manifestUrl}; skipping submission this build.`);
847
+ }
848
+ }
849
+ else {
850
+ const diff = diffManifests(previous, current);
851
+ logger.info(`IndexNow: ${diff.added.length} added, ${diff.updated.length} updated, ${diff.deleted.length} deleted.`);
852
+ await submit(changedUrls(diff));
853
+ }
755
854
  }
756
855
  }
757
856
  if (llmsTxt) {