@roottale/cms-core 0.68.0 → 0.70.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/dist/index.cjs +522 -80
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10183 -237
- package/dist/index.d.ts +10183 -237
- package/dist/index.js +500 -80
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.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,17 @@ __export(src_exports, {
|
|
|
72
81
|
cmsImageWidth: () => cmsImageWidth,
|
|
73
82
|
cmsLineHeight: () => cmsLineHeight,
|
|
74
83
|
cmsParagraphSpacing: () => cmsParagraphSpacing,
|
|
84
|
+
collectContentPolicyFieldRefs: () => collectContentPolicyFieldRefs,
|
|
85
|
+
collectContentQueryFieldRefs: () => collectContentQueryFieldRefs,
|
|
86
|
+
collectMetadataDefaultFieldRefs: () => collectMetadataDefaultFieldRefs,
|
|
87
|
+
contentPolicyBodyText: () => contentPolicyBodyText,
|
|
88
|
+
contentPolicySafeHttpsUrl: () => contentPolicySafeHttpsUrl,
|
|
75
89
|
createFieldBindingResolver: () => createFieldBindingResolver,
|
|
76
90
|
emptyPatternSlots: () => emptyPatternSlots,
|
|
77
91
|
ensureImportedHtmlDocumentRoots: () => ensureImportedHtmlDocumentRoots,
|
|
78
92
|
ensureStableIds: () => ensureStableIds,
|
|
93
|
+
evaluateContentPolicy: () => evaluateContentPolicy,
|
|
94
|
+
evaluateContentQuery: () => evaluateContentQuery,
|
|
79
95
|
extractFaqEntries: () => extractFaqEntries,
|
|
80
96
|
faqPageJsonLd: () => faqPageJsonLd,
|
|
81
97
|
findInternalContentLinkTokens: () => findInternalContentLinkTokens,
|
|
@@ -92,14 +108,17 @@ __export(src_exports, {
|
|
|
92
108
|
isAllowedMapEmbedSrc: () => isAllowedMapEmbedSrc,
|
|
93
109
|
isInCollectionMode: () => isInCollectionMode,
|
|
94
110
|
isProtectedRedirectPath: () => isProtectedRedirectPath,
|
|
111
|
+
isReservedCategoryTreeSlug: () => isReservedCategoryTreeSlug,
|
|
95
112
|
isSectionContentPropPath: () => isSectionContentPropPath,
|
|
96
113
|
isSitePatternSlotKey: () => isSitePatternSlotKey,
|
|
97
114
|
isValidBlock: () => isValidBlock,
|
|
98
115
|
isValidSitePatternKey: () => isValidSitePatternKey,
|
|
99
116
|
issuePreviewToken: () => issuePreviewToken,
|
|
117
|
+
matchesContentScalarCheck: () => matchesContentScalarCheck,
|
|
100
118
|
mergeMetadataPatch: () => mergeMetadataPatch,
|
|
101
119
|
metadataBodyParagraphs: () => metadataBodyParagraphs,
|
|
102
120
|
metadataContentImageUrl: () => metadataContentImageUrl,
|
|
121
|
+
metadataVideoThumbnail: () => metadataVideoThumbnail,
|
|
103
122
|
normalizeEmbed: () => normalizeEmbed,
|
|
104
123
|
normalizeHexColor: () => normalizeHexColor,
|
|
105
124
|
normalizeImageAlt: () => normalizeImageAlt,
|
|
@@ -118,11 +137,13 @@ __export(src_exports, {
|
|
|
118
137
|
renderInternalContentLinksInText: () => renderInternalContentLinksInText,
|
|
119
138
|
resolveCategoryPath: () => resolveCategoryPath,
|
|
120
139
|
resolveCategoryTreeChain: () => resolveCategoryTreeChain,
|
|
140
|
+
resolveCategoryTreeEntryCategory: () => resolveCategoryTreeEntryCategory,
|
|
121
141
|
resolveCategoryTreeEntryPath: () => resolveCategoryTreeEntryPath,
|
|
122
142
|
resolveCategoryTreeHubPaths: () => resolveCategoryTreeHubPaths,
|
|
123
143
|
resolveCategoryTreeSelection: () => resolveCategoryTreeSelection,
|
|
124
144
|
resolveMetadata: () => resolveMetadata,
|
|
125
145
|
resolvePatternSlots: () => resolvePatternSlots,
|
|
146
|
+
resolvePostBodySource: () => resolvePostBodySource,
|
|
126
147
|
resolvePostCategorySlug: () => resolvePostCategorySlug,
|
|
127
148
|
resolvePostCollection: () => resolvePostCollection,
|
|
128
149
|
resolvePostPath: () => resolvePostPath,
|
|
@@ -132,6 +153,7 @@ __export(src_exports, {
|
|
|
132
153
|
sanitizeImportedCss: () => sanitizeImportedCss,
|
|
133
154
|
sanitizeImportedHtml: () => sanitizeImportedHtml,
|
|
134
155
|
scopeImportedCss: () => scopeImportedCss,
|
|
156
|
+
selectContentQuery: () => selectContentQuery,
|
|
135
157
|
serializeBlock: () => serializeBlock,
|
|
136
158
|
setSitePatternPlacementRule: () => setSitePatternPlacementRule,
|
|
137
159
|
sitePatternButtonColor: () => sitePatternButtonColor,
|
|
@@ -368,6 +390,9 @@ function validateCollections(collections) {
|
|
|
368
390
|
}
|
|
369
391
|
|
|
370
392
|
// src/category-tree.ts
|
|
393
|
+
function isReservedCategoryTreeSlug(presentation, slug) {
|
|
394
|
+
return slug === presentation.categoryFallbackSlug || presentation.reservedCategorySlugs?.includes(slug) === true;
|
|
395
|
+
}
|
|
371
396
|
function basePathPrefix(basePath) {
|
|
372
397
|
if (!basePath.trim()) return null;
|
|
373
398
|
return basePath.replace(/\/+$/, "");
|
|
@@ -396,6 +421,7 @@ function hasChildCategory(termId, terms) {
|
|
|
396
421
|
function resolveCategoryTreeChain(presentation, termId, terms) {
|
|
397
422
|
const chain = categoryTreeAncestry(presentation.categoryDepth, termId, terms);
|
|
398
423
|
if (!chain) return null;
|
|
424
|
+
if (chain.some((term) => isReservedCategoryTreeSlug(presentation, term.slug))) return null;
|
|
399
425
|
if (presentation.entryCategory === "leaf") return chain;
|
|
400
426
|
if (hasChildCategory(termId, terms)) return null;
|
|
401
427
|
return chain.length === presentation.categoryDepth ? chain : null;
|
|
@@ -404,6 +430,33 @@ function resolveCategoryTreeSelection(presentation, termId, terms) {
|
|
|
404
430
|
if (hasChildCategory(termId, terms)) return null;
|
|
405
431
|
return resolveCategoryTreeChain(presentation, termId, terms);
|
|
406
432
|
}
|
|
433
|
+
function resolveCategoryTreeEntryCategory(presentation, entry, terms) {
|
|
434
|
+
const termsById = new Map(terms.map((term) => [term.id, term]));
|
|
435
|
+
const first = presentation.categorySelection === "first";
|
|
436
|
+
const categoryIds = [...new Set(entry.terms.filter((term) => term.taxonomy === "category" && (first || termsById.has(term.id))).map((term) => term.id))];
|
|
437
|
+
if (first) {
|
|
438
|
+
if (termsById.size !== terms.length) return null;
|
|
439
|
+
if (categoryIds.length === 0) {
|
|
440
|
+
const slug = presentation.categoryFallbackSlug;
|
|
441
|
+
if (presentation.categoryDepth !== 1 || !slug || presentation.reservedCategorySlugs?.includes(slug) || terms.some((term) => term.slug === slug)) return null;
|
|
442
|
+
return { kind: "fallback", slug };
|
|
443
|
+
}
|
|
444
|
+
const candidates = [];
|
|
445
|
+
for (const id of categoryIds) {
|
|
446
|
+
const category2 = termsById.get(id);
|
|
447
|
+
if (!category2 || !Number.isInteger(category2.termOrder ?? 0)) return null;
|
|
448
|
+
const chain2 = resolveCategoryTreeChain(presentation, id, terms);
|
|
449
|
+
if (!chain2) return null;
|
|
450
|
+
candidates.push({ category: category2, chain: chain2 });
|
|
451
|
+
}
|
|
452
|
+
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));
|
|
453
|
+
return { kind: "category", ...candidates[0] };
|
|
454
|
+
}
|
|
455
|
+
if (categoryIds.length !== 1) return null;
|
|
456
|
+
const category = termsById.get(categoryIds[0]);
|
|
457
|
+
const chain = resolveCategoryTreeChain(presentation, category.id, terms);
|
|
458
|
+
return chain ? { kind: "category", category, chain } : null;
|
|
459
|
+
}
|
|
407
460
|
function resolveCategoryTreeHubPaths(presentation, termId, terms) {
|
|
408
461
|
const chain = resolveCategoryTreeChain(presentation, termId, terms);
|
|
409
462
|
const prefix = basePathPrefix(presentation.basePath);
|
|
@@ -418,20 +471,11 @@ function resolveCategoryTreeHubPaths(presentation, termId, terms) {
|
|
|
418
471
|
}
|
|
419
472
|
function resolveCategoryTreeEntryPath(presentation, entry, terms) {
|
|
420
473
|
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;
|
|
474
|
+
const selected = resolveCategoryTreeEntryCategory(presentation, entry, terms);
|
|
475
|
+
const prefix = basePathPrefix(presentation.basePath);
|
|
476
|
+
if (!selected || prefix === null) return null;
|
|
477
|
+
const slugs = selected.kind === "fallback" ? [selected.slug] : selected.chain.map((term) => term.slug);
|
|
478
|
+
return `${prefix}/${[...slugs, entry.slug].map(encodeURIComponent).join("/")}`;
|
|
435
479
|
}
|
|
436
480
|
|
|
437
481
|
// src/public-path.ts
|
|
@@ -1116,10 +1160,10 @@ function scopeImportedCss(css, scope, allowedAssetUrls = []) {
|
|
|
1116
1160
|
|
|
1117
1161
|
// src/internal-content-links.ts
|
|
1118
1162
|
var INTERNAL_CONTENT_LINK_TOKEN_SOURCE = String.raw`\[\[internal:([a-z0-9가-힣]+(?:[.-][a-z0-9가-힣]+)*)\|([^\]\n|]+)\]\]`;
|
|
1119
|
-
function findInternalContentLinkTokens(
|
|
1163
|
+
function findInternalContentLinkTokens(text3) {
|
|
1120
1164
|
const pattern = new RegExp(INTERNAL_CONTENT_LINK_TOKEN_SOURCE, "giu");
|
|
1121
1165
|
const tokens = [];
|
|
1122
|
-
for (const match of
|
|
1166
|
+
for (const match of text3.matchAll(pattern)) {
|
|
1123
1167
|
const [raw, key, label] = match;
|
|
1124
1168
|
if (!key || !label || match.index === void 0) continue;
|
|
1125
1169
|
tokens.push({
|
|
@@ -1178,19 +1222,19 @@ function escapeHtmlText(value) {
|
|
|
1178
1222
|
var INTERNAL_CONTENT_LINK_CLASS = "rt-internal-link";
|
|
1179
1223
|
var INTERNAL_CONTENT_LINK_PENDING_CLASS = "rt-internal-link--pending";
|
|
1180
1224
|
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:
|
|
1225
|
+
function renderInternalContentLinksInText(text3, resolve) {
|
|
1226
|
+
const tokens = findInternalContentLinkTokens(text3);
|
|
1227
|
+
if (tokens.length === 0) return { html: text3, hasTokens: false };
|
|
1184
1228
|
let cursor = 0;
|
|
1185
1229
|
let html = "";
|
|
1186
1230
|
for (const token of tokens) {
|
|
1187
|
-
html +=
|
|
1231
|
+
html += text3.slice(cursor, token.index);
|
|
1188
1232
|
const path = resolve(token.key);
|
|
1189
1233
|
const label = escapeHtmlText(token.label);
|
|
1190
1234
|
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
1235
|
cursor = token.index + token.raw.length;
|
|
1192
1236
|
}
|
|
1193
|
-
html +=
|
|
1237
|
+
html += text3.slice(cursor);
|
|
1194
1238
|
return { html, hasTokens: true };
|
|
1195
1239
|
}
|
|
1196
1240
|
var INTERNAL_LINK_BLOCKED_HTML_TAGS = /* @__PURE__ */ new Set([
|
|
@@ -1224,12 +1268,12 @@ function renderInternalContentLinksInHtml(html, resolve) {
|
|
|
1224
1268
|
let cursor = 0;
|
|
1225
1269
|
let out = "";
|
|
1226
1270
|
let hasTokens = false;
|
|
1227
|
-
const emitText = (
|
|
1228
|
-
if (blockedDepth > 0 || !
|
|
1229
|
-
out +=
|
|
1271
|
+
const emitText = (text3) => {
|
|
1272
|
+
if (blockedDepth > 0 || !text3) {
|
|
1273
|
+
out += text3;
|
|
1230
1274
|
return;
|
|
1231
1275
|
}
|
|
1232
|
-
const rendered = renderInternalContentLinksInText(
|
|
1276
|
+
const rendered = renderInternalContentLinksInText(text3, resolve);
|
|
1233
1277
|
hasTokens ||= rendered.hasTokens;
|
|
1234
1278
|
out += rendered.html;
|
|
1235
1279
|
};
|
|
@@ -1949,13 +1993,13 @@ function findSectionLockViolations(beforeDoc, afterDoc) {
|
|
|
1949
1993
|
...Object.keys(next.attrs)
|
|
1950
1994
|
]);
|
|
1951
1995
|
fields.delete("props");
|
|
1952
|
-
for (const
|
|
1953
|
-
if (!valueEquals(section.attrs[
|
|
1996
|
+
for (const field3 of fields) {
|
|
1997
|
+
if (!valueEquals(section.attrs[field3], next.attrs[field3])) {
|
|
1954
1998
|
violations.push({
|
|
1955
|
-
code:
|
|
1999
|
+
code: field3 === "lock" ? "locked_section_lock_changed" : "locked_section_structure_changed",
|
|
1956
2000
|
sectionId: section.id,
|
|
1957
2001
|
sectionType: section.sectionType,
|
|
1958
|
-
field
|
|
2002
|
+
field: field3
|
|
1959
2003
|
});
|
|
1960
2004
|
}
|
|
1961
2005
|
}
|
|
@@ -2013,11 +2057,11 @@ function imageUrl(value, base) {
|
|
|
2013
2057
|
function resolveMetadata(input) {
|
|
2014
2058
|
const layers = [input.overrides, input.defaults, input.siteDefaults, input.fallback];
|
|
2015
2059
|
const result = {};
|
|
2016
|
-
for (const
|
|
2060
|
+
for (const field3 of ["title", "description", "ogImageAlt"]) {
|
|
2017
2061
|
for (const layer of layers) {
|
|
2018
|
-
const value = text(layer?.[
|
|
2062
|
+
const value = text(layer?.[field3]);
|
|
2019
2063
|
if (value !== void 0) {
|
|
2020
|
-
result[
|
|
2064
|
+
result[field3] = value;
|
|
2021
2065
|
break;
|
|
2022
2066
|
}
|
|
2023
2067
|
}
|
|
@@ -2029,11 +2073,11 @@ function resolveMetadata(input) {
|
|
|
2029
2073
|
break;
|
|
2030
2074
|
}
|
|
2031
2075
|
}
|
|
2032
|
-
for (const
|
|
2076
|
+
for (const field3 of ["noindex", "nofollow"]) {
|
|
2033
2077
|
for (const layer of layers) {
|
|
2034
|
-
const value = layer?.[
|
|
2078
|
+
const value = layer?.[field3];
|
|
2035
2079
|
if (typeof value === "boolean") {
|
|
2036
|
-
result[
|
|
2080
|
+
result[field3] = value;
|
|
2037
2081
|
break;
|
|
2038
2082
|
}
|
|
2039
2083
|
}
|
|
@@ -2080,30 +2124,32 @@ var MetadataPatchSchema = import_zod2.z.object({
|
|
|
2080
2124
|
}).strict();
|
|
2081
2125
|
var MetadataRobotsPolicySchema = import_zod2.z.object({
|
|
2082
2126
|
forceNoindex: import_zod2.z.boolean().optional(),
|
|
2083
|
-
forceNofollow: import_zod2.z.boolean().optional()
|
|
2127
|
+
forceNofollow: import_zod2.z.boolean().optional(),
|
|
2128
|
+
/** Stable policy reason codes. They explain an enforced exclusion, never relax it. */
|
|
2129
|
+
noindexReasons: import_zod2.z.array(import_zod2.z.string().trim().min(1).max(200)).max(64).optional()
|
|
2084
2130
|
}).strict();
|
|
2085
2131
|
function readMetadataValues(input) {
|
|
2086
2132
|
if (!input || typeof input !== "object" || Array.isArray(input)) return {};
|
|
2087
|
-
const
|
|
2133
|
+
const record2 = input;
|
|
2088
2134
|
const output = {};
|
|
2089
|
-
for (const
|
|
2090
|
-
const parsed = metadataFields[
|
|
2091
|
-
if (parsed.success) output[
|
|
2135
|
+
for (const field3 of METADATA_FIELDS) {
|
|
2136
|
+
const parsed = metadataFields[field3].safeParse(record2[field3]);
|
|
2137
|
+
if (parsed.success) output[field3] = parsed.data;
|
|
2092
2138
|
}
|
|
2093
2139
|
return MetadataValuesSchema.parse(output);
|
|
2094
2140
|
}
|
|
2095
2141
|
function mergeMetadataPatch(current, input) {
|
|
2096
2142
|
const patch = MetadataPatchSchema.parse(input);
|
|
2097
2143
|
const next = { ...current };
|
|
2098
|
-
for (const
|
|
2099
|
-
if (patch[
|
|
2100
|
-
else if (patch[
|
|
2144
|
+
for (const field3 of METADATA_FIELDS) {
|
|
2145
|
+
if (patch[field3] === null) delete next[field3];
|
|
2146
|
+
else if (patch[field3] !== void 0) next[field3] = patch[field3];
|
|
2101
2147
|
}
|
|
2102
2148
|
return next;
|
|
2103
2149
|
}
|
|
2104
2150
|
|
|
2105
2151
|
// src/metadata-defaults.ts
|
|
2106
|
-
var
|
|
2152
|
+
var import_zod6 = require("zod");
|
|
2107
2153
|
|
|
2108
2154
|
// src/metadata-text.ts
|
|
2109
2155
|
var import_zod3 = require("zod");
|
|
@@ -2134,13 +2180,13 @@ var MetadataTextRuleSchema = import_zod3.z.object({
|
|
|
2134
2180
|
fallbackBySlug: import_zod3.z.record(import_zod3.z.string().max(200), fallbackText).optional()
|
|
2135
2181
|
}).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
2182
|
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
|
|
2183
|
+
let text3 = value.trim();
|
|
2184
|
+
if (rule?.stripLeadingLabel === "question") text3 = text3.replace(/^\s*Q[..:]?\s*/iu, "");
|
|
2185
|
+
if (rule?.stripLeadingLabel === "answer") text3 = text3.replace(/^\s*A[..:]?\s*/iu, "");
|
|
2186
|
+
if (rule?.replaceLineBreakTokens) text3 = text3.replace(/\[\[br(?::[^\]]*)?\]\]/giu, " ");
|
|
2187
|
+
if (rule?.removeEmDashes) text3 = text3.replaceAll("\u2014", "");
|
|
2188
|
+
if (rule?.normalizeWhitespace) text3 = text3.replace(/\s+/gu, " ");
|
|
2189
|
+
return text3.trim();
|
|
2144
2190
|
}
|
|
2145
2191
|
function metadataBodyParagraphs(value) {
|
|
2146
2192
|
const paragraphs = [];
|
|
@@ -2158,37 +2204,37 @@ function metadataBodyParagraphs(value) {
|
|
|
2158
2204
|
if (!node || typeof node !== "object") return;
|
|
2159
2205
|
const item = node;
|
|
2160
2206
|
if (item.type === "paragraph") {
|
|
2161
|
-
const
|
|
2162
|
-
if (
|
|
2207
|
+
const text3 = textParts(item.content).join(" ").replace(/\s+/gu, " ").trim();
|
|
2208
|
+
if (text3) paragraphs.push(text3);
|
|
2163
2209
|
} else walk(item.content);
|
|
2164
2210
|
};
|
|
2165
2211
|
walk(value);
|
|
2166
2212
|
return paragraphs;
|
|
2167
2213
|
}
|
|
2168
2214
|
function formatMetadataText(value, rule, input, name) {
|
|
2169
|
-
let
|
|
2215
|
+
let text3 = rule?.source === "fallback" ? "" : cleanMetadataText(value ?? "", rule);
|
|
2170
2216
|
if (rule?.source === "template") {
|
|
2171
|
-
|
|
2217
|
+
text3 = cleanMetadataText((rule.template ?? "").replace(templateToken, (token) => {
|
|
2172
2218
|
if (token === "{{title}}") return name;
|
|
2173
|
-
const
|
|
2174
|
-
const value2 = Object.hasOwn(input.fields ?? {},
|
|
2219
|
+
const field3 = token.slice(8, -2);
|
|
2220
|
+
const value2 = Object.hasOwn(input.fields ?? {}, field3) ? input.fields?.[field3] : void 0;
|
|
2175
2221
|
return typeof value2 === "string" ? value2.trim() : "";
|
|
2176
2222
|
}), rule);
|
|
2177
2223
|
}
|
|
2178
|
-
if (!
|
|
2224
|
+
if (!text3 && rule?.bodyFallback) {
|
|
2179
2225
|
const paragraphs = input.bodyParagraphs ?? [];
|
|
2180
2226
|
const preferred = rule.bodyFallback.preferContaining;
|
|
2181
|
-
|
|
2182
|
-
if (rule.bodyFallback.stripLeadingLabel === "conclusion")
|
|
2183
|
-
|
|
2227
|
+
text3 = (preferred ? paragraphs.find((paragraph) => paragraph.includes(preferred)) : void 0) ?? paragraphs[0] ?? "";
|
|
2228
|
+
if (rule.bodyFallback.stripLeadingLabel === "conclusion") text3 = text3.replace(/^(먼저\s*)?결론부터\s*(말씀드리면|말씀드릴게요)[.!?]?\s*/u, "");
|
|
2229
|
+
text3 = cleanMetadataText(text3, rule);
|
|
2184
2230
|
}
|
|
2185
|
-
if (!
|
|
2231
|
+
if (!text3) {
|
|
2186
2232
|
const slugFallback = input.slug && Object.hasOwn(rule?.fallbackBySlug ?? {}, input.slug) ? rule?.fallbackBySlug?.[input.slug] : void 0;
|
|
2187
2233
|
const fallback = input.parentId ? rule?.fallbackWithParent ?? rule?.fallback : slugFallback ?? rule?.fallbackWithoutParent ?? rule?.fallback;
|
|
2188
|
-
|
|
2234
|
+
text3 = cleanMetadataText((fallback ?? "").replaceAll("%s", () => name), rule);
|
|
2189
2235
|
}
|
|
2190
|
-
if (!
|
|
2191
|
-
const units = rule?.lengthUnit === "code-points" ? [...
|
|
2236
|
+
if (!text3) return void 0;
|
|
2237
|
+
const units = rule?.lengthUnit === "code-points" ? [...text3] : text3.split("");
|
|
2192
2238
|
if (rule?.maxLength && units.length > rule.maxLength) {
|
|
2193
2239
|
const suffix = rule.ellipsis ?? "";
|
|
2194
2240
|
const suffixLength = rule.lengthUnit === "code-points" ? [...suffix].length : suffix.length;
|
|
@@ -2197,11 +2243,294 @@ function formatMetadataText(value, rule, input, name) {
|
|
|
2197
2243
|
if (rule.wordBoundaryMinLength && boundary >= rule.wordBoundaryMinLength) head = head.slice(0, boundary);
|
|
2198
2244
|
return suffix ? head.trimEnd() + suffix : head;
|
|
2199
2245
|
}
|
|
2200
|
-
return
|
|
2246
|
+
return text3;
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
// src/content-policy-schema.ts
|
|
2250
|
+
var import_zod4 = require("zod");
|
|
2251
|
+
var ContentPolicyFieldNameSchema = import_zod4.z.string().min(1).max(64).regex(/^[A-Za-z_][A-Za-z0-9_]*$/).refine((name) => name !== "prototype" && !Object.hasOwn(Object.prototype, name));
|
|
2252
|
+
var ContentPolicyScalarSchema = import_zod4.z.union([
|
|
2253
|
+
import_zod4.z.string().max(2048),
|
|
2254
|
+
import_zod4.z.number().finite(),
|
|
2255
|
+
import_zod4.z.boolean(),
|
|
2256
|
+
import_zod4.z.null()
|
|
2257
|
+
]);
|
|
2258
|
+
var contentPolicyEquality = import_zod4.z.object({
|
|
2259
|
+
field: ContentPolicyFieldNameSchema,
|
|
2260
|
+
equals: ContentPolicyScalarSchema
|
|
2261
|
+
}).strict();
|
|
2262
|
+
var ContentPolicyWhenSchema = import_zod4.z.union([
|
|
2263
|
+
contentPolicyEquality,
|
|
2264
|
+
import_zod4.z.object({
|
|
2265
|
+
all: import_zod4.z.array(contentPolicyEquality).min(1).max(4).optional(),
|
|
2266
|
+
unlessAll: import_zod4.z.array(contentPolicyEquality).min(1).max(4).optional()
|
|
2267
|
+
}).strict().refine(
|
|
2268
|
+
(when2) => when2.all !== void 0 || when2.unlessAll !== void 0,
|
|
2269
|
+
"\uCF58\uD150\uCE20 \uC870\uAC74\uC5D0\uB294 all \uB610\uB294 unlessAll \uADF8\uB8F9\uC774 \uD544\uC694\uD569\uB2C8\uB2E4"
|
|
2270
|
+
)
|
|
2271
|
+
]);
|
|
2272
|
+
var ContentPolicyInternalPathSchema = import_zod4.z.string().min(1).max(2048).refine((path) => {
|
|
2273
|
+
if (!path.startsWith("/") || path.startsWith("//") || /[\\?#\u0000-\u0020\u007f]/u.test(path)) return false;
|
|
2274
|
+
try {
|
|
2275
|
+
return path.split("/").every((part, index) => {
|
|
2276
|
+
if (index === 0) return true;
|
|
2277
|
+
const decoded2 = decodeURIComponent(part);
|
|
2278
|
+
return decoded2 !== "." && decoded2 !== ".." && !/[\\/\u0000-\u0020\u007f]/u.test(decoded2);
|
|
2279
|
+
});
|
|
2280
|
+
} catch {
|
|
2281
|
+
return false;
|
|
2282
|
+
}
|
|
2283
|
+
}, "\uCF58\uD150\uCE20 \uACBD\uB85C\uB294 \uC548\uC804\uD55C \uC0AC\uC774\uD2B8 \uB0B4\uBD80 \uACBD\uB85C\uC5EC\uC57C \uD569\uB2C8\uB2E4");
|
|
2284
|
+
var field = { field: ContentPolicyFieldNameSchema };
|
|
2285
|
+
var scalarChecks = [
|
|
2286
|
+
import_zod4.z.object({ ...field, op: import_zod4.z.literal("equals"), value: ContentPolicyScalarSchema }).strict(),
|
|
2287
|
+
import_zod4.z.object({ ...field, op: import_zod4.z.literal("notEquals"), value: ContentPolicyScalarSchema }).strict(),
|
|
2288
|
+
import_zod4.z.object({ ...field, op: import_zod4.z.literal("oneOf"), values: import_zod4.z.array(ContentPolicyScalarSchema).min(1).max(16), allowEmpty: import_zod4.z.boolean().optional() }).strict(),
|
|
2289
|
+
import_zod4.z.object({ ...field, op: import_zod4.z.literal("text") }).strict(),
|
|
2290
|
+
import_zod4.z.object({ ...field, op: import_zod4.z.literal("date") }).strict(),
|
|
2291
|
+
import_zod4.z.object({ ...field, op: import_zod4.z.literal("safeHttps") }).strict(),
|
|
2292
|
+
import_zod4.z.object({ ...field, op: import_zod4.z.literal("allowedPath"), paths: import_zod4.z.array(ContentPolicyInternalPathSchema).min(1).max(512) }).strict()
|
|
2293
|
+
];
|
|
2294
|
+
var ContentPolicyScalarCheckSchema = import_zod4.z.discriminatedUnion("op", scalarChecks);
|
|
2295
|
+
var when = { when: ContentPolicyWhenSchema.optional() };
|
|
2296
|
+
var ContentPolicyCheckSchema = import_zod4.z.discriminatedUnion("op", [
|
|
2297
|
+
scalarChecks[0].extend(when),
|
|
2298
|
+
scalarChecks[1].extend(when),
|
|
2299
|
+
scalarChecks[2].extend(when),
|
|
2300
|
+
scalarChecks[3].extend(when),
|
|
2301
|
+
scalarChecks[4].extend(when),
|
|
2302
|
+
scalarChecks[5].extend(when),
|
|
2303
|
+
scalarChecks[6].extend(when),
|
|
2304
|
+
import_zod4.z.object({ ...field, ...when, op: import_zod4.z.literal("some"), checks: import_zod4.z.array(ContentPolicyScalarCheckSchema).min(1).max(8) }).strict(),
|
|
2305
|
+
import_zod4.z.object({
|
|
2306
|
+
...when,
|
|
2307
|
+
op: import_zod4.z.literal("bodyContent"),
|
|
2308
|
+
allowExcerpt: import_zod4.z.boolean().optional(),
|
|
2309
|
+
allowFeaturedImage: import_zod4.z.boolean().optional(),
|
|
2310
|
+
additionalUrl: import_zod4.z.object({ ...field, ...when }).strict().optional()
|
|
2311
|
+
}).strict(),
|
|
2312
|
+
import_zod4.z.object({ ...when, op: import_zod4.z.literal("bodyText") }).strict()
|
|
2313
|
+
]);
|
|
2314
|
+
var contentPolicy = import_zod4.z.object({
|
|
2315
|
+
visibility: import_zod4.z.array(ContentPolicyCheckSchema).max(24),
|
|
2316
|
+
indexing: import_zod4.z.array(ContentPolicyCheckSchema).max(24),
|
|
2317
|
+
pathSegmentFormat: import_zod4.z.literal("lower-kebab").optional()
|
|
2318
|
+
}).strict();
|
|
2319
|
+
var ContentPolicySchema = contentPolicy.superRefine((policy, ctx) => {
|
|
2320
|
+
const refs = collectContentPolicyFieldRefs(policy);
|
|
2321
|
+
if (refs.length > 64) ctx.addIssue({ code: import_zod4.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" });
|
|
2322
|
+
for (const ref of refs) {
|
|
2323
|
+
if ((ref.children?.length ?? 0) > 8) ctx.addIssue({ code: import_zod4.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` });
|
|
2324
|
+
}
|
|
2325
|
+
});
|
|
2326
|
+
function collectContentPolicyFieldRefs(policy) {
|
|
2327
|
+
const refs = /* @__PURE__ */ new Map();
|
|
2328
|
+
const add = (name, children = []) => {
|
|
2329
|
+
const names = refs.get(name) ?? /* @__PURE__ */ new Set();
|
|
2330
|
+
for (const child of children) names.add(child);
|
|
2331
|
+
refs.set(name, names);
|
|
2332
|
+
};
|
|
2333
|
+
const addWhen = (when2) => {
|
|
2334
|
+
if (!when2) return;
|
|
2335
|
+
if ("field" in when2) add(when2.field);
|
|
2336
|
+
else for (const condition of [...when2.all ?? [], ...when2.unlessAll ?? []]) add(condition.field);
|
|
2337
|
+
};
|
|
2338
|
+
for (const check of [...policy.visibility, ...policy.indexing]) {
|
|
2339
|
+
addWhen(check.when);
|
|
2340
|
+
if ("field" in check) add(check.field, check.op === "some" ? check.checks.map((child) => child.field) : []);
|
|
2341
|
+
if (check.op === "bodyContent" && check.additionalUrl) {
|
|
2342
|
+
add(check.additionalUrl.field);
|
|
2343
|
+
addWhen(check.additionalUrl.when);
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
return [...refs].map(([name, children]) => ({ name, ...children.size ? { children: [...children] } : {} }));
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
// src/content-policy-condition.ts
|
|
2350
|
+
function matchesContentPolicyWhen(when2, fields) {
|
|
2351
|
+
if (!when2) return true;
|
|
2352
|
+
const matches = ({ field: field3, equals }) => {
|
|
2353
|
+
const value = Object.hasOwn(fields, field3) ? fields[field3] : void 0;
|
|
2354
|
+
return value === equals;
|
|
2355
|
+
};
|
|
2356
|
+
if ("field" in when2) return matches(when2);
|
|
2357
|
+
return (when2.all?.every(matches) ?? true) && !(when2.unlessAll?.every(matches) ?? false);
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
// src/post-body-source.ts
|
|
2361
|
+
function resolvePostBodySource(bodyHtml) {
|
|
2362
|
+
if (typeof bodyHtml !== "string") return { kind: "json" };
|
|
2363
|
+
const trimmed = bodyHtml.trim();
|
|
2364
|
+
if (trimmed.length === 0) return { kind: "json" };
|
|
2365
|
+
const sanitized = sanitizeHtml(trimmed);
|
|
2366
|
+
const visible = /<(?:img|hr|br|iframe|video|audio)\b/i.test(sanitized) || sanitized.replace(/<[^>]*>/g, "").replace(/ /gi, " ").trim().length > 0;
|
|
2367
|
+
return visible ? { kind: "html", html: sanitized } : { kind: "json" };
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
// src/content-policy.ts
|
|
2371
|
+
var text2 = (value) => typeof value === "string" ? value.trim() : "";
|
|
2372
|
+
var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2373
|
+
function fieldValue(fields, field3) {
|
|
2374
|
+
return Object.hasOwn(fields, field3) ? fields[field3] : void 0;
|
|
2375
|
+
}
|
|
2376
|
+
function contentPolicySafeHttpsUrl(value) {
|
|
2377
|
+
if (typeof value !== "string" || /[\\\u0000-\u0020\u007f]/u.test(value)) return null;
|
|
2378
|
+
try {
|
|
2379
|
+
const url = new URL(value);
|
|
2380
|
+
return url.protocol === "https:" && !url.username && !url.password ? url.href : null;
|
|
2381
|
+
} catch {
|
|
2382
|
+
return null;
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
function dateValue(value) {
|
|
2386
|
+
if (!value || typeof value !== "string" && typeof value !== "number") return null;
|
|
2387
|
+
if (!Number.isFinite(Date.parse(String(value)))) return null;
|
|
2388
|
+
const date = new Date(value).getTime();
|
|
2389
|
+
return Number.isFinite(date) ? date : null;
|
|
2390
|
+
}
|
|
2391
|
+
function contentPolicyBodyText(bodyJson) {
|
|
2392
|
+
const root = record(bodyJson);
|
|
2393
|
+
if (root?.type !== "doc" || !Array.isArray(root.content)) return "";
|
|
2394
|
+
const pending = [{ node: root, depth: 0 }];
|
|
2395
|
+
const output = [];
|
|
2396
|
+
let count = 0;
|
|
2397
|
+
while (pending.length) {
|
|
2398
|
+
const entry = pending.pop();
|
|
2399
|
+
if (++count > 1e5 || entry.depth > 128) throw new Error("Content policy body exceeds traversal limits");
|
|
2400
|
+
const node = record(entry.node);
|
|
2401
|
+
if (!node) continue;
|
|
2402
|
+
if (node.type === "text") {
|
|
2403
|
+
const value = text2(node.text);
|
|
2404
|
+
if (value) output.push(value);
|
|
2405
|
+
} else if (Array.isArray(node.content)) {
|
|
2406
|
+
for (let index = node.content.length - 1; index >= 0; index--) {
|
|
2407
|
+
pending.push({ node: node.content[index], depth: entry.depth + 1 });
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
return output.join(" ");
|
|
2412
|
+
}
|
|
2413
|
+
function matchesContentScalarCheck(check, fields) {
|
|
2414
|
+
const value = fieldValue(fields, check.field);
|
|
2415
|
+
switch (check.op) {
|
|
2416
|
+
case "equals":
|
|
2417
|
+
return value === check.value;
|
|
2418
|
+
case "notEquals":
|
|
2419
|
+
return value !== check.value;
|
|
2420
|
+
case "oneOf":
|
|
2421
|
+
return check.allowEmpty === true && !value || check.values.some((candidate) => candidate === value);
|
|
2422
|
+
case "text":
|
|
2423
|
+
return text2(value).length > 0;
|
|
2424
|
+
case "date":
|
|
2425
|
+
return dateValue(value) !== null;
|
|
2426
|
+
case "safeHttps":
|
|
2427
|
+
return contentPolicySafeHttpsUrl(value) !== null;
|
|
2428
|
+
case "allowedPath":
|
|
2429
|
+
return check.paths.includes(text2(value));
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
function bodyFacts(post) {
|
|
2433
|
+
const jsonText = contentPolicyBodyText(post.bodyJson);
|
|
2434
|
+
const source = resolvePostBodySource(post.bodyHtml);
|
|
2435
|
+
const html = source.kind === "html" ? source.html : "";
|
|
2436
|
+
return { jsonText, html, text: source.kind === "html" ? html.replace(/<[^>]*>/g, "").replace(/ /gi, " ").trim() : jsonText };
|
|
2437
|
+
}
|
|
2438
|
+
function matchesCheck(check, post, fields, getBody) {
|
|
2439
|
+
if (!matchesContentPolicyWhen(check.when, fields)) return true;
|
|
2440
|
+
if (check.op === "some") {
|
|
2441
|
+
const value = fieldValue(fields, check.field);
|
|
2442
|
+
return Array.isArray(value) && value.some((item) => {
|
|
2443
|
+
const child = record(item);
|
|
2444
|
+
return child !== null && check.checks.every((scalar) => matchesContentScalarCheck(scalar, child));
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
if (check.op === "bodyText") return Boolean(getBody().text);
|
|
2448
|
+
if (check.op === "bodyContent") {
|
|
2449
|
+
const body = getBody();
|
|
2450
|
+
return Boolean(body.jsonText || body.html || check.allowExcerpt && text2(post.excerpt) || check.allowFeaturedImage && contentPolicySafeHttpsUrl(post.featuredImageUrl) || check.additionalUrl && matchesContentPolicyWhen(check.additionalUrl.when, fields) && contentPolicySafeHttpsUrl(fieldValue(fields, check.additionalUrl.field)));
|
|
2451
|
+
}
|
|
2452
|
+
return matchesContentScalarCheck(check, fields);
|
|
2453
|
+
}
|
|
2454
|
+
function validPath(policy, post, options) {
|
|
2455
|
+
const path = text2(post.path).replace(/\/+$/, "");
|
|
2456
|
+
if (!path || !ContentPolicyInternalPathSchema.safeParse(path).success) return false;
|
|
2457
|
+
const segments = path.slice(1).split("/");
|
|
2458
|
+
if (segments.some((segment) => !segment)) return false;
|
|
2459
|
+
if (policy.pathSegmentFormat === "lower-kebab" && segments.some((segment) => !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(segment))) return false;
|
|
2460
|
+
if (decodeURIComponent(segments.at(-1)) !== post.slug) return false;
|
|
2461
|
+
if (options.expectedPath !== void 0) {
|
|
2462
|
+
return options.expectedPath !== null && path === options.expectedPath.replace(/\/+$/, "");
|
|
2463
|
+
}
|
|
2464
|
+
return true;
|
|
2465
|
+
}
|
|
2466
|
+
function evaluateContentPolicy(policy, post, options = {}) {
|
|
2467
|
+
const visibility = [];
|
|
2468
|
+
if (post.status !== "published") visibility.push("content:not-published");
|
|
2469
|
+
if (!text2(post.id) || !text2(post.title) || !text2(post.slug)) visibility.push("content:invalid-identity");
|
|
2470
|
+
const publishedAt = dateValue(post.publishedAt);
|
|
2471
|
+
if (publishedAt === null) visibility.push("content:invalid-published-at");
|
|
2472
|
+
else if (publishedAt > (options.now ?? Date.now())) visibility.push("content:future-publication");
|
|
2473
|
+
if (!validPath(policy, post, options)) visibility.push("content:invalid-path");
|
|
2474
|
+
const fields = post.fields ?? {};
|
|
2475
|
+
let body;
|
|
2476
|
+
const getBody = () => body ??= bodyFacts(post);
|
|
2477
|
+
const failures = (checks, stage) => checks.flatMap((check) => matchesCheck(check, post, fields, getBody) ? [] : [`content:${stage}:${check.op}${"field" in check ? `:${check.field}` : ""}`]);
|
|
2478
|
+
if (visibility.length === 0) visibility.push(...failures(policy.visibility, "visibility"));
|
|
2479
|
+
const visible = visibility.length === 0;
|
|
2480
|
+
const indexing = visible ? failures(policy.indexing, "indexing") : [];
|
|
2481
|
+
const forced = !visible || indexing.length > 0 || options.robots?.forceNoindex === true;
|
|
2482
|
+
const inheritedReasons = options.robots?.forceNoindex ? options.robots.noindexReasons?.length ? options.robots.noindexReasons : ["content:forced-noindex"] : [];
|
|
2483
|
+
const noindexReasons = [.../* @__PURE__ */ new Set([...visibility, ...indexing, ...inheritedReasons])].slice(0, 64);
|
|
2484
|
+
const manualNoindex = readMetadataValues(post.metaJson?.seo).noindex === true;
|
|
2485
|
+
return {
|
|
2486
|
+
visible,
|
|
2487
|
+
indexable: visible && !forced && !manualNoindex,
|
|
2488
|
+
robots: {
|
|
2489
|
+
...options.robots,
|
|
2490
|
+
...forced ? { forceNoindex: true } : {},
|
|
2491
|
+
...noindexReasons.length ? { noindexReasons } : {}
|
|
2492
|
+
},
|
|
2493
|
+
reasons: [...noindexReasons, ...manualNoindex ? ["content:manual-noindex"] : []]
|
|
2494
|
+
};
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
// src/metadata-video-thumbnail.ts
|
|
2498
|
+
var import_zod5 = require("zod");
|
|
2499
|
+
var MetadataVideoThumbnailRuleSchema = import_zod5.z.object({
|
|
2500
|
+
source: import_zod5.z.literal("youtube"),
|
|
2501
|
+
field: ContentPolicyFieldNameSchema,
|
|
2502
|
+
when: ContentPolicyWhenSchema.optional()
|
|
2503
|
+
}).strict();
|
|
2504
|
+
function metadataVideoThumbnail(rule, fields) {
|
|
2505
|
+
if (!rule || !matchesContentPolicyWhen(rule.when, fields ?? {})) return void 0;
|
|
2506
|
+
const value = fields && Object.hasOwn(fields, rule.field) ? fields[rule.field] : void 0;
|
|
2507
|
+
const safe = contentPolicySafeHttpsUrl(value);
|
|
2508
|
+
if (!safe) return void 0;
|
|
2509
|
+
const url = new URL(safe);
|
|
2510
|
+
const id = url.hostname === "youtu.be" ? url.pathname.slice(1) : ["www.youtube.com", "youtube.com", "m.youtube.com"].includes(url.hostname) ? url.pathname === "/watch" ? url.searchParams.get("v") : url.pathname.match(/^\/(?:shorts|embed)\/([^/]+)$/)?.[1] : null;
|
|
2511
|
+
return /^[a-zA-Z0-9_-]{11}$/.test(id ?? "") ? `https://i.ytimg.com/vi/${id}/hqdefault.jpg` : void 0;
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
// src/metadata-default-fields.ts
|
|
2515
|
+
function collectMetadataDefaultFieldRefs(rule) {
|
|
2516
|
+
const fields = /* @__PURE__ */ new Set();
|
|
2517
|
+
const video = rule?.videoThumbnail;
|
|
2518
|
+
if (video) {
|
|
2519
|
+
fields.add(video.field);
|
|
2520
|
+
if (video.when) {
|
|
2521
|
+
if ("field" in video.when) fields.add(video.when.field);
|
|
2522
|
+
else for (const condition of [...video.when.all ?? [], ...video.when.unlessAll ?? []]) fields.add(condition.field);
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
for (const text3 of [rule?.title, rule?.description]) {
|
|
2526
|
+
if (text3?.source !== "template") continue;
|
|
2527
|
+
for (const match of (text3.template ?? "").matchAll(/\{\{field:([a-zA-Z][a-zA-Z0-9_-]{0,159})\}\}/gu)) fields.add(match[1]);
|
|
2528
|
+
}
|
|
2529
|
+
return [...fields].map((name) => ({ name }));
|
|
2201
2530
|
}
|
|
2202
2531
|
|
|
2203
2532
|
// src/metadata-defaults.ts
|
|
2204
|
-
var imageOrigin =
|
|
2533
|
+
var imageOrigin = import_zod6.z.string().max(2048).refine((value) => {
|
|
2205
2534
|
try {
|
|
2206
2535
|
const url = new URL(value);
|
|
2207
2536
|
return url.protocol === "https:" && url.origin === value;
|
|
@@ -2209,23 +2538,34 @@ var imageOrigin = import_zod4.z.string().max(2048).refine((value) => {
|
|
|
2209
2538
|
return false;
|
|
2210
2539
|
}
|
|
2211
2540
|
}, "\uC774\uBBF8\uC9C0 \uD5C8\uC6A9 \uBAA9\uB85D\uC5D0\uB294 \uACBD\uB85C \uC5C6\uB294 HTTPS origin\uB9CC \uC120\uC5B8\uD569\uB2C8\uB2E4");
|
|
2212
|
-
var MetadataDefaultRulesSchema =
|
|
2541
|
+
var MetadataDefaultRulesSchema = import_zod6.z.object({
|
|
2213
2542
|
title: MetadataTextRuleSchema.optional(),
|
|
2214
|
-
titleTemplateMaxLength:
|
|
2543
|
+
titleTemplateMaxLength: import_zod6.z.number().int().min(1).max(1e3).optional(),
|
|
2215
2544
|
description: MetadataTextRuleSchema.optional(),
|
|
2216
2545
|
/** Code-owned defaults precede site defaults but always follow manual overrides. */
|
|
2217
2546
|
defaults: MetadataValuesSchema.optional(),
|
|
2218
2547
|
robots: MetadataRobotsPolicySchema.optional(),
|
|
2219
|
-
useFeaturedImage:
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2548
|
+
useFeaturedImage: import_zod6.z.boolean().optional(),
|
|
2549
|
+
videoThumbnail: MetadataVideoThumbnailRuleSchema.optional(),
|
|
2550
|
+
useFirstBodyImage: import_zod6.z.boolean().optional(),
|
|
2551
|
+
useTermImage: import_zod6.z.boolean().optional(),
|
|
2552
|
+
imageAltSource: import_zod6.z.enum(["content", "title"]).optional(),
|
|
2553
|
+
legacyImageSource: import_zod6.z.literal("legacy-thumbnail").optional(),
|
|
2554
|
+
imageAllowedOrigins: import_zod6.z.array(imageOrigin).min(1).max(32).optional(),
|
|
2555
|
+
legacyImageAllowedOrigins: import_zod6.z.array(imageOrigin).min(1).max(32).optional(),
|
|
2556
|
+
normalizeCloudflareImages: import_zod6.z.boolean().optional(),
|
|
2227
2557
|
fallback: MetadataValuesSchema.optional()
|
|
2228
|
-
}).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")
|
|
2558
|
+
}).strict().refine((rule) => !rule.legacyImageSource || Boolean(rule.legacyImageAllowedOrigins?.length), "\uC774\uC804 \uC774\uBBF8\uC9C0 \uC6D0\uBCF8\uC5D0\uB294 \uD5C8\uC6A9 origin \uC120\uC5B8\uC774 \uD544\uC694\uD569\uB2C8\uB2E4").superRefine((rule, context) => {
|
|
2559
|
+
if (!rule.videoThumbnail) return;
|
|
2560
|
+
const refs = collectMetadataDefaultFieldRefs(rule);
|
|
2561
|
+
if (refs.length > 64) context.addIssue({ code: import_zod6.z.ZodIssueCode.custom, message: "\uC601\uC0C1 \uC790\uB3D9\uAC12\uC740 \uACF5\uAC1C \uD544\uB4DC 64\uAC1C\uAE4C\uC9C0 \uCC38\uC870\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4" });
|
|
2562
|
+
for (const { name } of refs) {
|
|
2563
|
+
if (!ContentPolicyFieldNameSchema.safeParse(name).success) context.addIssue({
|
|
2564
|
+
code: import_zod6.z.ZodIssueCode.custom,
|
|
2565
|
+
message: "\uC601\uC0C1 \uC790\uB3D9\uAC12\uC758 \uD14D\uC2A4\uD2B8\xB7\uC870\uAC74\uC740 \uC548\uC804\uD55C \uACF5\uAC1C \uD544\uB4DC \uC774\uB984\uC744 \uC0AC\uC6A9\uD569\uB2C8\uB2E4"
|
|
2566
|
+
});
|
|
2567
|
+
}
|
|
2568
|
+
});
|
|
2229
2569
|
function metadataContentImageUrl(value, normalizeCloudflareImages = false) {
|
|
2230
2570
|
if (!normalizeCloudflareImages) return value;
|
|
2231
2571
|
try {
|
|
@@ -2272,8 +2612,9 @@ function formatMetadataDefaults(input, declaration) {
|
|
|
2272
2612
|
const finalTitle = templatedTitle && rule?.titleTemplateMaxLength && templatedTitle.length > rule.titleTemplateMaxLength ? title : templatedTitle;
|
|
2273
2613
|
const description = formatMetadataText(input.description, rule?.description, input, name);
|
|
2274
2614
|
const bodyImage = input.firstBodyImage && (input.firstBodyImage.startsWith("https://") || /^\/(?!\/)/u.test(input.firstBodyImage)) ? input.firstBodyImage : void 0;
|
|
2275
|
-
const
|
|
2276
|
-
const
|
|
2615
|
+
const rawFeaturedImage = (input.kind === "terms" ? rule?.useTermImage : rule?.useFeaturedImage) === false ? void 0 : input.featuredImage;
|
|
2616
|
+
const featuredImage = rule?.videoThumbnail ? contentPolicySafeHttpsUrl(rawFeaturedImage) : rawFeaturedImage;
|
|
2617
|
+
const candidateImage = featuredImage || (input.kind === "posts" ? metadataVideoThumbnail(rule?.videoThumbnail, input.fields) : void 0) || (rule?.useFirstBodyImage ? bodyImage : void 0) || legacyImageUrl(input.legacyImage, rule);
|
|
2277
2618
|
const ogImage = candidateImage && contentCandidateAllowed(candidateImage, input, rule) ? readMetadataValues({ ogImage: metadataContentImageUrl(candidateImage, rule?.normalizeCloudflareImages) }).ogImage : void 0;
|
|
2278
2619
|
return {
|
|
2279
2620
|
...readMetadataValues(rule?.defaults),
|
|
@@ -2283,6 +2624,85 @@ function formatMetadataDefaults(input, declaration) {
|
|
|
2283
2624
|
};
|
|
2284
2625
|
}
|
|
2285
2626
|
|
|
2627
|
+
// src/content-query.ts
|
|
2628
|
+
var import_zod7 = require("zod");
|
|
2629
|
+
var field2 = { field: ContentPolicyFieldNameSchema };
|
|
2630
|
+
var ContentQueryFilterSchema = import_zod7.z.discriminatedUnion("op", [
|
|
2631
|
+
import_zod7.z.object({ ...field2, op: import_zod7.z.literal("equals"), value: ContentPolicyScalarSchema }).strict(),
|
|
2632
|
+
import_zod7.z.object({ ...field2, op: import_zod7.z.literal("notEquals"), value: ContentPolicyScalarSchema }).strict(),
|
|
2633
|
+
import_zod7.z.object({ ...field2, op: import_zod7.z.literal("oneOf"), values: import_zod7.z.array(ContentPolicyScalarSchema).min(1).max(16), allowEmpty: import_zod7.z.boolean().optional() }).strict()
|
|
2634
|
+
]);
|
|
2635
|
+
var ContentQuerySchema = import_zod7.z.object({
|
|
2636
|
+
modelKeys: import_zod7.z.array(import_zod7.z.string().min(1).max(100).regex(/^[a-z][a-z0-9_-]*$/)).min(1).max(16),
|
|
2637
|
+
filters: import_zod7.z.array(ContentQueryFilterSchema).max(8).optional(),
|
|
2638
|
+
scope: import_zod7.z.enum(["currentTerm", "currentPath"]).optional(),
|
|
2639
|
+
pinnedField: ContentPolicyFieldNameSchema.optional(),
|
|
2640
|
+
pageSize: import_zod7.z.number().int().min(1).max(100),
|
|
2641
|
+
requireItems: import_zod7.z.boolean().optional()
|
|
2642
|
+
}).strict();
|
|
2643
|
+
function collectContentQueryFieldRefs(query) {
|
|
2644
|
+
return [.../* @__PURE__ */ new Set([...(query.filters ?? []).map((filter) => filter.field), ...query.pinnedField ? [query.pinnedField] : []])].map((name) => ({ name }));
|
|
2645
|
+
}
|
|
2646
|
+
function matchesScope(query, post, options) {
|
|
2647
|
+
if (query.scope === "currentTerm") return post.terms?.some((term) => term.id === options.termId) === true;
|
|
2648
|
+
if (query.scope === "currentPath") {
|
|
2649
|
+
const path = options.path.replace(/\/+$/, "");
|
|
2650
|
+
return post.path?.trim().startsWith(`${path}/`) === true;
|
|
2651
|
+
}
|
|
2652
|
+
return true;
|
|
2653
|
+
}
|
|
2654
|
+
function evaluateContentQuery(query, candidates, options) {
|
|
2655
|
+
if (!options.complete) throw new Error("Content query input is incomplete");
|
|
2656
|
+
const page = options.page ?? 1;
|
|
2657
|
+
if (!Number.isSafeInteger(page) || page < 1) throw new Error("Content query page must be a positive safe integer");
|
|
2658
|
+
if (query.scope === "currentTerm" && !options.termId) throw new Error("Content query requires the current term id");
|
|
2659
|
+
if (query.scope === "currentPath" && (!options.path || !ContentPolicyInternalPathSchema.safeParse(options.path).success)) throw new Error("Content query requires a safe current path");
|
|
2660
|
+
const visible = candidates.filter(({ policy }) => policy.visible);
|
|
2661
|
+
const pathCounts = /* @__PURE__ */ new Map();
|
|
2662
|
+
const canonical = (post) => post.path?.trim().replace(/\/+$/, "") ?? null;
|
|
2663
|
+
for (const { post } of visible) pathCounts.set(canonical(post), (pathCounts.get(canonical(post)) ?? 0) + 1);
|
|
2664
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2665
|
+
const selected = visible.filter(({ post }) => {
|
|
2666
|
+
if (pathCounts.get(canonical(post)) !== 1 || seen.has(post.id)) return false;
|
|
2667
|
+
seen.add(post.id);
|
|
2668
|
+
return query.modelKeys.includes(post.modelKey ?? post.collectionKey ?? "") && matchesScope(query, post, options) && (query.filters ?? []).every((filter) => matchesContentScalarCheck(filter, post.fields ?? {}));
|
|
2669
|
+
}).sort((left, right) => {
|
|
2670
|
+
if (query.pinnedField) {
|
|
2671
|
+
const delta = Number(right.post.fields?.[query.pinnedField] === true) - Number(left.post.fields?.[query.pinnedField] === true);
|
|
2672
|
+
if (delta) return delta;
|
|
2673
|
+
}
|
|
2674
|
+
return Date.parse(right.post.publishedAt) - Date.parse(left.post.publishedAt) || left.post.id.localeCompare(right.post.id);
|
|
2675
|
+
});
|
|
2676
|
+
const pageCount = Math.max(1, Math.ceil(selected.length / query.pageSize));
|
|
2677
|
+
const current = selected.slice((page - 1) * query.pageSize, page * query.pageSize);
|
|
2678
|
+
const pageVisible = page <= pageCount && (!query.requireItems || selected.length > 0);
|
|
2679
|
+
const hasIndexable = current.some(({ policy }) => policy.indexable);
|
|
2680
|
+
const forced = !hasIndexable || options.robots?.forceNoindex === true;
|
|
2681
|
+
const reasons = [.../* @__PURE__ */ new Set([
|
|
2682
|
+
...page > pageCount ? ["content:invalid-page"] : query.requireItems && !selected.length ? ["content:empty-required-list"] : !current.length ? ["content:empty-page"] : !hasIndexable ? ["content:no-indexable-items"] : [],
|
|
2683
|
+
...options.robots?.forceNoindex ? options.robots.noindexReasons?.length ? options.robots.noindexReasons : ["content:forced-noindex"] : []
|
|
2684
|
+
])].slice(0, 64);
|
|
2685
|
+
return {
|
|
2686
|
+
items: current.map(({ post }) => post),
|
|
2687
|
+
total: selected.length,
|
|
2688
|
+
page,
|
|
2689
|
+
pageCount,
|
|
2690
|
+
visible: pageVisible,
|
|
2691
|
+
indexable: pageVisible && !forced,
|
|
2692
|
+
robots: { ...options.robots, ...forced ? { forceNoindex: true } : {}, ...reasons.length ? { noindexReasons: reasons } : {} },
|
|
2693
|
+
reasons
|
|
2694
|
+
};
|
|
2695
|
+
}
|
|
2696
|
+
function selectContentQuery(query, posts, policies, options) {
|
|
2697
|
+
if (!options.complete) throw new Error("Content query input is incomplete");
|
|
2698
|
+
const candidates = posts.filter((post) => query.modelKeys.includes(post.modelKey ?? post.collectionKey ?? "")).map((post) => {
|
|
2699
|
+
const modelKey = post.modelKey ?? post.collectionKey ?? "";
|
|
2700
|
+
if (!Object.hasOwn(policies, modelKey)) throw new Error(`Content query has no policy for model ${modelKey}`);
|
|
2701
|
+
return { post, policy: evaluateContentPolicy(policies[modelKey], post, { now: options.now, ...options.postOptions(post) }) };
|
|
2702
|
+
});
|
|
2703
|
+
return evaluateContentQuery(query, candidates, options);
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2286
2706
|
// src/redirect-paths.ts
|
|
2287
2707
|
var CONTROL_OR_BACKSLASH = /[\\\u0000-\u001f\u007f]/u;
|
|
2288
2708
|
var PROTECTED_PREFIX = /^\/(?:api|_next|_vercel|auth|admin|login|logout|revalidate|preview|assets|static|images|fonts|\.well-known)(?:\/|$)/i;
|
|
@@ -2350,6 +2770,15 @@ function normalizeRedirectTarget(raw) {
|
|
|
2350
2770
|
CMS_IMAGE_WIDTHS,
|
|
2351
2771
|
CMS_LINE_HEIGHTS,
|
|
2352
2772
|
CMS_PARAGRAPH_SPACINGS,
|
|
2773
|
+
ContentPolicyCheckSchema,
|
|
2774
|
+
ContentPolicyFieldNameSchema,
|
|
2775
|
+
ContentPolicyInternalPathSchema,
|
|
2776
|
+
ContentPolicyScalarCheckSchema,
|
|
2777
|
+
ContentPolicyScalarSchema,
|
|
2778
|
+
ContentPolicySchema,
|
|
2779
|
+
ContentPolicyWhenSchema,
|
|
2780
|
+
ContentQueryFilterSchema,
|
|
2781
|
+
ContentQuerySchema,
|
|
2353
2782
|
DEFAULT_SITE_PATTERN_PRESENTATION,
|
|
2354
2783
|
EMPTY_SITE_PATTERN_PLACEMENTS,
|
|
2355
2784
|
FIELD_BINDING_SOURCE,
|
|
@@ -2387,10 +2816,17 @@ function normalizeRedirectTarget(raw) {
|
|
|
2387
2816
|
cmsImageWidth,
|
|
2388
2817
|
cmsLineHeight,
|
|
2389
2818
|
cmsParagraphSpacing,
|
|
2819
|
+
collectContentPolicyFieldRefs,
|
|
2820
|
+
collectContentQueryFieldRefs,
|
|
2821
|
+
collectMetadataDefaultFieldRefs,
|
|
2822
|
+
contentPolicyBodyText,
|
|
2823
|
+
contentPolicySafeHttpsUrl,
|
|
2390
2824
|
createFieldBindingResolver,
|
|
2391
2825
|
emptyPatternSlots,
|
|
2392
2826
|
ensureImportedHtmlDocumentRoots,
|
|
2393
2827
|
ensureStableIds,
|
|
2828
|
+
evaluateContentPolicy,
|
|
2829
|
+
evaluateContentQuery,
|
|
2394
2830
|
extractFaqEntries,
|
|
2395
2831
|
faqPageJsonLd,
|
|
2396
2832
|
findInternalContentLinkTokens,
|
|
@@ -2407,14 +2843,17 @@ function normalizeRedirectTarget(raw) {
|
|
|
2407
2843
|
isAllowedMapEmbedSrc,
|
|
2408
2844
|
isInCollectionMode,
|
|
2409
2845
|
isProtectedRedirectPath,
|
|
2846
|
+
isReservedCategoryTreeSlug,
|
|
2410
2847
|
isSectionContentPropPath,
|
|
2411
2848
|
isSitePatternSlotKey,
|
|
2412
2849
|
isValidBlock,
|
|
2413
2850
|
isValidSitePatternKey,
|
|
2414
2851
|
issuePreviewToken,
|
|
2852
|
+
matchesContentScalarCheck,
|
|
2415
2853
|
mergeMetadataPatch,
|
|
2416
2854
|
metadataBodyParagraphs,
|
|
2417
2855
|
metadataContentImageUrl,
|
|
2856
|
+
metadataVideoThumbnail,
|
|
2418
2857
|
normalizeEmbed,
|
|
2419
2858
|
normalizeHexColor,
|
|
2420
2859
|
normalizeImageAlt,
|
|
@@ -2433,11 +2872,13 @@ function normalizeRedirectTarget(raw) {
|
|
|
2433
2872
|
renderInternalContentLinksInText,
|
|
2434
2873
|
resolveCategoryPath,
|
|
2435
2874
|
resolveCategoryTreeChain,
|
|
2875
|
+
resolveCategoryTreeEntryCategory,
|
|
2436
2876
|
resolveCategoryTreeEntryPath,
|
|
2437
2877
|
resolveCategoryTreeHubPaths,
|
|
2438
2878
|
resolveCategoryTreeSelection,
|
|
2439
2879
|
resolveMetadata,
|
|
2440
2880
|
resolvePatternSlots,
|
|
2881
|
+
resolvePostBodySource,
|
|
2441
2882
|
resolvePostCategorySlug,
|
|
2442
2883
|
resolvePostCollection,
|
|
2443
2884
|
resolvePostPath,
|
|
@@ -2447,6 +2888,7 @@ function normalizeRedirectTarget(raw) {
|
|
|
2447
2888
|
sanitizeImportedCss,
|
|
2448
2889
|
sanitizeImportedHtml,
|
|
2449
2890
|
scopeImportedCss,
|
|
2891
|
+
selectContentQuery,
|
|
2450
2892
|
serializeBlock,
|
|
2451
2893
|
setSitePatternPlacementRule,
|
|
2452
2894
|
sitePatternButtonColor,
|