@roottale/cms-renderer-next 0.53.0 → 0.53.1
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 +17 -0
- package/README.md +10 -2
- package/dist/chunk-GVKENK6H.js +75 -0
- package/dist/chunk-GVKENK6H.js.map +1 -0
- package/dist/cms-public.css +27 -0
- package/dist/exposure-client.d.ts +9 -0
- package/dist/exposure-client.js +8 -0
- package/dist/exposure-client.js.map +1 -0
- package/dist/routes.d.ts +1 -1
- package/dist/routes.js +19 -12
- package/dist/routes.js.map +1 -1
- package/dist/server.d.ts +45 -1
- package/dist/server.js +106 -21
- package/dist/server.js.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @roottale/cms-renderer-next
|
|
2
2
|
|
|
3
|
+
## 0.53.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 1efecbb: 페이지·글·정보의 콘텐츠 모델 공개 계약과 `modelKey` 조회를 추가합니다. 개발자가 선언한 슬롯에서만 배너·팝업을 조회하고 접근성 있게 렌더하는 API, client, renderer, 예제 문서를 함께 제공합니다. 기존 collections와 collectionKey 계약은 유지합니다.
|
|
8
|
+
- 800a797: CMS 글 목록 API가 일시적으로 실패해도 `RootTaleBlogList`와 `RootTaleBlogCategories`가 빈 상태를 렌더하도록 바꿉니다. 신규 사이트의 최초 빌드와 공개 목록 페이지가 CMS 장애 때문에 전체 500으로 중단되지 않습니다.
|
|
9
|
+
- 5cd0e3b: 주소 이동 middleware가 API 장애 시 stale 규칙을 계속 사용하고, 순환 규칙은 브라우저를 왕복시키지 않도록 통과처리합니다. Site Materializer 납품물에 middleware와 회귀 검증 파일을 필수로 포함합니다.
|
|
10
|
+
- cf84636: ChatGPT 등에서 만든 완전한 HTML 문서의 태그 구조, class·id·`data-*`, inline style·`<style>` CSS와 반응형 레이아웃을 `importedHtml` 블록에 보존합니다. 본문의 `h1`은 글 제목과 충돌하지 않도록 `h2`로 바꾸되 원래 `h1` CSS 선택자는 변환된 제목에 이어서 적용합니다. 안전한 `!important`도 유지합니다. CSS는 블록별 고유 범위로 격리하며 스크립트·이벤트 속성·외부 CSS 로드·화면을 덮는 CSS만 제거합니다.
|
|
11
|
+
|
|
12
|
+
`html`·`body` 속성, SVG, `picture`·`srcset`, keyframes와 미러링한 CSS 이미지·woff/woff2 폰트도 보존합니다. 가져온 블록별로 문서 루트, animation과 font 이름을 격리해 다른 본문이나 사이트 UI와 충돌하지 않게 합니다.
|
|
13
|
+
|
|
14
|
+
- Updated dependencies [6df15b6]
|
|
15
|
+
- Updated dependencies [1efecbb]
|
|
16
|
+
- Updated dependencies [cf84636]
|
|
17
|
+
- @roottale/cms-client@0.53.1
|
|
18
|
+
- @roottale/cms-core@0.53.1
|
|
19
|
+
|
|
3
20
|
## 0.53.0
|
|
4
21
|
|
|
5
22
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -227,7 +227,7 @@ The form submits via HTML POST (no custom JS). In a Next.js site, point `action`
|
|
|
227
227
|
|
|
228
228
|
| Export | Use |
|
|
229
229
|
|---|---|
|
|
230
|
-
| `RootTaleBlogList` | RSC — fetches `/v1/cms/public/posts?type=post
|
|
230
|
+
| `RootTaleBlogList` | RSC — fetches `/v1/cms/public/posts?type=post`; renders an `<ul>` or a fail-soft empty state. |
|
|
231
231
|
| `RootTaleBlogPost` | RSC — fetches one post, renders Tiptap doc + optional TOC. |
|
|
232
232
|
| `RootTaleLeadForm` | RSC — HTML form for `/v1/public/inquiries` forward routes. |
|
|
233
233
|
| `RootTalePostCard` | Single card primitive (used internally by `RootTaleBlogList`). |
|
|
@@ -239,7 +239,15 @@ The form submits via HTML POST (no custom JS). In a Next.js site, point `action`
|
|
|
239
239
|
## Security model
|
|
240
240
|
|
|
241
241
|
- `@roottale/cms-client` throws if you import it into a browser bundle (`assertServer()`). Never expose `rtlk_cust_*` to the client.
|
|
242
|
-
- Block JSON is server-rendered with hardcoded mark/node mappings
|
|
242
|
+
- Block JSON is server-rendered with hardcoded mark/node mappings. The `importedHtml`
|
|
243
|
+
node is the only authored raw-HTML boundary: the renderer sanitizes its semantic
|
|
244
|
+
markup and imported CSS again before `dangerouslySetInnerHTML`. Imported selectors
|
|
245
|
+
are isolated to a per-block scope. Body `h1` elements become `h2` so the page title
|
|
246
|
+
remains the only `h1`; original `h1` selectors are mapped to the converted heading.
|
|
247
|
+
`data-*` selectors and safe `!important` declarations are preserved. Scripts, event
|
|
248
|
+
handlers, unsafe URL schemes, external CSS loads, and layout-overlay CSS are removed.
|
|
249
|
+
Links in ordinary Tiptap nodes pass an `isSafeHref()` allowlist
|
|
250
|
+
(http/https/mailto/tel/root-relative/fragment/query).
|
|
243
251
|
- The default `target="_blank"` on link marks gets `rel="noopener noreferrer"`.
|
|
244
252
|
|
|
245
253
|
## Compatibility
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/exposure-client.tsx
|
|
4
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
function storageKey(campaign) {
|
|
7
|
+
return `rt-exposure-dismissed:${campaign.id}`;
|
|
8
|
+
}
|
|
9
|
+
function wasDismissed(campaign) {
|
|
10
|
+
if (campaign.repeat === "always") return false;
|
|
11
|
+
try {
|
|
12
|
+
const storage = campaign.repeat === "session" ? sessionStorage : localStorage;
|
|
13
|
+
const value = storage.getItem(storageKey(campaign));
|
|
14
|
+
if (!value) return false;
|
|
15
|
+
if (campaign.repeat === "day") return Date.now() - Number(value) < 864e5;
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function rememberDismissal(campaign) {
|
|
22
|
+
if (campaign.repeat === "always") return;
|
|
23
|
+
try {
|
|
24
|
+
const storage = campaign.repeat === "session" ? sessionStorage : localStorage;
|
|
25
|
+
storage.setItem(storageKey(campaign), String(Date.now()));
|
|
26
|
+
} catch {
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function RootTaleExposure({
|
|
30
|
+
campaign,
|
|
31
|
+
allowedVariants
|
|
32
|
+
}) {
|
|
33
|
+
const [dismissed, setDismissed] = useState(false);
|
|
34
|
+
const closeRef = useRef(null);
|
|
35
|
+
const knownVariant = allowedVariants.includes(campaign.content.variant);
|
|
36
|
+
const eligible = useSyncExternalStore(
|
|
37
|
+
() => () => void 0,
|
|
38
|
+
() => !wasDismissed(campaign),
|
|
39
|
+
() => false
|
|
40
|
+
);
|
|
41
|
+
const visible = knownVariant && eligible && !dismissed;
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!visible || campaign.kind !== "popup") return;
|
|
44
|
+
closeRef.current?.focus();
|
|
45
|
+
const onKeyDown = (event) => {
|
|
46
|
+
if (event.key === "Escape") dismiss();
|
|
47
|
+
};
|
|
48
|
+
window.addEventListener("keydown", onKeyDown);
|
|
49
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
50
|
+
});
|
|
51
|
+
function dismiss() {
|
|
52
|
+
rememberDismissal(campaign);
|
|
53
|
+
setDismissed(true);
|
|
54
|
+
}
|
|
55
|
+
if (!knownVariant || !visible) return null;
|
|
56
|
+
const body = /* @__PURE__ */ jsxs("div", { className: "rt-exposure__body", children: [
|
|
57
|
+
campaign.content.assetUrl ? /* @__PURE__ */ jsx("img", { className: "rt-exposure__asset", src: campaign.content.assetUrl, alt: campaign.content.assetAlt ?? "" }) : null,
|
|
58
|
+
/* @__PURE__ */ jsxs("div", { className: "rt-exposure__copy", children: [
|
|
59
|
+
campaign.content.eyebrow ? /* @__PURE__ */ jsx("p", { className: "rt-exposure__eyebrow", children: campaign.content.eyebrow }) : null,
|
|
60
|
+
/* @__PURE__ */ jsx("h2", { className: "rt-exposure__title", children: campaign.content.title }),
|
|
61
|
+
campaign.content.description ? /* @__PURE__ */ jsx("p", { className: "rt-exposure__description", children: campaign.content.description }) : null,
|
|
62
|
+
campaign.content.linkUrl && campaign.content.linkLabel ? /* @__PURE__ */ jsx("a", { className: "rt-exposure__link", href: campaign.content.linkUrl, children: campaign.content.linkLabel }) : null
|
|
63
|
+
] }),
|
|
64
|
+
/* @__PURE__ */ jsx("button", { ref: closeRef, type: "button", className: "rt-exposure__close", "aria-label": "\uB2EB\uAE30", onClick: dismiss, children: "\xD7" })
|
|
65
|
+
] });
|
|
66
|
+
if (campaign.kind === "banner") {
|
|
67
|
+
return /* @__PURE__ */ jsx("aside", { className: `rt-exposure rt-exposure--banner rt-exposure--${campaign.content.variant}`, "aria-label": "\uC548\uB0B4", children: body });
|
|
68
|
+
}
|
|
69
|
+
return /* @__PURE__ */ jsx("div", { className: "rt-exposure-backdrop", role: "presentation", children: /* @__PURE__ */ jsx("section", { className: `rt-exposure rt-exposure--popup rt-exposure--${campaign.content.variant}`, role: "dialog", "aria-modal": "true", "aria-label": campaign.content.title, children: body }) });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export {
|
|
73
|
+
RootTaleExposure
|
|
74
|
+
};
|
|
75
|
+
//# sourceMappingURL=chunk-GVKENK6H.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/exposure-client.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState, useSyncExternalStore } from \"react\";\n\nimport type { CmsExposureCampaign } from \"@roottale/cms-client/server\";\n\nfunction storageKey(campaign: CmsExposureCampaign): string {\n return `rt-exposure-dismissed:${campaign.id}`;\n}\n\nfunction wasDismissed(campaign: CmsExposureCampaign): boolean {\n if (campaign.repeat === \"always\") return false;\n try {\n const storage = campaign.repeat === \"session\" ? sessionStorage : localStorage;\n const value = storage.getItem(storageKey(campaign));\n if (!value) return false;\n if (campaign.repeat === \"day\") return Date.now() - Number(value) < 86_400_000;\n return true;\n } catch {\n return false;\n }\n}\n\nfunction rememberDismissal(campaign: CmsExposureCampaign): void {\n if (campaign.repeat === \"always\") return;\n try {\n const storage = campaign.repeat === \"session\" ? sessionStorage : localStorage;\n storage.setItem(storageKey(campaign), String(Date.now()));\n } catch {\n // 저장소 차단 환경에서는 현재 화면에서만 닫는다.\n }\n}\n\nexport function RootTaleExposure({\n campaign,\n allowedVariants,\n}: {\n readonly campaign: CmsExposureCampaign;\n readonly allowedVariants: readonly string[];\n}) {\n const [dismissed, setDismissed] = useState(false);\n const closeRef = useRef<HTMLButtonElement>(null);\n const knownVariant = allowedVariants.includes(campaign.content.variant);\n const eligible = useSyncExternalStore(\n () => () => undefined,\n () => !wasDismissed(campaign),\n () => false,\n );\n const visible = knownVariant && eligible && !dismissed;\n\n useEffect(() => {\n if (!visible || campaign.kind !== \"popup\") return;\n closeRef.current?.focus();\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") dismiss();\n };\n window.addEventListener(\"keydown\", onKeyDown);\n return () => window.removeEventListener(\"keydown\", onKeyDown);\n });\n\n function dismiss() {\n rememberDismissal(campaign);\n setDismissed(true);\n }\n\n if (!knownVariant || !visible) return null;\n const body = <div className=\"rt-exposure__body\">\n {campaign.content.assetUrl ? <img className=\"rt-exposure__asset\" src={campaign.content.assetUrl} alt={campaign.content.assetAlt ?? \"\"} /> : null}\n <div className=\"rt-exposure__copy\">\n {campaign.content.eyebrow ? <p className=\"rt-exposure__eyebrow\">{campaign.content.eyebrow}</p> : null}\n <h2 className=\"rt-exposure__title\">{campaign.content.title}</h2>\n {campaign.content.description ? <p className=\"rt-exposure__description\">{campaign.content.description}</p> : null}\n {campaign.content.linkUrl && campaign.content.linkLabel ? <a className=\"rt-exposure__link\" href={campaign.content.linkUrl}>{campaign.content.linkLabel}</a> : null}\n </div>\n <button ref={closeRef} type=\"button\" className=\"rt-exposure__close\" aria-label=\"닫기\" onClick={dismiss}>×</button>\n </div>;\n\n if (campaign.kind === \"banner\") {\n return <aside className={`rt-exposure rt-exposure--banner rt-exposure--${campaign.content.variant}`} aria-label=\"안내\">{body}</aside>;\n }\n return <div className=\"rt-exposure-backdrop\" role=\"presentation\">\n <section className={`rt-exposure rt-exposure--popup rt-exposure--${campaign.content.variant}`} role=\"dialog\" aria-modal=\"true\" aria-label={campaign.content.title}>\n {body}\n </section>\n </div>;\n}\n"],"mappings":";AAEA,SAAS,WAAW,QAAQ,UAAU,4BAA4B;AAiEjC,cAC7B,YAD6B;AA7DjC,SAAS,WAAW,UAAuC;AACzD,SAAO,yBAAyB,SAAS,EAAE;AAC7C;AAEA,SAAS,aAAa,UAAwC;AAC5D,MAAI,SAAS,WAAW,SAAU,QAAO;AACzC,MAAI;AACF,UAAM,UAAU,SAAS,WAAW,YAAY,iBAAiB;AACjE,UAAM,QAAQ,QAAQ,QAAQ,WAAW,QAAQ,CAAC;AAClD,QAAI,CAAC,MAAO,QAAO;AACnB,QAAI,SAAS,WAAW,MAAO,QAAO,KAAK,IAAI,IAAI,OAAO,KAAK,IAAI;AACnE,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,kBAAkB,UAAqC;AAC9D,MAAI,SAAS,WAAW,SAAU;AAClC,MAAI;AACF,UAAM,UAAU,SAAS,WAAW,YAAY,iBAAiB;AACjE,YAAQ,QAAQ,WAAW,QAAQ,GAAG,OAAO,KAAK,IAAI,CAAC,CAAC;AAAA,EAC1D,QAAQ;AAAA,EAER;AACF;AAEO,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AACF,GAGG;AACD,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAChD,QAAM,WAAW,OAA0B,IAAI;AAC/C,QAAM,eAAe,gBAAgB,SAAS,SAAS,QAAQ,OAAO;AACtE,QAAM,WAAW;AAAA,IACf,MAAM,MAAM;AAAA,IACZ,MAAM,CAAC,aAAa,QAAQ;AAAA,IAC5B,MAAM;AAAA,EACR;AACA,QAAM,UAAU,gBAAgB,YAAY,CAAC;AAE7C,YAAU,MAAM;AACd,QAAI,CAAC,WAAW,SAAS,SAAS,QAAS;AAC3C,aAAS,SAAS,MAAM;AACxB,UAAM,YAAY,CAAC,UAAyB;AAC1C,UAAI,MAAM,QAAQ,SAAU,SAAQ;AAAA,IACtC;AACA,WAAO,iBAAiB,WAAW,SAAS;AAC5C,WAAO,MAAM,OAAO,oBAAoB,WAAW,SAAS;AAAA,EAC9D,CAAC;AAED,WAAS,UAAU;AACjB,sBAAkB,QAAQ;AAC1B,iBAAa,IAAI;AAAA,EACnB;AAEA,MAAI,CAAC,gBAAgB,CAAC,QAAS,QAAO;AACtC,QAAM,OAAO,qBAAC,SAAI,WAAU,qBACzB;AAAA,aAAS,QAAQ,WAAW,oBAAC,SAAI,WAAU,sBAAqB,KAAK,SAAS,QAAQ,UAAU,KAAK,SAAS,QAAQ,YAAY,IAAI,IAAK;AAAA,IAC5I,qBAAC,SAAI,WAAU,qBACZ;AAAA,eAAS,QAAQ,UAAU,oBAAC,OAAE,WAAU,wBAAwB,mBAAS,QAAQ,SAAQ,IAAO;AAAA,MACjG,oBAAC,QAAG,WAAU,sBAAsB,mBAAS,QAAQ,OAAM;AAAA,MAC1D,SAAS,QAAQ,cAAc,oBAAC,OAAE,WAAU,4BAA4B,mBAAS,QAAQ,aAAY,IAAO;AAAA,MAC5G,SAAS,QAAQ,WAAW,SAAS,QAAQ,YAAY,oBAAC,OAAE,WAAU,qBAAoB,MAAM,SAAS,QAAQ,SAAU,mBAAS,QAAQ,WAAU,IAAO;AAAA,OAChK;AAAA,IACA,oBAAC,YAAO,KAAK,UAAU,MAAK,UAAS,WAAU,sBAAqB,cAAW,gBAAK,SAAS,SAAS,kBAAC;AAAA,KACzG;AAEA,MAAI,SAAS,SAAS,UAAU;AAC9B,WAAO,oBAAC,WAAM,WAAW,gDAAgD,SAAS,QAAQ,OAAO,IAAI,cAAW,gBAAM,gBAAK;AAAA,EAC7H;AACA,SAAO,oBAAC,SAAI,WAAU,wBAAuB,MAAK,gBAChD,8BAAC,aAAQ,WAAW,+CAA+C,SAAS,QAAQ,OAAO,IAAI,MAAK,UAAS,cAAW,QAAO,cAAY,SAAS,QAAQ,OACzJ,gBACH,GACF;AACF;","names":[]}
|
package/dist/cms-public.css
CHANGED
|
@@ -23,6 +23,21 @@
|
|
|
23
23
|
list-style: none;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
.rt-sr-only { position: absolute; inline-size: 1px; block-size: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
27
|
+
.rt-exposure { color: var(--rt-color-foreground, #111827); background: var(--rt-color-background, #fff); border: 1px solid var(--rt-color-border, #e5e7eb); }
|
|
28
|
+
.rt-exposure__body { position: relative; display: grid; gap: 1rem; padding: 1rem; }
|
|
29
|
+
.rt-exposure__asset { max-inline-size: 100%; block-size: auto; border-radius: .5rem; }
|
|
30
|
+
.rt-exposure__copy { display: grid; gap: .5rem; }
|
|
31
|
+
.rt-exposure__eyebrow, .rt-exposure__title, .rt-exposure__description { margin: 0; }
|
|
32
|
+
.rt-exposure__eyebrow { font-size: .75rem; font-weight: 700; }
|
|
33
|
+
.rt-exposure__title { font-size: 1.25rem; line-height: 1.3; }
|
|
34
|
+
.rt-exposure__link { inline-size: fit-content; font-weight: 700; color: var(--rt-color-primary, #1d4ed8); }
|
|
35
|
+
.rt-exposure__close { position: absolute; inset-block-start: .5rem; inset-inline-end: .5rem; inline-size: 2.75rem; block-size: 2.75rem; border: 0; border-radius: 999px; background: color-mix(in srgb, var(--rt-color-background, #fff) 85%, transparent); color: inherit; cursor: pointer; font-size: 1.5rem; }
|
|
36
|
+
.rt-exposure-backdrop { position: fixed; inset: 0; z-index: 9999; display: grid; place-items: center; padding: 1rem; background: rgb(0 0 0 / .55); }
|
|
37
|
+
.rt-exposure--popup { inline-size: min(100%, 34rem); max-block-size: calc(100dvh - 2rem); overflow: auto; border-radius: 1rem; box-shadow: 0 1.5rem 4rem rgb(0 0 0 / .25); }
|
|
38
|
+
@media (min-width: 40rem) { .rt-exposure--banner .rt-exposure__body { grid-template-columns: minmax(0, 12rem) 1fr; align-items: center; } }
|
|
39
|
+
@media (prefers-reduced-motion: reduce) { .rt-exposure, .rt-exposure-backdrop { scroll-behavior: auto; animation: none !important; transition: none !important; } }
|
|
40
|
+
|
|
26
41
|
/* Category filter — chips are <a> links (/blog/categories/slug), SSR-only, no JS. */
|
|
27
42
|
[data-roottale-cms] :where(.rt-cms-filter) {
|
|
28
43
|
display: flex;
|
|
@@ -370,6 +385,18 @@
|
|
|
370
385
|
overflow-wrap: anywhere;
|
|
371
386
|
}
|
|
372
387
|
|
|
388
|
+
/* 외부 게시판 HTML 가져오기. 원문의 안전한 inline style은 유지하되,
|
|
389
|
+
넓은 표·이미지가 본문 measure 밖으로 페이지를 밀지 않게 경계를 둔다. */
|
|
390
|
+
[data-roottale-cms] :where(.rt-imported-html) {
|
|
391
|
+
min-inline-size: 0;
|
|
392
|
+
overflow-x: auto;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
[data-roottale-cms] :where(.rt-imported-html img) {
|
|
396
|
+
max-inline-size: 100%;
|
|
397
|
+
block-size: auto;
|
|
398
|
+
}
|
|
399
|
+
|
|
373
400
|
/* 시각 브레드크럼 (avcd 구조, opt-in — RootTaleBlogPost `breadcrumb` prop). */
|
|
374
401
|
[data-roottale-cms] :where(.rt-cms-breadcrumb) {
|
|
375
402
|
max-inline-size: var(--rt-cms-measure, 42.5rem);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { CmsExposureCampaign } from '@roottale/cms-client/server';
|
|
3
|
+
|
|
4
|
+
declare function RootTaleExposure({ campaign, allowedVariants, }: {
|
|
5
|
+
readonly campaign: CmsExposureCampaign;
|
|
6
|
+
readonly allowedVariants: readonly string[];
|
|
7
|
+
}): react_jsx_runtime.JSX.Element | null;
|
|
8
|
+
|
|
9
|
+
export { RootTaleExposure };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/routes.d.ts
CHANGED
|
@@ -389,7 +389,7 @@ declare function normalizeRedirectPath(path: string): string;
|
|
|
389
389
|
* 매칭하지 않는다. 절대 URL 도착지는 체인 종단(다른 origin 위임). 상태 코드는
|
|
390
390
|
* 실제 요청 경로에 매칭된 첫 규칙을 따른다. null = 해당 없음.
|
|
391
391
|
*/
|
|
392
|
-
declare function matchRedirect(redirects: readonly RootTaleRedirect[], pathname: string): {
|
|
392
|
+
declare function matchRedirect(redirects: readonly RootTaleRedirect[], pathname: string, origin?: string): {
|
|
393
393
|
toTarget: string;
|
|
394
394
|
statusCode: 301 | 302;
|
|
395
395
|
} | null;
|
package/dist/routes.js
CHANGED
|
@@ -791,7 +791,17 @@ function normalizeRedirectPath(path) {
|
|
|
791
791
|
function isAbsoluteUrl(target) {
|
|
792
792
|
return /^https?:\/\//i.test(target);
|
|
793
793
|
}
|
|
794
|
-
function
|
|
794
|
+
function internalTargetPath(target, origin) {
|
|
795
|
+
if (!isAbsoluteUrl(target)) return normalizeRedirectPath(target);
|
|
796
|
+
if (!origin) return null;
|
|
797
|
+
try {
|
|
798
|
+
const url = new URL(target);
|
|
799
|
+
return url.origin === origin ? normalizeRedirectPath(url.pathname) : null;
|
|
800
|
+
} catch {
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
function matchRedirect(redirects, pathname, origin) {
|
|
795
805
|
const byFrom = /* @__PURE__ */ new Map();
|
|
796
806
|
for (const r of redirects) {
|
|
797
807
|
const key = normalizeRedirectPath(r.fromPath);
|
|
@@ -802,21 +812,18 @@ function matchRedirect(redirects, pathname) {
|
|
|
802
812
|
let current = start;
|
|
803
813
|
let finalTarget = null;
|
|
804
814
|
let firstStatus = null;
|
|
805
|
-
for (
|
|
815
|
+
for (; ; ) {
|
|
806
816
|
const rule = byFrom.get(current);
|
|
807
817
|
if (!rule) break;
|
|
808
|
-
if (
|
|
809
|
-
|
|
818
|
+
if (firstStatus === null) firstStatus = rule.statusCode;
|
|
819
|
+
const nextPath = internalTargetPath(rule.toTarget, origin);
|
|
820
|
+
if (nextPath === null) {
|
|
810
821
|
finalTarget = rule.toTarget;
|
|
811
822
|
break;
|
|
812
823
|
}
|
|
813
|
-
|
|
814
|
-
if (nextPath === start) {
|
|
815
|
-
if (hop === 0) return null;
|
|
816
|
-
break;
|
|
817
|
-
}
|
|
824
|
+
if (nextPath === start) return null;
|
|
818
825
|
finalTarget = rule.toTarget;
|
|
819
|
-
if (visited.has(nextPath))
|
|
826
|
+
if (visited.has(nextPath)) return null;
|
|
820
827
|
visited.add(nextPath);
|
|
821
828
|
current = nextPath;
|
|
822
829
|
}
|
|
@@ -860,7 +867,7 @@ function createRedirectMiddleware(config) {
|
|
|
860
867
|
}
|
|
861
868
|
const now = Date.now();
|
|
862
869
|
const redirects = await loadRedirects(now);
|
|
863
|
-
const hit = matchRedirect(redirects, pathname);
|
|
870
|
+
const hit = matchRedirect(redirects, pathname, origin);
|
|
864
871
|
if (!hit) return null;
|
|
865
872
|
const dest = new URL(hit.toTarget, origin);
|
|
866
873
|
if (dest.origin === origin && normalizeRedirectPath(dest.pathname) === normalizeRedirectPath(pathname)) {
|
|
@@ -888,7 +895,7 @@ function createFleetInfoRoute(config = {}) {
|
|
|
888
895
|
site: config.site ?? env.VERCEL_PROJECT_PRODUCTION_URL ?? null,
|
|
889
896
|
framework: "next",
|
|
890
897
|
cms_client: CMS_CLIENT_VERSION,
|
|
891
|
-
cms_renderer: true ? "0.53.
|
|
898
|
+
cms_renderer: true ? "0.53.1" : "dev",
|
|
892
899
|
build_sha: env.VERCEL_GIT_COMMIT_SHA?.slice(0, 7) ?? null
|
|
893
900
|
},
|
|
894
901
|
{ headers: { "cache-control": "public, max-age=300" } }
|