@roottale/cms-renderer-next 0.46.0 → 0.48.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,53 @@
1
1
  # @roottale/cms-renderer-next
2
2
 
3
+ ## 0.48.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a4c7070: avcd 구조 — `RootTaleBlogPost`에 opt-in `breadcrumb` prop(시각 브레드크럼 +
8
+ `BreadcrumbList` JSON-LD) 추가, `author_slug` 있으면 작성자 이름을
9
+ `/blog/author/{slug}`로 자동 링크(`authorHref`로 override 가능). 헤더 발행일
10
+ 마크업이 `<span>`에서 `dateTime` 속성을 가진 `<time>`으로 변경(class·
11
+ data-rt-cms-meta 속성은 유지). `buildPostMetadata`에 Twitter Card 자동 emit,
12
+ RSS alternate(`feedPath`, 기본 `/feed.xml`), og article 확장(`modified`·
13
+ `section`·`tags`·`authors`) 추가.
14
+
15
+ ⚠️ 카테고리 칩·`RootTaleBlogCategories`·브레드크럼 카테고리 링크가 모두
16
+ `/blog/categories/{slug}`를 기본 링크한다 — 단일 블로그 사이트도 이 라우트를
17
+ 구현해야 404가 나지 않는다(참조 구현: `@roottale/cms-mcp` 예시
18
+ `app/blog/categories/[slug]/page.tsx`).
19
+
20
+ - ab5241a: 읽기 표면 조판 개선 — 네이버 프리미엄 콘텐츠·avcd 기준: 문단 간격 확대(1.35em), h2 섹션 대여백, h3/h4 크기 위계 수정, 리스트 간격·액센트 마커, 인용구 라운드 카드화, 이미지 여백·캡션 스타일, 표 라운드 외곽+수평 구분선, hr 여백 확대
21
+
22
+ ### Patch Changes
23
+
24
+ - Updated dependencies [a4c7070]
25
+ - @roottale/cms-client@0.48.0
26
+
27
+ ## 0.47.0
28
+
29
+ ### Minor Changes
30
+
31
+ - bfe0d14: 측정 계측 체계 v2(ADR-0038/ADR-0048 amend) 서버 수용 배선 — 유입 어트리뷰션에
32
+ utm_term/utm_content/광고 클릭 플래그/문의 직전 유입(last-touch) 필드를
33
+ 추가하고, 방문 여정(journey) JSON을 문의 제출에 함께 전달할 수 있게 했습니다.
34
+ - `@roottale/cms-client`: `InquiryAttribution`에 `utm_term`·`utm_content`·
35
+ `ad_click`·`last_touch_*` 필드 추가, `readAttribution()`/
36
+ `parseAttributionJson()`가 새 필드를 읽습니다. `SubmitInquiryFields`에
37
+ `journey?: string`를 추가해 방문 여정 원문을 `attr_journey` 폼 필드로 함께
38
+ 전송합니다.
39
+ - `@roottale/analytics-runtime`: 이벤트 사전(`TRACK_EVENTS`)에 `wireDims`
40
+ 필드를 추가해 `/collect` 비콘 payload의 실제 키 이름을 명시했습니다. 브라우저
41
+ 코드 없이 순수 이벤트 사전만 담은 `./events` 서브패스를 신설했습니다(서버
42
+ 런타임에서 안전하게 import).
43
+ - `@roottale/cms-renderer-next`: 위 cms-client 타입 변경에 따른 patch(연동
44
+ linked 그룹).
45
+
46
+ ### Patch Changes
47
+
48
+ - Updated dependencies [bfe0d14]
49
+ - @roottale/cms-client@0.47.0
50
+
3
51
  ## 0.46.0
4
52
 
5
53
  ### Minor Changes
@@ -23,13 +23,17 @@ import {
23
23
  StaggerGroup,
24
24
  StaggerItem
25
25
  } from "./chunk-ZL2NA3QT.js";
26
+ import {
27
+ LeadFormMeta
28
+ } from "./chunk-VG6UUF4D.js";
26
29
  import {
27
30
  collectCategories,
28
31
  collectCategoriesWithCounts,
29
32
  collectCollectionCategories,
30
33
  defaultCategoryHref,
31
- filterPostsByCategory
32
- } from "./chunk-FUA5H6JE.js";
34
+ filterPostsByCategory,
35
+ trimSlash
36
+ } from "./chunk-U2IM6OCQ.js";
33
37
  import {
34
38
  SectionSchema
35
39
  } from "./chunk-UM37DOIK.js";
@@ -40,6 +44,7 @@ import {
40
44
  // src/server.tsx
41
45
  import {
42
46
  DEFAULT_BLOG_SETTINGS,
47
+ breadcrumbSchema,
43
48
  fetchBlogSettings,
44
49
  fetchPost,
45
50
  fetchPosts,
@@ -126,6 +131,7 @@ function RootTaleLeadForm(props) {
126
131
  "data-roottale-cms": "lead-form",
127
132
  className: formClass,
128
133
  children: [
134
+ /* @__PURE__ */ jsx(LeadFormMeta, { formId: "contact" }),
129
135
  heading ? /* @__PURE__ */ jsx("h2", { className: "rt-cms-lead-heading", children: heading }) : null,
130
136
  description ? /* @__PURE__ */ jsx("p", { className: "rt-cms-lead-description", children: description }) : null,
131
137
  redirectUrl ? /* @__PURE__ */ jsx("input", { type: "hidden", name: "_redirect_url", value: redirectUrl }) : null,
@@ -270,7 +276,7 @@ function RootTaleLeadForm(props) {
270
276
  /* @__PURE__ */ jsx("strong", { children: "(\uD544\uC218)" }),
271
277
  " \uAC1C\uC778\uC815\uBCF4 \uC218\uC9D1\xB7\uC774\uC6A9\uC5D0 \uB3D9\uC758\uD569\uB2C8\uB2E4. (\uC218\uC9D1 \uD56D\uBAA9:",
272
278
  " ",
273
- isMedical ? "\uC774\uB984\xB7\uC5F0\uB77D\uCC98\xB7\uC774\uBA54\uC77C\xB7\uC0C1\uB2F4 \uB0B4\uC6A9(\uC791\uC131 \uC2DC)" : "\uC774\uB984\xB7\uC5F0\uB77D\uCC98\xB7\uC774\uBA54\uC77C\xB7\uC0AC\uC5C5\uCCB4\uBA85",
279
+ isMedical ? "\uC774\uB984\xB7\uC5F0\uB77D\uCC98\xB7\uC774\uBA54\uC77C\xB7\uC0C1\uB2F4 \uB0B4\uC6A9(\uC791\uC131 \uC2DC)\xB7\uC0AC\uC774\uD2B8 \uC774\uC6A9 \uAE30\uB85D(\uBC29\uBB38 \uD398\uC774\uC9C0\xB7\uC0C1\uD638\uC791\uC6A9)" : "\uC774\uB984\xB7\uC5F0\uB77D\uCC98\xB7\uC774\uBA54\uC77C\xB7\uC0AC\uC5C5\uCCB4\uBA85\xB7\uC0AC\uC774\uD2B8 \uC774\uC6A9 \uAE30\uB85D(\uBC29\uBB38 \uD398\uC774\uC9C0\xB7\uC0C1\uD638\uC791\uC6A9)",
274
280
  ". \uBCF4\uAD00 \uAE30\uAC04: \uBB38\uC758 \uC885\uB8CC \uD6C4 3\uB144)"
275
281
  ] })
276
282
  ] }),
@@ -364,6 +370,11 @@ function CtaLink({
364
370
  function ctaTrackForHref(href) {
365
371
  return href.startsWith("tel:") ? "cta_banner" : "cta_hero";
366
372
  }
373
+ function contactHrefTrack(href) {
374
+ if (href.startsWith("tel:")) return "phone_click";
375
+ if (href.startsWith("mailto:")) return "email_click";
376
+ return void 0;
377
+ }
367
378
  function TemplateHeroProof({
368
379
  proofMetrics
369
380
  }) {
@@ -813,8 +824,24 @@ function FooterContact({
813
824
  }
814
825
  return /* @__PURE__ */ jsxs2("address", { className: "rt-tpl-footer-contact", children: [
815
826
  contactInfo.address ? /* @__PURE__ */ jsx2("span", { children: contactInfo.address }) : null,
816
- contactInfo.phone ? /* @__PURE__ */ jsx2("a", { href: telHref(contactInfo.phone), children: contactInfo.phone }) : null,
817
- contactInfo.email ? /* @__PURE__ */ jsx2("a", { href: `mailto:${contactInfo.email}`, children: contactInfo.email }) : null
827
+ contactInfo.phone ? /* @__PURE__ */ jsx2(
828
+ "a",
829
+ {
830
+ href: telHref(contactInfo.phone),
831
+ "data-track": "phone_click",
832
+ "data-track-placement": "footer",
833
+ children: contactInfo.phone
834
+ }
835
+ ) : null,
836
+ contactInfo.email ? /* @__PURE__ */ jsx2(
837
+ "a",
838
+ {
839
+ href: `mailto:${contactInfo.email}`,
840
+ "data-track": "email_click",
841
+ "data-track-placement": "footer",
842
+ children: contactInfo.email
843
+ }
844
+ ) : null
818
845
  ] });
819
846
  }
820
847
  function FooterLegal(section) {
@@ -1631,7 +1658,18 @@ function ContactActions({
1631
1658
  actions
1632
1659
  }) {
1633
1660
  if (!actions || actions.length === 0) return null;
1634
- return /* @__PURE__ */ jsx2("ul", { className: "rt-tpl-contact-actions", children: actions.map((action) => /* @__PURE__ */ jsx2("li", { "data-kind": action.kind, children: /* @__PURE__ */ jsx2("a", { href: action.href, children: action.label }) }, `${action.label}-${action.href}`)) });
1661
+ return /* @__PURE__ */ jsx2("ul", { className: "rt-tpl-contact-actions", children: actions.map((action) => {
1662
+ const track = contactHrefTrack(action.href);
1663
+ return /* @__PURE__ */ jsx2("li", { "data-kind": action.kind, children: /* @__PURE__ */ jsx2(
1664
+ "a",
1665
+ {
1666
+ href: action.href,
1667
+ "data-track": track,
1668
+ "data-track-placement": track ? "contact_actions" : void 0,
1669
+ children: action.label
1670
+ }
1671
+ ) }, `${action.label}-${action.href}`);
1672
+ }) });
1635
1673
  }
1636
1674
  function TemplateContactChannelCards(section) {
1637
1675
  const { heading, channels, map, mapEmbedUrl, actions } = section.props;
@@ -1673,7 +1711,8 @@ function TemplateContactChannelCards(section) {
1673
1711
  {
1674
1712
  className: "rt-tpl-contact-channel-card",
1675
1713
  href: channel.href,
1676
- "data-track": channel.href.startsWith("tel:") ? "cta_call" : "cta_banner",
1714
+ "data-track": channel.href.startsWith("tel:") ? "cta_call" : channel.href.startsWith("mailto:") ? "email_click" : "cta_banner",
1715
+ "data-track-placement": channel.href.startsWith("mailto:") ? "contact_channels" : void 0,
1677
1716
  children: content
1678
1717
  }
1679
1718
  ) : /* @__PURE__ */ jsx2("div", { className: "rt-tpl-contact-channel-card", children: content })
@@ -1731,7 +1770,16 @@ function TemplateContactVisitBand(section) {
1731
1770
  /* @__PURE__ */ jsx2("h3", { className: "rt-tpl-contact-visit-title", children: "\uC624\uC2DC\uB294 \uAE38" }),
1732
1771
  /* @__PURE__ */ jsx2(ContactMapBlock, { map, mapEmbedUrl }),
1733
1772
  address ? /* @__PURE__ */ jsx2("address", { className: "rt-tpl-contact-visit-address", children: address }) : null,
1734
- mapAction ? /* @__PURE__ */ jsx2("a", { className: "rt-tpl-contact-visit-link", href: mapAction.href, children: mapAction.label }) : null
1773
+ mapAction ? /* @__PURE__ */ jsx2(
1774
+ "a",
1775
+ {
1776
+ className: "rt-tpl-contact-visit-link",
1777
+ href: mapAction.href,
1778
+ "data-track": contactHrefTrack(mapAction.href),
1779
+ "data-track-placement": contactHrefTrack(mapAction.href) ? "visit" : void 0,
1780
+ children: mapAction.label
1781
+ }
1782
+ ) : null
1735
1783
  ] }),
1736
1784
  /* @__PURE__ */ jsxs2("section", { className: "rt-tpl-contact-visit-panel", "aria-label": "\uC804\uD654", children: [
1737
1785
  /* @__PURE__ */ jsx2("h3", { className: "rt-tpl-contact-visit-title", children: "\uC804\uD654" }),
@@ -1745,7 +1793,16 @@ function TemplateContactVisitBand(section) {
1745
1793
  }
1746
1794
  ) : null,
1747
1795
  hours ? /* @__PURE__ */ jsx2(ContactHoursSummary, { hours: hours.slice(0, 2) }) : null,
1748
- ctaAction ? /* @__PURE__ */ jsx2("a", { className: "rt-tpl-btn rt-tpl-btn--primary", href: ctaAction.href, children: ctaAction.label }) : null
1796
+ ctaAction ? /* @__PURE__ */ jsx2(
1797
+ "a",
1798
+ {
1799
+ className: "rt-tpl-btn rt-tpl-btn--primary",
1800
+ href: ctaAction.href,
1801
+ "data-track": contactHrefTrack(ctaAction.href),
1802
+ "data-track-placement": contactHrefTrack(ctaAction.href) ? "visit" : void 0,
1803
+ children: ctaAction.label
1804
+ }
1805
+ ) : null
1749
1806
  ] })
1750
1807
  ] })
1751
1808
  ] })
@@ -2911,7 +2968,9 @@ async function RootTaleBlogPost(props) {
2911
2968
  relatedPostsTitle = t(locale, "relatedPostsTitle"),
2912
2969
  collections,
2913
2970
  showFieldFacts = false,
2914
- theme: themeProp
2971
+ theme: themeProp,
2972
+ authorHref = defaultAuthorHref,
2973
+ breadcrumb
2915
2974
  } = props;
2916
2975
  const [post, themeStyle, blogSettings] = await Promise.all([
2917
2976
  fetchPost({ apiKey, slugOrId, baseUrl, ...locale ? { locale } : {} }),
@@ -2948,6 +3007,7 @@ async function RootTaleBlogPost(props) {
2948
3007
  const authorName = post.authorName;
2949
3008
  const authorImageUrl = post.authorImageUrl;
2950
3009
  const postAuthorBio = post.authorBio;
3010
+ const authorSlug = post.authorSlug;
2951
3011
  const views = typeof post.viewCount === "number" ? post.viewCount : null;
2952
3012
  const publishedDate = display.showDate ? formatPublishedDate2(post.publishedAt) : "";
2953
3013
  const showTopAuthor = display.showAuthor && Boolean(authorName);
@@ -2967,14 +3027,24 @@ async function RootTaleBlogPost(props) {
2967
3027
  className: "rt-cms-article",
2968
3028
  style: themeStyle ?? void 0,
2969
3029
  children: [
3030
+ renderBreadcrumb(post, collections, relatedHref, breadcrumb),
2970
3031
  /* @__PURE__ */ jsxs9("header", { className: "rt-cms-post-header", children: [
2971
3032
  showTitle ? /* @__PURE__ */ jsx9("h1", { className: "rt-cms-title", children: post.title }) : null,
2972
3033
  post.excerpt ? /* @__PURE__ */ jsx9("p", { className: "rt-cms-excerpt", children: post.excerpt }) : null,
2973
3034
  publishedDate || showTopAuthor || views !== null ? /* @__PURE__ */ jsxs9("p", { className: "rt-cms-meta", children: [
2974
- publishedDate ? /* @__PURE__ */ jsx9("span", { className: "rt-cms-meta-item", "data-rt-cms-meta": "date", children: publishedDate }) : null,
3035
+ publishedDate ? /* @__PURE__ */ jsx9(
3036
+ "time",
3037
+ {
3038
+ dateTime: post.publishedAt ?? void 0,
3039
+ className: "rt-cms-meta-item",
3040
+ "data-rt-cms-meta": "date",
3041
+ children: publishedDate
3042
+ }
3043
+ ) : null,
2975
3044
  showTopAuthor ? /* @__PURE__ */ jsxs9("span", { className: "rt-cms-meta-item", "data-rt-cms-meta": "author", children: [
2976
- "\uC791\uC131\uC790 ",
2977
- authorName
3045
+ "\uC791\uC131\uC790",
3046
+ " ",
3047
+ authorSlug ? /* @__PURE__ */ jsx9("a", { className: "rt-cms-author-link", href: authorHref(authorSlug), children: authorName }) : authorName
2978
3048
  ] }) : null,
2979
3049
  views !== null ? /* @__PURE__ */ jsxs9("span", { className: "rt-cms-meta-item", "data-rt-cms-meta": "views", children: [
2980
3050
  "\uC870\uD68C ",
@@ -3019,7 +3089,7 @@ async function RootTaleBlogPost(props) {
3019
3089
  ) : /* @__PURE__ */ jsx9("div", { className: "rt-cms-author-card__avatar", "aria-hidden": "true", children: authorInitials(displayAuthorName) }),
3020
3090
  /* @__PURE__ */ jsxs9("div", { className: "rt-cms-author-card__body", children: [
3021
3091
  /* @__PURE__ */ jsx9("p", { className: "rt-cms-author-card__eyebrow", children: display.authorCardEyebrow }),
3022
- /* @__PURE__ */ jsx9("p", { className: "rt-cms-author-card__name", children: displayAuthorName }),
3092
+ /* @__PURE__ */ jsx9("p", { className: "rt-cms-author-card__name", children: authorSlug ? /* @__PURE__ */ jsx9("a", { className: "rt-cms-author-link", href: authorHref(authorSlug), children: displayAuthorName }) : displayAuthorName }),
3023
3093
  authorBio ? /* @__PURE__ */ jsx9("p", { className: "rt-cms-author-card__description", children: authorBio }) : null
3024
3094
  ] })
3025
3095
  ] }) : null,
@@ -3030,6 +3100,46 @@ async function RootTaleBlogPost(props) {
3030
3100
  }
3031
3101
  );
3032
3102
  }
3103
+ function renderBreadcrumb(post, collections, postHref, options) {
3104
+ if (!options) return null;
3105
+ const {
3106
+ siteUrl,
3107
+ homeLabel = "\uD648",
3108
+ homeHref = "/",
3109
+ listLabel = "\uBE14\uB85C\uADF8",
3110
+ showCategory = true,
3111
+ categoryHref = defaultCategoryHref
3112
+ } = options;
3113
+ const listHref = options.listHref ?? (collections ? resolvePostCollection(post, collections)?.basePath || "/blog" : "/blog");
3114
+ const categoryTerm = showCategory ? post.terms.find((t2) => t2.taxonomy === "category") : void 0;
3115
+ const crumbs = [
3116
+ { label: homeLabel, href: homeHref },
3117
+ { label: listLabel, href: listHref }
3118
+ ];
3119
+ if (categoryTerm) {
3120
+ crumbs.push({ label: categoryTerm.name, href: categoryHref(categoryTerm.slug) });
3121
+ }
3122
+ crumbs.push({ label: post.title, href: postHref(post) });
3123
+ const jsonLd = siteUrl ? breadcrumbSchema(
3124
+ crumbs.map((c) => ({ name: c.label, url: absoluteUrl(siteUrl, c.href) }))
3125
+ ) : null;
3126
+ return /* @__PURE__ */ jsxs9(Fragment3, { children: [
3127
+ /* @__PURE__ */ jsx9("nav", { "aria-label": "\uBE0C\uB808\uB4DC\uD06C\uB7FC", className: "rt-cms-breadcrumb", children: /* @__PURE__ */ jsx9("ol", { children: crumbs.map(
3128
+ (c, i) => i === crumbs.length - 1 ? /* @__PURE__ */ jsx9("li", { "aria-current": "page", children: c.label }, i) : /* @__PURE__ */ jsx9("li", { children: /* @__PURE__ */ jsx9("a", { href: c.href, children: c.label }) }, i)
3129
+ ) }) }),
3130
+ jsonLd ? /* @__PURE__ */ jsx9(
3131
+ "script",
3132
+ {
3133
+ type: "application/ld+json",
3134
+ dangerouslySetInnerHTML: { __html: jsonLdJson(jsonLd) }
3135
+ }
3136
+ ) : null
3137
+ ] });
3138
+ }
3139
+ function absoluteUrl(siteUrl, path) {
3140
+ const normalized = path.startsWith("/") ? path : `/${path}`;
3141
+ return `${trimSlash(siteUrl)}${normalized}`;
3142
+ }
3033
3143
  function renderArchiveRelated(groups, collections) {
3034
3144
  const visible = groups.filter((group) => group.items.length > 0);
3035
3145
  if (visible.length === 0) return null;
@@ -3229,6 +3339,9 @@ async function resolveThemeStyle(input) {
3229
3339
  function defaultPostHref(post) {
3230
3340
  return `/blog/${post.slug}`;
3231
3341
  }
3342
+ function defaultAuthorHref(slug) {
3343
+ return `/blog/author/${slug}`;
3344
+ }
3232
3345
  function makeCollectionPostHref(collections) {
3233
3346
  return (post) => {
3234
3347
  const path = resolvePostPath(post, collections);
@@ -3526,4 +3639,4 @@ export {
3526
3639
  RootTalePage,
3527
3640
  RenderTiptap
3528
3641
  };
3529
- //# sourceMappingURL=chunk-JXRHZGRJ.js.map
3642
+ //# sourceMappingURL=chunk-LIBSVC4M.js.map