@roottale/cms-core 0.68.0 → 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 +10 -0
- package/dist/index.cjs +433 -65
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4121 -104
- package/dist/index.d.ts +4121 -104
- package/dist/index.js +413 -65
- 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,11 +107,13 @@ __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,
|
|
101
118
|
metadataBodyParagraphs: () => metadataBodyParagraphs,
|
|
102
119
|
metadataContentImageUrl: () => metadataContentImageUrl,
|
|
@@ -118,11 +135,13 @@ __export(src_exports, {
|
|
|
118
135
|
renderInternalContentLinksInText: () => renderInternalContentLinksInText,
|
|
119
136
|
resolveCategoryPath: () => resolveCategoryPath,
|
|
120
137
|
resolveCategoryTreeChain: () => resolveCategoryTreeChain,
|
|
138
|
+
resolveCategoryTreeEntryCategory: () => resolveCategoryTreeEntryCategory,
|
|
121
139
|
resolveCategoryTreeEntryPath: () => resolveCategoryTreeEntryPath,
|
|
122
140
|
resolveCategoryTreeHubPaths: () => resolveCategoryTreeHubPaths,
|
|
123
141
|
resolveCategoryTreeSelection: () => resolveCategoryTreeSelection,
|
|
124
142
|
resolveMetadata: () => resolveMetadata,
|
|
125
143
|
resolvePatternSlots: () => resolvePatternSlots,
|
|
144
|
+
resolvePostBodySource: () => resolvePostBodySource,
|
|
126
145
|
resolvePostCategorySlug: () => resolvePostCategorySlug,
|
|
127
146
|
resolvePostCollection: () => resolvePostCollection,
|
|
128
147
|
resolvePostPath: () => resolvePostPath,
|
|
@@ -132,6 +151,7 @@ __export(src_exports, {
|
|
|
132
151
|
sanitizeImportedCss: () => sanitizeImportedCss,
|
|
133
152
|
sanitizeImportedHtml: () => sanitizeImportedHtml,
|
|
134
153
|
scopeImportedCss: () => scopeImportedCss,
|
|
154
|
+
selectContentQuery: () => selectContentQuery,
|
|
135
155
|
serializeBlock: () => serializeBlock,
|
|
136
156
|
setSitePatternPlacementRule: () => setSitePatternPlacementRule,
|
|
137
157
|
sitePatternButtonColor: () => sitePatternButtonColor,
|
|
@@ -368,6 +388,9 @@ function validateCollections(collections) {
|
|
|
368
388
|
}
|
|
369
389
|
|
|
370
390
|
// src/category-tree.ts
|
|
391
|
+
function isReservedCategoryTreeSlug(presentation, slug) {
|
|
392
|
+
return slug === presentation.categoryFallbackSlug || presentation.reservedCategorySlugs?.includes(slug) === true;
|
|
393
|
+
}
|
|
371
394
|
function basePathPrefix(basePath) {
|
|
372
395
|
if (!basePath.trim()) return null;
|
|
373
396
|
return basePath.replace(/\/+$/, "");
|
|
@@ -396,6 +419,7 @@ function hasChildCategory(termId, terms) {
|
|
|
396
419
|
function resolveCategoryTreeChain(presentation, termId, terms) {
|
|
397
420
|
const chain = categoryTreeAncestry(presentation.categoryDepth, termId, terms);
|
|
398
421
|
if (!chain) return null;
|
|
422
|
+
if (chain.some((term) => isReservedCategoryTreeSlug(presentation, term.slug))) return null;
|
|
399
423
|
if (presentation.entryCategory === "leaf") return chain;
|
|
400
424
|
if (hasChildCategory(termId, terms)) return null;
|
|
401
425
|
return chain.length === presentation.categoryDepth ? chain : null;
|
|
@@ -404,6 +428,33 @@ function resolveCategoryTreeSelection(presentation, termId, terms) {
|
|
|
404
428
|
if (hasChildCategory(termId, terms)) return null;
|
|
405
429
|
return resolveCategoryTreeChain(presentation, termId, terms);
|
|
406
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
|
+
}
|
|
407
458
|
function resolveCategoryTreeHubPaths(presentation, termId, terms) {
|
|
408
459
|
const chain = resolveCategoryTreeChain(presentation, termId, terms);
|
|
409
460
|
const prefix = basePathPrefix(presentation.basePath);
|
|
@@ -418,20 +469,11 @@ function resolveCategoryTreeHubPaths(presentation, termId, terms) {
|
|
|
418
469
|
}
|
|
419
470
|
function resolveCategoryTreeEntryPath(presentation, entry, terms) {
|
|
420
471
|
if (!entry.slug) return null;
|
|
421
|
-
const
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
];
|
|
427
|
-
if (categoryIds.length !== 1) return null;
|
|
428
|
-
const hubs = resolveCategoryTreeHubPaths(
|
|
429
|
-
presentation,
|
|
430
|
-
categoryIds[0],
|
|
431
|
-
terms
|
|
432
|
-
);
|
|
433
|
-
const leafPath = hubs?.at(-1);
|
|
434
|
-
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("/")}`;
|
|
435
477
|
}
|
|
436
478
|
|
|
437
479
|
// src/public-path.ts
|
|
@@ -1116,10 +1158,10 @@ function scopeImportedCss(css, scope, allowedAssetUrls = []) {
|
|
|
1116
1158
|
|
|
1117
1159
|
// src/internal-content-links.ts
|
|
1118
1160
|
var INTERNAL_CONTENT_LINK_TOKEN_SOURCE = String.raw`\[\[internal:([a-z0-9가-힣]+(?:[.-][a-z0-9가-힣]+)*)\|([^\]\n|]+)\]\]`;
|
|
1119
|
-
function findInternalContentLinkTokens(
|
|
1161
|
+
function findInternalContentLinkTokens(text3) {
|
|
1120
1162
|
const pattern = new RegExp(INTERNAL_CONTENT_LINK_TOKEN_SOURCE, "giu");
|
|
1121
1163
|
const tokens = [];
|
|
1122
|
-
for (const match of
|
|
1164
|
+
for (const match of text3.matchAll(pattern)) {
|
|
1123
1165
|
const [raw, key, label] = match;
|
|
1124
1166
|
if (!key || !label || match.index === void 0) continue;
|
|
1125
1167
|
tokens.push({
|
|
@@ -1178,19 +1220,19 @@ function escapeHtmlText(value) {
|
|
|
1178
1220
|
var INTERNAL_CONTENT_LINK_CLASS = "rt-internal-link";
|
|
1179
1221
|
var INTERNAL_CONTENT_LINK_PENDING_CLASS = "rt-internal-link--pending";
|
|
1180
1222
|
var INTERNAL_CONTENT_LINK_PENDING_ATTR = "data-rt-internal-link-pending";
|
|
1181
|
-
function renderInternalContentLinksInText(
|
|
1182
|
-
const tokens = findInternalContentLinkTokens(
|
|
1183
|
-
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 };
|
|
1184
1226
|
let cursor = 0;
|
|
1185
1227
|
let html = "";
|
|
1186
1228
|
for (const token of tokens) {
|
|
1187
|
-
html +=
|
|
1229
|
+
html += text3.slice(cursor, token.index);
|
|
1188
1230
|
const path = resolve(token.key);
|
|
1189
1231
|
const label = escapeHtmlText(token.label);
|
|
1190
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>`;
|
|
1191
1233
|
cursor = token.index + token.raw.length;
|
|
1192
1234
|
}
|
|
1193
|
-
html +=
|
|
1235
|
+
html += text3.slice(cursor);
|
|
1194
1236
|
return { html, hasTokens: true };
|
|
1195
1237
|
}
|
|
1196
1238
|
var INTERNAL_LINK_BLOCKED_HTML_TAGS = /* @__PURE__ */ new Set([
|
|
@@ -1224,12 +1266,12 @@ function renderInternalContentLinksInHtml(html, resolve) {
|
|
|
1224
1266
|
let cursor = 0;
|
|
1225
1267
|
let out = "";
|
|
1226
1268
|
let hasTokens = false;
|
|
1227
|
-
const emitText = (
|
|
1228
|
-
if (blockedDepth > 0 || !
|
|
1229
|
-
out +=
|
|
1269
|
+
const emitText = (text3) => {
|
|
1270
|
+
if (blockedDepth > 0 || !text3) {
|
|
1271
|
+
out += text3;
|
|
1230
1272
|
return;
|
|
1231
1273
|
}
|
|
1232
|
-
const rendered = renderInternalContentLinksInText(
|
|
1274
|
+
const rendered = renderInternalContentLinksInText(text3, resolve);
|
|
1233
1275
|
hasTokens ||= rendered.hasTokens;
|
|
1234
1276
|
out += rendered.html;
|
|
1235
1277
|
};
|
|
@@ -1949,13 +1991,13 @@ function findSectionLockViolations(beforeDoc, afterDoc) {
|
|
|
1949
1991
|
...Object.keys(next.attrs)
|
|
1950
1992
|
]);
|
|
1951
1993
|
fields.delete("props");
|
|
1952
|
-
for (const
|
|
1953
|
-
if (!valueEquals(section.attrs[
|
|
1994
|
+
for (const field3 of fields) {
|
|
1995
|
+
if (!valueEquals(section.attrs[field3], next.attrs[field3])) {
|
|
1954
1996
|
violations.push({
|
|
1955
|
-
code:
|
|
1997
|
+
code: field3 === "lock" ? "locked_section_lock_changed" : "locked_section_structure_changed",
|
|
1956
1998
|
sectionId: section.id,
|
|
1957
1999
|
sectionType: section.sectionType,
|
|
1958
|
-
field
|
|
2000
|
+
field: field3
|
|
1959
2001
|
});
|
|
1960
2002
|
}
|
|
1961
2003
|
}
|
|
@@ -2013,11 +2055,11 @@ function imageUrl(value, base) {
|
|
|
2013
2055
|
function resolveMetadata(input) {
|
|
2014
2056
|
const layers = [input.overrides, input.defaults, input.siteDefaults, input.fallback];
|
|
2015
2057
|
const result = {};
|
|
2016
|
-
for (const
|
|
2058
|
+
for (const field3 of ["title", "description", "ogImageAlt"]) {
|
|
2017
2059
|
for (const layer of layers) {
|
|
2018
|
-
const value = text(layer?.[
|
|
2060
|
+
const value = text(layer?.[field3]);
|
|
2019
2061
|
if (value !== void 0) {
|
|
2020
|
-
result[
|
|
2062
|
+
result[field3] = value;
|
|
2021
2063
|
break;
|
|
2022
2064
|
}
|
|
2023
2065
|
}
|
|
@@ -2029,11 +2071,11 @@ function resolveMetadata(input) {
|
|
|
2029
2071
|
break;
|
|
2030
2072
|
}
|
|
2031
2073
|
}
|
|
2032
|
-
for (const
|
|
2074
|
+
for (const field3 of ["noindex", "nofollow"]) {
|
|
2033
2075
|
for (const layer of layers) {
|
|
2034
|
-
const value = layer?.[
|
|
2076
|
+
const value = layer?.[field3];
|
|
2035
2077
|
if (typeof value === "boolean") {
|
|
2036
|
-
result[
|
|
2078
|
+
result[field3] = value;
|
|
2037
2079
|
break;
|
|
2038
2080
|
}
|
|
2039
2081
|
}
|
|
@@ -2080,24 +2122,26 @@ var MetadataPatchSchema = import_zod2.z.object({
|
|
|
2080
2122
|
}).strict();
|
|
2081
2123
|
var MetadataRobotsPolicySchema = import_zod2.z.object({
|
|
2082
2124
|
forceNoindex: import_zod2.z.boolean().optional(),
|
|
2083
|
-
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()
|
|
2084
2128
|
}).strict();
|
|
2085
2129
|
function readMetadataValues(input) {
|
|
2086
2130
|
if (!input || typeof input !== "object" || Array.isArray(input)) return {};
|
|
2087
|
-
const
|
|
2131
|
+
const record2 = input;
|
|
2088
2132
|
const output = {};
|
|
2089
|
-
for (const
|
|
2090
|
-
const parsed = metadataFields[
|
|
2091
|
-
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;
|
|
2092
2136
|
}
|
|
2093
2137
|
return MetadataValuesSchema.parse(output);
|
|
2094
2138
|
}
|
|
2095
2139
|
function mergeMetadataPatch(current, input) {
|
|
2096
2140
|
const patch = MetadataPatchSchema.parse(input);
|
|
2097
2141
|
const next = { ...current };
|
|
2098
|
-
for (const
|
|
2099
|
-
if (patch[
|
|
2100
|
-
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];
|
|
2101
2145
|
}
|
|
2102
2146
|
return next;
|
|
2103
2147
|
}
|
|
@@ -2134,13 +2178,13 @@ var MetadataTextRuleSchema = import_zod3.z.object({
|
|
|
2134
2178
|
fallbackBySlug: import_zod3.z.record(import_zod3.z.string().max(200), fallbackText).optional()
|
|
2135
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");
|
|
2136
2180
|
function cleanMetadataText(value, rule) {
|
|
2137
|
-
let
|
|
2138
|
-
if (rule?.stripLeadingLabel === "question")
|
|
2139
|
-
if (rule?.stripLeadingLabel === "answer")
|
|
2140
|
-
if (rule?.replaceLineBreakTokens)
|
|
2141
|
-
if (rule?.removeEmDashes)
|
|
2142
|
-
if (rule?.normalizeWhitespace)
|
|
2143
|
-
return
|
|
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();
|
|
2144
2188
|
}
|
|
2145
2189
|
function metadataBodyParagraphs(value) {
|
|
2146
2190
|
const paragraphs = [];
|
|
@@ -2158,37 +2202,37 @@ function metadataBodyParagraphs(value) {
|
|
|
2158
2202
|
if (!node || typeof node !== "object") return;
|
|
2159
2203
|
const item = node;
|
|
2160
2204
|
if (item.type === "paragraph") {
|
|
2161
|
-
const
|
|
2162
|
-
if (
|
|
2205
|
+
const text3 = textParts(item.content).join(" ").replace(/\s+/gu, " ").trim();
|
|
2206
|
+
if (text3) paragraphs.push(text3);
|
|
2163
2207
|
} else walk(item.content);
|
|
2164
2208
|
};
|
|
2165
2209
|
walk(value);
|
|
2166
2210
|
return paragraphs;
|
|
2167
2211
|
}
|
|
2168
2212
|
function formatMetadataText(value, rule, input, name) {
|
|
2169
|
-
let
|
|
2213
|
+
let text3 = rule?.source === "fallback" ? "" : cleanMetadataText(value ?? "", rule);
|
|
2170
2214
|
if (rule?.source === "template") {
|
|
2171
|
-
|
|
2215
|
+
text3 = cleanMetadataText((rule.template ?? "").replace(templateToken, (token) => {
|
|
2172
2216
|
if (token === "{{title}}") return name;
|
|
2173
|
-
const
|
|
2174
|
-
const value2 = Object.hasOwn(input.fields ?? {},
|
|
2217
|
+
const field3 = token.slice(8, -2);
|
|
2218
|
+
const value2 = Object.hasOwn(input.fields ?? {}, field3) ? input.fields?.[field3] : void 0;
|
|
2175
2219
|
return typeof value2 === "string" ? value2.trim() : "";
|
|
2176
2220
|
}), rule);
|
|
2177
2221
|
}
|
|
2178
|
-
if (!
|
|
2222
|
+
if (!text3 && rule?.bodyFallback) {
|
|
2179
2223
|
const paragraphs = input.bodyParagraphs ?? [];
|
|
2180
2224
|
const preferred = rule.bodyFallback.preferContaining;
|
|
2181
|
-
|
|
2182
|
-
if (rule.bodyFallback.stripLeadingLabel === "conclusion")
|
|
2183
|
-
|
|
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);
|
|
2184
2228
|
}
|
|
2185
|
-
if (!
|
|
2229
|
+
if (!text3) {
|
|
2186
2230
|
const slugFallback = input.slug && Object.hasOwn(rule?.fallbackBySlug ?? {}, input.slug) ? rule?.fallbackBySlug?.[input.slug] : void 0;
|
|
2187
2231
|
const fallback = input.parentId ? rule?.fallbackWithParent ?? rule?.fallback : slugFallback ?? rule?.fallbackWithoutParent ?? rule?.fallback;
|
|
2188
|
-
|
|
2232
|
+
text3 = cleanMetadataText((fallback ?? "").replaceAll("%s", () => name), rule);
|
|
2189
2233
|
}
|
|
2190
|
-
if (!
|
|
2191
|
-
const units = rule?.lengthUnit === "code-points" ? [...
|
|
2234
|
+
if (!text3) return void 0;
|
|
2235
|
+
const units = rule?.lengthUnit === "code-points" ? [...text3] : text3.split("");
|
|
2192
2236
|
if (rule?.maxLength && units.length > rule.maxLength) {
|
|
2193
2237
|
const suffix = rule.ellipsis ?? "";
|
|
2194
2238
|
const suffixLength = rule.lengthUnit === "code-points" ? [...suffix].length : suffix.length;
|
|
@@ -2197,7 +2241,7 @@ function formatMetadataText(value, rule, input, name) {
|
|
|
2197
2241
|
if (rule.wordBoundaryMinLength && boundary >= rule.wordBoundaryMinLength) head = head.slice(0, boundary);
|
|
2198
2242
|
return suffix ? head.trimEnd() + suffix : head;
|
|
2199
2243
|
}
|
|
2200
|
-
return
|
|
2244
|
+
return text3;
|
|
2201
2245
|
}
|
|
2202
2246
|
|
|
2203
2247
|
// src/metadata-defaults.ts
|
|
@@ -2283,6 +2327,310 @@ function formatMetadataDefaults(input, declaration) {
|
|
|
2283
2327
|
};
|
|
2284
2328
|
}
|
|
2285
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
|
+
|
|
2286
2634
|
// src/redirect-paths.ts
|
|
2287
2635
|
var CONTROL_OR_BACKSLASH = /[\\\u0000-\u001f\u007f]/u;
|
|
2288
2636
|
var PROTECTED_PREFIX = /^\/(?:api|_next|_vercel|auth|admin|login|logout|revalidate|preview|assets|static|images|fonts|\.well-known)(?:\/|$)/i;
|
|
@@ -2350,6 +2698,15 @@ function normalizeRedirectTarget(raw) {
|
|
|
2350
2698
|
CMS_IMAGE_WIDTHS,
|
|
2351
2699
|
CMS_LINE_HEIGHTS,
|
|
2352
2700
|
CMS_PARAGRAPH_SPACINGS,
|
|
2701
|
+
ContentPolicyCheckSchema,
|
|
2702
|
+
ContentPolicyFieldNameSchema,
|
|
2703
|
+
ContentPolicyInternalPathSchema,
|
|
2704
|
+
ContentPolicyScalarCheckSchema,
|
|
2705
|
+
ContentPolicyScalarSchema,
|
|
2706
|
+
ContentPolicySchema,
|
|
2707
|
+
ContentPolicyWhenSchema,
|
|
2708
|
+
ContentQueryFilterSchema,
|
|
2709
|
+
ContentQuerySchema,
|
|
2353
2710
|
DEFAULT_SITE_PATTERN_PRESENTATION,
|
|
2354
2711
|
EMPTY_SITE_PATTERN_PLACEMENTS,
|
|
2355
2712
|
FIELD_BINDING_SOURCE,
|
|
@@ -2387,10 +2744,16 @@ function normalizeRedirectTarget(raw) {
|
|
|
2387
2744
|
cmsImageWidth,
|
|
2388
2745
|
cmsLineHeight,
|
|
2389
2746
|
cmsParagraphSpacing,
|
|
2747
|
+
collectContentPolicyFieldRefs,
|
|
2748
|
+
collectContentQueryFieldRefs,
|
|
2749
|
+
contentPolicyBodyText,
|
|
2750
|
+
contentPolicySafeHttpsUrl,
|
|
2390
2751
|
createFieldBindingResolver,
|
|
2391
2752
|
emptyPatternSlots,
|
|
2392
2753
|
ensureImportedHtmlDocumentRoots,
|
|
2393
2754
|
ensureStableIds,
|
|
2755
|
+
evaluateContentPolicy,
|
|
2756
|
+
evaluateContentQuery,
|
|
2394
2757
|
extractFaqEntries,
|
|
2395
2758
|
faqPageJsonLd,
|
|
2396
2759
|
findInternalContentLinkTokens,
|
|
@@ -2407,11 +2770,13 @@ function normalizeRedirectTarget(raw) {
|
|
|
2407
2770
|
isAllowedMapEmbedSrc,
|
|
2408
2771
|
isInCollectionMode,
|
|
2409
2772
|
isProtectedRedirectPath,
|
|
2773
|
+
isReservedCategoryTreeSlug,
|
|
2410
2774
|
isSectionContentPropPath,
|
|
2411
2775
|
isSitePatternSlotKey,
|
|
2412
2776
|
isValidBlock,
|
|
2413
2777
|
isValidSitePatternKey,
|
|
2414
2778
|
issuePreviewToken,
|
|
2779
|
+
matchesContentScalarCheck,
|
|
2415
2780
|
mergeMetadataPatch,
|
|
2416
2781
|
metadataBodyParagraphs,
|
|
2417
2782
|
metadataContentImageUrl,
|
|
@@ -2433,11 +2798,13 @@ function normalizeRedirectTarget(raw) {
|
|
|
2433
2798
|
renderInternalContentLinksInText,
|
|
2434
2799
|
resolveCategoryPath,
|
|
2435
2800
|
resolveCategoryTreeChain,
|
|
2801
|
+
resolveCategoryTreeEntryCategory,
|
|
2436
2802
|
resolveCategoryTreeEntryPath,
|
|
2437
2803
|
resolveCategoryTreeHubPaths,
|
|
2438
2804
|
resolveCategoryTreeSelection,
|
|
2439
2805
|
resolveMetadata,
|
|
2440
2806
|
resolvePatternSlots,
|
|
2807
|
+
resolvePostBodySource,
|
|
2441
2808
|
resolvePostCategorySlug,
|
|
2442
2809
|
resolvePostCollection,
|
|
2443
2810
|
resolvePostPath,
|
|
@@ -2447,6 +2814,7 @@ function normalizeRedirectTarget(raw) {
|
|
|
2447
2814
|
sanitizeImportedCss,
|
|
2448
2815
|
sanitizeImportedHtml,
|
|
2449
2816
|
scopeImportedCss,
|
|
2817
|
+
selectContentQuery,
|
|
2450
2818
|
serializeBlock,
|
|
2451
2819
|
setSitePatternPlacementRule,
|
|
2452
2820
|
sitePatternButtonColor,
|