@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.
- package/AGENTS.md +46 -0
- package/README.md +131 -2
- package/dist/aggregator.js.map +1 -1
- package/dist/alternates.js.map +1 -1
- package/dist/api-catalog.js.map +1 -1
- package/dist/breadcrumbs.js.map +1 -1
- package/dist/components/seo-context.js.map +1 -1
- package/dist/components/seo-props.js.map +1 -1
- package/dist/content-helpers.d.ts +6 -31
- package/dist/content-helpers.d.ts.map +1 -1
- package/dist/content-helpers.js.map +1 -1
- package/dist/git-lastmod.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/indexnow-manifest.d.ts +73 -0
- package/dist/indexnow-manifest.d.ts.map +1 -0
- package/dist/indexnow-manifest.js +96 -0
- package/dist/indexnow-manifest.js.map +1 -0
- package/dist/indexnow.js.map +1 -1
- package/dist/integration.d.ts +86 -1
- package/dist/integration.d.ts.map +1 -1
- package/dist/integration.js +109 -10
- package/dist/integration.js.map +1 -1
- package/dist/llms-txt.js.map +1 -1
- package/dist/markdown-alternate.js.map +1 -1
- package/dist/markdown-routes.js.map +1 -1
- package/dist/routes.js.map +1 -1
- package/package.json +7 -7
|
@@ -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"]}
|
package/dist/indexnow.js.map
CHANGED
|
@@ -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"]}
|
package/dist/integration.d.ts
CHANGED
|
@@ -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":"AAIA,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../src/integration.ts"],"names":[],"mappings":"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"}
|
package/dist/integration.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
733
|
-
.map((f) => htmlFileToUrl(f, indexNow.siteUrl))
|
|
734
|
-
.filter((
|
|
735
|
-
.filter((
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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) {
|