@roottale/cms-client 0.65.1 → 0.67.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # @roottale/cms-client
2
2
 
3
+ ## 0.67.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 30d26ac: 공통 메타데이터 해석기·고유 경로 조회·Next SSR 빌더와 캐시 태그를 추가합니다. 기존 canonical과 강제 robots 정책을 보존하고 공유 이미지 대체 텍스트를 OG·X에 동일하게 적용합니다. 분류의 공유 전용 설정과 메타데이터 조회·수정 MCP 도구, Pro 하향 후 공개 적용 유지 계약을 문서와 예제에 반영합니다. 분류 색인 전환을 선언한 사이트가 공개 분류 응답의 `category_index_activated`를 확인한 뒤에만 분류 검색 제외를 적용하도록 안내합니다.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [30d26ac]
12
+ - @roottale/cms-core@0.67.0
13
+
14
+ ## 0.66.0
15
+
16
+ ### Minor Changes
17
+
18
+ - a58c5d2: 계층형 분류 모델(`category_tree`)에 `entryCategory: "leaf"`를 추가한다. 하위 분류가 없는
19
+ 분류면 단계와 무관하게 글을 붙일 수 있고(`categoryDepth`는 최대 단계), 새로 고를 때는
20
+ `resolveCategoryTreeSelection`으로 하위가 없는지 확인한다. 생략하면 기존처럼
21
+ `categoryDepth` 단계 말단에만 붙는다.
22
+ - 716dc9b: 콘텐츠 모델 presentation에 `listPaths`(글을 목록으로 보여 주는 고정 화면)를 추가합니다.
23
+ `data_only`·`detail`·`category_tree`에서 쓸 수 있고, 글 변경 웹훅 `paths`에 함께 실립니다.
24
+ 상세 주소가 없는 `data_only` 모델은 이 화면만 갱신하고, ROOT-ADMIN은 그 화면에서 현재
25
+ 제목으로 공개 반영을 확인합니다. 공개 콘텐츠 모델 API 응답에도 `listPaths`가 포함됩니다.
26
+ - bb7b516: 검색엔진 소유 확인 코드를 공개 테마 응답 `siteVerification`으로 내보냅니다.
27
+ 고객이 ROOT-ADMIN 설정 → 연동에서 Google Search Console·네이버 서치어드바이저 코드를
28
+ 붙여 넣으면 `theme.updated` 웹훅으로 바로 반영되고, 사이트는
29
+ `siteVerificationMetadata(theme.siteVerification)`를 루트 metadata `verification`에 넣어
30
+ 메타태그로 출력합니다.
31
+
3
32
  ## 0.65.1
4
33
 
5
34
  ## 0.65.0
package/dist/server.d.ts CHANGED
@@ -2,6 +2,8 @@ import { InquiryFormDefinition, InquiryAnswer } from '@roottale/inquiry-forms';
2
2
  export { InquiryAnswer, InquiryFormDefinition, InquiryFormField, InquiryKind } from '@roottale/inquiry-forms';
3
3
  import { InquiryAttribution } from './attribution.js';
4
4
  export { parseAttributionJson } from './attribution.js';
5
+ import { PublicRouteMetadata } from '@roottale/cms-core';
6
+ export { MetadataDefaultRules, MetadataDefaultRulesSchema, MetadataDefaultsDeclaration, MetadataDefaultsInput, MetadataField, MetadataPatch, MetadataResolutionInput, MetadataRobotsPolicy, MetadataTextRule, MetadataValues, PublicRouteMetadata, formatMetadataDefaults, metadataContentImageUrl, readMetadataValues, resolveMetadata } from '@roottale/cms-core';
5
7
 
6
8
  /**
7
9
  * `@roottale/cms-client/server` — RootTale CMS Public API server-side fetch client.
@@ -312,6 +314,16 @@ interface RootTaleTheme {
312
314
  actionDock?: ActionDockSettings;
313
315
  inquiryBoard?: InquiryBoardSettings;
314
316
  siteNav?: SiteNavSettings;
317
+ /**
318
+ * ROOT-ADMIN 연동 설정에 입력한 검색엔진 소유 확인 코드. `siteVerificationMetadata`
319
+ * 로 Next metadata `verification` 모양으로 바꿔 루트 layout 에 출력한다.
320
+ */
321
+ siteVerification?: SiteVerificationSettings;
322
+ }
323
+ /** 엔진별 소유 확인 코드(고객·RootTale 계정이 각각 확인할 수 있어 여러 개). */
324
+ interface SiteVerificationSettings {
325
+ google?: string[];
326
+ naver?: string[];
315
327
  }
316
328
  type AnnouncementVariant = "single" | "rotating" | "cta";
317
329
  type AnnouncementTone = "default" | "primary";
@@ -613,6 +625,7 @@ interface CmsContentModel {
613
625
  source: "code" | "site";
614
626
  presentation: {
615
627
  readonly kind: "data_only";
628
+ readonly listPaths?: readonly string[];
616
629
  } | {
617
630
  readonly kind: "fixed_page";
618
631
  readonly path: string;
@@ -622,11 +635,16 @@ interface CmsContentModel {
622
635
  readonly kind: "detail";
623
636
  readonly detailPath: string;
624
637
  readonly templateKey?: string;
638
+ /** 글을 목록으로 보여 주는 고정 화면(홈·회사소개 등). 글 변경 웹훅에 함께 실린다. */
639
+ readonly listPaths?: readonly string[];
625
640
  } | {
626
641
  readonly kind: "category_tree";
627
642
  readonly basePath: string;
628
643
  readonly categoryDepth: number;
644
+ /** `leaf`면 하위 분류가 없는 분류에 단계와 무관하게 글을 붙인다. 생략은 `deepest`. */
645
+ readonly entryCategory?: "deepest" | "leaf";
629
646
  readonly templateKey?: string;
647
+ readonly listPaths?: readonly string[];
630
648
  };
631
649
  }
632
650
  interface FetchContentModelsOptions {
@@ -738,6 +756,10 @@ interface CmsCategoryCount {
738
756
  description: string | null;
739
757
  seoTitle: string | null;
740
758
  seoDescription: string | null;
759
+ /** 공유 전용 설정. 본문 분류 이미지와 별도로 관리한다. */
760
+ seoOgImage?: string | null;
761
+ seoOgImageAlt?: string | null;
762
+ seoNofollow?: boolean | null;
741
763
  imageMediaId?: string | null;
742
764
  imageUrl?: string | null;
743
765
  imageAlt?: string | null;
@@ -1329,6 +1351,34 @@ declare function sitePatternPresentationAttributes(presentation: CmsSitePatternP
1329
1351
  */
1330
1352
  declare function selectSitePatternForSlot(patternSlots: Readonly<Record<string, string | null>> | null | undefined, patterns: readonly CmsSitePattern[], slot?: string): CmsSitePattern | null;
1331
1353
 
1354
+ declare const METADATA_CACHE_TAG = "rt-metadata";
1355
+ declare const SITE_METADATA_DEFAULTS_CACHE_TAG = "rt-metadata-defaults";
1356
+ /** locale를 생략한 요청은 사이트 기본 locale을 조회한다. */
1357
+ declare function routeMetadataCacheTag(routeKey: string, locale?: string): string;
1358
+ interface FetchRouteMetadataOptions {
1359
+ apiKey: string;
1360
+ baseUrl?: string;
1361
+ siteId?: string;
1362
+ routeKey: string;
1363
+ locale?: string;
1364
+ signal?: AbortSignal;
1365
+ /** 기본 300초. 웹훅 유실 시 재조회하며, 0이면 항상 최신값을 읽는다. */
1366
+ revalidate?: number;
1367
+ }
1368
+ /**
1369
+ * 고유 route key 직접 조회. 등록되지 않은 경로만 null이며, 연결된 빈 설정은
1370
+ * overrides:{}인 객체다. HTTP/네트워크/응답 오류는 throw하여 빈 설정으로 캐시하지 않는다.
1371
+ * 공개 읽기는 구독 등급을 검사하지 않아 요금제 하향 후에도 기존 설정을 유지한다.
1372
+ */
1373
+ declare function fetchRouteMetadata(options: FetchRouteMetadataOptions): Promise<PublicRouteMetadata | null>;
1374
+
1375
+ /**
1376
+ * 배포 도구가 정규화해 등록한 manifest의 SHA-256(64자리 hex).
1377
+ * 이 함수는 스키마 검사·기본값 채우기를 하지 않는다. 원본 JSON을 그대로 해시하면
1378
+ * 등록 API가 정규화한 계약과 달라질 수 있으므로 배포 시 정규화한 manifest를 사용한다.
1379
+ */
1380
+ declare function metadataContractChecksum(normalizedContract: unknown): Promise<string>;
1381
+
1332
1382
  /**
1333
1383
  * 설정류 fetch 캐시 tag 한 벌 (ADR-0096 Phase C).
1334
1384
  *
@@ -1522,6 +1572,16 @@ declare function contentIdMeta(postId: string): string;
1522
1572
  declare function jsonLdJson(schema: JsonLd | JsonLd[]): string;
1523
1573
  /** JSON-LD 객체 → `<script type="application/ld+json">` HTML 문자열 (XSS-safe). */
1524
1574
  declare function jsonLdScript(schema: JsonLd | JsonLd[]): string;
1575
+ /** Next.js `Metadata["verification"]` 와 구조 호환. */
1576
+ interface SiteVerificationMetadata {
1577
+ google?: string[];
1578
+ other?: Record<string, string[]>;
1579
+ }
1580
+ /**
1581
+ * `theme.siteVerification` → 루트 layout metadata 의 `verification`.
1582
+ * 코드가 없으면 undefined 를 돌려줘 spread 로 생략할 수 있다.
1583
+ */
1584
+ declare function siteVerificationMetadata(settings: SiteVerificationSettings | undefined): SiteVerificationMetadata | undefined;
1525
1585
 
1526
1586
  /**
1527
1587
  * `@roottale/cms-client` — 리드(문의) 접수 server-side 헬퍼.
@@ -1631,4 +1691,4 @@ declare function formatKoreanPhoneNumber(input: string): string;
1631
1691
  */
1632
1692
  declare function submitInquiry(options: SubmitInquiryOptions): Promise<SubmitInquiryResult>;
1633
1693
 
1634
- export { type ActionDockAction, type ActionDockKind, type ActionDockPosition, type ActionDockSettings, type AnalyticsTagConfig, type AnalyticsTagProvider, type AnnouncementItem, type AnnouncementSettings, type AnnouncementTone, type AnnouncementVariant, type ArticleSchemaInput, BLOG_SETTINGS_CACHE_TAG, BUSINESS_CACHE_TAG, type BusinessDayCode, type BusinessOpeningHours, type BusinessType, CMS_CLIENT_VERSION, COLLECTIONS_CACHE_TAG, CmsApiError, type CmsArchiveRelatedGroup, type CmsArchiveRelatedPost, type CmsCategoryCount, type CmsCategoryCounts, type CmsContentModel, type CmsExposureCampaign, type CmsExposureDecision, type CmsFieldMetaEntry, type CmsPostContent, type CmsPostContentPage, type CmsPostPreviewContent, type CmsPostPreviewInfo, type CmsPostTranslationRef, type CmsPostType, type CmsRouteCollection, type CmsSearchHit, type CmsSitePattern, type CmsSitePatternPresentation, type CmsTaxonomyKind, type CmsTermRef, DEFAULT_BLOG_SETTINGS, DEFAULT_SITE_PATTERN_PRESENTATION, EXPOSURE_CACHE_TAG, type FetchAnalyticsOptions, type FetchAuthorsOptions, type FetchBlogSettingsOptions, type FetchBusinessProfileOptions, type FetchCategoryCountsOptions, type FetchCollectionsOptions, type FetchContentModelsOptions, type FetchExposureDecisionOptions, type FetchExposureOptions, type FetchInquiryFormOptions, type FetchMenuOptions, type FetchMenusOptions, type FetchPageBySlugOptions, type FetchPostOptions, type FetchPostPreviewOptions, type FetchPostsOptions, type FetchRedirectsOptions, type FetchSiteInquiriesOptions, type FetchSiteInquiryOptions, type FetchSitePatternsOptions, type FetchThemeOptions, type FormInquiryReceipt, INQUIRY_ERROR_MESSAGES, InquiryAttribution, type InquiryBoardSettings, type InquiryVertical, LOCAL_BUSINESS_MAX_SERVICES, LOCAL_BUSINESS_SERVICE_MAX_LENGTH, type LocalBusinessProfileInput, MENUS_CACHE_TAG, POST_FOOTER_PATTERN_SLOT, type PublishedInquiryForm, 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, SETTINGS_CACHE_TAGS, SITE_PATTERNS_CACHE_TAG, type SearchPostsOptions, type SiteAnalyticsTags, type SiteInquiryDetail, type SiteInquiryListItem, type SiteInquiryListPage, type SiteInquiryStatus, type SiteNavColumn, type SiteNavCta, type SiteNavGroup, type SiteNavLink, type SiteNavSettings, type SubmitFormInquiryFields, type SubmitFormInquiryOptions, type SubmitFormInquiryResult, type SubmitInquiryFields, type SubmitInquiryOptions, type SubmitInquiryResult, type SubmitSiteInquiryFields, type SubmitSiteInquiryOptions, type SubmitSiteInquiryResult, THEME_CACHE_TAG, articleSchema, breadcrumbSchema, businessProfileFromWire, collectionPageSchema, contentIdMeta, faqSchema, fetchAnalyticsConfig, fetchAuthors, fetchBlogSettings, fetchBusinessProfile, fetchCategoryCounts, fetchCollections, fetchCollectionsSafeDefault, fetchContentModels, fetchExposure, fetchExposureDecision, fetchInquiryForm, fetchMenu, fetchMenus, fetchPageBySlug, fetchPost, fetchPostPreview, fetchPosts, fetchRedirects, fetchSiteInquiries, fetchSiteInquiry, fetchSitePatterns, fetchTheme, formatKoreanPhoneNumber, generateRssXml, inquiryErrorMessage, isPreviewExpiredError, jsonLdJson, jsonLdScript, localBusinessSchema, organizationSchema, parseInquiryRedirect, profilePageSchema, resolvePostDisplay, resolveSearchHitPath, rssItemsFromPosts, searchPosts, selectRelatedPosts, selectSitePatternForSlot, sitePatternPresentationAttributes, submitFormInquiry, submitInquiry, submitSiteInquiry, websiteSchema };
1694
+ export { type ActionDockAction, type ActionDockKind, type ActionDockPosition, type ActionDockSettings, type AnalyticsTagConfig, type AnalyticsTagProvider, type AnnouncementItem, type AnnouncementSettings, type AnnouncementTone, type AnnouncementVariant, type ArticleSchemaInput, BLOG_SETTINGS_CACHE_TAG, BUSINESS_CACHE_TAG, type BusinessDayCode, type BusinessOpeningHours, type BusinessType, CMS_CLIENT_VERSION, COLLECTIONS_CACHE_TAG, CmsApiError, type CmsArchiveRelatedGroup, type CmsArchiveRelatedPost, type CmsCategoryCount, type CmsCategoryCounts, type CmsContentModel, type CmsExposureCampaign, type CmsExposureDecision, type CmsFieldMetaEntry, type CmsPostContent, type CmsPostContentPage, type CmsPostPreviewContent, type CmsPostPreviewInfo, type CmsPostTranslationRef, type CmsPostType, type CmsRouteCollection, type CmsSearchHit, type CmsSitePattern, type CmsSitePatternPresentation, type CmsTaxonomyKind, type CmsTermRef, DEFAULT_BLOG_SETTINGS, DEFAULT_SITE_PATTERN_PRESENTATION, EXPOSURE_CACHE_TAG, type FetchAnalyticsOptions, type FetchAuthorsOptions, type FetchBlogSettingsOptions, type FetchBusinessProfileOptions, type FetchCategoryCountsOptions, type FetchCollectionsOptions, type FetchContentModelsOptions, type FetchExposureDecisionOptions, type FetchExposureOptions, type FetchInquiryFormOptions, type FetchMenuOptions, type FetchMenusOptions, type FetchPageBySlugOptions, type FetchPostOptions, type FetchPostPreviewOptions, type FetchPostsOptions, type FetchRedirectsOptions, type FetchRouteMetadataOptions, type FetchSiteInquiriesOptions, type FetchSiteInquiryOptions, type FetchSitePatternsOptions, type FetchThemeOptions, type FormInquiryReceipt, INQUIRY_ERROR_MESSAGES, InquiryAttribution, type InquiryBoardSettings, type InquiryVertical, LOCAL_BUSINESS_MAX_SERVICES, LOCAL_BUSINESS_SERVICE_MAX_LENGTH, type LocalBusinessProfileInput, MENUS_CACHE_TAG, METADATA_CACHE_TAG, POST_FOOTER_PATTERN_SLOT, type PublishedInquiryForm, 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, SETTINGS_CACHE_TAGS, SITE_METADATA_DEFAULTS_CACHE_TAG, SITE_PATTERNS_CACHE_TAG, type SearchPostsOptions, type SiteAnalyticsTags, type SiteInquiryDetail, type SiteInquiryListItem, type SiteInquiryListPage, type SiteInquiryStatus, type SiteNavColumn, type SiteNavCta, type SiteNavGroup, type SiteNavLink, type SiteNavSettings, type SiteVerificationMetadata, type SiteVerificationSettings, type SubmitFormInquiryFields, type SubmitFormInquiryOptions, type SubmitFormInquiryResult, type SubmitInquiryFields, type SubmitInquiryOptions, type SubmitInquiryResult, type SubmitSiteInquiryFields, type SubmitSiteInquiryOptions, type SubmitSiteInquiryResult, THEME_CACHE_TAG, articleSchema, breadcrumbSchema, businessProfileFromWire, collectionPageSchema, contentIdMeta, faqSchema, fetchAnalyticsConfig, fetchAuthors, fetchBlogSettings, fetchBusinessProfile, fetchCategoryCounts, fetchCollections, fetchCollectionsSafeDefault, fetchContentModels, fetchExposure, fetchExposureDecision, fetchInquiryForm, fetchMenu, fetchMenus, fetchPageBySlug, fetchPost, fetchPostPreview, fetchPosts, fetchRedirects, fetchRouteMetadata, fetchSiteInquiries, fetchSiteInquiry, fetchSitePatterns, fetchTheme, formatKoreanPhoneNumber, generateRssXml, inquiryErrorMessage, isPreviewExpiredError, jsonLdJson, jsonLdScript, localBusinessSchema, metadataContractChecksum, organizationSchema, parseInquiryRedirect, profilePageSchema, resolvePostDisplay, resolveSearchHitPath, routeMetadataCacheTag, rssItemsFromPosts, searchPosts, selectRelatedPosts, selectSitePatternForSlot, sitePatternPresentationAttributes, siteVerificationMetadata, submitFormInquiry, submitInquiry, submitSiteInquiry, 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.65.1" : "dev";
7
+ var CLIENT_VERSION = true ? "0.67.0" : "dev";
8
8
  var CMS_CLIENT_VERSION = CLIENT_VERSION;
9
9
  function authHeaders(apiKey) {
10
10
  return {
@@ -173,6 +173,7 @@ async function fetchTheme(options) {
173
173
  if (json.actionDock) theme.actionDock = json.actionDock;
174
174
  if (json.inquiryBoard) theme.inquiryBoard = json.inquiryBoard;
175
175
  if (json.siteNav) theme.siteNav = json.siteNav;
176
+ if (json.siteVerification) theme.siteVerification = json.siteVerification;
176
177
  return theme;
177
178
  }
178
179
  async function fetchAnalyticsConfig(options) {
@@ -373,6 +374,10 @@ async function fetchContentModels(options) {
373
374
  const cardinality = model.cardinality === "singleton" ? "singleton" : "collection";
374
375
  const preset = model.preset === "page" || model.preset === "article" || model.preset === "entity" ? model.preset : null;
375
376
  const rawPresentation = model.presentation && typeof model.presentation === "object" && !Array.isArray(model.presentation) ? model.presentation : {};
377
+ const listPaths = Array.isArray(rawPresentation.listPaths) ? rawPresentation.listPaths.filter(
378
+ (item) => typeof item === "string" && item.startsWith("/")
379
+ ) : [];
380
+ const withListPaths = listPaths.length > 0 ? { listPaths } : {};
376
381
  const presentation = rawPresentation.kind === "fixed_page" && typeof rawPresentation.path === "string" ? {
377
382
  kind: "fixed_page",
378
383
  path: rawPresentation.path,
@@ -383,13 +388,16 @@ async function fetchContentModels(options) {
383
388
  } : rawPresentation.kind === "detail" && typeof rawPresentation.detailPath === "string" ? {
384
389
  kind: "detail",
385
390
  detailPath: rawPresentation.detailPath,
386
- ...typeof rawPresentation.templateKey === "string" ? { templateKey: rawPresentation.templateKey } : {}
391
+ ...typeof rawPresentation.templateKey === "string" ? { templateKey: rawPresentation.templateKey } : {},
392
+ ...withListPaths
387
393
  } : rawPresentation.kind === "category_tree" && typeof rawPresentation.basePath === "string" && typeof rawPresentation.categoryDepth === "number" ? {
388
394
  kind: "category_tree",
389
395
  basePath: rawPresentation.basePath,
390
396
  categoryDepth: rawPresentation.categoryDepth,
391
- ...typeof rawPresentation.templateKey === "string" ? { templateKey: rawPresentation.templateKey } : {}
392
- } : { kind: "data_only" };
397
+ ...rawPresentation.entryCategory === "leaf" || rawPresentation.entryCategory === "deepest" ? { entryCategory: rawPresentation.entryCategory } : {},
398
+ ...typeof rawPresentation.templateKey === "string" ? { templateKey: rawPresentation.templateKey } : {},
399
+ ...withListPaths
400
+ } : { kind: "data_only", ...withListPaths };
393
401
  return [{
394
402
  key: model.key,
395
403
  label: model.label,
@@ -551,6 +559,9 @@ async function fetchCategoryCounts(options) {
551
559
  description: typeof o.description === "string" ? o.description : null,
552
560
  seoTitle: typeof o.seo_title === "string" ? o.seo_title : null,
553
561
  seoDescription: typeof o.seo_description === "string" ? o.seo_description : null,
562
+ seoOgImage: typeof o.seo_og_image === "string" ? o.seo_og_image : null,
563
+ seoOgImageAlt: typeof o.seo_og_image_alt === "string" ? o.seo_og_image_alt : null,
564
+ seoNofollow: typeof o.seo_nofollow === "boolean" ? o.seo_nofollow : null,
554
565
  imageMediaId: typeof o.image_media_id === "string" ? o.image_media_id : null,
555
566
  imageUrl: typeof o.image_url === "string" ? o.image_url : null,
556
567
  imageAlt: typeof o.image_alt === "string" ? o.image_alt : null,
@@ -1188,6 +1199,63 @@ function selectSitePatternForSlot(patternSlots, patterns, slot = POST_FOOTER_PAT
1188
1199
  return patterns.find((pattern) => pattern.key === key) ?? null;
1189
1200
  }
1190
1201
 
1202
+ // src/server-metadata.ts
1203
+ import { MetadataRobotsPolicySchema, MetadataValuesSchema } from "@roottale/cms-core";
1204
+ import { readMetadataValues, resolveMetadata, formatMetadataDefaults, metadataContentImageUrl, MetadataDefaultRulesSchema } from "@roottale/cms-core";
1205
+ var METADATA_CACHE_TAG = "rt-metadata";
1206
+ var SITE_METADATA_DEFAULTS_CACHE_TAG = "rt-metadata-defaults";
1207
+ function routeMetadataCacheTag(routeKey, locale) {
1208
+ if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(routeKey) || routeKey.length > 160) throw new Error("Invalid metadata route key");
1209
+ const language = locale?.trim().toLowerCase() || "default";
1210
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(language) || language.length > 40) {
1211
+ throw new Error("Invalid metadata locale");
1212
+ }
1213
+ return `${METADATA_CACHE_TAG}:${routeKey}:${language}`;
1214
+ }
1215
+ async function fetchRouteMetadata(options) {
1216
+ assertServer();
1217
+ const routeTag = routeMetadataCacheTag(options.routeKey, options.locale);
1218
+ const params = new URLSearchParams({ route_key: options.routeKey });
1219
+ if (options.locale) params.set("locale", options.locale);
1220
+ if (options.siteId) params.set("site_id", options.siteId);
1221
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
1222
+ const response = await fetch(`${baseUrl}/v1/cms/public/metadata?${params}`, buildGetInit(options.apiKey, {
1223
+ signal: options.signal,
1224
+ revalidate: options.revalidate ?? 300,
1225
+ tags: [METADATA_CACHE_TAG, SITE_METADATA_DEFAULTS_CACHE_TAG, routeTag]
1226
+ }));
1227
+ if (!response.ok) throw await toApiError(response);
1228
+ const json = await response.json();
1229
+ if (!json || typeof json !== "object" || !("metadata" in json)) throw unexpectedResponse(response.status);
1230
+ if (json.metadata === null) return null;
1231
+ if (!isRouteMetadata(json.metadata) || json.metadata.routeKey !== options.routeKey) throw unexpectedResponse(response.status);
1232
+ return json.metadata;
1233
+ }
1234
+ function isValues(value) {
1235
+ return MetadataValuesSchema.safeParse(value).success;
1236
+ }
1237
+ function isRouteMetadata(value) {
1238
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
1239
+ const row = value;
1240
+ return typeof row.routeKey === "string" && typeof row.locale === "string" && typeof row.path === "string" && row.path.startsWith("/") && !row.path.startsWith("//") && typeof row.version === "string" && isValues(row.overrides) && isValues(row.defaults) && isValues(row.effective) && (row.fallback === void 0 || isValues(row.fallback)) && (row.robots === void 0 || MetadataRobotsPolicySchema.safeParse(row.robots).success) && Array.isArray(row.supportedFields) && row.supportedFields.every((field) => ["title", "description", "ogImage", "ogImageAlt", "noindex", "nofollow"].includes(field));
1241
+ }
1242
+ function unexpectedResponse(status) {
1243
+ return new CmsApiError("RootTale CMS API returned invalid route metadata", status, "unexpected_response");
1244
+ }
1245
+
1246
+ // src/metadata-contract-checksum.ts
1247
+ function canonicalize(value) {
1248
+ if (Array.isArray(value)) return value.map(canonicalize);
1249
+ if (!value || typeof value !== "object") return value;
1250
+ return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, child]) => [key, canonicalize(child)]));
1251
+ }
1252
+ async function metadataContractChecksum(normalizedContract) {
1253
+ const json = JSON.stringify(canonicalize(normalizedContract));
1254
+ if (json === void 0) throw new Error("Metadata contract must be a normalized JSON value");
1255
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(json));
1256
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
1257
+ }
1258
+
1191
1259
  // src/server-cache-tags.ts
1192
1260
  var SETTINGS_CACHE_TAGS = [
1193
1261
  THEME_CACHE_TAG,
@@ -1196,7 +1264,9 @@ var SETTINGS_CACHE_TAGS = [
1196
1264
  BLOG_SETTINGS_CACHE_TAG,
1197
1265
  COLLECTIONS_CACHE_TAG,
1198
1266
  EXPOSURE_CACHE_TAG,
1199
- SITE_PATTERNS_CACHE_TAG
1267
+ SITE_PATTERNS_CACHE_TAG,
1268
+ METADATA_CACHE_TAG,
1269
+ SITE_METADATA_DEFAULTS_CACHE_TAG
1200
1270
  ];
1201
1271
 
1202
1272
  // src/seo.ts
@@ -1477,6 +1547,15 @@ function escapeXml(value) {
1477
1547
  function cdata(value) {
1478
1548
  return `<![CDATA[${value.replace(/]]>/g, "]]]]><![CDATA[>")}]]>`;
1479
1549
  }
1550
+ function siteVerificationMetadata(settings) {
1551
+ const google = settings?.google?.filter(Boolean) ?? [];
1552
+ const naver = settings?.naver?.filter(Boolean) ?? [];
1553
+ if (google.length === 0 && naver.length === 0) return void 0;
1554
+ return {
1555
+ ...google.length > 0 ? { google } : {},
1556
+ ...naver.length > 0 ? { other: { "naver-site-verification": naver } } : {}
1557
+ };
1558
+ }
1480
1559
 
1481
1560
  // src/inquiry.ts
1482
1561
  var DEFAULT_BASE_URL2 = "https://api.roottale.com";
@@ -1634,8 +1713,11 @@ export {
1634
1713
  LOCAL_BUSINESS_MAX_SERVICES,
1635
1714
  LOCAL_BUSINESS_SERVICE_MAX_LENGTH,
1636
1715
  MENUS_CACHE_TAG,
1716
+ METADATA_CACHE_TAG,
1717
+ MetadataDefaultRulesSchema,
1637
1718
  POST_FOOTER_PATTERN_SLOT,
1638
1719
  SETTINGS_CACHE_TAGS,
1720
+ SITE_METADATA_DEFAULTS_CACHE_TAG,
1639
1721
  SITE_PATTERNS_CACHE_TAG,
1640
1722
  THEME_CACHE_TAG,
1641
1723
  articleSchema,
@@ -1662,28 +1744,36 @@ export {
1662
1744
  fetchPostPreview,
1663
1745
  fetchPosts,
1664
1746
  fetchRedirects,
1747
+ fetchRouteMetadata,
1665
1748
  fetchSiteInquiries,
1666
1749
  fetchSiteInquiry,
1667
1750
  fetchSitePatterns,
1668
1751
  fetchTheme,
1669
1752
  formatKoreanPhoneNumber,
1753
+ formatMetadataDefaults,
1670
1754
  generateRssXml,
1671
1755
  inquiryErrorMessage,
1672
1756
  isPreviewExpiredError,
1673
1757
  jsonLdJson,
1674
1758
  jsonLdScript,
1675
1759
  localBusinessSchema,
1760
+ metadataContentImageUrl,
1761
+ metadataContractChecksum,
1676
1762
  organizationSchema,
1677
1763
  parseAttributionJson,
1678
1764
  parseInquiryRedirect,
1679
1765
  profilePageSchema,
1766
+ readMetadataValues,
1767
+ resolveMetadata,
1680
1768
  resolvePostDisplay,
1681
1769
  resolveSearchHitPath,
1770
+ routeMetadataCacheTag,
1682
1771
  rssItemsFromPosts,
1683
1772
  searchPosts,
1684
1773
  selectRelatedPosts,
1685
1774
  selectSitePatternForSlot,
1686
1775
  sitePatternPresentationAttributes,
1776
+ siteVerificationMetadata,
1687
1777
  submitFormInquiry,
1688
1778
  submitInquiry,
1689
1779
  submitSiteInquiry,