@jdevalk/seo-graph-core 0.6.0 → 0.6.2

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 CHANGED
@@ -24,6 +24,38 @@ Two packages:
24
24
 
25
25
  ---
26
26
 
27
+ ## Contents
28
+
29
+ **Schema core** — concepts and builders for the JSON-LD graph:
30
+
31
+ - [Architecture](#architecture)
32
+ - [Installation](#installation)
33
+ - [The @id system](#the-id-system)
34
+ - [Piece builders reference](#piece-builders-reference)
35
+
36
+ **Recipes and patterns** — how to model real sites:
37
+
38
+ - [Site type recipes](#site-type-recipes)
39
+ - [Trust and credibility signals](#trust-and-credibility-signals)
40
+ - [Choosing the right Article subtype](#choosing-the-right-article-subtype)
41
+ - [Actions: telling agents what they can do](#actions-telling-agents-what-they-can-do)
42
+ - [Multi-type entities](#multi-type-entities)
43
+ - [Rich Organization patterns](#rich-organization-patterns)
44
+ - [Rich Person patterns](#rich-person-patterns)
45
+ - [Reference implementations](#reference-implementations)
46
+
47
+ **Astro integration** — runtime component and build-time checks:
48
+
49
+ - [Astro integration guide](#astro-integration-guide) — the `<Seo>` component, hreflang, schema endpoints
50
+ - [Build-time integration](#build-time-integration) — `seoGraph()` hook, H1 validation, metadata uniqueness, IndexNow, `llms.txt`
51
+ - [Complete integration example](#complete-integration-example)
52
+ - [Advanced patterns](#advanced-patterns)
53
+ - [Common mistakes](#common-mistakes)
54
+ - [Validating your output](#validating-your-output)
55
+ - [Repository structure](#repository-structure)
56
+
57
+ ---
58
+
27
59
  ## Architecture
28
60
 
29
61
  ```
@@ -2484,10 +2516,89 @@ export const GET = createSchemaMap({
2484
2516
  **Schema map entry options:**
2485
2517
  | Field | Type | Default | Purpose |
2486
2518
  |---|---|---|---|
2487
- | `path` | `string` | — | Relative path to schema endpoint |
2488
- | `lastModified` | `Date` | — | Last modification date |
2489
- | `changeFreq` | Sitemap frequency string | `'daily'` | Update frequency hint |
2490
- | `priority` | `number` | `0.8` | Priority hint (0.0-1.0) |
2519
+ | `path` | `string` | — | Path of the schema endpoint. Leading slash optional. |
2520
+ | `lastModified` | `Date` | — | Last-modified date. Rendered as `YYYY-MM-DD`. |
2521
+
2522
+ **`createSchemaMap` options:**
2523
+ | Option | Type | Default | Purpose |
2524
+ |---|---|---|---|
2525
+ | `siteUrl` | `string` | — | Canonical origin. Trailing slash stripped. |
2526
+ | `entries` | `SchemaMapEntry[]` | — | One entry per schema endpoint. |
2527
+ | `cacheControl` | `string \| null` | `'max-age=300'` | Cache-Control header. `null` to omit. |
2528
+
2529
+ `<changefreq>` and `<priority>` are deliberately not emitted —
2530
+ Google and other major crawlers ignore them, and fabricating defaults
2531
+ is worse than omitting.
2532
+
2533
+ ### API catalog (RFC 9727)
2534
+
2535
+ Serve a standards-compliant API catalog at `/.well-known/api-catalog`
2536
+ ([RFC 9727](https://www.rfc-editor.org/rfc/rfc9727)) so agent crawlers
2537
+ can discover the site's APIs in one fetch:
2538
+
2539
+ ```ts
2540
+ // src/pages/.well-known/api-catalog.ts
2541
+ import { createApiCatalog } from '@jdevalk/astro-seo-graph';
2542
+
2543
+ export const GET = createApiCatalog({
2544
+ siteUrl: 'https://example.com',
2545
+ schemaEndpoints: [
2546
+ { path: '/schema/post.json', schemaType: 'BlogPosting', serviceDoc: '/seo-graph/' },
2547
+ { path: '/schema/page.json', schemaType: 'WebPage', serviceDoc: '/seo-graph/' },
2548
+ ],
2549
+ schemaMap: { path: '/schemamap.xml', serviceDoc: '/seo-graph/' },
2550
+ additional: [
2551
+ { anchor: '/ask', serviceDoc: '/ask-docs/', type: 'https://schema.org/SearchAction' },
2552
+ ],
2553
+ });
2554
+ ```
2555
+
2556
+ Output is `application/linkset+json` ([RFC 9264](https://www.rfc-editor.org/rfc/rfc9264)).
2557
+ `schemaEndpoints` get auto-typed as `https://schema.org/<schemaType>`;
2558
+ `schemaMap` is emitted without a `type` (no standard one exists);
2559
+ `additional` accepts free-form `anchor`/`serviceDoc`/`type` (each
2560
+ either `string` or `string[]`). Relative paths are absolutized against
2561
+ `siteUrl`; absolute URLs pass through unchanged.
2562
+
2563
+ The package also exports a `CATALOG_PATH` constant
2564
+ (`'/.well-known/api-catalog'`) so callers can reference the canonical
2565
+ location without duplicating the string.
2566
+
2567
+ **Options:**
2568
+ | Option | Type | Default | Purpose |
2569
+ |---|---|---|---|
2570
+ | `siteUrl` | `string` | — | Canonical origin. Trailing slash stripped. |
2571
+ | `schemaEndpoints` | `ApiCatalogSchemaEndpointEntry[]` | — | Schema.org JSON endpoints with `path` + `schemaType`. |
2572
+ | `schemaMap` | `ApiCatalogSchemaMapEntry` | — | Path of the `createSchemaMap` route. |
2573
+ | `additional` | `ApiCatalogEntry[]` | — | Site-specific APIs not covered by the package factories. |
2574
+ | `cacheControl` | `string \| null` | `'max-age=300'` | Cache-Control header. `null` to omit. |
2575
+ | `contentType` | `string` | `'application/linkset+json'` | Override only with reason. |
2576
+ | `indent` | `number` | `2` | JSON indentation. `0` for compact. |
2577
+
2578
+ ### Last-modified dates from git
2579
+
2580
+ `gitLastmod` reads the committer date of the most recent git commit
2581
+ that touched a file, with configurable `excludeCommits` (skip bulk
2582
+ imports / reformats / renames) and `depth` (how many commits to
2583
+ inspect). Use it to feed `dateModified` on JSON-LD pieces or
2584
+ `<lastmod>` in sitemaps without trusting filesystem `mtime` (which
2585
+ gets rewritten on every CI checkout):
2586
+
2587
+ ```ts
2588
+ import { gitLastmod } from '@jdevalk/astro-seo-graph';
2589
+
2590
+ const last = gitLastmod(`src/content/blog/${entry.id}/index.md`, {
2591
+ excludeCommits: ['52130a9', '989dc47'],
2592
+ depth: 20,
2593
+ });
2594
+ ```
2595
+
2596
+ Returns `null` when the file has no git history, git isn't on the
2597
+ PATH, or every commit in the inspected window is excluded — fall back
2598
+ to `publishDate` in that case. `excludeCommits` matches on the first 7
2599
+ characters of the SHA, so short hashes from `git log --oneline` work
2600
+ directly. Build-time only — shells out to the `git` binary via
2601
+ `execFileSync`.
2491
2602
 
2492
2603
  ### The `aggregate` function
2493
2604
 
@@ -2544,18 +2655,24 @@ const blog = defineCollection({
2544
2655
  }
2545
2656
  ```
2546
2657
 
2547
- ### `buildAstroSeoProps`
2658
+ ### `buildSeoContext`
2548
2659
 
2549
- Pure-TS function that powers `<Seo>` internally. Use when you want to feed a
2550
- different head component:
2660
+ Pure-TS function that powers `<Seo>` internally. Returns a flat,
2661
+ render-ready normalization of `SeoProps` (resolved title, canonical, OG
2662
+ fields, hreflang entries, robots directives, twitter overrides). Use when
2663
+ you want to render the head yourself:
2551
2664
 
2552
2665
  ```ts
2553
- import { buildAstroSeoProps } from '@jdevalk/astro-seo-graph';
2666
+ import { buildSeoContext } from '@jdevalk/astro-seo-graph';
2554
2667
 
2555
- const astroSeoProps = buildAstroSeoProps(mySeoProps, Astro.url.href);
2556
- // Pass to astro-seo's <SEO> or any custom head component
2668
+ const ctx = buildSeoContext(mySeoProps, Astro.url.href);
2669
+ // ctx.title, ctx.canonical, ctx.og.*, ctx.twitter, ctx.hreflangs, ...
2557
2670
  ```
2558
2671
 
2672
+ The companion constant `ROBOTS_EXTRAS` exposes the
2673
+ `max-snippet:-1, max-image-preview:large, max-video-preview:-1`
2674
+ directives that `<Seo>` always appends to the robots tag.
2675
+
2559
2676
  ### `buildAlternateLinks`
2560
2677
 
2561
2678
  Pure helper for hreflang link generation. No Astro runtime needed — safe
@@ -2576,6 +2693,200 @@ const links = buildAlternateLinks({
2576
2693
 
2577
2694
  ---
2578
2695
 
2696
+ ## Build-time integration
2697
+
2698
+ `@jdevalk/astro-seo-graph/integration` exports a default `seoGraph()`
2699
+ function that returns an Astro integration. It hooks `astro:build:done`
2700
+ to run cross-page SEO checks and optional post-build actions against the
2701
+ static HTML output. SSR pages aren't on disk at build time, so they're
2702
+ not checked.
2703
+
2704
+ ```js
2705
+ // astro.config.mjs
2706
+ import { defineConfig } from 'astro/config';
2707
+ import seoGraph from '@jdevalk/astro-seo-graph/integration';
2708
+
2709
+ export default defineConfig({
2710
+ site: 'https://example.com',
2711
+ integrations: [
2712
+ seoGraph({
2713
+ // All options below are optional; listed with defaults.
2714
+ validateH1: true,
2715
+ validateUniqueMetadata: true,
2716
+ validateImageAlt: true,
2717
+ validateMetadataLength: true, // or { title: { max: 60 }, ... }
2718
+ validateInternalLinks: true, // or { skip: (href) => href.startsWith('/api/') }
2719
+ // indexNow: { ... },
2720
+ // llmsTxt: { ... },
2721
+ }),
2722
+ ],
2723
+ });
2724
+ ```
2725
+
2726
+ ### Options
2727
+
2728
+ | Option | Default | Purpose |
2729
+ | ------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
2730
+ | `validateH1` | `true` | Warn when a page has zero or >1 `<h1>` elements. |
2731
+ | `validateUniqueMetadata` | `true` | Warn when two pages share the same `<title>` or meta description. |
2732
+ | `validateImageAlt` | `true` | Warn when a page has `<img>` tags missing an `alt` attribute. |
2733
+ | `validateMetadataLength` | `true` | Warn when `<title>` or description length falls outside SERP-friendly bounds (title 30–65, description 70–200 by default; overridable per-field). |
2734
+ | `validateInternalLinks` | `true` | Warn on trailing-slash mismatches and links to pages not in the build. Accepts `{ skip }` to exclude SSR-only routes. |
2735
+ | `indexNow` | — | Submit built URLs to IndexNow. Omit to disable. |
2736
+ | `llmsTxt` | — | Generate `llms.txt` at the root of the build output. Omit to disable. |
2737
+
2738
+ ### H1 and metadata validation
2739
+
2740
+ `validateH1` flags pages with missing or duplicate `<h1>` elements — the
2741
+ most common on-page SEO/accessibility miss. `validateUniqueMetadata`
2742
+ flags `<title>` or `<meta name="description">` values that repeat across
2743
+ pages; duplicates hurt Google's ability to pick a canonical result and
2744
+ can only be spotted across the whole corpus. `validateImageAlt` flags
2745
+ `<img>` tags missing an `alt` attribute. WCAG-sanctioned decorative
2746
+ markers are respected: `alt=""` (the canonical pattern) and
2747
+ `role="presentation"`/`role="none"` (removes the image from the
2748
+ accessibility tree) are not flagged. Only a tag with neither triggers a
2749
+ warning.
2750
+
2751
+ `validateMetadataLength` flags `<title>` and `<meta name="description">`
2752
+ values outside the configured bounds. Pass `true` for the defaults
2753
+ (title 30–65, description 70–200) or an object to override per-field:
2754
+
2755
+ ```ts
2756
+ validateMetadataLength: {
2757
+ title: { min: 40, max: 60 },
2758
+ description: { max: 160 },
2759
+ },
2760
+ ```
2761
+
2762
+ `validateInternalLinks` scans `<a href>` values across every built page
2763
+ and flags two classes of issue: trailing-slash mismatches (e.g. linking
2764
+ to `/about-me` when the built page is `/about-me/` — "works" via
2765
+ redirect but wastes a round-trip on every click) and true 404s (links
2766
+ to paths not in the build). Only same-origin (via `config.site`) and
2767
+ root-relative links are checked; external URLs, `mailto:`, `tel:`, and
2768
+ fragment-only links are skipped.
2769
+
2770
+ Explicit redirects are honored by default: sources in
2771
+ `public/_redirects` (Netlify / Cloudflare Pages format) and literal
2772
+ keys in Astro's `redirects` config are treated as valid link targets.
2773
+ Dynamic rules (`*`, `:splat`, `[slug]` params) are skipped — use
2774
+ `skip` for those cases. Set `honorRedirects: false` to opt out (useful
2775
+ when auditing for redirect hops).
2776
+
2777
+ Pass `{ skip: (href) => boolean }` to exclude SSR-only routes or paths
2778
+ handled at the host/CDN layer.
2779
+
2780
+ The extractors, helpers, and the length resolver are exported for reuse:
2781
+
2782
+ ```ts
2783
+ import {
2784
+ classifyInternalLink,
2785
+ collectAstroRedirectSources,
2786
+ countH1s,
2787
+ DEFAULT_METADATA_LENGTH_BOUNDS,
2788
+ extractAnchorHrefs,
2789
+ extractTitle,
2790
+ extractMetaDescription,
2791
+ findImagesWithoutAlt,
2792
+ htmlFileToPath,
2793
+ parseNetlifyRedirects,
2794
+ resolveMetadataLengthBounds,
2795
+ } from '@jdevalk/astro-seo-graph/integration';
2796
+ ```
2797
+
2798
+ ### IndexNow submission
2799
+
2800
+ ```js
2801
+ seoGraph({
2802
+ indexNow: {
2803
+ key: process.env.INDEXNOW_KEY!, // 8–128 chars from [A-Za-z0-9-]
2804
+ host: 'example.com',
2805
+ siteUrl: 'https://example.com',
2806
+ // keyLocation?: defaults to https://<host>/<key>.txt
2807
+ // endpoint?: defaults to api.indexnow.org
2808
+ // filter?: (url) => boolean — composed on top of the built-in
2809
+ // /404 exclusion. Example skipping paginated archives:
2810
+ // filter: (url) => !/^\/blog\/\d+\/$/.test(new URL(url).pathname),
2811
+ },
2812
+ });
2813
+ ```
2814
+
2815
+ Only URLs on `host` are submitted. `index.html` paths are rewritten to
2816
+ their trailing-slash form. `/404` (and `/404/`) are always excluded —
2817
+ search engines don't need to be notified about the 404 page and
2818
+ submitting it wastes daily IndexNow quota.
2819
+
2820
+ **Deploy the key file first.** IndexNow verifies ownership by fetching
2821
+ `https://<host>/<key>.txt` on every submission. Submissions sent before
2822
+ the key is reachable in production get rejected (HTTP 403) and the key
2823
+ is treated as invalid — you'll have to rotate it. Serve the key via
2824
+ `createIndexNowKeyRoute` (see below), deploy, confirm the `.txt` loads
2825
+ over HTTPS, _then_ enable `indexNow` in the integration.
2826
+
2827
+ ```ts
2828
+ // src/pages/[your-key-here].txt.ts
2829
+ import { createIndexNowKeyRoute } from '@jdevalk/astro-seo-graph';
2830
+
2831
+ export const GET = createIndexNowKeyRoute({ key: 'your-key-here' });
2832
+ ```
2833
+
2834
+ The filename (minus `.txt.ts`) must equal the key.
2835
+
2836
+ ### llms.txt generation
2837
+
2838
+ Generates an [`llms.txt`](https://llmstxt.org) file — a markdown summary
2839
+ of the site that LLMs can use as a concise entry point.
2840
+
2841
+ ```js
2842
+ seoGraph({
2843
+ llmsTxt: {
2844
+ title: 'Example Site',
2845
+ siteUrl: 'https://example.com',
2846
+ summary: 'A demo site about X, Y, and Z.',
2847
+ // details?: extra paragraphs between summary and sections
2848
+ // sections?: user-supplied sections (skips auto-collection)
2849
+ // filter?: (url) => boolean — drop URLs from auto-section
2850
+ // autoSectionName?: defaults to 'Pages'
2851
+ // outputPath?: defaults to 'llms.txt'
2852
+ },
2853
+ });
2854
+ ```
2855
+
2856
+ By default, one "Pages" section is auto-generated from every built HTML
2857
+ file's `<title>` + meta description. Supply `sections` to take full
2858
+ control of the structure:
2859
+
2860
+ ```js
2861
+ llmsTxt: {
2862
+ title: 'Example Site',
2863
+ siteUrl: 'https://example.com',
2864
+ sections: [
2865
+ {
2866
+ name: 'Docs',
2867
+ links: [
2868
+ { url: 'https://example.com/docs/intro/', title: 'Intro', description: 'Start here' },
2869
+ ],
2870
+ },
2871
+ { name: 'Blog', links: [/* ... */] },
2872
+ ],
2873
+ }
2874
+ ```
2875
+
2876
+ The renderer is also exported for non-Astro contexts:
2877
+
2878
+ ```ts
2879
+ import { renderLlmsTxt } from '@jdevalk/astro-seo-graph';
2880
+
2881
+ const markdown = renderLlmsTxt({
2882
+ title: 'Example',
2883
+ summary: 'A demo site.',
2884
+ sections: [{ name: 'Pages', links: [{ url: 'https://x/', title: 'Home' }] }],
2885
+ });
2886
+ ```
2887
+
2888
+ ---
2889
+
2579
2890
  ## Complete integration example
2580
2891
 
2581
2892
  Here's how joost.blog wires everything together. Use this as a reference
@@ -3095,7 +3406,8 @@ seo-graph/
3095
3406
  │ ├── alternates.ts # buildAlternateLinks
3096
3407
  │ ├── content.ts # seoSchema, imageSchema
3097
3408
  │ └── components/
3098
- └── seo-props.ts # buildAstroSeoProps
3409
+ ├── seo-props.ts # SeoProps interface
3410
+ │ └── seo-context.ts # buildSeoContext
3099
3411
  ├── AGENTS.md # This file
3100
3412
  ├── README.md # Project overview
3101
3413
  └── pnpm-workspace.yaml
package/README.md CHANGED
@@ -35,14 +35,19 @@ npm install @jdevalk/seo-graph-core
35
35
  | `assembleGraph(pieces)` | Wraps pieces in a `{ @context, @graph }` envelope with first-wins deduplication by `@id`. |
36
36
  | `deduplicateByGraphId(entities)` | The dedup engine on its own, in case you need custom assembly. |
37
37
 
38
+ Pass `{ warnOnDanglingReferences: true }` to `assembleGraph` to emit a
39
+ console warning when any `{ '@id': '...' }` reference doesn't resolve to
40
+ an entity in the graph — a cheap sanity check to catch typos and
41
+ missing pieces during development.
42
+
38
43
  ### IndexNow
39
44
 
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. |
45
+ | API | Purpose |
46
+ | --------------------------- | ------------------------------------------------------------------------------ |
47
+ | `submitToIndexNow` | POST URLs to the IndexNow aggregator. Filters by host, dedupes, chunks at 10k. |
48
+ | `generateIndexNowKey` | Generate a random hex key (Web Crypto). |
49
+ | `validateIndexNowKey` | Verify a key is 8–128 chars from `[A-Za-z0-9-]` (IndexNow spec allow-list). |
50
+ | `getIndexNowKeyFileContent` | Body to serve at `/<key>.txt` for host verification. |
46
51
 
47
52
  ### Piece builders
48
53
 
@@ -164,6 +169,12 @@ network errors — it returns one result per chunk with `ok`, `status`, and
164
169
  `message`. Pass `endpoint` to override the default aggregator, `keyLocation`
165
170
  to point at a non-default key-file path, or `fetch` for testing.
166
171
 
172
+ > **Deploy the key file before submitting.** IndexNow verifies host
173
+ > ownership by fetching `https://<host>/<key>.txt` (or `keyLocation`)
174
+ > on every call. Submissions sent before the file is publicly reachable
175
+ > are rejected with HTTP 403 and the key is marked invalid — rotate
176
+ > with `generateIndexNowKey()` if that happens.
177
+
167
178
  ## Why
168
179
 
169
180
  Read more about [why this project exists](https://joost.blog/seo-graph/).
@@ -5,7 +5,7 @@ export declare const INDEXNOW_MAX_URLS_PER_REQUEST = 10000;
5
5
  export interface SubmitToIndexNowOptions {
6
6
  /** Bare host, e.g. `example.com`. No scheme, no trailing slash. */
7
7
  host: string;
8
- /** 8–128 hex-character key. Must match the key file served on the host. */
8
+ /** 8–128 character key from `[A-Za-z0-9-]`. Must match the key file served on the host. */
9
9
  key: string;
10
10
  /**
11
11
  * Optional absolute URL where the key file is hosted. Defaults to
@@ -36,7 +36,7 @@ export interface IndexNowSubmitResult {
36
36
  * per chunk; a single-chunk submission returns a one-element array.
37
37
  */
38
38
  export declare function submitToIndexNow(options: SubmitToIndexNowOptions): Promise<IndexNowSubmitResult[]>;
39
- /** Returns true when `key` is 8–128 hexadecimal characters. */
39
+ /** Returns true when `key` is 8–128 characters drawn from `[A-Za-z0-9-]`. */
40
40
  export declare function validateIndexNowKey(key: string): boolean;
41
41
  /**
42
42
  * Generate a cryptographically random IndexNow key of `length` hex
@@ -1 +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"}
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;AAapD,MAAM,WAAW,uBAAuB;IACpC,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAC;IACb,2FAA2F;IAC3F,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,6EAA6E;AAC7E,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 CHANGED
@@ -8,7 +8,13 @@ export const INDEXNOW_MAX_URLS_PER_REQUEST = 10_000;
8
8
  /** Key length constraints per the spec. */
9
9
  const MIN_KEY_LENGTH = 8;
10
10
  const MAX_KEY_LENGTH = 128;
11
- const HEX_KEY_RE = /^[a-f0-9]+$/i;
11
+ // Per the IndexNow spec the key may contain [A-Za-z0-9-]. The wording at
12
+ // indexnow.org calls these "hexadecimal characters" but the explicit
13
+ // allow-list is the broader set, and Bing / Yandex both issue and accept
14
+ // non-hex keys (Yandex's own docs use a mixed-case example).
15
+ // Refs: https://www.indexnow.org/documentation
16
+ // https://yandex.com/support/webmaster/en/indexnow/key
17
+ const KEY_RE = /^[A-Za-z0-9-]+$/;
12
18
  /**
13
19
  * Submit URLs to IndexNow. Filters out URLs not on `host`, deduplicates,
14
20
  * and chunks at {@link INDEXNOW_MAX_URLS_PER_REQUEST}. Returns one result
@@ -17,7 +23,7 @@ const HEX_KEY_RE = /^[a-f0-9]+$/i;
17
23
  export async function submitToIndexNow(options) {
18
24
  const { host, key, urls } = options;
19
25
  if (!validateIndexNowKey(key)) {
20
- throw new Error(`IndexNow key must be ${MIN_KEY_LENGTH}–${MAX_KEY_LENGTH} hex characters.`);
26
+ throw new Error(`IndexNow key must be ${MIN_KEY_LENGTH}–${MAX_KEY_LENGTH} characters from [A-Za-z0-9-].`);
21
27
  }
22
28
  if (!host || host.includes('/') || host.includes(':')) {
23
29
  throw new Error(`IndexNow host must be a bare host (got: ${host}).`);
@@ -86,13 +92,13 @@ function filterUrlsForHost(urls, host) {
86
92
  }
87
93
  return out;
88
94
  }
89
- /** Returns true when `key` is 8–128 hexadecimal characters. */
95
+ /** Returns true when `key` is 8–128 characters drawn from `[A-Za-z0-9-]`. */
90
96
  export function validateIndexNowKey(key) {
91
97
  if (typeof key !== 'string')
92
98
  return false;
93
99
  if (key.length < MIN_KEY_LENGTH || key.length > MAX_KEY_LENGTH)
94
100
  return false;
95
- return HEX_KEY_RE.test(key);
101
+ return KEY_RE.test(key);
96
102
  }
97
103
  /**
98
104
  * Generate a cryptographically random IndexNow key of `length` hex
@@ -117,7 +123,7 @@ export function generateIndexNowKey(length = 32) {
117
123
  */
118
124
  export function getIndexNowKeyFileContent(key) {
119
125
  if (!validateIndexNowKey(key)) {
120
- throw new Error(`IndexNow key must be ${MIN_KEY_LENGTH}–${MAX_KEY_LENGTH} hex characters.`);
126
+ throw new Error(`IndexNow key must be ${MIN_KEY_LENGTH}–${MAX_KEY_LENGTH} characters from [A-Za-z0-9-].`);
121
127
  }
122
128
  return key;
123
129
  }
@@ -1 +1 @@
1
- {"version":3,"file":"indexnow.js","sourceRoot":"","sources":["../src/indexnow.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"indexnow.js","sourceRoot":"","sources":["../src/indexnow.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,oEAAoE;AACpE,6EAA6E;AAE7E,2EAA2E;AAC3E,MAAM,CAAC,MAAM,yBAAyB,GAAG,mCAAmC,CAAC;AAE7E,2DAA2D;AAC3D,MAAM,CAAC,MAAM,6BAA6B,GAAG,MAAM,CAAC;AAEpD,2CAA2C;AAC3C,MAAM,cAAc,GAAG,CAAC,CAAC;AACzB,MAAM,cAAc,GAAG,GAAG,CAAC;AAC3B,yEAAyE;AACzE,qEAAqE;AACrE,yEAAyE;AACzE,6DAA6D;AAC7D,+CAA+C;AAC/C,6DAA6D;AAC7D,MAAM,MAAM,GAAG,iBAAiB,CAAC;AAgCjC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAClC,OAAgC;IAEhC,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC;IACpC,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CACX,wBAAwB,cAAc,IAAI,cAAc,gCAAgC,CAC3F,CAAC;IACN,CAAC;IACD,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,KAAK,CAAC,2CAA2C,IAAI,IAAI,CAAC,CAAC;IACzE,CAAC;IAED,MAAM,QAAQ,GAAG,iBAAiB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC/C,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAErC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,yBAAyB,CAAC;IAC/D,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,WAAW,IAAI,IAAI,GAAG,MAAM,CAAC;IACxE,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,KAAK,CAAC;IAEvC,MAAM,OAAO,GAA2B,EAAE,CAAC;IAC3C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,6BAA6B,EAAE,CAAC;QACtE,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,6BAA6B,CAAC,CAAC;QACnE,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QACxE,IAAI,CAAC;YACD,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,QAAQ,EAAE;gBAChC,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACL,cAAc,EAAE,iCAAiC;oBACjD,MAAM,EAAE,kBAAkB;iBAC7B;gBACD,IAAI;aACP,CAAC,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;YAC9C,OAAO,CAAC,IAAI,CAAC;gBACT,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,EAAE,EAAE,GAAG,CAAC,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;gBAC5C,OAAO,EAAE,IAAI;gBACb,SAAS,EAAE,KAAK,CAAC,MAAM;aAC1B,CAAC,CAAC;QACP,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACX,OAAO,CAAC,IAAI,CAAC;gBACT,MAAM,EAAE,CAAC;gBACT,EAAE,EAAE,KAAK;gBACT,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;gBACzD,SAAS,EAAE,KAAK,CAAC,MAAM;aAC1B,CAAC,CAAC;QACP,CAAC;IACL,CAAC;IACD,OAAO,OAAO,CAAC;AACnB,CAAC;AAED;;;GAGG;AACH,SAAS,iBAAiB,CAAC,IAAuB,EAAE,IAAY;IAC5D,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IACrC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,MAAW,CAAC;QAChB,IAAI,CAAC;YACD,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QAC1B,CAAC;QAAC,MAAM,CAAC;YACL,SAAS;QACb,CAAC;QACD,IAAI,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,KAAK,SAAS;YAAE,SAAS;QAC1D,MAAM,UAAU,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC;QACrC,IAAI,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC;YAAE,SAAS;QACnC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACrB,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACzB,CAAC;IACD,OAAO,GAAG,CAAC;AACf,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC3C,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC1C,IAAI,GAAG,CAAC,MAAM,GAAG,cAAc,IAAI,GAAG,CAAC,MAAM,GAAG,cAAc;QAAE,OAAO,KAAK,CAAC;IAC7E,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC5B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAM,GAAG,EAAE;IAC3C,IAAI,MAAM,GAAG,cAAc,IAAI,MAAM,GAAG,cAAc,IAAI,MAAM,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;QACzE,MAAM,IAAI,KAAK,CACX,sDAAsD,cAAc,QAAQ,cAAc,GAAG,CAChG,CAAC;IACN,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACzC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;IAC9B,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,GAAG,IAAI,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC9D,OAAO,GAAG,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CAAC,GAAW;IACjD,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CACX,wBAAwB,cAAc,IAAI,cAAc,gCAAgC,CAC3F,CAAC;IACN,CAAC;IACD,OAAO,GAAG,CAAC;AACf,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jdevalk/seo-graph-core",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Pure schema.org JSON-LD graph builders. Runtime-agnostic core for agent-ready SEO.",
5
5
  "keywords": [
6
6
  "schema.org",