Ghost Static Pages part 1: Set up local Ghost for static pages generation
4 years ago, I have devised a Docker based pipeline for publishing static web pages from a local deployment of Ghost and publishing them to Gitlab using git.
I used Ghost 1.x, and the static page generator I used was a Python script that was an already abandoned project on Github and designed to work on early versions of Ghost.
I kept a fork ot it, and I had to patch the script and around the script (my Python is not great) to get the whole thing working and do it again from time to time to keep it working.
However, I couldn't update Ghost as the python script would stop working with more recent version and I had one too many failure (the last straw), so I've decided to rebuild from scratch. I will keep Ghost as the centrepiece as the reason I liked it in the first place is still valid today: fantastic Markdown-based graphical user interface for writing posts, tag-based site organisation and very simple infrastructure and operation (no web server to hook - although I kept a basic one for local preview -, no relational database server - It used embedded SQLite3) and a properly marked up web output for published articles. There are evolutions in the blogosphere and in Ghost's business model, and I have discovered the Fediverse and the Gemini protocol, all that made me starting to investigate various options (I'll touch on this in Part 4 hopefully), but for now I am very happy with it.
I will keep the same workflow as in my old pipeline, but this introductory post will focus on the first two transitions for exporting Ghost's posts as static web pages and previewing it.
┌──────────────────────────────┐ ┌──────────────────────────────┐ ┌──────────────────────────────┐ ┌──────────────────────────────┐
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ export │ │ preview │ │ git push │ │
│ Post editing and management ├────────────────────► Static pages on file system ├────────────────────────►Site served on local web server──────────────────►Site served on GitLab Pages │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
└──────────────────────────────┘ └──────────────────────────────┘ └──────────────────────────────┘ └──────────────────────────────┘
Ingredients:
- Sqlite3 (fast, lightweight, embedded SQL database engine)
- docker (Implementation of Linux containers with productive developer tooling)
- NodeJS (Javascript server)
- npm (Nodejs Package manager)
- ghost-static-static-generator, a.k.a gssg (a NPM package for generating static pages from a Ghost deployment)
- Nginx (Web server)
- wget (Web client)
- Ghost (A web-based commercial content publishing system for NodeJS with a free open-source self-hosted version)
Recipe:
Prepare the Dockerfile for the static page generator
I don't have to do this step and could use gssg
directly on the computer.
However, using a Dockerfile for packaging all the Javascript allows me to minimize software dependencies installed on my computer that I would have to track manually otherwise. Also, it allows for running this setup on any system that can run docker or other compatible container technologies (more on this in Part 3).
Prepare the docker-compose file:
We will need three container services:
- editor: that's the Ghost web application
- export: that's the
gssg
static site generator - preview: set up a web server pointing to the generated site for local previewing
I picked Ghost 4.x over the latest Ghost 5.x because I want to use Sqlite3:
I dont' want to operate a client/server RDBMS (the MySQL option) and I dont' need it since I don't serve the web site from the Ghost webapp directly. Ghost 5.x only support MySQL while Ghost 4.x is the lastest to support the embedded Sqlite3 [1].
The volumes
block is not strictly needed to get this workflow working, but it allows me to have the Ghost data and its configuration file outside of Docker so to be easily manageable (backup and further potential customisation).
The variable $REMOTE_URL
contains the public url of my blog. Docker expects it to be defined in an .env
file sitting alongside the Docker compose file. The build:
directive indicates that container service is using the default Docker file we have defined earlier. The command:
line is the actual invocation of the static site generator. The --ignore-absolute-paths
parameter tells gssg
to make all links relative to the site root. In preview mode it allows to navigate to all pages locally instead of jumping to remote site when navigating to the aggregations pages (like tags). The volumes:
block is the most important part, this is how we can retrieve the static pages that makes up our site on our computer in the site
directory.
The preview
container service allows me to navigate with my web browser to http://localhost:9999
and see the static version of the web site there.
gssg
has an internal previewing mechanism that function on the same principle, but it required installing an additonal NPM package and I wasn't able to get it working within Docker context. The approached I've taken, based on the nginx
web server is copied from my previous blog publishing pipeline (also Docker based) I've used for years and worked fine for my need.
Dealing with bugs in gssg
The premise of gssg
is that it can generate static page from a local deployment of Ghost (reachable at https://pommetab.com
), as well as from remote deployments. By default it expects a local deployment. In order to generate static pages for remote deployment of Ghost, one needs to use a --domain <url of remote Ghost deployment>
parameter.
When using Docker, the hostname for the Ghost deployment in the Docker compose context is the container service name (in our case editor
), and since we run gssg
in the same context, we would need to specify --domain http://editor:2368
to the command otherwise it won't be able to connect to the webapp running as a container service.
The problem is that gssg
has an issue whereby for some files (Site Maps XML files) it forgets to use the value of --domain
and use https://pommetab.com
instead which is probably hard-coded somewhere in the codebase.
To work around this issue, we add additional configuration directives to our container services so that:
editor
container is assigned a fixed IP address within the Docker networkexport
has an extra host mapping fromlocalhost
to that IP address
Then we don't need to use --domain
as gssg
thinks it is dealing with a local deployment all the way.
This a known issue to the gssg
developers and there is a Github ticket for it. [2]
With the above fix added, the full docker-compose.yml
looks like this:
Usage:
$ docker-compose build export
$ docker-compose up -d editor preview
$ docker-compose run --rm export
Conclusion
By now, I have a project where I can spin up a Ghost editor, write a blog post, then (re)generate the static version of the web site.
Provisional plan for follow-ups:
- Part 2: Publish the generated site to a staticpage-hosting forge (Github, Gitlab)
- Part 3: Deploy the setup on iPad using iSH
- Part 4: Thoughts on the state of blogging and future ideas
This post is my first one using the system described here. Until part 2 is done, I have to manually push the generated site to the last step of my old blogging pipeline.
[1] https://ghost.org/docs/update/
[2] https://github.com/Fried-Chicken/ghost-static-site-generator/issues/65