In a static site (or any small product site), β€œjust push HTML” works until the same titles and dates must appear in several places. The practical problem is: how do you keep lists, sitemaps, and deploy config consistent without relying on memory β€” β€œremember to update this page,” β€œremember to run that script”?

That is what a small CI/CD contract is for: one source of truth, a build that generates the rest, and a deploy artifact the host can trust. This post is about that idea on GitHub Pages and Actions. That problem is what led me to add a catalog file, a sync script, and an Actions deploy workflow for my own site β€” the same boundaries you can reuse on other static projects.

Why a clear deploy contract matters

The hard part of CI/CD is rarely the button that deploys. It is knowing which files are inputs, which files are generated, and which files must reach production. Without that contract, teams rely on memory: β€œremember to update this,” β€œremember to run that script,” β€œremember to include this file.”

That works until it does not. The site may still go live, but the result can be incomplete or inconsistent. So the useful question is not only β€œhow do we deploy?” It is β€œwhat does a correct release require?”

In practice, that usually means:

Two ways to ship on GitHub Pages

Deploy from a branch publishes whatever is in main (or docs/). It is fine for plain static files, but it assumes the repository already contains the final output.

GitHub Actions adds an explicit build stage. The repository can keep sources, templates, and config; the workflow turns those into a deployable artifact.

The difference is architectural: branch deploys publish repository state, while Actions deploys publish build output. Once you care about derived files, validation, or automation, that distinction matters.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Repo (source)                          β”‚
β”‚  content Β· config Β· templates           β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  GitHub Actions                         β”‚
β”‚  validate β†’ build β†’ package             β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Deploy artifact                        β”‚
β”‚  static HTML + assets + config          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Custom domain (CNAME + DNS)            β”‚
β”‚  domain points to deployed artifact     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

One source of truth

A good CI/CD pipeline starts by separating what people write from what the build should create. If a title, date, or slug appears in three places, it should live in one place and be reused during the build.

For a static site, that source can be a small JSON or YAML file. The pipeline reads it once and generates the repeated parts of the site automatically.

A simple way to think about it:

If something is just a copy of data you already have, do not maintain it by hand. Let CI generate it.

Why generate static HTML (not only JSON + JS)

One option is to load JSON in the browser and render repeated sections with JavaScript. That is often fine for performance. The tradeoff is that the browser becomes part of the build process.

Generating HTML in CI moves that responsibility back to the pipeline. The response already contains the final markup, which is usually better for SEO, previews, caching behavior, and debugging. The architecture principle is simple: do as much deterministic work as possible before runtime.

What the workflow does

A good GitHub Actions workflow for a static site usually has four stages:

  1. Checkout the repository.
  2. Validate and build generated files from the source data.
  3. Package only the files that belong in production.
  4. Deploy the artifact with the hosting provider's official action.

That structure scales well. Today the build step might only generate repeated listing sections. Tomorrow it can lint metadata, fail on broken links, verify feeds, minify assets, or publish preview environments on pull requests.

The important point is that deployment becomes the last stage of a build contract, not the first place where source files happen to land.

Automation boundaries

Not everything belongs in CI, and not everything belongs in the repo as static output. A useful boundary is:

Once those boundaries are clear, debugging becomes easier too. Incomplete generated content is a build issue. A broken domain is a deploy or DNS issue. A stale page is often cache invalidation. Good architecture makes failure modes easier to classify.

Principles that carry well beyond static sites

Those principles apply to mobile release pipelines, backend deploys, static sites, and internal tools. The platform changes; the boundary design does not change that much.

What we gained

The payoff is not only convenience. It is confidence. Once CI owns validation, generation, packaging, and deploy, publishing becomes a repeatable system instead of a checklist someone might forget.

Start small: one source file, one build step, one deploy workflow. Then add checks where mistakes actually happen. Good CI/CD is usually not fancy. It is clear, repeatable, and boring in the best way.

I build native iOS and Android apps and share notes like this as I ship. If architecture, automation, or delivery pipelines are your thing too, let's connect on LinkedIn.