Build Format

KINDAback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A portfolio publisher whose hard problems are processing large photographs once rather than on every build, publishing without ever leaving a half-updated site in front of visitors, and pointing a bare domain at it when the domain standard will not let you use an alias record there.

estimated effort weekendthe files for this build are in the project pack

ContentMarkdown with frontmatter plus a fixed vocabulary of section types, in a git repositoryImagesA derivative cache keyed by the original's content hash and the target widthBuildA deterministic static build into a fresh output directoryPublishUpload to a new remote directory, then switch the served path in one operationHostingAny static host or object store you control, with your own domain

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the build, the preview and the publish.

    Get it nodejs.org, or your version manager. open ↗

    Verify node --version prints v22 or higher

  2. installfree

    Why Every derivative goes through it, and its default limit and its orientation handling decide two of the traps in this build.

    Get it Install one for your runtime and read its constructor options before writing the cache.

    Verify A large panorama either processes or is refused with the limit named in the error

  3. have readya few pounds a month, often less

    Why The whole publish design depends on being able to upload to one prefix and serve from another, so check yours can do it before building around it.

    Get it Any object store with a serving layer, or a host that serves a directory you choose.

    Verify Two prefixes exist and switching which one is served takes one operation

  4. have readyroughly ten to fifteen pounds a year

    Why The bare domain is the part that trips people, and you cannot test the record shape without the domain in your hands.

    Get it Any registrar; you need the ability to create address records or your provider's flattened alias type.

    Verify The bare domain and the www name both resolve to your host and serve over a valid certificate

  5. decidefree

    Why This is the one decision that shapes every template, every schema and every later feature, and widening it later is cheap while narrowing it is not.

    Get it Write down the kinds and what each one holds before building any of them.

    Verify docs/sections.md lists each kind with its fields and the build validates against it

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- pages(slug, title, template, position, draft, updated_at) · slug is the URL and changing it creates a redirect, never a broken link
- sections(id, page_slug, kind, position, data_json) · kind is one of a closed list: gallery, text, image, embed, contact
- assets(id, path, sha256, width, height, taken_at, orientation, caption, alt) · the original is never modified
- derivatives(asset_id, width, format, path, bytes, built_at) · keyed on the original's hash, so an unchanged photograph is never re-encoded
- builds(id, started_at, finished_at, output_dir, asset_count, bytes, ok, error)
- publishes(id, build_id, remote_prefix, switched_at, previous_prefix) · the previous prefix is kept so a rollback is one switch
- redirects(from_path, to_path, created_at) · written whenever a slug changes, because somebody has linked to the old one

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
CONTENT_DIRrequired./contentPages and section files. This is the part you back up.
ORIGINALS_DIRrequired./originalsFull size photographs. Never modified by the build.
DERIVATIVE_CACHErequired./.cache/derivativesKeyed by original hash and width. Safe to delete at the cost of one slow build.
MAX_INPUT_PIXELSrequired268402689The decoder's input limit. Raise it deliberately for panoramas and record why.
OUTPUT_SIZE_CEILING_MBrequired2000A publish above this stops and lists the largest contributors.
REMOTE_BUCKETrequiredyour-portfolio-bucketWhere builds are uploaded, each to its own prefix.
REMOTE_KEYsecretrequiredAKIA...redactedCredentials for the object store. Scope them to this one bucket.
SITE_DOMAINrequiredyourname.exampleUsed for absolute URLs, the link check and the domain record check.

The build, in order

  1. A closed vocabulary of sections

    Pages are files made of a small fixed set of section types, and no section can contain arbitrary markup.

    1. The vocabulary is the product decision; the templates are details.

      Files docs/sections.mdsrc/content/schema.ts

    2. A page that renders wrong is worse than a build that stops.

      Files src/content/validate.tstests/schema.test.ts

    3. Frontmatter and markdown outlive whatever you build here.

      Files content/index.md

    done when · tick each as it passes
    watch out
    • Adding a raw HTML section because one page needs something unusual turns every later feature into a special case, and the responsive and image work you do afterwards silently does not apply inside it. Keep the list closed, add a kind when you genuinely need one, and take the pain of a schema rather than the pain of a page nobody can restyle.
  2. Process each photograph exactly once

    Image derivatives are cached by content hash and target width, and an unchanged photograph is never re-encoded.

    1. The cache key is the whole performance story here.

      Files src/images/cache.tstests/derivative-cache.test.ts

    2. Location data in a public portfolio is a leak, not a feature.

      Files src/images/process.ts

    3. Image libraries cap input pixels by default, and a stitched panorama will hit it; decide the number and record it rather than discovering it at publish time.

      Files src/images/process.tsdocs/limits.md

    done when · tick each as it passes
    watch out
    • Regenerating every size on every build makes the edit loop so slow that you stop previewing, and stripping the orientation flag without rotating the pixels leaves a third of the phone photographs on their side. Key derivatives on the original's hash so unchanged files are skipped, and apply orientation during processing rather than hoping the browser does it.
  3. A build you can trust and a preview you will use

    The build is deterministic, fails loudly, and a local preview shows exactly what will be published.

    1. Leftovers from a previous build are how deleted pages stay online.

      Files src/build/run.ts

    2. One code path means the preview can be believed.

      Files src/preview/server.ts

      terminal
      npm run build && npm run preview
    3. A portfolio with a broken image is worse than no portfolio.

      Files src/build/links.tstests/links.test.ts

    done when · tick each as it passes
    watch out
    • A development server that renders pages through a different code path from the build means the preview is a rehearsal for a different show, and the first time you find out is when the live site looks wrong. Preview the built output, even if that costs you a rebuild on every change.
  4. Publishing to your own domain without a gap

    A publish is one switch after a complete upload, and the bare domain points at the site correctly.

    1. The switch is the only moment visitors see a change.

      Files src/publish/upload.tstests/interrupted-publish.test.ts

    2. A rollback nobody has run is a plan, not a rollback.

      Files src/publish/rollback.ts

      terminal
      npm run rollback
    3. This is where a weekend gets lost, and it is documentation, not code.

      Files docs/domain.md

    done when · tick each as it passes
    watch out
    • A bare domain cannot be pointed at a host with an alias record: the domain standard says that if a canonical name record is present at a node no other data should be present, and the bare domain always carries its name server records at that same node. So either use address records and accept that you must update them when the host changes address, or use whichever flattened alias record type your DNS provider offers, and write down which you chose and why.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more website builders.