pagegraph 0.11.0 → 0.13.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 (100) hide show
  1. package/README.md +403 -148
  2. package/dist/audit.d.ts +2 -2
  3. package/dist/audit.js +4 -4
  4. package/dist/build-only/audit.d.ts +1 -0
  5. package/dist/build-only/audit.js +6 -0
  6. package/dist/build-only/audit.js.map +1 -0
  7. package/dist/build-only/claims.d.ts +1 -0
  8. package/dist/build-only/claims.js +6 -0
  9. package/dist/build-only/claims.js.map +1 -0
  10. package/dist/build-only/config.d.ts +1 -0
  11. package/dist/build-only/config.js +6 -0
  12. package/dist/build-only/config.js.map +1 -0
  13. package/dist/build-only/oxlint.d.ts +1 -0
  14. package/dist/build-only/oxlint.js +6 -0
  15. package/dist/build-only/oxlint.js.map +1 -0
  16. package/dist/build-only/tanstack-start.d.ts +1 -0
  17. package/dist/build-only/tanstack-start.js +6 -0
  18. package/dist/build-only/tanstack-start.js.map +1 -0
  19. package/dist/build-only/vite.d.ts +1 -0
  20. package/dist/build-only/vite.js +6 -0
  21. package/dist/build-only/vite.js.map +1 -0
  22. package/dist/{checks-nk8ZRRIA.js → checks-BS-aDXB_.js} +4 -142
  23. package/dist/checks-BS-aDXB_.js.map +1 -0
  24. package/dist/claims-Bch7Lxlo.d.ts +92 -0
  25. package/dist/claims-Bv7voeEV.js +431 -0
  26. package/dist/claims-Bv7voeEV.js.map +1 -0
  27. package/dist/claims.d.ts +94 -0
  28. package/dist/claims.js +2 -0
  29. package/dist/cli.js +10368 -8485
  30. package/dist/cli.js.map +1 -1
  31. package/dist/config.d.ts +4 -83
  32. package/dist/config.js +3 -122
  33. package/dist/config.js.map +1 -1
  34. package/dist/{content-CB_kHmJ3.js → content-CHd0X4s6.js} +3 -2
  35. package/dist/{content-CB_kHmJ3.js.map → content-CHd0X4s6.js.map} +1 -1
  36. package/dist/{content-DVL2iyGU.d.ts → content-DgNr7apS.d.ts} +5 -130
  37. package/dist/declare-D5o7hwmv.js +9 -0
  38. package/dist/declare-D5o7hwmv.js.map +1 -0
  39. package/dist/document-CztEu3Vd.d.ts +109 -0
  40. package/dist/document-Dvemcv7V.js +379 -0
  41. package/dist/document-Dvemcv7V.js.map +1 -0
  42. package/dist/facts-1OKclPOk.js +115 -0
  43. package/dist/facts-1OKclPOk.js.map +1 -0
  44. package/dist/graph-D1PIndsC.d.ts +12238 -0
  45. package/dist/graph-DMNOLXLu.js +172 -0
  46. package/dist/graph-DMNOLXLu.js.map +1 -0
  47. package/dist/index-C5w9ripz.d.ts +204 -0
  48. package/dist/index.d.ts +6 -196
  49. package/dist/index.js +55 -134
  50. package/dist/index.js.map +1 -1
  51. package/dist/inline-BbS93GAT.js +682 -0
  52. package/dist/inline-BbS93GAT.js.map +1 -0
  53. package/dist/llms-Dhr_5u2O.js +19 -0
  54. package/dist/llms-Dhr_5u2O.js.map +1 -0
  55. package/dist/markdown-C6dRE9zD.js +168 -0
  56. package/dist/markdown-C6dRE9zD.js.map +1 -0
  57. package/dist/markdown-path-lJB9BAVZ.js +6 -0
  58. package/dist/markdown-path-lJB9BAVZ.js.map +1 -0
  59. package/dist/message-CuBjgZ74.js +12 -0
  60. package/dist/message-CuBjgZ74.js.map +1 -0
  61. package/dist/oxlint.d.ts +58 -0
  62. package/dist/oxlint.js +202 -0
  63. package/dist/oxlint.js.map +1 -0
  64. package/dist/page-heads-BfjWQvau.d.ts +108 -0
  65. package/dist/page-heads-BgbaIjj9.js +25 -0
  66. package/dist/page-heads-BgbaIjj9.js.map +1 -0
  67. package/dist/projections-Bk-qOOsR.d.ts +78 -0
  68. package/dist/projections-D0oOFKiY.js +142 -0
  69. package/dist/projections-D0oOFKiY.js.map +1 -0
  70. package/dist/react.d.ts +27 -12051
  71. package/dist/react.js +14 -4
  72. package/dist/react.js.map +1 -1
  73. package/dist/route-config-35ZEzkNC.js +308 -0
  74. package/dist/route-config-35ZEzkNC.js.map +1 -0
  75. package/dist/route-config-wneg6ktn.d.ts +29 -0
  76. package/dist/tanstack-start/index.d.ts +56 -0
  77. package/dist/tanstack-start/index.js +717 -0
  78. package/dist/tanstack-start/index.js.map +1 -0
  79. package/dist/tanstack-start/markdown.d.ts +12 -0
  80. package/dist/tanstack-start/markdown.js +149 -0
  81. package/dist/tanstack-start/markdown.js.map +1 -0
  82. package/dist/tanstack-start/prerender-worker.d.ts +2 -0
  83. package/dist/tanstack-start/prerender-worker.js +7 -0
  84. package/dist/tanstack-start/prerender-worker.js.map +1 -0
  85. package/dist/tanstack-start/react.d.ts +7 -0
  86. package/dist/tanstack-start/react.js +12 -0
  87. package/dist/tanstack-start/react.js.map +1 -0
  88. package/dist/tanstack-start/server.d.ts +20 -0
  89. package/dist/tanstack-start/server.js +77 -0
  90. package/dist/tanstack-start/server.js.map +1 -0
  91. package/dist/vite-graph-loader-CsGiYNWE.js +105 -0
  92. package/dist/vite-graph-loader-CsGiYNWE.js.map +1 -0
  93. package/dist/vite-graph-loader-DZCwejuJ.d.ts +133 -0
  94. package/dist/vite.d.ts +2 -28
  95. package/dist/vite.js +1 -307
  96. package/dist/wire-B4RQMc4e.js +26 -0
  97. package/dist/wire-B4RQMc4e.js.map +1 -0
  98. package/package.json +70 -7
  99. package/dist/checks-nk8ZRRIA.js.map +0 -1
  100. package/dist/vite.js.map +0 -1
package/dist/oxlint.js ADDED
@@ -0,0 +1,202 @@
1
+ //#region src/oxlint/no-bare-text.js
2
+ function importedDocumentNames(program) {
3
+ const namespaces = /* @__PURE__ */ new Set();
4
+ const suppressNames = /* @__PURE__ */ new Set();
5
+ let runtimeImport = false;
6
+ for (const declaration of program.body) {
7
+ if (declaration.type !== "ImportDeclaration" || declaration.source.value !== "pagegraph/react" || declaration.importKind === "type") continue;
8
+ if (declaration.specifiers.length === 0) runtimeImport = true;
9
+ for (const specifier of declaration.specifiers) if (specifier.type === "ImportSpecifier" && specifier.importKind !== "type") {
10
+ const imported = specifier.imported.name ?? specifier.imported.value;
11
+ if ([
12
+ "T",
13
+ "Title",
14
+ "ForHumans"
15
+ ].includes(imported)) suppressNames.add(specifier.local.name);
16
+ runtimeImport = true;
17
+ } else if (specifier.type === "ImportNamespaceSpecifier") {
18
+ namespaces.add(specifier.local.name);
19
+ runtimeImport = true;
20
+ } else if (specifier.type === "ImportDefaultSpecifier") runtimeImport = true;
21
+ }
22
+ return {
23
+ namespaces,
24
+ suppressNames,
25
+ runtimeImport
26
+ };
27
+ }
28
+ function jsxName(node) {
29
+ if (node.type === "JSXIdentifier") return [node.name];
30
+ if (node.type === "JSXMemberExpression") return [...jsxName(node.object), node.property.name];
31
+ return [];
32
+ }
33
+ function matchesFiles(filename, files) {
34
+ if (!files?.length) return true;
35
+ const normalized = filename.replaceAll("\\", "/");
36
+ return files.some((pattern) => {
37
+ const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
38
+ return new RegExp(`(?:^|/)${escaped.replaceAll("**", "§").replaceAll("*", "[^/]*").replaceAll("§", ".*")}$`).test(normalized);
39
+ });
40
+ }
41
+ function* literalLabels(expression, stringsOnly = false) {
42
+ switch (expression.type) {
43
+ case "Literal":
44
+ if (typeof expression.value === "string" || !stringsOnly && typeof expression.value === "number") yield expression;
45
+ break;
46
+ case "ConditionalExpression":
47
+ yield* literalLabels(expression.consequent, stringsOnly);
48
+ yield* literalLabels(expression.alternate, stringsOnly);
49
+ break;
50
+ case "LogicalExpression":
51
+ if (expression.operator !== "&&") yield* literalLabels(expression.left, stringsOnly);
52
+ yield* literalLabels(expression.right, stringsOnly);
53
+ break;
54
+ case "BinaryExpression":
55
+ if (expression.operator === "+") {
56
+ yield* literalLabels(expression.left, true);
57
+ yield* literalLabels(expression.right, true);
58
+ }
59
+ break;
60
+ case "TemplateLiteral":
61
+ if (expression.quasis.some((part) => part.value.raw.trim())) yield expression;
62
+ for (const interpolation of expression.expressions) yield* literalLabels(interpolation, true);
63
+ }
64
+ }
65
+ var no_bare_text_default = {
66
+ meta: {
67
+ type: "problem",
68
+ docs: { description: "Require customer-facing text to use pagegraph primitives." },
69
+ schema: [{
70
+ type: "object",
71
+ properties: { files: {
72
+ type: "array",
73
+ items: { type: "string" }
74
+ } },
75
+ additionalProperties: false
76
+ }]
77
+ },
78
+ create(context) {
79
+ const files = context.options[0]?.files;
80
+ if (!matchesFiles(context.filename, files)) return {};
81
+ let active = false;
82
+ let names;
83
+ const suppressedRanges = [];
84
+ const attributeExpressions = /* @__PURE__ */ new Set();
85
+ const report = (node) => context.report({
86
+ node,
87
+ message: "Wrap customer-facing text in T, Title, or ForHumans."
88
+ });
89
+ const isSuppressed = (node) => suppressedRanges.some(([start, end]) => node.start >= start && node.end <= end);
90
+ return {
91
+ Program(node) {
92
+ const imports = importedDocumentNames(node);
93
+ active = imports.runtimeImport;
94
+ names = imports;
95
+ },
96
+ JSXElement(node) {
97
+ const parts = jsxName(node.openingElement.name);
98
+ const suppressed = parts.length === 1 && names.suppressNames.has(parts[0]) || parts.length === 2 && names.namespaces.has(parts[0]) && [
99
+ "T",
100
+ "Title",
101
+ "ForHumans"
102
+ ].includes(parts[1]);
103
+ if (active && suppressed) suppressedRanges.push([node.start, node.end]);
104
+ },
105
+ JSXText(node) {
106
+ if (active && !isSuppressed(node) && node.value.trim()) report(node);
107
+ },
108
+ JSXAttribute(node) {
109
+ if (active && node.value?.type === "JSXExpressionContainer") attributeExpressions.add(node.value.start);
110
+ },
111
+ JSXExpressionContainer(node) {
112
+ if (active && !attributeExpressions.has(node.start) && !isSuppressed(node)) for (const label of literalLabels(node.expression)) report(label);
113
+ }
114
+ };
115
+ }
116
+ };
117
+ //#endregion
118
+ //#region src/oxlint/t-children.js
119
+ const INLINE_TAGS = /* @__PURE__ */ new Set([
120
+ "em",
121
+ "strong",
122
+ "a",
123
+ "code",
124
+ "br"
125
+ ]);
126
+ function importedTNames(program) {
127
+ const named = /* @__PURE__ */ new Set();
128
+ const facts = /* @__PURE__ */ new Set();
129
+ const namespaces = /* @__PURE__ */ new Set();
130
+ for (const declaration of program.body) {
131
+ if (declaration.type !== "ImportDeclaration" || declaration.source.value !== "pagegraph/react" || declaration.importKind === "type") continue;
132
+ for (const specifier of declaration.specifiers) if (specifier.type === "ImportSpecifier" && specifier.importKind !== "type") {
133
+ const imported = specifier.imported.name ?? specifier.imported.value;
134
+ if (imported === "T" || imported === "Title") named.add(specifier.local.name);
135
+ if (imported === "Fact") facts.add(specifier.local.name);
136
+ } else if (specifier.type === "ImportNamespaceSpecifier") namespaces.add(specifier.local.name);
137
+ }
138
+ return {
139
+ named,
140
+ facts,
141
+ namespaces
142
+ };
143
+ }
144
+ function parts(node) {
145
+ if (node.type === "JSXIdentifier") return [node.name];
146
+ if (node.type === "JSXMemberExpression") return [...parts(node.object), node.property.name];
147
+ return [];
148
+ }
149
+ function isT(node, names) {
150
+ const path = parts(node);
151
+ return path.length === 1 && names.named.has(path[0]) || path.length === 2 && names.namespaces.has(path[0]) && ["T", "Title"].includes(path[1]);
152
+ }
153
+ function allowedElement(node, names) {
154
+ const path = parts(node);
155
+ if (path.length === 1 && INLINE_TAGS.has(path[0])) return true;
156
+ if (path.length === 1 && names.facts.has(path[0])) return true;
157
+ return path.length === 2 && names.namespaces.has(path[0]) && path[1] === "Fact";
158
+ }
159
+ //#endregion
160
+ //#region src/oxlint/index.js
161
+ var oxlint_default = {
162
+ meta: { name: "pagegraph" },
163
+ rules: {
164
+ "no-bare-text": no_bare_text_default,
165
+ "t-children": {
166
+ meta: {
167
+ type: "problem",
168
+ docs: { description: "Restrict T and Title children to plain text and approved inline content." }
169
+ },
170
+ create(context) {
171
+ const stack = [];
172
+ let names;
173
+ return {
174
+ Program(node) {
175
+ names = importedTNames(node);
176
+ },
177
+ JSXElement(node) {
178
+ const parent = stack.at(-1);
179
+ if (parent?.insideT && !allowedElement(node.openingElement.name, names)) context.report({
180
+ node: node.openingElement.name,
181
+ message: "T and Title accept text, em, strong, a, code, br, and Fact only."
182
+ });
183
+ stack.push({ insideT: isT(node.openingElement.name, names) || Boolean(parent?.insideT) });
184
+ },
185
+ "JSXElement:exit"() {
186
+ stack.pop();
187
+ },
188
+ JSXFragment(node) {
189
+ if (stack.at(-1)?.insideT) context.report({
190
+ node,
191
+ message: "T and Title accept text, em, strong, a, code, br, and Fact only; fragments are not supported."
192
+ });
193
+ }
194
+ };
195
+ }
196
+ }
197
+ }
198
+ };
199
+ //#endregion
200
+ export { oxlint_default as default };
201
+
202
+ //# sourceMappingURL=oxlint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oxlint.js","names":["noBareText","tChildren"],"sources":["../src/oxlint/no-bare-text.js","../src/oxlint/t-children.js","../src/oxlint/index.js"],"sourcesContent":["function importedDocumentNames(program) {\n const namespaces = new Set();\n const suppressNames = new Set();\n let runtimeImport = false;\n for (const declaration of program.body) {\n if (\n declaration.type !== \"ImportDeclaration\" ||\n declaration.source.value !== \"pagegraph/react\" ||\n declaration.importKind === \"type\"\n )\n continue;\n if (declaration.specifiers.length === 0) runtimeImport = true;\n for (const specifier of declaration.specifiers) {\n if (specifier.type === \"ImportSpecifier\" && specifier.importKind !== \"type\") {\n const imported = specifier.imported.name ?? specifier.imported.value;\n if ([\"T\", \"Title\", \"ForHumans\"].includes(imported)) suppressNames.add(specifier.local.name);\n runtimeImport = true;\n } else if (specifier.type === \"ImportNamespaceSpecifier\") {\n namespaces.add(specifier.local.name);\n runtimeImport = true;\n } else if (specifier.type === \"ImportDefaultSpecifier\") runtimeImport = true;\n }\n }\n return { namespaces, suppressNames, runtimeImport };\n}\n\nfunction jsxName(node) {\n if (node.type === \"JSXIdentifier\") return [node.name];\n if (node.type === \"JSXMemberExpression\") return [...jsxName(node.object), node.property.name];\n return [];\n}\n\nfunction matchesFiles(filename, files) {\n if (!files?.length) return true;\n const normalized = filename.replaceAll(\"\\\\\", \"/\");\n return files.some((pattern) => {\n const escaped = pattern.replace(/[|\\\\{}()[\\]^$+?.]/g, \"\\\\$&\");\n const regex = new RegExp(\n `(?:^|/)${escaped.replaceAll(\"**\", \"§\").replaceAll(\"*\", \"[^/]*\").replaceAll(\"§\", \".*\")}$`,\n );\n return regex.test(normalized);\n });\n}\n\nfunction* literalLabels(expression, stringsOnly = false) {\n switch (expression.type) {\n case \"Literal\":\n if (\n typeof expression.value === \"string\" ||\n (!stringsOnly && typeof expression.value === \"number\")\n )\n yield expression;\n break;\n case \"ConditionalExpression\":\n yield* literalLabels(expression.consequent, stringsOnly);\n yield* literalLabels(expression.alternate, stringsOnly);\n break;\n case \"LogicalExpression\":\n // A truthy string on the left of && is only a guard, never the returned child.\n if (expression.operator !== \"&&\") yield* literalLabels(expression.left, stringsOnly);\n yield* literalLabels(expression.right, stringsOnly);\n break;\n case \"BinaryExpression\":\n // Comparisons and numeric arithmetic consume their literals rather than displaying them.\n if (expression.operator === \"+\") {\n yield* literalLabels(expression.left, true);\n yield* literalLabels(expression.right, true);\n }\n break;\n case \"TemplateLiteral\":\n if (expression.quasis.some((part) => part.value.raw.trim())) yield expression;\n for (const interpolation of expression.expressions) yield* literalLabels(interpolation, true);\n break;\n }\n}\n\nexport default {\n meta: {\n type: \"problem\",\n docs: { description: \"Require customer-facing text to use pagegraph primitives.\" },\n schema: [\n {\n type: \"object\",\n properties: { files: { type: \"array\", items: { type: \"string\" } } },\n additionalProperties: false,\n },\n ],\n },\n create(context) {\n const files = context.options[0]?.files;\n if (!matchesFiles(context.filename, files)) return {};\n let active = false;\n let names;\n const suppressedRanges = [];\n const attributeExpressions = new Set();\n const report = (node) =>\n context.report({ node, message: \"Wrap customer-facing text in T, Title, or ForHumans.\" });\n const isSuppressed = (node) =>\n suppressedRanges.some(([start, end]) => node.start >= start && node.end <= end);\n return {\n Program(node) {\n const imports = importedDocumentNames(node);\n active = imports.runtimeImport;\n names = imports;\n },\n JSXElement(node) {\n const parts = jsxName(node.openingElement.name);\n const suppressed =\n (parts.length === 1 && names.suppressNames.has(parts[0])) ||\n (parts.length === 2 &&\n names.namespaces.has(parts[0]) &&\n [\"T\", \"Title\", \"ForHumans\"].includes(parts[1]));\n if (active && suppressed) suppressedRanges.push([node.start, node.end]);\n },\n JSXText(node) {\n if (active && !isSuppressed(node) && node.value.trim()) report(node);\n },\n JSXAttribute(node) {\n // Exempt the prop value itself; nested JSX has its own children to check.\n if (active && node.value?.type === \"JSXExpressionContainer\")\n attributeExpressions.add(node.value.start);\n },\n JSXExpressionContainer(node) {\n if (active && !attributeExpressions.has(node.start) && !isSuppressed(node))\n for (const label of literalLabels(node.expression)) report(label);\n },\n };\n },\n};\n","const INLINE_TAGS = new Set([\"em\", \"strong\", \"a\", \"code\", \"br\"]);\n\nfunction importedTNames(program) {\n const named = new Set();\n const facts = new Set();\n const namespaces = new Set();\n for (const declaration of program.body) {\n if (\n declaration.type !== \"ImportDeclaration\" ||\n declaration.source.value !== \"pagegraph/react\" ||\n declaration.importKind === \"type\"\n )\n continue;\n for (const specifier of declaration.specifiers) {\n if (specifier.type === \"ImportSpecifier\" && specifier.importKind !== \"type\") {\n const imported = specifier.imported.name ?? specifier.imported.value;\n if (imported === \"T\" || imported === \"Title\") named.add(specifier.local.name);\n if (imported === \"Fact\") facts.add(specifier.local.name);\n } else if (specifier.type === \"ImportNamespaceSpecifier\")\n namespaces.add(specifier.local.name);\n }\n }\n return { named, facts, namespaces };\n}\n\nfunction parts(node) {\n if (node.type === \"JSXIdentifier\") return [node.name];\n if (node.type === \"JSXMemberExpression\") return [...parts(node.object), node.property.name];\n return [];\n}\n\nfunction isT(node, names) {\n const path = parts(node);\n return (\n (path.length === 1 && names.named.has(path[0])) ||\n (path.length === 2 && names.namespaces.has(path[0]) && [\"T\", \"Title\"].includes(path[1]))\n );\n}\n\nfunction allowedElement(node, names) {\n const path = parts(node);\n if (path.length === 1 && INLINE_TAGS.has(path[0])) return true;\n if (path.length === 1 && names.facts.has(path[0])) return true;\n return path.length === 2 && names.namespaces.has(path[0]) && path[1] === \"Fact\";\n}\n\nexport default {\n meta: {\n type: \"problem\",\n docs: { description: \"Restrict T and Title children to plain text and approved inline content.\" },\n },\n create(context) {\n const stack = [];\n let names;\n return {\n Program(node) {\n names = importedTNames(node);\n },\n JSXElement(node) {\n const parent = stack.at(-1);\n if (parent?.insideT && !allowedElement(node.openingElement.name, names)) {\n context.report({\n node: node.openingElement.name,\n message: \"T and Title accept text, em, strong, a, code, br, and Fact only.\",\n });\n }\n stack.push({ insideT: isT(node.openingElement.name, names) || Boolean(parent?.insideT) });\n },\n \"JSXElement:exit\"() {\n stack.pop();\n },\n JSXFragment(node) {\n if (stack.at(-1)?.insideT)\n context.report({\n node,\n message:\n \"T and Title accept text, em, strong, a, code, br, and Fact only; fragments are not supported.\",\n });\n },\n };\n },\n};\n","import noBareText from \"./no-bare-text.js\";\nimport tChildren from \"./t-children.js\";\n\nexport default {\n meta: { name: \"pagegraph\" },\n rules: { \"no-bare-text\": noBareText, \"t-children\": tChildren },\n};\n"],"mappings":"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"}
@@ -0,0 +1,108 @@
1
+ import { a as SeoGraph, s as SeoNode } from "./graph-D1PIndsC.js";
2
+ //#region src/markdown/document.d.ts
3
+ type InlineNode = string | {
4
+ readonly fact: string;
5
+ readonly text: string;
6
+ } | {
7
+ readonly tag: "em" | "strong" | "a" | "code" | "br";
8
+ readonly href?: string;
9
+ readonly children: ReadonlyArray<InlineNode>;
10
+ };
11
+ interface Message {
12
+ readonly hash: string;
13
+ readonly tree: ReadonlyArray<InlineNode>;
14
+ readonly text: string;
15
+ readonly source: string;
16
+ readonly facts: ReadonlyArray<string>;
17
+ readonly audience: "all" | "agents" | "humans";
18
+ }
19
+ interface VisualDocument {
20
+ readonly type: "visual";
21
+ readonly text: string;
22
+ readonly source: string;
23
+ readonly audience: Message["audience"];
24
+ }
25
+ type BodyEntry = {
26
+ readonly type: "message";
27
+ readonly message: Message;
28
+ } | VisualDocument;
29
+ interface SectionItem {
30
+ readonly title?: Message;
31
+ /** Destination recorded from the item's rendered Section.Item.Link anchor. */
32
+ readonly href?: string;
33
+ readonly body: ReadonlyArray<BodyEntry>;
34
+ readonly cells: ReadonlyArray<Message>;
35
+ readonly source: string;
36
+ readonly audience: Message["audience"];
37
+ }
38
+ interface SectionDocument {
39
+ /** Captured with human-only messages included for offline custom serializers. */
40
+ readonly claimsMarkdown?: string;
41
+ readonly evidence?: boolean;
42
+ readonly kind: string;
43
+ readonly id: string;
44
+ readonly title?: Message;
45
+ readonly language?: string;
46
+ readonly body: ReadonlyArray<BodyEntry>;
47
+ readonly items: ReadonlyArray<SectionItem>;
48
+ readonly source: string;
49
+ readonly audience: Message["audience"];
50
+ }
51
+ interface PageDocument {
52
+ readonly path: string;
53
+ readonly hash: string;
54
+ readonly title: string;
55
+ readonly description: string;
56
+ readonly sections: ReadonlyArray<SectionDocument>;
57
+ readonly messages: ReadonlyArray<Message>;
58
+ readonly facts: ReadonlyArray<string>;
59
+ }
60
+ /** The document includes resolved values; source relocation alone is not a content edit. */
61
+ declare function hashDocument(document: Omit<PageDocument, "hash">): string;
62
+ //#endregion
63
+ //#region src/markdown/facts.d.ts
64
+ type Json = null | boolean | number | string | ReadonlyArray<Json> | {
65
+ readonly [key: string]: Json;
66
+ };
67
+ interface FactDefinition {
68
+ readonly kind: "number" | "money" | "list" | "text";
69
+ readonly value: Json;
70
+ readonly text: string;
71
+ readonly items?: ReadonlyArray<string>;
72
+ }
73
+ type Facts = Readonly<Record<string, FactDefinition>>;
74
+ /** Register code-owned values for ordinary React rendering as well as capture. */
75
+ declare function defineFacts<const F extends Facts>(facts: F): F;
76
+ declare const fact: {
77
+ number(value: number, options?: Intl.NumberFormatOptions): FactDefinition;
78
+ money(value: number, options?: {
79
+ currency?: string;
80
+ per?: string;
81
+ }): FactDefinition;
82
+ list<T extends Json>(values: ReadonlyArray<T>, options?: {
83
+ format: (value: T) => string;
84
+ }): FactDefinition;
85
+ text(value: string): FactDefinition;
86
+ };
87
+ declare function resolveFact(id: string, facts?: Facts): FactDefinition;
88
+ //#endregion
89
+ //#region src/core/page-heads.d.ts
90
+ /** Plain document metadata, without framework or schema dependencies. */
91
+ interface PageHead {
92
+ readonly path: string;
93
+ readonly title: string;
94
+ readonly description: string;
95
+ }
96
+ interface PageHeadsOptions {
97
+ /** Path globs: `*` matches a segment, `**` matches across segments. */
98
+ readonly exclude?: ReadonlyArray<string>;
99
+ /** Exclude noindex pages and redirects. Parameter templates are always excluded. Defaults to true. */
100
+ readonly indexable?: boolean;
101
+ }
102
+ /** Select page nodes before the host resolves metadata; sitemap-disabled hubs remain pages. */
103
+ declare function selectPageHeadNodes(graph: SeoGraph, options?: PageHeadsOptions): Array<SeoNode>;
104
+ /** Extract resolved heads, naming the selected page when metadata is missing or blank. */
105
+ declare function pageHeads(graph: SeoGraph, options?: PageHeadsOptions): Array<PageHead>;
106
+ //#endregion
107
+ 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
@@ -0,0 +1,25 @@
1
+ import { a as globToRegExp } from "./checks-BS-aDXB_.js";
2
+ //#region src/core/page-heads.ts
3
+ /** Select page nodes before the host resolves metadata; sitemap-disabled hubs remain pages. */
4
+ function selectPageHeadNodes(graph, options = {}) {
5
+ const excludes = (options.exclude ?? []).map(globToRegExp);
6
+ return [...graph.nodes.values()].filter((node) => node.kind !== "layout" && !node.path.includes("$") && (node.policy.sitemap !== void 0 || node.instance !== void 0) && !excludes.some((pattern) => pattern.test(node.path)) && (options.indexable === false || node.policy.redirectTo === void 0 && !node.policy.robots?.toLowerCase().split(/[\s,]+/).includes("noindex")));
7
+ }
8
+ /** Extract resolved heads, naming the selected page when metadata is missing or blank. */
9
+ function pageHeads(graph, options = {}) {
10
+ return selectPageHeadNodes(graph, options).map((node) => {
11
+ const title = node.head?.title;
12
+ const description = node.head?.description;
13
+ if (!title?.trim()) throw new Error(`Invalid page head for ${node.path}: missing title`);
14
+ if (!description?.trim()) throw new Error(`Invalid page head for ${node.path}: missing description`);
15
+ return {
16
+ path: node.path,
17
+ title,
18
+ description
19
+ };
20
+ });
21
+ }
22
+ //#endregion
23
+ export { selectPageHeadNodes as n, pageHeads as t };
24
+
25
+ //# sourceMappingURL=page-heads-BgbaIjj9.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-heads-BgbaIjj9.js","names":[],"sources":["../src/core/page-heads.ts"],"sourcesContent":["import { 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}\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 };\n });\n}\n"],"mappings":";;;AAkBA,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;EAAY;CAC/C,CAAC;AACH"}
@@ -0,0 +1,78 @@
1
+ import { a as SeoGraph, r as SeoEdge, s as SeoNode } from "./graph-D1PIndsC.js";
2
+ //#region src/core/projections.d.ts
3
+ interface ProjectionConfig {
4
+ origin: string;
5
+ indexable: boolean;
6
+ }
7
+ interface RobotsConfig extends ProjectionConfig {
8
+ /** Path prefixes to disallow on an indexable host (e.g. the app-only groups). */
9
+ disallow: ReadonlyArray<string>;
10
+ /**
11
+ * Origin-wide Content-Signal preferences
12
+ * (https://contentsignals.org/), emitted as `Content-Signal: <value>` under
13
+ * `User-agent: *` on an indexable host. Omit for none. The plugin never
14
+ * invents a default — pass the policy you want, e.g.
15
+ * `"search=yes, ai-input=yes, ai-train=yes"`.
16
+ */
17
+ contentSignal?: string | undefined;
18
+ /**
19
+ * Extra full lines in the indexable `User-agent: *` group, after
20
+ * Content-Signal (if any) and before Allow/Disallow. Use for directives the
21
+ * plugin does not model, or to compose {@link contentSignal} yourself.
22
+ */
23
+ directives?: ReadonlyArray<string> | undefined;
24
+ /**
25
+ * Last-mile override: receives the rendered robots.txt and returns the file
26
+ * to emit. Runs for indexable and preview hosts. Use when you need to wrap
27
+ * or replace the default body rather than add group lines.
28
+ */
29
+ transform?: ((robots: string) => string) | undefined;
30
+ }
31
+ interface NodeReport {
32
+ node: SeoNode;
33
+ inSitemap: boolean;
34
+ incoming: Array<SeoEdge>;
35
+ outgoing: Array<SeoEdge>;
36
+ }
37
+ /**
38
+ * A node belongs in the sitemap when it declares a positive sitemap policy, is not
39
+ * a redirect, is not robots-noindexed, and is not a param template (a route whose
40
+ * path still contains a `$` segment — those exist only so their instances inherit).
41
+ */
42
+ declare function isSitemapEligible(node: SeoNode): boolean;
43
+ /**
44
+ * The date a node's content last changed, as declared: an instance's
45
+ * `modifiedAt`, else its `publishedAt`; a route's own `modifiedAt`. A node with
46
+ * no date (a docs page without frontmatter dates, an undated route) returns
47
+ * undefined, and the sitemap omits its `<lastmod>`.
48
+ */
49
+ declare function lastModified(node: SeoNode): string | undefined;
50
+ /**
51
+ * Render sitemap.xml from the graph. Each entry's `<lastmod>` is its
52
+ * {@link lastModified} date: frontmatter for content instances, the declared
53
+ * `modifiedAt` for routes. Route entries are sorted by path, then instances follow in collection order.
54
+ * `indexable` is intentionally unused — the sitemap body is host-independent;
55
+ * robots.txt is what gates crawling.
56
+ */
57
+ declare function renderSitemap(graph: SeoGraph, cfg: ProjectionConfig): string;
58
+ /** Format a Content-Signal robots.txt directive from the preference list. */
59
+ declare function contentSignal(value: string): string;
60
+ /**
61
+ * Render robots.txt. A non-indexable host (previews) gets a disallow-all
62
+ * with no Sitemap line; an indexable host disallows exactly the prefixes the caller
63
+ * passes. Pages that declare `robots: noindex` are intentionally NOT added as
64
+ * Disallow entries — a Disallow would stop crawlers reaching the page to read its
65
+ * `noindex, follow` meta, so the graph's per-node robots policy never feeds this
66
+ * list. `graph` is unused — kept for signature parity with the other projections,
67
+ * which callers load the graph once for and pass to each.
68
+ *
69
+ * Origin-wide group directives (`contentSignal`, `directives`) are indexable-host
70
+ * only. {@link RobotsConfig.transform} always runs last so a consumer can override
71
+ * the whole file.
72
+ */
73
+ declare function renderRobots(_graph: SeoGraph, cfg: RobotsConfig): string;
74
+ /** Inspect a single node: its declaration, sitemap eligibility, and edges. */
75
+ declare function inspectNode(graph: SeoGraph, path: string): NodeReport | undefined;
76
+ //#endregion
77
+ export { inspectNode as a, renderRobots as c, contentSignal as i, renderSitemap as l, ProjectionConfig as n, isSitemapEligible as o, RobotsConfig as r, lastModified as s, NodeReport as t };
78
+ //# sourceMappingURL=projections-Bk-qOOsR.d.ts.map
@@ -0,0 +1,142 @@
1
+ //#region src/core/projections.ts
2
+ const escapeXml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
3
+ /**
4
+ * A node belongs in the sitemap when it declares a positive sitemap policy, is not
5
+ * a redirect, is not robots-noindexed, and is not a param template (a route whose
6
+ * path still contains a `$` segment — those exist only so their instances inherit).
7
+ */
8
+ function isSitemapEligible(node) {
9
+ if (node.policy.redirectTo !== void 0) return false;
10
+ if (node.policy.robots?.includes("noindex")) return false;
11
+ if (!node.policy.sitemap) return false;
12
+ if (node.path.includes("$")) return false;
13
+ return true;
14
+ }
15
+ /** Canonical absolute URL for a node under the given origin. */
16
+ function urlForNode(origin, node) {
17
+ return node.path === "/" ? origin : `${origin}${node.path}`;
18
+ }
19
+ /**
20
+ * The date a node's content last changed, as declared: an instance's
21
+ * `modifiedAt`, else its `publishedAt`; a route's own `modifiedAt`. A node with
22
+ * no date (a docs page without frontmatter dates, an undated route) returns
23
+ * undefined, and the sitemap omits its `<lastmod>`.
24
+ */
25
+ function lastModified(node) {
26
+ if (node.instance) return node.instance.modifiedAt ?? node.instance.publishedAt;
27
+ return node.policy.modifiedAt;
28
+ }
29
+ /** An ISO 8601 date, or a date-time that states its offset. */
30
+ const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2}))?$/;
31
+ /**
32
+ * Parse a declared date to epoch milliseconds, or undefined when it is not an
33
+ * ISO 8601 date (`2026-09-29`) or offset date-time (`2026-09-29T10:00:00Z`) on a
34
+ * real calendar day. `Date.parse` alone would accept "Sep 29" in the host's
35
+ * zone and roll "2026-02-30" into March; either would publish a wrong
36
+ * `<lastmod>`. The `invalid-date` check reports the undefined case, so
37
+ * projections that meet one fail loudly.
38
+ */
39
+ function parseDeclaredDate(value) {
40
+ const match = ISO_DATE.exec(value);
41
+ if (match === null) return void 0;
42
+ const [year, month, day] = [
43
+ Number(match[1]),
44
+ Number(match[2]),
45
+ Number(match[3])
46
+ ];
47
+ const calendar = new Date(Date.UTC(year, month - 1, day));
48
+ if (calendar.getUTCMonth() !== month - 1 || calendar.getUTCDate() !== day) return void 0;
49
+ const time = Date.parse(value);
50
+ return Number.isNaN(time) ? void 0 : time;
51
+ }
52
+ function sitemapLastmod(node) {
53
+ const date = lastModified(node);
54
+ if (date === void 0) return void 0;
55
+ const time = parseDeclaredDate(date);
56
+ if (time === void 0) throw new Error(`"${node.path}" declares an invalid date "${date}"; run \`pagegraph check\`.`);
57
+ return new Date(time).toISOString();
58
+ }
59
+ function renderUrlEntry(url, lastmod, node) {
60
+ const { changeFrequency, priority } = node.policy.sitemap;
61
+ const lines = [` <url>`, ` <loc>${escapeXml(url)}</loc>`];
62
+ if (lastmod) lines.push(` <lastmod>${lastmod}</lastmod>`);
63
+ lines.push(` <changefreq>${changeFrequency}</changefreq>`, ` <priority>${priority.toFixed(1)}</priority>`, ` </url>`);
64
+ return lines.join("\n");
65
+ }
66
+ /**
67
+ * Render sitemap.xml from the graph. Each entry's `<lastmod>` is its
68
+ * {@link lastModified} date: frontmatter for content instances, the declared
69
+ * `modifiedAt` for routes. Route entries are sorted by path, then instances follow in collection order.
70
+ * `indexable` is intentionally unused — the sitemap body is host-independent;
71
+ * robots.txt is what gates crawling.
72
+ */
73
+ function renderSitemap(graph, cfg) {
74
+ const nodes = [...graph.nodes.values()];
75
+ const staticNodes = nodes.filter((node) => node.source === "route" && isSitemapEligible(node)).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
76
+ const instanceNodes = nodes.filter((node) => node.source !== "route" && isSitemapEligible(node));
77
+ const seen = /* @__PURE__ */ new Set();
78
+ const entries = [];
79
+ for (const node of [...staticNodes, ...instanceNodes]) {
80
+ const url = urlForNode(cfg.origin, node);
81
+ const key = url.toLowerCase().replace(/\/$/, "");
82
+ if (seen.has(key)) continue;
83
+ seen.add(key);
84
+ entries.push(renderUrlEntry(url, sitemapLastmod(node), node));
85
+ }
86
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${entries.join("\n")}\n</urlset>\n`;
87
+ }
88
+ /** Format a Content-Signal robots.txt directive from the preference list. */
89
+ function contentSignal(value) {
90
+ return `Content-Signal: ${value}`;
91
+ }
92
+ const groupLines = (cfg) => {
93
+ const lines = [];
94
+ if (cfg.contentSignal !== void 0 && cfg.contentSignal !== "") lines.push(contentSignal(cfg.contentSignal));
95
+ for (const directive of cfg.directives ?? []) if (directive !== "") lines.push(directive);
96
+ return lines;
97
+ };
98
+ /**
99
+ * Render robots.txt. A non-indexable host (previews) gets a disallow-all
100
+ * with no Sitemap line; an indexable host disallows exactly the prefixes the caller
101
+ * passes. Pages that declare `robots: noindex` are intentionally NOT added as
102
+ * Disallow entries — a Disallow would stop crawlers reaching the page to read its
103
+ * `noindex, follow` meta, so the graph's per-node robots policy never feeds this
104
+ * list. `graph` is unused — kept for signature parity with the other projections,
105
+ * which callers load the graph once for and pass to each.
106
+ *
107
+ * Origin-wide group directives (`contentSignal`, `directives`) are indexable-host
108
+ * only. {@link RobotsConfig.transform} always runs last so a consumer can override
109
+ * the whole file.
110
+ */
111
+ function renderRobots(_graph, cfg) {
112
+ const rendered = cfg.indexable ? [
113
+ "User-agent: *",
114
+ ...groupLines(cfg),
115
+ "Allow: /",
116
+ ...cfg.disallow.map((path) => `Disallow: ${path}`),
117
+ "",
118
+ `Sitemap: ${cfg.origin}/sitemap.xml`,
119
+ `Host: ${cfg.origin}`,
120
+ ""
121
+ ].join("\n") : [
122
+ "User-agent: *",
123
+ "Disallow: /",
124
+ ""
125
+ ].join("\n");
126
+ return cfg.transform === void 0 ? rendered : cfg.transform(rendered);
127
+ }
128
+ /** Inspect a single node: its declaration, sitemap eligibility, and edges. */
129
+ function inspectNode(graph, path) {
130
+ const node = graph.nodes.get(path);
131
+ if (!node) return void 0;
132
+ return {
133
+ node,
134
+ inSitemap: isSitemapEligible(node),
135
+ incoming: graph.edges.filter((edge) => edge.to === path),
136
+ outgoing: graph.edges.filter((edge) => edge.from === path)
137
+ };
138
+ }
139
+ //#endregion
140
+ export { parseDeclaredDate as a, lastModified as i, inspectNode as n, renderRobots as o, isSitemapEligible as r, renderSitemap as s, contentSignal as t };
141
+
142
+ //# sourceMappingURL=projections-D0oOFKiY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projections-D0oOFKiY.js","names":[],"sources":["../src/core/projections.ts"],"sourcesContent":["/**\n * Projections of the SEO graph: sitemap.xml, robots.txt, and single-node\n * inspection. Pure functions — the origin, the host's indexability, and the\n * robots disallow list are injected by the caller, never read from the\n * environment or a generated file.\n */\n\nimport type { SeoEdge, SeoGraph, SeoNode } from \"./graph\";\n\nexport interface ProjectionConfig {\n origin: string;\n indexable: boolean;\n}\n\nexport interface RobotsConfig extends ProjectionConfig {\n /** Path prefixes to disallow on an indexable host (e.g. the app-only groups). */\n disallow: ReadonlyArray<string>;\n /**\n * Origin-wide Content-Signal preferences\n * (https://contentsignals.org/), emitted as `Content-Signal: <value>` under\n * `User-agent: *` on an indexable host. Omit for none. The plugin never\n * invents a default — pass the policy you want, e.g.\n * `\"search=yes, ai-input=yes, ai-train=yes\"`.\n */\n contentSignal?: string | undefined;\n /**\n * Extra full lines in the indexable `User-agent: *` group, after\n * Content-Signal (if any) and before Allow/Disallow. Use for directives the\n * plugin does not model, or to compose {@link contentSignal} yourself.\n */\n directives?: ReadonlyArray<string> | undefined;\n /**\n * Last-mile override: receives the rendered robots.txt and returns the file\n * to emit. Runs for indexable and preview hosts. Use when you need to wrap\n * or replace the default body rather than add group lines.\n */\n transform?: ((robots: string) => string) | undefined;\n}\n\nexport interface NodeReport {\n node: SeoNode;\n inSitemap: boolean;\n incoming: Array<SeoEdge>;\n outgoing: Array<SeoEdge>;\n}\n\nconst escapeXml = (value: string): string =>\n value\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\");\n\n/**\n * A node belongs in the sitemap when it declares a positive sitemap policy, is not\n * a redirect, is not robots-noindexed, and is not a param template (a route whose\n * path still contains a `$` segment — those exist only so their instances inherit).\n */\nexport function isSitemapEligible(node: SeoNode): boolean {\n if (node.policy.redirectTo !== undefined) return false;\n if (node.policy.robots?.includes(\"noindex\")) return false;\n if (!node.policy.sitemap) return false; // false or absent\n if (node.path.includes(\"$\")) return false;\n return true;\n}\n\n/** Canonical absolute URL for a node under the given origin. */\nfunction urlForNode(origin: string, node: SeoNode): string {\n return node.path === \"/\" ? origin : `${origin}${node.path}`;\n}\n\n/**\n * The date a node's content last changed, as declared: an instance's\n * `modifiedAt`, else its `publishedAt`; a route's own `modifiedAt`. A node with\n * no date (a docs page without frontmatter dates, an undated route) returns\n * undefined, and the sitemap omits its `<lastmod>`.\n */\nexport function lastModified(node: SeoNode): string | undefined {\n if (node.instance) return node.instance.modifiedAt ?? node.instance.publishedAt;\n return node.policy.modifiedAt;\n}\n\n/** An ISO 8601 date, or a date-time that states its offset. */\nconst ISO_DATE = /^(\\d{4})-(\\d{2})-(\\d{2})(?:T\\d{2}:\\d{2}(?::\\d{2}(?:\\.\\d+)?)?(?:Z|[+-]\\d{2}:\\d{2}))?$/;\n\n/**\n * Parse a declared date to epoch milliseconds, or undefined when it is not an\n * ISO 8601 date (`2026-09-29`) or offset date-time (`2026-09-29T10:00:00Z`) on a\n * real calendar day. `Date.parse` alone would accept \"Sep 29\" in the host's\n * zone and roll \"2026-02-30\" into March; either would publish a wrong\n * `<lastmod>`. The `invalid-date` check reports the undefined case, so\n * projections that meet one fail loudly.\n */\nexport function parseDeclaredDate(value: string): number | undefined {\n const match = ISO_DATE.exec(value);\n if (match === null) return undefined;\n const [year, month, day] = [Number(match[1]), Number(match[2]), Number(match[3])];\n const calendar = new Date(Date.UTC(year, month - 1, day));\n if (calendar.getUTCMonth() !== month - 1 || calendar.getUTCDate() !== day) return undefined;\n const time = Date.parse(value);\n return Number.isNaN(time) ? undefined : time;\n}\n\nfunction sitemapLastmod(node: SeoNode): string | undefined {\n const date = lastModified(node);\n if (date === undefined) return undefined;\n const time = parseDeclaredDate(date);\n if (time === undefined) {\n throw new Error(`\"${node.path}\" declares an invalid date \"${date}\"; run \\`pagegraph check\\`.`);\n }\n return new Date(time).toISOString();\n}\n\nfunction renderUrlEntry(url: string, lastmod: string | undefined, node: SeoNode): string {\n const sitemap = node.policy.sitemap;\n // isSitemapEligible guarantees a positive policy before this runs.\n const { changeFrequency, priority } = sitemap as { changeFrequency: string; priority: number };\n const lines = [` <url>`, ` <loc>${escapeXml(url)}</loc>`];\n if (lastmod) lines.push(` <lastmod>${lastmod}</lastmod>`);\n lines.push(\n ` <changefreq>${changeFrequency}</changefreq>`,\n ` <priority>${priority.toFixed(1)}</priority>`,\n ` </url>`,\n );\n return lines.join(\"\\n\");\n}\n\n/**\n * Render sitemap.xml from the graph. Each entry's `<lastmod>` is its\n * {@link lastModified} date: frontmatter for content instances, the declared\n * `modifiedAt` for routes. Route entries are sorted by path, then instances follow in collection order.\n * `indexable` is intentionally unused — the sitemap body is host-independent;\n * robots.txt is what gates crawling.\n */\nexport function renderSitemap(graph: SeoGraph, cfg: ProjectionConfig): string {\n const nodes = [...graph.nodes.values()];\n const staticNodes = nodes\n .filter((node) => node.source === \"route\" && isSitemapEligible(node))\n .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));\n const instanceNodes = nodes.filter((node) => node.source !== \"route\" && isSitemapEligible(node));\n\n const seen = new Set<string>();\n const entries: Array<string> = [];\n for (const node of [...staticNodes, ...instanceNodes]) {\n const url = urlForNode(cfg.origin, node);\n const key = url.toLowerCase().replace(/\\/$/, \"\");\n if (seen.has(key)) continue;\n seen.add(key);\n entries.push(renderUrlEntry(url, sitemapLastmod(node), node));\n }\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\\n${entries.join(\n \"\\n\",\n )}\\n</urlset>\\n`;\n}\n\n/** Format a Content-Signal robots.txt directive from the preference list. */\nexport function contentSignal(value: string): string {\n return `Content-Signal: ${value}`;\n}\n\nconst groupLines = (cfg: RobotsConfig): Array<string> => {\n const lines: Array<string> = [];\n if (cfg.contentSignal !== undefined && cfg.contentSignal !== \"\") {\n lines.push(contentSignal(cfg.contentSignal));\n }\n for (const directive of cfg.directives ?? []) {\n if (directive !== \"\") lines.push(directive);\n }\n return lines;\n};\n\n/**\n * Render robots.txt. A non-indexable host (previews) gets a disallow-all\n * with no Sitemap line; an indexable host disallows exactly the prefixes the caller\n * passes. Pages that declare `robots: noindex` are intentionally NOT added as\n * Disallow entries — a Disallow would stop crawlers reaching the page to read its\n * `noindex, follow` meta, so the graph's per-node robots policy never feeds this\n * list. `graph` is unused — kept for signature parity with the other projections,\n * which callers load the graph once for and pass to each.\n *\n * Origin-wide group directives (`contentSignal`, `directives`) are indexable-host\n * only. {@link RobotsConfig.transform} always runs last so a consumer can override\n * the whole file.\n */\nexport function renderRobots(_graph: SeoGraph, cfg: RobotsConfig): string {\n const rendered = cfg.indexable\n ? [\n \"User-agent: *\",\n ...groupLines(cfg),\n \"Allow: /\",\n ...cfg.disallow.map((path) => `Disallow: ${path}`),\n \"\",\n `Sitemap: ${cfg.origin}/sitemap.xml`,\n `Host: ${cfg.origin}`,\n \"\",\n ].join(\"\\n\")\n : [\"User-agent: *\", \"Disallow: /\", \"\"].join(\"\\n\");\n return cfg.transform === undefined ? rendered : cfg.transform(rendered);\n}\n\n/** Inspect a single node: its declaration, sitemap eligibility, and edges. */\nexport function inspectNode(graph: SeoGraph, path: string): NodeReport | undefined {\n const node = graph.nodes.get(path);\n if (!node) return undefined;\n return {\n node,\n inSitemap: isSitemapEligible(node),\n incoming: graph.edges.filter((edge) => edge.to === path),\n outgoing: graph.edges.filter((edge) => edge.from === path),\n };\n}\n"],"mappings":"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"}