Unsupervised Thoughts

Creating A Static HTML Blog Deployed with CI/CD

Written on

In thinking about creating and maintaining this blog I had a few requirements to keep it simple, learn a few things along the way and hopefully work on my goal of coding more this year.

You can find the an example git repistory here to start with.

Requirements

  1. Keep it as simple as possible with mostly static HTML, no CMS, relational database etc required
  2. Ensure the content is parsable so I can process it, manipulate it and/or move it if I decide in the future
  3. 100% maintained in git
  4. Use Python
  5. Tested and deployed with CI/CD

I plugged these requirements into ChatGPT and it recommended Pelican as good Python based static site generator with a bit of a git workflow. This gave me enough threads to pull on to land on some good tools.

Tools

  • Pelican: After playing around with Pelican a bit it proved to be simple, and flexible enough. I started with the Cid theme and customized it as I went.
  • Gitlab: I choose a free Gitlab account over GitHub just because we're currently using it at work and I want to take some of my learnings back to my day job.

Creating and Deploying Content

The Pelican quickstart tutorial was enough to learn the basics of configuring a site and writing a test blog posting. It really is a simple but powerful tool.

Start with a simple pelican site in a Python venv

% mkdir my-blog
% cd my-blog
% python -m venv venv
% source venv/bin/activate
% pip install pelican markdown"
% pelican-quickstart 
Welcome to pelican-quickstart v4.11.0.

This script will help you create a new Pelican-based website.

Please answer the following questions so this script can generate the files
needed by Pelican.


> Where do you want to create your new web site? [.] 
> What will be the title of this web site? Your Blog Title
> Who will be the author of this web site? Keith Lawson
> What will be the default language of this web site? [en] 
> Do you want to specify a URL prefix? e.g., https://example.com   (Y/n) 
> What is your URL prefix? (see above example; no trailing slash) https://keithlawson.me
> Do you want to enable article pagination? (Y/n) y
> How many articles per page do you want? [10] 
> What is your time zone? [Europe/Rome] America/New_York
> Do you want to generate a tasks.py/Makefile to automate generation and publishing? (Y/n) 
> Do you want to upload your website using FTP? (y/N) n
> Do you want to upload your website using SSH? (y/N) n
> Do you want to upload your website using Dropbox? (y/N) n
> Do you want to upload your website using S3? (y/N) n
> Do you want to upload your website using Rackspace Cloud Files? (y/N) n
> Do you want to upload your website using GitHub Pages? (y/N) n
Done. Your new project is available at /Users/username/code/my-blog-public

Create a Posting

Create a test post "content/testpost.md"

Title: Test Post
Date: 2025-01-01 
Category: Test

Hello world!

Test generating HTML

% pelican content 
Done: Processed 1 article, 0 drafts, 0 hidden articles, 0 pages, 0 hidden pages and 0 draft pages in 0.14 seconds.
% ls -l output 
total 48
-rw-r--r--@ 1 keith  staff  2953 28 Mar 06:43 archives.html
drwxr-xr-x@ 3 keith  staff    96 28 Mar 06:42 author
-rw-r--r--@ 1 keith  staff  2934 28 Mar 06:43 authors.html
-rw-r--r--@ 1 keith  staff  2927 28 Mar 06:43 categories.html
drwxr-xr-x@ 3 keith  staff    96 28 Mar 06:42 category
-rw-r--r--@ 1 keith  staff  3438 28 Mar 06:43 index.html
-rw-r--r--@ 1 keith  staff  2847 28 Mar 06:43 tags.html
-rw-r--r--@ 1 keith  staff  3592 28 Mar 06:43 test-post.html
drwxr-xr-x@ 4 keith  staff   128 28 Mar 06:42 theme

That's really enough to get a basic site up and running.

Git repository

Since we're going to build and deploy through CI/CD the repository just has to have the markdown source.

Create your .gitignore

# Ignore Pelican's output folder
/output/

# Ignore Python bytecode files
*.pyc
__pycache__/

# Ignore logs and databases
*.log
*.db

# Ignore virtual environment (if you're using one)
venv/
keith@lappy my-blog % cat .gitignore 
# Ignore Pelican's output folder
/output/

# Ignore Python bytecode files
*.pyc
__pycache__/

# Ignore logs and databases
*.log
*.db

# Ignore virtual environment (if you're using one)

Initialize repo and commit your test posting.

% git init && git add . && git commit -a

I'll leave out the steps of creating the gitlab repository and pushing the repo up so next steps assume you have your repository in a private gitlab repo.

CI Deployment

Create your ".gitlab-ci.yml" in the root of the repository. This configuration spins up a Docker container, runs Pelican to generate the content, rsyncs it to the server your site is hosted on and then runs a broken link checker across the published site.

image: python:3.11

stages:
  - build
  - deploy
  - test

variables:
  REMOTE_USER: "ssh_username"
  REMOTE_HOST: "server.example.com"
  REMOTE_PATH: "/home/ssh_user/myblog"
  PYTHON_VENV: "/home/ssh_user/myblog/venv/bin/activate"

before_script:
  - python -m pip install --upgrade pip
  - pip install -r requirements.txt

build:
  stage: build
  script:
    - pelican content -o output -s pelicanconf.py
  artifacts:
    paths:
      - output

deploy:
  stage: deploy
  before_script:
    - apt-get update -qq && apt-get install -y rsync openssh-client
    - mkdir -p ~/.ssh
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_ed25519
    - chmod 600 ~/.ssh/id_ed25519
    - eval "$(ssh-agent -s)"
    - ssh-keyscan -H $REMOTE_HOST > ~/.ssh/known_hosts
    - chmod 644 ~/.ssh/known_hosts
    - ssh-add ~/.ssh/id_ed25519
  script:
    - rsync -avz --delete output/ $REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH
  only:
    - main  # Adjust based on your branch

test:
  stage: test
  script:
    - pip install linkchecker
    - linkchecker --ignore-url="mailto:" --check-extern --no-warnings https://keithlawson.me
  only:
    - main    

Add your private key to the environment variables in your gitlab repo:

  1. Generate an SSH Key Pair (if you don’t have one already)

ssh-keygen -t rsa -b 4096 -C "gitlab-ci" -f gitlab_deploy_key

This gives you:

gitlab_deploy_key (private key)

gitlab_deploy_key.pub (public key)

⚠️ Never share the private key (gitlab_deploy_key)!
  1. Add the Private Key to GitLab CI/CD as a Variable
    Go into your gitlab repo. 

    Navigate to:
    Settings  CI/CD  Variables

    Click "Add variable".

        Key: SSH_PRIVATE_KEY

        Value: (paste the contents of gitlab_deploy_key)

         Mark as Protected and Masked

        🔒 Optional: Set scope to only environments where you need it

That's it! Use a standard git workflow create new content in content/posting.md, commit and push. Your Gitlab CI job will kick off, generate the Pelican content, deploy to your web server with rsync over SSH and test for broken links.

Gitlab deploy