---
title: React
description: "Lee lo que publicas desde cualquier app de React con @hollware/react: React Router, Next.js o TanStack Start, con vistas previas y tus componentes."
section: frameworks
order: 3
---

# React

`@hollware/react` funciona en cualquier app de React que renderiza en un servidor: un loader de React Router, un Server Component de Next.js o una server function de TanStack Start. Trae helpers de servidor que leen el contenido publicado, y componentes que lo renderizan con tus propios componentes de React y se hidratan.

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

## Lee desde el servidor

En Cloudflare, vincula tu Worker de Hollware en el `wrangler.jsonc` del sitio y lee por ahí, sin red y sin token:

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

```ts
// app/hollware.server.ts: el sufijo `.server` deja el binding fuera del bundle del navegador
import { env } from 'cloudflare:workers'
import { createHollware, hollwareBinding } from '@hollware/react'

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

En cualquier otro lugar, dale a `createHollware` la `url` de tu Hollware y un token de lectura (`npx hollware token create --read`) en vez de `reader`. Mantén el token en el servidor.

| Helper | Entrega |
| --- | --- |
| `getEntry(collection, locale, slug, { request })` | `{ entry, preview }`, o `undefined` si la entrada no está publicada |
| `listEntries(collection, { locale, tag, limit, cursor })` | una página de entradas publicadas, de la más nueva a la más antigua |
| `getTranslations(collection, group)` | el `{ locale, slug }` de cada entrada de un grupo de traducción |
| `search(query, options)` | las entradas publicadas que coinciden, y sus pasajes |
| `redirectFor(collection, locale, slug)` | dónde está ahora una entrada que cambió de slug |

Una entrada es lo que ve un lector: su frontmatter, con los campos que llena Hollware ya llenos, sus etiquetas, sus fechas, sus imágenes y su documento. Su veredicto y su revisión se quedan en el servidor, porque los datos de un loader llegan al navegador.

## 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', 'es', 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, un 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', 'es', (await params).slug, { preview })
  if (!answer) notFound()
  return <HollwareContent entry={answer.entry} components={{ Callout }} />
}
```

## Los componentes

- **`<HollwareContent entry components />`** renderiza la entrada desde el documento que Hollware guardó al escribirla: ningún parser de Markdown llega a la página. `components` asigna los componentes de tus entradas (`::callout` es `Callout`) a tus propios componentes de React; todo lo demás se renderiza como su etiqueta. Sus imágenes llegan con su `srcset`, `sizes`, `width` y `height`.
- **`<HollwareImage entry field />`** renderiza la imagen que guarda un campo del frontmatter, como una portada, con su alt, su tamaño y las copias que Hollware hizo de ella. Cualquier prop que le des gana: `<HollwareImage entry={post} field="img" loading="lazy" />`.

Para un renderizador propio, `imageOf(entry, field)` entrega los mismos atributos.

## Vistas previas

**Vista previa en el sitio**, en el admin, abre la página de la entrada con `?hollware-preview=<token>`, un enlace firmado solo para esa entrada, válido por una hora. `previewRedirect(request)` lo responde con un `303` que guarda el token en una cookie `HttpOnly`: en React Router, desde un middleware de `app/root.tsx`; en Next.js, desde `middleware.ts`. Luego `getEntry` lee la última revisión, borrador incluido, con `preview: true`, y `previewHeaders(request)` deja la página fuera de toda caché.

## Un sitio completo

`examples/react-router-blog` en el repositorio es un blog en React Router sobre un Worker que lee el playground, con vistas previas.
