Home Blog How to Automate Jekyll Builds with GitHub Actions
Tutorial

How to Automate Jekyll Builds with GitHub Actions

Set up GitHub Actions to automatically build and deploy your Jekyll site — with caching, HTML validation, link checking, and multi-environment deployments.

How to Automate Jekyll Builds with GitHub Actions

GitHub Actions can build, test, and deploy your Jekyll site automatically on every push. This guide covers everything from the basic setup to advanced workflows with caching, link checking, and staging environments.


Why Use GitHub Actions for Jekyll?

GitHub Pages has a built-in Jekyll build system, but it only supports a limited set of plugins. GitHub Actions removes this restriction — you can use any Jekyll plugin, any Ruby version, and any build configuration.

Additional benefits:

  • Build once, deploy anywhere — deploy to GitHub Pages, Netlify, Cloudflare Pages, or AWS S3
  • Run tests — validate HTML, check links, run linters before deploying
  • Cache dependencies — fast builds by reusing installed gems between runs
  • Preview branches — deploy PRs to a staging URL before merging

Basic Jekyll GitHub Actions Workflow

Create .github/workflows/deploy.yml in your repository:


name: Build and Deploy Jekyll

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: "pages"
  cancel-in-progress: false

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.3'
          bundler-cache: true  # Runs bundle install and caches gems

      - name: Setup Pages
        id: pages
        uses: actions/configure-pages@v5

      - name: Build Jekyll site
        run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}"
        env:
          JEKYLL_ENV: production

      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3

  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main'
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

What This Does

  • Triggers on every push to main and on pull requests
  • Sets up Ruby with the version specified in your .ruby-version file
  • Caches gems so subsequent builds are faster (typically 2–3× speedup)
  • Builds Jekyll with JEKYLL_ENV: production to enable production-only features
  • Deploys to GitHub Pages (only on pushes to main, not PRs)

Adding Gem Caching (Faster Builds)

The bundler-cache: true option in ruby/setup-ruby handles caching automatically. But you can also cache explicitly for more control:


- name: Cache gems
  uses: actions/cache@v4
  with:
    path: vendor/bundle
    key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
    restore-keys: |
      ${{ runner.os }}-gems-

- name: Install dependencies
  run: |
    bundle config path vendor/bundle
    bundle install --jobs 4 --retry 3

Add vendor/ to your .gitignore:

vendor/

Adding HTML Validation

Validate your built HTML before deploying to catch issues like missing alt text and broken markup:

- name: Validate HTML
  run: |
    gem install html-proofer
    htmlproofer ./_site \
      --disable-external \
      --checks Links,Images,Scripts \
      --allow-missing-href true \
      --ignore-urls "/localhost/"

Check for broken internal and external links:

- name: Check links
  run: |
    bundle exec htmlproofer ./_site \
      --checks Links \
      --external-only \
      --ignore-urls "https://twitter.com,https://linkedin.com"

Note: External link checking adds significant build time. Consider running it on a schedule rather than every push:

on:
  push:
    branches: [main]
  schedule:
    - cron: '0 6 * * 1'  # Every Monday at 6am

Multi-Environment Deployment

Staging on Pull Requests

Deploy pull requests to a preview URL so you can review changes before merging:


name: Deploy Preview

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  deploy-preview:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.3'
          bundler-cache: true

      - name: Build Jekyll
        run: bundle exec jekyll build
        env:
          JEKYLL_ENV: staging

      - name: Deploy to Netlify Preview
        uses: nwtgck/actions-netlify@v3
        with:
          publish-dir: './_site'
          github-token: ${{ secrets.GITHUB_TOKEN }}
          deploy-message: "PR Preview: ${{ github.event.pull_request.title }}"
        env:
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
          NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}

Production Deployment with Environment Protection

Add environment protection rules so deploys to production require manual approval:

  1. Go to repository Settings → Environments → New environment
  2. Name it production
  3. Add Required reviewers — only approved reviewers can trigger production deploys

Then reference the environment in your workflow:

deploy:
  environment:
    name: production
    url: https://yourdomain.com

Deploying to Netlify Instead of GitHub Pages

If you prefer Netlify for its CDN and preview deploys:


- name: Deploy to Netlify
  uses: nwtgck/actions-netlify@v3
  with:
    publish-dir: './_site'
    production-branch: main
    github-token: ${{ secrets.GITHUB_TOKEN }}
    deploy-message: "Deployed from GitHub Actions"
  env:
    NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
    NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}

Get your NETLIFY_AUTH_TOKEN from Netlify’s personal settings and add both secrets to your GitHub repository Settings → Secrets and variables → Actions.


Scheduled Builds

Useful for sites with content that updates from external sources (RSS feeds, APIs):

on:
  schedule:
    - cron: '0 6 * * *'  # Daily at 6am UTC
  push:
    branches: [main]

Full Production Workflow

Here’s a complete, production-ready workflow combining everything above:


name: Jekyll CI/CD

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 6 * * 1'  # Weekly link check

permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.3'
          bundler-cache: true

      - name: Build Jekyll
        run: bundle exec jekyll build
        env:
          JEKYLL_ENV: production

      - name: Validate HTML
        run: |
          bundle exec htmlproofer ./_site \
            --disable-external \
            --checks Links,Images

      - name: Upload artifact
        if: github.ref == 'refs/heads/main'
        uses: actions/upload-pages-artifact@v3

  deploy:
    needs: build-and-test
    if: github.ref == 'refs/heads/main'
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4


Looking for Jekyll themes that are already configured for GitHub Actions deployment? Browse themes on JekyllHub →

Share LinkedIn