What a static site pipeline is for
With no compilation and no test suite in the traditional sense, it is tempting to conclude that a static site does not need a pipeline. It does, but for different reasons than an application does. The pipeline exists to answer four questions before anybody sees the result:
- Is the content valid — does every JSON file parse?
- Is the markup sound — are there broken internal links or missing images?
- Has anything regressed — accessibility, page weight, headers?
- Is what is deployed exactly what was reviewed?
Validate the content layer first
On a configuration-driven site, a single trailing comma in a JSON file breaks navigation across every page, and it does so silently in the browser console rather than loudly at build time. This is the highest-value check in the whole pipeline and takes three lines.
Fail fast on malformed JSON
- name: Validate JSON
run: |
set -e
find config content -name '*.json' -print0 \
| xargs -0 -n1 python -m json.tool > /dev/null
echo "All JSON files parsed successfully."
Check links before your visitors do
Internal link rot is the most common defect on a corporate website and the least excusable, because it is entirely detectable. A crawl of the built output that fails the job on any internal 404 removes the class of problem permanently.
Treat external links differently. They fail for reasons outside your control, and a pipeline that goes red because someone else's site is having a bad afternoon quickly trains people to ignore red. Report external failures; do not fail on them.
Note
A check that is allowed to fail spuriously will be disabled within a month. Design every gate so that a failure is always the team's fault and always actionable.
Add the checks that machines are good at
Automated accessibility testing catches roughly a third of issues, which is a poor substitute for manual testing and an excellent substitute for nothing. Run it on a representative set of templates rather than every page — homepage, an article, a listing, a form and a legal page will surface almost everything.
| Gate | Runs on | Blocks merge? |
|---|---|---|
| JSON validation | Every push | Yes |
| HTML validation | Every push | Yes |
| Internal link check | Pull requests | Yes |
| External link check | Weekly schedule | No — report only |
| Accessibility scan | Pull requests | Yes, on new violations |
| Page weight budget | Pull requests | Yes |
| Header verification | After deployment | Alerts on failure |
Deploy from a protected branch, with a scoped identity
The deployment job is the only part of the pipeline with production rights, so it should be the only job with credentials, and those credentials should be federated rather than stored.
Scoping the deployment job
deploy:
needs: [validate, check-links]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
environment: production
permissions:
id-token: write # federated credential
contents: read # nothing else
Watch out
Pin third-party actions to a full commit SHA rather than a tag. Tags are mutable: an action you reviewed last year can be rewritten under the same name, and it runs with whatever permissions you granted the job.
Verify after deployment, not only before
A pipeline that goes green when the upload finishes has proved that the upload finished. Add a short post-deployment check that requests the live homepage and asserts the things you actually care about: a 200 status, the expected security headers, and a real 404 on a deliberately invalid path.
This is where header regressions get caught. Configuration files are edited far more often than anyone expects, and a policy that silently stopped applying three months ago is a common finding.
Keep it short enough to trust
A pipeline that takes eleven minutes stops being a feedback loop and starts being an interruption. For a site of this size, aim for under three minutes to the first useful signal. Everything slower belongs on a schedule rather than in the path of a merge.
The goal throughout is unremarkable: someone edits a paragraph, opens a pull request, sees a preview, gets a review, merges, and the change is live a few minutes later with nobody's heart rate elevated. That is the whole ambition.