@roottale/cms-renderer-next 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.
@@ -0,0 +1,94 @@
1
+ Copyright (c) 2021, Kil Hyung-jin (https://github.com/orioncactus/pretendard),
2
+ with Reserved Font Name Pretendard.
3
+
4
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
5
+ This license is copied below, and is also available with a FAQ at:
6
+ https://scripts.sil.org/OFL
7
+
8
+
9
+ -----------------------------------------------------------
10
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
11
+ -----------------------------------------------------------
12
+
13
+ PREAMBLE
14
+ The goals of the Open Font License (OFL) are to stimulate worldwide
15
+ development of collaborative font projects, to support the font creation
16
+ efforts of academic and linguistic communities, and to provide a free and
17
+ open framework in which fonts may be shared and improved in partnership
18
+ with others.
19
+
20
+ The OFL allows the licensed fonts to be used, studied, modified and
21
+ redistributed freely as long as they are not sold by themselves. The
22
+ fonts, including any derivative works, can be bundled, embedded,
23
+ redistributed and/or sold with any software provided that any reserved
24
+ names are not used by derivative works. The fonts and derivatives,
25
+ however, cannot be released under any other type of license. The
26
+ requirement for fonts to remain under this license does not apply
27
+ to any document created using the fonts or their derivatives.
28
+
29
+ DEFINITIONS
30
+ "Font Software" refers to the set of files released by the Copyright
31
+ Holder(s) under this license and clearly marked as such. This may
32
+ include source files, build scripts and documentation.
33
+
34
+ "Reserved Font Name" refers to any names specified as such after the
35
+ copyright statement(s).
36
+
37
+ "Original Version" refers to the collection of Font Software components as
38
+ distributed by the Copyright Holder(s).
39
+
40
+ "Modified Version" refers to any derivative made by adding to, deleting,
41
+ or substituting -- in part or in whole -- any of the components of the
42
+ Original Version, by changing formats or by porting the Font Software to a
43
+ new environment.
44
+
45
+ "Author" refers to any designer, engineer, programmer, technical
46
+ writer or other person who contributed to the Font Software.
47
+
48
+ PERMISSION & CONDITIONS
49
+ Permission is hereby granted, free of charge, to any person obtaining
50
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
51
+ redistribute, and sell modified and unmodified copies of the Font
52
+ Software, subject to the following conditions:
53
+
54
+ 1) Neither the Font Software nor any of its individual components,
55
+ in Original or Modified Versions, may be sold by itself.
56
+
57
+ 2) Original or Modified Versions of the Font Software may be bundled,
58
+ redistributed and/or sold with any software, provided that each copy
59
+ contains the above copyright notice and this license. These can be
60
+ included either as stand-alone text files, human-readable headers or
61
+ in the appropriate machine-readable metadata fields within text or
62
+ binary files as long as those fields can be easily viewed by the user.
63
+
64
+ 3) No Modified Version of the Font Software may use the Reserved Font
65
+ Name(s) unless explicit written permission is granted by the corresponding
66
+ Copyright Holder. This restriction only applies to the primary font name as
67
+ presented to the users.
68
+
69
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
70
+ Software shall not be used to promote, endorse or advertise any
71
+ Modified Version, except to acknowledge the contribution(s) of the
72
+ Copyright Holder(s) and the Author(s) or with their explicit written
73
+ permission.
74
+
75
+ 5) The Font Software, modified or unmodified, in part or in whole,
76
+ must be distributed entirely under this license, and must not be
77
+ distributed under any other license. The requirement for fonts to
78
+ remain under this license does not apply to any document created
79
+ using the Font Software.
80
+
81
+ TERMINATION
82
+ This license becomes null and void if any of the above conditions are
83
+ not met.
84
+
85
+ DISCLAIMER
86
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
87
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
88
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
89
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
90
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
91
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
92
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
93
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
94
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,10 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ /** HTML·Tiptap 본문 모두에 적용한다. 이미 링크나 버튼인 이미지는 원래 동작을 유지한다. */
5
+ declare function RootTaleImageLightbox({ children, imageSelector }: {
6
+ children: ReactNode;
7
+ imageSelector?: string;
8
+ }): react_jsx_runtime.JSX.Element;
9
+
10
+ export { RootTaleImageLightbox };
@@ -0,0 +1,8 @@
1
+ "use client";
2
+ import {
3
+ RootTaleImageLightbox
4
+ } from "./chunk-22MB5YER.js";
5
+ export {
6
+ RootTaleImageLightbox
7
+ };
8
+ //# sourceMappingURL=image-lightbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/dist/routes.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { CmsPostContent, RootTaleRedirect, CmsPostTranslationRef } from '@roottale/cms-client/server';
2
- import { RouteCollection, RoutablePost } from '@roottale/cms-core';
3
- export { RouteCollection, resolvePostCollection, resolvePostUrl } from '@roottale/cms-core';
4
- import { B as BlogCategoryWithCount } from './blog-filter-qE9Lj49C.js';
5
- export { m as makeCategoryHref } from './blog-filter-qE9Lj49C.js';
2
+ import { RouteCollection, MetadataRobotsPolicy, MetadataValues, MetadataResolutionInput, RoutablePost } from '@roottale/cms-core';
3
+ export { RouteCollection, normalizeRedirectPath, resolvePostCollection, resolvePostUrl } from '@roottale/cms-core';
4
+ import { B as BlogCategoryWithCount } from './blog-filter-CS4GZws8.js';
5
+ export { m as makeCategoryHref } from './blog-filter-CS4GZws8.js';
6
6
 
7
7
  /**
8
8
  * `routes` 공통 — config 타입, collections resolve, fetch 헬퍼. feed/sitemap/llms/
@@ -428,12 +428,6 @@ interface RedirectMiddlewareConfig {
428
428
  /** 규칙 캐시 TTL(ms). 기본 60_000 (1분). */
429
429
  cacheTtlMs?: number;
430
430
  }
431
- /**
432
- * 경로 정규화 — 앞 슬래시 보장, 뒤 슬래시 제거(루트 제외), 쿼리/해시 제거,
433
- * percent-decode. 출발 경로 비교·한글 slug 매칭·루프 탐지에 요청·규칙 양쪽에
434
- * 동일 적용한다. 쿼리 제거는 `/a → /a?x` 류의 무한 루프 방지에 중요.
435
- */
436
- declare function normalizeRedirectPath(path: string): string;
437
431
  /**
438
432
  * 요청 경로에 맞는 규칙을 찾는다(순수 함수, fetch 비의존 — 테스트 용이).
439
433
  * 정확 경로 일치만(와일드카드 없음).
@@ -541,6 +535,12 @@ interface PostMetadataInput {
541
535
  authors?: readonly string[] | null;
542
536
  }
543
537
  interface BuildPostMetadataOptions {
538
+ /** 기존 canonical override는 기본 유지. 코드가 URL을 소유하는 사이트는 false로 지정한다. */
539
+ allowCanonicalOverride?: boolean;
540
+ /** 미리보기·비공개 등의 강제 제한은 직접 SEO 설정으로 해제하지 않는다. */
541
+ robots?: MetadataRobotsPolicy;
542
+ /** 글 값도 비었을 때 적용할 사이트 설명·공유 이미지. */
543
+ siteDefaults?: MetadataValues;
544
544
  /**
545
545
  * self-canonical 기본값 origin (예: `https://example.com`).
546
546
  * `seo.canonical` override 가 있으면 무시된다. `path` 또는 `collections` 와
@@ -711,6 +711,89 @@ declare function buildPostMetadata(post: PostMetadataInput, options?: BuildPostM
711
711
  */
712
712
  declare function buildHreflangLanguages(translations: readonly CmsPostTranslationRef[] | undefined, siteUrl: string, locales: PostLocaleOptions): Record<string, string> | undefined;
713
713
 
714
+ interface MetadataGoogleBotPolicy {
715
+ index?: boolean;
716
+ follow?: boolean;
717
+ noimageindex?: boolean;
718
+ "max-image-preview"?: "none" | "standard" | "large";
719
+ "max-snippet"?: number;
720
+ "max-video-preview"?: number;
721
+ }
722
+ interface BuildPageMetadataOptions {
723
+ siteUrl?: string;
724
+ path?: string;
725
+ /** 문자열은 사이트의 최종 정책. null이면 생략하고, undefined이면 siteUrl+path를 사용한다. */
726
+ canonical?: string | null;
727
+ /** 예: " | 사이트 이름". 이미 붙어 있거나 제목이 사이트 이름이면 다시 붙이지 않는다. */
728
+ titleSuffix?: string;
729
+ openGraphType?: "website" | "article";
730
+ /** 사이트가 기존에 적용하던 봇별 제한. false 제한은 CMS 설정으로 해제하지 않는다. */
731
+ googleBot?: MetadataGoogleBotPolicy;
732
+ /** 실제 SSR 결과와 현재 저장 버전을 대조하는 공개 확인 표식. */
733
+ verification?: {
734
+ routeKey: string;
735
+ locale: string;
736
+ version?: string;
737
+ contractChecksum?: string;
738
+ };
739
+ }
740
+ interface PageMetadata {
741
+ title: {
742
+ absolute: string;
743
+ };
744
+ description?: string;
745
+ alternates?: {
746
+ canonical: string;
747
+ };
748
+ robots: {
749
+ index: boolean;
750
+ follow: boolean;
751
+ googleBot?: MetadataGoogleBotPolicy;
752
+ };
753
+ openGraph: {
754
+ type: "website" | "article";
755
+ title: string;
756
+ description?: string;
757
+ url?: string;
758
+ images?: {
759
+ url: string;
760
+ alt: string;
761
+ }[];
762
+ };
763
+ twitter: {
764
+ card: "summary_large_image";
765
+ title: string;
766
+ description?: string;
767
+ images?: {
768
+ url: string;
769
+ alt: string;
770
+ }[];
771
+ };
772
+ other?: Record<string, string>;
773
+ }
774
+ /** 글·분류·코드 페이지의 동일한 최종값을 Next Metadata로 변환한다. 네트워크 호출은 없다. */
775
+ declare function buildPageMetadata(input: MetadataResolutionInput, options?: BuildPageMetadataOptions): PageMetadata;
776
+
777
+ interface SitemapMetadataPolicy {
778
+ /** SSR buildPageMetadata에 전달한 것과 같은 입력을 사용한다. */
779
+ metadata?: MetadataResolutionInput;
780
+ /** 사이트의 최종 canonical. 다른 URL이 대표 주소면 현재 entry를 제외한다. */
781
+ canonical?: string | null;
782
+ published?: boolean;
783
+ }
784
+ /**
785
+ * 이미 조회한 메타데이터·활성 이동 규칙을 사용한다. 네트워크 호출을 하지 않으며
786
+ * 조회 실패와 빈 설정을 구별하는 책임은 호출자에게 있다(미확인 값은 기존 포함 유지).
787
+ */
788
+ declare function filterMetadataSitemapEntries<T extends {
789
+ url: string;
790
+ }>(entries: readonly T[], policyFor: (entry: T) => SitemapMetadataPolicy | undefined, options?: {
791
+ redirects?: readonly {
792
+ fromPath: string;
793
+ enabled?: boolean;
794
+ }[];
795
+ }): T[];
796
+
714
797
  /**
715
798
  * 글의 공개 URL — ADR-0105 Amendment 1.
716
799
  *
@@ -1076,4 +1159,4 @@ interface FleetInfoRouteConfig {
1076
1159
  */
1077
1160
  declare function createFleetInfoRoute(config?: FleetInfoRouteConfig): () => Promise<Response>;
1078
1161
 
1079
- export { ARCHIVE_POST_COUNT_LIMIT, type AlwaysNoindexArchiveKind, type ArchiveCategoriesConfig, type ArchiveCategoriesOptions, type ArchiveIndexDecision, type ArchiveIndexInput, type ArchiveKind, type ArchiveMetadata, type ArchiveMetadataFields, type ArchiveMetadataInput, type ArchivePromotion, type BuildArchiveMetadataOptions, type BuildPostMetadataOptions, type CollectionsInput, type ExposureRouteConfig, type FleetInfoRouteConfig, type ImageResponseLike, type LlmsTxtLink, type LlmsTxtRouteConfig, type LlmsTxtSection, OG_IMAGE_CONTENT_TYPE, OG_IMAGE_SIZE, type OgImageRouteConfig, type PostLocaleOptions, type PostMetadata, type PostMetadataInput, type PostSeoOverrides, type PreviewMetadata, type PromotableArchiveKind, type PublicUrlPost, type RedirectMiddlewareConfig, type RevalidateRejectReason, type RevalidateRelatedPathsContext, type RevalidateRouteConfig, type RobotsMeta, type RootTaleSiteConfig, type SitemapEntry, type SitemapIndexOptions, type SitemapSectionId, buildArchiveMetadata, buildHreflangLanguages, buildPostMetadata, buildPreviewMetadata, createExposureRoute, createFeedRoute, createFleetInfoRoute, createLlmsTxtRoute, createPostOgImage, createRedirectMiddleware, createRevalidateRoute, createRevalidateRouteHandlers, createSitemap, createSitemapIndex, decideArchiveIndex, fetchBlogArchiveCategories, fetchBlogArchivePosts, matchRedirect, normalizeRedirectPath, postRedirectPath, resolveAuthorPromotion, resolveCategoryPromotion, resolveCollections, resolveCollectionsResult, resolveRobotsMeta, safeFetchPosts, storedPostPath, trimSlash };
1162
+ export { ARCHIVE_POST_COUNT_LIMIT, type AlwaysNoindexArchiveKind, type ArchiveCategoriesConfig, type ArchiveCategoriesOptions, type ArchiveIndexDecision, type ArchiveIndexInput, type ArchiveKind, type ArchiveMetadata, type ArchiveMetadataFields, type ArchiveMetadataInput, type ArchivePromotion, type BuildArchiveMetadataOptions, type BuildPageMetadataOptions, type BuildPostMetadataOptions, type CollectionsInput, type ExposureRouteConfig, type FleetInfoRouteConfig, type ImageResponseLike, type LlmsTxtLink, type LlmsTxtRouteConfig, type LlmsTxtSection, type MetadataGoogleBotPolicy, OG_IMAGE_CONTENT_TYPE, OG_IMAGE_SIZE, type OgImageRouteConfig, type PageMetadata, type PostLocaleOptions, type PostMetadata, type PostMetadataInput, type PostSeoOverrides, type PreviewMetadata, type PromotableArchiveKind, type PublicUrlPost, type RedirectMiddlewareConfig, type RevalidateRejectReason, type RevalidateRelatedPathsContext, type RevalidateRouteConfig, type RobotsMeta, type RootTaleSiteConfig, type SitemapEntry, type SitemapIndexOptions, type SitemapMetadataPolicy, type SitemapSectionId, buildArchiveMetadata, buildHreflangLanguages, buildPageMetadata, buildPostMetadata, buildPreviewMetadata, createExposureRoute, createFeedRoute, createFleetInfoRoute, createLlmsTxtRoute, createPostOgImage, createRedirectMiddleware, createRevalidateRoute, createRevalidateRouteHandlers, createSitemap, createSitemapIndex, decideArchiveIndex, fetchBlogArchiveCategories, fetchBlogArchivePosts, filterMetadataSitemapEntries, matchRedirect, postRedirectPath, resolveAuthorPromotion, resolveCategoryPromotion, resolveCollections, resolveCollectionsResult, resolveRobotsMeta, safeFetchPosts, storedPostPath, trimSlash };
package/dist/routes.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  safeFetchPosts,
11
11
  storedPostPath,
12
12
  trimSlash
13
- } from "./chunk-GDP2VGJR.js";
13
+ } from "./chunk-3UBTE7BV.js";
14
14
  import {
15
15
  exposureHomePaths,
16
16
  isExposureHome
@@ -30,7 +30,7 @@ import {
30
30
  } from "@roottale/cms-core";
31
31
 
32
32
  // src/routes-metadata.ts
33
- import { resolvePostPath } from "@roottale/cms-core";
33
+ import { resolveMetadata, resolvePostPath } from "@roottale/cms-core";
34
34
  function resolveRobotsMeta(flags) {
35
35
  if (!flags.noindex && !flags.nofollow) return void 0;
36
36
  return { index: !flags.noindex, follow: !flags.nofollow };
@@ -45,10 +45,17 @@ function buildPreviewMetadata(input) {
45
45
  }
46
46
  function buildPostMetadata(post, options = {}) {
47
47
  const seo = post.seo ?? void 0;
48
- const title = seo?.title || post.title;
49
- const description = seo?.description || post.description || void 0;
50
- const ogImage = seo?.ogImage || post.image || void 0;
51
- let canonical = seo?.canonical || void 0;
48
+ const resolved = resolveMetadata({
49
+ overrides: seo,
50
+ defaults: { title: post.title, ...post.description ? { description: post.description } : {}, ...post.image ? { ogImage: post.image } : {} },
51
+ siteDefaults: options.siteDefaults,
52
+ robots: options.robots,
53
+ imageBaseUrl: options.siteUrl
54
+ });
55
+ const title = resolved.title ?? post.title;
56
+ const description = resolved.description;
57
+ const ogImage = resolved.ogImage;
58
+ let canonical = options.allowCanonicalOverride === false ? void 0 : seo?.canonical || void 0;
52
59
  if (!canonical && options.siteUrl) {
53
60
  let path = options.path ?? storedPostPath({
54
61
  path: post.path,
@@ -75,7 +82,7 @@ function buildPostMetadata(post, options = {}) {
75
82
  if (post.tags && post.tags.length > 0) openGraph.tags = [...post.tags];
76
83
  if (post.authors && post.authors.length > 0) openGraph.authors = [...post.authors];
77
84
  if (ogImage) {
78
- openGraph.images = [{ url: ogImage, alt: seo?.ogImageAlt || title }];
85
+ openGraph.images = [{ url: ogImage, alt: resolved.ogImageAlt ?? title }];
79
86
  }
80
87
  const meta = { title, openGraph };
81
88
  if (description) meta.description = description;
@@ -101,8 +108,8 @@ function buildPostMetadata(post, options = {}) {
101
108
  };
102
109
  }
103
110
  const baseRobots = resolveRobotsMeta({
104
- ...seo?.noindex !== void 0 && { noindex: seo.noindex },
105
- ...seo?.nofollow !== void 0 && { nofollow: seo.nofollow }
111
+ ...resolved.noindex !== void 0 && { noindex: resolved.noindex },
112
+ ...resolved.nofollow !== void 0 && { nofollow: resolved.nofollow }
106
113
  });
107
114
  const index = baseRobots?.index ?? true;
108
115
  const follow = baseRobots?.follow ?? true;
@@ -209,7 +216,9 @@ import {
209
216
  import {
210
217
  isInCollectionMode,
211
218
  resolveCategoryPath,
212
- resolvePostCollection as resolvePostCollection2
219
+ resolvePostCollection as resolvePostCollection2,
220
+ readMetadataValues,
221
+ resolveMetadata as resolveMetadata2
213
222
  } from "@roottale/cms-core";
214
223
 
215
224
  // src/archive-categories.ts
@@ -251,6 +260,11 @@ async function fetchServerCategoryCounts(config, options) {
251
260
  ...category.description ? { description: category.description } : {},
252
261
  ...category.seoTitle ? { seoTitle: category.seoTitle } : {},
253
262
  ...category.seoDescription ? { seoDescription: category.seoDescription } : {},
263
+ ...category.seoOgImage ? { seoOgImage: category.seoOgImage } : {},
264
+ ...category.seoOgImageAlt ? { seoOgImageAlt: category.seoOgImageAlt } : {},
265
+ ...typeof category.seoNofollow === "boolean" ? { seoNofollow: category.seoNofollow } : {},
266
+ ...typeof category.hubPromoted === "boolean" ? { hubPromoted: category.hubPromoted } : {},
267
+ ...category.path ? { path: category.path } : {},
254
268
  ...category.imageUrl ? { imageUrl: category.imageUrl } : {},
255
269
  ...category.imageAlt ? { imageAlt: category.imageAlt } : {}
256
270
  }));
@@ -347,6 +361,7 @@ function postEntries(config, siteUrl, posts, collections, settings, defaultLocal
347
361
  const inCollection = isInCollectionMode(collections);
348
362
  const blogBase = config.blogBasePath ?? "/blog";
349
363
  return posts.flatMap((p) => {
364
+ if (resolveMetadata2({ overrides: readMetadataValues(p.metaJson.seo) }).noindex === true) return [];
350
365
  const url = postPublicUrl(p, {
351
366
  siteUrl,
352
367
  collections,
@@ -627,6 +642,15 @@ function modelPathMismatch(body, collections) {
627
642
  function createRevalidateRoute(config) {
628
643
  const blogBase = config.blogBasePath ?? "/blog";
629
644
  const maxBodyBytes = config.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
645
+ const INVARIANTS = [
646
+ "/feed.xml",
647
+ "/llms.txt",
648
+ "/sitemap.xml",
649
+ "/sitemap/static.xml",
650
+ "/sitemap/blog.xml",
651
+ "/sitemap/categories.xml",
652
+ "/sitemap/authors.xml"
653
+ ];
630
654
  return async function POST(req) {
631
655
  const apiKey = config.apiKey?.trim();
632
656
  if (!apiKey || apiKey === "local_unconfigured") {
@@ -657,7 +681,7 @@ function createRevalidateRoute(config) {
657
681
  }
658
682
  }
659
683
  const parsed = body ?? { paths: [], postId: null, modelKey: null };
660
- if (result.event === "theme.updated") {
684
+ if (result.event === "theme.updated" || result.event === "taxonomy.updated") {
661
685
  const themeTag = config.themeTag ?? THEME_CACHE_TAG;
662
686
  const requestedTags = [
663
687
  .../* @__PURE__ */ new Set([
@@ -665,7 +689,7 @@ function createRevalidateRoute(config) {
665
689
  ...SETTINGS_CACHE_TAGS.filter((t) => t !== THEME_CACHE_TAG)
666
690
  ])
667
691
  ];
668
- const paths2 = parsed.paths;
692
+ const paths2 = result.event === "taxonomy.updated" ? [.../* @__PURE__ */ new Set([...parsed.paths, ...INVARIANTS, ...config.alsoRevalidate ?? []])] : parsed.paths;
669
693
  const failed = [];
670
694
  const attempt = async (target, run) => {
671
695
  try {
@@ -674,7 +698,7 @@ function createRevalidateRoute(config) {
674
698
  const reason = e instanceof Error ? e.message : String(e);
675
699
  failed.push({ target, reason });
676
700
  console.error(
677
- `[@roottale/cms-renderer-next/routes] theme.updated \uBB34\uD6A8\uD654 \uC2E4\uD328(${target}):`,
701
+ `[@roottale/cms-renderer-next/routes] ${result.event} \uBB34\uD6A8\uD654 \uC2E4\uD328(${target}):`,
678
702
  reason
679
703
  );
680
704
  }
@@ -709,17 +733,6 @@ function createRevalidateRoute(config) {
709
733
  failed.length === 0 ? 200 : 500
710
734
  );
711
735
  }
712
- const INVARIANTS = [
713
- "/feed.xml",
714
- "/llms.txt",
715
- "/sitemap.xml",
716
- "/sitemap/static.xml",
717
- "/sitemap/blog.xml",
718
- "/sitemap/categories.xml",
719
- // 작가 사이트맵 — 글 발행/해제로 작가 등장·lastmod 가 바뀐다(authors 토글 off 면
720
- // 빈 사이트맵이라 무효화해도 무해).
721
- "/sitemap/authors.xml"
722
- ];
723
736
  let alsoRevalidate;
724
737
  let warning;
725
738
  let knownCollections;
@@ -941,18 +954,9 @@ function createPostOgImage(config, deps) {
941
954
  }
942
955
 
943
956
  // src/routes-redirects.ts
957
+ import { normalizeRedirectPath, normalizeRedirectSource, normalizeRedirectTarget, isProtectedRedirectPath } from "@roottale/cms-core";
958
+ import { normalizeRedirectPath as normalizeRedirectPath2 } from "@roottale/cms-core";
944
959
  import { fetchRedirects } from "@roottale/cms-client/server";
945
- function normalizeRedirectPath(path) {
946
- let p = path;
947
- try {
948
- p = decodeURIComponent(path);
949
- } catch {
950
- }
951
- p = p.split(/[?#]/)[0] ?? p;
952
- if (!p.startsWith("/")) p = `/${p}`;
953
- if (p.length > 1) p = p.replace(/\/+$/, "");
954
- return p === "" ? "/" : p;
955
- }
956
960
  function isAbsoluteUrl(target) {
957
961
  return /^https?:\/\//i.test(target);
958
962
  }
@@ -967,10 +971,13 @@ function internalTargetPath(target, origin) {
967
971
  }
968
972
  }
969
973
  function matchRedirect(redirects, pathname, origin) {
974
+ if (isProtectedRedirectPath(pathname)) return null;
970
975
  const byFrom = /* @__PURE__ */ new Map();
971
976
  for (const r of redirects) {
972
- const key = normalizeRedirectPath(r.fromPath);
973
- if (!byFrom.has(key)) byFrom.set(key, r);
977
+ const source = normalizeRedirectSource(r.fromPath);
978
+ const target = normalizeRedirectTarget(r.toTarget);
979
+ if (!source.ok || !target.ok) continue;
980
+ if (!byFrom.has(source.value)) byFrom.set(source.value, { ...r, toTarget: target.value });
974
981
  }
975
982
  const start = normalizeRedirectPath(pathname);
976
983
  const visited = /* @__PURE__ */ new Set([start]);
@@ -1042,6 +1049,75 @@ function createRedirectMiddleware(config) {
1042
1049
  };
1043
1050
  }
1044
1051
 
1052
+ // src/routes-page-metadata.ts
1053
+ import { resolveMetadata as resolveMetadata3 } from "@roottale/cms-core";
1054
+ function buildPageMetadata(input, options = {}) {
1055
+ const canonical = options.canonical === void 0 ? options.siteUrl && options.path ? new URL(options.path, options.siteUrl).href : void 0 : options.canonical ?? void 0;
1056
+ const values = resolveMetadata3({ ...input, imageBaseUrl: input.imageBaseUrl ?? options.siteUrl ?? canonical });
1057
+ const baseTitle = values.title ?? "";
1058
+ const suffix = options.titleSuffix ?? "";
1059
+ const siteName = suffix.replace(/^[\s|·—–\-:]+/, "").trim();
1060
+ const title = suffix && baseTitle !== siteName && !baseTitle.endsWith(suffix) ? `${baseTitle}${suffix}` : baseTitle;
1061
+ const description = values.description;
1062
+ const images = values.ogImage ? [{ url: values.ogImage, alt: values.ogImageAlt ?? title }] : void 0;
1063
+ const index = values.noindex !== true;
1064
+ const follow = values.nofollow !== true;
1065
+ return {
1066
+ title: { absolute: title },
1067
+ ...description ? { description } : {},
1068
+ ...canonical ? { alternates: { canonical } } : {},
1069
+ robots: {
1070
+ index,
1071
+ follow,
1072
+ ...options.googleBot ? { googleBot: {
1073
+ ...options.googleBot,
1074
+ index: index && options.googleBot.index !== false,
1075
+ follow: follow && options.googleBot.follow !== false
1076
+ } } : {}
1077
+ },
1078
+ openGraph: {
1079
+ type: options.openGraphType ?? "website",
1080
+ title,
1081
+ ...description ? { description } : {},
1082
+ ...canonical ? { url: canonical } : {},
1083
+ ...images ? { images } : {}
1084
+ },
1085
+ twitter: { card: "summary_large_image", title, ...description ? { description } : {}, ...images ? { images } : {} },
1086
+ ...options.verification ? { other: {
1087
+ "rt:metadata-route-key": options.verification.routeKey,
1088
+ "rt:metadata-locale": options.verification.locale,
1089
+ ...options.verification.version ? { "rt:metadata-version": options.verification.version } : {},
1090
+ ...options.verification.contractChecksum ? { "rt:metadata-contract": options.verification.contractChecksum } : {}
1091
+ } } : {}
1092
+ };
1093
+ }
1094
+
1095
+ // src/routes-metadata-sitemap.ts
1096
+ import { resolveMetadata as resolveMetadata4 } from "@roottale/cms-core";
1097
+ function filterMetadataSitemapEntries(entries, policyFor, options = {}) {
1098
+ const redirected = new Set((options.redirects ?? []).filter((rule) => rule.enabled !== false).map((rule) => normalizeRedirectPath2(rule.fromPath)));
1099
+ return entries.filter((entry) => {
1100
+ let url;
1101
+ try {
1102
+ url = new URL(entry.url);
1103
+ } catch {
1104
+ return false;
1105
+ }
1106
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) return false;
1107
+ if (redirected.has(normalizeRedirectPath2(url.pathname))) return false;
1108
+ const policy = policyFor(entry);
1109
+ if (policy?.published === false || resolveMetadata4(policy?.metadata ?? {}).noindex === true) return false;
1110
+ if (policy?.canonical) {
1111
+ try {
1112
+ if (new URL(policy.canonical, url).href !== url.href) return false;
1113
+ } catch {
1114
+ return false;
1115
+ }
1116
+ }
1117
+ return true;
1118
+ });
1119
+ }
1120
+
1045
1121
  // src/routes.ts
1046
1122
  function postRedirectPath(post, requestedSlug, blogBasePath = "/blog") {
1047
1123
  let requested = requestedSlug;
@@ -1060,7 +1136,7 @@ function createFleetInfoRoute(config = {}) {
1060
1136
  site: config.site ?? env.VERCEL_PROJECT_PRODUCTION_URL ?? null,
1061
1137
  framework: "next",
1062
1138
  cms_client: CMS_CLIENT_VERSION,
1063
- cms_renderer: true ? "0.65.1" : "dev",
1139
+ cms_renderer: true ? "0.67.0" : "dev",
1064
1140
  build_sha: env.VERCEL_GIT_COMMIT_SHA?.slice(0, 7) ?? null,
1065
1141
  ...config.exposures ? { exposures: config.exposures } : {}
1066
1142
  },
@@ -1074,6 +1150,7 @@ export {
1074
1150
  OG_IMAGE_SIZE,
1075
1151
  buildArchiveMetadata,
1076
1152
  buildHreflangLanguages,
1153
+ buildPageMetadata,
1077
1154
  buildPostMetadata,
1078
1155
  buildPreviewMetadata,
1079
1156
  createExposureRoute,
@@ -1089,9 +1166,10 @@ export {
1089
1166
  decideArchiveIndex,
1090
1167
  fetchBlogArchiveCategories,
1091
1168
  fetchBlogArchivePosts,
1169
+ filterMetadataSitemapEntries,
1092
1170
  makeCategoryHref,
1093
1171
  matchRedirect,
1094
- normalizeRedirectPath,
1172
+ normalizeRedirectPath2 as normalizeRedirectPath,
1095
1173
  postRedirectPath,
1096
1174
  resolveAuthorPromotion,
1097
1175
  resolveCategoryPromotion,