@roottale/cms-core 0.67.1 → 0.68.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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @roottale/cms-core
2
2
 
3
+ ## 0.68.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d7130d8: 기존 고객 사이트의 검색·공유 기본값을 선언형 계약으로 보존합니다. 문자열 커스텀 필드 템플릿, 본문 문단 대체값, Unicode와 단어 경계 말줄임, 고정 이미지 기본값, 모델별 강제 robots와 분류 공개 경로를 지원합니다. 수동 SEO 설정은 우선하며 강제 검색 차단은 유지합니다.
8
+
9
+ 분류 조회 SDK에 색인 전환 상태와 추가 캐시 태그를 연결하고 외부 연동 문서·실행 예제를 함께 제공합니다.
10
+
3
11
  ## 0.67.1
4
12
 
5
13
  ## 0.67.0
package/dist/index.cjs CHANGED
@@ -98,6 +98,7 @@ __export(src_exports, {
98
98
  isValidSitePatternKey: () => isValidSitePatternKey,
99
99
  issuePreviewToken: () => issuePreviewToken,
100
100
  mergeMetadataPatch: () => mergeMetadataPatch,
101
+ metadataBodyParagraphs: () => metadataBodyParagraphs,
101
102
  metadataContentImageUrl: () => metadataContentImageUrl,
102
103
  normalizeEmbed: () => normalizeEmbed,
103
104
  normalizeHexColor: () => normalizeHexColor,
@@ -2102,42 +2103,37 @@ function mergeMetadataPatch(current, input) {
2102
2103
  }
2103
2104
 
2104
2105
  // src/metadata-defaults.ts
2106
+ var import_zod4 = require("zod");
2107
+
2108
+ // src/metadata-text.ts
2105
2109
  var import_zod3 = require("zod");
2106
2110
  var fallbackText = import_zod3.z.string().max(2e3);
2107
- var imageOrigin = import_zod3.z.string().max(2048).refine((value) => {
2108
- try {
2109
- const url = new URL(value);
2110
- return url.protocol === "https:" && url.origin === value;
2111
- } catch {
2112
- return false;
2113
- }
2114
- }, "\uC774\uBBF8\uC9C0 \uD5C8\uC6A9 \uBAA9\uB85D\uC5D0\uB294 \uACBD\uB85C \uC5C6\uB294 HTTPS origin\uB9CC \uC120\uC5B8\uD569\uB2C8\uB2E4");
2111
+ var templateToken = /\{\{(?:title|field:[a-zA-Z][a-zA-Z0-9_-]{0,159})\}\}/gu;
2112
+ var template = fallbackText.refine(
2113
+ (value) => !/[{}]/u.test(value.replace(templateToken, "")),
2114
+ "\uC790\uB3D9 \uD14D\uC2A4\uD2B8\uC5D0\uB294 {{title}} \uB610\uB294 {{field:\uD544\uB4DC\uC774\uB984}} \uD1A0\uD070\uB9CC \uC0AC\uC6A9\uD569\uB2C8\uB2E4"
2115
+ );
2115
2116
  var MetadataTextRuleSchema = import_zod3.z.object({
2116
- source: import_zod3.z.enum(["content", "fallback"]).optional(),
2117
+ source: import_zod3.z.enum(["content", "fallback", "template"]).optional(),
2118
+ template: template.optional(),
2117
2119
  stripLeadingLabel: import_zod3.z.enum(["question", "answer"]).optional(),
2118
2120
  replaceLineBreakTokens: import_zod3.z.boolean().optional(),
2119
2121
  removeEmDashes: import_zod3.z.boolean().optional(),
2120
2122
  normalizeWhitespace: import_zod3.z.boolean().optional(),
2121
2123
  maxLength: import_zod3.z.number().int().min(1).max(2e3).optional(),
2124
+ lengthUnit: import_zod3.z.enum(["code-units", "code-points"]).optional(),
2125
+ wordBoundaryMinLength: import_zod3.z.number().int().min(1).max(2e3).optional(),
2122
2126
  ellipsis: import_zod3.z.string().max(10).optional(),
2127
+ bodyFallback: import_zod3.z.object({
2128
+ preferContaining: import_zod3.z.string().trim().min(1).max(100).optional(),
2129
+ stripLeadingLabel: import_zod3.z.literal("conclusion").optional()
2130
+ }).strict().optional(),
2123
2131
  fallback: fallbackText.optional(),
2124
2132
  fallbackWithParent: fallbackText.optional(),
2125
2133
  fallbackWithoutParent: fallbackText.optional(),
2126
2134
  fallbackBySlug: import_zod3.z.record(import_zod3.z.string().max(200), fallbackText).optional()
2127
- }).strict().refine((rule) => !rule.ellipsis || rule.maxLength !== void 0 && rule.ellipsis.length < rule.maxLength, "\uB9D0\uC904\uC784\uD45C\uC5D0\uB294 \uB354 \uAE34 \uCD5C\uB300 \uAE38\uC774\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4");
2128
- var MetadataDefaultRulesSchema = import_zod3.z.object({
2129
- title: MetadataTextRuleSchema.optional(),
2130
- description: MetadataTextRuleSchema.optional(),
2131
- useFirstBodyImage: import_zod3.z.boolean().optional(),
2132
- useTermImage: import_zod3.z.boolean().optional(),
2133
- imageAltSource: import_zod3.z.enum(["content", "title"]).optional(),
2134
- legacyImageSource: import_zod3.z.literal("legacy-thumbnail").optional(),
2135
- imageAllowedOrigins: import_zod3.z.array(imageOrigin).min(1).max(32).optional(),
2136
- legacyImageAllowedOrigins: import_zod3.z.array(imageOrigin).min(1).max(32).optional(),
2137
- normalizeCloudflareImages: import_zod3.z.boolean().optional(),
2138
- fallback: MetadataValuesSchema.optional()
2139
- }).strict().refine((rule) => !rule.legacyImageSource || Boolean(rule.legacyImageAllowedOrigins?.length), "\uC774\uC804 \uC774\uBBF8\uC9C0 \uC6D0\uBCF8\uC5D0\uB294 \uD5C8\uC6A9 origin \uC120\uC5B8\uC774 \uD544\uC694\uD569\uB2C8\uB2E4");
2140
- function cleanText(value, rule) {
2135
+ }).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
+ function cleanMetadataText(value, rule) {
2141
2137
  let text2 = value.trim();
2142
2138
  if (rule?.stripLeadingLabel === "question") text2 = text2.replace(/^\s*Q[..:]?\s*/iu, "");
2143
2139
  if (rule?.stripLeadingLabel === "answer") text2 = text2.replace(/^\s*A[..:]?\s*/iu, "");
@@ -2146,20 +2142,90 @@ function cleanText(value, rule) {
2146
2142
  if (rule?.normalizeWhitespace) text2 = text2.replace(/\s+/gu, " ");
2147
2143
  return text2.trim();
2148
2144
  }
2149
- function formatText(value, rule, input, name) {
2150
- let text2 = rule?.source === "fallback" ? "" : cleanText(value ?? "", rule);
2145
+ function metadataBodyParagraphs(value) {
2146
+ const paragraphs = [];
2147
+ const textParts = (node) => {
2148
+ if (Array.isArray(node)) return node.flatMap(textParts);
2149
+ if (!node || typeof node !== "object") return [];
2150
+ const item = node;
2151
+ return [...typeof item.text === "string" ? [item.text] : [], ...textParts(item.content)];
2152
+ };
2153
+ const walk = (node) => {
2154
+ if (Array.isArray(node)) {
2155
+ node.forEach(walk);
2156
+ return;
2157
+ }
2158
+ if (!node || typeof node !== "object") return;
2159
+ const item = node;
2160
+ if (item.type === "paragraph") {
2161
+ const text2 = textParts(item.content).join(" ").replace(/\s+/gu, " ").trim();
2162
+ if (text2) paragraphs.push(text2);
2163
+ } else walk(item.content);
2164
+ };
2165
+ walk(value);
2166
+ return paragraphs;
2167
+ }
2168
+ function formatMetadataText(value, rule, input, name) {
2169
+ let text2 = rule?.source === "fallback" ? "" : cleanMetadataText(value ?? "", rule);
2170
+ if (rule?.source === "template") {
2171
+ text2 = cleanMetadataText((rule.template ?? "").replace(templateToken, (token) => {
2172
+ 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;
2175
+ return typeof value2 === "string" ? value2.trim() : "";
2176
+ }), rule);
2177
+ }
2178
+ if (!text2 && rule?.bodyFallback) {
2179
+ const paragraphs = input.bodyParagraphs ?? [];
2180
+ 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);
2184
+ }
2151
2185
  if (!text2) {
2152
- const fallback = input.parentId ? rule?.fallbackWithParent ?? rule?.fallback : (input.slug ? rule?.fallbackBySlug?.[input.slug] : void 0) ?? rule?.fallbackWithoutParent ?? rule?.fallback;
2153
- text2 = cleanText((fallback ?? "").replaceAll("%s", () => name), rule);
2186
+ const slugFallback = input.slug && Object.hasOwn(rule?.fallbackBySlug ?? {}, input.slug) ? rule?.fallbackBySlug?.[input.slug] : void 0;
2187
+ const fallback = input.parentId ? rule?.fallbackWithParent ?? rule?.fallback : slugFallback ?? rule?.fallbackWithoutParent ?? rule?.fallback;
2188
+ text2 = cleanMetadataText((fallback ?? "").replaceAll("%s", () => name), rule);
2154
2189
  }
2155
2190
  if (!text2) return void 0;
2156
- if (rule?.maxLength && text2.length > rule.maxLength) {
2191
+ const units = rule?.lengthUnit === "code-points" ? [...text2] : text2.split("");
2192
+ if (rule?.maxLength && units.length > rule.maxLength) {
2157
2193
  const suffix = rule.ellipsis ?? "";
2158
- const head = text2.slice(0, rule.maxLength - suffix.length);
2194
+ const suffixLength = rule.lengthUnit === "code-points" ? [...suffix].length : suffix.length;
2195
+ let head = units.slice(0, rule.maxLength - suffixLength).join("");
2196
+ const boundary = head.lastIndexOf(" ");
2197
+ if (rule.wordBoundaryMinLength && boundary >= rule.wordBoundaryMinLength) head = head.slice(0, boundary);
2159
2198
  return suffix ? head.trimEnd() + suffix : head;
2160
2199
  }
2161
2200
  return text2;
2162
2201
  }
2202
+
2203
+ // src/metadata-defaults.ts
2204
+ var imageOrigin = import_zod4.z.string().max(2048).refine((value) => {
2205
+ try {
2206
+ const url = new URL(value);
2207
+ return url.protocol === "https:" && url.origin === value;
2208
+ } catch {
2209
+ return false;
2210
+ }
2211
+ }, "\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({
2213
+ title: MetadataTextRuleSchema.optional(),
2214
+ titleTemplateMaxLength: import_zod4.z.number().int().min(1).max(1e3).optional(),
2215
+ description: MetadataTextRuleSchema.optional(),
2216
+ /** Code-owned defaults precede site defaults but always follow manual overrides. */
2217
+ defaults: MetadataValuesSchema.optional(),
2218
+ 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(),
2227
+ 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");
2163
2229
  function metadataContentImageUrl(value, normalizeCloudflareImages = false) {
2164
2230
  if (!normalizeCloudflareImages) return value;
2165
2231
  try {
@@ -2199,15 +2265,19 @@ function contentCandidateAllowed(value, input, rule) {
2199
2265
  function formatMetadataDefaults(input, declaration) {
2200
2266
  const dynamic = declaration[input.kind];
2201
2267
  const rule = dynamic?.defaultRules?.[input.modelKey];
2202
- const name = cleanText(input.title, rule?.title);
2203
- const title = formatText(input.title, rule?.title, input, name);
2204
- const template = dynamic?.titleTemplates?.[input.modelKey];
2205
- const description = formatText(input.description, rule?.description, input, name);
2268
+ const name = cleanMetadataText(input.title, rule?.title);
2269
+ const title = formatMetadataText(input.title, rule?.title, input, name);
2270
+ const template2 = dynamic?.titleTemplates?.[input.modelKey];
2271
+ const templatedTitle = title && template2 ? template2.replace("%s", () => title) : title;
2272
+ const finalTitle = templatedTitle && rule?.titleTemplateMaxLength && templatedTitle.length > rule.titleTemplateMaxLength ? title : templatedTitle;
2273
+ const description = formatMetadataText(input.description, rule?.description, input, name);
2206
2274
  const bodyImage = input.firstBodyImage && (input.firstBodyImage.startsWith("https://") || /^\/(?!\/)/u.test(input.firstBodyImage)) ? input.firstBodyImage : void 0;
2207
- const candidateImage = input.kind === "terms" && rule?.useTermImage === false ? void 0 : input.featuredImage || (rule?.useFirstBodyImage ? bodyImage : void 0) || legacyImageUrl(input.legacyImage, rule);
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);
2208
2277
  const ogImage = candidateImage && contentCandidateAllowed(candidateImage, input, rule) ? readMetadataValues({ ogImage: metadataContentImageUrl(candidateImage, rule?.normalizeCloudflareImages) }).ogImage : void 0;
2209
2278
  return {
2210
- ...title ? { title: template ? template.replace("%s", () => title) : title } : {},
2279
+ ...readMetadataValues(rule?.defaults),
2280
+ ...finalTitle ? { title: finalTitle } : {},
2211
2281
  ...description ? { description } : {},
2212
2282
  ...ogImage ? { ogImage } : {}
2213
2283
  };
@@ -2343,6 +2413,7 @@ function normalizeRedirectTarget(raw) {
2343
2413
  isValidSitePatternKey,
2344
2414
  issuePreviewToken,
2345
2415
  mergeMetadataPatch,
2416
+ metadataBodyParagraphs,
2346
2417
  metadataContentImageUrl,
2347
2418
  normalizeEmbed,
2348
2419
  normalizeHexColor,