@roottale/cms-core 0.69.0 → 0.70.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @roottale/cms-core
2
2
 
3
+ ## 0.70.1
4
+
5
+ ## 0.70.0
6
+
7
+ ### Minor Changes
8
+
9
+ - 49145e4: 콘텐츠 공개·색인 정책의 기존 단일 when 조건을 유지하면서, 공개 필드가 모두 일치하는 all과 모두 일치할 때만 검사를 제외하는 unlessAll을 추가합니다. 각 그룹은 equality 조건 1~4개만 허용하고 중첩하지 않으며, 기존 공개 필드 참조·검사 개수 한도에 포함합니다.
10
+
11
+ 명시적인 방송 기록 조건이 모두 충족된 경우에만 검수·출연자 조건의 예외를 적용하고, 출처 필수와 강제 검색 제외를 함께 선언하는 공개 예제를 제공합니다. 누락 값과 null은 false로 바꾸지 않으며 관리자 projection·웹훅·FRONT가 같은 정책을 사용합니다.
12
+
13
+ 글의 공개 영상 필드를 `videoThumbnail: { source: "youtube", field, when? }`로 연결합니다. 선언한 모델에만 안전한 대표 이미지 다음으로 정확한 YouTube ID의 썸네일을 사용하며, 수동 SEO와 기존 무옵션 모델의 이미지 의미는 유지합니다. 같은 공개 필드와 조건을 관리자 자동값·버전·검증과 FRONT 카드/메타데이터에 재사용할 수 있습니다.
14
+
3
15
  ## 0.69.0
4
16
 
5
17
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -83,6 +83,7 @@ __export(src_exports, {
83
83
  cmsParagraphSpacing: () => cmsParagraphSpacing,
84
84
  collectContentPolicyFieldRefs: () => collectContentPolicyFieldRefs,
85
85
  collectContentQueryFieldRefs: () => collectContentQueryFieldRefs,
86
+ collectMetadataDefaultFieldRefs: () => collectMetadataDefaultFieldRefs,
86
87
  contentPolicyBodyText: () => contentPolicyBodyText,
87
88
  contentPolicySafeHttpsUrl: () => contentPolicySafeHttpsUrl,
88
89
  createFieldBindingResolver: () => createFieldBindingResolver,
@@ -117,6 +118,7 @@ __export(src_exports, {
117
118
  mergeMetadataPatch: () => mergeMetadataPatch,
118
119
  metadataBodyParagraphs: () => metadataBodyParagraphs,
119
120
  metadataContentImageUrl: () => metadataContentImageUrl,
121
+ metadataVideoThumbnail: () => metadataVideoThumbnail,
120
122
  normalizeEmbed: () => normalizeEmbed,
121
123
  normalizeHexColor: () => normalizeHexColor,
122
124
  normalizeImageAlt: () => normalizeImageAlt,
@@ -2147,7 +2149,7 @@ function mergeMetadataPatch(current, input) {
2147
2149
  }
2148
2150
 
2149
2151
  // src/metadata-defaults.ts
2150
- var import_zod4 = require("zod");
2152
+ var import_zod6 = require("zod");
2151
2153
 
2152
2154
  // src/metadata-text.ts
2153
2155
  var import_zod3 = require("zod");
@@ -2244,113 +2246,30 @@ function formatMetadataText(value, rule, input, name) {
2244
2246
  return text3;
2245
2247
  }
2246
2248
 
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");
2273
- function metadataContentImageUrl(value, normalizeCloudflareImages = false) {
2274
- if (!normalizeCloudflareImages) return value;
2275
- try {
2276
- const url = new URL(value);
2277
- const [account, id, variant] = url.pathname.split("/").filter(Boolean);
2278
- if (url.protocol === "https:" && url.hostname === "imagedelivery.net" && account && id && variant) {
2279
- url.pathname = `/${account}/${id}/lg`;
2280
- return url.href;
2281
- }
2282
- } catch {
2283
- }
2284
- return value;
2285
- }
2286
- function legacyImageUrl(value, rule) {
2287
- if (!value || rule?.legacyImageSource !== "legacy-thumbnail") return void 0;
2288
- try {
2289
- const url = new URL(value);
2290
- if (!rule.legacyImageAllowedOrigins?.includes(url.origin)) return void 0;
2291
- return readMetadataValues({ ogImage: url.href }).ogImage;
2292
- } catch {
2293
- return void 0;
2294
- }
2295
- }
2296
- function contentCandidateAllowed(value, input, rule) {
2297
- if (!rule?.imageAllowedOrigins) return true;
2298
- if (value.startsWith("/") && !value.startsWith("//")) return true;
2299
- try {
2300
- const url = new URL(value);
2301
- if (url.protocol !== "https:") return false;
2302
- const origins = [...rule.imageAllowedOrigins];
2303
- if (input.imageBaseUrl) origins.push(new URL(input.imageBaseUrl).origin);
2304
- return origins.includes(url.origin);
2305
- } catch {
2306
- return false;
2307
- }
2308
- }
2309
- function formatMetadataDefaults(input, declaration) {
2310
- const dynamic = declaration[input.kind];
2311
- const rule = dynamic?.defaultRules?.[input.modelKey];
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);
2318
- const bodyImage = input.firstBodyImage && (input.firstBodyImage.startsWith("https://") || /^\/(?!\/)/u.test(input.firstBodyImage)) ? input.firstBodyImage : void 0;
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);
2321
- const ogImage = candidateImage && contentCandidateAllowed(candidateImage, input, rule) ? readMetadataValues({ ogImage: metadataContentImageUrl(candidateImage, rule?.normalizeCloudflareImages) }).ogImage : void 0;
2322
- return {
2323
- ...readMetadataValues(rule?.defaults),
2324
- ...finalTitle ? { title: finalTitle } : {},
2325
- ...description ? { description } : {},
2326
- ...ogImage ? { ogImage } : {}
2327
- };
2328
- }
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
2249
  // 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()
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()
2348
2257
  ]);
2349
- var ContentPolicyWhenSchema = import_zod5.z.object({
2258
+ var contentPolicyEquality = import_zod4.z.object({
2350
2259
  field: ContentPolicyFieldNameSchema,
2351
2260
  equals: ContentPolicyScalarSchema
2352
2261
  }).strict();
2353
- var ContentPolicyInternalPathSchema = import_zod5.z.string().min(1).max(2048).refine((path) => {
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) => {
2354
2273
  if (!path.startsWith("/") || path.startsWith("//") || /[\\?#\u0000-\u0020\u007f]/u.test(path)) return false;
2355
2274
  try {
2356
2275
  return path.split("/").every((part, index) => {
@@ -2364,17 +2283,17 @@ var ContentPolicyInternalPathSchema = import_zod5.z.string().min(1).max(2048).re
2364
2283
  }, "\uCF58\uD150\uCE20 \uACBD\uB85C\uB294 \uC548\uC804\uD55C \uC0AC\uC774\uD2B8 \uB0B4\uBD80 \uACBD\uB85C\uC5EC\uC57C \uD569\uB2C8\uB2E4");
2365
2284
  var field = { field: ContentPolicyFieldNameSchema };
2366
2285
  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()
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()
2374
2293
  ];
2375
- var ContentPolicyScalarCheckSchema = import_zod5.z.discriminatedUnion("op", scalarChecks);
2294
+ var ContentPolicyScalarCheckSchema = import_zod4.z.discriminatedUnion("op", scalarChecks);
2376
2295
  var when = { when: ContentPolicyWhenSchema.optional() };
2377
- var ContentPolicyCheckSchema = import_zod5.z.discriminatedUnion("op", [
2296
+ var ContentPolicyCheckSchema = import_zod4.z.discriminatedUnion("op", [
2378
2297
  scalarChecks[0].extend(when),
2379
2298
  scalarChecks[1].extend(when),
2380
2299
  scalarChecks[2].extend(when),
@@ -2382,26 +2301,26 @@ var ContentPolicyCheckSchema = import_zod5.z.discriminatedUnion("op", [
2382
2301
  scalarChecks[4].extend(when),
2383
2302
  scalarChecks[5].extend(when),
2384
2303
  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({
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({
2387
2306
  ...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()
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()
2392
2311
  }).strict(),
2393
- import_zod5.z.object({ ...when, op: import_zod5.z.literal("bodyText") }).strict()
2312
+ import_zod4.z.object({ ...when, op: import_zod4.z.literal("bodyText") }).strict()
2394
2313
  ]);
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()
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()
2399
2318
  }).strict();
2400
2319
  var ContentPolicySchema = contentPolicy.superRefine((policy, ctx) => {
2401
2320
  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" });
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" });
2403
2322
  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` });
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` });
2405
2324
  }
2406
2325
  });
2407
2326
  function collectContentPolicyFieldRefs(policy) {
@@ -2411,17 +2330,43 @@ function collectContentPolicyFieldRefs(policy) {
2411
2330
  for (const child of children) names.add(child);
2412
2331
  refs.set(name, names);
2413
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
+ };
2414
2338
  for (const check of [...policy.visibility, ...policy.indexing]) {
2415
- if (check.when) add(check.when.field);
2339
+ addWhen(check.when);
2416
2340
  if ("field" in check) add(check.field, check.op === "some" ? check.checks.map((child) => child.field) : []);
2417
2341
  if (check.op === "bodyContent" && check.additionalUrl) {
2418
2342
  add(check.additionalUrl.field);
2419
- if (check.additionalUrl.when) add(check.additionalUrl.when.field);
2343
+ addWhen(check.additionalUrl.when);
2420
2344
  }
2421
2345
  }
2422
2346
  return [...refs].map(([name, children]) => ({ name, ...children.size ? { children: [...children] } : {} }));
2423
2347
  }
2424
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
+
2425
2370
  // src/content-policy.ts
2426
2371
  var text2 = (value) => typeof value === "string" ? value.trim() : "";
2427
2372
  var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -2484,9 +2429,6 @@ function matchesContentScalarCheck(check, fields) {
2484
2429
  return check.paths.includes(text2(value));
2485
2430
  }
2486
2431
  }
2487
- function matchesWhen(when2, fields) {
2488
- return !when2 || fieldValue(fields, when2.field) === when2.equals;
2489
- }
2490
2432
  function bodyFacts(post) {
2491
2433
  const jsonText = contentPolicyBodyText(post.bodyJson);
2492
2434
  const source = resolvePostBodySource(post.bodyHtml);
@@ -2494,7 +2436,7 @@ function bodyFacts(post) {
2494
2436
  return { jsonText, html, text: source.kind === "html" ? html.replace(/<[^>]*>/g, "").replace(/&nbsp;/gi, " ").trim() : jsonText };
2495
2437
  }
2496
2438
  function matchesCheck(check, post, fields, getBody) {
2497
- if (!matchesWhen(check.when, fields)) return true;
2439
+ if (!matchesContentPolicyWhen(check.when, fields)) return true;
2498
2440
  if (check.op === "some") {
2499
2441
  const value = fieldValue(fields, check.field);
2500
2442
  return Array.isArray(value) && value.some((item) => {
@@ -2505,7 +2447,7 @@ function matchesCheck(check, post, fields, getBody) {
2505
2447
  if (check.op === "bodyText") return Boolean(getBody().text);
2506
2448
  if (check.op === "bodyContent") {
2507
2449
  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)));
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)));
2509
2451
  }
2510
2452
  return matchesContentScalarCheck(check, fields);
2511
2453
  }
@@ -2552,21 +2494,151 @@ function evaluateContentPolicy(policy, post, options = {}) {
2552
2494
  };
2553
2495
  }
2554
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 }));
2530
+ }
2531
+
2532
+ // src/metadata-defaults.ts
2533
+ var imageOrigin = import_zod6.z.string().max(2048).refine((value) => {
2534
+ try {
2535
+ const url = new URL(value);
2536
+ return url.protocol === "https:" && url.origin === value;
2537
+ } catch {
2538
+ return false;
2539
+ }
2540
+ }, "\uC774\uBBF8\uC9C0 \uD5C8\uC6A9 \uBAA9\uB85D\uC5D0\uB294 \uACBD\uB85C \uC5C6\uB294 HTTPS origin\uB9CC \uC120\uC5B8\uD569\uB2C8\uB2E4");
2541
+ var MetadataDefaultRulesSchema = import_zod6.z.object({
2542
+ title: MetadataTextRuleSchema.optional(),
2543
+ titleTemplateMaxLength: import_zod6.z.number().int().min(1).max(1e3).optional(),
2544
+ description: MetadataTextRuleSchema.optional(),
2545
+ /** Code-owned defaults precede site defaults but always follow manual overrides. */
2546
+ defaults: MetadataValuesSchema.optional(),
2547
+ robots: MetadataRobotsPolicySchema.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(),
2557
+ fallback: MetadataValuesSchema.optional()
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
+ });
2569
+ function metadataContentImageUrl(value, normalizeCloudflareImages = false) {
2570
+ if (!normalizeCloudflareImages) return value;
2571
+ try {
2572
+ const url = new URL(value);
2573
+ const [account, id, variant] = url.pathname.split("/").filter(Boolean);
2574
+ if (url.protocol === "https:" && url.hostname === "imagedelivery.net" && account && id && variant) {
2575
+ url.pathname = `/${account}/${id}/lg`;
2576
+ return url.href;
2577
+ }
2578
+ } catch {
2579
+ }
2580
+ return value;
2581
+ }
2582
+ function legacyImageUrl(value, rule) {
2583
+ if (!value || rule?.legacyImageSource !== "legacy-thumbnail") return void 0;
2584
+ try {
2585
+ const url = new URL(value);
2586
+ if (!rule.legacyImageAllowedOrigins?.includes(url.origin)) return void 0;
2587
+ return readMetadataValues({ ogImage: url.href }).ogImage;
2588
+ } catch {
2589
+ return void 0;
2590
+ }
2591
+ }
2592
+ function contentCandidateAllowed(value, input, rule) {
2593
+ if (!rule?.imageAllowedOrigins) return true;
2594
+ if (value.startsWith("/") && !value.startsWith("//")) return true;
2595
+ try {
2596
+ const url = new URL(value);
2597
+ if (url.protocol !== "https:") return false;
2598
+ const origins = [...rule.imageAllowedOrigins];
2599
+ if (input.imageBaseUrl) origins.push(new URL(input.imageBaseUrl).origin);
2600
+ return origins.includes(url.origin);
2601
+ } catch {
2602
+ return false;
2603
+ }
2604
+ }
2605
+ function formatMetadataDefaults(input, declaration) {
2606
+ const dynamic = declaration[input.kind];
2607
+ const rule = dynamic?.defaultRules?.[input.modelKey];
2608
+ const name = cleanMetadataText(input.title, rule?.title);
2609
+ const title = formatMetadataText(input.title, rule?.title, input, name);
2610
+ const template2 = dynamic?.titleTemplates?.[input.modelKey];
2611
+ const templatedTitle = title && template2 ? template2.replace("%s", () => title) : title;
2612
+ const finalTitle = templatedTitle && rule?.titleTemplateMaxLength && templatedTitle.length > rule.titleTemplateMaxLength ? title : templatedTitle;
2613
+ const description = formatMetadataText(input.description, rule?.description, input, name);
2614
+ const bodyImage = input.firstBodyImage && (input.firstBodyImage.startsWith("https://") || /^\/(?!\/)/u.test(input.firstBodyImage)) ? input.firstBodyImage : void 0;
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);
2618
+ const ogImage = candidateImage && contentCandidateAllowed(candidateImage, input, rule) ? readMetadataValues({ ogImage: metadataContentImageUrl(candidateImage, rule?.normalizeCloudflareImages) }).ogImage : void 0;
2619
+ return {
2620
+ ...readMetadataValues(rule?.defaults),
2621
+ ...finalTitle ? { title: finalTitle } : {},
2622
+ ...description ? { description } : {},
2623
+ ...ogImage ? { ogImage } : {}
2624
+ };
2625
+ }
2626
+
2555
2627
  // src/content-query.ts
2556
- var import_zod6 = require("zod");
2628
+ var import_zod7 = require("zod");
2557
2629
  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()
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()
2562
2634
  ]);
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(),
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(),
2567
2639
  pinnedField: ContentPolicyFieldNameSchema.optional(),
2568
- pageSize: import_zod6.z.number().int().min(1).max(100),
2569
- requireItems: import_zod6.z.boolean().optional()
2640
+ pageSize: import_zod7.z.number().int().min(1).max(100),
2641
+ requireItems: import_zod7.z.boolean().optional()
2570
2642
  }).strict();
2571
2643
  function collectContentQueryFieldRefs(query) {
2572
2644
  return [.../* @__PURE__ */ new Set([...(query.filters ?? []).map((filter) => filter.field), ...query.pinnedField ? [query.pinnedField] : []])].map((name) => ({ name }));
@@ -2746,6 +2818,7 @@ function normalizeRedirectTarget(raw) {
2746
2818
  cmsParagraphSpacing,
2747
2819
  collectContentPolicyFieldRefs,
2748
2820
  collectContentQueryFieldRefs,
2821
+ collectMetadataDefaultFieldRefs,
2749
2822
  contentPolicyBodyText,
2750
2823
  contentPolicySafeHttpsUrl,
2751
2824
  createFieldBindingResolver,
@@ -2780,6 +2853,7 @@ function normalizeRedirectTarget(raw) {
2780
2853
  mergeMetadataPatch,
2781
2854
  metadataBodyParagraphs,
2782
2855
  metadataContentImageUrl,
2856
+ metadataVideoThumbnail,
2783
2857
  normalizeEmbed,
2784
2858
  normalizeHexColor,
2785
2859
  normalizeImageAlt,