Example site walkthrough
Tour the demo site in the examples/demo-site folder, from its content schema and pull step to rendered articles, the preview route and the health check.
The demo site is a complete TanStack Start website that uses the SDK. It is a fictional news blog for "Acme", with a home page, a blog listing and one page per article. It pulls a published snapshot at build time, prerenders every page, renders rich text, and has a preview route for drafts and a health check. Read it alongside the other SDK guides: each part of the demo links to the page that explains it.
The demo lives in the examples/demo-site folder of the Areca repository. It is a reference for your own project, not a template you must copy.
Framework and tools
| Part | Choice |
|---|---|
| Framework | TanStack Start with TanStack Router and React 19 |
| Server | Nitro, built to .output/server/index.mjs |
| Styling | Tailwind CSS |
| Rich text | @tanstack/markdown with the React Markdown component |
| Validation | Zod, for server function inputs |
| Tests | Vitest |
File layout
Three groups matter most:
areca.config.tsdescribes the content schema that you push to the CMS.src/generated/content/holds the snapshot thatareca pullwrites. The site reads only these files at build time.src/routes/holds the pages, the health check and the preview route.
Define the content
areca.config.ts uses defineSiteContent to describe one collection, articles. Each field has a key, a label and a type:
import { defineSiteContent } from '@areca-cms/site-sdk'
export default defineSiteContent({
siteKey: 'acme-site',
collections: {
articles: {
route: '/blog/$slug',
label: 'Articles',
fields: [
{ key: 'title', label: 'Title', type: 'text', required: true },
{ key: 'slug', label: 'Slug', type: 'slug', required: true },
{ key: 'excerpt', label: 'Excerpt', type: 'textarea' },
{ key: 'body', label: 'Body', type: 'richtext', required: true },
{ key: 'heroImage', label: 'Hero image', type: 'image' },
{ key: 'publishedAt', label: 'Published at', type: 'datetime' },
{ key: 'category', label: 'Category', type: 'select', options: ['news', 'updates'] },
],
},
},
})To send changes to the CMS, run the sync-manifest script. It needs the schema token in .env.local. See Defining content and Schema sync. Routine builds do not need the schema token.
Pull the snapshot
The content:pull script runs the SDK's CLI and writes the snapshot into src/generated/content:
pnpm --filter @areca/demo-site content:pullThe prebuild script, which runs before build, decides where the content comes from:
- If
ARECA_CMS_URLandARECA_SITE_TOKENare set, it always pulls from the CMS. A failed pull fails the build. It never falls back to old files. - If they are not set, and content already exists in
src/generated/content, it reuses that content. - Otherwise it stops with a message that tells you how to pull or create fixture content.
For the pull options, see Pulling content.
Read the content
src/lib/content.ts reads the snapshot through the typed content object in src/generated/content/areca.gen.ts, so the records are typed without any hand-written types, and exposes four functions:
| Function | What it does |
|---|---|
getArticles() | Returns all articles, sorted newest first by publishedAt |
getArticle(slug) | Returns one article, or undefined |
getManifest() | Reads manifest.json, which holds the publicationId |
listPrerenderPages() | Lists /, /blog and every /blog/<slug> for prerendering |
src/lib/queries.ts wraps the first three in TanStack server functions: loadArticles, loadArticle and loadManifest. Route loaders call those functions, not the content functions directly:
const article = await loadArticle({ data: { slug: params.slug } })areca.gen.ts is written by areca pull and committed to the repository. See typed content and Reading content for the full reading API.
The site content sections are read from content.globals instead of a collection. loadCompanyInfo supplies the company details for the header and footer on every page, loadHome the home-page hero, and loadAbout the About page. The home and About pages also pass the value of ?arecaPreview to previewGlobals. See site content.
Render articles
/blog/$slug loads one article in its loader and throws notFound() when the slug is unknown. Its head() sets the page title and, when there is one, the description from the excerpt. The demo does not call seoHead; see SEO helpers for the full version.
The page renders ArticleView, which shows the hero image, the category and date, the title, the excerpt and the body. The body goes through ArticleBody:
import { Markdown } from '@tanstack/markdown/react'
import { richTextDocument } from '@areca-cms/site-sdk'
export function ArticleBody({ body }) {
return <Markdown components={articleComponents}>{richTextDocument(body)}</Markdown>
}articleComponents is defined in the same file. It is an object with one React component for each element, such as h2, p, a and img. The site owns the look of every element, so the CMS never sends markup. The external-link rule in the a component adds rel="noopener noreferrer" and target="_blank". See Rendering rich text.
Prerender the pages
vite.config.ts turns on TanStack Start's prerendering. It lists the pages from listPrerenderPages(), follows links, fails the build on any error, and skips /__preview and /api. The relevant part of the file looks like this:
tanstackStart({
prerender: {
enabled: true,
crawlLinks: true,
failOnError: true,
filter: (page) =>
!page.path.startsWith('/__preview') && !page.path.startsWith('/api'),
},
pages: listPrerenderPages(),
})Published articles are plain HTML files after the build. The verify:prerender script checks that each article has its file.
The preview route
src/routes/[__preview]/$collection/$slug.tsx handles the URL that the CMS opens for a draft. Its server function calls getPreviewEntry with the token from the query string, and sets the previewHeaders() on the response. The page then shows a banner and the same ArticleView as published articles. Errors show a message. The route is excluded from prerendering.
The home page (/) and the About page (/about) show site content drafts instead. Their previewPath is set in areca.config.ts, so the CMS opens /?arecaPreview=<token> or /about?arecaPreview=<token>. Each page reads the token, sets the preview headers and overlays the draft only when the token is for that section. A stale link shows the published page.
For the full pattern, including headers, security and error handling, see Draft preview.
The health check
src/routes/api/health.ts returns the publicationId from the manifest as JSON, with status 200. If the manifest is missing, it returns 503. The CMS uses this URL after a deploy. See Building during Go live.
curl http://localhost:3001/api/healthRun it locally against a CMS
You need a running CMS, a site with at least one published release, and that site's token.
Start the CMS. From the Areca repository root, run pnpm --filter @areca/cms dev. It listens on http://localhost:3000.
Copy the site token. Open the site in the CMS, open the Developer tab of the site settings, and copy the site token. It is shown only once when you create or rotate it. If you lost it, rotate it.
Create .env.local. In examples/demo-site, copy .env.example to .env.local, then set both values:
ARECA_CMS_URL=http://localhost:3000
ARECA_SITE_TOKEN=<your-site-token>The file is ignored by git. Do not commit it.
Pull the snapshot. From the repository root:
pnpm --filter @areca/demo-site content:pullThis fails with a publish-first message if the site has no published release.
Start the site. Run the dev server, which listens on http://localhost:3001:
pnpm --filter @areca/demo-site devTo preview drafts locally, set the site's production URL in the CMS to http://localhost:3001. Preview links are built from that URL, so Preview will open the local demo.
Run without a CMS
To work on the site's pages without a CMS, write the checked-in sample content and start the dev server:
pnpm --filter @areca/demo-site content:fixture
pnpm --filter @areca/demo-site devThe sample content has three articles. Preview and the live pull need a CMS, so they do not work in this mode.
Scripts
| Command | Purpose |
|---|---|
pnpm --filter @areca/demo-site dev | Run the dev server on port 3001 |
pnpm --filter @areca/demo-site build | Pull (when configured), then build the production output |
pnpm --filter @areca/demo-site start | Serve the built output on port 3000 |
pnpm --filter @areca/demo-site content:pull | Pull the published snapshot into src/generated/content |
pnpm --filter @areca/demo-site content:fixture | Write the sample content |
pnpm --filter @areca/demo-site sync-manifest | Send the schema in areca.config.ts to the CMS. Needs ARECA_SCHEMA_TOKEN. |
pnpm --filter @areca/demo-site typecheck | Check the TypeScript types |
pnpm --filter @areca/demo-site test | Run the unit tests |
pnpm --filter @areca/demo-site verify:prerender | Check that each article was prerendered |
Docker
The demo has its own Dockerfile. It follows the same pattern as the Go live build guide: the snapshot ID is a build argument, and the site token is a BuildKit secret. Run the build from the repository root, because the Dockerfile copies the SDK package from there:
docker build -f examples/demo-site/Dockerfile \
--build-arg ARECA_CMS_URL=http://host.docker.internal:3000 \
--build-arg ARECA_SNAPSHOT_ID=local-test \
--secret id=ARECA_SITE_TOKEN,env=ARECA_SITE_TOKEN \
-t areca-demo-site .Set ARECA_SITE_TOKEN in your shell before you run it. Use the CMS address that the build container can reach. On Docker Desktop, host.docker.internal points at your machine.
Related
- Installation for adding the SDK to your own project.
- Defining content for writing the schema.
- CLI reference for every
arecacommand.
Building during Go live
What your website build must do when Areca deploys it on Go live, including the snapshot build argument, the pull step, the Dockerfile and the health check.
Quickstart with an AI agent
Let an AI coding agent set up Areca in a new website project. The agent checks your project, asks a few questions, builds the content model, connects the site SDK and makes a first release.