@roottale/cms-core 0.67.1 → 0.69.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 +18 -0
- package/dist/index.cjs +529 -90
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4566 -43
- package/dist/index.d.ts +4566 -43
- package/dist/index.js +508 -90
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -35,6 +35,15 @@ __export(src_exports, {
|
|
|
35
35
|
CMS_IMAGE_WIDTHS: () => CMS_IMAGE_WIDTHS,
|
|
36
36
|
CMS_LINE_HEIGHTS: () => CMS_LINE_HEIGHTS,
|
|
37
37
|
CMS_PARAGRAPH_SPACINGS: () => CMS_PARAGRAPH_SPACINGS,
|
|
38
|
+
ContentPolicyCheckSchema: () => ContentPolicyCheckSchema,
|
|
39
|
+
ContentPolicyFieldNameSchema: () => ContentPolicyFieldNameSchema,
|
|
40
|
+
ContentPolicyInternalPathSchema: () => ContentPolicyInternalPathSchema,
|
|
41
|
+
ContentPolicyScalarCheckSchema: () => ContentPolicyScalarCheckSchema,
|
|
42
|
+
ContentPolicyScalarSchema: () => ContentPolicyScalarSchema,
|
|
43
|
+
ContentPolicySchema: () => ContentPolicySchema,
|
|
44
|
+
ContentPolicyWhenSchema: () => ContentPolicyWhenSchema,
|
|
45
|
+
ContentQueryFilterSchema: () => ContentQueryFilterSchema,
|
|
46
|
+
ContentQuerySchema: () => ContentQuerySchema,
|
|
38
47
|
DEFAULT_SITE_PATTERN_PRESENTATION: () => DEFAULT_SITE_PATTERN_PRESENTATION,
|
|
39
48
|
EMPTY_SITE_PATTERN_PLACEMENTS: () => EMPTY_SITE_PATTERN_PLACEMENTS,
|
|
40
49
|
FIELD_BINDING_SOURCE: () => FIELD_BINDING_SOURCE,
|
|
@@ -72,10 +81,16 @@ __export(src_exports, {
|
|
|
72
81
|
cmsImageWidth: () => cmsImageWidth,
|
|
73
82
|
cmsLineHeight: () => cmsLineHeight,
|
|
74
83
|
cmsParagraphSpacing: () => cmsParagraphSpacing,
|
|
84
|
+
collectContentPolicyFieldRefs: () => collectContentPolicyFieldRefs,
|
|
85
|
+
collectContentQueryFieldRefs: () => collectContentQueryFieldRefs,
|
|
86
|
+
contentPolicyBodyText: () => contentPolicyBodyText,
|
|
87
|
+
contentPolicySafeHttpsUrl: () => contentPolicySafeHttpsUrl,
|
|
75
88
|
createFieldBindingResolver: () => createFieldBindingResolver,
|
|
76
89
|
emptyPatternSlots: () => emptyPatternSlots,
|
|
77
90
|
ensureImportedHtmlDocumentRoots: () => ensureImportedHtmlDocumentRoots,
|
|
78
91
|
ensureStableIds: () => ensureStableIds,
|
|
92
|
+
evaluateContentPolicy: () => evaluateContentPolicy,
|
|
93
|
+
evaluateContentQuery: () => evaluateContentQuery,
|
|
79
94
|
extractFaqEntries: () => extractFaqEntries,
|
|
80
95
|
faqPageJsonLd: () => faqPageJsonLd,
|
|
81
96
|
findInternalContentLinkTokens: () => findInternalContentLinkTokens,
|
|
@@ -92,12 +107,15 @@ __export(src_exports, {
|
|
|
92
107
|
isAllowedMapEmbedSrc: () => isAllowedMapEmbedSrc,
|
|
93
108
|
isInCollectionMode: () => isInCollectionMode,
|
|
94
109
|
isProtectedRedirectPath: () => isProtectedRedirectPath,
|
|
110
|
+
isReservedCategoryTreeSlug: () => isReservedCategoryTreeSlug,
|
|
95
111
|
isSectionContentPropPath: () => isSectionContentPropPath,
|
|
96
112
|
isSitePatternSlotKey: () => isSitePatternSlotKey,
|
|
97
113
|
isValidBlock: () => isValidBlock,
|
|
98
114
|
isValidSitePatternKey: () => isValidSitePatternKey,
|
|
99
115
|
issuePreviewToken: () => issuePreviewToken,
|
|
116
|
+
matchesContentScalarCheck: () => matchesContentScalarCheck,
|
|
100
117
|
mergeMetadataPatch: () => mergeMetadataPatch,
|
|
118
|
+
metadataBodyParagraphs: () => metadataBodyParagraphs,
|
|
101
119
|
metadataContentImageUrl: () => metadataContentImageUrl,
|
|
102
120
|
normalizeEmbed: () => normalizeEmbed,
|
|
103
121
|
normalizeHexColor: () => normalizeHexColor,
|
|
@@ -117,11 +135,13 @@ __export(src_exports, {
|
|
|
117
135
|
renderInternalContentLinksInText: () => renderInternalContentLinksInText,
|
|
118
136
|
resolveCategoryPath: () => resolveCategoryPath,
|
|
119
137
|
resolveCategoryTreeChain: () => resolveCategoryTreeChain,
|
|
138
|
+
resolveCategoryTreeEntryCategory: () => resolveCategoryTreeEntryCategory,
|
|
120
139
|
resolveCategoryTreeEntryPath: () => resolveCategoryTreeEntryPath,
|
|
121
140
|
resolveCategoryTreeHubPaths: () => resolveCategoryTreeHubPaths,
|
|
122
141
|
resolveCategoryTreeSelection: () => resolveCategoryTreeSelection,
|
|
123
142
|
resolveMetadata: () => resolveMetadata,
|
|
124
143
|
resolvePatternSlots: () => resolvePatternSlots,
|
|
144
|
+
resolvePostBodySource: () => resolvePostBodySource,
|
|
125
145
|
resolvePostCategorySlug: () => resolvePostCategorySlug,
|
|
126
146
|
resolvePostCollection: () => resolvePostCollection,
|
|
127
147
|
resolvePostPath: () => resolvePostPath,
|
|
@@ -131,6 +151,7 @@ __export(src_exports, {
|
|
|
131
151
|
sanitizeImportedCss: () => sanitizeImportedCss,
|
|
132
152
|
sanitizeImportedHtml: () => sanitizeImportedHtml,
|
|
133
153
|
scopeImportedCss: () => scopeImportedCss,
|
|
154
|
+
selectContentQuery: () => selectContentQuery,
|
|
134
155
|
serializeBlock: () => serializeBlock,
|
|
135
156
|
setSitePatternPlacementRule: () => setSitePatternPlacementRule,
|
|
136
157
|
sitePatternButtonColor: () => sitePatternButtonColor,
|
|
@@ -367,6 +388,9 @@ function validateCollections(collections) {
|
|
|
367
388
|
}
|
|
368
389
|
|
|
369
390
|
// src/category-tree.ts
|
|
391
|
+
function isReservedCategoryTreeSlug(presentation, slug) {
|
|
392
|
+
return slug === presentation.categoryFallbackSlug || presentation.reservedCategorySlugs?.includes(slug) === true;
|
|
393
|
+
}
|
|
370
394
|
function basePathPrefix(basePath) {
|
|
371
395
|
if (!basePath.trim()) return null;
|
|
372
396
|
return basePath.replace(/\/+$/, "");
|
|
@@ -395,6 +419,7 @@ function hasChildCategory(termId, terms) {
|
|
|
395
419
|
function resolveCategoryTreeChain(presentation, termId, terms) {
|
|
396
420
|
const chain = categoryTreeAncestry(presentation.categoryDepth, termId, terms);
|
|
397
421
|
if (!chain) return null;
|
|
422
|
+
if (chain.some((term) => isReservedCategoryTreeSlug(presentation, term.slug))) return null;
|
|
398
423
|
if (presentation.entryCategory === "leaf") return chain;
|
|
399
424
|
if (hasChildCategory(termId, terms)) return null;
|
|
400
425
|
return chain.length === presentation.categoryDepth ? chain : null;
|
|
@@ -403,6 +428,33 @@ function resolveCategoryTreeSelection(presentation, termId, terms) {
|
|
|
403
428
|
if (hasChildCategory(termId, terms)) return null;
|
|
404
429
|
return resolveCategoryTreeChain(presentation, termId, terms);
|
|
405
430
|
}
|
|
431
|
+
function resolveCategoryTreeEntryCategory(presentation, entry, terms) {
|
|
432
|
+
const termsById = new Map(terms.map((term) => [term.id, term]));
|
|
433
|
+
const first = presentation.categorySelection === "first";
|
|
434
|
+
const categoryIds = [...new Set(entry.terms.filter((term) => term.taxonomy === "category" && (first || termsById.has(term.id))).map((term) => term.id))];
|
|
435
|
+
if (first) {
|
|
436
|
+
if (termsById.size !== terms.length) return null;
|
|
437
|
+
if (categoryIds.length === 0) {
|
|
438
|
+
const slug = presentation.categoryFallbackSlug;
|
|
439
|
+
if (presentation.categoryDepth !== 1 || !slug || presentation.reservedCategorySlugs?.includes(slug) || terms.some((term) => term.slug === slug)) return null;
|
|
440
|
+
return { kind: "fallback", slug };
|
|
441
|
+
}
|
|
442
|
+
const candidates = [];
|
|
443
|
+
for (const id of categoryIds) {
|
|
444
|
+
const category2 = termsById.get(id);
|
|
445
|
+
if (!category2 || !Number.isInteger(category2.termOrder ?? 0)) return null;
|
|
446
|
+
const chain2 = resolveCategoryTreeChain(presentation, id, terms);
|
|
447
|
+
if (!chain2) return null;
|
|
448
|
+
candidates.push({ category: category2, chain: chain2 });
|
|
449
|
+
}
|
|
450
|
+
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));
|
|
451
|
+
return { kind: "category", ...candidates[0] };
|
|
452
|
+
}
|
|
453
|
+
if (categoryIds.length !== 1) return null;
|
|
454
|
+
const category = termsById.get(categoryIds[0]);
|
|
455
|
+
const chain = resolveCategoryTreeChain(presentation, category.id, terms);
|
|
456
|
+
return chain ? { kind: "category", category, chain } : null;
|
|
457
|
+
}
|
|
406
458
|
function resolveCategoryTreeHubPaths(presentation, termId, terms) {
|
|
407
459
|
const chain = resolveCategoryTreeChain(presentation, termId, terms);
|
|
408
460
|
const prefix = basePathPrefix(presentation.basePath);
|
|
@@ -417,20 +469,11 @@ function resolveCategoryTreeHubPaths(presentation, termId, terms) {
|
|
|
417
469
|
}
|
|
418
470
|
function resolveCategoryTreeEntryPath(presentation, entry, terms) {
|
|
419
471
|
if (!entry.slug) return null;
|
|
420
|
-
const
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
];
|
|
426
|
-
if (categoryIds.length !== 1) return null;
|
|
427
|
-
const hubs = resolveCategoryTreeHubPaths(
|
|
428
|
-
presentation,
|
|
429
|
-
categoryIds[0],
|
|
430
|
-
terms
|
|
431
|
-
);
|
|
432
|
-
const leafPath = hubs?.at(-1);
|
|
433
|
-
return leafPath ? `${leafPath}/${encodeURIComponent(entry.slug)}` : null;
|
|
472
|
+
const selected = resolveCategoryTreeEntryCategory(presentation, entry, terms);
|
|
473
|
+
const prefix = basePathPrefix(presentation.basePath);
|
|
474
|
+
if (!selected || prefix === null) return null;
|
|
475
|
+
const slugs = selected.kind === "fallback" ? [selected.slug] : selected.chain.map((term) => term.slug);
|
|
476
|
+
return `${prefix}/${[...slugs, entry.slug].map(encodeURIComponent).join("/")}`;
|
|
434
477
|
}
|
|
435
478
|
|
|
436
479
|
// src/public-path.ts
|
|
@@ -1115,10 +1158,10 @@ function scopeImportedCss(css, scope, allowedAssetUrls = []) {
|
|
|
1115
1158
|
|
|
1116
1159
|
// src/internal-content-links.ts
|
|
1117
1160
|
var INTERNAL_CONTENT_LINK_TOKEN_SOURCE = String.raw`\[\[internal:([a-z0-9가-힣]+(?:[.-][a-z0-9가-힣]+)*)\|([^\]\n|]+)\]\]`;
|
|
1118
|
-
function findInternalContentLinkTokens(
|
|
1161
|
+
function findInternalContentLinkTokens(text3) {
|
|
1119
1162
|
const pattern = new RegExp(INTERNAL_CONTENT_LINK_TOKEN_SOURCE, "giu");
|
|
1120
1163
|
const tokens = [];
|
|
1121
|
-
for (const match of
|
|
1164
|
+
for (const match of text3.matchAll(pattern)) {
|
|
1122
1165
|
const [raw, key, label] = match;
|
|
1123
1166
|
if (!key || !label || match.index === void 0) continue;
|
|
1124
1167
|
tokens.push({
|
|
@@ -1177,19 +1220,19 @@ function escapeHtmlText(value) {
|
|
|
1177
1220
|
var INTERNAL_CONTENT_LINK_CLASS = "rt-internal-link";
|
|
1178
1221
|
var INTERNAL_CONTENT_LINK_PENDING_CLASS = "rt-internal-link--pending";
|
|
1179
1222
|
var INTERNAL_CONTENT_LINK_PENDING_ATTR = "data-rt-internal-link-pending";
|
|
1180
|
-
function renderInternalContentLinksInText(
|
|
1181
|
-
const tokens = findInternalContentLinkTokens(
|
|
1182
|
-
if (tokens.length === 0) return { html:
|
|
1223
|
+
function renderInternalContentLinksInText(text3, resolve) {
|
|
1224
|
+
const tokens = findInternalContentLinkTokens(text3);
|
|
1225
|
+
if (tokens.length === 0) return { html: text3, hasTokens: false };
|
|
1183
1226
|
let cursor = 0;
|
|
1184
1227
|
let html = "";
|
|
1185
1228
|
for (const token of tokens) {
|
|
1186
|
-
html +=
|
|
1229
|
+
html += text3.slice(cursor, token.index);
|
|
1187
1230
|
const path = resolve(token.key);
|
|
1188
1231
|
const label = escapeHtmlText(token.label);
|
|
1189
1232
|
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>`;
|
|
1190
1233
|
cursor = token.index + token.raw.length;
|
|
1191
1234
|
}
|
|
1192
|
-
html +=
|
|
1235
|
+
html += text3.slice(cursor);
|
|
1193
1236
|
return { html, hasTokens: true };
|
|
1194
1237
|
}
|
|
1195
1238
|
var INTERNAL_LINK_BLOCKED_HTML_TAGS = /* @__PURE__ */ new Set([
|
|
@@ -1223,12 +1266,12 @@ function renderInternalContentLinksInHtml(html, resolve) {
|
|
|
1223
1266
|
let cursor = 0;
|
|
1224
1267
|
let out = "";
|
|
1225
1268
|
let hasTokens = false;
|
|
1226
|
-
const emitText = (
|
|
1227
|
-
if (blockedDepth > 0 || !
|
|
1228
|
-
out +=
|
|
1269
|
+
const emitText = (text3) => {
|
|
1270
|
+
if (blockedDepth > 0 || !text3) {
|
|
1271
|
+
out += text3;
|
|
1229
1272
|
return;
|
|
1230
1273
|
}
|
|
1231
|
-
const rendered = renderInternalContentLinksInText(
|
|
1274
|
+
const rendered = renderInternalContentLinksInText(text3, resolve);
|
|
1232
1275
|
hasTokens ||= rendered.hasTokens;
|
|
1233
1276
|
out += rendered.html;
|
|
1234
1277
|
};
|
|
@@ -1948,13 +1991,13 @@ function findSectionLockViolations(beforeDoc, afterDoc) {
|
|
|
1948
1991
|
...Object.keys(next.attrs)
|
|
1949
1992
|
]);
|
|
1950
1993
|
fields.delete("props");
|
|
1951
|
-
for (const
|
|
1952
|
-
if (!valueEquals(section.attrs[
|
|
1994
|
+
for (const field3 of fields) {
|
|
1995
|
+
if (!valueEquals(section.attrs[field3], next.attrs[field3])) {
|
|
1953
1996
|
violations.push({
|
|
1954
|
-
code:
|
|
1997
|
+
code: field3 === "lock" ? "locked_section_lock_changed" : "locked_section_structure_changed",
|
|
1955
1998
|
sectionId: section.id,
|
|
1956
1999
|
sectionType: section.sectionType,
|
|
1957
|
-
field
|
|
2000
|
+
field: field3
|
|
1958
2001
|
});
|
|
1959
2002
|
}
|
|
1960
2003
|
}
|
|
@@ -2012,11 +2055,11 @@ function imageUrl(value, base) {
|
|
|
2012
2055
|
function resolveMetadata(input) {
|
|
2013
2056
|
const layers = [input.overrides, input.defaults, input.siteDefaults, input.fallback];
|
|
2014
2057
|
const result = {};
|
|
2015
|
-
for (const
|
|
2058
|
+
for (const field3 of ["title", "description", "ogImageAlt"]) {
|
|
2016
2059
|
for (const layer of layers) {
|
|
2017
|
-
const value = text(layer?.[
|
|
2060
|
+
const value = text(layer?.[field3]);
|
|
2018
2061
|
if (value !== void 0) {
|
|
2019
|
-
result[
|
|
2062
|
+
result[field3] = value;
|
|
2020
2063
|
break;
|
|
2021
2064
|
}
|
|
2022
2065
|
}
|
|
@@ -2028,11 +2071,11 @@ function resolveMetadata(input) {
|
|
|
2028
2071
|
break;
|
|
2029
2072
|
}
|
|
2030
2073
|
}
|
|
2031
|
-
for (const
|
|
2074
|
+
for (const field3 of ["noindex", "nofollow"]) {
|
|
2032
2075
|
for (const layer of layers) {
|
|
2033
|
-
const value = layer?.[
|
|
2076
|
+
const value = layer?.[field3];
|
|
2034
2077
|
if (typeof value === "boolean") {
|
|
2035
|
-
result[
|
|
2078
|
+
result[field3] = value;
|
|
2036
2079
|
break;
|
|
2037
2080
|
}
|
|
2038
2081
|
}
|
|
@@ -2079,87 +2122,154 @@ var MetadataPatchSchema = import_zod2.z.object({
|
|
|
2079
2122
|
}).strict();
|
|
2080
2123
|
var MetadataRobotsPolicySchema = import_zod2.z.object({
|
|
2081
2124
|
forceNoindex: import_zod2.z.boolean().optional(),
|
|
2082
|
-
forceNofollow: import_zod2.z.boolean().optional()
|
|
2125
|
+
forceNofollow: import_zod2.z.boolean().optional(),
|
|
2126
|
+
/** Stable policy reason codes. They explain an enforced exclusion, never relax it. */
|
|
2127
|
+
noindexReasons: import_zod2.z.array(import_zod2.z.string().trim().min(1).max(200)).max(64).optional()
|
|
2083
2128
|
}).strict();
|
|
2084
2129
|
function readMetadataValues(input) {
|
|
2085
2130
|
if (!input || typeof input !== "object" || Array.isArray(input)) return {};
|
|
2086
|
-
const
|
|
2131
|
+
const record2 = input;
|
|
2087
2132
|
const output = {};
|
|
2088
|
-
for (const
|
|
2089
|
-
const parsed = metadataFields[
|
|
2090
|
-
if (parsed.success) output[
|
|
2133
|
+
for (const field3 of METADATA_FIELDS) {
|
|
2134
|
+
const parsed = metadataFields[field3].safeParse(record2[field3]);
|
|
2135
|
+
if (parsed.success) output[field3] = parsed.data;
|
|
2091
2136
|
}
|
|
2092
2137
|
return MetadataValuesSchema.parse(output);
|
|
2093
2138
|
}
|
|
2094
2139
|
function mergeMetadataPatch(current, input) {
|
|
2095
2140
|
const patch = MetadataPatchSchema.parse(input);
|
|
2096
2141
|
const next = { ...current };
|
|
2097
|
-
for (const
|
|
2098
|
-
if (patch[
|
|
2099
|
-
else if (patch[
|
|
2142
|
+
for (const field3 of METADATA_FIELDS) {
|
|
2143
|
+
if (patch[field3] === null) delete next[field3];
|
|
2144
|
+
else if (patch[field3] !== void 0) next[field3] = patch[field3];
|
|
2100
2145
|
}
|
|
2101
2146
|
return next;
|
|
2102
2147
|
}
|
|
2103
2148
|
|
|
2104
2149
|
// src/metadata-defaults.ts
|
|
2150
|
+
var import_zod4 = require("zod");
|
|
2151
|
+
|
|
2152
|
+
// src/metadata-text.ts
|
|
2105
2153
|
var import_zod3 = require("zod");
|
|
2106
2154
|
var fallbackText = import_zod3.z.string().max(2e3);
|
|
2107
|
-
var
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
return false;
|
|
2113
|
-
}
|
|
2114
|
-
}, "\uC774\uBBF8\uC9C0 \uD5C8\uC6A9 \uBAA9\uB85D\uC5D0\uB294 \uACBD\uB85C \uC5C6\uB294 HTTPS origin\uB9CC \uC120\uC5B8\uD569\uB2C8\uB2E4");
|
|
2155
|
+
var templateToken = /\{\{(?:title|field:[a-zA-Z][a-zA-Z0-9_-]{0,159})\}\}/gu;
|
|
2156
|
+
var template = fallbackText.refine(
|
|
2157
|
+
(value) => !/[{}]/u.test(value.replace(templateToken, "")),
|
|
2158
|
+
"\uC790\uB3D9 \uD14D\uC2A4\uD2B8\uC5D0\uB294 {{title}} \uB610\uB294 {{field:\uD544\uB4DC\uC774\uB984}} \uD1A0\uD070\uB9CC \uC0AC\uC6A9\uD569\uB2C8\uB2E4"
|
|
2159
|
+
);
|
|
2115
2160
|
var MetadataTextRuleSchema = import_zod3.z.object({
|
|
2116
|
-
source: import_zod3.z.enum(["content", "fallback"]).optional(),
|
|
2161
|
+
source: import_zod3.z.enum(["content", "fallback", "template"]).optional(),
|
|
2162
|
+
template: template.optional(),
|
|
2117
2163
|
stripLeadingLabel: import_zod3.z.enum(["question", "answer"]).optional(),
|
|
2118
2164
|
replaceLineBreakTokens: import_zod3.z.boolean().optional(),
|
|
2119
2165
|
removeEmDashes: import_zod3.z.boolean().optional(),
|
|
2120
2166
|
normalizeWhitespace: import_zod3.z.boolean().optional(),
|
|
2121
2167
|
maxLength: import_zod3.z.number().int().min(1).max(2e3).optional(),
|
|
2168
|
+
lengthUnit: import_zod3.z.enum(["code-units", "code-points"]).optional(),
|
|
2169
|
+
wordBoundaryMinLength: import_zod3.z.number().int().min(1).max(2e3).optional(),
|
|
2122
2170
|
ellipsis: import_zod3.z.string().max(10).optional(),
|
|
2171
|
+
bodyFallback: import_zod3.z.object({
|
|
2172
|
+
preferContaining: import_zod3.z.string().trim().min(1).max(100).optional(),
|
|
2173
|
+
stripLeadingLabel: import_zod3.z.literal("conclusion").optional()
|
|
2174
|
+
}).strict().optional(),
|
|
2123
2175
|
fallback: fallbackText.optional(),
|
|
2124
2176
|
fallbackWithParent: fallbackText.optional(),
|
|
2125
2177
|
fallbackWithoutParent: fallbackText.optional(),
|
|
2126
2178
|
fallbackBySlug: import_zod3.z.record(import_zod3.z.string().max(200), fallbackText).optional()
|
|
2127
|
-
}).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");
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2179
|
+
}).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");
|
|
2180
|
+
function cleanMetadataText(value, rule) {
|
|
2181
|
+
let text3 = value.trim();
|
|
2182
|
+
if (rule?.stripLeadingLabel === "question") text3 = text3.replace(/^\s*Q[..:]?\s*/iu, "");
|
|
2183
|
+
if (rule?.stripLeadingLabel === "answer") text3 = text3.replace(/^\s*A[..:]?\s*/iu, "");
|
|
2184
|
+
if (rule?.replaceLineBreakTokens) text3 = text3.replace(/\[\[br(?::[^\]]*)?\]\]/giu, " ");
|
|
2185
|
+
if (rule?.removeEmDashes) text3 = text3.replaceAll("\u2014", "");
|
|
2186
|
+
if (rule?.normalizeWhitespace) text3 = text3.replace(/\s+/gu, " ");
|
|
2187
|
+
return text3.trim();
|
|
2188
|
+
}
|
|
2189
|
+
function metadataBodyParagraphs(value) {
|
|
2190
|
+
const paragraphs = [];
|
|
2191
|
+
const textParts = (node) => {
|
|
2192
|
+
if (Array.isArray(node)) return node.flatMap(textParts);
|
|
2193
|
+
if (!node || typeof node !== "object") return [];
|
|
2194
|
+
const item = node;
|
|
2195
|
+
return [...typeof item.text === "string" ? [item.text] : [], ...textParts(item.content)];
|
|
2196
|
+
};
|
|
2197
|
+
const walk = (node) => {
|
|
2198
|
+
if (Array.isArray(node)) {
|
|
2199
|
+
node.forEach(walk);
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
if (!node || typeof node !== "object") return;
|
|
2203
|
+
const item = node;
|
|
2204
|
+
if (item.type === "paragraph") {
|
|
2205
|
+
const text3 = textParts(item.content).join(" ").replace(/\s+/gu, " ").trim();
|
|
2206
|
+
if (text3) paragraphs.push(text3);
|
|
2207
|
+
} else walk(item.content);
|
|
2208
|
+
};
|
|
2209
|
+
walk(value);
|
|
2210
|
+
return paragraphs;
|
|
2211
|
+
}
|
|
2212
|
+
function formatMetadataText(value, rule, input, name) {
|
|
2213
|
+
let text3 = rule?.source === "fallback" ? "" : cleanMetadataText(value ?? "", rule);
|
|
2214
|
+
if (rule?.source === "template") {
|
|
2215
|
+
text3 = cleanMetadataText((rule.template ?? "").replace(templateToken, (token) => {
|
|
2216
|
+
if (token === "{{title}}") return name;
|
|
2217
|
+
const field3 = token.slice(8, -2);
|
|
2218
|
+
const value2 = Object.hasOwn(input.fields ?? {}, field3) ? input.fields?.[field3] : void 0;
|
|
2219
|
+
return typeof value2 === "string" ? value2.trim() : "";
|
|
2220
|
+
}), rule);
|
|
2221
|
+
}
|
|
2222
|
+
if (!text3 && rule?.bodyFallback) {
|
|
2223
|
+
const paragraphs = input.bodyParagraphs ?? [];
|
|
2224
|
+
const preferred = rule.bodyFallback.preferContaining;
|
|
2225
|
+
text3 = (preferred ? paragraphs.find((paragraph) => paragraph.includes(preferred)) : void 0) ?? paragraphs[0] ?? "";
|
|
2226
|
+
if (rule.bodyFallback.stripLeadingLabel === "conclusion") text3 = text3.replace(/^(먼저\s*)?결론부터\s*(말씀드리면|말씀드릴게요)[.!?]?\s*/u, "");
|
|
2227
|
+
text3 = cleanMetadataText(text3, rule);
|
|
2228
|
+
}
|
|
2229
|
+
if (!text3) {
|
|
2230
|
+
const slugFallback = input.slug && Object.hasOwn(rule?.fallbackBySlug ?? {}, input.slug) ? rule?.fallbackBySlug?.[input.slug] : void 0;
|
|
2231
|
+
const fallback = input.parentId ? rule?.fallbackWithParent ?? rule?.fallback : slugFallback ?? rule?.fallbackWithoutParent ?? rule?.fallback;
|
|
2232
|
+
text3 = cleanMetadataText((fallback ?? "").replaceAll("%s", () => name), rule);
|
|
2233
|
+
}
|
|
2234
|
+
if (!text3) return void 0;
|
|
2235
|
+
const units = rule?.lengthUnit === "code-points" ? [...text3] : text3.split("");
|
|
2236
|
+
if (rule?.maxLength && units.length > rule.maxLength) {
|
|
2157
2237
|
const suffix = rule.ellipsis ?? "";
|
|
2158
|
-
const
|
|
2238
|
+
const suffixLength = rule.lengthUnit === "code-points" ? [...suffix].length : suffix.length;
|
|
2239
|
+
let head = units.slice(0, rule.maxLength - suffixLength).join("");
|
|
2240
|
+
const boundary = head.lastIndexOf(" ");
|
|
2241
|
+
if (rule.wordBoundaryMinLength && boundary >= rule.wordBoundaryMinLength) head = head.slice(0, boundary);
|
|
2159
2242
|
return suffix ? head.trimEnd() + suffix : head;
|
|
2160
2243
|
}
|
|
2161
|
-
return
|
|
2244
|
+
return text3;
|
|
2162
2245
|
}
|
|
2246
|
+
|
|
2247
|
+
// src/metadata-defaults.ts
|
|
2248
|
+
var imageOrigin = import_zod4.z.string().max(2048).refine((value) => {
|
|
2249
|
+
try {
|
|
2250
|
+
const url = new URL(value);
|
|
2251
|
+
return url.protocol === "https:" && url.origin === value;
|
|
2252
|
+
} catch {
|
|
2253
|
+
return false;
|
|
2254
|
+
}
|
|
2255
|
+
}, "\uC774\uBBF8\uC9C0 \uD5C8\uC6A9 \uBAA9\uB85D\uC5D0\uB294 \uACBD\uB85C \uC5C6\uB294 HTTPS origin\uB9CC \uC120\uC5B8\uD569\uB2C8\uB2E4");
|
|
2256
|
+
var MetadataDefaultRulesSchema = import_zod4.z.object({
|
|
2257
|
+
title: MetadataTextRuleSchema.optional(),
|
|
2258
|
+
titleTemplateMaxLength: import_zod4.z.number().int().min(1).max(1e3).optional(),
|
|
2259
|
+
description: MetadataTextRuleSchema.optional(),
|
|
2260
|
+
/** Code-owned defaults precede site defaults but always follow manual overrides. */
|
|
2261
|
+
defaults: MetadataValuesSchema.optional(),
|
|
2262
|
+
robots: MetadataRobotsPolicySchema.optional(),
|
|
2263
|
+
useFeaturedImage: import_zod4.z.boolean().optional(),
|
|
2264
|
+
useFirstBodyImage: import_zod4.z.boolean().optional(),
|
|
2265
|
+
useTermImage: import_zod4.z.boolean().optional(),
|
|
2266
|
+
imageAltSource: import_zod4.z.enum(["content", "title"]).optional(),
|
|
2267
|
+
legacyImageSource: import_zod4.z.literal("legacy-thumbnail").optional(),
|
|
2268
|
+
imageAllowedOrigins: import_zod4.z.array(imageOrigin).min(1).max(32).optional(),
|
|
2269
|
+
legacyImageAllowedOrigins: import_zod4.z.array(imageOrigin).min(1).max(32).optional(),
|
|
2270
|
+
normalizeCloudflareImages: import_zod4.z.boolean().optional(),
|
|
2271
|
+
fallback: MetadataValuesSchema.optional()
|
|
2272
|
+
}).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");
|
|
2163
2273
|
function metadataContentImageUrl(value, normalizeCloudflareImages = false) {
|
|
2164
2274
|
if (!normalizeCloudflareImages) return value;
|
|
2165
2275
|
try {
|
|
@@ -2199,20 +2309,328 @@ function contentCandidateAllowed(value, input, rule) {
|
|
|
2199
2309
|
function formatMetadataDefaults(input, declaration) {
|
|
2200
2310
|
const dynamic = declaration[input.kind];
|
|
2201
2311
|
const rule = dynamic?.defaultRules?.[input.modelKey];
|
|
2202
|
-
const name =
|
|
2203
|
-
const title =
|
|
2204
|
-
const
|
|
2205
|
-
const
|
|
2312
|
+
const name = cleanMetadataText(input.title, rule?.title);
|
|
2313
|
+
const title = formatMetadataText(input.title, rule?.title, input, name);
|
|
2314
|
+
const template2 = dynamic?.titleTemplates?.[input.modelKey];
|
|
2315
|
+
const templatedTitle = title && template2 ? template2.replace("%s", () => title) : title;
|
|
2316
|
+
const finalTitle = templatedTitle && rule?.titleTemplateMaxLength && templatedTitle.length > rule.titleTemplateMaxLength ? title : templatedTitle;
|
|
2317
|
+
const description = formatMetadataText(input.description, rule?.description, input, name);
|
|
2206
2318
|
const bodyImage = input.firstBodyImage && (input.firstBodyImage.startsWith("https://") || /^\/(?!\/)/u.test(input.firstBodyImage)) ? input.firstBodyImage : void 0;
|
|
2207
|
-
const
|
|
2319
|
+
const featuredImage = (input.kind === "terms" ? rule?.useTermImage : rule?.useFeaturedImage) === false ? void 0 : input.featuredImage;
|
|
2320
|
+
const candidateImage = featuredImage || (rule?.useFirstBodyImage ? bodyImage : void 0) || legacyImageUrl(input.legacyImage, rule);
|
|
2208
2321
|
const ogImage = candidateImage && contentCandidateAllowed(candidateImage, input, rule) ? readMetadataValues({ ogImage: metadataContentImageUrl(candidateImage, rule?.normalizeCloudflareImages) }).ogImage : void 0;
|
|
2209
2322
|
return {
|
|
2210
|
-
...
|
|
2323
|
+
...readMetadataValues(rule?.defaults),
|
|
2324
|
+
...finalTitle ? { title: finalTitle } : {},
|
|
2211
2325
|
...description ? { description } : {},
|
|
2212
2326
|
...ogImage ? { ogImage } : {}
|
|
2213
2327
|
};
|
|
2214
2328
|
}
|
|
2215
2329
|
|
|
2330
|
+
// src/post-body-source.ts
|
|
2331
|
+
function resolvePostBodySource(bodyHtml) {
|
|
2332
|
+
if (typeof bodyHtml !== "string") return { kind: "json" };
|
|
2333
|
+
const trimmed = bodyHtml.trim();
|
|
2334
|
+
if (trimmed.length === 0) return { kind: "json" };
|
|
2335
|
+
const sanitized = sanitizeHtml(trimmed);
|
|
2336
|
+
const visible = /<(?:img|hr|br|iframe|video|audio)\b/i.test(sanitized) || sanitized.replace(/<[^>]*>/g, "").replace(/ /gi, " ").trim().length > 0;
|
|
2337
|
+
return visible ? { kind: "html", html: sanitized } : { kind: "json" };
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
// src/content-policy-schema.ts
|
|
2341
|
+
var import_zod5 = require("zod");
|
|
2342
|
+
var ContentPolicyFieldNameSchema = import_zod5.z.string().min(1).max(64).regex(/^[A-Za-z_][A-Za-z0-9_]*$/).refine((name) => name !== "prototype" && !Object.hasOwn(Object.prototype, name));
|
|
2343
|
+
var ContentPolicyScalarSchema = import_zod5.z.union([
|
|
2344
|
+
import_zod5.z.string().max(2048),
|
|
2345
|
+
import_zod5.z.number().finite(),
|
|
2346
|
+
import_zod5.z.boolean(),
|
|
2347
|
+
import_zod5.z.null()
|
|
2348
|
+
]);
|
|
2349
|
+
var ContentPolicyWhenSchema = import_zod5.z.object({
|
|
2350
|
+
field: ContentPolicyFieldNameSchema,
|
|
2351
|
+
equals: ContentPolicyScalarSchema
|
|
2352
|
+
}).strict();
|
|
2353
|
+
var ContentPolicyInternalPathSchema = import_zod5.z.string().min(1).max(2048).refine((path) => {
|
|
2354
|
+
if (!path.startsWith("/") || path.startsWith("//") || /[\\?#\u0000-\u0020\u007f]/u.test(path)) return false;
|
|
2355
|
+
try {
|
|
2356
|
+
return path.split("/").every((part, index) => {
|
|
2357
|
+
if (index === 0) return true;
|
|
2358
|
+
const decoded2 = decodeURIComponent(part);
|
|
2359
|
+
return decoded2 !== "." && decoded2 !== ".." && !/[\\/\u0000-\u0020\u007f]/u.test(decoded2);
|
|
2360
|
+
});
|
|
2361
|
+
} catch {
|
|
2362
|
+
return false;
|
|
2363
|
+
}
|
|
2364
|
+
}, "\uCF58\uD150\uCE20 \uACBD\uB85C\uB294 \uC548\uC804\uD55C \uC0AC\uC774\uD2B8 \uB0B4\uBD80 \uACBD\uB85C\uC5EC\uC57C \uD569\uB2C8\uB2E4");
|
|
2365
|
+
var field = { field: ContentPolicyFieldNameSchema };
|
|
2366
|
+
var scalarChecks = [
|
|
2367
|
+
import_zod5.z.object({ ...field, op: import_zod5.z.literal("equals"), value: ContentPolicyScalarSchema }).strict(),
|
|
2368
|
+
import_zod5.z.object({ ...field, op: import_zod5.z.literal("notEquals"), value: ContentPolicyScalarSchema }).strict(),
|
|
2369
|
+
import_zod5.z.object({ ...field, op: import_zod5.z.literal("oneOf"), values: import_zod5.z.array(ContentPolicyScalarSchema).min(1).max(16), allowEmpty: import_zod5.z.boolean().optional() }).strict(),
|
|
2370
|
+
import_zod5.z.object({ ...field, op: import_zod5.z.literal("text") }).strict(),
|
|
2371
|
+
import_zod5.z.object({ ...field, op: import_zod5.z.literal("date") }).strict(),
|
|
2372
|
+
import_zod5.z.object({ ...field, op: import_zod5.z.literal("safeHttps") }).strict(),
|
|
2373
|
+
import_zod5.z.object({ ...field, op: import_zod5.z.literal("allowedPath"), paths: import_zod5.z.array(ContentPolicyInternalPathSchema).min(1).max(512) }).strict()
|
|
2374
|
+
];
|
|
2375
|
+
var ContentPolicyScalarCheckSchema = import_zod5.z.discriminatedUnion("op", scalarChecks);
|
|
2376
|
+
var when = { when: ContentPolicyWhenSchema.optional() };
|
|
2377
|
+
var ContentPolicyCheckSchema = import_zod5.z.discriminatedUnion("op", [
|
|
2378
|
+
scalarChecks[0].extend(when),
|
|
2379
|
+
scalarChecks[1].extend(when),
|
|
2380
|
+
scalarChecks[2].extend(when),
|
|
2381
|
+
scalarChecks[3].extend(when),
|
|
2382
|
+
scalarChecks[4].extend(when),
|
|
2383
|
+
scalarChecks[5].extend(when),
|
|
2384
|
+
scalarChecks[6].extend(when),
|
|
2385
|
+
import_zod5.z.object({ ...field, ...when, op: import_zod5.z.literal("some"), checks: import_zod5.z.array(ContentPolicyScalarCheckSchema).min(1).max(8) }).strict(),
|
|
2386
|
+
import_zod5.z.object({
|
|
2387
|
+
...when,
|
|
2388
|
+
op: import_zod5.z.literal("bodyContent"),
|
|
2389
|
+
allowExcerpt: import_zod5.z.boolean().optional(),
|
|
2390
|
+
allowFeaturedImage: import_zod5.z.boolean().optional(),
|
|
2391
|
+
additionalUrl: import_zod5.z.object({ ...field, ...when }).strict().optional()
|
|
2392
|
+
}).strict(),
|
|
2393
|
+
import_zod5.z.object({ ...when, op: import_zod5.z.literal("bodyText") }).strict()
|
|
2394
|
+
]);
|
|
2395
|
+
var contentPolicy = import_zod5.z.object({
|
|
2396
|
+
visibility: import_zod5.z.array(ContentPolicyCheckSchema).max(24),
|
|
2397
|
+
indexing: import_zod5.z.array(ContentPolicyCheckSchema).max(24),
|
|
2398
|
+
pathSegmentFormat: import_zod5.z.literal("lower-kebab").optional()
|
|
2399
|
+
}).strict();
|
|
2400
|
+
var ContentPolicySchema = contentPolicy.superRefine((policy, ctx) => {
|
|
2401
|
+
const refs = collectContentPolicyFieldRefs(policy);
|
|
2402
|
+
if (refs.length > 64) ctx.addIssue({ code: import_zod5.z.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" });
|
|
2403
|
+
for (const ref of refs) {
|
|
2404
|
+
if ((ref.children?.length ?? 0) > 8) ctx.addIssue({ code: import_zod5.z.ZodIssueCode.custom, message: `\uBC18\uBCF5 \uD544\uB4DC ${ref.name}\uC758 \uC790\uC2DD \uCC38\uC870\uB294 8\uAC1C\uAE4C\uC9C0 \uD5C8\uC6A9\uD569\uB2C8\uB2E4` });
|
|
2405
|
+
}
|
|
2406
|
+
});
|
|
2407
|
+
function collectContentPolicyFieldRefs(policy) {
|
|
2408
|
+
const refs = /* @__PURE__ */ new Map();
|
|
2409
|
+
const add = (name, children = []) => {
|
|
2410
|
+
const names = refs.get(name) ?? /* @__PURE__ */ new Set();
|
|
2411
|
+
for (const child of children) names.add(child);
|
|
2412
|
+
refs.set(name, names);
|
|
2413
|
+
};
|
|
2414
|
+
for (const check of [...policy.visibility, ...policy.indexing]) {
|
|
2415
|
+
if (check.when) add(check.when.field);
|
|
2416
|
+
if ("field" in check) add(check.field, check.op === "some" ? check.checks.map((child) => child.field) : []);
|
|
2417
|
+
if (check.op === "bodyContent" && check.additionalUrl) {
|
|
2418
|
+
add(check.additionalUrl.field);
|
|
2419
|
+
if (check.additionalUrl.when) add(check.additionalUrl.when.field);
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
return [...refs].map(([name, children]) => ({ name, ...children.size ? { children: [...children] } : {} }));
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
// src/content-policy.ts
|
|
2426
|
+
var text2 = (value) => typeof value === "string" ? value.trim() : "";
|
|
2427
|
+
var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2428
|
+
function fieldValue(fields, field3) {
|
|
2429
|
+
return Object.hasOwn(fields, field3) ? fields[field3] : void 0;
|
|
2430
|
+
}
|
|
2431
|
+
function contentPolicySafeHttpsUrl(value) {
|
|
2432
|
+
if (typeof value !== "string" || /[\\\u0000-\u0020\u007f]/u.test(value)) return null;
|
|
2433
|
+
try {
|
|
2434
|
+
const url = new URL(value);
|
|
2435
|
+
return url.protocol === "https:" && !url.username && !url.password ? url.href : null;
|
|
2436
|
+
} catch {
|
|
2437
|
+
return null;
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
function dateValue(value) {
|
|
2441
|
+
if (!value || typeof value !== "string" && typeof value !== "number") return null;
|
|
2442
|
+
if (!Number.isFinite(Date.parse(String(value)))) return null;
|
|
2443
|
+
const date = new Date(value).getTime();
|
|
2444
|
+
return Number.isFinite(date) ? date : null;
|
|
2445
|
+
}
|
|
2446
|
+
function contentPolicyBodyText(bodyJson) {
|
|
2447
|
+
const root = record(bodyJson);
|
|
2448
|
+
if (root?.type !== "doc" || !Array.isArray(root.content)) return "";
|
|
2449
|
+
const pending = [{ node: root, depth: 0 }];
|
|
2450
|
+
const output = [];
|
|
2451
|
+
let count = 0;
|
|
2452
|
+
while (pending.length) {
|
|
2453
|
+
const entry = pending.pop();
|
|
2454
|
+
if (++count > 1e5 || entry.depth > 128) throw new Error("Content policy body exceeds traversal limits");
|
|
2455
|
+
const node = record(entry.node);
|
|
2456
|
+
if (!node) continue;
|
|
2457
|
+
if (node.type === "text") {
|
|
2458
|
+
const value = text2(node.text);
|
|
2459
|
+
if (value) output.push(value);
|
|
2460
|
+
} else if (Array.isArray(node.content)) {
|
|
2461
|
+
for (let index = node.content.length - 1; index >= 0; index--) {
|
|
2462
|
+
pending.push({ node: node.content[index], depth: entry.depth + 1 });
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
return output.join(" ");
|
|
2467
|
+
}
|
|
2468
|
+
function matchesContentScalarCheck(check, fields) {
|
|
2469
|
+
const value = fieldValue(fields, check.field);
|
|
2470
|
+
switch (check.op) {
|
|
2471
|
+
case "equals":
|
|
2472
|
+
return value === check.value;
|
|
2473
|
+
case "notEquals":
|
|
2474
|
+
return value !== check.value;
|
|
2475
|
+
case "oneOf":
|
|
2476
|
+
return check.allowEmpty === true && !value || check.values.some((candidate) => candidate === value);
|
|
2477
|
+
case "text":
|
|
2478
|
+
return text2(value).length > 0;
|
|
2479
|
+
case "date":
|
|
2480
|
+
return dateValue(value) !== null;
|
|
2481
|
+
case "safeHttps":
|
|
2482
|
+
return contentPolicySafeHttpsUrl(value) !== null;
|
|
2483
|
+
case "allowedPath":
|
|
2484
|
+
return check.paths.includes(text2(value));
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
function matchesWhen(when2, fields) {
|
|
2488
|
+
return !when2 || fieldValue(fields, when2.field) === when2.equals;
|
|
2489
|
+
}
|
|
2490
|
+
function bodyFacts(post) {
|
|
2491
|
+
const jsonText = contentPolicyBodyText(post.bodyJson);
|
|
2492
|
+
const source = resolvePostBodySource(post.bodyHtml);
|
|
2493
|
+
const html = source.kind === "html" ? source.html : "";
|
|
2494
|
+
return { jsonText, html, text: source.kind === "html" ? html.replace(/<[^>]*>/g, "").replace(/ /gi, " ").trim() : jsonText };
|
|
2495
|
+
}
|
|
2496
|
+
function matchesCheck(check, post, fields, getBody) {
|
|
2497
|
+
if (!matchesWhen(check.when, fields)) return true;
|
|
2498
|
+
if (check.op === "some") {
|
|
2499
|
+
const value = fieldValue(fields, check.field);
|
|
2500
|
+
return Array.isArray(value) && value.some((item) => {
|
|
2501
|
+
const child = record(item);
|
|
2502
|
+
return child !== null && check.checks.every((scalar) => matchesContentScalarCheck(scalar, child));
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
if (check.op === "bodyText") return Boolean(getBody().text);
|
|
2506
|
+
if (check.op === "bodyContent") {
|
|
2507
|
+
const body = getBody();
|
|
2508
|
+
return Boolean(body.jsonText || body.html || check.allowExcerpt && text2(post.excerpt) || check.allowFeaturedImage && contentPolicySafeHttpsUrl(post.featuredImageUrl) || check.additionalUrl && matchesWhen(check.additionalUrl.when, fields) && contentPolicySafeHttpsUrl(fieldValue(fields, check.additionalUrl.field)));
|
|
2509
|
+
}
|
|
2510
|
+
return matchesContentScalarCheck(check, fields);
|
|
2511
|
+
}
|
|
2512
|
+
function validPath(policy, post, options) {
|
|
2513
|
+
const path = text2(post.path).replace(/\/+$/, "");
|
|
2514
|
+
if (!path || !ContentPolicyInternalPathSchema.safeParse(path).success) return false;
|
|
2515
|
+
const segments = path.slice(1).split("/");
|
|
2516
|
+
if (segments.some((segment) => !segment)) return false;
|
|
2517
|
+
if (policy.pathSegmentFormat === "lower-kebab" && segments.some((segment) => !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(segment))) return false;
|
|
2518
|
+
if (decodeURIComponent(segments.at(-1)) !== post.slug) return false;
|
|
2519
|
+
if (options.expectedPath !== void 0) {
|
|
2520
|
+
return options.expectedPath !== null && path === options.expectedPath.replace(/\/+$/, "");
|
|
2521
|
+
}
|
|
2522
|
+
return true;
|
|
2523
|
+
}
|
|
2524
|
+
function evaluateContentPolicy(policy, post, options = {}) {
|
|
2525
|
+
const visibility = [];
|
|
2526
|
+
if (post.status !== "published") visibility.push("content:not-published");
|
|
2527
|
+
if (!text2(post.id) || !text2(post.title) || !text2(post.slug)) visibility.push("content:invalid-identity");
|
|
2528
|
+
const publishedAt = dateValue(post.publishedAt);
|
|
2529
|
+
if (publishedAt === null) visibility.push("content:invalid-published-at");
|
|
2530
|
+
else if (publishedAt > (options.now ?? Date.now())) visibility.push("content:future-publication");
|
|
2531
|
+
if (!validPath(policy, post, options)) visibility.push("content:invalid-path");
|
|
2532
|
+
const fields = post.fields ?? {};
|
|
2533
|
+
let body;
|
|
2534
|
+
const getBody = () => body ??= bodyFacts(post);
|
|
2535
|
+
const failures = (checks, stage) => checks.flatMap((check) => matchesCheck(check, post, fields, getBody) ? [] : [`content:${stage}:${check.op}${"field" in check ? `:${check.field}` : ""}`]);
|
|
2536
|
+
if (visibility.length === 0) visibility.push(...failures(policy.visibility, "visibility"));
|
|
2537
|
+
const visible = visibility.length === 0;
|
|
2538
|
+
const indexing = visible ? failures(policy.indexing, "indexing") : [];
|
|
2539
|
+
const forced = !visible || indexing.length > 0 || options.robots?.forceNoindex === true;
|
|
2540
|
+
const inheritedReasons = options.robots?.forceNoindex ? options.robots.noindexReasons?.length ? options.robots.noindexReasons : ["content:forced-noindex"] : [];
|
|
2541
|
+
const noindexReasons = [.../* @__PURE__ */ new Set([...visibility, ...indexing, ...inheritedReasons])].slice(0, 64);
|
|
2542
|
+
const manualNoindex = readMetadataValues(post.metaJson?.seo).noindex === true;
|
|
2543
|
+
return {
|
|
2544
|
+
visible,
|
|
2545
|
+
indexable: visible && !forced && !manualNoindex,
|
|
2546
|
+
robots: {
|
|
2547
|
+
...options.robots,
|
|
2548
|
+
...forced ? { forceNoindex: true } : {},
|
|
2549
|
+
...noindexReasons.length ? { noindexReasons } : {}
|
|
2550
|
+
},
|
|
2551
|
+
reasons: [...noindexReasons, ...manualNoindex ? ["content:manual-noindex"] : []]
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
// src/content-query.ts
|
|
2556
|
+
var import_zod6 = require("zod");
|
|
2557
|
+
var field2 = { field: ContentPolicyFieldNameSchema };
|
|
2558
|
+
var ContentQueryFilterSchema = import_zod6.z.discriminatedUnion("op", [
|
|
2559
|
+
import_zod6.z.object({ ...field2, op: import_zod6.z.literal("equals"), value: ContentPolicyScalarSchema }).strict(),
|
|
2560
|
+
import_zod6.z.object({ ...field2, op: import_zod6.z.literal("notEquals"), value: ContentPolicyScalarSchema }).strict(),
|
|
2561
|
+
import_zod6.z.object({ ...field2, op: import_zod6.z.literal("oneOf"), values: import_zod6.z.array(ContentPolicyScalarSchema).min(1).max(16), allowEmpty: import_zod6.z.boolean().optional() }).strict()
|
|
2562
|
+
]);
|
|
2563
|
+
var ContentQuerySchema = import_zod6.z.object({
|
|
2564
|
+
modelKeys: import_zod6.z.array(import_zod6.z.string().min(1).max(100).regex(/^[a-z][a-z0-9_-]*$/)).min(1).max(16),
|
|
2565
|
+
filters: import_zod6.z.array(ContentQueryFilterSchema).max(8).optional(),
|
|
2566
|
+
scope: import_zod6.z.enum(["currentTerm", "currentPath"]).optional(),
|
|
2567
|
+
pinnedField: ContentPolicyFieldNameSchema.optional(),
|
|
2568
|
+
pageSize: import_zod6.z.number().int().min(1).max(100),
|
|
2569
|
+
requireItems: import_zod6.z.boolean().optional()
|
|
2570
|
+
}).strict();
|
|
2571
|
+
function collectContentQueryFieldRefs(query) {
|
|
2572
|
+
return [.../* @__PURE__ */ new Set([...(query.filters ?? []).map((filter) => filter.field), ...query.pinnedField ? [query.pinnedField] : []])].map((name) => ({ name }));
|
|
2573
|
+
}
|
|
2574
|
+
function matchesScope(query, post, options) {
|
|
2575
|
+
if (query.scope === "currentTerm") return post.terms?.some((term) => term.id === options.termId) === true;
|
|
2576
|
+
if (query.scope === "currentPath") {
|
|
2577
|
+
const path = options.path.replace(/\/+$/, "");
|
|
2578
|
+
return post.path?.trim().startsWith(`${path}/`) === true;
|
|
2579
|
+
}
|
|
2580
|
+
return true;
|
|
2581
|
+
}
|
|
2582
|
+
function evaluateContentQuery(query, candidates, options) {
|
|
2583
|
+
if (!options.complete) throw new Error("Content query input is incomplete");
|
|
2584
|
+
const page = options.page ?? 1;
|
|
2585
|
+
if (!Number.isSafeInteger(page) || page < 1) throw new Error("Content query page must be a positive safe integer");
|
|
2586
|
+
if (query.scope === "currentTerm" && !options.termId) throw new Error("Content query requires the current term id");
|
|
2587
|
+
if (query.scope === "currentPath" && (!options.path || !ContentPolicyInternalPathSchema.safeParse(options.path).success)) throw new Error("Content query requires a safe current path");
|
|
2588
|
+
const visible = candidates.filter(({ policy }) => policy.visible);
|
|
2589
|
+
const pathCounts = /* @__PURE__ */ new Map();
|
|
2590
|
+
const canonical = (post) => post.path?.trim().replace(/\/+$/, "") ?? null;
|
|
2591
|
+
for (const { post } of visible) pathCounts.set(canonical(post), (pathCounts.get(canonical(post)) ?? 0) + 1);
|
|
2592
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2593
|
+
const selected = visible.filter(({ post }) => {
|
|
2594
|
+
if (pathCounts.get(canonical(post)) !== 1 || seen.has(post.id)) return false;
|
|
2595
|
+
seen.add(post.id);
|
|
2596
|
+
return query.modelKeys.includes(post.modelKey ?? post.collectionKey ?? "") && matchesScope(query, post, options) && (query.filters ?? []).every((filter) => matchesContentScalarCheck(filter, post.fields ?? {}));
|
|
2597
|
+
}).sort((left, right) => {
|
|
2598
|
+
if (query.pinnedField) {
|
|
2599
|
+
const delta = Number(right.post.fields?.[query.pinnedField] === true) - Number(left.post.fields?.[query.pinnedField] === true);
|
|
2600
|
+
if (delta) return delta;
|
|
2601
|
+
}
|
|
2602
|
+
return Date.parse(right.post.publishedAt) - Date.parse(left.post.publishedAt) || left.post.id.localeCompare(right.post.id);
|
|
2603
|
+
});
|
|
2604
|
+
const pageCount = Math.max(1, Math.ceil(selected.length / query.pageSize));
|
|
2605
|
+
const current = selected.slice((page - 1) * query.pageSize, page * query.pageSize);
|
|
2606
|
+
const pageVisible = page <= pageCount && (!query.requireItems || selected.length > 0);
|
|
2607
|
+
const hasIndexable = current.some(({ policy }) => policy.indexable);
|
|
2608
|
+
const forced = !hasIndexable || options.robots?.forceNoindex === true;
|
|
2609
|
+
const reasons = [.../* @__PURE__ */ new Set([
|
|
2610
|
+
...page > pageCount ? ["content:invalid-page"] : query.requireItems && !selected.length ? ["content:empty-required-list"] : !current.length ? ["content:empty-page"] : !hasIndexable ? ["content:no-indexable-items"] : [],
|
|
2611
|
+
...options.robots?.forceNoindex ? options.robots.noindexReasons?.length ? options.robots.noindexReasons : ["content:forced-noindex"] : []
|
|
2612
|
+
])].slice(0, 64);
|
|
2613
|
+
return {
|
|
2614
|
+
items: current.map(({ post }) => post),
|
|
2615
|
+
total: selected.length,
|
|
2616
|
+
page,
|
|
2617
|
+
pageCount,
|
|
2618
|
+
visible: pageVisible,
|
|
2619
|
+
indexable: pageVisible && !forced,
|
|
2620
|
+
robots: { ...options.robots, ...forced ? { forceNoindex: true } : {}, ...reasons.length ? { noindexReasons: reasons } : {} },
|
|
2621
|
+
reasons
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
function selectContentQuery(query, posts, policies, options) {
|
|
2625
|
+
if (!options.complete) throw new Error("Content query input is incomplete");
|
|
2626
|
+
const candidates = posts.filter((post) => query.modelKeys.includes(post.modelKey ?? post.collectionKey ?? "")).map((post) => {
|
|
2627
|
+
const modelKey = post.modelKey ?? post.collectionKey ?? "";
|
|
2628
|
+
if (!Object.hasOwn(policies, modelKey)) throw new Error(`Content query has no policy for model ${modelKey}`);
|
|
2629
|
+
return { post, policy: evaluateContentPolicy(policies[modelKey], post, { now: options.now, ...options.postOptions(post) }) };
|
|
2630
|
+
});
|
|
2631
|
+
return evaluateContentQuery(query, candidates, options);
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2216
2634
|
// src/redirect-paths.ts
|
|
2217
2635
|
var CONTROL_OR_BACKSLASH = /[\\\u0000-\u001f\u007f]/u;
|
|
2218
2636
|
var PROTECTED_PREFIX = /^\/(?:api|_next|_vercel|auth|admin|login|logout|revalidate|preview|assets|static|images|fonts|\.well-known)(?:\/|$)/i;
|
|
@@ -2280,6 +2698,15 @@ function normalizeRedirectTarget(raw) {
|
|
|
2280
2698
|
CMS_IMAGE_WIDTHS,
|
|
2281
2699
|
CMS_LINE_HEIGHTS,
|
|
2282
2700
|
CMS_PARAGRAPH_SPACINGS,
|
|
2701
|
+
ContentPolicyCheckSchema,
|
|
2702
|
+
ContentPolicyFieldNameSchema,
|
|
2703
|
+
ContentPolicyInternalPathSchema,
|
|
2704
|
+
ContentPolicyScalarCheckSchema,
|
|
2705
|
+
ContentPolicyScalarSchema,
|
|
2706
|
+
ContentPolicySchema,
|
|
2707
|
+
ContentPolicyWhenSchema,
|
|
2708
|
+
ContentQueryFilterSchema,
|
|
2709
|
+
ContentQuerySchema,
|
|
2283
2710
|
DEFAULT_SITE_PATTERN_PRESENTATION,
|
|
2284
2711
|
EMPTY_SITE_PATTERN_PLACEMENTS,
|
|
2285
2712
|
FIELD_BINDING_SOURCE,
|
|
@@ -2317,10 +2744,16 @@ function normalizeRedirectTarget(raw) {
|
|
|
2317
2744
|
cmsImageWidth,
|
|
2318
2745
|
cmsLineHeight,
|
|
2319
2746
|
cmsParagraphSpacing,
|
|
2747
|
+
collectContentPolicyFieldRefs,
|
|
2748
|
+
collectContentQueryFieldRefs,
|
|
2749
|
+
contentPolicyBodyText,
|
|
2750
|
+
contentPolicySafeHttpsUrl,
|
|
2320
2751
|
createFieldBindingResolver,
|
|
2321
2752
|
emptyPatternSlots,
|
|
2322
2753
|
ensureImportedHtmlDocumentRoots,
|
|
2323
2754
|
ensureStableIds,
|
|
2755
|
+
evaluateContentPolicy,
|
|
2756
|
+
evaluateContentQuery,
|
|
2324
2757
|
extractFaqEntries,
|
|
2325
2758
|
faqPageJsonLd,
|
|
2326
2759
|
findInternalContentLinkTokens,
|
|
@@ -2337,12 +2770,15 @@ function normalizeRedirectTarget(raw) {
|
|
|
2337
2770
|
isAllowedMapEmbedSrc,
|
|
2338
2771
|
isInCollectionMode,
|
|
2339
2772
|
isProtectedRedirectPath,
|
|
2773
|
+
isReservedCategoryTreeSlug,
|
|
2340
2774
|
isSectionContentPropPath,
|
|
2341
2775
|
isSitePatternSlotKey,
|
|
2342
2776
|
isValidBlock,
|
|
2343
2777
|
isValidSitePatternKey,
|
|
2344
2778
|
issuePreviewToken,
|
|
2779
|
+
matchesContentScalarCheck,
|
|
2345
2780
|
mergeMetadataPatch,
|
|
2781
|
+
metadataBodyParagraphs,
|
|
2346
2782
|
metadataContentImageUrl,
|
|
2347
2783
|
normalizeEmbed,
|
|
2348
2784
|
normalizeHexColor,
|
|
@@ -2362,11 +2798,13 @@ function normalizeRedirectTarget(raw) {
|
|
|
2362
2798
|
renderInternalContentLinksInText,
|
|
2363
2799
|
resolveCategoryPath,
|
|
2364
2800
|
resolveCategoryTreeChain,
|
|
2801
|
+
resolveCategoryTreeEntryCategory,
|
|
2365
2802
|
resolveCategoryTreeEntryPath,
|
|
2366
2803
|
resolveCategoryTreeHubPaths,
|
|
2367
2804
|
resolveCategoryTreeSelection,
|
|
2368
2805
|
resolveMetadata,
|
|
2369
2806
|
resolvePatternSlots,
|
|
2807
|
+
resolvePostBodySource,
|
|
2370
2808
|
resolvePostCategorySlug,
|
|
2371
2809
|
resolvePostCollection,
|
|
2372
2810
|
resolvePostPath,
|
|
@@ -2376,6 +2814,7 @@ function normalizeRedirectTarget(raw) {
|
|
|
2376
2814
|
sanitizeImportedCss,
|
|
2377
2815
|
sanitizeImportedHtml,
|
|
2378
2816
|
scopeImportedCss,
|
|
2817
|
+
selectContentQuery,
|
|
2379
2818
|
serializeBlock,
|
|
2380
2819
|
setSitePatternPlacementRule,
|
|
2381
2820
|
sitePatternButtonColor,
|