@roottale/cms-renderer-next 0.10.0 → 0.12.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,26 @@
1
1
  # @roottale/cms-renderer-next
2
2
 
3
+ ## 0.12.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [de4b90a]
8
+ - @roottale/cms-client@0.12.0
9
+
10
+ ## 0.11.0
11
+
12
+ ### Minor Changes
13
+
14
+ - `@roottale/cms-renderer-next/routes` 추가 — 외부 Next.js 사이트의 공통 라우트를
15
+ 1줄 팩토리로 표준화. 블로그 조회와 동일한 단일 `ROOTTALE_API_KEY` 사용.
16
+ - `createFeedRoute(config)` — `app/feed.xml/route.ts` GET (RSS 2.0). `fetchPosts`
17
+ 실패를 swallow → 라우트가 _항상 200_(빌드 시 정적 생성 throw 로 인한 404 방지).
18
+ - `createSitemap(config, staticRoutes)` — `app/sitemap.ts` default export.
19
+ - `createRevalidateRoute({ apiKey, revalidate, alsoRevalidate? })` —
20
+ `app/api/revalidate/route.ts` POST. admin(mysite.roottale.com)의 글/카테고리/미디어
21
+ 변경 서명 webhook 검증 후 본문 paths + 파생 경로(feed·sitemap·블로그 인덱스)를
22
+ 모두 revalidate → 각 사이트 자동 반영. `revalidatePath` 주입(패키지가 next 비의존).
23
+
3
24
  ## 0.10.0
4
25
 
5
26
  ### Patch Changes
package/README.md CHANGED
@@ -39,6 +39,30 @@ import "@roottale/cms-renderer-next/styles";
39
39
 
40
40
  The CSS is scoped under `[data-roottale-cms]` and uses `:where()` so customer styles win without `!important`. Every `--rt-*` variable has a static fallback.
41
41
 
42
+ ## Revalidation webhook
43
+
44
+ For near-real-time blog updates, expose `POST /api/revalidate` on the customer
45
+ site and register that URL in ADMIN. ISR settings such as `revalidate = 1800`
46
+ are fallback only.
47
+
48
+ ```ts
49
+ // app/api/revalidate/route.ts
50
+ import { revalidatePath } from "next/cache";
51
+
52
+ import { createRevalidateRoute } from "@roottale/cms-renderer-next/routes";
53
+
54
+ export const POST = createRevalidateRoute({
55
+ apiKey: process.env.ROOTTALE_API_KEY!,
56
+ apiBase: process.env.ROOTTALE_API_BASE,
57
+ revalidate: revalidatePath,
58
+ });
59
+ ```
60
+
61
+ ADMIN setup: open `/s/{tenant-slug}/sites/{site-id}`, set **Webhook URL** to
62
+ `https://<customer-domain>/api/revalidate`, keep it enabled, and save. See
63
+ [`docs/cms-revalidation-webhooks.md`](../../docs/cms-revalidation-webhooks.md)
64
+ for the full operational contract.
65
+
42
66
  ## Blog list — `app/blog/page.tsx`
43
67
 
44
68
  ```tsx
@@ -15,52 +15,159 @@
15
15
  */
16
16
 
17
17
  [data-roottale-cms] :where(.rt-cms-list) {
18
- display: flex;
19
- flex-direction: column;
20
- gap: var(--rt-spacing-6, 1.5rem);
18
+ display: grid;
19
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
20
+ gap: var(--rt-spacing-5, 1.25rem);
21
21
  padding: 0;
22
22
  margin: 0;
23
23
  list-style: none;
24
24
  }
25
25
 
26
26
  [data-roottale-cms] :where(.rt-cms-list-item) {
27
- border-bottom: 1px solid var(--rt-color-border, #e5e7eb);
28
- padding-bottom: var(--rt-spacing-4, 1rem);
29
- }
30
-
31
- [data-roottale-cms] :where(.rt-cms-list-item:last-child) {
32
- border-bottom: 0;
33
- padding-bottom: 0;
27
+ min-inline-size: 0;
34
28
  }
35
29
 
36
30
  [data-roottale-cms] :where(.rt-cms-card-link) {
31
+ position: relative;
32
+ overflow: hidden;
37
33
  color: inherit;
38
34
  text-decoration: none;
39
- display: block;
35
+ display: grid;
36
+ align-content: start;
37
+ min-block-size: 100%;
38
+ border: 1px solid color-mix(in srgb, var(--rt-color-border, #e5e7eb) 78%, transparent);
39
+ border-radius: var(--rt-radius-md, 0.5rem);
40
+ background:
41
+ linear-gradient(
42
+ 180deg,
43
+ rgb(255 255 255 / 0.94),
44
+ color-mix(in srgb, var(--rt-color-muted, #f8fafc) 70%, white)
45
+ ),
46
+ radial-gradient(
47
+ circle at top right,
48
+ color-mix(in srgb, var(--rt-color-primary, #4f46e5) 8%, transparent),
49
+ transparent 46%
50
+ );
51
+ box-shadow:
52
+ 0 16px 38px rgb(17 24 39 / 0.045),
53
+ inset 0 1px 0 rgb(255 255 255 / 0.78);
54
+ padding: var(--rt-spacing-5, 1.25rem);
55
+ transition:
56
+ border-color 180ms ease,
57
+ box-shadow 180ms ease,
58
+ transform 180ms ease;
59
+ }
60
+
61
+ [data-roottale-cms] :where(.rt-cms-card-link)::before {
62
+ content: "";
63
+ position: absolute;
64
+ inset-inline: 1px;
65
+ inset-block-start: 0;
66
+ block-size: 1px;
67
+ background: linear-gradient(
68
+ 90deg,
69
+ transparent,
70
+ color-mix(in srgb, var(--rt-color-primary, #4f46e5) 30%, transparent),
71
+ transparent
72
+ );
73
+ }
74
+
75
+ [data-roottale-cms] :where(.rt-cms-card-link:hover),
76
+ [data-roottale-cms] :where(.rt-cms-card-link:focus-visible) {
77
+ border-color: color-mix(in srgb, var(--rt-color-primary, #4f46e5) 32%, var(--rt-color-border, #e5e7eb));
78
+ box-shadow:
79
+ 0 20px 46px rgb(17 24 39 / 0.07),
80
+ inset 0 1px 0 rgb(255 255 255 / 0.82);
81
+ transform: translateY(-0.125rem);
82
+ }
83
+
84
+ [data-roottale-cms] :where(.rt-cms-card .rt-cms-title) {
85
+ font-size: var(--rt-font-size-xl, 1.375rem);
86
+ font-weight: var(--rt-font-weight-bold, 800);
87
+ line-height: 1.28;
88
+ margin-block-end: var(--rt-spacing-3, 0.75rem);
89
+ }
90
+
91
+ [data-roottale-cms] :where(.rt-cms-card .rt-cms-excerpt) {
92
+ font-size: var(--rt-font-size-base, 1rem);
93
+ line-height: 1.7;
94
+ }
95
+
96
+ [data-roottale-cms] :where(.rt-cms-card .rt-cms-meta) {
97
+ margin-block-end: var(--rt-spacing-4, 1rem);
98
+ }
99
+
100
+ [data-roottale-cms] :where(.rt-cms-card-action) {
101
+ display: inline-flex;
102
+ align-items: center;
103
+ gap: 0.375rem;
104
+ justify-self: start;
105
+ margin-block-start: var(--rt-spacing-5, 1.25rem);
106
+ color: color-mix(in srgb, var(--rt-color-foreground, #111827) 62%, transparent);
107
+ font-family: var(--rt-font-body, system-ui, sans-serif);
108
+ font-size: 0.8125rem;
109
+ font-weight: 800;
110
+ line-height: 1.2;
111
+ }
112
+
113
+ [data-roottale-cms] :where(.rt-cms-card-action)::after {
114
+ content: "";
115
+ inline-size: 1.75rem;
116
+ block-size: 1px;
117
+ background: color-mix(in srgb, var(--rt-color-primary, #4f46e5) 46%, transparent);
118
+ transition: inline-size 180ms ease;
119
+ }
120
+
121
+ [data-roottale-cms] :where(.rt-cms-card-link:hover .rt-cms-card-action)::after,
122
+ [data-roottale-cms] :where(.rt-cms-card-link:focus-visible .rt-cms-card-action)::after {
123
+ inline-size: 2.25rem;
40
124
  }
41
125
 
42
126
  [data-roottale-cms] :where(.rt-cms-title) {
43
127
  font-family: var(--rt-font-display, system-ui, sans-serif);
44
128
  font-size: var(--rt-font-size-2xl, 1.5rem);
45
129
  font-weight: var(--rt-font-weight-bold, 700);
46
- color: var(--rt-color-foreground, #0f172a);
47
- margin: 0 0 var(--rt-spacing-2, 0.5rem);
130
+ color: var(--rt-color-foreground, #111827);
131
+ letter-spacing: 0;
48
132
  line-height: 1.25;
133
+ margin: 0 0 var(--rt-spacing-2, 0.5rem);
49
134
  }
50
135
 
51
136
  [data-roottale-cms] :where(.rt-cms-excerpt) {
52
137
  font-family: var(--rt-font-body, system-ui, sans-serif);
53
- font-size: var(--rt-font-size-base, 1rem);
54
- color: var(--rt-color-muted-foreground, #64748b);
138
+ font-size: var(--rt-font-size-base, 1.0625rem);
139
+ color: color-mix(in srgb, var(--rt-color-foreground, #111827) 68%, transparent);
55
140
  margin: 0;
56
- line-height: 1.5;
141
+ line-height: 1.75;
142
+ max-inline-size: 42rem;
57
143
  }
58
144
 
59
145
  [data-roottale-cms] :where(.rt-cms-meta) {
60
146
  font-family: var(--rt-font-body, system-ui, sans-serif);
61
- font-size: var(--rt-font-size-sm, 0.875rem);
62
- color: var(--rt-color-muted-foreground, #64748b);
63
- margin: 0 0 var(--rt-spacing-2, 0.5rem);
147
+ display: flex;
148
+ flex-wrap: wrap;
149
+ align-items: center;
150
+ gap: 0.375rem 0.75rem;
151
+ font-size: var(--rt-font-size-sm, 0.8125rem);
152
+ font-weight: 600;
153
+ color: color-mix(in srgb, var(--rt-color-foreground, #111827) 56%, transparent);
154
+ margin: 0 0 var(--rt-spacing-3, 0.75rem);
155
+ }
156
+
157
+ [data-roottale-cms] :where(.rt-cms-meta-item) {
158
+ display: inline-flex;
159
+ align-items: center;
160
+ min-block-size: 1.625rem;
161
+ white-space: nowrap;
162
+ }
163
+
164
+ [data-roottale-cms] :where(.rt-cms-meta-item + .rt-cms-meta-item)::before {
165
+ content: "";
166
+ inline-size: 0.25rem;
167
+ block-size: 0.25rem;
168
+ border-radius: 999px;
169
+ background: color-mix(in srgb, var(--rt-color-primary, #4f46e5) 70%, white);
170
+ margin-inline-end: 0.75rem;
64
171
  }
65
172
 
66
173
  [data-roottale-cms] :where(.rt-cms-empty) {
@@ -71,14 +178,50 @@
71
178
 
72
179
  [data-roottale-cms] :where(.rt-cms-article) {
73
180
  font-family: var(--rt-font-body, system-ui, sans-serif);
74
- color: var(--rt-color-foreground, #0f172a);
181
+ color: var(--rt-color-foreground, #111827);
182
+ }
183
+
184
+ [data-roottale-cms] :where(.rt-cms-post-header) {
185
+ position: relative;
186
+ overflow: hidden;
187
+ border: 1px solid color-mix(in srgb, var(--rt-color-border, #e5e7eb) 78%, transparent);
188
+ border-radius: calc(var(--rt-radius-md, 0.5rem) * 1.5);
189
+ background:
190
+ linear-gradient(135deg, rgb(255 255 255 / 0.92), color-mix(in srgb, var(--rt-color-muted, #f8fafc) 70%, white)),
191
+ radial-gradient(circle at top right, color-mix(in srgb, var(--rt-color-primary, #4f46e5) 10%, transparent), transparent 42%);
192
+ box-shadow: 0 20px 55px rgb(17 24 39 / 0.05);
193
+ padding: clamp(1.5rem, 3vw, 2.5rem);
194
+ margin-block-end: var(--rt-spacing-6, 1.5rem);
195
+ }
196
+
197
+ [data-roottale-cms] :where(.rt-cms-post-header .rt-cms-title) {
198
+ font-size: var(--rt-font-size-2xl, 2.75rem);
199
+ font-weight: var(--rt-font-weight-bold, 800);
200
+ line-height: 1.12;
201
+ margin-block-end: var(--rt-spacing-3, 0.75rem);
202
+ max-inline-size: 15ch;
203
+ }
204
+
205
+ [data-roottale-cms] :where(.rt-cms-post-header)::after {
206
+ content: "";
207
+ position: absolute;
208
+ inset-inline-start: 0;
209
+ inset-block: 0;
210
+ inline-size: 0.25rem;
211
+ background: linear-gradient(
212
+ 180deg,
213
+ color-mix(in srgb, var(--rt-color-primary, #4f46e5) 72%, white),
214
+ transparent
215
+ );
75
216
  }
76
217
 
77
- [data-roottale-cms] :where(.rt-cms-article h1),
78
218
  [data-roottale-cms] :where(.rt-cms-article h2),
79
- [data-roottale-cms] :where(.rt-cms-article h3) {
219
+ [data-roottale-cms] :where(.rt-cms-article h3),
220
+ [data-roottale-cms] :where(.rt-cms-article h4) {
80
221
  font-family: var(--rt-font-display, system-ui, sans-serif);
81
- margin-top: var(--rt-spacing-6, 1.5rem);
222
+ color: var(--rt-color-foreground, #111827);
223
+ letter-spacing: 0;
224
+ margin-top: var(--rt-spacing-7, 2rem);
82
225
  margin-bottom: var(--rt-spacing-3, 0.75rem);
83
226
  }
84
227
 
@@ -99,50 +242,281 @@
99
242
 
100
243
  /* RootTaleTableOfContents */
101
244
  [data-roottale-cms] :where(.rt-cms-toc) {
102
- background: var(--rt-color-muted, #f8fafc);
103
- border: 1px solid var(--rt-color-border, #e5e7eb);
104
- border-radius: var(--rt-radius-md, 0.5rem);
105
- padding: var(--rt-spacing-4, 1rem);
106
- margin: 0 0 var(--rt-spacing-6, 1.5rem);
245
+ position: relative;
246
+ isolation: isolate;
247
+ overflow: hidden;
248
+ background:
249
+ linear-gradient(
250
+ 180deg,
251
+ rgb(255 255 255 / 0.9),
252
+ color-mix(in srgb, var(--rt-color-muted, #f8fafc) 70%, white)
253
+ ),
254
+ radial-gradient(
255
+ circle at top left,
256
+ color-mix(in srgb, var(--rt-color-primary, #4f46e5) 8%, transparent),
257
+ transparent 42%
258
+ );
259
+ border: 1px solid color-mix(in srgb, var(--rt-color-border, #e5e7eb) 78%, transparent);
260
+ border-radius: calc(var(--rt-radius-md, 0.5rem) * 1.25);
261
+ box-shadow:
262
+ 0 16px 42px rgb(17 24 39 / 0.045),
263
+ inset 0 1px 0 rgb(255 255 255 / 0.78);
264
+ padding: 1.25rem 1.375rem 1.25rem 1.5rem;
265
+ margin: 0 0 var(--rt-spacing-7, 2rem);
266
+ }
267
+
268
+ [data-roottale-cms] :where(.rt-cms-toc)::before {
269
+ content: "";
270
+ position: absolute;
271
+ inset-inline: 1px;
272
+ inset-block-start: 0;
273
+ block-size: 1px;
274
+ background: linear-gradient(
275
+ 90deg,
276
+ transparent,
277
+ color-mix(in srgb, var(--rt-color-primary, #4f46e5) 32%, transparent),
278
+ transparent
279
+ );
280
+ z-index: 0;
281
+ }
282
+
283
+ [data-roottale-cms] :where(.rt-cms-toc-details) {
284
+ position: relative;
285
+ }
286
+
287
+ [data-roottale-cms] :where(.rt-cms-toc-summary) {
288
+ display: flex;
289
+ align-items: center;
290
+ gap: 0.75rem;
291
+ cursor: pointer;
292
+ list-style: none;
293
+ }
294
+
295
+ [data-roottale-cms] :where(.rt-cms-toc-summary)::-webkit-details-marker {
296
+ display: none;
107
297
  }
108
298
 
109
299
  [data-roottale-cms] :where(.rt-cms-toc-title) {
110
300
  font-family: var(--rt-font-display, system-ui, sans-serif);
111
- font-size: var(--rt-font-size-sm, 0.875rem);
112
- font-weight: var(--rt-font-weight-bold, 700);
301
+ display: flex;
302
+ align-items: center;
303
+ gap: 0.75rem;
304
+ flex: 1 1 auto;
305
+ font-size: 0.8125rem;
306
+ font-weight: var(--rt-font-weight-bold, 800);
307
+ letter-spacing: 0;
308
+ margin: 0;
309
+ color: color-mix(in srgb, var(--rt-color-foreground, #111827) 58%, transparent);
113
310
  text-transform: uppercase;
114
- letter-spacing: 0.05em;
115
- margin: 0 0 var(--rt-spacing-3, 0.75rem);
116
- color: var(--rt-color-muted-foreground, #64748b);
311
+ }
312
+
313
+ [data-roottale-cms] :where(.rt-cms-toc-title)::after {
314
+ content: "";
315
+ flex: 1 1 auto;
316
+ block-size: 1px;
317
+ background: linear-gradient(
318
+ 90deg,
319
+ color-mix(in srgb, var(--rt-color-border, #e5e7eb) 88%, transparent),
320
+ transparent
321
+ );
322
+ }
323
+
324
+ [data-roottale-cms] :where(.rt-cms-toc-toggle) {
325
+ position: relative;
326
+ flex: 0 0 auto;
327
+ inline-size: 1.75rem;
328
+ block-size: 1.75rem;
329
+ border: 1px solid color-mix(in srgb, var(--rt-color-border, #e5e7eb) 82%, transparent);
330
+ border-radius: 999px;
331
+ background: rgb(255 255 255 / 0.74);
332
+ }
333
+
334
+ [data-roottale-cms] :where(.rt-cms-toc-toggle)::before,
335
+ [data-roottale-cms] :where(.rt-cms-toc-toggle)::after {
336
+ content: "";
337
+ position: absolute;
338
+ inset-block-start: 50%;
339
+ inset-inline-start: 50%;
340
+ inline-size: 0.625rem;
341
+ block-size: 1px;
342
+ background: color-mix(in srgb, var(--rt-color-foreground, #111827) 66%, transparent);
343
+ transform: translate(-50%, -50%);
344
+ }
345
+
346
+ [data-roottale-cms] :where(.rt-cms-toc-toggle)::after {
347
+ transform: translate(-50%, -50%) rotate(90deg);
348
+ transition: transform 160ms ease;
349
+ }
350
+
351
+ [data-roottale-cms] :where(.rt-cms-toc-details[open] .rt-cms-toc-toggle)::after {
352
+ transform: translate(-50%, -50%) rotate(0deg);
117
353
  }
118
354
 
119
355
  [data-roottale-cms] :where(.rt-cms-toc-list) {
120
- margin: 0;
121
- padding-left: var(--rt-spacing-4, 1rem);
122
- display: flex;
123
- flex-direction: column;
124
- gap: var(--rt-spacing-1, 0.25rem);
356
+ position: relative;
357
+ margin: 0.875rem 0 0;
358
+ padding: 0 0 0 0.875rem;
359
+ display: grid;
360
+ gap: 0.125rem;
361
+ list-style: none;
362
+ }
363
+
364
+ [data-roottale-cms] :where(.rt-cms-toc-list)::before {
365
+ content: "";
366
+ position: absolute;
367
+ inset-block: 0.75rem;
368
+ inset-inline-start: 0.1875rem;
369
+ inline-size: 1px;
370
+ background: linear-gradient(
371
+ 180deg,
372
+ color-mix(in srgb, var(--rt-color-primary, #4f46e5) 34%, transparent),
373
+ color-mix(in srgb, var(--rt-color-border, #e5e7eb) 72%, transparent)
374
+ );
125
375
  }
126
376
 
127
377
  [data-roottale-cms] :where(.rt-cms-toc-item) {
378
+ position: relative;
128
379
  font-family: var(--rt-font-body, system-ui, sans-serif);
129
- font-size: var(--rt-font-size-sm, 0.875rem);
130
- line-height: 1.5;
380
+ font-size: var(--rt-font-size-sm, 0.9375rem);
381
+ line-height: 1.45;
382
+ }
383
+
384
+ [data-roottale-cms] :where(.rt-cms-toc-item)::before {
385
+ content: "";
386
+ position: absolute;
387
+ inset-block-start: 0.875rem;
388
+ inset-inline-start: -0.875rem;
389
+ inline-size: 0.4375rem;
390
+ block-size: 0.4375rem;
391
+ border: 1px solid color-mix(in srgb, var(--rt-color-primary, #4f46e5) 42%, white);
392
+ border-radius: 999px;
393
+ background: var(--rt-color-background, #ffffff);
394
+ box-shadow: 0 0 0 0.1875rem
395
+ color-mix(in srgb, var(--rt-color-background, #ffffff) 86%, transparent);
131
396
  }
132
397
 
133
398
  [data-roottale-cms] :where(.rt-cms-toc-level-3) {
134
- padding-left: var(--rt-spacing-4, 1rem);
399
+ margin-left: var(--rt-spacing-5, 1.25rem);
400
+ }
401
+
402
+ [data-roottale-cms] :where(.rt-cms-toc-level-4) {
403
+ margin-left: var(--rt-spacing-7, 2rem);
135
404
  }
136
405
 
137
406
  [data-roottale-cms] :where(.rt-cms-toc-item a) {
138
- color: var(--rt-color-foreground, #0f172a);
407
+ display: block;
408
+ color: color-mix(in srgb, var(--rt-color-foreground, #111827) 78%, transparent);
139
409
  text-decoration: none;
410
+ border-radius: calc(var(--rt-radius-md, 0.5rem) * 0.75);
411
+ padding: 0.375rem 0.625rem;
412
+ transition:
413
+ background-color 160ms ease,
414
+ box-shadow 160ms ease,
415
+ color 160ms ease,
416
+ transform 160ms ease;
140
417
  }
141
418
 
142
419
  [data-roottale-cms] :where(.rt-cms-toc-item a:hover),
143
420
  [data-roottale-cms] :where(.rt-cms-toc-item a:focus) {
144
- color: var(--rt-color-primary, #0070f3);
145
- text-decoration: underline;
421
+ color: var(--rt-color-foreground, #111827);
422
+ background: rgb(255 255 255 / 0.74);
423
+ box-shadow: inset 0 0 0 1px
424
+ color-mix(in srgb, var(--rt-color-border, #e5e7eb) 70%, transparent);
425
+ text-decoration: none;
426
+ transform: translateX(0.1875rem);
427
+ }
428
+
429
+ [data-roottale-cms] :where(.rt-cms-author-card) {
430
+ display: flex;
431
+ align-items: center;
432
+ gap: var(--rt-spacing-4, 1rem);
433
+ margin-block-start: var(--rt-spacing-8, 3rem);
434
+ border: 1px solid color-mix(in srgb, var(--rt-color-border, #e5e7eb) 82%, transparent);
435
+ border-radius: calc(var(--rt-radius-md, 0.5rem) * 1.25);
436
+ background:
437
+ linear-gradient(135deg, rgb(255 255 255 / 0.92), color-mix(in srgb, var(--rt-color-muted, #f8fafc) 82%, white)),
438
+ radial-gradient(circle at bottom left, color-mix(in srgb, var(--rt-color-primary, #4f46e5) 9%, transparent), transparent 48%);
439
+ box-shadow: 0 18px 44px rgb(17 24 39 / 0.055);
440
+ padding: var(--rt-spacing-5, 1.25rem);
441
+ }
442
+
443
+ [data-roottale-cms] :where(.rt-cms-author-card__avatar),
444
+ [data-roottale-cms] :where(.rt-cms-author-card__image) {
445
+ flex: 0 0 auto;
446
+ inline-size: 3.25rem;
447
+ block-size: 3.25rem;
448
+ border-radius: 999px;
449
+ }
450
+
451
+ [data-roottale-cms] :where(.rt-cms-author-card__avatar) {
452
+ display: grid;
453
+ place-items: center;
454
+ background:
455
+ linear-gradient(135deg, var(--rt-color-primary, #4f46e5), color-mix(in srgb, var(--rt-color-primary, #4f46e5) 42%, #111827));
456
+ color: var(--rt-color-primary-foreground, #ffffff);
457
+ font-family: var(--rt-font-display, system-ui, sans-serif);
458
+ font-size: 0.9375rem;
459
+ font-weight: 800;
460
+ letter-spacing: 0;
461
+ box-shadow: 0 12px 30px color-mix(in srgb, var(--rt-color-primary, #4f46e5) 18%, transparent);
462
+ }
463
+
464
+ [data-roottale-cms] :where(.rt-cms-author-card__image) {
465
+ display: block;
466
+ object-fit: cover;
467
+ box-shadow: 0 12px 30px rgb(17 24 39 / 0.12);
468
+ }
469
+
470
+ [data-roottale-cms] :where(.rt-cms-author-card__body) {
471
+ min-inline-size: 0;
472
+ }
473
+
474
+ [data-roottale-cms] :where(.rt-cms-author-card__eyebrow) {
475
+ margin: 0 0 0.125rem;
476
+ color: color-mix(in srgb, var(--rt-color-foreground, #111827) 46%, transparent);
477
+ font-size: 0.75rem;
478
+ font-weight: 700;
479
+ line-height: 1.2;
480
+ }
481
+
482
+ [data-roottale-cms] :where(.rt-cms-author-card__name) {
483
+ margin: 0;
484
+ color: var(--rt-color-foreground, #111827);
485
+ font-family: var(--rt-font-display, system-ui, sans-serif);
486
+ font-size: 1.0625rem;
487
+ font-weight: 800;
488
+ line-height: 1.3;
489
+ }
490
+
491
+ [data-roottale-cms] :where(.rt-cms-author-card__description) {
492
+ margin: 0.375rem 0 0;
493
+ color: color-mix(in srgb, var(--rt-color-foreground, #111827) 62%, transparent);
494
+ font-size: 0.9375rem;
495
+ line-height: 1.55;
496
+ max-inline-size: 42rem;
497
+ }
498
+
499
+ @media (max-width: 640px) {
500
+ [data-roottale-cms] :where(.rt-cms-title) {
501
+ font-size: var(--rt-font-size-2xl, 1.5rem);
502
+ }
503
+
504
+ [data-roottale-cms] :where(.rt-cms-post-header .rt-cms-title) {
505
+ font-size: var(--rt-font-size-2xl, 2.125rem);
506
+ max-inline-size: 100%;
507
+ }
508
+
509
+ [data-roottale-cms] :where(.rt-cms-toc) {
510
+ padding: 1.125rem;
511
+ }
512
+
513
+ [data-roottale-cms] :where(.rt-cms-toc-item a) {
514
+ padding-inline: 0.5rem;
515
+ }
516
+
517
+ [data-roottale-cms] :where(.rt-cms-author-card) {
518
+ align-items: flex-start;
519
+ }
146
520
  }
147
521
 
148
522
  /* RootTaleFloatingCta */
@@ -0,0 +1,101 @@
1
+ /**
2
+ * `@roottale/cms-renderer-next/routes` — Next.js App Router 통합 라우트 팩토리.
3
+ *
4
+ * 외부 고객사 사이트가 매번 직접 작성하던 공통 라우트(RSS feed / sitemap /
5
+ * revalidate webhook)를 1줄 팩토리로 표준화. 블로그 조회와 *동일한* 단일
6
+ * customer API key(`ROOTTALE_API_KEY`)를 사용한다.
7
+ *
8
+ * 설계 원칙:
9
+ * - feed/sitemap 은 `fetchPosts` 실패를 swallow → 라우트가 *항상 200* (빌드 시
10
+ * 정적 생성이 throw 해서 404 나는 사고 방지).
11
+ * - revalidate 는 admin(mysite.roottale.com)에서 글/카테고리/미디어 변경 시 보내는
12
+ * 서명 webhook 을 검증하고, 본문 paths + 파생 경로(feed·sitemap·블로그 인덱스)를
13
+ * 모두 revalidate → 각 사이트에 자동 반영.
14
+ * - `next` 에 직접 의존하지 않음: revalidate 콜백(`revalidatePath`)을 주입받는다.
15
+ *
16
+ * 사용 예는 roottale-docs `services/external-site-integration.md` 참조.
17
+ */
18
+ interface RootTaleSiteConfig {
19
+ /** `rtlk_cust_*` — 블로그 조회와 같은 키. 서버에서만(절대 브라우저 금지). */
20
+ apiKey: string;
21
+ /** API base 오버라이드. 기본 `https://api.roottale.com`. */
22
+ apiBase?: string;
23
+ /** 사이트 절대 URL (뒤 슬래시 없이). 예: `https://kjmtax.roottale.app`. */
24
+ siteUrl: string;
25
+ /** 피드/사이트 제목. */
26
+ title: string;
27
+ description?: string;
28
+ /** 기본 "ko-KR". */
29
+ language?: string;
30
+ /** 블로그 base path. 기본 "/blog". */
31
+ blogBasePath?: string;
32
+ }
33
+ /**
34
+ * `app/feed.xml/route.ts` 용 GET 핸들러. RSS 2.0.
35
+ *
36
+ * ```ts
37
+ * import { createFeedRoute } from "@roottale/cms-renderer-next/routes";
38
+ * export const dynamic = "force-dynamic";
39
+ * export const GET = createFeedRoute({
40
+ * apiKey: process.env.ROOTTALE_API_KEY!,
41
+ * siteUrl: "https://example.com",
42
+ * title: "Example 블로그",
43
+ * description: "...",
44
+ * });
45
+ * ```
46
+ */
47
+ declare function createFeedRoute(config: RootTaleSiteConfig): () => Promise<Response>;
48
+ /** Next `MetadataRoute.Sitemap[number]` 와 구조 호환(타입 결합 회피). */
49
+ interface SitemapEntry {
50
+ url: string;
51
+ lastModified?: string | Date;
52
+ changeFrequency?: "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
53
+ priority?: number;
54
+ }
55
+ /**
56
+ * `app/sitemap.ts` default export. 정적 경로 + 글 경로를 합친다.
57
+ *
58
+ * ```ts
59
+ * import { createSitemap } from "@roottale/cms-renderer-next/routes";
60
+ * export default createSitemap(
61
+ * { apiKey: process.env.ROOTTALE_API_KEY!, siteUrl: "https://example.com", title: "..." },
62
+ * [{ url: "https://example.com", changeFrequency: "weekly", priority: 1 }],
63
+ * );
64
+ * ```
65
+ */
66
+ declare function createSitemap(config: RootTaleSiteConfig, staticRoutes?: SitemapEntry[]): () => Promise<SitemapEntry[]>;
67
+ interface RevalidateRouteConfig {
68
+ /** `rtlk_cust_*` — 블로그 조회/webhook 검증 공통 키. */
69
+ apiKey: string;
70
+ apiBase?: string;
71
+ /** Next 의 `revalidatePath` (또는 동등 콜백) 주입. */
72
+ revalidate: (path: string) => void | Promise<void>;
73
+ /** 블로그 base path. 기본 "/blog". */
74
+ blogBasePath?: string;
75
+ /**
76
+ * 모든 글/분류/미디어 변경에 대해 *항상 함께* revalidate 할 파생 경로.
77
+ * 피드·사이트맵·블로그 인덱스처럼 "글 목록"에 의존하는 경로 — admin 변경이
78
+ * 자동 반영되려면 필수. 기본: ["/feed.xml", "/sitemap.xml", blogBase].
79
+ */
80
+ alsoRevalidate?: string[];
81
+ /** (옵션) jti replay 방지 hook (cms-client verify 로 전달). */
82
+ consumeJti?: (jti: string, exp: number) => Promise<"new" | "seen">;
83
+ }
84
+ /**
85
+ * `app/api/revalidate/route.ts` 용 POST 핸들러.
86
+ *
87
+ * admin(mysite.roottale.com)이 글/카테고리/미디어 변경 시 보내는 서명 webhook 을
88
+ * 검증하고, 본문 paths + 파생 경로(feed·sitemap·블로그 인덱스)를 revalidate.
89
+ *
90
+ * ```ts
91
+ * import { revalidatePath } from "next/cache";
92
+ * import { createRevalidateRoute } from "@roottale/cms-renderer-next/routes";
93
+ * export const POST = createRevalidateRoute({
94
+ * apiKey: process.env.ROOTTALE_API_KEY!,
95
+ * revalidate: revalidatePath,
96
+ * });
97
+ * ```
98
+ */
99
+ declare function createRevalidateRoute(config: RevalidateRouteConfig): (req: Request) => Promise<Response>;
100
+
101
+ export { type RevalidateRouteConfig, type RootTaleSiteConfig, type SitemapEntry, createFeedRoute, createRevalidateRoute, createSitemap };