M31V

A file-based publishing system that replaced fragile manual releases with repeatable builds, atomic deploys, and secure delivery.

Personal project

Project brief

Period
2015
Status
Archived
Focus
Repeatable publishing
Constraint
Preview and publication had to describe the exact same artifact.
Result
Validated source produced atomic, reproducible releases.

M31V was a 2015 personal publishing project built for a small volunteer magazine I invented as a realistic constraint. I had already used server-rendered templates and simple release scripts in C62Y, but those tools assumed one developer could remember how the site fit together. M31V asked what would happen when several writers needed previews, scheduled publication, durable history, and dependable releases without learning the internals of the build.

When publishing became a systems problem

The starting workflow was deliberately ordinary for the period: document attachments passed around by email, manually copied metadata, templates edited beside content, and FTP uploads into the live directory. A partial upload could leave old and new files mixed together. An image could be referenced locally and omitted from the server. Reconstructing a previous publication state meant searching laptops and mail threads.

I could have installed a large database-backed CMS and declared the workflow solved. Instead, I wanted to test whether a small file-based system could provide the important guarantees without accumulating an administration surface larger than the publication.

M31V made the release itself part of the product. A reader-facing page, an editorial preview, a source commit, an asset manifest, and the files active on the server all needed to refer to the same version.

The project used a compact contract:

  • Every public page could be reconstructed from versioned source.
  • A preview used the same compiler and templates as publication.
  • Invalid metadata, links, or assets stopped activation before affecting the live site.
  • A release became visible atomically rather than file by file.
  • The previous known-good release remained available for rollback.
  • Feed, sitemap, archive, and social metadata came from the same content source as the page.
  • Transport security renewal and failure were observable, not annual calendar chores.

The contract did not promise real-time collaborative editing or arbitrary page building. Those features were attractive and not central to the problem I wanted to solve.

The project was harder than X8B6 in a different direction. X8B6 protected one device’s unfinished intent. M31V coordinated multiple artifacts and human review around one reproducible public state.

Articles moved into Markdown with a small metadata header. I kept the schema narrow: title, description, publication date, status, author display name, tags, canonical path, and optional social image.

The compiler rejected unknown required-field shapes instead of guessing. Dates used one format and included an explicit timezone policy. Paths were normalized and checked for duplicates. Tags came from a maintained vocabulary so a capitalization difference could not silently create a new archive.

The body remained Markdown because the publication mostly needed headings, paragraphs, lists, links, quotations, images, and code. A few controlled extensions handled figures and callouts. Arbitrary template code inside an article was forbidden; content should not acquire the power to alter the whole build.

Server-rendered Handlebars templates owned the document structure. Helpers were pure and small. A helper could format a date or generate a URL, but it could not fetch data or mutate shared state. Keeping the render deterministic made preview and production meaningfully comparable.

Images lived beside content with a manifest entry for alt text, caption, credit, and intrinsic size. The build generated derivatives and warned when the source was too small for its requested placement. A missing alt value was an error for informative images, while an explicitly decorative image used an empty alternative rather than a fabricated description.

Validation as editorial feedback

The first validator printed stack traces and template paths. It technically identified defects and made writers depend on me to translate them. I rebuilt errors around the content file, field, and action:

articles/repair-culture.md
  socialImage: images/repair-culture-wide.jpg
  The file is missing. Add it, or remove socialImage to use the default.

Validation covered:

  • Required and unknown metadata fields.
  • Duplicate and reserved paths.
  • Broken internal links and missing fragments.
  • Referenced images, dimensions, and alternative text.
  • Invalid future dates and conflicting draft states.
  • Feed descriptions that exceeded their useful length.
  • Template output with missing page titles or canonical URLs.

I initially treated every concern as fatal. Writers learned that a build could fail over an optional caption style and began ignoring the whole report. I separated errors that made publication unsafe from warnings that deserved review. Warnings appeared in preview and release notes without blocking activation.

The distinction became an early governance lesson. A rule earns enforcement only when the system can explain why the outcome must stop. Everything else needs an owner and a review path, not a red label by default.

One compiler, several outputs

The same normalized content graph produced HTML pages, tag and date archives, a syndication feed, sitemap entries, search metadata, and social-card metadata. Each output referenced the same canonical path and build identity.

This removed a collection of subtle inconsistencies. Before M31V, changing a title in the page did not guarantee it changed in the feed. A moved article could disappear from one archive while an old hand-written sitemap kept its prior path.

I did not create a universal content abstraction capable of expressing any publication. The graph represented this project’s article and page types. That constraint made validation useful because the system could know what a valid M31V publication meant.

Build order was explicit. Content parsed before templates rendered. Asset derivatives completed before HTML referenced their fingerprints. Cross-document link checking ran after every route was known. The feed and sitemap ran after canonical output existed. A failure in any required phase prevented a release artifact from being marked complete.

Every build emitted a manifest containing the source commit, compiler version, dependency lock digest, generated routes, asset fingerprints, and build time. The manifest became the bridge between preview, deployment, and diagnosis.

Preview was a proposed release

The earliest preview rendered unsaved Markdown in a separate editor template. It was quick and deceptive. Differences in helpers, navigation, styles, and asset paths meant approval did not apply to what would actually be published.

M31V previewed a proposed source commit through the complete build pipeline. The preview URL displayed its commit and build identity in a quiet banner. Scheduled and draft content could appear there without entering public indexes.

The small web editor handled common article creation and metadata changes. Accepted edits became versioned commits with the writer’s display identity in structured metadata. Git remained an implementation detail, but the system did not recreate a second mutable database beside it.

A preview report summarized new, changed, moved, and removed routes. Redirect proposals appeared beside moved articles. Warnings were grouped by content file. Reviewers could move from the report into the exact generated page.

Publishing meant selecting a successful preview build. The deployment did not rebuild from a potentially changed dependency state. It promoted the exact immutable artifact that had been inspected.

That decision later influenced R7K1. A review URL is valuable only if the thing reviewed has a stable identity and can be connected to the thing activated.

Immutable releases instead of careful uploads

Every successful build produced a complete release directory named by its build identity. Fingerprinted assets allowed long cache lifetimes because changed content received a new URL.

Deployment followed a small state machine:

  1. Build

    Create one immutable release

    Compile pages and assets from a named source revision, then preserve the manifest and build identity together.

  2. Verify

    Upload away from live traffic

    Request the candidate through an internal route and check pages, assets, feeds, and canonical metadata before activation.

  3. Activate

    Move one pointer

    Switch the live route atomically while retaining the previous verified release as the rollback target.

The server received the complete release under a new directory. Smoke checks requested the release through a temporary internal route and verified important pages, assets, feed validity, and canonical metadata. Only then did one symlink change atomically to make the release current.

The previous active release stayed intact. Rollback changed the symlink to a known-good target and ran the same smoke checks. Cleanup retained a small history and refused to remove the active or rollback candidate.

The first script had combined build, upload, activation, and cleanup into one imperative sequence. A network interruption left an ambiguous mixture and cleanup could remove evidence needed to understand it. Separating states made retry safe: an already uploaded immutable release could be checked again; an already active release did not need another copy.

The pattern was modest, but it changed my understanding of deployment. A release was not an upload activity. It was a named artifact moving through verified states.

Moving articles introduced another form of state. A redirect was not a temporary line in a server configuration I could forget. It represented a maintained relationship between an old public identifier and a new one.

The build kept a redirect map in source. It rejected loops, chains longer than one hop, conflicts with active routes, and destinations that did not exist. The preview report showed the redirect beside the moved article.

When a route disappeared without a redirect, the build asked for an explicit choice: restore it, redirect it, or mark it intentionally gone. This prevented accidental link loss while allowing deletion when it was the honest outcome.

Canonical URLs, feed entries, internal links, and the sitemap used the final route. Old routes were tested after activation. The project taught me that URL design has an operational lifecycle; publication history continues to matter after the current navigation changes.

HTTPS beyond obtaining a certificate

Let’s Encrypt entered public beta in December 2015, making automated certificates plausible at exactly the period in which M31V’s final release work took shape. Certificate issuance was the smallest part of the migration.

I centralized the canonical origin, found absolute internal HTTP URLs in content, classified insecure third-party assets, updated feeds and social metadata, tightened administrative cookies, and tested old hostnames and callbacks. HTTP-to-HTTPS redirects expanded in stages.

An early universal redirect broke a test publishing callback that still reached an uncovered hostname. The defect was synthetic and instructive. A broad security rule can be correct in intent and unsafe in rollout when dependencies have not been inventoried.

Renewal ran automatically and reported the remaining certificate lifetime. A scheduled dry run verified the challenge path. Failure alerts fired well before expiry and included the specific hostname and validation result.

Strict transport policy waited until subdomains and recovery implications were understood. I resisted adding a strong header merely to receive a better scanner score. A browser remembering HTTPS is useful only when every affected host can uphold the promise.

Transport security did not replace request authenticity. Publishing webhooks still required signed payloads and replay protection. HTTPS protected the channel; it did not establish which application had created a message.

Fingerprinted assets received long immutable cache headers. HTML remained short-lived and revalidated so a new active release became visible without asking readers to clear caches. Feeds used conservative freshness because subscribers varied widely in behavior.

The smoke checks requested both a fresh URL and a previously cached asset path. A new release could reference only assets present in its own manifest, while an older HTML document still in a cache needed its fingerprinted asset to remain available. Cleanup therefore retained assets with the releases that referenced them.

I added the active build identity to response headers. When a reader reported a strange page, I could distinguish an old cached document, a current release, and a preview without changing the visible design.

One test revealed that the service worker experiment I had added for enthusiasm cached HTML too aggressively and kept serving an old article after activation. The publication did not need offline mutation, and its normal HTTP cache strategy was already clear. I removed the service worker. X8B6 had taught me that offline behavior needs a precise contract; M31V did not have one worth the additional state.

Removing that feature was an important kind of growth. A technology appropriate to one project does not become a default requirement for the next.

Editorial recovery

Versioned source made reverting a text change possible, but editorial recovery needed more than a Git command. The web interface showed prior published versions with a diff and allowed a new correction commit derived from one of them. History remained append-only; correcting a page did not pretend the mistaken release had never existed.

Scheduled posts were evaluated in one declared timezone. The build displayed the exact publication instant in preview. A daylight-saving test prevented a date-only assumption from publishing an article an hour earlier than intended.

If the editor lost connection while drafting, the browser retained a local draft and clearly separated it from a saved proposed commit. Unlike X8B6, M31V did not attempt general offline publishing. It protected text from an accidental refresh and required reconnection before preview or publication.

Backups included source repositories, release manifests, redirect maps, and the small editor-authentication store. I tested restoration into a clean environment and rebuilt an old release from its locked dependencies. A backup was not accepted merely because an archive file existed.

The exercise exposed a limit: some old dependency downloads were not as durable as the source repository. Keeping generated release artifacts for important milestones gave recovery another path and foreshadowed R7K1’s stronger artifact identity.

Rebuilding the same release

The phrase “reproducible build” was initially more confident than the implementation. Templates depended on a package range, image compression could vary between local machines, and the build inserted its current time into generated pages. Rebuilding one commit a month later could produce different bytes and sometimes different markup.

I tightened the claim rather than pretending to have achieved perfect bit-for-bit reproducibility. Dependencies were locked. The build ran with an explicit locale and timezone. Publication dates came from content, and volatile build time moved into the manifest instead of reader-visible HTML. Asset tools were versioned as part of the build recipe.

The manifest separated source identity from artifact identity. A source commit answered what had been requested; a digest of the generated release answered what had actually been reviewed and deployed. Two builds of the same source could be compared route by route and asset by asset. If they differed, the preview interface did not silently treat them as interchangeable.

A rebuild audit periodically selected an earlier source commit, restored its locked environment, and compared the output with the retained artifact. Differences were classified as expected metadata, tool drift, or genuine nondeterminism. The process found a template helper that sorted tags according to the host locale and an image tool that retained varying metadata.

I did not solve the general reproducible-build problem. I made the remaining variability visible and stopped using one identifier for two different things. That distinction became essential in R7K1, where concurrent branch builds could only share work when their declared inputs and resulting artifact were tracked separately.

Small permissions with explicit consequences

The editor used three actions rather than a hierarchy of impressive job titles: propose, approve, and publish. In the tiny test roster, one person could hold more than one capability, but the interface still made the transition explicit.

Proposing created a versioned change and preview. Approval attached a review record to the exact build identity. Publishing promoted that artifact if its approval was still current. A changed source or rebuilt artifact invalidated the prior approval. This prevented a green check beside one preview from floating onto a later build.

Destructive actions required specificity. Removing an article showed the affected route, incoming internal links, redirect status, and feed consequence. Rolling back named the target build and warned that it created a new activation event rather than erasing the current release from history.

The permissions were not presented as a security boundary strong enough for a hostile multi-tenant service. They were workflow guardrails for a small personal system. Authentication, signed sessions, request-forgery protection, and an audit trail still applied. The interface did not use the project’s small size as permission to make consequential actions ambiguous.

This was the first project where I treated approval as data tied to an immutable subject. Later AI-assisted systems would need the same discipline at a much more difficult boundary: a person must approve the actual proposal and intended tool action, not a label that survives after the underlying content changes.

The original editor hid Git by recreating a mutable content database. The build then had two sources of truth and an unreliable export step. Making versioned files authoritative removed the synchronization problem.

The first deployment script treated a sequence of commands as a transaction. It was not one. Immutable directories and an atomic activation point made partial progress observable and recoverable.

The initial validator made every warning fatal. It converted helpful guidance into noise and taught users to route around it. The revised severity model tied blocking rules to publication safety.

The first HTTPS rule was applied before every hostname and callback had been mapped. Staged redirects and active monitoring replaced the one-line migration.

The premature service worker introduced a second publication cache with no user problem strong enough to justify it. Removing it kept the release model explainable.

M31V was the first personal project where writing, build engineering, deployment, security, caching, and recovery formed one product surface. A page was not complete when a template rendered. It was complete when the exact reviewed artifact could be activated, served securely, identified, and rolled back.

Compared with X8B6, the hard state moved from a browser database into a release pipeline. The same habits survived: stable identities, explicit transitions, durable receipts, and recovery after interruption. A build manifest played a role similar to an operation receipt—it connected an intent to a concrete result that could be recognized later.

The system remained appropriately small. Markdown and a focused editor served the chosen publication. Immutable static output made release behavior simple. Features that would have turned M31V into a general CMS stayed outside the boundary.

The next project raised the difficulty again. R7K1 would take the exact-build and preview ideas and apply them to arbitrary branches running unaccepted code. That required scheduling, resource limits, routing, isolation, expiry, and content-addressed reuse. M31V learned to promote one safe artifact. R7K1 would learn to produce many disposable ones concurrently.

What the build could not promise

Markdown was not the right authoring environment for everyone. The web editor remained intentionally narrow. Complex media processing, simultaneous rich-text collaboration, and live personalization were outside scope. Static publication introduced build latency for changes.

The volunteer-magazine scenario and content were invented for the project. The system demonstrated a defensible publishing pipeline, not the organizational complexity of a real newsroom. Its value was the coherence of its guarantees: one versioned source produced one identifiable release, reviewed and activated through one recoverable path.

Technology: Node.js, Markdown, Handlebars, Git-backed content, generated asset derivatives, fingerprinted files, immutable release directories, atomic symlink activation, signed publishing hooks, automated TLS certificates, and restoration tests.