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).
A post's type is derived from its blocks; media claim it only caption-deep. The /type/ listings need every post filed exactly once, and asking the author to pick a type on every post would be one more prompt that's usually inferable -- so the type is derived, with an explicit type: in the frontmatter as the override for the post that disagrees. The rules, measured against a real archive before choosing: media (video > audio > image) win only while the post's text stays under 500 characters -- past that it's an article and the media are illustrations; a quote post is one that opens with a quote, so a quote cited mid-text doesn't reclassify an essay; any chat block makes a chat post, since a transcript is always a deliberate choice. The rejected alternatives misfiled real posts: first-block-wins turns a photo tweet into text (the text arrives first), a block-count majority turns a greeting card with two caption sentences into text. Cost: any heuristic misfiles some edge case -- that's what the explicit override is for.
The nav lists what a reader browses; the feed isn't that. RSS left the menu: subscribers find the feed through the autodiscovery <link> every page carries (and the URL never changed), while a nav slot is paid for by every visitor on every page. With quote and chat arriving, seven items was the budget.
The bar sizes itself instead of being sized for a label set. That "budget" was a count, and a count is the wrong unit: the document type made it nine items, and a site that uses all of them ran its longest label under the search field -- measured, one locale overflowed at the full width and the other two had almost no slack, and anywhere between the mobile breakpoint and the full width every locale did. Three rules replace the budget: the menu may wrap to a second row, the search box never shrinks or is overlapped, and the gap between items is tighter -- across eight gaps that recovers more than the whole overflow without shortening a single label. A tenth type, a longer translation or a different font now costs a taller bar rather than a hidden item. Cost: a denser menu, and a two-row bar in the narrow band where the longest locale still doesn't fit on one.
The menu lists only types the site actually has. Nav items, /type/ listings and their sitemap entries exist only for content types with at least one published post -- a young site's menu grows with its content instead of offering six links into empty listings, and a type the engine gains later stays invisible on every site until its first such post. A type emptied by unpublishing disappears again on the next build; the deploy prunes its listing pages as orphans. Cost: an external link to a type's listing dies if the type empties -- it pointed at an empty page anyway.
The bar is a filter, so the first item says All and the current one shows. Every item but the first narrows the listing to one content type; the first clears the filter, which is what "All" names and what "Home" did not -- in a row of filters it read as a destination that had wandered in. The item matching the page carries aria-current="page" and is filled with the accent colour, so a reader can see which listing they are in without reading the heading. Only listings light up: a post deliberately doesn't highlight its own type, since the menu filters listings and a video post is not the video listing. The banner still links home, as it always did. Cost: the label lives on every page, so changing it rewrites the whole archive -- which is why the rename happened before 1.0 rather than after, along with its locale key (nav.home -> nav.all), which would otherwise have broken every translation written in the meantime.
Media always lives next to its post; nothing is hotlinked. Imported posts survive their source platform dying (the Twitter/Tumblr archives this engine was born from are full of dead CDN links). Cost: disk space, and migrations must download everything up front.
Every card in a listing says how long its post takes to read, including the ones that take under a minute. Stars, boosts and comments come from an announcement, which on a real archive almost no post has -- so a meta row fed by those appeared on a handful of cards and read as a break in the listing, while a reading time is the one thing every post can report, which is what keeps the row (and the column of cards) constant. Cost: on an archive of short posts most cards carry the same three words -- a column that keeps its shape was judged worth more than a field that earns its place on every row.
