Goodbye, WordPress - Hello, Hugo & nginx
For the past 4 or so years, I’ve been using WordPress to push content to this blog. I originally chose it because my knowledge of anything to do with Web and Mobile Development is spotty at best - and because “back in the day"™, it was the only CMS I was somewhat familiar with.
Maybe you’ve seen the sluggish mess that the blog is - and today, we’ll change it.
The issues I’ve had with WordPress were numerous, but to name a few:
- You have to be online
- A complicated setup to keep PHP, MariaDB, nginx and other components working
- Several security issues in the past
- A horrible WYSIWYG editor, without any markdown support, save for external plugins
vibindings, such as in
VS Code, or, well,
vim- everything is mouse-driven
- The entire thing is slow - navigating, updating, preview
- Constant bot-spam in the comments
- No version control
- No decent template model
- No external contributors
- No native syntax highlighting; Plugins have a tendency to mess things up
- 2.5MB transfer and 47 requests / network activity per page load
- Up to 40s (!) loading times on slow connections
While I’m sure somebody who is super familiar with WordPress can figure some of those out, but one thing that comes to mind is this horrifying contraption for running Wordpress.
For comparison: My actual website, which is plain
CSS, loads 32KB (most of which is the little Tux
favicon) and hence in under 0.3s on a similarly horrible connection.
My workflow, however, was the real pain point (I accept judgment for this):
- Write the content locally in Sublime or vim, ignoring formatting, so I don’t have to deal with network issues and can work offline
- Copy-paste the code to WordPress, waiting for it to mess up
- Manually add the code with an external SyntaxHighlighter plugin
- Edit the generated HTML to fix any messed up code formatting
- Cry and drink
- Repeat this every 3 months
In other words, its worse than writing a Confluence page on dial up.
Accepting feedback and contributions is also difficult - while I have added feedback in the past, it requires me to do it. With a more standardized CI/CD process, it would be trivial to simply accept merge requests.
But since the only person to blame here is me, I now finally managed to fix it.
Using a static side generator is a really old idea and I don’t claim any intellectual points for it. However, I am documenting this process for the sake of documenting it.
The idea here is to have a workflow that looks like this:
- Write posts in markdown. Add code in pre-defined blocks/shortcodes.
- Store images locally
- Push to Git (GitHub, GitLab)
- Deploy to the web server using a CI/CD flow
Hugo is a simple static-site generator written in
golang. After thinking about
Jekyll and the joys of Ruby, I’ve decided to go with the simpler alternative, as it only requires one binary, has a relatively simply syntax, a decent suite of tools and plugins, nice, open source / MIT licensed templates and has been in my bookmarks since I saw it on HackerNews ages ago. There are other alternatives available, but after playing around with
Hugo for a bit, it seemed exactly what I want.
We just grab the latest binary for a 64-bit Linux from GitHub, unpack it, and store it somewhere on the
We can then set up a page:
After some small edits to the
config.toml, we should be good to go and only need content.
Exporting all old posts
I want to export all old blog posts to Markdown. There’s a great little tool by
lonekorean on GitHub.
All we need is an export of WordPress and run:
git clone https://github.com/lonekorean/wordpress-export-to-markdown cd wordpress-export-to-markdown npm install node index.js
After that, we can copy the generated files to the posts directory.
There are other tools available here, but since I’ve already customized WordPress quite a bit, the above export was a possible route. Your mileage may wary.
Start a dev server that automatically refreshes once we save a file:
We also need a real theme for the page.
My personal decision criteria for a theme were the following:
- No external dependencies to CDNs, googleapis, Google Analytics, trackers…
- Simplistic interface, without omitting information
- Fast load times even on slow connections
Adjustments can be easily made using fairly standard HTML (w/ Hugo’s injections) and CSS, which I found easy to figure out, despite being anything but a Web Dev. :)
The changes made to that theme were the following:
- Removed all references to Google’s font-CDN
- Removed Analytics code, even if it was controlled by a variable
- Modified the CSS to
- Order all tags inline, as opposed to as a list
- Change the background color for Syntax Highlighting, otherwise we’re looking at grey code on a grey background
- Added some classes for a Back button
- Added a Back button to all posts
- Added a TOC, controlled by a variable, to all posts
- Added a word count, tags, and an approximate read time to the overview
- Added very serious, random messages at the end of the posts
Most of it was simple HTML and CSS updates, like this update for the Back button:
The last part with the random messages was interesting, as
Hugo / the underlying
go Syntax allows you to pipe commands:
And I am reasonably happy with the result:
Feel free to check out the theme’s repository here: https://github.com/chollinger93/hugo-ink
Adjusting exported blog posts
Due to my rather interesting
WordPress configuration, the exported posts from above need some help.
The issues I’ve found where the following:
- GitHub Gits are not rendered
- Internal Syntax Formatting caused everything to be escaped with
\, breaking code
- Headlines are missing
- Tags are missing
- Descriptions are missing
Gists get inserted as such:
And look like that:
Whereas we are expecting:
We can fix that by replacing
But, of course, doing that by hand would be tedious, so we can script that:
I’ve also added a
Visual Studio Code shortcut in
keybindings.json to insert Hugo’s Syntax Highlighter blocks:
In order to avoid breaking links, and hence any bit of SEO we might have, we need to make sure that either all posts follow the same naming schema - they do in my case - or add a setting in
In order to ensure this is indeed a 100% match, I wrote this horrible little
go script to compare both the local (after
hugo build) and remote
Which did find some errors.
Which I was able to fix after deploying the first version of this article. Psst, don’t check the Git commits. :)
There were more adjustments that needed to be done - like re-adding videos - but for the most part, everything was working fine. I will spare you the tedious details.
For the deployment, the goal is to deploy the static HTML to the existing webserver over at chollinger.com, which runs
docker containers. Externally hosted sites, like
GitHub Pages, are an option, but I would like to keep as much “in house” as possible.
We have 2 convenient (and free for Open Source) options for CI/build servers:
GitHub Actions. In this case, we’ll be using
GitHub Actions, as it avoids having yet another external dependency.
Our flow will look like this:
- Checkout the master branch
- Update the theme via its submodule
- Download hugo
- Build the static HTML/CSS/JS
- Deploy via SCP to a specific
- Do any other action, like updating a
dockerimage, on the server (although nothing stops you from doing this in the pipeline)
We’ll create a
.github/workflows/workflow.yml file as such:
You can use some of the pre-configured SCP Actions instead of using
bash. I had issues with those and since all Actions I’ve found either don’t support key passphrases or don’t supply proper debug logs, I gave up and used a
bashaction for deployment.
If you have any idea what “Exit code 1”, despite a successful connection on the server (as per
auth.log), means, do let me know!
We’ll deploy via SCP, but only with a specific user that has restricted permissions.
Add the user:
Make the user’s home owned by
Next, let’s compile
sftp only shell.
We need to allow the user access, so we need to edit
/usr/local/etc/rssh.conf as such:
This translates to:
username: The username of the user for whom the entry provides options
umask: The umask for this user, in octal, just as it would be specified to the shell
access bits: Five binary digits, which indicate whether the user is allowed to use rsync, rdist, cvs, sftp, and scp, in that order. One means the command is allowed, zero means it is not.
path: The directory to which this user should be chrooted (this is not a command, it is a directory name). See chroot_path above for complete details.
Please see the man page.
Especially setting up a
chroot is a bit more involved than setting a single config file, so be advised to read through the appropriate
man pages and
CHROOT in the
rssh source directory.
Make sure the users exists in
Edit the SSH config:
Ensure that the user is in the
AllowUserdirective. At this point, you might want to think about a separate
And restart the SSH daemon:
Finally, change the user’s shell to
Of course, at this point, you will also need to generate your
ssh keys (use
PEM for GitHub Actions, as I’m pretty sure the
Ubuntu image used by their
bash action uses a weird
openssh version), set up whatever flow you need to actually get the HTML served (e.g.,
docker) but I will skip over that part in the interest of time.
I for one am pretty happy to have made this change. Granted, I don’t get a ton of traffic here (but since I don’t run Analytics, I don’t really know), but for my own sanity, this new workflow is clean, easy, and collaborative.
But despite few views, blogging is fun! Every time I write something, my goal is always to learn something - it doesn’t matter if is something complex (like understanding some ML/Statistics theory) or something simple like today, where I had to get back into some minimal web development and “DevOps” (even though I thoroughly hate the term).
Having this new setup allows me to blog more and spend more time on writing both code and posts, and less on headaches through WordPress. Shorter loading times should cause less frustrations for everyone involved and not being locked-in within WordPress (but rather, having version-controlled markdown files) allows for a more open environment.
I can only implore you to try it yourself - blogging, that is - and if you want to use GitHub + Hugo + Nginx (+ Docker), maybe this helps.