Vibecode Format
track this build4 phases, 12 steps, beginner friendly0%The visible portfolio website builder loop is buildable, but a credible replacement needs more than the first screen. Format earns its keep through editor, CMS, hosting, polish, so expect a weekend or multi-day build and a narrower personal scope.
You are building a lean indie version of Format. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Format · indie build 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: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Content | Markdown with frontmatter plus a fixed vocabulary of section types, in a git repository | a portfolio has about six kinds of block, and inventing a page language is how this project dies | | Images | A derivative cache keyed by the original's content hash and the target width | reprocessing four hundred photographs on every build turns a two minute edit into a coffee break | | Build | A deterministic static build into a fresh output directory | same input, same bytes, or you cannot tell what a deploy changed | | Publish | Upload to a new remote directory, then switch the served path in one operation | visitors must never see a site half replaced | | Hosting | Any static host or object store you control, with your own domain | the point of leaving a builder is that the result is yours | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the build, the preview and the publish. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **An image processing library with a documented input pixel limit** · free - 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 - [ ] **A static host or object store you control, with prefix switching** · a 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 - [ ] **A domain you own, with access to its DNS records** · roughly 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 - [ ] **The list of section kinds** · free - 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 ## Quick start ```sh npm run build && npm run preview ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for Format, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Format The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a portfolio site generator that publishes static files to a domain you own to replace Format. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Content: Markdown with frontmatter plus a fixed vocabulary of section types, in a git repository · a portfolio has about six kinds of block, and inventing a page language is how this project dies - Images: A derivative cache keyed by the original's content hash and the target width · reprocessing four hundred photographs on every build turns a two minute edit into a coffee break - Build: A deterministic static build into a fresh output directory · same input, same bytes, or you cannot tell what a deploy changed - Publish: Upload to a new remote directory, then switch the served path in one operation · visitors must never see a site half replaced - Hosting: Any static host or object store you control, with your own domain · the point of leaving a builder is that the result is yours ### Data model (create this before Phase 1) - 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 ### Phase 1 · A closed vocabulary of sections Build: Define at most six section kinds with a schema each: gallery, text, image, embed and contact are usually enough. Pages are markdown files with frontmatter and an ordered list of sections. Validate every section against its schema on load and refuse to build on a failure, naming the file and the field. Trap: 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. Done when: Every section kind has a schema and a section failing it stops the build with the file name and the field, tested with a broken fixture. No section kind accepts raw markup, verified by review. A page renders identically from its file after a round trip through the editor, tested. Adding a new page is one file and needs no code change. Do not build yet: No image processing, build output, publishing or domains yet. ### Phase 2 · Process each photograph exactly once Build: On import, hash the original, read its dimensions and orientation, and record them. Generate the widths your templates actually request, writing a derivative keyed on hash and width. Apply the orientation so nothing renders sideways, and strip metadata apart from a copyright field you choose to keep. Emit a width set and sizes attribute per gallery. Trap: 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. Done when: A second build with no content change produces no new derivatives and completes in a stated fraction of the first, measured. A photograph with a rotation flag is rendered upright in the output, tested with a fixture of each orientation value. Every gallery image emits a width set and a sizes attribute, and the largest derivative is never wider than the original. Changing one photograph rebuilds only that photograph's derivatives, tested. Do not build yet: No build output, preview, publishing or domains yet. ### Phase 3 · A build you can trust and a preview you will use Build: Build into a fresh directory each time and record the build row. Serve the same output directory locally for preview rather than a separate development renderer. Check internal links and image references during the build and fail on a broken one. Report the output size and the largest files. Trap: 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. Done when: Two builds from unchanged content produce byte-identical output, tested by hashing the directories. A broken internal link or missing image fails the build with the page and the reference named, tested. The preview serves the same directory the publish step uploads, verified by inspecting the code path. The build prints total output size and the ten largest files. Do not build yet: No publishing, domain setup or redirects yet. ### Phase 4 · Publishing to your own domain without a gap Build: Upload the build to a new remote prefix, verify a manifest of files and sizes, then switch the served prefix in one operation and keep the previous one for rollback. Document the domain records your host needs, for both the bare domain and the www name, and check them from the tool before a first publish. Trap: 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. Done when: A publish uploads to a new prefix and only switches after every file in the manifest is verified present and the right size, tested by interrupting an upload. An interrupted publish leaves the live site untouched, tested. A rollback switches back to the previous prefix in one operation and is tested, not documented. The tool checks the domain's records before a first publish and explains, for the bare domain specifically, why an alias record cannot be used there. Do not build yet: No redirects, link checking over time or handover yet. ### Phase 5 · A site that outlives the generator Build: Write a redirect whenever a slug changes and emit them in the form your host understands. Keep an archive of published builds with their manifests. Provide a content export as plain files and images. Add a scheduled external link check that reports rather than edits, and a stated ceiling on total output size so a gallery import cannot quietly triple your hosting bill. Trap: 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. Done when: Changing a page's slug creates a redirect automatically and the old path resolves after publishing, tested end to end. The exported content directory builds on another machine with no database and no cache, tested. External link checking reports broken destinations without modifying content, and runs on a schedule. Exceeding the configured output size ceiling stops the publish with the largest contributors listed. Do not build yet: Nothing after this. ### Out of scope (and why) - Client galleries with proofing and selection, which is a different product with access control, expiry and someone else's deadline attached. - A store selling prints, which brings tax, fulfilment and refunds into what is currently a static site. - A general purpose visual editor, which is the paid product's actual moat and takes far longer than the rest of this put together. - Managed hosting with forms and analytics, since the point of this build is that you hold the hosting relationship and the domain. ### README must contain - That section kinds are a closed list with schemas, and that adding a kind is a code change on purpose - That image derivatives are cached by the original's content hash, so a rebuild without changes does no image work - That the decoder's input pixel limit is a setting with a stated number, and what happens when a very large panorama exceeds it - That publishing uploads to a new prefix and switches once, and how to roll back to the previous one - The exact domain records for the bare domain and the www name, and why an alias record cannot be used at the bare domain ===== AGENTS.md ===== # Agent instructions · Format indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Markdown with frontmatter plus a fixed vocabulary of section types, in a git repository, A derivative cache keyed by the original's content hash and the target width, A deterministic static build into a fresh output directory, Upload to a new remote directory, then switch the served path in one operation, Any static host or object store you control, with your own domain. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · Format 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 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. ### Steps 1. Write the section schemas before the renderer The vocabulary is the product decision; the templates are details. Files: `docs/sections.md`, `src/content/schema.ts` 2. Fail the build on an invalid section A page that renders wrong is worse than a build that stops. Files: `src/content/validate.ts`, `tests/schema.test.ts` 3. Keep pages as files that survive this tool Frontmatter and markdown outlive whatever you build here. Files: `content/index.md` ### Done when - [ ] Every section kind validates against a schema - [ ] An invalid section names the file and field - [ ] No section accepts raw markup - [ ] A new page is one file, no code ### 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. ## Phase 2 · Process each photograph exactly once Image derivatives are cached by content hash and target width, and an unchanged photograph is never re-encoded. ### Steps 1. Hash originals and key every derivative on the hash The cache key is the whole performance story here. Files: `src/images/cache.ts`, `tests/derivative-cache.test.ts` 2. Apply orientation and strip what you did not choose to keep Location data in a public portfolio is a leak, not a feature. Files: `src/images/process.ts` 3. Raise the decoder's pixel limit deliberately, or let it refuse 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.ts`, `docs/limits.md` ### Done when - [ ] A no-change build produces no new derivatives - [ ] Rotated originals render upright - [ ] Galleries emit a width set and sizes - [ ] The decoder pixel limit is an explicit setting ### 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. ## Phase 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. ### Steps 1. Build into a fresh directory every time Leftovers from a previous build are how deleted pages stay online. Files: `src/build/run.ts` 2. Preview the built output, not a second renderer One code path means the preview can be believed. Files: `src/preview/server.ts` ```sh npm run build && npm run preview ``` 3. Check links and image references in the build A portfolio with a broken image is worse than no portfolio. Files: `src/build/links.ts`, `tests/links.test.ts` ### Done when - [ ] Two builds of the same content are byte-identical - [ ] Broken links and missing images fail the build - [ ] Preview serves the published directory - [ ] Build reports size and the largest files ### 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. ## Phase 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. ### Steps 1. Upload to a new prefix, verify, then switch The switch is the only moment visitors see a change. Files: `src/publish/upload.ts`, `tests/interrupted-publish.test.ts` 2. Keep the previous prefix and test the rollback A rollback nobody has run is a plan, not a rollback. Files: `src/publish/rollback.ts` ```sh npm run rollback ``` 3. Write down the domain records, bare and www This is where a weekend gets lost, and it is documentation, not code. Files: `docs/domain.md` ### Done when - [ ] An interrupted upload never reaches the live site - [ ] The switch happens only after manifest verification - [ ] Rollback is exercised, not just written down - [ ] Domain records are checked and explained before publishing ### 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. ## Not in this build - Client galleries with proofing. - A print or download store. - A general purpose visual editor. - Managed hosting with forms and analytics. ## After v1, if you want it - 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 ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Pages and section files. This is the part you back up. CONTENT_DIR=./content # Required. Full size photographs. Never modified by the build. ORIGINALS_DIR=./originals # Required. Keyed by original hash and width. Safe to delete at the cost of one slow build. DERIVATIVE_CACHE=./.cache/derivatives # Required. The decoder's input limit. Raise it deliberately for panoramas and record why. MAX_INPUT_PIXELS=268402689 # Required. A publish above this stops and lists the largest contributors. OUTPUT_SIZE_CEILING_MB=2000 # Required. Where builds are uploaded, each to its own prefix. REMOTE_BUCKET=your-portfolio-bucket # Required · secret. Credentials for the object store. Scope them to this one bucket. REMOTE_KEY=AKIA...redacted # Required. Used for absolute URLs, the link check and the domain record check. SITE_DOMAIN=yourname.example
You are building a lean indie version of Format. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Format · indie build 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: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Content | Markdown with frontmatter plus a fixed vocabulary of section types, in a git repository | a portfolio has about six kinds of block, and inventing a page language is how this project dies | | Images | A derivative cache keyed by the original's content hash and the target width | reprocessing four hundred photographs on every build turns a two minute edit into a coffee break | | Build | A deterministic static build into a fresh output directory | same input, same bytes, or you cannot tell what a deploy changed | | Publish | Upload to a new remote directory, then switch the served path in one operation | visitors must never see a site half replaced | | Hosting | Any static host or object store you control, with your own domain | the point of leaving a builder is that the result is yours | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the build, the preview and the publish. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **An image processing library with a documented input pixel limit** · free - 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 - [ ] **A static host or object store you control, with prefix switching** · a 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 - [ ] **A domain you own, with access to its DNS records** · roughly 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 - [ ] **The list of section kinds** · free - 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 ## Quick start ```sh npm run build && npm run preview ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for Format, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Format The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a portfolio site generator that publishes static files to a domain you own to replace Format. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Content: Markdown with frontmatter plus a fixed vocabulary of section types, in a git repository · a portfolio has about six kinds of block, and inventing a page language is how this project dies - Images: A derivative cache keyed by the original's content hash and the target width · reprocessing four hundred photographs on every build turns a two minute edit into a coffee break - Build: A deterministic static build into a fresh output directory · same input, same bytes, or you cannot tell what a deploy changed - Publish: Upload to a new remote directory, then switch the served path in one operation · visitors must never see a site half replaced - Hosting: Any static host or object store you control, with your own domain · the point of leaving a builder is that the result is yours ### Data model (create this before Phase 1) - 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 ### Phase 1 · A closed vocabulary of sections Build: Define at most six section kinds with a schema each: gallery, text, image, embed and contact are usually enough. Pages are markdown files with frontmatter and an ordered list of sections. Validate every section against its schema on load and refuse to build on a failure, naming the file and the field. Trap: 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. Done when: Every section kind has a schema and a section failing it stops the build with the file name and the field, tested with a broken fixture. No section kind accepts raw markup, verified by review. A page renders identically from its file after a round trip through the editor, tested. Adding a new page is one file and needs no code change. Do not build yet: No image processing, build output, publishing or domains yet. ### Phase 2 · Process each photograph exactly once Build: On import, hash the original, read its dimensions and orientation, and record them. Generate the widths your templates actually request, writing a derivative keyed on hash and width. Apply the orientation so nothing renders sideways, and strip metadata apart from a copyright field you choose to keep. Emit a width set and sizes attribute per gallery. Trap: 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. Done when: A second build with no content change produces no new derivatives and completes in a stated fraction of the first, measured. A photograph with a rotation flag is rendered upright in the output, tested with a fixture of each orientation value. Every gallery image emits a width set and a sizes attribute, and the largest derivative is never wider than the original. Changing one photograph rebuilds only that photograph's derivatives, tested. Do not build yet: No build output, preview, publishing or domains yet. ### Phase 3 · A build you can trust and a preview you will use Build: Build into a fresh directory each time and record the build row. Serve the same output directory locally for preview rather than a separate development renderer. Check internal links and image references during the build and fail on a broken one. Report the output size and the largest files. Trap: 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. Done when: Two builds from unchanged content produce byte-identical output, tested by hashing the directories. A broken internal link or missing image fails the build with the page and the reference named, tested. The preview serves the same directory the publish step uploads, verified by inspecting the code path. The build prints total output size and the ten largest files. Do not build yet: No publishing, domain setup or redirects yet. ### Phase 4 · Publishing to your own domain without a gap Build: Upload the build to a new remote prefix, verify a manifest of files and sizes, then switch the served prefix in one operation and keep the previous one for rollback. Document the domain records your host needs, for both the bare domain and the www name, and check them from the tool before a first publish. Trap: 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. Done when: A publish uploads to a new prefix and only switches after every file in the manifest is verified present and the right size, tested by interrupting an upload. An interrupted publish leaves the live site untouched, tested. A rollback switches back to the previous prefix in one operation and is tested, not documented. The tool checks the domain's records before a first publish and explains, for the bare domain specifically, why an alias record cannot be used there. Do not build yet: No redirects, link checking over time or handover yet. ### Phase 5 · A site that outlives the generator Build: Write a redirect whenever a slug changes and emit them in the form your host understands. Keep an archive of published builds with their manifests. Provide a content export as plain files and images. Add a scheduled external link check that reports rather than edits, and a stated ceiling on total output size so a gallery import cannot quietly triple your hosting bill. Trap: 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. Done when: Changing a page's slug creates a redirect automatically and the old path resolves after publishing, tested end to end. The exported content directory builds on another machine with no database and no cache, tested. External link checking reports broken destinations without modifying content, and runs on a schedule. Exceeding the configured output size ceiling stops the publish with the largest contributors listed. Do not build yet: Nothing after this. ### Out of scope (and why) - Client galleries with proofing and selection, which is a different product with access control, expiry and someone else's deadline attached. - A store selling prints, which brings tax, fulfilment and refunds into what is currently a static site. - A general purpose visual editor, which is the paid product's actual moat and takes far longer than the rest of this put together. - Managed hosting with forms and analytics, since the point of this build is that you hold the hosting relationship and the domain. ### README must contain - That section kinds are a closed list with schemas, and that adding a kind is a code change on purpose - That image derivatives are cached by the original's content hash, so a rebuild without changes does no image work - That the decoder's input pixel limit is a setting with a stated number, and what happens when a very large panorama exceeds it - That publishing uploads to a new prefix and switches once, and how to roll back to the previous one - The exact domain records for the bare domain and the www name, and why an alias record cannot be used at the bare domain ===== AGENTS.md ===== # Agent instructions · Format indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Markdown with frontmatter plus a fixed vocabulary of section types, in a git repository, A derivative cache keyed by the original's content hash and the target width, A deterministic static build into a fresh output directory, Upload to a new remote directory, then switch the served path in one operation, Any static host or object store you control, with your own domain. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · Format 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 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. ### Steps 1. Write the section schemas before the renderer The vocabulary is the product decision; the templates are details. Files: `docs/sections.md`, `src/content/schema.ts` 2. Fail the build on an invalid section A page that renders wrong is worse than a build that stops. Files: `src/content/validate.ts`, `tests/schema.test.ts` 3. Keep pages as files that survive this tool Frontmatter and markdown outlive whatever you build here. Files: `content/index.md` ### Done when - [ ] Every section kind validates against a schema - [ ] An invalid section names the file and field - [ ] No section accepts raw markup - [ ] A new page is one file, no code ### 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. ## Phase 2 · Process each photograph exactly once Image derivatives are cached by content hash and target width, and an unchanged photograph is never re-encoded. ### Steps 1. Hash originals and key every derivative on the hash The cache key is the whole performance story here. Files: `src/images/cache.ts`, `tests/derivative-cache.test.ts` 2. Apply orientation and strip what you did not choose to keep Location data in a public portfolio is a leak, not a feature. Files: `src/images/process.ts` 3. Raise the decoder's pixel limit deliberately, or let it refuse 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.ts`, `docs/limits.md` ### Done when - [ ] A no-change build produces no new derivatives - [ ] Rotated originals render upright - [ ] Galleries emit a width set and sizes - [ ] The decoder pixel limit is an explicit setting ### 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. ## Phase 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. ### Steps 1. Build into a fresh directory every time Leftovers from a previous build are how deleted pages stay online. Files: `src/build/run.ts` 2. Preview the built output, not a second renderer One code path means the preview can be believed. Files: `src/preview/server.ts` ```sh npm run build && npm run preview ``` 3. Check links and image references in the build A portfolio with a broken image is worse than no portfolio. Files: `src/build/links.ts`, `tests/links.test.ts` ### Done when - [ ] Two builds of the same content are byte-identical - [ ] Broken links and missing images fail the build - [ ] Preview serves the published directory - [ ] Build reports size and the largest files ### 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. ## Phase 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. ### Steps 1. Upload to a new prefix, verify, then switch The switch is the only moment visitors see a change. Files: `src/publish/upload.ts`, `tests/interrupted-publish.test.ts` 2. Keep the previous prefix and test the rollback A rollback nobody has run is a plan, not a rollback. Files: `src/publish/rollback.ts` ```sh npm run rollback ``` 3. Write down the domain records, bare and www This is where a weekend gets lost, and it is documentation, not code. Files: `docs/domain.md` ### Done when - [ ] An interrupted upload never reaches the live site - [ ] The switch happens only after manifest verification - [ ] Rollback is exercised, not just written down - [ ] Domain records are checked and explained before publishing ### 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. ## Not in this build - Client galleries with proofing. - A print or download store. - A general purpose visual editor. - Managed hosting with forms and analytics. ## After v1, if you want it - 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 ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Pages and section files. This is the part you back up. CONTENT_DIR=./content # Required. Full size photographs. Never modified by the build. ORIGINALS_DIR=./originals # Required. Keyed by original hash and width. Safe to delete at the cost of one slow build. DERIVATIVE_CACHE=./.cache/derivatives # Required. The decoder's input limit. Raise it deliberately for panoramas and record why. MAX_INPUT_PIXELS=268402689 # Required. A publish above this stops and lists the largest contributors. OUTPUT_SIZE_CEILING_MB=2000 # Required. Where builds are uploaded, each to its own prefix. REMOTE_BUCKET=your-portfolio-bucket # Required · secret. Credentials for the object store. Scope them to this one bucket. REMOTE_KEY=AKIA...redacted # Required. Used for absolute URLs, the link check and the domain record check. SITE_DOMAIN=yourname.example
You are building a production product version of Format. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Format · product brief ## Problem The visible portfolio website builder loop is buildable, but a credible replacement needs more than the first screen. Format earns its keep through editor, CMS, hosting, polish, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A portfolio whose content is files you own, whose images are processed once, whose publishes are atomic with a tested rollback, and whose bare domain is configured in a way that actually works. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node.js 22 - deploy target or static hosting - user-owned domain for publishing ## Explicit non-goals for v1 - 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 ## Success criteria - Two builds of unchanged content are confirmed byte-identical - A no-change build is confirmed to produce no new image derivatives - An interrupted upload is confirmed never to reach the live site - Rollback to the previous prefix is confirmed by running it - A slug change is confirmed to produce a working redirect after publishing ===== BRIEF.md ===== # Build brief · Format The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a portfolio site generator that publishes static files to a domain you own to replace Format. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Content: Markdown with frontmatter plus a fixed vocabulary of section types, in a git repository · a portfolio has about six kinds of block, and inventing a page language is how this project dies - Images: A derivative cache keyed by the original's content hash and the target width · reprocessing four hundred photographs on every build turns a two minute edit into a coffee break - Build: A deterministic static build into a fresh output directory · same input, same bytes, or you cannot tell what a deploy changed - Publish: Upload to a new remote directory, then switch the served path in one operation · visitors must never see a site half replaced - Hosting: Any static host or object store you control, with your own domain · the point of leaving a builder is that the result is yours ### Data model (create this before Phase 1) - 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 ### Phase 1 · A closed vocabulary of sections Build: Define at most six section kinds with a schema each: gallery, text, image, embed and contact are usually enough. Pages are markdown files with frontmatter and an ordered list of sections. Validate every section against its schema on load and refuse to build on a failure, naming the file and the field. Trap: 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. Done when: Every section kind has a schema and a section failing it stops the build with the file name and the field, tested with a broken fixture. No section kind accepts raw markup, verified by review. A page renders identically from its file after a round trip through the editor, tested. Adding a new page is one file and needs no code change. Do not build yet: No image processing, build output, publishing or domains yet. ### Phase 2 · Process each photograph exactly once Build: On import, hash the original, read its dimensions and orientation, and record them. Generate the widths your templates actually request, writing a derivative keyed on hash and width. Apply the orientation so nothing renders sideways, and strip metadata apart from a copyright field you choose to keep. Emit a width set and sizes attribute per gallery. Trap: 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. Done when: A second build with no content change produces no new derivatives and completes in a stated fraction of the first, measured. A photograph with a rotation flag is rendered upright in the output, tested with a fixture of each orientation value. Every gallery image emits a width set and a sizes attribute, and the largest derivative is never wider than the original. Changing one photograph rebuilds only that photograph's derivatives, tested. Do not build yet: No build output, preview, publishing or domains yet. ### Phase 3 · A build you can trust and a preview you will use Build: Build into a fresh directory each time and record the build row. Serve the same output directory locally for preview rather than a separate development renderer. Check internal links and image references during the build and fail on a broken one. Report the output size and the largest files. Trap: 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. Done when: Two builds from unchanged content produce byte-identical output, tested by hashing the directories. A broken internal link or missing image fails the build with the page and the reference named, tested. The preview serves the same directory the publish step uploads, verified by inspecting the code path. The build prints total output size and the ten largest files. Do not build yet: No publishing, domain setup or redirects yet. ### Phase 4 · Publishing to your own domain without a gap Build: Upload the build to a new remote prefix, verify a manifest of files and sizes, then switch the served prefix in one operation and keep the previous one for rollback. Document the domain records your host needs, for both the bare domain and the www name, and check them from the tool before a first publish. Trap: 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. Done when: A publish uploads to a new prefix and only switches after every file in the manifest is verified present and the right size, tested by interrupting an upload. An interrupted publish leaves the live site untouched, tested. A rollback switches back to the previous prefix in one operation and is tested, not documented. The tool checks the domain's records before a first publish and explains, for the bare domain specifically, why an alias record cannot be used there. Do not build yet: No redirects, link checking over time or handover yet. ### Phase 5 · A site that outlives the generator Build: Write a redirect whenever a slug changes and emit them in the form your host understands. Keep an archive of published builds with their manifests. Provide a content export as plain files and images. Add a scheduled external link check that reports rather than edits, and a stated ceiling on total output size so a gallery import cannot quietly triple your hosting bill. Trap: 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. Done when: Changing a page's slug creates a redirect automatically and the old path resolves after publishing, tested end to end. The exported content directory builds on another machine with no database and no cache, tested. External link checking reports broken destinations without modifying content, and runs on a schedule. Exceeding the configured output size ceiling stops the publish with the largest contributors listed. Do not build yet: Nothing after this. ### Out of scope (and why) - Client galleries with proofing and selection, which is a different product with access control, expiry and someone else's deadline attached. - A store selling prints, which brings tax, fulfilment and refunds into what is currently a static site. - A general purpose visual editor, which is the paid product's actual moat and takes far longer than the rest of this put together. - Managed hosting with forms and analytics, since the point of this build is that you hold the hosting relationship and the domain. ### README must contain - That section kinds are a closed list with schemas, and that adding a kind is a code change on purpose - That image derivatives are cached by the original's content hash, so a rebuild without changes does no image work - That the decoder's input pixel limit is a setting with a stated number, and what happens when a very large panorama exceeds it - That publishing uploads to a new prefix and switches once, and how to roll back to the previous one - The exact domain records for the bare domain and the www name, and why an alias record cannot be used at the bare domain ===== ARCHITECTURE.md ===== # Architecture · Format ## Stack | Part | Choice | Why | | --- | --- | --- | | Content | Markdown with frontmatter plus a fixed vocabulary of section types, in a git repository | a portfolio has about six kinds of block, and inventing a page language is how this project dies | | Images | A derivative cache keyed by the original's content hash and the target width | reprocessing four hundred photographs on every build turns a two minute edit into a coffee break | | Build | A deterministic static build into a fresh output directory | same input, same bytes, or you cannot tell what a deploy changed | | Publish | Upload to a new remote directory, then switch the served path in one operation | visitors must never see a site half replaced | | Hosting | Any static host or object store you control, with your own domain | the point of leaving a builder is that the result is yours | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Content | Pages, sections and their schemas. | Any front matter format, provided the section list stays closed. | | Images | Hash-keyed derivatives and orientation. | Any library, provided the input pixel limit is explicit. | | Build | Deterministic output and link checking. | Any generator, provided preview and publish share the output. | | Publish | Prefix upload, verification and switching. | Any host, provided a switch and a rollback are single operations. | | Redirects | Slug history and emitted redirects. | Not swappable for breaking old links. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `CONTENT_DIR` · required · Pages and section files. This is the part you back up. - `ORIGINALS_DIR` · required · Full size photographs. Never modified by the build. - `DERIVATIVE_CACHE` · required · Keyed by original hash and width. Safe to delete at the cost of one slow build. - `MAX_INPUT_PIXELS` · required · The decoder's input limit. Raise it deliberately for panoramas and record why. - `OUTPUT_SIZE_CEILING_MB` · required · A publish above this stops and lists the largest contributors. - `REMOTE_BUCKET` · required · Where builds are uploaded, each to its own prefix. - `REMOTE_KEY` · required, secret · Credentials for the object store. Scope them to this one bucket. - `SITE_DOMAIN` · required · Used for absolute URLs, the link check and the domain record check. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Format product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Markdown with frontmatter plus a fixed vocabulary of section types, in a git repository, A derivative cache keyed by the original's content hash and the target width, A deterministic static build into a fresh output directory, Upload to a new remote directory, then switch the served path in one operation, Any static host or object store you control, with your own domain. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - 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. - 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 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. - 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. - 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. ===== MILESTONES.md ===== # Delivery milestones · Format Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A closed vocabulary of sections Pages are files made of a small fixed set of section types, and no section can contain arbitrary markup. ### Steps 1. Write the section schemas before the renderer The vocabulary is the product decision; the templates are details. Files: `docs/sections.md`, `src/content/schema.ts` 2. Fail the build on an invalid section A page that renders wrong is worse than a build that stops. Files: `src/content/validate.ts`, `tests/schema.test.ts` 3. Keep pages as files that survive this tool Frontmatter and markdown outlive whatever you build here. Files: `content/index.md` ### Done when - [ ] Every section kind validates against a schema - [ ] An invalid section names the file and field - [ ] No section accepts raw markup - [ ] A new page is one file, no code ### 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. ## M2 · Process each photograph exactly once Image derivatives are cached by content hash and target width, and an unchanged photograph is never re-encoded. ### Steps 1. Hash originals and key every derivative on the hash The cache key is the whole performance story here. Files: `src/images/cache.ts`, `tests/derivative-cache.test.ts` 2. Apply orientation and strip what you did not choose to keep Location data in a public portfolio is a leak, not a feature. Files: `src/images/process.ts` 3. Raise the decoder's pixel limit deliberately, or let it refuse 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.ts`, `docs/limits.md` ### Done when - [ ] A no-change build produces no new derivatives - [ ] Rotated originals render upright - [ ] Galleries emit a width set and sizes - [ ] The decoder pixel limit is an explicit setting ### 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. ## M3 · 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. ### Steps 1. Build into a fresh directory every time Leftovers from a previous build are how deleted pages stay online. Files: `src/build/run.ts` 2. Preview the built output, not a second renderer One code path means the preview can be believed. Files: `src/preview/server.ts` ```sh npm run build && npm run preview ``` 3. Check links and image references in the build A portfolio with a broken image is worse than no portfolio. Files: `src/build/links.ts`, `tests/links.test.ts` ### Done when - [ ] Two builds of the same content are byte-identical - [ ] Broken links and missing images fail the build - [ ] Preview serves the published directory - [ ] Build reports size and the largest files ### 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. ## M4 · 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. ### Steps 1. Upload to a new prefix, verify, then switch The switch is the only moment visitors see a change. Files: `src/publish/upload.ts`, `tests/interrupted-publish.test.ts` 2. Keep the previous prefix and test the rollback A rollback nobody has run is a plan, not a rollback. Files: `src/publish/rollback.ts` ```sh npm run rollback ``` 3. Write down the domain records, bare and www This is where a weekend gets lost, and it is documentation, not code. Files: `docs/domain.md` ### Done when - [ ] An interrupted upload never reaches the live site - [ ] The switch happens only after manifest verification - [ ] Rollback is exercised, not just written down - [ ] Domain records are checked and explained before publishing ### 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. ## M5 · A site that outlives the generator (production only) Renaming, archiving and leaving are all supported, and nothing about the output depends on this tool still existing. ### Steps 1. Record a redirect on every slug change Other people's links are the only traffic a portfolio has. Files: `src/content/redirects.ts`, `tests/redirects.test.ts` 2. Prove the export builds elsewhere That is the difference between owning the site and renting it. Files: `src/export/content.ts` ```sh npm run export && cd /tmp/export && npm run build ``` 3. Put a ceiling on total output size One raw gallery import can multiply the bill without a warning. Files: `src/build/limits.ts` ### Done when - [ ] Slug changes produce working redirects - [ ] The export builds on a clean machine - [ ] Link checking reports without editing - [ ] The size ceiling stops a publish with details ### Watch 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. ===== OPERATIONS.md ===== # Operations · Format ## Backup The content directory and the originals, which together rebuild everything else. The derivative cache and the output directory are both disposable, so back up neither. ## Restore Restore content and originals on a clean machine, run a build, and compare the output hash against the last published manifest. A difference means something outside those two directories mattered, which is a bug. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Total output size against the ceiling, the build time trend, and the external link check results. A build time that suddenly doubles usually means the derivative cache was invalidated by a settings change rather than by new photographs. ## Incident checklist The realistic incident is a publish that went out wrong or a domain record that expired. Rollback handles the first in one operation; the second is why the domain records are written down in the repository rather than remembered. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Two builds of unchanged content are confirmed byte-identical - [ ] A no-change build is confirmed to produce no new image derivatives - [ ] An interrupted upload is confirmed never to reach the live site - [ ] Rollback to the previous prefix is confirmed by running it - [ ] A slug change is confirmed to produce a working redirect after publishing ## Launch constraint Do not market omitted Format capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Pages and section files. This is the part you back up. CONTENT_DIR=./content # Required. Full size photographs. Never modified by the build. ORIGINALS_DIR=./originals # Required. Keyed by original hash and width. Safe to delete at the cost of one slow build. DERIVATIVE_CACHE=./.cache/derivatives # Required. The decoder's input limit. Raise it deliberately for panoramas and record why. MAX_INPUT_PIXELS=268402689 # Required. A publish above this stops and lists the largest contributors. OUTPUT_SIZE_CEILING_MB=2000 # Required. Where builds are uploaded, each to its own prefix. REMOTE_BUCKET=your-portfolio-bucket # Required · secret. Credentials for the object store. Scope them to this one bucket. REMOTE_KEY=AKIA...redacted # Required. Used for absolute URLs, the link check and the domain record check. SITE_DOMAIN=yourname.example
# Format · indie build 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: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Content | Markdown with frontmatter plus a fixed vocabulary of section types, in a git repository | a portfolio has about six kinds of block, and inventing a page language is how this project dies | | Images | A derivative cache keyed by the original's content hash and the target width | reprocessing four hundred photographs on every build turns a two minute edit into a coffee break | | Build | A deterministic static build into a fresh output directory | same input, same bytes, or you cannot tell what a deploy changed | | Publish | Upload to a new remote directory, then switch the served path in one operation | visitors must never see a site half replaced | | Hosting | Any static host or object store you control, with your own domain | the point of leaving a builder is that the result is yours | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the build, the preview and the publish. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **An image processing library with a documented input pixel limit** · free - 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 - [ ] **A static host or object store you control, with prefix switching** · a 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 - [ ] **A domain you own, with access to its DNS records** · roughly 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 - [ ] **The list of section kinds** · free - 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 ## Quick start ```sh npm run build && npm run preview ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for Format, and the README should say so rather than pretend.
# Build brief · Format The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a portfolio site generator that publishes static files to a domain you own to replace Format. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Content: Markdown with frontmatter plus a fixed vocabulary of section types, in a git repository · a portfolio has about six kinds of block, and inventing a page language is how this project dies - Images: A derivative cache keyed by the original's content hash and the target width · reprocessing four hundred photographs on every build turns a two minute edit into a coffee break - Build: A deterministic static build into a fresh output directory · same input, same bytes, or you cannot tell what a deploy changed - Publish: Upload to a new remote directory, then switch the served path in one operation · visitors must never see a site half replaced - Hosting: Any static host or object store you control, with your own domain · the point of leaving a builder is that the result is yours ### Data model (create this before Phase 1) - 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 ### Phase 1 · A closed vocabulary of sections Build: Define at most six section kinds with a schema each: gallery, text, image, embed and contact are usually enough. Pages are markdown files with frontmatter and an ordered list of sections. Validate every section against its schema on load and refuse to build on a failure, naming the file and the field. Trap: 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. Done when: Every section kind has a schema and a section failing it stops the build with the file name and the field, tested with a broken fixture. No section kind accepts raw markup, verified by review. A page renders identically from its file after a round trip through the editor, tested. Adding a new page is one file and needs no code change. Do not build yet: No image processing, build output, publishing or domains yet. ### Phase 2 · Process each photograph exactly once Build: On import, hash the original, read its dimensions and orientation, and record them. Generate the widths your templates actually request, writing a derivative keyed on hash and width. Apply the orientation so nothing renders sideways, and strip metadata apart from a copyright field you choose to keep. Emit a width set and sizes attribute per gallery. Trap: 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. Done when: A second build with no content change produces no new derivatives and completes in a stated fraction of the first, measured. A photograph with a rotation flag is rendered upright in the output, tested with a fixture of each orientation value. Every gallery image emits a width set and a sizes attribute, and the largest derivative is never wider than the original. Changing one photograph rebuilds only that photograph's derivatives, tested. Do not build yet: No build output, preview, publishing or domains yet. ### Phase 3 · A build you can trust and a preview you will use Build: Build into a fresh directory each time and record the build row. Serve the same output directory locally for preview rather than a separate development renderer. Check internal links and image references during the build and fail on a broken one. Report the output size and the largest files. Trap: 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. Done when: Two builds from unchanged content produce byte-identical output, tested by hashing the directories. A broken internal link or missing image fails the build with the page and the reference named, tested. The preview serves the same directory the publish step uploads, verified by inspecting the code path. The build prints total output size and the ten largest files. Do not build yet: No publishing, domain setup or redirects yet. ### Phase 4 · Publishing to your own domain without a gap Build: Upload the build to a new remote prefix, verify a manifest of files and sizes, then switch the served prefix in one operation and keep the previous one for rollback. Document the domain records your host needs, for both the bare domain and the www name, and check them from the tool before a first publish. Trap: 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. Done when: A publish uploads to a new prefix and only switches after every file in the manifest is verified present and the right size, tested by interrupting an upload. An interrupted publish leaves the live site untouched, tested. A rollback switches back to the previous prefix in one operation and is tested, not documented. The tool checks the domain's records before a first publish and explains, for the bare domain specifically, why an alias record cannot be used there. Do not build yet: No redirects, link checking over time or handover yet. ### Phase 5 · A site that outlives the generator Build: Write a redirect whenever a slug changes and emit them in the form your host understands. Keep an archive of published builds with their manifests. Provide a content export as plain files and images. Add a scheduled external link check that reports rather than edits, and a stated ceiling on total output size so a gallery import cannot quietly triple your hosting bill. Trap: 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. Done when: Changing a page's slug creates a redirect automatically and the old path resolves after publishing, tested end to end. The exported content directory builds on another machine with no database and no cache, tested. External link checking reports broken destinations without modifying content, and runs on a schedule. Exceeding the configured output size ceiling stops the publish with the largest contributors listed. Do not build yet: Nothing after this. ### Out of scope (and why) - Client galleries with proofing and selection, which is a different product with access control, expiry and someone else's deadline attached. - A store selling prints, which brings tax, fulfilment and refunds into what is currently a static site. - A general purpose visual editor, which is the paid product's actual moat and takes far longer than the rest of this put together. - Managed hosting with forms and analytics, since the point of this build is that you hold the hosting relationship and the domain. ### README must contain - That section kinds are a closed list with schemas, and that adding a kind is a code change on purpose - That image derivatives are cached by the original's content hash, so a rebuild without changes does no image work - That the decoder's input pixel limit is a setting with a stated number, and what happens when a very large panorama exceeds it - That publishing uploads to a new prefix and switches once, and how to roll back to the previous one - The exact domain records for the bare domain and the www name, and why an alias record cannot be used at the bare domain
# Agent instructions · Format indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Markdown with frontmatter plus a fixed vocabulary of section types, in a git repository, A derivative cache keyed by the original's content hash and the target width, A deterministic static build into a fresh output directory, Upload to a new remote directory, then switch the served path in one operation, Any static host or object store you control, with your own domain. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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 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. - 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.
# Build plan · Format 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 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. ### Steps 1. Write the section schemas before the renderer The vocabulary is the product decision; the templates are details. Files: `docs/sections.md`, `src/content/schema.ts` 2. Fail the build on an invalid section A page that renders wrong is worse than a build that stops. Files: `src/content/validate.ts`, `tests/schema.test.ts` 3. Keep pages as files that survive this tool Frontmatter and markdown outlive whatever you build here. Files: `content/index.md` ### Done when - [ ] Every section kind validates against a schema - [ ] An invalid section names the file and field - [ ] No section accepts raw markup - [ ] A new page is one file, no code ### 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. ## Phase 2 · Process each photograph exactly once Image derivatives are cached by content hash and target width, and an unchanged photograph is never re-encoded. ### Steps 1. Hash originals and key every derivative on the hash The cache key is the whole performance story here. Files: `src/images/cache.ts`, `tests/derivative-cache.test.ts` 2. Apply orientation and strip what you did not choose to keep Location data in a public portfolio is a leak, not a feature. Files: `src/images/process.ts` 3. Raise the decoder's pixel limit deliberately, or let it refuse 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.ts`, `docs/limits.md` ### Done when - [ ] A no-change build produces no new derivatives - [ ] Rotated originals render upright - [ ] Galleries emit a width set and sizes - [ ] The decoder pixel limit is an explicit setting ### 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. ## Phase 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. ### Steps 1. Build into a fresh directory every time Leftovers from a previous build are how deleted pages stay online. Files: `src/build/run.ts` 2. Preview the built output, not a second renderer One code path means the preview can be believed. Files: `src/preview/server.ts` ```sh npm run build && npm run preview ``` 3. Check links and image references in the build A portfolio with a broken image is worse than no portfolio. Files: `src/build/links.ts`, `tests/links.test.ts` ### Done when - [ ] Two builds of the same content are byte-identical - [ ] Broken links and missing images fail the build - [ ] Preview serves the published directory - [ ] Build reports size and the largest files ### 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. ## Phase 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. ### Steps 1. Upload to a new prefix, verify, then switch The switch is the only moment visitors see a change. Files: `src/publish/upload.ts`, `tests/interrupted-publish.test.ts` 2. Keep the previous prefix and test the rollback A rollback nobody has run is a plan, not a rollback. Files: `src/publish/rollback.ts` ```sh npm run rollback ``` 3. Write down the domain records, bare and www This is where a weekend gets lost, and it is documentation, not code. Files: `docs/domain.md` ### Done when - [ ] An interrupted upload never reaches the live site - [ ] The switch happens only after manifest verification - [ ] Rollback is exercised, not just written down - [ ] Domain records are checked and explained before publishing ### 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. ## Not in this build - Client galleries with proofing. - A print or download store. - A general purpose visual editor. - Managed hosting with forms and analytics. ## After v1, if you want it - 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
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Pages and section files. This is the part you back up. CONTENT_DIR=./content # Required. Full size photographs. Never modified by the build. ORIGINALS_DIR=./originals # Required. Keyed by original hash and width. Safe to delete at the cost of one slow build. DERIVATIVE_CACHE=./.cache/derivatives # Required. The decoder's input limit. Raise it deliberately for panoramas and record why. MAX_INPUT_PIXELS=268402689 # Required. A publish above this stops and lists the largest contributors. OUTPUT_SIZE_CEILING_MB=2000 # Required. Where builds are uploaded, each to its own prefix. REMOTE_BUCKET=your-portfolio-bucket # Required · secret. Credentials for the object store. Scope them to this one bucket. REMOTE_KEY=AKIA...redacted # Required. Used for absolute URLs, the link check and the domain record check. SITE_DOMAIN=yourname.example
# Format · product brief ## Problem The visible portfolio website builder loop is buildable, but a credible replacement needs more than the first screen. Format earns its keep through editor, CMS, hosting, polish, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A portfolio whose content is files you own, whose images are processed once, whose publishes are atomic with a tested rollback, and whose bare domain is configured in a way that actually works. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node.js 22 - deploy target or static hosting - user-owned domain for publishing ## Explicit non-goals for v1 - 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 ## Success criteria - Two builds of unchanged content are confirmed byte-identical - A no-change build is confirmed to produce no new image derivatives - An interrupted upload is confirmed never to reach the live site - Rollback to the previous prefix is confirmed by running it - A slug change is confirmed to produce a working redirect after publishing
# Build brief · Format The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a portfolio site generator that publishes static files to a domain you own to replace Format. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Content: Markdown with frontmatter plus a fixed vocabulary of section types, in a git repository · a portfolio has about six kinds of block, and inventing a page language is how this project dies - Images: A derivative cache keyed by the original's content hash and the target width · reprocessing four hundred photographs on every build turns a two minute edit into a coffee break - Build: A deterministic static build into a fresh output directory · same input, same bytes, or you cannot tell what a deploy changed - Publish: Upload to a new remote directory, then switch the served path in one operation · visitors must never see a site half replaced - Hosting: Any static host or object store you control, with your own domain · the point of leaving a builder is that the result is yours ### Data model (create this before Phase 1) - 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 ### Phase 1 · A closed vocabulary of sections Build: Define at most six section kinds with a schema each: gallery, text, image, embed and contact are usually enough. Pages are markdown files with frontmatter and an ordered list of sections. Validate every section against its schema on load and refuse to build on a failure, naming the file and the field. Trap: 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. Done when: Every section kind has a schema and a section failing it stops the build with the file name and the field, tested with a broken fixture. No section kind accepts raw markup, verified by review. A page renders identically from its file after a round trip through the editor, tested. Adding a new page is one file and needs no code change. Do not build yet: No image processing, build output, publishing or domains yet. ### Phase 2 · Process each photograph exactly once Build: On import, hash the original, read its dimensions and orientation, and record them. Generate the widths your templates actually request, writing a derivative keyed on hash and width. Apply the orientation so nothing renders sideways, and strip metadata apart from a copyright field you choose to keep. Emit a width set and sizes attribute per gallery. Trap: 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. Done when: A second build with no content change produces no new derivatives and completes in a stated fraction of the first, measured. A photograph with a rotation flag is rendered upright in the output, tested with a fixture of each orientation value. Every gallery image emits a width set and a sizes attribute, and the largest derivative is never wider than the original. Changing one photograph rebuilds only that photograph's derivatives, tested. Do not build yet: No build output, preview, publishing or domains yet. ### Phase 3 · A build you can trust and a preview you will use Build: Build into a fresh directory each time and record the build row. Serve the same output directory locally for preview rather than a separate development renderer. Check internal links and image references during the build and fail on a broken one. Report the output size and the largest files. Trap: 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. Done when: Two builds from unchanged content produce byte-identical output, tested by hashing the directories. A broken internal link or missing image fails the build with the page and the reference named, tested. The preview serves the same directory the publish step uploads, verified by inspecting the code path. The build prints total output size and the ten largest files. Do not build yet: No publishing, domain setup or redirects yet. ### Phase 4 · Publishing to your own domain without a gap Build: Upload the build to a new remote prefix, verify a manifest of files and sizes, then switch the served prefix in one operation and keep the previous one for rollback. Document the domain records your host needs, for both the bare domain and the www name, and check them from the tool before a first publish. Trap: 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. Done when: A publish uploads to a new prefix and only switches after every file in the manifest is verified present and the right size, tested by interrupting an upload. An interrupted publish leaves the live site untouched, tested. A rollback switches back to the previous prefix in one operation and is tested, not documented. The tool checks the domain's records before a first publish and explains, for the bare domain specifically, why an alias record cannot be used there. Do not build yet: No redirects, link checking over time or handover yet. ### Phase 5 · A site that outlives the generator Build: Write a redirect whenever a slug changes and emit them in the form your host understands. Keep an archive of published builds with their manifests. Provide a content export as plain files and images. Add a scheduled external link check that reports rather than edits, and a stated ceiling on total output size so a gallery import cannot quietly triple your hosting bill. Trap: 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. Done when: Changing a page's slug creates a redirect automatically and the old path resolves after publishing, tested end to end. The exported content directory builds on another machine with no database and no cache, tested. External link checking reports broken destinations without modifying content, and runs on a schedule. Exceeding the configured output size ceiling stops the publish with the largest contributors listed. Do not build yet: Nothing after this. ### Out of scope (and why) - Client galleries with proofing and selection, which is a different product with access control, expiry and someone else's deadline attached. - A store selling prints, which brings tax, fulfilment and refunds into what is currently a static site. - A general purpose visual editor, which is the paid product's actual moat and takes far longer than the rest of this put together. - Managed hosting with forms and analytics, since the point of this build is that you hold the hosting relationship and the domain. ### README must contain - That section kinds are a closed list with schemas, and that adding a kind is a code change on purpose - That image derivatives are cached by the original's content hash, so a rebuild without changes does no image work - That the decoder's input pixel limit is a setting with a stated number, and what happens when a very large panorama exceeds it - That publishing uploads to a new prefix and switches once, and how to roll back to the previous one - The exact domain records for the bare domain and the www name, and why an alias record cannot be used at the bare domain
# Architecture · Format ## Stack | Part | Choice | Why | | --- | --- | --- | | Content | Markdown with frontmatter plus a fixed vocabulary of section types, in a git repository | a portfolio has about six kinds of block, and inventing a page language is how this project dies | | Images | A derivative cache keyed by the original's content hash and the target width | reprocessing four hundred photographs on every build turns a two minute edit into a coffee break | | Build | A deterministic static build into a fresh output directory | same input, same bytes, or you cannot tell what a deploy changed | | Publish | Upload to a new remote directory, then switch the served path in one operation | visitors must never see a site half replaced | | Hosting | Any static host or object store you control, with your own domain | the point of leaving a builder is that the result is yours | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Content | Pages, sections and their schemas. | Any front matter format, provided the section list stays closed. | | Images | Hash-keyed derivatives and orientation. | Any library, provided the input pixel limit is explicit. | | Build | Deterministic output and link checking. | Any generator, provided preview and publish share the output. | | Publish | Prefix upload, verification and switching. | Any host, provided a switch and a rollback are single operations. | | Redirects | Slug history and emitted redirects. | Not swappable for breaking old links. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `CONTENT_DIR` · required · Pages and section files. This is the part you back up. - `ORIGINALS_DIR` · required · Full size photographs. Never modified by the build. - `DERIVATIVE_CACHE` · required · Keyed by original hash and width. Safe to delete at the cost of one slow build. - `MAX_INPUT_PIXELS` · required · The decoder's input limit. Raise it deliberately for panoramas and record why. - `OUTPUT_SIZE_CEILING_MB` · required · A publish above this stops and lists the largest contributors. - `REMOTE_BUCKET` · required · Where builds are uploaded, each to its own prefix. - `REMOTE_KEY` · required, secret · Credentials for the object store. Scope them to this one bucket. - `SITE_DOMAIN` · required · Used for absolute URLs, the link check and the domain record check. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Format product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Markdown with frontmatter plus a fixed vocabulary of section types, in a git repository, A derivative cache keyed by the original's content hash and the target width, A deterministic static build into a fresh output directory, Upload to a new remote directory, then switch the served path in one operation, Any static host or object store you control, with your own domain. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - 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. - 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 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. - 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. - 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.
# Delivery milestones · Format Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A closed vocabulary of sections Pages are files made of a small fixed set of section types, and no section can contain arbitrary markup. ### Steps 1. Write the section schemas before the renderer The vocabulary is the product decision; the templates are details. Files: `docs/sections.md`, `src/content/schema.ts` 2. Fail the build on an invalid section A page that renders wrong is worse than a build that stops. Files: `src/content/validate.ts`, `tests/schema.test.ts` 3. Keep pages as files that survive this tool Frontmatter and markdown outlive whatever you build here. Files: `content/index.md` ### Done when - [ ] Every section kind validates against a schema - [ ] An invalid section names the file and field - [ ] No section accepts raw markup - [ ] A new page is one file, no code ### 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. ## M2 · Process each photograph exactly once Image derivatives are cached by content hash and target width, and an unchanged photograph is never re-encoded. ### Steps 1. Hash originals and key every derivative on the hash The cache key is the whole performance story here. Files: `src/images/cache.ts`, `tests/derivative-cache.test.ts` 2. Apply orientation and strip what you did not choose to keep Location data in a public portfolio is a leak, not a feature. Files: `src/images/process.ts` 3. Raise the decoder's pixel limit deliberately, or let it refuse 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.ts`, `docs/limits.md` ### Done when - [ ] A no-change build produces no new derivatives - [ ] Rotated originals render upright - [ ] Galleries emit a width set and sizes - [ ] The decoder pixel limit is an explicit setting ### 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. ## M3 · 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. ### Steps 1. Build into a fresh directory every time Leftovers from a previous build are how deleted pages stay online. Files: `src/build/run.ts` 2. Preview the built output, not a second renderer One code path means the preview can be believed. Files: `src/preview/server.ts` ```sh npm run build && npm run preview ``` 3. Check links and image references in the build A portfolio with a broken image is worse than no portfolio. Files: `src/build/links.ts`, `tests/links.test.ts` ### Done when - [ ] Two builds of the same content are byte-identical - [ ] Broken links and missing images fail the build - [ ] Preview serves the published directory - [ ] Build reports size and the largest files ### 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. ## M4 · 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. ### Steps 1. Upload to a new prefix, verify, then switch The switch is the only moment visitors see a change. Files: `src/publish/upload.ts`, `tests/interrupted-publish.test.ts` 2. Keep the previous prefix and test the rollback A rollback nobody has run is a plan, not a rollback. Files: `src/publish/rollback.ts` ```sh npm run rollback ``` 3. Write down the domain records, bare and www This is where a weekend gets lost, and it is documentation, not code. Files: `docs/domain.md` ### Done when - [ ] An interrupted upload never reaches the live site - [ ] The switch happens only after manifest verification - [ ] Rollback is exercised, not just written down - [ ] Domain records are checked and explained before publishing ### 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. ## M5 · A site that outlives the generator (production only) Renaming, archiving and leaving are all supported, and nothing about the output depends on this tool still existing. ### Steps 1. Record a redirect on every slug change Other people's links are the only traffic a portfolio has. Files: `src/content/redirects.ts`, `tests/redirects.test.ts` 2. Prove the export builds elsewhere That is the difference between owning the site and renting it. Files: `src/export/content.ts` ```sh npm run export && cd /tmp/export && npm run build ``` 3. Put a ceiling on total output size One raw gallery import can multiply the bill without a warning. Files: `src/build/limits.ts` ### Done when - [ ] Slug changes produce working redirects - [ ] The export builds on a clean machine - [ ] Link checking reports without editing - [ ] The size ceiling stops a publish with details ### Watch 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.
# Operations · Format ## Backup The content directory and the originals, which together rebuild everything else. The derivative cache and the output directory are both disposable, so back up neither. ## Restore Restore content and originals on a clean machine, run a build, and compare the output hash against the last published manifest. A difference means something outside those two directories mattered, which is a bug. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Total output size against the ceiling, the build time trend, and the external link check results. A build time that suddenly doubles usually means the derivative cache was invalidated by a settings change rather than by new photographs. ## Incident checklist The realistic incident is a publish that went out wrong or a domain record that expired. Rollback handles the first in one operation; the second is why the domain records are written down in the repository rather than remembered. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Two builds of unchanged content are confirmed byte-identical - [ ] A no-change build is confirmed to produce no new image derivatives - [ ] An interrupted upload is confirmed never to reach the live site - [ ] Rollback to the previous prefix is confirmed by running it - [ ] A slug change is confirmed to produce a working redirect after publishing ## Launch constraint Do not market omitted Format capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Pages and section files. This is the part you back up. CONTENT_DIR=./content # Required. Full size photographs. Never modified by the build. ORIGINALS_DIR=./originals # Required. Keyed by original hash and width. Safe to delete at the cost of one slow build. DERIVATIVE_CACHE=./.cache/derivatives # Required. The decoder's input limit. Raise it deliberately for panoramas and record why. MAX_INPUT_PIXELS=268402689 # Required. A publish above this stops and lists the largest contributors. OUTPUT_SIZE_CEILING_MB=2000 # Required. Where builds are uploaded, each to its own prefix. REMOTE_BUCKET=your-portfolio-bucket # Required · secret. Credentials for the object store. Scope them to this one bucket. REMOTE_KEY=AKIA...redacted # Required. Used for absolute URLs, the link check and the domain record check. SITE_DOMAIN=yourname.example
$ choose a build depth, inspect the files, then open the complete pack in your agent
Format: A single site is easy to code. People pay for safe ongoing edits, templates, publishing, domains, collaboration, and no-maintenance hosting.
xlarge template and component libraries
xCMS collaboration and client handoff
xmanaged hosting, forms, and analytics
xgeneral-purpose visual editing
Format pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| basic | $14 | $10 | 10 website pages; 70 high-resolution images; 10 client galleries; 2 GB file transfers |
| pro | $24 | $12 | Unlimited pages; 1,500 high-resolution images; 100 GB image storage; 15 hosted-video minutes; 15 store products; 50 client galleries; 5 GB transfers |
| pro plus | $36 | $15 | Unlimited pages/images; 1 TB image storage; 120 hosted-video minutes; 1,000 store products; 250 client galleries; 10 GB transfers |
free tierno free tier; 14-day trial with no credit card
billingmonthly + annual; annual plans are paid upfront and promotional first-year prices renew at the then-current full-year price
hidden costsA professional branded email inbox costs $100/year on every plan; the free custom domain is only for the first year, and discounted annual pricing may be first-year-only.
verified 2026-08-14 · source ↗
Vibecode Format
Kinda. The core of Format is buildable in a weekend with the prompt on this page, but there are real gaps: large template and component libraries, CMS collaboration and client handoff. Read the honest list above before committing.
How much does Format cost?
Format's pricing is usage-based or varies by plan · The official pricing URL is recorded, but a stable current amount was not safely recoverable in this pass. Do not publish a number until rechecked..
What do I lose by replacing Format?
Honestly: large template and component libraries; CMS collaboration and client handoff; managed hosting, forms, and analytics; general-purpose visual editing. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Format?
Yes: Webstudio (Open-source visual website builder.), GrapesJS (Open-source web-builder framework.). Using prior art is also vibecoding; the prompt is for when you want it exactly your way.