Save your time and just use Ghost

So, I have migrated my old Gatsby-based website to Ghost, as I‘ve realized my writing time is more valuable than maintaining my website every year to yet another framework and/or version.

Let me explain my frustration. If you run your own website on some sort of framework, some might feel familiar:

  • every time I wanted to write a new blog post, I wanted to improve some other part of the website — but I just wanted to share knowledge
  • the version of Gatsby I was using became unsupported, so updating it would be necessary but very painful
  • ever so often, those frameworks (or the people running them) get acquired/acquihired, leaving that project without a roadmap and lackluster support
  • wanting to build a self-hosted mailing list is possible but painful, that time is better spent writing new posts
  • using a managed service would ease the pain, but the lock-in is considerable and the services tend to become pricy once their focus (understandably) shifts away from early adopters to well-paying enterprises
  • sometimes, I have time to write a small post but running a development setup is infeasible, even with the glorious iPad setup I am writing this on

Things to consider before switching over

  • custom component logic/designs can be tough to port over, but components with low logic are easily ported to Handlebars
  • some additional configuration and adaptations is necessary if page speed is of higher priority (see later section)
  • if you have the money to spend and care less about the liberties involved with self-hosting, a managed service such as Ghost(Pro) probably saves some time and configuration frustration; self-host if you feel confident enough in your skills and accept the added maintenance “burden“

Setting up the Docker container and nginx

If you have multiple services running on your server, using the provided Docker setup can give you a good encapsulation of services. One of the downsides, is that due to proxying the requests to the container, nginx cannot facilitate the caching of static assets.

The configuration of the community-provided Docker container and docker-compose scripts can be used as a starting point:

version: '3.1'

services:

  ghost:
    image: ghost:5-alpine
    restart: always
    ports:
      - 8080:2368
    environment:
      # see https://ghost.org/docs/config/#configuration-options
      database__client: mysql
      database__connection__host: db
      database__connection__user: root
      database__connection__password: example
      database__connection__database: ghost
      url: <your-website>
      # see later section for using your own server for the assets
      sodoSearch__url: "https://<your-website>/assets/js/unbundled/sodo-search.min.js"
      sodoSearch__styles: "https://<your-website>/assets/css/sodo-search.css"
      mail__transport: "SMTP"
      mail__options__service: "Mailgun"
      mail__options__auth__user: "<mailgun-user>"
      mail__options__auth__pass: "<mailgun-password>"
      mail__from: "Your Name <name@your-website>"
    volumes:
      - ghost:/var/lib/ghost/content

  db:
    image: mysql:8.0
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: example
    volumes:
      - db:/var/lib/mysql

volumes:
  ghost:
  db:

Replace <your-website> with the base URL of your website. Also change your mysql password, even if it is not accessible from the internet.

Configuration of email is necessary, and this allows you to send newsletters and notifications to your subscribers. Follow the configuration guide on how to integrate the email service of your choice.

If you are hosting Ghost on your server without Docker, also check the configuration guide on how to add the configuration.

Adding the nginx Server Block

You'll need to set up a basic server block in nginx that proxys requests to the Ghost docker container.

server {
  listen 80;
  server_name         timweiss.net;

  gzip on;
  gzip_types      text/plain application/xml;
  gzip_proxied    no-cache no-store private expired auth;
  gzip_min_length 1000;

  location / {
    proxy_set_header  Host                $host;
    proxy_set_header  X-Forwarded-Proto   $scheme;
    proxy_set_header  X-Real-IP           $remote_addr;
    proxy_set_header  X-Forwarded-For     $proxy_add_x_forwarded_for;

    # From: https://ghost.org/forum/bugs-suggestions/469-blog-cover-won-t-upload/11/
    client_max_body_size                  50m;
    client_body_buffer_size               128k;

    proxy_pass                            http://localhost:8080;
  }
}

After you have set up the basic entry and added it to sites-enabled, reload nginx. Then, I recommend running certbot for enabling SSL using their provided guide. It will also take care of setting the config within the server block.

Porting over your design

Even if you have never used the Handlebars syntax before, picking it up will not take long. Component-like structures can be made with separate files and Ghost provides some rich helper functions to make template creation smart and easy.

To get started with theming, setup Ghost on your local machine, copy over a theme you like after selecting it from the official themes gallery (under /ghost/#/settings/design/change-theme). On your filesystem, clone the theme's folder in content/themes to a new folder.

I suggest setting up a git repository within the theme folder to track changes. It's also useful if you want to quickly try something out.

Feel free to alter the behavior, such as removing pagination in favor of a single page, for example:

{{!-- All posts --}}
{{#match feed "index"}}
    {{#get "posts" limit="all" include="authors"}}
        {{#foreach posts}}
            {{> "post-card" lazyLoad=true}}
        {{/foreach}}
    {{/get}}
{{/match}}

Cloned from the casper theme, in partials/components/post-list.hbs

I've also decided to create a custom page for the blog within the CMS and created a template file for some custom design, as well as to include the post list. It has been copied from post.hbs and slightly adapted.

{{#post}}

<main class="gh-main">
    <article class="gh-article {{post_class}}">
        {{#match @page.show_title_and_feature_image}}
            <header class="gh-article-header gh-canvas">
                <h1 class="gh-article-title is-title">{{title}}</h1>
                {{#if custom_excerpt}}
                    <p class="gh-article-excerpt is-body">{{custom_excerpt}}</p>
                {{/if}}
                {{> "feature-image"}}
            </header>
        {{/match}}

        <section class="gh-content gh-canvas is-body">
            {{content}}
        </section>

        {{> "components/post-list" feed="index" postFeedStyle=@custom.post_feed_style showTitle=false showSidebar=@custom.show_publication_info_sidebar}}
    </article>
</main>

{{/post}}

If you want to setup syntax highlighting, I suggest checking out the tutorial provided by Ghost:

A complete guide to code snippets
Developers write code. Some developers write about writing code. But when they try to share that code on the web, everything that makes code more readable – like formatting and syntax highlighting – is gone!

Once your design is finished, run the following command within the theme's repository to export. The file can then be uploaded to your blog:

npm run zip

Improving page speed (a little bit)

If you theme uses images, it can be useful to make them smaller. While the {{img_url}} helper exists, it can only be used with content uploaded from within Ghost, not with images supplied by the theme. For example, I have resized the picture on the home page and converted it to WebP, as it‘s much smaller and widely supported.

ImageMagick is a useful CLI tool to convert and downsize a (quadratic) image. For the profile picture, for example:

magick profile.jpg -resize 300x300 profile.webp

It is also really useful to enable gzip compression in nginx, as it greatly reduces the size of the bundles transferred (for example, even the minified portal is around 1MB in size). Inside the server block for Ghost, be sure to add the following:

server {
  ...
  gzip on;
  gzip_types      text/plain application/xml;
  gzip_proxied    no-cache no-store private expired auth;
  gzip_min_length 1000;


}

Keeping everything self-hosted

Ghost by default uses some scripts that are hosted on jsdelivr, which is reasonably fast and generally seems to has good backing by the community.

Self-hosting those scripts helps us to minimize the exposure to third-parties.

It depends on how you construct your threat model: is it more likely that a supply chain attack happens or that your server gets “hacked“? The former is a more attractive target but well equipped against it, while the latter might be unnoticed for longer but is not as attractive (and also depends on your sysadmin skills). I‘ll leave the bayesian modeling of that problem to others.

  1. Check which scripts need to be mirrored locally (by using the network inspector), it will most likely be sodo-search and portal
  2. Download the CSS and JS files for both
  3. Place them in your theme‘s assets folder, create a folder within the assets/js folder so they don‘t get bundled by the default Gulp script
  4. Link all files in the config/environment

If you use the Docker container to host Ghost, here‘s the configuration for loading both scripts from “within“ the theme:

version: '3.1'

services:
  ghost:
    ...
    environment:
      sodoSearch__url: "https://<your-website>/assets/js/unbundled/sodo-search.min.js"
      sodoSearch__styles: "https://<your-website>/assets/css/sodo-search.css"
      portal__url: "https://<your-website>/assets/js/unbundled/portal.min.js"

Replace <your-website> with the hostname or URL that points to your Ghost instance.

What to do next

If hosting locally, it makes a lot of sense to have some kind of database backup running regularly, I could also make a post on this, but am not yet sure – while this does not directly translate to MySQL, the following post also includes a backup strategy:

Self-hosting your own email list
Just today (12.05.2023) I have open-sourced optiboy. While functionality is limited, it provides a clear way forward to build my own email list. Several bloggers and marketers have iterated on why it is important. Even if blogging is just a thing you do from time to time, it