@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,456 @@
|
|
|
1
|
+
// src/core/graph.ts
|
|
2
|
+
const DEFAULT_CONTEXT = "https://schema.org";
|
|
3
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
|
+
const isNode = (value) => isObject(value) && (typeof value["@type"] === "string" || Array.isArray(value["@type"]) && value["@type"].every((item) => typeof item === "string"));
|
|
5
|
+
const normalizeType = (type) => {
|
|
6
|
+
const value = type.trim();
|
|
7
|
+
if (!value) throw new Error("SEO node type cannot be empty.");
|
|
8
|
+
if (/^[A-Za-z][A-Za-z0-9]*$/.test(value)) return `https://schema.org/${value}`;
|
|
9
|
+
return value.replace(/^http:\/\/schema\.org\//, "https://schema.org/");
|
|
10
|
+
};
|
|
11
|
+
const compareStrings = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
12
|
+
const slugify = (value) => value.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
|
|
13
|
+
const canonicalize = (value) => {
|
|
14
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
15
|
+
if (!isObject(value)) return value;
|
|
16
|
+
const result = {};
|
|
17
|
+
const preferredOrder = [
|
|
18
|
+
"@context",
|
|
19
|
+
"@id",
|
|
20
|
+
"@type"
|
|
21
|
+
];
|
|
22
|
+
const keys = Object.keys(value).sort((a, b) => {
|
|
23
|
+
const ai = preferredOrder.indexOf(a);
|
|
24
|
+
const bi = preferredOrder.indexOf(b);
|
|
25
|
+
if (ai !== -1 || bi !== -1) {
|
|
26
|
+
if (ai === -1) return 1;
|
|
27
|
+
if (bi === -1) return -1;
|
|
28
|
+
return ai - bi;
|
|
29
|
+
}
|
|
30
|
+
return compareStrings(a, b);
|
|
31
|
+
});
|
|
32
|
+
for (const key of keys) result[key] = canonicalize(value[key]);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
const stableStringify = (value) => JSON.stringify(canonicalize(value));
|
|
36
|
+
const hashString = (value) => {
|
|
37
|
+
let hash = 2166136261;
|
|
38
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
39
|
+
hash ^= value.charCodeAt(index);
|
|
40
|
+
hash = Math.imul(hash, 16777619);
|
|
41
|
+
}
|
|
42
|
+
return (hash >>> 0).toString(36);
|
|
43
|
+
};
|
|
44
|
+
const normalizeBaseUrl = (baseUrl) => {
|
|
45
|
+
if (!baseUrl) return;
|
|
46
|
+
const url = new URL(baseUrl);
|
|
47
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("SEO baseUrl must use HTTP or HTTPS.");
|
|
48
|
+
url.hash = "";
|
|
49
|
+
return url.toString().replace(/\/$/, "");
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Converts an absolute, relative or fragment identifier
|
|
53
|
+
* into a canonical identifier.
|
|
54
|
+
*
|
|
55
|
+
* Relative identifiers require a base URL.
|
|
56
|
+
*/
|
|
57
|
+
const normalizeSeoId = (id, baseUrl) => {
|
|
58
|
+
const value = id.trim();
|
|
59
|
+
if (!value) throw new Error("SEO node ID cannot be empty.");
|
|
60
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(value)) try {
|
|
61
|
+
const url = new URL(value);
|
|
62
|
+
if (url.protocol === "http:" && url.hostname === "schema.org") url.protocol = "https:";
|
|
63
|
+
return url.toString();
|
|
64
|
+
} catch {
|
|
65
|
+
throw new Error(`Invalid SEO node ID: ${value}`);
|
|
66
|
+
}
|
|
67
|
+
const base = normalizeBaseUrl(baseUrl);
|
|
68
|
+
if (!base) {
|
|
69
|
+
if (value.startsWith("#")) return value;
|
|
70
|
+
throw new Error(`Relative SEO node ID "${value}" requires a baseUrl.`);
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
return new URL(value, `${base}/`).toString();
|
|
74
|
+
} catch {
|
|
75
|
+
throw new Error(`Invalid SEO node ID: ${value}`);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const createAutomaticId = (node, options) => {
|
|
79
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
80
|
+
const type = (Array.isArray(node["@type"]) ? node["@type"] : [node["@type"]])[0] ?? "Thing";
|
|
81
|
+
const typeSlug = slugify(type.split("/").pop() ?? "thing");
|
|
82
|
+
const identityProperties = [
|
|
83
|
+
"url",
|
|
84
|
+
"mainEntityOfPage",
|
|
85
|
+
"name",
|
|
86
|
+
"headline",
|
|
87
|
+
"title"
|
|
88
|
+
];
|
|
89
|
+
let identity;
|
|
90
|
+
for (const property of identityProperties) {
|
|
91
|
+
const value = node[property];
|
|
92
|
+
if (typeof value === "string" && value.trim()) {
|
|
93
|
+
identity = value;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
if (isObject(value) && typeof value["@id"] === "string") {
|
|
97
|
+
identity = value["@id"];
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const fragment = `${typeSlug}-${(identity ? slugify(identity) : hashString(stableStringify(node))) || hashString(stableStringify(node))}`;
|
|
102
|
+
return baseUrl ? `${baseUrl}/#${fragment}` : `#${fragment}`;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Removes values that cannot be represented safely in JSON-LD.
|
|
106
|
+
*
|
|
107
|
+
* Internal Puzzle properties beginning with __puzzle are excluded.
|
|
108
|
+
* User-defined Schema.org properties remain untouched.
|
|
109
|
+
*/
|
|
110
|
+
const cleanValue = (value) => {
|
|
111
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
112
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
113
|
+
if (typeof value === "undefined" || typeof value === "function" || typeof value === "symbol" || typeof value === "bigint") return;
|
|
114
|
+
if (Array.isArray(value)) return value.map(cleanValue).filter((item) => item !== void 0);
|
|
115
|
+
if (value instanceof Date) return value.toISOString();
|
|
116
|
+
if (!isObject(value)) return;
|
|
117
|
+
const result = {};
|
|
118
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
119
|
+
if (key.startsWith("__puzzle")) continue;
|
|
120
|
+
const cleaned = cleanValue(nestedValue);
|
|
121
|
+
if (cleaned !== void 0) result[key] = cleaned;
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
};
|
|
125
|
+
const normalizeNodeId = (node, options) => {
|
|
126
|
+
const explicitId = options.id ?? node["@id"];
|
|
127
|
+
if (typeof explicitId === "string") return normalizeSeoId(explicitId, options.baseUrl);
|
|
128
|
+
return createAutomaticId(node, options);
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Creates a normalized, JSON-compatible SEO node.
|
|
132
|
+
*
|
|
133
|
+
* Explicit IDs always override generated IDs.
|
|
134
|
+
* The original input is never mutated.
|
|
135
|
+
*/
|
|
136
|
+
const createSeoNode = (input, options = {}) => {
|
|
137
|
+
if (!isNode(input)) throw new Error("SEO node must contain a valid @type.");
|
|
138
|
+
const cleaned = cleanValue(input);
|
|
139
|
+
if (!isObject(cleaned)) throw new Error("Unable to normalize SEO node.");
|
|
140
|
+
const type = Array.isArray(cleaned["@type"]) ? cleaned["@type"].map((item) => normalizeType(String(item))) : normalizeType(String(cleaned["@type"]));
|
|
141
|
+
return {
|
|
142
|
+
...cleaned,
|
|
143
|
+
"@id": normalizeNodeId(input, options),
|
|
144
|
+
"@type": type
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
const cloneNode = (node) => {
|
|
148
|
+
const cloned = cleanValue(node);
|
|
149
|
+
if (!isObject(cloned) || !isNode(cloned)) throw new Error("Invalid SEO node.");
|
|
150
|
+
return cloned;
|
|
151
|
+
};
|
|
152
|
+
const isEmptyValue = (value) => value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0;
|
|
153
|
+
const mergeArrays = (left, right) => {
|
|
154
|
+
const values = /* @__PURE__ */ new Map();
|
|
155
|
+
for (const value of [...left, ...right]) values.set(stableStringify(value), value);
|
|
156
|
+
return [...values.entries()].sort(([a], [b]) => compareStrings(a, b)).map(([, value]) => value);
|
|
157
|
+
};
|
|
158
|
+
const mergeValues = (left, right, context, path) => {
|
|
159
|
+
if (isEmptyValue(left)) return right;
|
|
160
|
+
if (isEmptyValue(right)) return left;
|
|
161
|
+
if (path === "@type") {
|
|
162
|
+
const sorted = [.../* @__PURE__ */ new Set([...Array.isArray(left) ? left : [left], ...Array.isArray(right) ? right : [right]])].sort((a, b) => compareStrings(String(a), String(b)));
|
|
163
|
+
return sorted.length === 1 ? sorted[0] : sorted;
|
|
164
|
+
}
|
|
165
|
+
if (stableStringify(left) === stableStringify(right)) return left;
|
|
166
|
+
if (Array.isArray(left) && Array.isArray(right)) return mergeArrays(left, right);
|
|
167
|
+
if (isObject(left) && isObject(right)) {
|
|
168
|
+
const result = { ...left };
|
|
169
|
+
for (const [key, value] of Object.entries(right)) {
|
|
170
|
+
if (!(key in result)) {
|
|
171
|
+
result[key] = value;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
result[key] = mergeValues(result[key], value, context, path ? `${path}.${key}` : key);
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
const severity = context.strategy === "error" ? "error" : "warning";
|
|
179
|
+
context.issues.push({
|
|
180
|
+
code: "graph-property-conflict",
|
|
181
|
+
severity,
|
|
182
|
+
message: `Conflicting values found at ${path}.`,
|
|
183
|
+
nodeId: context.nodeId,
|
|
184
|
+
property: path
|
|
185
|
+
});
|
|
186
|
+
switch (context.strategy) {
|
|
187
|
+
case "first": return left;
|
|
188
|
+
case "last": return right;
|
|
189
|
+
case "error": return compareStrings(stableStringify(left), stableStringify(right)) <= 0 ? left : right;
|
|
190
|
+
default: return compareStrings(stableStringify(left), stableStringify(right)) <= 0 ? left : right;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
/**
|
|
194
|
+
* Merges two nodes with the same @id.
|
|
195
|
+
*
|
|
196
|
+
* Complementary values are combined.
|
|
197
|
+
* Arrays are deduplicated.
|
|
198
|
+
* Scalar conflicts are reported.
|
|
199
|
+
*/
|
|
200
|
+
const mergeSeoNodes = (left, right, options = {}) => {
|
|
201
|
+
const normalizedLeft = createSeoNode(left);
|
|
202
|
+
const normalizedRight = createSeoNode(right);
|
|
203
|
+
const leftId = normalizedLeft["@id"];
|
|
204
|
+
const rightId = normalizedRight["@id"];
|
|
205
|
+
if (!leftId || leftId !== rightId) throw new Error("Only SEO nodes with identical @id values can be merged.");
|
|
206
|
+
const issues = [];
|
|
207
|
+
const context = {
|
|
208
|
+
strategy: options.conflictStrategy ?? "deterministic",
|
|
209
|
+
issues,
|
|
210
|
+
nodeId: String(leftId)
|
|
211
|
+
};
|
|
212
|
+
return {
|
|
213
|
+
node: mergeValues(cloneNode(normalizedLeft), cloneNode(normalizedRight), context, ""),
|
|
214
|
+
issues
|
|
215
|
+
};
|
|
216
|
+
};
|
|
217
|
+
const isReference = (value) => isObject(value) && typeof value["@id"] === "string" && Object.keys(value).every((key) => key === "@id");
|
|
218
|
+
const createReference = (id) => ({ "@id": id });
|
|
219
|
+
/**
|
|
220
|
+
* Resolves inline nodes recursively.
|
|
221
|
+
*
|
|
222
|
+
* Nested objects with @type become independent graph nodes.
|
|
223
|
+
* Their original position is replaced by an @id reference.
|
|
224
|
+
*/
|
|
225
|
+
const resolveInlineValue = (value, state) => {
|
|
226
|
+
if (Array.isArray(value)) return value.map((item) => resolveInlineValue(item, state));
|
|
227
|
+
if (!isObject(value)) return value;
|
|
228
|
+
if (isNode(value)) {
|
|
229
|
+
const nestedNode = createSeoNode(value, {
|
|
230
|
+
baseUrl: state.options.baseUrl,
|
|
231
|
+
context: state.options.context
|
|
232
|
+
});
|
|
233
|
+
const resolved = resolveNode(nestedNode, state);
|
|
234
|
+
addNodeToState(resolved, state);
|
|
235
|
+
return createReference(String(resolved["@id"]));
|
|
236
|
+
}
|
|
237
|
+
if (isReference(value)) return { "@id": normalizeSeoId(value["@id"], state.options.baseUrl) };
|
|
238
|
+
const result = {};
|
|
239
|
+
for (const [key, nestedValue] of Object.entries(value)) result[key] = resolveInlineValue(nestedValue, state);
|
|
240
|
+
return result;
|
|
241
|
+
};
|
|
242
|
+
const resolveNode = (node, state) => {
|
|
243
|
+
const result = cloneNode(node);
|
|
244
|
+
for (const [key, value] of Object.entries(result)) {
|
|
245
|
+
if (key === "@id" || key === "@type" || key === "@context") continue;
|
|
246
|
+
result[key] = resolveInlineValue(value, state);
|
|
247
|
+
}
|
|
248
|
+
return result;
|
|
249
|
+
};
|
|
250
|
+
const addNodeToState = (node, state) => {
|
|
251
|
+
const id = String(node["@id"]);
|
|
252
|
+
const existing = state.nodes.get(id);
|
|
253
|
+
if (!existing) {
|
|
254
|
+
state.nodes.set(id, node);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const merged = mergeSeoNodes(existing, node, { conflictStrategy: state.options.conflictStrategy });
|
|
258
|
+
state.nodes.set(id, merged.node);
|
|
259
|
+
state.issues.push(...merged.issues);
|
|
260
|
+
};
|
|
261
|
+
const collectReferences = (value, references) => {
|
|
262
|
+
if (Array.isArray(value)) {
|
|
263
|
+
for (const item of value) collectReferences(item, references);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (!isObject(value)) return;
|
|
267
|
+
if (isReference(value)) {
|
|
268
|
+
references.add(value["@id"]);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
for (const nestedValue of Object.values(value)) collectReferences(nestedValue, references);
|
|
272
|
+
};
|
|
273
|
+
const isLocalReference = (id, baseUrl) => {
|
|
274
|
+
if (id.startsWith("#")) return true;
|
|
275
|
+
if (!baseUrl) return false;
|
|
276
|
+
try {
|
|
277
|
+
const reference = new URL(id);
|
|
278
|
+
const base = new URL(baseUrl);
|
|
279
|
+
return reference.origin === base.origin;
|
|
280
|
+
} catch {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
const validateReferences = (graph, options = {}) => {
|
|
285
|
+
const ids = new Set(graph["@graph"].map((node) => String(node["@id"])));
|
|
286
|
+
const references = /* @__PURE__ */ new Set();
|
|
287
|
+
for (const node of graph["@graph"]) collectReferences(node, references);
|
|
288
|
+
const issues = [];
|
|
289
|
+
for (const reference of references) {
|
|
290
|
+
if (ids.has(reference)) continue;
|
|
291
|
+
if (options.allowedExternalReferences !== false && !isLocalReference(reference, options.baseUrl)) continue;
|
|
292
|
+
issues.push({
|
|
293
|
+
code: "unresolved-reference",
|
|
294
|
+
severity: options.strictReferences ? "error" : "warning",
|
|
295
|
+
message: `Unable to resolve SEO reference: ${reference}`,
|
|
296
|
+
nodeId: reference
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return issues;
|
|
300
|
+
};
|
|
301
|
+
/**
|
|
302
|
+
* Builds a complete JSON-LD graph from a collection of nodes.
|
|
303
|
+
*
|
|
304
|
+
* Each builder instance owns its own state. No global state is used.
|
|
305
|
+
*/
|
|
306
|
+
const createSeoGraphBuilder = (options = {}) => {
|
|
307
|
+
const state = {
|
|
308
|
+
nodes: /* @__PURE__ */ new Map(),
|
|
309
|
+
issues: [],
|
|
310
|
+
options: {
|
|
311
|
+
conflictStrategy: "deterministic",
|
|
312
|
+
strictReferences: false,
|
|
313
|
+
...options
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
const builder = {
|
|
317
|
+
add(input) {
|
|
318
|
+
const node = createSeoNode(input, {
|
|
319
|
+
baseUrl: state.options.baseUrl,
|
|
320
|
+
context: state.options.context
|
|
321
|
+
});
|
|
322
|
+
const resolved = resolveNode(node, state);
|
|
323
|
+
addNodeToState(resolved, state);
|
|
324
|
+
return builder;
|
|
325
|
+
},
|
|
326
|
+
addMany(nodes) {
|
|
327
|
+
for (const node of nodes) builder.add(node);
|
|
328
|
+
return builder;
|
|
329
|
+
},
|
|
330
|
+
build() {
|
|
331
|
+
const nodes = [...state.nodes.values()].sort((a, b) => {
|
|
332
|
+
const left = String(a["@id"]);
|
|
333
|
+
const right = String(b["@id"]);
|
|
334
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
335
|
+
});
|
|
336
|
+
return {
|
|
337
|
+
"@context": state.options.context ?? DEFAULT_CONTEXT,
|
|
338
|
+
"@graph": nodes
|
|
339
|
+
};
|
|
340
|
+
},
|
|
341
|
+
getIssues() {
|
|
342
|
+
const graph = {
|
|
343
|
+
"@context": state.options.context ?? DEFAULT_CONTEXT,
|
|
344
|
+
"@graph": [...state.nodes.values()]
|
|
345
|
+
};
|
|
346
|
+
return [...state.issues, ...validateReferences(graph, {
|
|
347
|
+
baseUrl: state.options.baseUrl,
|
|
348
|
+
strictReferences: state.options.strictReferences,
|
|
349
|
+
allowedExternalReferences: true
|
|
350
|
+
})];
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
return builder;
|
|
354
|
+
};
|
|
355
|
+
/**
|
|
356
|
+
* Convenience function for building a graph in one operation.
|
|
357
|
+
*/
|
|
358
|
+
const buildSeoGraph = (nodes, options = {}) => {
|
|
359
|
+
return createSeoGraphBuilder(options).addMany(nodes).build();
|
|
360
|
+
};
|
|
361
|
+
/**
|
|
362
|
+
* Normalizes and validates an existing graph.
|
|
363
|
+
*
|
|
364
|
+
* Inline nodes are extracted and references are normalized.
|
|
365
|
+
*/
|
|
366
|
+
const resolveSeoReferences = (graph, options = {}) => {
|
|
367
|
+
return createSeoGraphBuilder({
|
|
368
|
+
...options,
|
|
369
|
+
context: options.context ?? graph["@context"] ?? DEFAULT_CONTEXT
|
|
370
|
+
}).addMany(graph["@graph"]).build();
|
|
371
|
+
};
|
|
372
|
+
/**
|
|
373
|
+
* Validates graph structure, IDs and references.
|
|
374
|
+
*/
|
|
375
|
+
const validateSeoGraph = (graph, options = {}) => {
|
|
376
|
+
const issues = [];
|
|
377
|
+
if (!Array.isArray(graph["@graph"])) return {
|
|
378
|
+
valid: false,
|
|
379
|
+
issues: [{
|
|
380
|
+
code: "invalid-graph",
|
|
381
|
+
severity: "error",
|
|
382
|
+
message: "SEO graph must contain an @graph array."
|
|
383
|
+
}]
|
|
384
|
+
};
|
|
385
|
+
const ids = /* @__PURE__ */ new Set();
|
|
386
|
+
for (let index = 0; index < graph["@graph"].length; index += 1) {
|
|
387
|
+
const node = graph["@graph"][index];
|
|
388
|
+
if (!isNode(node)) {
|
|
389
|
+
issues.push({
|
|
390
|
+
code: "invalid-node",
|
|
391
|
+
severity: "error",
|
|
392
|
+
message: `Invalid SEO node at index ${index}.`,
|
|
393
|
+
path: `@graph[${index}]`
|
|
394
|
+
});
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const id = node["@id"];
|
|
398
|
+
if (typeof id !== "string" || !id.trim()) {
|
|
399
|
+
issues.push({
|
|
400
|
+
code: "missing-node-id",
|
|
401
|
+
severity: "error",
|
|
402
|
+
message: `SEO node at index ${index} has no valid @id.`,
|
|
403
|
+
path: `@graph[${index}].@id`
|
|
404
|
+
});
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (ids.has(id)) issues.push({
|
|
408
|
+
code: "duplicate-node-id",
|
|
409
|
+
severity: "error",
|
|
410
|
+
message: `Duplicate SEO node ID: ${id}`,
|
|
411
|
+
nodeId: id
|
|
412
|
+
});
|
|
413
|
+
ids.add(id);
|
|
414
|
+
const types = Array.isArray(node["@type"]) ? node["@type"] : [node["@type"]];
|
|
415
|
+
if (types.length === 0 || types.some((type) => typeof type !== "string" || !type.trim())) issues.push({
|
|
416
|
+
code: "invalid-node-type",
|
|
417
|
+
severity: "error",
|
|
418
|
+
message: `SEO node ${id} has an invalid @type.`,
|
|
419
|
+
nodeId: id
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
issues.push(...validateReferences(graph, options));
|
|
423
|
+
return {
|
|
424
|
+
valid: !issues.some((issue) => issue.severity === "error"),
|
|
425
|
+
issues
|
|
426
|
+
};
|
|
427
|
+
};
|
|
428
|
+
const escapeJsonForHtml = (value) => value.replace(/&/g, "\\u0026").replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
429
|
+
/**
|
|
430
|
+
* Serializes a graph into deterministic JSON-LD.
|
|
431
|
+
*
|
|
432
|
+
* scriptSafe protects the serialized content when embedded in
|
|
433
|
+
* an HTML script element.
|
|
434
|
+
*/
|
|
435
|
+
const serializeSeoGraph = (graph, options = {}) => {
|
|
436
|
+
const validation = options.validate === false ? {
|
|
437
|
+
valid: true,
|
|
438
|
+
issues: []
|
|
439
|
+
} : validateSeoGraph(graph);
|
|
440
|
+
const cleaned = cleanValue(graph);
|
|
441
|
+
if (!isObject(cleaned)) throw new Error("Unable to serialize SEO graph.");
|
|
442
|
+
const serializable = { ...cleaned };
|
|
443
|
+
if (options.includeContext === false) delete serializable["@context"];
|
|
444
|
+
const value = JSON.stringify(canonicalize(serializable), null, options.pretty ? 2 : void 0) ?? "null";
|
|
445
|
+
return {
|
|
446
|
+
value: options.scriptSafe ?? options.safe ?? true ? escapeJsonForHtml(value) : value,
|
|
447
|
+
errors: validation.issues
|
|
448
|
+
};
|
|
449
|
+
};
|
|
450
|
+
/**
|
|
451
|
+
* Returns the serialized JSON-LD string directly.
|
|
452
|
+
*/
|
|
453
|
+
const stringifySeoGraph = (graph, options = {}) => serializeSeoGraph(graph, options).value;
|
|
454
|
+
export { buildSeoGraph, createSeoGraphBuilder, createSeoNode, mergeSeoNodes, normalizeSeoId, resolveSeoReferences, serializeSeoGraph, stringifySeoGraph, validateSeoGraph };
|
|
455
|
+
|
|
456
|
+
//# sourceMappingURL=graph.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph.js","names":[],"sources":["../../src/core/graph.ts"],"sourcesContent":["import type {\n SeoGraph,\n SeoGraphBuilder,\n SeoGraphBuilderOptions,\n SeoGraphSerializationOptions,\n SeoGraphValidationOptions,\n SeoNode,\n SeoNodeOptions,\n SeoValidationIssue,\n SeoValidationResult,\n} from \"./types\";\n\nconst DEFAULT_CONTEXT = \"https://schema.org\";\n\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst isNode = (value: unknown): value is SeoNode =>\n isObject(value) &&\n (typeof value[\"@type\"] === \"string\" ||\n (Array.isArray(value[\"@type\"]) &&\n value[\"@type\"].every((item) => typeof item === \"string\")));\n\nconst normalizeType = (type: string): string => {\n const value = type.trim();\n\n if (!value) {\n throw new Error(\"SEO node type cannot be empty.\");\n }\n\n if (/^[A-Za-z][A-Za-z0-9]*$/.test(value)) {\n return `https://schema.org/${value}`;\n }\n\n return value.replace(/^http:\\/\\/schema\\.org\\//, \"https://schema.org/\");\n};\n\nconst compareStrings = (left: string, right: string): number =>\n left < right ? -1 : left > right ? 1 : 0;\n\nconst slugify = (value: string): string =>\n value\n .normalize(\"NFKD\")\n .replace(/[\\u0300-\\u036f]/g, \"\")\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 64);\n\nconst canonicalize = (value: unknown): unknown => {\n if (Array.isArray(value)) {\n return value.map(canonicalize);\n }\n\n if (!isObject(value)) {\n return value;\n }\n\n const result: Record<string, unknown> = {};\n\n const preferredOrder = [\"@context\", \"@id\", \"@type\"];\n\n const keys = Object.keys(value).sort((a, b) => {\n const ai = preferredOrder.indexOf(a);\n const bi = preferredOrder.indexOf(b);\n\n if (ai !== -1 || bi !== -1) {\n if (ai === -1) return 1;\n if (bi === -1) return -1;\n return ai - bi;\n }\n\n return compareStrings(a, b);\n });\n\n for (const key of keys) {\n result[key] = canonicalize(value[key]);\n }\n\n return result;\n};\n\nconst stableStringify = (value: unknown): string =>\n JSON.stringify(canonicalize(value));\n\nconst hashString = (value: string): string => {\n let hash = 2166136261;\n\n for (let index = 0; index < value.length; index += 1) {\n hash ^= value.charCodeAt(index);\n hash = Math.imul(hash, 16777619);\n }\n\n return (hash >>> 0).toString(36);\n};\n\nconst normalizeBaseUrl = (baseUrl?: string): string | undefined => {\n if (!baseUrl) {\n return undefined;\n }\n\n const url = new URL(baseUrl);\n\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n throw new Error(\"SEO baseUrl must use HTTP or HTTPS.\");\n }\n\n url.hash = \"\";\n\n return url.toString().replace(/\\/$/, \"\");\n};\n\n/**\n * Converts an absolute, relative or fragment identifier\n * into a canonical identifier.\n *\n * Relative identifiers require a base URL.\n */\nexport const normalizeSeoId = (id: string, baseUrl?: string): string => {\n const value = id.trim();\n\n if (!value) {\n throw new Error(\"SEO node ID cannot be empty.\");\n }\n\n if (/^[a-z][a-z\\d+.-]*:/i.test(value)) {\n try {\n const url = new URL(value);\n\n if (url.protocol === \"http:\" && url.hostname === \"schema.org\") {\n url.protocol = \"https:\";\n }\n\n return url.toString();\n } catch {\n throw new Error(`Invalid SEO node ID: ${value}`);\n }\n }\n\n const base = normalizeBaseUrl(baseUrl);\n\n if (!base) {\n if (value.startsWith(\"#\")) {\n return value;\n }\n\n throw new Error(`Relative SEO node ID \"${value}\" requires a baseUrl.`);\n }\n\n try {\n return new URL(value, `${base}/`).toString();\n } catch {\n throw new Error(`Invalid SEO node ID: ${value}`);\n }\n};\n\nconst createAutomaticId = (node: SeoNode, options: SeoNodeOptions): string => {\n const baseUrl = normalizeBaseUrl(options.baseUrl);\n\n const types = Array.isArray(node[\"@type\"]) ? node[\"@type\"] : [node[\"@type\"]];\n\n const type = types[0] ?? \"Thing\";\n const typeSlug = slugify(type.split(\"/\").pop() ?? \"thing\");\n\n const identityProperties = [\n \"url\",\n \"mainEntityOfPage\",\n \"name\",\n \"headline\",\n \"title\",\n ];\n\n let identity: string | undefined;\n\n for (const property of identityProperties) {\n const value = node[property];\n\n if (typeof value === \"string\" && value.trim()) {\n identity = value;\n break;\n }\n\n if (isObject(value) && typeof value[\"@id\"] === \"string\") {\n identity = value[\"@id\"];\n break;\n }\n }\n\n const identitySlug = identity\n ? slugify(identity)\n : hashString(stableStringify(node));\n\n const fragment = `${typeSlug}-${identitySlug || hashString(stableStringify(node))}`;\n\n return baseUrl ? `${baseUrl}/#${fragment}` : `#${fragment}`;\n};\n\n/**\n * Removes values that cannot be represented safely in JSON-LD.\n *\n * Internal Puzzle properties beginning with __puzzle are excluded.\n * User-defined Schema.org properties remain untouched.\n */\nconst cleanValue = (value: unknown): unknown => {\n if (\n value === null ||\n typeof value === \"string\" ||\n typeof value === \"boolean\"\n ) {\n return value;\n }\n\n if (typeof value === \"number\") {\n return Number.isFinite(value) ? value : null;\n }\n\n if (\n typeof value === \"undefined\" ||\n typeof value === \"function\" ||\n typeof value === \"symbol\" ||\n typeof value === \"bigint\"\n ) {\n return undefined;\n }\n\n if (Array.isArray(value)) {\n return value.map(cleanValue).filter((item) => item !== undefined);\n }\n\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n if (!isObject(value)) {\n return undefined;\n }\n\n const result: Record<string, unknown> = {};\n\n for (const [key, nestedValue] of Object.entries(value)) {\n if (key.startsWith(\"__puzzle\")) {\n continue;\n }\n\n const cleaned = cleanValue(nestedValue);\n\n if (cleaned !== undefined) {\n result[key] = cleaned;\n }\n }\n\n return result;\n};\n\nconst normalizeNodeId = (node: SeoNode, options: SeoNodeOptions): string => {\n // An explicit options.id takes precedence over the node's @id.\n const explicitId = options.id ?? node[\"@id\"];\n\n if (typeof explicitId === \"string\") {\n return normalizeSeoId(explicitId, options.baseUrl);\n }\n\n return createAutomaticId(node, options);\n};\n\n/**\n * Creates a normalized, JSON-compatible SEO node.\n *\n * Explicit IDs always override generated IDs.\n * The original input is never mutated.\n */\nexport const createSeoNode = (\n input: SeoNode,\n options: SeoNodeOptions = {},\n): SeoNode => {\n if (!isNode(input)) {\n throw new Error(\"SEO node must contain a valid @type.\");\n }\n\n const cleaned = cleanValue(input);\n\n if (!isObject(cleaned)) {\n throw new Error(\"Unable to normalize SEO node.\");\n }\n\n const type = Array.isArray(cleaned[\"@type\"])\n ? cleaned[\"@type\"].map((item) => normalizeType(String(item)))\n : normalizeType(String(cleaned[\"@type\"]));\n\n const node: SeoNode = {\n ...cleaned,\n \"@id\": normalizeNodeId(input, options),\n \"@type\": type,\n };\n\n return node;\n};\n\nconst cloneNode = (node: SeoNode): SeoNode => {\n const cloned = cleanValue(node);\n\n if (!isObject(cloned) || !isNode(cloned)) {\n throw new Error(\"Invalid SEO node.\");\n }\n\n return cloned as SeoNode;\n};\n\ninterface MergeContext {\n strategy: NonNullable<SeoGraphBuilderOptions[\"conflictStrategy\"]>;\n issues: SeoValidationIssue[];\n nodeId: string;\n}\n\nconst isEmptyValue = (value: unknown): boolean =>\n value === undefined ||\n value === null ||\n value === \"\" ||\n (Array.isArray(value) && value.length === 0);\n\nconst mergeArrays = (left: unknown[], right: unknown[]): unknown[] => {\n const values = new Map<string, unknown>();\n\n for (const value of [...left, ...right]) {\n values.set(stableStringify(value), value);\n }\n\n return [...values.entries()]\n .sort(([a], [b]) => compareStrings(a, b))\n .map(([, value]) => value);\n};\n\nconst mergeValues = (\n left: unknown,\n right: unknown,\n context: MergeContext,\n path: string,\n): unknown => {\n if (isEmptyValue(left)) return right;\n if (isEmptyValue(right)) return left;\n\n if (path === \"@type\") {\n const types = new Set([\n ...(Array.isArray(left) ? left : [left]),\n ...(Array.isArray(right) ? right : [right]),\n ]);\n\n const sorted = [...types].sort((a, b) =>\n compareStrings(String(a), String(b)),\n );\n\n return sorted.length === 1 ? sorted[0] : sorted;\n }\n\n if (stableStringify(left) === stableStringify(right)) {\n return left;\n }\n\n if (Array.isArray(left) && Array.isArray(right)) {\n return mergeArrays(left, right);\n }\n\n if (isObject(left) && isObject(right)) {\n const result: Record<string, unknown> = {\n ...left,\n };\n\n for (const [key, value] of Object.entries(right)) {\n if (!(key in result)) {\n result[key] = value;\n continue;\n }\n\n result[key] = mergeValues(\n result[key],\n value,\n context,\n path ? `${path}.${key}` : key,\n );\n }\n\n return result;\n }\n\n const severity = context.strategy === \"error\" ? \"error\" : \"warning\";\n\n context.issues.push({\n code: \"graph-property-conflict\",\n severity,\n message: `Conflicting values found at ${path}.`,\n nodeId: context.nodeId,\n property: path,\n });\n\n switch (context.strategy) {\n case \"first\":\n return left;\n\n case \"last\":\n return right;\n\n case \"error\":\n // Keep the deterministic existing value while reporting an error.\n return compareStrings(stableStringify(left), stableStringify(right)) <= 0\n ? left\n : right;\n\n case \"deterministic\":\n default:\n return compareStrings(stableStringify(left), stableStringify(right)) <= 0\n ? left\n : right;\n }\n};\n\n/**\n * Merges two nodes with the same @id.\n *\n * Complementary values are combined.\n * Arrays are deduplicated.\n * Scalar conflicts are reported.\n */\nexport const mergeSeoNodes = (\n left: SeoNode,\n right: SeoNode,\n options: {\n conflictStrategy?: SeoGraphBuilderOptions[\"conflictStrategy\"];\n } = {},\n): {\n node: SeoNode;\n issues: SeoValidationIssue[];\n} => {\n const normalizedLeft = createSeoNode(left);\n const normalizedRight = createSeoNode(right);\n\n const leftId = normalizedLeft[\"@id\"];\n const rightId = normalizedRight[\"@id\"];\n\n if (!leftId || leftId !== rightId) {\n throw new Error(\"Only SEO nodes with identical @id values can be merged.\");\n }\n\n const issues: SeoValidationIssue[] = [];\n\n const context: MergeContext = {\n strategy: options.conflictStrategy ?? \"deterministic\",\n issues,\n nodeId: String(leftId),\n };\n\n const node = mergeValues(\n cloneNode(normalizedLeft),\n cloneNode(normalizedRight),\n context,\n \"\",\n ) as SeoNode;\n\n return {\n node,\n issues,\n };\n};\n\nconst isReference = (value: unknown): value is { \"@id\": string } =>\n isObject(value) &&\n typeof value[\"@id\"] === \"string\" &&\n Object.keys(value).every((key) => key === \"@id\");\n\nconst createReference = (id: string): { \"@id\": string } => ({\n \"@id\": id,\n});\n\ninterface BuilderState {\n nodes: Map<string, SeoNode>;\n issues: SeoValidationIssue[];\n options: SeoGraphBuilderOptions;\n}\n\n/**\n * Resolves inline nodes recursively.\n *\n * Nested objects with @type become independent graph nodes.\n * Their original position is replaced by an @id reference.\n */\nconst resolveInlineValue = (value: unknown, state: BuilderState): unknown => {\n if (Array.isArray(value)) {\n return value.map((item) => resolveInlineValue(item, state));\n }\n\n if (!isObject(value)) {\n return value;\n }\n\n if (isNode(value)) {\n const nestedNode = createSeoNode(value as SeoNode, {\n baseUrl: state.options.baseUrl,\n context: state.options.context,\n });\n\n const resolved = resolveNode(nestedNode, state);\n\n addNodeToState(resolved, state);\n\n return createReference(String(resolved[\"@id\"]));\n }\n\n if (isReference(value)) {\n return {\n \"@id\": normalizeSeoId(value[\"@id\"], state.options.baseUrl),\n };\n }\n\n const result: Record<string, unknown> = {};\n\n for (const [key, nestedValue] of Object.entries(value)) {\n result[key] = resolveInlineValue(nestedValue, state);\n }\n\n return result;\n};\n\nconst resolveNode = (node: SeoNode, state: BuilderState): SeoNode => {\n const result = cloneNode(node);\n\n for (const [key, value] of Object.entries(result)) {\n if (key === \"@id\" || key === \"@type\" || key === \"@context\") {\n continue;\n }\n\n result[key] = resolveInlineValue(value, state);\n }\n\n return result;\n};\n\nconst addNodeToState = (node: SeoNode, state: BuilderState): void => {\n const id = String(node[\"@id\"]);\n\n const existing = state.nodes.get(id);\n\n if (!existing) {\n state.nodes.set(id, node);\n return;\n }\n\n const merged = mergeSeoNodes(existing, node, {\n conflictStrategy: state.options.conflictStrategy,\n });\n\n state.nodes.set(id, merged.node);\n state.issues.push(...merged.issues);\n};\n\nconst collectReferences = (value: unknown, references: Set<string>): void => {\n if (Array.isArray(value)) {\n for (const item of value) {\n collectReferences(item, references);\n }\n\n return;\n }\n\n if (!isObject(value)) {\n return;\n }\n\n if (isReference(value)) {\n references.add(value[\"@id\"]);\n return;\n }\n\n for (const nestedValue of Object.values(value)) {\n collectReferences(nestedValue, references);\n }\n};\n\nconst isLocalReference = (id: string, baseUrl?: string): boolean => {\n if (id.startsWith(\"#\")) {\n return true;\n }\n\n if (!baseUrl) {\n return false;\n }\n\n try {\n const reference = new URL(id);\n const base = new URL(baseUrl);\n\n return reference.origin === base.origin;\n } catch {\n return false;\n }\n};\n\nconst validateReferences = (\n graph: SeoGraph,\n options: SeoGraphValidationOptions = {},\n): SeoValidationIssue[] => {\n const ids = new Set(graph[\"@graph\"].map((node) => String(node[\"@id\"])));\n\n const references = new Set<string>();\n\n for (const node of graph[\"@graph\"]) {\n collectReferences(node, references);\n }\n\n const issues: SeoValidationIssue[] = [];\n\n for (const reference of references) {\n if (ids.has(reference)) {\n continue;\n }\n\n if (\n options.allowedExternalReferences !== false &&\n !isLocalReference(reference, options.baseUrl)\n ) {\n continue;\n }\n\n issues.push({\n code: \"unresolved-reference\",\n severity: options.strictReferences ? \"error\" : \"warning\",\n message: `Unable to resolve SEO reference: ${reference}`,\n nodeId: reference,\n });\n }\n\n return issues;\n};\n\n/**\n * Builds a complete JSON-LD graph from a collection of nodes.\n *\n * Each builder instance owns its own state. No global state is used.\n */\nexport const createSeoGraphBuilder = (\n options: SeoGraphBuilderOptions = {},\n): SeoGraphBuilder => {\n const state: BuilderState = {\n nodes: new Map(),\n issues: [],\n options: {\n conflictStrategy: \"deterministic\",\n strictReferences: false,\n ...options,\n },\n };\n\n const builder: SeoGraphBuilder = {\n add(input) {\n const node = createSeoNode(input, {\n baseUrl: state.options.baseUrl,\n context: state.options.context,\n });\n\n const resolved = resolveNode(node, state);\n\n addNodeToState(resolved, state);\n\n return builder;\n },\n\n addMany(nodes) {\n for (const node of nodes) {\n builder.add(node);\n }\n\n return builder;\n },\n\n build() {\n const nodes = [...state.nodes.values()].sort((a, b) => {\n const left = String(a[\"@id\"]);\n const right = String(b[\"@id\"]);\n\n return left < right ? -1 : left > right ? 1 : 0;\n });\n\n const graph: SeoGraph = {\n \"@context\": state.options.context ?? DEFAULT_CONTEXT,\n \"@graph\": nodes,\n };\n\n return graph;\n },\n\n getIssues() {\n const graph: SeoGraph = {\n \"@context\": state.options.context ?? DEFAULT_CONTEXT,\n \"@graph\": [...state.nodes.values()],\n };\n\n return [\n ...state.issues,\n ...validateReferences(graph, {\n baseUrl: state.options.baseUrl,\n strictReferences: state.options.strictReferences,\n allowedExternalReferences: true,\n }),\n ];\n },\n };\n\n return builder;\n};\n\n/**\n * Convenience function for building a graph in one operation.\n */\nexport const buildSeoGraph = (\n nodes: readonly SeoNode[],\n options: SeoGraphBuilderOptions = {},\n): SeoGraph => {\n return createSeoGraphBuilder(options).addMany(nodes).build();\n};\n\n/**\n * Normalizes and validates an existing graph.\n *\n * Inline nodes are extracted and references are normalized.\n */\nexport const resolveSeoReferences = (\n graph: SeoGraph,\n options: SeoGraphBuilderOptions = {},\n): SeoGraph => {\n return createSeoGraphBuilder({\n ...options,\n context: options.context ?? graph[\"@context\"] ?? DEFAULT_CONTEXT,\n })\n .addMany(graph[\"@graph\"])\n .build();\n};\n\n/**\n * Validates graph structure, IDs and references.\n */\nexport const validateSeoGraph = (\n graph: SeoGraph,\n options: SeoGraphValidationOptions = {},\n): SeoValidationResult => {\n const issues: SeoValidationIssue[] = [];\n\n if (!Array.isArray(graph[\"@graph\"])) {\n return {\n valid: false,\n issues: [\n {\n code: \"invalid-graph\",\n severity: \"error\",\n message: \"SEO graph must contain an @graph array.\",\n },\n ],\n };\n }\n\n const ids = new Set<string>();\n\n for (let index = 0; index < graph[\"@graph\"].length; index += 1) {\n const node = graph[\"@graph\"][index];\n\n if (!isNode(node)) {\n issues.push({\n code: \"invalid-node\",\n severity: \"error\",\n message: `Invalid SEO node at index ${index}.`,\n path: `@graph[${index}]`,\n });\n\n continue;\n }\n\n const id = node[\"@id\"];\n\n if (typeof id !== \"string\" || !id.trim()) {\n issues.push({\n code: \"missing-node-id\",\n severity: \"error\",\n message: `SEO node at index ${index} has no valid @id.`,\n path: `@graph[${index}].@id`,\n });\n\n continue;\n }\n\n if (ids.has(id)) {\n issues.push({\n code: \"duplicate-node-id\",\n severity: \"error\",\n message: `Duplicate SEO node ID: ${id}`,\n nodeId: id,\n });\n }\n\n ids.add(id);\n\n const types = Array.isArray(node[\"@type\"])\n ? node[\"@type\"]\n : [node[\"@type\"]];\n\n if (\n types.length === 0 ||\n types.some((type) => typeof type !== \"string\" || !type.trim())\n ) {\n issues.push({\n code: \"invalid-node-type\",\n severity: \"error\",\n message: `SEO node ${id} has an invalid @type.`,\n nodeId: id,\n });\n }\n }\n\n issues.push(...validateReferences(graph, options));\n\n return {\n valid: !issues.some((issue) => issue.severity === \"error\"),\n issues,\n };\n};\n\nconst escapeJsonForHtml = (value: string): string =>\n value\n .replace(/&/g, \"\\\\u0026\")\n .replace(/</g, \"\\\\u003c\")\n .replace(/>/g, \"\\\\u003e\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n\n/**\n * Serializes a graph into deterministic JSON-LD.\n *\n * scriptSafe protects the serialized content when embedded in\n * an HTML script element.\n */\nexport const serializeSeoGraph = (\n graph: SeoGraph,\n options: SeoGraphSerializationOptions = {},\n): {\n value: string;\n errors: SeoValidationIssue[];\n} => {\n const validation =\n options.validate === false\n ? { valid: true, issues: [] }\n : validateSeoGraph(graph);\n\n const cleaned = cleanValue(graph);\n\n if (!isObject(cleaned)) {\n throw new Error(\"Unable to serialize SEO graph.\");\n }\n\n const serializable: Record<string, unknown> = {\n ...cleaned,\n };\n\n if (options.includeContext === false) {\n delete serializable[\"@context\"];\n }\n\n const value =\n JSON.stringify(\n canonicalize(serializable),\n null,\n options.pretty ? 2 : undefined,\n ) ?? \"null\";\n\n const scriptSafe = options.scriptSafe ?? options.safe ?? true;\n\n return {\n value: scriptSafe ? escapeJsonForHtml(value) : value,\n errors: validation.issues,\n };\n};\n\n/**\n * Returns the serialized JSON-LD string directly.\n */\nexport const stringifySeoGraph = (\n graph: SeoGraph,\n options: SeoGraphSerializationOptions = {},\n): string => serializeSeoGraph(graph, options).value;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_graph = require("./graph.cjs");
|
|
3
|
+
exports.buildSeoGraph = require_graph.buildSeoGraph;
|
|
4
|
+
exports.createSeoGraphBuilder = require_graph.createSeoGraphBuilder;
|
|
5
|
+
exports.createSeoNode = require_graph.createSeoNode;
|
|
6
|
+
exports.mergeSeoNodes = require_graph.mergeSeoNodes;
|
|
7
|
+
exports.normalizeSeoId = require_graph.normalizeSeoId;
|
|
8
|
+
exports.resolveSeoReferences = require_graph.resolveSeoReferences;
|
|
9
|
+
exports.serializeSeoGraph = require_graph.serializeSeoGraph;
|
|
10
|
+
exports.stringifySeoGraph = require_graph.stringifySeoGraph;
|
|
11
|
+
exports.validateSeoGraph = require_graph.validateSeoGraph;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { Brand, JsonObject, JsonPrimitive, JsonValue, SeoGraph, SeoGraphBuilder, SeoGraphBuilderOptions, SeoGraphConflictStrategy, SeoGraphOptions, SeoGraphRoot, SeoGraphSerializationOptions, SeoGraphValidationOptions, SeoLocale, SeoMetadata, SeoNode, SeoNodeId, SeoNodeOptions, SeoNodeReference, SeoNodeType, SeoReference, SeoSerializationOptions, SeoSerializationResult, SeoUrl, SeoValidationIssue, SeoValidationResult, SeoValidationSeverity } from "./types.cjs";
|
|
2
|
+
import { buildSeoGraph, createSeoGraphBuilder, createSeoNode, mergeSeoNodes, normalizeSeoId, resolveSeoReferences, serializeSeoGraph, stringifySeoGraph, validateSeoGraph } from "./graph.cjs";
|
|
3
|
+
export { type Brand, type JsonObject, type JsonPrimitive, type JsonValue, type SeoGraph, type SeoGraphBuilder, type SeoGraphBuilderOptions, type SeoGraphConflictStrategy, type SeoGraphOptions, type SeoGraphRoot, type SeoGraphSerializationOptions, type SeoGraphValidationOptions, type SeoLocale, type SeoMetadata, type SeoNode, type SeoNodeId, type SeoNodeOptions, type SeoNodeReference, type SeoNodeType, type SeoReference, type SeoSerializationOptions, type SeoSerializationResult, type SeoUrl, type SeoValidationIssue, type SeoValidationResult, type SeoValidationSeverity, buildSeoGraph, createSeoGraphBuilder, createSeoNode, mergeSeoNodes, normalizeSeoId, resolveSeoReferences, serializeSeoGraph, stringifySeoGraph, validateSeoGraph };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { Brand, JsonObject, JsonPrimitive, JsonValue, SeoGraph, SeoGraphBuilder, SeoGraphBuilderOptions, SeoGraphConflictStrategy, SeoGraphOptions, SeoGraphRoot, SeoGraphSerializationOptions, SeoGraphValidationOptions, SeoLocale, SeoMetadata, SeoNode, SeoNodeId, SeoNodeOptions, SeoNodeReference, SeoNodeType, SeoReference, SeoSerializationOptions, SeoSerializationResult, SeoUrl, SeoValidationIssue, SeoValidationResult, SeoValidationSeverity } from "./types.js";
|
|
2
|
+
import { buildSeoGraph, createSeoGraphBuilder, createSeoNode, mergeSeoNodes, normalizeSeoId, resolveSeoReferences, serializeSeoGraph, stringifySeoGraph, validateSeoGraph } from "./graph.js";
|
|
3
|
+
export { type Brand, type JsonObject, type JsonPrimitive, type JsonValue, type SeoGraph, type SeoGraphBuilder, type SeoGraphBuilderOptions, type SeoGraphConflictStrategy, type SeoGraphOptions, type SeoGraphRoot, type SeoGraphSerializationOptions, type SeoGraphValidationOptions, type SeoLocale, type SeoMetadata, type SeoNode, type SeoNodeId, type SeoNodeOptions, type SeoNodeReference, type SeoNodeType, type SeoReference, type SeoSerializationOptions, type SeoSerializationResult, type SeoUrl, type SeoValidationIssue, type SeoValidationResult, type SeoValidationSeverity, buildSeoGraph, createSeoGraphBuilder, createSeoNode, mergeSeoNodes, normalizeSeoId, resolveSeoReferences, serializeSeoGraph, stringifySeoGraph, validateSeoGraph };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { buildSeoGraph, createSeoGraphBuilder, createSeoNode, mergeSeoNodes, normalizeSeoId, resolveSeoReferences, serializeSeoGraph, stringifySeoGraph, validateSeoGraph } from "./graph.js";
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export type Brand<T, Name extends string> = T & {
|
|
2
|
+
readonly __brand: Name;
|
|
3
|
+
};
|
|
4
|
+
export type SeoUrl = Brand<string, "SeoUrl">;
|
|
5
|
+
export type SeoNodeId = Brand<string, "SeoNodeId">;
|
|
6
|
+
export type SeoLocale = Brand<string, "SeoLocale">;
|
|
7
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
8
|
+
export type JsonValue = JsonPrimitive | JsonValue[] | {
|
|
9
|
+
[key: string]: JsonValue;
|
|
10
|
+
};
|
|
11
|
+
export type JsonObject = {
|
|
12
|
+
[key: string]: JsonValue;
|
|
13
|
+
};
|
|
14
|
+
export type SeoNodeType = string | string[];
|
|
15
|
+
export interface SeoNode {
|
|
16
|
+
"@id"?: SeoNodeId | string;
|
|
17
|
+
"@type": SeoNodeType;
|
|
18
|
+
"@context"?: string | Record<string, JsonValue>;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
export interface SeoGraph {
|
|
22
|
+
"@context"?: string | Record<string, JsonValue>;
|
|
23
|
+
"@graph": SeoNode[];
|
|
24
|
+
}
|
|
25
|
+
export interface SeoGraphOptions {
|
|
26
|
+
baseUrl?: string;
|
|
27
|
+
locale?: SeoLocale | string;
|
|
28
|
+
context?: string | Record<string, JsonValue>;
|
|
29
|
+
rootIds?: string[];
|
|
30
|
+
}
|
|
31
|
+
export interface SeoNodeReference {
|
|
32
|
+
"@id": SeoNodeId | string;
|
|
33
|
+
}
|
|
34
|
+
export interface SeoMetadata {
|
|
35
|
+
title?: string;
|
|
36
|
+
description?: string;
|
|
37
|
+
canonical?: SeoUrl | string;
|
|
38
|
+
robots?: string;
|
|
39
|
+
}
|
|
40
|
+
export type SeoValidationSeverity = "error" | "warning" | "info";
|
|
41
|
+
export interface SeoValidationIssue {
|
|
42
|
+
code: string;
|
|
43
|
+
message: string;
|
|
44
|
+
severity: SeoValidationSeverity;
|
|
45
|
+
path?: string;
|
|
46
|
+
nodeId?: string;
|
|
47
|
+
property?: string;
|
|
48
|
+
}
|
|
49
|
+
export interface SeoValidationResult {
|
|
50
|
+
valid: boolean;
|
|
51
|
+
issues: SeoValidationIssue[];
|
|
52
|
+
}
|
|
53
|
+
export interface SeoSerializationOptions {
|
|
54
|
+
pretty?: boolean;
|
|
55
|
+
safe?: boolean;
|
|
56
|
+
scriptSafe?: boolean;
|
|
57
|
+
}
|
|
58
|
+
export interface SeoSerializationResult {
|
|
59
|
+
value: string;
|
|
60
|
+
errors: SeoValidationIssue[];
|
|
61
|
+
}
|
|
62
|
+
export interface SeoReference {
|
|
63
|
+
id: SeoNodeId | string;
|
|
64
|
+
}
|
|
65
|
+
export interface SeoGraphRoot {
|
|
66
|
+
id: SeoNodeId | string;
|
|
67
|
+
primary?: boolean;
|
|
68
|
+
}
|
|
69
|
+
export type SeoGraphConflictStrategy = "deterministic" | "first" | "last" | "error";
|
|
70
|
+
export interface SeoNodeOptions extends SeoGraphOptions {
|
|
71
|
+
id?: string;
|
|
72
|
+
type?: string;
|
|
73
|
+
}
|
|
74
|
+
export interface SeoGraphBuilderOptions extends SeoGraphOptions {
|
|
75
|
+
conflictStrategy?: SeoGraphConflictStrategy;
|
|
76
|
+
strictReferences?: boolean;
|
|
77
|
+
}
|
|
78
|
+
export interface SeoGraphBuilder {
|
|
79
|
+
add(node: SeoNode): this;
|
|
80
|
+
addMany(nodes: readonly SeoNode[]): this;
|
|
81
|
+
build(): SeoGraph;
|
|
82
|
+
getIssues(): readonly SeoValidationIssue[];
|
|
83
|
+
}
|
|
84
|
+
export interface SeoGraphValidationOptions {
|
|
85
|
+
baseUrl?: string;
|
|
86
|
+
strictReferences?: boolean;
|
|
87
|
+
allowedExternalReferences?: boolean;
|
|
88
|
+
}
|
|
89
|
+
export interface SeoGraphSerializationOptions extends SeoSerializationOptions {
|
|
90
|
+
includeContext?: boolean;
|
|
91
|
+
validate?: boolean;
|
|
92
|
+
}
|