@roottale/cms-renderer-next
v0.25.0
Published
RootTale CMS public-render React/Next.js Server Components. SSR-only RSC components (RootTaleBlogList / RootTaleBlogPost / RootTaleLeadForm) for external customer sites. Companion of @roottale/cms-renderer-astro (ADR-0034 §1.5 amended).
Maintainers
Readme
@roottale/cms-renderer-next
RootTale CMS public-render React Server Components for external customer sites.
Drop-in <RootTaleBlogList> / <RootTaleBlogPost> / <RootTaleLeadForm> for Next.js App Router, server components elsewhere. SSR-only — no 'use client' boundary, no API key in the browser bundle.
Pair with @roottale/cms-client (the fetch client) and the RootTale CMS admin (mysite.roottale.com) where you author the content.
Astro counterpart: @roottale/cms-renderer-astro — equivalent surface (ADR-0034 §1.5).
Install
npm install @roottale/cms-renderer-next @roottale/cms-client
# or
pnpm add @roottale/cms-renderer-next @roottale/cms-clientPeer dep: react@^19.
Setup
- Grab your API key from the admin (
Settings → API keys→ "발급"). Format:rtlk_cust_*. Server-side only. - Store it as a server env var:
# .env.local
ROOTTALE_API_KEY=rtlk_cust_xxxxxxxxxxxxxxxxxx
# optional — override the default https://api.roottale.com
ROOTTALE_API_BASE=https://api.roottale.com- Import the scoped CSS once (root layout):
// app/layout.tsx
import "@roottale/cms-renderer-next/styles";The CSS is scoped under [data-roottale-cms] and uses :where() so customer styles win without !important. Every --rt-* variable has a static fallback.
Revalidation webhook
For near-real-time blog updates, expose POST /api/revalidate on the customer
site and register that URL in ADMIN. ISR settings such as revalidate = 1800
are fallback only.
// app/api/revalidate/route.ts
import { revalidatePath } from "next/cache";
import { createRevalidateRoute } from "@roottale/cms-renderer-next/routes";
export const POST = createRevalidateRoute({
apiKey: process.env.ROOTTALE_API_KEY!,
apiBase: process.env.ROOTTALE_API_BASE,
revalidate: revalidatePath,
});ADMIN setup: open /s/{tenant-slug}/sites/{site-id}, set Webhook URL to
https://<customer-domain>/api/revalidate, keep it enabled, and save. See
docs/cms-revalidation-webhooks.md
for the full operational contract.
Blog list — app/blog/page.tsx
import { RootTaleBlogList } from "@roottale/cms-renderer-next/server";
export default function BlogPage() {
return (
<main>
<h1>Blog</h1>
<RootTaleBlogList
apiKey={process.env.ROOTTALE_API_KEY!}
limit={20}
showCategoryFilter
postHref={(post) => `/blog/${post.slug}`}
/>
</main>
);
}Blog categories — app/blog/categories/page.tsx
import { RootTaleBlogCategories } from "@roottale/cms-renderer-next/server";
export default function CategoriesPage() {
return <RootTaleBlogCategories apiKey={process.env.ROOTTALE_API_KEY!} />;
}Blog category — app/blog/categories/[category]/page.tsx
import { RootTaleBlogList } from "@roottale/cms-renderer-next/server";
export default async function CategoryPage({
params,
}: {
params: Promise<{ category: string }>;
}) {
const { category } = await params;
return (
<RootTaleBlogList
apiKey={process.env.ROOTTALE_API_KEY!}
showCategoryFilter
activeCategory={decodeURIComponent(category)}
/>
);
}Blog post — app/blog/[slug]/page.tsx
import { RootTaleBlogPost } from "@roottale/cms-renderer-next/server";
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return (
<main>
<RootTaleBlogPost
apiKey={process.env.ROOTTALE_API_KEY!}
slugOrId={slug}
showTableOfContents
tableOfContentsTitle="목차"
/>
</main>
);
}Lead form — app/contact/page.tsx
import { RootTaleLeadForm } from "@roottale/cms-renderer-next/server";
export default function ContactPage() {
return (
<main>
<h1>진단 신청</h1>
<RootTaleLeadForm
action="https://mysite.roottale.com/api/lead-intake"
vertical="tax"
redirectUrl="https://kjmtax.roottale.app/contact"
heading="무료 진단 신청"
description="평일 기준 1-2 영업일 내에 회신드립니다."
/>
</main>
);
}The form submits via HTML POST (no JS, no fetch). After insert, the admin 302s back to redirectUrl with ?ok=1 (or ?err=<reason>). The admin checks the URL against its LEAD_INTAKE_ALLOWED_ORIGINS env — if you skip redirectUrl, the admin falls back to its LEAD_INTAKE_REDIRECT_BASE.
vertical-specific behavior
vertical="medical"adds the 의료 PII 국외이전 동의 checkbox (ADR-0018).- Omit
verticalto render a select dropdown.
Exports
| Export | Use |
|---|---|
| RootTaleBlogList | RSC — fetches /v1/cms/public/posts?type=post and renders an <ul>. |
| RootTaleBlogPost | RSC — fetches one post, renders Tiptap doc + optional TOC. |
| RootTaleLeadForm | RSC — HTML form, POST → admin /api/lead-intake. |
| RootTalePostCard | Single card primitive (used internally by RootTaleBlogList). |
| RootTaleTableOfContents | Standalone TOC (heading list). |
| RootTaleFloatingCta | Floating CTA buttons. |
| renderBlocks / renderBlock | Block JSON → React element. |
| attachHeadingIds / extractToc | Tiptap doc helpers. |
Security model
@roottale/cms-clientthrows if you import it into a browser bundle (assertServer()). Never exposertlk_cust_*to the client.- Block JSON is server-rendered with hardcoded mark/node mappings — no
dangerouslySetInnerHTMLfrom authored content. Links pass anisSafeHref()allowlist (http/https/mailto/tel/root-relative/fragment/query). - The default
target="_blank"on link marks getsrel="noopener noreferrer".
Compatibility
| Dependency | Range |
|---|---|
| react | ^19 (peer) |
| next | ^14 / ^15 (any RSC-capable framework actually) |
| node | >=18.18 |
License
Proprietary (UNLICENSED). Issued under the RootTale customer contract. Contact [email protected] for usage outside of an active subscription.
