Tagdecisions6

Content

Why the engine is shaped the way it is -- the trade-offs behind what Architecture describes. Format: what was decided, why, and what it costs.

Typed JSON blocks, parsed once -- not markdown rendered at build time. Markdown is parsed exactly once, when the author saves; the build only ever assembles already-structured blocks, and every importer targets the same schema, so imported and hand-written posts are indistinguishable downstream. Cost: editing needs the inverse renderer (markdown_writer.rb), and content markdown can't express is guarded by a loss check instead of just surviving a round-trip.

Inline formatting as offsets into plain text, not nested HTML. The NPF-style shape Tumblr's API already uses -- imports keep their formatting without HTML parsing, stored content contains no markup to sanitize, and escaping stays entirely the renderer's job. Cost: overlapping-span rendering is genuinely fiddly (see apply_formatting and render_markdown_range).

Read more

Publishing and comments

Comments are replies to an announcement post -- on exactly one network. No comment database, no moderation queue, no GDPR surface, no spam plugin -- replies live where people already are (Mastodon or Bluesky, whose thread APIs a visitor's browser can read directly -- when the server allows it. GoToSocial does not: reading a thread there needs a token on every request, with nothing to configure, and a Mastodon in secure mode behaves the same. On such a server the live mode cannot work at all, and comments.approval: fav -- where a cron reads the thread WITH a token and only starred replies reach the page -- is the whole of what is available. doctor --online asks the question and says which one you have). The two networks are deliberately exclusive: configuring both would split every post's discussion into two half-threads, so the build refuses it. Cost: comments require a presence on the chosen network, and deleting the announcement deletes the discussion (which unpublish does deliberately, to never leave an announcement pointing at a dead URL).

Read more

Build and deploy

Static output, rebuilt incrementally -- only changed bytes are ever written or uploaded. emit compares before writing, the deploy manifest diffs by hash (with a size+mtime fast path), and RSS avoids embedding "now". A one-character edit deploys a handful of files, which also keeps cloud-synced and content-hashed targets calm.

Since 1.6 there is a layer above that: the build records what went into each page and does not render the ones whose inputs have not moved, because comparing bytes still costs rendering them first. The record is never an authority -- anything it cannot vouch for is rendered and compared as before -- and it is thrown away whole whenever a template, a locale, the configuration or the engine changes, since any of those can reach every page.

Read more

Dependencies and security

Ruby stdlib only -- no gems, no Bundler; external binaries are fine. git clone + system Ruby is the entire installation, and nothing can bit-rot in a dependency tree. Where stdlib can't reach (rsync, git, rclone, sftp, $EDITOR), the engine shells out to system binaries the user already understands. Cost: some things are hand-rolled that a gem would provide -- multipart uploads, JPEG/MP4 header parsing, YAML-adjacent frontmatter, and a static file server for ./blog.sh preview (lib/preview_server.rb, plain TCPServer) instead of the webrick-dependent ruby -run -e httpd one-liner.

Read more

Terminal UI

Interactive niceties, but the plain path stays authoritative. Arrow menus, single-key answers and colors appear only on a real TTY; everything the CLI does must still work identically when piped, and no escape code may ever reach a log. Cost: two code paths in the dialogs -- worth it, since cron and scripts drive the same commands humans do.

No curses, no dependencies. io/console plus VT100 sequences cover what this needs. Cost: no complex layouts -- deliberately not the goal.

Read more

Configuration

config/site.yml (identity, versioned) vs env.sh (secrets, mode 600, never in git). Split by sensitivity, not by topic: the site config is meant to sync across environments so local and production render identically; tokens are meant to exist only where they're used.

Config written by the engine is edited as text, never dumped from a parsed hash. Loading config/site.yml, changing a key and writing it back is one line of Ruby and would have destroyed the thing that makes the file usable: most of its lines are comments and commented-out blocks, only a fraction are keys. The rest is the documentation for every setting the engine has, plus the commented-out blocks you uncomment when you want a widget or a custom font -- and real sites hand-edit around it (sean.cz keeps an <img> inside about.html's folded scalar). So lib/config_writer.rb substitutes values line by line and leaves every other byte identical, and a config that doesn't exist yet is seeded from the example verbatim so every key it will ever set is already present to be substituted into. Cost: a text editor for a structured format, which is only safe because it is anchored (key name plus the exact indentation its parent implies, searched only inside that parent) and verified afterwards by reparsing the file and comparing the values back. Prose comments in the template parse as keys otherwise -- # Optional: posts per listing page would index as Optional:.

Read more