McGarrah Technical Blog

Building This Blog: Jekyll on GitHub Pages from Zero to 130+ Posts

· 10 min read

I’ve been writing about technology since 2004 — first on WordPress, then Blogger, and now Jekyll on GitHub Pages. The migration to Jekyll happened in 2023 when I wanted full control over the site without paying for hosting or fighting with WordPress plugin conflicts.

Two years and 130+ posts later, the blog has grown from a basic theme into a heavily customized platform with Mermaid diagrams, GDPR compliance, Pandoc PDF exports, and an automated SEO pipeline. This post covers how I set it all up — the order of operations, the decisions I made, and what I’d do differently.

For the day-to-day writing reference (markdown syntax, code blocks, diagrams, embeds), see the companion post: How the Sausage Is Made: Every Feature Powering This Jekyll Blog.

Why Jekyll?

After years on WordPress, I wanted:

The tradeoff is that Jekyll requires comfort with the command line, Git, and Ruby. If you’re reading this blog, you probably have that.

Create the GitHub Pages Repository

GitHub Pages serves a static site from any repository named <username>.github.io. Create the repo, clone it, and you have a working site:

git clone git@github.com:<username>/<username>.github.io.git
cd <username>.github.io

GitHub automatically builds and serves anything pushed to the default branch at https://<username>.github.io.

Pick a Theme

Rather than starting from a blank index.html, pick a Jekyll theme. This blog uses the Contrast theme by Niklas Buschmann — minimal, responsive, and easy to extend. I’ve since modified it heavily, but the bones are still Contrast.

The fastest way to start:

  1. Fork or copy the theme repository into your <username>.github.io repo
  2. Run bundle install to pull dependencies
  3. Run bundle exec jekyll serve to preview locally at http://localhost:4000
gem install bundler jekyll
bundle install
bundle exec jekyll serve

See Running GitHub Pages Jekyll Locally for the full local development setup including Ruby version management.

Initial Configuration

The _config.yml file controls everything. The essential settings to change immediately:

title: "Your Site Title"
description: "A brief description for SEO and social sharing"
url: "https://www.yourdomain.com"
author:
  name: "Your Name"
  email: "you@example.com"

plugins:
  - jekyll-feed          # RSS feed
  - jekyll-sitemap       # XML sitemap for search engines
  - jekyll-paginate      # Post pagination
  - jekyll-seo-tag       # SEO meta tags and structured data
  - jekyll-redirect-from # Redirect support for moved pages

Changes to _config.yml require restarting jekyll serve — it’s not hot-reloaded like post content.

File Structure

After theme setup, the key files:

├── _config.yml          # Site configuration
├── _layouts/            # Page templates (default, post, page)
├── _includes/           # Reusable HTML components
├── _sass/               # SCSS stylesheets
├── _posts/              # Published articles (YYYY-MM-DD-title.md)
├── _drafts/             # Work-in-progress (no date prefix)
├── _plugins/            # Custom Ruby plugins
├── assets/              # CSS, JS, images, fonts
├── Gemfile              # Ruby dependencies
├── CNAME                # Custom domain mapping
└── index.html           # Homepage

Custom Domain

To serve the site from a custom domain instead of <username>.github.io:

  1. Create a CNAME file in the repository root with your apex domain (no www):

     mcgarrah.org
    
  2. Configure DNS with your registrar:
    • A records for the apex domain (@) pointing to GitHub Pages IPs:
      • 185.199.108.153
      • 185.199.109.153
      • 185.199.110.153
      • 185.199.111.153
    • CNAME record for www pointing to <username>.github.io
  3. In the GitHub repository settings under Pages, set the custom domain to the apex domain (e.g., mcgarrah.org) and enable Enforce HTTPS

GitHub handles the Let’s Encrypt certificate automatically. With this configuration, GitHub Pages serves the site from the apex domain and automatically 301-redirects www to the apex.

The GitHub Pages custom domain docs cover the full DNS setup.

I’m currently migrating my domains from Squarespace to Porkbun for better API access and lower costs — relevant if you’re choosing a registrar for a new blog.

The www vs Apex Domain Trap

I ran into a subtle but damaging SEO issue that’s worth documenting. My original CNAME file was set to www.mcgarrah.org, but my _config.yml had:

url: "https://mcgarrah.org"

This created a mismatch across the entire site:

Component Domain Used
CNAME (GitHub Pages primary) www.mcgarrah.org
_config.yml url mcgarrah.org
jekyll-seo-tag canonical URLs mcgarrah.org
jekyll-sitemap URLs mcgarrah.org
robots.txt sitemap reference mcgarrah.org

GitHub Pages treated www.mcgarrah.org as the primary domain and 301-redirected the apex mcgarrah.orgwww.mcgarrah.org. But every canonical tag and sitemap entry pointed to mcgarrah.org (no www). Google Search Console saw the site served from one domain with canonical tags pointing to another — splitting crawl data and confusing search rankings.

The fix was straightforward: change the CNAME file from www.mcgarrah.org to mcgarrah.org and update the GitHub Pages custom domain setting to match. My existing DNS records already supported this:

A    @    185.199.108.153  (GitHub Pages)
A    @    185.199.109.153  (GitHub Pages)
A    @    185.199.110.153  (GitHub Pages)
A    @    185.199.111.153  (GitHub Pages)
CNAME www mcgarrah.github.io

After the change, the redirect flipped correctly:

The lesson: your CNAME file, _config.yml url, and GitHub Pages custom domain setting must all agree on the same domain. If you use the apex domain in _config.yml, use it everywhere. The www CNAME DNS record still exists so GitHub Pages can handle the redirect — it just shouldn’t be the primary domain in the CNAME file.

GitHub Actions CI/CD

GitHub Pages can build Jekyll sites automatically, but a custom GitHub Actions workflow gives you more control — pinned Ruby versions, additional plugins not in the GitHub Pages whitelist, and build validation:

# .github/workflows/jekyll.yml
name: Build and Deploy Jekyll
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.2'
          bundler-cache: true
      - run: bundle exec jekyll build
      - uses: actions/upload-pages-artifact@v3
  deploy:
    needs: build
    permissions:
      pages: write
      id-token: write
    environment:
      name: github-pages
    runs-on: ubuntu-latest
    steps:
      - uses: actions/deploy-pages@v4

This is essential once you add custom plugins (like the tag/category generator or Pandoc exports) that aren’t supported by GitHub’s default Jekyll build.

I also run an SEO health check workflow on every push that validates canonical URLs, sitemap XML, meta tags, structured data, and broken links.

SEO Setup

SEO was an afterthought when I started. It shouldn’t have been. Here’s what I added over time:

jekyll-seo-tag Plugin

This plugin generates Open Graph tags, Twitter cards, JSON-LD structured data, and canonical URLs automatically from your _config.yml and post front matter:

# _config.yml
seo:
  type: Person
  name: "Michael McGarrah"
  links:
    - "https://www.linkedin.com/in/michaelmcgarrah/"
    - "https://github.com/mcgarrah"

Per-Post SEO

Every post should include a description and seo block in front matter:

---
title: "Post Title"
description: "A longer description for search engines, separate from excerpt."
seo:
  type: BlogPosting
  date_published: 2026-04-19
  date_modified: 2026-04-19
---

I learned this the hard way — see Jekyll SEO Sitemap and Canonical URL Fixes and Improving E-E-A-T for Jekyll and AdSense for the full story.

Sitemap and Robots.txt

The jekyll-sitemap plugin generates sitemap.xml automatically. I had to fix sitemap bloat from tag and category pages inflating the sitemap with low-value URLs.

Comments with Giscus

I chose Giscus for comments — it uses GitHub Discussions as the backend, so there’s no separate service to maintain. Readers need a GitHub account to comment, which is fine for a technical blog.

Configuration in _config.yml:

giscus:
  repo: mcgarrah/mcgarrah.github.io
  repo_id: R_kgDOKBKIdw
  category: Announcements
  category_id: DIC_kwDOKBKId84Cq3DK
  mapping: pathname
  theme: preferred_color_scheme
  loading: lazy

The _includes/ template renders the Giscus widget at the bottom of each post. Comments are stored as GitHub Discussions and are fully searchable.

Required for Google AdSense and Analytics in the EU. I built a custom GDPR implementation that:

This was more complex than expected — the AdSense verification fix documents how my initial implementation was too aggressive and blocked Google’s own verification crawler.

Resume as a Separate Repository

My online resume lives in a separate resume repository. GitHub Pages serves project repositories at <username>.github.io/<repo-name>/, so with the custom domain it appears at www.mcgarrah.org/resume/.

This keeps the resume’s Jekyll theme, dependencies, and build independent from the blog. The Pandoc exports integration generates downloadable PDF and DOCX versions at build time.

I explored merging the two sites but decided the separation is worth the minor complexity.

Favicon

Drop a favicon.ico in the repository root. For broader browser support, generate multiple sizes using RealFaviconGenerator and add the <link> tags to your _includes/head.html or _layouts/default.html.

What I’d Do Differently

If I started over today:

  1. Set up SEO from day one. Adding description, structured data, and canonical URLs retroactively across 130+ posts was painful.

  2. Use jekyll-redirect-from immediately. I renamed several posts early on and broke links. The redirect plugin would have prevented that.

  3. Pick a theme with dark mode support. Contrast supports it but I haven’t exposed the toggle yet. Starting with it enabled would have been easier.

  4. Automate image optimization. I still manually compress images. A build-time optimization step would save time and improve performance.

  5. Write the feature reference post first. Having a single place to check syntax for every feature would have saved hours of Googling the same Mermaid or KaTeX syntax repeatedly.

References

Categories: web-development, jekyll

About the Author: Michael McGarrah is a Cloud Architect with 25+ years in enterprise infrastructure, machine learning, and system administration. He holds an M.S. in Computer Science (AI/ML) from Georgia Tech and a B.S. in Computer Science from NC State University, and is currently pursuing an Executive MBA at UNC Wilmington. LinkedIn · GitHub · ORCID · Google Scholar · Resume