Pablo Santalla

Website publication workflow

The content of my site (/content) is a set of markdown files (with accompanying media) that Eleventy compiles to HTML.

/content lives in a repository, cloned on my local machine (a MacBook).

Repository cloned on the MacBook

From that local copy on my MacBook, /content is synced via Syncthing with a folder on a hard drive connected to a Raspberry Pi.

Syncthing between MacBook and Raspberry Pi

The Pi doesn't only sync with my MacBook — it also syncs with my phone. Syncthing lets you re-distribute the same content to more devices from a single sync node.

Syncthing also syncing with the phone

Here's what we've got so far:

  1. A GitHub repository holds the site, including the /content folder.
  2. The full repository is cloned on my MacBook.
  3. The MacBook syncs the files inside /content with a drive attached to a Raspberry Pi.
  4. The Pi also syncs that content with my phone.

I also keep a vault of notes and files synced between my phone and my MacBook. That vault contains the website's content files too.

Vault and content overlap

So the MacBook intentionally holds replicas of a few things:

These replicas live inside Syncthing's sync flow on purpose, which avoids conflicts that would arise from any process outside that flow touching the same files.

Automatic publishing

Obsidian is convenient for editing markdown. For publishing, I'd rather lean on an independent Git system.

There are Obsidian plugins that would resolve a commit from inside the app, but the mobile integration has been reported as unstable, and I don't want the publishing path to depend on it.

The Raspberry Pi is always on. It's the stable node of the system.

/content lives in the repo next to project scaffolding: Nunjucks templates, Eleventy config, build data. The Pi doesn't need any of that — it's a content node, not a code node.

The Syncthing folder shared between the MacBook and the Pi is /content only, not the whole repository. Everything else (.git, Eleventy config, dependencies) never touches Syncthing.

Syncthing is the transport. Git stays the source of truth.

Inside /content, a small ignore list filters leftover OS files (.DS_Store) and generated assets the build regenerates (.njk, .js, /feed, .virtual). What flows is content only: markdown, images, video.

The repository is cloned on the Pi with sparse-checkout. Only /content and the repo's root files (README.md, package.json, the Eleventy config, etc.) end up on disk. The other directories (templates, includes, build assets, dependencies) stay out.

A cron job runs every five minutes, calling a small script:

*/5 * * * * ~/.local/bin/sync-content.sh

The script pulls with rebase and autostash first, so anything I push directly from the MacBook or from GitHub web comes down before new local changes get committed. Then it stages everything under /content, commits if there's a diff, and pushes.

If the push is rejected because the remote moved in between, the script pulls again and retries once. If Syncthing left a conflict file behind, the script skips it — those mean two devices edited the same file at the same time and I want to resolve them by hand.

A flock keeps two runs from overlapping. If the repo is mid-rebase or mid-merge for any reason, the script aborts cleanly instead of trying to fix it.

cron because it needs no dependencies, starts with the system, and requires no maintenance. No daemons, no extra processes.

The push triggers GitHub Actions. Eleventy compiles. The site updates.

The script writes a log on the Pi, rotated weekly. If something breaks (a real merge conflict, a push that can't recover, sync-conflict files waiting to be resolved), it sends me an email with the short reason and the relevant log lines. It sends another one when things recover. So I don't need to watch the Pi; the Pi tells me when I need to look.

If the markdown I publish has a real error (broken frontmatter, an image path that doesn't exist), GitHub Actions fails the build and keeps the previous version live. GitHub emails me about the failed build. I fix the file, save it, and the next Pi run picks it up.