> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stage.systems/llms.txt
> Use this file to discover all available pages before exploring further.

# Wiring

> The files a Next.js Stage site has and what each one does: registry, public page, edit canvas, browse canvas, admin, article body, media slot, publish handler, env.

## Registry

One renderer per component type. It is a client module because it imports the site's components, which may use hooks or animation libraries.

```tsx theme={null}
// lib/registry.tsx
'use client'
import { createPageRenderer, createArticleBody } from '@sp-stage/next/render';
import { Hero } from '@/components/home/Hero';
import { FeatureGrid } from '@/components/home/FeatureGrid';
import { Slider } from '@/components/blog/Slider';

export const PageRenderer = createPageRenderer({ hero: Hero, 'feature-grid': FeatureGrid });
export const ArticleBody = createArticleBody({ slider: Slider });   // per-site blocks only; prose is built in
```

## A renderer

The same anchor helpers, spread as props:

```tsx theme={null}
import { stageAttrs, stagePath } from '@sp-stage/next';
import { StageMedia } from '@sp-stage/next/render';
import { sanitizeHtml } from '@sp-stage/sdk';

export function Hero({ title, body, image, imageVideo, _stageId, _stageType }: HeroProps) {
  return (
    <section {...stageAttrs(_stageId, _stageType)}>
      <h1 {...stagePath('props.title')}>{title}</h1>
      <div dangerouslySetInnerHTML={{ __html: sanitizeHtml(body) }} {...stagePath('props.body', { rich: true })} />
      <StageMedia image={image} video={imageVideo} path="props.image" alt="" sizes="100vw" className="…" priority />
    </section>
  );
}
```

`StageMedia` is the parity of the Astro `<Media>` slot: an image key beside a video key, one element chosen from the pair, static markup safe on the zero-JS canvas, in-view playback as progressive enhancement. `priority` goes on the one above-the-fold slot only. Logos and social-card images stay plain `<img>`.

## A public page

A server component with a per-request cached read:

```tsx theme={null}
// app/(site)/about/page.tsx
import { cache } from 'react';
import { ensureStage } from '@sp-stage/next/server';
import { getPages, getGlobals } from '@sp-stage/sdk';
import { PageRenderer } from '@/lib/registry';

const load = cache(async () => {
  ensureStage();
  const [pages, globals] = await Promise.all([getPages(), getGlobals()]);
  return { page: pages.find((p) => p.slug === 'about') ?? null, globals };
});

export async function generateMetadata() { const { page } = await load(); /* seo fields */ }

export default async function About() {
  const { page, globals } = await load();
  return page ? <PageRenderer page={page} globals={globals} /> : null;
}
```

No `export const revalidate`. Static at build, regenerated only by the publish handler.

## The edit canvas

The one pages-router file. The config line must appear literally; Next extracts it statically and a library cannot export it for you.

```tsx theme={null}
// pages/preview/[[...slug]].tsx
import { getPreviewPageProps } from '@sp-stage/next/server';
import { PreviewDenied } from '@sp-stage/next/render';
import { PageRenderer } from '@/lib/registry';

export const config = { unstable_runtimeJS: false };
export const getServerSideProps = (ctx) => getPreviewPageProps(ctx);

export default function Preview({ page, globals, articles, denied }) {
  if (denied) return <PreviewDenied />;
  return page ? <PageRenderer page={page} globals={globals} articles={articles} /> : <p>No page</p>;
}
```

`getPreviewPageProps` runs the gate (the admin's preview cookie, membership-checked), reads live on every load so a save shows on reload, and renders staged adds from the `?staged=` handoff. `PreviewDenied` is a 404 body that also tells the admin shell to refresh a stale cookie and retry.

The article canvas is the same shape at `pages/preview/<base>/[slug].tsx` with `getPreviewArticleProps`, wrapped in the site's shell with globals passed as props.

## The browse canvas

The hydrated live site, gated, so the admin can browse normally and only swap to the inert canvas while editing:

```tsx theme={null}
// app/browse/[[...slug]]/page.tsx
import { cookies } from 'next/headers';
import { resolvePreviewPage } from '@sp-stage/next/server';
import { PreviewDenied } from '@sp-stage/next/render';

export const dynamic = 'force-dynamic';

export default async function Browse({ params, searchParams }) {
  const [{ slug }, sp, cookieStore] = await Promise.all([params, searchParams, cookies()]);
  const r = await resolvePreviewPage({ cookieHeader: cookieStore.toString(), slug, stagedToken: typeof sp.staged === 'string' ? sp.staged : null });
  if (r.denied) return <PreviewDenied />;
  return r.page ? <PageRenderer page={r.page} globals={r.globals} articles={r.articles} /> : <p>No page</p>;
}
```

`resolvePreviewPage` is the router-neutral core; `getPreviewPageProps` wraps it for the pages canvas.

## The admin

```tsx theme={null}
// app/admin/page.tsx — server shell for metadata; renders the client component below
// app/admin/AdminApp.tsx
'use client'
import { StageAdmin } from '@sp-stage/next/admin';
import { ADMIN_SCHEMA } from '@/lib/admin-schema';

export function AdminApp() {
  return <StageAdmin schema={ADMIN_SCHEMA} previewBase="/preview/" browseBase="/browse/" editChrome="notch" />;
}
```

`browseBase` is optional. Without it the canvas always loads the inert preview. The [edit notch](/developers/structure/edit-notch) works unchanged; a stateful component on the zero-JS canvas ships a tiny inline script that listens for `stage:notch` on its wrapper, pages its state, and stamps `data-e-current`.

## Article bodies

```tsx theme={null}
<ArticleBody body={article.body} className="flex flex-col gap-2" _stageId={article.id} _stageType="article-body" />
```

Prose runs render inline as sanitised HTML with their anchors, including inline images and videos as static markup. The registry only needs the site's custom blocks. Layout stays the site's through `className`.

## Publishing

Set the org's publish mode to `revalidate` and its secret. The admin's Publish button is unchanged; the API posts `{ secret, paths }` to the site's own handler, which regenerates in seconds.

```ts theme={null}
// app/api/revalidate/route.ts
import { createRevalidateRouteHandler, ensureStage } from '@sp-stage/next/server';
import { getArticles } from '@sp-stage/sdk';

export const POST = createRevalidateRouteHandler({
  // The API sweeps page paths. Add the routes only this site knows.
  expandPaths: async (paths) => {
    ensureStage();
    const articles = await getArticles();
    return [...paths, '/news', ...articles.map((a) => `/news/${a.slug}`)];
  },
});
```

`createRevalidateHandler` is the pages-router equivalent with the same options. The secret is compared in constant time; a throwing `expandPaths` still regenerates what the API asked for and reports the failure rather than answering fully successful. Set `REVALIDATE_SECRET` in the site's env to the same value as the org secret.

## Env

| Variable                                                                          | Used by                                              |
| --------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `SUPABASE_URL`, `SUPABASE_ANON_KEY`, `ORG_ID`                                     | Server reads and the canvas gate                     |
| `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_ANON_KEY`, `NEXT_PUBLIC_ORG_ID` | The `/admin` route (or pass props to `<StageAdmin>`) |
| `NEXT_PUBLIC_STAGE_API_URL`                                                       | The shared API                                       |
| `REVALIDATE_SECRET`                                                               | The publish handler                                  |

## Exports at a glance

| Import path             | Exports                                                                                                                                                                                  |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `@sp-stage/next`        | `stageAttrs`, `stagePath`, `stageMeta`, `stageArray`, `stageField`, `stageRef`, `stageMedia`, `EMPTY_IMAGE`                                                                              |
| `@sp-stage/next/render` | `createPageRenderer`, `createArticleBody`, `StageMedia`, `PreviewDenied`                                                                                                                 |
| `@sp-stage/next/server` | `ensureStage`, `verifyEditor`, `getPreviewPageProps`, `resolvePreviewPage`, `getPreviewArticleProps`, `resolvePreviewArticle`, `createRevalidateRouteHandler`, `createRevalidateHandler` |
| `@sp-stage/next/admin`  | `StageAdmin`                                                                                                                                                                             |
