How to Use Scaffold

A guide to installing, running, and managing the Scaffold template.

Scaffold is a website starter built with Astro. It gives you a ready-to-use site with a page builder, a content management system, and a set of reusable components.

This guide walks you through installation, day-to-day editing, and the main concepts you need to know.

Getting started

Prerequisites

You'll need Git, Node.js (v20 or later), and a GitHub account. A basic familiarity with the command line is helpful but not required beyond the steps listed here.

Installation

Clone the repository and install dependencies:

Terminal window
npx create-astro --template draftlab-org/scaffold
cd your-project-name
npm install

Running the development server

Terminal window
npm run dev

This starts a local server, usually at http://localhost:4321. Changes you make to files will appear in the browser almost instantly.

Building for production

Terminal window
npm run build

This generates a fully static site in the dist/ folder. You can preview the production build locally with:

Terminal window
npm run preview

Deploying

Scaffold is pre-configured for Netlify. Connect your GitHub repository to a Netlify site and every push to main will trigger a new build. No additional configuration is needed — the netlify.toml and Astro adapter are already set up.

You can also deploy to any platform that supports static sites (Vercel, Cloudflare Pages, GitHub Pages, etc.) by swapping the Astro adapter in astro.config.mjs.

How the site works

Pages and sections

Every page on the site is defined by a YAML file in src/content/pages/. A page file looks something like this:

title: About Us
status: published
permalink: about
sections:
- type: hero
title: About Our Organization
subtitle: Building technology for the public good.
- type: richText
content: |
We are a small team of designers and engineers...
- type: people
category: Leadership

The sections array is the core of the page builder. Each section has a type that determines which component renders it. Sections are rendered in order, top to bottom.

Available section types

TypeWhat it does
heroFull-width banner with title, optional subtitle, and optional background image or side image
richTextMarkdown content — supports headings, lists, links, images, code blocks, tables. Optional table of contents sidebar
buttonA row of call-to-action buttons with configurable variants and sizes
cardA grid of content cards, each with optional image, markdown text, and button
peopleA grid of team member profiles pulled from the People collection. Filterable by category
partnersA grid of partner logos pulled from the Partners collection. Filterable by category
articlesRollA feed of recent articles, with optional category filter and limit
featuredPartnersA curated row of partner logos, selected by ID or shown by featured flag
resourcesRollA feed of recent resources (reports, guides, case studies)
featuredArticlesHand-picked articles, shown in the order an editor chose them
callToActionA heading, short pitch and buttons, with an optional image beside them
miniCtaOne line of text and one button — a light nudge between sections
testimonialsQuotes with name, role and photo, or linked to a person. One quote becomes a pull quote
logoWallPartner logos in a grid or a pausable scrolling row, optionally full width

Every section can also set a background to create visual rhythm on the page. bgColor picks from a named palette — white, gray, gradient, primary-light, secondary-light, highlight-light, dark, and highlight-dark — and bgType sets whether it spans the full width, sits behind the content only, or draws the highlight shape. Dark backgrounds automatically switch text, links, and markdown to light colours. The palette lives in src/utils/backgrounds.ts; add an entry there (and to the dropdown in .pages.yml) to offer a new background.

Newer sections (callToAction, testimonials, logoWall, featuredArticles) share a section title with an optional marker-style highlight, and a markdown subtitle.

Buttons

Buttons in any section pick their destination from a page picker (internal pages) or take an external URL. The old free-text href field is still available as "Custom URL" for anchors and mailto: links. Each button has a variant (primary, secondary, outline, ghost), a size, and an optional icon.

Content collections

Beyond pages, the site manages several types of content:

  • Articles (src/content/articles/) — Markdown blog posts with frontmatter for title, excerpt, authors, categories, and hero image.

  • Docs (src/content/docs/) — Multi-chapter markdown documentation. Each doc belongs to a chapter and has an order, so you can build a structured handbook, user guide, or reference section.

  • People (src/content/people/) — Team member profiles with name, title, headshot, affiliation, category tags, and optional social links.

  • Partners (src/content/partners/) — Organizations with logo, URL, one or more categories, and display order.

  • Resources (src/content/resources/) — Publications like reports, whitepapers, and guides with external download links and optional related resources.

  • Navigation (src/content/navigation/) — Menu definitions for the header and footer. Supports single links and dropdown menus up to three levels deep: a dropdown can hold links and labelled groups of links. Breadcrumbs are built from the same menus.

  • Site Settings (src/content/site/config.json) — Global configuration: site title, description, contact email, social links, favicon, Open Graph image, cookie consent, analytics, share links, breadcrumbs, and footer text. Every collection also has optional SEO Title and SEO Description fields. They replace what search engines and social cards show without changing the visible title (the site name is appended automatically).

  • Categories (src/content/categories/) — The category lists for articles, people, partners, and resources. These feed the filters on landing pages and the category dropdowns in PagesCMS (see "Keeping category dropdowns in sync" below).

Each collection has a schema defined in src/content.config.ts. Astro validates every file against its schema at build time, so you'll get a clear error message if something is missing or malformed.

Content status

All content supports a status field with three values:

  • draft — Only visible when running the development server or on a preview build (PUBLIC_PREVIEW=true), where it carries a DRAFT label.

  • published — Visible everywhere.

  • archived — Still visible, but can be styled differently or filtered out.

This lets you work on new content without publishing it, and retire old content without deleting it.

Embedding videos and social posts

Any rich-text or markdown content — article bodies, docs, and richText page sections — supports embeds for YouTube, Vimeo, Bluesky, and Mastodon using a plain markdown link with the literal text EmbedLink:

[EmbedLink](https://www.youtube.com/watch?v=dQw4w9WgXcQ)
[EmbedLink](https://vimeo.com/76979871)
[EmbedLink](https://bsky.app/profile/bsky.app/post/3l6oveex3ii2l)
[EmbedLink](https://mastodon.social/@Mastodon/116539053870420123)

The link must be the sole content of its paragraph (a blank line before and after). YouTube and Vimeo render as lazy-loaded video players; Bluesky and Mastodon posts are fetched at build time and inlined as static cards. If a URL isn't supported or a post can't be fetched, the original link is kept as a regular link.

Using PagesCMS

What is PagesCMS?

PagesCMS is a free, open-source content management system that works directly with your GitHub repository. It gives you a visual interface for editing content — no need to open YAML or JSON files by hand.

When you save changes in PagesCMS, it commits them directly to your repository, which triggers a new build and deploy.

Setting it up

  1. Go to app.pagescms.org and sign in with your GitHub account.

  2. Select the repository where your Scaffold site lives.

  3. PagesCMS reads the .pages.yml file in the root of the repository and automatically sets up the editing interface.

That's it. No API keys, no database, no separate hosting.

What you can do in PagesCMS

  • Edit pages — Add, remove, and reorder sections. Each section type has a form with the relevant fields.

  • Write articles — Use the built-in Markdown editor with live preview.

  • Manage people — Add team members with headshots and roles.

  • Manage partners — Upload partner logos, set display order, and toggle featured status.

  • Manage resources — Add publications with download links and contributor cross-references.

  • Edit navigation — Change header and footer menus, add dropdown menus and grouped sub-menus, link to internal pages or external URLs.

  • Manage redirects — Under Redirects, send visitors from old URLs to new ones (301 or 302). Changes apply on the next deploy.

  • Update site settings — Change the site title, description, social links, cookie consent copy, share buttons, breadcrumbs, and analytics. Umami analytics is off until you fill in its script URL and website ID.

  • Upload images — All images are stored in src/assets/ and automatically optimized at build time.

How images work

When you upload an image through PagesCMS, it gets committed to src/assets/ in your repository. At build time, Astro optimizes these images — resizing, converting to modern formats, and generating responsive srcset attributes.

Image paths in content files usually start with /src/assets/. For example: /src/assets/team/headshot.jpg. Images inserted into article and doc bodies use a path relative to the markdown file instead (../../assets/...), which PagesCMS writes for you.

Keeping category dropdowns in sync

PagesCMS dropdowns can't load their options from elsewhere, so the category dropdowns in .pages.yml are generated from src/content/categories/*.json. After editing a category file, run:

Terminal window
npm run sync:pages-categories

A GitHub Action does the same on every push, and npm run update-from-scaffold re-syncs after each update.

For developers

Project structure

src/
├── assets/ Images and media
├── components/
│ ├── atoms/ Basic elements (Button, Link, Image)
│ ├── molecules/ Combinations (Card, NavItem)
│ ├── organisms/ Complex components (Navigation, Footer)
│ └── sections/ Page section components
├── content/ Content collections (YAML, JSON, Markdown)
├── layouts/ Page layouts
├── pages/ File-based routing
├── styles/ Tailwind theme and global CSS
└── utils/ Utility functions

Components follow atomic design — atoms are the smallest building blocks, molecules combine atoms, organisms combine molecules, and sections are full-width page blocks.

Adding a new page

Create a new YAML file in src/content/pages/. The filename becomes the URL path (e.g., privacy-policy.yaml → /privacy-policy). A GitHub Action keeps filenames and permalink fields in sync automatically.

Customizing styles

Scaffold uses Tailwind CSS v4 with custom theme tokens defined in src/styles/:

  • colors.css — Color palette (primary, secondary, highlight, gray)

  • typography.css — Font families, prose styling, heading scales

  • breakpoints.css — Responsive breakpoints

Change the color palette in colors.css and the entire site updates.

Changing fonts

Fonts are configured in fonts.config.mjs at the repo root — split out from astro.config.mjs so you can change them without conflicting with template updates. The file is also protected on merge, so your font choices survive npm run update-from-scaffold.

Scaffold uses Bunny Fonts as the provider — a privacy-friendly CDN that mirrors Google Fonts' catalogue without third-party tracking. To change a font:

  1. Browse fonts.bunny.net and pick what you want.
  2. Open fonts.config.mjs and update the name and weights for the slot you want to change (--font-sans, --font-serif, or --font-mono).
  3. Restart npm run dev so Astro re-fetches the new font.

Keep the cssVariable names as they are — --font-sans / --font-serif / --font-mono are referenced from typography, components, and Tailwind utilities. Just point them at different fonts.

If you want a different provider (Google Fonts, local files, etc.) see Astro's font docs.

API endpoints

Every content collection is automatically exposed as a JSON API:

  • /api/pages.json

  • /api/articles.json

  • /api/people.json

  • /api/partners.json

  • /api/resources.json

  • /api/search.json — the compact index used by site search

Drafts are left out of every endpoint in production. These are generated at build time and can be used by external tools or client-side JavaScript.

Staying up to date with Scaffold

Scaffold is designed to be forked. Once you've replaced the demo content with your own, you can still pull future Scaffold improvements (components, layouts, utils, tooling) without losing the work you've done.

Pulling updates

Add Scaffold as a remote (one-time):

Terminal window
git remote add template https://github.com/draftlab-org/scaffold.git

Then, whenever you want updates:

Terminal window
npm run update-from-scaffold

That's it. The script handles --allow-unrelated-histories on the first merge, re-applies any deletions you've made in protected paths (so demo content doesn't reappear via modify/delete conflicts), and auto-removes any brand-new upstream files in src/content/, src/assets/, and public/ so our demo content stays out of your production site.

What's protected

Scaffold ships a .gitattributes file that marks these paths as downstream-wins on merge — your version is always kept:

  • src/content/** — all content collections (pages, articles, people, etc.)
  • src/assets/** — uploaded images, logos, artwork
  • src/styles/** — your theme tokens, typography, component utilities
  • public/** — favicons, OG images, and anything else you've added there (robots.txt and llms.txt are generated, so don't add them here)
  • fonts.config.mjs — your font choices (see "Changing fonts" below)

Everything else merges normally. Real code conflicts get flagged like any merge, and the script stops so you can resolve them by hand.

New upstream files and content collections

When upstream adds a brand-new file in src/content/, src/assets/, or public/, the script removes it as part of the merge. Demo content shouldn't sneak into your production site, and you can always add your own files later.

Brand-new files in src/styles/ are not auto-removed — new stylesheets may be required by new components in the merge. Styles that Scaffold's own components need live in src/styles/scaffold.css: leave that file as it is and override in your own stylesheets, so updates keep reaching it.

After merging, the script also re-syncs the category dropdowns in .pages.yml from your own src/content/categories/ files.

If Scaffold ships an entirely new content collection (a new directory under src/content/), you'll see a notice at the end of the run:

ℹ There are new content collections on Scaffold — check out https://scaffold.org to see what's new

The collection's schema arrives via src/content.config.ts (which isn't protected and merges in normally); the demo files in the new directory are removed. If you want to use the new collection, head to scaffold.org to see what it is, then add your own content under that directory.

If you forked before this update script existed

Git reads .gitattributes from your working tree at the start of a merge, so existing forks need a one-time bootstrap to land the protection rules and the script itself:

Terminal window
git fetch template
git checkout template/main -- .gitattributes scripts/scaffold-update.sh
git commit -m "Adopt Scaffold update script"
bash scripts/scaffold-update.sh

Then add the npm shortcut to your package.json scripts so you don't have to remember the bash path:

"update-from-scaffold": "bash scripts/scaffold-update.sh"

After this, npm run update-from-scaffold is all you need.

Doing it without the script

If you'd rather invoke Git directly, the equivalent is:

Terminal window
git fetch template
git merge template/main # add --allow-unrelated-histories on first run

You'll be on your own for modify/delete conflict resolution and new-file review.