@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 +12 -0
- package/dist/index.cjs +217 -143
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8744 -2815
- package/dist/index.d.ts +8744 -2815
- package/dist/index.js +215 -143
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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(/ /gi, " ").trim().length > 0;
|
|
2337
|
-
return visible ? { kind: "html", html: sanitized } : { kind: "json" };
|
|
2338
|
-
}
|
|
2339
|
-
|
|
2340
2249
|
// src/content-policy-schema.ts
|
|
2341
|
-
var
|
|
2342
|
-
var ContentPolicyFieldNameSchema =
|
|
2343
|
-
var ContentPolicyScalarSchema =
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
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
|
|
2258
|
+
var contentPolicyEquality = import_zod4.z.object({
|
|
2350
2259
|
field: ContentPolicyFieldNameSchema,
|
|
2351
2260
|
equals: ContentPolicyScalarSchema
|
|
2352
2261
|
}).strict();
|
|
2353
|
-
var
|
|
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
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
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 =
|
|
2294
|
+
var ContentPolicyScalarCheckSchema = import_zod4.z.discriminatedUnion("op", scalarChecks);
|
|
2376
2295
|
var when = { when: ContentPolicyWhenSchema.optional() };
|
|
2377
|
-
var ContentPolicyCheckSchema =
|
|
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
|
-
|
|
2386
|
-
|
|
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:
|
|
2389
|
-
allowExcerpt:
|
|
2390
|
-
allowFeaturedImage:
|
|
2391
|
-
additionalUrl:
|
|
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
|
-
|
|
2312
|
+
import_zod4.z.object({ ...when, op: import_zod4.z.literal("bodyText") }).strict()
|
|
2394
2313
|
]);
|
|
2395
|
-
var contentPolicy =
|
|
2396
|
-
visibility:
|
|
2397
|
-
indexing:
|
|
2398
|
-
pathSegmentFormat:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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(/ /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(/ /gi, " ").trim() : jsonText };
|
|
2495
2437
|
}
|
|
2496
2438
|
function matchesCheck(check, post, fields, getBody) {
|
|
2497
|
-
if (!
|
|
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 &&
|
|
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
|
|
2628
|
+
var import_zod7 = require("zod");
|
|
2557
2629
|
var field2 = { field: ContentPolicyFieldNameSchema };
|
|
2558
|
-
var ContentQueryFilterSchema =
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
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 =
|
|
2564
|
-
modelKeys:
|
|
2565
|
-
filters:
|
|
2566
|
-
scope:
|
|
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:
|
|
2569
|
-
requireItems:
|
|
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,
|