@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 +43 -0
- package/README.md +20 -0
- package/dist/chunk-26TNINA6.js +146 -0
- package/dist/chunk-26TNINA6.js.map +1 -0
- package/dist/cms-public.css +46 -5
- package/dist/routes.d.ts +14 -3
- package/dist/routes.js +60 -29
- package/dist/routes.js.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.js +8 -0
- package/dist/server.js.map +1 -1
- package/dist/visual-edit-bridge.d.ts +14 -0
- package/dist/visual-edit-bridge.js +8 -0
- package/dist/visual-edit-bridge.js.map +1 -0
- package/dist/visual-edit.d.ts +3 -0
- package/dist/visual-edit.js +21 -0
- package/dist/visual-edit.js.map +1 -0
- package/package.json +9 -4
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":[]}
|
package/dist/cms-public.css
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
733
|
+
await attempt2(`tag:${tag}`, () => revalidateTag(tag));
|
|
717
734
|
}
|
|
718
735
|
}
|
|
719
|
-
await
|
|
736
|
+
await attempt2("layout:/", () => config.revalidate("/", "layout"));
|
|
720
737
|
for (const p of paths2) {
|
|
721
|
-
await
|
|
738
|
+
await attempt2(`path:${p}`, () => config.revalidate(p));
|
|
722
739
|
}
|
|
723
740
|
return jsonResponse(
|
|
724
741
|
{
|
|
725
|
-
ok:
|
|
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
|
-
...
|
|
755
|
+
...failed2.length > 0 ? { failed: failed2 } : {}
|
|
739
756
|
},
|
|
740
|
-
|
|
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 (
|
|
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
|
-
|
|
777
|
-
|
|
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:
|
|
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.
|
|
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,
|