@jdevalk/seo-graph-core 0.5.2 → 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 +44 -9
- package/dist/index.d.ts +2 -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.d.ts +53 -0
- package/dist/indexnow.d.ts.map +1 -0
- package/dist/indexnow.js +124 -0
- package/dist/indexnow.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# @jdevalk/seo-graph-core
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@jdevalk/seo-graph-core)
|
|
4
|
+
[](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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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"}
|
package/dist/indexnow.js
ADDED
|
@@ -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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"}
|