Pipeline stages (arjankc.com.np)

  1. Checkout repo + submodules (optional skip flags)
  2. npm ci + Jekyll build for blog
  3. deploy.sh — rsync static root, prune forbidden artifacts
  4. Sitemap generation (tools/build/generate-sitemap.js)
  5. Upload dist/ to GitHub Pages

This is not a theoretical pipeline — it is what runs on every push to main for arjankc.com.np. At Gurkha Technology we use the same pattern for client static sites, documentation hubs, and hybrid repos where marketing pages live beside a Jekyll or Vite sub-app. The goal is simple: one commit, one reproducible build, zero manual FTP uploads.

Why not build on Pages alone?

This site mixes root static HTML, Jekyll blog, and Vite sub-apps (NetQuest, etc.). A single Jekyll-only build cannot produce the full dist/.

GitHub Pages’ built-in Jekyll mode works for pure Jekyll repos. It breaks down when you need:

  • Root-level static HTML outside the Jekyll source folder
  • Multiple Vite or React sub-apps compiled into the same output tree
  • Post-build SEO scripts (sitemap, schema injection, canonical normalization)
  • Artifact validation that fails the build if scripts/ or source tooling leaks into production

Our deploy.sh orchestrates rsync from the repo root, Jekyll output from /blog, compiled submodule assets, and post-processing scripts under tools/build/. Treating CI as “just run Jekyll” would ship an incomplete site.

GitHub Actions CI/CD for Static Sites (Jekyll & Vite) — Production Pipeline

For agencies in Nepal building client sites, the lesson is: match the pipeline to the repo structure, not the hosting marketing page. A brochure site with one Vite contact form needs a different workflow than a pure Markdown docs site.

Step-by-step: production workflow structure

Here is how we split the workflow into reliable stages:

Stage Purpose Typical failure
Checkout Full history + submodules Submodule auth or stale .gitmodules
Path filter Skip submodule rebuilds when unchanged Wrong glob in filter config
Node + Ruby setup Lock versions via .nvmrc / Gemfile.lock Version drift between local and CI
Cache restore Jekyll + submodule build cache Cache key too broad or too narrow
Lint + test Validate tooling before expensive build ESLint on build scripts blocks deploy
Build npm run builddeploy.sh Missing env flag for skip logic
Validate validate-dist-artifacts.js Forbidden files in dist/
Upload + deploy Pages artifact → live Wrong artifact path

1. Checkout and permissions

The workflow needs pages: write and id-token: write for OIDC-based Pages deployment. Use fetch-depth: 0 if your build depends on git history (ours does for some SEO tooling).

2. Conditional submodule builds

Submodule Vite apps are expensive. We use dorny/paths-filter to detect changes under submodule paths and a build cache keyed on submodule file hashes. When nothing changed and cache hits, BUILD_SKIP_SUBMODULES=1 skips recompilation. This cut average build time significantly on blog-only commits.

3. Dual runtime setup

Jekyll lives in /blog with its own Gemfile. Root-level Node handles build orchestration. Setup both explicitly:

  • actions/setup-node@v4 with cache: npm
  • ruby/setup-ruby@v1 with working-directory: blog and bundler-cache: true

Nepali dev teams often develop on Windows or mixed OS — lock Ruby and Node versions in repo config, not in someone’s laptop notes.

4. Pre-build validation

Before the heavy build, we run npm test on API JSON fixtures and ESLint/Prettier on tools/build/ scripts. Catching a broken sitemap generator before Jekyll runs saves 5+ minutes per failed deploy.

5. Build and post-build guards

npm run build invokes deploy.sh, which:

  • Rsyncs static root HTML, assets, and legal pages
  • Builds Jekyll into the unified dist/ tree
  • Runs submodule builds unless skipped
  • Executes post-build SEO scripts (sitemap, schema, canonical fixes)
  • Strips forbidden paths (docs/, tools/, raw scripts/)

Then validate-dist-artifacts.js fails the job if production artifacts contain development-only files. We have seen agencies accidentally ship .env.example or internal PDFs — this guard is non-negotiable for client work.

Essential guards

  • validate-dist-artifacts.js — fail if deploy.sh or scripts/ leak
  • Strip AggregateRating from service pages post-build
  • Exclude docs/ and tools/ from rsync

SEO-specific post-processing

Static sites do not get a CMS early warning system. Broken canonicals and duplicate sitemap entries accumulate silently. Our pipeline runs:

  • generate-sitemap.js — unified sitemap for root + blog + sub-apps
  • normalize-seo.js / inject-schema.js — structured data consistency
  • AggregateRating stripping on service pages where auto-generated schema would violate Google guidelines

After deploy, IndexNow submission notifies search engines of sitemap updates. See our automated SEO pipeline architecture for the broader content automation context.

Starter workflow snippet

- run: npm ci
- run: BUILD_SKIP_SUBMODULES=1 bash deploy.sh
- run: node tools/test/validate-dist-artifacts.js
- uses: actions/upload-pages-artifact@v3
  with:
    path: dist

For a minimal Jekyll-only site, replace deploy.sh with cd blog && bundle exec jekyll build --destination ../dist. Keep the artifact upload pattern identical.

Full deploy job pattern

deploy:
  environment:
    name: github-pages
    url: $
  runs-on: ubuntu-latest
  needs: build
  steps:
    - uses: actions/deploy-pages@v4
      id: deployment

Enable GitHub Pages source = “GitHub Actions” in repo settings. The first deploy often fails until this is set — a common support ticket pattern for client handoffs.

Common mistakes (and what we learned)

Mistake Symptom Fix
Building on master vs main mismatch Push does not trigger workflow Align branch names in on.push.branches
Submodule SSH URLs in CI Checkout fails Use HTTPS URLs or deploy keys
No artifact validation Internal paths live in production Add dist validation script
Caching Jekyll without key on _config.yml Stale layouts after config change Include config in cache hash
Skipping concurrency group Overlapping deploys race Use concurrency: group: pages
Local-only build flags “Works on my machine” Document env vars in README

Nepal-specific notes

  • Power and connectivity: CI runs in the cloud — local load-shedding does not block deploys. That alone justifies GitHub Actions for Kathmandu teams.
  • Registrar DNS lag: After first Pages deploy, .com.np nameserver or CNAME changes at register.com.np can take 24–48 hours. Plan client launch dates accordingly — see web hosting in Nepal.
  • Client repos on free tiers: Public client portfolio sites fit free Actions minutes. Private client repos on org free plans — monitor minute usage if you rebuild heavy frontends daily.
  • Handoff documentation: Nepali SMEs rarely maintain CI config themselves. Deliver a one-page “how to publish” doc: edit content → push to main → wait for green check.

When to add preview deployments

GitHub Pages lacks Netlify-style branch previews out of the box. Options:

  • Separate workflow on pull_request uploading artifact without deploy (download ZIP for QA)
  • Cloudflare Pages or Netlify for preview, GitHub Pages for production
  • Playwright visual diff in CI (heavier, but catches layout breaks)

For agency review cycles, preview URLs reduce “please check on mobile” back-and-forth. Compare platform limits in GitHub Pages vs Netlify vs Cloudflare Pages.

Monitoring and rollback

GitHub Pages keeps deployment history in the Actions tab. Rollback = revert commit and redeploy. There is no one-click “previous artifact” button like some PaaS hosts offer.

We recommend:

  • Tag releases for major site overhauls (v2026.08-redesign)
  • Keep _redirects and meta-refresh stubs in sync when URL structures change
  • Run Core Web Vitals checks after deploy, not before — CDN cache may delay visible changes

Cost vs WordPress maintenance

Static CI/CD front-loads complexity but reduces ongoing server patching. For TCO comparison with WordPress hosting and plugin updates, see WordPress vs static TCO. Most Nepali brochure sites under 50 pages land cheaper on static + Actions within 12 months if you already pay for managed WordPress maintenance.