# @roottale/cms-core

## 0.64.0

## 0.63.0

### Minor Changes

- a858d8e: 목차 배치를 사이트 코드의 `RootTaleBlogPost.tocPosition`으로 관리할 수 있습니다.
  명시한 값은 CMS 설정보다 우선하며, 생략한 기존 연동은 CMS 배치를 유지합니다.
  현재 배치를 prop으로 명시하면 실제 화면을 바꾸지 않고 관리 주체를 코드로 옮깁니다.

  공통 블록 디자인은 `RootTaleBlogPost.footerPatternPresentation`과
  `RootTalePostPattern.presentation`으로 전체 대체할 수 있습니다. `null`은 원격
  디자인 주입을 생략하고 사이트 CSS에 맡기며, 생략한 기존 연동은 원격값을 유지합니다.
  고객 콘텐츠와 제작 디자인을 분리하는 문서·발행/미리보기 예제를 함께 갱신합니다.

  공개 `RootTaleCssVars` 타입의 비공개 UI 패키지 참조도 제거해 독립 소비자의
  strict 타입 검사를 지원합니다.

  섹션 구성·디자인 변경 검사 함수를 공개 CMS core에서 공유해 편집기와 저장 단계가
  같은 고객 편집 경계를 적용합니다.

## 0.62.0

## 0.61.1

### Patch Changes

- HTML 정화 후에도 이미지와 사진 설명의 좌우 자동 여백과 블록 배치를 유지해
  에디터의 이미지 정렬이 미리보기와 HTML 기반 사이트에서 동일하게 표시되도록 합니다.

## 0.61.0

### Minor Changes

- 공통 에디터의 글씨 크기·서식 초기화·줄간격·문단 간격·이미지 너비·정렬·사진 설명을
  저장하고 공개 글에 출력합니다. ROOT-ADMIN에서 선택한 내용을 사이트 공통 블록으로
  저장하고 독립 복사본으로 재사용할 수 있습니다.

## 0.60.0

## 0.59.0

### Minor Changes

- c0b3679: 공통 블록 배치 규칙의 유형 키를 컬렉션(`collectionKey`)에서 콘텐츠 모델(`modelKey`)로 바꾼다
  (ADR-0109 Amendment 1 · ADR-0105). `SitePatternPlacementRule.modelKey`,
  `findSitePatternPlacementRule`/`setSitePatternPlacementRule`/`resolvePatternSlots` 가 `modelKey` 를
  받는다. 저장된 옛 규칙의 `collectionKey` 필드는 `readSitePatternPlacements` 가 `modelKey` 로 읽는다.
  공개 API 응답 모양(`pattern_slots`)은 그대로이며, 계산 근거가 글의 `model_key` 로 바뀐다 —
  컬렉션 설정이 없는 사이트 글·2단계 분류 모델(FAQ)·모델 key 와 컬렉션 key 가 다른 글도 유형별
  규칙을 받는다. cms-mcp `api-reference.md` 의 `pattern_slots` 설명을 맞춘다.

## 0.58.0

### Minor Changes

- dd82c92: 공통 블록 표시 형태(presentation) — 블록의 모양(본문처럼/카드·배경색·링크 버튼·버튼 색)을 어드민이
  정해 공개 API `presentation` 으로 내려준다. 사이트는 색·모양을 CSS 에 고정하지 않는다.
  - `@roottale/cms-core`: `readSitePatternPresentation`·`sitePatternPresentationAttributes`·
    `DEFAULT_SITE_PATTERN_PRESENTATION`.
  - `@roottale/cms-client`: `CmsSitePattern.presentation`(구 서버는 기본값), `sitePatternPresentationAttributes`
    (래퍼 data 속성 + `--rt-pattern-bg`·`--rt-pattern-btn-n` CSS 변수).
  - `@roottale/cms-renderer-next`: `RootTaleBlogPost`·`RootTalePostPattern` 이 data 속성·변수를 얹고,
    `cms-public.css` 가 카드/버튼 모양을 그 값으로만 그린다.

## 0.57.0

### Minor Changes

- e222471: 본문 예약 내부 링크(`[[internal:키|문구]]`)를 공용 렌더러가 해석한다.
  - `@roottale/cms-core`: 토큰 정규식·탐색(`findInternalContentLinkTokens`), 경로→키
    (`internalContentKeyFromPath`), 발행 글 경로 색인(`internalContentPathIndex`, 옛 slug 포함),
    텍스트·정화된 HTML 안의 토큰을 링크로 바꾸는 `renderInternalContentLinksInText/InHtml`.
  - `@roottale/cms-renderer-next`: `RenderTiptap`·`RootTaleBlogPost` 에 `internalLinks`
    (Map·함수) 옵션. `RootTaleBlogPost` 는 기본 `"auto"` — 본문에 토큰이 있을 때만
    `fetchInternalContentPathIndex` 로 발행 글 경로 색인을 만들어 링크로 렌더한다.
    대상이 없는 토큰은 문구만 남기고 `data-rt-internal-link-pending="키"` 를 단다.
    `<code>`·`<pre>`·기존 링크 안은 건드리지 않는다.
  - 문서: FRONT 가 토큰을 직접 해석하지 않아도 되는 경로를 안내한다.

- 92d0ad4: 공통 블록(Site Patterns) — 여러 글의 같은 자리(글 하단)에 붙는 재사용 본문.
  - `@roottale/cms-client`: 글 응답 `patternSlots`(자리 key → 블록 key | null, 구 서버는 빈
    객체), `fetchSitePatterns`(발행 블록 목록, `GET /v1/cms/public/patterns`),
    `selectSitePatternForSlot`, `SITE_PATTERNS_CACHE_TAG`(`SETTINGS_CACHE_TAGS` 에 포함 —
    `theme.updated` 로 함께 갱신), `POST_FOOTER_PATTERN_SLOT`.
  - `@roottale/cms-renderer-next`: `RootTaleBlogPost` 가 `patternSlots.post_footer` 블록을 본문
    바로 아래에 자동으로 그린다(목록 요청 실패는 블록만 생략). 자체 글 화면용
    `RootTalePostPattern` 서버 컴포넌트와 `.rt-cms-post-pattern` 스타일 추가.
  - `@roottale/cms-core`: 자리 선언·배치 규칙 정규화·`resolvePatternSlots` (플랫폼 서버·어드민과
    공유하는 순수 규칙).
  - 문서: `theme-and-settings.md` "공통 블록", `api-reference.md` `GET /patterns`·`pattern_slots`.

## 0.56.0

### Minor Changes

- 9eb39e8: ADR-0105 개정 — 글의 정규 공개 경로 계산기 `resolvePostPublicPath` 를 추가합니다.
  모델 presentation(fixed_page·detail·category_tree) → 호환 컬렉션 → 레거시 `/blog` 순서
  하나로 계산하며, 플랫폼의 웹훅·사이트맵·단축링크·내부 링크 키·`posts.public_path`
  물질화가 모두 이 함수를 씁니다.

## 0.55.0

## 0.54.0

### Minor Changes

- befa5bb: 콘텐츠 유형별 카테고리 주소와 카테고리 SEO 공개 필드에 더해, 1~3단계 부모·자식
  분류로 목록·허브·상세 주소를 구성하는 범용 콘텐츠 모델 계약을 추가합니다.

## 0.53.1

### Patch Changes

- 1efecbb: 페이지·글·정보의 콘텐츠 모델 공개 계약과 `modelKey` 조회를 추가합니다. 개발자가 선언한 슬롯에서만 배너·팝업을 조회하고 접근성 있게 렌더하는 API, client, renderer, 예제 문서를 함께 제공합니다. 기존 collections와 collectionKey 계약은 유지합니다.
- cf84636: ChatGPT 등에서 만든 완전한 HTML 문서의 태그 구조, class·id·`data-*`, inline style·`<style>` CSS와 반응형 레이아웃을 `importedHtml` 블록에 보존합니다. 본문의 `h1`은 글 제목과 충돌하지 않도록 `h2`로 바꾸되 원래 `h1` CSS 선택자는 변환된 제목에 이어서 적용합니다. 안전한 `!important`도 유지합니다. CSS는 블록별 고유 범위로 격리하며 스크립트·이벤트 속성·외부 CSS 로드·화면을 덮는 CSS만 제거합니다.

  `html`·`body` 속성, SVG, `picture`·`srcset`, keyframes와 미러링한 CSS 이미지·woff/woff2 폰트도 보존합니다. 가져온 블록별로 문서 루트, animation과 font 이름을 격리해 다른 본문이나 사이트 UI와 충돌하지 않게 합니다.

## 0.53.0

### Minor Changes

- a133f27: 블로그 상세 글의 작성·출력 기준을 하나로 맞췄습니다.
  - 이미지 대체 텍스트를 한 줄, 최대 160자로 정규화하는 공용 계약을 추가했습니다.
  - 상세 글 위에 카테고리를 연결하고, 발행일과 실제로 다른 수정일을 구분해 표시합니다.
  - 본문의 공식 출처 블록을 제목과 목록이 있는 시맨틱 구간으로 렌더합니다.
  - 블로그 전용 SEO 점검에서 목차, 외부 링크가 있는 공식 출처, 내용이 채워진 FAQ를 확인합니다.
  - Next.js 상세 라우트가 글을 먼저 조회한 뒤 `notFound()`를 호출해야 실제 404가 된다는 통합 예제를 보강했습니다.

### Patch Changes

- f743a06: HTML sanitizer가 안전한 HTTP·HTTPS 이미지 주소를 보존하도록 URL 허용 규칙을 바로잡습니다.

## 0.46.0

### Minor Changes

- 69f7d52: 콘텐츠 유형(Collection)에 **주소 없는 유형**(`basePath: ""`)을 지원합니다(ADR-0060). 강사·리뷰처럼 페이지 안에서 불러와 쓰는 콘텐츠는 이제 라우트 없이 운영할 수 있습니다 — `resolvePostUrl`/`resolvePostPath`는 이런 유형의 글에 항상 `null`을 반환하고, `validateCollections`는 basePath 형식·중복 검사를 건너뜁니다(여러 개 공존 허용). 외부 사이트는 posts API의 `collectionKey` 필터로 내용만 가져다 페이지 안에서 렌더하면 됩니다(`RootTaleBlogList`를 임베디드로 쓸 때는 자체 `postHref` 제공 권장). 소속 판정(`resolvePostCollection`)은 영향 없음 — key만으로 소속이 성립합니다. 기존 유형(basePath 있는 유형)은 동작 변화가 없습니다.

## 0.45.0

### Patch Changes

- 23aa6ae: 카테고리 소속을 콘텐츠 유형 설정과 분리하고, 공개 collections 응답의 categories를 분류 원장에서 조립합니다. 사이트 내부 경로 검증을 강화하고 taxonomy.updated 집계 웹훅 계약과 연동 예제를 추가합니다.

## 0.43.0

### Minor Changes

- 4ec1a83: 콘텐츠 유형(Collections, ADR-0060)에 `detail?: boolean` 옵션 추가 — 기본 `true`(하위호환), `false`면 "목록 전용" 유형(예: 후기 — 목록에서 전문 노출, 글마다 별도 상세 URL 없음).
  - `@roottale/cms-core`: `RouteCollection.detail` 필드. `resolvePostUrl`/`resolvePostPath` 가 `collection.detail === false` 인 글에 `null` 반환(소속 판정 `resolvePostCollection` 은 영향 없음 — 소속과 상세 URL 존재 여부는 별개).
  - `@roottale/cms-client`: `fetchCollections()`(`CmsRouteCollection.detail`)가 공개 API의 새 `detail` 필드를 매핑(미전달 시 `true`).
  - `@roottale/cms-renderer-next`: `createSitemapIndex`/`createSitemap`(기존 `resolvePostUrl` 경유라 자동 반영)·`createFeedRoute`(RSS 아이템에서 `detail:false` 스트림 글 제외)·`createLlmsTxtRoute`가 `detail:false` 스트림 글의 상세 URL 을 발행하지 않는다. `RootTaleBlogList`/관련 글 카드·아카이브 연관 위젯의 postHref 가 `detail:false` 스트림에서는 존재하지 않는 상세 경로 대신 그 유형의 목록 페이지(`basePath`)로 링크한다.

  어드민 "콘텐츠 유형" 편집기에 "글별 상세 페이지" 토글 추가(기본 켬). 코드 변경 없이 어드민에서 끄면 즉시 반영.

## 0.42.0

### Minor Changes

- d7dfe0a: `postsNotInAnyCollection`(cms-core) — `postsInAnyCollection` 의 대칭으로, 어느 스트림에도 안 속한("미소속") 글만 남긴다.

  `RootTaleBlogList` 에 `excludeCollections?: boolean` prop 추가 — `collections` 와 함께 주면 스트림 소속 글을 전부 제외해 `/blog` 목록의 스트림 글 누출을 막는다(`collection` 과 배타, `collections` 없이 단독 지정은 throw). 아카이브 스트림(증상/질환/치료)만 선언하고 나머지 글을 `/blog` 로 보여주는 사이트용 — `blog` 자체가 선언 섹션인 사이트는 반대로 `collection="blog"` 가 정경로.

- 8e8bc1c: `sanitizeHtml` 를 정규식 deny-list 에서 `sanitize-html` 기반 parser allow-list 로 교체했다. Tiptap/renderer 출력 태그와 RootTale `rt-*` class, 안전 URL scheme, raster `data:image/*` 정책을 보존하면서 미지 태그·이벤트 속성·비허용 style·SVG data URL 을 제거한다.

## 0.41.0

### Minor Changes

- 3768f72: FAQ 블록 — `<details>` 아코디언 + FAQPage 구조화데이터(JSON-LD).

  어드민 에디터의 FAQ 블록(`roottale/faq` → faqItem/faqQuestion/faqAnswer)이
  발행되면 `RootTaleBlogPost`가 자동으로:
  - `<details class="rt-cms-faq-item"><summary class="rt-cms-faq-q">…</summary>
    <div class="rt-cms-faq-a">…</div></details>` 아코디언(0 JS)으로 렌더하고,
  - 글 단위 `FAQPage` JSON-LD(`<script type="application/ld+json">`)를 1개 삽입한다.

  추가 연동/키 불필요 — 기존 단일 API 키 위에서 동작하며, 업그레이드 시 기존 글에도
  FAQ 블록이 있으면 즉시 반영된다. `@roottale/cms-core` 에 추출 유틸
  `extractFaqEntries(doc)` / `faqPageJsonLd(entries)` 추가(렌더러 공유). 스타일은
  `cms-public.css` 의 `.rt-cms-faq*` 클래스로 테마 토큰을 따른다.

## 0.39.0

### Patch Changes

- chore: linked cms-\* 그룹 버전 정렬 — cms-media(0.27.0)·cms-mcp(0.37.0) 이
  부분 릴리스로 그룹(0.38.0)에서 드리프트. 6개 linked 패키지를 한 릴리스에 묶어
  공통 버전으로 재정렬한다.

## 0.38.0

### Patch Changes

- 0d6ec9f: 보안(XSS) 수정 — 외부 고객 사이트 렌더 경로의 stored XSS 2건 차단.
  - `cms-core` `sanitizeHtml`: 이벤트 핸들러/위험 URL 정규식 앵커를 `\s+` →
    `[\s/]+` 로 강화. HTML5 파서가 `/` 도 속성 구분자로 허용하므로 과거
    `<img/src=x/onerror=...>` 같은 슬래시-구분 토큰이 deny-list 를 우회해
    무인터랙션 stored XSS 가 가능했다. 회귀 테스트 추가.
  - `cms-renderer-astro` `renderSeoHead` / `seo-core` `serializeJsonLd`(신규
    export): JSON-LD 를 `<script type="application/ld+json">` 에 넣을 때
    `JSON.stringify` 가 `<`/`>`/`&` 를 escape 하지 않아 사용자 제어 title 등에
    `</script>` 가 들어가면 head 에 임의 마크업이 주입됐다. `\uXXXX` escape 로 차단.
  - 소비자(web-front / starter)에 strict CSP 배선(인라인 스크립트 실행 차단,
    cms-core `PHASE_1_CSP` 정합) — deny-list 살균의 2차 방어선.

## 0.34.0

### Minor Changes

- 4b21cb9: ADR-0060 Amendment 1 M4 — 카테고리-소유권 파생 제거 (섹션은 collectionKey 전용)

  `resolvePostCollection` 이 이제 글의 명시 `collectionKey` 만 본다. 카테고리 slug
  파생·catch-all·first-wins 로직 제거. `RouteCollection.categories` 는 라우팅
  소유권이 아니라 **주제(category) allowlist**(아카이브 범위)로만 의미가 바뀜.
  `validateCollections` 의 "catch-all 도달 불가" 검사도 제거.

  **동작 변경(주의):** collectionKey 없는 글은 컬렉션 모드 사이트에서 어느 스트림에도
  안 속해 sitemap/상세 라우트에서 제외된다. 마이그레이션(M1 백필 + M2 에디터)로 모든
  글에 collection_key 가 채워진 뒤 적용할 것. 단일 `/blog` 레거시(컬렉션 미선언) 사이트는
  무영향.

## 0.33.0

### Minor Changes

- 2e6cbd0: ADR-0060 Amendment 1 (Option A) — 글의 섹션을 명시 필드로

  `resolvePostCollection` 이 글의 명시 `collectionKey` 를 카테고리 slug 파생보다
  우선 사용한다. `RoutablePost`·`CmsPostContent` 에 `collectionKey` 추가, 공개 API
  post 페이로드에 `collection_key` 포함. 미설정/구 서버면 기존 카테고리 파생으로
  fallback(완전 하위호환). sitemap/feed/가드/아카이브가 명시 섹션을 따른다.

## 0.30.0

### Minor Changes

- 8de6895: collections: catch-all 스트림 (빈 `categories`) 지원 (ADR-0060)

  `RouteCollection.categories` 가 빈 배열이면 **catch-all** — 앞선 스트림이 잡지 않은 글을
  전부 흡수한다. "공지=[notice] 먼저, 블로그=[] catch-all 뒤" 처럼 _블로그 카테고리가
  개방형(admin이 계속 추가)인 사이트_ 가 카테고리를 일일이 열거하지 않고 공지+블로그를
  분리할 수 있다(kjmtax 등 가장 흔한 패턴). `validateCollections` 가 catch-all 뒤의 도달
  불가 스트림을 경고한다. 선언 순서 = first-wins 라 catch-all 은 맨 뒤에 둔다.

## 0.29.0

### Minor Changes

- e147b33: 다중 스트림 콘텐츠 라우팅 (ADR-0060 — 공지 게시판 + 블로그 분리)

  같은 글 풀(post)을 둘 이상의 스트림으로 분리(공지=`/notice`, 블로그=`/blog` 등)하는
  `collections` 프리미티브 추가. 기존 팩토리의 "모든 글 = 단일 `/blog`" 가정을 깨고,
  글을 카테고리로 소속 스트림 basePath 에 라우팅한다 — sitemap/feed/llms.txt/revalidate
  가 스트림별로 파생되고, 어느 스트림에도 안 속한 글은 sitemap 에서 제외된다.
  - `@roottale/cms-core`: `RouteCollection`·`resolvePostCollection`·`resolvePostUrl`·
    `resolvePostPath`·`postsForCollection`·`validateCollections` (cms-client/프레임워크
    비의존, next/astro 공유 — ADR-0029 parity).
  - `@roottale/cms-renderer-next`: `RootTaleSiteConfig`·`RevalidateRouteConfig` 에 옵셔널
    `collections` 추가. 지정 시 collection 모드, 비우거나 미전달이면 레거시 단일 `blogBasePath`
    동작 유지(**하위호환** — 기존 사이트 무영향).
  - `@roottale/cms-renderer-astro`: `renderBlogList` 에 `collections` 옵션 + 동일 resolver 재export.

  선언 순서 = 라우팅 우선순위(first-wins). 한 category 는 한 스트림에만(`validateCollections`
  로 검증).

## 0.18.0

### Minor Changes

- 39fbe3d: ACF 커스텀 필드 (ADR-0041) M2·M3-D
  - cms-core: Block Bindings 헬퍼 — `applyBlockBindings` + `createFieldBindingResolver`
    (`roottale/field` + `acf/field` alias) + `chainResolvers`.
  - cms-renderer-next/-astro: `renderBoundBlocks(blocks, { fields })` — 바인딩 적용 후 렌더.
  - cms-client: `CmsPostContent.fields` — 서버가 포맷한 커스텀 필드 값
    (image/file/gallery 는 media variant URL 객체). 구 서버는 null.

- f313e76: 필드 바인딩 라이브 렌더 (ADR-0041 D3·D4, 바인딩 PR-2)
  - cms-core: `boundFieldValue` / `bindingTextValue` / `bindingSrcValue` — Tiptap
    라이브 경로의 바인딩 해석 헬퍼 (값은 항상 text, raw HTML 진입 금지).
  - cms-renderer-next: `RenderTiptap` export + `fields` prop — 블록 바인딩을
    공개 API `post.fields` 값으로 치환(값 없으면 본문 fallback). `RootTaleBlogPost`
    가 자동 전달 — 고객 사이트 코드 변경 없이 동작.
  - cms-renderer-astro: `renderTiptapDoc(doc, { fields })` 동등 표면 + blog 경로 자동 전달.

## 0.7.0

### Minor Changes

- e2067cf: feat: 영상 임베드 블록 + 공개 글 작성자 byline + Astro 목차(TOC)
  - **cms-core**: `normalizeEmbed`/`isAllowedEmbedSrc` 추가 — YouTube·Vimeo
    provider allowlist 기반 임베드 URL 정규화 + 렌더 단계 host·path 재검증(SSRF 방어).
  - **cms-client**: `CmsPostContent.authorName` 노출(공개 표시명, email 비노출).
  - **cms-renderer-next**: `core/embed` 노드 렌더(allowlist iframe, 미허용 시 원본
    링크 fallback) + 글 상세 작성자 byline.
  - **cms-renderer-astro**: 위와 동등한 embed 렌더 + 작성자 byline + Next 와 동등한
    TOC 지원(`extractToc`/`attachHeadingIds`, `showTableOfContents` 옵션).

## 0.2.1

### Patch Changes

- dist build 도입 — `.ts` 소스 직접 publish 폐기.

  기존 (0.1.0/0.2.0): `exports` 가 `./src/server.ts` 가리킴 → 외부 Next.js
  Turbopack/Webpack 이 npm 모듈에서 TS 자동 컴파일 안 해 build 실패 (customer
  site 가 `transpilePackages` 명시해야 했음).

  수정: `tsup` 으로 `dist/*.js` + `dist/*.d.ts` (ESM) 출력. `exports` 가 dist
  가리킴. customer site 측 `transpilePackages` 불필요.
  - `cms-client@0.1.1` — ESM `dist/server.js` + types
  - `cms-core@0.2.1` — ESM `dist/index.js` + types
  - `cms-renderer-next@0.2.1` — ESM `dist/{server,index}.js` + types + `dist/cms-public.css`
    - `server.tsx` 의 `import "./styles/cms-public.css"` 제거 — customer 가
      `@roottale/cms-renderer-next/styles` 로 명시 import (README 정합).
  - `cms-renderer-astro@0.2.1` — ESM `dist/index.js` + types

  후속 (customer site PR):
  - roottale-web / kjmtax / theoneulsan 의 `next.config` 의 `transpilePackages`
    에서 `@roottale/cms-*` 제거. `pnpm update @roottale/cms-client @roottale/cms-renderer-next` 로 patch 적용.

## 0.2.0

### Minor Changes

- d1b5d35: cms-\* public renderer 패키지 npm public publish — Phase 1 dogfood.

  ADR-0029 §0 amend (publish-only dormant) 는 design system 5 패키지 (tokens, ui-css, ui-react, ui-astro, ui-admin) 에 한정. cms-\* 는 별도 정책 — 실행 로직 + 5-20 외부 customer site 직접 의존 + schema 호환 + 보안 경계. Codex consult verdict (session `019e6703…`) 정합.

  변경:
  - 4 패키지 `private: true` 해제 + `publishConfig.access: "public"`
  - `@roottale/cms-renderer-next` 에서 `@roottale/tokens/tokens.css` import 제거 — 모든 `--rt-*` 변수에 static fallback 으로 self-contained. tokens dormant 와 무관하게 동작.
  - `RootTaleLeadForm` RSC 추가 — 외부 사이트 진단 폼 (`vertical`/`redirectUrl` props, medical 국외이전 동의 자동).
  - README + repository / homepage / keywords 메타 정비
  - `peerDependencies.react: ^19` 명시 (cms-renderer-next)
  - `cms-renderer-astro` 도 동등 surface 유지를 위해 동시 publish

  후속:
  - ADR 신규 — cms-\* publish 정책 (별 PR)
  - Astro 측 LeadForm 컴포넌트 (현재 `@roottale/ui-astro` 위치, `cms-renderer-astro` 로 이동 검토)
