@jdevalk/seo-graph-core 0.5.1 → 0.6.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/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # @jdevalk/seo-graph-core
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@jdevalk/seo-graph-core)](https://www.npmjs.com/package/@jdevalk/seo-graph-core)
4
+ [![license](https://img.shields.io/npm/l/@jdevalk/seo-graph-core)](https://github.com/jdevalk/seo-graph/blob/main/LICENSE)
5
+
3
6
  Pure schema.org JSON-LD graph builders. Runtime-agnostic core for agent-ready SEO.
4
7
 
5
8
  ## What this is
@@ -32,6 +35,15 @@ npm install @jdevalk/seo-graph-core
32
35
  | `assembleGraph(pieces)` | Wraps pieces in a `{ @context, @graph }` envelope with first-wins deduplication by `@id`. |
33
36
  | `deduplicateByGraphId(entities)` | The dedup engine on its own, in case you need custom assembly. |
34
37
 
38
+ ### IndexNow
39
+
40
+ | API | Purpose |
41
+ | ---------------------------- | ----------------------------------------------------------------------------- |
42
+ | `submitToIndexNow` | POST URLs to the IndexNow aggregator. Filters by host, dedupes, chunks at 10k. |
43
+ | `generateIndexNowKey` | Generate a random hex key (Web Crypto). |
44
+ | `validateIndexNowKey` | Verify a key is 8–128 hex chars. |
45
+ | `getIndexNowKeyFileContent` | Body to serve at `/<key>.txt` for host verification. |
46
+
35
47
  ### Piece builders
36
48
 
37
49
  All builders take an input object and the `IdFactory`, and return a plain
@@ -120,18 +132,41 @@ const graph = assembleGraph([
120
132
  // graph === { '@context': 'https://schema.org', '@graph': [...] }
121
133
  ```
122
134
 
123
- ## Why
135
+ ## IndexNow
136
+
137
+ Runtime-agnostic helpers for the [IndexNow](https://www.indexnow.org) protocol.
138
+ Submit URLs to participating search engines (Bing, Yandex, Seznam, Naver, Yep)
139
+ through the neutral aggregator at `api.indexnow.org`.
140
+
141
+ ```ts
142
+ import {
143
+ generateIndexNowKey,
144
+ getIndexNowKeyFileContent,
145
+ submitToIndexNow,
146
+ validateIndexNowKey,
147
+ } from '@jdevalk/seo-graph-core';
124
148
 
125
- The [agent-ready web](https://joost.blog/tag/agent-ready/) needs every
126
- publisher to expose a rich, linked knowledge graph for their content. Hand-
127
- writing JSON-LD is error-prone; writing it once per framework is worse.
128
- `@jdevalk/seo-graph-core` is the shared engine behind two downstream packages,
129
- both in production:
149
+ const key = generateIndexNowKey(); // 32-char hex, persist this
130
150
 
131
- - [`@jdevalk/astro-seo-graph`](https://www.npmjs.com/package/@jdevalk/astro-seo-graph) the Astro integration (`<Seo>` + route factories). Used in production by [joost.blog](https://joost.blog) and [limonaia.house](https://limonaia.house).
132
- - [`@jdevalk/emdash-plugin-seo`](https://www.npmjs.com/package/@jdevalk/emdash-plugin-seo) the EmDash CMS plugin. Uses `assembleGraph` directly (EmDash contributes metadata through hooks, not through templates, so it doesn't go through the `<Seo>` component).
151
+ // Serve this body at https://example.com/<key>.txt
152
+ const keyFileBody = getIndexNowKeyFileContent(key);
153
+
154
+ const results = await submitToIndexNow({
155
+ host: 'example.com',
156
+ key,
157
+ urls: ['https://example.com/blog/new-post/'],
158
+ });
159
+ ```
160
+
161
+ URLs not on `host` and duplicates are filtered automatically. Bulk submissions
162
+ are chunked at 10,000 URLs per request. `submitToIndexNow` never throws on
163
+ network errors — it returns one result per chunk with `ok`, `status`, and
164
+ `message`. Pass `endpoint` to override the default aggregator, `keyLocation`
165
+ to point at a non-default key-file path, or `fetch` for testing.
166
+
167
+ ## Why
133
168
 
134
- Two different integration runtimes, one graph engine.
169
+ Read more about [why this project exists](https://joost.blog/seo-graph/).
135
170
 
136
171
  ## License
137
172
 
package/dist/index.d.ts CHANGED
@@ -20,4 +20,6 @@ export type { ImageObjectInput } from './pieces/image.js';
20
20
  export { buildVideoObject } from './pieces/video.js';
21
21
  export type { VideoObjectInput } from './pieces/video.js';
22
22
  export { buildPiece } from './pieces/custom.js';
23
+ export { submitToIndexNow, validateIndexNowKey, generateIndexNowKey, getIndexNowKeyFileContent, DEFAULT_INDEXNOW_ENDPOINT, INDEXNOW_MAX_URLS_PER_REQUEST, } from './indexnow.js';
24
+ export type { SubmitToIndexNowOptions, IndexNowSubmitResult } from './indexnow.js';
23
25
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC1F,OAAO,EAAE,uBAAuB,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACpG,YAAY,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACnC,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,YAAY,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAG1D,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,OAAO,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AACpE,YAAY,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAElF,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAErE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAErE,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,YAAY,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAElF,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC1F,OAAO,EAAE,uBAAuB,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACpG,YAAY,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACnC,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,YAAY,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAG1D,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,OAAO,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AACpE,YAAY,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAElF,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAErE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAErE,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,YAAY,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAElF,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,OAAO,EACH,gBAAgB,EAChB,mBAAmB,EACnB,mBAAmB,EACnB,yBAAyB,EACzB,yBAAyB,EACzB,6BAA6B,GAChC,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC"}
package/dist/index.js CHANGED
@@ -12,4 +12,6 @@ export { buildBreadcrumbList } from './pieces/breadcrumb.js';
12
12
  export { buildImageObject } from './pieces/image.js';
13
13
  export { buildVideoObject } from './pieces/video.js';
14
14
  export { buildPiece } from './pieces/custom.js';
15
+ // IndexNow protocol
16
+ export { submitToIndexNow, validateIndexNowKey, generateIndexNowKey, getIndexNowKeyFileContent, DEFAULT_INDEXNOW_ENDPOINT, INDEXNOW_MAX_URLS_PER_REQUEST, } from './indexnow.js';
15
17
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAG3E,OAAO,EAAE,uBAAuB,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEpG,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACnC,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAG9C,iBAAiB;AACjB,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGnD,OAAO,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AAGpE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGnD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAG7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAGrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAGrD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAG3E,OAAO,EAAE,uBAAuB,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEpG,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACnC,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAG9C,iBAAiB;AACjB,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGnD,OAAO,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AAGpE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGnD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAG7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAGrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAGrD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD,oBAAoB;AACpB,OAAO,EACH,gBAAgB,EAChB,mBAAmB,EACnB,mBAAmB,EACnB,yBAAyB,EACzB,yBAAyB,EACzB,6BAA6B,GAChC,MAAM,eAAe,CAAC"}
@@ -0,0 +1,53 @@
1
+ /** Default aggregator endpoint. Fans out to every participating engine. */
2
+ export declare const DEFAULT_INDEXNOW_ENDPOINT = "https://api.indexnow.org/IndexNow";
3
+ /** IndexNow accepts up to 10,000 URLs per bulk request. */
4
+ export declare const INDEXNOW_MAX_URLS_PER_REQUEST = 10000;
5
+ export interface SubmitToIndexNowOptions {
6
+ /** Bare host, e.g. `example.com`. No scheme, no trailing slash. */
7
+ host: string;
8
+ /** 8–128 hex-character key. Must match the key file served on the host. */
9
+ key: string;
10
+ /**
11
+ * Optional absolute URL where the key file is hosted. Defaults to
12
+ * `https://<host>/<key>.txt`. Supply this when the key file lives at
13
+ * a non-default path.
14
+ */
15
+ keyLocation?: string;
16
+ /** URLs to submit. Must all be on `host`. Empty list is a no-op. */
17
+ urls: readonly string[];
18
+ /** Override the endpoint. Defaults to {@link DEFAULT_INDEXNOW_ENDPOINT}. */
19
+ endpoint?: string;
20
+ /** Injectable fetch for testing. Defaults to global `fetch`. */
21
+ fetch?: typeof fetch;
22
+ }
23
+ export interface IndexNowSubmitResult {
24
+ /** HTTP status from the IndexNow server. `0` when the request failed. */
25
+ status: number;
26
+ /** Whether the submission was accepted (200 or 202). */
27
+ ok: boolean;
28
+ /** Response body text if any, or error message on network failure. */
29
+ message: string;
30
+ /** Number of URLs actually submitted after filtering/dedup. */
31
+ submitted: number;
32
+ }
33
+ /**
34
+ * Submit URLs to IndexNow. Filters out URLs not on `host`, deduplicates,
35
+ * and chunks at {@link INDEXNOW_MAX_URLS_PER_REQUEST}. Returns one result
36
+ * per chunk; a single-chunk submission returns a one-element array.
37
+ */
38
+ export declare function submitToIndexNow(options: SubmitToIndexNowOptions): Promise<IndexNowSubmitResult[]>;
39
+ /** Returns true when `key` is 8–128 hexadecimal characters. */
40
+ export declare function validateIndexNowKey(key: string): boolean;
41
+ /**
42
+ * Generate a cryptographically random IndexNow key of `length` hex
43
+ * characters. Default 32. Uses Web Crypto, available in Node 20+,
44
+ * browsers, and edge/worker runtimes.
45
+ */
46
+ export declare function generateIndexNowKey(length?: number): string;
47
+ /**
48
+ * Plain-text body to serve at `https://<host>/<key>.txt`. IndexNow
49
+ * verifies ownership by fetching this file and comparing to the
50
+ * submitted key.
51
+ */
52
+ export declare function getIndexNowKeyFileContent(key: string): string;
53
+ //# sourceMappingURL=indexnow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"indexnow.d.ts","sourceRoot":"","sources":["../src/indexnow.ts"],"names":[],"mappings":"AAIA,2EAA2E;AAC3E,eAAO,MAAM,yBAAyB,sCAAsC,CAAC;AAE7E,2DAA2D;AAC3D,eAAO,MAAM,6BAA6B,QAAS,CAAC;AAOpD,MAAM,WAAW,uBAAuB;IACpC,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB;AAED,MAAM,WAAW,oBAAoB;IACjC,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,EAAE,EAAE,OAAO,CAAC;IACZ,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,SAAS,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CAClC,OAAO,EAAE,uBAAuB,GACjC,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAgDjC;AA0BD,+DAA+D;AAC/D,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAIxD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,SAAK,GAAG,MAAM,CAWvD;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO7D"}
@@ -0,0 +1,124 @@
1
+ // IndexNow protocol support. Submits URLs to participating search engines
2
+ // (Bing, Yandex, Seznam, Naver, Yep) through the neutral aggregator
3
+ // endpoint at api.indexnow.org. Spec: https://www.indexnow.org/documentation
4
+ /** Default aggregator endpoint. Fans out to every participating engine. */
5
+ export const DEFAULT_INDEXNOW_ENDPOINT = 'https://api.indexnow.org/IndexNow';
6
+ /** IndexNow accepts up to 10,000 URLs per bulk request. */
7
+ export const INDEXNOW_MAX_URLS_PER_REQUEST = 10_000;
8
+ /** Key length constraints per the spec. */
9
+ const MIN_KEY_LENGTH = 8;
10
+ const MAX_KEY_LENGTH = 128;
11
+ const HEX_KEY_RE = /^[a-f0-9]+$/i;
12
+ /**
13
+ * Submit URLs to IndexNow. Filters out URLs not on `host`, deduplicates,
14
+ * and chunks at {@link INDEXNOW_MAX_URLS_PER_REQUEST}. Returns one result
15
+ * per chunk; a single-chunk submission returns a one-element array.
16
+ */
17
+ export async function submitToIndexNow(options) {
18
+ const { host, key, urls } = options;
19
+ if (!validateIndexNowKey(key)) {
20
+ throw new Error(`IndexNow key must be ${MIN_KEY_LENGTH}–${MAX_KEY_LENGTH} hex characters.`);
21
+ }
22
+ if (!host || host.includes('/') || host.includes(':')) {
23
+ throw new Error(`IndexNow host must be a bare host (got: ${host}).`);
24
+ }
25
+ const filtered = filterUrlsForHost(urls, host);
26
+ if (filtered.length === 0)
27
+ return [];
28
+ const endpoint = options.endpoint ?? DEFAULT_INDEXNOW_ENDPOINT;
29
+ const keyLocation = options.keyLocation ?? `https://${host}/${key}.txt`;
30
+ const doFetch = options.fetch ?? fetch;
31
+ const results = [];
32
+ for (let i = 0; i < filtered.length; i += INDEXNOW_MAX_URLS_PER_REQUEST) {
33
+ const chunk = filtered.slice(i, i + INDEXNOW_MAX_URLS_PER_REQUEST);
34
+ const body = JSON.stringify({ host, key, keyLocation, urlList: chunk });
35
+ try {
36
+ const res = await doFetch(endpoint, {
37
+ method: 'POST',
38
+ headers: {
39
+ 'Content-Type': 'application/json; charset=utf-8',
40
+ Accept: 'application/json',
41
+ },
42
+ body,
43
+ });
44
+ const text = await res.text().catch(() => '');
45
+ results.push({
46
+ status: res.status,
47
+ ok: res.status === 200 || res.status === 202,
48
+ message: text,
49
+ submitted: chunk.length,
50
+ });
51
+ }
52
+ catch (err) {
53
+ results.push({
54
+ status: 0,
55
+ ok: false,
56
+ message: err instanceof Error ? err.message : String(err),
57
+ submitted: chunk.length,
58
+ });
59
+ }
60
+ }
61
+ return results;
62
+ }
63
+ /**
64
+ * Keep only URLs whose hostname matches `host` (case-insensitive) and
65
+ * deduplicate while preserving first-seen order.
66
+ */
67
+ function filterUrlsForHost(urls, host) {
68
+ const lowerHost = host.toLowerCase();
69
+ const seen = new Set();
70
+ const out = [];
71
+ for (const raw of urls) {
72
+ let parsed;
73
+ try {
74
+ parsed = new URL(raw);
75
+ }
76
+ catch {
77
+ continue;
78
+ }
79
+ if (parsed.hostname.toLowerCase() !== lowerHost)
80
+ continue;
81
+ const normalized = parsed.toString();
82
+ if (seen.has(normalized))
83
+ continue;
84
+ seen.add(normalized);
85
+ out.push(normalized);
86
+ }
87
+ return out;
88
+ }
89
+ /** Returns true when `key` is 8–128 hexadecimal characters. */
90
+ export function validateIndexNowKey(key) {
91
+ if (typeof key !== 'string')
92
+ return false;
93
+ if (key.length < MIN_KEY_LENGTH || key.length > MAX_KEY_LENGTH)
94
+ return false;
95
+ return HEX_KEY_RE.test(key);
96
+ }
97
+ /**
98
+ * Generate a cryptographically random IndexNow key of `length` hex
99
+ * characters. Default 32. Uses Web Crypto, available in Node 20+,
100
+ * browsers, and edge/worker runtimes.
101
+ */
102
+ export function generateIndexNowKey(length = 32) {
103
+ if (length < MIN_KEY_LENGTH || length > MAX_KEY_LENGTH || length % 2 !== 0) {
104
+ throw new Error(`IndexNow key length must be an even number between ${MIN_KEY_LENGTH} and ${MAX_KEY_LENGTH}.`);
105
+ }
106
+ const bytes = new Uint8Array(length / 2);
107
+ crypto.getRandomValues(bytes);
108
+ let hex = '';
109
+ for (const b of bytes)
110
+ hex += b.toString(16).padStart(2, '0');
111
+ return hex;
112
+ }
113
+ /**
114
+ * Plain-text body to serve at `https://<host>/<key>.txt`. IndexNow
115
+ * verifies ownership by fetching this file and comparing to the
116
+ * submitted key.
117
+ */
118
+ export function getIndexNowKeyFileContent(key) {
119
+ if (!validateIndexNowKey(key)) {
120
+ throw new Error(`IndexNow key must be ${MIN_KEY_LENGTH}–${MAX_KEY_LENGTH} hex characters.`);
121
+ }
122
+ return key;
123
+ }
124
+ //# sourceMappingURL=indexnow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"indexnow.js","sourceRoot":"","sources":["../src/indexnow.ts"],"names":[],"mappings":"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"}
@@ -5,6 +5,8 @@ export interface BreadcrumbItem {
5
5
  name: string;
6
6
  /** URL for this crumb. */
7
7
  url: string;
8
+ /** Optional @id override. When set, the `item` value uses this @id instead of the URL. */
9
+ id?: string;
8
10
  }
9
11
  interface BreadcrumbListCoreFields {
10
12
  /** The URL of the page this breadcrumb belongs to. */
@@ -1 +1 @@
1
- {"version":3,"file":"breadcrumb.d.ts","sourceRoot":"","sources":["../../src/pieces/breadcrumb.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAG3C,MAAM,WAAW,cAAc;IAC3B,uEAAuE;IACvE,IAAI,EAAE,MAAM,CAAC;IACb,0BAA0B;IAC1B,GAAG,EAAE,MAAM,CAAC;CACf;AAED,UAAU,wBAAwB;IAC9B,sDAAsD;IACtD,GAAG,EAAE,MAAM,CAAC;IACZ,uDAAuD;IACvD,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;CACpC;AAED,MAAM,MAAM,mBAAmB,GAAG,wBAAwB,GACtD,IAAI,CAAC,OAAO,CAAC,kBAAkB,CAAC,EAAE,MAAM,wBAAwB,GAAG,OAAO,CAAC,CAAC;AAIhF;;GAEG;AACH,wBAAgB,mBAAmB,CAC/B,KAAK,EAAE,mBAAmB,EAC1B,GAAG,EAAE,SAAS,GACf,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAkBzB"}
1
+ {"version":3,"file":"breadcrumb.d.ts","sourceRoot":"","sources":["../../src/pieces/breadcrumb.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAG3C,MAAM,WAAW,cAAc;IAC3B,uEAAuE;IACvE,IAAI,EAAE,MAAM,CAAC;IACb,0BAA0B;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,0FAA0F;IAC1F,EAAE,CAAC,EAAE,MAAM,CAAC;CACf;AAED,UAAU,wBAAwB;IAC9B,sDAAsD;IACtD,GAAG,EAAE,MAAM,CAAC;IACZ,uDAAuD;IACvD,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;CACpC;AAED,MAAM,MAAM,mBAAmB,GAAG,wBAAwB,GACtD,IAAI,CAAC,OAAO,CAAC,kBAAkB,CAAC,EAAE,MAAM,wBAAwB,GAAG,OAAO,CAAC,CAAC;AAIhF;;GAEG;AACH,wBAAgB,mBAAmB,CAC/B,KAAK,EAAE,mBAAmB,EAC1B,GAAG,EAAE,SAAS,GACf,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAsBzB"}
@@ -9,7 +9,11 @@ export function buildBreadcrumbList(input, ids) {
9
9
  '@type': 'ListItem',
10
10
  position: index + 1,
11
11
  name: item.name,
12
- item: index === lastIndex ? { '@id': ids.webPage(item.url) } : item.url,
12
+ item: item.id
13
+ ? { '@id': item.id }
14
+ : index === lastIndex
15
+ ? { '@id': ids.webPage(item.url) }
16
+ : item.url,
13
17
  }));
14
18
  const piece = {
15
19
  '@type': 'BreadcrumbList',
@@ -1 +1 @@
1
- {"version":3,"file":"breadcrumb.js","sourceRoot":"","sources":["../../src/pieces/breadcrumb.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAmBxD,MAAM,YAAY,GAAG,IAAI,GAAG,CAAS,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC;AAEvD;;GAEG;AACH,MAAM,UAAU,mBAAmB,CAC/B,KAA0B,EAC1B,GAAc;IAEd,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;IACzC,MAAM,eAAe,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;QACtD,OAAO,EAAE,UAAU;QACnB,QAAQ,EAAE,KAAK,GAAG,CAAC;QACnB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,IAAI,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG;KAC1E,CAAC,CAAC,CAAC;IAEJ,MAAM,KAAK,GAA4B;QACnC,OAAO,EAAE,gBAAgB;QACzB,KAAK,EAAE,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC;QAChC,eAAe;KAClB,CAAC;IAEF,yBAAyB,CAAC,KAAK,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC;IAEtD,OAAO,KAAK,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"breadcrumb.js","sourceRoot":"","sources":["../../src/pieces/breadcrumb.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAqBxD,MAAM,YAAY,GAAG,IAAI,GAAG,CAAS,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC;AAEvD;;GAEG;AACH,MAAM,UAAU,mBAAmB,CAC/B,KAA0B,EAC1B,GAAc;IAEd,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;IACzC,MAAM,eAAe,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;QACtD,OAAO,EAAE,UAAU;QACnB,QAAQ,EAAE,KAAK,GAAG,CAAC;QACnB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,IAAI,EAAE,IAAI,CAAC,EAAE;YACT,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,EAAE;YACpB,CAAC,CAAC,KAAK,KAAK,SAAS;gBACnB,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;gBAClC,CAAC,CAAC,IAAI,CAAC,GAAG;KACnB,CAAC,CAAC,CAAC;IAEJ,MAAM,KAAK,GAA4B;QACnC,OAAO,EAAE,gBAAgB;QACzB,KAAK,EAAE,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC;QAChC,eAAe;KAClB,CAAC;IAEF,yBAAyB,CAAC,KAAK,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC;IAEtD,OAAO,KAAK,CAAC;AACjB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jdevalk/seo-graph-core",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Pure schema.org JSON-LD graph builders. Runtime-agnostic core for agent-ready SEO.",
5
5
  "keywords": [
6
6
  "schema.org",