# React components

Articles arrive as typed blocks, so each part of an article is a component you control: restyle the callout, swap the call to action for your signup form, hand images to your framework's image component. The set below renders every block type with semantic HTML and one stylesheet.

Example article: https://api.seoforsaas.dev/public/v1/fixtures/article

## Install

```bash group=install title="shadcn"
npx shadcn@latest add https://seoforsaas.dev/r/article.json
```

```bash group=install title="curl"
for f in lib/seoforsaas/types.ts components/seoforsaas/article.tsx components/seoforsaas/blocks.tsx \
  components/seoforsaas/rich-text.tsx components/seoforsaas/video.tsx components/seoforsaas/json-ld.tsx \
  components/seoforsaas/article.css; do
  curl -fsSL --create-dirs -o "src/$f" "https://seoforsaas.dev/r/files/$f"
done
```

You get `components/seoforsaas/` and `lib/seoforsaas/types.ts`, as source files in your repository: no package to keep updated, no dependency beyond React (tested with React 19). The components import the types relatively, so keep the two folders side by side. Customize through `components` and CSS variables instead of editing the files, so reinstalling a newer version stays a clean overwrite.

## Render

```tsx
import { Article } from "@/components/seoforsaas/article";
import { JsonLd } from "@/components/seoforsaas/json-ld";
import "@/components/seoforsaas/article.css";

<h1>{article.title}</h1>
<Article blocks={article.blocks} />
<JsonLd data={article.seo.jsonLd} />
```

The title isn't a block: render it as the page's `h1`. Only `video.tsx` is a client component (click to play); everything else renders on the server, and without JavaScript the video's poster links to YouTube.

## Replace a block

`components` merges over the defaults: you replace only what you pass, and each replacement receives its block, typed.

```tsx
import Image from "next/image";
import { Article } from "@/components/seoforsaas/article";
import { safeHref } from "@/components/seoforsaas/rich-text";
import type { CtaBlock, ImageBlock } from "@/lib/seoforsaas/types";

function SignupCta({ block }: { block: CtaBlock }) {
  const href = safeHref(block.buttonHref); // the same link check the defaults use
  return (
    <section className="signup-cta">
      <h2>{block.heading}</h2>
      {block.body && <p>{block.body}</p>}
      {href && <a href={href}>{block.buttonLabel}</a>}
    </section>
  );
}

function ArticleImage({ block }: { block: ImageBlock }) {
  // Set the size your layout needs.
  return <Image src={block.url} alt={block.alt} width={1536} height={1024} />;
}

<Article blocks={article.blocks} components={{ cta: SignupCta, image: ArticleImage }} />
```

With `next/image`, allow the SEO for SaaS CDN in `next.config`: `images: { remotePatterns: [new URL("https://media.seoforsaas.dev/**")] }`.

## Block types added later

A type the components don't know renders through `components.unknown`, which renders nothing. Your pages never break when we add one. To log unknown types in development:

```tsx
import { Article } from "@/components/seoforsaas/article";

<Article
  blocks={article.blocks}
  components={{
    unknown: ({ block }) => {
      if (process.env.NODE_ENV === "development") console.warn(`Unrendered block type: ${block.type}`);
      return null;
    },
  }}
/>
```

## Theming

Everything is scoped under `.sfs-article` and driven by CSS variables. The defaults derive from `currentColor`, so the article reads well on light and dark pages as installed, and the stylesheet restores list markers and table borders a CSS reset removes.

```css title="your global CSS"
.sfs-article {
  --sfs-font: "Inter", sans-serif;
  --sfs-heading-font: "Bricolage Grotesque", sans-serif;
  --sfs-accent: #7c3aed;          /* links, quote rule, CTA */
  --sfs-accent-contrast: #ffffff; /* text on the CTA */
  --sfs-radius: 16px;
  --sfs-space: 1.25em;            /* rhythm between blocks */
}
```

| Variable | Default | Used by |
|---|---|---|
| `--sfs-font`, `--sfs-heading-font`, `--sfs-mono-font` | inherit, inherit, system mono | text, headings, code |
| `--sfs-text`, `--sfs-muted` | `currentColor`, 65% of it | body, captions |
| `--sfs-border`, `--sfs-surface` | 14% and 5% of `currentColor` | tables, callouts, code |
| `--sfs-accent`, `--sfs-accent-contrast` | teal, white | links, CTA |
| `--sfs-radius`, `--sfs-space` | `12px`, `1.25em` | corners, spacing |

## Every block

Each block has a page with a live render, its JSON and how to replace it: [heading](/docs/components/heading), [paragraph](/docs/components/paragraph), [list](/docs/components/list), [quote](/docs/components/quote), [code](/docs/components/code), [image](/docs/components/image), [callout](/docs/components/callout), [call to action](/docs/components/cta), [FAQ](/docs/components/faq), [table](/docs/components/table), [video](/docs/components/video).
