@roottale/cms-client 0.40.0 → 0.41.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 +39 -0
- package/dist/server.d.ts +81 -1
- package/dist/server.js +89 -62
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# @roottale/cms-client
|
|
2
2
|
|
|
3
|
+
## 0.41.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 3810879: Add Turnstile-aware inquiry submission fields, rate-limit error handling, and docs for protected lead intake.
|
|
8
|
+
|
|
9
|
+
## 0.41.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- e8b1d33: 관련 글(related posts) — 글 하단에 같은 카테고리 최근 글 노출.
|
|
14
|
+
|
|
15
|
+
`RootTaleBlogPost`에 옵션 추가:
|
|
16
|
+
- `relatedPostsCount`(기본 0 = off — 기존 마크업 보존), `relatedPostsTitle`(기본
|
|
17
|
+
"관련 글"), `postHref` / `collections`(링크 라우팅, 목록과 동일 규칙).
|
|
18
|
+
- 켜면 같은 **카테고리**를 1개 이상 공유하는 최근 글을 N개 `<nav
|
|
19
|
+
class="rt-cms-related">` 로 렌더(현재 글 제외, 발행일 내림차순). 현재 글에
|
|
20
|
+
카테고리가 없거나 후보가 없으면 렌더되지 않는다.
|
|
21
|
+
|
|
22
|
+
`@roottale/cms-client` 에 순수 헬퍼 `selectRelatedPosts(current, candidates,
|
|
23
|
+
limit)` 추가(서버 카테고리 필터가 없어 최근 글 배치에서 선별). 스타일은
|
|
24
|
+
`cms-public.css` 의 `.rt-cms-related*` 클래스(테마 토큰 기반).
|
|
25
|
+
|
|
26
|
+
- 9f246f4: 목차(TOC) 사이드바 배치 opt-in — 넓은 화면에서 본문 오른쪽 sticky 목차.
|
|
27
|
+
|
|
28
|
+
블로그 설정 `toc_position`(`"inline"` | `"sidebar"`) 추가. 기본은 기존과 동일한
|
|
29
|
+
`"inline"`(본문 위 접이식). `"sidebar"` 선택 시:
|
|
30
|
+
- 넓은 화면(본문 컬럼 ≥ 60rem)에서 본문 오른쪽에 스크롤을 따라오는 sticky 목차.
|
|
31
|
+
- 좁은 화면은 CSS 컨테이너 쿼리로 자동 인라인(본문 위)로 폴백 — 마크업 동일.
|
|
32
|
+
- 뷰포트가 아닌 **본문 컬럼 폭** 기준(`@container`)이라 임베드 위치·주변
|
|
33
|
+
레이아웃과 무관하게 안전.
|
|
34
|
+
|
|
35
|
+
`RootTaleBlogPost`는 배치를 자동 반영하므로 고객 연동 코드 변경은 불필요.
|
|
36
|
+
`RootTaleBlogSettings` / `ResolvedPostDisplay` 에 `tocPosition` 필드 추가
|
|
37
|
+
(cms-client). 어드민 "블로그 설정 → 목차 위치" 에서 선택.
|
|
38
|
+
|
|
39
|
+
공개 API `GET /v1/cms/public/blog-settings` 응답에 `toc_position` 필드 추가
|
|
40
|
+
(cms-mcp api-reference 문서 동반 갱신).
|
|
41
|
+
|
|
3
42
|
## 0.40.0
|
|
4
43
|
|
|
5
44
|
### Minor Changes
|
package/dist/server.d.ts
CHANGED
|
@@ -28,6 +28,18 @@ interface CmsTermRef {
|
|
|
28
28
|
slug: string;
|
|
29
29
|
name: string;
|
|
30
30
|
}
|
|
31
|
+
interface CmsArchiveRelatedPost {
|
|
32
|
+
id: string;
|
|
33
|
+
title: string;
|
|
34
|
+
slug: string;
|
|
35
|
+
excerpt: string | null;
|
|
36
|
+
collectionKey: string;
|
|
37
|
+
}
|
|
38
|
+
interface CmsArchiveRelatedGroup {
|
|
39
|
+
collectionKey: string;
|
|
40
|
+
label: string;
|
|
41
|
+
items: CmsArchiveRelatedPost[];
|
|
42
|
+
}
|
|
31
43
|
interface CmsPostContent {
|
|
32
44
|
/** UUIDv7. */
|
|
33
45
|
id: string;
|
|
@@ -78,6 +90,7 @@ interface CmsPostContent {
|
|
|
78
90
|
* 빈 배열. category 만 추출하려면 `terms.filter(t => t.taxonomy === "category")`.
|
|
79
91
|
*/
|
|
80
92
|
terms: CmsTermRef[];
|
|
93
|
+
archiveRelated: CmsArchiveRelatedGroup[];
|
|
81
94
|
/**
|
|
82
95
|
* ADR-0065 — 누적 조회수. 어드민에서 "공개 조회수"를 켠 사이트에서만 내려온다
|
|
83
96
|
* (꺼져 있으면 `undefined`). "조회 N" 표시에 사용. 비콘이 채우므로 신규 글은 0.
|
|
@@ -114,6 +127,8 @@ interface FetchPostsOptions {
|
|
|
114
127
|
siteId?: string;
|
|
115
128
|
/** Optional AbortSignal. */
|
|
116
129
|
signal?: AbortSignal;
|
|
130
|
+
/** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */
|
|
131
|
+
revalidate?: number;
|
|
117
132
|
}
|
|
118
133
|
interface FetchPostOptions {
|
|
119
134
|
apiKey: string;
|
|
@@ -123,6 +138,8 @@ interface FetchPostOptions {
|
|
|
123
138
|
/** Override the auto-resolved site id. */
|
|
124
139
|
siteId?: string;
|
|
125
140
|
signal?: AbortSignal;
|
|
141
|
+
/** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */
|
|
142
|
+
revalidate?: number;
|
|
126
143
|
}
|
|
127
144
|
declare class CmsApiError extends Error {
|
|
128
145
|
readonly status: number;
|
|
@@ -163,12 +180,32 @@ interface RootTaleTheme {
|
|
|
163
180
|
lg?: string;
|
|
164
181
|
xl?: string;
|
|
165
182
|
};
|
|
183
|
+
/**
|
|
184
|
+
* W6-3 — `sites.name`(DB SSoT, `rt init --name` 이 저장) 미러. 외부 사이트가
|
|
185
|
+
* `<title>`/OG/헤더·푸터 브랜드명을 site.config.ts 정적 리터럴 대신 런타임
|
|
186
|
+
* 반영하는 데 쓴다. 미설정이면 소비자가 site.config.ts 값으로 fallback.
|
|
187
|
+
*/
|
|
188
|
+
siteName?: string;
|
|
166
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* 테마 fetch 캐시 tag 의 정본 이름 (W0-4). starter layout 의 `fetchTheme` 호출에
|
|
192
|
+
* `tags: [THEME_CACHE_TAG]` 로 부착하고, revalidate route 가 `theme.updated`
|
|
193
|
+
* 수신 시 `revalidateTag(THEME_CACHE_TAG)` 로 무효화한다. 양쪽이 이 상수를 공유해
|
|
194
|
+
* drift 를 방지한다.
|
|
195
|
+
*/
|
|
196
|
+
declare const THEME_CACHE_TAG = "rt-theme";
|
|
167
197
|
interface FetchThemeOptions {
|
|
168
198
|
apiKey: string;
|
|
169
199
|
baseUrl?: string;
|
|
170
200
|
siteId?: string;
|
|
171
201
|
signal?: AbortSignal;
|
|
202
|
+
/** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */
|
|
203
|
+
revalidate?: number;
|
|
204
|
+
/**
|
|
205
|
+
* on-demand 무효화용 Next fetch cache tags(opt-in, W0-4). 보통
|
|
206
|
+
* `[THEME_CACHE_TAG]`. 미지정 시 현행 동작 유지.
|
|
207
|
+
*/
|
|
208
|
+
tags?: string[];
|
|
172
209
|
}
|
|
173
210
|
/**
|
|
174
211
|
* tenant 의 RootTale 디자인 토큰을 가져온다. admin /design 의 마지막 저장값
|
|
@@ -194,6 +231,8 @@ interface FetchAnalyticsOptions {
|
|
|
194
231
|
baseUrl?: string;
|
|
195
232
|
siteId?: string;
|
|
196
233
|
signal?: AbortSignal;
|
|
234
|
+
/** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */
|
|
235
|
+
revalidate?: number;
|
|
197
236
|
}
|
|
198
237
|
/**
|
|
199
238
|
* ADR-0038 plane ① — 외부 사이트가 `<head>` 에 주입할 런타임 태그(GA4 measurement
|
|
@@ -204,6 +243,15 @@ interface FetchAnalyticsOptions {
|
|
|
204
243
|
declare function fetchAnalyticsConfig(options: FetchAnalyticsOptions): Promise<SiteAnalyticsTags>;
|
|
205
244
|
declare function fetchPosts(options: FetchPostsOptions): Promise<CmsPostContentPage>;
|
|
206
245
|
declare function fetchPost(options: FetchPostOptions): Promise<CmsPostContent | null>;
|
|
246
|
+
interface FetchPageBySlugOptions {
|
|
247
|
+
readonly apiKey: string;
|
|
248
|
+
readonly baseUrl?: string;
|
|
249
|
+
readonly slug: string;
|
|
250
|
+
readonly siteId?: string;
|
|
251
|
+
readonly signal?: AbortSignal;
|
|
252
|
+
readonly revalidate?: number;
|
|
253
|
+
}
|
|
254
|
+
declare function fetchPageBySlug(options: FetchPageBySlugOptions): Promise<CmsPostContent | null>;
|
|
207
255
|
|
|
208
256
|
/**
|
|
209
257
|
* 블로그 표시 설정 — admin /design 의 "블로그 표시" 에서 저장한 사이트 기본값.
|
|
@@ -252,6 +300,11 @@ interface RootTaleBlogSettings {
|
|
|
252
300
|
showAuthorCard: boolean;
|
|
253
301
|
/** 목차 상단 제목. null = renderer 기본("목차"). */
|
|
254
302
|
tocTitle: string | null;
|
|
303
|
+
/**
|
|
304
|
+
* 목차 배치. "inline"=본문 위 접이식(기본), "sidebar"=넓은 화면에서 본문
|
|
305
|
+
* 오른쪽 sticky 사이드바(좁은 화면은 자동 인라인 폴백).
|
|
306
|
+
*/
|
|
307
|
+
tocPosition: "inline" | "sidebar";
|
|
255
308
|
/** 작성자 카드 상단 라벨. null = renderer 기본("작성자"). */
|
|
256
309
|
authorCardEyebrow: string | null;
|
|
257
310
|
/** 작성자 카드 설명. null = 설명 없음. */
|
|
@@ -283,6 +336,8 @@ interface FetchBlogSettingsOptions {
|
|
|
283
336
|
baseUrl?: string;
|
|
284
337
|
siteId?: string;
|
|
285
338
|
signal?: AbortSignal;
|
|
339
|
+
/** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */
|
|
340
|
+
revalidate?: number;
|
|
286
341
|
}
|
|
287
342
|
/**
|
|
288
343
|
* tenant 의 블로그 표시 기본값을 가져온다. 네트워크/서버 에러는 throw —
|
|
@@ -311,6 +366,8 @@ interface FetchCollectionsOptions {
|
|
|
311
366
|
baseUrl?: string;
|
|
312
367
|
siteId?: string;
|
|
313
368
|
signal?: AbortSignal;
|
|
369
|
+
/** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */
|
|
370
|
+
revalidate?: number;
|
|
314
371
|
}
|
|
315
372
|
/**
|
|
316
373
|
* 사이트의 콘텐츠 Collections (공지/블로그 등 스트림 분리)를 가져온다. 미설정이면
|
|
@@ -341,6 +398,8 @@ interface FetchAuthorsOptions {
|
|
|
341
398
|
baseUrl?: string;
|
|
342
399
|
siteId?: string;
|
|
343
400
|
signal?: AbortSignal;
|
|
401
|
+
/** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */
|
|
402
|
+
revalidate?: number;
|
|
344
403
|
}
|
|
345
404
|
/**
|
|
346
405
|
* 사이트의 작가 목록(작가 아카이브·사이트맵 파생용)을 가져온다. author_slug 가 있고
|
|
@@ -356,6 +415,7 @@ interface ResolvedPostDisplay {
|
|
|
356
415
|
showDate: boolean;
|
|
357
416
|
showAuthorCard: boolean;
|
|
358
417
|
tocTitle: string;
|
|
418
|
+
tocPosition: "inline" | "sidebar";
|
|
359
419
|
authorCardEyebrow: string;
|
|
360
420
|
authorCardDescription: string | null;
|
|
361
421
|
authorProfileName: string | null;
|
|
@@ -376,6 +436,15 @@ declare function resolvePostDisplay(post: {
|
|
|
376
436
|
showTableOfContents?: boolean;
|
|
377
437
|
tableOfContentsTitle?: string;
|
|
378
438
|
}): ResolvedPostDisplay;
|
|
439
|
+
/**
|
|
440
|
+
* 관련 글 선별 — 현재 글과 **같은 카테고리**를 1개 이상 공유하는 후보를
|
|
441
|
+
* 발행일 내림차순으로 골라 최대 `limit` 개 반환 (자기 자신 제외).
|
|
442
|
+
*
|
|
443
|
+
* 순수 함수: 후보(`candidates`)는 호출부가 `fetchPosts` 로 미리 받아 전달한다
|
|
444
|
+
* (서버에 카테고리 필터 파라미터가 없어 최근 글 배치에서 선별). 현재 글에
|
|
445
|
+
* 카테고리가 없으면 빈 배열.
|
|
446
|
+
*/
|
|
447
|
+
declare function selectRelatedPosts(current: CmsPostContent, candidates: CmsPostContent[], limit?: number): CmsPostContent[];
|
|
379
448
|
|
|
380
449
|
/** Schema.org LocalBusiness 하위 타입 (admin "운영 > 비즈니스 프로필" 의 업종). */
|
|
381
450
|
type BusinessType = "LocalBusiness" | "ProfessionalService" | "AccountingService" | "LegalService" | "MedicalClinic" | "Dentist" | "RealEstateAgent" | "Restaurant" | "BeautySalon";
|
|
@@ -430,6 +499,8 @@ interface FetchBusinessProfileOptions {
|
|
|
430
499
|
baseUrl?: string;
|
|
431
500
|
siteId?: string;
|
|
432
501
|
signal?: AbortSignal;
|
|
502
|
+
/** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */
|
|
503
|
+
revalidate?: number;
|
|
433
504
|
}
|
|
434
505
|
type BusinessProfileWire = {
|
|
435
506
|
configured?: boolean;
|
|
@@ -483,6 +554,8 @@ interface SearchPostsOptions {
|
|
|
483
554
|
type?: CmsPostType;
|
|
484
555
|
siteId?: string;
|
|
485
556
|
signal?: AbortSignal;
|
|
557
|
+
/** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */
|
|
558
|
+
revalidate?: number;
|
|
486
559
|
}
|
|
487
560
|
/** 검색 결과 카드용 슬림 hit — body 미포함. */
|
|
488
561
|
interface CmsSearchHit {
|
|
@@ -526,12 +599,16 @@ interface FetchMenuOptions {
|
|
|
526
599
|
slug: string;
|
|
527
600
|
siteId?: string;
|
|
528
601
|
signal?: AbortSignal;
|
|
602
|
+
/** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */
|
|
603
|
+
revalidate?: number;
|
|
529
604
|
}
|
|
530
605
|
interface FetchMenusOptions {
|
|
531
606
|
apiKey: string;
|
|
532
607
|
baseUrl?: string;
|
|
533
608
|
siteId?: string;
|
|
534
609
|
signal?: AbortSignal;
|
|
610
|
+
/** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */
|
|
611
|
+
revalidate?: number;
|
|
535
612
|
}
|
|
536
613
|
/**
|
|
537
614
|
* 위치 핸들 slug 로 네비게이션 메뉴 1개를 가져온다. admin "디자인 > 메뉴" 의
|
|
@@ -555,6 +632,8 @@ interface FetchRedirectsOptions {
|
|
|
555
632
|
baseUrl?: string;
|
|
556
633
|
siteId?: string;
|
|
557
634
|
signal?: AbortSignal;
|
|
635
|
+
/** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */
|
|
636
|
+
revalidate?: number;
|
|
558
637
|
}
|
|
559
638
|
/**
|
|
560
639
|
* 사이트의 활성 커스텀 리다이렉트 규칙 전체. 고객 사이트 middleware
|
|
@@ -741,6 +820,7 @@ interface SubmitInquiryFields {
|
|
|
741
820
|
* sales 는 medical 국외이전 동의 게이트 면제.
|
|
742
821
|
*/
|
|
743
822
|
leadKind?: "patient" | "sales";
|
|
823
|
+
turnstileToken?: string;
|
|
744
824
|
/**
|
|
745
825
|
* 표준 필드 외에 폼이 받은 임의 항목(key→value)을 있는 그대로 전달. intake 가
|
|
746
826
|
* 암호화 보관하고 CRM 상세에서 노출한다. 예약 키(표준 필드)·`_` 접두 키는 무시.
|
|
@@ -784,4 +864,4 @@ declare function formatKoreanPhoneNumber(input: string): string;
|
|
|
784
864
|
*/
|
|
785
865
|
declare function submitInquiry(options: SubmitInquiryOptions): Promise<SubmitInquiryResult>;
|
|
786
866
|
|
|
787
|
-
export { type AnalyticsTagConfig, type AnalyticsTagProvider, type ArticleSchemaInput, type BusinessDayCode, type BusinessOpeningHours, type BusinessType, CMS_CLIENT_VERSION, CmsApiError, type CmsPostContent, type CmsPostContentPage, type CmsPostType, type CmsRouteCollection, type CmsSearchHit, type CmsTaxonomyKind, type CmsTermRef, DEFAULT_BLOG_SETTINGS, type FetchAnalyticsOptions, type FetchAuthorsOptions, type FetchBlogSettingsOptions, type FetchBusinessProfileOptions, type FetchCollectionsOptions, type FetchMenuOptions, type FetchMenusOptions, type FetchPostOptions, type FetchPostsOptions, type FetchRedirectsOptions, type FetchThemeOptions, INQUIRY_ERROR_MESSAGES, InquiryAttribution, type InquiryVertical, type LocalBusinessProfileInput, type ResolvedPostDisplay, type RootTaleAuthor, type RootTaleBlogPostCta, type RootTaleBlogSettings, type RootTaleBusinessProfile, type RootTaleMenu, type RootTaleMenuItem, type RootTaleRedirect, type RootTaleSiteProfile, type RootTaleSitemapSettings, type RootTaleTheme, type RssFeedInput, type RssItemInput, type SearchPostsOptions, type SiteAnalyticsTags, type SubmitInquiryFields, type SubmitInquiryOptions, type SubmitInquiryResult, articleSchema, breadcrumbSchema, businessProfileFromWire, contentIdMeta, faqSchema, fetchAnalyticsConfig, fetchAuthors, fetchBlogSettings, fetchBusinessProfile, fetchCollections, fetchCollectionsSafeDefault, fetchMenu, fetchMenus, fetchPost, fetchPosts, fetchRedirects, fetchTheme, formatKoreanPhoneNumber, generateRssXml, inquiryErrorMessage, jsonLdScript, localBusinessSchema, organizationSchema, parseInquiryRedirect, resolvePostDisplay, rssItemsFromPosts, searchPosts, submitInquiry, websiteSchema };
|
|
867
|
+
export { type AnalyticsTagConfig, type AnalyticsTagProvider, type ArticleSchemaInput, type BusinessDayCode, type BusinessOpeningHours, type BusinessType, CMS_CLIENT_VERSION, CmsApiError, type CmsArchiveRelatedGroup, type CmsArchiveRelatedPost, type CmsPostContent, type CmsPostContentPage, type CmsPostType, type CmsRouteCollection, type CmsSearchHit, type CmsTaxonomyKind, type CmsTermRef, DEFAULT_BLOG_SETTINGS, type FetchAnalyticsOptions, type FetchAuthorsOptions, type FetchBlogSettingsOptions, type FetchBusinessProfileOptions, type FetchCollectionsOptions, type FetchMenuOptions, type FetchMenusOptions, type FetchPageBySlugOptions, type FetchPostOptions, type FetchPostsOptions, type FetchRedirectsOptions, type FetchThemeOptions, INQUIRY_ERROR_MESSAGES, InquiryAttribution, type InquiryVertical, type LocalBusinessProfileInput, type ResolvedPostDisplay, type RootTaleAuthor, type RootTaleBlogPostCta, type RootTaleBlogSettings, type RootTaleBusinessProfile, type RootTaleMenu, type RootTaleMenuItem, type RootTaleRedirect, type RootTaleSiteProfile, type RootTaleSitemapSettings, type RootTaleTheme, type RssFeedInput, type RssItemInput, type SearchPostsOptions, type SiteAnalyticsTags, type SubmitInquiryFields, type SubmitInquiryOptions, type SubmitInquiryResult, THEME_CACHE_TAG, articleSchema, breadcrumbSchema, businessProfileFromWire, contentIdMeta, faqSchema, fetchAnalyticsConfig, fetchAuthors, fetchBlogSettings, fetchBusinessProfile, fetchCollections, fetchCollectionsSafeDefault, fetchMenu, fetchMenus, fetchPageBySlug, fetchPost, fetchPosts, fetchRedirects, fetchTheme, formatKoreanPhoneNumber, generateRssXml, inquiryErrorMessage, jsonLdScript, localBusinessSchema, organizationSchema, parseInquiryRedirect, resolvePostDisplay, rssItemsFromPosts, searchPosts, selectRelatedPosts, submitInquiry, websiteSchema };
|
package/dist/server.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
|
|
5
5
|
// src/server-base.ts
|
|
6
6
|
var DEFAULT_BASE_URL = "https://api.roottale.com";
|
|
7
|
-
var CLIENT_VERSION = true ? "0.
|
|
7
|
+
var CLIENT_VERSION = true ? "0.41.1" : "dev";
|
|
8
8
|
var CMS_CLIENT_VERSION = CLIENT_VERSION;
|
|
9
9
|
function authHeaders(apiKey) {
|
|
10
10
|
return {
|
|
@@ -12,6 +12,20 @@ function authHeaders(apiKey) {
|
|
|
12
12
|
"x-roottale-client": `cms-client/${CLIENT_VERSION}`
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
|
+
function buildGetInit(apiKey, opts) {
|
|
16
|
+
const init = {
|
|
17
|
+
method: "GET",
|
|
18
|
+
headers: authHeaders(apiKey),
|
|
19
|
+
signal: opts.signal
|
|
20
|
+
};
|
|
21
|
+
if (opts.revalidate !== void 0 || opts.tags && opts.tags.length > 0) {
|
|
22
|
+
init.next = {
|
|
23
|
+
...opts.revalidate !== void 0 ? { revalidate: opts.revalidate } : {},
|
|
24
|
+
...opts.tags && opts.tags.length > 0 ? { tags: opts.tags } : {}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return init;
|
|
28
|
+
}
|
|
15
29
|
var CmsApiError = class extends Error {
|
|
16
30
|
status;
|
|
17
31
|
code;
|
|
@@ -42,6 +56,7 @@ function assertServer() {
|
|
|
42
56
|
}
|
|
43
57
|
|
|
44
58
|
// src/server-posts-theme.ts
|
|
59
|
+
var THEME_CACHE_TAG = "rt-theme";
|
|
45
60
|
async function fetchTheme(options) {
|
|
46
61
|
assertServer();
|
|
47
62
|
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
@@ -49,11 +64,7 @@ async function fetchTheme(options) {
|
|
|
49
64
|
if (options.siteId) params.set("site_id", options.siteId);
|
|
50
65
|
const qs = params.toString();
|
|
51
66
|
const url = `${baseUrl}/v1/cms/public/theme${qs ? `?${qs}` : ""}`;
|
|
52
|
-
const response = await fetch(url,
|
|
53
|
-
method: "GET",
|
|
54
|
-
headers: authHeaders(options.apiKey),
|
|
55
|
-
signal: options.signal
|
|
56
|
-
});
|
|
67
|
+
const response = await fetch(url, buildGetInit(options.apiKey, options));
|
|
57
68
|
if (!response.ok) {
|
|
58
69
|
throw await toApiError(response);
|
|
59
70
|
}
|
|
@@ -62,6 +73,7 @@ async function fetchTheme(options) {
|
|
|
62
73
|
if (json.colors) theme.colors = json.colors;
|
|
63
74
|
if (json.fonts) theme.fonts = json.fonts;
|
|
64
75
|
if (json.radius) theme.radius = json.radius;
|
|
76
|
+
if (json.site_name) theme.siteName = json.site_name;
|
|
65
77
|
return theme;
|
|
66
78
|
}
|
|
67
79
|
async function fetchAnalyticsConfig(options) {
|
|
@@ -71,11 +83,7 @@ async function fetchAnalyticsConfig(options) {
|
|
|
71
83
|
if (options.siteId) params.set("site_id", options.siteId);
|
|
72
84
|
const qs = params.toString();
|
|
73
85
|
const url = `${baseUrl}/v1/cms/public/analytics${qs ? `?${qs}` : ""}`;
|
|
74
|
-
const response = await fetch(url,
|
|
75
|
-
method: "GET",
|
|
76
|
-
headers: authHeaders(options.apiKey),
|
|
77
|
-
signal: options.signal
|
|
78
|
-
});
|
|
86
|
+
const response = await fetch(url, buildGetInit(options.apiKey, options));
|
|
79
87
|
if (!response.ok) {
|
|
80
88
|
throw await toApiError(response);
|
|
81
89
|
}
|
|
@@ -93,11 +101,7 @@ async function fetchPosts(options) {
|
|
|
93
101
|
if (options.siteId) params.set("site_id", options.siteId);
|
|
94
102
|
const qs = params.toString();
|
|
95
103
|
const url = `${baseUrl}/v1/cms/public/posts${qs ? `?${qs}` : ""}`;
|
|
96
|
-
const response = await fetch(url,
|
|
97
|
-
method: "GET",
|
|
98
|
-
headers: authHeaders(options.apiKey),
|
|
99
|
-
signal: options.signal
|
|
100
|
-
});
|
|
104
|
+
const response = await fetch(url, buildGetInit(options.apiKey, options));
|
|
101
105
|
if (!response.ok) {
|
|
102
106
|
throw await toApiError(response);
|
|
103
107
|
}
|
|
@@ -115,11 +119,7 @@ async function fetchPost(options) {
|
|
|
115
119
|
if (options.siteId) params.set("site_id", options.siteId);
|
|
116
120
|
const qs = params.toString();
|
|
117
121
|
const url = `${baseUrl}/v1/cms/public/posts/${encodeURIComponent(options.slugOrId)}${qs ? `?${qs}` : ""}`;
|
|
118
|
-
const response = await fetch(url,
|
|
119
|
-
method: "GET",
|
|
120
|
-
headers: authHeaders(options.apiKey),
|
|
121
|
-
signal: options.signal
|
|
122
|
-
});
|
|
122
|
+
const response = await fetch(url, buildGetInit(options.apiKey, options));
|
|
123
123
|
if (response.status === 404) return null;
|
|
124
124
|
if (!response.ok) {
|
|
125
125
|
throw await toApiError(response);
|
|
@@ -127,6 +127,25 @@ async function fetchPost(options) {
|
|
|
127
127
|
const json = await response.json();
|
|
128
128
|
return fromWire(json);
|
|
129
129
|
}
|
|
130
|
+
async function fetchPageBySlug(options) {
|
|
131
|
+
let cursor;
|
|
132
|
+
do {
|
|
133
|
+
const page = await fetchPosts({
|
|
134
|
+
apiKey: options.apiKey,
|
|
135
|
+
baseUrl: options.baseUrl,
|
|
136
|
+
limit: 100,
|
|
137
|
+
...cursor ? { cursor } : {},
|
|
138
|
+
type: "page",
|
|
139
|
+
siteId: options.siteId,
|
|
140
|
+
signal: options.signal,
|
|
141
|
+
revalidate: options.revalidate
|
|
142
|
+
});
|
|
143
|
+
const match = page.items.find((item) => item.slug === options.slug);
|
|
144
|
+
if (match) return match;
|
|
145
|
+
cursor = page.nextCursor ?? void 0;
|
|
146
|
+
} while (cursor);
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
130
149
|
function fromWire(wire) {
|
|
131
150
|
return {
|
|
132
151
|
id: wire.id,
|
|
@@ -148,6 +167,17 @@ function fromWire(wire) {
|
|
|
148
167
|
metaJson: wire.meta_json,
|
|
149
168
|
fields: wire.fields ?? null,
|
|
150
169
|
terms: wire.terms ?? [],
|
|
170
|
+
archiveRelated: (wire.archive_related ?? []).map((group) => ({
|
|
171
|
+
collectionKey: group.collection_key,
|
|
172
|
+
label: group.label,
|
|
173
|
+
items: group.items.map((item) => ({
|
|
174
|
+
id: item.id,
|
|
175
|
+
title: item.title,
|
|
176
|
+
slug: item.slug,
|
|
177
|
+
excerpt: item.excerpt,
|
|
178
|
+
collectionKey: item.collection_key
|
|
179
|
+
}))
|
|
180
|
+
})),
|
|
151
181
|
...typeof wire.view_count === "number" ? { viewCount: wire.view_count } : {},
|
|
152
182
|
publishedAt: wire.published_at,
|
|
153
183
|
createdAt: wire.created_at,
|
|
@@ -163,11 +193,7 @@ async function fetchBlogSettings(options) {
|
|
|
163
193
|
if (options.siteId) params.set("site_id", options.siteId);
|
|
164
194
|
const qs = params.toString();
|
|
165
195
|
const url = `${baseUrl}/v1/cms/public/blog-settings${qs ? `?${qs}` : ""}`;
|
|
166
|
-
const response = await fetch(url,
|
|
167
|
-
method: "GET",
|
|
168
|
-
headers: authHeaders(options.apiKey),
|
|
169
|
-
signal: options.signal
|
|
170
|
-
});
|
|
196
|
+
const response = await fetch(url, buildGetInit(options.apiKey, options));
|
|
171
197
|
if (!response.ok) {
|
|
172
198
|
throw await toApiError(response);
|
|
173
199
|
}
|
|
@@ -181,6 +207,7 @@ async function fetchBlogSettings(options) {
|
|
|
181
207
|
showDate: json.show_date !== false,
|
|
182
208
|
showAuthorCard: json.show_author_card !== false,
|
|
183
209
|
tocTitle: typeof json.toc_title === "string" && json.toc_title.length > 0 ? json.toc_title : null,
|
|
210
|
+
tocPosition: json.toc_position === "sidebar" ? "sidebar" : "inline",
|
|
184
211
|
authorCardEyebrow: typeof json.author_card_eyebrow === "string" && json.author_card_eyebrow.length > 0 ? json.author_card_eyebrow : null,
|
|
185
212
|
authorCardDescription: typeof json.author_card_description === "string" && json.author_card_description.length > 0 ? json.author_card_description : null,
|
|
186
213
|
authorProfileName: typeof json.author_profile_name === "string" && json.author_profile_name.length > 0 ? json.author_profile_name : null,
|
|
@@ -217,11 +244,7 @@ async function fetchCollections(options) {
|
|
|
217
244
|
if (options.siteId) params.set("site_id", options.siteId);
|
|
218
245
|
const qs = params.toString();
|
|
219
246
|
const url = `${baseUrl}/v1/cms/public/collections${qs ? `?${qs}` : ""}`;
|
|
220
|
-
const response = await fetch(url,
|
|
221
|
-
method: "GET",
|
|
222
|
-
headers: authHeaders(options.apiKey),
|
|
223
|
-
signal: options.signal
|
|
224
|
-
});
|
|
247
|
+
const response = await fetch(url, buildGetInit(options.apiKey, options));
|
|
225
248
|
if (!response.ok) {
|
|
226
249
|
throw await toApiError(response);
|
|
227
250
|
}
|
|
@@ -256,11 +279,7 @@ async function fetchAuthors(options) {
|
|
|
256
279
|
if (options.siteId) params.set("site_id", options.siteId);
|
|
257
280
|
const qs = params.toString();
|
|
258
281
|
const url = `${baseUrl}/v1/cms/public/authors${qs ? `?${qs}` : ""}`;
|
|
259
|
-
const response = await fetch(url,
|
|
260
|
-
method: "GET",
|
|
261
|
-
headers: authHeaders(options.apiKey),
|
|
262
|
-
signal: options.signal
|
|
263
|
-
});
|
|
282
|
+
const response = await fetch(url, buildGetInit(options.apiKey, options));
|
|
264
283
|
if (!response.ok) {
|
|
265
284
|
throw await toApiError(response);
|
|
266
285
|
}
|
|
@@ -288,6 +307,7 @@ var DEFAULT_BLOG_SETTINGS = {
|
|
|
288
307
|
showDate: true,
|
|
289
308
|
showAuthorCard: true,
|
|
290
309
|
tocTitle: null,
|
|
310
|
+
tocPosition: "inline",
|
|
291
311
|
authorCardEyebrow: null,
|
|
292
312
|
authorCardDescription: null,
|
|
293
313
|
authorProfileName: null,
|
|
@@ -318,6 +338,7 @@ function resolvePostDisplay(post, settings, explicit) {
|
|
|
318
338
|
showDate: ov("date") ?? settings.showDate,
|
|
319
339
|
showAuthorCard: settings.showAuthorCard,
|
|
320
340
|
tocTitle: explicit?.tableOfContentsTitle ?? settings.tocTitle ?? "\uBAA9\uCC28",
|
|
341
|
+
tocPosition: settings.tocPosition ?? "inline",
|
|
321
342
|
authorCardEyebrow: settings.authorCardEyebrow ?? "\uC791\uC131\uC790",
|
|
322
343
|
authorCardDescription: settings.authorCardDescription,
|
|
323
344
|
authorProfileName: settings.authorProfileName,
|
|
@@ -328,6 +349,18 @@ function resolvePostDisplay(post, settings, explicit) {
|
|
|
328
349
|
authorProfileImagePositionY: settings.authorProfileImagePositionY
|
|
329
350
|
};
|
|
330
351
|
}
|
|
352
|
+
function selectRelatedPosts(current, candidates, limit = 3) {
|
|
353
|
+
if (limit <= 0) return [];
|
|
354
|
+
const currentCats = new Set(
|
|
355
|
+
current.terms.filter((t) => t.taxonomy === "category").map((t) => t.id)
|
|
356
|
+
);
|
|
357
|
+
if (currentCats.size === 0) return [];
|
|
358
|
+
return candidates.filter(
|
|
359
|
+
(p) => p.id !== current.id && p.terms.some(
|
|
360
|
+
(t) => t.taxonomy === "category" && currentCats.has(t.id)
|
|
361
|
+
)
|
|
362
|
+
).sort((a, b) => b.publishedAt.localeCompare(a.publishedAt)).slice(0, limit);
|
|
363
|
+
}
|
|
331
364
|
|
|
332
365
|
// src/server-business.ts
|
|
333
366
|
function businessProfileFromWire(json) {
|
|
@@ -373,11 +406,7 @@ async function fetchBusinessProfile(options) {
|
|
|
373
406
|
if (options.siteId) params.set("site_id", options.siteId);
|
|
374
407
|
const qs = params.toString();
|
|
375
408
|
const url = `${baseUrl}/v1/cms/public/business-profile${qs ? `?${qs}` : ""}`;
|
|
376
|
-
const response = await fetch(url,
|
|
377
|
-
method: "GET",
|
|
378
|
-
headers: authHeaders(options.apiKey),
|
|
379
|
-
signal: options.signal
|
|
380
|
-
});
|
|
409
|
+
const response = await fetch(url, buildGetInit(options.apiKey, options));
|
|
381
410
|
if (response.status === 404) return null;
|
|
382
411
|
if (!response.ok) {
|
|
383
412
|
throw await toApiError(response);
|
|
@@ -396,11 +425,7 @@ async function searchPosts(options) {
|
|
|
396
425
|
if (options.type) params.set("type", options.type);
|
|
397
426
|
if (options.siteId) params.set("site_id", options.siteId);
|
|
398
427
|
const url = `${baseUrl}/v1/cms/public/search?${params.toString()}`;
|
|
399
|
-
const response = await fetch(url,
|
|
400
|
-
method: "GET",
|
|
401
|
-
headers: authHeaders(options.apiKey),
|
|
402
|
-
signal: options.signal
|
|
403
|
-
});
|
|
428
|
+
const response = await fetch(url, buildGetInit(options.apiKey, options));
|
|
404
429
|
if (response.status === 404) return [];
|
|
405
430
|
if (!response.ok) {
|
|
406
431
|
throw await toApiError(response);
|
|
@@ -440,11 +465,7 @@ async function fetchMenu(options) {
|
|
|
440
465
|
if (options.siteId) params.set("site_id", options.siteId);
|
|
441
466
|
const qs = params.toString();
|
|
442
467
|
const url = `${baseUrl}/v1/cms/public/menus/${encodeURIComponent(options.slug)}${qs ? `?${qs}` : ""}`;
|
|
443
|
-
const response = await fetch(url,
|
|
444
|
-
method: "GET",
|
|
445
|
-
headers: authHeaders(options.apiKey),
|
|
446
|
-
signal: options.signal
|
|
447
|
-
});
|
|
468
|
+
const response = await fetch(url, buildGetInit(options.apiKey, options));
|
|
448
469
|
if (response.status === 404) return null;
|
|
449
470
|
if (!response.ok) {
|
|
450
471
|
throw await toApiError(response);
|
|
@@ -459,11 +480,7 @@ async function fetchMenus(options) {
|
|
|
459
480
|
if (options.siteId) params.set("site_id", options.siteId);
|
|
460
481
|
const qs = params.toString();
|
|
461
482
|
const url = `${baseUrl}/v1/cms/public/menus${qs ? `?${qs}` : ""}`;
|
|
462
|
-
const response = await fetch(url,
|
|
463
|
-
method: "GET",
|
|
464
|
-
headers: authHeaders(options.apiKey),
|
|
465
|
-
signal: options.signal
|
|
466
|
-
});
|
|
483
|
+
const response = await fetch(url, buildGetInit(options.apiKey, options));
|
|
467
484
|
if (response.status === 404) return [];
|
|
468
485
|
if (!response.ok) {
|
|
469
486
|
throw await toApiError(response);
|
|
@@ -478,11 +495,7 @@ async function fetchRedirects(options) {
|
|
|
478
495
|
if (options.siteId) params.set("site_id", options.siteId);
|
|
479
496
|
const qs = params.toString();
|
|
480
497
|
const url = `${baseUrl}/v1/cms/public/redirects${qs ? `?${qs}` : ""}`;
|
|
481
|
-
const response = await fetch(url,
|
|
482
|
-
method: "GET",
|
|
483
|
-
headers: authHeaders(options.apiKey),
|
|
484
|
-
signal: options.signal
|
|
485
|
-
});
|
|
498
|
+
const response = await fetch(url, buildGetInit(options.apiKey, options));
|
|
486
499
|
if (response.status === 404) return [];
|
|
487
500
|
if (!response.ok) {
|
|
488
501
|
throw await toApiError(response);
|
|
@@ -709,6 +722,8 @@ var INQUIRY_ERROR_MESSAGES = {
|
|
|
709
722
|
missing_fields: "\uD544\uC218 \uD56D\uBAA9\uC774 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.",
|
|
710
723
|
invalid_email: "\uC774\uBA54\uC77C \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
|
|
711
724
|
invalid_api_key: "\uC5F0\uB3D9 \uC778\uC99D\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. API \uD0A4\uB97C \uD655\uC778\uD574\uC8FC\uC138\uC694.",
|
|
725
|
+
rate_limited: "\uC694\uCCAD\uC774 \uB108\uBB34 \uB9CE\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.",
|
|
726
|
+
turnstile_failed: "\uBCF4\uC548 \uD655\uC778\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. \uC0C8\uB85C\uACE0\uCE68 \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.",
|
|
712
727
|
internal: "\uC804\uC1A1\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694."
|
|
713
728
|
};
|
|
714
729
|
var RESERVED_INQUIRY_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -722,7 +737,8 @@ var RESERVED_INQUIRY_KEYS = /* @__PURE__ */ new Set([
|
|
|
722
737
|
"current_site_url",
|
|
723
738
|
"privacy_consent",
|
|
724
739
|
"overseas_transfer_consent",
|
|
725
|
-
"lead_kind"
|
|
740
|
+
"lead_kind",
|
|
741
|
+
"cf-turnstile-response"
|
|
726
742
|
]);
|
|
727
743
|
function inquiryErrorMessage(code) {
|
|
728
744
|
return code && INQUIRY_ERROR_MESSAGES[code] || INQUIRY_ERROR_MESSAGES.internal;
|
|
@@ -776,6 +792,7 @@ async function submitInquiry(options) {
|
|
|
776
792
|
if (f.privacyConsent) body.set("privacy_consent", "on");
|
|
777
793
|
if (f.overseasTransferConsent) body.set("overseas_transfer_consent", "on");
|
|
778
794
|
if (f.leadKind) body.set("lead_kind", f.leadKind);
|
|
795
|
+
if (f.turnstileToken) body.set("cf-turnstile-response", f.turnstileToken);
|
|
779
796
|
if (f.attribution) {
|
|
780
797
|
for (const [key, value] of Object.entries(f.attribution)) {
|
|
781
798
|
if (typeof value !== "string" || !value.trim()) continue;
|
|
@@ -813,6 +830,13 @@ async function submitInquiry(options) {
|
|
|
813
830
|
message: inquiryErrorMessage("invalid_api_key")
|
|
814
831
|
};
|
|
815
832
|
}
|
|
833
|
+
if (response.status === 429) {
|
|
834
|
+
return {
|
|
835
|
+
ok: false,
|
|
836
|
+
code: "rate_limited",
|
|
837
|
+
message: inquiryErrorMessage("rate_limited")
|
|
838
|
+
};
|
|
839
|
+
}
|
|
816
840
|
const location = response.headers.get("location");
|
|
817
841
|
if (location) return parseInquiryRedirect(location, baseUrl);
|
|
818
842
|
if (response.ok) return { ok: true };
|
|
@@ -830,6 +854,7 @@ export {
|
|
|
830
854
|
CmsApiError,
|
|
831
855
|
DEFAULT_BLOG_SETTINGS,
|
|
832
856
|
INQUIRY_ERROR_MESSAGES,
|
|
857
|
+
THEME_CACHE_TAG,
|
|
833
858
|
articleSchema,
|
|
834
859
|
breadcrumbSchema,
|
|
835
860
|
businessProfileFromWire,
|
|
@@ -843,6 +868,7 @@ export {
|
|
|
843
868
|
fetchCollectionsSafeDefault,
|
|
844
869
|
fetchMenu,
|
|
845
870
|
fetchMenus,
|
|
871
|
+
fetchPageBySlug,
|
|
846
872
|
fetchPost,
|
|
847
873
|
fetchPosts,
|
|
848
874
|
fetchRedirects,
|
|
@@ -858,6 +884,7 @@ export {
|
|
|
858
884
|
resolvePostDisplay,
|
|
859
885
|
rssItemsFromPosts,
|
|
860
886
|
searchPosts,
|
|
887
|
+
selectRelatedPosts,
|
|
861
888
|
submitInquiry,
|
|
862
889
|
websiteSchema
|
|
863
890
|
};
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server-base.ts","../src/server-posts-theme.ts","../src/server-blog.ts","../src/server-business.ts","../src/server-discovery.ts","../src/seo.ts","../src/inquiry.ts"],"sourcesContent":["/**\n * `@roottale/cms-client/server` — RootTale CMS Public API server-side fetch client.\n *\n * 외부 고객사 사이트가 RootTale CMS 콘텐츠를 fetch 하기 위한 SSR-only client.\n * Bearer (`rtlk_cust_*`) 인증, 브라우저 직접 호출 금지 (ADR-0023 §5.1 #15).\n *\n * 사용 예 (Next.js server component):\n * ```ts\n * import { fetchPosts } from \"@roottale/cms-client/server\";\n *\n * const posts = await fetchPosts({\n * apiKey: process.env.ROOTTALE_API_KEY!,\n * limit: 10,\n * });\n * ```\n *\n * 데이터 모델 = ADR-0034 `posts` (WP 7.0-aligned, type=post|page).\n * Phase 1 scope: list + get. WP import/export, draft preview = 별도 PR.\n */\n\nexport type CmsPostType = \"post\" | \"page\";\n\nexport type CmsTaxonomyKind = \"category\" | \"tag\" | \"custom\";\n\nexport interface CmsTermRef {\n id: string;\n taxonomy: CmsTaxonomyKind;\n slug: string;\n name: string;\n}\n\nexport interface CmsPostContent {\n /** UUIDv7. */\n id: string;\n /** Owning tenant id. */\n tenantId: string;\n /** Owning site id (ADR-0034 P4 dual-key). */\n siteId: string;\n /** WP post type. */\n type: CmsPostType;\n /**\n * ADR-0060 Amendment 1 — 글의 명시 섹션(content collection) key. 라우팅/스트림\n * 판정에 카테고리 파생보다 우선. 구 서버/미설정이면 null.\n */\n collectionKey: string | null;\n /** Lowercase kebab-case slug (1-120 chars). */\n slug: string;\n title: string;\n excerpt: string | null;\n /** Media id (resolved by renderer / consumer to URL). */\n featuredMediaId: string | null;\n /**\n * featured 이미지 delivery URL (CF Images variant, md 우선). 서버가 `featured_media_id`\n * → `media.variants_json` 를 해석해 제공. 대표 이미지 미설정 시 null.\n */\n featuredImageUrl: string | null;\n authorId: string | null;\n /** Public display name for the post author. Email is not exposed. */\n authorName: string | null;\n /** Public profile image URL for the post author. */\n authorImageUrl: string | null;\n /** Public biography text for the post author. */\n authorBio: string | null;\n /** Tiptap / Gutenberg-compatible Block JSON. Sanitization is renderer's responsibility. */\n bodyJson: Record<string, unknown>;\n /** Public API only exposes `published`. */\n status: \"published\";\n /** Per-post meta (SEO overrides, canonical, og, ...). */\n metaJson: Record<string, unknown>;\n /**\n * ADR-0041 — 포맷된 커스텀 필드(ACF) 값. 서버가 필드 정의(코드 ⊕ DB 그룹)로\n * `meta_json.acf` 를 포맷 — image/file/gallery 는 media variant URL 객체.\n * 해석된 그룹이 없거나 구 서버면 null.\n */\n fields: Record<string, unknown> | null;\n /**\n * Attached taxonomy terms (categories + tags + custom). Server 가 category →\n * tag → custom 순서로 정렬해 보내며, 각 taxonomy 내부는 name asc. attach 가 0 개면\n * 빈 배열. category 만 추출하려면 `terms.filter(t => t.taxonomy === \"category\")`.\n */\n terms: CmsTermRef[];\n /**\n * ADR-0065 — 누적 조회수. 어드민에서 \"공개 조회수\"를 켠 사이트에서만 내려온다\n * (꺼져 있으면 `undefined`). \"조회 N\" 표시에 사용. 비콘이 채우므로 신규 글은 0.\n */\n viewCount?: number;\n /** ISO 8601. */\n publishedAt: string;\n /** ISO 8601. */\n createdAt: string;\n /** ISO 8601. */\n updatedAt: string;\n}\n\nexport interface CmsPostContentPage {\n items: CmsPostContent[];\n nextCursor: string | null;\n hasMore: boolean;\n}\n\nexport const DEFAULT_BASE_URL = \"https://api.roottale.com\";\n\n// fleet 가시성 — 자기 버전을 자가 보고 (build-time 주입, tsup define).\n// api-core 가 사이트별 check-in 으로 기록 → internal `GET /v1/internal/sites`.\ndeclare const __CMS_CLIENT_VERSION__: string | undefined;\nconst CLIENT_VERSION =\n typeof __CMS_CLIENT_VERSION__ === \"string\" ? __CMS_CLIENT_VERSION__ : \"dev\";\n\n/** 설치된 cms-client 버전 (build-time 주입) — fleet well-known 라우트 등에서 사용. */\nexport const CMS_CLIENT_VERSION = CLIENT_VERSION;\n\nexport function authHeaders(apiKey: string): Record<string, string> {\n return {\n authorization: `Bearer ${apiKey}`,\n \"x-roottale-client\": `cms-client/${CLIENT_VERSION}`,\n };\n}\n\nexport interface FetchPostsOptions {\n /** `rtlk_cust_*` API key. Required. */\n apiKey: string;\n /** Override the API base URL. Defaults to `https://api.roottale.com`. */\n baseUrl?: string;\n /** Page size, 1-100, default 20. */\n limit?: number;\n /** Opaque pagination cursor returned from previous call. */\n cursor?: string;\n /** Filter by post type (`post` or `page`). Default = both. */\n type?: CmsPostType;\n /** 작가 slug 필터 — 작가 아카이브(/blog/author/{slug})용. 지정 시 발행 글(post)만. */\n author?: string;\n /** Override the auto-resolved site id (Phase 1 = 1 site/tenant, usually omitted). */\n siteId?: string;\n /** Optional AbortSignal. */\n signal?: AbortSignal;\n}\n\nexport interface FetchPostOptions {\n apiKey: string;\n baseUrl?: string;\n /** Slug (kebab-case) or UUIDv7 id. */\n slugOrId: string;\n /** Override the auto-resolved site id. */\n siteId?: string;\n signal?: AbortSignal;\n}\n\n\nexport class CmsApiError extends Error {\n readonly status: number;\n readonly code: string;\n constructor(message: string, status: number, code: string) {\n super(message);\n this.name = \"CmsApiError\";\n this.status = status;\n this.code = code;\n }\n}\n\nexport async function toApiError(response: Response): Promise<CmsApiError> {\n let code = \"request_failed\";\n let message = `RootTale CMS API error: ${response.status}`;\n try {\n const body = (await response.json()) as { code?: string; message?: string };\n if (body.code) code = body.code;\n if (body.message) message = body.message;\n } catch {\n /* ignore body parse */\n }\n return new CmsApiError(message, response.status, code);\n}\n\nexport function assertServer(): void {\n if (typeof window !== \"undefined\") {\n throw new Error(\n \"@roottale/cms-client/server must not run in the browser. \" +\n \"Move this call to a Server Component, getServerSideProps, or a Route Handler. \" +\n \"Shipping API keys to the browser breaks the security model (ADR-0023 §5.1 #15).\",\n );\n }\n}\n\n// 프레임워크 무관 SEO 헬퍼 (RSS 2.0 + Schema.org JSON-LD) 재노출.\n","import {\n type CmsPostType,\n type CmsPostContent,\n type CmsPostContentPage,\n type CmsTermRef,\n type FetchPostOptions,\n type FetchPostsOptions,\n DEFAULT_BASE_URL,\n assertServer,\n authHeaders,\n toApiError,\n} from \"./server-base\";\n\n/**\n * 외부 사이트가 cms-renderer 에 주입할 RootTale 디자인 토큰.\n * admin /design (mysite.roottale.com) 에서 저장한 값과 동일한 shape.\n * 미설정 필드는 응답에서 누락 → renderer 의 CSS fallback 이 사용된다.\n */\nexport interface RootTaleTheme {\n colors?: {\n primary?: string;\n primaryForeground?: string;\n foreground?: string;\n background?: string;\n secondary?: string;\n secondaryForeground?: string;\n accent?: string;\n accentForeground?: string;\n muted?: string;\n mutedForeground?: string;\n destructive?: string;\n destructiveForeground?: string;\n border?: string;\n input?: string;\n ring?: string;\n };\n fonts?: {\n body?: string;\n display?: string;\n };\n radius?: {\n sm?: string;\n md?: string;\n lg?: string;\n xl?: string;\n };\n}\n\nexport interface FetchThemeOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n}\n\n/**\n * tenant 의 RootTale 디자인 토큰을 가져온다. admin /design 의 마지막 저장값\n * (없으면 빈 객체) — renderer 가 이 값을 CSS 변수로 주입한다.\n *\n * 자동 fetch (renderer 가 theme prop 없을 때) 와 명시 호출 모두 지원.\n * 네트워크/서버 에러는 throw — 호출자가 fallback 처리.\n */\nexport async function fetchTheme(options: FetchThemeOptions): Promise<RootTaleTheme> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/theme${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: authHeaders(options.apiKey),\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n colors?: RootTaleTheme[\"colors\"];\n fonts?: RootTaleTheme[\"fonts\"];\n radius?: RootTaleTheme[\"radius\"];\n };\n const theme: RootTaleTheme = {};\n if (json.colors) theme.colors = json.colors;\n if (json.fonts) theme.fonts = json.fonts;\n if (json.radius) theme.radius = json.radius;\n return theme;\n}\n\nexport type AnalyticsTagProvider = \"ga4\" | \"clarity\" | \"meta_pixel\" | \"naver\";\n\n/** analytics-runtime `TagConfig` 와 구조 호환 — `buildManifest` 입력. */\nexport interface AnalyticsTagConfig {\n provider: AnalyticsTagProvider;\n id: string;\n enabled: boolean;\n}\n\nexport interface SiteAnalyticsTags {\n siteId: string;\n tags: AnalyticsTagConfig[];\n}\n\nexport interface FetchAnalyticsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n}\n\n/**\n * ADR-0038 plane ① — 외부 사이트가 `<head>` 에 주입할 런타임 태그(GA4 measurement\n * id / Clarity project id 등, 비밀 아님). RootTale 관리용 default + 테넌트 고객용\n * 설정을 머지한 결과를 반환한다. analytics-runtime `buildManifest` 입력으로 쓰고,\n * 동의는 항상 fail-closed(런타임이 처리)이므로 키 노출 위험 없음(public id).\n */\nexport async function fetchAnalyticsConfig(\n options: FetchAnalyticsOptions,\n): Promise<SiteAnalyticsTags> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/analytics${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: authHeaders(options.apiKey),\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n site_id: string;\n tags?: AnalyticsTagConfig[];\n };\n return { siteId: json.site_id, tags: json.tags ?? [] };\n}\n\n\nexport async function fetchPosts(options: FetchPostsOptions): Promise<CmsPostContentPage> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.limit !== undefined) params.set(\"limit\", String(options.limit));\n if (options.cursor) params.set(\"cursor\", options.cursor);\n if (options.type) params.set(\"type\", options.type);\n if (options.author) params.set(\"author\", options.author);\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/posts${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: authHeaders(options.apiKey),\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n items: CmsPostWire[];\n has_more: boolean;\n next_cursor: string | null;\n };\n return {\n items: json.items.map(fromWire),\n hasMore: json.has_more,\n nextCursor: json.next_cursor,\n };\n}\n\nexport async function fetchPost(options: FetchPostOptions): Promise<CmsPostContent | null> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/posts/${encodeURIComponent(options.slugOrId)}${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: authHeaders(options.apiKey),\n signal: options.signal,\n });\n\n if (response.status === 404) return null;\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as CmsPostWire;\n return fromWire(json);\n}\n\ntype CmsPostWire = {\n id: string;\n tenant_id: string;\n site_id: string;\n type: CmsPostType;\n /** ADR-0060 Amendment 1 — 명시 섹션 key. 구 서버는 미포함 — optional. */\n collection_key?: string | null;\n slug: string;\n title: string;\n excerpt: string | null;\n featured_media_id: string | null;\n /** featured 이미지 delivery URL. 구 서버는 미포함 — backward-compat optional. */\n featured_media_url?: string | null;\n author_id: string | null;\n author_name?: string | null;\n author_image_url?: string | null;\n author_bio?: string | null;\n body_json: Record<string, unknown>;\n status: \"published\";\n meta_json: Record<string, unknown>;\n /** ADR-0041 포맷 필드. 구 서버는 미포함 — backward-compat optional. */\n fields?: Record<string, unknown> | null;\n /** 0.2.0 이전 서버는 미포함 — backward-compat 차원에서 optional. */\n terms?: CmsTermRef[];\n /** ADR-0065 — 누적 조회수. 사이트가 공개 조회수를 켠 경우에만 포함. */\n view_count?: number;\n published_at: string;\n created_at: string;\n updated_at: string;\n};\n\nfunction fromWire(wire: CmsPostWire): CmsPostContent {\n return {\n id: wire.id,\n tenantId: wire.tenant_id,\n siteId: wire.site_id,\n type: wire.type,\n collectionKey: wire.collection_key ?? null,\n slug: wire.slug,\n title: wire.title,\n excerpt: wire.excerpt,\n featuredMediaId: wire.featured_media_id,\n featuredImageUrl: wire.featured_media_url ?? null,\n authorId: wire.author_id,\n authorName: wire.author_name ?? null,\n authorImageUrl: wire.author_image_url ?? null,\n authorBio: wire.author_bio ?? null,\n bodyJson: wire.body_json,\n status: wire.status,\n metaJson: wire.meta_json,\n fields: wire.fields ?? null,\n terms: wire.terms ?? [],\n ...(typeof wire.view_count === \"number\" ? { viewCount: wire.view_count } : {}),\n publishedAt: wire.published_at,\n createdAt: wire.created_at,\n updatedAt: wire.updated_at,\n };\n}\n","import { DEFAULT_BASE_URL, assertServer, authHeaders, toApiError } from \"./server-base\";\n\n/**\n * 블로그 표시 설정 — admin /design 의 \"블로그 표시\" 에서 저장한 사이트 기본값.\n * renderer 가 prop 미지정 시 이 값을 기본으로 적용 (글별 override 는 post 의\n * metaJson.display 가 우선).\n */\n/**\n * 글 하단 고정 CTA — admin 블로그 설정에서 활성화 시 모든 글 본문 끝에 그려진다.\n * null(미설정/비활성)이면 렌더러는 CTA 블록을 생략. buttonLabel/buttonHref 는\n * 객체가 존재하면 항상 non-empty(서버가 둘 다 있을 때만 객체를 내려줌).\n */\nexport interface RootTaleBlogPostCta {\n title: string | null;\n description: string | null;\n buttonLabel: string;\n buttonHref: string;\n}\n\n/**\n * 사이트 레벨 SEO 기본값 — 외부 사이트 `<head>`(설명·로고·파비콘)와 글별 OG\n * 미설정 시 공유 썸네일 폴백(defaultOgImageUrl)에 쓴다. 모두 nullable.\n */\nexport interface RootTaleSiteProfile {\n siteDescription: string | null;\n logoUrl: string | null;\n faviconUrl: string | null;\n /** 글별 og:image 미설정 시 폴백(1200×630 권장). */\n defaultOgImageUrl: string | null;\n}\n\n/**\n * 사이트맵 생성 정책 (admin 설정) — `createSitemapIndex` 가 소비해 무엇을 내보낼지\n * 결정한다. blanche식 인덱스 분리(static/blog/categories[/authors]) 위에 이미지·\n * 다국어 토글을 얹는다.\n */\nexport interface RootTaleSitemapSettings {\n /** 블로그 글 entry 에 대표 이미지(featuredImageUrl)를 `<image:image>` 로 포함. */\n blogImages: boolean;\n /** 다국어 hreflang locales (BCP-47). 빈 배열 = 단일 로케일(alternates 미emit). */\n locales: string[];\n /** authors 하위 사이트맵 포함 (작가 아카이브 라우트 필요). */\n authors: boolean;\n}\n\nexport interface RootTaleBlogSettings {\n showTableOfContents: boolean;\n showAuthor: boolean;\n showDate: boolean;\n showAuthorCard: boolean;\n /** 목차 상단 제목. null = renderer 기본(\"목차\"). */\n tocTitle: string | null;\n /** 작성자 카드 상단 라벨. null = renderer 기본(\"작성자\"). */\n authorCardEyebrow: string | null;\n /** 작성자 카드 설명. null = 설명 없음. */\n authorCardDescription: string | null;\n /** 작성자 프로필 이름. null = 글 작성자 이름 사용. */\n authorProfileName: string | null;\n /** 작성자 프로필 bio. null = 설명 없음. */\n authorProfileBio: string | null;\n /** 작성자 프로필 이미지 URL. null = 이니셜 아바타 사용. */\n authorProfileImageUrl: string | null;\n /** 작성자 프로필 이미지 모서리. */\n authorProfileImageRadius: \"circle\" | \"rounded\" | \"square\";\n /** 작성자 프로필 이미지 object-position x (%). */\n authorProfileImagePositionX: number;\n /** 작성자 프로필 이미지 object-position y (%). */\n authorProfileImagePositionY: number;\n /** 사이트 레벨 SEO 기본값(설명·로고·파비콘·기본 OG). */\n siteProfile: RootTaleSiteProfile;\n /** 글 하단 고정 CTA. null = 미설정/비활성. */\n postCta: RootTaleBlogPostCta | null;\n /**\n * 사이트맵 생성 정책 (admin 설정). createSitemapIndex 가 소비. optional —\n * 구버전 응답/직접 구성한 객체와의 하위호환(소비 시 기본값으로 fail-soft).\n */\n sitemap?: RootTaleSitemapSettings;\n}\n\nexport interface FetchBlogSettingsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n}\n\n/**\n * tenant 의 블로그 표시 기본값을 가져온다. 네트워크/서버 에러는 throw —\n * 호출자(renderer)가 안전 기본값으로 fallback.\n */\nexport async function fetchBlogSettings(\n options: FetchBlogSettingsOptions,\n): Promise<RootTaleBlogSettings> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/blog-settings${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: authHeaders(options.apiKey),\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n show_table_of_contents?: boolean;\n show_author?: boolean;\n show_date?: boolean;\n show_author_card?: boolean;\n toc_title?: string | null;\n author_card_eyebrow?: string | null;\n author_card_description?: string | null;\n author_profile_name?: string | null;\n author_profile_bio?: string | null;\n author_profile_image_url?: string | null;\n author_profile_image_radius?: string | null;\n author_profile_image_position_x?: number;\n author_profile_image_position_y?: number;\n site_profile?: {\n site_description?: string | null;\n logo_url?: string | null;\n favicon_url?: string | null;\n default_og_image_url?: string | null;\n } | null;\n post_cta?: {\n title?: string | null;\n description?: string | null;\n button_label?: string;\n button_href?: string;\n } | null;\n sitemap?: {\n blog_images?: boolean;\n locales?: string[];\n authors?: boolean;\n } | null;\n };\n const radius =\n json.author_profile_image_radius === \"rounded\" ||\n json.author_profile_image_radius === \"square\"\n ? json.author_profile_image_radius\n : \"circle\";\n const percent = (x: unknown): number =>\n typeof x === \"number\" && Number.isFinite(x)\n ? Math.max(0, Math.min(100, Math.round(x)))\n : 50;\n const nonEmpty = (x: unknown): string | null =>\n typeof x === \"string\" && x.length > 0 ? x : null;\n return {\n showTableOfContents: json.show_table_of_contents === true,\n showAuthor: json.show_author !== false,\n showDate: json.show_date !== false,\n showAuthorCard: json.show_author_card !== false,\n tocTitle:\n typeof json.toc_title === \"string\" && json.toc_title.length > 0\n ? json.toc_title\n : null,\n authorCardEyebrow:\n typeof json.author_card_eyebrow === \"string\" &&\n json.author_card_eyebrow.length > 0\n ? json.author_card_eyebrow\n : null,\n authorCardDescription:\n typeof json.author_card_description === \"string\" &&\n json.author_card_description.length > 0\n ? json.author_card_description\n : null,\n authorProfileName:\n typeof json.author_profile_name === \"string\" &&\n json.author_profile_name.length > 0\n ? json.author_profile_name\n : null,\n authorProfileBio:\n typeof json.author_profile_bio === \"string\" &&\n json.author_profile_bio.length > 0\n ? json.author_profile_bio\n : null,\n authorProfileImageUrl:\n typeof json.author_profile_image_url === \"string\" &&\n json.author_profile_image_url.length > 0\n ? json.author_profile_image_url\n : null,\n authorProfileImageRadius: radius,\n authorProfileImagePositionX: percent(json.author_profile_image_position_x),\n authorProfileImagePositionY: percent(json.author_profile_image_position_y),\n siteProfile: {\n siteDescription: nonEmpty(json.site_profile?.site_description),\n logoUrl: nonEmpty(json.site_profile?.logo_url),\n faviconUrl: nonEmpty(json.site_profile?.favicon_url),\n defaultOgImageUrl: nonEmpty(json.site_profile?.default_og_image_url),\n },\n postCta:\n json.post_cta &&\n typeof json.post_cta.button_label === \"string\" &&\n json.post_cta.button_label.length > 0 &&\n typeof json.post_cta.button_href === \"string\" &&\n json.post_cta.button_href.length > 0\n ? {\n title:\n typeof json.post_cta.title === \"string\" &&\n json.post_cta.title.length > 0\n ? json.post_cta.title\n : null,\n description:\n typeof json.post_cta.description === \"string\" &&\n json.post_cta.description.length > 0\n ? json.post_cta.description\n : null,\n buttonLabel: json.post_cta.button_label,\n buttonHref: json.post_cta.button_href,\n }\n : null,\n sitemap: {\n blogImages: json.sitemap?.blog_images !== false,\n locales: Array.isArray(json.sitemap?.locales)\n ? json.sitemap.locales.filter(\n (s): s is string => typeof s === \"string\" && s.length > 0,\n )\n : [],\n authors: json.sitemap?.authors === true,\n },\n };\n}\n\n// ── 콘텐츠 Collections (ADR-0060) ────────────────────────────────────────────\n\n/**\n * 콘텐츠 스트림 1개 (camelCase). cms-core `RouteCollection` 과 구조 호환 —\n * `createSitemap`/`createFeedRoute` 의 `collections` 에 그대로 넘길 수 있다.\n */\nexport interface CmsRouteCollection {\n key: string;\n label: string;\n basePath: string;\n layout: string;\n categories: string[];\n feed: boolean;\n archives: boolean;\n og: boolean;\n requireSubcategory: boolean;\n pinning: boolean;\n icon: string | null;\n}\n\nexport interface FetchCollectionsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n}\n\n/**\n * 사이트의 콘텐츠 Collections (공지/블로그 등 스트림 분리)를 가져온다. 미설정이면\n * 빈 배열(레거시 단일 /blog). 네트워크/서버 에러는 throw — 호출자가 빈 배열로\n * fail-soft 하도록 한다(렌더가 collections 때문에 죽지 않는 것이 계약).\n */\nexport async function fetchCollections(\n options: FetchCollectionsOptions,\n): Promise<CmsRouteCollection[]> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/collections${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: authHeaders(options.apiKey),\n signal: options.signal,\n });\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n collections?: Array<Record<string, unknown>>;\n };\n const items = Array.isArray(json.collections) ? json.collections : [];\n return items.flatMap((o) => {\n const key = typeof o.key === \"string\" ? o.key : \"\";\n const basePath = typeof o.base_path === \"string\" ? o.base_path : \"\";\n if (!key || !basePath) return [];\n return [\n {\n key,\n basePath,\n label: typeof o.label === \"string\" ? o.label : key,\n layout: typeof o.layout === \"string\" ? o.layout : \"grid\",\n categories: Array.isArray(o.categories)\n ? o.categories.filter((s): s is string => typeof s === \"string\")\n : [],\n feed: o.feed === true,\n archives: o.archives === true,\n og: o.og === true,\n requireSubcategory: o.require_subcategory === true,\n pinning: o.pinning === true,\n icon: typeof o.icon === \"string\" ? o.icon : null,\n },\n ];\n });\n}\n\n/**\n * fetch 실패/미설정 시 안전 기본값 — 빈 배열(레거시 단일 /blog). 사이트가\n * `fetchCollections().catch(() => fetchCollectionsSafeDefault())` 로 쓴다.\n */\nexport const fetchCollectionsSafeDefault: CmsRouteCollection[] = [];\n\n// ── 작가 아카이브 (blanche식 /blog/author/{slug}) ────────────────────────────\n\n/** 작가 1명 (camelCase) — author_slug 가 있고 발행 글이 1건 이상인 작가만. */\nexport interface RootTaleAuthor {\n /** 아카이브 URL slug (/blog/author/{slug}). */\n slug: string;\n /** 표시 이름 (author_display_name > user.name 폴백). */\n name: string;\n imageUrl: string | null;\n bio: string | null;\n /** 발행 글(type=post) 수 (≥1). */\n postCount: number;\n /** 작가의 최근 글 수정일 (사이트맵 lastmod). */\n lastUpdatedAt: string | null;\n}\n\nexport interface FetchAuthorsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n}\n\n/**\n * 사이트의 작가 목록(작가 아카이브·사이트맵 파생용)을 가져온다. author_slug 가 있고\n * 발행 글이 1건 이상인 작가만. 네트워크/서버 에러는 throw — 호출자(라우트/사이트맵\n * 팩토리)가 빈 배열로 fail-soft 한다.\n */\nexport async function fetchAuthors(\n options: FetchAuthorsOptions,\n): Promise<RootTaleAuthor[]> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/authors${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: authHeaders(options.apiKey),\n signal: options.signal,\n });\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n authors?: Array<Record<string, unknown>>;\n };\n const items = Array.isArray(json.authors) ? json.authors : [];\n return items.flatMap((o) => {\n const slug = typeof o.slug === \"string\" ? o.slug : \"\";\n const name = typeof o.name === \"string\" ? o.name : \"\";\n if (!slug || !name) return [];\n return [\n {\n slug,\n name,\n imageUrl: typeof o.image_url === \"string\" ? o.image_url : null,\n bio: typeof o.bio === \"string\" ? o.bio : null,\n postCount: typeof o.post_count === \"number\" ? o.post_count : 0,\n lastUpdatedAt:\n typeof o.last_updated_at === \"string\" ? o.last_updated_at : null,\n },\n ];\n });\n}\n\n/** fetch 실패/미설정 시 사용할 안전 기본값 (기존 렌더러 동작). */\nexport const DEFAULT_BLOG_SETTINGS: RootTaleBlogSettings = {\n showTableOfContents: false,\n showAuthor: true,\n showDate: true,\n showAuthorCard: true,\n tocTitle: null,\n authorCardEyebrow: null,\n authorCardDescription: null,\n authorProfileName: null,\n authorProfileBio: null,\n authorProfileImageUrl: null,\n authorProfileImageRadius: \"circle\",\n authorProfileImagePositionX: 50,\n authorProfileImagePositionY: 50,\n siteProfile: {\n siteDescription: null,\n logoUrl: null,\n faviconUrl: null,\n defaultOgImageUrl: null,\n },\n postCta: null,\n sitemap: {\n blogImages: true,\n locales: [],\n authors: false,\n },\n};\n\n\nexport interface ResolvedPostDisplay {\n showTableOfContents: boolean;\n showAuthor: boolean;\n showDate: boolean;\n showAuthorCard: boolean;\n tocTitle: string;\n authorCardEyebrow: string;\n authorCardDescription: string | null;\n authorProfileName: string | null;\n authorProfileBio: string | null;\n authorProfileImageUrl: string | null;\n authorProfileImageRadius: \"circle\" | \"rounded\" | \"square\";\n authorProfileImagePositionX: number;\n authorProfileImagePositionY: number;\n}\n\n/**\n * 글 1건의 실제 표시 여부 결정. 우선순위:\n * 명시 prop > 글별 override(post.metaJson.display) > 사이트 기본값(settings).\n * tocTitle: 명시 prop > settings.tocTitle > \"목차\".\n */\nexport function resolvePostDisplay(\n post: { metaJson?: Record<string, unknown> | null },\n settings: RootTaleBlogSettings,\n explicit?: { showTableOfContents?: boolean; tableOfContentsTitle?: string },\n): ResolvedPostDisplay {\n const display =\n post.metaJson && typeof post.metaJson === \"object\"\n ? ((post.metaJson as Record<string, unknown>).display as\n | Record<string, unknown>\n | undefined)\n : undefined;\n const ov = (key: string): boolean | undefined =>\n display && typeof display[key] === \"boolean\"\n ? (display[key] as boolean)\n : undefined;\n return {\n showTableOfContents:\n explicit?.showTableOfContents ?? ov(\"toc\") ?? settings.showTableOfContents,\n showAuthor: ov(\"author\") ?? settings.showAuthor,\n showDate: ov(\"date\") ?? settings.showDate,\n showAuthorCard: settings.showAuthorCard,\n tocTitle: explicit?.tableOfContentsTitle ?? settings.tocTitle ?? \"목차\",\n authorCardEyebrow: settings.authorCardEyebrow ?? \"작성자\",\n authorCardDescription: settings.authorCardDescription,\n authorProfileName: settings.authorProfileName,\n authorProfileBio: settings.authorProfileBio,\n authorProfileImageUrl: settings.authorProfileImageUrl,\n authorProfileImageRadius: settings.authorProfileImageRadius,\n authorProfileImagePositionX: settings.authorProfileImagePositionX,\n authorProfileImagePositionY: settings.authorProfileImagePositionY,\n };\n}\n","import { DEFAULT_BASE_URL, assertServer, authHeaders, toApiError } from \"./server-base\";\n\n// ── 비즈니스 프로필 (로컬 SEO) ───────────────────────────────────────────────\n\n/** Schema.org LocalBusiness 하위 타입 (admin \"운영 > 비즈니스 프로필\" 의 업종). */\nexport type BusinessType =\n | \"LocalBusiness\"\n | \"ProfessionalService\"\n | \"AccountingService\"\n | \"LegalService\"\n | \"MedicalClinic\"\n | \"Dentist\"\n | \"RealEstateAgent\"\n | \"Restaurant\"\n | \"BeautySalon\";\n\n/** Schema.org openingHours 요일 약어. */\nexport type BusinessDayCode = \"Mo\" | \"Tu\" | \"We\" | \"Th\" | \"Fr\" | \"Sa\" | \"Su\";\n\nexport interface BusinessOpeningHours {\n days: BusinessDayCode[];\n /** HH:MM (24시간). */\n opens: string;\n /** HH:MM (24시간). */\n closes: string;\n}\n\n/**\n * 비즈니스 프로필 — admin \"운영 > 비즈니스 프로필\" 에서 저장한 사업장 정보.\n * `localBusinessSchema(profile, { url })` 로 LocalBusiness JSON-LD 를 만들어\n * layout 에 1회 렌더한다 (로컬 SEO).\n */\nexport interface RootTaleBusinessProfile {\n /** 사업장 이름 (간판명). */\n name: string;\n /** 사업자등록 상호 — 간판명과 다를 때만 설정. */\n legalName: string | null;\n businessType: BusinessType;\n telephone: string | null;\n email: string | null;\n address: {\n streetAddress: string;\n addressLocality: string;\n addressRegion: string | null;\n postalCode: string | null;\n } | null;\n geo: { latitude: number; longitude: number } | null;\n openingHours: BusinessOpeningHours[];\n /** 가격대 표기 (예: \"₩₩\"). */\n priceRange: string | null;\n /** 서비스 지역 (예: \"서울 강남구\"). */\n areaServed: string[];\n /** 외부 프로필 URL — JSON-LD sameAs 로 들어가 동일 사업장임을 연결. */\n profiles: {\n naverPlace: string | null;\n googleBusiness: string | null;\n kakaoChannel: string | null;\n instagram: string | null;\n naverBlog: string | null;\n };\n}\n\nexport interface FetchBusinessProfileOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n}\n\ntype BusinessProfileWire = {\n configured?: boolean;\n name?: string | null;\n legal_name?: string | null;\n business_type?: string | null;\n telephone?: string | null;\n email?: string | null;\n address?: {\n street_address?: string;\n address_locality?: string;\n address_region?: string | null;\n postal_code?: string | null;\n } | null;\n geo?: { latitude?: number; longitude?: number } | null;\n opening_hours?: {\n days?: BusinessDayCode[];\n opens?: string;\n closes?: string;\n }[];\n price_range?: string | null;\n area_served?: string[];\n profiles?: {\n naver_place?: string | null;\n google_business?: string | null;\n kakao_channel?: string | null;\n instagram?: string | null;\n naver_blog?: string | null;\n } | null;\n};\n\n/** wire(snake_case) → RootTaleBusinessProfile. 미설정이면 null (export for tests). */\nexport function businessProfileFromWire(\n json: BusinessProfileWire,\n): RootTaleBusinessProfile | null {\n // configured=false 또는 이름 없음 = 미설정 — 호출자는 JSON-LD 렌더를 건너뛴다.\n if (json.configured === false) return null;\n if (typeof json.name !== \"string\" || json.name.length === 0) return null;\n const address =\n json.address &&\n typeof json.address.street_address === \"string\" &&\n typeof json.address.address_locality === \"string\"\n ? {\n streetAddress: json.address.street_address,\n addressLocality: json.address.address_locality,\n addressRegion: json.address.address_region ?? null,\n postalCode: json.address.postal_code ?? null,\n }\n : null;\n const geo =\n json.geo &&\n typeof json.geo.latitude === \"number\" &&\n typeof json.geo.longitude === \"number\"\n ? { latitude: json.geo.latitude, longitude: json.geo.longitude }\n : null;\n const openingHours: BusinessOpeningHours[] = [];\n for (const row of json.opening_hours ?? []) {\n if (\n Array.isArray(row.days) &&\n row.days.length > 0 &&\n typeof row.opens === \"string\" &&\n typeof row.closes === \"string\"\n ) {\n openingHours.push({ days: row.days, opens: row.opens, closes: row.closes });\n }\n }\n return {\n name: json.name,\n legalName: json.legal_name ?? null,\n businessType:\n typeof json.business_type === \"string\" && json.business_type.length > 0\n ? (json.business_type as BusinessType)\n : \"LocalBusiness\",\n telephone: json.telephone ?? null,\n email: json.email ?? null,\n address,\n geo,\n openingHours,\n priceRange: json.price_range ?? null,\n areaServed: json.area_served ?? [],\n profiles: {\n naverPlace: json.profiles?.naver_place ?? null,\n googleBusiness: json.profiles?.google_business ?? null,\n kakaoChannel: json.profiles?.kakao_channel ?? null,\n instagram: json.profiles?.instagram ?? null,\n naverBlog: json.profiles?.naver_blog ?? null,\n },\n };\n}\n\n/**\n * tenant 의 비즈니스 프로필을 가져온다. 미설정이면 null — 호출자는 null 일 때\n * JSON-LD 렌더를 건너뛰면 된다. 네트워크/서버 에러는 throw — 호출자가\n * `.catch(() => null)` 등으로 fallback.\n */\nexport async function fetchBusinessProfile(\n options: FetchBusinessProfileOptions,\n): Promise<RootTaleBusinessProfile | null> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/business-profile${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: authHeaders(options.apiKey),\n signal: options.signal,\n });\n\n // 구 서버(라우트 미배포)는 404 — 미설정과 동일하게 null.\n if (response.status === 404) return null;\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as BusinessProfileWire;\n return businessProfileFromWire(json);\n}\n\n","import {\n type CmsPostType,\n DEFAULT_BASE_URL,\n assertServer,\n authHeaders,\n toApiError,\n} from \"./server-base\";\n\n// ── 공개 검색 (WP `?s=` 패리티) ────────────────────────────────────────────\n\nexport interface SearchPostsOptions {\n apiKey: string;\n baseUrl?: string;\n /** 검색 키워드 (title·excerpt·본문 텍스트 매칭). */\n query: string;\n /** 결과 수 1-50, default 10. */\n limit?: number;\n /** 기본 = `post` 만. page 포함 검색은 명시. */\n type?: CmsPostType;\n siteId?: string;\n signal?: AbortSignal;\n}\n\n/** 검색 결과 카드용 슬림 hit — body 미포함. */\nexport interface CmsSearchHit {\n id: string;\n type: CmsPostType;\n title: string;\n slug: string;\n excerpt: string | null;\n featuredImageUrl: string | null;\n /** ISO 8601. */\n publishedAt: string;\n}\n\n/**\n * published 글 키워드 검색 (사이트 내 검색). 최신 발행순, body 미포함 슬림\n * hit — 상세는 slug 로 `fetchPost`. 구 서버(라우트 미배포)는 404 — 빈 배열.\n */\nexport async function searchPosts(\n options: SearchPostsOptions,\n): Promise<CmsSearchHit[]> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n params.set(\"q\", options.query);\n if (options.limit !== undefined) params.set(\"limit\", String(options.limit));\n if (options.type) params.set(\"type\", options.type);\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const url = `${baseUrl}/v1/cms/public/search?${params.toString()}`;\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: authHeaders(options.apiKey),\n signal: options.signal,\n });\n\n if (response.status === 404) return [];\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n items: {\n id: string;\n type: CmsPostType;\n title: string;\n slug: string;\n excerpt: string | null;\n featured_media_url: string | null;\n published_at: string;\n }[];\n };\n return json.items.map((w) => ({\n id: w.id,\n type: w.type,\n title: w.title,\n slug: w.slug,\n excerpt: w.excerpt,\n featuredImageUrl: w.featured_media_url ?? null,\n publishedAt: w.published_at,\n }));\n}\n\n// ── 네비게이션 메뉴 (WP 외모>메뉴 패리티) ──────────────────────────────────\n\n/** 메뉴 항목 트리 노드 (깊이 ≤ 2). */\nexport interface RootTaleMenuItem {\n id: string;\n label: string;\n /** 상대 경로(`/about`) 또는 절대 http(s) URL — 서버가 sanitize. */\n url: string;\n /** true 면 `target=\"_blank\" rel=\"noopener\"` 로 렌더할 것. */\n newTab?: boolean;\n children?: RootTaleMenuItem[];\n}\n\nexport interface RootTaleMenu {\n id: string;\n /** 사람용 이름 — \"헤더 메뉴\". */\n name: string;\n /** 위치 핸들 — \"primary\" | \"footer\" 등. */\n slug: string;\n items: RootTaleMenuItem[];\n updatedAt: string | null;\n}\n\nexport interface FetchMenuOptions {\n apiKey: string;\n baseUrl?: string;\n /** 위치 핸들 — \"primary\" | \"footer\" 등. */\n slug: string;\n siteId?: string;\n signal?: AbortSignal;\n}\n\nexport interface FetchMenusOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n}\n\ntype CmsMenuItemWire = {\n id: string;\n label: string;\n url: string;\n new_tab?: boolean;\n children?: CmsMenuItemWire[];\n};\n\ntype CmsMenuWire = {\n id: string;\n name: string;\n slug: string;\n items: CmsMenuItemWire[];\n updated_at: string | null;\n};\n\nfunction menuItemFromWire(w: CmsMenuItemWire): RootTaleMenuItem {\n const item: RootTaleMenuItem = { id: w.id, label: w.label, url: w.url };\n if (w.new_tab) item.newTab = true;\n if (w.children && w.children.length > 0) {\n item.children = w.children.map(menuItemFromWire);\n }\n return item;\n}\n\nfunction menuFromWire(w: CmsMenuWire): RootTaleMenu {\n return {\n id: w.id,\n name: w.name,\n slug: w.slug,\n items: (w.items ?? []).map(menuItemFromWire),\n updatedAt: w.updated_at ?? null,\n };\n}\n\n/**\n * 위치 핸들 slug 로 네비게이션 메뉴 1개를 가져온다. admin \"디자인 > 메뉴\" 의\n * 저장값. 미설정/미존재(404)는 null — 사이트는 자체 fallback 네비를 렌더한다.\n * 구 서버(라우트 미배포)도 404 → null 로 동일하게 fail-soft.\n */\nexport async function fetchMenu(\n options: FetchMenuOptions,\n): Promise<RootTaleMenu | null> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/menus/${encodeURIComponent(options.slug)}${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: authHeaders(options.apiKey),\n signal: options.signal,\n });\n\n if (response.status === 404) return null;\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as CmsMenuWire;\n return menuFromWire(json);\n}\n\n/** 사이트의 네비게이션 메뉴 전체. 구 서버(라우트 미배포)는 404 — 빈 배열. */\nexport async function fetchMenus(\n options: FetchMenusOptions,\n): Promise<RootTaleMenu[]> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/menus${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: authHeaders(options.apiKey),\n signal: options.signal,\n });\n\n if (response.status === 404) return [];\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as { items: CmsMenuWire[] };\n return (json.items ?? []).map(menuFromWire);\n}\n\n// ── 커스텀 리다이렉트 (WP RankMath Redirections 패리티) ─────────────────────\n\nexport interface RootTaleRedirect {\n id: string;\n /** 정규화된 출발 경로 (앞 슬래시, 쿼리 제외). 예: `/old-event`. */\n fromPath: string;\n /** 도착지 — 사이트 내부 경로(`/promo`) 또는 절대 URL. */\n toTarget: string;\n /** 301 영구 / 302 임시. */\n statusCode: 301 | 302;\n}\n\nexport interface FetchRedirectsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n}\n\ntype CmsRedirectWire = {\n id: string;\n from_path: string;\n to_target: string;\n status_code: number;\n};\n\n/**\n * 사이트의 활성 커스텀 리다이렉트 규칙 전체. 고객 사이트 middleware\n * (`createRedirectMiddleware`)가 요청 경로 매칭에 사용한다. 구 서버(라우트\n * 미배포)는 404 — 빈 배열로 fail-soft (트래픽을 막지 않음).\n */\nexport async function fetchRedirects(\n options: FetchRedirectsOptions,\n): Promise<RootTaleRedirect[]> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/redirects${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: authHeaders(options.apiKey),\n signal: options.signal,\n });\n\n if (response.status === 404) return [];\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as { items: CmsRedirectWire[] };\n return (json.items ?? []).map((w) => ({\n id: w.id,\n fromPath: w.from_path,\n toTarget: w.to_target,\n statusCode: (w.status_code === 302 ? 302 : 301) as 301 | 302,\n }));\n}\n","// 프레임워크 무관 SEO 헬퍼 — RSS 2.0 피드 + Schema.org JSON-LD 빌더.\n// 외부 사이트(Next route handler / Astro endpoint / RSC)가 fetchPosts 결과로\n// /feed.xml 응답이나 <head> JSON-LD 를 만들 때 사용. 순수 함수(네트워크 없음).\n\nimport type { CmsPostContent, RootTaleBusinessProfile } from \"./server\";\n\n// ── RSS 2.0 ────────────────────────────────────────────────────────────────\n\nexport interface RssItemInput {\n title: string;\n /** 글 절대 URL. */\n link: string;\n /** 고유 식별자. 미지정 시 link 사용. */\n guid?: string;\n description?: string | null;\n /** 발행 시각. */\n pubDate?: string | Date | null;\n}\n\nexport interface RssFeedInput {\n /** 사이트/피드 제목. */\n title: string;\n /** 사이트 홈 URL. */\n link: string;\n /** 이 피드 자체의 URL (atom:self). */\n feedUrl: string;\n description?: string;\n /** 기본 \"ko-KR\". */\n language?: string;\n items: RssItemInput[];\n}\n\n/** RSS 2.0 XML 문자열 생성. */\nexport function generateRssXml(input: RssFeedInput): string {\n const lang = input.language ?? \"ko-KR\";\n const lastBuild =\n input.items.length > 0 ? toRfc822(input.items[0]!.pubDate) : \"\";\n const lines: string[] = [];\n lines.push('<?xml version=\"1.0\" encoding=\"UTF-8\"?>');\n lines.push(\n '<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">',\n );\n lines.push(\"<channel>\");\n lines.push(`<title>${escapeXml(input.title)}</title>`);\n lines.push(`<link>${escapeXml(input.link)}</link>`);\n lines.push(\n `<atom:link href=\"${escapeXml(input.feedUrl)}\" rel=\"self\" type=\"application/rss+xml\" />`,\n );\n lines.push(\n `<description>${escapeXml(input.description ?? input.title)}</description>`,\n );\n lines.push(`<language>${escapeXml(lang)}</language>`);\n if (lastBuild) lines.push(`<lastBuildDate>${lastBuild}</lastBuildDate>`);\n for (const item of input.items) {\n const guid = item.guid ?? item.link;\n lines.push(\"<item>\");\n lines.push(`<title>${escapeXml(item.title)}</title>`);\n lines.push(`<link>${escapeXml(item.link)}</link>`);\n lines.push(`<guid isPermaLink=\"false\">${escapeXml(guid)}</guid>`);\n if (item.description) {\n lines.push(`<description>${cdata(item.description)}</description>`);\n }\n const pub = toRfc822(item.pubDate);\n if (pub) lines.push(`<pubDate>${pub}</pubDate>`);\n lines.push(\"</item>\");\n }\n lines.push(\"</channel>\");\n lines.push(\"</rss>\");\n return lines.join(\"\\n\");\n}\n\n/** CmsPostContent[] → RSS item[]. postUrl 로 글 절대 URL 생성. */\nexport function rssItemsFromPosts(\n posts: CmsPostContent[],\n opts: { postUrl: (post: CmsPostContent) => string },\n): RssItemInput[] {\n return posts.map((p) => ({\n title: p.title,\n link: opts.postUrl(p),\n guid: opts.postUrl(p),\n description: p.excerpt,\n pubDate: p.publishedAt,\n }));\n}\n\nfunction toRfc822(value: string | Date | null | undefined): string {\n if (!value) return \"\";\n const d = value instanceof Date ? value : new Date(value);\n return Number.isNaN(d.getTime()) ? \"\" : d.toUTCString();\n}\n\n// ── Schema.org JSON-LD 빌더 ─────────────────────────────────────────────────\n\ntype JsonLd = Record<string, unknown>;\n\nexport interface ArticleSchemaInput {\n headline: string;\n url: string;\n description?: string | null;\n image?: string | null;\n datePublished?: string | Date | null;\n dateModified?: string | Date | null;\n authorName?: string | null;\n publisherName?: string;\n publisherLogo?: string;\n}\n\nexport function articleSchema(input: ArticleSchemaInput): JsonLd {\n const schema: JsonLd = {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Article\",\n headline: input.headline,\n mainEntityOfPage: { \"@type\": \"WebPage\", \"@id\": input.url },\n };\n if (input.description) schema.description = input.description;\n if (input.image) schema.image = input.image;\n const pub = toIso(input.datePublished);\n if (pub) schema.datePublished = pub;\n const mod = toIso(input.dateModified);\n if (mod) schema.dateModified = mod;\n if (input.authorName)\n schema.author = { \"@type\": \"Person\", name: input.authorName };\n if (input.publisherName) {\n schema.publisher = {\n \"@type\": \"Organization\",\n name: input.publisherName,\n ...(input.publisherLogo\n ? { logo: { \"@type\": \"ImageObject\", url: input.publisherLogo } }\n : {}),\n };\n }\n return schema;\n}\n\nexport function breadcrumbSchema(\n items: { name: string; url: string }[],\n): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"BreadcrumbList\",\n itemListElement: items.map((it, i) => ({\n \"@type\": \"ListItem\",\n position: i + 1,\n name: it.name,\n item: it.url,\n })),\n };\n}\n\nexport function organizationSchema(input: {\n name: string;\n url: string;\n logo?: string;\n sameAs?: string[];\n}): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Organization\",\n name: input.name,\n url: input.url,\n ...(input.logo ? { logo: input.logo } : {}),\n ...(input.sameAs && input.sameAs.length > 0 ? { sameAs: input.sameAs } : {}),\n };\n}\n\n// ── LocalBusiness (로컬 SEO) ────────────────────────────────────────────────\n\n/** Mo~Su 약어 → Schema.org dayOfWeek 전체 이름. */\nconst DAY_OF_WEEK: Record<string, string> = {\n Mo: \"Monday\",\n Tu: \"Tuesday\",\n We: \"Wednesday\",\n Th: \"Thursday\",\n Fr: \"Friday\",\n Sa: \"Saturday\",\n Su: \"Sunday\",\n};\n\n/**\n * `localBusinessSchema` 입력 — `fetchBusinessProfile` 의 반환값\n * (RootTaleBusinessProfile) 을 그대로 받도록 느슨하게 정의 (null/undefined 모두\n * 허용). 수동 구성도 가능.\n */\nexport interface LocalBusinessProfileInput {\n name: string;\n legalName?: string | null;\n /** Schema.org LocalBusiness 하위 타입. 미지정 시 \"LocalBusiness\". */\n businessType?: string | null;\n telephone?: string | null;\n email?: string | null;\n address?: {\n streetAddress: string;\n addressLocality: string;\n addressRegion?: string | null;\n postalCode?: string | null;\n } | null;\n geo?: { latitude: number; longitude: number } | null;\n openingHours?:\n | { days: string[]; opens: string; closes: string }[]\n | null;\n priceRange?: string | null;\n areaServed?: string[] | null;\n /** 외부 프로필 URL — 값들이 sameAs 로 들어간다 (네이버플레이스·구글 등). */\n profiles?: Record<string, string | null | undefined> | null;\n}\n\n/**\n * LocalBusiness JSON-LD — admin \"운영 > 비즈니스 프로필\" 의\n * `fetchBusinessProfile` 결과로 layout 에 1회 렌더한다 (로컬 SEO).\n *\n * - `@type` 은 `[businessType, \"LocalBusiness\"]` 중복 제거 배열.\n * - `profiles` 의 URL 들은 `sameAs` 로 — 검색엔진이 네이버플레이스/구글\n * 비즈니스 프로필과 동일 사업장임을 연결한다.\n */\nexport function localBusinessSchema(\n profile: LocalBusinessProfileInput | RootTaleBusinessProfile,\n opts: { url: string; image?: string | null },\n): JsonLd {\n const input = profile as LocalBusinessProfileInput;\n const types = Array.from(\n new Set([input.businessType || \"LocalBusiness\", \"LocalBusiness\"]),\n );\n const schema: JsonLd = {\n \"@context\": \"https://schema.org\",\n \"@type\": types,\n name: input.name,\n url: opts.url,\n };\n if (input.legalName) schema.legalName = input.legalName;\n if (opts.image) schema.image = opts.image;\n if (input.telephone) schema.telephone = input.telephone;\n if (input.email) schema.email = input.email;\n if (input.address) {\n schema.address = {\n \"@type\": \"PostalAddress\",\n streetAddress: input.address.streetAddress,\n addressLocality: input.address.addressLocality,\n ...(input.address.addressRegion\n ? { addressRegion: input.address.addressRegion }\n : {}),\n ...(input.address.postalCode\n ? { postalCode: input.address.postalCode }\n : {}),\n addressCountry: \"KR\",\n };\n }\n if (input.geo) {\n schema.geo = {\n \"@type\": \"GeoCoordinates\",\n latitude: input.geo.latitude,\n longitude: input.geo.longitude,\n };\n }\n const openingHours = (input.openingHours ?? []).filter(\n (row) => row.days.length > 0 && row.opens && row.closes,\n );\n if (openingHours.length > 0) {\n schema.openingHoursSpecification = openingHours.map((row) => ({\n \"@type\": \"OpeningHoursSpecification\",\n dayOfWeek: row.days.map((d) => DAY_OF_WEEK[d] ?? d),\n opens: row.opens,\n closes: row.closes,\n }));\n }\n if (input.priceRange) schema.priceRange = input.priceRange;\n if (input.areaServed && input.areaServed.length > 0) {\n schema.areaServed = input.areaServed;\n }\n const sameAs = Object.values(input.profiles ?? {}).filter(\n (u): u is string => typeof u === \"string\" && u.length > 0,\n );\n if (sameAs.length > 0) schema.sameAs = sameAs;\n return schema;\n}\n\nexport function websiteSchema(input: {\n name: string;\n url: string;\n searchUrl?: string;\n}): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"WebSite\",\n name: input.name,\n url: input.url,\n ...(input.searchUrl\n ? {\n potentialAction: {\n \"@type\": \"SearchAction\",\n target: `${input.searchUrl}{search_term_string}`,\n \"query-input\": \"required name=search_term_string\",\n },\n }\n : {}),\n };\n}\n\nexport function faqSchema(\n qa: { question: string; answer: string }[],\n): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"FAQPage\",\n mainEntity: qa.map((x) => ({\n \"@type\": \"Question\",\n name: x.question,\n acceptedAnswer: { \"@type\": \"Answer\", text: x.answer },\n })),\n };\n}\n\n/**\n * ADR-0065 — 글 조회수 식별자 meta 태그 HTML.\n *\n * 글 상세 페이지 `<head>` 에 렌더하면, RootTale 비콘이 이 값을 읽어 pageview 에\n * `post_id` 를 동봉한다 → URL/컬렉션 basePath 변경에 무관하게 조회수가 해당 글에\n * 정확히 집계된다. `post.id`(uuidv7) 를 그대로 넘기면 된다.\n *\n * Next App Router 는 `generateMetadata` 의 `other: { \"rt:content-id\": post.id }`\n * 로도 동일 태그를 낼 수 있다 — 그 경우 본 헬퍼 대신 metadata 를 쓴다.\n *\n * @example\n * // 프레임워크 무관(Astro/RSC set:html 등):\n * <head>{contentIdMeta(post.id)}</head>\n */\nexport function contentIdMeta(postId: string): string {\n return `<meta name=\"rt:content-id\" content=\"${escapeXml(postId)}\" />`;\n}\n\n/** JSON-LD 객체 → `<script type=\"application/ld+json\">` HTML 문자열 (XSS-safe). */\nexport function jsonLdScript(schema: JsonLd | JsonLd[]): string {\n // `</script>` 주입 방지를 위해 `<` 이스케이프.\n const json = JSON.stringify(schema).replace(/</g, \"\\\\u003c\");\n return `<script type=\"application/ld+json\">${json}</script>`;\n}\n\nfunction toIso(value: string | Date | null | undefined): string | null {\n if (!value) return null;\n const d = value instanceof Date ? value : new Date(value);\n return Number.isNaN(d.getTime()) ? null : d.toISOString();\n}\n\nfunction escapeXml(value: string): string {\n return value\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\");\n}\n\nfunction cdata(value: string): string {\n return `<![CDATA[${value.replace(/]]>/g, \"]]]]><![CDATA[>\")}]]>`;\n}\n","/**\n * `@roottale/cms-client` — 리드(문의) 접수 server-side 헬퍼.\n *\n * 외부 고객사 사이트가 *블로그 조회(`fetchPosts`)와 동일한 API key 하나* 로 리드도\n * 접수하기 위한 SSR-only 클라이언트. 브라우저가 아니라 사이트 서버(Server Action /\n * Route Handler)에서 호출해야 한다 — API key 는 절대 브라우저로 노출 금지\n * (ADR-0023 §5.1 #15). 키가 tenant 를 권위 식별하므로 `tenant_slug` 불필요.\n *\n * 사용 예 (Next.js server action):\n * ```ts\n * \"use server\";\n * import { submitInquiry } from \"@roottale/cms-client/server\";\n *\n * const result = await submitInquiry({\n * apiKey: process.env.ROOTTALE_API_KEY!, // fetchPosts 와 같은 키\n * fields: {\n * vertical: \"tax\",\n * contactName: name,\n * businessName: company,\n * email,\n * phone,\n * message,\n * privacyConsent: true,\n * },\n * });\n * if (result.ok) showSuccess(); // 그 키의 tenant /crm 에 도착\n * else showError(result.message);\n * ```\n *\n * intake API 는 302 `?ok=1` / `?err=*` 로 응답 — 본 헬퍼가 파싱해 구조화 결과 반환.\n */\n\nimport type { InquiryAttribution } from \"./attribution\";\n\nconst DEFAULT_BASE_URL = \"https://api.roottale.com\";\n\nexport type InquiryVertical = \"consulting\" | \"medical\" | \"tax\" | \"legal\";\n\n/** intake API 의 `err` 코드 → 한국어 사용자 메시지. */\nexport const INQUIRY_ERROR_MESSAGES: Record<string, string> = {\n consent_privacy: \"개인정보 수집·이용 동의가 필요합니다.\",\n consent_overseas: \"의료 분야는 국외이전 동의가 필요합니다.\",\n invalid_vertical: \"업종 분류가 유효하지 않습니다.\",\n missing_fields: \"필수 항목이 비어 있습니다.\",\n invalid_email: \"이메일 형식이 올바르지 않습니다.\",\n invalid_api_key: \"연동 인증에 실패했습니다. API 키를 확인해주세요.\",\n internal: \"전송에 실패했습니다. 잠시 후 다시 시도해주세요.\",\n};\n\nexport interface SubmitInquiryFields {\n vertical: InquiryVertical;\n contactName: string;\n businessName: string;\n email: string;\n phone: string;\n /** 자유 문의 내용 (선택). */\n message?: string;\n /** 상담 분야 라벨 등 부가 메타 (선택). */\n consultationField?: string;\n /** 현재 사이트 URL (선택). */\n currentSiteUrl?: string;\n /** 개인정보 수집·이용 동의 (필수 true). */\n privacyConsent: boolean;\n /** 의료 vertical 의 국외이전 동의 (medical 일 때 필수). */\n overseasTransferConsent?: boolean;\n /**\n * \"patient\"(고객사 방문자 문의, 기본) | \"sales\"(RootTale 자체 B2B 영업 리드).\n * sales 는 medical 국외이전 동의 게이트 면제.\n */\n leadKind?: \"patient\" | \"sales\";\n /**\n * 표준 필드 외에 폼이 받은 임의 항목(key→value)을 있는 그대로 전달. intake 가\n * 암호화 보관하고 CRM 상세에서 노출한다. 예약 키(표준 필드)·`_` 접두 키는 무시.\n * 주의: 개인정보가 담길 수 있으므로 폼의 동의 고지에 수집 항목을 반영할 것.\n */\n extras?: Record<string, string>;\n /**\n * 유입 first-touch 어트리뷰션 (ADR-0048 §2, 선택). 브라우저에서\n * `readAttribution()`(`@roottale/cms-client/attribution`)으로 읽어 Server\n * Action 인자로 넘긴 값을 그대로 전달하면 된다. CRM 에 \"이 문의가 어느\n * 글/링크/QR 에서 왔는지\"가 표시된다. null/undefined 면 미동반 제출.\n */\n attribution?: InquiryAttribution | null;\n}\n\n/** submitInquiry 가 전용으로 매핑하는 키 — extras 에서 충돌 시 무시. */\nconst RESERVED_INQUIRY_KEYS = new Set([\n \"vertical\",\n \"contact_name\",\n \"business_name\",\n \"email\",\n \"phone\",\n \"message\",\n \"consultation_field\",\n \"current_site_url\",\n \"privacy_consent\",\n \"overseas_transfer_consent\",\n \"lead_kind\",\n]);\n\nexport interface SubmitInquiryOptions {\n /** `rtlk_cust_*` API key — `fetchPosts` 와 동일한 키. Required. */\n apiKey: string;\n /** API base URL 오버라이드. 기본 `https://api.roottale.com`. */\n baseUrl?: string;\n fields: SubmitInquiryFields;\n signal?: AbortSignal;\n}\n\nexport type SubmitInquiryResult =\n | { ok: true }\n | { ok: false; code: string | null; message: string };\n\n/** `err` 코드를 사용자 메시지로 (미지의 코드는 generic). */\nexport function inquiryErrorMessage(code: string | null | undefined): string {\n return (code && INQUIRY_ERROR_MESSAGES[code]) || INQUIRY_ERROR_MESSAGES.internal;\n}\n\n/**\n * intake API 의 302 Location (`?ok=1` / `?err=*`) 을 구조화 결과로 파싱.\n * 외부 사이트가 ERROR_MESSAGES·파싱을 각자 재구현하지 않도록 공유.\n */\nexport function parseInquiryRedirect(\n location: string | null,\n base: string = DEFAULT_BASE_URL,\n): SubmitInquiryResult {\n if (!location) {\n return { ok: false, code: null, message: inquiryErrorMessage(null) };\n }\n let url: URL;\n try {\n url = new URL(location, base);\n } catch {\n return { ok: false, code: null, message: inquiryErrorMessage(null) };\n }\n if (url.searchParams.get(\"ok\") === \"1\") return { ok: true };\n const code = url.searchParams.get(\"err\");\n return { ok: false, code, message: inquiryErrorMessage(code) };\n}\n\nexport function formatKoreanPhoneNumber(input: string): string {\n let digits = input.replace(/\\D/g, \"\");\n if (digits.startsWith(\"82\") && digits.length >= 11) {\n digits = `0${digits.slice(2)}`;\n }\n if (digits.length === 11) {\n return `${digits.slice(0, 3)}-${digits.slice(3, 7)}-${digits.slice(7)}`;\n }\n if (digits.length === 10) {\n if (digits.startsWith(\"02\")) {\n return `${digits.slice(0, 2)}-${digits.slice(2, 6)}-${digits.slice(6)}`;\n }\n return `${digits.slice(0, 3)}-${digits.slice(3, 6)}-${digits.slice(6)}`;\n }\n if (digits.length === 8) {\n return `${digits.slice(0, 4)}-${digits.slice(4)}`;\n }\n return input.trim();\n}\n\n/**\n * 리드를 RootTale intake API 로 제출 (server-to-server, Bearer 인증).\n * 키의 tenant 로 라우팅되어 admin(mysite.roottale.com) /crm 에서 관리된다.\n */\nexport async function submitInquiry(\n options: SubmitInquiryOptions,\n): Promise<SubmitInquiryResult> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const f = options.fields;\n\n const body = new FormData();\n body.set(\"vertical\", f.vertical);\n body.set(\"contact_name\", f.contactName);\n body.set(\"business_name\", f.businessName);\n body.set(\"email\", f.email);\n body.set(\"phone\", formatKoreanPhoneNumber(f.phone));\n if (f.message) body.set(\"message\", f.message);\n if (f.consultationField) body.set(\"consultation_field\", f.consultationField);\n if (f.currentSiteUrl) body.set(\"current_site_url\", f.currentSiteUrl);\n // 사용자가 명시 동의한 경우에만 전달 (자동 부여 금지).\n if (f.privacyConsent) body.set(\"privacy_consent\", \"on\");\n if (f.overseasTransferConsent) body.set(\"overseas_transfer_consent\", \"on\");\n if (f.leadKind) body.set(\"lead_kind\", f.leadKind);\n // 유입 어트리뷰션 → intake `attr_*` 필드 (허용 키만, 빈 값 생략).\n if (f.attribution) {\n for (const [key, value] of Object.entries(f.attribution)) {\n if (typeof value !== \"string\" || !value.trim()) continue;\n body.set(`attr_${key}`, value.trim());\n }\n }\n // 표준 외 임의 항목 — 예약 키/`_`접두/빈 값/중복은 건너뛰고 원본 그대로 전달.\n if (f.extras) {\n for (const [rawKey, rawValue] of Object.entries(f.extras)) {\n const key = rawKey.trim();\n const value = String(rawValue ?? \"\").trim();\n if (!key || !value) continue;\n if (key.startsWith(\"_\") || RESERVED_INQUIRY_KEYS.has(key)) continue;\n if (key.startsWith(\"attr_\")) continue; // 어트리뷰션 채널과 충돌 방지\n\n if (body.has(key)) continue;\n body.set(key, value);\n }\n }\n\n let response: Response;\n try {\n response = await fetch(`${baseUrl}/v1/public/inquiries`, {\n method: \"POST\",\n headers: { authorization: `Bearer ${options.apiKey}` },\n body,\n // intake 는 302 로 결과 신호 — 자동 추적 금지하고 Location 직접 읽음.\n redirect: \"manual\",\n signal: options.signal,\n });\n } catch {\n return { ok: false, code: \"internal\", message: inquiryErrorMessage(\"internal\") };\n }\n\n // 잘못된/만료된 키 → 라우트가 401.\n if (response.status === 401) {\n return {\n ok: false,\n code: \"invalid_api_key\",\n message: inquiryErrorMessage(\"invalid_api_key\"),\n };\n }\n\n const location = response.headers.get(\"location\");\n if (location) return parseInquiryRedirect(location, baseUrl);\n\n // redirect 없이 성공 응답이면 ok 로 간주, 그 외엔 generic 실패.\n if (response.ok) return { ok: true };\n return { ok: false, code: \"internal\", message: inquiryErrorMessage(\"internal\") };\n}\n\nfunction assertServer(): void {\n if (typeof window !== \"undefined\") {\n throw new Error(\n \"@roottale/cms-client submitInquiry must not run in the browser. \" +\n \"Call it from a Server Action / Route Handler. \" +\n \"Shipping the API key to the browser breaks the security model (ADR-0023 §5.1 #15).\",\n );\n }\n}\n"],"mappings":"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","names":["DEFAULT_BASE_URL","assertServer"]}
|
|
1
|
+
{"version":3,"sources":["../src/server-base.ts","../src/server-posts-theme.ts","../src/server-blog.ts","../src/server-business.ts","../src/server-discovery.ts","../src/seo.ts","../src/inquiry.ts"],"sourcesContent":["/**\n * `@roottale/cms-client/server` — RootTale CMS Public API server-side fetch client.\n *\n * 외부 고객사 사이트가 RootTale CMS 콘텐츠를 fetch 하기 위한 SSR-only client.\n * Bearer (`rtlk_cust_*`) 인증, 브라우저 직접 호출 금지 (ADR-0023 §5.1 #15).\n *\n * 사용 예 (Next.js server component):\n * ```ts\n * import { fetchPosts } from \"@roottale/cms-client/server\";\n *\n * const posts = await fetchPosts({\n * apiKey: process.env.ROOTTALE_API_KEY!,\n * limit: 10,\n * });\n * ```\n *\n * 데이터 모델 = ADR-0034 `posts` (WP 7.0-aligned, type=post|page).\n * Phase 1 scope: list + get. WP import/export, draft preview = 별도 PR.\n */\n\nexport type CmsPostType = \"post\" | \"page\";\n\nexport type CmsTaxonomyKind = \"category\" | \"tag\" | \"custom\";\n\nexport interface CmsTermRef {\n id: string;\n taxonomy: CmsTaxonomyKind;\n slug: string;\n name: string;\n}\n\nexport interface CmsArchiveRelatedPost {\n id: string;\n title: string;\n slug: string;\n excerpt: string | null;\n collectionKey: string;\n}\n\nexport interface CmsArchiveRelatedGroup {\n collectionKey: string;\n label: string;\n items: CmsArchiveRelatedPost[];\n}\n\nexport interface CmsPostContent {\n /** UUIDv7. */\n id: string;\n /** Owning tenant id. */\n tenantId: string;\n /** Owning site id (ADR-0034 P4 dual-key). */\n siteId: string;\n /** WP post type. */\n type: CmsPostType;\n /**\n * ADR-0060 Amendment 1 — 글의 명시 섹션(content collection) key. 라우팅/스트림\n * 판정에 카테고리 파생보다 우선. 구 서버/미설정이면 null.\n */\n collectionKey: string | null;\n /** Lowercase kebab-case slug (1-120 chars). */\n slug: string;\n title: string;\n excerpt: string | null;\n /** Media id (resolved by renderer / consumer to URL). */\n featuredMediaId: string | null;\n /**\n * featured 이미지 delivery URL (CF Images variant, md 우선). 서버가 `featured_media_id`\n * → `media.variants_json` 를 해석해 제공. 대표 이미지 미설정 시 null.\n */\n featuredImageUrl: string | null;\n authorId: string | null;\n /** Public display name for the post author. Email is not exposed. */\n authorName: string | null;\n /** Public profile image URL for the post author. */\n authorImageUrl: string | null;\n /** Public biography text for the post author. */\n authorBio: string | null;\n /** Tiptap / Gutenberg-compatible Block JSON. Sanitization is renderer's responsibility. */\n bodyJson: Record<string, unknown>;\n /** Public API only exposes `published`. */\n status: \"published\";\n /** Per-post meta (SEO overrides, canonical, og, ...). */\n metaJson: Record<string, unknown>;\n /**\n * ADR-0041 — 포맷된 커스텀 필드(ACF) 값. 서버가 필드 정의(코드 ⊕ DB 그룹)로\n * `meta_json.acf` 를 포맷 — image/file/gallery 는 media variant URL 객체.\n * 해석된 그룹이 없거나 구 서버면 null.\n */\n fields: Record<string, unknown> | null;\n /**\n * Attached taxonomy terms (categories + tags + custom). Server 가 category →\n * tag → custom 순서로 정렬해 보내며, 각 taxonomy 내부는 name asc. attach 가 0 개면\n * 빈 배열. category 만 추출하려면 `terms.filter(t => t.taxonomy === \"category\")`.\n */\n terms: CmsTermRef[];\n archiveRelated: CmsArchiveRelatedGroup[];\n /**\n * ADR-0065 — 누적 조회수. 어드민에서 \"공개 조회수\"를 켠 사이트에서만 내려온다\n * (꺼져 있으면 `undefined`). \"조회 N\" 표시에 사용. 비콘이 채우므로 신규 글은 0.\n */\n viewCount?: number;\n /** ISO 8601. */\n publishedAt: string;\n /** ISO 8601. */\n createdAt: string;\n /** ISO 8601. */\n updatedAt: string;\n}\n\nexport interface CmsPostContentPage {\n items: CmsPostContent[];\n nextCursor: string | null;\n hasMore: boolean;\n}\n\nexport const DEFAULT_BASE_URL = \"https://api.roottale.com\";\n\n// fleet 가시성 — 자기 버전을 자가 보고 (build-time 주입, tsup define).\n// api-core 가 사이트별 check-in 으로 기록 → internal `GET /v1/internal/sites`.\ndeclare const __CMS_CLIENT_VERSION__: string | undefined;\nconst CLIENT_VERSION =\n typeof __CMS_CLIENT_VERSION__ === \"string\" ? __CMS_CLIENT_VERSION__ : \"dev\";\n\n/** 설치된 cms-client 버전 (build-time 주입) — fleet well-known 라우트 등에서 사용. */\nexport const CMS_CLIENT_VERSION = CLIENT_VERSION;\n\nexport function authHeaders(apiKey: string): Record<string, string> {\n return {\n authorization: `Bearer ${apiKey}`,\n \"x-roottale-client\": `cms-client/${CLIENT_VERSION}`,\n };\n}\n\n/**\n * ISR 시간 기반 fallback(W0-9). 지정 시 fetch 에 Next 의 `next: { revalidate }`\n * 를 붙여 webhook 유실 시에도 이 주기(초) 내에 캐시가 자가 치유된다.\n * **미지정(undefined)이면 아무 것도 붙이지 않아 현행 동작을 그대로 유지**(파괴적\n * 변경 없음) — Next 기본 캐시/정적화 정책을 따른다. `0` 은 항상 재검증(동적).\n */\nexport interface RevalidatableFetchOptions {\n /** Optional AbortSignal. */\n signal?: AbortSignal;\n /**\n * ISR revalidate 초. opt-in — 미지정 시 현행 캐시 동작 유지.\n * (Next.js `next: { revalidate }` 로 전달.)\n */\n revalidate?: number;\n}\n\n/** Next `fetch` 확장 — `next.revalidate`/`next.tags` 지원(런타임 무해, non-Next 무시). */\ntype NextFetchInit = RequestInit & {\n next?: { revalidate?: number; tags?: string[] };\n};\n\n/**\n * GET fetch 용 RequestInit 빌더 — Bearer 헤더 + signal + opt-in ISR revalidate\n * + opt-in cache tags. cms-client 의 모든 공개 콘텐츠/테마 fetch 가 공유해\n * `next.revalidate`/`next.tags` 지원을 일관되게 얻는다.\n *\n * `tags` 는 on-demand 무효화 경로(W0-4): 지정 시 fetch 캐시 엔트리에 tag 를\n * 붙여, 수신 route 의 `revalidateTag(tag)` 가 해당 엔트리(+그 fetch 를 쓴 route)를\n * 무효화한다. 미지정 = 붙이지 않음(현행 동작 유지).\n */\nexport function buildGetInit(\n apiKey: string,\n opts: { signal?: AbortSignal; revalidate?: number; tags?: string[] },\n): NextFetchInit {\n const init: NextFetchInit = {\n method: \"GET\",\n headers: authHeaders(apiKey),\n signal: opts.signal,\n };\n if (opts.revalidate !== undefined || (opts.tags && opts.tags.length > 0)) {\n init.next = {\n ...(opts.revalidate !== undefined ? { revalidate: opts.revalidate } : {}),\n ...(opts.tags && opts.tags.length > 0 ? { tags: opts.tags } : {}),\n };\n }\n return init;\n}\n\nexport interface FetchPostsOptions {\n /** `rtlk_cust_*` API key. Required. */\n apiKey: string;\n /** Override the API base URL. Defaults to `https://api.roottale.com`. */\n baseUrl?: string;\n /** Page size, 1-100, default 20. */\n limit?: number;\n /** Opaque pagination cursor returned from previous call. */\n cursor?: string;\n /** Filter by post type (`post` or `page`). Default = both. */\n type?: CmsPostType;\n /** 작가 slug 필터 — 작가 아카이브(/blog/author/{slug})용. 지정 시 발행 글(post)만. */\n author?: string;\n /** Override the auto-resolved site id (Phase 1 = 1 site/tenant, usually omitted). */\n siteId?: string;\n /** Optional AbortSignal. */\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n}\n\nexport interface FetchPostOptions {\n apiKey: string;\n baseUrl?: string;\n /** Slug (kebab-case) or UUIDv7 id. */\n slugOrId: string;\n /** Override the auto-resolved site id. */\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n}\n\n\nexport class CmsApiError extends Error {\n readonly status: number;\n readonly code: string;\n constructor(message: string, status: number, code: string) {\n super(message);\n this.name = \"CmsApiError\";\n this.status = status;\n this.code = code;\n }\n}\n\nexport async function toApiError(response: Response): Promise<CmsApiError> {\n let code = \"request_failed\";\n let message = `RootTale CMS API error: ${response.status}`;\n try {\n const body = (await response.json()) as { code?: string; message?: string };\n if (body.code) code = body.code;\n if (body.message) message = body.message;\n } catch {\n /* ignore body parse */\n }\n return new CmsApiError(message, response.status, code);\n}\n\nexport function assertServer(): void {\n if (typeof window !== \"undefined\") {\n throw new Error(\n \"@roottale/cms-client/server must not run in the browser. \" +\n \"Move this call to a Server Component, getServerSideProps, or a Route Handler. \" +\n \"Shipping API keys to the browser breaks the security model (ADR-0023 §5.1 #15).\",\n );\n }\n}\n\n// 프레임워크 무관 SEO 헬퍼 (RSS 2.0 + Schema.org JSON-LD) 재노출.\n","import {\n type CmsPostType,\n type CmsPostContent,\n type CmsPostContentPage,\n type CmsTermRef,\n type FetchPostOptions,\n type FetchPostsOptions,\n DEFAULT_BASE_URL,\n assertServer,\n buildGetInit,\n toApiError,\n} from \"./server-base\";\n\n/**\n * 외부 사이트가 cms-renderer 에 주입할 RootTale 디자인 토큰.\n * admin /design (mysite.roottale.com) 에서 저장한 값과 동일한 shape.\n * 미설정 필드는 응답에서 누락 → renderer 의 CSS fallback 이 사용된다.\n */\nexport interface RootTaleTheme {\n colors?: {\n primary?: string;\n primaryForeground?: string;\n foreground?: string;\n background?: string;\n secondary?: string;\n secondaryForeground?: string;\n accent?: string;\n accentForeground?: string;\n muted?: string;\n mutedForeground?: string;\n destructive?: string;\n destructiveForeground?: string;\n border?: string;\n input?: string;\n ring?: string;\n };\n fonts?: {\n body?: string;\n display?: string;\n };\n radius?: {\n sm?: string;\n md?: string;\n lg?: string;\n xl?: string;\n };\n /**\n * W6-3 — `sites.name`(DB SSoT, `rt init --name` 이 저장) 미러. 외부 사이트가\n * `<title>`/OG/헤더·푸터 브랜드명을 site.config.ts 정적 리터럴 대신 런타임\n * 반영하는 데 쓴다. 미설정이면 소비자가 site.config.ts 값으로 fallback.\n */\n siteName?: string;\n}\n\n/**\n * 테마 fetch 캐시 tag 의 정본 이름 (W0-4). starter layout 의 `fetchTheme` 호출에\n * `tags: [THEME_CACHE_TAG]` 로 부착하고, revalidate route 가 `theme.updated`\n * 수신 시 `revalidateTag(THEME_CACHE_TAG)` 로 무효화한다. 양쪽이 이 상수를 공유해\n * drift 를 방지한다.\n */\nexport const THEME_CACHE_TAG = \"rt-theme\";\n\nexport interface FetchThemeOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n /**\n * on-demand 무효화용 Next fetch cache tags(opt-in, W0-4). 보통\n * `[THEME_CACHE_TAG]`. 미지정 시 현행 동작 유지.\n */\n tags?: string[];\n}\n\n/**\n * tenant 의 RootTale 디자인 토큰을 가져온다. admin /design 의 마지막 저장값\n * (없으면 빈 객체) — renderer 가 이 값을 CSS 변수로 주입한다.\n *\n * 자동 fetch (renderer 가 theme prop 없을 때) 와 명시 호출 모두 지원.\n * 네트워크/서버 에러는 throw — 호출자가 fallback 처리.\n */\nexport async function fetchTheme(options: FetchThemeOptions): Promise<RootTaleTheme> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/theme${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n colors?: RootTaleTheme[\"colors\"];\n fonts?: RootTaleTheme[\"fonts\"];\n radius?: RootTaleTheme[\"radius\"];\n site_name?: string;\n };\n const theme: RootTaleTheme = {};\n if (json.colors) theme.colors = json.colors;\n if (json.fonts) theme.fonts = json.fonts;\n if (json.radius) theme.radius = json.radius;\n if (json.site_name) theme.siteName = json.site_name;\n return theme;\n}\n\nexport type AnalyticsTagProvider = \"ga4\" | \"clarity\" | \"meta_pixel\" | \"naver\";\n\n/** analytics-runtime `TagConfig` 와 구조 호환 — `buildManifest` 입력. */\nexport interface AnalyticsTagConfig {\n provider: AnalyticsTagProvider;\n id: string;\n enabled: boolean;\n}\n\nexport interface SiteAnalyticsTags {\n siteId: string;\n tags: AnalyticsTagConfig[];\n}\n\nexport interface FetchAnalyticsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n}\n\n/**\n * ADR-0038 plane ① — 외부 사이트가 `<head>` 에 주입할 런타임 태그(GA4 measurement\n * id / Clarity project id 등, 비밀 아님). RootTale 관리용 default + 테넌트 고객용\n * 설정을 머지한 결과를 반환한다. analytics-runtime `buildManifest` 입력으로 쓰고,\n * 동의는 항상 fail-closed(런타임이 처리)이므로 키 노출 위험 없음(public id).\n */\nexport async function fetchAnalyticsConfig(\n options: FetchAnalyticsOptions,\n): Promise<SiteAnalyticsTags> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/analytics${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n site_id: string;\n tags?: AnalyticsTagConfig[];\n };\n return { siteId: json.site_id, tags: json.tags ?? [] };\n}\n\n\nexport async function fetchPosts(options: FetchPostsOptions): Promise<CmsPostContentPage> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.limit !== undefined) params.set(\"limit\", String(options.limit));\n if (options.cursor) params.set(\"cursor\", options.cursor);\n if (options.type) params.set(\"type\", options.type);\n if (options.author) params.set(\"author\", options.author);\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/posts${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n items: CmsPostWire[];\n has_more: boolean;\n next_cursor: string | null;\n };\n return {\n items: json.items.map(fromWire),\n hasMore: json.has_more,\n nextCursor: json.next_cursor,\n };\n}\n\nexport async function fetchPost(options: FetchPostOptions): Promise<CmsPostContent | null> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/posts/${encodeURIComponent(options.slugOrId)}${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (response.status === 404) return null;\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as CmsPostWire;\n return fromWire(json);\n}\n\nexport interface FetchPageBySlugOptions {\n readonly apiKey: string;\n readonly baseUrl?: string;\n readonly slug: string;\n readonly siteId?: string;\n readonly signal?: AbortSignal;\n readonly revalidate?: number;\n}\n\nexport async function fetchPageBySlug(\n options: FetchPageBySlugOptions,\n): Promise<CmsPostContent | null> {\n let cursor: string | undefined;\n do {\n const page = await fetchPosts({\n apiKey: options.apiKey,\n baseUrl: options.baseUrl,\n limit: 100,\n ...(cursor ? { cursor } : {}),\n type: \"page\",\n siteId: options.siteId,\n signal: options.signal,\n revalidate: options.revalidate,\n });\n const match = page.items.find((item) => item.slug === options.slug);\n if (match) return match;\n cursor = page.nextCursor ?? undefined;\n } while (cursor);\n return null;\n}\n\ntype CmsPostWire = {\n id: string;\n tenant_id: string;\n site_id: string;\n type: CmsPostType;\n /** ADR-0060 Amendment 1 — 명시 섹션 key. 구 서버는 미포함 — optional. */\n collection_key?: string | null;\n slug: string;\n title: string;\n excerpt: string | null;\n featured_media_id: string | null;\n /** featured 이미지 delivery URL. 구 서버는 미포함 — backward-compat optional. */\n featured_media_url?: string | null;\n author_id: string | null;\n author_name?: string | null;\n author_image_url?: string | null;\n author_bio?: string | null;\n body_json: Record<string, unknown>;\n status: \"published\";\n meta_json: Record<string, unknown>;\n /** ADR-0041 포맷 필드. 구 서버는 미포함 — backward-compat optional. */\n fields?: Record<string, unknown> | null;\n /** 0.2.0 이전 서버는 미포함 — backward-compat 차원에서 optional. */\n terms?: CmsTermRef[];\n archive_related?: Array<{\n collection_key: string;\n label: string;\n items: Array<{\n id: string;\n title: string;\n slug: string;\n excerpt: string | null;\n collection_key: string;\n }>;\n }>;\n /** ADR-0065 — 누적 조회수. 사이트가 공개 조회수를 켠 경우에만 포함. */\n view_count?: number;\n published_at: string;\n created_at: string;\n updated_at: string;\n};\n\nfunction fromWire(wire: CmsPostWire): CmsPostContent {\n return {\n id: wire.id,\n tenantId: wire.tenant_id,\n siteId: wire.site_id,\n type: wire.type,\n collectionKey: wire.collection_key ?? null,\n slug: wire.slug,\n title: wire.title,\n excerpt: wire.excerpt,\n featuredMediaId: wire.featured_media_id,\n featuredImageUrl: wire.featured_media_url ?? null,\n authorId: wire.author_id,\n authorName: wire.author_name ?? null,\n authorImageUrl: wire.author_image_url ?? null,\n authorBio: wire.author_bio ?? null,\n bodyJson: wire.body_json,\n status: wire.status,\n metaJson: wire.meta_json,\n fields: wire.fields ?? null,\n terms: wire.terms ?? [],\n archiveRelated: (wire.archive_related ?? []).map((group) => ({\n collectionKey: group.collection_key,\n label: group.label,\n items: group.items.map((item) => ({\n id: item.id,\n title: item.title,\n slug: item.slug,\n excerpt: item.excerpt,\n collectionKey: item.collection_key,\n })),\n })),\n ...(typeof wire.view_count === \"number\" ? { viewCount: wire.view_count } : {}),\n publishedAt: wire.published_at,\n createdAt: wire.created_at,\n updatedAt: wire.updated_at,\n };\n}\n","import {\n DEFAULT_BASE_URL,\n assertServer,\n buildGetInit,\n toApiError,\n type CmsPostContent,\n} from \"./server-base\";\n\n/**\n * 블로그 표시 설정 — admin /design 의 \"블로그 표시\" 에서 저장한 사이트 기본값.\n * renderer 가 prop 미지정 시 이 값을 기본으로 적용 (글별 override 는 post 의\n * metaJson.display 가 우선).\n */\n/**\n * 글 하단 고정 CTA — admin 블로그 설정에서 활성화 시 모든 글 본문 끝에 그려진다.\n * null(미설정/비활성)이면 렌더러는 CTA 블록을 생략. buttonLabel/buttonHref 는\n * 객체가 존재하면 항상 non-empty(서버가 둘 다 있을 때만 객체를 내려줌).\n */\nexport interface RootTaleBlogPostCta {\n title: string | null;\n description: string | null;\n buttonLabel: string;\n buttonHref: string;\n}\n\n/**\n * 사이트 레벨 SEO 기본값 — 외부 사이트 `<head>`(설명·로고·파비콘)와 글별 OG\n * 미설정 시 공유 썸네일 폴백(defaultOgImageUrl)에 쓴다. 모두 nullable.\n */\nexport interface RootTaleSiteProfile {\n siteDescription: string | null;\n logoUrl: string | null;\n faviconUrl: string | null;\n /** 글별 og:image 미설정 시 폴백(1200×630 권장). */\n defaultOgImageUrl: string | null;\n}\n\n/**\n * 사이트맵 생성 정책 (admin 설정) — `createSitemapIndex` 가 소비해 무엇을 내보낼지\n * 결정한다. blanche식 인덱스 분리(static/blog/categories[/authors]) 위에 이미지·\n * 다국어 토글을 얹는다.\n */\nexport interface RootTaleSitemapSettings {\n /** 블로그 글 entry 에 대표 이미지(featuredImageUrl)를 `<image:image>` 로 포함. */\n blogImages: boolean;\n /** 다국어 hreflang locales (BCP-47). 빈 배열 = 단일 로케일(alternates 미emit). */\n locales: string[];\n /** authors 하위 사이트맵 포함 (작가 아카이브 라우트 필요). */\n authors: boolean;\n}\n\nexport interface RootTaleBlogSettings {\n showTableOfContents: boolean;\n showAuthor: boolean;\n showDate: boolean;\n showAuthorCard: boolean;\n /** 목차 상단 제목. null = renderer 기본(\"목차\"). */\n tocTitle: string | null;\n /**\n * 목차 배치. \"inline\"=본문 위 접이식(기본), \"sidebar\"=넓은 화면에서 본문\n * 오른쪽 sticky 사이드바(좁은 화면은 자동 인라인 폴백).\n */\n tocPosition: \"inline\" | \"sidebar\";\n /** 작성자 카드 상단 라벨. null = renderer 기본(\"작성자\"). */\n authorCardEyebrow: string | null;\n /** 작성자 카드 설명. null = 설명 없음. */\n authorCardDescription: string | null;\n /** 작성자 프로필 이름. null = 글 작성자 이름 사용. */\n authorProfileName: string | null;\n /** 작성자 프로필 bio. null = 설명 없음. */\n authorProfileBio: string | null;\n /** 작성자 프로필 이미지 URL. null = 이니셜 아바타 사용. */\n authorProfileImageUrl: string | null;\n /** 작성자 프로필 이미지 모서리. */\n authorProfileImageRadius: \"circle\" | \"rounded\" | \"square\";\n /** 작성자 프로필 이미지 object-position x (%). */\n authorProfileImagePositionX: number;\n /** 작성자 프로필 이미지 object-position y (%). */\n authorProfileImagePositionY: number;\n /** 사이트 레벨 SEO 기본값(설명·로고·파비콘·기본 OG). */\n siteProfile: RootTaleSiteProfile;\n /** 글 하단 고정 CTA. null = 미설정/비활성. */\n postCta: RootTaleBlogPostCta | null;\n /**\n * 사이트맵 생성 정책 (admin 설정). createSitemapIndex 가 소비. optional —\n * 구버전 응답/직접 구성한 객체와의 하위호환(소비 시 기본값으로 fail-soft).\n */\n sitemap?: RootTaleSitemapSettings;\n}\n\nexport interface FetchBlogSettingsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n}\n\n/**\n * tenant 의 블로그 표시 기본값을 가져온다. 네트워크/서버 에러는 throw —\n * 호출자(renderer)가 안전 기본값으로 fallback.\n */\nexport async function fetchBlogSettings(\n options: FetchBlogSettingsOptions,\n): Promise<RootTaleBlogSettings> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/blog-settings${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n show_table_of_contents?: boolean;\n show_author?: boolean;\n show_date?: boolean;\n show_author_card?: boolean;\n toc_title?: string | null;\n toc_position?: string | null;\n author_card_eyebrow?: string | null;\n author_card_description?: string | null;\n author_profile_name?: string | null;\n author_profile_bio?: string | null;\n author_profile_image_url?: string | null;\n author_profile_image_radius?: string | null;\n author_profile_image_position_x?: number;\n author_profile_image_position_y?: number;\n site_profile?: {\n site_description?: string | null;\n logo_url?: string | null;\n favicon_url?: string | null;\n default_og_image_url?: string | null;\n } | null;\n post_cta?: {\n title?: string | null;\n description?: string | null;\n button_label?: string;\n button_href?: string;\n } | null;\n sitemap?: {\n blog_images?: boolean;\n locales?: string[];\n authors?: boolean;\n } | null;\n };\n const radius =\n json.author_profile_image_radius === \"rounded\" ||\n json.author_profile_image_radius === \"square\"\n ? json.author_profile_image_radius\n : \"circle\";\n const percent = (x: unknown): number =>\n typeof x === \"number\" && Number.isFinite(x)\n ? Math.max(0, Math.min(100, Math.round(x)))\n : 50;\n const nonEmpty = (x: unknown): string | null =>\n typeof x === \"string\" && x.length > 0 ? x : null;\n return {\n showTableOfContents: json.show_table_of_contents === true,\n showAuthor: json.show_author !== false,\n showDate: json.show_date !== false,\n showAuthorCard: json.show_author_card !== false,\n tocTitle:\n typeof json.toc_title === \"string\" && json.toc_title.length > 0\n ? json.toc_title\n : null,\n tocPosition: json.toc_position === \"sidebar\" ? \"sidebar\" : \"inline\",\n authorCardEyebrow:\n typeof json.author_card_eyebrow === \"string\" &&\n json.author_card_eyebrow.length > 0\n ? json.author_card_eyebrow\n : null,\n authorCardDescription:\n typeof json.author_card_description === \"string\" &&\n json.author_card_description.length > 0\n ? json.author_card_description\n : null,\n authorProfileName:\n typeof json.author_profile_name === \"string\" &&\n json.author_profile_name.length > 0\n ? json.author_profile_name\n : null,\n authorProfileBio:\n typeof json.author_profile_bio === \"string\" &&\n json.author_profile_bio.length > 0\n ? json.author_profile_bio\n : null,\n authorProfileImageUrl:\n typeof json.author_profile_image_url === \"string\" &&\n json.author_profile_image_url.length > 0\n ? json.author_profile_image_url\n : null,\n authorProfileImageRadius: radius,\n authorProfileImagePositionX: percent(json.author_profile_image_position_x),\n authorProfileImagePositionY: percent(json.author_profile_image_position_y),\n siteProfile: {\n siteDescription: nonEmpty(json.site_profile?.site_description),\n logoUrl: nonEmpty(json.site_profile?.logo_url),\n faviconUrl: nonEmpty(json.site_profile?.favicon_url),\n defaultOgImageUrl: nonEmpty(json.site_profile?.default_og_image_url),\n },\n postCta:\n json.post_cta &&\n typeof json.post_cta.button_label === \"string\" &&\n json.post_cta.button_label.length > 0 &&\n typeof json.post_cta.button_href === \"string\" &&\n json.post_cta.button_href.length > 0\n ? {\n title:\n typeof json.post_cta.title === \"string\" &&\n json.post_cta.title.length > 0\n ? json.post_cta.title\n : null,\n description:\n typeof json.post_cta.description === \"string\" &&\n json.post_cta.description.length > 0\n ? json.post_cta.description\n : null,\n buttonLabel: json.post_cta.button_label,\n buttonHref: json.post_cta.button_href,\n }\n : null,\n sitemap: {\n blogImages: json.sitemap?.blog_images !== false,\n locales: Array.isArray(json.sitemap?.locales)\n ? json.sitemap.locales.filter(\n (s): s is string => typeof s === \"string\" && s.length > 0,\n )\n : [],\n authors: json.sitemap?.authors === true,\n },\n };\n}\n\n// ── 콘텐츠 Collections (ADR-0060) ────────────────────────────────────────────\n\n/**\n * 콘텐츠 스트림 1개 (camelCase). cms-core `RouteCollection` 과 구조 호환 —\n * `createSitemap`/`createFeedRoute` 의 `collections` 에 그대로 넘길 수 있다.\n */\nexport interface CmsRouteCollection {\n key: string;\n label: string;\n basePath: string;\n layout: string;\n categories: string[];\n feed: boolean;\n archives: boolean;\n og: boolean;\n requireSubcategory: boolean;\n pinning: boolean;\n icon: string | null;\n}\n\nexport interface FetchCollectionsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n}\n\n/**\n * 사이트의 콘텐츠 Collections (공지/블로그 등 스트림 분리)를 가져온다. 미설정이면\n * 빈 배열(레거시 단일 /blog). 네트워크/서버 에러는 throw — 호출자가 빈 배열로\n * fail-soft 하도록 한다(렌더가 collections 때문에 죽지 않는 것이 계약).\n */\nexport async function fetchCollections(\n options: FetchCollectionsOptions,\n): Promise<CmsRouteCollection[]> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/collections${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n collections?: Array<Record<string, unknown>>;\n };\n const items = Array.isArray(json.collections) ? json.collections : [];\n return items.flatMap((o) => {\n const key = typeof o.key === \"string\" ? o.key : \"\";\n const basePath = typeof o.base_path === \"string\" ? o.base_path : \"\";\n if (!key || !basePath) return [];\n return [\n {\n key,\n basePath,\n label: typeof o.label === \"string\" ? o.label : key,\n layout: typeof o.layout === \"string\" ? o.layout : \"grid\",\n categories: Array.isArray(o.categories)\n ? o.categories.filter((s): s is string => typeof s === \"string\")\n : [],\n feed: o.feed === true,\n archives: o.archives === true,\n og: o.og === true,\n requireSubcategory: o.require_subcategory === true,\n pinning: o.pinning === true,\n icon: typeof o.icon === \"string\" ? o.icon : null,\n },\n ];\n });\n}\n\n/**\n * fetch 실패/미설정 시 안전 기본값 — 빈 배열(레거시 단일 /blog). 사이트가\n * `fetchCollections().catch(() => fetchCollectionsSafeDefault())` 로 쓴다.\n */\nexport const fetchCollectionsSafeDefault: CmsRouteCollection[] = [];\n\n// ── 작가 아카이브 (blanche식 /blog/author/{slug}) ────────────────────────────\n\n/** 작가 1명 (camelCase) — author_slug 가 있고 발행 글이 1건 이상인 작가만. */\nexport interface RootTaleAuthor {\n /** 아카이브 URL slug (/blog/author/{slug}). */\n slug: string;\n /** 표시 이름 (author_display_name > user.name 폴백). */\n name: string;\n imageUrl: string | null;\n bio: string | null;\n /** 발행 글(type=post) 수 (≥1). */\n postCount: number;\n /** 작가의 최근 글 수정일 (사이트맵 lastmod). */\n lastUpdatedAt: string | null;\n}\n\nexport interface FetchAuthorsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n}\n\n/**\n * 사이트의 작가 목록(작가 아카이브·사이트맵 파생용)을 가져온다. author_slug 가 있고\n * 발행 글이 1건 이상인 작가만. 네트워크/서버 에러는 throw — 호출자(라우트/사이트맵\n * 팩토리)가 빈 배열로 fail-soft 한다.\n */\nexport async function fetchAuthors(\n options: FetchAuthorsOptions,\n): Promise<RootTaleAuthor[]> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/authors${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n authors?: Array<Record<string, unknown>>;\n };\n const items = Array.isArray(json.authors) ? json.authors : [];\n return items.flatMap((o) => {\n const slug = typeof o.slug === \"string\" ? o.slug : \"\";\n const name = typeof o.name === \"string\" ? o.name : \"\";\n if (!slug || !name) return [];\n return [\n {\n slug,\n name,\n imageUrl: typeof o.image_url === \"string\" ? o.image_url : null,\n bio: typeof o.bio === \"string\" ? o.bio : null,\n postCount: typeof o.post_count === \"number\" ? o.post_count : 0,\n lastUpdatedAt:\n typeof o.last_updated_at === \"string\" ? o.last_updated_at : null,\n },\n ];\n });\n}\n\n/** fetch 실패/미설정 시 사용할 안전 기본값 (기존 렌더러 동작). */\nexport const DEFAULT_BLOG_SETTINGS: RootTaleBlogSettings = {\n showTableOfContents: false,\n showAuthor: true,\n showDate: true,\n showAuthorCard: true,\n tocTitle: null,\n tocPosition: \"inline\",\n authorCardEyebrow: null,\n authorCardDescription: null,\n authorProfileName: null,\n authorProfileBio: null,\n authorProfileImageUrl: null,\n authorProfileImageRadius: \"circle\",\n authorProfileImagePositionX: 50,\n authorProfileImagePositionY: 50,\n siteProfile: {\n siteDescription: null,\n logoUrl: null,\n faviconUrl: null,\n defaultOgImageUrl: null,\n },\n postCta: null,\n sitemap: {\n blogImages: true,\n locales: [],\n authors: false,\n },\n};\n\n\nexport interface ResolvedPostDisplay {\n showTableOfContents: boolean;\n showAuthor: boolean;\n showDate: boolean;\n showAuthorCard: boolean;\n tocTitle: string;\n tocPosition: \"inline\" | \"sidebar\";\n authorCardEyebrow: string;\n authorCardDescription: string | null;\n authorProfileName: string | null;\n authorProfileBio: string | null;\n authorProfileImageUrl: string | null;\n authorProfileImageRadius: \"circle\" | \"rounded\" | \"square\";\n authorProfileImagePositionX: number;\n authorProfileImagePositionY: number;\n}\n\n/**\n * 글 1건의 실제 표시 여부 결정. 우선순위:\n * 명시 prop > 글별 override(post.metaJson.display) > 사이트 기본값(settings).\n * tocTitle: 명시 prop > settings.tocTitle > \"목차\".\n */\nexport function resolvePostDisplay(\n post: { metaJson?: Record<string, unknown> | null },\n settings: RootTaleBlogSettings,\n explicit?: { showTableOfContents?: boolean; tableOfContentsTitle?: string },\n): ResolvedPostDisplay {\n const display =\n post.metaJson && typeof post.metaJson === \"object\"\n ? ((post.metaJson as Record<string, unknown>).display as\n | Record<string, unknown>\n | undefined)\n : undefined;\n const ov = (key: string): boolean | undefined =>\n display && typeof display[key] === \"boolean\"\n ? (display[key] as boolean)\n : undefined;\n return {\n showTableOfContents:\n explicit?.showTableOfContents ?? ov(\"toc\") ?? settings.showTableOfContents,\n showAuthor: ov(\"author\") ?? settings.showAuthor,\n showDate: ov(\"date\") ?? settings.showDate,\n showAuthorCard: settings.showAuthorCard,\n tocTitle: explicit?.tableOfContentsTitle ?? settings.tocTitle ?? \"목차\",\n tocPosition: settings.tocPosition ?? \"inline\",\n authorCardEyebrow: settings.authorCardEyebrow ?? \"작성자\",\n authorCardDescription: settings.authorCardDescription,\n authorProfileName: settings.authorProfileName,\n authorProfileBio: settings.authorProfileBio,\n authorProfileImageUrl: settings.authorProfileImageUrl,\n authorProfileImageRadius: settings.authorProfileImageRadius,\n authorProfileImagePositionX: settings.authorProfileImagePositionX,\n authorProfileImagePositionY: settings.authorProfileImagePositionY,\n };\n}\n\n/**\n * 관련 글 선별 — 현재 글과 **같은 카테고리**를 1개 이상 공유하는 후보를\n * 발행일 내림차순으로 골라 최대 `limit` 개 반환 (자기 자신 제외).\n *\n * 순수 함수: 후보(`candidates`)는 호출부가 `fetchPosts` 로 미리 받아 전달한다\n * (서버에 카테고리 필터 파라미터가 없어 최근 글 배치에서 선별). 현재 글에\n * 카테고리가 없으면 빈 배열.\n */\nexport function selectRelatedPosts(\n current: CmsPostContent,\n candidates: CmsPostContent[],\n limit = 3,\n): CmsPostContent[] {\n if (limit <= 0) return [];\n const currentCats = new Set(\n current.terms\n .filter((t) => t.taxonomy === \"category\")\n .map((t) => t.id),\n );\n if (currentCats.size === 0) return [];\n return candidates\n .filter(\n (p) =>\n p.id !== current.id &&\n p.terms.some(\n (t) => t.taxonomy === \"category\" && currentCats.has(t.id),\n ),\n )\n .sort((a, b) => b.publishedAt.localeCompare(a.publishedAt))\n .slice(0, limit);\n}\n","import { DEFAULT_BASE_URL, assertServer, buildGetInit, toApiError } from \"./server-base\";\n\n// ── 비즈니스 프로필 (로컬 SEO) ───────────────────────────────────────────────\n\n/** Schema.org LocalBusiness 하위 타입 (admin \"운영 > 비즈니스 프로필\" 의 업종). */\nexport type BusinessType =\n | \"LocalBusiness\"\n | \"ProfessionalService\"\n | \"AccountingService\"\n | \"LegalService\"\n | \"MedicalClinic\"\n | \"Dentist\"\n | \"RealEstateAgent\"\n | \"Restaurant\"\n | \"BeautySalon\";\n\n/** Schema.org openingHours 요일 약어. */\nexport type BusinessDayCode = \"Mo\" | \"Tu\" | \"We\" | \"Th\" | \"Fr\" | \"Sa\" | \"Su\";\n\nexport interface BusinessOpeningHours {\n days: BusinessDayCode[];\n /** HH:MM (24시간). */\n opens: string;\n /** HH:MM (24시간). */\n closes: string;\n}\n\n/**\n * 비즈니스 프로필 — admin \"운영 > 비즈니스 프로필\" 에서 저장한 사업장 정보.\n * `localBusinessSchema(profile, { url })` 로 LocalBusiness JSON-LD 를 만들어\n * layout 에 1회 렌더한다 (로컬 SEO).\n */\nexport interface RootTaleBusinessProfile {\n /** 사업장 이름 (간판명). */\n name: string;\n /** 사업자등록 상호 — 간판명과 다를 때만 설정. */\n legalName: string | null;\n businessType: BusinessType;\n telephone: string | null;\n email: string | null;\n address: {\n streetAddress: string;\n addressLocality: string;\n addressRegion: string | null;\n postalCode: string | null;\n } | null;\n geo: { latitude: number; longitude: number } | null;\n openingHours: BusinessOpeningHours[];\n /** 가격대 표기 (예: \"₩₩\"). */\n priceRange: string | null;\n /** 서비스 지역 (예: \"서울 강남구\"). */\n areaServed: string[];\n /** 외부 프로필 URL — JSON-LD sameAs 로 들어가 동일 사업장임을 연결. */\n profiles: {\n naverPlace: string | null;\n googleBusiness: string | null;\n kakaoChannel: string | null;\n instagram: string | null;\n naverBlog: string | null;\n };\n}\n\nexport interface FetchBusinessProfileOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n}\n\ntype BusinessProfileWire = {\n configured?: boolean;\n name?: string | null;\n legal_name?: string | null;\n business_type?: string | null;\n telephone?: string | null;\n email?: string | null;\n address?: {\n street_address?: string;\n address_locality?: string;\n address_region?: string | null;\n postal_code?: string | null;\n } | null;\n geo?: { latitude?: number; longitude?: number } | null;\n opening_hours?: {\n days?: BusinessDayCode[];\n opens?: string;\n closes?: string;\n }[];\n price_range?: string | null;\n area_served?: string[];\n profiles?: {\n naver_place?: string | null;\n google_business?: string | null;\n kakao_channel?: string | null;\n instagram?: string | null;\n naver_blog?: string | null;\n } | null;\n};\n\n/** wire(snake_case) → RootTaleBusinessProfile. 미설정이면 null (export for tests). */\nexport function businessProfileFromWire(\n json: BusinessProfileWire,\n): RootTaleBusinessProfile | null {\n // configured=false 또는 이름 없음 = 미설정 — 호출자는 JSON-LD 렌더를 건너뛴다.\n if (json.configured === false) return null;\n if (typeof json.name !== \"string\" || json.name.length === 0) return null;\n const address =\n json.address &&\n typeof json.address.street_address === \"string\" &&\n typeof json.address.address_locality === \"string\"\n ? {\n streetAddress: json.address.street_address,\n addressLocality: json.address.address_locality,\n addressRegion: json.address.address_region ?? null,\n postalCode: json.address.postal_code ?? null,\n }\n : null;\n const geo =\n json.geo &&\n typeof json.geo.latitude === \"number\" &&\n typeof json.geo.longitude === \"number\"\n ? { latitude: json.geo.latitude, longitude: json.geo.longitude }\n : null;\n const openingHours: BusinessOpeningHours[] = [];\n for (const row of json.opening_hours ?? []) {\n if (\n Array.isArray(row.days) &&\n row.days.length > 0 &&\n typeof row.opens === \"string\" &&\n typeof row.closes === \"string\"\n ) {\n openingHours.push({ days: row.days, opens: row.opens, closes: row.closes });\n }\n }\n return {\n name: json.name,\n legalName: json.legal_name ?? null,\n businessType:\n typeof json.business_type === \"string\" && json.business_type.length > 0\n ? (json.business_type as BusinessType)\n : \"LocalBusiness\",\n telephone: json.telephone ?? null,\n email: json.email ?? null,\n address,\n geo,\n openingHours,\n priceRange: json.price_range ?? null,\n areaServed: json.area_served ?? [],\n profiles: {\n naverPlace: json.profiles?.naver_place ?? null,\n googleBusiness: json.profiles?.google_business ?? null,\n kakaoChannel: json.profiles?.kakao_channel ?? null,\n instagram: json.profiles?.instagram ?? null,\n naverBlog: json.profiles?.naver_blog ?? null,\n },\n };\n}\n\n/**\n * tenant 의 비즈니스 프로필을 가져온다. 미설정이면 null — 호출자는 null 일 때\n * JSON-LD 렌더를 건너뛰면 된다. 네트워크/서버 에러는 throw — 호출자가\n * `.catch(() => null)` 등으로 fallback.\n */\nexport async function fetchBusinessProfile(\n options: FetchBusinessProfileOptions,\n): Promise<RootTaleBusinessProfile | null> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/business-profile${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n // 구 서버(라우트 미배포)는 404 — 미설정과 동일하게 null.\n if (response.status === 404) return null;\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as BusinessProfileWire;\n return businessProfileFromWire(json);\n}\n\n","import {\n type CmsPostType,\n DEFAULT_BASE_URL,\n assertServer,\n buildGetInit,\n toApiError,\n} from \"./server-base\";\n\n// ── 공개 검색 (WP `?s=` 패리티) ────────────────────────────────────────────\n\nexport interface SearchPostsOptions {\n apiKey: string;\n baseUrl?: string;\n /** 검색 키워드 (title·excerpt·본문 텍스트 매칭). */\n query: string;\n /** 결과 수 1-50, default 10. */\n limit?: number;\n /** 기본 = `post` 만. page 포함 검색은 명시. */\n type?: CmsPostType;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n}\n\n/** 검색 결과 카드용 슬림 hit — body 미포함. */\nexport interface CmsSearchHit {\n id: string;\n type: CmsPostType;\n title: string;\n slug: string;\n excerpt: string | null;\n featuredImageUrl: string | null;\n /** ISO 8601. */\n publishedAt: string;\n}\n\n/**\n * published 글 키워드 검색 (사이트 내 검색). 최신 발행순, body 미포함 슬림\n * hit — 상세는 slug 로 `fetchPost`. 구 서버(라우트 미배포)는 404 — 빈 배열.\n */\nexport async function searchPosts(\n options: SearchPostsOptions,\n): Promise<CmsSearchHit[]> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n params.set(\"q\", options.query);\n if (options.limit !== undefined) params.set(\"limit\", String(options.limit));\n if (options.type) params.set(\"type\", options.type);\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const url = `${baseUrl}/v1/cms/public/search?${params.toString()}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (response.status === 404) return [];\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n items: {\n id: string;\n type: CmsPostType;\n title: string;\n slug: string;\n excerpt: string | null;\n featured_media_url: string | null;\n published_at: string;\n }[];\n };\n return json.items.map((w) => ({\n id: w.id,\n type: w.type,\n title: w.title,\n slug: w.slug,\n excerpt: w.excerpt,\n featuredImageUrl: w.featured_media_url ?? null,\n publishedAt: w.published_at,\n }));\n}\n\n// ── 네비게이션 메뉴 (WP 외모>메뉴 패리티) ──────────────────────────────────\n\n/** 메뉴 항목 트리 노드 (깊이 ≤ 2). */\nexport interface RootTaleMenuItem {\n id: string;\n label: string;\n /** 상대 경로(`/about`) 또는 절대 http(s) URL — 서버가 sanitize. */\n url: string;\n /** true 면 `target=\"_blank\" rel=\"noopener\"` 로 렌더할 것. */\n newTab?: boolean;\n children?: RootTaleMenuItem[];\n}\n\nexport interface RootTaleMenu {\n id: string;\n /** 사람용 이름 — \"헤더 메뉴\". */\n name: string;\n /** 위치 핸들 — \"primary\" | \"footer\" 등. */\n slug: string;\n items: RootTaleMenuItem[];\n updatedAt: string | null;\n}\n\nexport interface FetchMenuOptions {\n apiKey: string;\n baseUrl?: string;\n /** 위치 핸들 — \"primary\" | \"footer\" 등. */\n slug: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n}\n\nexport interface FetchMenusOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n}\n\ntype CmsMenuItemWire = {\n id: string;\n label: string;\n url: string;\n new_tab?: boolean;\n children?: CmsMenuItemWire[];\n};\n\ntype CmsMenuWire = {\n id: string;\n name: string;\n slug: string;\n items: CmsMenuItemWire[];\n updated_at: string | null;\n};\n\nfunction menuItemFromWire(w: CmsMenuItemWire): RootTaleMenuItem {\n const item: RootTaleMenuItem = { id: w.id, label: w.label, url: w.url };\n if (w.new_tab) item.newTab = true;\n if (w.children && w.children.length > 0) {\n item.children = w.children.map(menuItemFromWire);\n }\n return item;\n}\n\nfunction menuFromWire(w: CmsMenuWire): RootTaleMenu {\n return {\n id: w.id,\n name: w.name,\n slug: w.slug,\n items: (w.items ?? []).map(menuItemFromWire),\n updatedAt: w.updated_at ?? null,\n };\n}\n\n/**\n * 위치 핸들 slug 로 네비게이션 메뉴 1개를 가져온다. admin \"디자인 > 메뉴\" 의\n * 저장값. 미설정/미존재(404)는 null — 사이트는 자체 fallback 네비를 렌더한다.\n * 구 서버(라우트 미배포)도 404 → null 로 동일하게 fail-soft.\n */\nexport async function fetchMenu(\n options: FetchMenuOptions,\n): Promise<RootTaleMenu | null> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/menus/${encodeURIComponent(options.slug)}${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (response.status === 404) return null;\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as CmsMenuWire;\n return menuFromWire(json);\n}\n\n/** 사이트의 네비게이션 메뉴 전체. 구 서버(라우트 미배포)는 404 — 빈 배열. */\nexport async function fetchMenus(\n options: FetchMenusOptions,\n): Promise<RootTaleMenu[]> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/menus${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (response.status === 404) return [];\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as { items: CmsMenuWire[] };\n return (json.items ?? []).map(menuFromWire);\n}\n\n// ── 커스텀 리다이렉트 (WP RankMath Redirections 패리티) ─────────────────────\n\nexport interface RootTaleRedirect {\n id: string;\n /** 정규화된 출발 경로 (앞 슬래시, 쿼리 제외). 예: `/old-event`. */\n fromPath: string;\n /** 도착지 — 사이트 내부 경로(`/promo`) 또는 절대 URL. */\n toTarget: string;\n /** 301 영구 / 302 임시. */\n statusCode: 301 | 302;\n}\n\nexport interface FetchRedirectsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n /** ISR revalidate 초(opt-in, W0-9). 미지정 시 현행 동작 유지. */\n revalidate?: number;\n}\n\ntype CmsRedirectWire = {\n id: string;\n from_path: string;\n to_target: string;\n status_code: number;\n};\n\n/**\n * 사이트의 활성 커스텀 리다이렉트 규칙 전체. 고객 사이트 middleware\n * (`createRedirectMiddleware`)가 요청 경로 매칭에 사용한다. 구 서버(라우트\n * 미배포)는 404 — 빈 배열로 fail-soft (트래픽을 막지 않음).\n */\nexport async function fetchRedirects(\n options: FetchRedirectsOptions,\n): Promise<RootTaleRedirect[]> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/redirects${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, buildGetInit(options.apiKey, options));\n\n if (response.status === 404) return [];\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as { items: CmsRedirectWire[] };\n return (json.items ?? []).map((w) => ({\n id: w.id,\n fromPath: w.from_path,\n toTarget: w.to_target,\n statusCode: (w.status_code === 302 ? 302 : 301) as 301 | 302,\n }));\n}\n","// 프레임워크 무관 SEO 헬퍼 — RSS 2.0 피드 + Schema.org JSON-LD 빌더.\n// 외부 사이트(Next route handler / Astro endpoint / RSC)가 fetchPosts 결과로\n// /feed.xml 응답이나 <head> JSON-LD 를 만들 때 사용. 순수 함수(네트워크 없음).\n\nimport type { CmsPostContent, RootTaleBusinessProfile } from \"./server\";\n\n// ── RSS 2.0 ────────────────────────────────────────────────────────────────\n\nexport interface RssItemInput {\n title: string;\n /** 글 절대 URL. */\n link: string;\n /** 고유 식별자. 미지정 시 link 사용. */\n guid?: string;\n description?: string | null;\n /** 발행 시각. */\n pubDate?: string | Date | null;\n}\n\nexport interface RssFeedInput {\n /** 사이트/피드 제목. */\n title: string;\n /** 사이트 홈 URL. */\n link: string;\n /** 이 피드 자체의 URL (atom:self). */\n feedUrl: string;\n description?: string;\n /** 기본 \"ko-KR\". */\n language?: string;\n items: RssItemInput[];\n}\n\n/** RSS 2.0 XML 문자열 생성. */\nexport function generateRssXml(input: RssFeedInput): string {\n const lang = input.language ?? \"ko-KR\";\n const lastBuild =\n input.items.length > 0 ? toRfc822(input.items[0]!.pubDate) : \"\";\n const lines: string[] = [];\n lines.push('<?xml version=\"1.0\" encoding=\"UTF-8\"?>');\n lines.push(\n '<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">',\n );\n lines.push(\"<channel>\");\n lines.push(`<title>${escapeXml(input.title)}</title>`);\n lines.push(`<link>${escapeXml(input.link)}</link>`);\n lines.push(\n `<atom:link href=\"${escapeXml(input.feedUrl)}\" rel=\"self\" type=\"application/rss+xml\" />`,\n );\n lines.push(\n `<description>${escapeXml(input.description ?? input.title)}</description>`,\n );\n lines.push(`<language>${escapeXml(lang)}</language>`);\n if (lastBuild) lines.push(`<lastBuildDate>${lastBuild}</lastBuildDate>`);\n for (const item of input.items) {\n const guid = item.guid ?? item.link;\n lines.push(\"<item>\");\n lines.push(`<title>${escapeXml(item.title)}</title>`);\n lines.push(`<link>${escapeXml(item.link)}</link>`);\n lines.push(`<guid isPermaLink=\"false\">${escapeXml(guid)}</guid>`);\n if (item.description) {\n lines.push(`<description>${cdata(item.description)}</description>`);\n }\n const pub = toRfc822(item.pubDate);\n if (pub) lines.push(`<pubDate>${pub}</pubDate>`);\n lines.push(\"</item>\");\n }\n lines.push(\"</channel>\");\n lines.push(\"</rss>\");\n return lines.join(\"\\n\");\n}\n\n/** CmsPostContent[] → RSS item[]. postUrl 로 글 절대 URL 생성. */\nexport function rssItemsFromPosts(\n posts: CmsPostContent[],\n opts: { postUrl: (post: CmsPostContent) => string },\n): RssItemInput[] {\n return posts.map((p) => ({\n title: p.title,\n link: opts.postUrl(p),\n guid: opts.postUrl(p),\n description: p.excerpt,\n pubDate: p.publishedAt,\n }));\n}\n\nfunction toRfc822(value: string | Date | null | undefined): string {\n if (!value) return \"\";\n const d = value instanceof Date ? value : new Date(value);\n return Number.isNaN(d.getTime()) ? \"\" : d.toUTCString();\n}\n\n// ── Schema.org JSON-LD 빌더 ─────────────────────────────────────────────────\n\ntype JsonLd = Record<string, unknown>;\n\nexport interface ArticleSchemaInput {\n headline: string;\n url: string;\n description?: string | null;\n image?: string | null;\n datePublished?: string | Date | null;\n dateModified?: string | Date | null;\n authorName?: string | null;\n publisherName?: string;\n publisherLogo?: string;\n}\n\nexport function articleSchema(input: ArticleSchemaInput): JsonLd {\n const schema: JsonLd = {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Article\",\n headline: input.headline,\n mainEntityOfPage: { \"@type\": \"WebPage\", \"@id\": input.url },\n };\n if (input.description) schema.description = input.description;\n if (input.image) schema.image = input.image;\n const pub = toIso(input.datePublished);\n if (pub) schema.datePublished = pub;\n const mod = toIso(input.dateModified);\n if (mod) schema.dateModified = mod;\n if (input.authorName)\n schema.author = { \"@type\": \"Person\", name: input.authorName };\n if (input.publisherName) {\n schema.publisher = {\n \"@type\": \"Organization\",\n name: input.publisherName,\n ...(input.publisherLogo\n ? { logo: { \"@type\": \"ImageObject\", url: input.publisherLogo } }\n : {}),\n };\n }\n return schema;\n}\n\nexport function breadcrumbSchema(\n items: { name: string; url: string }[],\n): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"BreadcrumbList\",\n itemListElement: items.map((it, i) => ({\n \"@type\": \"ListItem\",\n position: i + 1,\n name: it.name,\n item: it.url,\n })),\n };\n}\n\nexport function organizationSchema(input: {\n name: string;\n url: string;\n logo?: string;\n sameAs?: string[];\n}): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Organization\",\n name: input.name,\n url: input.url,\n ...(input.logo ? { logo: input.logo } : {}),\n ...(input.sameAs && input.sameAs.length > 0 ? { sameAs: input.sameAs } : {}),\n };\n}\n\n// ── LocalBusiness (로컬 SEO) ────────────────────────────────────────────────\n\n/** Mo~Su 약어 → Schema.org dayOfWeek 전체 이름. */\nconst DAY_OF_WEEK: Record<string, string> = {\n Mo: \"Monday\",\n Tu: \"Tuesday\",\n We: \"Wednesday\",\n Th: \"Thursday\",\n Fr: \"Friday\",\n Sa: \"Saturday\",\n Su: \"Sunday\",\n};\n\n/**\n * `localBusinessSchema` 입력 — `fetchBusinessProfile` 의 반환값\n * (RootTaleBusinessProfile) 을 그대로 받도록 느슨하게 정의 (null/undefined 모두\n * 허용). 수동 구성도 가능.\n */\nexport interface LocalBusinessProfileInput {\n name: string;\n legalName?: string | null;\n /** Schema.org LocalBusiness 하위 타입. 미지정 시 \"LocalBusiness\". */\n businessType?: string | null;\n telephone?: string | null;\n email?: string | null;\n address?: {\n streetAddress: string;\n addressLocality: string;\n addressRegion?: string | null;\n postalCode?: string | null;\n } | null;\n geo?: { latitude: number; longitude: number } | null;\n openingHours?:\n | { days: string[]; opens: string; closes: string }[]\n | null;\n priceRange?: string | null;\n areaServed?: string[] | null;\n /** 외부 프로필 URL — 값들이 sameAs 로 들어간다 (네이버플레이스·구글 등). */\n profiles?: Record<string, string | null | undefined> | null;\n}\n\n/**\n * LocalBusiness JSON-LD — admin \"운영 > 비즈니스 프로필\" 의\n * `fetchBusinessProfile` 결과로 layout 에 1회 렌더한다 (로컬 SEO).\n *\n * - `@type` 은 `[businessType, \"LocalBusiness\"]` 중복 제거 배열.\n * - `profiles` 의 URL 들은 `sameAs` 로 — 검색엔진이 네이버플레이스/구글\n * 비즈니스 프로필과 동일 사업장임을 연결한다.\n */\nexport function localBusinessSchema(\n profile: LocalBusinessProfileInput | RootTaleBusinessProfile,\n opts: { url: string; image?: string | null },\n): JsonLd {\n const input = profile as LocalBusinessProfileInput;\n const types = Array.from(\n new Set([input.businessType || \"LocalBusiness\", \"LocalBusiness\"]),\n );\n const schema: JsonLd = {\n \"@context\": \"https://schema.org\",\n \"@type\": types,\n name: input.name,\n url: opts.url,\n };\n if (input.legalName) schema.legalName = input.legalName;\n if (opts.image) schema.image = opts.image;\n if (input.telephone) schema.telephone = input.telephone;\n if (input.email) schema.email = input.email;\n if (input.address) {\n schema.address = {\n \"@type\": \"PostalAddress\",\n streetAddress: input.address.streetAddress,\n addressLocality: input.address.addressLocality,\n ...(input.address.addressRegion\n ? { addressRegion: input.address.addressRegion }\n : {}),\n ...(input.address.postalCode\n ? { postalCode: input.address.postalCode }\n : {}),\n addressCountry: \"KR\",\n };\n }\n if (input.geo) {\n schema.geo = {\n \"@type\": \"GeoCoordinates\",\n latitude: input.geo.latitude,\n longitude: input.geo.longitude,\n };\n }\n const openingHours = (input.openingHours ?? []).filter(\n (row) => row.days.length > 0 && row.opens && row.closes,\n );\n if (openingHours.length > 0) {\n schema.openingHoursSpecification = openingHours.map((row) => ({\n \"@type\": \"OpeningHoursSpecification\",\n dayOfWeek: row.days.map((d) => DAY_OF_WEEK[d] ?? d),\n opens: row.opens,\n closes: row.closes,\n }));\n }\n if (input.priceRange) schema.priceRange = input.priceRange;\n if (input.areaServed && input.areaServed.length > 0) {\n schema.areaServed = input.areaServed;\n }\n const sameAs = Object.values(input.profiles ?? {}).filter(\n (u): u is string => typeof u === \"string\" && u.length > 0,\n );\n if (sameAs.length > 0) schema.sameAs = sameAs;\n return schema;\n}\n\nexport function websiteSchema(input: {\n name: string;\n url: string;\n searchUrl?: string;\n}): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"WebSite\",\n name: input.name,\n url: input.url,\n ...(input.searchUrl\n ? {\n potentialAction: {\n \"@type\": \"SearchAction\",\n target: `${input.searchUrl}{search_term_string}`,\n \"query-input\": \"required name=search_term_string\",\n },\n }\n : {}),\n };\n}\n\nexport function faqSchema(\n qa: { question: string; answer: string }[],\n): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"FAQPage\",\n mainEntity: qa.map((x) => ({\n \"@type\": \"Question\",\n name: x.question,\n acceptedAnswer: { \"@type\": \"Answer\", text: x.answer },\n })),\n };\n}\n\n/**\n * ADR-0065 — 글 조회수 식별자 meta 태그 HTML.\n *\n * 글 상세 페이지 `<head>` 에 렌더하면, RootTale 비콘이 이 값을 읽어 pageview 에\n * `post_id` 를 동봉한다 → URL/컬렉션 basePath 변경에 무관하게 조회수가 해당 글에\n * 정확히 집계된다. `post.id`(uuidv7) 를 그대로 넘기면 된다.\n *\n * Next App Router 는 `generateMetadata` 의 `other: { \"rt:content-id\": post.id }`\n * 로도 동일 태그를 낼 수 있다 — 그 경우 본 헬퍼 대신 metadata 를 쓴다.\n *\n * @example\n * // 프레임워크 무관(Astro/RSC set:html 등):\n * <head>{contentIdMeta(post.id)}</head>\n */\nexport function contentIdMeta(postId: string): string {\n return `<meta name=\"rt:content-id\" content=\"${escapeXml(postId)}\" />`;\n}\n\n/** JSON-LD 객체 → `<script type=\"application/ld+json\">` HTML 문자열 (XSS-safe). */\nexport function jsonLdScript(schema: JsonLd | JsonLd[]): string {\n // `</script>` 주입 방지를 위해 `<` 이스케이프.\n const json = JSON.stringify(schema).replace(/</g, \"\\\\u003c\");\n return `<script type=\"application/ld+json\">${json}</script>`;\n}\n\nfunction toIso(value: string | Date | null | undefined): string | null {\n if (!value) return null;\n const d = value instanceof Date ? value : new Date(value);\n return Number.isNaN(d.getTime()) ? null : d.toISOString();\n}\n\nfunction escapeXml(value: string): string {\n return value\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\");\n}\n\nfunction cdata(value: string): string {\n return `<![CDATA[${value.replace(/]]>/g, \"]]]]><![CDATA[>\")}]]>`;\n}\n","/**\n * `@roottale/cms-client` — 리드(문의) 접수 server-side 헬퍼.\n *\n * 외부 고객사 사이트가 *블로그 조회(`fetchPosts`)와 동일한 API key 하나* 로 리드도\n * 접수하기 위한 SSR-only 클라이언트. 브라우저가 아니라 사이트 서버(Server Action /\n * Route Handler)에서 호출해야 한다 — API key 는 절대 브라우저로 노출 금지\n * (ADR-0023 §5.1 #15). 키가 tenant 를 권위 식별하므로 `tenant_slug` 불필요.\n *\n * 사용 예 (Next.js server action):\n * ```ts\n * \"use server\";\n * import { submitInquiry } from \"@roottale/cms-client/server\";\n *\n * const result = await submitInquiry({\n * apiKey: process.env.ROOTTALE_API_KEY!, // fetchPosts 와 같은 키\n * fields: {\n * vertical: \"tax\",\n * contactName: name,\n * businessName: company,\n * email,\n * phone,\n * message,\n * privacyConsent: true,\n * },\n * });\n * if (result.ok) showSuccess(); // 그 키의 tenant /crm 에 도착\n * else showError(result.message);\n * ```\n *\n * intake API 는 302 `?ok=1` / `?err=*` 로 응답 — 본 헬퍼가 파싱해 구조화 결과 반환.\n */\n\nimport type { InquiryAttribution } from \"./attribution\";\n\nconst DEFAULT_BASE_URL = \"https://api.roottale.com\";\n\nexport type InquiryVertical = \"consulting\" | \"medical\" | \"tax\" | \"legal\";\n\n/** intake API 의 `err` 코드 → 한국어 사용자 메시지. */\nexport const INQUIRY_ERROR_MESSAGES: Record<string, string> = {\n consent_privacy: \"개인정보 수집·이용 동의가 필요합니다.\",\n consent_overseas: \"의료 분야는 국외이전 동의가 필요합니다.\",\n invalid_vertical: \"업종 분류가 유효하지 않습니다.\",\n missing_fields: \"필수 항목이 비어 있습니다.\",\n invalid_email: \"이메일 형식이 올바르지 않습니다.\",\n invalid_api_key: \"연동 인증에 실패했습니다. API 키를 확인해주세요.\",\n rate_limited: \"요청이 너무 많습니다. 잠시 후 다시 시도해주세요.\",\n turnstile_failed: \"보안 확인에 실패했습니다. 새로고침 후 다시 시도해주세요.\",\n internal: \"전송에 실패했습니다. 잠시 후 다시 시도해주세요.\",\n};\n\nexport interface SubmitInquiryFields {\n vertical: InquiryVertical;\n contactName: string;\n businessName: string;\n email: string;\n phone: string;\n /** 자유 문의 내용 (선택). */\n message?: string;\n /** 상담 분야 라벨 등 부가 메타 (선택). */\n consultationField?: string;\n /** 현재 사이트 URL (선택). */\n currentSiteUrl?: string;\n /** 개인정보 수집·이용 동의 (필수 true). */\n privacyConsent: boolean;\n /** 의료 vertical 의 국외이전 동의 (medical 일 때 필수). */\n overseasTransferConsent?: boolean;\n /**\n * \"patient\"(고객사 방문자 문의, 기본) | \"sales\"(RootTale 자체 B2B 영업 리드).\n * sales 는 medical 국외이전 동의 게이트 면제.\n */\n leadKind?: \"patient\" | \"sales\";\n turnstileToken?: string;\n /**\n * 표준 필드 외에 폼이 받은 임의 항목(key→value)을 있는 그대로 전달. intake 가\n * 암호화 보관하고 CRM 상세에서 노출한다. 예약 키(표준 필드)·`_` 접두 키는 무시.\n * 주의: 개인정보가 담길 수 있으므로 폼의 동의 고지에 수집 항목을 반영할 것.\n */\n extras?: Record<string, string>;\n /**\n * 유입 first-touch 어트리뷰션 (ADR-0048 §2, 선택). 브라우저에서\n * `readAttribution()`(`@roottale/cms-client/attribution`)으로 읽어 Server\n * Action 인자로 넘긴 값을 그대로 전달하면 된다. CRM 에 \"이 문의가 어느\n * 글/링크/QR 에서 왔는지\"가 표시된다. null/undefined 면 미동반 제출.\n */\n attribution?: InquiryAttribution | null;\n}\n\n/** submitInquiry 가 전용으로 매핑하는 키 — extras 에서 충돌 시 무시. */\nconst RESERVED_INQUIRY_KEYS = new Set([\n \"vertical\",\n \"contact_name\",\n \"business_name\",\n \"email\",\n \"phone\",\n \"message\",\n \"consultation_field\",\n \"current_site_url\",\n \"privacy_consent\",\n \"overseas_transfer_consent\",\n \"lead_kind\",\n \"cf-turnstile-response\",\n]);\n\nexport interface SubmitInquiryOptions {\n /** `rtlk_cust_*` API key — `fetchPosts` 와 동일한 키. Required. */\n apiKey: string;\n /** API base URL 오버라이드. 기본 `https://api.roottale.com`. */\n baseUrl?: string;\n fields: SubmitInquiryFields;\n signal?: AbortSignal;\n}\n\nexport type SubmitInquiryResult =\n | { ok: true }\n | { ok: false; code: string | null; message: string };\n\n/** `err` 코드를 사용자 메시지로 (미지의 코드는 generic). */\nexport function inquiryErrorMessage(code: string | null | undefined): string {\n return (code && INQUIRY_ERROR_MESSAGES[code]) || INQUIRY_ERROR_MESSAGES.internal;\n}\n\n/**\n * intake API 의 302 Location (`?ok=1` / `?err=*`) 을 구조화 결과로 파싱.\n * 외부 사이트가 ERROR_MESSAGES·파싱을 각자 재구현하지 않도록 공유.\n */\nexport function parseInquiryRedirect(\n location: string | null,\n base: string = DEFAULT_BASE_URL,\n): SubmitInquiryResult {\n if (!location) {\n return { ok: false, code: null, message: inquiryErrorMessage(null) };\n }\n let url: URL;\n try {\n url = new URL(location, base);\n } catch {\n return { ok: false, code: null, message: inquiryErrorMessage(null) };\n }\n if (url.searchParams.get(\"ok\") === \"1\") return { ok: true };\n const code = url.searchParams.get(\"err\");\n return { ok: false, code, message: inquiryErrorMessage(code) };\n}\n\nexport function formatKoreanPhoneNumber(input: string): string {\n let digits = input.replace(/\\D/g, \"\");\n if (digits.startsWith(\"82\") && digits.length >= 11) {\n digits = `0${digits.slice(2)}`;\n }\n if (digits.length === 11) {\n return `${digits.slice(0, 3)}-${digits.slice(3, 7)}-${digits.slice(7)}`;\n }\n if (digits.length === 10) {\n if (digits.startsWith(\"02\")) {\n return `${digits.slice(0, 2)}-${digits.slice(2, 6)}-${digits.slice(6)}`;\n }\n return `${digits.slice(0, 3)}-${digits.slice(3, 6)}-${digits.slice(6)}`;\n }\n if (digits.length === 8) {\n return `${digits.slice(0, 4)}-${digits.slice(4)}`;\n }\n return input.trim();\n}\n\n/**\n * 리드를 RootTale intake API 로 제출 (server-to-server, Bearer 인증).\n * 키의 tenant 로 라우팅되어 admin(mysite.roottale.com) /crm 에서 관리된다.\n */\nexport async function submitInquiry(\n options: SubmitInquiryOptions,\n): Promise<SubmitInquiryResult> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const f = options.fields;\n\n const body = new FormData();\n body.set(\"vertical\", f.vertical);\n body.set(\"contact_name\", f.contactName);\n body.set(\"business_name\", f.businessName);\n body.set(\"email\", f.email);\n body.set(\"phone\", formatKoreanPhoneNumber(f.phone));\n if (f.message) body.set(\"message\", f.message);\n if (f.consultationField) body.set(\"consultation_field\", f.consultationField);\n if (f.currentSiteUrl) body.set(\"current_site_url\", f.currentSiteUrl);\n // 사용자가 명시 동의한 경우에만 전달 (자동 부여 금지).\n if (f.privacyConsent) body.set(\"privacy_consent\", \"on\");\n if (f.overseasTransferConsent) body.set(\"overseas_transfer_consent\", \"on\");\n if (f.leadKind) body.set(\"lead_kind\", f.leadKind);\n if (f.turnstileToken) body.set(\"cf-turnstile-response\", f.turnstileToken);\n // 유입 어트리뷰션 → intake `attr_*` 필드 (허용 키만, 빈 값 생략).\n if (f.attribution) {\n for (const [key, value] of Object.entries(f.attribution)) {\n if (typeof value !== \"string\" || !value.trim()) continue;\n body.set(`attr_${key}`, value.trim());\n }\n }\n // 표준 외 임의 항목 — 예약 키/`_`접두/빈 값/중복은 건너뛰고 원본 그대로 전달.\n if (f.extras) {\n for (const [rawKey, rawValue] of Object.entries(f.extras)) {\n const key = rawKey.trim();\n const value = String(rawValue ?? \"\").trim();\n if (!key || !value) continue;\n if (key.startsWith(\"_\") || RESERVED_INQUIRY_KEYS.has(key)) continue;\n if (key.startsWith(\"attr_\")) continue; // 어트리뷰션 채널과 충돌 방지\n\n if (body.has(key)) continue;\n body.set(key, value);\n }\n }\n\n let response: Response;\n try {\n response = await fetch(`${baseUrl}/v1/public/inquiries`, {\n method: \"POST\",\n headers: { authorization: `Bearer ${options.apiKey}` },\n body,\n // intake 는 302 로 결과 신호 — 자동 추적 금지하고 Location 직접 읽음.\n redirect: \"manual\",\n signal: options.signal,\n });\n } catch {\n return { ok: false, code: \"internal\", message: inquiryErrorMessage(\"internal\") };\n }\n\n // 잘못된/만료된 키 → 라우트가 401.\n if (response.status === 401) {\n return {\n ok: false,\n code: \"invalid_api_key\",\n message: inquiryErrorMessage(\"invalid_api_key\"),\n };\n }\n if (response.status === 429) {\n return {\n ok: false,\n code: \"rate_limited\",\n message: inquiryErrorMessage(\"rate_limited\"),\n };\n }\n\n const location = response.headers.get(\"location\");\n if (location) return parseInquiryRedirect(location, baseUrl);\n\n // redirect 없이 성공 응답이면 ok 로 간주, 그 외엔 generic 실패.\n if (response.ok) return { ok: true };\n return { ok: false, code: \"internal\", message: inquiryErrorMessage(\"internal\") };\n}\n\nfunction assertServer(): void {\n if (typeof window !== \"undefined\") {\n throw new Error(\n \"@roottale/cms-client submitInquiry must not run in the browser. \" +\n \"Call it from a Server Action / Route Handler. \" +\n \"Shipping the API key to the browser breaks the security model (ADR-0023 §5.1 #15).\",\n );\n }\n}\n"],"mappings":"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","names":["DEFAULT_BASE_URL","assertServer"]}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roottale/cms-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "RootTale CMS Public API server-side fetch client (Bearer rtlk_cust_* auth). SSR-only — refuses to run in the browser to prevent key leak (ADR-0023 §5.1 #15). Pairs with @roottale/cms-renderer-next
|
|
5
|
+
"description": "RootTale CMS Public API server-side fetch client (Bearer rtlk_cust_* auth). SSR-only — refuses to run in the browser to prevent key leak (ADR-0023 §5.1 #15). Pairs with @roottale/cms-renderer-next.",
|
|
6
6
|
"main": "./dist/server.js",
|
|
7
7
|
"types": "./dist/server.d.ts",
|
|
8
8
|
"exports": {
|