How to make static site publishing easier

So you just got your static site up and running, and if you’re like me, you spent way too much time making your own custom design and other niceties.

After you’ve published your obligatory “I switched from X to Y!” post that tons of blogs use as their only content for years, you’re ready to start writing for real!

You decided to go static for one or a combination of the following reasons:

I’m only half kidding on that last bit, handling images is definitely a major Pain Point with static sites.

Before I go into problems I’ve encountered and how I solved them, let’s wind up, rotate hip, extend shoulder, and knock the obvious catch-all solution right back into the undersea internet volcano it crawled out of:

Using a CMS is not a good option

All of the pain points I’m going to outline below can be solved by using a content management system (CMS).

If you’ve switched your site to static because you didn’t want to pay for hosting, or reduce server overhead, using a CMS defeats the purpose.

You’re either paying a startup that might not be around in a few years, or you’re using a self-hosted solution that adds overhead, complexity, and security considerations.

And while a CMS is supposed to make it easier for you to publish, the initial integration can be difficult and complex, and not migration-friendly.

Images and content might live outside your repo (API solutions), which is bad for longevity. And there’s few CMSs that work with static files and commit to git. And even if they do commit to git, sometimes they add clutter.

Using a CMS with a static site only makes sense if you’re leveraging the huge traffic handling ability of a static site and need multiple people to manage content.

I’m talking to myself here: hundreds of thousands of people aren’t going to visit your little personal blog on a daily basis, and you’re not hiring an editor.

There’s no need to over-engineer this, as much fun as that is. Cross that bridge when you get there.

If you prefer to use a CMS, use a popular, well-supported one-stop-shop CMS, like WordPress, Ghost, or any number of alternatives.

A note: I’m going to mention “longevity” a lot. To me, longevity in terms of a site’s content means that for at least 5 years, it’s able to be easily adapted to a new site design or migrated to another system, and not depend on a third-party service.

Static site pain points and how I solved them

Starting a new post needs preparation

Not including the initial site clone, possible dependency install, and build error wrangling, each post in a static site needs some sort of preparation.

You’ve got to create the file, and if you’ve got a folder structure like mine, that involves creating a new folder too.

If you’re using an SSG, you’ve got to put in frontmatter to tell the SSG what the title of the post is, date, and optional tags, description, cover image, draft status, layout options, etc.

If you’re using straight HTML, you’ve got to add your HTML structure if you want styling (you do, full-width text is awful to read) and maybe navigation links.

And then you can start writing.

It doesn’t sound like a lot, but when you have to do it every time, it quickly becomes a chore.

My solution: Use a template, and then automate it

This is probably a “duh” moment, but have a blank file with your frontmatter or HTML already in place, that you can just duplicate and start writing in. This is also referred to as “scaffolding”.

And keep new post creation to one (1) command or action. Don’t take multiple steps to create the folder, copy the template over, and open it in your editor.

Write a script, or even just use the bash && operator and string multiple commands together and save it in a note and your terminal history.

You can even automate the post title and date creation.

The SSG I use on this site, Hugo, has a built-in scaffolding system called Archetypes, and to create this post, I just typed one command in my project root:

hugo new blog/2022/04/easier-static-site-publishing/

This creates new folders if they’re not there, creates the Markdown file with the frontmatter, including title and date from the command, and even opens it in my text editor, ready to write.

You can tell Hugo what your text editor is in your config file, here’s mine for example (YAML):

newContentEditor: open -a typora

You may also have to add the command to your security configuration:

- ^subl$
- ^open$

For other SSGs, Bryce Wray has an excellent article on creating an interactive shell script.

Adding images is a pain

I love writing in Markdown, but adding images is annoying. Most solutions I’ve seen involve uploading to a third party service and then pasting the URL in your Markdown, which means cost and broken image URLs later that you might have to manually fix across hundreds of posts. Third-party image hosts aren’t a good solution.

My solution #1: Use a Markdown editor

With a good Markdown editor, you can just drop the images straight into your post, like you would with a CMS, and it will copy the image files where they need to go.

I personally use Typora for this. My images live in the same folder as the post, which makes it easy to delete all the images and other related files when I delete a post. Typora’s image options panel has a preset option for this:

Typora's image options

You can also set a custom folder and construct the path how you want it:

More Typora image options showing custom settings

If the folder isn’t created, Typora will prompt you before it creates it.

More documentation on this feature is available here.

Other things I like about Typora: You can use formatting shortcuts, including my favorite, Command+K, to link selected text with a URL from your clipboard. There’s also a neat package for Sublime Text to open the current Markdown file in Typora.

Jim’s solution: Build your own CDN

Jim Nielsen recently wrote about his setup for easily uploading and adding images to his posts, where he puts them in a folder automatically synced to Netlify, and then uses macOS Shortcuts to be able to right-click and copy the uploaded image URL straight from Finder.

I like this solution because you have full control of the folder and the images have a known path — if you stop using this method in the future, you can easily update all your posts with a quick find-replace across files, straight from your text editor or terminal.

Managing images is a pain

Most CMSs have a media library where you can easily see all your images and pick them for a post. You can’t really do that with a static site, or can you?

My solution: Your OS file manager

Your operating system’s file manager is a great way to view thumbnails of all your images, and you can easily edit them with whatever your preferred photo editor is. Mine is Affinity Photo.

Keeping all your images in one folder is a good way to see them all in your file manager, however I didn’t like doing this because when I delete a post, the images don’t get deleted with it. This is a problem I have with CMSs, too.

So I set up my file structure to have individual folders for each post, and then the images + file go in there, along with any other static files I want to link in the post (like text or PDFs). This also makes it easy to diagnose build errors (I had some old corrupted images causing the build to fail).

But then you’ve got to go folder by folder to see all your images, right? Nope! Depending on your file manager, you can set up a saved search for all images in your posts directory and then you can view them all in one place[1].

Optimizing images is a pain

You’ve got your images in your post and saved in your repo. Good!

They’re taking 5 minutes to load and aren’t responsive. Bad!

So many pain points with images. This was the biggest one for me. I used to run my images through PhotoBulk to resize them, and then run them through ImageOptim to optimize them. Very complicated process and not as good as responsive images + WebP.

My solution: Use an SSG with image optimization

Taking care of this depends on your SSG or build tools. I try to not use build tools outside of the SSG as much as possible, to avoid dependencies and my experience with tools like Gulp has not been successful[2].

If you’re using an SSG, try to avoid using shortcodes in your Markdown like the plague, especially for images. If you do use shortcodes, you’re going to have a hard time later fixing your markup across hundreds of posts when you switch generators or move to different setup. Unless you’re good at regex (I’m not).

Hugo has built-in image optimization and a convenient Markdown image hook template you can use to optimize and generate responsive image sets. I wrote more about this here, but the gist is to use Daniel F. Dickerson’s handy module. It’s pretty much drop-in-and-go, with a couple config file additions.

I no longer feel the need resize and run my original images through ImageOptim, as Hugo does it all for me, and only outputs WebP, so the user never sees the original gigantic image. I do need to set it up for an optimized JPEG fallback though.

I switched to Hugo from 11ty because it lacked support for optimizing images linked in Markdown, but it appears Alexs7zzh figured out how do it with eleventy-img.

Whatever your SSG is, it’s most likely not going to be a batteries-included setup, and it could be as simple as adding a dependency or heavily involved, where you’re writing your own solution.

Another solution: Use a script to optimize your images

You can build your own optimization script, and run it on deployment. CSS-Tricks has a good article on building an image optimization script. This would be a great combination with Jim Nielsen’s build-your-own-CDN method, linked above.

You won’t have responsive images and width/height attributes on Markdown images, which isn’t great, but it’s also better than serving gigantic images.

I’d use WebP until AVIF support has full browser adoption (probably in about 60 years), and serve a maximum size of 2000x2000 px.

Forms and comments

You want dynamic stuff? On your static site? What?!

Some SSGs can act as hybrids, where they can be mostly static with dynamic elements, like Next.js and Eleventy Serverless. This is also referred to as server-side rendering (SSR). I haven’t worked with this before, so I’m not sure how to handle forms using SSR.

Other SSGs need some extra wiring.

My solution #1: Email

It’s the simplest solution, and it works so well, even if you use a one-stop-shop CMS.

Make a new alias just for handling emails related to the blog, and use it on your site. I recommend routing those emails to a folder in your inbox, in case you get an angry mob or something. Mine goes to “Comments”.

This can act as both your contact form and comments solution, and I really prefer it over commenting systems and forms. You can even add a pre-filled subject line and email body.

There’s 0 spam (thanks CloudFlare) and 0 moderating. If you do need to moderate, just block the sender, right from your inbox. And I love the one-on-one penpal feeling, in a time when casual emails are a rarity.

For your contact page, spell out the full email address instead of hiding it behind a button — sometimes the user can’t use their default mail client.

For your posts, I wrote more about adding a “reply via email” button here for 11ty. On this Hugo site, I use Bryce Wray’s code, and Kev Quirk has a guide for WordPress (had to mention it even though I’m talking about SSGs).

My solution #2: HTML forms and third-party handler

Sometimes there’s no way around it, and you absolutely need a form for your static site. The best way to handle that is to write an HTML form, and then use a third-party service that you can just drop in the action attribute.

I’m still looking for a solution to self-host, maybe using functions on Netlify/Vercel or CloudFlare Workers, but in the meantime, I broke my own rule and use Formcake for a few sites. Netlify Forms is fine too.

I’m okay with using a third-party form handler for HTML forms. If it goes down, I can just update the action attribute to point to a different URL.

Forms are so prone to spam though, even with reCAPTCHA and CloudFlare’s challenge turned on just for the page with the form. Also, did you know reCAPTCHA breaks HTML form validation? Now you know.

I do miss Akismet on WordPress. Caught 99% of spam submissions for me.

Mobile and away-from-home publishing

Not having a CMS is a big disadvantage here. WordPress has a fantastic mobile app, and there are tons of other apps that also post to WordPress or Ghost, including desktop apps like Ulysses and iA Writer. It’s a really solid experience, and there’s nothing like that for static sites.

I usually don’t publish on mobile unless I absolutely have to, but I do edit posts I’m working on from time to time.

My solution for mobile: Working Copy

I use an iPhone, and Working Copy is the best git client I’ve found. The Markdown editor isn’t bad either. You’ll have to manually copy over your new post template though, and if your repo size is large, it could take a while to clone for the first time over a data connection. And you’re back to painfully inserting images by adding them to the folder and then typing out the image names in Markdown.

My solution for desktop: GitHub Codespaces

If your repo is on GitHub, you can use GitHub Codespaces, which is VS Code in your browser. Just navigate to your repo and hit . (period) on your keyboard, and it will fire right up.

If you prefer Vim and have your own git server, you can ssh into your server and use Vim there, though if you have your own git server you probably do that already.

Complicated publishing

This is supposed to be the simplest part, right? Deploy your site to a host that automatically runs your build command and hosts your static files, and you’re done. However, I found myself adding unnecessary steps to my workflow.

I used to use two separate branches in Git: one for writing and testing, dev, and one for production deployment, main.

This is what my publishing workflow looked like:

  1. Checkout branch dev
  2. Start up live reload dev environment (hugo server -DF)
  3. Write the post and probably tweak the design along the way
  4. Commit, usually multiple times
  5. Push to origin
  6. Wait for my site to build on CloudFlare Pages (2-5 minutes)
  7. Check the preview URL to make sure things looked right
  8. Merge dev with main
  9. Commit
  10. Push to origin
  11. Wait for build
  12. Check production site to make sure things looked right

This is a workflow for a large enterprise or software project, not a personal blog.

My solution: Write and deploy on main

To simplify my publishing process, I write on main and design on dev.

My repo is set up on both CloudFlare Pages (primary) and Vercel (backup), and Vercel sends me helpful emails on whether my build succeeded or failed (should set this up for CFP too), so I rarely check the published article now.

And if a build fails, it doesn’t take my site down, it just stays at the previous successful commit. But most of my previous build errors were from messing with the design, and I haven’t seen a build error with my current method.

It does help to use a Markdown editor with live preview and spellcheck.

This is what my new publishing workflow looks like:

  1. Checkout branch main
  2. Write the post
  3. Commit
  4. Push to origin

Much less complicated.

And if you’re worried about spelling or formatting mistakes, don’t — it’s a personal blog post, not a press release. Mistakes make it seem more authentic, or so I tell myself.

Reduce your barriers to writing

This is a long article to say just this, but it can’t be overstated. This concept can be applied to any form of creation, not just writing.

The more complexity you face while creating new content, the less you’ll want to create.

If using a one-stop-shop CMS helps you write and publish more often, don’t waste time using a static site generator. Likewise, if your CMS is complicated, slow, or if you have unstable Internet, an SSG might be better suited.

  1. Currently I’m unable to do so on macOS and I’m not sure why, it’s possible my Spotlight index is broken. It works in my documents but not in my repo. ↩︎

  2. Learning Gulp is something I should probably do at some point, I’m sure I’ll run across it on another project. ↩︎