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
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)
flexiA container for nesting other sections (richText, button, card) inside a single block

Every section can also set a background with bgColor (white, gray, or gradient) to create visual rhythm on the page.

Content collections

Beyond pages, the site manages several types of content:

  • Articles (src/content/articles/) — Markdown blog posts with frontmatter for title, excerpt, authors, tags, 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, and category tags.

  • Partners (src/content/partners/) — Organizations with logo, URL, category, and display order.

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

  • Navigation (src/content/navigation/) — Menu definitions for the header and footer. Supports single links and dropdown menus.

  • Site Settings (src/content/site/config.json) — Global configuration: site title, description, social links, favicon, Open Graph image, cookie consent, and footer text.

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.

  • 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, link to internal pages or external URLs.

  • Update site settings — Change the site title, description, social links, analytics ID, and cookie consent copy.

  • 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 always start with /src/assets/. For example: /src/assets/team/headshot.jpg.

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

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, robots.txt, and anything else you've added there
  • 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.

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.