@risali/react
v0.7.0
Published
React server components for Risali.app — SSR-fetch editable text/image/rich-text blocks from a client's Risali site.
Downloads
822
Maintainers
Readme
@risali/react
React Server Components for Risali.app — fetch editable text/image/rich-text blocks from a client's Risali site at SSR time.
Built for Next.js 15 App Router (React 19). Zero client-side JavaScript: every block is rendered server-side, the editable markers ship in plain HTML.
Install
npm install @risali/reactConfigure
Set the site slug once, in your project's .env:
RISALI_SITE_SLUG=mojweb
# Optional — defaults to https://app.risali.app
RISALI_API_BASE=https://app.risali.app
# Optional — Next.js fetch revalidate seconds (default 60)
RISALI_REVALIDATE_SECONDS=60NEXT_PUBLIC_RISALI_SITE_SLUG is also accepted if you'd rather expose it.
Usage
Text
// app/page.tsx
import { RisaliText } from "@risali/react";
export default function HomePage() {
return (
<section>
<RisaliText
as="h1"
pageKey="hero_title"
defaultValue="Vitajte na našom webe"
path="/"
className="text-5xl font-bold"
/>
<RisaliText
pageKey="hero_subtitle"
defaultValue="Robíme weby, ktoré klient sám upravuje."
path="/"
/>
</section>
);
}The component fetches once per (slug, path) pair — Next.js's built-in fetch cache deduplicates identical URLs in a request, so multiple <RisaliText> calls on the same page share one network call.
Image
import { RisaliImage } from "@risali/react";
<RisaliImage
pageKey="hero_photo"
defaultSrc="/hero-fallback.jpg"
defaultAlt="Naša prevádzka"
path="/"
width={1200}
height={600}
className="rounded-2xl"
/>;Only http(s)://… and /local-path sources from the Risali block are honoured; anything else falls back to defaultSrc.
Rich text (bold / italic / palette colour)
import { RisaliRichText } from "@risali/react";
<RisaliRichText
as="div"
pageKey="about_body"
defaultValue="<p>Sme tu už <strong>10 rokov</strong>.</p>"
path="/o-nas"
/>;Block HTML is sanitised on the server before render. Whitelisted tags: span, strong, b, em, i, u, br, a, p. Whitelisted attributes: style (only color:#RRGGBB + font-size:<n>px|rem|em) and href (only http(s)://, mailto:, tel:, or /local). Everything else — <script>, <img> (use <RisaliImage> instead), onerror, javascript:, style="background: url(…)" — is stripped.
Sharing one fetch across many components
If a page renders many Risali blocks, you can pull content once and thread it down — the components skip their own fetch when content is provided:
import { getRisaliContent, RisaliText, RisaliImage } from "@risali/react";
export default async function HomePage() {
const content = await getRisaliContent("/");
return (
<>
<RisaliText content={content} pageKey="hero_title" defaultValue="Vitajte" />
<RisaliImage content={content} pageKey="hero_photo" defaultSrc="/h.jpg" />
</>
);
}Pure helpers for client components ("use client")
The <Risali*> components are async server components — they can't be used directly inside a "use client" boundary. For those cases, fetch content once in your server page and use the pure helpers inline:
// app/page.tsx — server component
import { getRisaliContent } from "@risali/react";
import { HomePage } from "@/components/HomePage";
export default async function Page() {
const content = await getRisaliContent("/");
return <HomePage content={content} />;
}
// components/HomePage.tsx — client component
"use client";
import { getBlockValue, getBlockImage, getBlockRichText } from "@risali/react";
import type { RisaliContent } from "@risali/react";
export function HomePage({ content }: { content: RisaliContent | null }) {
const heroTitle = getBlockValue(content, "hero_title", "Vitajte");
const heroPhoto = getBlockImage(content, "hero_photo", { src: "/h.jpg", alt: "Hero" });
const aboutHtml = getBlockRichText(content, "about_body", "<p>O nás</p>");
return (
<>
<h1 data-risali-key="hero_title">{heroTitle}</h1>
<img data-risali-key="hero_photo" src={heroPhoto.src} alt={heroPhoto.alt} />
<div data-risali-key="about_body" dangerouslySetInnerHTML={{ __html: aboutHtml }} />
</>
);
}Helpers always validate the block type (a block typed image will return defaultValue from getBlockValue) and getBlockRichText always sanitises — including the fallback HTML — so a stored XSS payload can't reach the browser even if the block is missing.
Builder pages (v0.7.0)
A page with render_mode='builder' in the Risali dashboard is a DB-driven list of sections. Built-in section types: hero, text_image, features, gallery, cta, contact, plain, plus (v0.7.0) testimonials, pricing, faq, stats, team and logo_cloud. Every section is styled inline from the brand palette (gradients / shadows / radius derived via color-mix() from --risali-color-*) and is responsive without media queries, so it looks designed even without the host app's Tailwind theme. <RisaliPage> renders the whole page server-side; pages still in code mode return notFound, so a catch-all route coexists with your static routes:
// app/[[...rest]]/page.tsx
import { notFound } from "next/navigation";
import { RisaliPage } from "@risali/react";
export default async function Page({ params }: { params: Promise<{ rest?: string[] }> }) {
const { rest } = await params;
const path = "/" + (rest ?? []).join("/");
const page = await RisaliPage({ path });
if (!page) notFound();
return page;
}Signature sections you build in code register via customSections={{ moja_sekcia: MyComponent }} and win over the built-in catalog on a type collision.
<RisaliBrand /> in the root layout emits the brand palette + fonts as CSS variables (--risali-color-<key>, --risali-font-heading, --risali-font-body) — builder sections consume them automatically, and your own components/Tailwind theme can too. A brand change in the dashboard revalidates the site instantly, no deploy.
Editor mode
Every rendered block carries a data-risali-key="<pageKey>" attribute. The Risali editor iframe (/risali.js?site=<slug>&risali_edit=1) uses that marker to wire up click-to-edit — no CSS selector generation, no content-hash drift, no fragile DOM scanning.
What this package does NOT do
- No client-side DOM patching. Content lives in the server-rendered HTML; the visitor browser never re-renders Risali content.
- No analytics or form capture. Those still ship via
<script async src="https://app.risali.app/risali.js?site=…"></script>(the Risali snippet handles pageview events, form beacons, the cookie banner, and the editor iframe). - No widgets (booking, pricelist, contact form) — yet. Coming in a follow-up package release.
License
Closed-source. Used by Risali.app clients under their SaaS subscription.
