My main blogging setup is with Emacs, Org mode and ox-hugo. I like having the source in one Org file, and I have no plans to give that up for longer posts. But a quick note, link or a photo is a different kind of posting. Over time, I have realized that for quick posts, friction matters a lot more than for longer ones. If I have to open my laptop, find the right heading, export, preview, commit and push, I might never write it down.
I wanted a low-friction path from an idea on my phone to a post in my Hugo-powered website. I found Pages CMS, which edits files directly in my GitHub repository, so it fits the existing Hugo and Netlify pipeline without introducing a new content source. The standard Pages CMS installation was uneventful; the interesting part was making this second authoring path coexist with the first one. In this post I focus on the decisions and the relevant pieces of my configuration.
Joining multiple directories into a single Hugo section#
The Org file content-org/zzamboni.org remains the source for my longer articles; ox-hugo exports those to content/post/. Pages CMS writes its own Markdown files under content-pagescms/. I learned that Hugo module mounts can make multiple directories appear in the same content/post section at build time. I added the following to my module.toml Hugo configuration file:
[[mounts]]
source = "content"
target = "content"
[[mounts]]
source = "content-pagescms"
target = "content/post"This keeps the content ownership clear: If a post came from Org, I edit the Org source, not just its generated Markdown. If it came from Pages CMS, its Markdown file is the source. Hugo and Blowfish see both as ordinary posts.
In .pages.yml, I use format: toml-frontmatter to match the existing posts, and a dated, title-based filename to match the filenames I have been using already for exporting from the org-mode file:
content:
- name: posts
type: collection
path: content-pagescms
format: toml-frontmatter
filename: "{year}-{month}-{day}-{primary}.md"In the .pages.yml field configuration I put the title, summary and body near the top of the editor and leave the less frequently changed metadata below. Posts default to draft, so saving a new post does not immediately publish it.
fields:
- name: title
label: Title
type: string
required: true
- name: summary
label: Summary
type: text
options:
maxlength: 400
- name: body
label: Body
type: rich-text
- name: date
label: Date
type: date
required: true
options:
time: true
format: "yyyy-MM-dd'T'HH:mm:ss.SSS"
- name: tags
label: Tags
type: string
list: true
- name: draft
label: Draft
type: boolean
default: true
- name: featureimage
label: Feature image
type: string
default: "img/tram-zurich.jpg"
hidden: false
- name: toc
label: Table of contents
type: boolean
default: falseTime zone woes#
The least obvious setting is the date format:
- name: date
type: date
required: true
options:
time: true
format: "yyyy-MM-dd'T'HH:mm:ss.SSS"There is deliberately no Z or timezone offset. The timestamp is a Zürich wall-clock time, and Hugo’s timeZone is set to Europe/Zurich. This is to work around a bug in which Pages CMS writes the local clock time with a Z suffix, which makes Hugo interpret it as UTC and sometimes hide a just-created post as a future post. I wrote about the bug and the tradeoff in more detail separately.
This is a pragmatic convention for a personal site with one expected timezone. It would need revisiting if I began editing across timezones or with collaborators, or when the PR that fixes this gets merged into Pages CMS.
Help with summaries, review for images#
If I leave the summary blank on a Pages CMS post, a GitHub Actions workflow (generate-pagescms-summary.yml) generates a short one and commits it back to the file. If the post already contains a summary (e.g. one I wrote when posting it), it is left alone. In addition to getting triggered automatically, I can also trigger it with an action button in Pages CMS:
actions:
- name: generate-summary
label: Generate summary
scope: entry
workflow: generate-pagescms-summary.yml
ref: current
confirm:
title: Generate a missing summary?
message: Uses the saved post or linked article and commits a summary if the summary field is blank. Save your changes first.
button: Generate summaryAnother workflow (generate-feature-image.yml) uses AI to generate an image to be used as the thumbnail of a new blog post (I’m not yet convinced that this is a good idea, the images look too “AI generated” for my taste). These are not generated automatically, only on request using an action button:
actions:
- name: generate-image
label: Generate image
scope: entry
workflow: generate-feature-image.yml
ref: currentAfter I save the post, the button dispatches the workflow on the branch I am editing. The dialog also offers the chance to provide extra artistic direction and a checkbox to replace an existing image. The workflow reads the saved Markdown, creates a visual brief, generates a landscape WebP under assets/img/generated/, and sets featureimage to the corresponding img/generated/...webp path. Keeping the image in Hugo’s assets/ pipeline matters for Blowfish’s resource lookup. For an Org-authored post, the same workflow can be run manually and updates the matching Org metadata as well.
The image workflow opens a pull request instead of merging its result. I can inspect the image and the Netlify deploy preview before accepting it. That little review step is especially useful for images: even an attractive result can be wrong for the post.
The useful boundary#
The two writing paths now have distinct jobs. Org mode is where I develop a deeper article; Pages CMS is where I can capture and edit a short post without turning it into a laptop project. Both still end up as files in Git and pages built by Hugo. The configuration is a collection of small choices, but together they remove just enough friction that a quick idea has a reasonable chance of becoming an actual post.
In future posts I’ll show how I have created link- and photo-specific sections (including custom rendering) of my blog, and matching iOS Shortcuts to easily post them.
