npm.io
0.35.0 • Published 4d ago

@roottale/cms-renderer-next

Licence
UNLICENSED
Version
0.35.0
Deps
3
Size
382 kB
Vulns
0
Weekly
457

@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-client

Peer dep: react@^19.

Setup

  1. Grab your API key from the admin (Settings → API keys → "발급"). Format: rtlk_cust_*. Server-side only.
  2. 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
  1. 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 vertical to 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-client throws if you import it into a browser bundle (assertServer()). Never expose rtlk_cust_* to the client.
  • Block JSON is server-rendered with hardcoded mark/node mappings — no dangerouslySetInnerHTML from authored content. Links pass an isSafeHref() allowlist (http/https/mailto/tel/root-relative/fragment/query).
  • The default target="_blank" on link marks gets rel="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 contact@roottale.com for usage outside of an active subscription.

Keywords