@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/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 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;
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(text2) {
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 text2.matchAll(pattern)) {
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(text2, resolve) {
1182
- const tokens = findInternalContentLinkTokens(text2);
1183
- if (tokens.length === 0) return { html: text2, hasTokens: false };
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 += text2.slice(cursor, token.index);
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 += text2.slice(cursor);
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 = (text2) => {
1228
- if (blockedDepth > 0 || !text2) {
1229
- out += text2;
1271
+ const emitText = (text3) => {
1272
+ if (blockedDepth > 0 || !text3) {
1273
+ out += text3;
1230
1274
  return;
1231
1275
  }
1232
- const rendered = renderInternalContentLinksInText(text2, resolve);
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 field of fields) {
1953
- if (!valueEquals(section.attrs[field], next.attrs[field])) {
1996
+ for (const field3 of fields) {
1997
+ if (!valueEquals(section.attrs[field3], next.attrs[field3])) {
1954
1998
  violations.push({
1955
- code: field === "lock" ? "locked_section_lock_changed" : "locked_section_structure_changed",
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 field of ["title", "description", "ogImageAlt"]) {
2060
+ for (const field3 of ["title", "description", "ogImageAlt"]) {
2017
2061
  for (const layer of layers) {
2018
- const value = text(layer?.[field]);
2062
+ const value = text(layer?.[field3]);
2019
2063
  if (value !== void 0) {
2020
- result[field] = value;
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 field of ["noindex", "nofollow"]) {
2076
+ for (const field3 of ["noindex", "nofollow"]) {
2033
2077
  for (const layer of layers) {
2034
- const value = layer?.[field];
2078
+ const value = layer?.[field3];
2035
2079
  if (typeof value === "boolean") {
2036
- result[field] = value;
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 record = input;
2133
+ const record2 = input;
2088
2134
  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;
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 field of METADATA_FIELDS) {
2099
- if (patch[field] === null) delete next[field];
2100
- else if (patch[field] !== void 0) next[field] = patch[field];
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 import_zod4 = require("zod");
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 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();
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 text2 = textParts(item.content).join(" ").replace(/\s+/gu, " ").trim();
2162
- if (text2) paragraphs.push(text2);
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 text2 = rule?.source === "fallback" ? "" : cleanMetadataText(value ?? "", rule);
2215
+ let text3 = rule?.source === "fallback" ? "" : cleanMetadataText(value ?? "", rule);
2170
2216
  if (rule?.source === "template") {
2171
- text2 = cleanMetadataText((rule.template ?? "").replace(templateToken, (token) => {
2217
+ text3 = cleanMetadataText((rule.template ?? "").replace(templateToken, (token) => {
2172
2218
  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;
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 (!text2 && rule?.bodyFallback) {
2224
+ if (!text3 && rule?.bodyFallback) {
2179
2225
  const paragraphs = input.bodyParagraphs ?? [];
2180
2226
  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);
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 (!text2) {
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
- text2 = cleanMetadataText((fallback ?? "").replaceAll("%s", () => name), rule);
2234
+ text3 = cleanMetadataText((fallback ?? "").replaceAll("%s", () => name), rule);
2189
2235
  }
2190
- if (!text2) return void 0;
2191
- const units = rule?.lengthUnit === "code-points" ? [...text2] : text2.split("");
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 text2;
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(/&nbsp;/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(/&nbsp;/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 = import_zod4.z.string().max(2048).refine((value) => {
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 = import_zod4.z.object({
2541
+ var MetadataDefaultRulesSchema = import_zod6.z.object({
2213
2542
  title: MetadataTextRuleSchema.optional(),
2214
- titleTemplateMaxLength: import_zod4.z.number().int().min(1).max(1e3).optional(),
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: import_zod4.z.boolean().optional(),
2220
- useFirstBodyImage: import_zod4.z.boolean().optional(),
2221
- useTermImage: import_zod4.z.boolean().optional(),
2222
- imageAltSource: import_zod4.z.enum(["content", "title"]).optional(),
2223
- legacyImageSource: import_zod4.z.literal("legacy-thumbnail").optional(),
2224
- imageAllowedOrigins: import_zod4.z.array(imageOrigin).min(1).max(32).optional(),
2225
- legacyImageAllowedOrigins: import_zod4.z.array(imageOrigin).min(1).max(32).optional(),
2226
- normalizeCloudflareImages: import_zod4.z.boolean().optional(),
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 featuredImage = (input.kind === "terms" ? rule?.useTermImage : rule?.useFeaturedImage) === false ? void 0 : input.featuredImage;
2276
- const candidateImage = featuredImage || (rule?.useFirstBodyImage ? bodyImage : void 0) || legacyImageUrl(input.legacyImage, rule);
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,