Build Format
KINDAback to the verdict
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.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- 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 - 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 - 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 - 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 - 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.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
CONTENT_DIR | required | ./content | Pages and section files. This is the part you back up. |
ORIGINALS_DIR | required | ./originals | Full size photographs. Never modified by the build. |
DERIVATIVE_CACHE | required | ./.cache/derivatives | Keyed by original hash and width. Safe to delete at the cost of one slow build. |
MAX_INPUT_PIXELS | required | 268402689 | The decoder's input limit. Raise it deliberately for panoramas and record why. |
OUTPUT_SIZE_CEILING_MB | required | 2000 | A publish above this stops and lists the largest contributors. |
REMOTE_BUCKET | required | your-portfolio-bucket | Where builds are uploaded, each to its own prefix. |
REMOTE_KEYsecret | required | AKIA...redacted | Credentials for the object store. Scope them to this one bucket. |
SITE_DOMAIN | required | yourname.example | Used for absolute URLs, the link check and the domain record check. |
The build, in order
A closed vocabulary of sections
Pages are files made of a small fixed set of section types, and no section can contain arbitrary markup.
The vocabulary is the product decision; the templates are details.
Files
docs/sections.mdsrc/content/schema.tsA page that renders wrong is worse than a build that stops.
Files
src/content/validate.tstests/schema.test.tsFrontmatter and markdown outlive whatever you build here.
Files
content/index.md
done when · tick each as it passeswatch 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.
Process each photograph exactly once
Image derivatives are cached by content hash and target width, and an unchanged photograph is never re-encoded.
The cache key is the whole performance story here.
Files
src/images/cache.tstests/derivative-cache.test.tsLocation data in a public portfolio is a leak, not a feature.
Files
src/images/process.tsImage 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 passeswatch 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.
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.
Leftovers from a previous build are how deleted pages stay online.
Files
src/build/run.tsOne code path means the preview can be believed.
Files
src/preview/server.tsterminalnpm run build && npm run preview
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 passeswatch 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.
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.
The switch is the only moment visitors see a change.
Files
src/publish/upload.tstests/interrupted-publish.test.tsA rollback nobody has run is a plan, not a rollback.
Files
src/publish/rollback.tsterminalnpm run rollback
This is where a weekend gets lost, and it is documentation, not code.
Files
docs/domain.md
done when · tick each as it passeswatch 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.
A site that outlives the generatorproduct builder
Renaming, archiving and leaving are all supported, and nothing about the output depends on this tool still existing.
Other people's links are the only traffic a portfolio has.
Files
src/content/redirects.tstests/redirects.test.tsThat is the difference between owning the site and renting it.
Files
src/export/content.tsterminalnpm run export && cd /tmp/export && npm run build
One raw gallery import can multiply the bill without a warning.
Files
src/build/limits.ts
done when · tick each as it passeswatch out- Renaming a page because the title improved breaks every link anyone has ever sent to it, and a portfolio lives on links other people sent. Record a redirect automatically on every slug change, keep them forever, and never make removing one easy.
That is the whole plan for Format. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Client galleries with proofing.
- A print or download store.
- A general purpose visual editor.
- Managed hosting with forms and analytics.
- large template and component libraries
- CMS collaboration and client handoff
- managed hosting, forms, and analytics
- general-purpose visual editing
- A contact form posted to something you already run, rather than a mail server inside a static site
- Per-gallery ordering by capture time with a manual override, which is the edit you will make most often
- A yearly archive build kept offline, so a site you stop paying for still exists somewhere
Need the files? The project pack on the verdict page hands your agent the whole brief · more website builders.