@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,131 @@
|
|
|
1
|
+
// src/schema/validate.ts
|
|
2
|
+
const normalizeType = (type) => type.replace(/^http:\/\/schema\.org\//, "https://schema.org/");
|
|
3
|
+
const getNodeTypes = (node) => {
|
|
4
|
+
const value = node["@type"];
|
|
5
|
+
return (Array.isArray(value) ? value : [value]).filter((type) => typeof type === "string" && type.trim().length > 0);
|
|
6
|
+
};
|
|
7
|
+
const getExpectedTypes = (property) => {
|
|
8
|
+
return (property.expectedTypes ?? property.rangeTypes ?? (Array.isArray(property.type) ? property.type : [property.type])).map(normalizeType);
|
|
9
|
+
};
|
|
10
|
+
const getLocalTypeName = (type) => normalizeType(type).split(/[\/#]/).at(-1) ?? type;
|
|
11
|
+
const matchesPrimitiveType = (value, expectedType) => {
|
|
12
|
+
const type = getLocalTypeName(expectedType);
|
|
13
|
+
switch (type) {
|
|
14
|
+
case "Text":
|
|
15
|
+
case "URL":
|
|
16
|
+
case "CssSelector":
|
|
17
|
+
case "XPath":
|
|
18
|
+
case "Date":
|
|
19
|
+
case "DateTime":
|
|
20
|
+
case "Time":
|
|
21
|
+
case "Duration":
|
|
22
|
+
case "Number":
|
|
23
|
+
case "Integer":
|
|
24
|
+
case "Float":
|
|
25
|
+
case "Boolean": break;
|
|
26
|
+
default: return false;
|
|
27
|
+
}
|
|
28
|
+
switch (type) {
|
|
29
|
+
case "Text":
|
|
30
|
+
case "URL":
|
|
31
|
+
case "CssSelector":
|
|
32
|
+
case "XPath":
|
|
33
|
+
case "Date":
|
|
34
|
+
case "DateTime":
|
|
35
|
+
case "Time":
|
|
36
|
+
case "Duration": return typeof value === "string";
|
|
37
|
+
case "Number":
|
|
38
|
+
case "Integer":
|
|
39
|
+
case "Float": return typeof value === "number" && Number.isFinite(value) && (type !== "Integer" || Number.isInteger(value));
|
|
40
|
+
case "Boolean": return typeof value === "boolean";
|
|
41
|
+
default: return false;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const matchesExpectedType = (value, expectedTypes, registry) => {
|
|
45
|
+
if (value === null || value === void 0) return true;
|
|
46
|
+
if (Array.isArray(value)) return value.every((item) => matchesExpectedType(item, expectedTypes, registry));
|
|
47
|
+
if (typeof value !== "object") return expectedTypes.some((expectedType) => matchesPrimitiveType(value, expectedType));
|
|
48
|
+
const objectValue = value;
|
|
49
|
+
const actualTypes = getNodeTypes(objectValue);
|
|
50
|
+
if (actualTypes.length === 0) return typeof objectValue["@id"] === "string";
|
|
51
|
+
return actualTypes.some((actualType) => expectedTypes.some((expectedType) => {
|
|
52
|
+
try {
|
|
53
|
+
return registry.isTypeOf(actualType, expectedType);
|
|
54
|
+
} catch {
|
|
55
|
+
return normalizeType(actualType) === normalizeType(expectedType);
|
|
56
|
+
}
|
|
57
|
+
}));
|
|
58
|
+
};
|
|
59
|
+
const createIssue = (code, message, severity, property) => ({
|
|
60
|
+
code,
|
|
61
|
+
message,
|
|
62
|
+
severity,
|
|
63
|
+
...property ? {
|
|
64
|
+
property,
|
|
65
|
+
path: `$.${property}`
|
|
66
|
+
} : {}
|
|
67
|
+
});
|
|
68
|
+
/**
|
|
69
|
+
* Validates a Schema.org node against a schema registry.
|
|
70
|
+
*
|
|
71
|
+
* Defaults:
|
|
72
|
+
* - Unknown properties are allowed in non-strict mode.
|
|
73
|
+
* - Unknown properties are rejected in strict mode.
|
|
74
|
+
* - Required properties are not checked unless explicitly enabled.
|
|
75
|
+
*
|
|
76
|
+
* The function is deliberately non-mutating. It does not normalize or modify
|
|
77
|
+
* the supplied node.
|
|
78
|
+
*/
|
|
79
|
+
const validateSchemaNode = (node, registry, options = {}) => {
|
|
80
|
+
const strict = options.strict ?? false;
|
|
81
|
+
const allowUnknownProperties = options.allowUnknownProperties ?? !strict;
|
|
82
|
+
const validateRequiredProperties = options.validateRequiredProperties ?? false;
|
|
83
|
+
const issues = [];
|
|
84
|
+
const schemaNode = node;
|
|
85
|
+
const nodeTypes = getNodeTypes(schemaNode);
|
|
86
|
+
if (nodeTypes.length === 0) {
|
|
87
|
+
issues.push(createIssue("missing-type", "The Schema.org node must have a non-empty @type.", "error"));
|
|
88
|
+
return {
|
|
89
|
+
valid: false,
|
|
90
|
+
issues
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const knownTypes = nodeTypes.filter((type) => registry.has(type));
|
|
94
|
+
for (const type of nodeTypes) {
|
|
95
|
+
if (registry.has(type)) continue;
|
|
96
|
+
issues.push(createIssue("unknown-type", `Unknown Schema.org type: ${type}.`, strict ? "error" : "warning"));
|
|
97
|
+
}
|
|
98
|
+
if (knownTypes.length === 0) return {
|
|
99
|
+
valid: !issues.some((issue) => issue.severity === "error"),
|
|
100
|
+
issues
|
|
101
|
+
};
|
|
102
|
+
const allowedProperties = /* @__PURE__ */ new Map();
|
|
103
|
+
for (const type of knownTypes) for (const property of registry.getProperties(type)) if (!allowedProperties.has(property.name)) allowedProperties.set(property.name, property);
|
|
104
|
+
const suppliedProperties = new Set(Object.keys(schemaNode).filter((key) => !key.startsWith("@")));
|
|
105
|
+
for (const propertyName of suppliedProperties) {
|
|
106
|
+
const definition = allowedProperties.get(propertyName);
|
|
107
|
+
if (!definition) {
|
|
108
|
+
if (!allowUnknownProperties) issues.push(createIssue("unknown-property", `Property "${propertyName}" is not defined for ${nodeTypes.join(", ")}.`, "error", propertyName));
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const value = schemaNode[propertyName];
|
|
112
|
+
if (value === void 0 || value === null) continue;
|
|
113
|
+
if (definition.multiple === false && Array.isArray(value) && value.length > 1) {
|
|
114
|
+
issues.push(createIssue("property-not-multiple", `Property "${propertyName}" does not accept multiple values.`, "error", propertyName));
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const expectedTypes = getExpectedTypes(definition);
|
|
118
|
+
if (expectedTypes.length > 0 && !matchesExpectedType(value, expectedTypes, registry)) issues.push(createIssue("invalid-property-value", `Property "${propertyName}" expects ${expectedTypes.join(" or ")}.`, "error", propertyName));
|
|
119
|
+
}
|
|
120
|
+
if (validateRequiredProperties) for (const [propertyName, definition] of allowedProperties) {
|
|
121
|
+
if (!definition.required) continue;
|
|
122
|
+
if (!Object.prototype.hasOwnProperty.call(schemaNode, propertyName) || schemaNode[propertyName] === void 0 || schemaNode[propertyName] === null) issues.push(createIssue("missing-required-property", `Required property "${propertyName}" is missing.`, "error", propertyName));
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
valid: !issues.some((issue) => issue.severity === "error"),
|
|
126
|
+
issues
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
exports.validateSchemaNode = validateSchemaNode;
|
|
130
|
+
|
|
131
|
+
//# sourceMappingURL=validate.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.cjs","names":[],"sources":["../../src/schema/validate.ts"],"sourcesContent":["import type {\n JsonObject,\n SeoValidationIssue,\n SeoValidationResult,\n} from \"../core/types\";\nimport type {\n SchemaPropertyDefinition,\n SchemaRegistry,\n SchemaValidationOptions,\n} from \"./types\";\n\ntype SchemaNode = Record<string, unknown>;\n\nconst normalizeType = (type: string): string =>\n type.replace(/^http:\\/\\/schema\\.org\\//, \"https://schema.org/\");\n\nconst getNodeTypes = (node: SchemaNode): string[] => {\n const value = node[\"@type\"];\n const types = Array.isArray(value) ? value : [value];\n\n return types.filter(\n (type): type is string =>\n typeof type === \"string\" && type.trim().length > 0,\n );\n};\n\nconst getExpectedTypes = (property: SchemaPropertyDefinition): string[] => {\n const types =\n property.expectedTypes ??\n property.rangeTypes ??\n (Array.isArray(property.type) ? property.type : [property.type]);\n\n return types.map(normalizeType);\n};\n\nconst getLocalTypeName = (type: string): string =>\n normalizeType(type).split(/[\\/#]/).at(-1) ?? type;\n\nconst matchesPrimitiveType = (\n value: unknown,\n expectedType: string,\n): boolean => {\n const type = getLocalTypeName(expectedType);\n\n switch (type) {\n case \"Text\":\n case \"URL\":\n case \"CssSelector\":\n case \"XPath\":\n case \"Date\":\n case \"DateTime\":\n case \"Time\":\n case \"Duration\":\n case \"Number\":\n case \"Integer\":\n case \"Float\":\n case \"Boolean\":\n break;\n default:\n return false;\n }\n\n switch (type) {\n case \"Text\":\n case \"URL\":\n case \"CssSelector\":\n case \"XPath\":\n case \"Date\":\n case \"DateTime\":\n case \"Time\":\n case \"Duration\":\n return typeof value === \"string\";\n\n case \"Number\":\n case \"Integer\":\n case \"Float\":\n return (\n typeof value === \"number\" &&\n Number.isFinite(value) &&\n (type !== \"Integer\" || Number.isInteger(value))\n );\n\n case \"Boolean\":\n return typeof value === \"boolean\";\n\n default:\n return false;\n }\n};\n\nconst matchesExpectedType = (\n value: unknown,\n expectedTypes: readonly string[],\n registry: SchemaRegistry,\n): boolean => {\n if (value === null || value === undefined) {\n return true;\n }\n\n if (Array.isArray(value)) {\n return value.every((item) =>\n matchesExpectedType(item, expectedTypes, registry),\n );\n }\n\n if (typeof value !== \"object\") {\n return expectedTypes.some((expectedType) =>\n matchesPrimitiveType(value, expectedType),\n );\n }\n\n const objectValue = value as SchemaNode;\n const actualTypes = getNodeTypes(objectValue);\n\n // A reference with only @id does not provide enough information to\n // validate its concrete type. It is therefore accepted.\n if (actualTypes.length === 0) {\n return typeof objectValue[\"@id\"] === \"string\";\n }\n\n return actualTypes.some((actualType) =>\n expectedTypes.some((expectedType) => {\n try {\n return registry.isTypeOf(actualType, expectedType);\n } catch {\n return normalizeType(actualType) === normalizeType(expectedType);\n }\n }),\n );\n};\n\nconst createIssue = (\n code: string,\n message: string,\n severity: SeoValidationIssue[\"severity\"],\n property?: string,\n): SeoValidationIssue => ({\n code,\n message,\n severity,\n ...(property ? { property, path: `$.${property}` } : {}),\n});\n\n/**\n * Validates a Schema.org node against a schema registry.\n *\n * Defaults:\n * - Unknown properties are allowed in non-strict mode.\n * - Unknown properties are rejected in strict mode.\n * - Required properties are not checked unless explicitly enabled.\n *\n * The function is deliberately non-mutating. It does not normalize or modify\n * the supplied node.\n */\nexport const validateSchemaNode = (\n node: JsonObject | SchemaNode,\n registry: SchemaRegistry,\n options: SchemaValidationOptions = {},\n): SeoValidationResult => {\n const strict = options.strict ?? false;\n const allowUnknownProperties = options.allowUnknownProperties ?? !strict;\n const validateRequiredProperties =\n options.validateRequiredProperties ?? false;\n\n const issues: SeoValidationIssue[] = [];\n const schemaNode = node as SchemaNode;\n const nodeTypes = getNodeTypes(schemaNode);\n\n if (nodeTypes.length === 0) {\n issues.push(\n createIssue(\n \"missing-type\",\n \"The Schema.org node must have a non-empty @type.\",\n \"error\",\n ),\n );\n\n return {\n valid: false,\n issues,\n };\n }\n\n const knownTypes = nodeTypes.filter((type) => registry.has(type));\n\n for (const type of nodeTypes) {\n if (registry.has(type)) {\n continue;\n }\n\n issues.push(\n createIssue(\n \"unknown-type\",\n `Unknown Schema.org type: ${type}.`,\n strict ? \"error\" : \"warning\",\n ),\n );\n }\n\n // Without a known type, property validation cannot be performed reliably.\n if (knownTypes.length === 0) {\n return {\n valid: !issues.some((issue) => issue.severity === \"error\"),\n issues,\n };\n }\n\n const allowedProperties = new Map<string, SchemaPropertyDefinition>();\n\n for (const type of knownTypes) {\n for (const property of registry.getProperties(type)) {\n // Preserve the first definition encountered. Registry inheritance\n // already resolves parent/child property overrides.\n if (!allowedProperties.has(property.name)) {\n allowedProperties.set(property.name, property);\n }\n }\n }\n\n const suppliedProperties = new Set(\n Object.keys(schemaNode).filter((key) => !key.startsWith(\"@\")),\n );\n\n for (const propertyName of suppliedProperties) {\n const definition = allowedProperties.get(propertyName);\n\n if (!definition) {\n if (!allowUnknownProperties) {\n issues.push(\n createIssue(\n \"unknown-property\",\n `Property \"${propertyName}\" is not defined for ${nodeTypes.join(\", \")}.`,\n \"error\",\n propertyName,\n ),\n );\n }\n\n continue;\n }\n\n const value = schemaNode[propertyName];\n\n if (value === undefined || value === null) {\n continue;\n }\n\n if (\n definition.multiple === false &&\n Array.isArray(value) &&\n value.length > 1\n ) {\n issues.push(\n createIssue(\n \"property-not-multiple\",\n `Property \"${propertyName}\" does not accept multiple values.`,\n \"error\",\n propertyName,\n ),\n );\n\n continue;\n }\n\n const expectedTypes = getExpectedTypes(definition);\n\n if (\n expectedTypes.length > 0 &&\n !matchesExpectedType(value, expectedTypes, registry)\n ) {\n issues.push(\n createIssue(\n \"invalid-property-value\",\n `Property \"${propertyName}\" expects ${expectedTypes.join(\" or \")}.`,\n \"error\",\n propertyName,\n ),\n );\n }\n }\n\n if (validateRequiredProperties) {\n for (const [propertyName, definition] of allowedProperties) {\n if (!definition.required) {\n continue;\n }\n\n if (\n !Object.prototype.hasOwnProperty.call(schemaNode, propertyName) ||\n schemaNode[propertyName] === undefined ||\n schemaNode[propertyName] === null\n ) {\n issues.push(\n createIssue(\n \"missing-required-property\",\n `Required property \"${propertyName}\" is missing.`,\n \"error\",\n propertyName,\n ),\n );\n }\n }\n }\n\n return {\n valid: !issues.some((issue) => issue.severity === \"error\"),\n issues,\n };\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { JsonObject, SeoValidationResult } from "../core/types.cjs";
|
|
2
|
+
import { SchemaRegistry, SchemaValidationOptions } from "./types.cjs";
|
|
3
|
+
type SchemaNode = Record<string, unknown>;
|
|
4
|
+
/**
|
|
5
|
+
* Validates a Schema.org node against a schema registry.
|
|
6
|
+
*
|
|
7
|
+
* Defaults:
|
|
8
|
+
* - Unknown properties are allowed in non-strict mode.
|
|
9
|
+
* - Unknown properties are rejected in strict mode.
|
|
10
|
+
* - Required properties are not checked unless explicitly enabled.
|
|
11
|
+
*
|
|
12
|
+
* The function is deliberately non-mutating. It does not normalize or modify
|
|
13
|
+
* the supplied node.
|
|
14
|
+
*/
|
|
15
|
+
export declare const validateSchemaNode: (node: JsonObject | SchemaNode, registry: SchemaRegistry, options?: SchemaValidationOptions) => SeoValidationResult;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { JsonObject, SeoValidationResult } from "../core/types.js";
|
|
2
|
+
import { SchemaRegistry, SchemaValidationOptions } from "./types.js";
|
|
3
|
+
type SchemaNode = Record<string, unknown>;
|
|
4
|
+
/**
|
|
5
|
+
* Validates a Schema.org node against a schema registry.
|
|
6
|
+
*
|
|
7
|
+
* Defaults:
|
|
8
|
+
* - Unknown properties are allowed in non-strict mode.
|
|
9
|
+
* - Unknown properties are rejected in strict mode.
|
|
10
|
+
* - Required properties are not checked unless explicitly enabled.
|
|
11
|
+
*
|
|
12
|
+
* The function is deliberately non-mutating. It does not normalize or modify
|
|
13
|
+
* the supplied node.
|
|
14
|
+
*/
|
|
15
|
+
export declare const validateSchemaNode: (node: JsonObject | SchemaNode, registry: SchemaRegistry, options?: SchemaValidationOptions) => SeoValidationResult;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// src/schema/validate.ts
|
|
2
|
+
const normalizeType = (type) => type.replace(/^http:\/\/schema\.org\//, "https://schema.org/");
|
|
3
|
+
const getNodeTypes = (node) => {
|
|
4
|
+
const value = node["@type"];
|
|
5
|
+
return (Array.isArray(value) ? value : [value]).filter((type) => typeof type === "string" && type.trim().length > 0);
|
|
6
|
+
};
|
|
7
|
+
const getExpectedTypes = (property) => {
|
|
8
|
+
return (property.expectedTypes ?? property.rangeTypes ?? (Array.isArray(property.type) ? property.type : [property.type])).map(normalizeType);
|
|
9
|
+
};
|
|
10
|
+
const getLocalTypeName = (type) => normalizeType(type).split(/[\/#]/).at(-1) ?? type;
|
|
11
|
+
const matchesPrimitiveType = (value, expectedType) => {
|
|
12
|
+
const type = getLocalTypeName(expectedType);
|
|
13
|
+
switch (type) {
|
|
14
|
+
case "Text":
|
|
15
|
+
case "URL":
|
|
16
|
+
case "CssSelector":
|
|
17
|
+
case "XPath":
|
|
18
|
+
case "Date":
|
|
19
|
+
case "DateTime":
|
|
20
|
+
case "Time":
|
|
21
|
+
case "Duration":
|
|
22
|
+
case "Number":
|
|
23
|
+
case "Integer":
|
|
24
|
+
case "Float":
|
|
25
|
+
case "Boolean": break;
|
|
26
|
+
default: return false;
|
|
27
|
+
}
|
|
28
|
+
switch (type) {
|
|
29
|
+
case "Text":
|
|
30
|
+
case "URL":
|
|
31
|
+
case "CssSelector":
|
|
32
|
+
case "XPath":
|
|
33
|
+
case "Date":
|
|
34
|
+
case "DateTime":
|
|
35
|
+
case "Time":
|
|
36
|
+
case "Duration": return typeof value === "string";
|
|
37
|
+
case "Number":
|
|
38
|
+
case "Integer":
|
|
39
|
+
case "Float": return typeof value === "number" && Number.isFinite(value) && (type !== "Integer" || Number.isInteger(value));
|
|
40
|
+
case "Boolean": return typeof value === "boolean";
|
|
41
|
+
default: return false;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const matchesExpectedType = (value, expectedTypes, registry) => {
|
|
45
|
+
if (value === null || value === void 0) return true;
|
|
46
|
+
if (Array.isArray(value)) return value.every((item) => matchesExpectedType(item, expectedTypes, registry));
|
|
47
|
+
if (typeof value !== "object") return expectedTypes.some((expectedType) => matchesPrimitiveType(value, expectedType));
|
|
48
|
+
const objectValue = value;
|
|
49
|
+
const actualTypes = getNodeTypes(objectValue);
|
|
50
|
+
if (actualTypes.length === 0) return typeof objectValue["@id"] === "string";
|
|
51
|
+
return actualTypes.some((actualType) => expectedTypes.some((expectedType) => {
|
|
52
|
+
try {
|
|
53
|
+
return registry.isTypeOf(actualType, expectedType);
|
|
54
|
+
} catch {
|
|
55
|
+
return normalizeType(actualType) === normalizeType(expectedType);
|
|
56
|
+
}
|
|
57
|
+
}));
|
|
58
|
+
};
|
|
59
|
+
const createIssue = (code, message, severity, property) => ({
|
|
60
|
+
code,
|
|
61
|
+
message,
|
|
62
|
+
severity,
|
|
63
|
+
...property ? {
|
|
64
|
+
property,
|
|
65
|
+
path: `$.${property}`
|
|
66
|
+
} : {}
|
|
67
|
+
});
|
|
68
|
+
/**
|
|
69
|
+
* Validates a Schema.org node against a schema registry.
|
|
70
|
+
*
|
|
71
|
+
* Defaults:
|
|
72
|
+
* - Unknown properties are allowed in non-strict mode.
|
|
73
|
+
* - Unknown properties are rejected in strict mode.
|
|
74
|
+
* - Required properties are not checked unless explicitly enabled.
|
|
75
|
+
*
|
|
76
|
+
* The function is deliberately non-mutating. It does not normalize or modify
|
|
77
|
+
* the supplied node.
|
|
78
|
+
*/
|
|
79
|
+
const validateSchemaNode = (node, registry, options = {}) => {
|
|
80
|
+
const strict = options.strict ?? false;
|
|
81
|
+
const allowUnknownProperties = options.allowUnknownProperties ?? !strict;
|
|
82
|
+
const validateRequiredProperties = options.validateRequiredProperties ?? false;
|
|
83
|
+
const issues = [];
|
|
84
|
+
const schemaNode = node;
|
|
85
|
+
const nodeTypes = getNodeTypes(schemaNode);
|
|
86
|
+
if (nodeTypes.length === 0) {
|
|
87
|
+
issues.push(createIssue("missing-type", "The Schema.org node must have a non-empty @type.", "error"));
|
|
88
|
+
return {
|
|
89
|
+
valid: false,
|
|
90
|
+
issues
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const knownTypes = nodeTypes.filter((type) => registry.has(type));
|
|
94
|
+
for (const type of nodeTypes) {
|
|
95
|
+
if (registry.has(type)) continue;
|
|
96
|
+
issues.push(createIssue("unknown-type", `Unknown Schema.org type: ${type}.`, strict ? "error" : "warning"));
|
|
97
|
+
}
|
|
98
|
+
if (knownTypes.length === 0) return {
|
|
99
|
+
valid: !issues.some((issue) => issue.severity === "error"),
|
|
100
|
+
issues
|
|
101
|
+
};
|
|
102
|
+
const allowedProperties = /* @__PURE__ */ new Map();
|
|
103
|
+
for (const type of knownTypes) for (const property of registry.getProperties(type)) if (!allowedProperties.has(property.name)) allowedProperties.set(property.name, property);
|
|
104
|
+
const suppliedProperties = new Set(Object.keys(schemaNode).filter((key) => !key.startsWith("@")));
|
|
105
|
+
for (const propertyName of suppliedProperties) {
|
|
106
|
+
const definition = allowedProperties.get(propertyName);
|
|
107
|
+
if (!definition) {
|
|
108
|
+
if (!allowUnknownProperties) issues.push(createIssue("unknown-property", `Property "${propertyName}" is not defined for ${nodeTypes.join(", ")}.`, "error", propertyName));
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const value = schemaNode[propertyName];
|
|
112
|
+
if (value === void 0 || value === null) continue;
|
|
113
|
+
if (definition.multiple === false && Array.isArray(value) && value.length > 1) {
|
|
114
|
+
issues.push(createIssue("property-not-multiple", `Property "${propertyName}" does not accept multiple values.`, "error", propertyName));
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const expectedTypes = getExpectedTypes(definition);
|
|
118
|
+
if (expectedTypes.length > 0 && !matchesExpectedType(value, expectedTypes, registry)) issues.push(createIssue("invalid-property-value", `Property "${propertyName}" expects ${expectedTypes.join(" or ")}.`, "error", propertyName));
|
|
119
|
+
}
|
|
120
|
+
if (validateRequiredProperties) for (const [propertyName, definition] of allowedProperties) {
|
|
121
|
+
if (!definition.required) continue;
|
|
122
|
+
if (!Object.prototype.hasOwnProperty.call(schemaNode, propertyName) || schemaNode[propertyName] === void 0 || schemaNode[propertyName] === null) issues.push(createIssue("missing-required-property", `Required property "${propertyName}" is missing.`, "error", propertyName));
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
valid: !issues.some((issue) => issue.severity === "error"),
|
|
126
|
+
issues
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
export { validateSchemaNode };
|
|
130
|
+
|
|
131
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","names":[],"sources":["../../src/schema/validate.ts"],"sourcesContent":["import type {\n JsonObject,\n SeoValidationIssue,\n SeoValidationResult,\n} from \"../core/types\";\nimport type {\n SchemaPropertyDefinition,\n SchemaRegistry,\n SchemaValidationOptions,\n} from \"./types\";\n\ntype SchemaNode = Record<string, unknown>;\n\nconst normalizeType = (type: string): string =>\n type.replace(/^http:\\/\\/schema\\.org\\//, \"https://schema.org/\");\n\nconst getNodeTypes = (node: SchemaNode): string[] => {\n const value = node[\"@type\"];\n const types = Array.isArray(value) ? value : [value];\n\n return types.filter(\n (type): type is string =>\n typeof type === \"string\" && type.trim().length > 0,\n );\n};\n\nconst getExpectedTypes = (property: SchemaPropertyDefinition): string[] => {\n const types =\n property.expectedTypes ??\n property.rangeTypes ??\n (Array.isArray(property.type) ? property.type : [property.type]);\n\n return types.map(normalizeType);\n};\n\nconst getLocalTypeName = (type: string): string =>\n normalizeType(type).split(/[\\/#]/).at(-1) ?? type;\n\nconst matchesPrimitiveType = (\n value: unknown,\n expectedType: string,\n): boolean => {\n const type = getLocalTypeName(expectedType);\n\n switch (type) {\n case \"Text\":\n case \"URL\":\n case \"CssSelector\":\n case \"XPath\":\n case \"Date\":\n case \"DateTime\":\n case \"Time\":\n case \"Duration\":\n case \"Number\":\n case \"Integer\":\n case \"Float\":\n case \"Boolean\":\n break;\n default:\n return false;\n }\n\n switch (type) {\n case \"Text\":\n case \"URL\":\n case \"CssSelector\":\n case \"XPath\":\n case \"Date\":\n case \"DateTime\":\n case \"Time\":\n case \"Duration\":\n return typeof value === \"string\";\n\n case \"Number\":\n case \"Integer\":\n case \"Float\":\n return (\n typeof value === \"number\" &&\n Number.isFinite(value) &&\n (type !== \"Integer\" || Number.isInteger(value))\n );\n\n case \"Boolean\":\n return typeof value === \"boolean\";\n\n default:\n return false;\n }\n};\n\nconst matchesExpectedType = (\n value: unknown,\n expectedTypes: readonly string[],\n registry: SchemaRegistry,\n): boolean => {\n if (value === null || value === undefined) {\n return true;\n }\n\n if (Array.isArray(value)) {\n return value.every((item) =>\n matchesExpectedType(item, expectedTypes, registry),\n );\n }\n\n if (typeof value !== \"object\") {\n return expectedTypes.some((expectedType) =>\n matchesPrimitiveType(value, expectedType),\n );\n }\n\n const objectValue = value as SchemaNode;\n const actualTypes = getNodeTypes(objectValue);\n\n // A reference with only @id does not provide enough information to\n // validate its concrete type. It is therefore accepted.\n if (actualTypes.length === 0) {\n return typeof objectValue[\"@id\"] === \"string\";\n }\n\n return actualTypes.some((actualType) =>\n expectedTypes.some((expectedType) => {\n try {\n return registry.isTypeOf(actualType, expectedType);\n } catch {\n return normalizeType(actualType) === normalizeType(expectedType);\n }\n }),\n );\n};\n\nconst createIssue = (\n code: string,\n message: string,\n severity: SeoValidationIssue[\"severity\"],\n property?: string,\n): SeoValidationIssue => ({\n code,\n message,\n severity,\n ...(property ? { property, path: `$.${property}` } : {}),\n});\n\n/**\n * Validates a Schema.org node against a schema registry.\n *\n * Defaults:\n * - Unknown properties are allowed in non-strict mode.\n * - Unknown properties are rejected in strict mode.\n * - Required properties are not checked unless explicitly enabled.\n *\n * The function is deliberately non-mutating. It does not normalize or modify\n * the supplied node.\n */\nexport const validateSchemaNode = (\n node: JsonObject | SchemaNode,\n registry: SchemaRegistry,\n options: SchemaValidationOptions = {},\n): SeoValidationResult => {\n const strict = options.strict ?? false;\n const allowUnknownProperties = options.allowUnknownProperties ?? !strict;\n const validateRequiredProperties =\n options.validateRequiredProperties ?? false;\n\n const issues: SeoValidationIssue[] = [];\n const schemaNode = node as SchemaNode;\n const nodeTypes = getNodeTypes(schemaNode);\n\n if (nodeTypes.length === 0) {\n issues.push(\n createIssue(\n \"missing-type\",\n \"The Schema.org node must have a non-empty @type.\",\n \"error\",\n ),\n );\n\n return {\n valid: false,\n issues,\n };\n }\n\n const knownTypes = nodeTypes.filter((type) => registry.has(type));\n\n for (const type of nodeTypes) {\n if (registry.has(type)) {\n continue;\n }\n\n issues.push(\n createIssue(\n \"unknown-type\",\n `Unknown Schema.org type: ${type}.`,\n strict ? \"error\" : \"warning\",\n ),\n );\n }\n\n // Without a known type, property validation cannot be performed reliably.\n if (knownTypes.length === 0) {\n return {\n valid: !issues.some((issue) => issue.severity === \"error\"),\n issues,\n };\n }\n\n const allowedProperties = new Map<string, SchemaPropertyDefinition>();\n\n for (const type of knownTypes) {\n for (const property of registry.getProperties(type)) {\n // Preserve the first definition encountered. Registry inheritance\n // already resolves parent/child property overrides.\n if (!allowedProperties.has(property.name)) {\n allowedProperties.set(property.name, property);\n }\n }\n }\n\n const suppliedProperties = new Set(\n Object.keys(schemaNode).filter((key) => !key.startsWith(\"@\")),\n );\n\n for (const propertyName of suppliedProperties) {\n const definition = allowedProperties.get(propertyName);\n\n if (!definition) {\n if (!allowUnknownProperties) {\n issues.push(\n createIssue(\n \"unknown-property\",\n `Property \"${propertyName}\" is not defined for ${nodeTypes.join(\", \")}.`,\n \"error\",\n propertyName,\n ),\n );\n }\n\n continue;\n }\n\n const value = schemaNode[propertyName];\n\n if (value === undefined || value === null) {\n continue;\n }\n\n if (\n definition.multiple === false &&\n Array.isArray(value) &&\n value.length > 1\n ) {\n issues.push(\n createIssue(\n \"property-not-multiple\",\n `Property \"${propertyName}\" does not accept multiple values.`,\n \"error\",\n propertyName,\n ),\n );\n\n continue;\n }\n\n const expectedTypes = getExpectedTypes(definition);\n\n if (\n expectedTypes.length > 0 &&\n !matchesExpectedType(value, expectedTypes, registry)\n ) {\n issues.push(\n createIssue(\n \"invalid-property-value\",\n `Property \"${propertyName}\" expects ${expectedTypes.join(\" or \")}.`,\n \"error\",\n propertyName,\n ),\n );\n }\n }\n\n if (validateRequiredProperties) {\n for (const [propertyName, definition] of allowedProperties) {\n if (!definition.required) {\n continue;\n }\n\n if (\n !Object.prototype.hasOwnProperty.call(schemaNode, propertyName) ||\n schemaNode[propertyName] === undefined ||\n schemaNode[propertyName] === null\n ) {\n issues.push(\n createIssue(\n \"missing-required-property\",\n `Required property \"${propertyName}\" is missing.`,\n \"error\",\n propertyName,\n ),\n );\n }\n }\n }\n\n return {\n valid: !issues.some((issue) => issue.severity === \"error\"),\n issues,\n };\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const require_generate = require("../sitemap/generate.cjs");
|
|
2
|
+
const require_generate$1 = require("../robots/generate.cjs");
|
|
3
|
+
// src/server/handlers.ts
|
|
4
|
+
const DEFAULT_CACHE_CONTROL = "public, max-age=3600, s-maxage=3600";
|
|
5
|
+
const XML_CONTENT_TYPE = "application/xml; charset=utf-8";
|
|
6
|
+
const TEXT_CONTENT_TYPE = "text/plain; charset=utf-8";
|
|
7
|
+
const createTextResponse = (body, options) => ({
|
|
8
|
+
status: options.status ?? 200,
|
|
9
|
+
headers: {
|
|
10
|
+
"Content-Type": options.contentType,
|
|
11
|
+
"Cache-Control": options.cacheControl ?? DEFAULT_CACHE_CONTROL,
|
|
12
|
+
"X-Content-Type-Options": "nosniff",
|
|
13
|
+
...options.headers
|
|
14
|
+
},
|
|
15
|
+
body
|
|
16
|
+
});
|
|
17
|
+
const createNotFoundResponse = () => createTextResponse("Not found.", {
|
|
18
|
+
contentType: TEXT_CONTENT_TYPE,
|
|
19
|
+
status: 404,
|
|
20
|
+
cacheControl: "no-store"
|
|
21
|
+
});
|
|
22
|
+
const createErrorResponse = (error, fallback) => createTextResponse(error instanceof Error ? error.message : fallback, {
|
|
23
|
+
contentType: TEXT_CONTENT_TYPE,
|
|
24
|
+
status: 500,
|
|
25
|
+
cacheControl: "no-store"
|
|
26
|
+
});
|
|
27
|
+
const getRequestPathname = (request) => {
|
|
28
|
+
if (!request) return;
|
|
29
|
+
try {
|
|
30
|
+
return new URL(request.url).pathname;
|
|
31
|
+
} catch {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const normalizeRoutePath = (path) => {
|
|
36
|
+
return (path.startsWith("/") ? path : `/${path}`).replace(/\/+/g, "/");
|
|
37
|
+
};
|
|
38
|
+
const getSitemapIndexPath = (options) => {
|
|
39
|
+
if (options.path) return normalizeRoutePath(options.path);
|
|
40
|
+
return normalizeRoutePath(`/${options.sitemap.filename ?? "sitemap"}.xml`);
|
|
41
|
+
};
|
|
42
|
+
const getSitemapFilePath = (filename) => normalizeRoutePath(`/${filename}`);
|
|
43
|
+
const createSitemapHandler = (options) => {
|
|
44
|
+
return (request) => {
|
|
45
|
+
try {
|
|
46
|
+
const result = require_generate.generateSitemap(options.sitemap);
|
|
47
|
+
if (result.issues.length > 0) return createTextResponse(result.issues.map((issue) => issue.message).join("\n"), {
|
|
48
|
+
contentType: TEXT_CONTENT_TYPE,
|
|
49
|
+
status: 500,
|
|
50
|
+
cacheControl: "no-store"
|
|
51
|
+
});
|
|
52
|
+
const requestedPath = getRequestPathname(request);
|
|
53
|
+
const indexPath = getSitemapIndexPath(options);
|
|
54
|
+
if (requestedPath && requestedPath !== indexPath) {
|
|
55
|
+
const requestedFile = result.files.find((file) => getSitemapFilePath(file.filename) === requestedPath);
|
|
56
|
+
if (!requestedFile) return createNotFoundResponse();
|
|
57
|
+
return createTextResponse(requestedFile.xml, {
|
|
58
|
+
contentType: XML_CONTENT_TYPE,
|
|
59
|
+
cacheControl: options.cacheControl
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const body = result.indexXml ?? result.files[0]?.xml ?? ["<?xml version=\"1.0\" encoding=\"UTF-8\"?>", "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"></urlset>"].join("");
|
|
63
|
+
return createTextResponse(body, {
|
|
64
|
+
contentType: XML_CONTENT_TYPE,
|
|
65
|
+
cacheControl: options.cacheControl
|
|
66
|
+
});
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return createErrorResponse(error, "Sitemap generation failed.");
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
const createRobotsHandler = (options) => {
|
|
73
|
+
return (request) => {
|
|
74
|
+
const requestedPath = getRequestPathname(request);
|
|
75
|
+
const robotsPath = normalizeRoutePath(options.path ?? "/robots.txt");
|
|
76
|
+
if (requestedPath && requestedPath !== robotsPath) return createNotFoundResponse();
|
|
77
|
+
try {
|
|
78
|
+
const result = require_generate$1.generateRobotsTxt(options.robots);
|
|
79
|
+
if (result.issues.length > 0) return createTextResponse(result.issues.map((issue) => issue.message).join("\n"), {
|
|
80
|
+
contentType: TEXT_CONTENT_TYPE,
|
|
81
|
+
status: 500,
|
|
82
|
+
cacheControl: "no-store"
|
|
83
|
+
});
|
|
84
|
+
return createTextResponse(result.content, {
|
|
85
|
+
contentType: TEXT_CONTENT_TYPE,
|
|
86
|
+
cacheControl: options.cacheControl
|
|
87
|
+
});
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return createErrorResponse(error, "Robots.txt generation failed.");
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
exports.createRobotsHandler = createRobotsHandler;
|
|
94
|
+
exports.createSitemapHandler = createSitemapHandler;
|
|
95
|
+
|
|
96
|
+
//# sourceMappingURL=handlers.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handlers.cjs","names":["generateSitemap","generateRobotsTxt"],"sources":["../../src/server/handlers.ts"],"sourcesContent":["import { generateSitemap } from \"../sitemap\";\nimport { generateRobotsTxt } from \"../robots\";\n\nimport type {\n SeoTextResponse,\n SeoSitemapHandlerOptions,\n SeoRobotsHandlerOptions,\n} from \"./types\";\n\nconst DEFAULT_CACHE_CONTROL = \"public, max-age=3600, s-maxage=3600\";\n\nconst XML_CONTENT_TYPE = \"application/xml; charset=utf-8\";\nconst TEXT_CONTENT_TYPE = \"text/plain; charset=utf-8\";\n\nconst createTextResponse = (\n body: string,\n options: {\n contentType: string;\n cacheControl?: string;\n headers?: Record<string, string>;\n status?: number;\n },\n): SeoTextResponse => ({\n status: options.status ?? 200,\n headers: {\n \"Content-Type\": options.contentType,\n \"Cache-Control\": options.cacheControl ?? DEFAULT_CACHE_CONTROL,\n \"X-Content-Type-Options\": \"nosniff\",\n ...options.headers,\n },\n body,\n});\n\nconst createNotFoundResponse = (): SeoTextResponse =>\n createTextResponse(\"Not found.\", {\n contentType: TEXT_CONTENT_TYPE,\n status: 404,\n cacheControl: \"no-store\",\n });\n\nconst createErrorResponse = (\n error: unknown,\n fallback: string,\n): SeoTextResponse =>\n createTextResponse(error instanceof Error ? error.message : fallback, {\n contentType: TEXT_CONTENT_TYPE,\n status: 500,\n cacheControl: \"no-store\",\n });\n\nconst getRequestPathname = (request?: Request): string | undefined => {\n if (!request) {\n return undefined;\n }\n\n try {\n return new URL(request.url).pathname;\n } catch {\n return \"\";\n }\n};\n\nconst normalizeRoutePath = (path: string): string => {\n const normalized = path.startsWith(\"/\") ? path : `/${path}`;\n\n return normalized.replace(/\\/+/g, \"/\");\n};\n\nconst getSitemapIndexPath = (options: SeoSitemapHandlerOptions): string => {\n if (options.path) {\n return normalizeRoutePath(options.path);\n }\n\n return normalizeRoutePath(`/${options.sitemap.filename ?? \"sitemap\"}.xml`);\n};\n\nconst getSitemapFilePath = (filename: string): string =>\n normalizeRoutePath(`/${filename}`);\n\nexport const createSitemapHandler = (options: SeoSitemapHandlerOptions) => {\n return (request?: Request): SeoTextResponse => {\n try {\n const result = generateSitemap(options.sitemap);\n\n if (result.issues.length > 0) {\n return createTextResponse(\n result.issues.map((issue) => issue.message).join(\"\\n\"),\n {\n contentType: TEXT_CONTENT_TYPE,\n status: 500,\n cacheControl: \"no-store\",\n },\n );\n }\n\n const requestedPath = getRequestPathname(request);\n const indexPath = getSitemapIndexPath(options);\n\n if (requestedPath && requestedPath !== indexPath) {\n const requestedFile = result.files.find(\n (file) => getSitemapFilePath(file.filename) === requestedPath,\n );\n\n if (!requestedFile) {\n return createNotFoundResponse();\n }\n\n return createTextResponse(requestedFile.xml, {\n contentType: XML_CONTENT_TYPE,\n cacheControl: options.cacheControl,\n });\n }\n\n const body =\n result.indexXml ??\n result.files[0]?.xml ??\n [\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n '<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"></urlset>',\n ].join(\"\");\n\n return createTextResponse(body, {\n contentType: XML_CONTENT_TYPE,\n cacheControl: options.cacheControl,\n });\n } catch (error) {\n return createErrorResponse(error, \"Sitemap generation failed.\");\n }\n };\n};\n\nexport const createRobotsHandler = (options: SeoRobotsHandlerOptions) => {\n return (request?: Request): SeoTextResponse => {\n const requestedPath = getRequestPathname(request);\n const robotsPath = normalizeRoutePath(options.path ?? \"/robots.txt\");\n\n if (requestedPath && requestedPath !== robotsPath) {\n return createNotFoundResponse();\n }\n\n try {\n const result = generateRobotsTxt(options.robots);\n\n if (result.issues.length > 0) {\n return createTextResponse(\n result.issues.map((issue) => issue.message).join(\"\\n\"),\n {\n contentType: TEXT_CONTENT_TYPE,\n status: 500,\n cacheControl: \"no-store\",\n },\n );\n }\n\n return createTextResponse(result.content, {\n contentType: TEXT_CONTENT_TYPE,\n cacheControl: options.cacheControl,\n });\n } catch (error) {\n return createErrorResponse(error, \"Robots.txt generation failed.\");\n }\n };\n};\n"],"mappings":";;;AASA,MAAM,wBAAwB;AAE9B,MAAM,mBAAmB;AACzB,MAAM,oBAAoB;AAE1B,MAAM,sBACJ,MACA,aAMqB;CACrB,QAAQ,QAAQ,UAAU;CAC1B,SAAS;EACP,gBAAgB,QAAQ;EACxB,iBAAiB,QAAQ,gBAAgB;EACzC,0BAA0B;EAC1B,GAAG,QAAQ;CACb;CACA;AACF;AAEA,MAAM,+BACJ,mBAAmB,cAAc;CAC/B,aAAa;CACb,QAAQ;CACR,cAAc;AAChB,CAAC;AAEH,MAAM,uBACJ,OACA,aAEA,mBAAmB,iBAAiB,QAAQ,MAAM,UAAU,UAAU;CACpE,aAAa;CACb,QAAQ;CACR,cAAc;AAChB,CAAC;AAEH,MAAM,sBAAsB,YAA0C;CACpE,IAAI,CAAC,SACH;CAGF,IAAI;EACF,OAAO,IAAI,IAAI,QAAQ,GAAG,CAAC,CAAC;CAC9B,QAAQ;EACN,OAAO;CACT;AACF;AAEA,MAAM,sBAAsB,SAAyB;CAGnD,QAFmB,KAAK,WAAW,GAAG,IAAI,OAAO,IAAI,OAAA,CAEnC,QAAQ,QAAQ,GAAG;AACvC;AAEA,MAAM,uBAAuB,YAA8C;CACzE,IAAI,QAAQ,MACV,OAAO,mBAAmB,QAAQ,IAAI;CAGxC,OAAO,mBAAmB,IAAI,QAAQ,QAAQ,YAAY,UAAU,KAAK;AAC3E;AAEA,MAAM,sBAAsB,aAC1B,mBAAmB,IAAI,UAAU;AAEnC,MAAa,wBAAwB,YAAsC;CACzE,QAAQ,YAAuC;EAC7C,IAAI;GACF,MAAM,SAASA,iBAAAA,gBAAgB,QAAQ,OAAO;GAE9C,IAAI,OAAO,OAAO,SAAS,GACzB,OAAO,mBACL,OAAO,OAAO,KAAK,UAAU,MAAM,OAAO,CAAC,CAAC,KAAK,IAAI,GACrD;IACE,aAAa;IACb,QAAQ;IACR,cAAc;GAChB,CACF;GAGF,MAAM,gBAAgB,mBAAmB,OAAO;GAChD,MAAM,YAAY,oBAAoB,OAAO;GAE7C,IAAI,iBAAiB,kBAAkB,WAAW;IAChD,MAAM,gBAAgB,OAAO,MAAM,MAChC,SAAS,mBAAmB,KAAK,QAAQ,MAAM,aAClD;IAEA,IAAI,CAAC,eACH,OAAO,uBAAuB;IAGhC,OAAO,mBAAmB,cAAc,KAAK;KAC3C,aAAa;KACb,cAAc,QAAQ;IACxB,CAAC;GACH;GAEA,MAAM,OACJ,OAAO,YACP,OAAO,MAAM,EAAE,EAAE,OACjB,CACE,8CACA,yEACF,CAAC,CAAC,KAAK,EAAE;GAEX,OAAO,mBAAmB,MAAM;IAC9B,aAAa;IACb,cAAc,QAAQ;GACxB,CAAC;EACH,SAAS,OAAO;GACd,OAAO,oBAAoB,OAAO,4BAA4B;EAChE;CACF;AACF;AAEA,MAAa,uBAAuB,YAAqC;CACvE,QAAQ,YAAuC;EAC7C,MAAM,gBAAgB,mBAAmB,OAAO;EAChD,MAAM,aAAa,mBAAmB,QAAQ,QAAQ,aAAa;EAEnE,IAAI,iBAAiB,kBAAkB,YACrC,OAAO,uBAAuB;EAGhC,IAAI;GACF,MAAM,SAASC,mBAAAA,kBAAkB,QAAQ,MAAM;GAE/C,IAAI,OAAO,OAAO,SAAS,GACzB,OAAO,mBACL,OAAO,OAAO,KAAK,UAAU,MAAM,OAAO,CAAC,CAAC,KAAK,IAAI,GACrD;IACE,aAAa;IACb,QAAQ;IACR,cAAc;GAChB,CACF;GAGF,OAAO,mBAAmB,OAAO,SAAS;IACxC,aAAa;IACb,cAAc,QAAQ;GACxB,CAAC;EACH,SAAS,OAAO;GACd,OAAO,oBAAoB,OAAO,+BAA+B;EACnE;CACF;AACF"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { SeoRobotsHandlerOptions, SeoSitemapHandlerOptions, SeoTextResponse } from "./types.cjs";
|
|
2
|
+
export declare const createSitemapHandler: (options: SeoSitemapHandlerOptions) => (request?: Request) => SeoTextResponse;
|
|
3
|
+
export declare const createRobotsHandler: (options: SeoRobotsHandlerOptions) => (request?: Request) => SeoTextResponse;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { SeoRobotsHandlerOptions, SeoSitemapHandlerOptions, SeoTextResponse } from "./types.js";
|
|
2
|
+
export declare const createSitemapHandler: (options: SeoSitemapHandlerOptions) => (request?: Request) => SeoTextResponse;
|
|
3
|
+
export declare const createRobotsHandler: (options: SeoRobotsHandlerOptions) => (request?: Request) => SeoTextResponse;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { generateSitemap } from "../sitemap/generate.js";
|
|
2
|
+
import { generateRobotsTxt } from "../robots/generate.js";
|
|
3
|
+
// src/server/handlers.ts
|
|
4
|
+
const DEFAULT_CACHE_CONTROL = "public, max-age=3600, s-maxage=3600";
|
|
5
|
+
const XML_CONTENT_TYPE = "application/xml; charset=utf-8";
|
|
6
|
+
const TEXT_CONTENT_TYPE = "text/plain; charset=utf-8";
|
|
7
|
+
const createTextResponse = (body, options) => ({
|
|
8
|
+
status: options.status ?? 200,
|
|
9
|
+
headers: {
|
|
10
|
+
"Content-Type": options.contentType,
|
|
11
|
+
"Cache-Control": options.cacheControl ?? DEFAULT_CACHE_CONTROL,
|
|
12
|
+
"X-Content-Type-Options": "nosniff",
|
|
13
|
+
...options.headers
|
|
14
|
+
},
|
|
15
|
+
body
|
|
16
|
+
});
|
|
17
|
+
const createNotFoundResponse = () => createTextResponse("Not found.", {
|
|
18
|
+
contentType: TEXT_CONTENT_TYPE,
|
|
19
|
+
status: 404,
|
|
20
|
+
cacheControl: "no-store"
|
|
21
|
+
});
|
|
22
|
+
const createErrorResponse = (error, fallback) => createTextResponse(error instanceof Error ? error.message : fallback, {
|
|
23
|
+
contentType: TEXT_CONTENT_TYPE,
|
|
24
|
+
status: 500,
|
|
25
|
+
cacheControl: "no-store"
|
|
26
|
+
});
|
|
27
|
+
const getRequestPathname = (request) => {
|
|
28
|
+
if (!request) return;
|
|
29
|
+
try {
|
|
30
|
+
return new URL(request.url).pathname;
|
|
31
|
+
} catch {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const normalizeRoutePath = (path) => {
|
|
36
|
+
return (path.startsWith("/") ? path : `/${path}`).replace(/\/+/g, "/");
|
|
37
|
+
};
|
|
38
|
+
const getSitemapIndexPath = (options) => {
|
|
39
|
+
if (options.path) return normalizeRoutePath(options.path);
|
|
40
|
+
return normalizeRoutePath(`/${options.sitemap.filename ?? "sitemap"}.xml`);
|
|
41
|
+
};
|
|
42
|
+
const getSitemapFilePath = (filename) => normalizeRoutePath(`/${filename}`);
|
|
43
|
+
const createSitemapHandler = (options) => {
|
|
44
|
+
return (request) => {
|
|
45
|
+
try {
|
|
46
|
+
const result = generateSitemap(options.sitemap);
|
|
47
|
+
if (result.issues.length > 0) return createTextResponse(result.issues.map((issue) => issue.message).join("\n"), {
|
|
48
|
+
contentType: TEXT_CONTENT_TYPE,
|
|
49
|
+
status: 500,
|
|
50
|
+
cacheControl: "no-store"
|
|
51
|
+
});
|
|
52
|
+
const requestedPath = getRequestPathname(request);
|
|
53
|
+
const indexPath = getSitemapIndexPath(options);
|
|
54
|
+
if (requestedPath && requestedPath !== indexPath) {
|
|
55
|
+
const requestedFile = result.files.find((file) => getSitemapFilePath(file.filename) === requestedPath);
|
|
56
|
+
if (!requestedFile) return createNotFoundResponse();
|
|
57
|
+
return createTextResponse(requestedFile.xml, {
|
|
58
|
+
contentType: XML_CONTENT_TYPE,
|
|
59
|
+
cacheControl: options.cacheControl
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const body = result.indexXml ?? result.files[0]?.xml ?? ["<?xml version=\"1.0\" encoding=\"UTF-8\"?>", "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"></urlset>"].join("");
|
|
63
|
+
return createTextResponse(body, {
|
|
64
|
+
contentType: XML_CONTENT_TYPE,
|
|
65
|
+
cacheControl: options.cacheControl
|
|
66
|
+
});
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return createErrorResponse(error, "Sitemap generation failed.");
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
const createRobotsHandler = (options) => {
|
|
73
|
+
return (request) => {
|
|
74
|
+
const requestedPath = getRequestPathname(request);
|
|
75
|
+
const robotsPath = normalizeRoutePath(options.path ?? "/robots.txt");
|
|
76
|
+
if (requestedPath && requestedPath !== robotsPath) return createNotFoundResponse();
|
|
77
|
+
try {
|
|
78
|
+
const result = generateRobotsTxt(options.robots);
|
|
79
|
+
if (result.issues.length > 0) return createTextResponse(result.issues.map((issue) => issue.message).join("\n"), {
|
|
80
|
+
contentType: TEXT_CONTENT_TYPE,
|
|
81
|
+
status: 500,
|
|
82
|
+
cacheControl: "no-store"
|
|
83
|
+
});
|
|
84
|
+
return createTextResponse(result.content, {
|
|
85
|
+
contentType: TEXT_CONTENT_TYPE,
|
|
86
|
+
cacheControl: options.cacheControl
|
|
87
|
+
});
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return createErrorResponse(error, "Robots.txt generation failed.");
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
export { createRobotsHandler, createSitemapHandler };
|
|
94
|
+
|
|
95
|
+
//# sourceMappingURL=handlers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handlers.js","names":[],"sources":["../../src/server/handlers.ts"],"sourcesContent":["import { generateSitemap } from \"../sitemap\";\nimport { generateRobotsTxt } from \"../robots\";\n\nimport type {\n SeoTextResponse,\n SeoSitemapHandlerOptions,\n SeoRobotsHandlerOptions,\n} from \"./types\";\n\nconst DEFAULT_CACHE_CONTROL = \"public, max-age=3600, s-maxage=3600\";\n\nconst XML_CONTENT_TYPE = \"application/xml; charset=utf-8\";\nconst TEXT_CONTENT_TYPE = \"text/plain; charset=utf-8\";\n\nconst createTextResponse = (\n body: string,\n options: {\n contentType: string;\n cacheControl?: string;\n headers?: Record<string, string>;\n status?: number;\n },\n): SeoTextResponse => ({\n status: options.status ?? 200,\n headers: {\n \"Content-Type\": options.contentType,\n \"Cache-Control\": options.cacheControl ?? DEFAULT_CACHE_CONTROL,\n \"X-Content-Type-Options\": \"nosniff\",\n ...options.headers,\n },\n body,\n});\n\nconst createNotFoundResponse = (): SeoTextResponse =>\n createTextResponse(\"Not found.\", {\n contentType: TEXT_CONTENT_TYPE,\n status: 404,\n cacheControl: \"no-store\",\n });\n\nconst createErrorResponse = (\n error: unknown,\n fallback: string,\n): SeoTextResponse =>\n createTextResponse(error instanceof Error ? error.message : fallback, {\n contentType: TEXT_CONTENT_TYPE,\n status: 500,\n cacheControl: \"no-store\",\n });\n\nconst getRequestPathname = (request?: Request): string | undefined => {\n if (!request) {\n return undefined;\n }\n\n try {\n return new URL(request.url).pathname;\n } catch {\n return \"\";\n }\n};\n\nconst normalizeRoutePath = (path: string): string => {\n const normalized = path.startsWith(\"/\") ? path : `/${path}`;\n\n return normalized.replace(/\\/+/g, \"/\");\n};\n\nconst getSitemapIndexPath = (options: SeoSitemapHandlerOptions): string => {\n if (options.path) {\n return normalizeRoutePath(options.path);\n }\n\n return normalizeRoutePath(`/${options.sitemap.filename ?? \"sitemap\"}.xml`);\n};\n\nconst getSitemapFilePath = (filename: string): string =>\n normalizeRoutePath(`/${filename}`);\n\nexport const createSitemapHandler = (options: SeoSitemapHandlerOptions) => {\n return (request?: Request): SeoTextResponse => {\n try {\n const result = generateSitemap(options.sitemap);\n\n if (result.issues.length > 0) {\n return createTextResponse(\n result.issues.map((issue) => issue.message).join(\"\\n\"),\n {\n contentType: TEXT_CONTENT_TYPE,\n status: 500,\n cacheControl: \"no-store\",\n },\n );\n }\n\n const requestedPath = getRequestPathname(request);\n const indexPath = getSitemapIndexPath(options);\n\n if (requestedPath && requestedPath !== indexPath) {\n const requestedFile = result.files.find(\n (file) => getSitemapFilePath(file.filename) === requestedPath,\n );\n\n if (!requestedFile) {\n return createNotFoundResponse();\n }\n\n return createTextResponse(requestedFile.xml, {\n contentType: XML_CONTENT_TYPE,\n cacheControl: options.cacheControl,\n });\n }\n\n const body =\n result.indexXml ??\n result.files[0]?.xml ??\n [\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n '<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"></urlset>',\n ].join(\"\");\n\n return createTextResponse(body, {\n contentType: XML_CONTENT_TYPE,\n cacheControl: options.cacheControl,\n });\n } catch (error) {\n return createErrorResponse(error, \"Sitemap generation failed.\");\n }\n };\n};\n\nexport const createRobotsHandler = (options: SeoRobotsHandlerOptions) => {\n return (request?: Request): SeoTextResponse => {\n const requestedPath = getRequestPathname(request);\n const robotsPath = normalizeRoutePath(options.path ?? \"/robots.txt\");\n\n if (requestedPath && requestedPath !== robotsPath) {\n return createNotFoundResponse();\n }\n\n try {\n const result = generateRobotsTxt(options.robots);\n\n if (result.issues.length > 0) {\n return createTextResponse(\n result.issues.map((issue) => issue.message).join(\"\\n\"),\n {\n contentType: TEXT_CONTENT_TYPE,\n status: 500,\n cacheControl: \"no-store\",\n },\n );\n }\n\n return createTextResponse(result.content, {\n contentType: TEXT_CONTENT_TYPE,\n cacheControl: options.cacheControl,\n });\n } catch (error) {\n return createErrorResponse(error, \"Robots.txt generation failed.\");\n }\n };\n};\n"],"mappings":";;;AASA,MAAM,wBAAwB;AAE9B,MAAM,mBAAmB;AACzB,MAAM,oBAAoB;AAE1B,MAAM,sBACJ,MACA,aAMqB;CACrB,QAAQ,QAAQ,UAAU;CAC1B,SAAS;EACP,gBAAgB,QAAQ;EACxB,iBAAiB,QAAQ,gBAAgB;EACzC,0BAA0B;EAC1B,GAAG,QAAQ;CACb;CACA;AACF;AAEA,MAAM,+BACJ,mBAAmB,cAAc;CAC/B,aAAa;CACb,QAAQ;CACR,cAAc;AAChB,CAAC;AAEH,MAAM,uBACJ,OACA,aAEA,mBAAmB,iBAAiB,QAAQ,MAAM,UAAU,UAAU;CACpE,aAAa;CACb,QAAQ;CACR,cAAc;AAChB,CAAC;AAEH,MAAM,sBAAsB,YAA0C;CACpE,IAAI,CAAC,SACH;CAGF,IAAI;EACF,OAAO,IAAI,IAAI,QAAQ,GAAG,CAAC,CAAC;CAC9B,QAAQ;EACN,OAAO;CACT;AACF;AAEA,MAAM,sBAAsB,SAAyB;CAGnD,QAFmB,KAAK,WAAW,GAAG,IAAI,OAAO,IAAI,OAAA,CAEnC,QAAQ,QAAQ,GAAG;AACvC;AAEA,MAAM,uBAAuB,YAA8C;CACzE,IAAI,QAAQ,MACV,OAAO,mBAAmB,QAAQ,IAAI;CAGxC,OAAO,mBAAmB,IAAI,QAAQ,QAAQ,YAAY,UAAU,KAAK;AAC3E;AAEA,MAAM,sBAAsB,aAC1B,mBAAmB,IAAI,UAAU;AAEnC,MAAa,wBAAwB,YAAsC;CACzE,QAAQ,YAAuC;EAC7C,IAAI;GACF,MAAM,SAAS,gBAAgB,QAAQ,OAAO;GAE9C,IAAI,OAAO,OAAO,SAAS,GACzB,OAAO,mBACL,OAAO,OAAO,KAAK,UAAU,MAAM,OAAO,CAAC,CAAC,KAAK,IAAI,GACrD;IACE,aAAa;IACb,QAAQ;IACR,cAAc;GAChB,CACF;GAGF,MAAM,gBAAgB,mBAAmB,OAAO;GAChD,MAAM,YAAY,oBAAoB,OAAO;GAE7C,IAAI,iBAAiB,kBAAkB,WAAW;IAChD,MAAM,gBAAgB,OAAO,MAAM,MAChC,SAAS,mBAAmB,KAAK,QAAQ,MAAM,aAClD;IAEA,IAAI,CAAC,eACH,OAAO,uBAAuB;IAGhC,OAAO,mBAAmB,cAAc,KAAK;KAC3C,aAAa;KACb,cAAc,QAAQ;IACxB,CAAC;GACH;GAEA,MAAM,OACJ,OAAO,YACP,OAAO,MAAM,EAAE,EAAE,OACjB,CACE,8CACA,yEACF,CAAC,CAAC,KAAK,EAAE;GAEX,OAAO,mBAAmB,MAAM;IAC9B,aAAa;IACb,cAAc,QAAQ;GACxB,CAAC;EACH,SAAS,OAAO;GACd,OAAO,oBAAoB,OAAO,4BAA4B;EAChE;CACF;AACF;AAEA,MAAa,uBAAuB,YAAqC;CACvE,QAAQ,YAAuC;EAC7C,MAAM,gBAAgB,mBAAmB,OAAO;EAChD,MAAM,aAAa,mBAAmB,QAAQ,QAAQ,aAAa;EAEnE,IAAI,iBAAiB,kBAAkB,YACrC,OAAO,uBAAuB;EAGhC,IAAI;GACF,MAAM,SAAS,kBAAkB,QAAQ,MAAM;GAE/C,IAAI,OAAO,OAAO,SAAS,GACzB,OAAO,mBACL,OAAO,OAAO,KAAK,UAAU,MAAM,OAAO,CAAC,CAAC,KAAK,IAAI,GACrD;IACE,aAAa;IACb,QAAQ;IACR,cAAc;GAChB,CACF;GAGF,OAAO,mBAAmB,OAAO,SAAS;IACxC,aAAa;IACb,cAAc,QAAQ;GACxB,CAAC;EACH,SAAS,OAAO;GACd,OAAO,oBAAoB,OAAO,+BAA+B;EACnE;CACF;AACF"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_handlers = require("./handlers.cjs");
|
|
3
|
+
const require_routes = require("./routes.cjs");
|
|
4
|
+
exports.createRobotsHandler = require_handlers.createRobotsHandler;
|
|
5
|
+
exports.createSeoRouteHandlers = require_routes.createSeoRouteHandlers;
|
|
6
|
+
exports.createSitemapHandler = require_handlers.createSitemapHandler;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { SeoHttpHandler, SeoRequestContext, SeoRobotsHandlerOptions, SeoRouteHandlers, SeoRouteOptions, SeoServerAdapter, SeoServerResult, SeoSitemapHandlerOptions, SeoTextHandler, SeoTextHandlerOptions, SeoTextResponse } from "./types.cjs";
|
|
2
|
+
import { createRobotsHandler, createSitemapHandler } from "./handlers.cjs";
|
|
3
|
+
import { createSeoRouteHandlers } from "./routes.cjs";
|
|
4
|
+
export { type SeoHttpHandler, type SeoRequestContext, type SeoRobotsHandlerOptions, type SeoRouteHandlers, type SeoRouteOptions, type SeoServerAdapter, type SeoServerResult, type SeoSitemapHandlerOptions, type SeoTextHandler, type SeoTextHandlerOptions, type SeoTextResponse, createRobotsHandler, createSeoRouteHandlers, createSitemapHandler };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { SeoHttpHandler, SeoRequestContext, SeoRobotsHandlerOptions, SeoRouteHandlers, SeoRouteOptions, SeoServerAdapter, SeoServerResult, SeoSitemapHandlerOptions, SeoTextHandler, SeoTextHandlerOptions, SeoTextResponse } from "./types.js";
|
|
2
|
+
import { createRobotsHandler, createSitemapHandler } from "./handlers.js";
|
|
3
|
+
import { createSeoRouteHandlers } from "./routes.js";
|
|
4
|
+
export { type SeoHttpHandler, type SeoRequestContext, type SeoRobotsHandlerOptions, type SeoRouteHandlers, type SeoRouteOptions, type SeoServerAdapter, type SeoServerResult, type SeoSitemapHandlerOptions, type SeoTextHandler, type SeoTextHandlerOptions, type SeoTextResponse, createRobotsHandler, createSeoRouteHandlers, createSitemapHandler };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const require_handlers = require("./handlers.cjs");
|
|
2
|
+
// src/server/routes.ts
|
|
3
|
+
const toResponse = (result) => new Response(result.body, {
|
|
4
|
+
status: result.status,
|
|
5
|
+
headers: result.headers
|
|
6
|
+
});
|
|
7
|
+
const createSeoRouteHandlers = (options) => {
|
|
8
|
+
const sitemapHandler = require_handlers.createSitemapHandler(options.sitemap);
|
|
9
|
+
const robotsHandler = require_handlers.createRobotsHandler(options.robots);
|
|
10
|
+
return {
|
|
11
|
+
sitemap: async (request) => toResponse(await sitemapHandler(request)),
|
|
12
|
+
robots: async (request) => toResponse(await robotsHandler(request))
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
exports.createSeoRouteHandlers = createSeoRouteHandlers;
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=routes.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.cjs","names":["createSitemapHandler","createRobotsHandler"],"sources":["../../src/server/routes.ts"],"sourcesContent":["import { createRobotsHandler, createSitemapHandler } from \"./handlers\";\n\nimport type {\n SeoRouteHandlers,\n SeoRouteOptions,\n SeoTextResponse,\n} from \"./types\";\n\nconst toResponse = (result: SeoTextResponse): Response =>\n new Response(result.body, {\n status: result.status,\n headers: result.headers,\n });\n\nexport const createSeoRouteHandlers = (\n options: SeoRouteOptions,\n): SeoRouteHandlers => {\n const sitemapHandler = createSitemapHandler(options.sitemap);\n const robotsHandler = createRobotsHandler(options.robots);\n\n return {\n sitemap: async (request) => toResponse(await sitemapHandler(request)),\n\n robots: async (request) => toResponse(await robotsHandler(request)),\n };\n};\n"],"mappings":";;AAQA,MAAM,cAAc,WAClB,IAAI,SAAS,OAAO,MAAM;CACxB,QAAQ,OAAO;CACf,SAAS,OAAO;AAClB,CAAC;AAEH,MAAa,0BACX,YACqB;CACrB,MAAM,iBAAiBA,iBAAAA,qBAAqB,QAAQ,OAAO;CAC3D,MAAM,gBAAgBC,iBAAAA,oBAAoB,QAAQ,MAAM;CAExD,OAAO;EACL,SAAS,OAAO,YAAY,WAAW,MAAM,eAAe,OAAO,CAAC;EAEpE,QAAQ,OAAO,YAAY,WAAW,MAAM,cAAc,OAAO,CAAC;CACpE;AACF"}
|