Getting started
Building and handing over a site
End to end for the developer: build the site, hand it over, and test that the hand-over worked.
End to end: building a site, handing it over, and what the client does with it. Written to be followed in order.
If you are about to build a test site, Part 1 is the checklist and Part 5 is what to verify.
Part 1 — Build a site BooshCMS can edit
1.1 The minimum
your-site/
├── studio.config.json ← declares this site to Boosh CMS
├── package.json ← must list astro as a dependency
├── package-lock.json ← commit it; it is what makes installs safe
└── src/
├── content.config.ts ← collections, typed with Zod
├── content/
│ └── blog/*.md ← one file per entry
├── components/
│ └── Hero/
│ ├── Hero.astro ← interface Props, marked @client
│ └── Hero.css
├── layouts/
│ └── Layout.astro ← its props become the page's settings
├── pages/
│ └── index.astro ← layout + component elements, nothing else
└── assets/ ← where pictures land
fixtures/demo-site in this repository is exactly this, working. Copy it as a
starting point rather than assembling one from memory.
1.2 studio.config.json — at the repo root
{
"name": "Bramble & Co",
"publish": {
"mode": "direct",
"branch": "content-updates",
"note": "Your changes go straight to the website."
}
}
This file is what makes the site appear in the client’s picker, and name is
what they see — so they get “Bramble & Co”, not “acme-client-v2”.
mode is direct (straight live) or review (to a branch you merge). Leave the
whole file out and everything still works, except the site will not be listed.
1.3 Collections — for anything repeatable
Blog posts, team, reviews, FAQs, case studies. One markdown file per item.
// src/content.config.ts
import { defineCollection, reference, z } from 'astro:content'
import { glob } from 'astro/loaders'
const blog = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
schema: ({ image }) => z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
cover: image().optional(),
draft: z.boolean().default(false),
tags: z.array(z.string()).default([]),
author: reference('team').optional(),
}),
})
export const collections = { blog }
Each field becomes a control. z.enum gives radio buttons, image() a picture
picker, reference() a dropdown, an array of objects a repeater. A field named
draft gets the client a Save as draft button.
Post metadata for SEO goes here, not on a layout — the client never opens
[slug].astro.
1.4 Components — for the blocks on a page
---
/** @client */
import './Hero.css';
interface TrustItem { stat: string; label: string }
interface Props {
/** The big line at the top of the page. */
heading: string;
subheading?: string;
align?: 'left' | 'center';
trustItems?: TrustItem[];
}
const { heading, subheading, align = 'left', trustItems } = Astro.props;
---
@clientin the frontmatter marks it as placeable. Mark none and everything is offered; mark one and only marked ones are — so mark them all in the same pass.interface Propsis the form. No declaration, no fields.- Declare nested shapes in the same file. An imported type renders read-only.
<slot />lets a component hold other blocks.- A JSDoc comment above a prop becomes help text.
1.5 Pages — layout plus blocks, nothing else
---
import Layout from '../layouts/Layout.astro';
import Hero from '../components/Hero/Hero.astro';
---
<Layout title="Bramble & Co" description="Garden design and upkeep.">
<Hero heading="Gardens that look after themselves" />
</Layout>
One line of hand-written markup makes the page read-only in the app. A stray
<main>, an <h1>, a loop — any of it. That is deliberate: half-parsing a page
and writing it back would corrupt your work.
Listing pages and [slug].astro belong to you. The client is told so plainly.
1.6 The layout — its props are the page’s settings
---
interface Props {
/** Shown in the browser tab and in search results. */
title: string;
description?: string;
socialImage?: string;
noindex?: boolean;
}
---
These appear as Page settings when no block is selected. Put SEO here for pages; in the collection schema for posts.
1.7 Lock a prop you do not want touched
<Reviews items={reviews} heading="What people say" />
An expression is preserved verbatim and shown as “Set by your developer”. This is how you wire a block to a collection: the client edits reviews in one place and the block picks them up.
1.8 Check it before anyone else sees it
npm run check -- /path/to/your-site
Reports settings that no longer match their component, blocks whose component has gone, and entries that would fail their own schema. Exits non-zero, so it works on a pre-commit hook or in CI.
Part 2 — Put it on GitHub and hand it over
2.1 One repository per client site
Two topologies, both fine:
| You own it | Add the client as an outside collaborator with write access. You keep continuity. |
| The client owns it | You are the collaborator. Handover is already done and they can revoke you — the better story, and the product’s own pitch. |
2.2 Access settings that actually matter
- Write, not admin. Write publishes; admin can delete the repository.
- If you use an organisation, set base permissions to “No permission” (Organisation → Settings → Member privileges) and grant per repository. The default hands every member every repository. This is the one that catches people out.
- No secrets in the repository. The client can read every file. Form endpoints and API keys belong in your host’s environment variables.
2.3 Commit the lockfile
package-lock.json is what lets the app install the versions you chose rather
than resolving fresh on the client’s machine. Without it the app holds resolution
a week back as a precaution, which is worse than just committing the file.
2.4 Tell the client to make a GitHub account
If they do not have one. Free. It should exist for editing the website and nothing else — that is also what keeps the token’s reach uninteresting.
Part 3 — What the client does
- Opens BooshCMS, clicks Sign in with GitHub
- A code appears; a GitHub page opens in their browser; they type the code in
- Their websites are listed — by the
namefromstudio.config.json - They pick one; it is fetched; they are editing
- Save keeps work on their computer. Publish — the branching icon on the left bar — sends it live
- In review mode, Publish reads Send for review and goes to a branch you merge
They never see a folder, a path, a branch, a commit, or a filename.
Part 4 — Running it while developing
npm run dev # the app, with hot reload
npm test # 100 tests, core only, no Electron needed
npm run typecheck
npm run check -- /path/to/a/site
npm run build
npm run dist:mac # untested — see the roadmap
Work on a copy of any site you care about. The app writes real files immediately; there is no undo beyond git.
Part 5 — Testing the concept
In order. Each step assumes the one before worked.
5.1 The site itself
-
npx astro buildsucceeds -
npm run check -- /path/to/sitereports nothing -
studio.config.jsonat the root, with aname - At least two components marked
@client, one with a<slot /> - At least one collection with several field types
- One page that is layout + blocks, and one that is not — to see both
5.2 Opening it
- Sign in with GitHub completes and the code is accepted
- The site appears in the list, under its
nameand not the repo slug - Fetching shows progress and finishes
- Collections appear as tabs; pages list; the hand-written one says View only
5.3 Editing
- A post’s fields match the schema, and the right controls appear
-
/in the writing area opens the insert menu - A picture can be added and lands smaller than it started
- A block can be added, reordered by dragging, and renamed
- Page settings appear when no block is selected
5.4 Publishing
- Save leaves it unpublished; the status strip says so
- Publish succeeds and the commit appears on GitHub
- The site rebuilds on your host
- History lists versions and one can be put back
5.5 Two people at once
- Edit a page in the app; push a different change to the same page from elsewhere; publish. Only the field both changed should be asked about
- Do the same with a post. That should be a whole-file choice
5.6 The preview
- Settings → Functionality offers to set the preview up
- The consent screen names the packages and the registry
- Anything published in the last week is flagged
- Afterwards the preview shows the live site and updates as you type
When something does not work
| What you see | What it means |
|---|---|
| Site not in the list | No studio.config.json at the repo root, or the account has no write access. Use show everything else to confirm |
| View only on a page | It has hand-written markup. Expected for listings and anything with a loop |
| A block has no fields | No interface Props, or the props are all imported types |
| Set by your developer | The value is an expression, or a type that cannot be a field. Working as intended |
| A setting no longer fits | A prop was renamed. Move or discard it; npm run check catches these first |
| Preview unavailable | The site’s dependencies are not installed. Editing works regardless |
| Cannot publish | Not signed in, no internet, or the repository has no remote |
| Not connected to an online copy | The folder is not a git repository with a remote |