@roottale/cms-core 0.68.0 → 0.70.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/CHANGELOG.md +20 -0
- package/dist/index.cjs +522 -80
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10183 -237
- package/dist/index.d.ts +10183 -237
- package/dist/index.js +500 -80
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -225,6 +225,9 @@ function validateCollections(collections) {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
// src/category-tree.ts
|
|
228
|
+
function isReservedCategoryTreeSlug(presentation, slug) {
|
|
229
|
+
return slug === presentation.categoryFallbackSlug || presentation.reservedCategorySlugs?.includes(slug) === true;
|
|
230
|
+
}
|
|
228
231
|
function basePathPrefix(basePath) {
|
|
229
232
|
if (!basePath.trim()) return null;
|
|
230
233
|
return basePath.replace(/\/+$/, "");
|
|
@@ -253,6 +256,7 @@ function hasChildCategory(termId, terms) {
|
|
|
253
256
|
function resolveCategoryTreeChain(presentation, termId, terms) {
|
|
254
257
|
const chain = categoryTreeAncestry(presentation.categoryDepth, termId, terms);
|
|
255
258
|
if (!chain) return null;
|
|
259
|
+
if (chain.some((term) => isReservedCategoryTreeSlug(presentation, term.slug))) return null;
|
|
256
260
|
if (presentation.entryCategory === "leaf") return chain;
|
|
257
261
|
if (hasChildCategory(termId, terms)) return null;
|
|
258
262
|
return chain.length === presentation.categoryDepth ? chain : null;
|
|
@@ -261,6 +265,33 @@ function resolveCategoryTreeSelection(presentation, termId, terms) {
|
|
|
261
265
|
if (hasChildCategory(termId, terms)) return null;
|
|
262
266
|
return resolveCategoryTreeChain(presentation, termId, terms);
|
|
263
267
|
}
|
|
268
|
+
function resolveCategoryTreeEntryCategory(presentation, entry, terms) {
|
|
269
|
+
const termsById = new Map(terms.map((term) => [term.id, term]));
|
|
270
|
+
const first = presentation.categorySelection === "first";
|
|
271
|
+
const categoryIds = [...new Set(entry.terms.filter((term) => term.taxonomy === "category" && (first || termsById.has(term.id))).map((term) => term.id))];
|
|
272
|
+
if (first) {
|
|
273
|
+
if (termsById.size !== terms.length) return null;
|
|
274
|
+
if (categoryIds.length === 0) {
|
|
275
|
+
const slug = presentation.categoryFallbackSlug;
|
|
276
|
+
if (presentation.categoryDepth !== 1 || !slug || presentation.reservedCategorySlugs?.includes(slug) || terms.some((term) => term.slug === slug)) return null;
|
|
277
|
+
return { kind: "fallback", slug };
|
|
278
|
+
}
|
|
279
|
+
const candidates = [];
|
|
280
|
+
for (const id of categoryIds) {
|
|
281
|
+
const category2 = termsById.get(id);
|
|
282
|
+
if (!category2 || !Number.isInteger(category2.termOrder ?? 0)) return null;
|
|
283
|
+
const chain2 = resolveCategoryTreeChain(presentation, id, terms);
|
|
284
|
+
if (!chain2) return null;
|
|
285
|
+
candidates.push({ category: category2, chain: chain2 });
|
|
286
|
+
}
|
|
287
|
+
candidates.sort((a, b) => (a.category.termOrder ?? 0) - (b.category.termOrder ?? 0) || (a.category.id < b.category.id ? -1 : a.category.id > b.category.id ? 1 : 0));
|
|
288
|
+
return { kind: "category", ...candidates[0] };
|
|
289
|
+
}
|
|
290
|
+
if (categoryIds.length !== 1) return null;
|
|
291
|
+
const category = termsById.get(categoryIds[0]);
|
|
292
|
+
const chain = resolveCategoryTreeChain(presentation, category.id, terms);
|
|
293
|
+
return chain ? { kind: "category", category, chain } : null;
|
|
294
|
+
}
|
|
264
295
|
function resolveCategoryTreeHubPaths(presentation, termId, terms) {
|
|
265
296
|
const chain = resolveCategoryTreeChain(presentation, termId, terms);
|
|
266
297
|
const prefix = basePathPrefix(presentation.basePath);
|
|
@@ -275,20 +306,11 @@ function resolveCategoryTreeHubPaths(presentation, termId, terms) {
|
|
|
275
306
|
}
|
|
276
307
|
function resolveCategoryTreeEntryPath(presentation, entry, terms) {
|
|
277
308
|
if (!entry.slug) return null;
|
|
278
|
-
const
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
];
|
|
284
|
-
if (categoryIds.length !== 1) return null;
|
|
285
|
-
const hubs = resolveCategoryTreeHubPaths(
|
|
286
|
-
presentation,
|
|
287
|
-
categoryIds[0],
|
|
288
|
-
terms
|
|
289
|
-
);
|
|
290
|
-
const leafPath = hubs?.at(-1);
|
|
291
|
-
return leafPath ? `${leafPath}/${encodeURIComponent(entry.slug)}` : null;
|
|
309
|
+
const selected = resolveCategoryTreeEntryCategory(presentation, entry, terms);
|
|
310
|
+
const prefix = basePathPrefix(presentation.basePath);
|
|
311
|
+
if (!selected || prefix === null) return null;
|
|
312
|
+
const slugs = selected.kind === "fallback" ? [selected.slug] : selected.chain.map((term) => term.slug);
|
|
313
|
+
return `${prefix}/${[...slugs, entry.slug].map(encodeURIComponent).join("/")}`;
|
|
292
314
|
}
|
|
293
315
|
|
|
294
316
|
// src/public-path.ts
|
|
@@ -973,10 +995,10 @@ function scopeImportedCss(css, scope, allowedAssetUrls = []) {
|
|
|
973
995
|
|
|
974
996
|
// src/internal-content-links.ts
|
|
975
997
|
var INTERNAL_CONTENT_LINK_TOKEN_SOURCE = String.raw`\[\[internal:([a-z0-9가-힣]+(?:[.-][a-z0-9가-힣]+)*)\|([^\]\n|]+)\]\]`;
|
|
976
|
-
function findInternalContentLinkTokens(
|
|
998
|
+
function findInternalContentLinkTokens(text3) {
|
|
977
999
|
const pattern = new RegExp(INTERNAL_CONTENT_LINK_TOKEN_SOURCE, "giu");
|
|
978
1000
|
const tokens = [];
|
|
979
|
-
for (const match of
|
|
1001
|
+
for (const match of text3.matchAll(pattern)) {
|
|
980
1002
|
const [raw, key, label] = match;
|
|
981
1003
|
if (!key || !label || match.index === void 0) continue;
|
|
982
1004
|
tokens.push({
|
|
@@ -1035,19 +1057,19 @@ function escapeHtmlText(value) {
|
|
|
1035
1057
|
var INTERNAL_CONTENT_LINK_CLASS = "rt-internal-link";
|
|
1036
1058
|
var INTERNAL_CONTENT_LINK_PENDING_CLASS = "rt-internal-link--pending";
|
|
1037
1059
|
var INTERNAL_CONTENT_LINK_PENDING_ATTR = "data-rt-internal-link-pending";
|
|
1038
|
-
function renderInternalContentLinksInText(
|
|
1039
|
-
const tokens = findInternalContentLinkTokens(
|
|
1040
|
-
if (tokens.length === 0) return { html:
|
|
1060
|
+
function renderInternalContentLinksInText(text3, resolve) {
|
|
1061
|
+
const tokens = findInternalContentLinkTokens(text3);
|
|
1062
|
+
if (tokens.length === 0) return { html: text3, hasTokens: false };
|
|
1041
1063
|
let cursor = 0;
|
|
1042
1064
|
let html = "";
|
|
1043
1065
|
for (const token of tokens) {
|
|
1044
|
-
html +=
|
|
1066
|
+
html += text3.slice(cursor, token.index);
|
|
1045
1067
|
const path = resolve(token.key);
|
|
1046
1068
|
const label = escapeHtmlText(token.label);
|
|
1047
1069
|
html += isSafeInternalPath(path) ? `<a href="${escapeHtmlText(path)}" class="${INTERNAL_CONTENT_LINK_CLASS}">${label}</a>` : `<span class="${INTERNAL_CONTENT_LINK_CLASS} ${INTERNAL_CONTENT_LINK_PENDING_CLASS}" ${INTERNAL_CONTENT_LINK_PENDING_ATTR}="${escapeHtmlText(token.key)}">${label}</span>`;
|
|
1048
1070
|
cursor = token.index + token.raw.length;
|
|
1049
1071
|
}
|
|
1050
|
-
html +=
|
|
1072
|
+
html += text3.slice(cursor);
|
|
1051
1073
|
return { html, hasTokens: true };
|
|
1052
1074
|
}
|
|
1053
1075
|
var INTERNAL_LINK_BLOCKED_HTML_TAGS = /* @__PURE__ */ new Set([
|
|
@@ -1081,12 +1103,12 @@ function renderInternalContentLinksInHtml(html, resolve) {
|
|
|
1081
1103
|
let cursor = 0;
|
|
1082
1104
|
let out = "";
|
|
1083
1105
|
let hasTokens = false;
|
|
1084
|
-
const emitText = (
|
|
1085
|
-
if (blockedDepth > 0 || !
|
|
1086
|
-
out +=
|
|
1106
|
+
const emitText = (text3) => {
|
|
1107
|
+
if (blockedDepth > 0 || !text3) {
|
|
1108
|
+
out += text3;
|
|
1087
1109
|
return;
|
|
1088
1110
|
}
|
|
1089
|
-
const rendered = renderInternalContentLinksInText(
|
|
1111
|
+
const rendered = renderInternalContentLinksInText(text3, resolve);
|
|
1090
1112
|
hasTokens ||= rendered.hasTokens;
|
|
1091
1113
|
out += rendered.html;
|
|
1092
1114
|
};
|
|
@@ -1806,13 +1828,13 @@ function findSectionLockViolations(beforeDoc, afterDoc) {
|
|
|
1806
1828
|
...Object.keys(next.attrs)
|
|
1807
1829
|
]);
|
|
1808
1830
|
fields.delete("props");
|
|
1809
|
-
for (const
|
|
1810
|
-
if (!valueEquals(section.attrs[
|
|
1831
|
+
for (const field3 of fields) {
|
|
1832
|
+
if (!valueEquals(section.attrs[field3], next.attrs[field3])) {
|
|
1811
1833
|
violations.push({
|
|
1812
|
-
code:
|
|
1834
|
+
code: field3 === "lock" ? "locked_section_lock_changed" : "locked_section_structure_changed",
|
|
1813
1835
|
sectionId: section.id,
|
|
1814
1836
|
sectionType: section.sectionType,
|
|
1815
|
-
field
|
|
1837
|
+
field: field3
|
|
1816
1838
|
});
|
|
1817
1839
|
}
|
|
1818
1840
|
}
|
|
@@ -1870,11 +1892,11 @@ function imageUrl(value, base) {
|
|
|
1870
1892
|
function resolveMetadata(input) {
|
|
1871
1893
|
const layers = [input.overrides, input.defaults, input.siteDefaults, input.fallback];
|
|
1872
1894
|
const result = {};
|
|
1873
|
-
for (const
|
|
1895
|
+
for (const field3 of ["title", "description", "ogImageAlt"]) {
|
|
1874
1896
|
for (const layer of layers) {
|
|
1875
|
-
const value = text(layer?.[
|
|
1897
|
+
const value = text(layer?.[field3]);
|
|
1876
1898
|
if (value !== void 0) {
|
|
1877
|
-
result[
|
|
1899
|
+
result[field3] = value;
|
|
1878
1900
|
break;
|
|
1879
1901
|
}
|
|
1880
1902
|
}
|
|
@@ -1886,11 +1908,11 @@ function resolveMetadata(input) {
|
|
|
1886
1908
|
break;
|
|
1887
1909
|
}
|
|
1888
1910
|
}
|
|
1889
|
-
for (const
|
|
1911
|
+
for (const field3 of ["noindex", "nofollow"]) {
|
|
1890
1912
|
for (const layer of layers) {
|
|
1891
|
-
const value = layer?.[
|
|
1913
|
+
const value = layer?.[field3];
|
|
1892
1914
|
if (typeof value === "boolean") {
|
|
1893
|
-
result[
|
|
1915
|
+
result[field3] = value;
|
|
1894
1916
|
break;
|
|
1895
1917
|
}
|
|
1896
1918
|
}
|
|
@@ -1937,30 +1959,32 @@ var MetadataPatchSchema = z2.object({
|
|
|
1937
1959
|
}).strict();
|
|
1938
1960
|
var MetadataRobotsPolicySchema = z2.object({
|
|
1939
1961
|
forceNoindex: z2.boolean().optional(),
|
|
1940
|
-
forceNofollow: z2.boolean().optional()
|
|
1962
|
+
forceNofollow: z2.boolean().optional(),
|
|
1963
|
+
/** Stable policy reason codes. They explain an enforced exclusion, never relax it. */
|
|
1964
|
+
noindexReasons: z2.array(z2.string().trim().min(1).max(200)).max(64).optional()
|
|
1941
1965
|
}).strict();
|
|
1942
1966
|
function readMetadataValues(input) {
|
|
1943
1967
|
if (!input || typeof input !== "object" || Array.isArray(input)) return {};
|
|
1944
|
-
const
|
|
1968
|
+
const record2 = input;
|
|
1945
1969
|
const output = {};
|
|
1946
|
-
for (const
|
|
1947
|
-
const parsed = metadataFields[
|
|
1948
|
-
if (parsed.success) output[
|
|
1970
|
+
for (const field3 of METADATA_FIELDS) {
|
|
1971
|
+
const parsed = metadataFields[field3].safeParse(record2[field3]);
|
|
1972
|
+
if (parsed.success) output[field3] = parsed.data;
|
|
1949
1973
|
}
|
|
1950
1974
|
return MetadataValuesSchema.parse(output);
|
|
1951
1975
|
}
|
|
1952
1976
|
function mergeMetadataPatch(current, input) {
|
|
1953
1977
|
const patch = MetadataPatchSchema.parse(input);
|
|
1954
1978
|
const next = { ...current };
|
|
1955
|
-
for (const
|
|
1956
|
-
if (patch[
|
|
1957
|
-
else if (patch[
|
|
1979
|
+
for (const field3 of METADATA_FIELDS) {
|
|
1980
|
+
if (patch[field3] === null) delete next[field3];
|
|
1981
|
+
else if (patch[field3] !== void 0) next[field3] = patch[field3];
|
|
1958
1982
|
}
|
|
1959
1983
|
return next;
|
|
1960
1984
|
}
|
|
1961
1985
|
|
|
1962
1986
|
// src/metadata-defaults.ts
|
|
1963
|
-
import { z as
|
|
1987
|
+
import { z as z6 } from "zod";
|
|
1964
1988
|
|
|
1965
1989
|
// src/metadata-text.ts
|
|
1966
1990
|
import { z as z3 } from "zod";
|
|
@@ -1991,13 +2015,13 @@ var MetadataTextRuleSchema = z3.object({
|
|
|
1991
2015
|
fallbackBySlug: z3.record(z3.string().max(200), fallbackText).optional()
|
|
1992
2016
|
}).strict().refine((rule) => !rule.ellipsis || rule.maxLength !== void 0 && rule.ellipsis.length < rule.maxLength, "\uB9D0\uC904\uC784\uD45C\uC5D0\uB294 \uB354 \uAE34 \uCD5C\uB300 \uAE38\uC774\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4").refine((rule) => rule.source === "template" ? rule.template !== void 0 : rule.template === void 0, "\uD14D\uC2A4\uD2B8 template\uC740 source: template\uACFC \uD568\uAED8 \uC120\uC5B8\uD569\uB2C8\uB2E4").refine((rule) => !rule.wordBoundaryMinLength || Boolean(rule.maxLength && rule.wordBoundaryMinLength < rule.maxLength), "\uB2E8\uC5B4 \uACBD\uACC4\uC5D0\uB294 \uB354 \uAE34 \uCD5C\uB300 \uAE38\uC774\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4");
|
|
1993
2017
|
function cleanMetadataText(value, rule) {
|
|
1994
|
-
let
|
|
1995
|
-
if (rule?.stripLeadingLabel === "question")
|
|
1996
|
-
if (rule?.stripLeadingLabel === "answer")
|
|
1997
|
-
if (rule?.replaceLineBreakTokens)
|
|
1998
|
-
if (rule?.removeEmDashes)
|
|
1999
|
-
if (rule?.normalizeWhitespace)
|
|
2000
|
-
return
|
|
2018
|
+
let text3 = value.trim();
|
|
2019
|
+
if (rule?.stripLeadingLabel === "question") text3 = text3.replace(/^\s*Q[..:]?\s*/iu, "");
|
|
2020
|
+
if (rule?.stripLeadingLabel === "answer") text3 = text3.replace(/^\s*A[..:]?\s*/iu, "");
|
|
2021
|
+
if (rule?.replaceLineBreakTokens) text3 = text3.replace(/\[\[br(?::[^\]]*)?\]\]/giu, " ");
|
|
2022
|
+
if (rule?.removeEmDashes) text3 = text3.replaceAll("\u2014", "");
|
|
2023
|
+
if (rule?.normalizeWhitespace) text3 = text3.replace(/\s+/gu, " ");
|
|
2024
|
+
return text3.trim();
|
|
2001
2025
|
}
|
|
2002
2026
|
function metadataBodyParagraphs(value) {
|
|
2003
2027
|
const paragraphs = [];
|
|
@@ -2015,37 +2039,37 @@ function metadataBodyParagraphs(value) {
|
|
|
2015
2039
|
if (!node || typeof node !== "object") return;
|
|
2016
2040
|
const item = node;
|
|
2017
2041
|
if (item.type === "paragraph") {
|
|
2018
|
-
const
|
|
2019
|
-
if (
|
|
2042
|
+
const text3 = textParts(item.content).join(" ").replace(/\s+/gu, " ").trim();
|
|
2043
|
+
if (text3) paragraphs.push(text3);
|
|
2020
2044
|
} else walk(item.content);
|
|
2021
2045
|
};
|
|
2022
2046
|
walk(value);
|
|
2023
2047
|
return paragraphs;
|
|
2024
2048
|
}
|
|
2025
2049
|
function formatMetadataText(value, rule, input, name) {
|
|
2026
|
-
let
|
|
2050
|
+
let text3 = rule?.source === "fallback" ? "" : cleanMetadataText(value ?? "", rule);
|
|
2027
2051
|
if (rule?.source === "template") {
|
|
2028
|
-
|
|
2052
|
+
text3 = cleanMetadataText((rule.template ?? "").replace(templateToken, (token) => {
|
|
2029
2053
|
if (token === "{{title}}") return name;
|
|
2030
|
-
const
|
|
2031
|
-
const value2 = Object.hasOwn(input.fields ?? {},
|
|
2054
|
+
const field3 = token.slice(8, -2);
|
|
2055
|
+
const value2 = Object.hasOwn(input.fields ?? {}, field3) ? input.fields?.[field3] : void 0;
|
|
2032
2056
|
return typeof value2 === "string" ? value2.trim() : "";
|
|
2033
2057
|
}), rule);
|
|
2034
2058
|
}
|
|
2035
|
-
if (!
|
|
2059
|
+
if (!text3 && rule?.bodyFallback) {
|
|
2036
2060
|
const paragraphs = input.bodyParagraphs ?? [];
|
|
2037
2061
|
const preferred = rule.bodyFallback.preferContaining;
|
|
2038
|
-
|
|
2039
|
-
if (rule.bodyFallback.stripLeadingLabel === "conclusion")
|
|
2040
|
-
|
|
2062
|
+
text3 = (preferred ? paragraphs.find((paragraph) => paragraph.includes(preferred)) : void 0) ?? paragraphs[0] ?? "";
|
|
2063
|
+
if (rule.bodyFallback.stripLeadingLabel === "conclusion") text3 = text3.replace(/^(먼저\s*)?결론부터\s*(말씀드리면|말씀드릴게요)[.!?]?\s*/u, "");
|
|
2064
|
+
text3 = cleanMetadataText(text3, rule);
|
|
2041
2065
|
}
|
|
2042
|
-
if (!
|
|
2066
|
+
if (!text3) {
|
|
2043
2067
|
const slugFallback = input.slug && Object.hasOwn(rule?.fallbackBySlug ?? {}, input.slug) ? rule?.fallbackBySlug?.[input.slug] : void 0;
|
|
2044
2068
|
const fallback = input.parentId ? rule?.fallbackWithParent ?? rule?.fallback : slugFallback ?? rule?.fallbackWithoutParent ?? rule?.fallback;
|
|
2045
|
-
|
|
2069
|
+
text3 = cleanMetadataText((fallback ?? "").replaceAll("%s", () => name), rule);
|
|
2046
2070
|
}
|
|
2047
|
-
if (!
|
|
2048
|
-
const units = rule?.lengthUnit === "code-points" ? [...
|
|
2071
|
+
if (!text3) return void 0;
|
|
2072
|
+
const units = rule?.lengthUnit === "code-points" ? [...text3] : text3.split("");
|
|
2049
2073
|
if (rule?.maxLength && units.length > rule.maxLength) {
|
|
2050
2074
|
const suffix = rule.ellipsis ?? "";
|
|
2051
2075
|
const suffixLength = rule.lengthUnit === "code-points" ? [...suffix].length : suffix.length;
|
|
@@ -2054,11 +2078,294 @@ function formatMetadataText(value, rule, input, name) {
|
|
|
2054
2078
|
if (rule.wordBoundaryMinLength && boundary >= rule.wordBoundaryMinLength) head = head.slice(0, boundary);
|
|
2055
2079
|
return suffix ? head.trimEnd() + suffix : head;
|
|
2056
2080
|
}
|
|
2057
|
-
return
|
|
2081
|
+
return text3;
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
// src/content-policy-schema.ts
|
|
2085
|
+
import { z as z4 } from "zod";
|
|
2086
|
+
var ContentPolicyFieldNameSchema = z4.string().min(1).max(64).regex(/^[A-Za-z_][A-Za-z0-9_]*$/).refine((name) => name !== "prototype" && !Object.hasOwn(Object.prototype, name));
|
|
2087
|
+
var ContentPolicyScalarSchema = z4.union([
|
|
2088
|
+
z4.string().max(2048),
|
|
2089
|
+
z4.number().finite(),
|
|
2090
|
+
z4.boolean(),
|
|
2091
|
+
z4.null()
|
|
2092
|
+
]);
|
|
2093
|
+
var contentPolicyEquality = z4.object({
|
|
2094
|
+
field: ContentPolicyFieldNameSchema,
|
|
2095
|
+
equals: ContentPolicyScalarSchema
|
|
2096
|
+
}).strict();
|
|
2097
|
+
var ContentPolicyWhenSchema = z4.union([
|
|
2098
|
+
contentPolicyEquality,
|
|
2099
|
+
z4.object({
|
|
2100
|
+
all: z4.array(contentPolicyEquality).min(1).max(4).optional(),
|
|
2101
|
+
unlessAll: z4.array(contentPolicyEquality).min(1).max(4).optional()
|
|
2102
|
+
}).strict().refine(
|
|
2103
|
+
(when2) => when2.all !== void 0 || when2.unlessAll !== void 0,
|
|
2104
|
+
"\uCF58\uD150\uCE20 \uC870\uAC74\uC5D0\uB294 all \uB610\uB294 unlessAll \uADF8\uB8F9\uC774 \uD544\uC694\uD569\uB2C8\uB2E4"
|
|
2105
|
+
)
|
|
2106
|
+
]);
|
|
2107
|
+
var ContentPolicyInternalPathSchema = z4.string().min(1).max(2048).refine((path) => {
|
|
2108
|
+
if (!path.startsWith("/") || path.startsWith("//") || /[\\?#\u0000-\u0020\u007f]/u.test(path)) return false;
|
|
2109
|
+
try {
|
|
2110
|
+
return path.split("/").every((part, index) => {
|
|
2111
|
+
if (index === 0) return true;
|
|
2112
|
+
const decoded2 = decodeURIComponent(part);
|
|
2113
|
+
return decoded2 !== "." && decoded2 !== ".." && !/[\\/\u0000-\u0020\u007f]/u.test(decoded2);
|
|
2114
|
+
});
|
|
2115
|
+
} catch {
|
|
2116
|
+
return false;
|
|
2117
|
+
}
|
|
2118
|
+
}, "\uCF58\uD150\uCE20 \uACBD\uB85C\uB294 \uC548\uC804\uD55C \uC0AC\uC774\uD2B8 \uB0B4\uBD80 \uACBD\uB85C\uC5EC\uC57C \uD569\uB2C8\uB2E4");
|
|
2119
|
+
var field = { field: ContentPolicyFieldNameSchema };
|
|
2120
|
+
var scalarChecks = [
|
|
2121
|
+
z4.object({ ...field, op: z4.literal("equals"), value: ContentPolicyScalarSchema }).strict(),
|
|
2122
|
+
z4.object({ ...field, op: z4.literal("notEquals"), value: ContentPolicyScalarSchema }).strict(),
|
|
2123
|
+
z4.object({ ...field, op: z4.literal("oneOf"), values: z4.array(ContentPolicyScalarSchema).min(1).max(16), allowEmpty: z4.boolean().optional() }).strict(),
|
|
2124
|
+
z4.object({ ...field, op: z4.literal("text") }).strict(),
|
|
2125
|
+
z4.object({ ...field, op: z4.literal("date") }).strict(),
|
|
2126
|
+
z4.object({ ...field, op: z4.literal("safeHttps") }).strict(),
|
|
2127
|
+
z4.object({ ...field, op: z4.literal("allowedPath"), paths: z4.array(ContentPolicyInternalPathSchema).min(1).max(512) }).strict()
|
|
2128
|
+
];
|
|
2129
|
+
var ContentPolicyScalarCheckSchema = z4.discriminatedUnion("op", scalarChecks);
|
|
2130
|
+
var when = { when: ContentPolicyWhenSchema.optional() };
|
|
2131
|
+
var ContentPolicyCheckSchema = z4.discriminatedUnion("op", [
|
|
2132
|
+
scalarChecks[0].extend(when),
|
|
2133
|
+
scalarChecks[1].extend(when),
|
|
2134
|
+
scalarChecks[2].extend(when),
|
|
2135
|
+
scalarChecks[3].extend(when),
|
|
2136
|
+
scalarChecks[4].extend(when),
|
|
2137
|
+
scalarChecks[5].extend(when),
|
|
2138
|
+
scalarChecks[6].extend(when),
|
|
2139
|
+
z4.object({ ...field, ...when, op: z4.literal("some"), checks: z4.array(ContentPolicyScalarCheckSchema).min(1).max(8) }).strict(),
|
|
2140
|
+
z4.object({
|
|
2141
|
+
...when,
|
|
2142
|
+
op: z4.literal("bodyContent"),
|
|
2143
|
+
allowExcerpt: z4.boolean().optional(),
|
|
2144
|
+
allowFeaturedImage: z4.boolean().optional(),
|
|
2145
|
+
additionalUrl: z4.object({ ...field, ...when }).strict().optional()
|
|
2146
|
+
}).strict(),
|
|
2147
|
+
z4.object({ ...when, op: z4.literal("bodyText") }).strict()
|
|
2148
|
+
]);
|
|
2149
|
+
var contentPolicy = z4.object({
|
|
2150
|
+
visibility: z4.array(ContentPolicyCheckSchema).max(24),
|
|
2151
|
+
indexing: z4.array(ContentPolicyCheckSchema).max(24),
|
|
2152
|
+
pathSegmentFormat: z4.literal("lower-kebab").optional()
|
|
2153
|
+
}).strict();
|
|
2154
|
+
var ContentPolicySchema = contentPolicy.superRefine((policy, ctx) => {
|
|
2155
|
+
const refs = collectContentPolicyFieldRefs(policy);
|
|
2156
|
+
if (refs.length > 64) ctx.addIssue({ code: z4.ZodIssueCode.custom, message: "\uCF58\uD150\uCE20 \uC815\uCC45\uC740 \uACF5\uAC1C \uD544\uB4DC 64\uAC1C\uAE4C\uC9C0 \uCC38\uC870\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4" });
|
|
2157
|
+
for (const ref of refs) {
|
|
2158
|
+
if ((ref.children?.length ?? 0) > 8) ctx.addIssue({ code: z4.ZodIssueCode.custom, message: `\uBC18\uBCF5 \uD544\uB4DC ${ref.name}\uC758 \uC790\uC2DD \uCC38\uC870\uB294 8\uAC1C\uAE4C\uC9C0 \uD5C8\uC6A9\uD569\uB2C8\uB2E4` });
|
|
2159
|
+
}
|
|
2160
|
+
});
|
|
2161
|
+
function collectContentPolicyFieldRefs(policy) {
|
|
2162
|
+
const refs = /* @__PURE__ */ new Map();
|
|
2163
|
+
const add = (name, children = []) => {
|
|
2164
|
+
const names = refs.get(name) ?? /* @__PURE__ */ new Set();
|
|
2165
|
+
for (const child of children) names.add(child);
|
|
2166
|
+
refs.set(name, names);
|
|
2167
|
+
};
|
|
2168
|
+
const addWhen = (when2) => {
|
|
2169
|
+
if (!when2) return;
|
|
2170
|
+
if ("field" in when2) add(when2.field);
|
|
2171
|
+
else for (const condition of [...when2.all ?? [], ...when2.unlessAll ?? []]) add(condition.field);
|
|
2172
|
+
};
|
|
2173
|
+
for (const check of [...policy.visibility, ...policy.indexing]) {
|
|
2174
|
+
addWhen(check.when);
|
|
2175
|
+
if ("field" in check) add(check.field, check.op === "some" ? check.checks.map((child) => child.field) : []);
|
|
2176
|
+
if (check.op === "bodyContent" && check.additionalUrl) {
|
|
2177
|
+
add(check.additionalUrl.field);
|
|
2178
|
+
addWhen(check.additionalUrl.when);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
return [...refs].map(([name, children]) => ({ name, ...children.size ? { children: [...children] } : {} }));
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
// src/content-policy-condition.ts
|
|
2185
|
+
function matchesContentPolicyWhen(when2, fields) {
|
|
2186
|
+
if (!when2) return true;
|
|
2187
|
+
const matches = ({ field: field3, equals }) => {
|
|
2188
|
+
const value = Object.hasOwn(fields, field3) ? fields[field3] : void 0;
|
|
2189
|
+
return value === equals;
|
|
2190
|
+
};
|
|
2191
|
+
if ("field" in when2) return matches(when2);
|
|
2192
|
+
return (when2.all?.every(matches) ?? true) && !(when2.unlessAll?.every(matches) ?? false);
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
// src/post-body-source.ts
|
|
2196
|
+
function resolvePostBodySource(bodyHtml) {
|
|
2197
|
+
if (typeof bodyHtml !== "string") return { kind: "json" };
|
|
2198
|
+
const trimmed = bodyHtml.trim();
|
|
2199
|
+
if (trimmed.length === 0) return { kind: "json" };
|
|
2200
|
+
const sanitized = sanitizeHtml(trimmed);
|
|
2201
|
+
const visible = /<(?:img|hr|br|iframe|video|audio)\b/i.test(sanitized) || sanitized.replace(/<[^>]*>/g, "").replace(/ /gi, " ").trim().length > 0;
|
|
2202
|
+
return visible ? { kind: "html", html: sanitized } : { kind: "json" };
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
// src/content-policy.ts
|
|
2206
|
+
var text2 = (value) => typeof value === "string" ? value.trim() : "";
|
|
2207
|
+
var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2208
|
+
function fieldValue(fields, field3) {
|
|
2209
|
+
return Object.hasOwn(fields, field3) ? fields[field3] : void 0;
|
|
2210
|
+
}
|
|
2211
|
+
function contentPolicySafeHttpsUrl(value) {
|
|
2212
|
+
if (typeof value !== "string" || /[\\\u0000-\u0020\u007f]/u.test(value)) return null;
|
|
2213
|
+
try {
|
|
2214
|
+
const url = new URL(value);
|
|
2215
|
+
return url.protocol === "https:" && !url.username && !url.password ? url.href : null;
|
|
2216
|
+
} catch {
|
|
2217
|
+
return null;
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
function dateValue(value) {
|
|
2221
|
+
if (!value || typeof value !== "string" && typeof value !== "number") return null;
|
|
2222
|
+
if (!Number.isFinite(Date.parse(String(value)))) return null;
|
|
2223
|
+
const date = new Date(value).getTime();
|
|
2224
|
+
return Number.isFinite(date) ? date : null;
|
|
2225
|
+
}
|
|
2226
|
+
function contentPolicyBodyText(bodyJson) {
|
|
2227
|
+
const root = record(bodyJson);
|
|
2228
|
+
if (root?.type !== "doc" || !Array.isArray(root.content)) return "";
|
|
2229
|
+
const pending = [{ node: root, depth: 0 }];
|
|
2230
|
+
const output = [];
|
|
2231
|
+
let count = 0;
|
|
2232
|
+
while (pending.length) {
|
|
2233
|
+
const entry = pending.pop();
|
|
2234
|
+
if (++count > 1e5 || entry.depth > 128) throw new Error("Content policy body exceeds traversal limits");
|
|
2235
|
+
const node = record(entry.node);
|
|
2236
|
+
if (!node) continue;
|
|
2237
|
+
if (node.type === "text") {
|
|
2238
|
+
const value = text2(node.text);
|
|
2239
|
+
if (value) output.push(value);
|
|
2240
|
+
} else if (Array.isArray(node.content)) {
|
|
2241
|
+
for (let index = node.content.length - 1; index >= 0; index--) {
|
|
2242
|
+
pending.push({ node: node.content[index], depth: entry.depth + 1 });
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
return output.join(" ");
|
|
2247
|
+
}
|
|
2248
|
+
function matchesContentScalarCheck(check, fields) {
|
|
2249
|
+
const value = fieldValue(fields, check.field);
|
|
2250
|
+
switch (check.op) {
|
|
2251
|
+
case "equals":
|
|
2252
|
+
return value === check.value;
|
|
2253
|
+
case "notEquals":
|
|
2254
|
+
return value !== check.value;
|
|
2255
|
+
case "oneOf":
|
|
2256
|
+
return check.allowEmpty === true && !value || check.values.some((candidate) => candidate === value);
|
|
2257
|
+
case "text":
|
|
2258
|
+
return text2(value).length > 0;
|
|
2259
|
+
case "date":
|
|
2260
|
+
return dateValue(value) !== null;
|
|
2261
|
+
case "safeHttps":
|
|
2262
|
+
return contentPolicySafeHttpsUrl(value) !== null;
|
|
2263
|
+
case "allowedPath":
|
|
2264
|
+
return check.paths.includes(text2(value));
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
function bodyFacts(post) {
|
|
2268
|
+
const jsonText = contentPolicyBodyText(post.bodyJson);
|
|
2269
|
+
const source = resolvePostBodySource(post.bodyHtml);
|
|
2270
|
+
const html = source.kind === "html" ? source.html : "";
|
|
2271
|
+
return { jsonText, html, text: source.kind === "html" ? html.replace(/<[^>]*>/g, "").replace(/ /gi, " ").trim() : jsonText };
|
|
2272
|
+
}
|
|
2273
|
+
function matchesCheck(check, post, fields, getBody) {
|
|
2274
|
+
if (!matchesContentPolicyWhen(check.when, fields)) return true;
|
|
2275
|
+
if (check.op === "some") {
|
|
2276
|
+
const value = fieldValue(fields, check.field);
|
|
2277
|
+
return Array.isArray(value) && value.some((item) => {
|
|
2278
|
+
const child = record(item);
|
|
2279
|
+
return child !== null && check.checks.every((scalar) => matchesContentScalarCheck(scalar, child));
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
if (check.op === "bodyText") return Boolean(getBody().text);
|
|
2283
|
+
if (check.op === "bodyContent") {
|
|
2284
|
+
const body = getBody();
|
|
2285
|
+
return Boolean(body.jsonText || body.html || check.allowExcerpt && text2(post.excerpt) || check.allowFeaturedImage && contentPolicySafeHttpsUrl(post.featuredImageUrl) || check.additionalUrl && matchesContentPolicyWhen(check.additionalUrl.when, fields) && contentPolicySafeHttpsUrl(fieldValue(fields, check.additionalUrl.field)));
|
|
2286
|
+
}
|
|
2287
|
+
return matchesContentScalarCheck(check, fields);
|
|
2288
|
+
}
|
|
2289
|
+
function validPath(policy, post, options) {
|
|
2290
|
+
const path = text2(post.path).replace(/\/+$/, "");
|
|
2291
|
+
if (!path || !ContentPolicyInternalPathSchema.safeParse(path).success) return false;
|
|
2292
|
+
const segments = path.slice(1).split("/");
|
|
2293
|
+
if (segments.some((segment) => !segment)) return false;
|
|
2294
|
+
if (policy.pathSegmentFormat === "lower-kebab" && segments.some((segment) => !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(segment))) return false;
|
|
2295
|
+
if (decodeURIComponent(segments.at(-1)) !== post.slug) return false;
|
|
2296
|
+
if (options.expectedPath !== void 0) {
|
|
2297
|
+
return options.expectedPath !== null && path === options.expectedPath.replace(/\/+$/, "");
|
|
2298
|
+
}
|
|
2299
|
+
return true;
|
|
2300
|
+
}
|
|
2301
|
+
function evaluateContentPolicy(policy, post, options = {}) {
|
|
2302
|
+
const visibility = [];
|
|
2303
|
+
if (post.status !== "published") visibility.push("content:not-published");
|
|
2304
|
+
if (!text2(post.id) || !text2(post.title) || !text2(post.slug)) visibility.push("content:invalid-identity");
|
|
2305
|
+
const publishedAt = dateValue(post.publishedAt);
|
|
2306
|
+
if (publishedAt === null) visibility.push("content:invalid-published-at");
|
|
2307
|
+
else if (publishedAt > (options.now ?? Date.now())) visibility.push("content:future-publication");
|
|
2308
|
+
if (!validPath(policy, post, options)) visibility.push("content:invalid-path");
|
|
2309
|
+
const fields = post.fields ?? {};
|
|
2310
|
+
let body;
|
|
2311
|
+
const getBody = () => body ??= bodyFacts(post);
|
|
2312
|
+
const failures = (checks, stage) => checks.flatMap((check) => matchesCheck(check, post, fields, getBody) ? [] : [`content:${stage}:${check.op}${"field" in check ? `:${check.field}` : ""}`]);
|
|
2313
|
+
if (visibility.length === 0) visibility.push(...failures(policy.visibility, "visibility"));
|
|
2314
|
+
const visible = visibility.length === 0;
|
|
2315
|
+
const indexing = visible ? failures(policy.indexing, "indexing") : [];
|
|
2316
|
+
const forced = !visible || indexing.length > 0 || options.robots?.forceNoindex === true;
|
|
2317
|
+
const inheritedReasons = options.robots?.forceNoindex ? options.robots.noindexReasons?.length ? options.robots.noindexReasons : ["content:forced-noindex"] : [];
|
|
2318
|
+
const noindexReasons = [.../* @__PURE__ */ new Set([...visibility, ...indexing, ...inheritedReasons])].slice(0, 64);
|
|
2319
|
+
const manualNoindex = readMetadataValues(post.metaJson?.seo).noindex === true;
|
|
2320
|
+
return {
|
|
2321
|
+
visible,
|
|
2322
|
+
indexable: visible && !forced && !manualNoindex,
|
|
2323
|
+
robots: {
|
|
2324
|
+
...options.robots,
|
|
2325
|
+
...forced ? { forceNoindex: true } : {},
|
|
2326
|
+
...noindexReasons.length ? { noindexReasons } : {}
|
|
2327
|
+
},
|
|
2328
|
+
reasons: [...noindexReasons, ...manualNoindex ? ["content:manual-noindex"] : []]
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
// src/metadata-video-thumbnail.ts
|
|
2333
|
+
import { z as z5 } from "zod";
|
|
2334
|
+
var MetadataVideoThumbnailRuleSchema = z5.object({
|
|
2335
|
+
source: z5.literal("youtube"),
|
|
2336
|
+
field: ContentPolicyFieldNameSchema,
|
|
2337
|
+
when: ContentPolicyWhenSchema.optional()
|
|
2338
|
+
}).strict();
|
|
2339
|
+
function metadataVideoThumbnail(rule, fields) {
|
|
2340
|
+
if (!rule || !matchesContentPolicyWhen(rule.when, fields ?? {})) return void 0;
|
|
2341
|
+
const value = fields && Object.hasOwn(fields, rule.field) ? fields[rule.field] : void 0;
|
|
2342
|
+
const safe = contentPolicySafeHttpsUrl(value);
|
|
2343
|
+
if (!safe) return void 0;
|
|
2344
|
+
const url = new URL(safe);
|
|
2345
|
+
const id = url.hostname === "youtu.be" ? url.pathname.slice(1) : ["www.youtube.com", "youtube.com", "m.youtube.com"].includes(url.hostname) ? url.pathname === "/watch" ? url.searchParams.get("v") : url.pathname.match(/^\/(?:shorts|embed)\/([^/]+)$/)?.[1] : null;
|
|
2346
|
+
return /^[a-zA-Z0-9_-]{11}$/.test(id ?? "") ? `https://i.ytimg.com/vi/${id}/hqdefault.jpg` : void 0;
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
// src/metadata-default-fields.ts
|
|
2350
|
+
function collectMetadataDefaultFieldRefs(rule) {
|
|
2351
|
+
const fields = /* @__PURE__ */ new Set();
|
|
2352
|
+
const video = rule?.videoThumbnail;
|
|
2353
|
+
if (video) {
|
|
2354
|
+
fields.add(video.field);
|
|
2355
|
+
if (video.when) {
|
|
2356
|
+
if ("field" in video.when) fields.add(video.when.field);
|
|
2357
|
+
else for (const condition of [...video.when.all ?? [], ...video.when.unlessAll ?? []]) fields.add(condition.field);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
for (const text3 of [rule?.title, rule?.description]) {
|
|
2361
|
+
if (text3?.source !== "template") continue;
|
|
2362
|
+
for (const match of (text3.template ?? "").matchAll(/\{\{field:([a-zA-Z][a-zA-Z0-9_-]{0,159})\}\}/gu)) fields.add(match[1]);
|
|
2363
|
+
}
|
|
2364
|
+
return [...fields].map((name) => ({ name }));
|
|
2058
2365
|
}
|
|
2059
2366
|
|
|
2060
2367
|
// src/metadata-defaults.ts
|
|
2061
|
-
var imageOrigin =
|
|
2368
|
+
var imageOrigin = z6.string().max(2048).refine((value) => {
|
|
2062
2369
|
try {
|
|
2063
2370
|
const url = new URL(value);
|
|
2064
2371
|
return url.protocol === "https:" && url.origin === value;
|
|
@@ -2066,23 +2373,34 @@ var imageOrigin = z4.string().max(2048).refine((value) => {
|
|
|
2066
2373
|
return false;
|
|
2067
2374
|
}
|
|
2068
2375
|
}, "\uC774\uBBF8\uC9C0 \uD5C8\uC6A9 \uBAA9\uB85D\uC5D0\uB294 \uACBD\uB85C \uC5C6\uB294 HTTPS origin\uB9CC \uC120\uC5B8\uD569\uB2C8\uB2E4");
|
|
2069
|
-
var MetadataDefaultRulesSchema =
|
|
2376
|
+
var MetadataDefaultRulesSchema = z6.object({
|
|
2070
2377
|
title: MetadataTextRuleSchema.optional(),
|
|
2071
|
-
titleTemplateMaxLength:
|
|
2378
|
+
titleTemplateMaxLength: z6.number().int().min(1).max(1e3).optional(),
|
|
2072
2379
|
description: MetadataTextRuleSchema.optional(),
|
|
2073
2380
|
/** Code-owned defaults precede site defaults but always follow manual overrides. */
|
|
2074
2381
|
defaults: MetadataValuesSchema.optional(),
|
|
2075
2382
|
robots: MetadataRobotsPolicySchema.optional(),
|
|
2076
|
-
useFeaturedImage:
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2383
|
+
useFeaturedImage: z6.boolean().optional(),
|
|
2384
|
+
videoThumbnail: MetadataVideoThumbnailRuleSchema.optional(),
|
|
2385
|
+
useFirstBodyImage: z6.boolean().optional(),
|
|
2386
|
+
useTermImage: z6.boolean().optional(),
|
|
2387
|
+
imageAltSource: z6.enum(["content", "title"]).optional(),
|
|
2388
|
+
legacyImageSource: z6.literal("legacy-thumbnail").optional(),
|
|
2389
|
+
imageAllowedOrigins: z6.array(imageOrigin).min(1).max(32).optional(),
|
|
2390
|
+
legacyImageAllowedOrigins: z6.array(imageOrigin).min(1).max(32).optional(),
|
|
2391
|
+
normalizeCloudflareImages: z6.boolean().optional(),
|
|
2084
2392
|
fallback: MetadataValuesSchema.optional()
|
|
2085
|
-
}).strict().refine((rule) => !rule.legacyImageSource || Boolean(rule.legacyImageAllowedOrigins?.length), "\uC774\uC804 \uC774\uBBF8\uC9C0 \uC6D0\uBCF8\uC5D0\uB294 \uD5C8\uC6A9 origin \uC120\uC5B8\uC774 \uD544\uC694\uD569\uB2C8\uB2E4")
|
|
2393
|
+
}).strict().refine((rule) => !rule.legacyImageSource || Boolean(rule.legacyImageAllowedOrigins?.length), "\uC774\uC804 \uC774\uBBF8\uC9C0 \uC6D0\uBCF8\uC5D0\uB294 \uD5C8\uC6A9 origin \uC120\uC5B8\uC774 \uD544\uC694\uD569\uB2C8\uB2E4").superRefine((rule, context) => {
|
|
2394
|
+
if (!rule.videoThumbnail) return;
|
|
2395
|
+
const refs = collectMetadataDefaultFieldRefs(rule);
|
|
2396
|
+
if (refs.length > 64) context.addIssue({ code: z6.ZodIssueCode.custom, message: "\uC601\uC0C1 \uC790\uB3D9\uAC12\uC740 \uACF5\uAC1C \uD544\uB4DC 64\uAC1C\uAE4C\uC9C0 \uCC38\uC870\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4" });
|
|
2397
|
+
for (const { name } of refs) {
|
|
2398
|
+
if (!ContentPolicyFieldNameSchema.safeParse(name).success) context.addIssue({
|
|
2399
|
+
code: z6.ZodIssueCode.custom,
|
|
2400
|
+
message: "\uC601\uC0C1 \uC790\uB3D9\uAC12\uC758 \uD14D\uC2A4\uD2B8\xB7\uC870\uAC74\uC740 \uC548\uC804\uD55C \uACF5\uAC1C \uD544\uB4DC \uC774\uB984\uC744 \uC0AC\uC6A9\uD569\uB2C8\uB2E4"
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
});
|
|
2086
2404
|
function metadataContentImageUrl(value, normalizeCloudflareImages = false) {
|
|
2087
2405
|
if (!normalizeCloudflareImages) return value;
|
|
2088
2406
|
try {
|
|
@@ -2129,8 +2447,9 @@ function formatMetadataDefaults(input, declaration) {
|
|
|
2129
2447
|
const finalTitle = templatedTitle && rule?.titleTemplateMaxLength && templatedTitle.length > rule.titleTemplateMaxLength ? title : templatedTitle;
|
|
2130
2448
|
const description = formatMetadataText(input.description, rule?.description, input, name);
|
|
2131
2449
|
const bodyImage = input.firstBodyImage && (input.firstBodyImage.startsWith("https://") || /^\/(?!\/)/u.test(input.firstBodyImage)) ? input.firstBodyImage : void 0;
|
|
2132
|
-
const
|
|
2133
|
-
const
|
|
2450
|
+
const rawFeaturedImage = (input.kind === "terms" ? rule?.useTermImage : rule?.useFeaturedImage) === false ? void 0 : input.featuredImage;
|
|
2451
|
+
const featuredImage = rule?.videoThumbnail ? contentPolicySafeHttpsUrl(rawFeaturedImage) : rawFeaturedImage;
|
|
2452
|
+
const candidateImage = featuredImage || (input.kind === "posts" ? metadataVideoThumbnail(rule?.videoThumbnail, input.fields) : void 0) || (rule?.useFirstBodyImage ? bodyImage : void 0) || legacyImageUrl(input.legacyImage, rule);
|
|
2134
2453
|
const ogImage = candidateImage && contentCandidateAllowed(candidateImage, input, rule) ? readMetadataValues({ ogImage: metadataContentImageUrl(candidateImage, rule?.normalizeCloudflareImages) }).ogImage : void 0;
|
|
2135
2454
|
return {
|
|
2136
2455
|
...readMetadataValues(rule?.defaults),
|
|
@@ -2140,6 +2459,85 @@ function formatMetadataDefaults(input, declaration) {
|
|
|
2140
2459
|
};
|
|
2141
2460
|
}
|
|
2142
2461
|
|
|
2462
|
+
// src/content-query.ts
|
|
2463
|
+
import { z as z7 } from "zod";
|
|
2464
|
+
var field2 = { field: ContentPolicyFieldNameSchema };
|
|
2465
|
+
var ContentQueryFilterSchema = z7.discriminatedUnion("op", [
|
|
2466
|
+
z7.object({ ...field2, op: z7.literal("equals"), value: ContentPolicyScalarSchema }).strict(),
|
|
2467
|
+
z7.object({ ...field2, op: z7.literal("notEquals"), value: ContentPolicyScalarSchema }).strict(),
|
|
2468
|
+
z7.object({ ...field2, op: z7.literal("oneOf"), values: z7.array(ContentPolicyScalarSchema).min(1).max(16), allowEmpty: z7.boolean().optional() }).strict()
|
|
2469
|
+
]);
|
|
2470
|
+
var ContentQuerySchema = z7.object({
|
|
2471
|
+
modelKeys: z7.array(z7.string().min(1).max(100).regex(/^[a-z][a-z0-9_-]*$/)).min(1).max(16),
|
|
2472
|
+
filters: z7.array(ContentQueryFilterSchema).max(8).optional(),
|
|
2473
|
+
scope: z7.enum(["currentTerm", "currentPath"]).optional(),
|
|
2474
|
+
pinnedField: ContentPolicyFieldNameSchema.optional(),
|
|
2475
|
+
pageSize: z7.number().int().min(1).max(100),
|
|
2476
|
+
requireItems: z7.boolean().optional()
|
|
2477
|
+
}).strict();
|
|
2478
|
+
function collectContentQueryFieldRefs(query) {
|
|
2479
|
+
return [.../* @__PURE__ */ new Set([...(query.filters ?? []).map((filter) => filter.field), ...query.pinnedField ? [query.pinnedField] : []])].map((name) => ({ name }));
|
|
2480
|
+
}
|
|
2481
|
+
function matchesScope(query, post, options) {
|
|
2482
|
+
if (query.scope === "currentTerm") return post.terms?.some((term) => term.id === options.termId) === true;
|
|
2483
|
+
if (query.scope === "currentPath") {
|
|
2484
|
+
const path = options.path.replace(/\/+$/, "");
|
|
2485
|
+
return post.path?.trim().startsWith(`${path}/`) === true;
|
|
2486
|
+
}
|
|
2487
|
+
return true;
|
|
2488
|
+
}
|
|
2489
|
+
function evaluateContentQuery(query, candidates, options) {
|
|
2490
|
+
if (!options.complete) throw new Error("Content query input is incomplete");
|
|
2491
|
+
const page = options.page ?? 1;
|
|
2492
|
+
if (!Number.isSafeInteger(page) || page < 1) throw new Error("Content query page must be a positive safe integer");
|
|
2493
|
+
if (query.scope === "currentTerm" && !options.termId) throw new Error("Content query requires the current term id");
|
|
2494
|
+
if (query.scope === "currentPath" && (!options.path || !ContentPolicyInternalPathSchema.safeParse(options.path).success)) throw new Error("Content query requires a safe current path");
|
|
2495
|
+
const visible = candidates.filter(({ policy }) => policy.visible);
|
|
2496
|
+
const pathCounts = /* @__PURE__ */ new Map();
|
|
2497
|
+
const canonical = (post) => post.path?.trim().replace(/\/+$/, "") ?? null;
|
|
2498
|
+
for (const { post } of visible) pathCounts.set(canonical(post), (pathCounts.get(canonical(post)) ?? 0) + 1);
|
|
2499
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2500
|
+
const selected = visible.filter(({ post }) => {
|
|
2501
|
+
if (pathCounts.get(canonical(post)) !== 1 || seen.has(post.id)) return false;
|
|
2502
|
+
seen.add(post.id);
|
|
2503
|
+
return query.modelKeys.includes(post.modelKey ?? post.collectionKey ?? "") && matchesScope(query, post, options) && (query.filters ?? []).every((filter) => matchesContentScalarCheck(filter, post.fields ?? {}));
|
|
2504
|
+
}).sort((left, right) => {
|
|
2505
|
+
if (query.pinnedField) {
|
|
2506
|
+
const delta = Number(right.post.fields?.[query.pinnedField] === true) - Number(left.post.fields?.[query.pinnedField] === true);
|
|
2507
|
+
if (delta) return delta;
|
|
2508
|
+
}
|
|
2509
|
+
return Date.parse(right.post.publishedAt) - Date.parse(left.post.publishedAt) || left.post.id.localeCompare(right.post.id);
|
|
2510
|
+
});
|
|
2511
|
+
const pageCount = Math.max(1, Math.ceil(selected.length / query.pageSize));
|
|
2512
|
+
const current = selected.slice((page - 1) * query.pageSize, page * query.pageSize);
|
|
2513
|
+
const pageVisible = page <= pageCount && (!query.requireItems || selected.length > 0);
|
|
2514
|
+
const hasIndexable = current.some(({ policy }) => policy.indexable);
|
|
2515
|
+
const forced = !hasIndexable || options.robots?.forceNoindex === true;
|
|
2516
|
+
const reasons = [.../* @__PURE__ */ new Set([
|
|
2517
|
+
...page > pageCount ? ["content:invalid-page"] : query.requireItems && !selected.length ? ["content:empty-required-list"] : !current.length ? ["content:empty-page"] : !hasIndexable ? ["content:no-indexable-items"] : [],
|
|
2518
|
+
...options.robots?.forceNoindex ? options.robots.noindexReasons?.length ? options.robots.noindexReasons : ["content:forced-noindex"] : []
|
|
2519
|
+
])].slice(0, 64);
|
|
2520
|
+
return {
|
|
2521
|
+
items: current.map(({ post }) => post),
|
|
2522
|
+
total: selected.length,
|
|
2523
|
+
page,
|
|
2524
|
+
pageCount,
|
|
2525
|
+
visible: pageVisible,
|
|
2526
|
+
indexable: pageVisible && !forced,
|
|
2527
|
+
robots: { ...options.robots, ...forced ? { forceNoindex: true } : {}, ...reasons.length ? { noindexReasons: reasons } : {} },
|
|
2528
|
+
reasons
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
function selectContentQuery(query, posts, policies, options) {
|
|
2532
|
+
if (!options.complete) throw new Error("Content query input is incomplete");
|
|
2533
|
+
const candidates = posts.filter((post) => query.modelKeys.includes(post.modelKey ?? post.collectionKey ?? "")).map((post) => {
|
|
2534
|
+
const modelKey = post.modelKey ?? post.collectionKey ?? "";
|
|
2535
|
+
if (!Object.hasOwn(policies, modelKey)) throw new Error(`Content query has no policy for model ${modelKey}`);
|
|
2536
|
+
return { post, policy: evaluateContentPolicy(policies[modelKey], post, { now: options.now, ...options.postOptions(post) }) };
|
|
2537
|
+
});
|
|
2538
|
+
return evaluateContentQuery(query, candidates, options);
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2143
2541
|
// src/redirect-paths.ts
|
|
2144
2542
|
var CONTROL_OR_BACKSLASH = /[\\\u0000-\u001f\u007f]/u;
|
|
2145
2543
|
var PROTECTED_PREFIX = /^\/(?:api|_next|_vercel|auth|admin|login|logout|revalidate|preview|assets|static|images|fonts|\.well-known)(?:\/|$)/i;
|
|
@@ -2206,6 +2604,15 @@ export {
|
|
|
2206
2604
|
CMS_IMAGE_WIDTHS,
|
|
2207
2605
|
CMS_LINE_HEIGHTS,
|
|
2208
2606
|
CMS_PARAGRAPH_SPACINGS,
|
|
2607
|
+
ContentPolicyCheckSchema,
|
|
2608
|
+
ContentPolicyFieldNameSchema,
|
|
2609
|
+
ContentPolicyInternalPathSchema,
|
|
2610
|
+
ContentPolicyScalarCheckSchema,
|
|
2611
|
+
ContentPolicyScalarSchema,
|
|
2612
|
+
ContentPolicySchema,
|
|
2613
|
+
ContentPolicyWhenSchema,
|
|
2614
|
+
ContentQueryFilterSchema,
|
|
2615
|
+
ContentQuerySchema,
|
|
2209
2616
|
DEFAULT_SITE_PATTERN_PRESENTATION,
|
|
2210
2617
|
EMPTY_SITE_PATTERN_PLACEMENTS,
|
|
2211
2618
|
FIELD_BINDING_SOURCE,
|
|
@@ -2243,10 +2650,17 @@ export {
|
|
|
2243
2650
|
cmsImageWidth,
|
|
2244
2651
|
cmsLineHeight,
|
|
2245
2652
|
cmsParagraphSpacing,
|
|
2653
|
+
collectContentPolicyFieldRefs,
|
|
2654
|
+
collectContentQueryFieldRefs,
|
|
2655
|
+
collectMetadataDefaultFieldRefs,
|
|
2656
|
+
contentPolicyBodyText,
|
|
2657
|
+
contentPolicySafeHttpsUrl,
|
|
2246
2658
|
createFieldBindingResolver,
|
|
2247
2659
|
emptyPatternSlots,
|
|
2248
2660
|
ensureImportedHtmlDocumentRoots,
|
|
2249
2661
|
ensureStableIds,
|
|
2662
|
+
evaluateContentPolicy,
|
|
2663
|
+
evaluateContentQuery,
|
|
2250
2664
|
extractFaqEntries,
|
|
2251
2665
|
faqPageJsonLd,
|
|
2252
2666
|
findInternalContentLinkTokens,
|
|
@@ -2263,14 +2677,17 @@ export {
|
|
|
2263
2677
|
isAllowedMapEmbedSrc,
|
|
2264
2678
|
isInCollectionMode,
|
|
2265
2679
|
isProtectedRedirectPath,
|
|
2680
|
+
isReservedCategoryTreeSlug,
|
|
2266
2681
|
isSectionContentPropPath,
|
|
2267
2682
|
isSitePatternSlotKey,
|
|
2268
2683
|
isValidBlock,
|
|
2269
2684
|
isValidSitePatternKey,
|
|
2270
2685
|
issuePreviewToken,
|
|
2686
|
+
matchesContentScalarCheck,
|
|
2271
2687
|
mergeMetadataPatch,
|
|
2272
2688
|
metadataBodyParagraphs,
|
|
2273
2689
|
metadataContentImageUrl,
|
|
2690
|
+
metadataVideoThumbnail,
|
|
2274
2691
|
normalizeEmbed,
|
|
2275
2692
|
normalizeHexColor,
|
|
2276
2693
|
normalizeImageAlt,
|
|
@@ -2289,11 +2706,13 @@ export {
|
|
|
2289
2706
|
renderInternalContentLinksInText,
|
|
2290
2707
|
resolveCategoryPath,
|
|
2291
2708
|
resolveCategoryTreeChain,
|
|
2709
|
+
resolveCategoryTreeEntryCategory,
|
|
2292
2710
|
resolveCategoryTreeEntryPath,
|
|
2293
2711
|
resolveCategoryTreeHubPaths,
|
|
2294
2712
|
resolveCategoryTreeSelection,
|
|
2295
2713
|
resolveMetadata,
|
|
2296
2714
|
resolvePatternSlots,
|
|
2715
|
+
resolvePostBodySource,
|
|
2297
2716
|
resolvePostCategorySlug,
|
|
2298
2717
|
resolvePostCollection,
|
|
2299
2718
|
resolvePostPath,
|
|
@@ -2303,6 +2722,7 @@ export {
|
|
|
2303
2722
|
sanitizeImportedCss,
|
|
2304
2723
|
sanitizeImportedHtml,
|
|
2305
2724
|
scopeImportedCss,
|
|
2725
|
+
selectContentQuery,
|
|
2306
2726
|
serializeBlock,
|
|
2307
2727
|
setSitePatternPlacementRule,
|
|
2308
2728
|
sitePatternButtonColor,
|