@roottale/cms-core 0.53.1 → 0.56.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,23 @@
1
1
  # @roottale/cms-core
2
2
 
3
+ ## 0.56.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9eb39e8: ADR-0105 개정 — 글의 정규 공개 경로 계산기 `resolvePostPublicPath` 를 추가합니다.
8
+ 모델 presentation(fixed_page·detail·category_tree) → 호환 컬렉션 → 레거시 `/blog` 순서
9
+ 하나로 계산하며, 플랫폼의 웹훅·사이트맵·단축링크·내부 링크 키·`posts.public_path`
10
+ 물질화가 모두 이 함수를 씁니다.
11
+
12
+ ## 0.55.0
13
+
14
+ ## 0.54.0
15
+
16
+ ### Minor Changes
17
+
18
+ - befa5bb: 콘텐츠 유형별 카테고리 주소와 카테고리 SEO 공개 필드에 더해, 1~3단계 부모·자식
19
+ 분류로 목록·허브·상세 주소를 구성하는 범용 콘텐츠 모델 계약을 추가합니다.
20
+
3
21
  ## 0.53.1
4
22
 
5
23
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -34,6 +34,7 @@ __export(src_exports, {
34
34
  FIELD_BINDING_SOURCE: () => FIELD_BINDING_SOURCE,
35
35
  FIELD_BINDING_SOURCE_ALIAS: () => FIELD_BINDING_SOURCE_ALIAS,
36
36
  IMAGE_ALT_MAX_LENGTH: () => IMAGE_ALT_MAX_LENGTH,
37
+ LEGACY_BLOG_DETAIL_PATH: () => LEGACY_BLOG_DETAIL_PATH,
37
38
  PHASE_1_CORE_BLOCKS: () => PHASE_1_CORE_BLOCKS,
38
39
  PHASE_1_CSP: () => PHASE_1_CSP,
39
40
  applyBlockBindings: () => applyBlockBindings,
@@ -60,8 +61,14 @@ __export(src_exports, {
60
61
  postsForCollection: () => postsForCollection,
61
62
  postsInAnyCollection: () => postsInAnyCollection,
62
63
  postsNotInAnyCollection: () => postsNotInAnyCollection,
64
+ resolveCategoryPath: () => resolveCategoryPath,
65
+ resolveCategoryTreeChain: () => resolveCategoryTreeChain,
66
+ resolveCategoryTreeEntryPath: () => resolveCategoryTreeEntryPath,
67
+ resolveCategoryTreeHubPaths: () => resolveCategoryTreeHubPaths,
68
+ resolvePostCategorySlug: () => resolvePostCategorySlug,
63
69
  resolvePostCollection: () => resolvePostCollection,
64
70
  resolvePostPath: () => resolvePostPath,
71
+ resolvePostPublicPath: () => resolvePostPublicPath,
65
72
  resolvePostUrl: () => resolvePostUrl,
66
73
  sanitizeHtml: () => sanitizeHtml,
67
74
  sanitizeImportedCss: () => sanitizeImportedCss,
@@ -202,18 +209,38 @@ function resolvePostCollection(post, collections) {
202
209
  return collections.find((c) => c.key === post.collectionKey) ?? null;
203
210
  }
204
211
  function resolvePostUrl(siteUrl, post, collections) {
205
- const collection = resolvePostCollection(post, collections);
206
- if (!collection) return null;
207
- if (collection.detail === false) return null;
208
- if (!collection.basePath) return null;
209
- return `${trimTrailingSlash(siteUrl)}${trimTrailingSlash(collection.basePath)}/${encodeURIComponent(post.slug)}`;
212
+ const path = resolvePostPath(post, collections);
213
+ return path ? `${trimTrailingSlash(siteUrl)}${path}` : null;
214
+ }
215
+ function resolvePostCategorySlug(post, collection) {
216
+ const allowed = new Set(collection.categories);
217
+ const slugs = [
218
+ ...new Set(
219
+ post.terms.filter(
220
+ (term) => term.taxonomy === "category" && allowed.has(term.slug)
221
+ ).map((term) => term.slug)
222
+ )
223
+ ];
224
+ return slugs.length === 1 ? slugs[0] : null;
225
+ }
226
+ function resolveCategoryPath(collection, categorySlug) {
227
+ if (!collection.archives || !collection.basePath || !categorySlug) return null;
228
+ const basePath = trimTrailingSlash(collection.basePath);
229
+ const encodedSlug = encodeURIComponent(categorySlug);
230
+ return collection.categoryPath === "direct" ? `${basePath}/${encodedSlug}` : `${basePath}/categories/${encodedSlug}`;
210
231
  }
211
232
  function resolvePostPath(post, collections) {
212
233
  const collection = resolvePostCollection(post, collections);
213
234
  if (!collection) return null;
214
235
  if (collection.detail === false) return null;
215
236
  if (!collection.basePath) return null;
216
- return `${trimTrailingSlash(collection.basePath)}/${encodeURIComponent(post.slug)}`;
237
+ const basePath = trimTrailingSlash(collection.basePath);
238
+ const encodedPostSlug = encodeURIComponent(post.slug);
239
+ if (collection.detailPath !== "category") {
240
+ return `${basePath}/${encodedPostSlug}`;
241
+ }
242
+ const categorySlug = resolvePostCategorySlug(post, collection);
243
+ return categorySlug ? `${basePath}/${encodeURIComponent(categorySlug)}/${encodedPostSlug}` : null;
217
244
  }
218
245
  function postsForCollection(posts, collections, key) {
219
246
  return posts.filter((p) => resolvePostCollection(p, collections)?.key === key);
@@ -253,10 +280,108 @@ function validateCollections(collections) {
253
280
  }
254
281
  categoryOwner.set(slug, c.key);
255
282
  }
283
+ if (c.detailPath === "category") {
284
+ if (!c.basePath) {
285
+ problems.push(
286
+ `category \uC0C1\uC138 \uACBD\uB85C\uB294 basePath \uAC00 \uD544\uC694\uD568: "${c.key}"`
287
+ );
288
+ }
289
+ if (c.detail === false) {
290
+ problems.push(
291
+ `category \uC0C1\uC138 \uACBD\uB85C\uB294 \uAE00\uBCC4 \uC0C1\uC138 \uD398\uC774\uC9C0\uAC00 \uD544\uC694\uD568: "${c.key}"`
292
+ );
293
+ }
294
+ if (!c.archives) {
295
+ problems.push(
296
+ `category \uC0C1\uC138 \uACBD\uB85C\uB294 \uCE74\uD14C\uACE0\uB9AC \uC544\uCE74\uC774\uBE0C\uAC00 \uD544\uC694\uD568: "${c.key}"`
297
+ );
298
+ }
299
+ if (c.categoryCardinality !== "exactly-one") {
300
+ problems.push(
301
+ `category \uC0C1\uC138 \uACBD\uB85C\uB294 categoryCardinality "exactly-one" \uC774 \uD544\uC694\uD568: "${c.key}"`
302
+ );
303
+ }
304
+ }
256
305
  }
257
306
  return problems;
258
307
  }
259
308
 
309
+ // src/category-tree.ts
310
+ function normalizeBasePath(basePath) {
311
+ return basePath.replace(/\/+$/, "");
312
+ }
313
+ function resolveCategoryTreeChain(presentation, leafId, terms) {
314
+ if (!Number.isInteger(presentation.categoryDepth) || presentation.categoryDepth < 1) {
315
+ return null;
316
+ }
317
+ const termsById = new Map(terms.map((term) => [term.id, term]));
318
+ if (termsById.size !== terms.length) return null;
319
+ if (terms.some((term) => term.parentId === leafId)) return null;
320
+ const reversed = [];
321
+ const visited = /* @__PURE__ */ new Set();
322
+ let currentId = leafId;
323
+ while (currentId) {
324
+ if (visited.has(currentId)) return null;
325
+ visited.add(currentId);
326
+ const current = termsById.get(currentId);
327
+ if (!current || !current.slug) return null;
328
+ reversed.push(current);
329
+ currentId = current.parentId ?? null;
330
+ }
331
+ if (reversed.length !== presentation.categoryDepth) return null;
332
+ return reversed.reverse();
333
+ }
334
+ function resolveCategoryTreeHubPaths(presentation, termId, terms) {
335
+ const chain = resolveCategoryTreeChain(presentation, termId, terms);
336
+ const basePath = normalizeBasePath(presentation.basePath);
337
+ if (!chain || !basePath) return null;
338
+ const paths = [basePath];
339
+ const segments = [];
340
+ for (const term of chain) {
341
+ segments.push(encodeURIComponent(term.slug));
342
+ paths.push(`${basePath}/${segments.join("/")}`);
343
+ }
344
+ return paths;
345
+ }
346
+ function resolveCategoryTreeEntryPath(presentation, entry, terms) {
347
+ if (!entry.slug) return null;
348
+ const knownIds = new Set(terms.map((term) => term.id));
349
+ const categoryIds = [
350
+ ...new Set(
351
+ entry.terms.filter((term) => term.taxonomy === "category" && knownIds.has(term.id)).map((term) => term.id)
352
+ )
353
+ ];
354
+ if (categoryIds.length !== 1) return null;
355
+ const hubs = resolveCategoryTreeHubPaths(
356
+ presentation,
357
+ categoryIds[0],
358
+ terms
359
+ );
360
+ const leafPath = hubs?.at(-1);
361
+ return leafPath ? `${leafPath}/${encodeURIComponent(entry.slug)}` : null;
362
+ }
363
+
364
+ // src/public-path.ts
365
+ var LEGACY_BLOG_DETAIL_PATH = "/blog";
366
+ function resolvePostPublicPath(post, context) {
367
+ const presentation = context.presentation;
368
+ if (presentation && presentation.kind !== "data_only") {
369
+ if (presentation.kind === "fixed_page") return presentation.path;
370
+ if (presentation.kind === "detail") {
371
+ if (!presentation.detailPath.includes(":slug")) return null;
372
+ return presentation.detailPath.replace(":slug", encodeURIComponent(post.slug));
373
+ }
374
+ return resolveCategoryTreeEntryPath(presentation, post, context.categories);
375
+ }
376
+ const collectionKey = post.collectionKey ?? (presentation ? context.compatibilityCollectionKey ?? post.modelKey ?? null : null);
377
+ const routable = { slug: post.slug, terms: post.terms, collectionKey };
378
+ if (!resolvePostCollection(routable, context.collections)) {
379
+ if (presentation) return null;
380
+ return `${LEGACY_BLOG_DETAIL_PATH}/${post.slug}`;
381
+ }
382
+ return resolvePostPath(routable, context.collections);
383
+ }
384
+
260
385
  // src/delimiter.ts
261
386
  function serializeBlock(block) {
262
387
  const exportable = sanitizeAttributesForExport(block);
@@ -1229,6 +1354,7 @@ function injectStableIdsTree(blocks) {
1229
1354
  FIELD_BINDING_SOURCE,
1230
1355
  FIELD_BINDING_SOURCE_ALIAS,
1231
1356
  IMAGE_ALT_MAX_LENGTH,
1357
+ LEGACY_BLOG_DETAIL_PATH,
1232
1358
  PHASE_1_CORE_BLOCKS,
1233
1359
  PHASE_1_CSP,
1234
1360
  applyBlockBindings,
@@ -1255,8 +1381,14 @@ function injectStableIdsTree(blocks) {
1255
1381
  postsForCollection,
1256
1382
  postsInAnyCollection,
1257
1383
  postsNotInAnyCollection,
1384
+ resolveCategoryPath,
1385
+ resolveCategoryTreeChain,
1386
+ resolveCategoryTreeEntryPath,
1387
+ resolveCategoryTreeHubPaths,
1388
+ resolvePostCategorySlug,
1258
1389
  resolvePostCollection,
1259
1390
  resolvePostPath,
1391
+ resolvePostPublicPath,
1260
1392
  resolvePostUrl,
1261
1393
  sanitizeHtml,
1262
1394
  sanitizeImportedCss,