CI/CD Pipelines for Static Sites — Automation & Deployment Guide
In this tutorial, you'll learn about CI/CD Pipelines for Static Sites. We cover key concepts, practical examples, and best practices.
CI/CD pipelines automate building, testing, and deploying static sites on every code change — eliminating manual deployments, catching errors early, and enabling preview environments for every pull request before reaching production.
What You'll Learn
Why It Matters
Manual deployments are error-prone and time-consuming. A well-configured CI/CD pipeline builds your site, runs validation checks, deploys to a staging environment, and promotes to production — all triggered by a single git push. This reduces deployment time from minutes to seconds and ensures every change is tested before reaching users.
Real-World Use
A documentation team deploys preview sites for every pull request, letting reviewers verify changes before merging. A marketing team runs Lighthouse performance tests in CI and blocks deploys that drop below a score threshold. An open-source project automatically deploys the latest documentation whenever a new release tag is pushed.
CI/CD Pipeline Architecture
flowchart LR
A[Git Push] --> B[CI Trigger]
B --> C{Build Stage}
C --> D[Hugo Build]
D --> E[Test Stage]
E --> F{Check Type}
F --> G[HTML Validation]
F --> H[Broken Links]
F --> I[Lighthouse CI]
G --> J[Deploy Stage]
H --> J
I --> J
J --> K{Environment}
K -->|PR| L[Preview Deploy]
K -->|Main| M[Staging Deploy]
K -->|Tag| N[Production Deploy]
style A fill:#f90,color:#fff
style B fill:#09f,color:#fff
CI/CD Platform Comparison
| Feature | GitHub Actions | GitLab CI | Cloudflare Pages | Netlify |
|---|---|---|---|---|
| Free minutes | 2,000/mo | 400/mo | Unlimited builds | 300 builds/mo |
| Concurrent jobs | 20 | Unlimited (self-hosted) | Unlimited | 1 |
| Cache support | Yes (actions/cache) | Yes | Built-in | Built-in |
| Preview deploys | Manual setup | Manual setup | Automatic | Automatic |
| Secrets management | Encrypted secrets | Masked variables | Encrypted variables | Encrypted |
| Matrix builds | Yes | Yes | Limited | No |
| Deploy to multiple | Custom script | Custom script | Built-in | Built-in |
GitHub Actions Pipeline
Basic Build and Deploy Workflow
# .github/workflows/deploy.yml
name: Build and Deploy Hugo Site
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
fetch-depth: 0
- name: Setup Hugo
uses: peaceiris/actions-hugo@v3
with:
hugo-version: '0.128.0'
extended: true
- name: Build
run: hugo --gc --minify
- name: Check for broken links
run: |
npx linkinator ./public --recurse --skip "^(?!https?://)"
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./public
Expected behavior: On every push to main or pull request, the workflow checks out the repository, installs Hugo 0.128.0 extended version, builds the site with garbage collection and minification, checks for broken links using linkinator, and uploads the public/ directory as a deploy artifact.
Deploy to Cloudflare Pages via API
# .github/workflows/deploy-cloudflare.yml
name: Deploy to Cloudflare Pages
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Hugo
uses: peaceiris/actions-hugo@v3
with:
hugo-version: '0.128.0'
extended: true
- name: Build site
run: hugo --gc --minify
- name: Deploy to Cloudflare Pages
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: my-hugo-site
directory: public
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
Expected behavior: After a successful Hugo build, the Cloudflare Pages action uploads the public/ directory to your Cloudflare Pages project. The apiToken and accountId are stored as GitHub secrets for security.
GitLab CI Pipeline
# .gitlab-ci.yml
image: registry.gitlab.com/pages/hugo/hugo:latest
variables:
HUGO_ENV: production
before_script:
- apk add --no-cache curl linkinator
pages:
stage: deploy
script:
- hugo --gc --minify
- linkinator ./public --recurse --skip "^(?!https?://)"
artifacts:
paths:
- public
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: always
- if: $CI_MERGE_REQUEST_IID
when: always
environment:
name: production
url: https://example.com
Testing and Validation
HTML Validation
- name: Validate HTML
run: |
npm install -g html-validate
html-validate ./public/**/*.html
Lighthouse CI
- name: Lighthouse CI
run: |
npm install -g @lhci/cli
lhci autorun --upload.target=temporary-public-storage
Expected behavior: Lighthouse CI runs performance, accessibility, SEO, and best-practice audits against the built site. Results are stored temporarily and linked in the CI output. You can configure score thresholds to fail the build if any score drops below a minimum.
Broken Link Detection
- name: Check broken links
run: npx linkinator ./public --recurse --skip "^(?!https?://)"
Environment Management
| Environment | Branch | URL | Indexing | Purpose |
|---|---|---|---|---|
| Development | Feature branches | *.pages.dev |
No | Developer testing |
| Staging | staging |
staging.example.com |
No | QA review |
| Production | main |
example.com |
Yes | Live site |
| Pull request | PR source | pr-123.pages.dev |
No | Code review |
Common Errors
1. CI Pipeline Fails with "command not found: hugo"
The CI runner does not have Hugo pre-installed. Always install Hugo explicitly using an action or script. For GitHub Actions, use peaceiris/actions-hugo. For GitLab CI, use a Hugo Docker image.
2. Build Times Exceeding CI Timeout
Large sites may exceed the default CI timeout (6 hours for GitHub Actions, but build minutes are limited). Optimize build times by caching Hugo's resources/_gen/ directory between runs:
- name: Cache Hugo resources
uses: actions/cache@v4
with:
path: /tmp/hugo_cache
key: ${{ runner.os }}-hugo-${{ hashFiles('**/go.mod') }}
3. Secrets Leaking in CI Logs
Never hardcode API tokens or secrets in workflow files. Use encrypted secrets in your CI platform settings and reference them as ${{ secrets.MY_SECRET }}. Mask secrets in log output.
4. Preview Deployments Not Updating on Subsequent Pushes
If a preview deployment URL remains unchanged but the content is stale, check that the CI pipeline is triggered on every push (including force pushes). Configure Git hooks or webhooks to rebuild when dependencies change.
5. Different Build Outputs Between Local and CI
CI environments may use different Hugo versions, operating systems, or environment variables than your local machine. Pin the Hugo version in CI with hugo-version: '0.128.0' and test builds locally with the same version.
Practice Questions
1. What is the purpose of fetch-depth: 0 in the GitHub Actions checkout step?
It fetches the full Git history, which is required for Hugo's enableGitInfo feature to display accurate "last modified" dates from Git commit timestamps.
2. How do you deploy to multiple environments from a single CI pipeline?
Use conditional rules based on branch names or tags. For example, push to main deploys to production, push to staging deploys to staging, and pull requests deploy to preview environments.
3. What is the benefit of caching Hugo's resources/_gen directory?
It avoids recompiling SCSS, reprocessing images, and reparsing templates on every build, reducing build times by 50-80% for subsequent runs.
4. Why should secrets be stored as CI encrypted variables rather than in the workflow file?
Workflow files are stored in the repository and visible to anyone with repository access. Encrypted variables are masked in logs and only accessible to authorized CI runs.
5. Challenge: Create a GitHub Actions workflow that builds a Hugo site, runs Lighthouse CI with a minimum Performance score of 90, checks for broken links, deploys to Cloudflare Pages staging on PR, and promotes to production only when a GitHub Release is created.
Mini Project: Complete CI/CD Pipeline for a Hugo Site
Build a production-grade CI/CD pipeline:
- Create a GitHub Actions workflow that builds on every push
- Add a caching step for Hugo resources to speed up builds
- Implement broken link checking and HTML validation as test steps
- Deploy preview environments for every pull request
- Deploy to production only from the
mainbranch - Add a
CHANGELOG.mdstep that generates release notes from commit messages - Configure Slack or email notifications for failed builds
Start with the workflow structure:
mkdir -p .github/workflows
touch .github/workflows/ci.yml
Test the pipeline by pushing to a feature branch, creating a pull request, and verifying the preview deployment appears with the correct content. Then merge to main and confirm the production deployment updates.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro