Published: 2025-08-07
This is a Jekyll site hosted on GitHub Pages. In this post, I explain why I chose this as my personal documentation system, provide a step-by-step guide to creating a similar site, and share some reflections on using AI coding tools for this project.
Why GitHub Pages? First, it is a free hosting feature on a well-supported platform - suitable for my blog. In addition, site content sits in a Git repository and is published automatically when pushed to GitHub, which fits my workflows and gives me reliable, versioned backups. Finally, with the Jekyll static site generator I get full control over all site files, a clean separation of content and design, and no vendor lock-in - making it easy to update the UI or switch providers at any time.
How GitHub Pages? Follow this simple shell script Gist to build a Jekyll site hosted on GitHub Pages.
What About AI Coding Tools? While they provide great speed improvements, effective guidance is critical. They are a complement, not a replacement, to good architecture, design, and planning.
GitHub Pages is a GitHub feature that hosts a static website from a repository. You create a GitHub Pages site by configuring a GitHub repository as its publishing source and specifying a theme that controls the site’s UX layout and styling. When updates are pushed to that repository, a GitHub Actions workflow is triggered that runs a build process on the source code in the repo, and then deploys the resulting static files to GitHub Pages’ hosting environment.
Let’s unpack this to explain my decision after having explored several options:
GitHub Pages is a GitHub feature. GitHub has a huge user base and hosts a ton of open-source projects. This means there are a lot of popular GitHub Pages that serve as examples, and there is a large community for support.
GitHub Pages hosts static websites. GitHub as a hosting service is very attractive because it is free for static websites and Microsoft has an interest in ensuring GitHub remains a solid platform. A static website is really all I need for my personal documents.
Repositories are publishing sources. I spend a good amount of time creating material that is version-controlled with Git and shared on GitHub. Creating and maintaining my own documentation using the same tools and workflow I already use is a huge boon. No context switching. No need to navigate to a separate tool or UI to work on my personal docs. Efficiency. I can perform all the publishing tasks I want within my current workflows:
Repositories are stored locally and on GitHub. Have you ever lost important data stored in an online service because of a snafu? Yeah…not an experience one boasts about. Storing my content on GitHub and on local repositories across multiple machines, as I usually have, is a good safety net supporting the 3-2-1 backup rule, regardless of a service provider’s disaster recovery plan.
A build process runs automatically on updates to the repo. A build process for a static website is of great value to me. Having pre-built static pages eliminates the need for a back-end database to hold any data or server-side processing to generate pages on the fly, which makes the site simpler to set up, easier to maintain, and faster to serve up.
GitHub Pages can be configured to automatically run a build and publish a site when changes are pushed to a repo. This automated workflow makes the publishing aspect efficient, consistent, and reliable. I can also trigger local or remote builds at any time, enabling me to preview updates or troubleshoot any build issues on demand.
This biggest benefit of this model, however, is that it gives me full access to every file created or generated, which means I am not locked into any provider, I am not limited by theme restrictions or drag-and-drop GUIs, and I can move all my content at will.
The build process is configurable. GitHub Pages’ build process is very flexible: it supports dozens of static site generators (SSGs) like Jekyll, VuePress, Next.js, and others. This allows me to choose a framework based on my preferred language, available plugins, and personal preference. And while the tinkerer in me gets excited about the possibility of trying out different frameworks in the future as requirements change, the pragmatist in me recognizes that the default build process is good enough for my current needs.
The default build process uses Jekyll. Jekyll is an SSG that takes content files written in Markdown and combines it with page template files written in HTML/CSS/JavaScript/Liquid to produce a complete static website. These are all languages I am well-versed with. The GitHub Pages documentation and the Jekyll documentation are well organized and easy to follow - I was able to quickly prototype and evaluate a Jekyll site. This was a big plus for me - the lack of good documentation and/or test environments often hinder a product from selling itself.
Jekyll site content is written Markdown and other familiar languages. On a Jekyll site, content files are written in Markdown - a simple markup language used to format plain text - or HTML.
The most recent iteration of my personal docs were written in HTML - I had transitioned away from using Word because I was tinkering with UI frameworks and found it easier to switch within my IDE and use HTML to write my documents. I found the experience of writing content without any predefined formatting or tied to a specific program liberating.
Moving to Markdown for my next iteration feels natural and straight-forward. It is more lightweight and readable than HTML, and it plays nice with HTML - I can inject HTML/CSS/JavaScript code into a Markdown document without breaking its meaning or the Jekyll build tools, and it just works. Mostly. Having the flexibility to do this in a streamlined fashion is invaluable to me - I can use or experiment with different client-side frameworks and technologies (e.g., jQuery, TailwindCSS, HTMX, Vue, Angular, React) on different pages without having to refactor the entire site.
Content and Presentation are deliberately separate in a Jekyll project. One of the design principles of Jekyll as a tool is to keep content and presentation separate: Content is stored in some folders (_posts/, images/, etc.), and presentation is handled through template files in other folders (_layouts/, _includes/, assets/, etc.). At build-time, Jekyll combines these to generate the complete site in a single folder (_site/) which is then published.
There are two big advantages to this design:
GitHub Pages allows multiple sites per account. As an added bonus, GitHub Pages allows a single User Site to be created for an account (at https://{username}.github.io) and separate Project Sites for each repository in that account (at https://{username}.github.io/{repo_name}), which means I can apply everything I learn from building out my personal docs to document any project repo, like these popular GitHub Pages.
This section walks you through creating a simple Jekyll site on GitHub Pages, with the ability to run local builds and customize the site’s template files. It is part runbook, the result of many prototypes and experiments; and part guide, with notes that explain some design decisions. As such, there is plenty of room to do or explain things differently - and better.
Start a new shell session to avoid conflicts with the existing environment.
If you are testing the commands in this blog entry by pasting them into a terminal window, we recommend you start a new shell session first:
$ zsh # Start a new shell session.
The guide uses the readonly command, which prevents environment variables, once declared, from being modified or unset within a session. If you make a mistake, you must exit the current session with $ exit, start a new one with $ zsh, and re-run the corrected commands.
Alternatively, if you want to change and test the script with different variable settings, you can simply omit the readonly keyword from a command statement and avoid having to restart subshells.
Conversely, you can also clone this simple shell script Gist that contains all commands herein in a single script file, make any modifications you want, and run the script as often as you want. When invoked with zsh (i.e., $ zsh script.zsh) or directly from a terminal window (i.e., $ chmod +x script.zsh ; ./script.zsh), the script (which has the shebang #!/bin/zsh as first line) will be executed in a subshell (a child process), and any changes made by the script to environment variables will be contained within the subshell’s environment. When the script ends, the subshell will exit and return you back to the parent shell.
Install Jekyll on your local environment to run local Jekyll builds.
#!/bin/zsh
# Verify environment.
if [ -z "${ZSH_NAME}" ]; then # Fail fast if not running in zsh.
echo "This is a pseudo-zsh script. Adjust for other shells."
exit 1
fi
# Install Homebrew to install Jekyll and Bundler.
if ( ! brew -v ); then # Install Homebrew.
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
else
brew update # Update Homebrew.
brew upgrade # Install the newest versions of all packages.
fi
# Install Jekyll and Bundler to run local Jekyll builds.
if ( jekyll -v ); then # Update Jekyll and Bundler.
gem update jekyll # Update Jekyll gem to the latest version.
gem update bundler # Update Bundler gem to the latest version.
else # Install Jekyll and Bundler.
brew install chruby # Install Ruby version manager.
brew install ruby-install # Install Ruby installer.
ruby-install ruby 3.4.1 # Install Ruby for Jekyll.
# Update the zsh configuration file to use chruby by default.
echo "source $(brew --prefix)/opt/chruby/share/chruby/chruby.sh" >> ~/.zshrc
echo "source $(brew --prefix)/opt/chruby/share/chruby/auto.sh" >> ~/.zshrc
echo "chruby ruby-3.4.1" >> ~/.zshrc
source ~/.zshrc # Apply updates (alternate: `exec zsh`).
gem install jekyll # Install the latest Jekyll gem.
gem install bundler # Install the latest Bundler gem.
fi
# Check versions for compatibility.
ruby -v # Check version: gem "ruby", "3.4.1"
jekyll -v # Check version: gem "jekyll", ">=4.4.1"
bundler -v # Check version: gem "bundler", ">=2.6.9"
# Run Homebrew diagnostics to check for issues.
brew doctor # Run Homebrew diagnostics, as an FYI.
This pseudo-script, like others I post, is designed to be followed by hand. It assumes familiarity with shell scripting, and with the Z Shell syntax, in particular. For example, if ( jekyll -v ) ; then... is equivalent to running the command $ jekyll -v with its exit status indicating whether Jekyll is installed or not. Guides I write automated scripts for are not perfect but more robust (i.e., configurable, modular, idempotent, structured logging, etc.) and kept in the bin/ folder of my shell-scripts repository.
About what this script installs:
See the Jekyll Installation docs and the Jekyll Step by Step Tutorial for more details.
Create a GitHub Pages repo and clone it locally to build the site on your machine.
#!/bin/zsh
# Script settings.
readonly _USERNAME=$(gh api user -q ".login")
readonly _REPO_NAME="${_USERNAME}.github.io"
readonly _REPO_DESCRIPTION="Personal blog source."
readonly _DEV_ROOT="${HOME}/_dev"
readonly _WORKSPACE_DIR="${_DEV_ROOT}/repos/github.com/${_USERNAME}"
readonly _TMP_DIR="_tmp"
readonly _JEKYLL_GITIGNORE="https://raw.githubusercontent.com/github/gitignore/HEAD/Jekyll.gitignore"
readonly _MACOS_GITIGNORE="https://raw.githubusercontent.com/github/gitignore/HEAD/Global/macOS.gitignore"
if [[ "${_REPO_NAME}" == "${_USERNAME}.github.io" ]]; then
readonly _REPO_HOMEPAGE="https://${_REPO_NAME}/" # User site.
else
readonly _REPO_HOMEPAGE="https://${_USERNAME}.github.io/${_REPO_NAME}/" # Project site.
fi
# Move to the workspace directory to create a repo.
mkdir -p "${_WORKSPACE_DIR}" # Create intermediate directories, if needed.
cd "${_WORKSPACE_DIR}" # Move to the directory.
# Create a repo for the GitHub Pages site and clone it locally.
gh repo create "${_REPO_NAME}" --public --gitignore Jekyll --clone --description "${_REPO_DESCRIPTION}" --homepage "${_REPO_HOMEPAGE}"
# Move to the project directory.
cd "${_REPO_NAME}"
# Update the `.gitignore` file with custom + Jekyll + macOS entries.
( echo "### Source: Custom\n"; echo "${_TMP_DIR}/"; \
echo "\n### Source: ${_JEKYLL_GITIGNORE}\n"; curl -fsSL ${_JEKYLL_GITIGNORE}; \
echo "\n### Source: ${_MACOS_GITIGNORE}\n"; curl -fsSL ${_MACOS_GITIGNORE} \
) > GITIGNORE
mv GITIGNORE .gitignore
# Confirm the remote GitHub fetch/push URLs.
git remote -v
We create the repos first because the name is needed to configure Jekyll for GitHub Pages later on.
~/_dev is the root directory for all our development projects.
The $ gh api user -q ".login" command retrieves the current GitHub username. Using this command helps us set up the site on different GitHub accounts. It also genericizes the documentation.
The $ gh repo create ... command creates a remote repository and clones it to a local directory of the same name.
The repo {username}.github.io is required for the user site https://{username}.github.io.
To keep things simple, GitHub Pages will be configured to publish from the main branch and from the / (root) folder, so there is no need to create, checkout, or clear a new branch.
The --public flag creates a public repository. We need this because GitHub Pages is only available for public repositories on free GitHub accounts and ours is a free account.
The --gitignore Jekyll option adds a .gitignore template file to a Jekyll project. We rewrite it next, but we include it to describe how to access other gitignore templates:
For gitignore keywords run $ gh repo gitignore list.
To view other templates files visit https://github.com/github/gitignore.
Downloading the Jekyll gitignore file programmatically is equivalent to the command $ curl https://raw.githubusercontent.com/github/gitignore/HEAD/Jekyll.gitignore -o .gitignore.
The --clone flag clones the remote repo to a local directory with the same name.
The --description option adds a description in the “About” section of the repo’s GitHub page.
The --homepage option adds a URL in the “About” section of the repo’s GitHub page.
The final set of echo, curl, and mv commands create a single .gitignore file that includes custom, Jekyll, and macOS entries.
The macOS entries are for the macOS environment we are on. Change it for a different OS.
The custom entry is a temporary directory, _tmp/, that will be used to store a reference library of Jekyll themes.
Having it in the .gitignore file ensures it won’t be saved with the repo.
Having it prefixed with _ ensures Jekyll does not include it in the build because Jekyll ignores folders that start with _.
I decided to protect all content I create on this site under an exclusive copyright, while allowing free use of the site’s source code. The repository’s README.md file that describes this will be created in the next steps. To publish the site under a global license, add a LICENSE file when creating the repo:
#!/bin/zsh
# Script settings.
readonly _CONTACT_INFO="Mauricio Lomelin <maulomelin@gmail.com>"
readonly _LICENSE="MIT"
# Create a GitHub repo and clone it locally.
gh repo create "${_REPO_NAME}" --public --license {$_LICENSE} --gitignore Jekyll --clone --description "${_REPO_DESCRIPTION}" --homepage "${_REPO_HOMEPAGE}"
# Update contact info in the LICENSE file.
sed -i "" "s/${_USERNAME}/${_CONTACT_INFO}/" LICENSE
The --license MIT option adds a LICENSE file to the repo with the MIT License. The installed license file pre-populates the copyright line with the current year and with the current GitHub username.
For license keywords run $ gh repo license list.
To view other license templates visit https://choosealicense.com or https://github.com/licenses/license-templates/. These templates will generally have a placeholder for various fields (e.g., [year], {{year}}, [fullname], {{organization}}, etc.), which must be replaced with your own information.
The $ sed ... command changes the GitHub username in the LICENSE file with more specific contact info. For other licenses, check what fields you may want to replace.
As a side note, while many of these steps could be done manually in the GitHub.com UI, using the GitHub CLI and shell commands is faster and easier. For comparison, see how the script commands above compare with their manual, GUI-based counterparts:
.gitignore file to include a custom entry and entries from the Jekyll and macOS .gitignore templates.This example shows why I often prefer to write shell scripts over documenting GUI-based instructions. Scripts are less ambiguous and easier to reuse than following manual steps. Fortunately, many IDEs now integrate with CLIs through built-in terminals and extensions, making scripting easier to manage and execute. However, scripting is a double-edged sword: they’re a powerful tool if you’re proficient, but if you’re not you could cause serious problems - always double-check and test your scripts!
Create a README.md file in the project root to provide copyright and licensing terms.
#!/bin/zsh
# Script settings.
readonly _CONTACT_INFO="Mauricio Lomelin <maulomelin@gmail.com>"
# Create a README.md file with copyright and licensing terms.
cat <<CONTENT > README.md
# About
This is my personal blog - a collection of notes, tutorials, and code snippets.
It's built with Jekyll and hosted on GitHub Pages.
## Copyright and Licensing
The content in the \`_posts/\` and \`images/\` directories of this project is the copyright of ${_CONTACT_INFO}. Do not use without permission.
All other files and content are licensed under the MIT License.
CONTENT
Create a Gemfile file in the project root so that local builds match GitHub Pages builds.
#!/bin/zsh
# Create a `Gemfile` file to match local builds with GitHub Pages builds.
cat <<CONTENT > Gemfile
source "https://rubygems.org"
gem "github-pages", group: :jekyll_plugins
CONTENT
The resulting file should look like this:
# filename: Gemfile
source "https://rubygems.org"
gem "github-pages", group: :jekyll_plugins
The Gemfile file contains a description of all gem dependencies for a Ruby program.
Gems are installed from the gem hosting service specified in the sources setting. Bundler installs gems from https://rubygems.org by default, but I prefer being explicit in my configuration files.
The CLI command $ bundle init could have been used to create a default Gemfile file, but it would still need to be edited to add the GitHub Pages gem and remove any incompatible gems. Copy/pasting the two lines above is easier.
The CLI command $ bundle install is used to install all project gems listed in its Gemfile file. We’ll cover this in more detail when it’s time to run this command.
For more information, run $ jekyll new . inside a temp directory to create a default Jekyll site and view its default Gemfile file.
The github-pages gem sets up and maintains the local Jekyll environment in sync with GitHub Pages’ Jekyll environment. It includes the jekyll gem and all other GitHub Pages Dependency versions, which allows us to build and test the site locally.
github-pages is a wrapper gem that includes all gems needed to make your local Jekyll build environment match the GitHub Pages build environment. See https://pages.github.com/versions.json for all gem dependencies.
Run $ bundle update github-pages periodically to update the local github-pages gem to stay in sync with the github-pages gem on the GitHub Pages server.
For more information, see the GitHub Pages Ruby Gem repo.
Create a minimal _config.yml file in the project root to configure Jekyll for GitHub Pages.
#!/bin/zsh
# Create a minimal `_config.yml` file to configure Jekyll for GitHub Pages.
cat <<CONTENT > _config.yml
title: Personal Documents
description: A collection of notes, tutorials, and code snippets.
repository: ${_USERNAME}/${_REPO_NAME}
CONTENT
The resulting file should look like this:
# filename: _config.yml
title: Personal Documents
description: A collection of notes, tutorials, and code snippets.
repository: maulomelin/maulomelin.github.io
The _config.yml file is Jekyll’s main configuration file. It defines site-wide settings, global variables, and controls how the static website is built.
This is the minimal _config.yml file for a GitHub Pages site. The basic Jekyll setup instructions do not require this file (a basic Jekyll setup uses system defaults). The github-pages gem, however, does require a _config.yml file with at minimum the settings shown above to build the site.
For more information, run $ jekyll new . inside a temp directory to create a default Jekyll site and view its default _config.yml file.
Add the Jekyll theme “Primer” to _config.yml to set up a minimal UX for the site.
#!/bin/zsh
# Add the Jekyll theme "Primer" to `_config.yml` to give a minimal UX to the site.
cat <<CONTENT >> _config.yml
theme: jekyll-theme-primer
CONTENT
The resulting file should look like this:
# filename: _config.yml
# ...
theme: jekyll-theme-primer
A Jekyll theme is a collection of template files that define the look & feel (UX) of the website. Jekyll uses these files at build-time to generate a complete, styled, static website.
A project’s theme can be implemented in different ways:
Gem-Based Themes. A gem-based theme is a Jekyll theme packaged as a Ruby gem. To use one, you simply install the gem and specify that theme in the project. When Jekyll builds the site it will pull the theme’s template files from the gem. These are good to use when you want to use a theme as-is.
Local Themes. A local theme is a Jekyll theme where all of the template files are in a project’s directory. To use this, you simply create template files for your site and place them inside your project directory. When Jekyll builds the site it will use those template files to generate all site pages. These are good to use when you want to build your own custom theme and want complete control.
Theme Overrides. A theme override lets you make targeted changes to a gem-based theme. To override a file, you first specify the theme in the project. Then, you place a new version of the template file to override in the project directory using the same name and relative path as the file in the theme. When Jekyll builds the site, it will use the local version of any template files it finds instead of the ones from the theme. This is a good approach to follow if you want to customize parts of an existing theme.
For a GitHub Pages site, a gem-based theme can be specified in different ways:
Use a theme that is pre-bundled with github-pages. The github-pages gem includes a bunch of dependencies, including many gem-based themes, that are automatically installed with it. To use one of these themes, simply specify it in the _config.yml file using the theme setting.
# filename: _config.yml
# ...
theme: jekyll-theme-slate # Bundled with "gem github-pages".
#theme: minima # Default for "$ jekyll new".
#theme: jekyll-theme-primer # Default for "gem github-pages".
# ...
To see a list of the gem-based themes bundled with github-pages, run $ bundle install to install it, then run $ bundle list to view the installed gems. Look for entries that follow the jekyll-theme-{name} naming convention. One entry, minima, does not follow this naming convention. You need to know what to look for to check if it’s installed.
Alternatively, the full list of github-pages gem dependencies on https://pages.github.com/versions.json will also show any bundled themes.
Install and specify a new gem-based theme. Install a theme by adding it to the project’s Gemfile file, and specify it in the _config.yml file using the theme setting so Jekyll uses it when building the site.
# filename: Gemfile
# ...
gem "foobar"
# ...
# filename: _config.yml
# ...
theme: foobar
# ...
Use a theme hosted on GitHub. Reference a theme that is hosted on GitHub by specifying it in the _config.yml file with the remote_theme setting. This option tells Jekyll to pull the theme files from the remote repository when building the site, without having to download or install the template files locally.
# filename: _config.yml
# ...
remote_theme: account-name/repo-name@v3.2.1
# ...
Format: remote_theme: {account_name}/{repository_name}[@{version}].
This option works because the github-pages gem includes the jekyll-remote-theme plugin, which handles the fetching of all remote template files at build-time.
I wanted the first version of this site to have a simple design with very little styling that I could tweak and augment over time. The idea was to start off with a gem-based theme and write theme overrides to add new features; building a bespoke local theme was not appealing given all the available themes out there.
I discovered a rich ecosystem of Jekyll themes, and since trying out different themes is easy (see above), I was able to quickly try out different ones. I eventually picked the Jekyll theme “Primer” theme to start - an official, minimalist theme with a bare-bones UX design, developed by GitHub for Jekyll sites.
Jekyll theme sites:
github-pages: https://github.com/pages-themes#jekyll-theme: https://jekyllrb.com/docs/themes/BUG: Annoying bug when setting up a public repo with a LICENSE file using the Primer theme. The Primer theme uses the Liquid tag {% github_edit_link ... %} in the _layouts/default.html template. That tag is triggered on public repos with a LICENSE file using the Primer theme, and causes the local build to break. STEPS TO REPRO: Create a repo with [visibility = Public], [theme = Primer], and a LICENSE file, and build the site locally. OBSERVED RESULTS: A Liquid Exception: '@Jekyll::GitHubMetadata::EditLinkTag#parts' ... error. EXPECTED RESULTS: A clean build. ROOT CAUSE: We believe the root cause is that it attempts to generate an “edit” link that points to the gh-pages branch by default, and since we decided to use the main branch for publishing, and no gh-pages branch was configured, executing this tag will break the build in our setup. STATUS: This is a known issue of the github-metadata plugin. WORKAROUNDS: 1) Don’t use a global LICENSE file; 2) override the _layouts/default.html template file to delete that flag; 3) override the _layouts/default.html template file to build your own link; 4) investigate whether deploying from the gh-pages branch is indeed the root cause and do that.
Export the Jekyll theme “Primer” to a local folder, to use as reference for files to override.
#!/bin/zsh
# Script settings.
readonly _THEME_REPO="https://github.com/pages-themes/primer/"
readonly _THEME_REPO_DTSLUG="${${${_THEME_REPO:t3}// /}//\//--}--$(date +%Y%m%dT%H%M%S)"
# Export the Jekyll theme "Primer" to a local folder, to use as reference for template files.
git clone --depth=1 "${_THEME_REPO}" "${_TMP_DIR}/${_THEME_REPO_DTSLUG}"
rm -rf ./"${_TMP_DIR}/${_THEME_REPO_DTSLUG}"/.git
Exporting a theme’s files locally gives us a complete reference to all template files, making it easy to identify which specific files to create overrides for and customize the site. Follow this same approach to download other themes and add new features using templates and design elements from multiple sources.
Since we only want the theme’s template files as reference, we don’t need any Git metadata. To export a repo, we first create a shallow clone ($ git clone --depth=1 ...) to fetch as little metadata as possible, then we remove what little was downloaded ($ rm .../.git).
The temp directory we export to is _tmp/. The name is prefixed with _ to ensure Jekyll ignores it - Jekyll ignores folders that start with _ unless explicitly included in the _config.yml file. This temp directory is also in the .gitignore file, reducing clutter in the project repository.
The specific directory the theme is exported to is just a datetime-stamped slug of the repo we’re downloading the theme from (e.g., github.com--pages-themes--primer--19950624T181853). The datetime stamp ensures every export is unique, in case the source repo changes between exports. We found ourselves downloading many different themes and this helped keep our local reference library of themes organized.
Create a _posts/ directory in the project root and create at least one post file therein.
#!/bin/zsh
# Create a `_posts/` directory for all blog posts.
mkdir "_posts"
# Create an initial stub post.
cat <<CONTENT > _posts/1993-09-26--my-first-post.md
---
layout: post
title: My 1st Post
---
This is my first post.
CONTENT
The resulting file should look like this:
# filename: _posts/1993-09-26--my-first-post.md
---
layout: post
title: My 1st Post
---
This is my first post.
Posts are written in Markdown or HTML, they reside in the _posts/ directory, and they require the filename format YYYY-MM-DD-{title}.md for Jekyll to process them.
The {title} can be whatever you want - Jekyll will slugify it to create a valid URL for the post (i.e., my-first-post), and titleize it to make it available for rendering (i.e., “My First Post”).
For clarity, I use a double-dash delimiter, --, between the date and title parts of the filename. Jekyll handles it correctly.
The block at the top of the file delineated by three-dashed lines --- is called a YAML front matter block. It contains a snippet of YAML that sets variables for the page used by the build engine. If a file has front matter then it is processed by Jeckyll at build-time and Liquid objects, tags, and filters can be used on the page. Without a front matter block, the file is not processed and is simply copied into the resulting static site as is.
The layout: post page variable tells Jekyll to use the _layouts/post.html template for this page.
A title: My 1st Post page variable will override the title Jekyll titelizes off the filename slug (“My First Post” in this example).
Create an index.html file in the project root to serve as homepage and list all posts.
#!/bin/zsh
# Create an index file that simply lists all posts.
cat <<CONTENT > index.html
---
layout: default
---
<ul>
{% for post in site.posts %}
<li>
[{{ post.date | date: "%Y-%m-%d" }}]
<a href="{{ post.url | relative_url }}">{{ post.title }}</a>
</li>
{% endfor %}
</ul>
CONTENT
The resulting file should look like this:
# filename: index.html
---
layout: default
---
<ul>
{% for post in site.posts %}
<li>
[{{ post.date | date: "%Y-%m-%d" }}]
<a href="{{ post.url | relative_url }}">{{ post.title }}</a>
</li>
{% endfor %}
</ul>
The layout: default page variable tells Jekyll to use the _layouts/default.html template for this page.
Build the site locally and preview it to verify that it works.
#!/bin/zsh
# Build the site locally to verify that it works.
bundle install # Install all project gems.
bundle exec jekyll serve & # Run a Jekyll server in the background.
# View the local site.
open "http://localhost:4000/"
# Stop the background Jekyll server to clean up.
kill %"bundle exec jekyll serve"
The command $ bundle install installs all project gems.
This command installs all the gems required for the project: First, it reads a project’s Gemfile to determine which gems are needed; next, it resolves all the dependencies and versions for all the listed gems; then, it fetches all the needed gems and installs them on the system; finally, it creates a Gemfile.lock file with the names and versions of the gems required by the project, ensuring the project has a consistent gem environment.
From this point on, best practice is to prefix any jekyll command with bundle exec (e.g., $ bundle exec jekyll serve). This ensures that Jekyll is using the exact versions of all project gems as specified in the Gemfile.lock file, ensuring a consistent gem environment.
The command $ bundle exec jekyll serve & builds and runs a local Jekyll server in the background.
If the build is successful, the command will output the local URL http://localhost:4000/.
Append the ampersand character & to a command to run it as a background process. Running the Jekyll server in the background does not block access to the shell, allowing us to continue interacting with the terminal window and run other commands while the server is still running.
The command $ kill %"bundle exec jekyll serve" terminates the Jekyll server running in the background.
Useful shell commands:
$ gem environment # Display info on the project's gem environment.
$ gem list # List of all gems installed in the system.
$ gem list --details # Detailed information on installed gems.
$ bundle show # List of names and versions of all project gems.
$ bundle show --paths # List of paths to all project gems.
$ bundle install # Install all project gems.
$ bundle exec <cmd> # Run <cmd> using gems in the `Gemfile.lock` file.
$ bundle exec jekyll serve # Build and serve the site locally.
Stage all changes, commit them locally, and push them to GitHub.
#!/bin/zsh
# Stage all changes, commit them locally, and push them to GitHub.
git add . # Stage all files.
git commit -m "Initial commit from local" # Commit changes to local repo.
git push origin # Push changes to remote repo.
Configure GitHub Pages for this repo.
#!/bin/zsh
# Configure GitHub Pages for this repo:
echo "Configure GitHub Pages for this repo:"
echo " 1. Go to the GitHub Pages Settings for this repo:"
echo " https://github.com/${_USERNAME}/${_REPO_NAME}/settings/pages"
echo " 2. Select [ Source = \"Deploy from a branch\" ]."
echo " 3. Select [ Branch > Select branch = \"main\" ]."
echo " 4. Select [ Branch > Select folder = \"/ (root)\" ]."
echo " 5. Click on \"Save\"."
open "https://github.com/${_USERNAME}/${_REPO_NAME}/settings/pages"
Online docs: [ GitHub Pages docs > Configuring publishing source > Publishing from a branch ].
After a repo is updated, it takes a few minutes for the build process and publishing to complete.
View the published site.
#!/bin/zsh
# Script settings.
if [[ "${_REPO_NAME}" == "${_USERNAME}.github.io" ]]; then
readonly _REPO_HOMEPAGE="https://${_REPO_NAME}/" # User site.
else
readonly _REPO_HOMEPAGE="https://${_USERNAME}.github.io/${_REPO_NAME}/" # Project site.
fi
# View the published site.
open "${_REPO_HOMEPAGE}"
https://{username}.github.io.Exit the shell session.
$ exit # Exit the shell session.
That’s it. You now have created a very simple GitHub Pages site.
There are a ton of things you can do next. In addition to creating more content (posts), you can also change the UX of the site by trying out different templates, or adding new features to yours via plugins or custom code. All of these changes are straight-forward to implement. The Jekyll docs and the GitHub Pages docs are good places to start.
Good luck!
There are currently many AI coding tools out there: ChatGPT, Gemini, Claude Code, GitHub Copilot, etc.
I vibe-coded a working Jekyll site for GitHub Pages as one of my experiments. This took longer than expected and required a lot of hand-holding. Many of the build errors were resolved by crafting prompts to clarify intent, narrow scope, and guide output. To craft more effective prompts I found myself reading the GitHub Pages and Jekyll docs. By the time I was done with these docs I had a working site and most of this runbook developed.
Here is what I learned from this exercise:
For this project, I expect these tools will be most useful in writing HTML/CSS/JavaScript code to update the site’s templates, and in integrating and configuring feature plugins.