Getting Started

Install#

The current release is 0.9.2.

macOS

brew tap gdonald/blogin
brew trust gdonald/blogin
brew install blogin

Homebrew refuses to load a formula from a tap outside its own until you trust it. That is once per machine, not once per install or upgrade, and it applies to every third-party tap rather than to this one in particular. Only homebrew/core and homebrew/cask are trusted without asking.

Blogin can join homebrew/core, which would drop both the tap and the trust step, once it meets Homebrew's notability bar: 225 stars, 90 forks, or 90 watchers when the author submits it. If you find Blogin useful, a star on github.com/gdonald/Blogin counts toward that.

Linux, and Windows through WSL2

curl -LO https://github.com/gdonald/Blogin/releases/latest/download/blogin-linux-x86_64
chmod +x blogin-linux-x86_64
sudo mv blogin-linux-x86_64 /usr/local/bin/blogin

Swap x86_64 for arm64 on an ARM machine. See Other ways to install to check the download against its checksum, for the universal macOS binary, and for building from source.

Confirm it runs. With no command it prints its usage:

blogin

Your first site#

blogin init myblog        # write a site that already builds
cd myblog
blogin new "Hello World"  # a dated post with its front matter filled in
blogin serve              # http://127.0.0.1:3000, rebuilding as you edit

Open http://127.0.0.1:3000 and edit content/posts/. The page reloads itself as you save. When you are ready to publish:

blogin build

The finished site lands in public/, ready for any static host. See Deploying.

blogin init takes --framework bootstrap5 to scaffold against Bootstrap 5 rather than plain semantic HTML. See CSS Frameworks.

Where things go#

Directory What belongs there
content/ Markdown posts. A subdirectory is a section.
layouts/ HAML templates and partials.
assets/css, assets/js, assets/img Stylesheets, scripts, and images, copied to public/assets/. Minifying, fingerprinting, and responsive images apply here.
static/ Copied to the site root untouched, keeping exact names, for favicon.ico, CNAME, .well-known/, and the like.
data/ Optional JSON and YAML a layout reads. See Data files.
shortcodes/ Optional shortcode templates. See Writing Posts.
themes/ Optional layout and asset sets to fall back to. See Themes.
public/ The built site. Named by output-dir.
.blogin-preview/ What blogin serve builds. Never deployed, and safe to ignore in version control.

Blogin's own stylesheet and search assets are written under public/assets/ too, so everything the site serves lives in one place.

Upgrading#

brew upgrade blogin

On Linux, download the new binary over the old one with the same three commands as the install. blogin build reads the previous build's records, so a version change rebuilds what it needs to and nothing more.

Other ways to install#

The macOS binary without Homebrew#

Prefer Homebrew on macOS, because it clears the quarantine attribute Gatekeeper checks. The binary is unsigned, so a browser download is quarantined and macOS refuses to run it. Installing through Homebrew removes that attribute.

blogin-macos-universal carries Apple silicon and Intel in one file. Fetch it with curl, which sets no quarantine attribute:

curl -LO https://github.com/gdonald/Blogin/releases/latest/download/blogin-macos-universal
curl -LO https://github.com/gdonald/Blogin/releases/latest/download/blogin-macos-universal.sha256
shasum -a 256 -c blogin-macos-universal.sha256
chmod +x blogin-macos-universal
sudo mv blogin-macos-universal /usr/local/bin/blogin

If you did download it through a browser, clear the attribute yourself:

xattr -d com.apple.quarantine blogin

Checking a Linux download#

Every binary ships with a .sha256 beside it:

curl -LO https://github.com/gdonald/Blogin/releases/latest/download/blogin-linux-x86_64.sha256
sha256sum -c blogin-linux-x86_64.sha256

Verifying a download came from the release build#

A checksum proves a file arrived intact. It does not prove where it came from, because whoever replaced the file could replace the checksum beside it. The release also carries a build provenance attestation, signed by GitHub with a short-lived certificate issued to the workflow that built it. No long-lived private key is involved.

Verifying needs the GitHub CLI:

gh attestation verify blogin-linux-x86_64 --repo gdonald/Blogin

It reports which workflow built the file, from which commit. A file built anywhere else fails, even with a matching checksum. The macOS binary, both Linux binaries, and SHA256SUMS are all attested.

Building from source#

This works on any platform, and is the only route on anything the release binaries do not cover.

You need a C++23 compiler and CMake 3.28 or newer. Ninja is optional and used when it is present. There is nothing else to install: no runtime, no package manager, and no libraries beyond what the compiler ships.

With clang, on macOS or Linux:

git clone https://github.com/gdonald/Blogin.git
cd Blogin
cmake --preset release
cmake --build build/release -j
sudo cmake --install build/release

With GCC 14 or newer, on Linux:

git clone https://github.com/gdonald/Blogin.git
cd Blogin
cmake --preset gcc-release
cmake --build build/gcc-release -j
sudo cmake --install build/gcc-release

Both install to /usr/local/bin/blogin. Set CMAKE_INSTALL_PREFIX to install somewhere else, or copy the built binary wherever you keep your own.

The presets that do not say gcc select clang, which is what the release binaries are built with. Both compilers build and run the whole test suite on every push.

Platforms#

Platform Install Notes
macOS 13.3 and newer Homebrew, or the universal binary Apple silicon and Intel in one file.
Linux x86_64 and arm64 The static binary Any distribution. No libc, no loader, no glibc version floor.
Windows WSL2, then follow Linux No native build.

The Linux binaries are statically linked, so one file per architecture covers Debian, Ubuntu, Fedora, Arch, Alpine, and anything else. The release build checks this by running the binary in a container holding a shell and no libraries at all, so a build that picked up a dependency fails rather than ships.

There is no .deb or .rpm, and none is planned. A package would carry the same single file the download does, with a version floor the static binary does not have.

Homebrew runs on Linux, but it wants glibc and gcc and installs its own when the system's are too old. That is a lot of machinery to deliver one file that needs nothing, so the download is the shorter path even where brew is already there.

Windows has no native build, since the file watcher behind blogin serve is written against FSEvents and inotify and the preview server uses POSIX sockets. WSL2 is a Linux kernel, so the Linux binary works there with live reload and everything else. WSL1 is not enough, because its inotify support is incomplete.

When something goes wrong#

cannot watch N directories: the per-user limit was reached

blogin serve on Linux watches one inotify handle per directory, and a large site can pass the per-user cap. Raise it:

sudo sysctl fs.inotify.max_user_watches=524288

Add fs.inotify.max_user_watches=524288 to /etc/sysctl.conf, or a file under /etc/sysctl.d/, to keep it across reboots. blogin build never watches anything, so it is unaffected.

two posts write the same page '/posts/hello-world/'

Two posts in one section produced the same slug, so one would overwrite the other. The message names both files. Give one of them a slug in its front matter, or retitle it. See Writing Posts.

no content directory at 'content'. Is this a Blogin site?

blogin build and blogin serve are run from the site root, beside blogin.json. Pass --src if your content lives somewhere else.

port 3000 is already in use. Pass --port to choose another

Another process holds the port, often a blogin serve you left running. blogin serve --port 4000 picks a different one.

post already exists: ... (pass --force to overwrite it)

blogin new refuses to write over a post. Pick a different title, or pass --force when you meant to replace it.

unparseable date 'soon' in 'content/posts/x.md'

A date in front matter is YYYY-MM-DD. A date that does not exist, such as 2026-02-30, is refused the same way.

missing title in 'content/posts/x.md'

title is the only required front matter key.

blogin: command not found

The binary is not on your PATH. Check where it landed with ls -l /usr/local/bin/blogin, and confirm /usr/local/bin is on your PATH with echo $PATH.

Responsive images are not being written

image-widths needs an image resizer on the build host: ImageMagick, or sips on macOS. Without one the build says so and carries on with the images unchanged. brew install imagemagick, or apt install imagemagick.

Extensionless URLs give a 404 once deployed

clean-urls writes posts/hello.html and links it as /posts/hello, which the host has to rewrite. See Deploying for the nginx and Apache configuration, or leave clean-urls off and every host serves the site with no setup at all.