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;
---
  • @client in 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 Props is 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

  1. Opens BooshCMS, clicks Sign in with GitHub
  2. A code appears; a GitHub page opens in their browser; they type the code in
  3. Their websites are listed — by the name from studio.config.json
  4. They pick one; it is fetched; they are editing
  5. Save keeps work on their computer. Publish — the branching icon on the left bar — sends it live
  6. 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 build succeeds
  • npm run check -- /path/to/site reports nothing
  • studio.config.json at the root, with a name
  • 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 name and 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