---
title: React
description: "Read what you publish from any React app with @hollware/react: React Router, Next.js or TanStack Start, with previews of drafts and your own components."
section: frameworks
order: 3
---

# React

`@hollware/react` works in any React app that renders on a server: a React Router loader, a Next.js Server Component or a TanStack Start server function. It has server helpers that read published content, and components that render it with your own React components and hydrate.

```sh
npm install @hollware/react
```

## Read from the server

On Cloudflare, bind your Hollware Worker in the site's `wrangler.jsonc` and read over it, with no network and no token:

```jsonc
"services": [{ "binding": "HOLLWARE", "service": "my-cms", "entrypoint": "Hollware" }]
```

```ts
// app/hollware.server.ts: the `.server` suffix keeps the binding out of the browser's bundle
import { env } from 'cloudflare:workers'
import { createHollware, hollwareBinding } from '@hollware/react'

export const hollware = createHollware({
  site: 'my-site',
  reader: () => hollwareBinding(env.HOLLWARE),
})
```

Anywhere else, give `createHollware` your Hollware's `url` and a read token (`npx hollware token create --read`) instead of `reader`. Keep the token on the server.

| Helper | Gives |
| --- | --- |
| `getEntry(collection, locale, slug, { request })` | `{ entry, preview }`, or `undefined` for an entry that is not published |
| `listEntries(collection, { locale, tag, limit, cursor })` | a page of published entries, newest first |
| `getTranslations(collection, group)` | the `{ locale, slug }` of each entry of a translation group |
| `search(query, options)` | the published entries that match, and their passages |
| `redirectFor(collection, locale, slug)` | where an entry that changed its slug is now |

An entry is what a reader sees: its frontmatter, with the fields Hollware fills already filled, its tags, its dates, its images and its document. Its verdict and its revision stay on the server, because a loader's data reaches the browser.

## React Router

```tsx
// app/routes/post.tsx
import { data, useLoaderData, type LoaderFunctionArgs } from 'react-router'
import { HollwareContent, HollwareImage, previewHeaders } from '@hollware/react'
import { hollware } from '../hollware.server'

export async function loader({ request, params }: LoaderFunctionArgs) {
  const answer = await hollware.getEntry('blog', 'en', params.slug!, { request })
  if (!answer) throw data('Not found', { status: 404 })
  return data({ entry: answer.entry }, { headers: previewHeaders(request) })
}

export default function Post() {
  const { entry } = useLoaderData<typeof loader>()
  return (
    <article>
      <h1>{String(entry.frontmatter.title)}</h1>
      <HollwareImage entry={entry} field="img" />
      <HollwareContent entry={entry} components={{ Callout }} />
    </article>
  )
}
```

## Next.js

```tsx
// app/blog/[slug]/page.tsx, a Server Component
import { cookies } from 'next/headers'
import { notFound } from 'next/navigation'
import { createHollware, HollwareContent, PREVIEW_COOKIE } from '@hollware/react'

const hollware = createHollware({ site: 'my-site', url: process.env.HOLLWARE_URL!, token: process.env.HOLLWARE_TOKEN! })

export default async function Post({ params }: { params: Promise<{ slug: string }> }) {
  const preview = (await cookies()).get(PREVIEW_COOKIE)?.value
  const answer = await hollware.getEntry('blog', 'en', (await params).slug, { preview })
  if (!answer) notFound()
  return <HollwareContent entry={answer.entry} components={{ Callout }} />
}
```

## The components

- **`<HollwareContent entry components />`** renders the entry from the document Hollware stored when it was written: no Markdown parser reaches the page. `components` maps the components of your entries (`::callout` is `Callout`) to your own React components; anything else renders as its plain tag. Its images come with their `srcset`, `sizes`, `width` and `height`.
- **`<HollwareImage entry field />`** renders the image a frontmatter field holds, such as a cover, with its alt, its size and the copies Hollware made of it. Any prop you give wins: `<HollwareImage entry={post} field="img" loading="lazy" />`.

For a renderer of your own, `imageOf(entry, field)` gives the same attributes.

## Previews

**Preview on the site**, in the admin, opens the entry's page with `?hollware-preview=<token>`, a signed link for that one entry, valid for an hour. `previewRedirect(request)` answers it with a `303` that keeps the token in an `HttpOnly` cookie: in React Router, from a middleware of `app/root.tsx`; in Next.js, from `middleware.ts`. `getEntry` then reads the latest revision, a draft included, with `preview: true`, and `previewHeaders(request)` keeps the page out of every cache.

## A whole site

`examples/react-router-blog` in the repository is a React Router blog on a Worker that reads the playground, previews included.
