@roottale/cms-renderer-next 0.70.1 → 0.72.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,48 @@
1
1
  # @roottale/cms-renderer-next
2
2
 
3
+ ## 0.72.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 4341571: 공개 콘텐츠에 섹션 표시 상태·대표 분류·CTA와 콘텐츠 버전·페이지 계약 key를 전달합니다. 미리보기는 지정한 작업본 버전의 전체 내용을 사용하고, 교체된 작업본은 `preview_superseded` 오류와 새 미리보기 안내로 처리합니다. SDK·렌더러·외부 연동 문서와 Next.js 예제를 함께 갱신합니다.
8
+
9
+ 실제화면 편집 계약에 등록된 템플릿은 허용된 최상위 필드의 저장 구조를 보존합니다. `visualEditing` 선언 사이트의 일반 필드 포맷에서도 반복 항목의 UUID `_id`를 유지해 순서 변경 후 편집 주소가 달라지지 않습니다. 미선언 사이트에는 새 응답 필드·반복행 ID·계약 기반 경로 계산을 적용하지 않습니다. `cms-fields`의 직접 소비자는 `ResolveContext.preserveRowIds`로 명시적으로 활성화할 수 있습니다.
10
+
11
+ - c60835a: 미리보기 메시지 조립·수신 필터·계약 호환성·관리자 origin 목록을 React 없는
12
+ `cms-core/visual-edit`에 추가하고 renderer에서도 공개합니다. 기존 wire 프로토콜과
13
+ 브리지 동작을 유지하면서 관리자와 고객 사이트가 같은 규칙을 사용합니다.
14
+ - 4341571: 실제화면 콘텐츠 편집의 안정 필드 주소·draft 렌더 영수증·iframe 메시지 검증 계약과
15
+ `@roottale/cms-renderer-next/visual-edit` bridge를 추가합니다. 브라우저 전용 계약은
16
+ `@roottale/cms-core/visual-edit`로 서버용 토큰 코드와 분리합니다. 기존 사이트 템플릿을
17
+ 그대로 렌더하고 저장된 draft의 PC·모바일 결과를 확인할 수 있습니다.
18
+
19
+ `createRevalidateRoute`에 선택적 `contentEditing`과 `CONTENT_EDITING_CACHE_TAG`를
20
+ 추가합니다. 활성화한 사이트는 글 발행·수정·삭제와 분류 변경 시 공통 콘텐츠 태그와
21
+ 루트 layout, 기존 경로를 함께 갱신하며 일부 실패도 나머지 갱신 후 HTTP 500으로
22
+ 알립니다. 기본 동작은 유지하며 연동 문서·Next.js 예제를 제공합니다.
23
+
24
+ ### Patch Changes
25
+
26
+ - 5caea6c: 가져온 HTML의 줄·빈 문단에 CMS 기본 문단 여백이 중복으로 붙는 문제를 수정한다. 원문에 지정한 CSS·인라인 간격을 유지한다. 편집기·미리보기·공개 본문의 공통 문단 간격은 16px, 큰 소제목 위 간격은 48px, 목록 항목 간격은 8px로 조정한다.
27
+ - 395a97f: CMS 편집기와 공개 본문의 다단·영상·표 틀을 공용 스타일로 통합하고 Tiptap의 긴 단어 줄바꿈을 공개 본문과 맞춘다. 고객 사이트의 독립 본문 서식과 전역 줄바꿈을 CMS 경계에서 제외하도록 안내한다.
28
+ - Updated dependencies [280e850]
29
+ - Updated dependencies [fa50476]
30
+ - Updated dependencies [4341571]
31
+ - Updated dependencies [9f0d086]
32
+ - Updated dependencies [c60835a]
33
+ - Updated dependencies [e97a94f]
34
+ - Updated dependencies [4341571]
35
+ - Updated dependencies [fa50476]
36
+ - @roottale/cms-client@0.72.0
37
+ - @roottale/cms-core@0.72.0
38
+
39
+ ## 0.71.0
40
+
41
+ ### Patch Changes
42
+
43
+ - @roottale/cms-client@0.71.0
44
+ - @roottale/cms-core@0.71.0
45
+
3
46
  ## 0.70.1
4
47
 
5
48
  ### Patch Changes
package/README.md CHANGED
@@ -41,6 +41,22 @@ import "@roottale/cms-renderer-next/styles";
41
41
 
42
42
  The CSS is scoped under `[data-roottale-cms]` and uses `:where()` so customer styles win without `!important`. Every `--rt-*` variable has a static fallback.
43
43
 
44
+ Body typography is shared by the CMS editor, preview, and public renderer through
45
+ `rt-cms-body` or `data-roottale-cms="body"`: line height 1.7, paragraph gap 16px,
46
+ section heading top gap 48px, and list item gap 8px. Sites that require editor parity
47
+ must use this scope without overriding its typography and spacing.
48
+
49
+ Imported HTML paragraphs do not receive the ordinary editor paragraph gap. Existing
50
+ blank paragraphs still occupy a line, and authored inline or scoped CSS margins take
51
+ precedence. This applies to the CMS editor, its preview, and the public renderer.
52
+
53
+ To check paragraph spacing in a real browser, run
54
+ `node packages/cms-renderer-next/scripts/spacing-fixture.mjs` from the repository root,
55
+ then execute `packages/cms-renderer-next/scripts/check-imported-spacing.aside.js`
56
+ through the local Aside REPL. The fixture listens only on loopback and has no CMS
57
+ connection. It checks desktop/mobile editor, preview, and public scopes, authored
58
+ spacing, blank lines, headings, and list items.
59
+
44
60
  ## Revalidation webhook
45
61
 
46
62
  For near-real-time blog updates, expose `POST /api/revalidate` on the customer
@@ -300,3 +316,7 @@ The form submits via HTML POST (no custom JS). In a Next.js site, point `action`
300
316
  ## License
301
317
 
302
318
  Proprietary (UNLICENSED). Issued under the RootTale customer contract. Contact `contact@roottale.com` for usage outside of an active subscription.
319
+
320
+ ### 편집기와 공개 본문의 표시 일치
321
+
322
+ CMS 편집기·미리보기와 같은 본문을 표시하려면 `rt-cms-body`의 글자·색·강조·목록·표·FAQ 서식을 사이트에서 덮어쓰지 않는다. 호스트의 `:lang(ko)`·`p/li`·제목 줄바꿈 규칙도 `.rt-cms-body`와 그 자손을 제외한다. 작성자의 inline 서식·가져온 원문 CSS는 계속 우선한다. 다단(`rt-columns`)·영상(`rt-embed`)·표 스크롤(`rt-table-wrap`)은 공용 CSS가 소유한다. Tiptap 본문과 가져온 HTML 내부 편집 영역도 같은 긴 단어 줄바꿈을 쓰며, 모션 축소에서는 FAQ 화살표 전환을 끈다. 편집기 도구·선택 상태는 관리자 전용 UI이며 공개 FAQ는 미리보기와 같은 disclosure다. 같은 문서와 같은 **본문 내부 폭**에서 실제 줄바꿈을 대조한다.
@@ -0,0 +1,146 @@
1
+ "use client";
2
+
3
+ // src/visual-edit-bridge.tsx
4
+ import { useEffect, useRef, useState } from "react";
5
+ import { createPortal } from "react-dom";
6
+ import {
7
+ createVisualEditMessage,
8
+ isCurrentVisualEditMessage,
9
+ isVisualEditOrigin,
10
+ parseVisualEditAddress,
11
+ parseVisualEditMessage,
12
+ visualEditorOrigins,
13
+ visualEditAddressKey,
14
+ VISUAL_EDIT_ATTRIBUTE
15
+ } from "@roottale/cms-core/visual-edit";
16
+ import { jsx } from "react/jsx-runtime";
17
+ var DEFAULT_ORIGINS = visualEditorOrigins([]);
18
+ var SCROLL_DEBOUNCE_MS = 80;
19
+ var SESSION_UUID_PATTERN = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i;
20
+ function readConnection(origins) {
21
+ if (typeof window === "undefined" || window.parent === window) return null;
22
+ const params = new URLSearchParams(window.location.hash.slice(1));
23
+ const session = params.get("rt-visual-edit");
24
+ const origin = params.get("origin");
25
+ if (!session || !SESSION_UUID_PATTERN.test(session) || !origin || !isVisualEditOrigin(origin) || !origins.includes(origin)) return null;
26
+ return { session, origin };
27
+ }
28
+ function readAddress(element) {
29
+ try {
30
+ return parseVisualEditAddress(JSON.parse(element.getAttribute(VISUAL_EDIT_ATTRIBUTE) ?? "null"));
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+ function RootTaleVisualEditBridge({ receipt, allowedOrigins = DEFAULT_ORIGINS, allowedPaths = [] }) {
36
+ const [connection] = useState(() => readConnection(allowedOrigins));
37
+ const [rectangle, setRectangle] = useState(null);
38
+ const selected = useRef(null);
39
+ const receiptKey = JSON.stringify(receipt);
40
+ const pathsKey = JSON.stringify(allowedPaths);
41
+ useEffect(() => {
42
+ if (!connection) return;
43
+ const rendered = JSON.parse(receiptKey);
44
+ const paths = JSON.parse(pathsKey);
45
+ let outgoing = 0;
46
+ let incoming = 0;
47
+ let mode = "select";
48
+ let scrollTimer;
49
+ const target = {
50
+ entityId: rendered.entityId,
51
+ draftId: rendered.draftId,
52
+ draftVersion: rendered.draftVersion,
53
+ contractVersion: rendered.contractVersion,
54
+ templateVersion: rendered.templateVersion
55
+ };
56
+ const reply = (payload) => window.parent.postMessage(
57
+ createVisualEditMessage({ session: connection.session, sequence: ++outgoing, target }, payload),
58
+ connection.origin
59
+ );
60
+ const ready = () => {
61
+ reply({ type: "ready" });
62
+ reply({ type: "rendered", receipt: rendered });
63
+ };
64
+ const highlight = () => setRectangle(selected.current?.isConnected ? selected.current.getBoundingClientRect() : null);
65
+ const position = () => ({ left: Math.max(0, window.scrollX), top: Math.max(0, window.scrollY) });
66
+ function receive(event) {
67
+ if (event.source !== window.parent || event.origin !== connection.origin) return;
68
+ const message = parseVisualEditMessage(event.data);
69
+ if (!isCurrentVisualEditMessage(message, { session: connection.session, sequence: incoming, target })) return;
70
+ incoming = message.sequence;
71
+ if (message.type === "connect") ready();
72
+ if (message.type === "mode") mode = message.mode;
73
+ if (message.type === "refresh") window.location.reload();
74
+ if (message.type === "focus") {
75
+ if (message.address) {
76
+ const key = visualEditAddressKey(message.address);
77
+ selected.current = [...document.querySelectorAll(`[${VISUAL_EDIT_ATTRIBUTE}]`)].find((element) => {
78
+ const address = readAddress(element);
79
+ return address && visualEditAddressKey(address) === key;
80
+ }) ?? null;
81
+ }
82
+ if (message.position) window.scrollTo(message.position.left, message.position.top);
83
+ else selected.current?.scrollIntoView?.({ block: "nearest", behavior: "instant" });
84
+ highlight();
85
+ }
86
+ }
87
+ function click(event) {
88
+ const element = event.target instanceof Element ? event.target : null;
89
+ const editable = element?.closest(`[${VISUAL_EDIT_ATTRIBUTE}]`);
90
+ const link = element?.closest("a[href]");
91
+ event.preventDefault();
92
+ event.stopImmediatePropagation();
93
+ if (mode === "navigate" && link) {
94
+ const url = new URL(link.href, window.location.href);
95
+ if (url.origin === window.location.origin && !url.search && paths.includes(url.pathname)) reply({ type: "navigate", pathname: url.pathname });
96
+ return;
97
+ }
98
+ const address = editable ? readAddress(editable) : null;
99
+ if (!address) return;
100
+ selected.current = editable ?? null;
101
+ highlight();
102
+ reply({ type: "select", address, position: position() });
103
+ }
104
+ const block = (event) => {
105
+ event.preventDefault();
106
+ event.stopImmediatePropagation();
107
+ };
108
+ const scroll = () => {
109
+ highlight();
110
+ clearTimeout(scrollTimer);
111
+ scrollTimer = setTimeout(() => reply({ type: "viewport", position: position() }), SCROLL_DEBOUNCE_MS);
112
+ };
113
+ window.addEventListener("message", receive);
114
+ document.addEventListener("click", click, true);
115
+ document.addEventListener("auxclick", block, true);
116
+ document.addEventListener("submit", block, true);
117
+ window.addEventListener("scroll", scroll, true);
118
+ window.addEventListener("resize", highlight);
119
+ ready();
120
+ return () => {
121
+ clearTimeout(scrollTimer);
122
+ window.removeEventListener("message", receive);
123
+ document.removeEventListener("click", click, true);
124
+ document.removeEventListener("auxclick", block, true);
125
+ document.removeEventListener("submit", block, true);
126
+ window.removeEventListener("scroll", scroll, true);
127
+ window.removeEventListener("resize", highlight);
128
+ };
129
+ }, [connection, receiptKey, pathsKey]);
130
+ return connection && rectangle ? createPortal(/* @__PURE__ */ jsx("div", { "aria-hidden": "true", "data-rt-edit-outline": "", style: {
131
+ position: "fixed",
132
+ zIndex: 2147483647,
133
+ pointerEvents: "none",
134
+ left: rectangle.left,
135
+ top: rectangle.top,
136
+ width: rectangle.width,
137
+ height: rectangle.height,
138
+ outline: "2px solid var(--rt-editor-selection, #2563eb)",
139
+ outlineOffset: 3
140
+ } }), document.body) : null;
141
+ }
142
+
143
+ export {
144
+ RootTaleVisualEditBridge
145
+ };
146
+ //# sourceMappingURL=chunk-26TNINA6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/visual-edit-bridge.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n createVisualEditMessage, isCurrentVisualEditMessage, isVisualEditOrigin,\n parseVisualEditAddress, parseVisualEditMessage, visualEditorOrigins,\n visualEditAddressKey, VISUAL_EDIT_ATTRIBUTE,\n type VisualEditAddress, type VisualEditMode, type VisualEditPayload, type VisualRenderReceipt,\n} from \"@roottale/cms-core/visual-edit\";\n\nconst DEFAULT_ORIGINS = visualEditorOrigins([]);\nconst SCROLL_DEBOUNCE_MS = 80;\nconst SESSION_UUID_PATTERN = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i;\n\nexport interface RootTaleVisualEditBridgeProps {\n /** 인증된 서버가 실제로 렌더한 불변 snapshot의 영수증. 공개 라우트에 설치하지 않는다. */\n receipt: VisualRenderReceipt;\n allowedOrigins?: readonly string[];\n /** 사이트 계약에 선언한 내부 경로. 브라우저 이동 대신 관리자에 탐색을 요청한다. */\n allowedPaths?: readonly string[];\n}\n\nfunction readConnection(origins: readonly string[]) {\n if (typeof window === \"undefined\" || window.parent === window) return null;\n const params = new URLSearchParams(window.location.hash.slice(1));\n const session = params.get(\"rt-visual-edit\");\n const origin = params.get(\"origin\");\n if (!session || !SESSION_UUID_PATTERN.test(session)\n || !origin || !isVisualEditOrigin(origin) || !origins.includes(origin)) return null;\n return { session, origin };\n}\n\nfunction readAddress(element: Element): VisualEditAddress | null {\n try { return parseVisualEditAddress(JSON.parse(element.getAttribute(VISUAL_EDIT_ATTRIBUTE) ?? \"null\")); }\n catch { return null; }\n}\n\n/** 연결·선택만 담당한다. 입력값을 DOM에 덮어쓰거나 서버 콘텐츠를 재해석하지 않는다. */\nexport function RootTaleVisualEditBridge({ receipt, allowedOrigins = DEFAULT_ORIGINS, allowedPaths = [] }: RootTaleVisualEditBridgeProps) {\n const [connection] = useState(() => readConnection(allowedOrigins));\n const [rectangle, setRectangle] = useState<DOMRect | null>(null);\n const selected = useRef<Element | null>(null);\n const receiptKey = JSON.stringify(receipt);\n const pathsKey = JSON.stringify(allowedPaths);\n\n useEffect(() => {\n if (!connection) return;\n const rendered: VisualRenderReceipt = JSON.parse(receiptKey);\n const paths: string[] = JSON.parse(pathsKey);\n let outgoing = 0;\n let incoming = 0;\n let mode: VisualEditMode = \"select\";\n let scrollTimer: ReturnType<typeof setTimeout> | undefined;\n const target = { entityId: rendered.entityId, draftId: rendered.draftId, draftVersion: rendered.draftVersion,\n contractVersion: rendered.contractVersion, templateVersion: rendered.templateVersion };\n const reply = (payload: VisualEditPayload) => window.parent.postMessage(\n createVisualEditMessage({ session: connection.session, sequence: ++outgoing, target }, payload), connection.origin,\n );\n const ready = () => { reply({ type: \"ready\" }); reply({ type: \"rendered\", receipt: rendered }); };\n const highlight = () => setRectangle(selected.current?.isConnected ? selected.current.getBoundingClientRect() : null);\n const position = () => ({ left: Math.max(0, window.scrollX), top: Math.max(0, window.scrollY) });\n\n function receive(event: MessageEvent) {\n if (event.source !== window.parent || event.origin !== connection!.origin) return;\n const message = parseVisualEditMessage(event.data);\n if (!isCurrentVisualEditMessage(message, { session: connection!.session, sequence: incoming, target })) return;\n incoming = message.sequence;\n if (message.type === \"connect\") ready();\n if (message.type === \"mode\") mode = message.mode;\n if (message.type === \"refresh\") window.location.reload();\n if (message.type === \"focus\") {\n if (message.address) {\n const key = visualEditAddressKey(message.address);\n selected.current = [...document.querySelectorAll(`[${VISUAL_EDIT_ATTRIBUTE}]`)].find(element => {\n const address = readAddress(element);\n return address && visualEditAddressKey(address) === key;\n }) ?? null;\n }\n if (message.position) window.scrollTo(message.position.left, message.position.top);\n else selected.current?.scrollIntoView?.({ block: \"nearest\", behavior: \"instant\" });\n highlight();\n }\n }\n\n function click(event: MouseEvent) {\n const element = event.target instanceof Element ? event.target : null;\n const editable = element?.closest(`[${VISUAL_EDIT_ATTRIBUTE}]`);\n const link = element?.closest<HTMLAnchorElement>(\"a[href]\");\n // 모든 실제 문의·결제·사이트 동작은 미리보기에서 실행되지 않는다.\n event.preventDefault(); event.stopImmediatePropagation();\n if (mode === \"navigate\" && link) {\n const url = new URL(link.href, window.location.href);\n if (url.origin === window.location.origin && !url.search && paths.includes(url.pathname)) reply({ type: \"navigate\", pathname: url.pathname });\n return;\n }\n const address = editable ? readAddress(editable) : null;\n if (!address) return;\n selected.current = editable ?? null; highlight();\n reply({ type: \"select\", address, position: position() });\n }\n const block = (event: Event) => { event.preventDefault(); event.stopImmediatePropagation(); };\n const scroll = () => {\n highlight(); clearTimeout(scrollTimer);\n scrollTimer = setTimeout(() => reply({ type: \"viewport\", position: position() }), SCROLL_DEBOUNCE_MS);\n };\n window.addEventListener(\"message\", receive);\n document.addEventListener(\"click\", click, true);\n document.addEventListener(\"auxclick\", block, true);\n document.addEventListener(\"submit\", block, true);\n window.addEventListener(\"scroll\", scroll, true);\n window.addEventListener(\"resize\", highlight);\n ready();\n return () => {\n clearTimeout(scrollTimer);\n window.removeEventListener(\"message\", receive);\n document.removeEventListener(\"click\", click, true);\n document.removeEventListener(\"auxclick\", block, true);\n document.removeEventListener(\"submit\", block, true);\n window.removeEventListener(\"scroll\", scroll, true);\n window.removeEventListener(\"resize\", highlight);\n };\n }, [connection, receiptKey, pathsKey]);\n\n return connection && rectangle ? createPortal(<div aria-hidden=\"true\" data-rt-edit-outline=\"\" style={{\n position: \"fixed\", zIndex: 2147483647, pointerEvents: \"none\", left: rectangle.left, top: rectangle.top,\n width: rectangle.width, height: rectangle.height, outline: \"2px solid var(--rt-editor-selection, #2563eb)\", outlineOffset: 3,\n }} />, document.body) : null;\n}\n"],"mappings":"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","names":[]}
@@ -755,6 +755,41 @@
755
755
  white-space: pre-line;
756
756
  }
757
757
 
758
+ /* Tiptap 주입 CSS의 word-wrap 기본값도 공개 본문과 맞춘다. */
759
+ :is([data-roottale-cms="body"], .rt-cms-body).ProseMirror {
760
+ overflow-wrap: anywhere;
761
+ }
762
+ :is([data-roottale-cms="body"], .rt-cms-body).ProseMirror .rt-imported-html > [contenteditable] {
763
+ white-space: normal;
764
+ overflow-wrap: inherit;
765
+ }
766
+
767
+ /* 다단·영상·표도 편집기와 공개 본문에서 같은 틀을 쓴다. */
768
+ [data-roottale-cms] :where(.rt-columns) {
769
+ display: grid;
770
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
771
+ gap: var(--rt-spacing-6, 24px) var(--rt-spacing-8, 32px);
772
+ margin-block: var(--rt-spacing-8, 32px);
773
+ }
774
+ [data-roottale-cms] :where(.rt-column) {
775
+ min-width: 0;
776
+ }
777
+ [data-roottale-cms] :where(.rt-column > :first-child) { margin-top: 0; }
778
+ [data-roottale-cms] :where(.rt-column > :last-child) { margin-bottom: 0; }
779
+ [data-roottale-cms] :where(.rt-embed) {
780
+ margin-block: var(--rt-spacing-8, 32px);
781
+ overflow: hidden;
782
+ }
783
+ [data-roottale-cms] :where(.rt-embed iframe) { margin: 0; }
784
+ [data-roottale-cms] :where(.rt-table-wrap) {
785
+ overflow-x: auto;
786
+ overscroll-behavior-x: contain;
787
+ }
788
+
789
+ @media (prefers-reduced-motion: reduce) {
790
+ [data-roottale-cms] .rt-cms-faq-q::after { transition: none; }
791
+ }
792
+
758
793
  /* AVCD article 조판. li까지 reset을 복원하며 독립 본문에도 적용한다. */
759
794
  :is([data-roottale-cms="body"], .rt-cms-body) {
760
795
  min-width: 0;
@@ -776,8 +811,14 @@
776
811
  letter-spacing: inherit;
777
812
  }
778
813
 
779
- :is([data-roottale-cms="body"], .rt-cms-body) :where(p) {
780
- margin-block: 0 24px;
814
+ :is([data-roottale-cms="body"], .rt-cms-body) :where(p:not(.rt-imported-html p)) {
815
+ margin-block: 0 16px;
816
+ }
817
+
818
+ /* 가져온 원문은 <p>와 빈 <p><br></p>로 줄·문단 간격을 이미 표현한다.
819
+ 일반 문단 여백을 더하지 않는다. 특이도 0으로 원문 CSS·inline 간격을 우선한다. */
820
+ :where([data-roottale-cms="body"], .rt-cms-body) :where(.rt-imported-html p) {
821
+ margin-block: 0;
781
822
  }
782
823
 
783
824
  :is([data-roottale-cms="body"], .rt-cms-body) :where(:is(h2, h3, h4)) {
@@ -793,7 +834,7 @@
793
834
  }
794
835
 
795
836
  :is([data-roottale-cms="body"], .rt-cms-body) :where(h2) {
796
- margin-block: 110px 20px;
837
+ margin-block: 48px 20px;
797
838
  font-size: 32px;
798
839
  --rt-cms-heading-size: 32px;
799
840
  font-weight: 700;
@@ -872,7 +913,7 @@
872
913
  :is([data-roottale-cms="body"], .rt-cms-body) :where(li) {
873
914
  display: list-item;
874
915
  list-style: inherit;
875
- margin: 0 0 20px;
916
+ margin: 0 0 8px;
876
917
  padding: 0;
877
918
  }
878
919
 
@@ -881,7 +922,7 @@
881
922
  }
882
923
 
883
924
  :is([data-roottale-cms="body"], .rt-cms-body) :where(li > :is(ul, ol)) {
884
- margin-block: 20px 0;
925
+ margin-block: 8px 0;
885
926
  }
886
927
 
887
928
  :is([data-roottale-cms="body"], .rt-cms-body) :where(li > p) {
package/dist/routes.d.ts CHANGED
@@ -198,8 +198,6 @@ type LlmsTxtRouteConfig = RootTaleSiteConfig & {
198
198
  */
199
199
  declare function createLlmsTxtRoute(config: LlmsTxtRouteConfig): () => Promise<Response>;
200
200
 
201
- /** `routes` — revalidate webhook 핸들러 (서명 검증 + 파생 경로 재검증). */
202
-
203
201
  interface RevalidateRouteConfig {
204
202
  /** `rtlk_cust_*` — 블로그 조회/webhook 검증 공통 키. */
205
203
  apiKey: string;
@@ -235,6 +233,13 @@ interface RevalidateRouteConfig {
235
233
  * Requires revalidateTag with immediate expiry; tag policy fact fetches with CONTENT_POLICY_CACHE_TAG.
236
234
  */
237
235
  contentPolicies?: Readonly<Record<string, ContentPolicy>>;
236
+ /**
237
+ * 실제화면 편집을 연결한 사이트의 공통 콘텐츠·관련 목록·CTA를 함께 갱신한다.
238
+ * post.published/updated/deleted와 taxonomy.updated에서 rt-content 태그와 루트
239
+ * layout을 무효화한다. 모든 콘텐츠 조회에 CONTENT_EDITING_CACHE_TAG를 붙이고
240
+ * revalidateTag에 즉시 만료 콜백을 제공해야 한다. 기본 false.
241
+ */
242
+ contentEditing?: boolean;
238
243
  /** 테마 fetch 캐시 tag 이름. 기본 "rt-theme"(cms-client `THEME_CACHE_TAG`). */
239
244
  themeTag?: string;
240
245
  /** 블로그 base path. 기본 "/blog". */
@@ -276,6 +281,12 @@ interface RevalidateRelatedPathsContext {
276
281
  postId: string | null;
277
282
  modelKey: string | null;
278
283
  }
284
+
285
+ /** 공통 콘텐츠·관련 목록·CTA 조회가 공유하는 캐시 태그. */
286
+ declare const CONTENT_EDITING_CACHE_TAG = "rt-content";
287
+
288
+ /** `routes` — revalidate webhook 핸들러 (서명 검증 + 파생 경로 재검증). */
289
+
279
290
  /** 4xx·5xx 거절 사유 — 응답 본문 `reason` 으로 ROOT-ADMIN 전달 표에 그대로 보인다. */
280
291
  type RevalidateRejectReason = "service_unconfigured" | "payload_too_large" | "invalid_body" | "missing_paths" | "model_path_mismatch";
281
292
  /**
@@ -1177,4 +1188,4 @@ interface FleetInfoRouteConfig {
1177
1188
  */
1178
1189
  declare function createFleetInfoRoute(config?: FleetInfoRouteConfig): () => Promise<Response>;
1179
1190
 
1180
- export { ARCHIVE_POST_COUNT_LIMIT, type AlwaysNoindexArchiveKind, type ArchiveCategoriesConfig, type ArchiveCategoriesOptions, type ArchiveIndexDecision, type ArchiveIndexInput, type ArchiveKind, type ArchiveMetadata, type ArchiveMetadataFields, type ArchiveMetadataInput, type ArchivePromotion, type BuildArchiveMetadataOptions, type BuildPageMetadataOptions, type BuildPostMetadataOptions, type CollectionsInput, type ExposureRouteConfig, type FleetInfoRouteConfig, type ImageResponseLike, type LlmsTxtLink, type LlmsTxtRouteConfig, type LlmsTxtSection, type MetadataGoogleBotPolicy, OG_IMAGE_CONTENT_TYPE, OG_IMAGE_SIZE, type OgImageRouteConfig, type PageMetadata, type PostLocaleOptions, type PostMetadata, type PostMetadataInput, type PostSeoOverrides, type PreviewMetadata, type PromotableArchiveKind, type PublicUrlPost, type RedirectMiddlewareConfig, type RevalidateRejectReason, type RevalidateRelatedPathsContext, type RevalidateRouteConfig, type RobotsMeta, type RootTaleSiteConfig, type SitemapEntry, type SitemapIndexOptions, type SitemapMetadataPolicy, type SitemapSectionId, buildArchiveMetadata, buildHreflangLanguages, buildPageMetadata, buildPostMetadata, buildPreviewMetadata, createExposureRoute, createFeedRoute, createFleetInfoRoute, createLlmsTxtRoute, createPostOgImage, createRedirectMiddleware, createRevalidateRoute, createRevalidateRouteHandlers, createSitemap, createSitemapIndex, decideArchiveIndex, fetchBlogArchiveCategories, fetchBlogArchivePosts, filterMetadataSitemapEntries, matchRedirect, postRedirectPath, resolveAuthorPromotion, resolveCategoryPromotion, resolveCollections, resolveCollectionsResult, resolveRobotsMeta, safeFetchPosts, storedPostPath, trimSlash };
1191
+ export { ARCHIVE_POST_COUNT_LIMIT, type AlwaysNoindexArchiveKind, type ArchiveCategoriesConfig, type ArchiveCategoriesOptions, type ArchiveIndexDecision, type ArchiveIndexInput, type ArchiveKind, type ArchiveMetadata, type ArchiveMetadataFields, type ArchiveMetadataInput, type ArchivePromotion, type BuildArchiveMetadataOptions, type BuildPageMetadataOptions, type BuildPostMetadataOptions, CONTENT_EDITING_CACHE_TAG, type CollectionsInput, type ExposureRouteConfig, type FleetInfoRouteConfig, type ImageResponseLike, type LlmsTxtLink, type LlmsTxtRouteConfig, type LlmsTxtSection, type MetadataGoogleBotPolicy, OG_IMAGE_CONTENT_TYPE, OG_IMAGE_SIZE, type OgImageRouteConfig, type PageMetadata, type PostLocaleOptions, type PostMetadata, type PostMetadataInput, type PostSeoOverrides, type PreviewMetadata, type PromotableArchiveKind, type PublicUrlPost, type RedirectMiddlewareConfig, type RevalidateRejectReason, type RevalidateRelatedPathsContext, type RevalidateRouteConfig, type RobotsMeta, type RootTaleSiteConfig, type SitemapEntry, type SitemapIndexOptions, type SitemapMetadataPolicy, type SitemapSectionId, buildArchiveMetadata, buildHreflangLanguages, buildPageMetadata, buildPostMetadata, buildPreviewMetadata, createExposureRoute, createFeedRoute, createFleetInfoRoute, createLlmsTxtRoute, createPostOgImage, createRedirectMiddleware, createRevalidateRoute, createRevalidateRouteHandlers, createSitemap, createSitemapIndex, decideArchiveIndex, fetchBlogArchiveCategories, fetchBlogArchivePosts, filterMetadataSitemapEntries, matchRedirect, postRedirectPath, resolveAuthorPromotion, resolveCategoryPromotion, resolveCollections, resolveCollectionsResult, resolveRobotsMeta, safeFetchPosts, storedPostPath, trimSlash };
package/dist/routes.js CHANGED
@@ -594,6 +594,30 @@ import {
594
594
  verifyRootTaleWebhook
595
595
  } from "@roottale/cms-client/webhook";
596
596
  import { isInCollectionMode as isInCollectionMode4 } from "@roottale/cms-core";
597
+
598
+ // src/routes-revalidate-invalidation.ts
599
+ var CONTENT_EDITING_CACHE_TAG = "rt-content";
600
+ function isContentEditingEvent(event) {
601
+ return event === "post.published" || event === "post.updated" || event === "post.deleted" || event === "taxonomy.updated";
602
+ }
603
+ function createInvalidationBatch(event) {
604
+ const failed = [];
605
+ const recordFailure = (target, error) => {
606
+ const reason = error instanceof Error ? error.message : String(error);
607
+ failed.push({ target, reason });
608
+ console.error(`[@roottale/cms-renderer-next/routes] ${event} \uBB34\uD6A8\uD654 \uC2E4\uD328(${target}):`, reason);
609
+ };
610
+ const attempt = async (target, run) => {
611
+ try {
612
+ await run();
613
+ } catch (error) {
614
+ recordFailure(target, error);
615
+ }
616
+ };
617
+ return { failed, attempt, recordFailure };
618
+ }
619
+
620
+ // src/routes-revalidate.ts
597
621
  var DEFAULT_MAX_BODY_BYTES = 64 * 1024;
598
622
  var NO_STORE = { "cache-control": "no-store" };
599
623
  function jsonResponse(body, status) {
@@ -646,6 +670,9 @@ function createRevalidateRoute(config) {
646
670
  if (hasContentPolicies && !config.revalidateTag) {
647
671
  throw new Error("contentPolicies requires revalidateTag with immediate expiry");
648
672
  }
673
+ if (config.contentEditing && !config.revalidateTag) {
674
+ throw new Error("contentEditing requires revalidateTag with immediate expiry");
675
+ }
649
676
  const blogBase = config.blogBasePath ?? "/blog";
650
677
  const maxBodyBytes = config.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
651
678
  const INVARIANTS = [
@@ -687,42 +714,32 @@ function createRevalidateRoute(config) {
687
714
  }
688
715
  }
689
716
  const parsed = body ?? { paths: [], postId: null, modelKey: null };
717
+ const contentEditing = config.contentEditing === true && isContentEditingEvent(result.event);
690
718
  if (result.event === "theme.updated" || result.event === "taxonomy.updated") {
691
719
  const themeTag = config.themeTag ?? THEME_CACHE_TAG;
692
720
  const requestedTags = [
693
721
  .../* @__PURE__ */ new Set([
694
722
  themeTag,
695
723
  ...SETTINGS_CACHE_TAGS.filter((t) => t !== THEME_CACHE_TAG),
696
- ...hasContentPolicies ? [CONTENT_POLICY_CACHE_TAG] : []
724
+ ...hasContentPolicies ? [CONTENT_POLICY_CACHE_TAG] : [],
725
+ ...contentEditing ? [CONTENT_EDITING_CACHE_TAG] : []
697
726
  ])
698
727
  ];
699
728
  const paths2 = result.event === "taxonomy.updated" ? [.../* @__PURE__ */ new Set([...parsed.paths, ...INVARIANTS, ...config.alsoRevalidate ?? []])] : parsed.paths;
700
- const failed = [];
701
- const attempt = async (target, run) => {
702
- try {
703
- await run();
704
- } catch (e) {
705
- const reason = e instanceof Error ? e.message : String(e);
706
- failed.push({ target, reason });
707
- console.error(
708
- `[@roottale/cms-renderer-next/routes] ${result.event} \uBB34\uD6A8\uD654 \uC2E4\uD328(${target}):`,
709
- reason
710
- );
711
- }
712
- };
729
+ const { failed: failed2, attempt: attempt2 } = createInvalidationBatch(result.event);
713
730
  if (config.revalidateTag) {
714
731
  const revalidateTag = config.revalidateTag;
715
732
  for (const tag of requestedTags) {
716
- await attempt(`tag:${tag}`, () => revalidateTag(tag));
733
+ await attempt2(`tag:${tag}`, () => revalidateTag(tag));
717
734
  }
718
735
  }
719
- await attempt("layout:/", () => config.revalidate("/", "layout"));
736
+ await attempt2("layout:/", () => config.revalidate("/", "layout"));
720
737
  for (const p of paths2) {
721
- await attempt(`path:${p}`, () => config.revalidate(p));
738
+ await attempt2(`path:${p}`, () => config.revalidate(p));
722
739
  }
723
740
  return jsonResponse(
724
741
  {
725
- ok: failed.length === 0,
742
+ ok: failed2.length === 0,
726
743
  event: result.event,
727
744
  deliveryId: result.deliveryId,
728
745
  revalidated: {
@@ -735,9 +752,9 @@ function createRevalidateRoute(config) {
735
752
  layout: "/",
736
753
  paths: paths2
737
754
  },
738
- ...failed.length > 0 ? { failed } : {}
755
+ ...failed2.length > 0 ? { failed: failed2 } : {}
739
756
  },
740
- failed.length === 0 ? 200 : 500
757
+ failed2.length === 0 ? 200 : 500
741
758
  );
742
759
  }
743
760
  let alsoRevalidate;
@@ -747,9 +764,9 @@ function createRevalidateRoute(config) {
747
764
  alsoRevalidate = config.alsoRevalidate;
748
765
  if (Array.isArray(config.collections)) knownCollections = config.collections;
749
766
  } else {
750
- const { collections, failed } = await resolveCollectionsResult(config.collections);
767
+ const { collections, failed: failed2 } = await resolveCollectionsResult(config.collections);
751
768
  knownCollections = collections;
752
- if (failed) {
769
+ if (failed2) {
753
770
  alsoRevalidate = INVARIANTS;
754
771
  warning = "collections_resolve_failed";
755
772
  } else if (isInCollectionMode4(collections)) {
@@ -772,9 +789,18 @@ function createRevalidateRoute(config) {
772
789
  pathCount: parsed.paths.length
773
790
  });
774
791
  }
792
+ const { failed, attempt, recordFailure } = createInvalidationBatch(result.event);
793
+ if (contentEditing && config.revalidateTag) {
794
+ const revalidateTag = config.revalidateTag;
795
+ await attempt(`tag:${CONTENT_EDITING_CACHE_TAG}`, () => revalidateTag(CONTENT_EDITING_CACHE_TAG));
796
+ await attempt("layout:/", () => config.revalidate("/", "layout"));
797
+ }
775
798
  if (hasContentPolicies && config.revalidateTag && (isPostEvent(result.event) || result.event.startsWith("media.") || result.event === "metadata.updated")) {
776
- await config.revalidateTag(CONTENT_POLICY_CACHE_TAG);
777
- await config.revalidateTag(METADATA_CACHE_TAG);
799
+ const revalidateTag = config.revalidateTag;
800
+ for (const tag of [CONTENT_POLICY_CACHE_TAG, METADATA_CACHE_TAG]) {
801
+ if (contentEditing) await attempt(`tag:${tag}`, () => revalidateTag(tag));
802
+ else await revalidateTag(tag);
803
+ }
778
804
  }
779
805
  let related = [];
780
806
  if (config.relatedPaths && isPostEvent(result.event)) {
@@ -787,6 +813,7 @@ function createRevalidateRoute(config) {
787
813
  modelKey: parsed.modelKey
788
814
  })).filter(isSafePath);
789
815
  } catch (e) {
816
+ if (contentEditing) recordFailure("relatedPaths", e);
790
817
  console.error(
791
818
  "[@roottale/cms-renderer-next/routes] relatedPaths \uD6C5 \uC2E4\uD328(\uBCF8\uBB38 paths \uB9CC \uAC31\uC2E0):",
792
819
  e instanceof Error ? e.message : String(e)
@@ -796,17 +823,20 @@ function createRevalidateRoute(config) {
796
823
  }
797
824
  const paths = [.../* @__PURE__ */ new Set([...parsed.paths, ...related, ...alsoRevalidate])];
798
825
  for (const p of paths) {
799
- await config.revalidate(p);
826
+ if (contentEditing) await attempt(`path:${p}`, () => config.revalidate(p));
827
+ else await config.revalidate(p);
800
828
  }
801
829
  return jsonResponse(
802
830
  {
803
- ok: true,
831
+ ok: failed.length === 0,
804
832
  event: result.event,
805
833
  deliveryId: result.deliveryId,
806
834
  revalidated: paths,
807
- ...warning ? { warning } : {}
835
+ ...warning ? { warning } : {},
836
+ ...contentEditing ? { contentEditing: { tag: CONTENT_EDITING_CACHE_TAG, layout: "/" } } : {},
837
+ ...failed.length > 0 ? { failed } : {}
808
838
  },
809
- 200
839
+ failed.length === 0 ? 200 : 500
810
840
  );
811
841
  };
812
842
  }
@@ -1160,7 +1190,7 @@ function createFleetInfoRoute(config = {}) {
1160
1190
  site: config.site ?? env.VERCEL_PROJECT_PRODUCTION_URL ?? null,
1161
1191
  framework: "next",
1162
1192
  cms_client: CMS_CLIENT_VERSION,
1163
- cms_renderer: true ? "0.70.1" : "dev",
1193
+ cms_renderer: true ? "0.72.0" : "dev",
1164
1194
  build_sha: env.VERCEL_GIT_COMMIT_SHA?.slice(0, 7) ?? null,
1165
1195
  ...config.exposures ? { exposures: config.exposures } : {}
1166
1196
  },
@@ -1170,6 +1200,7 @@ function createFleetInfoRoute(config = {}) {
1170
1200
  }
1171
1201
  export {
1172
1202
  ARCHIVE_POST_COUNT_LIMIT,
1203
+ CONTENT_EDITING_CACHE_TAG,
1173
1204
  OG_IMAGE_CONTENT_TYPE,
1174
1205
  OG_IMAGE_SIZE,
1175
1206
  buildArchiveMetadata,