# Astro (/docs/getting-started/astro)

This sets up an Astro site that renders every page entry at its path on the server and shows drafts in the canvas. It uses the JS SDK only; the page reloads after each save in the editor.

## 1. Start the repository on a space [#1-start-the-repository-on-a-space]

Create a **management token** under **Settings → API tokens**, then run in the site's root:

```sh
export CONTENT_MANAGEMENT_TOKEN=cms_m_…
npx @obelism/content init
```

`init` writes the model file (`content/content-model.json`), an example of what the delivery API answers for every content type (`content/examples/`), installs `@obelism/content`, and adds the token variables to `.env.example`. Put a delivery token in `.env` (never commit it):

```sh
CONTENT_DELIVERY_TOKEN=cms_d_…
# The space and environment it reads (Space settings), so the canvas's
# token for another space reads nothing here.
CONTENT_SPACE_ID=space_…
CONTENT_ENVIRONMENT=production
```

## 2. Render on the server [#2-render-on-the-server]

Drafts depend on the request, so the pages render on the server: set `output: 'server'` and add the adapter for where the site runs (Node, Cloudflare, Netlify, Vercel).

```js title="astro.config.mjs"
import node from '@astrojs/node'
import { defineConfig } from 'astro/config'

export default defineConfig({
	output: 'server',
	adapter: node({ mode: 'standalone' }),
})
```

## 3. The clients [#3-the-clients]

One client for published content. In the canvas the page's URL carries `cms_token`, a token for the person editing that reads drafts; that request gets a client of its own.

```ts title="src/lib/content.ts"
import { ContentClient } from '@obelism/content'

export const delivery = new ContentClient({
	token: import.meta.env.CONTENT_DELIVERY_TOKEN,
})

// Drafts in the canvas, with the token it adds to the URL. Anyone can
// add `cms_preview=1` to a URL, so that alone shows nothing. The space
// and environment make a token for another space read nothing here.
export const getContent = (url: URL) => {
	const token = url.searchParams.get('cms_token')
	return token
		? new ContentClient({
				token,
				space: import.meta.env.CONTENT_SPACE_ID,
				environment: import.meta.env.CONTENT_ENVIRONMENT,
			})
		: delivery
}
```

## 4. Every page by path [#4-every-page-by-path]

```astro title="src/pages/[...slug].astro"
---
import {
	ContentApiError,
	editable,
	richTextToHtml,
	type Block,
	type Entry,
	type RichTextNode,
} from '@obelism/content'

import { delivery, getContent } from '../lib/content'

type HeroBlock = Block<{ heading: string | null }, 'hero'>
type TextBlock = Block<{ text: RichTextNode | null }, 'text'>
type PageEntry = Entry<
	{ title: string | null; body: (HeroBlock | TextBlock)[] | null },
	'page'
>

const path = Astro.params.slug || 'home'
const entry = await getContent(Astro.url)
	.getPage<PageEntry>(path, { depth: 2 })
	.catch((error: unknown) => {
		// A made-up, expired or other space's `cms_token`: show what's published.
		if (error instanceof ContentApiError && error.status === 401) {
			return delivery.getPage<PageEntry>(path, { depth: 2 })
		}
		throw error
	})
if (!entry) return new Response(null, { status: 404 })

// Let the canvas show the site in its frames.
Astro.response.headers.set(
	'Content-Security-Policy',
	"frame-ancestors 'self' https://content.obelism.studio",
)
const { fields } = entry
---

<html lang="en">
	<head>
		<title>{fields.title}</title>
	</head>
	<body>
		<main {...editable(entry)}>
			<h1>{fields.title}</h1>
			{fields.body?.map((block) =>
				block._type === 'hero' ? (
					<section {...editable(block)}>
						<h2>{block.heading}</h2>
					</section>
				) : (
					<section {...editable(block)} set:html={richTextToHtml(block.text)} />
				),
			)}
		</main>
		<script is:inline src="https://content.obelism.studio/bridge.js"></script>
	</body>
</html>
```

`content/examples/` shows what each of your content types looks like; adjust `PageEntry` to your own model.

- `editable` marks the entry and its blocks (`data-cms-uid`), so editors can click them.
- The bridge script does nothing outside the editor. Inside it, the page reloads after every save; to re-render on each keystroke instead, listen for `ObelismBridge.on('update', ({ entry }) => …)` (see the [canvas](/docs/guides/canvas)).
- The `frame-ancestors` header lets the CMS frame the site. Set it wherever your site sets headers if it sends a stricter policy, and drop `X-Frame-Options: DENY`.

Then set the environment's **preview URL** (Settings) to where the site runs, such as `http://localhost:4321`, and open the **Canvas**.

## Next steps [#next-steps]

- [Rebuild or revalidate on publish](/docs/guides/webhooks) with a webhook.
- [Images](/docs/api/images): resized and re-encoded copies with a query on the URL.
- [A/B tests in content](/docs/guides/ab-tests), with Obelism Improve.
