pagegraph 0.14.0 → 0.15.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.
Files changed (55) hide show
  1. package/README.md +169 -10
  2. package/dist/audit.d.ts +1 -1
  3. package/dist/audit.js +1 -1
  4. package/dist/{checks-BS-aDXB_.js → checks-BEhJPNzN.js} +12 -1
  5. package/dist/checks-BEhJPNzN.js.map +1 -0
  6. package/dist/{claims-Bch7Lxlo.d.ts → claims-CCf2_Jox.d.ts} +2 -2
  7. package/dist/{claims-Bv7voeEV.js → claims-CQlzze1K.js} +2 -2
  8. package/dist/{claims-Bv7voeEV.js.map → claims-CQlzze1K.js.map} +1 -1
  9. package/dist/claims.d.ts +2 -2
  10. package/dist/claims.js +1 -1
  11. package/dist/cli.js +1355 -616
  12. package/dist/cli.js.map +1 -1
  13. package/dist/config.d.ts +7 -3
  14. package/dist/config.js +1 -1
  15. package/dist/config.js.map +1 -1
  16. package/dist/{content-CHd0X4s6.js → content-D5jZprMq.js} +2 -2
  17. package/dist/{content-CHd0X4s6.js.map → content-D5jZprMq.js.map} +1 -1
  18. package/dist/{content-DgNr7apS.d.ts → content-DerWgQy9.d.ts} +11 -3
  19. package/dist/{dist-B6zg_Z3Z.js → dist-Br2oOB2Q.js} +2 -2
  20. package/dist/{dist-B6zg_Z3Z.js.map → dist-Br2oOB2Q.js.map} +1 -1
  21. package/dist/{document-CztEu3Vd.d.ts → document-D9P8yY5g.d.ts} +4 -4
  22. package/dist/{graph-D1PIndsC.d.ts → graph-ChrvU-6X.d.ts} +27 -4
  23. package/dist/{graph-DMNOLXLu.js → graph-UONEaqIX.js} +43 -5
  24. package/dist/graph-UONEaqIX.js.map +1 -0
  25. package/dist/{index-C5w9ripz.d.ts → index-DzjUmbHY.d.ts} +5 -5
  26. package/dist/index.d.ts +6 -6
  27. package/dist/index.js +5 -6
  28. package/dist/index.js.map +1 -1
  29. package/dist/{page-heads-BfjWQvau.d.ts → page-heads-DDz_2_hq.d.ts} +3 -2
  30. package/dist/{page-heads-BgbaIjj9.js → page-heads-YL8nyYjF.js} +4 -3
  31. package/dist/page-heads-YL8nyYjF.js.map +1 -0
  32. package/dist/{pi-Bvw6J0gh.js → pi-Bz4BgZdC.js} +207 -167
  33. package/dist/pi-Bz4BgZdC.js.map +1 -0
  34. package/dist/{projections-Bk-qOOsR.d.ts → projections-Dqo_ih-J.d.ts} +2 -2
  35. package/dist/react.d.ts +5 -3
  36. package/dist/react.js +43 -19
  37. package/dist/react.js.map +1 -1
  38. package/dist/{state-BuJP_3Lr.js → state-2ikjo7uI.js} +638 -25
  39. package/dist/state-2ikjo7uI.js.map +1 -0
  40. package/dist/tanstack-start/index.d.ts +1 -1
  41. package/dist/tanstack-start/index.js +6 -4
  42. package/dist/tanstack-start/index.js.map +1 -1
  43. package/dist/tanstack-start/react.d.ts +1 -1
  44. package/dist/tanstack-start/server.d.ts +2 -2
  45. package/dist/tanstack-start/server.js +1 -1
  46. package/dist/tanstack-start/server.js.map +1 -1
  47. package/dist/{vite-graph-loader-DZCwejuJ.d.ts → vite-graph-loader-BtLgkN0V.d.ts} +7 -5
  48. package/package.json +2 -2
  49. package/dist/checks-BS-aDXB_.js.map +0 -1
  50. package/dist/declare-D5o7hwmv.js +0 -9
  51. package/dist/declare-D5o7hwmv.js.map +0 -1
  52. package/dist/graph-DMNOLXLu.js.map +0 -1
  53. package/dist/page-heads-BgbaIjj9.js.map +0 -1
  54. package/dist/pi-Bvw6J0gh.js.map +0 -1
  55. package/dist/state-BuJP_3Lr.js.map +0 -1
@@ -1,6 +1,6 @@
1
- import { m as Register } from "./graph-D1PIndsC.js";
2
- import { f as Message, h as SectionItem, m as SectionDocument, o as Facts, p as PageDocument, u as BodyEntry } from "./page-heads-BfjWQvau.js";
3
- import { D as SectionKind } from "./index-C5w9ripz.js";
1
+ import { _ as Register } from "./graph-ChrvU-6X.js";
2
+ import { f as Message, h as SectionItem, m as SectionDocument, o as Facts, p as PageDocument, u as BodyEntry } from "./page-heads-DDz_2_hq.js";
3
+ import { D as SectionKind } from "./index-DzjUmbHY.js";
4
4
  import { ComponentPropsWithRef, ReactNode } from "react";
5
5
  //#region src/react/document.d.ts
6
6
  /** Augment with `facts: typeof facts` to check Fact ids across the application. */
@@ -106,4 +106,4 @@ declare function useCapturePage(): string | undefined;
106
106
  declare function CaptureAnchor(props: ComponentPropsWithRef<"a">): import("react").JSX.Element;
107
107
  //#endregion
108
108
  export { Fact as a, ForHumans as c, Title as d, Visual as f, useCapturePage as g, messageText as h, DocumentProvider as i, Section as l, finishDocument as m, CaptureRequest as n, FactId as o, createCollector as p, Collector as r, ForAgents as s, CaptureAnchor as t, T as u };
109
- //# sourceMappingURL=document-CztEu3Vd.d.ts.map
109
+ //# sourceMappingURL=document-D9P8yY5g.d.ts.map
@@ -12023,6 +12023,13 @@ interface SeoFaq {
12023
12023
  }
12024
12024
  /** A top-level JSON-LD entity or graph ready to serialize into a script element. */
12025
12025
  type JsonLdDocument = WithContext<Thing> | Graph;
12026
+ /** A consumer-owned social image, as an absolute URL or site-relative asset path. */
12027
+ interface SeoImage {
12028
+ readonly url: string;
12029
+ readonly width?: number | undefined;
12030
+ readonly height?: number | undefined;
12031
+ readonly alt?: string | undefined;
12032
+ }
12026
12033
  /**
12027
12034
  * The document head a page ships. A static route declares it once in
12028
12035
  * `staticData.seo.head` and renders it with `seo.head` from `pagegraph/react`;
@@ -12036,6 +12043,8 @@ interface SeoPageHead {
12036
12043
  */
12037
12044
  title: string;
12038
12045
  description: string;
12046
+ /** Social image override; takes precedence over article.image and the site resolver. */
12047
+ image?: SeoImage | undefined;
12039
12048
  ogTitle?: string | undefined;
12040
12049
  ogDescription?: string | undefined;
12041
12050
  /** Override of the route's robots policy. */
@@ -12163,8 +12172,12 @@ interface SeoNode {
12163
12172
  * instance without a description still carries its title.
12164
12173
  */
12165
12174
  interface SeoNodeHead {
12166
- readonly title: string;
12175
+ /** May be absent when only the image resolver supplies head data. */
12176
+ readonly title?: string | undefined;
12167
12177
  readonly description?: string | undefined;
12178
+ /** Authored canonical override used for the image resolver, without changing graph path ownership. */
12179
+ readonly canonicalPath?: string | undefined;
12180
+ readonly image?: SeoImage | undefined;
12168
12181
  readonly faqs?: ReadonlyArray<{
12169
12182
  readonly question: string;
12170
12183
  readonly answer: string;
@@ -12191,6 +12204,7 @@ interface SeoInstance {
12191
12204
  readonly path: string;
12192
12205
  readonly title: string;
12193
12206
  readonly description?: string | undefined;
12207
+ readonly image?: SeoImage | undefined;
12194
12208
  readonly publishedAt?: string | undefined;
12195
12209
  readonly modifiedAt?: string | undefined;
12196
12210
  }
@@ -12222,17 +12236,26 @@ interface SeoRouteNode {
12222
12236
  };
12223
12237
  readonly children?: ReadonlyArray<SeoRouteNode> | undefined;
12224
12238
  }
12239
+ /** Metadata shared by graph construction and rendering, excluding build-only collection/source details. */
12240
+ type OgImageNode = Pick<SeoNode, "path" | "kind" | "head">;
12241
+ /** Supplies a social image only when the page has no explicit image. Keep it deterministic for builds and rendering. */
12242
+ type OgImageResolver = (node: OgImageNode) => SeoImage | undefined;
12243
+ /** Resolve the shared page/article/resolver precedence and site-relative URL semantics. */
12244
+ declare function resolveOgImage(node: OgImageNode, origin?: string, resolver?: OgImageResolver): SeoImage | undefined;
12225
12245
  interface BuildSeoGraphInput {
12226
12246
  readonly routeTree: AnyRoute | SeoRouteNode;
12247
+ /** Resolve image paths against this site origin; omitted preserves the declared URL. */
12248
+ readonly origin?: string | undefined;
12249
+ readonly ogImage?: OgImageResolver | undefined;
12227
12250
  readonly collections?: ReadonlyArray<SeoCollection> | undefined;
12228
12251
  }
12229
12252
  /**
12230
12253
  * Build the SEO graph from route declarations and content collections.
12231
12254
  *
12232
12255
  * Synchronous: the caller materializes its collections before calling, so there is
12233
- * no async work here. Callers own the origin.
12256
+ * no async work here. The optional origin resolves social-image asset paths.
12234
12257
  */
12235
12258
  declare function buildSeoGraph(input: BuildSeoGraphInput): SeoGraph;
12236
12259
  //#endregion
12237
- export { WithContext as A, FAQPage as C, Service as D, Organization as E, Thing as O, BreadcrumbList as S, ItemList as T, SeoKind as _, SeoGraph as a, applyTitleTemplate as b, SeoNodeHead as c, buildSeoGraph as d, JsonLdDocument as f, SeoFaq as g, RouteSeo as h, SeoEdgeType as i, IdReference as j, WebSite as k, SeoRouteNode as l, Register as m, SeoCollection as n, SeoInstance as o, PublicPath as p, SeoEdge as r, SeoNode as s, BuildSeoGraphInput as t, SeoSource as u, SeoPageHead as v, Graph as w, Article as x, SitemapPolicy as y };
12238
- //# sourceMappingURL=graph-D1PIndsC.d.ts.map
12260
+ export { Organization as A, SitemapPolicy as C, FAQPage as D, BreadcrumbList as E, IdReference as F, Thing as M, WebSite as N, Graph as O, WithContext as P, SeoPageHead as S, Article as T, Register as _, SeoEdge as a, SeoImage as b, SeoInstance as c, SeoRouteNode as d, SeoSource as f, PublicPath as g, JsonLdDocument as h, SeoCollection as i, Service as j, ItemList as k, SeoNode as l, resolveOgImage as m, OgImageNode as n, SeoEdgeType as o, buildSeoGraph as p, OgImageResolver as r, SeoGraph as s, BuildSeoGraphInput as t, SeoNodeHead as u, RouteSeo as v, applyTitleTemplate as w, SeoKind as x, SeoFaq as y };
12261
+ //# sourceMappingURL=graph-ChrvU-6X.d.ts.map
@@ -1,5 +1,35 @@
1
- import { t as applyTitleTemplate } from "./declare-D5o7hwmv.js";
1
+ //#region src/core/declare.ts
2
+ /** Apply a {@link RouteSeo.titleTemplate}; no template means the title is already full. */
3
+ function applyTitleTemplate(template, title) {
4
+ return template === void 0 ? title : template.replaceAll("%s", () => title);
5
+ }
6
+ //#endregion
2
7
  //#region src/core/graph.ts
8
+ /** Resolve the shared page/article/resolver precedence and site-relative URL semantics. */
9
+ function resolveOgImage(node, origin, resolver) {
10
+ const image = node.head?.image ?? resolver?.({
11
+ path: node.head?.canonicalPath ?? node.path,
12
+ kind: node.kind,
13
+ head: node.head === void 0 ? void 0 : {
14
+ title: node.head.title,
15
+ description: node.head.description,
16
+ faqs: node.head.faqs,
17
+ image: node.head.image
18
+ }
19
+ });
20
+ if (image === void 0) return void 0;
21
+ if (origin === void 0) return image;
22
+ const base = new URL("/", origin);
23
+ try {
24
+ return {
25
+ ...image,
26
+ url: new URL(image.url, base).href
27
+ };
28
+ } catch (cause) {
29
+ if (cause instanceof TypeError) return image;
30
+ throw cause;
31
+ }
32
+ }
3
33
  /** Kind for instances whose collection route carries no declaration to inherit. */
4
34
  const FALLBACK_KIND = "page";
5
35
  /**
@@ -19,6 +49,8 @@ function declaredHead(seo) {
19
49
  return {
20
50
  title: applyTitleTemplate(seo.titleTemplate, seo.head.title),
21
51
  description: seo.head.description,
52
+ ...seo.head.canonicalPath === void 0 ? {} : { canonicalPath: seo.head.canonicalPath },
53
+ image: seo.head.image ?? (seo.head.article?.image === void 0 ? void 0 : { url: seo.head.article.image }),
22
54
  faqs: seo.head.faqs?.map(({ question, answer }) => ({
23
55
  question,
24
56
  answer
@@ -120,7 +152,8 @@ function addCollection(nodes, edges, collisions, collection) {
120
152
  },
121
153
  head: {
122
154
  title: applyTitleTemplate(titleTemplate, instance.title),
123
- description: instance.description
155
+ description: instance.description,
156
+ image: instance.image
124
157
  },
125
158
  instance: {
126
159
  title: instance.title,
@@ -139,7 +172,7 @@ function addCollection(nodes, edges, collisions, collection) {
139
172
  * Build the SEO graph from route declarations and content collections.
140
173
  *
141
174
  * Synchronous: the caller materializes its collections before calling, so there is
142
- * no async work here. Callers own the origin.
175
+ * no async work here. The optional origin resolves social-image asset paths.
143
176
  */
144
177
  function buildSeoGraph(input) {
145
178
  const nodes = /* @__PURE__ */ new Map();
@@ -148,6 +181,11 @@ function buildSeoGraph(input) {
148
181
  walkRoutes(input.routeTree, "/", true, nodes, edges, []);
149
182
  for (const collection of input.collections ?? []) addCollection(nodes, edges, collisions, collection);
150
183
  for (const node of nodes.values()) {
184
+ const image = node.kind === "layout" || node.path.includes("$") ? void 0 : resolveOgImage(node, input.origin, input.ogImage);
185
+ if (image !== void 0) node.head = {
186
+ ...node.head,
187
+ image
188
+ };
151
189
  if (node.source !== "route") continue;
152
190
  for (const to of node.policy.related ?? []) edges.push({
153
191
  from: node.path,
@@ -167,6 +205,6 @@ function buildSeoGraph(input) {
167
205
  };
168
206
  }
169
207
  //#endregion
170
- export { buildSeoGraph as t };
208
+ export { resolveOgImage as n, applyTitleTemplate as r, buildSeoGraph as t };
171
209
 
172
- //# sourceMappingURL=graph-DMNOLXLu.js.map
210
+ //# sourceMappingURL=graph-UONEaqIX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-UONEaqIX.js","names":[],"sources":["../src/core/declare.ts","../src/core/graph.ts"],"sourcesContent":["/**\n * The declaration surface: what a route says about itself in\n * `staticData.seo`. Everything else in this package derives from it.\n *\n * Consumers supply valid route paths, page kinds, and authored fact ids by\n * augmenting {@link Register}, the same pattern TanStack Router uses for its own\n * typed registry. Unaugmented, these fall back to `string`, so the package\n * compiles and behaves on its own.\n */\n\nimport type { AnyRouteMatch } from \"@tanstack/react-router\";\nimport type { Graph, Thing, WithContext } from \"schema-dts\";\n\n/**\n * Consumer-augmented registry.\n *\n * ```ts\n * declare module \"pagegraph\" {\n * interface Register {\n * paths: FileRouteTypes[\"fullPaths\"];\n * kinds: \"page\" | \"article\" | \"hub\";\n * facts: typeof facts;\n * }\n * }\n * ```\n */\nexport interface Register {}\n\n/** Every public full path in the consumer's route tree (type-only; erased at runtime). */\nexport type PublicPath = Register extends { paths: infer P extends string } ? P : string;\n\n/** The consumer's page taxonomy. A label: nothing in the core branches on it. */\nexport type SeoKind = Register extends { kinds: infer K extends string } ? K : string;\n\nexport interface SitemapPolicy {\n priority: number;\n changeFrequency: \"weekly\" | \"monthly\";\n}\n\n/** A question and answer the page shows and marks up as FAQPage. */\nexport interface SeoFaq {\n question: string;\n answer: string;\n category?: string | undefined;\n isHighlighted?: boolean | undefined;\n}\n\n/** A top-level JSON-LD entity or graph ready to serialize into a script element. */\nexport type JsonLdDocument = WithContext<Thing> | Graph;\n\n/** A consumer-owned social image, as an absolute URL or site-relative asset path. */\nexport interface SeoImage {\n readonly url: string;\n readonly width?: number | undefined;\n readonly height?: number | undefined;\n readonly alt?: string | undefined;\n}\n\n/**\n * The document head a page ships. A static route declares it once in\n * `staticData.seo.head` and renders it with `seo.head` from `pagegraph/react`;\n * the graph, `pageHeads`, and the checks read the same title, description, and\n * FAQs. A dynamic route passes it to `seoHead(ctx, head)` from its `head()`.\n */\nexport interface SeoPageHead {\n /**\n * The page title. A route that declares {@link RouteSeo.titleTemplate} gets\n * it applied; otherwise this is the full `<title>`.\n */\n title: string;\n description: string;\n /** Social image override; takes precedence over article.image and the site resolver. */\n image?: SeoImage | undefined;\n ogTitle?: string | undefined;\n ogDescription?: string | undefined;\n /** Override of the route's robots policy. */\n robots?: string | undefined;\n article?:\n | {\n publishedAt: string;\n modifiedAt?: string | undefined;\n author?: { name: string; url?: string | undefined } | undefined;\n image?: string | undefined;\n tags?: ReadonlyArray<string> | undefined;\n }\n | undefined;\n faqs?: ReadonlyArray<SeoFaq> | undefined;\n service?: { name: string; description?: string | undefined; serviceType: string } | undefined;\n /** Standalone keywords meta for non-article pages; article pages derive it from tags. */\n keywords?: ReadonlyArray<string> | undefined;\n /** Visible canonical pages rendered as an ordered collection on this page. */\n itemList?: { name: string; items: ReadonlyArray<{ name: string; url: string }> } | undefined;\n /** Additional schema.org documents for this route. */\n jsonLd?: ReadonlyArray<JsonLdDocument> | undefined;\n /**\n * Explicit canonical path (used for canonical + og:url instead of the match pathname).\n * For routes whose canonical is computed independently of the URL, e.g. a docs splat\n * that derives it from the resolved slug segments in its loader.\n */\n canonicalPath?: string | undefined;\n /**\n * Explicit breadcrumb trail for the BreadcrumbList JSON-LD, overriding the match-chain\n * trail. For routes whose hierarchy lives outside the route tree. Each `path` is a URL path.\n */\n breadcrumbs?: ReadonlyArray<{ name: string; path: string }> | undefined;\n}\n\nexport interface RouteSeo {\n kind: SeoKind;\n /** Breadcrumb label; fn form reads the match (e.g. loaderData frontmatter title). */\n crumb?: string | ((match: AnyRouteMatch) => string) | undefined;\n /** false = excluded from sitemap. Absent on non-indexable kinds is fine. */\n sitemap?: SitemapPolicy | false | undefined;\n /** meta robots value, e.g. \"noindex, follow\" on a hub page. */\n robots?: string | undefined;\n /** Typed cross-link edges. */\n related?: ReadonlyArray<PublicPath> | undefined;\n /** How OTHER pages render a card for this page. */\n link?: { title: string; description: string } | undefined;\n /** This route is a redirect/alias. */\n redirectTo?: PublicPath | undefined;\n /**\n * When this page's content last changed meaningfully, as an ISO 8601 date.\n * Feeds the sitemap `<lastmod>` and the freshness report. Content instances\n * carry their own dates instead; a param route's value is never inherited.\n */\n modifiedAt?: string | undefined;\n /** A static page's head, rendered by `seo.head`; the graph and `pageHeads` read it. */\n head?: SeoPageHead | undefined;\n /**\n * Full-title template for pages rendered through this route, with `%s` for the\n * page's own title (e.g. `\"%s | Example Blog\"`). `seoHead` applies it to the\n * title a dynamic route passes, and the graph applies it to the instances a\n * collection routes through it, so the suffix lives in one place.\n */\n titleTemplate?: string | undefined;\n}\n\n/** Apply a {@link RouteSeo.titleTemplate}; no template means the title is already full. */\nexport function applyTitleTemplate(template: string | undefined, title: string): string {\n // A replacer function keeps `$&` and friends in the title literal.\n return template === undefined ? title : template.replaceAll(\"%s\", () => title);\n}\n\ndeclare module \"@tanstack/react-router\" {\n interface StaticDataRouteOption {\n seo?: RouteSeo | undefined;\n /** How this route supplies its authored document. */\n markdown?: \"rendered\" | \"source\" | undefined;\n /** Group label for this page in llms.txt. */\n llms?: string | undefined;\n }\n}\n","/**\n * SEO graph — the derived model that projections (sitemap, robots), the CLI, and\n * the check engine all read from. Built from route declarations (`staticData.seo`)\n * plus consumer-supplied content collections. This module is pure: no React, no\n * env, no knowledge of where instances come from. The caller supplies an origin\n * when resolving social images; projections receive their own origin.\n *\n * A node is one of:\n * - a structural route (`source: \"route\"`), keyed by its normalized full path,\n * merging a layout route's declaration (crumb) with its index child's (kind,\n * sitemap policy) when both resolve to the same URL;\n * - a content instance (`source` = the collection's label), keyed by the page URL\n * and carrying page-level metadata.\n *\n * Edges: `crumb-parent` (breadcrumb ancestry), `related` (deliberate cross-links),\n * `collection-member` (membership in a curated set), and `redirect` (route aliases).\n * The route walk emits crumb/related/redirect edges from declarations; a collection\n * may declare any additional edges its instances need.\n */\n\nimport type { AnyRoute } from \"@tanstack/react-router\";\n\nimport { applyTitleTemplate, type PublicPath, type RouteSeo, type SeoKind, type SeoImage } from \"./declare\";\n\n/**\n * Where a node came from: `\"route\"` for a structural route declaration, or the\n * `source` label of the collection that produced the instance (e.g. \"blog\").\n */\nexport type SeoSource = string;\n\nexport interface SeoNode {\n /** Canonical path, no origin (e.g. \"/pricing\", \"/blog/my-post\"). */\n path: string;\n kind: SeoKind;\n source: SeoSource;\n /** Route-declared authored-document mode, inherited by collection instances. */\n markdown?: \"rendered\" | \"source\" | undefined;\n /** Group label for llms.txt, inherited by collection instances. */\n llms?: string | undefined;\n /** Route-declared policy, or synthesized (kind + inherited sitemap) for instances. */\n policy: RouteSeo;\n /**\n * The head the page ships: a static route's declared `head`, or an instance's\n * title (through its route's `titleTemplate`) and description. Absent when the\n * route computes its head at request time and no collection supplies it.\n */\n head?: SeoNodeHead | undefined;\n instance?:\n | {\n title: string;\n description?: string | undefined;\n publishedAt?: string | undefined;\n modifiedAt?: string | undefined;\n }\n | undefined;\n}\n\n/**\n * A node's head as graph data: the full title, description, and FAQ text. An\n * instance without a description still carries its title.\n */\nexport interface SeoNodeHead {\n /** May be absent when only the image resolver supplies head data. */\n readonly title?: string | undefined;\n readonly description?: string | undefined;\n /** Authored canonical override used for the image resolver, without changing graph path ownership. */\n readonly canonicalPath?: string | undefined;\n readonly image?: SeoImage | undefined;\n readonly faqs?: ReadonlyArray<{ readonly question: string; readonly answer: string }> | undefined;\n}\n\nexport type SeoEdgeType = \"crumb-parent\" | \"related\" | \"redirect\" | \"collection-member\";\n\nexport interface SeoEdge {\n from: string;\n to: string;\n type: SeoEdgeType;\n}\n\nexport interface SeoGraph {\n nodes: Map<string, SeoNode>;\n edges: Array<SeoEdge>;\n /** Exact-path ownership conflicts encountered while assembling graph sources. */\n collisions?: ReadonlyArray<{ path: string; sources: ReadonlyArray<SeoSource> }> | undefined;\n}\n\n/** One concrete page produced by a collection. */\nexport interface SeoInstance {\n /** Canonical path of the page (e.g. \"/blog/my-post\"). */\n readonly path: string;\n readonly title: string;\n readonly description?: string | undefined;\n readonly image?: SeoImage | undefined;\n readonly publishedAt?: string | undefined;\n readonly modifiedAt?: string | undefined;\n}\n\nexport interface SeoCollection {\n /**\n * The param route these instances render through (e.g. \"/blog/$slug\"). Instances\n * inherit this route's declared policy (kind + sitemap) — the graph reads it from\n * the structural node, so declarations stay the single source of truth.\n */\n readonly route: PublicPath;\n /** The `source` stamped on every node this collection produces (e.g. \"blog\"). */\n readonly source: SeoSource;\n readonly instances: ReadonlyArray<SeoInstance>;\n /** Edges the collection declares between its instances and the rest of the graph. */\n readonly edges?: ReadonlyArray<SeoEdge> | undefined;\n}\n\n/**\n * The structural route shape the graph walks: a TanStack route tree has it\n * before `init()`, and an adapter can build it from parsed route files.\n */\nexport interface SeoRouteNode {\n readonly options: {\n readonly path?: string | undefined;\n readonly staticData?:\n | {\n readonly seo?: RouteSeo | undefined;\n readonly markdown?: SeoNode[\"markdown\"];\n readonly llms?: SeoNode[\"llms\"];\n }\n | undefined;\n };\n readonly children?: ReadonlyArray<SeoRouteNode> | undefined;\n}\n\n/** Metadata shared by graph construction and rendering, excluding build-only collection/source details. */\nexport type OgImageNode = Pick<SeoNode, \"path\" | \"kind\" | \"head\">;\n\n/** Supplies a social image only when the page has no explicit image. Keep it deterministic for builds and rendering. */\nexport type OgImageResolver = (node: OgImageNode) => SeoImage | undefined;\n\n/** Resolve the shared page/article/resolver precedence and site-relative URL semantics. */\nexport function resolveOgImage(node: OgImageNode, origin?: string, resolver?: OgImageResolver): SeoImage | undefined {\n const image = node.head?.image ?? resolver?.({\n path: node.head?.canonicalPath ?? node.path,\n kind: node.kind,\n head: node.head === undefined ? undefined : {\n title: node.head.title,\n description: node.head.description,\n faqs: node.head.faqs,\n image: node.head.image,\n },\n });\n if (image === undefined) return undefined;\n if (origin === undefined) return image;\n const base = new URL(\"/\", origin);\n try {\n return { ...image, url: new URL(image.url, base).href };\n } catch (cause) {\n // Preserve invalid declarations so the check engine can apply the configured severity.\n if (cause instanceof TypeError) return image;\n throw cause;\n }\n}\n\nexport interface BuildSeoGraphInput {\n readonly routeTree: AnyRoute | SeoRouteNode;\n /** Resolve image paths against this site origin; omitted preserves the declared URL. */\n readonly origin?: string | undefined;\n readonly ogImage?: OgImageResolver | undefined;\n readonly collections?: ReadonlyArray<SeoCollection> | undefined;\n}\n\n/** Kind for instances whose collection route carries no declaration to inherit. */\nconst FALLBACK_KIND: SeoKind = \"page\";\n\n\n/**\n * Join a child's local path onto its parent's computed full path with the same\n * semantics as TanStack's route init (relative segments, index \"/\" inherits the\n * parent, pathless/group routes are transparent), then normalize trailing slashes.\n */\nfunction joinPath(parent: string, seg: string | undefined): string {\n if (seg === undefined) return parent; // pathless layout / route group\n if (seg === \"/\") return parent; // index route resolves to its parent's URL\n const trimmed = seg.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\");\n const joined = `${parent === \"/\" ? \"\" : parent}/${trimmed}`;\n return joined.replace(/\\/{2,}/g, \"/\");\n}\n\n/** A route's declared head as graph data: templated title, description, and plain FAQs. */\nfunction declaredHead(seo: RouteSeo): SeoNodeHead | undefined {\n if (seo.head === undefined) return undefined;\n return {\n title: applyTitleTemplate(seo.titleTemplate, seo.head.title),\n description: seo.head.description,\n ...(seo.head.canonicalPath === undefined ? {} : { canonicalPath: seo.head.canonicalPath }),\n image: seo.head.image ?? (seo.head.article?.image === undefined ? undefined : { url: seo.head.article.image }),\n faqs: seo.head.faqs?.map(({ question, answer }) => ({ question, answer })),\n };\n}\n\n/** Merge a route's declaration into an existing same-path node (deeper route wins). */\nfunction mergeSeo(base: RouteSeo, override: RouteSeo): RouteSeo {\n return {\n kind: override.kind,\n crumb: override.crumb ?? base.crumb,\n sitemap: override.sitemap ?? base.sitemap,\n robots: override.robots ?? base.robots,\n related: override.related ?? base.related,\n link: override.link ?? base.link,\n redirectTo: override.redirectTo ?? base.redirectTo,\n modifiedAt: override.modifiedAt ?? base.modifiedAt,\n head: override.head ?? base.head,\n titleTemplate: override.titleTemplate ?? base.titleTemplate,\n };\n}\n\n/**\n * Walk the route tree building structural nodes and crumb-parent edges. `crumbStack`\n * holds the paths of crumb-declaring ancestors so each crumb node links to its nearest\n * crumb ancestor down the real route-parent chain (matching render-time breadcrumbs).\n */\nfunction walkRoutes(\n route: SeoRouteNode,\n parentPath: string,\n isRoot: boolean,\n nodes: Map<string, SeoNode>,\n edges: Array<SeoEdge>,\n crumbStack: Array<string>,\n): void {\n const path = isRoot ? \"/\" : joinPath(parentPath, route.options.path);\n const data = route.options.staticData;\n const seo = data?.seo;\n\n if (seo || data?.markdown !== undefined || data?.llms !== undefined) {\n const existing = nodes.get(path);\n if (existing) {\n if (seo) existing.policy = mergeSeo(existing.policy, seo);\n existing.kind = existing.policy.kind;\n // A head renders with its own route's template, never one merged from a layout.\n existing.head = (seo && declaredHead(seo)) ?? existing.head;\n existing.markdown = data?.markdown ?? existing.markdown;\n existing.llms = data?.llms ?? existing.llms;\n } else {\n const policy = seo ?? { kind: FALLBACK_KIND };\n nodes.set(path, {\n path,\n kind: policy.kind,\n source: \"route\",\n policy: { ...policy },\n head: seo && declaredHead(seo),\n markdown: data?.markdown,\n llms: data?.llms,\n });\n }\n\n const nearestCrumbAncestor = crumbStack[crumbStack.length - 1];\n if (seo?.crumb !== undefined && nearestCrumbAncestor !== undefined) {\n edges.push({ from: path, to: nearestCrumbAncestor, type: \"crumb-parent\" });\n }\n }\n\n const pushedCrumb = seo?.crumb !== undefined;\n if (pushedCrumb) crumbStack.push(path);\n for (const child of route.children ?? []) {\n walkRoutes(child, path, false, nodes, edges, crumbStack);\n }\n if (pushedCrumb) crumbStack.pop();\n}\n\n/**\n * Add one collection's instance nodes, inheriting kind + sitemap policy from the\n * collection route's declaration, then append the edges it declares. A path already\n * owned by another node is a collision: the first owner keeps the path and the\n * conflict is reported (the `path-owner-collision` check turns it into a violation).\n */\nfunction addCollection(\n nodes: Map<string, SeoNode>,\n edges: Array<SeoEdge>,\n collisions: Array<{ path: string; sources: ReadonlyArray<SeoSource> }>,\n collection: SeoCollection,\n): void {\n const collectionNode = nodes.get(collection.route);\n const kind = collectionNode?.kind ?? FALLBACK_KIND;\n const sitemap = collectionNode?.policy.sitemap;\n const titleTemplate = collectionNode?.policy.titleTemplate;\n\n /**\n * Paths this collection lost to an earlier owner. Their instances never enter\n * the graph, so any edge declared out of them would dangle — and the dead-edge\n * check only validates an edge's `to`, so nothing downstream would catch it.\n */\n const rejected = new Set<string>();\n\n for (const instance of collection.instances) {\n const existing = nodes.get(instance.path);\n if (existing) {\n collisions.push({ path: instance.path, sources: [existing.source, collection.source] });\n rejected.add(instance.path);\n continue;\n }\n nodes.set(instance.path, {\n path: instance.path,\n kind,\n source: collection.source,\n markdown: collectionNode?.markdown,\n llms: collectionNode?.llms,\n policy: { kind, sitemap },\n head: { title: applyTitleTemplate(titleTemplate, instance.title), description: instance.description, image: instance.image },\n instance: {\n title: instance.title,\n description: instance.description,\n publishedAt: instance.publishedAt,\n modifiedAt: instance.modifiedAt,\n },\n });\n }\n\n for (const edge of collection.edges ?? []) {\n if (rejected.has(edge.from)) continue;\n edges.push(edge);\n }\n}\n\n/**\n * Build the SEO graph from route declarations and content collections.\n *\n * Synchronous: the caller materializes its collections before calling, so there is\n * no async work here. The optional origin resolves social-image asset paths.\n */\nexport function buildSeoGraph(input: BuildSeoGraphInput): SeoGraph {\n const nodes = new Map<string, SeoNode>();\n const edges: Array<SeoEdge> = [];\n const collisions: Array<{ path: string; sources: ReadonlyArray<SeoSource> }> = [];\n\n walkRoutes(input.routeTree as unknown as SeoRouteNode, \"/\", true, nodes, edges, []);\n\n for (const collection of input.collections ?? []) {\n addCollection(nodes, edges, collisions, collection);\n }\n\n for (const node of nodes.values()) {\n const image = node.kind === \"layout\" || node.path.includes(\"$\")\n ? undefined\n : resolveOgImage(node, input.origin, input.ogImage);\n if (image !== undefined) node.head = { ...node.head, image };\n if (node.source !== \"route\") continue;\n for (const to of node.policy.related ?? []) {\n edges.push({ from: node.path, to, type: \"related\" });\n }\n if (node.policy.redirectTo !== undefined) {\n edges.push({ from: node.path, to: node.policy.redirectTo, type: \"redirect\" });\n }\n }\n\n return { nodes, edges, collisions };\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
- import { a as SeoGraph, h as RouteSeo, n as SeoCollection, o as SeoInstance, p as PublicPath, r as SeoEdge, s as SeoNode, u as SeoSource } from "./graph-D1PIndsC.js";
2
- import { N as SimpleEdge, w as AnchorRegion } from "./content-DgNr7apS.js";
3
- import { f as Message, m as SectionDocument, p as PageDocument } from "./page-heads-BfjWQvau.js";
4
- import "./projections-Bk-qOOsR.js";
1
+ import { a as SeoEdge, c as SeoInstance, f as SeoSource, g as PublicPath, i as SeoCollection, l as SeoNode, s as SeoGraph, v as RouteSeo } from "./graph-ChrvU-6X.js";
2
+ import { P as SimpleEdge, T as AnchorRegion } from "./content-DerWgQy9.js";
3
+ import { f as Message, m as SectionDocument, p as PageDocument } from "./page-heads-DDz_2_hq.js";
4
+ import "./projections-Dqo_ih-J.js";
5
5
  import { AnyRouter } from "@tanstack/react-router";
6
6
  //#region src/markdown/hash.d.ts
7
7
  declare const contentHash: (value: unknown) => string;
@@ -201,4 +201,4 @@ declare const hasBlockingIssues: (report: LiveHeadReport) => boolean;
201
201
  declare function resolveRouteLink(router: AnyRouter, path: string): RouteSeo["link"];
202
202
  //#endregion
203
203
  export { sectionMarkdown as A, LlmsPage as C, SectionKind as D, MarkdownWriter as E, defineSectionKind as O, createMarkdownLock as S, llmsSection as T, graphToJson as _, inspectHtml as a, contentCollection as b, LinkCandidateResult as c, decodeRenderedEdges as d, generateLinkCandidates as f, graphFromJson as g, SeoGraphJson as h, hasBlockingIssues as i, contentHash as j, documentMarkdown as k, LinkClusterSummary as l, undirectedEdgeKey as m, JsonLdReport as n, LinkCandidateOptions as o, matchesClusterFilter as p, LiveHeadReport as r, LinkCandidatePair as s, resolveRouteLink as t, candidateSourceText as u, ContentCollectionOptions as v, llmsMarkdown as w, MarkdownLock as x, ContentEntry as y };
204
- //# sourceMappingURL=index-C5w9ripz.d.ts.map
204
+ //# sourceMappingURL=index-DzjUmbHY.d.ts.map
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { _ as SeoKind, a as SeoGraph, b as applyTitleTemplate, c as SeoNodeHead, d as buildSeoGraph, f as JsonLdDocument, g as SeoFaq, h as RouteSeo, i as SeoEdgeType, l as SeoRouteNode, m as Register, n as SeoCollection, o as SeoInstance, p as PublicPath, r as SeoEdge, s as SeoNode, t as BuildSeoGraphInput, u as SeoSource, v as SeoPageHead, y as SitemapPolicy } from "./graph-D1PIndsC.js";
2
- import { A as RenderedEdgeFailure, B as FreshnessEntry, C as Anchor, D as RENDERED_EDGE_ARTIFACT_SCHEMA_VERSION, E as LinkGraphDiff, F as decodeRenderedEdgeArtifact, H as FreshnessReport, I as diffLinkGraph, L as extractAnchors, M as RenderedPage, N as SimpleEdge, O as RenderedEdgeArtifact, P as buildRenderedGraph, R as normalizePath, S as hasStructuralViolations, T as LinkEdge, U as freshnessReport, V as FreshnessPolicy, _ as Violation, a as Heading, b as checkGraph, c as RenderedDocument, d as checkRenderedContent, f as extractPageContent, g as Severity, h as CoverageRule, i as ContentRuleName, j as RenderedGraph, k as RenderedEdgeArtifactCrawl, l as WordCountRule, m as CheckGraphOptions, n as ContentFinding, o as JsonLdNode, p as renderedCheckTargets, r as ContentPolicy, s as PageContent, t as ContentCheckOptions, u as checkPageContent, v as checkCoverage, w as AnchorRegion, x as checkRenderedCoverage, y as checkFreshness, z as renderedGraphFromEdges } from "./content-DgNr7apS.js";
3
- import { _ as hashDocument, a as FactDefinition, c as fact, d as InlineNode, f as Message, g as VisualDocument, h as SectionItem, i as selectPageHeadNodes, l as resolveFact, m as SectionDocument, n as PageHeadsOptions, o as Facts, p as PageDocument, r as pageHeads, s as defineFacts, t as PageHead, u as BodyEntry } from "./page-heads-BfjWQvau.js";
4
- import { A as sectionMarkdown, C as LlmsPage, D as SectionKind, E as MarkdownWriter, O as defineSectionKind, S as createMarkdownLock, T as llmsSection, _ as graphToJson, a as inspectHtml, b as contentCollection, c as LinkCandidateResult, d as decodeRenderedEdges, f as generateLinkCandidates, g as graphFromJson, h as SeoGraphJson, i as hasBlockingIssues, j as contentHash, k as documentMarkdown, l as LinkClusterSummary, m as undirectedEdgeKey, n as JsonLdReport, o as LinkCandidateOptions, p as matchesClusterFilter, r as LiveHeadReport, s as LinkCandidatePair, t as resolveRouteLink, u as candidateSourceText, v as ContentCollectionOptions, w as llmsMarkdown, x as MarkdownLock, y as ContentEntry } from "./index-C5w9ripz.js";
5
- import { a as inspectNode, c as renderRobots, i as contentSignal, l as renderSitemap, n as ProjectionConfig, o as isSitemapEligible, r as RobotsConfig, s as lastModified, t as NodeReport } from "./projections-Bk-qOOsR.js";
6
- export { type Anchor, type AnchorRegion, type BodyEntry, type BuildSeoGraphInput, type CheckGraphOptions, type ContentCheckOptions, type ContentCollectionOptions, type ContentEntry, type ContentFinding, type ContentPolicy, type ContentRuleName, type CoverageRule, type FactDefinition, type Facts, type FreshnessEntry, type FreshnessPolicy, type FreshnessReport, type Heading, type InlineNode, type JsonLdDocument, type JsonLdNode, type JsonLdReport, type LinkCandidateOptions, type LinkCandidatePair, type LinkCandidateResult, type LinkClusterSummary, type LinkEdge, type LinkGraphDiff, type LiveHeadReport, type LlmsPage, type MarkdownLock, type MarkdownWriter, type Message, type NodeReport, type PageContent, type PageDocument, type PageHead, type PageHeadsOptions, type ProjectionConfig, type PublicPath, RENDERED_EDGE_ARTIFACT_SCHEMA_VERSION, type Register, type RenderedDocument, type RenderedEdgeArtifact, type RenderedEdgeArtifactCrawl, type RenderedEdgeFailure, type RenderedGraph, type RenderedPage, type RobotsConfig, type RouteSeo, type SectionDocument, type SectionItem, type SectionKind, type SeoCollection, type SeoEdge, type SeoEdgeType, type SeoFaq, type SeoGraph, type SeoGraphJson, type SeoInstance, type SeoKind, type SeoNode, type SeoNodeHead, type SeoPageHead, type SeoRouteNode, type SeoSource, type Severity, type SimpleEdge, type SitemapPolicy, type Violation, type VisualDocument, type WordCountRule, applyTitleTemplate, buildRenderedGraph, buildSeoGraph, candidateSourceText, checkCoverage, checkFreshness, checkGraph, checkPageContent, checkRenderedContent, checkRenderedCoverage, contentCollection, contentHash, contentSignal, createMarkdownLock, decodeRenderedEdgeArtifact, decodeRenderedEdges, defineFacts, defineSectionKind, diffLinkGraph, documentMarkdown, extractAnchors, extractPageContent, fact, freshnessReport, generateLinkCandidates, graphFromJson, graphToJson, hasBlockingIssues, hasStructuralViolations, hashDocument, inspectHtml, inspectNode, isSitemapEligible, lastModified, llmsMarkdown, llmsSection, matchesClusterFilter, normalizePath, pageHeads, renderRobots, renderSitemap, renderedCheckTargets, renderedGraphFromEdges, resolveFact, resolveRouteLink, sectionMarkdown, selectPageHeadNodes, undirectedEdgeKey };
1
+ import { C as SitemapPolicy, S as SeoPageHead, _ as Register, a as SeoEdge, b as SeoImage, c as SeoInstance, d as SeoRouteNode, f as SeoSource, g as PublicPath, h as JsonLdDocument, i as SeoCollection, l as SeoNode, m as resolveOgImage, n as OgImageNode, o as SeoEdgeType, p as buildSeoGraph, r as OgImageResolver, s as SeoGraph, t as BuildSeoGraphInput, u as SeoNodeHead, v as RouteSeo, w as applyTitleTemplate, x as SeoKind, y as SeoFaq } from "./graph-ChrvU-6X.js";
2
+ import { A as RenderedEdgeArtifactCrawl, B as renderedGraphFromEdges, C as hasStructuralViolations, D as LinkGraphDiff, E as LinkEdge, F as buildRenderedGraph, H as FreshnessPolicy, I as decodeRenderedEdgeArtifact, L as diffLinkGraph, M as RenderedGraph, N as RenderedPage, O as RENDERED_EDGE_ARTIFACT_SCHEMA_VERSION, P as SimpleEdge, R as extractAnchors, S as checkRenderedCoverage, T as AnchorRegion, U as FreshnessReport, V as FreshnessEntry, W as freshnessReport, _ as Severity, a as Heading, b as checkFreshness, c as RenderedDocument, d as checkRenderedContent, f as extractPageContent, g as OgImagePolicy, h as CoverageRule, i as ContentRuleName, j as RenderedEdgeFailure, k as RenderedEdgeArtifact, l as WordCountRule, m as CheckGraphOptions, n as ContentFinding, o as JsonLdNode, p as renderedCheckTargets, r as ContentPolicy, s as PageContent, t as ContentCheckOptions, u as checkPageContent, v as Violation, w as Anchor, x as checkGraph, y as checkCoverage, z as normalizePath } from "./content-DerWgQy9.js";
3
+ import { _ as hashDocument, a as FactDefinition, c as fact, d as InlineNode, f as Message, g as VisualDocument, h as SectionItem, i as selectPageHeadNodes, l as resolveFact, m as SectionDocument, n as PageHeadsOptions, o as Facts, p as PageDocument, r as pageHeads, s as defineFacts, t as PageHead, u as BodyEntry } from "./page-heads-DDz_2_hq.js";
4
+ import { A as sectionMarkdown, C as LlmsPage, D as SectionKind, E as MarkdownWriter, O as defineSectionKind, S as createMarkdownLock, T as llmsSection, _ as graphToJson, a as inspectHtml, b as contentCollection, c as LinkCandidateResult, d as decodeRenderedEdges, f as generateLinkCandidates, g as graphFromJson, h as SeoGraphJson, i as hasBlockingIssues, j as contentHash, k as documentMarkdown, l as LinkClusterSummary, m as undirectedEdgeKey, n as JsonLdReport, o as LinkCandidateOptions, p as matchesClusterFilter, r as LiveHeadReport, s as LinkCandidatePair, t as resolveRouteLink, u as candidateSourceText, v as ContentCollectionOptions, w as llmsMarkdown, x as MarkdownLock, y as ContentEntry } from "./index-DzjUmbHY.js";
5
+ import { a as inspectNode, c as renderRobots, i as contentSignal, l as renderSitemap, n as ProjectionConfig, o as isSitemapEligible, r as RobotsConfig, s as lastModified, t as NodeReport } from "./projections-Dqo_ih-J.js";
6
+ export { type Anchor, type AnchorRegion, type BodyEntry, type BuildSeoGraphInput, type CheckGraphOptions, type ContentCheckOptions, type ContentCollectionOptions, type ContentEntry, type ContentFinding, type ContentPolicy, type ContentRuleName, type CoverageRule, type FactDefinition, type Facts, type FreshnessEntry, type FreshnessPolicy, type FreshnessReport, type Heading, type InlineNode, type JsonLdDocument, type JsonLdNode, type JsonLdReport, type LinkCandidateOptions, type LinkCandidatePair, type LinkCandidateResult, type LinkClusterSummary, type LinkEdge, type LinkGraphDiff, type LiveHeadReport, type LlmsPage, type MarkdownLock, type MarkdownWriter, type Message, type NodeReport, type OgImageNode, type OgImagePolicy, type OgImageResolver, type PageContent, type PageDocument, type PageHead, type PageHeadsOptions, type ProjectionConfig, type PublicPath, RENDERED_EDGE_ARTIFACT_SCHEMA_VERSION, type Register, type RenderedDocument, type RenderedEdgeArtifact, type RenderedEdgeArtifactCrawl, type RenderedEdgeFailure, type RenderedGraph, type RenderedPage, type RobotsConfig, type RouteSeo, type SectionDocument, type SectionItem, type SectionKind, type SeoCollection, type SeoEdge, type SeoEdgeType, type SeoFaq, type SeoGraph, type SeoGraphJson, type SeoImage, type SeoInstance, type SeoKind, type SeoNode, type SeoNodeHead, type SeoPageHead, type SeoRouteNode, type SeoSource, type Severity, type SimpleEdge, type SitemapPolicy, type Violation, type VisualDocument, type WordCountRule, applyTitleTemplate, buildRenderedGraph, buildSeoGraph, candidateSourceText, checkCoverage, checkFreshness, checkGraph, checkPageContent, checkRenderedContent, checkRenderedCoverage, contentCollection, contentHash, contentSignal, createMarkdownLock, decodeRenderedEdgeArtifact, decodeRenderedEdges, defineFacts, defineSectionKind, diffLinkGraph, documentMarkdown, extractAnchors, extractPageContent, fact, freshnessReport, generateLinkCandidates, graphFromJson, graphToJson, hasBlockingIssues, hasStructuralViolations, hashDocument, inspectHtml, inspectNode, isSitemapEligible, lastModified, llmsMarkdown, llmsSection, matchesClusterFilter, normalizePath, pageHeads, renderRobots, renderSitemap, renderedCheckTargets, renderedGraphFromEdges, resolveFact, resolveOgImage, resolveRouteLink, sectionMarkdown, selectPageHeadNodes, undirectedEdgeKey };
package/dist/index.js CHANGED
@@ -1,10 +1,9 @@
1
- import { t as applyTitleTemplate } from "./declare-D5o7hwmv.js";
2
- import { t as buildSeoGraph } from "./graph-DMNOLXLu.js";
1
+ import { n as resolveOgImage, r as applyTitleTemplate, t as buildSeoGraph } from "./graph-UONEaqIX.js";
3
2
  import { n as graphToJson, t as graphFromJson } from "./wire-B4RQMc4e.js";
4
3
  import { i as lastModified, n as inspectNode, o as renderRobots, r as isSitemapEligible, s as renderSitemap, t as contentSignal } from "./projections-D0oOFKiY.js";
5
- import { i as checkRenderedCoverage, n as checkFreshness, o as hasStructuralViolations, r as checkGraph, s as freshnessReport, t as checkCoverage } from "./checks-BS-aDXB_.js";
6
- import { a as RENDERED_EDGE_ARTIFACT_SCHEMA_VERSION, c as diffLinkGraph, d as renderedGraphFromEdges, f as hasBlockingIssues, i as renderedCheckTargets, l as extractAnchors, n as checkRenderedContent, o as buildRenderedGraph, p as inspectHtml, r as extractPageContent, s as decodeRenderedEdgeArtifact, t as checkPageContent, u as normalizePath } from "./content-CHd0X4s6.js";
7
- import { n as selectPageHeadNodes, t as pageHeads } from "./page-heads-BgbaIjj9.js";
4
+ import { i as checkRenderedCoverage, n as checkFreshness, o as hasStructuralViolations, r as checkGraph, s as freshnessReport, t as checkCoverage } from "./checks-BEhJPNzN.js";
5
+ import { a as RENDERED_EDGE_ARTIFACT_SCHEMA_VERSION, c as diffLinkGraph, d as renderedGraphFromEdges, f as hasBlockingIssues, i as renderedCheckTargets, l as extractAnchors, n as checkRenderedContent, o as buildRenderedGraph, p as inspectHtml, r as extractPageContent, s as decodeRenderedEdgeArtifact, t as checkPageContent, u as normalizePath } from "./content-D5jZprMq.js";
6
+ import { n as selectPageHeadNodes, t as pageHeads } from "./page-heads-YL8nyYjF.js";
8
7
  import { o as hashDocument, s as contentHash } from "./inline-BbS93GAT.js";
9
8
  import { i as resolveFact, n as defineFacts, r as fact } from "./facts-1OKclPOk.js";
10
9
  import { n as documentMarkdown, r as sectionMarkdown, t as defineSectionKind } from "./markdown-C6dRE9zD.js";
@@ -203,6 +202,6 @@ function createMarkdownLock(documents) {
203
202
  };
204
203
  }
205
204
  //#endregion
206
- export { RENDERED_EDGE_ARTIFACT_SCHEMA_VERSION, applyTitleTemplate, buildRenderedGraph, buildSeoGraph, candidateSourceText, checkCoverage, checkFreshness, checkGraph, checkPageContent, checkRenderedContent, checkRenderedCoverage, contentCollection, contentHash, contentSignal, createMarkdownLock, decodeRenderedEdgeArtifact, decodeRenderedEdges, defineFacts, defineSectionKind, diffLinkGraph, documentMarkdown, extractAnchors, extractPageContent, fact, freshnessReport, generateLinkCandidates, graphFromJson, graphToJson, hasBlockingIssues, hasStructuralViolations, hashDocument, inspectHtml, inspectNode, isSitemapEligible, lastModified, llmsMarkdown, llmsSection, matchesClusterFilter, normalizePath, pageHeads, renderRobots, renderSitemap, renderedCheckTargets, renderedGraphFromEdges, resolveFact, resolveRouteLink, sectionMarkdown, selectPageHeadNodes, undirectedEdgeKey };
205
+ export { RENDERED_EDGE_ARTIFACT_SCHEMA_VERSION, applyTitleTemplate, buildRenderedGraph, buildSeoGraph, candidateSourceText, checkCoverage, checkFreshness, checkGraph, checkPageContent, checkRenderedContent, checkRenderedCoverage, contentCollection, contentHash, contentSignal, createMarkdownLock, decodeRenderedEdgeArtifact, decodeRenderedEdges, defineFacts, defineSectionKind, diffLinkGraph, documentMarkdown, extractAnchors, extractPageContent, fact, freshnessReport, generateLinkCandidates, graphFromJson, graphToJson, hasBlockingIssues, hasStructuralViolations, hashDocument, inspectHtml, inspectNode, isSitemapEligible, lastModified, llmsMarkdown, llmsSection, matchesClusterFilter, normalizePath, pageHeads, renderRobots, renderSitemap, renderedCheckTargets, renderedGraphFromEdges, resolveFact, resolveOgImage, resolveRouteLink, sectionMarkdown, selectPageHeadNodes, undirectedEdgeKey };
207
206
 
208
207
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/core/collections.ts","../src/core/link-candidates.ts","../src/core/resolve-route-link.ts","../src/markdown/lock.ts"],"sourcesContent":["/**\n * Content collections — pages a param route renders from content (MDX, a CMS,\n * a manifest) — in the shape the graph reads. Works with any source whose\n * pages carry a URL, which covers Fumadocs `loader().getPages()` directly.\n */\n\nimport type { PublicPath } from \"./declare\";\nimport type { SeoCollection, SeoEdge, SeoInstance, SeoSource } from \"./graph\";\n\n/** What a page contributes: its instance metadata plus the paths it links to deliberately. */\nexport interface ContentEntry extends Omit<SeoInstance, \"path\"> {\n /** Defaults to the page's `url`. */\n readonly path?: string | undefined;\n /** Deliberate cross-links, emitted as `related` edges from this page. */\n readonly related?: ReadonlyArray<string> | undefined;\n}\n\nexport interface ContentCollectionOptions<Page> {\n /** The param route these pages render through, e.g. `/blog/$slug`. */\n readonly route: PublicPath;\n /** The `source` label stamped on every node, e.g. `\"blog\"`. */\n readonly source: SeoSource;\n readonly pages: ReadonlyArray<Page>;\n /** Map one page to its entry; decode frontmatter here. */\n readonly entry: (page: Page) => ContentEntry;\n /** Edges beyond `related` (crumb parents, collection membership). */\n readonly edges?: ReadonlyArray<SeoEdge> | undefined;\n}\n\nexport function contentCollection<Page extends { readonly url: string }>(\n options: ContentCollectionOptions<Page>,\n): SeoCollection {\n const entries = options.pages.map((page) => {\n const entry = options.entry(page);\n return { ...entry, path: entry.path ?? page.url };\n });\n return {\n route: options.route,\n source: options.source,\n instances: entries.map(({ related: _related, ...instance }) => instance),\n edges: [\n ...entries.flatMap((entry) =>\n [...new Set(entry.related ?? [])].map((to): SeoEdge => ({ from: entry.path, to, type: \"related\" })),\n ),\n ...(options.edges ?? []),\n ],\n };\n}\n","/**\n * Pure candidate generation for contextual cross-links.\n *\n * Given the declared graph, enumerate `(source, destination)` pairs that are\n * *plausible* contextual links and *not already connected*. A pair is plausible\n * when the two pages sit in the same cluster: a shared top-level section (e.g.\n * `/blog/a` and `/blog/b` under `/blog`) or, for root-level pages that share no\n * section, the same `kind`. Only sitemap-eligible pages are candidates — a\n * noindex, redirect, or param template is not a link surface.\n *\n * The plan is a proposal, never an application: nothing here writes a\n * declaration. Effect- and framework-free so it runs in the CLI, a Worker, or a\n * test on the same graph.\n */\n\nimport type { SeoGraph, SeoNode } from \"./graph\";\nimport type { AnchorRegion, SimpleEdge } from \"./links\";\nimport { isSitemapEligible } from \"./projections\";\n\n/** One proposed contextual link, in the direction the link would be authored. */\nexport interface LinkCandidatePair {\n readonly source: string;\n readonly destination: string;\n /** The cluster the pair shares: a top-level section, or `kind:<kind>`. */\n readonly cluster: string;\n /** Why the pair is plausible, e.g. `same top-level section \"/blog\"`. */\n readonly reason: string;\n}\n\nexport interface LinkCandidateOptions {\n /** Maximum candidates to return after ordering (default 50). */\n readonly limit?: number;\n /** Restrict to these clusters; a filter may be a section or a bare kind. */\n readonly clusters?: ReadonlyArray<string>;\n /** Anchors already served in HTML. Only body-region edges exclude their own direction. */\n readonly renderedEdges?: ReadonlyArray<SimpleEdge & { readonly region?: AnchorRegion }>;\n}\n\n/** One cluster and how many candidate pairs it contributes (before `limit`). */\nexport interface LinkClusterSummary {\n readonly key: string;\n readonly candidates: number;\n}\n\nexport interface LinkCandidateResult {\n readonly candidates: ReadonlyArray<LinkCandidatePair>;\n /** Candidate pairs before `limit` was applied (after cluster filters). */\n readonly total: number;\n readonly truncated: boolean;\n readonly clusters: ReadonlyArray<LinkClusterSummary>;\n}\n\n/**\n * Canonical path key: query and hash stripped, trailing slashes removed, \"/\"\n * preserved. This is the same path key the graph and the rendered-link core\n * use, so a served anchor like `/blog/a?ref=nav` still matches the graph pair\n * `/blog/a`.\n */\nconst normalize = (path: string): string => {\n const pathname = path.split(/[?#]/, 1)[0] ?? \"\";\n const trimmed = pathname.replace(/\\/+$/, \"\");\n return trimmed === \"\" ? \"/\" : trimmed;\n};\n\n/** Directionless pair key, so an edge in either direction means \"connected\". */\nexport const undirectedEdgeKey = (from: string, to: string): string => {\n const [a, b] = [normalize(from), normalize(to)].sort();\n return `${a}\\u0000${b}`;\n};\n\nconst directedEdgeKey = (from: string, to: string): string => `${normalize(from)}\\u0000${normalize(to)}`;\n\n/** First path segment, or undefined for the root page. */\nconst topSegment = (path: string): string | undefined => {\n const segment = path.split(\"/\").filter(Boolean)[0];\n return segment === undefined || segment === \"\" ? undefined : segment;\n};\n\n/** A page with no nested segment (`/`, `/pricing`, `/blog`) is root-level. */\nconst isRootLevel = (path: string): boolean => path.split(\"/\").filter(Boolean).length <= 1;\n\n/**\n * The cluster a pair shares, or undefined when they share none. Section-first:\n * a shared top-level section wins. The kind fallback is local to root-level\n * pages, so it never pairs two nested pages from different sections.\n */\nexport const candidateClusterOf = (a: SeoNode, b: SeoNode): { key: string; reason: string } | undefined => {\n const aSegment = topSegment(a.path);\n const bSegment = topSegment(b.path);\n if (aSegment !== undefined && aSegment === bSegment) {\n return { key: aSegment, reason: `same top-level section \"/${aSegment}\"` };\n }\n if (isRootLevel(a.path) && isRootLevel(b.path) && a.kind === b.kind) {\n return { key: `kind:${a.kind}`, reason: `same kind \"${a.kind}\"` };\n }\n return undefined;\n};\n\n/** A `--cluster` filter matches a section by name, or a kind via its bare label. */\nexport const matchesClusterFilter = (cluster: string, filter: string): boolean => {\n const normalized = filter.replace(/^\\/+/, \"\").toLowerCase();\n const key = cluster.toLowerCase();\n return key === normalized || key === `kind:${normalized}`;\n};\n\n/** Human reason string for a candidate's source page, used when handing a plan to Jev. */\nexport const candidateSourceText = (node: SeoNode): string =>\n node.instance?.description?.trim() ||\n node.instance?.title?.trim() ||\n node.policy.link?.description?.trim() ||\n node.policy.link?.title?.trim() ||\n node.path;\n\n/**\n * Enumerate reviewable contextual-link candidates from the declared graph.\n *\n * Excluded: self-pairs, pages that are not sitemap-eligible, pairs already\n * declared as a `related` edge, and — when {@link LinkCandidateOptions.renderedEdges}\n * is supplied — pairs already rendered as an anchor. Queries, hashes, and\n * trailing slashes are normalized so both sides compare by the graph's path key.\n */\nexport const generateLinkCandidates = (\n graph: SeoGraph,\n options: LinkCandidateOptions = {},\n): LinkCandidateResult => {\n const limit = options.limit ?? 50;\n const filters = options.clusters ?? [];\n // Legacy bare edges have no region. Treat them as body links so a v1 edge dump\n // remains conservative; region-aware artifacts retain nav/header/footer edges.\n const rendered = new Set(\n (options.renderedEdges ?? []).filter((edge) => edge.region === undefined || edge.region === \"body\")\n .map((edge) => directedEdgeKey(edge.from, edge.to)),\n );\n const declared = new Set(\n graph.edges\n .filter((edge) => edge.type === \"related\")\n .map((edge) => directedEdgeKey(edge.from, edge.to)),\n );\n\n const eligible = [...graph.nodes.values()].filter(isSitemapEligible);\n\n const all: Array<LinkCandidatePair> = [];\n for (let i = 0; i < eligible.length; i++) {\n for (let j = i + 1; j < eligible.length; j++) {\n const a = eligible[i]!;\n const b = eligible[j]!;\n const cluster = candidateClusterOf(a, b);\n if (cluster === undefined) continue;\n if (filters.length > 0 && !filters.some((filter) => matchesClusterFilter(cluster.key, filter))) {\n continue;\n }\n for (const [source, destination] of [[a.path, b.path], [b.path, a.path]]) {\n const key = directedEdgeKey(source!, destination!);\n if (declared.has(key) || rendered.has(key)) continue;\n all.push({ source: source!, destination: destination!, cluster: cluster.key, reason: cluster.reason });\n }\n }\n }\n\n const compare = (x: LinkCandidatePair, y: LinkCandidatePair): number =>\n x.cluster < y.cluster\n ? -1\n : x.cluster > y.cluster\n ? 1\n : x.source < y.source\n ? -1\n : x.source > y.source\n ? 1\n : x.destination < y.destination\n ? -1\n : x.destination > y.destination\n ? 1\n : 0;\n all.sort(compare);\n\n const counts = new Map<string, number>();\n for (const pair of all) counts.set(pair.cluster, (counts.get(pair.cluster) ?? 0) + 1);\n\n const candidates = all.slice(0, limit);\n return {\n candidates,\n total: all.length,\n truncated: all.length > candidates.length,\n clusters: [...counts.entries()].map(([key, count]) => ({ key, candidates: count })),\n };\n};\n\n/**\n * Decode a rendered-edge dump supplied to the CLI: either a bare array of\n * `{ from, to }` edges, or an object carrying `edges` (or `internalEdges`).\n * Throws on any other shape — this is user input, not a provider payload.\n */\nexport const decodeRenderedEdges = (input: unknown): ReadonlyArray<SimpleEdge & { readonly region?: AnchorRegion }> => {\n const array =\n Array.isArray(input)\n ? input\n : input !== null && typeof input === \"object\" && Array.isArray((input as { edges?: unknown }).edges)\n ? (input as { edges: ReadonlyArray<unknown> }).edges\n : input !== null &&\n typeof input === \"object\" &&\n Array.isArray((input as { internalEdges?: unknown }).internalEdges)\n ? (input as { internalEdges: ReadonlyArray<unknown> }).internalEdges\n : undefined;\n\n if (array === undefined) {\n throw new Error(\n \"expected an array of { from, to } edges, or an object with an `edges` array\",\n );\n }\n\n return array.map((item) => {\n if (item === null || typeof item !== \"object\") {\n throw new Error(\"each rendered edge must be an object with string `from` and `to`\");\n }\n const { from, to, region } = item as { from?: unknown; to?: unknown; region?: unknown };\n if (typeof from !== \"string\" || typeof to !== \"string\") {\n throw new Error(\"each rendered edge must have string `from` and `to`\");\n }\n if (region !== undefined && ![\"nav\", \"header\", \"footer\", \"body\"].includes(region as string)) {\n throw new Error(\"rendered edge region must be nav, header, footer, or body\");\n }\n return region === undefined ? { from, to } : { from, to, region: region as AnchorRegion };\n });\n};\n","import type { AnyRoute, AnyRouter } from \"@tanstack/react-router\";\n\nimport type { RouteSeo } from \"./declare\";\n\ninterface RouteMaps {\n routesByPath: Record<string, AnyRoute | undefined>;\n routesById: Record<string, AnyRoute>;\n}\n\n/**\n * Resolve a route's declared `seo.link` card (title + description) by its full\n * path.\n *\n * `useRouter()` returns a Router typed to this app's exact route tree, so\n * `routesByPath` is keyed by the literal `FileRouteTypes[\"fullPaths\"]` union —\n * but callers here (declared `related` targets) hold arbitrary runtime path\n * strings, not that literal type. `routesByPath` and `routesById` are plain\n * Records on every Router instance regardless of which route tree it's\n * parameterized over, so this narrows to that structural shape once, at this\n * boundary, instead of threading an `AnyRoute` cast through every call site.\n */\nexport function resolveRouteLink(router: AnyRouter, path: string): RouteSeo[\"link\"] {\n const { routesByPath, routesById } = router as unknown as RouteMaps;\n\n const direct = routesByPath[path];\n if (direct) return direct.options.staticData?.seo?.link;\n\n const byFullPath = Object.values(routesById).find((route) => route.fullPath === path);\n return byFullPath?.options.staticData?.seo?.link;\n}\n","import type { PageDocument } from \"./document\";\n\n/** Stable, reviewable snapshot of document and message hashes with source locations. */\nexport interface MarkdownLock {\n readonly version: 1;\n readonly pages: Readonly<Record<string, {\n readonly document: string;\n readonly messages: Readonly<Record<string, string>>;\n }>>;\n}\n\nconst compare = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);\n\n/** Build the v1 lock without changing document hashes, message hashes, or sources. */\nexport function createMarkdownLock(documents: ReadonlyArray<PageDocument>): MarkdownLock {\n const pages: Record<string, { document: string; messages: Record<string, string> }> =\n Object.create(null);\n\n for (const document of [...documents].sort((left, right) => compare(left.path, right.path))) {\n if (Object.hasOwn(pages, document.path))\n throw new Error(`Duplicate document page path \"${document.path}\"`);\n if (!/^[0-9a-f]{64}$/.test(document.hash))\n throw new Error(`Invalid document document hash \"${document.hash}\" on \"${document.path}\"`);\n\n const messages: Record<string, string> = Object.create(null);\n for (const message of [...document.messages].sort(\n (left, right) => compare(left.hash, right.hash) || compare(left.source, right.source),\n )) {\n if (!/^[0-9a-f]{16}$/.test(message.hash))\n throw new Error(`Invalid document message hash \"${message.hash}\" on \"${document.path}\"`);\n if (!Object.hasOwn(messages, message.hash)) messages[message.hash] = message.source;\n }\n pages[document.path] = { document: document.hash, messages };\n }\n\n // Typed callers supply strings, but the lock remains a validating runtime boundary.\n for (const [path, page] of Object.entries(pages)) {\n if (typeof page.document !== \"string\")\n throw new Error(`Expected string\\n at [\"pages\"][${JSON.stringify(path)}][\"document\"]`);\n for (const [hash, source] of Object.entries(page.messages)) {\n if (typeof source !== \"string\")\n throw new Error(`Expected string\\n at [\"pages\"][${JSON.stringify(path)}][\"messages\"][${JSON.stringify(hash)}]`);\n }\n }\n return { version: 1, pages };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/core/collections.ts","../src/core/link-candidates.ts","../src/core/resolve-route-link.ts","../src/markdown/lock.ts"],"sourcesContent":["/**\n * Content collections — pages a param route renders from content (MDX, a CMS,\n * a manifest) — in the shape the graph reads. Works with any source whose\n * pages carry a URL, which covers Fumadocs `loader().getPages()` directly.\n */\n\nimport type { PublicPath } from \"./declare\";\nimport type { SeoCollection, SeoEdge, SeoInstance, SeoSource } from \"./graph\";\n\n/** What a page contributes: its instance metadata plus the paths it links to deliberately. */\nexport interface ContentEntry extends Omit<SeoInstance, \"path\"> {\n /** Defaults to the page's `url`. */\n readonly path?: string | undefined;\n /** Deliberate cross-links, emitted as `related` edges from this page. */\n readonly related?: ReadonlyArray<string> | undefined;\n}\n\nexport interface ContentCollectionOptions<Page> {\n /** The param route these pages render through, e.g. `/blog/$slug`. */\n readonly route: PublicPath;\n /** The `source` label stamped on every node, e.g. `\"blog\"`. */\n readonly source: SeoSource;\n readonly pages: ReadonlyArray<Page>;\n /** Map one page to its entry; decode frontmatter here. */\n readonly entry: (page: Page) => ContentEntry;\n /** Edges beyond `related` (crumb parents, collection membership). */\n readonly edges?: ReadonlyArray<SeoEdge> | undefined;\n}\n\nexport function contentCollection<Page extends { readonly url: string }>(\n options: ContentCollectionOptions<Page>,\n): SeoCollection {\n const entries = options.pages.map((page) => {\n const entry = options.entry(page);\n return { ...entry, path: entry.path ?? page.url };\n });\n return {\n route: options.route,\n source: options.source,\n instances: entries.map(({ related: _related, ...instance }) => instance),\n edges: [\n ...entries.flatMap((entry) =>\n [...new Set(entry.related ?? [])].map((to): SeoEdge => ({ from: entry.path, to, type: \"related\" })),\n ),\n ...(options.edges ?? []),\n ],\n };\n}\n","/**\n * Pure candidate generation for contextual cross-links.\n *\n * Given the declared graph, enumerate `(source, destination)` pairs that are\n * *plausible* contextual links and *not already connected*. A pair is plausible\n * when the two pages sit in the same cluster: a shared top-level section (e.g.\n * `/blog/a` and `/blog/b` under `/blog`) or, for root-level pages that share no\n * section, the same `kind`. Only sitemap-eligible pages are candidates — a\n * noindex, redirect, or param template is not a link surface.\n *\n * The plan is a proposal, never an application: nothing here writes a\n * declaration. Effect- and framework-free so it runs in the CLI, a Worker, or a\n * test on the same graph.\n */\n\nimport type { SeoGraph, SeoNode } from \"./graph\";\nimport type { AnchorRegion, SimpleEdge } from \"./links\";\nimport { isSitemapEligible } from \"./projections\";\n\n/** One proposed contextual link, in the direction the link would be authored. */\nexport interface LinkCandidatePair {\n readonly source: string;\n readonly destination: string;\n /** The cluster the pair shares: a top-level section, or `kind:<kind>`. */\n readonly cluster: string;\n /** Why the pair is plausible, e.g. `same top-level section \"/blog\"`. */\n readonly reason: string;\n}\n\nexport interface LinkCandidateOptions {\n /** Maximum candidates to return after ordering (default 50). */\n readonly limit?: number;\n /** Restrict to these clusters; a filter may be a section or a bare kind. */\n readonly clusters?: ReadonlyArray<string>;\n /** Anchors already served in HTML. Only body-region edges exclude their own direction. */\n readonly renderedEdges?: ReadonlyArray<SimpleEdge & { readonly region?: AnchorRegion }>;\n}\n\n/** One cluster and how many candidate pairs it contributes (before `limit`). */\nexport interface LinkClusterSummary {\n readonly key: string;\n readonly candidates: number;\n}\n\nexport interface LinkCandidateResult {\n readonly candidates: ReadonlyArray<LinkCandidatePair>;\n /** Candidate pairs before `limit` was applied (after cluster filters). */\n readonly total: number;\n readonly truncated: boolean;\n readonly clusters: ReadonlyArray<LinkClusterSummary>;\n}\n\n/**\n * Canonical path key: query and hash stripped, trailing slashes removed, \"/\"\n * preserved. This is the same path key the graph and the rendered-link core\n * use, so a served anchor like `/blog/a?ref=nav` still matches the graph pair\n * `/blog/a`.\n */\nconst normalize = (path: string): string => {\n const pathname = path.split(/[?#]/, 1)[0] ?? \"\";\n const trimmed = pathname.replace(/\\/+$/, \"\");\n return trimmed === \"\" ? \"/\" : trimmed;\n};\n\n/** Directionless pair key, so an edge in either direction means \"connected\". */\nexport const undirectedEdgeKey = (from: string, to: string): string => {\n const [a, b] = [normalize(from), normalize(to)].sort();\n return `${a}\\u0000${b}`;\n};\n\nconst directedEdgeKey = (from: string, to: string): string => `${normalize(from)}\\u0000${normalize(to)}`;\n\n/** First path segment, or undefined for the root page. */\nconst topSegment = (path: string): string | undefined => {\n const segment = path.split(\"/\").filter(Boolean)[0];\n return segment === undefined || segment === \"\" ? undefined : segment;\n};\n\n/** A page with no nested segment (`/`, `/pricing`, `/blog`) is root-level. */\nconst isRootLevel = (path: string): boolean => path.split(\"/\").filter(Boolean).length <= 1;\n\n/**\n * The cluster a pair shares, or undefined when they share none. Section-first:\n * a shared top-level section wins. The kind fallback is local to root-level\n * pages, so it never pairs two nested pages from different sections.\n */\nexport const candidateClusterOf = (a: SeoNode, b: SeoNode): { key: string; reason: string } | undefined => {\n const aSegment = topSegment(a.path);\n const bSegment = topSegment(b.path);\n if (aSegment !== undefined && aSegment === bSegment) {\n return { key: aSegment, reason: `same top-level section \"/${aSegment}\"` };\n }\n if (isRootLevel(a.path) && isRootLevel(b.path) && a.kind === b.kind) {\n return { key: `kind:${a.kind}`, reason: `same kind \"${a.kind}\"` };\n }\n return undefined;\n};\n\n/** A `--cluster` filter matches a section by name, or a kind via its bare label. */\nexport const matchesClusterFilter = (cluster: string, filter: string): boolean => {\n const normalized = filter.replace(/^\\/+/, \"\").toLowerCase();\n const key = cluster.toLowerCase();\n return key === normalized || key === `kind:${normalized}`;\n};\n\n/** Human reason string for a candidate's source page, used when handing a plan to Jev. */\nexport const candidateSourceText = (node: SeoNode): string =>\n node.instance?.description?.trim() ||\n node.instance?.title?.trim() ||\n node.policy.link?.description?.trim() ||\n node.policy.link?.title?.trim() ||\n node.path;\n\n/**\n * Enumerate reviewable contextual-link candidates from the declared graph.\n *\n * Excluded: self-pairs, pages that are not sitemap-eligible, pairs already\n * declared as a `related` edge, and — when {@link LinkCandidateOptions.renderedEdges}\n * is supplied — pairs already rendered as an anchor. Queries, hashes, and\n * trailing slashes are normalized so both sides compare by the graph's path key.\n */\nexport const generateLinkCandidates = (\n graph: SeoGraph,\n options: LinkCandidateOptions = {},\n): LinkCandidateResult => {\n const limit = options.limit ?? 50;\n const filters = options.clusters ?? [];\n // Legacy bare edges have no region. Treat them as body links so a v1 edge dump\n // remains conservative; region-aware artifacts retain nav/header/footer edges.\n const rendered = new Set(\n (options.renderedEdges ?? []).filter((edge) => edge.region === undefined || edge.region === \"body\")\n .map((edge) => directedEdgeKey(edge.from, edge.to)),\n );\n const declared = new Set(\n graph.edges\n .filter((edge) => edge.type === \"related\")\n .map((edge) => directedEdgeKey(edge.from, edge.to)),\n );\n\n const eligible = [...graph.nodes.values()].filter(isSitemapEligible);\n\n const all: Array<LinkCandidatePair> = [];\n for (let i = 0; i < eligible.length; i++) {\n for (let j = i + 1; j < eligible.length; j++) {\n const a = eligible[i]!;\n const b = eligible[j]!;\n const cluster = candidateClusterOf(a, b);\n if (cluster === undefined) continue;\n if (filters.length > 0 && !filters.some((filter) => matchesClusterFilter(cluster.key, filter))) {\n continue;\n }\n for (const [source, destination] of [[a.path, b.path], [b.path, a.path]]) {\n const key = directedEdgeKey(source!, destination!);\n if (declared.has(key) || rendered.has(key)) continue;\n all.push({ source: source!, destination: destination!, cluster: cluster.key, reason: cluster.reason });\n }\n }\n }\n\n const compare = (x: LinkCandidatePair, y: LinkCandidatePair): number =>\n x.cluster < y.cluster\n ? -1\n : x.cluster > y.cluster\n ? 1\n : x.source < y.source\n ? -1\n : x.source > y.source\n ? 1\n : x.destination < y.destination\n ? -1\n : x.destination > y.destination\n ? 1\n : 0;\n all.sort(compare);\n\n const counts = new Map<string, number>();\n for (const pair of all) counts.set(pair.cluster, (counts.get(pair.cluster) ?? 0) + 1);\n\n const candidates = all.slice(0, limit);\n return {\n candidates,\n total: all.length,\n truncated: all.length > candidates.length,\n clusters: [...counts.entries()].map(([key, count]) => ({ key, candidates: count })),\n };\n};\n\n/**\n * Decode a rendered-edge dump supplied to the CLI: either a bare array of\n * `{ from, to }` edges, or an object carrying `edges` (or `internalEdges`).\n * Throws on any other shape — this is user input, not a provider payload.\n */\nexport const decodeRenderedEdges = (input: unknown): ReadonlyArray<SimpleEdge & { readonly region?: AnchorRegion }> => {\n const array =\n Array.isArray(input)\n ? input\n : input !== null && typeof input === \"object\" && Array.isArray((input as { edges?: unknown }).edges)\n ? (input as { edges: ReadonlyArray<unknown> }).edges\n : input !== null &&\n typeof input === \"object\" &&\n Array.isArray((input as { internalEdges?: unknown }).internalEdges)\n ? (input as { internalEdges: ReadonlyArray<unknown> }).internalEdges\n : undefined;\n\n if (array === undefined) {\n throw new Error(\n \"expected an array of { from, to } edges, or an object with an `edges` array\",\n );\n }\n\n return array.map((item) => {\n if (item === null || typeof item !== \"object\") {\n throw new Error(\"each rendered edge must be an object with string `from` and `to`\");\n }\n const { from, to, region } = item as { from?: unknown; to?: unknown; region?: unknown };\n if (typeof from !== \"string\" || typeof to !== \"string\") {\n throw new Error(\"each rendered edge must have string `from` and `to`\");\n }\n if (region !== undefined && ![\"nav\", \"header\", \"footer\", \"body\"].includes(region as string)) {\n throw new Error(\"rendered edge region must be nav, header, footer, or body\");\n }\n return region === undefined ? { from, to } : { from, to, region: region as AnchorRegion };\n });\n};\n","import type { AnyRoute, AnyRouter } from \"@tanstack/react-router\";\n\nimport type { RouteSeo } from \"./declare\";\n\ninterface RouteMaps {\n routesByPath: Record<string, AnyRoute | undefined>;\n routesById: Record<string, AnyRoute>;\n}\n\n/**\n * Resolve a route's declared `seo.link` card (title + description) by its full\n * path.\n *\n * `useRouter()` returns a Router typed to this app's exact route tree, so\n * `routesByPath` is keyed by the literal `FileRouteTypes[\"fullPaths\"]` union —\n * but callers here (declared `related` targets) hold arbitrary runtime path\n * strings, not that literal type. `routesByPath` and `routesById` are plain\n * Records on every Router instance regardless of which route tree it's\n * parameterized over, so this narrows to that structural shape once, at this\n * boundary, instead of threading an `AnyRoute` cast through every call site.\n */\nexport function resolveRouteLink(router: AnyRouter, path: string): RouteSeo[\"link\"] {\n const { routesByPath, routesById } = router as unknown as RouteMaps;\n\n const direct = routesByPath[path];\n if (direct) return direct.options.staticData?.seo?.link;\n\n const byFullPath = Object.values(routesById).find((route) => route.fullPath === path);\n return byFullPath?.options.staticData?.seo?.link;\n}\n","import type { PageDocument } from \"./document\";\n\n/** Stable, reviewable snapshot of document and message hashes with source locations. */\nexport interface MarkdownLock {\n readonly version: 1;\n readonly pages: Readonly<Record<string, {\n readonly document: string;\n readonly messages: Readonly<Record<string, string>>;\n }>>;\n}\n\nconst compare = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);\n\n/** Build the v1 lock without changing document hashes, message hashes, or sources. */\nexport function createMarkdownLock(documents: ReadonlyArray<PageDocument>): MarkdownLock {\n const pages: Record<string, { document: string; messages: Record<string, string> }> =\n Object.create(null);\n\n for (const document of [...documents].sort((left, right) => compare(left.path, right.path))) {\n if (Object.hasOwn(pages, document.path))\n throw new Error(`Duplicate document page path \"${document.path}\"`);\n if (!/^[0-9a-f]{64}$/.test(document.hash))\n throw new Error(`Invalid document document hash \"${document.hash}\" on \"${document.path}\"`);\n\n const messages: Record<string, string> = Object.create(null);\n for (const message of [...document.messages].sort(\n (left, right) => compare(left.hash, right.hash) || compare(left.source, right.source),\n )) {\n if (!/^[0-9a-f]{16}$/.test(message.hash))\n throw new Error(`Invalid document message hash \"${message.hash}\" on \"${document.path}\"`);\n if (!Object.hasOwn(messages, message.hash)) messages[message.hash] = message.source;\n }\n pages[document.path] = { document: document.hash, messages };\n }\n\n // Typed callers supply strings, but the lock remains a validating runtime boundary.\n for (const [path, page] of Object.entries(pages)) {\n if (typeof page.document !== \"string\")\n throw new Error(`Expected string\\n at [\"pages\"][${JSON.stringify(path)}][\"document\"]`);\n for (const [hash, source] of Object.entries(page.messages)) {\n if (typeof source !== \"string\")\n throw new Error(`Expected string\\n at [\"pages\"][${JSON.stringify(path)}][\"messages\"][${JSON.stringify(hash)}]`);\n }\n }\n return { version: 1, pages };\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { a as SeoGraph, s as SeoNode } from "./graph-D1PIndsC.js";
1
+ import { b as SeoImage, l as SeoNode, s as SeoGraph } from "./graph-ChrvU-6X.js";
2
2
  //#region src/markdown/document.d.ts
3
3
  type InlineNode = string | {
4
4
  readonly fact: string;
@@ -92,6 +92,7 @@ interface PageHead {
92
92
  readonly path: string;
93
93
  readonly title: string;
94
94
  readonly description: string;
95
+ readonly image?: SeoImage | undefined;
95
96
  }
96
97
  interface PageHeadsOptions {
97
98
  /** Path globs: `*` matches a segment, `**` matches across segments. */
@@ -105,4 +106,4 @@ declare function selectPageHeadNodes(graph: SeoGraph, options?: PageHeadsOptions
105
106
  declare function pageHeads(graph: SeoGraph, options?: PageHeadsOptions): Array<PageHead>;
106
107
  //#endregion
107
108
  export { hashDocument as _, FactDefinition as a, fact as c, InlineNode as d, Message as f, VisualDocument as g, SectionItem as h, selectPageHeadNodes as i, resolveFact as l, SectionDocument as m, PageHeadsOptions as n, Facts as o, PageDocument as p, pageHeads as r, defineFacts as s, PageHead as t, BodyEntry as u };
108
- //# sourceMappingURL=page-heads-BfjWQvau.d.ts.map
109
+ //# sourceMappingURL=page-heads-DDz_2_hq.d.ts.map
@@ -1,4 +1,4 @@
1
- import { a as globToRegExp } from "./checks-BS-aDXB_.js";
1
+ import { a as globToRegExp } from "./checks-BEhJPNzN.js";
2
2
  //#region src/core/page-heads.ts
3
3
  /** Select page nodes before the host resolves metadata; sitemap-disabled hubs remain pages. */
4
4
  function selectPageHeadNodes(graph, options = {}) {
@@ -15,11 +15,12 @@ function pageHeads(graph, options = {}) {
15
15
  return {
16
16
  path: node.path,
17
17
  title,
18
- description
18
+ description,
19
+ ...node.head?.image === void 0 ? {} : { image: node.head.image }
19
20
  };
20
21
  });
21
22
  }
22
23
  //#endregion
23
24
  export { selectPageHeadNodes as n, pageHeads as t };
24
25
 
25
- //# sourceMappingURL=page-heads-BgbaIjj9.js.map
26
+ //# sourceMappingURL=page-heads-YL8nyYjF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-heads-YL8nyYjF.js","names":[],"sources":["../src/core/page-heads.ts"],"sourcesContent":["import type { SeoImage } from \"./declare\";\nimport { globToRegExp } from \"./checks\";\nimport type { SeoGraph, SeoNode } from \"./graph\";\n\n/** Plain document metadata, without framework or schema dependencies. */\nexport interface PageHead {\n readonly path: string;\n readonly title: string;\n readonly description: string;\n readonly image?: SeoImage | undefined;\n}\n\nexport interface PageHeadsOptions {\n /** Path globs: `*` matches a segment, `**` matches across segments. */\n readonly exclude?: ReadonlyArray<string>;\n /** Exclude noindex pages and redirects. Parameter templates are always excluded. Defaults to true. */\n readonly indexable?: boolean;\n}\n\n/** Select page nodes before the host resolves metadata; sitemap-disabled hubs remain pages. */\nexport function selectPageHeadNodes(graph: SeoGraph, options: PageHeadsOptions = {}): Array<SeoNode> {\n const excludes = (options.exclude ?? []).map(globToRegExp);\n return [...graph.nodes.values()].filter((node) =>\n node.kind !== \"layout\" &&\n !node.path.includes(\"$\") &&\n (node.policy.sitemap !== undefined || node.instance !== undefined) &&\n !excludes.some((pattern) => pattern.test(node.path)) &&\n (options.indexable === false || (\n node.policy.redirectTo === undefined &&\n !node.policy.robots?.toLowerCase().split(/[\\s,]+/).includes(\"noindex\")\n )),\n );\n}\n\n/** Extract resolved heads, naming the selected page when metadata is missing or blank. */\nexport function pageHeads(graph: SeoGraph, options: PageHeadsOptions = {}): Array<PageHead> {\n return selectPageHeadNodes(graph, options).map((node) => {\n const title = node.head?.title;\n const description = node.head?.description;\n if (!title?.trim()) throw new Error(`Invalid page head for ${node.path}: missing title`);\n if (!description?.trim()) throw new Error(`Invalid page head for ${node.path}: missing description`);\n return { path: node.path, title, description, ...(node.head?.image === undefined ? {} : { image: node.head.image }) };\n });\n}\n"],"mappings":";;;AAoBA,SAAgB,oBAAoB,OAAiB,UAA4B,CAAC,GAAmB;CACnG,MAAM,YAAY,QAAQ,WAAW,CAAC,EAAA,CAAG,IAAI,YAAY;CACzD,OAAO,CAAC,GAAG,MAAM,MAAM,OAAO,CAAC,CAAC,CAAC,QAAQ,SACvC,KAAK,SAAS,YACd,CAAC,KAAK,KAAK,SAAS,GAAG,MACtB,KAAK,OAAO,YAAY,KAAA,KAAa,KAAK,aAAa,KAAA,MACxD,CAAC,SAAS,MAAM,YAAY,QAAQ,KAAK,KAAK,IAAI,CAAC,MAClD,QAAQ,cAAc,SACrB,KAAK,OAAO,eAAe,KAAA,KAC3B,CAAC,KAAK,OAAO,QAAQ,YAAY,CAAC,CAAC,MAAM,QAAQ,CAAC,CAAC,SAAS,SAAS,EAEzE;AACF;;AAGA,SAAgB,UAAU,OAAiB,UAA4B,CAAC,GAAoB;CAC1F,OAAO,oBAAoB,OAAO,OAAO,CAAC,CAAC,KAAK,SAAS;EACvD,MAAM,QAAQ,KAAK,MAAM;EACzB,MAAM,cAAc,KAAK,MAAM;EAC/B,IAAI,CAAC,OAAO,KAAK,GAAG,MAAM,IAAI,MAAM,yBAAyB,KAAK,KAAK,gBAAgB;EACvF,IAAI,CAAC,aAAa,KAAK,GAAG,MAAM,IAAI,MAAM,yBAAyB,KAAK,KAAK,sBAAsB;EACnG,OAAO;GAAE,MAAM,KAAK;GAAM;GAAO;GAAa,GAAI,KAAK,MAAM,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO,KAAK,KAAK,MAAM;EAAG;CACtH,CAAC;AACH"}