@puzzle-ui/seo 0.0.0-stage → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +755 -2
- package/dist/LICENSE +21 -0
- package/dist/context/config.cjs +82 -0
- package/dist/context/config.cjs.map +1 -0
- package/dist/context/config.d.cts +6 -0
- package/dist/context/config.d.ts +6 -0
- package/dist/context/config.js +80 -0
- package/dist/context/config.js.map +1 -0
- package/dist/context/index.cjs +6 -0
- package/dist/context/index.d.cts +4 -0
- package/dist/context/index.d.ts +4 -0
- package/dist/context/index.js +3 -0
- package/dist/context/types.d.cts +46 -0
- package/dist/context/types.d.ts +46 -0
- package/dist/core/graph.cjs +464 -0
- package/dist/core/graph.cjs.map +1 -0
- package/dist/core/graph.d.cts +62 -0
- package/dist/core/graph.d.ts +62 -0
- package/dist/core/graph.js +456 -0
- package/dist/core/graph.js.map +1 -0
- package/dist/core/index.cjs +11 -0
- package/dist/core/index.d.cts +3 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +1 -0
- package/dist/core/types.d.cts +92 -0
- package/dist/core/types.d.ts +92 -0
- package/dist/index.cjs +96 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +48 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/metadata/index.cjs +4 -0
- package/dist/metadata/index.d.cts +3 -0
- package/dist/metadata/index.d.ts +3 -0
- package/dist/metadata/index.js +1 -0
- package/dist/metadata/resolve.cjs +122 -0
- package/dist/metadata/resolve.cjs.map +1 -0
- package/dist/metadata/resolve.d.cts +4 -0
- package/dist/metadata/resolve.d.ts +4 -0
- package/dist/metadata/resolve.js +121 -0
- package/dist/metadata/resolve.js.map +1 -0
- package/dist/metadata/types.d.cts +82 -0
- package/dist/metadata/types.d.ts +82 -0
- package/dist/next/index.cjs +13 -0
- package/dist/next/index.cjs.map +1 -0
- package/dist/next/index.d.cts +15 -0
- package/dist/next/index.d.ts +14 -0
- package/dist/next/index.js +12 -0
- package/dist/next/index.js.map +1 -0
- package/dist/react/context.cjs +30 -0
- package/dist/react/context.cjs.map +1 -0
- package/dist/react/context.d.cts +14 -0
- package/dist/react/context.d.ts +14 -0
- package/dist/react/context.js +27 -0
- package/dist/react/context.js.map +1 -0
- package/dist/react/index.cjs +48 -0
- package/dist/react/index.d.cts +6 -0
- package/dist/react/index.d.ts +6 -0
- package/dist/react/index.js +5 -0
- package/dist/react/metadata.cjs +100 -0
- package/dist/react/metadata.cjs.map +1 -0
- package/dist/react/metadata.d.cts +18 -0
- package/dist/react/metadata.d.ts +18 -0
- package/dist/react/metadata.js +99 -0
- package/dist/react/metadata.js.map +1 -0
- package/dist/react/provider.cjs +42 -0
- package/dist/react/provider.cjs.map +1 -0
- package/dist/react/provider.d.cts +3 -0
- package/dist/react/provider.d.ts +3 -0
- package/dist/react/provider.js +42 -0
- package/dist/react/provider.js.map +1 -0
- package/dist/react/schema-components.cjs +294 -0
- package/dist/react/schema-components.cjs.map +1 -0
- package/dist/react/schema-components.d.cts +84 -0
- package/dist/react/schema-components.d.ts +80 -0
- package/dist/react/schema-components.js +254 -0
- package/dist/react/schema-components.js.map +1 -0
- package/dist/react/types.d.cts +81 -0
- package/dist/react/types.d.ts +80 -0
- package/dist/react-router/index.cjs +13 -0
- package/dist/react-router/index.cjs.map +1 -0
- package/dist/react-router/index.d.cts +21 -0
- package/dist/react-router/index.d.ts +20 -0
- package/dist/react-router/index.js +12 -0
- package/dist/react-router/index.js.map +1 -0
- package/dist/robots/generate.cjs +88 -0
- package/dist/robots/generate.cjs.map +1 -0
- package/dist/robots/generate.d.cts +4 -0
- package/dist/robots/generate.d.ts +4 -0
- package/dist/robots/generate.js +86 -0
- package/dist/robots/generate.js.map +1 -0
- package/dist/robots/index.cjs +5 -0
- package/dist/robots/index.d.cts +3 -0
- package/dist/robots/index.d.ts +3 -0
- package/dist/robots/index.js +1 -0
- package/dist/robots/types.d.cts +27 -0
- package/dist/robots/types.d.ts +27 -0
- package/dist/schema/create-registry.cjs +12 -0
- package/dist/schema/create-registry.cjs.map +1 -0
- package/dist/schema/create-registry.d.cts +2 -0
- package/dist/schema/create-registry.d.ts +2 -0
- package/dist/schema/create-registry.js +12 -0
- package/dist/schema/create-registry.js.map +1 -0
- package/dist/schema/generated/index.d.cts +2 -0
- package/dist/schema/generated/schemaorg.cjs +36386 -0
- package/dist/schema/generated/schemaorg.cjs.map +1 -0
- package/dist/schema/generated/schemaorg.d.cts +945 -0
- package/dist/schema/generated/schemaorg.d.ts +945 -0
- package/dist/schema/generated/schemaorg.js +36384 -0
- package/dist/schema/generated/schemaorg.js.map +1 -0
- package/dist/schema/index.cjs +11 -0
- package/dist/schema/index.d.cts +7 -0
- package/dist/schema/index.d.ts +6 -0
- package/dist/schema/index.js +5 -0
- package/dist/schema/registry.cjs +194 -0
- package/dist/schema/registry.cjs.map +1 -0
- package/dist/schema/registry.d.cts +2 -0
- package/dist/schema/registry.d.ts +2 -0
- package/dist/schema/registry.js +194 -0
- package/dist/schema/registry.js.map +1 -0
- package/dist/schema/types.d.cts +79 -0
- package/dist/schema/types.d.ts +79 -0
- package/dist/schema/validate.cjs +131 -0
- package/dist/schema/validate.cjs.map +1 -0
- package/dist/schema/validate.d.cts +16 -0
- package/dist/schema/validate.d.ts +16 -0
- package/dist/schema/validate.js +131 -0
- package/dist/schema/validate.js.map +1 -0
- package/dist/server/handlers.cjs +96 -0
- package/dist/server/handlers.cjs.map +1 -0
- package/dist/server/handlers.d.cts +3 -0
- package/dist/server/handlers.d.ts +3 -0
- package/dist/server/handlers.js +95 -0
- package/dist/server/handlers.js.map +1 -0
- package/dist/server/index.cjs +6 -0
- package/dist/server/index.d.cts +4 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +3 -0
- package/dist/server/routes.cjs +17 -0
- package/dist/server/routes.cjs.map +1 -0
- package/dist/server/routes.d.cts +2 -0
- package/dist/server/routes.d.ts +2 -0
- package/dist/server/routes.js +17 -0
- package/dist/server/routes.js.map +1 -0
- package/dist/server/types.d.cts +61 -0
- package/dist/server/types.d.ts +61 -0
- package/dist/sitemap/generate.cjs +147 -0
- package/dist/sitemap/generate.cjs.map +1 -0
- package/dist/sitemap/generate.d.cts +3 -0
- package/dist/sitemap/generate.d.ts +3 -0
- package/dist/sitemap/generate.js +146 -0
- package/dist/sitemap/generate.js.map +1 -0
- package/dist/sitemap/index.cjs +11 -0
- package/dist/sitemap/index.d.cts +4 -0
- package/dist/sitemap/index.d.ts +4 -0
- package/dist/sitemap/index.js +3 -0
- package/dist/sitemap/types.d.cts +82 -0
- package/dist/sitemap/types.d.ts +82 -0
- package/dist/sitemap/xml.cjs +215 -0
- package/dist/sitemap/xml.cjs.map +1 -0
- package/dist/sitemap/xml.d.cts +16 -0
- package/dist/sitemap/xml.d.ts +16 -0
- package/dist/sitemap/xml.js +210 -0
- package/dist/sitemap/xml.js.map +1 -0
- package/dist/tanstack/index.cjs +13 -0
- package/dist/tanstack/index.cjs.map +1 -0
- package/dist/tanstack/index.d.cts +19 -0
- package/dist/tanstack/index.d.ts +18 -0
- package/dist/tanstack/index.js +12 -0
- package/dist/tanstack/index.js.map +1 -0
- package/dist/testing/diagnostics.cjs +27 -0
- package/dist/testing/diagnostics.cjs.map +1 -0
- package/dist/testing/diagnostics.d.cts +3 -0
- package/dist/testing/diagnostics.d.ts +3 -0
- package/dist/testing/diagnostics.js +27 -0
- package/dist/testing/diagnostics.js.map +1 -0
- package/dist/testing/fixture.cjs +80 -0
- package/dist/testing/fixture.cjs.map +1 -0
- package/dist/testing/fixture.d.cts +4 -0
- package/dist/testing/fixture.d.ts +4 -0
- package/dist/testing/fixture.js +78 -0
- package/dist/testing/fixture.js.map +1 -0
- package/dist/testing/index.cjs +9 -0
- package/dist/testing/index.d.cts +5 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.js +4 -0
- package/dist/testing/inspect.cjs +63 -0
- package/dist/testing/inspect.cjs.map +1 -0
- package/dist/testing/inspect.d.cts +5 -0
- package/dist/testing/inspect.d.ts +5 -0
- package/dist/testing/inspect.js +63 -0
- package/dist/testing/inspect.js.map +1 -0
- package/dist/testing/types.d.cts +40 -0
- package/dist/testing/types.d.ts +40 -0
- package/dist/urls/index.cjs +8 -0
- package/dist/urls/index.d.cts +3 -0
- package/dist/urls/index.d.ts +3 -0
- package/dist/urls/index.js +1 -0
- package/dist/urls/normalize.cjs +65 -0
- package/dist/urls/normalize.cjs.map +1 -0
- package/dist/urls/normalize.d.cts +14 -0
- package/dist/urls/normalize.d.ts +14 -0
- package/dist/urls/normalize.js +60 -0
- package/dist/urls/normalize.js.map +1 -0
- package/dist/urls/types.d.cts +22 -0
- package/dist/urls/types.d.ts +22 -0
- package/dist/validation/index.cjs +5 -0
- package/dist/validation/index.d.cts +3 -0
- package/dist/validation/index.d.ts +3 -0
- package/dist/validation/index.js +1 -0
- package/dist/validation/types.d.cts +75 -0
- package/dist/validation/types.d.ts +75 -0
- package/dist/validation/validate.cjs +452 -0
- package/dist/validation/validate.cjs.map +1 -0
- package/dist/validation/validate.d.cts +7 -0
- package/dist/validation/validate.d.ts +6 -0
- package/dist/validation/validate.js +450 -0
- package/dist/validation/validate.js.map +1 -0
- package/package.json +203 -4
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { createSeoNode, validateSeoGraph } from "../core/graph.js";
|
|
2
|
+
import { createDefaultSchemaRegistry } from "../schema/create-registry.js";
|
|
3
|
+
import { validateSchemaNode } from "../schema/validate.js";
|
|
4
|
+
// src/validation/validate.ts
|
|
5
|
+
const defaultRegistry = createDefaultSchemaRegistry();
|
|
6
|
+
const SCHEMA_DOCS = "https://schema.org/";
|
|
7
|
+
const GOOGLE_DOCS = "https://developers.google.com/search/docs/appearance/structured-data";
|
|
8
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
|
+
const getTypes = (node) => {
|
|
10
|
+
const value = node["@type"];
|
|
11
|
+
return (Array.isArray(value) ? value : [value]).filter((type) => typeof type === "string" && type.trim().length > 0);
|
|
12
|
+
};
|
|
13
|
+
const hasValue = (value) => {
|
|
14
|
+
if (value === void 0 || value === null) return false;
|
|
15
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
16
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
17
|
+
return true;
|
|
18
|
+
};
|
|
19
|
+
const getNodes = (graph) => graph?.["@graph"] ?? [];
|
|
20
|
+
const createIssue = (source, code, message, severity = "warning", details = {}) => ({
|
|
21
|
+
source,
|
|
22
|
+
code,
|
|
23
|
+
message,
|
|
24
|
+
severity,
|
|
25
|
+
...details
|
|
26
|
+
});
|
|
27
|
+
const normalizeMode = (mode) => mode;
|
|
28
|
+
const normalizeSeverity = (severity, mode) => {
|
|
29
|
+
if (mode === "strict" && severity === "warning") return "error";
|
|
30
|
+
return severity;
|
|
31
|
+
};
|
|
32
|
+
const createSummary = (issues) => {
|
|
33
|
+
const summary = {
|
|
34
|
+
errors: 0,
|
|
35
|
+
warnings: 0,
|
|
36
|
+
infos: 0,
|
|
37
|
+
total: issues.length
|
|
38
|
+
};
|
|
39
|
+
for (const issue of issues) switch (issue.severity) {
|
|
40
|
+
case "error":
|
|
41
|
+
summary.errors += 1;
|
|
42
|
+
break;
|
|
43
|
+
case "warning":
|
|
44
|
+
summary.warnings += 1;
|
|
45
|
+
break;
|
|
46
|
+
case "info": summary.infos += 1;
|
|
47
|
+
}
|
|
48
|
+
return summary;
|
|
49
|
+
};
|
|
50
|
+
const createReport = (mode, issues) => {
|
|
51
|
+
const normalizedIssues = issues.map((issue) => ({
|
|
52
|
+
...issue,
|
|
53
|
+
severity: normalizeSeverity(issue.severity, mode)
|
|
54
|
+
}));
|
|
55
|
+
const summary = createSummary(normalizedIssues);
|
|
56
|
+
return {
|
|
57
|
+
valid: summary.errors === 0,
|
|
58
|
+
mode,
|
|
59
|
+
issues: normalizedIssues,
|
|
60
|
+
summary
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
const validateGraph = (graph, options, registry) => {
|
|
64
|
+
const issues = [];
|
|
65
|
+
if (!Array.isArray(graph["@graph"])) return [createIssue("graph", "invalid-graph", "SEO graph must contain an @graph array.", "error")];
|
|
66
|
+
const result = validateSeoGraph(graph, {
|
|
67
|
+
baseUrl: options.baseUrl,
|
|
68
|
+
strictReferences: options.mode === "strict",
|
|
69
|
+
allowedExternalReferences: options.allowExternalReferences ?? true
|
|
70
|
+
});
|
|
71
|
+
for (const issue of result.issues) issues.push(createIssue("graph", issue.code, issue.message, issue.severity, {
|
|
72
|
+
path: issue.path,
|
|
73
|
+
nodeId: issue.nodeId,
|
|
74
|
+
property: issue.property,
|
|
75
|
+
suggestion: issue.code === "unresolved-reference" ? "Register the referenced node or verify its @id." : void 0
|
|
76
|
+
}));
|
|
77
|
+
for (let index = 0; index < graph["@graph"].length; index += 1) {
|
|
78
|
+
const node = graph["@graph"][index];
|
|
79
|
+
if (!isObject(node)) continue;
|
|
80
|
+
const nodeId = typeof node["@id"] === "string" ? node["@id"] : void 0;
|
|
81
|
+
const result = validateSchemaNode(node, registry, {
|
|
82
|
+
strict: options.mode === "strict",
|
|
83
|
+
allowUnknownProperties: options.allowUnknownProperties ?? options.mode !== "strict",
|
|
84
|
+
validateRequiredProperties: options.validateRequiredProperties ?? false
|
|
85
|
+
});
|
|
86
|
+
for (const issue of result.issues) issues.push(createIssue("schema", issue.code, issue.message, issue.severity, {
|
|
87
|
+
path: issue.path ? `@graph[${index}]${issue.path.replace("$", "")}` : `@graph[${index}]`,
|
|
88
|
+
nodeId,
|
|
89
|
+
property: issue.property,
|
|
90
|
+
suggestion: issue.code === "unknown-property" ? "Check the Schema.org property name or extend the registry." : issue.code === "unknown-type" ? "Use a registered Schema.org type or add a custom definition." : void 0,
|
|
91
|
+
docsUrl: SCHEMA_DOCS
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
return issues;
|
|
95
|
+
};
|
|
96
|
+
const validateDate = (value) => {
|
|
97
|
+
if (typeof value !== "string") return false;
|
|
98
|
+
return value.trim().length > 0 && !Number.isNaN(Date.parse(value));
|
|
99
|
+
};
|
|
100
|
+
const validateSemanticNode = (node, index) => {
|
|
101
|
+
const issues = [];
|
|
102
|
+
const types = getTypes(node);
|
|
103
|
+
const nodeId = typeof node["@id"] === "string" ? node["@id"] : void 0;
|
|
104
|
+
const path = `@graph[${index}]`;
|
|
105
|
+
const add = (code, message, property, suggestion) => {
|
|
106
|
+
issues.push(createIssue("semantic", code, message, "warning", {
|
|
107
|
+
path: `${path}.${property}`,
|
|
108
|
+
nodeId,
|
|
109
|
+
property,
|
|
110
|
+
suggestion,
|
|
111
|
+
docsUrl: SCHEMA_DOCS
|
|
112
|
+
}));
|
|
113
|
+
};
|
|
114
|
+
if (types.some((type) => [
|
|
115
|
+
"Article",
|
|
116
|
+
"NewsArticle",
|
|
117
|
+
"BlogPosting",
|
|
118
|
+
"TechArticle"
|
|
119
|
+
].includes(type.split("/").pop() ?? type))) {
|
|
120
|
+
if (!hasValue(node.headline)) add("article-missing-headline", "Article nodes should provide a headline.", "headline", "Add a descriptive headline.");
|
|
121
|
+
if (hasValue(node.datePublished) && !validateDate(node.datePublished)) add("invalid-publication-date", "Article datePublished is not a valid date.", "datePublished", "Use a valid ISO 8601 date or datetime.");
|
|
122
|
+
if (hasValue(node.dateModified) && !validateDate(node.dateModified)) add("invalid-modification-date", "Article dateModified is not a valid date.", "dateModified", "Use a valid ISO 8601 date or datetime.");
|
|
123
|
+
if (hasValue(node.datePublished) && hasValue(node.dateModified) && validateDate(node.datePublished) && validateDate(node.dateModified) && Date.parse(String(node.dateModified)) < Date.parse(String(node.datePublished))) add("modification-before-publication", "Article dateModified is earlier than datePublished.", "dateModified", "Verify the publication and modification dates.");
|
|
124
|
+
}
|
|
125
|
+
if (types.includes("Product") || types.includes("https://schema.org/Product")) {
|
|
126
|
+
if (!hasValue(node.name)) add("product-missing-name", "Product nodes should provide a name.", "name", "Add the product's visible name.");
|
|
127
|
+
if (hasValue(node.offers) && !Array.isArray(node.offers) && !isObject(node.offers)) add("invalid-product-offers", "Product offers must be an Offer node or an array of Offer nodes.", "offers", "Provide an Offer object or an array of Offer objects.");
|
|
128
|
+
}
|
|
129
|
+
if (types.includes("Offer") || types.includes("https://schema.org/Offer")) {
|
|
130
|
+
if (hasValue(node.price)) {
|
|
131
|
+
const price = Number(node.price);
|
|
132
|
+
if (!Number.isFinite(price) || price < 0) add("invalid-offer-price", "Offer price must be a non-negative finite number.", "price", "Provide a valid numeric price.");
|
|
133
|
+
}
|
|
134
|
+
if (hasValue(node.price) && !hasValue(node.priceCurrency)) add("offer-missing-currency", "Offers with a price should specify priceCurrency.", "priceCurrency", "Provide an ISO 4217 currency code, such as EUR or USD.");
|
|
135
|
+
if (hasValue(node.priceCurrency) && !/^[A-Z]{3}$/.test(String(node.priceCurrency))) add("invalid-price-currency", "Offer priceCurrency should use an ISO 4217 currency code.", "priceCurrency", "Use a three-letter uppercase currency code.");
|
|
136
|
+
}
|
|
137
|
+
if (types.includes("BreadcrumbList") || types.includes("https://schema.org/BreadcrumbList")) {
|
|
138
|
+
const elements = node.itemListElement;
|
|
139
|
+
if (!Array.isArray(elements) || elements.length === 0) add("breadcrumb-missing-items", "BreadcrumbList should contain itemListElement entries.", "itemListElement", "Add at least one ListItem.");
|
|
140
|
+
else {
|
|
141
|
+
const positions = /* @__PURE__ */ new Set();
|
|
142
|
+
elements.forEach((item, itemIndex) => {
|
|
143
|
+
if (!isObject(item)) {
|
|
144
|
+
add("invalid-breadcrumb-item", "Breadcrumb entries should be ListItem objects.", `itemListElement[${itemIndex}]`, "Provide a ListItem with position, name and item.");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const position = Number(item.position);
|
|
148
|
+
if (!Number.isInteger(position) || position < 1 || positions.has(position)) add("invalid-breadcrumb-position", "Breadcrumb positions must be unique positive integers.", `itemListElement[${itemIndex}].position`, "Use sequential positions starting at 1.");
|
|
149
|
+
else positions.add(position);
|
|
150
|
+
if (!hasValue(item.name)) add("breadcrumb-missing-name", "Breadcrumb ListItem should provide a name.", `itemListElement[${itemIndex}].name`, "Provide the visible breadcrumb label.");
|
|
151
|
+
if (!hasValue(item.item) && itemIndex !== elements.length - 1) add("breadcrumb-missing-item", "Breadcrumb ListItem should provide an item URL.", `itemListElement[${itemIndex}].item`, "Provide the destination URL.");
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return issues;
|
|
156
|
+
};
|
|
157
|
+
const richResultRules = {
|
|
158
|
+
article: {
|
|
159
|
+
type: "Article",
|
|
160
|
+
required: [],
|
|
161
|
+
recommended: [
|
|
162
|
+
"headline",
|
|
163
|
+
"image",
|
|
164
|
+
"datePublished",
|
|
165
|
+
"dateModified",
|
|
166
|
+
"author"
|
|
167
|
+
],
|
|
168
|
+
docsUrl: `${GOOGLE_DOCS}/article`
|
|
169
|
+
},
|
|
170
|
+
product: {
|
|
171
|
+
type: "Product",
|
|
172
|
+
required: ["name"],
|
|
173
|
+
requireAny: [
|
|
174
|
+
"offers",
|
|
175
|
+
"aggregateRating",
|
|
176
|
+
"review"
|
|
177
|
+
],
|
|
178
|
+
recommended: ["image", "description"],
|
|
179
|
+
docsUrl: `${GOOGLE_DOCS}/product`
|
|
180
|
+
},
|
|
181
|
+
breadcrumb: {
|
|
182
|
+
type: "BreadcrumbList",
|
|
183
|
+
required: ["itemListElement"],
|
|
184
|
+
docsUrl: `${GOOGLE_DOCS}/breadcrumb`
|
|
185
|
+
},
|
|
186
|
+
event: {
|
|
187
|
+
type: "Event",
|
|
188
|
+
required: [
|
|
189
|
+
"name",
|
|
190
|
+
"startDate",
|
|
191
|
+
"location"
|
|
192
|
+
],
|
|
193
|
+
docsUrl: `${GOOGLE_DOCS}/event`
|
|
194
|
+
},
|
|
195
|
+
recipe: {
|
|
196
|
+
type: "Recipe",
|
|
197
|
+
required: ["name", "image"],
|
|
198
|
+
recommended: [
|
|
199
|
+
"author",
|
|
200
|
+
"description",
|
|
201
|
+
"prepTime",
|
|
202
|
+
"cookTime",
|
|
203
|
+
"recipeYield",
|
|
204
|
+
"nutrition"
|
|
205
|
+
],
|
|
206
|
+
docsUrl: `${GOOGLE_DOCS}/recipe`
|
|
207
|
+
},
|
|
208
|
+
"job-posting": {
|
|
209
|
+
type: "JobPosting",
|
|
210
|
+
required: [
|
|
211
|
+
"title",
|
|
212
|
+
"description",
|
|
213
|
+
"datePosted",
|
|
214
|
+
"hiringOrganization",
|
|
215
|
+
"jobLocation"
|
|
216
|
+
],
|
|
217
|
+
recommended: [
|
|
218
|
+
"validThrough",
|
|
219
|
+
"employmentType",
|
|
220
|
+
"baseSalary"
|
|
221
|
+
],
|
|
222
|
+
docsUrl: `${GOOGLE_DOCS}/job-posting`
|
|
223
|
+
},
|
|
224
|
+
"local-business": {
|
|
225
|
+
type: "LocalBusiness",
|
|
226
|
+
required: ["name", "address"],
|
|
227
|
+
recommended: [
|
|
228
|
+
"telephone",
|
|
229
|
+
"url",
|
|
230
|
+
"image",
|
|
231
|
+
"openingHoursSpecification"
|
|
232
|
+
],
|
|
233
|
+
docsUrl: `${GOOGLE_DOCS}/local-business`
|
|
234
|
+
},
|
|
235
|
+
"software-application": {
|
|
236
|
+
type: "SoftwareApplication",
|
|
237
|
+
required: ["name", "offers"],
|
|
238
|
+
requireAny: ["aggregateRating", "review"],
|
|
239
|
+
recommended: ["applicationCategory", "operatingSystem"],
|
|
240
|
+
docsUrl: `${GOOGLE_DOCS}/software-app`
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
const profileMatches = (node, profile, registry) => {
|
|
244
|
+
const expected = richResultRules[profile].type;
|
|
245
|
+
return getTypes(node).some((type) => registry.isTypeOf(type, expected));
|
|
246
|
+
};
|
|
247
|
+
const getNestedObjects = (value) => {
|
|
248
|
+
return (Array.isArray(value) ? value : [value]).filter(isObject);
|
|
249
|
+
};
|
|
250
|
+
const hasNestedValue = (value, property) => getNestedObjects(value).some((item) => hasValue(item[property]));
|
|
251
|
+
const validateRichResults = (nodes, profiles, registry) => {
|
|
252
|
+
const issues = [];
|
|
253
|
+
for (const profile of profiles) {
|
|
254
|
+
const rule = richResultRules[profile];
|
|
255
|
+
nodes.forEach((node, nodeIndex) => {
|
|
256
|
+
if (!profileMatches(node, profile, registry)) return;
|
|
257
|
+
const nodeId = typeof node["@id"] === "string" ? node["@id"] : void 0;
|
|
258
|
+
const path = `@graph[${nodeIndex}]`;
|
|
259
|
+
const add = (code, message, property, severity = "warning", suggestion) => {
|
|
260
|
+
issues.push(createIssue("rich-results", code, message, severity, {
|
|
261
|
+
nodeId,
|
|
262
|
+
property,
|
|
263
|
+
path: property ? `${path}.${property}` : path,
|
|
264
|
+
suggestion,
|
|
265
|
+
docsUrl: rule.docsUrl
|
|
266
|
+
}));
|
|
267
|
+
};
|
|
268
|
+
for (const property of rule.required) {
|
|
269
|
+
if (hasValue(node[property])) continue;
|
|
270
|
+
add(`rich-results-missing-${property}`, `${rule.type} is missing the required Rich Results property "${property}".`, property, "warning", `Add the ${property} property to meet the ${profile} structured data requirements.`);
|
|
271
|
+
}
|
|
272
|
+
for (const property of rule.recommended ?? []) {
|
|
273
|
+
if (hasValue(node[property])) continue;
|
|
274
|
+
add(`rich-results-recommended-${property}`, `${rule.type} should provide the recommended property "${property}".`, property, "info", `Consider adding ${property} to provide more complete structured data.`);
|
|
275
|
+
}
|
|
276
|
+
if (rule.requireAny && !rule.requireAny.some((property) => hasValue(node[property]))) add("rich-results-missing-alternative", `${rule.type} must provide at least one of: ${rule.requireAny.join(", ")}.`, void 0, "warning", `Provide at least one of: ${rule.requireAny.join(", ")}.`);
|
|
277
|
+
if (profile === "product" && hasValue(node.offers)) {
|
|
278
|
+
if (!hasNestedValue(node.offers, "price")) add("rich-results-product-offer-missing-price", "Product offers should include a price.", "offers", "warning", "Add a price to each Offer object.");
|
|
279
|
+
}
|
|
280
|
+
if (profile === "breadcrumb") {
|
|
281
|
+
const elements = node.itemListElement;
|
|
282
|
+
if (Array.isArray(elements) && elements.length < 2) add("rich-results-breadcrumb-minimum-items", "BreadcrumbList must contain at least two ListItem entries.", "itemListElement", "warning", "Provide at least two breadcrumb items.");
|
|
283
|
+
}
|
|
284
|
+
if (profile === "event" && isObject(node.location)) {
|
|
285
|
+
if (!hasValue(node.location.name)) add("rich-results-event-missing-location-name", "Event location should include a name.", "location.name", "warning", "Provide the name of the physical event venue.");
|
|
286
|
+
if (!hasValue(node.location.address)) add("rich-results-event-missing-address", "Event location should include a detailed address.", "location.address", "warning", "Provide the venue's PostalAddress.");
|
|
287
|
+
}
|
|
288
|
+
if (profile === "local-business" && hasValue(node.address)) {
|
|
289
|
+
if (!isObject(node.address)) add("rich-results-business-invalid-address", "LocalBusiness address should be a PostalAddress object.", "address", "warning", "Provide an address object with the relevant PostalAddress fields.");
|
|
290
|
+
}
|
|
291
|
+
if (profile === "software-application" && hasValue(node.offers)) {
|
|
292
|
+
if (!hasNestedValue(node.offers, "price")) add("rich-results-software-missing-price", "SoftwareApplication offers must include a price.", "offers.price", "warning", "Provide an Offer object with a price. Use 0 for a free application.");
|
|
293
|
+
}
|
|
294
|
+
if (profile === "job-posting") {
|
|
295
|
+
if (isObject(node.hiringOrganization) && !hasValue(node.hiringOrganization.name)) add("rich-results-job-missing-employer-name", "JobPosting hiringOrganization should include a name.", "hiringOrganization.name", "warning", "Provide the employer's organization name.");
|
|
296
|
+
const locations = getNestedObjects(node.jobLocation);
|
|
297
|
+
const hasCountry = locations.some((location) => {
|
|
298
|
+
if (!isObject(location.address)) return false;
|
|
299
|
+
return hasValue(location.address.addressCountry);
|
|
300
|
+
});
|
|
301
|
+
if (locations.length > 0 && !hasCountry) add("rich-results-job-missing-country", "JobPosting jobLocation address should include addressCountry.", "jobLocation.address.addressCountry", "warning", "Provide the country code or country name in the job location address.");
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
return issues;
|
|
306
|
+
};
|
|
307
|
+
const isSafeHttpUrl = (value) => {
|
|
308
|
+
try {
|
|
309
|
+
const url = new URL(value);
|
|
310
|
+
return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password;
|
|
311
|
+
} catch {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
const validateMetadata = (metadata) => {
|
|
316
|
+
const issues = [];
|
|
317
|
+
const add = (code, message, property, suggestion, severity = "warning") => {
|
|
318
|
+
issues.push(createIssue("metadata", code, message, severity, {
|
|
319
|
+
path: property,
|
|
320
|
+
property,
|
|
321
|
+
suggestion
|
|
322
|
+
}));
|
|
323
|
+
};
|
|
324
|
+
if (!hasValue(metadata.title)) add("missing-title", "SEO title is missing.", "title", "Provide a descriptive page title.");
|
|
325
|
+
else {
|
|
326
|
+
const title = String(metadata.title);
|
|
327
|
+
if (title.length > 60) add("title-too-long", "SEO title exceeds the recommended 60 characters.", "title", "Consider shortening the title.", "info");
|
|
328
|
+
if (title.length < 15) add("title-too-short", "SEO title is unusually short.", "title", "Consider providing a more descriptive title.", "info");
|
|
329
|
+
}
|
|
330
|
+
if (!hasValue(metadata.description)) add("missing-description", "SEO meta description is missing.", "description", "Provide a concise description of the page.");
|
|
331
|
+
else {
|
|
332
|
+
const description = String(metadata.description);
|
|
333
|
+
if (description.length > 160) add("description-too-long", "SEO description exceeds the recommended 160 characters.", "description", "Consider shortening the description.", "info");
|
|
334
|
+
if (description.length < 50) add("description-too-short", "SEO description is unusually short.", "description", "Consider providing more descriptive content.", "info");
|
|
335
|
+
}
|
|
336
|
+
if (hasValue(metadata.canonical)) {
|
|
337
|
+
if (!isSafeHttpUrl(String(metadata.canonical))) add("invalid-canonical", "Canonical URL must be an absolute HTTP(S) URL without credentials.", "canonical", "Provide a valid canonical URL.", "error");
|
|
338
|
+
}
|
|
339
|
+
if (hasValue(metadata.locale)) try {
|
|
340
|
+
Intl.getCanonicalLocales(String(metadata.locale));
|
|
341
|
+
} catch {
|
|
342
|
+
add("invalid-metadata-locale", "Metadata locale is invalid.", "locale", "Use a valid BCP 47 locale such as en, nl or nl-NL.", "error");
|
|
343
|
+
}
|
|
344
|
+
const openGraph = metadata.openGraph;
|
|
345
|
+
if (openGraph) {
|
|
346
|
+
if (hasValue(openGraph.url) && !isSafeHttpUrl(String(openGraph.url))) add("invalid-og-url", "Open Graph URL must be an absolute HTTP(S) URL.", "openGraph.url", "Provide a valid Open Graph URL.", "error");
|
|
347
|
+
if (!hasValue(openGraph.title)) add("missing-og-title", "Open Graph title is missing.", "openGraph.title", "Provide an Open Graph title.");
|
|
348
|
+
if (!hasValue(openGraph.description)) add("missing-og-description", "Open Graph description is missing.", "openGraph.description", "Provide an Open Graph description.");
|
|
349
|
+
for (const [index, image] of (openGraph.images ?? []).entries()) {
|
|
350
|
+
if (!isSafeHttpUrl(String(image.url))) add("invalid-og-image-url", "Open Graph image URL must be an absolute HTTP(S) URL.", `openGraph.images[${index}].url`, "Provide an absolute image URL.", "error");
|
|
351
|
+
if (!hasValue(image.alt)) add("missing-og-image-alt", "Open Graph image should have alternative text.", `openGraph.images[${index}].alt`, "Provide descriptive image alternative text.", "info");
|
|
352
|
+
if (image.width !== void 0 && (!Number.isInteger(image.width) || image.width <= 0)) add("invalid-og-image-width", "Open Graph image width must be a positive integer.", `openGraph.images[${index}].width`, "Provide a positive integer width.", "error");
|
|
353
|
+
if (image.height !== void 0 && (!Number.isInteger(image.height) || image.height <= 0)) add("invalid-og-image-height", "Open Graph image height must be a positive integer.", `openGraph.images[${index}].height`, "Provide a positive integer height.", "error");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (metadata.twitter?.images && metadata.twitter.images.some((image) => !isSafeHttpUrl(image))) add("invalid-twitter-image-url", "Twitter image URLs must be absolute HTTP(S) URLs.", "twitter.images", "Provide valid absolute image URLs.", "error");
|
|
357
|
+
const alternates = metadata.alternates?.alternates ?? [];
|
|
358
|
+
const seenLocales = /* @__PURE__ */ new Set();
|
|
359
|
+
for (const [index, alternate] of alternates.entries()) {
|
|
360
|
+
const locale = alternate.locale.toLowerCase();
|
|
361
|
+
if (seenLocales.has(locale)) add("duplicate-hreflang", `Duplicate hreflang locale: ${alternate.locale}.`, `alternates.alternates[${index}].locale`, "Each hreflang locale should appear only once.", "error");
|
|
362
|
+
seenLocales.add(locale);
|
|
363
|
+
if (!isSafeHttpUrl(String(alternate.url))) add("invalid-hreflang-url", "Hreflang URL must be an absolute HTTP(S) URL.", `alternates.alternates[${index}].url`, "Provide a valid absolute URL.", "error");
|
|
364
|
+
}
|
|
365
|
+
for (const property of ["publishedTime", "modifiedTime"]) {
|
|
366
|
+
const value = metadata[property];
|
|
367
|
+
if (hasValue(value) && !validateDate(value)) add(`invalid-${property}`, `${property} must be a valid date or datetime.`, property, "Use an ISO 8601 date or datetime.", "error");
|
|
368
|
+
}
|
|
369
|
+
const robots = metadata.robots;
|
|
370
|
+
if (robots) for (const property of ["maxSnippet", "maxVideoPreview"]) {
|
|
371
|
+
const value = robots[property];
|
|
372
|
+
if (value !== void 0 && (!Number.isInteger(value) || value < -1)) add(`invalid-robots-${property}`, `robots.${property} must be an integer greater than or equal to -1.`, `robots.${property}`, "Use -1 or a non-negative integer.", "error");
|
|
373
|
+
}
|
|
374
|
+
return issues;
|
|
375
|
+
};
|
|
376
|
+
const executeCustomValidators = (validators, context) => {
|
|
377
|
+
const issues = [];
|
|
378
|
+
for (const validator of validators) try {
|
|
379
|
+
const result = validator(context);
|
|
380
|
+
if (!Array.isArray(result)) continue;
|
|
381
|
+
issues.push(...result.map((issue) => ({
|
|
382
|
+
...issue,
|
|
383
|
+
source: "custom"
|
|
384
|
+
})));
|
|
385
|
+
} catch (error) {
|
|
386
|
+
issues.push(createIssue("custom", "custom-validator-failed", error instanceof Error ? error.message : "A custom SEO validator failed.", "error"));
|
|
387
|
+
}
|
|
388
|
+
return issues;
|
|
389
|
+
};
|
|
390
|
+
const validateSeo = (input, options = {}) => {
|
|
391
|
+
const mode = normalizeMode(options.mode ?? "warn");
|
|
392
|
+
if (mode === "off") return createReport("off", []);
|
|
393
|
+
const registry = options.registry ?? defaultRegistry;
|
|
394
|
+
const graph = input.graph ?? (input.nodes ? { "@graph": [...input.nodes] } : void 0);
|
|
395
|
+
const issues = [];
|
|
396
|
+
for (const conflict of registry.getConflicts()) issues.push(createIssue("registry", conflict.code, conflict.message, "warning", {
|
|
397
|
+
path: conflict.property,
|
|
398
|
+
property: conflict.property,
|
|
399
|
+
suggestion: "Resolve the conflicting Schema.org definitions.",
|
|
400
|
+
docsUrl: SCHEMA_DOCS
|
|
401
|
+
}));
|
|
402
|
+
if (graph) {
|
|
403
|
+
issues.push(...validateGraph(graph, {
|
|
404
|
+
...options,
|
|
405
|
+
mode
|
|
406
|
+
}, registry));
|
|
407
|
+
if (Array.isArray(graph["@graph"])) {
|
|
408
|
+
graph["@graph"].forEach((node, index) => {
|
|
409
|
+
if (isObject(node)) issues.push(...validateSemanticNode(node, index));
|
|
410
|
+
});
|
|
411
|
+
if (options.profiles?.length) issues.push(...validateRichResults(getNodes(graph), options.profiles, registry));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (input.metadata) issues.push(...validateMetadata(input.metadata));
|
|
415
|
+
if (options.validators?.length) {
|
|
416
|
+
const context = {
|
|
417
|
+
graph,
|
|
418
|
+
metadata: input.metadata,
|
|
419
|
+
registry,
|
|
420
|
+
mode
|
|
421
|
+
};
|
|
422
|
+
issues.push(...executeCustomValidators(options.validators, context));
|
|
423
|
+
}
|
|
424
|
+
return createReport(mode, issues);
|
|
425
|
+
};
|
|
426
|
+
const validateSeoMetadata = (metadata, options = {}) => {
|
|
427
|
+
return validateSeo({ metadata }, options);
|
|
428
|
+
};
|
|
429
|
+
/**
|
|
430
|
+
* A standalone node receives its @id when it is added to a graph, so assign the
|
|
431
|
+
* same generated ID here instead of reporting it as missing. Invalid nodes are
|
|
432
|
+
* left untouched so their issues are still reported.
|
|
433
|
+
*/
|
|
434
|
+
const withNodeId = (node, options) => {
|
|
435
|
+
if (typeof node["@id"] === "string") return node;
|
|
436
|
+
try {
|
|
437
|
+
return {
|
|
438
|
+
...node,
|
|
439
|
+
"@id": createSeoNode(node, { baseUrl: options.baseUrl })["@id"]
|
|
440
|
+
};
|
|
441
|
+
} catch {
|
|
442
|
+
return node;
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
const validateSeoNode = (node, options = {}) => {
|
|
446
|
+
return validateSeo({ nodes: [withNodeId(node, options)] }, options);
|
|
447
|
+
};
|
|
448
|
+
export { validateSeo, validateSeoMetadata, validateSeoNode };
|
|
449
|
+
|
|
450
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","names":[],"sources":["../../src/validation/validate.ts"],"sourcesContent":["import { createSeoNode, validateSeoGraph } from \"../core\";\n\nimport type { SeoGraph, SeoNode } from \"../core\";\n\nimport { createDefaultSchemaRegistry, validateSchemaNode } from \"../schema\";\n\nimport type { SchemaRegistry } from \"../schema\";\n\nimport type { ResolvedSeoMetadata, SeoPageMetadata } from \"../metadata/types\";\n\nimport type {\n SeoCustomValidator,\n SeoRichResultProfile,\n SeoValidationContext,\n SeoValidationIssue,\n SeoValidationMode,\n SeoValidationOptions,\n SeoValidationReport,\n SeoValidationSource,\n SeoValidationSummary,\n SeoValidationInput,\n} from \"./types\";\n\nconst defaultRegistry = createDefaultSchemaRegistry();\n\nconst SCHEMA_DOCS = \"https://schema.org/\";\n\nconst GOOGLE_DOCS =\n \"https://developers.google.com/search/docs/appearance/structured-data\";\n\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst getTypes = (node: SeoNode): string[] => {\n const value = node[\"@type\"];\n\n return (Array.isArray(value) ? value : [value]).filter(\n (type): type is string =>\n typeof type === \"string\" && type.trim().length > 0,\n );\n};\n\nconst hasValue = (value: unknown): boolean => {\n if (value === undefined || value === null) {\n return false;\n }\n\n if (typeof value === \"string\") {\n return value.trim().length > 0;\n }\n\n if (Array.isArray(value)) {\n return value.length > 0;\n }\n\n return true;\n};\n\nconst getNodes = (graph?: SeoGraph): SeoNode[] => graph?.[\"@graph\"] ?? [];\n\nconst createIssue = (\n source: SeoValidationSource,\n code: string,\n message: string,\n severity: SeoValidationIssue[\"severity\"] = \"warning\",\n details: Partial<\n Pick<\n SeoValidationIssue,\n \"path\" | \"nodeId\" | \"property\" | \"suggestion\" | \"docsUrl\"\n >\n > = {},\n): SeoValidationIssue => ({\n source,\n code,\n message,\n severity,\n ...details,\n});\n\nconst normalizeMode = (mode: SeoValidationMode): SeoValidationMode => mode;\n\nconst normalizeSeverity = (\n severity: SeoValidationIssue[\"severity\"],\n mode: SeoValidationMode,\n): SeoValidationIssue[\"severity\"] => {\n if (mode === \"strict\" && severity === \"warning\") {\n return \"error\";\n }\n\n return severity;\n};\n\nconst createSummary = (\n issues: readonly SeoValidationIssue[],\n): SeoValidationSummary => {\n const summary = {\n errors: 0,\n warnings: 0,\n infos: 0,\n total: issues.length,\n };\n\n for (const issue of issues) {\n switch (issue.severity) {\n case \"error\":\n summary.errors += 1;\n break;\n\n case \"warning\":\n summary.warnings += 1;\n break;\n\n case \"info\":\n summary.infos += 1;\n break;\n }\n }\n\n return summary;\n};\n\nconst createReport = (\n mode: SeoValidationMode,\n issues: SeoValidationIssue[],\n): SeoValidationReport => {\n const normalizedIssues = issues.map((issue) => ({\n ...issue,\n severity: normalizeSeverity(issue.severity, mode),\n }));\n\n const summary = createSummary(normalizedIssues);\n\n return {\n valid: summary.errors === 0,\n mode,\n issues: normalizedIssues,\n summary,\n };\n};\n\n// ---------------------------------------------------------------------------\n// Graph validation\n// ---------------------------------------------------------------------------\n\nconst validateGraph = (\n graph: SeoGraph,\n options: SeoValidationOptions,\n registry: SchemaRegistry,\n): SeoValidationIssue[] => {\n const issues: SeoValidationIssue[] = [];\n\n if (!Array.isArray(graph[\"@graph\"])) {\n return [\n createIssue(\n \"graph\",\n \"invalid-graph\",\n \"SEO graph must contain an @graph array.\",\n \"error\",\n ),\n ];\n }\n\n const result = validateSeoGraph(graph, {\n baseUrl: options.baseUrl,\n strictReferences: options.mode === \"strict\",\n allowedExternalReferences: options.allowExternalReferences ?? true,\n });\n\n for (const issue of result.issues) {\n issues.push(\n createIssue(\"graph\", issue.code, issue.message, issue.severity, {\n path: issue.path,\n nodeId: issue.nodeId,\n property: issue.property,\n suggestion:\n issue.code === \"unresolved-reference\"\n ? \"Register the referenced node or verify its @id.\"\n : undefined,\n }),\n );\n }\n\n for (let index = 0; index < graph[\"@graph\"].length; index += 1) {\n const node = graph[\"@graph\"][index];\n\n if (!isObject(node)) {\n continue;\n }\n\n const nodeId = typeof node[\"@id\"] === \"string\" ? node[\"@id\"] : undefined;\n\n const result = validateSchemaNode(node, registry, {\n strict: options.mode === \"strict\",\n allowUnknownProperties:\n options.allowUnknownProperties ?? options.mode !== \"strict\",\n validateRequiredProperties: options.validateRequiredProperties ?? false,\n });\n\n for (const issue of result.issues) {\n issues.push(\n createIssue(\"schema\", issue.code, issue.message, issue.severity, {\n path: issue.path\n ? `@graph[${index}]${issue.path.replace(\"$\", \"\")}`\n : `@graph[${index}]`,\n nodeId,\n property: issue.property,\n suggestion:\n issue.code === \"unknown-property\"\n ? \"Check the Schema.org property name or extend the registry.\"\n : issue.code === \"unknown-type\"\n ? \"Use a registered Schema.org type or add a custom definition.\"\n : undefined,\n docsUrl: SCHEMA_DOCS,\n }),\n );\n }\n }\n\n return issues;\n};\n\n// ---------------------------------------------------------------------------\n// Semantic validation\n// ---------------------------------------------------------------------------\n\nconst validateDate = (value: unknown): boolean => {\n if (typeof value !== \"string\") {\n return false;\n }\n\n return value.trim().length > 0 && !Number.isNaN(Date.parse(value));\n};\n\nconst validateSemanticNode = (\n node: SeoNode,\n index: number,\n): SeoValidationIssue[] => {\n const issues: SeoValidationIssue[] = [];\n\n const types = getTypes(node);\n\n const nodeId = typeof node[\"@id\"] === \"string\" ? node[\"@id\"] : undefined;\n\n const path = `@graph[${index}]`;\n\n const add = (\n code: string,\n message: string,\n property: string,\n suggestion: string,\n ) => {\n issues.push(\n createIssue(\"semantic\", code, message, \"warning\", {\n path: `${path}.${property}`,\n nodeId,\n property,\n suggestion,\n docsUrl: SCHEMA_DOCS,\n }),\n );\n };\n\n // Article\n\n if (\n types.some((type) =>\n [\"Article\", \"NewsArticle\", \"BlogPosting\", \"TechArticle\"].includes(\n type.split(\"/\").pop() ?? type,\n ),\n )\n ) {\n if (!hasValue(node.headline)) {\n add(\n \"article-missing-headline\",\n \"Article nodes should provide a headline.\",\n \"headline\",\n \"Add a descriptive headline.\",\n );\n }\n\n if (hasValue(node.datePublished) && !validateDate(node.datePublished)) {\n add(\n \"invalid-publication-date\",\n \"Article datePublished is not a valid date.\",\n \"datePublished\",\n \"Use a valid ISO 8601 date or datetime.\",\n );\n }\n\n if (hasValue(node.dateModified) && !validateDate(node.dateModified)) {\n add(\n \"invalid-modification-date\",\n \"Article dateModified is not a valid date.\",\n \"dateModified\",\n \"Use a valid ISO 8601 date or datetime.\",\n );\n }\n\n if (\n hasValue(node.datePublished) &&\n hasValue(node.dateModified) &&\n validateDate(node.datePublished) &&\n validateDate(node.dateModified) &&\n Date.parse(String(node.dateModified)) <\n Date.parse(String(node.datePublished))\n ) {\n add(\n \"modification-before-publication\",\n \"Article dateModified is earlier than datePublished.\",\n \"dateModified\",\n \"Verify the publication and modification dates.\",\n );\n }\n }\n\n // Product\n\n if (\n types.includes(\"Product\") ||\n types.includes(\"https://schema.org/Product\")\n ) {\n if (!hasValue(node.name)) {\n add(\n \"product-missing-name\",\n \"Product nodes should provide a name.\",\n \"name\",\n \"Add the product's visible name.\",\n );\n }\n\n if (\n hasValue(node.offers) &&\n !Array.isArray(node.offers) &&\n !isObject(node.offers)\n ) {\n add(\n \"invalid-product-offers\",\n \"Product offers must be an Offer node or an array of Offer nodes.\",\n \"offers\",\n \"Provide an Offer object or an array of Offer objects.\",\n );\n }\n }\n\n // Offer\n\n if (types.includes(\"Offer\") || types.includes(\"https://schema.org/Offer\")) {\n if (hasValue(node.price)) {\n const price = Number(node.price);\n\n if (!Number.isFinite(price) || price < 0) {\n add(\n \"invalid-offer-price\",\n \"Offer price must be a non-negative finite number.\",\n \"price\",\n \"Provide a valid numeric price.\",\n );\n }\n }\n\n if (hasValue(node.price) && !hasValue(node.priceCurrency)) {\n add(\n \"offer-missing-currency\",\n \"Offers with a price should specify priceCurrency.\",\n \"priceCurrency\",\n \"Provide an ISO 4217 currency code, such as EUR or USD.\",\n );\n }\n\n if (\n hasValue(node.priceCurrency) &&\n !/^[A-Z]{3}$/.test(String(node.priceCurrency))\n ) {\n add(\n \"invalid-price-currency\",\n \"Offer priceCurrency should use an ISO 4217 currency code.\",\n \"priceCurrency\",\n \"Use a three-letter uppercase currency code.\",\n );\n }\n }\n\n // BreadcrumbList\n\n if (\n types.includes(\"BreadcrumbList\") ||\n types.includes(\"https://schema.org/BreadcrumbList\")\n ) {\n const elements = node.itemListElement;\n\n if (!Array.isArray(elements) || elements.length === 0) {\n add(\n \"breadcrumb-missing-items\",\n \"BreadcrumbList should contain itemListElement entries.\",\n \"itemListElement\",\n \"Add at least one ListItem.\",\n );\n } else {\n const positions = new Set<number>();\n\n elements.forEach((item, itemIndex) => {\n if (!isObject(item)) {\n add(\n \"invalid-breadcrumb-item\",\n \"Breadcrumb entries should be ListItem objects.\",\n `itemListElement[${itemIndex}]`,\n \"Provide a ListItem with position, name and item.\",\n );\n\n return;\n }\n\n const position = Number(item.position);\n\n if (\n !Number.isInteger(position) ||\n position < 1 ||\n positions.has(position)\n ) {\n add(\n \"invalid-breadcrumb-position\",\n \"Breadcrumb positions must be unique positive integers.\",\n `itemListElement[${itemIndex}].position`,\n \"Use sequential positions starting at 1.\",\n );\n } else {\n positions.add(position);\n }\n\n if (!hasValue(item.name)) {\n add(\n \"breadcrumb-missing-name\",\n \"Breadcrumb ListItem should provide a name.\",\n `itemListElement[${itemIndex}].name`,\n \"Provide the visible breadcrumb label.\",\n );\n }\n\n if (!hasValue(item.item) && itemIndex !== elements.length - 1) {\n add(\n \"breadcrumb-missing-item\",\n \"Breadcrumb ListItem should provide an item URL.\",\n `itemListElement[${itemIndex}].item`,\n \"Provide the destination URL.\",\n );\n }\n });\n }\n }\n\n return issues;\n};\n\n// ---------------------------------------------------------------------------\n// Rich Results profiles\n// ---------------------------------------------------------------------------\n\ninterface RichResultRule {\n type: string;\n required: readonly string[];\n recommended?: readonly string[];\n requireAny?: readonly string[];\n docsUrl: string;\n}\n\nconst richResultRules: Record<SeoRichResultProfile, RichResultRule> = {\n article: {\n type: \"Article\",\n required: [],\n recommended: [\n \"headline\",\n \"image\",\n \"datePublished\",\n \"dateModified\",\n \"author\",\n ],\n docsUrl: `${GOOGLE_DOCS}/article`,\n },\n\n product: {\n type: \"Product\",\n required: [\"name\"],\n requireAny: [\"offers\", \"aggregateRating\", \"review\"],\n recommended: [\"image\", \"description\"],\n docsUrl: `${GOOGLE_DOCS}/product`,\n },\n\n breadcrumb: {\n type: \"BreadcrumbList\",\n required: [\"itemListElement\"],\n docsUrl: `${GOOGLE_DOCS}/breadcrumb`,\n },\n\n event: {\n type: \"Event\",\n required: [\"name\", \"startDate\", \"location\"],\n docsUrl: `${GOOGLE_DOCS}/event`,\n },\n\n recipe: {\n type: \"Recipe\",\n required: [\"name\", \"image\"],\n recommended: [\n \"author\",\n \"description\",\n \"prepTime\",\n \"cookTime\",\n \"recipeYield\",\n \"nutrition\",\n ],\n docsUrl: `${GOOGLE_DOCS}/recipe`,\n },\n\n \"job-posting\": {\n type: \"JobPosting\",\n required: [\n \"title\",\n \"description\",\n \"datePosted\",\n \"hiringOrganization\",\n \"jobLocation\",\n ],\n recommended: [\"validThrough\", \"employmentType\", \"baseSalary\"],\n docsUrl: `${GOOGLE_DOCS}/job-posting`,\n },\n\n \"local-business\": {\n type: \"LocalBusiness\",\n required: [\"name\", \"address\"],\n recommended: [\"telephone\", \"url\", \"image\", \"openingHoursSpecification\"],\n docsUrl: `${GOOGLE_DOCS}/local-business`,\n },\n\n \"software-application\": {\n type: \"SoftwareApplication\",\n required: [\"name\", \"offers\"],\n requireAny: [\"aggregateRating\", \"review\"],\n recommended: [\"applicationCategory\", \"operatingSystem\"],\n docsUrl: `${GOOGLE_DOCS}/software-app`,\n },\n};\n\nconst profileMatches = (\n node: SeoNode,\n profile: SeoRichResultProfile,\n registry: SchemaRegistry,\n): boolean => {\n const expected = richResultRules[profile].type;\n\n return getTypes(node).some((type) => registry.isTypeOf(type, expected));\n};\n\nconst getNestedObjects = (value: unknown): Record<string, unknown>[] => {\n const values = Array.isArray(value) ? value : [value];\n\n return values.filter(isObject);\n};\n\nconst hasNestedValue = (value: unknown, property: string): boolean =>\n getNestedObjects(value).some((item) => hasValue(item[property]));\n\nconst validateRichResults = (\n nodes: readonly SeoNode[],\n profiles: readonly SeoRichResultProfile[],\n registry: SchemaRegistry,\n): SeoValidationIssue[] => {\n const issues: SeoValidationIssue[] = [];\n\n for (const profile of profiles) {\n const rule = richResultRules[profile];\n\n nodes.forEach((node, nodeIndex) => {\n if (!profileMatches(node, profile, registry)) {\n return;\n }\n\n const nodeId = typeof node[\"@id\"] === \"string\" ? node[\"@id\"] : undefined;\n\n const path = `@graph[${nodeIndex}]`;\n\n const add = (\n code: string,\n message: string,\n property?: string,\n severity: SeoValidationIssue[\"severity\"] = \"warning\",\n suggestion?: string,\n ) => {\n issues.push(\n createIssue(\"rich-results\", code, message, severity, {\n nodeId,\n property,\n path: property ? `${path}.${property}` : path,\n suggestion,\n docsUrl: rule.docsUrl,\n }),\n );\n };\n\n // Required properties\n\n for (const property of rule.required) {\n if (hasValue(node[property])) {\n continue;\n }\n\n add(\n `rich-results-missing-${property}`,\n `${rule.type} is missing the required Rich Results property \"${property}\".`,\n property,\n \"warning\",\n `Add the ${property} property to meet the ${profile} structured data requirements.`,\n );\n }\n\n // Recommended properties\n\n for (const property of rule.recommended ?? []) {\n if (hasValue(node[property])) {\n continue;\n }\n\n add(\n `rich-results-recommended-${property}`,\n `${rule.type} should provide the recommended property \"${property}\".`,\n property,\n \"info\",\n `Consider adding ${property} to provide more complete structured data.`,\n );\n }\n\n // At least one of a group of alternative properties\n\n if (\n rule.requireAny &&\n !rule.requireAny.some((property) => hasValue(node[property]))\n ) {\n add(\n \"rich-results-missing-alternative\",\n `${rule.type} must provide at least one of: ${rule.requireAny.join(\", \")}.`,\n undefined,\n \"warning\",\n `Provide at least one of: ${rule.requireAny.join(\", \")}.`,\n );\n }\n\n // Product-specific checks\n\n if (profile === \"product\" && hasValue(node.offers)) {\n if (!hasNestedValue(node.offers, \"price\")) {\n add(\n \"rich-results-product-offer-missing-price\",\n \"Product offers should include a price.\",\n \"offers\",\n \"warning\",\n \"Add a price to each Offer object.\",\n );\n }\n }\n\n // Breadcrumb-specific checks\n\n if (profile === \"breadcrumb\") {\n const elements = node.itemListElement;\n\n if (Array.isArray(elements) && elements.length < 2) {\n add(\n \"rich-results-breadcrumb-minimum-items\",\n \"BreadcrumbList must contain at least two ListItem entries.\",\n \"itemListElement\",\n \"warning\",\n \"Provide at least two breadcrumb items.\",\n );\n }\n }\n\n // Event-specific checks\n\n if (profile === \"event\" && isObject(node.location)) {\n if (!hasValue(node.location.name)) {\n add(\n \"rich-results-event-missing-location-name\",\n \"Event location should include a name.\",\n \"location.name\",\n \"warning\",\n \"Provide the name of the physical event venue.\",\n );\n }\n\n if (!hasValue(node.location.address)) {\n add(\n \"rich-results-event-missing-address\",\n \"Event location should include a detailed address.\",\n \"location.address\",\n \"warning\",\n \"Provide the venue's PostalAddress.\",\n );\n }\n }\n\n // LocalBusiness-specific checks\n\n if (profile === \"local-business\" && hasValue(node.address)) {\n if (!isObject(node.address)) {\n add(\n \"rich-results-business-invalid-address\",\n \"LocalBusiness address should be a PostalAddress object.\",\n \"address\",\n \"warning\",\n \"Provide an address object with the relevant PostalAddress fields.\",\n );\n }\n }\n\n // SoftwareApplication-specific checks\n\n if (profile === \"software-application\" && hasValue(node.offers)) {\n if (!hasNestedValue(node.offers, \"price\")) {\n add(\n \"rich-results-software-missing-price\",\n \"SoftwareApplication offers must include a price.\",\n \"offers.price\",\n \"warning\",\n \"Provide an Offer object with a price. Use 0 for a free application.\",\n );\n }\n }\n\n // JobPosting-specific checks\n\n if (profile === \"job-posting\") {\n if (\n isObject(node.hiringOrganization) &&\n !hasValue(node.hiringOrganization.name)\n ) {\n add(\n \"rich-results-job-missing-employer-name\",\n \"JobPosting hiringOrganization should include a name.\",\n \"hiringOrganization.name\",\n \"warning\",\n \"Provide the employer's organization name.\",\n );\n }\n\n const locations = getNestedObjects(node.jobLocation);\n\n const hasCountry = locations.some((location) => {\n if (!isObject(location.address)) {\n return false;\n }\n\n return hasValue(location.address.addressCountry);\n });\n\n if (locations.length > 0 && !hasCountry) {\n add(\n \"rich-results-job-missing-country\",\n \"JobPosting jobLocation address should include addressCountry.\",\n \"jobLocation.address.addressCountry\",\n \"warning\",\n \"Provide the country code or country name in the job location address.\",\n );\n }\n }\n });\n }\n\n return issues;\n};\n\n// ---------------------------------------------------------------------------\n// Metadata validation\n// ---------------------------------------------------------------------------\n\nconst isSafeHttpUrl = (value: string): boolean => {\n try {\n const url = new URL(value);\n\n return (\n (url.protocol === \"http:\" || url.protocol === \"https:\") &&\n !url.username &&\n !url.password\n );\n } catch {\n return false;\n }\n};\n\nconst validateMetadata = (\n metadata: SeoPageMetadata | ResolvedSeoMetadata,\n): SeoValidationIssue[] => {\n const issues: SeoValidationIssue[] = [];\n\n const add = (\n code: string,\n message: string,\n property: string,\n suggestion: string,\n severity: SeoValidationIssue[\"severity\"] = \"warning\",\n ) => {\n issues.push(\n createIssue(\"metadata\", code, message, severity, {\n path: property,\n property,\n suggestion,\n }),\n );\n };\n\n // Title\n\n if (!hasValue(metadata.title)) {\n add(\n \"missing-title\",\n \"SEO title is missing.\",\n \"title\",\n \"Provide a descriptive page title.\",\n );\n } else {\n const title = String(metadata.title);\n\n if (title.length > 60) {\n add(\n \"title-too-long\",\n \"SEO title exceeds the recommended 60 characters.\",\n \"title\",\n \"Consider shortening the title.\",\n \"info\",\n );\n }\n\n if (title.length < 15) {\n add(\n \"title-too-short\",\n \"SEO title is unusually short.\",\n \"title\",\n \"Consider providing a more descriptive title.\",\n \"info\",\n );\n }\n }\n\n // Description\n\n if (!hasValue(metadata.description)) {\n add(\n \"missing-description\",\n \"SEO meta description is missing.\",\n \"description\",\n \"Provide a concise description of the page.\",\n );\n } else {\n const description = String(metadata.description);\n\n if (description.length > 160) {\n add(\n \"description-too-long\",\n \"SEO description exceeds the recommended 160 characters.\",\n \"description\",\n \"Consider shortening the description.\",\n \"info\",\n );\n }\n\n if (description.length < 50) {\n add(\n \"description-too-short\",\n \"SEO description is unusually short.\",\n \"description\",\n \"Consider providing more descriptive content.\",\n \"info\",\n );\n }\n }\n\n // Canonical\n\n if (hasValue(metadata.canonical)) {\n if (!isSafeHttpUrl(String(metadata.canonical))) {\n add(\n \"invalid-canonical\",\n \"Canonical URL must be an absolute HTTP(S) URL without credentials.\",\n \"canonical\",\n \"Provide a valid canonical URL.\",\n \"error\",\n );\n }\n }\n\n // Locale\n\n if (hasValue(metadata.locale)) {\n try {\n Intl.getCanonicalLocales(String(metadata.locale));\n } catch {\n add(\n \"invalid-metadata-locale\",\n \"Metadata locale is invalid.\",\n \"locale\",\n \"Use a valid BCP 47 locale such as en, nl or nl-NL.\",\n \"error\",\n );\n }\n }\n\n // Open Graph\n\n const openGraph = metadata.openGraph;\n\n if (openGraph) {\n if (hasValue(openGraph.url) && !isSafeHttpUrl(String(openGraph.url))) {\n add(\n \"invalid-og-url\",\n \"Open Graph URL must be an absolute HTTP(S) URL.\",\n \"openGraph.url\",\n \"Provide a valid Open Graph URL.\",\n \"error\",\n );\n }\n\n if (!hasValue(openGraph.title)) {\n add(\n \"missing-og-title\",\n \"Open Graph title is missing.\",\n \"openGraph.title\",\n \"Provide an Open Graph title.\",\n );\n }\n\n if (!hasValue(openGraph.description)) {\n add(\n \"missing-og-description\",\n \"Open Graph description is missing.\",\n \"openGraph.description\",\n \"Provide an Open Graph description.\",\n );\n }\n\n for (const [index, image] of (openGraph.images ?? []).entries()) {\n if (!isSafeHttpUrl(String(image.url))) {\n add(\n \"invalid-og-image-url\",\n \"Open Graph image URL must be an absolute HTTP(S) URL.\",\n `openGraph.images[${index}].url`,\n \"Provide an absolute image URL.\",\n \"error\",\n );\n }\n\n if (!hasValue(image.alt)) {\n add(\n \"missing-og-image-alt\",\n \"Open Graph image should have alternative text.\",\n `openGraph.images[${index}].alt`,\n \"Provide descriptive image alternative text.\",\n \"info\",\n );\n }\n\n if (\n image.width !== undefined &&\n (!Number.isInteger(image.width) || image.width <= 0)\n ) {\n add(\n \"invalid-og-image-width\",\n \"Open Graph image width must be a positive integer.\",\n `openGraph.images[${index}].width`,\n \"Provide a positive integer width.\",\n \"error\",\n );\n }\n\n if (\n image.height !== undefined &&\n (!Number.isInteger(image.height) || image.height <= 0)\n ) {\n add(\n \"invalid-og-image-height\",\n \"Open Graph image height must be a positive integer.\",\n `openGraph.images[${index}].height`,\n \"Provide a positive integer height.\",\n \"error\",\n );\n }\n }\n }\n\n // Twitter\n\n if (\n metadata.twitter?.images &&\n metadata.twitter.images.some((image) => !isSafeHttpUrl(image))\n ) {\n add(\n \"invalid-twitter-image-url\",\n \"Twitter image URLs must be absolute HTTP(S) URLs.\",\n \"twitter.images\",\n \"Provide valid absolute image URLs.\",\n \"error\",\n );\n }\n\n // Hreflang\n\n const alternates = metadata.alternates?.alternates ?? [];\n\n const seenLocales = new Set<string>();\n\n for (const [index, alternate] of alternates.entries()) {\n const locale = alternate.locale.toLowerCase();\n\n if (seenLocales.has(locale)) {\n add(\n \"duplicate-hreflang\",\n `Duplicate hreflang locale: ${alternate.locale}.`,\n `alternates.alternates[${index}].locale`,\n \"Each hreflang locale should appear only once.\",\n \"error\",\n );\n }\n\n seenLocales.add(locale);\n\n if (!isSafeHttpUrl(String(alternate.url))) {\n add(\n \"invalid-hreflang-url\",\n \"Hreflang URL must be an absolute HTTP(S) URL.\",\n `alternates.alternates[${index}].url`,\n \"Provide a valid absolute URL.\",\n \"error\",\n );\n }\n }\n\n // Article metadata\n\n for (const property of [\"publishedTime\", \"modifiedTime\"] as const) {\n const value = metadata[property];\n\n if (hasValue(value) && !validateDate(value)) {\n add(\n `invalid-${property}`,\n `${property} must be a valid date or datetime.`,\n property,\n \"Use an ISO 8601 date or datetime.\",\n \"error\",\n );\n }\n }\n\n // Robots\n\n const robots = metadata.robots;\n\n if (robots) {\n for (const property of [\"maxSnippet\", \"maxVideoPreview\"] as const) {\n const value = robots[property];\n\n if (value !== undefined && (!Number.isInteger(value) || value < -1)) {\n add(\n `invalid-robots-${property}`,\n `robots.${property} must be an integer greater than or equal to -1.`,\n `robots.${property}`,\n \"Use -1 or a non-negative integer.\",\n \"error\",\n );\n }\n }\n }\n\n return issues;\n};\n\n// ---------------------------------------------------------------------------\n// Custom validators\n// ---------------------------------------------------------------------------\n\nconst executeCustomValidators = (\n validators: readonly SeoCustomValidator[],\n context: SeoValidationContext,\n): SeoValidationIssue[] => {\n const issues: SeoValidationIssue[] = [];\n\n for (const validator of validators) {\n try {\n const result = validator(context);\n\n if (!Array.isArray(result)) {\n continue;\n }\n\n issues.push(\n ...result.map((issue) => ({\n ...issue,\n source: \"custom\" as const,\n })),\n );\n } catch (error) {\n issues.push(\n createIssue(\n \"custom\",\n \"custom-validator-failed\",\n error instanceof Error\n ? error.message\n : \"A custom SEO validator failed.\",\n \"error\",\n ),\n );\n }\n }\n\n return issues;\n};\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\nexport const validateSeo = (\n input: SeoValidationInput,\n options: SeoValidationOptions = {},\n): SeoValidationReport => {\n const mode = normalizeMode(options.mode ?? \"warn\");\n\n if (mode === \"off\") {\n return createReport(\"off\", []);\n }\n\n const registry = options.registry ?? defaultRegistry;\n\n const graph =\n input.graph ??\n (input.nodes\n ? {\n \"@graph\": [...input.nodes],\n }\n : undefined);\n\n const issues: SeoValidationIssue[] = [];\n\n // Registry diagnostics\n\n for (const conflict of registry.getConflicts()) {\n issues.push(\n createIssue(\"registry\", conflict.code, conflict.message, \"warning\", {\n path: conflict.property,\n property: conflict.property,\n suggestion: \"Resolve the conflicting Schema.org definitions.\",\n docsUrl: SCHEMA_DOCS,\n }),\n );\n }\n\n // Graph and Schema.org validation\n\n if (graph) {\n issues.push(\n ...validateGraph(\n graph,\n {\n ...options,\n mode,\n },\n registry,\n ),\n );\n\n if (Array.isArray(graph[\"@graph\"])) {\n graph[\"@graph\"].forEach((node, index) => {\n if (isObject(node)) {\n issues.push(...validateSemanticNode(node, index));\n }\n });\n\n if (options.profiles?.length) {\n issues.push(\n ...validateRichResults(getNodes(graph), options.profiles, registry),\n );\n }\n }\n }\n\n // Metadata validation\n\n if (input.metadata) {\n issues.push(...validateMetadata(input.metadata));\n }\n\n // Custom validators\n\n if (options.validators?.length) {\n const context: SeoValidationContext = {\n graph,\n metadata: input.metadata,\n registry,\n mode,\n };\n\n issues.push(...executeCustomValidators(options.validators, context));\n }\n\n return createReport(mode, issues);\n};\n\nexport const validateSeoMetadata = (\n metadata: SeoPageMetadata | ResolvedSeoMetadata,\n options: Pick<SeoValidationOptions, \"mode\"> = {},\n): SeoValidationReport => {\n return validateSeo(\n {\n metadata,\n },\n options,\n );\n};\n\n/**\n * A standalone node receives its @id when it is added to a graph, so assign the\n * same generated ID here instead of reporting it as missing. Invalid nodes are\n * left untouched so their issues are still reported.\n */\nconst withNodeId = (node: SeoNode, options: SeoValidationOptions): SeoNode => {\n if (typeof node[\"@id\"] === \"string\") {\n return node;\n }\n\n try {\n return {\n ...node,\n \"@id\": createSeoNode(node, { baseUrl: options.baseUrl })[\"@id\"],\n };\n } catch {\n return node;\n }\n};\n\nexport const validateSeoNode = (\n node: SeoNode,\n options: SeoValidationOptions = {},\n): SeoValidationReport => {\n return validateSeo(\n {\n nodes: [withNodeId(node, options)],\n },\n options,\n );\n};\n"],"mappings":"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"}
|