@roottale/cms-core 0.67.1 → 0.69.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -35,6 +35,15 @@ __export(src_exports, {
35
35
  CMS_IMAGE_WIDTHS: () => CMS_IMAGE_WIDTHS,
36
36
  CMS_LINE_HEIGHTS: () => CMS_LINE_HEIGHTS,
37
37
  CMS_PARAGRAPH_SPACINGS: () => CMS_PARAGRAPH_SPACINGS,
38
+ ContentPolicyCheckSchema: () => ContentPolicyCheckSchema,
39
+ ContentPolicyFieldNameSchema: () => ContentPolicyFieldNameSchema,
40
+ ContentPolicyInternalPathSchema: () => ContentPolicyInternalPathSchema,
41
+ ContentPolicyScalarCheckSchema: () => ContentPolicyScalarCheckSchema,
42
+ ContentPolicyScalarSchema: () => ContentPolicyScalarSchema,
43
+ ContentPolicySchema: () => ContentPolicySchema,
44
+ ContentPolicyWhenSchema: () => ContentPolicyWhenSchema,
45
+ ContentQueryFilterSchema: () => ContentQueryFilterSchema,
46
+ ContentQuerySchema: () => ContentQuerySchema,
38
47
  DEFAULT_SITE_PATTERN_PRESENTATION: () => DEFAULT_SITE_PATTERN_PRESENTATION,
39
48
  EMPTY_SITE_PATTERN_PLACEMENTS: () => EMPTY_SITE_PATTERN_PLACEMENTS,
40
49
  FIELD_BINDING_SOURCE: () => FIELD_BINDING_SOURCE,
@@ -72,10 +81,16 @@ __export(src_exports, {
72
81
  cmsImageWidth: () => cmsImageWidth,
73
82
  cmsLineHeight: () => cmsLineHeight,
74
83
  cmsParagraphSpacing: () => cmsParagraphSpacing,
84
+ collectContentPolicyFieldRefs: () => collectContentPolicyFieldRefs,
85
+ collectContentQueryFieldRefs: () => collectContentQueryFieldRefs,
86
+ contentPolicyBodyText: () => contentPolicyBodyText,
87
+ contentPolicySafeHttpsUrl: () => contentPolicySafeHttpsUrl,
75
88
  createFieldBindingResolver: () => createFieldBindingResolver,
76
89
  emptyPatternSlots: () => emptyPatternSlots,
77
90
  ensureImportedHtmlDocumentRoots: () => ensureImportedHtmlDocumentRoots,
78
91
  ensureStableIds: () => ensureStableIds,
92
+ evaluateContentPolicy: () => evaluateContentPolicy,
93
+ evaluateContentQuery: () => evaluateContentQuery,
79
94
  extractFaqEntries: () => extractFaqEntries,
80
95
  faqPageJsonLd: () => faqPageJsonLd,
81
96
  findInternalContentLinkTokens: () => findInternalContentLinkTokens,
@@ -92,12 +107,15 @@ __export(src_exports, {
92
107
  isAllowedMapEmbedSrc: () => isAllowedMapEmbedSrc,
93
108
  isInCollectionMode: () => isInCollectionMode,
94
109
  isProtectedRedirectPath: () => isProtectedRedirectPath,
110
+ isReservedCategoryTreeSlug: () => isReservedCategoryTreeSlug,
95
111
  isSectionContentPropPath: () => isSectionContentPropPath,
96
112
  isSitePatternSlotKey: () => isSitePatternSlotKey,
97
113
  isValidBlock: () => isValidBlock,
98
114
  isValidSitePatternKey: () => isValidSitePatternKey,
99
115
  issuePreviewToken: () => issuePreviewToken,
116
+ matchesContentScalarCheck: () => matchesContentScalarCheck,
100
117
  mergeMetadataPatch: () => mergeMetadataPatch,
118
+ metadataBodyParagraphs: () => metadataBodyParagraphs,
101
119
  metadataContentImageUrl: () => metadataContentImageUrl,
102
120
  normalizeEmbed: () => normalizeEmbed,
103
121
  normalizeHexColor: () => normalizeHexColor,
@@ -117,11 +135,13 @@ __export(src_exports, {
117
135
  renderInternalContentLinksInText: () => renderInternalContentLinksInText,
118
136
  resolveCategoryPath: () => resolveCategoryPath,
119
137
  resolveCategoryTreeChain: () => resolveCategoryTreeChain,
138
+ resolveCategoryTreeEntryCategory: () => resolveCategoryTreeEntryCategory,
120
139
  resolveCategoryTreeEntryPath: () => resolveCategoryTreeEntryPath,
121
140
  resolveCategoryTreeHubPaths: () => resolveCategoryTreeHubPaths,
122
141
  resolveCategoryTreeSelection: () => resolveCategoryTreeSelection,
123
142
  resolveMetadata: () => resolveMetadata,
124
143
  resolvePatternSlots: () => resolvePatternSlots,
144
+ resolvePostBodySource: () => resolvePostBodySource,
125
145
  resolvePostCategorySlug: () => resolvePostCategorySlug,
126
146
  resolvePostCollection: () => resolvePostCollection,
127
147
  resolvePostPath: () => resolvePostPath,
@@ -131,6 +151,7 @@ __export(src_exports, {
131
151
  sanitizeImportedCss: () => sanitizeImportedCss,
132
152
  sanitizeImportedHtml: () => sanitizeImportedHtml,
133
153
  scopeImportedCss: () => scopeImportedCss,
154
+ selectContentQuery: () => selectContentQuery,
134
155
  serializeBlock: () => serializeBlock,
135
156
  setSitePatternPlacementRule: () => setSitePatternPlacementRule,
136
157
  sitePatternButtonColor: () => sitePatternButtonColor,
@@ -367,6 +388,9 @@ function validateCollections(collections) {
367
388
  }
368
389
 
369
390
  // src/category-tree.ts
391
+ function isReservedCategoryTreeSlug(presentation, slug) {
392
+ return slug === presentation.categoryFallbackSlug || presentation.reservedCategorySlugs?.includes(slug) === true;
393
+ }
370
394
  function basePathPrefix(basePath) {
371
395
  if (!basePath.trim()) return null;
372
396
  return basePath.replace(/\/+$/, "");
@@ -395,6 +419,7 @@ function hasChildCategory(termId, terms) {
395
419
  function resolveCategoryTreeChain(presentation, termId, terms) {
396
420
  const chain = categoryTreeAncestry(presentation.categoryDepth, termId, terms);
397
421
  if (!chain) return null;
422
+ if (chain.some((term) => isReservedCategoryTreeSlug(presentation, term.slug))) return null;
398
423
  if (presentation.entryCategory === "leaf") return chain;
399
424
  if (hasChildCategory(termId, terms)) return null;
400
425
  return chain.length === presentation.categoryDepth ? chain : null;
@@ -403,6 +428,33 @@ function resolveCategoryTreeSelection(presentation, termId, terms) {
403
428
  if (hasChildCategory(termId, terms)) return null;
404
429
  return resolveCategoryTreeChain(presentation, termId, terms);
405
430
  }
431
+ function resolveCategoryTreeEntryCategory(presentation, entry, terms) {
432
+ const termsById = new Map(terms.map((term) => [term.id, term]));
433
+ const first = presentation.categorySelection === "first";
434
+ const categoryIds = [...new Set(entry.terms.filter((term) => term.taxonomy === "category" && (first || termsById.has(term.id))).map((term) => term.id))];
435
+ if (first) {
436
+ if (termsById.size !== terms.length) return null;
437
+ if (categoryIds.length === 0) {
438
+ const slug = presentation.categoryFallbackSlug;
439
+ if (presentation.categoryDepth !== 1 || !slug || presentation.reservedCategorySlugs?.includes(slug) || terms.some((term) => term.slug === slug)) return null;
440
+ return { kind: "fallback", slug };
441
+ }
442
+ const candidates = [];
443
+ for (const id of categoryIds) {
444
+ const category2 = termsById.get(id);
445
+ if (!category2 || !Number.isInteger(category2.termOrder ?? 0)) return null;
446
+ const chain2 = resolveCategoryTreeChain(presentation, id, terms);
447
+ if (!chain2) return null;
448
+ candidates.push({ category: category2, chain: chain2 });
449
+ }
450
+ candidates.sort((a, b) => (a.category.termOrder ?? 0) - (b.category.termOrder ?? 0) || (a.category.id < b.category.id ? -1 : a.category.id > b.category.id ? 1 : 0));
451
+ return { kind: "category", ...candidates[0] };
452
+ }
453
+ if (categoryIds.length !== 1) return null;
454
+ const category = termsById.get(categoryIds[0]);
455
+ const chain = resolveCategoryTreeChain(presentation, category.id, terms);
456
+ return chain ? { kind: "category", category, chain } : null;
457
+ }
406
458
  function resolveCategoryTreeHubPaths(presentation, termId, terms) {
407
459
  const chain = resolveCategoryTreeChain(presentation, termId, terms);
408
460
  const prefix = basePathPrefix(presentation.basePath);
@@ -417,20 +469,11 @@ function resolveCategoryTreeHubPaths(presentation, termId, terms) {
417
469
  }
418
470
  function resolveCategoryTreeEntryPath(presentation, entry, terms) {
419
471
  if (!entry.slug) return null;
420
- const knownIds = new Set(terms.map((term) => term.id));
421
- const categoryIds = [
422
- ...new Set(
423
- entry.terms.filter((term) => term.taxonomy === "category" && knownIds.has(term.id)).map((term) => term.id)
424
- )
425
- ];
426
- if (categoryIds.length !== 1) return null;
427
- const hubs = resolveCategoryTreeHubPaths(
428
- presentation,
429
- categoryIds[0],
430
- terms
431
- );
432
- const leafPath = hubs?.at(-1);
433
- return leafPath ? `${leafPath}/${encodeURIComponent(entry.slug)}` : null;
472
+ const selected = resolveCategoryTreeEntryCategory(presentation, entry, terms);
473
+ const prefix = basePathPrefix(presentation.basePath);
474
+ if (!selected || prefix === null) return null;
475
+ const slugs = selected.kind === "fallback" ? [selected.slug] : selected.chain.map((term) => term.slug);
476
+ return `${prefix}/${[...slugs, entry.slug].map(encodeURIComponent).join("/")}`;
434
477
  }
435
478
 
436
479
  // src/public-path.ts
@@ -1115,10 +1158,10 @@ function scopeImportedCss(css, scope, allowedAssetUrls = []) {
1115
1158
 
1116
1159
  // src/internal-content-links.ts
1117
1160
  var INTERNAL_CONTENT_LINK_TOKEN_SOURCE = String.raw`\[\[internal:([a-z0-9가-힣]+(?:[.-][a-z0-9가-힣]+)*)\|([^\]\n|]+)\]\]`;
1118
- function findInternalContentLinkTokens(text2) {
1161
+ function findInternalContentLinkTokens(text3) {
1119
1162
  const pattern = new RegExp(INTERNAL_CONTENT_LINK_TOKEN_SOURCE, "giu");
1120
1163
  const tokens = [];
1121
- for (const match of text2.matchAll(pattern)) {
1164
+ for (const match of text3.matchAll(pattern)) {
1122
1165
  const [raw, key, label] = match;
1123
1166
  if (!key || !label || match.index === void 0) continue;
1124
1167
  tokens.push({
@@ -1177,19 +1220,19 @@ function escapeHtmlText(value) {
1177
1220
  var INTERNAL_CONTENT_LINK_CLASS = "rt-internal-link";
1178
1221
  var INTERNAL_CONTENT_LINK_PENDING_CLASS = "rt-internal-link--pending";
1179
1222
  var INTERNAL_CONTENT_LINK_PENDING_ATTR = "data-rt-internal-link-pending";
1180
- function renderInternalContentLinksInText(text2, resolve) {
1181
- const tokens = findInternalContentLinkTokens(text2);
1182
- 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 };
1183
1226
  let cursor = 0;
1184
1227
  let html = "";
1185
1228
  for (const token of tokens) {
1186
- html += text2.slice(cursor, token.index);
1229
+ html += text3.slice(cursor, token.index);
1187
1230
  const path = resolve(token.key);
1188
1231
  const label = escapeHtmlText(token.label);
1189
1232
  html += isSafeInternalPath(path) ? `<a href="${escapeHtmlText(path)}" class="${INTERNAL_CONTENT_LINK_CLASS}">${label}</a>` : `<span class="${INTERNAL_CONTENT_LINK_CLASS} ${INTERNAL_CONTENT_LINK_PENDING_CLASS}" ${INTERNAL_CONTENT_LINK_PENDING_ATTR}="${escapeHtmlText(token.key)}">${label}</span>`;
1190
1233
  cursor = token.index + token.raw.length;
1191
1234
  }
1192
- html += text2.slice(cursor);
1235
+ html += text3.slice(cursor);
1193
1236
  return { html, hasTokens: true };
1194
1237
  }
1195
1238
  var INTERNAL_LINK_BLOCKED_HTML_TAGS = /* @__PURE__ */ new Set([
@@ -1223,12 +1266,12 @@ function renderInternalContentLinksInHtml(html, resolve) {
1223
1266
  let cursor = 0;
1224
1267
  let out = "";
1225
1268
  let hasTokens = false;
1226
- const emitText = (text2) => {
1227
- if (blockedDepth > 0 || !text2) {
1228
- out += text2;
1269
+ const emitText = (text3) => {
1270
+ if (blockedDepth > 0 || !text3) {
1271
+ out += text3;
1229
1272
  return;
1230
1273
  }
1231
- const rendered = renderInternalContentLinksInText(text2, resolve);
1274
+ const rendered = renderInternalContentLinksInText(text3, resolve);
1232
1275
  hasTokens ||= rendered.hasTokens;
1233
1276
  out += rendered.html;
1234
1277
  };
@@ -1948,13 +1991,13 @@ function findSectionLockViolations(beforeDoc, afterDoc) {
1948
1991
  ...Object.keys(next.attrs)
1949
1992
  ]);
1950
1993
  fields.delete("props");
1951
- for (const field of fields) {
1952
- if (!valueEquals(section.attrs[field], next.attrs[field])) {
1994
+ for (const field3 of fields) {
1995
+ if (!valueEquals(section.attrs[field3], next.attrs[field3])) {
1953
1996
  violations.push({
1954
- code: field === "lock" ? "locked_section_lock_changed" : "locked_section_structure_changed",
1997
+ code: field3 === "lock" ? "locked_section_lock_changed" : "locked_section_structure_changed",
1955
1998
  sectionId: section.id,
1956
1999
  sectionType: section.sectionType,
1957
- field
2000
+ field: field3
1958
2001
  });
1959
2002
  }
1960
2003
  }
@@ -2012,11 +2055,11 @@ function imageUrl(value, base) {
2012
2055
  function resolveMetadata(input) {
2013
2056
  const layers = [input.overrides, input.defaults, input.siteDefaults, input.fallback];
2014
2057
  const result = {};
2015
- for (const field of ["title", "description", "ogImageAlt"]) {
2058
+ for (const field3 of ["title", "description", "ogImageAlt"]) {
2016
2059
  for (const layer of layers) {
2017
- const value = text(layer?.[field]);
2060
+ const value = text(layer?.[field3]);
2018
2061
  if (value !== void 0) {
2019
- result[field] = value;
2062
+ result[field3] = value;
2020
2063
  break;
2021
2064
  }
2022
2065
  }
@@ -2028,11 +2071,11 @@ function resolveMetadata(input) {
2028
2071
  break;
2029
2072
  }
2030
2073
  }
2031
- for (const field of ["noindex", "nofollow"]) {
2074
+ for (const field3 of ["noindex", "nofollow"]) {
2032
2075
  for (const layer of layers) {
2033
- const value = layer?.[field];
2076
+ const value = layer?.[field3];
2034
2077
  if (typeof value === "boolean") {
2035
- result[field] = value;
2078
+ result[field3] = value;
2036
2079
  break;
2037
2080
  }
2038
2081
  }
@@ -2079,87 +2122,154 @@ var MetadataPatchSchema = import_zod2.z.object({
2079
2122
  }).strict();
2080
2123
  var MetadataRobotsPolicySchema = import_zod2.z.object({
2081
2124
  forceNoindex: import_zod2.z.boolean().optional(),
2082
- forceNofollow: import_zod2.z.boolean().optional()
2125
+ forceNofollow: import_zod2.z.boolean().optional(),
2126
+ /** Stable policy reason codes. They explain an enforced exclusion, never relax it. */
2127
+ noindexReasons: import_zod2.z.array(import_zod2.z.string().trim().min(1).max(200)).max(64).optional()
2083
2128
  }).strict();
2084
2129
  function readMetadataValues(input) {
2085
2130
  if (!input || typeof input !== "object" || Array.isArray(input)) return {};
2086
- const record = input;
2131
+ const record2 = input;
2087
2132
  const output = {};
2088
- for (const field of METADATA_FIELDS) {
2089
- const parsed = metadataFields[field].safeParse(record[field]);
2090
- 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;
2091
2136
  }
2092
2137
  return MetadataValuesSchema.parse(output);
2093
2138
  }
2094
2139
  function mergeMetadataPatch(current, input) {
2095
2140
  const patch = MetadataPatchSchema.parse(input);
2096
2141
  const next = { ...current };
2097
- for (const field of METADATA_FIELDS) {
2098
- if (patch[field] === null) delete next[field];
2099
- 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];
2100
2145
  }
2101
2146
  return next;
2102
2147
  }
2103
2148
 
2104
2149
  // src/metadata-defaults.ts
2150
+ var import_zod4 = require("zod");
2151
+
2152
+ // src/metadata-text.ts
2105
2153
  var import_zod3 = require("zod");
2106
2154
  var fallbackText = import_zod3.z.string().max(2e3);
2107
- var imageOrigin = import_zod3.z.string().max(2048).refine((value) => {
2108
- try {
2109
- const url = new URL(value);
2110
- return url.protocol === "https:" && url.origin === value;
2111
- } catch {
2112
- return false;
2113
- }
2114
- }, "\uC774\uBBF8\uC9C0 \uD5C8\uC6A9 \uBAA9\uB85D\uC5D0\uB294 \uACBD\uB85C \uC5C6\uB294 HTTPS origin\uB9CC \uC120\uC5B8\uD569\uB2C8\uB2E4");
2155
+ var templateToken = /\{\{(?:title|field:[a-zA-Z][a-zA-Z0-9_-]{0,159})\}\}/gu;
2156
+ var template = fallbackText.refine(
2157
+ (value) => !/[{}]/u.test(value.replace(templateToken, "")),
2158
+ "\uC790\uB3D9 \uD14D\uC2A4\uD2B8\uC5D0\uB294 {{title}} \uB610\uB294 {{field:\uD544\uB4DC\uC774\uB984}} \uD1A0\uD070\uB9CC \uC0AC\uC6A9\uD569\uB2C8\uB2E4"
2159
+ );
2115
2160
  var MetadataTextRuleSchema = import_zod3.z.object({
2116
- source: import_zod3.z.enum(["content", "fallback"]).optional(),
2161
+ source: import_zod3.z.enum(["content", "fallback", "template"]).optional(),
2162
+ template: template.optional(),
2117
2163
  stripLeadingLabel: import_zod3.z.enum(["question", "answer"]).optional(),
2118
2164
  replaceLineBreakTokens: import_zod3.z.boolean().optional(),
2119
2165
  removeEmDashes: import_zod3.z.boolean().optional(),
2120
2166
  normalizeWhitespace: import_zod3.z.boolean().optional(),
2121
2167
  maxLength: import_zod3.z.number().int().min(1).max(2e3).optional(),
2168
+ lengthUnit: import_zod3.z.enum(["code-units", "code-points"]).optional(),
2169
+ wordBoundaryMinLength: import_zod3.z.number().int().min(1).max(2e3).optional(),
2122
2170
  ellipsis: import_zod3.z.string().max(10).optional(),
2171
+ bodyFallback: import_zod3.z.object({
2172
+ preferContaining: import_zod3.z.string().trim().min(1).max(100).optional(),
2173
+ stripLeadingLabel: import_zod3.z.literal("conclusion").optional()
2174
+ }).strict().optional(),
2123
2175
  fallback: fallbackText.optional(),
2124
2176
  fallbackWithParent: fallbackText.optional(),
2125
2177
  fallbackWithoutParent: fallbackText.optional(),
2126
2178
  fallbackBySlug: import_zod3.z.record(import_zod3.z.string().max(200), fallbackText).optional()
2127
- }).strict().refine((rule) => !rule.ellipsis || rule.maxLength !== void 0 && rule.ellipsis.length < rule.maxLength, "\uB9D0\uC904\uC784\uD45C\uC5D0\uB294 \uB354 \uAE34 \uCD5C\uB300 \uAE38\uC774\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4");
2128
- var MetadataDefaultRulesSchema = import_zod3.z.object({
2129
- title: MetadataTextRuleSchema.optional(),
2130
- description: MetadataTextRuleSchema.optional(),
2131
- useFirstBodyImage: import_zod3.z.boolean().optional(),
2132
- useTermImage: import_zod3.z.boolean().optional(),
2133
- imageAltSource: import_zod3.z.enum(["content", "title"]).optional(),
2134
- legacyImageSource: import_zod3.z.literal("legacy-thumbnail").optional(),
2135
- imageAllowedOrigins: import_zod3.z.array(imageOrigin).min(1).max(32).optional(),
2136
- legacyImageAllowedOrigins: import_zod3.z.array(imageOrigin).min(1).max(32).optional(),
2137
- normalizeCloudflareImages: import_zod3.z.boolean().optional(),
2138
- fallback: MetadataValuesSchema.optional()
2139
- }).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");
2140
- function cleanText(value, rule) {
2141
- let text2 = value.trim();
2142
- if (rule?.stripLeadingLabel === "question") text2 = text2.replace(/^\s*Q[..:]?\s*/iu, "");
2143
- if (rule?.stripLeadingLabel === "answer") text2 = text2.replace(/^\s*A[..:]?\s*/iu, "");
2144
- if (rule?.replaceLineBreakTokens) text2 = text2.replace(/\[\[br(?::[^\]]*)?\]\]/giu, " ");
2145
- if (rule?.removeEmDashes) text2 = text2.replaceAll("\u2014", "");
2146
- if (rule?.normalizeWhitespace) text2 = text2.replace(/\s+/gu, " ");
2147
- return text2.trim();
2148
- }
2149
- function formatText(value, rule, input, name) {
2150
- let text2 = rule?.source === "fallback" ? "" : cleanText(value ?? "", rule);
2151
- if (!text2) {
2152
- const fallback = input.parentId ? rule?.fallbackWithParent ?? rule?.fallback : (input.slug ? rule?.fallbackBySlug?.[input.slug] : void 0) ?? rule?.fallbackWithoutParent ?? rule?.fallback;
2153
- text2 = cleanText((fallback ?? "").replaceAll("%s", () => name), rule);
2154
- }
2155
- if (!text2) return void 0;
2156
- if (rule?.maxLength && text2.length > rule.maxLength) {
2179
+ }).strict().refine((rule) => !rule.ellipsis || rule.maxLength !== void 0 && rule.ellipsis.length < rule.maxLength, "\uB9D0\uC904\uC784\uD45C\uC5D0\uB294 \uB354 \uAE34 \uCD5C\uB300 \uAE38\uC774\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4").refine((rule) => rule.source === "template" ? rule.template !== void 0 : rule.template === void 0, "\uD14D\uC2A4\uD2B8 template\uC740 source: template\uACFC \uD568\uAED8 \uC120\uC5B8\uD569\uB2C8\uB2E4").refine((rule) => !rule.wordBoundaryMinLength || Boolean(rule.maxLength && rule.wordBoundaryMinLength < rule.maxLength), "\uB2E8\uC5B4 \uACBD\uACC4\uC5D0\uB294 \uB354 \uAE34 \uCD5C\uB300 \uAE38\uC774\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4");
2180
+ function cleanMetadataText(value, rule) {
2181
+ let text3 = value.trim();
2182
+ if (rule?.stripLeadingLabel === "question") text3 = text3.replace(/^\s*Q[..:]?\s*/iu, "");
2183
+ if (rule?.stripLeadingLabel === "answer") text3 = text3.replace(/^\s*A[..:]?\s*/iu, "");
2184
+ if (rule?.replaceLineBreakTokens) text3 = text3.replace(/\[\[br(?::[^\]]*)?\]\]/giu, " ");
2185
+ if (rule?.removeEmDashes) text3 = text3.replaceAll("\u2014", "");
2186
+ if (rule?.normalizeWhitespace) text3 = text3.replace(/\s+/gu, " ");
2187
+ return text3.trim();
2188
+ }
2189
+ function metadataBodyParagraphs(value) {
2190
+ const paragraphs = [];
2191
+ const textParts = (node) => {
2192
+ if (Array.isArray(node)) return node.flatMap(textParts);
2193
+ if (!node || typeof node !== "object") return [];
2194
+ const item = node;
2195
+ return [...typeof item.text === "string" ? [item.text] : [], ...textParts(item.content)];
2196
+ };
2197
+ const walk = (node) => {
2198
+ if (Array.isArray(node)) {
2199
+ node.forEach(walk);
2200
+ return;
2201
+ }
2202
+ if (!node || typeof node !== "object") return;
2203
+ const item = node;
2204
+ if (item.type === "paragraph") {
2205
+ const text3 = textParts(item.content).join(" ").replace(/\s+/gu, " ").trim();
2206
+ if (text3) paragraphs.push(text3);
2207
+ } else walk(item.content);
2208
+ };
2209
+ walk(value);
2210
+ return paragraphs;
2211
+ }
2212
+ function formatMetadataText(value, rule, input, name) {
2213
+ let text3 = rule?.source === "fallback" ? "" : cleanMetadataText(value ?? "", rule);
2214
+ if (rule?.source === "template") {
2215
+ text3 = cleanMetadataText((rule.template ?? "").replace(templateToken, (token) => {
2216
+ if (token === "{{title}}") return name;
2217
+ const field3 = token.slice(8, -2);
2218
+ const value2 = Object.hasOwn(input.fields ?? {}, field3) ? input.fields?.[field3] : void 0;
2219
+ return typeof value2 === "string" ? value2.trim() : "";
2220
+ }), rule);
2221
+ }
2222
+ if (!text3 && rule?.bodyFallback) {
2223
+ const paragraphs = input.bodyParagraphs ?? [];
2224
+ const preferred = rule.bodyFallback.preferContaining;
2225
+ text3 = (preferred ? paragraphs.find((paragraph) => paragraph.includes(preferred)) : void 0) ?? paragraphs[0] ?? "";
2226
+ if (rule.bodyFallback.stripLeadingLabel === "conclusion") text3 = text3.replace(/^(먼저\s*)?결론부터\s*(말씀드리면|말씀드릴게요)[.!?]?\s*/u, "");
2227
+ text3 = cleanMetadataText(text3, rule);
2228
+ }
2229
+ if (!text3) {
2230
+ const slugFallback = input.slug && Object.hasOwn(rule?.fallbackBySlug ?? {}, input.slug) ? rule?.fallbackBySlug?.[input.slug] : void 0;
2231
+ const fallback = input.parentId ? rule?.fallbackWithParent ?? rule?.fallback : slugFallback ?? rule?.fallbackWithoutParent ?? rule?.fallback;
2232
+ text3 = cleanMetadataText((fallback ?? "").replaceAll("%s", () => name), rule);
2233
+ }
2234
+ if (!text3) return void 0;
2235
+ const units = rule?.lengthUnit === "code-points" ? [...text3] : text3.split("");
2236
+ if (rule?.maxLength && units.length > rule.maxLength) {
2157
2237
  const suffix = rule.ellipsis ?? "";
2158
- const head = text2.slice(0, rule.maxLength - suffix.length);
2238
+ const suffixLength = rule.lengthUnit === "code-points" ? [...suffix].length : suffix.length;
2239
+ let head = units.slice(0, rule.maxLength - suffixLength).join("");
2240
+ const boundary = head.lastIndexOf(" ");
2241
+ if (rule.wordBoundaryMinLength && boundary >= rule.wordBoundaryMinLength) head = head.slice(0, boundary);
2159
2242
  return suffix ? head.trimEnd() + suffix : head;
2160
2243
  }
2161
- return text2;
2244
+ return text3;
2162
2245
  }
2246
+
2247
+ // src/metadata-defaults.ts
2248
+ var imageOrigin = import_zod4.z.string().max(2048).refine((value) => {
2249
+ try {
2250
+ const url = new URL(value);
2251
+ return url.protocol === "https:" && url.origin === value;
2252
+ } catch {
2253
+ return false;
2254
+ }
2255
+ }, "\uC774\uBBF8\uC9C0 \uD5C8\uC6A9 \uBAA9\uB85D\uC5D0\uB294 \uACBD\uB85C \uC5C6\uB294 HTTPS origin\uB9CC \uC120\uC5B8\uD569\uB2C8\uB2E4");
2256
+ var MetadataDefaultRulesSchema = import_zod4.z.object({
2257
+ title: MetadataTextRuleSchema.optional(),
2258
+ titleTemplateMaxLength: import_zod4.z.number().int().min(1).max(1e3).optional(),
2259
+ description: MetadataTextRuleSchema.optional(),
2260
+ /** Code-owned defaults precede site defaults but always follow manual overrides. */
2261
+ defaults: MetadataValuesSchema.optional(),
2262
+ robots: MetadataRobotsPolicySchema.optional(),
2263
+ useFeaturedImage: import_zod4.z.boolean().optional(),
2264
+ useFirstBodyImage: import_zod4.z.boolean().optional(),
2265
+ useTermImage: import_zod4.z.boolean().optional(),
2266
+ imageAltSource: import_zod4.z.enum(["content", "title"]).optional(),
2267
+ legacyImageSource: import_zod4.z.literal("legacy-thumbnail").optional(),
2268
+ imageAllowedOrigins: import_zod4.z.array(imageOrigin).min(1).max(32).optional(),
2269
+ legacyImageAllowedOrigins: import_zod4.z.array(imageOrigin).min(1).max(32).optional(),
2270
+ normalizeCloudflareImages: import_zod4.z.boolean().optional(),
2271
+ fallback: MetadataValuesSchema.optional()
2272
+ }).strict().refine((rule) => !rule.legacyImageSource || Boolean(rule.legacyImageAllowedOrigins?.length), "\uC774\uC804 \uC774\uBBF8\uC9C0 \uC6D0\uBCF8\uC5D0\uB294 \uD5C8\uC6A9 origin \uC120\uC5B8\uC774 \uD544\uC694\uD569\uB2C8\uB2E4");
2163
2273
  function metadataContentImageUrl(value, normalizeCloudflareImages = false) {
2164
2274
  if (!normalizeCloudflareImages) return value;
2165
2275
  try {
@@ -2199,20 +2309,328 @@ function contentCandidateAllowed(value, input, rule) {
2199
2309
  function formatMetadataDefaults(input, declaration) {
2200
2310
  const dynamic = declaration[input.kind];
2201
2311
  const rule = dynamic?.defaultRules?.[input.modelKey];
2202
- const name = cleanText(input.title, rule?.title);
2203
- const title = formatText(input.title, rule?.title, input, name);
2204
- const template = dynamic?.titleTemplates?.[input.modelKey];
2205
- const description = formatText(input.description, rule?.description, input, name);
2312
+ const name = cleanMetadataText(input.title, rule?.title);
2313
+ const title = formatMetadataText(input.title, rule?.title, input, name);
2314
+ const template2 = dynamic?.titleTemplates?.[input.modelKey];
2315
+ const templatedTitle = title && template2 ? template2.replace("%s", () => title) : title;
2316
+ const finalTitle = templatedTitle && rule?.titleTemplateMaxLength && templatedTitle.length > rule.titleTemplateMaxLength ? title : templatedTitle;
2317
+ const description = formatMetadataText(input.description, rule?.description, input, name);
2206
2318
  const bodyImage = input.firstBodyImage && (input.firstBodyImage.startsWith("https://") || /^\/(?!\/)/u.test(input.firstBodyImage)) ? input.firstBodyImage : void 0;
2207
- const candidateImage = input.kind === "terms" && rule?.useTermImage === false ? void 0 : input.featuredImage || (rule?.useFirstBodyImage ? bodyImage : void 0) || legacyImageUrl(input.legacyImage, rule);
2319
+ const featuredImage = (input.kind === "terms" ? rule?.useTermImage : rule?.useFeaturedImage) === false ? void 0 : input.featuredImage;
2320
+ const candidateImage = featuredImage || (rule?.useFirstBodyImage ? bodyImage : void 0) || legacyImageUrl(input.legacyImage, rule);
2208
2321
  const ogImage = candidateImage && contentCandidateAllowed(candidateImage, input, rule) ? readMetadataValues({ ogImage: metadataContentImageUrl(candidateImage, rule?.normalizeCloudflareImages) }).ogImage : void 0;
2209
2322
  return {
2210
- ...title ? { title: template ? template.replace("%s", () => title) : title } : {},
2323
+ ...readMetadataValues(rule?.defaults),
2324
+ ...finalTitle ? { title: finalTitle } : {},
2211
2325
  ...description ? { description } : {},
2212
2326
  ...ogImage ? { ogImage } : {}
2213
2327
  };
2214
2328
  }
2215
2329
 
2330
+ // src/post-body-source.ts
2331
+ function resolvePostBodySource(bodyHtml) {
2332
+ if (typeof bodyHtml !== "string") return { kind: "json" };
2333
+ const trimmed = bodyHtml.trim();
2334
+ if (trimmed.length === 0) return { kind: "json" };
2335
+ const sanitized = sanitizeHtml(trimmed);
2336
+ const visible = /<(?:img|hr|br|iframe|video|audio)\b/i.test(sanitized) || sanitized.replace(/<[^>]*>/g, "").replace(/&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
+
2216
2634
  // src/redirect-paths.ts
2217
2635
  var CONTROL_OR_BACKSLASH = /[\\\u0000-\u001f\u007f]/u;
2218
2636
  var PROTECTED_PREFIX = /^\/(?:api|_next|_vercel|auth|admin|login|logout|revalidate|preview|assets|static|images|fonts|\.well-known)(?:\/|$)/i;
@@ -2280,6 +2698,15 @@ function normalizeRedirectTarget(raw) {
2280
2698
  CMS_IMAGE_WIDTHS,
2281
2699
  CMS_LINE_HEIGHTS,
2282
2700
  CMS_PARAGRAPH_SPACINGS,
2701
+ ContentPolicyCheckSchema,
2702
+ ContentPolicyFieldNameSchema,
2703
+ ContentPolicyInternalPathSchema,
2704
+ ContentPolicyScalarCheckSchema,
2705
+ ContentPolicyScalarSchema,
2706
+ ContentPolicySchema,
2707
+ ContentPolicyWhenSchema,
2708
+ ContentQueryFilterSchema,
2709
+ ContentQuerySchema,
2283
2710
  DEFAULT_SITE_PATTERN_PRESENTATION,
2284
2711
  EMPTY_SITE_PATTERN_PLACEMENTS,
2285
2712
  FIELD_BINDING_SOURCE,
@@ -2317,10 +2744,16 @@ function normalizeRedirectTarget(raw) {
2317
2744
  cmsImageWidth,
2318
2745
  cmsLineHeight,
2319
2746
  cmsParagraphSpacing,
2747
+ collectContentPolicyFieldRefs,
2748
+ collectContentQueryFieldRefs,
2749
+ contentPolicyBodyText,
2750
+ contentPolicySafeHttpsUrl,
2320
2751
  createFieldBindingResolver,
2321
2752
  emptyPatternSlots,
2322
2753
  ensureImportedHtmlDocumentRoots,
2323
2754
  ensureStableIds,
2755
+ evaluateContentPolicy,
2756
+ evaluateContentQuery,
2324
2757
  extractFaqEntries,
2325
2758
  faqPageJsonLd,
2326
2759
  findInternalContentLinkTokens,
@@ -2337,12 +2770,15 @@ function normalizeRedirectTarget(raw) {
2337
2770
  isAllowedMapEmbedSrc,
2338
2771
  isInCollectionMode,
2339
2772
  isProtectedRedirectPath,
2773
+ isReservedCategoryTreeSlug,
2340
2774
  isSectionContentPropPath,
2341
2775
  isSitePatternSlotKey,
2342
2776
  isValidBlock,
2343
2777
  isValidSitePatternKey,
2344
2778
  issuePreviewToken,
2779
+ matchesContentScalarCheck,
2345
2780
  mergeMetadataPatch,
2781
+ metadataBodyParagraphs,
2346
2782
  metadataContentImageUrl,
2347
2783
  normalizeEmbed,
2348
2784
  normalizeHexColor,
@@ -2362,11 +2798,13 @@ function normalizeRedirectTarget(raw) {
2362
2798
  renderInternalContentLinksInText,
2363
2799
  resolveCategoryPath,
2364
2800
  resolveCategoryTreeChain,
2801
+ resolveCategoryTreeEntryCategory,
2365
2802
  resolveCategoryTreeEntryPath,
2366
2803
  resolveCategoryTreeHubPaths,
2367
2804
  resolveCategoryTreeSelection,
2368
2805
  resolveMetadata,
2369
2806
  resolvePatternSlots,
2807
+ resolvePostBodySource,
2370
2808
  resolvePostCategorySlug,
2371
2809
  resolvePostCollection,
2372
2810
  resolvePostPath,
@@ -2376,6 +2814,7 @@ function normalizeRedirectTarget(raw) {
2376
2814
  sanitizeImportedCss,
2377
2815
  sanitizeImportedHtml,
2378
2816
  scopeImportedCss,
2817
+ selectContentQuery,
2379
2818
  serializeBlock,
2380
2819
  setSitePatternPlacementRule,
2381
2820
  sitePatternButtonColor,