JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 347
  • Score
    100M100P100Q90780F
  • License UNLICENSED

React server components for Risali.app — SSR-fetch editable text/image/rich-text blocks from a client's Risali site.

Package Exports

  • @risali/react

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/react

Configure

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=60

NEXT_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.