@jdevalk/astro-seo-graph 1.3.0 → 1.4.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 CHANGED
@@ -2516,10 +2516,89 @@ export const GET = createSchemaMap({
2516
2516
  **Schema map entry options:**
2517
2517
  | Field | Type | Default | Purpose |
2518
2518
  |---|---|---|---|
2519
- | `path` | `string` | — | Relative path to schema endpoint |
2520
- | `lastModified` | `Date` | — | Last modification date |
2521
- | `changeFreq` | Sitemap frequency string | `'daily'` | Update frequency hint |
2522
- | `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`.
2523
2602
 
2524
2603
  ### The `aggregate` function
2525
2604
 
package/README.md CHANGED
@@ -32,6 +32,8 @@ schema.org best practices — see [AGENTS.md](https://github.com/jdevalk/seo-gra
32
32
  | **`createIndexNowKeyRoute`** | Factory returning an `APIRoute` that serves the IndexNow key-verification file at `/<key>.txt`. Pair with the `indexNow` option on the integration to auto-submit built URLs on `astro:build:done`. |
33
33
  | **`createMarkdownEndpoint`** | Factory returning an `APIRoute` that serves a clean markdown version of a content-collection entry (frontmatter + body + token count). Pair with the auto-emitted `<link rel="alternate" type="text/markdown">` on `<Seo>` for AI-agent discovery. |
34
34
  | **`renderMarkdownAlternate`** | Pure renderer behind the endpoint. Importable from non-Astro code for the same markdown output. |
35
+ | **`gitLastmod`** | Reads the committer date of the most recent git commit that touched a file, skipping caller-supplied bulk commits. Use it to derive trustworthy `dateModified` / `<lastmod>` values from git history. |
36
+ | **`createApiCatalog`** | Factory returning an `APIRoute` that serves an [RFC 9727](https://www.rfc-editor.org/rfc/rfc9727) API catalog at `/.well-known/api-catalog`. Lists schema endpoints, the schema map, and any additional APIs as `application/linkset+json`. |
35
37
 
36
38
  ## Installation
37
39
 
@@ -396,6 +398,50 @@ const { markdown, tokenCount, canonicalHref } = renderMarkdownAlternate({
396
398
  });
397
399
  ```
398
400
 
401
+ ## Last-modified dates from git
402
+
403
+ `gitLastmod` reads the committer date of the most recent git commit that
404
+ touched a file. Use it to feed `dateModified` on JSON-LD pieces or
405
+ `<lastmod>` in sitemaps without trusting filesystem `mtime` (which gets
406
+ rewritten on every CI checkout).
407
+
408
+ ```ts
409
+ import { gitLastmod } from '@jdevalk/astro-seo-graph';
410
+
411
+ const last = gitLastmod(`src/content/blog/${entry.id}/index.md`, {
412
+ // Hashes of bulk commits (imports, reformats, renames) that
413
+ // shouldn't count as a "real" content update. Short or full SHAs.
414
+ excludeCommits: ['52130a9', '989dc47'],
415
+ // How many commits back to inspect. Default: 10.
416
+ depth: 20,
417
+ });
418
+ ```
419
+
420
+ Returns `null` when the file has no git history, when git isn't on the
421
+ PATH, or when every commit in the inspected window is excluded — callers
422
+ should fall back to `publishDate` (or skip the field) in that case.
423
+
424
+ `excludeCommits` matches on the first 7 characters of the SHA, so short
425
+ hashes from `git log --oneline` work directly. Increase `depth` if your
426
+ exclusion list could shadow the real last-modified commit; the default
427
+ of 10 is enough for most blogs.
428
+
429
+ A common pattern is to use `gitLastmod` to compute an effective
430
+ "updated" date only when it materially differs from the publish date:
431
+
432
+ ```ts
433
+ function computeUpdatedDate(filePath: string, publishDate: Date): Date | null {
434
+ const last = gitLastmod(filePath);
435
+ if (!last) return null;
436
+ const ONE_DAY = 24 * 60 * 60 * 1000;
437
+ return last.getTime() - publishDate.getTime() > ONE_DAY ? last : null;
438
+ }
439
+ ```
440
+
441
+ `gitLastmod` shells out to the `git` binary, so it only works during
442
+ build (Node) — not in browser bundles or edge runtimes that lack
443
+ `child_process`. Resolve the path relative to your build CWD.
444
+
399
445
  ## Schema map discovery
400
446
 
401
447
  ```ts
@@ -411,6 +457,60 @@ export const GET = createSchemaMap({
411
457
  });
412
458
  ```
413
459
 
460
+ ## API catalog (RFC 9727)
461
+
462
+ [RFC 9727](https://www.rfc-editor.org/rfc/rfc9727) defines
463
+ `/.well-known/api-catalog` as the standard discovery point for a site's
464
+ APIs. `createApiCatalog` returns an `APIRoute` that serves an
465
+ `application/linkset+json` document ([RFC 9264](https://www.rfc-editor.org/rfc/rfc9264))
466
+ listing each endpoint with its anchor URL, optional documentation
467
+ links, and optional type pointers.
468
+
469
+ ```ts
470
+ // src/pages/.well-known/api-catalog.ts
471
+ import { createApiCatalog } from '@jdevalk/astro-seo-graph';
472
+
473
+ export const GET = createApiCatalog({
474
+ siteUrl: 'https://example.com',
475
+ schemaEndpoints: [
476
+ { path: '/schema/post.json', schemaType: 'BlogPosting', serviceDoc: '/seo-graph/' },
477
+ { path: '/schema/page.json', schemaType: 'WebPage', serviceDoc: '/seo-graph/' },
478
+ ],
479
+ schemaMap: { path: '/schemamap.xml', serviceDoc: '/seo-graph/' },
480
+ additional: [
481
+ {
482
+ anchor: '/ask',
483
+ serviceDoc: '/ask-docs/',
484
+ type: 'https://schema.org/SearchAction',
485
+ },
486
+ ],
487
+ });
488
+ ```
489
+
490
+ Three categories of entry:
491
+
492
+ - **`schemaEndpoints`** — the same set you wired into `createSchemaEndpoint`. Each becomes a linkset entry with `type: [{ href: 'https://schema.org/<schemaType>' }]` filled in for you.
493
+ - **`schemaMap`** — the path of the route from `createSchemaMap`. Emitted without a `type` (no standard type exists for the schemamap format).
494
+ - **`additional`** — site-specific APIs not covered by the package's own factories. Pass `anchor`, optional `serviceDoc`, and optional `type` (each accepting `string` or `string[]`).
495
+
496
+ Relative paths in any field are absolutized against `siteUrl`; absolute
497
+ URLs pass through unchanged. The trailing slash on `siteUrl` is
498
+ stripped, mirroring `createSchemaMap`.
499
+
500
+ The package also exports a `CATALOG_PATH` constant
501
+ (`'/.well-known/api-catalog'`) for callers who want to reference the
502
+ catalog from `_headers` files, the schemamap, or documentation links
503
+ without duplicating the string.
504
+
505
+ Response headers:
506
+
507
+ - `Content-Type: application/linkset+json`
508
+ - `Cache-Control: max-age=300` (override or pass `null` to omit)
509
+ - `X-Robots-Tag: noindex, follow`
510
+
511
+ Empty options yield `{ "linkset": [] }` — valid per RFC 9727 §3 and
512
+ useful for sites that want to declare "no APIs" explicitly.
513
+
414
514
  ## Zod content helpers
415
515
 
416
516
  ```ts
@@ -0,0 +1,93 @@
1
+ import type { APIRoute } from 'astro';
2
+ /**
3
+ * Standard path for the API catalog per RFC 9727. Exported so callers
4
+ * can reference it from `_headers` files, schemamap entries, or
5
+ * documentation links without duplicating the string.
6
+ */
7
+ export declare const CATALOG_PATH = "/.well-known/api-catalog";
8
+ export interface ApiCatalogEntry {
9
+ /**
10
+ * URL or path of the API endpoint. Relative paths are resolved
11
+ * against `siteUrl`; absolute URLs pass through unchanged.
12
+ */
13
+ anchor: string;
14
+ /**
15
+ * Documentation URL(s). Single string is normalized to a one-item
16
+ * array. Relative paths resolved against `siteUrl`.
17
+ */
18
+ serviceDoc?: string | readonly string[];
19
+ /**
20
+ * Type URL(s). For schema.org endpoints, a `https://schema.org/<Type>`
21
+ * URL. For OpenAPI-described services, the OAS document URL. Single
22
+ * string is normalized to a one-item array.
23
+ */
24
+ type?: string | readonly string[];
25
+ }
26
+ export interface ApiCatalogSchemaEndpointEntry {
27
+ /** Path of the endpoint, e.g. `/schema/post.json`. */
28
+ path: string;
29
+ /**
30
+ * Schema.org type emitted, e.g. `BlogPosting`. Becomes
31
+ * `https://schema.org/<Type>` in the `type` field.
32
+ */
33
+ schemaType: string;
34
+ /** Optional documentation URL. */
35
+ serviceDoc?: string;
36
+ }
37
+ export interface ApiCatalogSchemaMapEntry {
38
+ /** Path of the schemamap, e.g. `/schemamap.xml`. */
39
+ path: string;
40
+ /** Optional documentation URL. */
41
+ serviceDoc?: string;
42
+ }
43
+ export interface ApiCatalogOptions {
44
+ /** Canonical site URL. Trailing slash is stripped. */
45
+ siteUrl: string;
46
+ /**
47
+ * Schema.org JSON endpoints, typically the same set wired into
48
+ * `createSchemaEndpoint` calls. Each entry is emitted with
49
+ * `type: [{ href: "https://schema.org/<schemaType>" }]`.
50
+ */
51
+ schemaEndpoints?: readonly ApiCatalogSchemaEndpointEntry[];
52
+ /**
53
+ * Schema map endpoint, typically the path of the route created by
54
+ * `createSchemaMap`. Emitted without a `type` (no standard type
55
+ * exists for the schemamap format).
56
+ */
57
+ schemaMap?: ApiCatalogSchemaMapEntry;
58
+ /** Site-specific APIs not covered by the package's own factories. */
59
+ additional?: readonly ApiCatalogEntry[];
60
+ /** Defaults to `max-age=300`. Pass `null` to omit. */
61
+ cacheControl?: string | null;
62
+ /**
63
+ * Content-Type header. Defaults to `application/linkset+json` per
64
+ * RFC 9727 / RFC 9264. Override only if you have a reason.
65
+ */
66
+ contentType?: string;
67
+ /** JSON indentation. Defaults to `2`. Pass `0` for compact output. */
68
+ indent?: number;
69
+ }
70
+ /**
71
+ * Returns an Astro `APIRoute` that serves an RFC 9727 API catalog
72
+ * (`application/linkset+json`, RFC 9264) at `/.well-known/api-catalog`.
73
+ *
74
+ * Lists the site's APIs — schema.org JSON endpoints, the schema map,
75
+ * and any additional services — with anchor URLs, optional service-doc
76
+ * URLs, and type pointers. Drop the returned handler into
77
+ * `src/pages/.well-known/api-catalog.ts`.
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * import { createApiCatalog } from '@jdevalk/astro-seo-graph';
82
+ *
83
+ * export const GET = createApiCatalog({
84
+ * siteUrl: 'https://example.com',
85
+ * schemaEndpoints: [
86
+ * { path: '/schema/post.json', schemaType: 'BlogPosting' },
87
+ * ],
88
+ * schemaMap: { path: '/schemamap.xml' },
89
+ * });
90
+ * ```
91
+ */
92
+ export declare function createApiCatalog(options: ApiCatalogOptions): APIRoute;
93
+ //# sourceMappingURL=api-catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-catalog.d.ts","sourceRoot":"","sources":["../src/api-catalog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEtC;;;;GAIG;AACH,eAAO,MAAM,YAAY,6BAA6B,CAAC;AAEvD,MAAM,WAAW,eAAe;IAC5B;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IACxC;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,6BAA6B;IAC1C,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,kCAAkC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,wBAAwB;IACrC,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAC9B,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,eAAe,CAAC,EAAE,SAAS,6BAA6B,EAAE,CAAC;IAC3D;;;;OAIG;IACH,SAAS,CAAC,EAAE,wBAAwB,CAAC;IACrC,qEAAqE;IACrE,UAAU,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IACxC,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAwBD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,QAAQ,CA6CrE"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Standard path for the API catalog per RFC 9727. Exported so callers
3
+ * can reference it from `_headers` files, schemamap entries, or
4
+ * documentation links without duplicating the string.
5
+ */
6
+ export const CATALOG_PATH = '/.well-known/api-catalog';
7
+ function absolutize(value, site) {
8
+ if (/^https?:\/\//i.test(value))
9
+ return value;
10
+ const path = value.startsWith('/') ? value : `/${value}`;
11
+ return `${site}${path}`;
12
+ }
13
+ function toHrefArray(value, site) {
14
+ if (value === undefined)
15
+ return undefined;
16
+ const arr = typeof value === 'string' ? [value] : value;
17
+ if (arr.length === 0)
18
+ return undefined;
19
+ return arr.map((v) => ({ href: absolutize(v, site) }));
20
+ }
21
+ /**
22
+ * Returns an Astro `APIRoute` that serves an RFC 9727 API catalog
23
+ * (`application/linkset+json`, RFC 9264) at `/.well-known/api-catalog`.
24
+ *
25
+ * Lists the site's APIs — schema.org JSON endpoints, the schema map,
26
+ * and any additional services — with anchor URLs, optional service-doc
27
+ * URLs, and type pointers. Drop the returned handler into
28
+ * `src/pages/.well-known/api-catalog.ts`.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * import { createApiCatalog } from '@jdevalk/astro-seo-graph';
33
+ *
34
+ * export const GET = createApiCatalog({
35
+ * siteUrl: 'https://example.com',
36
+ * schemaEndpoints: [
37
+ * { path: '/schema/post.json', schemaType: 'BlogPosting' },
38
+ * ],
39
+ * schemaMap: { path: '/schemamap.xml' },
40
+ * });
41
+ * ```
42
+ */
43
+ export function createApiCatalog(options) {
44
+ const cacheControl = options.cacheControl === undefined ? 'max-age=300' : options.cacheControl;
45
+ const contentType = options.contentType ?? 'application/linkset+json';
46
+ const indent = options.indent ?? 2;
47
+ const site = options.siteUrl.replace(/\/+$/, '');
48
+ return async () => {
49
+ const linkset = [];
50
+ if (options.schemaEndpoints) {
51
+ for (const e of options.schemaEndpoints) {
52
+ const entry = { anchor: absolutize(e.path, site) };
53
+ const sd = toHrefArray(e.serviceDoc, site);
54
+ if (sd)
55
+ entry['service-doc'] = sd;
56
+ entry.type = [{ href: `https://schema.org/${e.schemaType}` }];
57
+ linkset.push(entry);
58
+ }
59
+ }
60
+ if (options.schemaMap) {
61
+ const entry = { anchor: absolutize(options.schemaMap.path, site) };
62
+ const sd = toHrefArray(options.schemaMap.serviceDoc, site);
63
+ if (sd)
64
+ entry['service-doc'] = sd;
65
+ linkset.push(entry);
66
+ }
67
+ if (options.additional) {
68
+ for (const e of options.additional) {
69
+ const entry = { anchor: absolutize(e.anchor, site) };
70
+ const sd = toHrefArray(e.serviceDoc, site);
71
+ if (sd)
72
+ entry['service-doc'] = sd;
73
+ const t = toHrefArray(e.type, site);
74
+ if (t)
75
+ entry.type = t;
76
+ linkset.push(entry);
77
+ }
78
+ }
79
+ const headers = {
80
+ 'Content-Type': contentType,
81
+ 'X-Robots-Tag': 'noindex, follow',
82
+ };
83
+ if (cacheControl !== null)
84
+ headers['Cache-Control'] = cacheControl;
85
+ return new Response(JSON.stringify({ linkset }, null, indent), { headers });
86
+ };
87
+ }
88
+ //# sourceMappingURL=api-catalog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-catalog.js","sourceRoot":"","sources":["../src/api-catalog.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,0BAA0B,CAAC;AA0EvD,SAAS,UAAU,CAAC,KAAa,EAAE,IAAY;IAC3C,IAAI,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9C,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC;IACzD,OAAO,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC;AAC5B,CAAC;AAED,SAAS,WAAW,CAChB,KAA6C,EAC7C,IAAY;IAEZ,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC1C,MAAM,GAAG,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IACxD,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACvC,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAA0B;IACvD,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC;IAC/F,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,0BAA0B,CAAC;IACtE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC;IACnC,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAEjD,OAAO,KAAK,IAAI,EAAE;QACd,MAAM,OAAO,GAAmB,EAAE,CAAC;QAEnC,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;YAC1B,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;gBACtC,MAAM,KAAK,GAAiB,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;gBACjE,MAAM,EAAE,GAAG,WAAW,CAAC,CAAC,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;gBAC3C,IAAI,EAAE;oBAAE,KAAK,CAAC,aAAa,CAAC,GAAG,EAAE,CAAC;gBAClC,KAAK,CAAC,IAAI,GAAG,CAAC,EAAE,IAAI,EAAE,sBAAsB,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;gBAC9D,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACxB,CAAC;QACL,CAAC;QAED,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;YACpB,MAAM,KAAK,GAAiB,EAAE,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;YACjF,MAAM,EAAE,GAAG,WAAW,CAAC,OAAO,CAAC,SAAS,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;YAC3D,IAAI,EAAE;gBAAE,KAAK,CAAC,aAAa,CAAC,GAAG,EAAE,CAAC;YAClC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxB,CAAC;QAED,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;YACrB,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;gBACjC,MAAM,KAAK,GAAiB,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC;gBACnE,MAAM,EAAE,GAAG,WAAW,CAAC,CAAC,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;gBAC3C,IAAI,EAAE;oBAAE,KAAK,CAAC,aAAa,CAAC,GAAG,EAAE,CAAC;gBAClC,MAAM,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;gBACpC,IAAI,CAAC;oBAAE,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC;gBACtB,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACxB,CAAC;QACL,CAAC;QAED,MAAM,OAAO,GAA2B;YACpC,cAAc,EAAE,WAAW;YAC3B,cAAc,EAAE,iBAAiB;SACpC,CAAC;QACF,IAAI,YAAY,KAAK,IAAI;YAAE,OAAO,CAAC,eAAe,CAAC,GAAG,YAAY,CAAC;QAEnE,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;IAChF,CAAC,CAAC;AACN,CAAC"}
@@ -0,0 +1,16 @@
1
+ export interface GitLastmodOptions {
2
+ /** Commit hashes (short or full) to skip when searching for the last meaningful change. */
3
+ excludeCommits?: string[];
4
+ /** How many commits to inspect. Defaults to 10. */
5
+ depth?: number;
6
+ }
7
+ /**
8
+ * Returns the committer date of the most recent git commit that touched
9
+ * `filePath`, skipping any commits listed in `excludeCommits`. Useful for
10
+ * generating accurate `<lastmod>` values in sitemaps when bulk commits (
11
+ * imports, reformats, renames) would otherwise produce misleading dates.
12
+ *
13
+ * Returns `null` when the file has no git history or git is unavailable.
14
+ */
15
+ export declare function gitLastmod(filePath: string, options?: GitLastmodOptions): Date | null;
16
+ //# sourceMappingURL=git-lastmod.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-lastmod.d.ts","sourceRoot":"","sources":["../src/git-lastmod.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,iBAAiB;IAC9B,2FAA2F;IAC3F,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,mDAAmD;IACnD,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,IAAI,GAAG,IAAI,CAkBzF"}
@@ -0,0 +1,31 @@
1
+ import { execFileSync } from 'child_process';
2
+ /**
3
+ * Returns the committer date of the most recent git commit that touched
4
+ * `filePath`, skipping any commits listed in `excludeCommits`. Useful for
5
+ * generating accurate `<lastmod>` values in sitemaps when bulk commits (
6
+ * imports, reformats, renames) would otherwise produce misleading dates.
7
+ *
8
+ * Returns `null` when the file has no git history or git is unavailable.
9
+ */
10
+ export function gitLastmod(filePath, options = {}) {
11
+ const { excludeCommits = [], depth = 10 } = options;
12
+ const excluded = new Set(excludeCommits.map((h) => h.slice(0, 7)));
13
+ try {
14
+ const log = execFileSync('git', ['log', `-${depth}`, '--format=%H\t%cI', '--', filePath], {
15
+ encoding: 'utf-8',
16
+ }).trim();
17
+ if (!log)
18
+ return null;
19
+ for (const line of log.split('\n')) {
20
+ const [hash, date] = line.split('\t');
21
+ if (!excluded.has(hash.slice(0, 7))) {
22
+ return date ? new Date(date) : null;
23
+ }
24
+ }
25
+ return null;
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ //# sourceMappingURL=git-lastmod.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-lastmod.js","sourceRoot":"","sources":["../src/git-lastmod.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAS7C;;;;;;;GAOG;AACH,MAAM,UAAU,UAAU,CAAC,QAAgB,EAAE,UAA6B,EAAE;IACxE,MAAM,EAAE,cAAc,GAAG,EAAE,EAAE,KAAK,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC;IACpD,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACnE,IAAI,CAAC;QACD,MAAM,GAAG,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,IAAI,KAAK,EAAE,EAAE,kBAAkB,EAAE,IAAI,EAAE,QAAQ,CAAC,EAAE;YACtF,QAAQ,EAAE,OAAO;SACpB,CAAC,CAAC,IAAI,EAAE,CAAC;QACV,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAC;QACtB,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACjC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAiC,CAAC;YACtE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAClC,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YACxC,CAAC;QACL,CAAC;QACD,OAAO,IAAI,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,IAAI,CAAC;IAChB,CAAC;AACL,CAAC"}
package/dist/index.d.ts CHANGED
@@ -18,4 +18,8 @@ export { renderMarkdownAlternate, deriveMdUrl } from './markdown-alternate.js';
18
18
  export type { MarkdownAlternateFrontmatter, RenderMarkdownAlternateOptions, RenderedMarkdownAlternate, } from './markdown-alternate.js';
19
19
  export { createMarkdownEndpoint } from './markdown-routes.js';
20
20
  export type { MarkdownEndpointOptions } from './markdown-routes.js';
21
+ export { gitLastmod } from './git-lastmod.js';
22
+ export type { GitLastmodOptions } from './git-lastmod.js';
23
+ export { createApiCatalog, CATALOG_PATH } from './api-catalog.js';
24
+ export type { ApiCatalogOptions, ApiCatalogEntry, ApiCatalogSchemaEndpointEntry, ApiCatalogSchemaMapEntry, } from './api-catalog.js';
21
25
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAE1E,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACpE,YAAY,EAAE,qBAAqB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE3F,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAE9D,YAAY,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC7E,YAAY,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,YAAY,EAAE,aAAa,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAE/E,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,YAAY,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAEhE,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAC9F,YAAY,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAEnF,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE/E,OAAO,EAAE,uBAAuB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC/E,YAAY,EACR,4BAA4B,EAC5B,8BAA8B,EAC9B,yBAAyB,GAC5B,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,YAAY,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAE1E,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACpE,YAAY,EAAE,qBAAqB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE3F,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAE9D,YAAY,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC7E,YAAY,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,YAAY,EAAE,aAAa,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAE/E,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,YAAY,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAEhE,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAC9F,YAAY,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAEnF,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE/E,OAAO,EAAE,uBAAuB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC/E,YAAY,EACR,4BAA4B,EAC5B,8BAA8B,EAC9B,yBAAyB,GAC5B,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,YAAY,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAEpE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAClE,YAAY,EACR,iBAAiB,EACjB,eAAe,EACf,6BAA6B,EAC7B,wBAAwB,GAC3B,MAAM,kBAAkB,CAAC"}
package/dist/index.js CHANGED
@@ -13,4 +13,6 @@ export { createIndexNowKeyRoute, submitToIndexNow, validateIndexNowKey } from '.
13
13
  export { renderLlmsTxt } from './llms-txt.js';
14
14
  export { renderMarkdownAlternate, deriveMdUrl } from './markdown-alternate.js';
15
15
  export { createMarkdownEndpoint } from './markdown-routes.js';
16
+ export { gitLastmod } from './git-lastmod.js';
17
+ export { createApiCatalog, CATALOG_PATH } from './api-catalog.js';
16
18
  //# 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,+EAA+E;AAC/E,EAAE;AACF,wEAAwE;AACxE,8EAA8E;AAC9E,sDAAsD;AAEtD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAG5C,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGpE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAG9D,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAG7E,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAGtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAGtD,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAG9F,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAG9C,OAAO,EAAE,uBAAuB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAO/E,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,EAAE;AACF,wEAAwE;AACxE,8EAA8E;AAC9E,sDAAsD;AAEtD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAG5C,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGpE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAG9D,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAG7E,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAGtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAGtD,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAG9F,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAG9C,OAAO,EAAE,uBAAuB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAO/E,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAG9D,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG9C,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jdevalk/astro-seo-graph",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Astro integration for @jdevalk/seo-graph-core. Seo component, route factories, content-collection aggregator, Zod content helpers.",
5
5
  "keywords": [
6
6
  "astro",