@roottale/cms-renderer-next 0.47.0 → 0.51.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/dist/blog-filter-BKCGM0qd.d.ts +29 -0
  3. package/dist/{chunk-6MFHA555.js → chunk-27RT6B3O.js} +1 -1
  4. package/dist/chunk-27RT6B3O.js.map +1 -0
  5. package/dist/chunk-2QU4JU46.js +3927 -0
  6. package/dist/chunk-2QU4JU46.js.map +1 -0
  7. package/dist/chunk-4RQ3SEDL.js +106 -0
  8. package/dist/chunk-4RQ3SEDL.js.map +1 -0
  9. package/dist/{chunk-OMXEU4LS.js → chunk-QYNEOFL7.js} +140 -136
  10. package/dist/chunk-QYNEOFL7.js.map +1 -0
  11. package/dist/{chunk-FUA5H6JE.js → chunk-U2IM6OCQ.js} +46 -2
  12. package/dist/chunk-U2IM6OCQ.js.map +1 -0
  13. package/dist/cms-public.css +137 -18
  14. package/dist/concerns-client.d.ts +1 -1
  15. package/dist/expertise-client.d.ts +1 -1
  16. package/dist/expertise-client.js +1 -1
  17. package/dist/hero-carousel-client.js +1 -1
  18. package/dist/routes.d.ts +377 -21
  19. package/dist/routes.js +273 -106
  20. package/dist/routes.js.map +1 -1
  21. package/dist/seo-asset-hosts.d.ts +36 -0
  22. package/dist/seo-asset-hosts.js +391 -0
  23. package/dist/seo-asset-hosts.js.map +1 -0
  24. package/dist/server.d.ts +34 -216
  25. package/dist/server.js +7 -7
  26. package/dist/{site-config-Dicv57oY.d.ts → site-config-DNpvlqA5.d.ts} +201 -201
  27. package/dist/spaces-client.d.ts +1 -1
  28. package/dist/team-expanding-stage-client.d.ts +1 -1
  29. package/dist/team-expanding-stage-client.js +1 -1
  30. package/dist/templates.d.ts +64 -25
  31. package/dist/templates.js +14 -7
  32. package/dist/templates.js.map +1 -1
  33. package/package.json +7 -2
  34. package/dist/chunk-6MFHA555.js.map +0 -1
  35. package/dist/chunk-FUA5H6JE.js.map +0 -1
  36. package/dist/chunk-FZK3EXYZ.js +0 -103
  37. package/dist/chunk-FZK3EXYZ.js.map +0 -1
  38. package/dist/chunk-OMXEU4LS.js.map +0 -1
  39. package/dist/chunk-ZHXYCABN.js +0 -3584
  40. package/dist/chunk-ZHXYCABN.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,85 @@
1
1
  # @roottale/cms-renderer-next
2
2
 
3
+ ## 0.51.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [ca4b1c7]
8
+ - @roottale/cms-client@0.51.0
9
+
10
+ ## 0.50.0
11
+
12
+ ### Minor Changes
13
+
14
+ - ebd8fa1: 아카이브(모음 페이지) 색인 정책을 단일 SSoT로 모은다 — 새 `decideArchiveIndex`·`buildArchiveMetadata`(`@roottale/cms-renderer-next/routes`)가 사이트맵 포함 여부와 `robots` 를 **같은 판정**에서 파생하므로 "noindex 인데 사이트맵에 제출된 URL" 이 구조적으로 생기지 않는다. 정책: 0건 아카이브는 404(검색 결과 제외), 2페이지 이상·태그·날짜·검색은 noindex, 분류·작가는 어드민에서 승격한 것만 색인.
15
+
16
+ **승격 상태는 boolean 이 아니라 3-state 다** — `resolveCategoryPromotion(slugs, slug)`·`resolveAuthorPromotion(enabled)` 가 `promoted`/`not-promoted`/`unknown` 을 만들고, 사이트맵과 아카이브 페이지가 **같은 헬퍼로 같은 입력**을 조립한다. `unknown`(구버전 API 응답·설정 조회 실패·`skipRemoteSettings`)은 정책 미적용 = 기존 동작 보존으로 fail-soft 한다 — 설정을 못 읽었다는 이유로 이미 색인된 URL 을 지우지 않기 위해서다. 판정 함수만 공유하고 입력 조립을 각자 하면 한쪽은 "모름 = 통과", 다른 쪽은 "모름 = 미승격" 으로 결론이 갈린다.
17
+
18
+ 글 수를 세는 모집단 한도는 새 상수 `ARCHIVE_POST_COUNT_LIMIT` 로 공개한다 — 사이트맵과 아카이브 페이지가 이 값을 함께 써야 "사이트맵엔 URL 이 있는데 페이지는 404" 가 생기지 않는다. 이 한도를 넘는 사이트에서는 한도 밖에만 글이 있는 분류가 양쪽 모두에서 빠진다(같은 기준으로 틀리므로 색인 어긋남은 없음).
19
+
20
+ **모집단(어떤 글에서 분류를 세는가)도 한 함수로 모은다** — 새 `fetchBlogArchivePosts`·`fetchBlogArchiveCategories`(`@roottale/cms-renderer-next/routes`). `/blog` 계열 아카이브는 콘텐츠 유형(collection) 스트림에 속하지 않은 "미소속" 글만 다루는데(`blog-list-collection-scope.md` §2①) 사이트맵은 발행 글 **전체**에서 분류를 세고 있었다 — 스트림 글에만 달린 분류가 사이트맵엔 URL 이 있고 페이지는 404 였다. 이제 양쪽이 같은 함수를 호출하고, "미소속" 판정은 서버(`exclude_collections`)가 하므로 소비처가 collections 목록을 각자 조립하다 갈릴 여지가 없다. 스트림 아카이브(`{basePath}/categories/{slug}`)의 짝은 `collectCollectionCategories` 다.
21
+
22
+ **동작 변경(주의)**: 콘텐츠 유형을 선언한 사이트에서 `/blog/categories/*` 사이트맵 항목이 미소속 글 기준으로 좁아진다(스트림 글에만 달린 분류는 빠진다 — 그 URL 은 원래 404 였다). 유형을 선언하지 않은 사이트는 diff 0.
23
+
24
+ 입력 경계도 강제한다 — `postCount` 가 0 이상 유한 정수가 아니거나 `page` 가 1 이상 유한 정수가 아니면 화면은 살리고 색인만 보류(noindex + 사이트맵 제외)한다.
25
+
26
+ **동작 변경(주의)**: `createSitemapIndex` 의 `categories` 섹션이 이제 공개 설정 `sitemap.promotedCategorySlugs` 에 있는 분류의 아카이브만 내보낸다. 분류 목록 페이지(`/blog/categories`)와 섹션 id 는 그대로 유지된다(`/sitemap/categories.xml` 404 방지 — 이 예외는 내비게이션 허브라는 근거로 정책 대상에서 뺀 것이며 정본 스펙 개정이 필요하다). 작가 사이트맵은 기존 토글을 승격 신호로 쓰고 발행 글 0건 작가만 추가로 제외하며, **설정 조회가 실패해도 `authors` 섹션 id 를 목록에서 빼지 않는다** — 빼면 이미 배포된 사이트맵 인덱스가 가리키던 `/sitemap/authors.xml` 이 404 가 되기 때문이다(섹션은 남기고 내용만 비운다).
27
+
28
+ - ef72e0a: 분류별 글 수를 서버가 센다 — 오래된 글에만 달린 분류가 검색에서 사라지지 않게
29
+
30
+ 분류 모음 페이지의 "글 0건이면 404" 판정과 사이트맵 포함 여부는 분류별 글 수에서
31
+ 나옵니다. 지금까지는 최근 200건을 받아 세었기 때문에, 글이 많은 사이트에서 그
32
+ 범위 밖에만 글이 있는 분류가 **모음 페이지(404)와 사이트맵 양쪽에서** 함께
33
+ 빠졌습니다.
34
+ - 공개 API에 `GET /v1/cms/public/categories` 추가 — 모든 분류와 발행 글 수를
35
+ 서버가 SQL로 집계해 내려줍니다. 집계 범위는 글 목록 API와 같은 어휘
36
+ (`exclude_collections` / `collection_key` / `locale`)로 지정합니다.
37
+ - `@roottale/cms-client` 에 `fetchCategoryCounts()` 추가.
38
+ - `fetchBlogArchiveCategories`(cms-renderer-next)가 이 API를 먼저 씁니다. 못 받으면
39
+ (구버전 API·일시 장애) 예전 방식으로 자동 복귀하므로 기존 동작이 깨지지 않고,
40
+ 사이트맵과 모음 페이지는 어느 경로에서든 같은 값을 봅니다.
41
+
42
+ 사이트 코드 변경은 필요 없습니다 — 패키지를 올리면 적용됩니다. 다만 사이트가
43
+ 바라보는 API가 아직 이 엔드포인트를 배포하지 않았다면 예전 동작이 그대로
44
+ 유지됩니다.
45
+
46
+ - 3865354: 본문 `section` 노드 렌더링을 코어에서 분리해 templates 계층이 주입하도록 바꿨습니다. 공개 API는 그대로이며(`RenderTiptap`·templates 서브패스 동작 동일), templates 서브패스에 `registerSectionTemplates`·`renderSectionNode` export가 추가됐습니다. ADR-0040 P2의 "코어 ◄ templates" 단방향 계약이 이제 코드로 강제됩니다.
47
+ - 1787ac0: 렌더 산출물의 에셋 호스트가 운영 도메인·승인 CDN인지 검사하는 `scanAssetHosts`를 새 서브패스 `@roottale/cms-renderer-next/seo-asset-hosts`로 공개한다.
48
+
49
+ HTML은 quote-aware 토크나이저로 읽어 `img`/`source`(`src`·`srcset`), `link`(에셋을 실제로 로드하는 `rel`만 — canonical·alternate 는 제외), `script`, `video`/`audio`/`track`, `base`, `iframe`, `embed`/`object`, `input[type=image]`, `imagesrcset` 을 스캔하고, 인라인·외부 CSS 의 `url()`/`@import`/`image-set()` 을 함께 본다. 따옴표 없는 속성값과 속성값 안의 `>` 도 정상 처리하며, 속성값의 HTML 엔티티를 디코드한 뒤 파싱해 `user@host` 형태로 진짜 목적지를 가리는 우회를 막는다. 절대 URL 인데 파싱에 실패하면 호스트를 추측하지 않고 위반으로 보고한다(fail-closed).
50
+
51
+ 스테이징·프리뷰 호스트(`*.vercel.app`·`*.trycloudflare.com`·`staging.*` 등)는 허용목록에 있어도 항상 위반으로 잡는다.
52
+
53
+ ### Patch Changes
54
+
55
+ - Updated dependencies [ebd8fa1]
56
+ - Updated dependencies [ef72e0a]
57
+ - @roottale/cms-client@0.50.0
58
+
59
+ ## 0.48.0
60
+
61
+ ### Minor Changes
62
+
63
+ - a4c7070: avcd 구조 — `RootTaleBlogPost`에 opt-in `breadcrumb` prop(시각 브레드크럼 +
64
+ `BreadcrumbList` JSON-LD) 추가, `author_slug` 있으면 작성자 이름을
65
+ `/blog/author/{slug}`로 자동 링크(`authorHref`로 override 가능). 헤더 발행일
66
+ 마크업이 `<span>`에서 `dateTime` 속성을 가진 `<time>`으로 변경(class·
67
+ data-rt-cms-meta 속성은 유지). `buildPostMetadata`에 Twitter Card 자동 emit,
68
+ RSS alternate(`feedPath`, 기본 `/feed.xml`), og article 확장(`modified`·
69
+ `section`·`tags`·`authors`) 추가.
70
+
71
+ ⚠️ 카테고리 칩·`RootTaleBlogCategories`·브레드크럼 카테고리 링크가 모두
72
+ `/blog/categories/{slug}`를 기본 링크한다 — 단일 블로그 사이트도 이 라우트를
73
+ 구현해야 404가 나지 않는다(참조 구현: `@roottale/cms-mcp` 예시
74
+ `app/blog/categories/[slug]/page.tsx`).
75
+
76
+ - ab5241a: 읽기 표면 조판 개선 — 네이버 프리미엄 콘텐츠·avcd 기준: 문단 간격 확대(1.35em), h2 섹션 대여백, h3/h4 크기 위계 수정, 리스트 간격·액센트 마커, 인용구 라운드 카드화, 이미지 여백·캡션 스타일, 표 라운드 외곽+수평 구분선, hr 여백 확대
77
+
78
+ ### Patch Changes
79
+
80
+ - Updated dependencies [a4c7070]
81
+ - @roottale/cms-client@0.48.0
82
+
3
83
  ## 0.47.0
4
84
 
5
85
  ### Minor Changes
@@ -0,0 +1,29 @@
1
+ import { CmsPostContent } from '@roottale/cms-client/server';
2
+
3
+ /**
4
+ * `RootTaleBlogList` 카테고리 필터 — 순수 헬퍼(서버 필터, client JS 불요).
5
+ *
6
+ * SSR-only 정책 유지: 필터는 `?category=<slug>` URL 파라미터로 동작하고(칩 =
7
+ * 서버 렌더 `<a>` 링크), consumer 가 searchParams 의 category 를 `activeCategory`
8
+ * 로 주입한다. hydration 없이 SEO 친화 + 공유 가능한 URL.
9
+ */
10
+
11
+ interface BlogCategory {
12
+ name: string;
13
+ slug: string;
14
+ }
15
+ interface BlogCategoryWithCount extends BlogCategory {
16
+ count: number;
17
+ }
18
+ /**
19
+ * 글 목록에 등장하는 카테고리를 slug 기준 unique 로 수집. 서버가 보내는 정렬
20
+ * (category → name asc)을 보존하기 위해 처음 등장 순서를 유지한다.
21
+ */
22
+ declare function collectCategories(posts: CmsPostContent[]): BlogCategory[];
23
+ declare function collectCategoriesWithCounts(posts: CmsPostContent[]): BlogCategoryWithCount[];
24
+ /** activeSlug 가 비면 전체 반환. 아니면 그 카테고리를 가진 글만. */
25
+ declare function filterPostsByCategory(posts: CmsPostContent[], activeSlug: string | null | undefined): CmsPostContent[];
26
+ /** 기본 카테고리 링크: 전체 → `/blog`, 특정 → `/blog/categories/<slug>`. */
27
+ declare function defaultCategoryHref(slug: string | null): string;
28
+
29
+ export { type BlogCategoryWithCount as B, type BlogCategory as a, collectCategoriesWithCounts as b, collectCategories as c, defaultCategoryHref as d, filterPostsByCategory as f };
@@ -90,4 +90,4 @@ function ExpertiseTabStage({
90
90
  export {
91
91
  ExpertiseTabStage
92
92
  };
93
- //# sourceMappingURL=chunk-6MFHA555.js.map
93
+ //# sourceMappingURL=chunk-27RT6B3O.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/templates/expertise-client.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState } from \"react\";\n\nimport type { ExpertiseSection } from \"./site-config.js\";\nimport { useRovingTablist } from \"./use-roving-tablist.js\";\n\ntype ExpertiseItem = ExpertiseSection[\"props\"][\"items\"][number];\n\n/**\n * `expertise` `tab-stage` variant — 좌측 탭 목록(항상 세로 1열, templates.css\n * `.rt-tpl-expertise-tabs`) + 우측 상세 패널. 키보드 계약은 WAI-ARIA APG\n * tablist 패턴(roving tabindex, 단일 tab stop) — `use-roving-tablist.ts` 공용\n * hook 사용(D4, 적대 리뷰 R1 실측 지적 대응).\n */\nexport function ExpertiseTabStage({\n items,\n}: {\n readonly items: readonly ExpertiseItem[];\n}) {\n const [selectedIndex, setSelectedIndex] = useState(0);\n const selected = items[selectedIndex] ?? items[0];\n\n const { getTabProps, onKeyDown } = useRovingTablist<HTMLButtonElement>({\n count: items.length,\n selectedIndex,\n onSelect: setSelectedIndex,\n // 레이아웃은 항상 세로 1열(templates.css)이지만 좌우 화살표도 관대하게\n // 허용 — 반응형 변형이나 향후 가로 배치 재사용에도 동일 훅이 그대로 맞는다.\n orientation: \"both\",\n });\n\n if (!selected) return null;\n\n return (\n <div className=\"rt-tpl-expertise-stage\">\n {/* eslint-disable-next-line jsx-a11y/interactive-supports-focus -- WAI-ARIA 탭 패턴(roving tabindex). 초점은 개별 탭이 갖고 컨테이너는 키 이벤트만 위임받는다. */}\n <div\n className=\"rt-tpl-expertise-tabs\"\n role=\"tablist\"\n aria-label=\"전문 분야 선택\"\n aria-orientation=\"vertical\"\n onKeyDown={onKeyDown}\n >\n {items.map((item, index) => {\n const active = index === selectedIndex;\n const tabProps = getTabProps(index);\n return (\n <button\n key={item.id}\n type=\"button\"\n role=\"tab\"\n className=\"rt-tpl-expertise-tab\"\n aria-selected={active}\n aria-controls={`rt-expertise-panel-${item.id}`}\n id={`rt-expertise-tab-${item.id}`}\n data-active={active ? \"true\" : \"false\"}\n tabIndex={tabProps.tabIndex}\n ref={tabProps.ref}\n onClick={() => setSelectedIndex(index)}\n >\n <span className=\"rt-tpl-expertise-tab-label\">{item.label}</span>\n <span className=\"rt-tpl-expertise-tab-title\">{item.title}</span>\n </button>\n );\n })}\n </div>\n <article\n className=\"rt-tpl-expertise-panel\"\n role=\"tabpanel\"\n id={`rt-expertise-panel-${selected.id}`}\n aria-labelledby={`rt-expertise-tab-${selected.id}`}\n tabIndex={0}\n >\n {selected.imageSrc ? (\n <img\n className=\"rt-tpl-expertise-panel-img\"\n src={selected.imageSrc}\n alt={selected.imageAlt ?? \"\"}\n loading=\"lazy\"\n />\n ) : null}\n <div className=\"rt-tpl-expertise-panel-body\">\n {selected.kicker ? (\n <p className=\"rt-tpl-expertise-kicker\">{selected.kicker}</p>\n ) : null}\n <h3 className=\"rt-tpl-expertise-title\">{selected.title}</h3>\n <p className=\"rt-tpl-expertise-body\">{selected.body}</p>\n {selected.tags ? (\n <ul className=\"rt-tpl-expertise-tags\" aria-label={`${selected.title} 태그`}>\n {selected.tags.map((tag) => (\n <li key={tag}>{tag}</li>\n ))}\n </ul>\n ) : null}\n {selected.href ? (\n <a className=\"rt-tpl-expertise-cta\" href={selected.href}>\n {selected.ctaLabel ?? \"자세히 보기\"}\n </a>\n ) : null}\n </div>\n </article>\n </div>\n );\n}\n"],"mappings":";;;;;AAEA,SAAS,gBAAgB;AA8Cb,SAaE,KAbF;AAjCL,SAAS,kBAAkB;AAAA,EAChC;AACF,GAEG;AACD,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAS,CAAC;AACpD,QAAM,WAAW,MAAM,aAAa,KAAK,MAAM,CAAC;AAEhD,QAAM,EAAE,aAAa,UAAU,IAAI,iBAAoC;AAAA,IACrE,OAAO,MAAM;AAAA,IACb;AAAA,IACA,UAAU;AAAA;AAAA;AAAA,IAGV,aAAa;AAAA,EACf,CAAC;AAED,MAAI,CAAC,SAAU,QAAO;AAEtB,SACE,qBAAC,SAAI,WAAU,0BAEb;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV,MAAK;AAAA,QACL,cAAW;AAAA,QACX,oBAAiB;AAAA,QACjB;AAAA,QAEC,gBAAM,IAAI,CAAC,MAAM,UAAU;AAC1B,gBAAM,SAAS,UAAU;AACzB,gBAAM,WAAW,YAAY,KAAK;AAClC,iBACE;AAAA,YAAC;AAAA;AAAA,cAEC,MAAK;AAAA,cACL,MAAK;AAAA,cACL,WAAU;AAAA,cACV,iBAAe;AAAA,cACf,iBAAe,sBAAsB,KAAK,EAAE;AAAA,cAC5C,IAAI,oBAAoB,KAAK,EAAE;AAAA,cAC/B,eAAa,SAAS,SAAS;AAAA,cAC/B,UAAU,SAAS;AAAA,cACnB,KAAK,SAAS;AAAA,cACd,SAAS,MAAM,iBAAiB,KAAK;AAAA,cAErC;AAAA,oCAAC,UAAK,WAAU,8BAA8B,eAAK,OAAM;AAAA,gBACzD,oBAAC,UAAK,WAAU,8BAA8B,eAAK,OAAM;AAAA;AAAA;AAAA,YAbpD,KAAK;AAAA,UAcZ;AAAA,QAEJ,CAAC;AAAA;AAAA,IACH;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV,MAAK;AAAA,QACL,IAAI,sBAAsB,SAAS,EAAE;AAAA,QACrC,mBAAiB,oBAAoB,SAAS,EAAE;AAAA,QAChD,UAAU;AAAA,QAET;AAAA,mBAAS,WACR;AAAA,YAAC;AAAA;AAAA,cACC,WAAU;AAAA,cACV,KAAK,SAAS;AAAA,cACd,KAAK,SAAS,YAAY;AAAA,cAC1B,SAAQ;AAAA;AAAA,UACV,IACE;AAAA,UACJ,qBAAC,SAAI,WAAU,+BACZ;AAAA,qBAAS,SACR,oBAAC,OAAE,WAAU,2BAA2B,mBAAS,QAAO,IACtD;AAAA,YACJ,oBAAC,QAAG,WAAU,0BAA0B,mBAAS,OAAM;AAAA,YACvD,oBAAC,OAAE,WAAU,yBAAyB,mBAAS,MAAK;AAAA,YACnD,SAAS,OACR,oBAAC,QAAG,WAAU,yBAAwB,cAAY,GAAG,SAAS,KAAK,iBAChE,mBAAS,KAAK,IAAI,CAAC,QAClB,oBAAC,QAAc,iBAAN,GAAU,CACpB,GACH,IACE;AAAA,YACH,SAAS,OACR,oBAAC,OAAE,WAAU,wBAAuB,MAAM,SAAS,MAChD,mBAAS,YAAY,mCACxB,IACE;AAAA,aACN;AAAA;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;","names":[]}