@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/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 knownIds = new Set(terms.map((term) => term.id));
422
- const categoryIds = [
423
- ...new Set(
424
- entry.terms.filter((term) => term.taxonomy === "category" && knownIds.has(term.id)).map((term) => term.id)
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(text2) {
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 text2.matchAll(pattern)) {
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(text2, resolve) {
1182
- const tokens = findInternalContentLinkTokens(text2);
1183
- if (tokens.length === 0) return { html: text2, hasTokens: false };
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 += text2.slice(cursor, token.index);
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 += text2.slice(cursor);
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 = (text2) => {
1228
- if (blockedDepth > 0 || !text2) {
1229
- out += text2;
1269
+ const emitText = (text3) => {
1270
+ if (blockedDepth > 0 || !text3) {
1271
+ out += text3;
1230
1272
  return;
1231
1273
  }
1232
- const rendered = renderInternalContentLinksInText(text2, resolve);
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 field of fields) {
1953
- if (!valueEquals(section.attrs[field], next.attrs[field])) {
1994
+ for (const field3 of fields) {
1995
+ if (!valueEquals(section.attrs[field3], next.attrs[field3])) {
1954
1996
  violations.push({
1955
- code: field === "lock" ? "locked_section_lock_changed" : "locked_section_structure_changed",
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 field of ["title", "description", "ogImageAlt"]) {
2058
+ for (const field3 of ["title", "description", "ogImageAlt"]) {
2017
2059
  for (const layer of layers) {
2018
- const value = text(layer?.[field]);
2060
+ const value = text(layer?.[field3]);
2019
2061
  if (value !== void 0) {
2020
- result[field] = value;
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 field of ["noindex", "nofollow"]) {
2074
+ for (const field3 of ["noindex", "nofollow"]) {
2033
2075
  for (const layer of layers) {
2034
- const value = layer?.[field];
2076
+ const value = layer?.[field3];
2035
2077
  if (typeof value === "boolean") {
2036
- result[field] = value;
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 record = input;
2131
+ const record2 = input;
2088
2132
  const output = {};
2089
- for (const field of METADATA_FIELDS) {
2090
- const parsed = metadataFields[field].safeParse(record[field]);
2091
- if (parsed.success) output[field] = parsed.data;
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 field of METADATA_FIELDS) {
2099
- if (patch[field] === null) delete next[field];
2100
- else if (patch[field] !== void 0) next[field] = patch[field];
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 text2 = value.trim();
2138
- if (rule?.stripLeadingLabel === "question") text2 = text2.replace(/^\s*Q[..:]?\s*/iu, "");
2139
- if (rule?.stripLeadingLabel === "answer") text2 = text2.replace(/^\s*A[..:]?\s*/iu, "");
2140
- if (rule?.replaceLineBreakTokens) text2 = text2.replace(/\[\[br(?::[^\]]*)?\]\]/giu, " ");
2141
- if (rule?.removeEmDashes) text2 = text2.replaceAll("\u2014", "");
2142
- if (rule?.normalizeWhitespace) text2 = text2.replace(/\s+/gu, " ");
2143
- return text2.trim();
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 text2 = textParts(item.content).join(" ").replace(/\s+/gu, " ").trim();
2162
- if (text2) paragraphs.push(text2);
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 text2 = rule?.source === "fallback" ? "" : cleanMetadataText(value ?? "", rule);
2213
+ let text3 = rule?.source === "fallback" ? "" : cleanMetadataText(value ?? "", rule);
2170
2214
  if (rule?.source === "template") {
2171
- text2 = cleanMetadataText((rule.template ?? "").replace(templateToken, (token) => {
2215
+ text3 = cleanMetadataText((rule.template ?? "").replace(templateToken, (token) => {
2172
2216
  if (token === "{{title}}") return name;
2173
- const field = token.slice(8, -2);
2174
- const value2 = Object.hasOwn(input.fields ?? {}, field) ? input.fields?.[field] : void 0;
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 (!text2 && rule?.bodyFallback) {
2222
+ if (!text3 && rule?.bodyFallback) {
2179
2223
  const paragraphs = input.bodyParagraphs ?? [];
2180
2224
  const preferred = rule.bodyFallback.preferContaining;
2181
- text2 = (preferred ? paragraphs.find((paragraph) => paragraph.includes(preferred)) : void 0) ?? paragraphs[0] ?? "";
2182
- if (rule.bodyFallback.stripLeadingLabel === "conclusion") text2 = text2.replace(/^(먼저\s*)?결론부터\s*(말씀드리면|말씀드릴게요)[.!?]?\s*/u, "");
2183
- text2 = cleanMetadataText(text2, rule);
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 (!text2) {
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
- text2 = cleanMetadataText((fallback ?? "").replaceAll("%s", () => name), rule);
2232
+ text3 = cleanMetadataText((fallback ?? "").replaceAll("%s", () => name), rule);
2189
2233
  }
2190
- if (!text2) return void 0;
2191
- const units = rule?.lengthUnit === "code-points" ? [...text2] : text2.split("");
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 text2;
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(/&nbsp;/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(/&nbsp;/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,