Vibecode Carbonmade
track this build4 phases, 12 steps, beginner friendly0%The visible portfolio builder loop is buildable, but a credible replacement needs more than the first screen. Carbonmade 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 Carbonmade. 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 ===== # Carbonmade · indie build A static portfolio generator whose hard problems are an image pipeline that respects the originals and the phone, a publish that either completes or does not happen, and a URL you can keep sending to clients after you rename a project. 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 | Projects as folders on disk, one metadata file each, images beside them | the content has to outlive whatever generator you get bored of | | Generator | A local static build producing plain files, nothing running in production | a portfolio should not have a server you are responsible for patching | | Images | One derivative ladder generated from untouched originals | originals are the only thing in this project you cannot regenerate | | Editor | A local admin that edits metadata and ordering, never raw markup | an editor that writes HTML becomes the only thing that can read it | | Publish | Build into a fresh directory, verify, then swap, keeping the previous build | a half uploaded portfolio is worse than an old one | ## 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 generator, the admin and the image pipeline. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **An image processing library that reads orientation metadata** · free - Why: The whole derivative pipeline rests on it, and the ones that quietly drop orientation are the reason portfolios launch sideways. - Get it: Choose a maintained native library for your runtime and read its documentation on metadata handling before you build the ladder. - Verify: A deliberately rotated fixture comes out upright with no orientation tag - [ ] **Somewhere to put static files, with a custom domain** · usually free at portfolio traffic; the domain is the real cost - Why: Publishing is a file upload, and the domain is the thing that makes the portfolio yours rather than a tenant of someone else's brand. - Get it: Any static host or object storage with a CDN in front, plus your own domain pointed at it. - Verify: A published build is reachable at your domain over https - [ ] **Your actual originals, including the awkward ones** · free - Why: A pipeline tested on three tidy exports fails on the panorama, the portrait scan and the thirty megabyte camera file that are already in your archive. - Get it: Copy a representative selection into the content directory, including at least one rotated photograph and one very large file. - Verify: A full build completes and reports derivative sizes per project - [ ] **What counts as above the fold on your layout** · free - Why: The priority image rule needs a definition, and deciding it per page after the fact means deciding it inconsistently. - Get it: Write down the rule, for example the project cover on a project page and the first grid item on the index. - Verify: The rule is in the README and enforced by the build ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - A general purpose visual editor. - Client collaboration and approvals. - Hosted forms and analytics. - A template marketplace. - general-purpose visual editing - large template and component libraries - CMS collaboration and client handoff - managed hosting, forms, and analytics - high-fidelity color, format, and export handling If one of those is essential to you, that is the reason to keep paying for Carbonmade, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Carbonmade 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 publisher that turns project folders into a static site you own to replace Carbonmade. 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: Projects as folders on disk, one metadata file each, images beside them · the content has to outlive whatever generator you get bored of - Generator: A local static build producing plain files, nothing running in production · a portfolio should not have a server you are responsible for patching - Images: One derivative ladder generated from untouched originals · originals are the only thing in this project you cannot regenerate - Editor: A local admin that edits metadata and ordering, never raw markup · an editor that writes HTML becomes the only thing that can read it - Publish: Build into a fresh directory, verify, then swap, keeping the previous build · a half uploaded portfolio is worse than an old one ### Data model (create this before Phase 1) - projects(slug, title, year, client, summary, body_md, cover_media_id, ordinal, status) · status is draft or published - media(id, project_id, original_path, original_bytes, width, height, exif_orientation, sha256, alt_text, ordinal) - derivatives(media_id, width, format, path, bytes) · regenerated from the original, never edited in place, safe to delete - pages(slug, title, body_md, ordinal) · the about and contact pages every portfolio has - builds(id, started_at, finished_at, output_dir, content_hash, status, bytes, page_count, lcp_ms) - redirects(from_path, to_path, code, created_at) · written automatically whenever a published slug changes - alt_text is a column rather than an optional front matter key, and a published image without one fails the build. ### Phase 1 · Content on disk that outlives the generator Build: One directory per project holding a metadata file and the original images. The database is a cache built from the directory, not the source of truth: deleting it and re-scanning produces the same site. Slugs come from a field in the metadata file, not from the folder name, so a folder can be renamed without changing a URL. Trap: Keeping the canonical content in a database and the images in an uploads folder means the day you abandon this generator your portfolio is trapped in a schema, which is precisely the situation you left the paid product to avoid. Keep the folders canonical, treat the database as derived, and prove it by deleting the database and rebuilding byte-identical output. Done when: Deleting the database and re-scanning the content directory produces a build with the same content hash, tested. A project folder renamed on disk keeps its URL because the slug is in the metadata, tested. Every metadata file is valid against a schema and the scan reports the file and line on failure. An image sitting in a project folder but absent from the metadata is reported rather than silently ignored. Do not build yet: No image pipeline, page templates or publishing yet. ### Phase 2 · Derivatives from originals nobody touches Build: Read each original's dimensions and orientation tag once. Generate a fixed set of widths in a modern format plus a fallback, writing the orientation into the pixels and stripping the tag from the output. Cache by the original's hash plus the ladder settings so a rebuild does not redo the work, and never write back into the original file. Trap: Resizing with a library that ignores the orientation tag turns a third of a photographer's portfolio sideways, and nobody notices before launch because the operating system's viewer applies the tag and shows the originals correctly. Read the tag, bake the rotation into the derivative, strip the tag from the output so the browser does not rotate it a second time, and put a deliberately rotated photograph in the fixtures. Trap: Writing the resized version over the original saves disk and destroys the one artefact you cannot regenerate, usually in a script somebody ran to fix something else. Make the originals directory read only in the pipeline and assert it in a test. Done when: A fixture photograph with a non-default orientation tag renders upright in every derivative and the output carries no orientation tag, tested by reading the output metadata. Original files are byte-identical before and after a full build, asserted by hash. Changing the ladder settings regenerates derivatives and changing nothing regenerates none, both tested. Derivative total size per project is reported at build time. Do not build yet: No templates, editing or publishing yet. ### Phase 3 · The page that loads fast on a phone Build: Emit width, height, srcset and sizes on every image so nothing reflows. Mark exactly one image per page as the priority image: no lazy attribute, fetchpriority high, and preloaded. Everything below gets lazy loading. Fail the build if a page declares two priority images or none, and record a measured load figure per build so a regression is visible. Trap: Adding loading lazy to every image because it is the obvious optimisation puts the hero, which on a portfolio is almost always the largest contentful paint element, behind layout and the published guidance is explicit that an above the fold image with lazy loading will likely have a large negative effect on that metric. Choose one priority image per page, give it fetchpriority high and no lazy attribute, and lazy load only what is genuinely below the fold. Trap: Omitting width and height means the page reflows as each image arrives, which on a slow phone connection is the difference between a portfolio and a slideshow of jumping boxes. Emit intrinsic dimensions from the stored values on every single image, and fail the build when one is missing. Done when: Every image element in the output carries width, height, srcset and sizes, asserted by parsing the built HTML. Exactly one image per page carries the priority attributes and none of them carries lazy loading, asserted. A page with zero or two priority images fails the build with the page named. A measured load figure on a throttled connection is recorded per build and the previous build's figure is shown alongside it. Do not build yet: No editor, publishing or redirects yet. ### Phase 4 · Editing without breaking the site Build: Edit project metadata and body text, drag to reorder projects and images, set the cover, and write alt text. Preview builds against the same renderer as production so preview and live cannot diverge. Validation runs on save and blocks publishing rather than blocking editing, so a half-finished project can sit in draft. Trap: A preview served by a different code path from the build is the reason things look right locally and wrong live, and you only find out after you have sent the link. Run the real build into a temporary directory and serve that, even though it is slower, because the alternative is a class of bug you cannot reproduce. Trap: Making alt text optional means it is never written, and a portfolio is the one kind of site that is entirely images. Block publishing on a missing alt text, name the image, and let the project stay in draft as long as it needs to. Done when: Preview is produced by the same build code as publishing, verified by review and by comparing output for a fixture project. Publishing a project with an image missing alt text is refused and the image is named. Reordering in the admin writes the ordinal back into the metadata file on disk, not only into the database. A draft project appears in no built output, asserted by searching the build directory for its slug. Do not build yet: No publishing, redirects or rollback yet. ### Phase 5 · Publish, roll back, keep the links working Build: Build into a fresh directory, verify it, then swap it into place and keep the previous build for rollback. Record a redirect automatically whenever a published slug changes, and refuse to publish when a previously published slug has neither a page nor a redirect. Ship a useful 404 and a rollback command that swaps the previous build back in one step. Trap: Uploading file by file over the live site means a visitor during the upload gets new HTML pointing at images that are not there yet, and a failure halfway leaves that state permanently. Build complete, verify complete, swap complete, and keep the previous directory so rollback is a rename rather than a rebuild. Trap: Renaming a project to fix a typo changes its URL and every link in every email you sent a client goes to a 404, which you discover when one of them mentions it months later. Diff published slugs against the last build, write the redirect yourself, and block a publish that would orphan a slug with no redirect. Done when: A publish interrupted part way leaves the previously published site serving unchanged, tested by killing the process mid-swap. Renaming a published project writes a redirect automatically and the old URL resolves after publishing, tested end to end. Rollback restores the previous build and is exercised in the suite rather than documented. The build refuses to publish when a previously published slug has no page and no redirect, naming the slug. Do not build yet: Nothing after this. ### Out of scope (and why) - A general purpose visual editor, which is a different product with a much larger surface and is what the paid tools actually sell. - Client collaboration and approvals, which need accounts, permissions and a hosted service you would then have to keep running. - Hosted contact forms and analytics, which turn a static site into a service with data obligations; link to something else or run a separate small service. - A template marketplace, which is content work rather than engineering and is where the polish you are comparing against comes from. ### README must contain - That the project folders are canonical and the database is a cache you can delete and rebuild - That originals are never modified and every displayed image is a generated derivative - That exactly one image per page is the priority image and why lazy loading the hero is the wrong default - That publishing builds, verifies and swaps, that the previous build is kept, and the rollback command - That renaming a project writes a redirect automatically and that publishing is blocked if one is missing ===== AGENTS.md ===== # Agent instructions · Carbonmade indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Projects as folders on disk, one metadata file each, images beside them, A local static build producing plain files, nothing running in production, One derivative ladder generated from untouched originals, A local admin that edits metadata and ordering, never raw markup, Build into a fresh directory, verify, then swap, keeping the previous build. 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 - Keeping the canonical content in a database and the images in an uploads folder means the day you abandon this generator your portfolio is trapped in a schema, which is precisely the situation you left the paid product to avoid. Keep the folders canonical, treat the database as derived, and prove it by deleting the database and rebuilding byte-identical output. - Resizing with a library that ignores the orientation tag turns a third of a photographer's portfolio sideways, and nobody notices before launch because the operating system's viewer applies the tag and shows the originals correctly. Read the tag, bake the rotation into the derivative, strip the tag from the output so the browser does not rotate it a second time, and put a deliberately rotated photograph in the fixtures. - Writing the resized version over the original saves disk and destroys the one artefact you cannot regenerate, usually in a script somebody ran to fix something else. Make the originals directory read only in the pipeline and assert it in a test. - Adding loading lazy to every image because it is the obvious optimisation puts the hero, which on a portfolio is almost always the largest contentful paint element, behind layout and the published guidance is explicit that an above the fold image with lazy loading will likely have a large negative effect on that metric. Choose one priority image per page, give it fetchpriority high and no lazy attribute, and lazy load only what is genuinely below the fold. - Omitting width and height means the page reflows as each image arrives, which on a slow phone connection is the difference between a portfolio and a slideshow of jumping boxes. Emit intrinsic dimensions from the stored values on every single image, and fail the build when one is missing. - A preview served by a different code path from the build is the reason things look right locally and wrong live, and you only find out after you have sent the link. Run the real build into a temporary directory and serve that, even though it is slower, because the alternative is a class of bug you cannot reproduce. - Making alt text optional means it is never written, and a portfolio is the one kind of site that is entirely images. Block publishing on a missing alt text, name the image, and let the project stay in draft as long as it needs to. ===== BUILD_PLAN.md ===== # Build plan · Carbonmade A static portfolio generator whose hard problems are an image pipeline that respects the originals and the phone, a publish that either completes or does not happen, and a URL you can keep sending to clients after you rename a project. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Content on disk that outlives the generator Everything the site is made of lives in plain files you could edit with any text editor. ### Steps 1. Make folders canonical and the database derived Your content should not need this program to be readable. Files: `src/content/scan.ts`, `tests/rebuild.test.ts` 2. Put the slug in the metadata, not the folder name Folders get tidied; URLs you sent people do not. Files: `src/content/schema.ts` 3. Report unreferenced files instead of ignoring them A missing image is usually a typo, not a decision. Files: `src/content/scan.ts` ### Done when - [ ] A rebuild from scratch matches the content hash - [ ] Renaming a folder does not change a URL - [ ] Invalid metadata reports file and line - [ ] Unreferenced images in a project folder are reported ### Watch out - Keeping the canonical content in a database and the images in an uploads folder means the day you abandon this generator your portfolio is trapped in a schema, which is precisely the situation you left the paid product to avoid. Keep the folders canonical, treat the database as derived, and prove it by deleting the database and rebuilding byte-identical output. ## Phase 2 · Derivatives from originals nobody touches Every displayed image is generated from an untouched original at a fixed ladder of widths. ### Steps 1. Apply orientation once and strip the tag Otherwise the browser rotates an already rotated image. Files: `src/images/derive.ts`, `tests/orientation.test.ts` 2. Cache on original hash plus ladder settings A rebuild of an unchanged portfolio should be fast. Files: `src/images/cache.ts` 3. Assert the originals are never written to This is the file you cannot get back. Files: `tests/originals-immutable.test.ts` ### Done when - [ ] A rotated fixture renders upright with no tag in the output - [ ] Originals are byte-identical after a build - [ ] Unchanged content regenerates no derivatives - [ ] Derivative size per project is reported ### Watch out - Resizing with a library that ignores the orientation tag turns a third of a photographer's portfolio sideways, and nobody notices before launch because the operating system's viewer applies the tag and shows the originals correctly. Read the tag, bake the rotation into the derivative, strip the tag from the output so the browser does not rotate it a second time, and put a deliberately rotated photograph in the fixtures. - Writing the resized version over the original saves disk and destroys the one artefact you cannot regenerate, usually in a script somebody ran to fix something else. Make the originals directory read only in the pipeline and assert it in a test. ## Phase 3 · The page that loads fast on a phone Each page declares exactly one priority image and everything else defers, with the build enforcing it. ### Steps 1. Declare one priority image per page and enforce it The hero is the number that decides how the site feels. Files: `src/render/image.ts`, `tests/priority-image.test.ts` 2. Emit intrinsic dimensions on every image Reflow is the other half of a slow-feeling page. Files: `src/render/image.ts` 3. Record a measured load figure per build A number nobody records is a regression nobody notices. Files: `scripts/measure.mjs`, `src/build/record.ts` ### Done when - [ ] Every image has dimensions, srcset and sizes - [ ] Exactly one priority image per page, never lazy - [ ] Zero or two priority images fails the build - [ ] A measured load figure is recorded per build ### Watch out - Adding loading lazy to every image because it is the obvious optimisation puts the hero, which on a portfolio is almost always the largest contentful paint element, behind layout and the published guidance is explicit that an above the fold image with lazy loading will likely have a large negative effect on that metric. Choose one priority image per page, give it fetchpriority high and no lazy attribute, and lazy load only what is genuinely below the fold. - Omitting width and height means the page reflows as each image arrives, which on a slow phone connection is the difference between a portfolio and a slideshow of jumping boxes. Emit intrinsic dimensions from the stored values on every single image, and fail the build when one is missing. ## Phase 4 · Editing without breaking the site A local admin edits metadata, reorders images and previews, and cannot publish something invalid. ### Steps 1. Preview through the real build Two renderers means two behaviours and one surprise. Files: `src/admin/preview.ts` 2. Block publish on missing alt text, naming the image It is the only accessibility rule that matters here. Files: `src/build/validate.ts`, `tests/alt-text.test.ts` 3. Write ordering back to the metadata files The folders are canonical, including the order. Files: `src/admin/reorder.ts` ### Done when - [ ] Preview and publish share the build code - [ ] Publishing without alt text is refused by name - [ ] Reordering persists to the metadata files - [ ] Draft slugs appear nowhere in the build output ### Watch out - A preview served by a different code path from the build is the reason things look right locally and wrong live, and you only find out after you have sent the link. Run the real build into a temporary directory and serve that, even though it is slower, because the alternative is a class of bug you cannot reproduce. - Making alt text optional means it is never written, and a portfolio is the one kind of site that is entirely images. Block publishing on a missing alt text, name the image, and let the project stay in draft as long as it needs to. ## Not in this build - A general purpose visual editor. - Client collaboration and approvals. - Hosted forms and analytics. - A template marketplace. ## After v1, if you want it - Case study layouts with per-project template overrides, once the base renderer is stable enough to be worth overriding - A password-protected draft URL for work you cannot show publicly yet, which is a hosting feature more than a build one - An archive page generated from the builds table, so the portfolio's own history is a page rather than a folder ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Project folders and originals. This is the portfolio; back it up first. CONTENT_DIR=./content # Required. Numbered build outputs. The previous one is kept for rollback. BUILD_DIR=./builds # Required. The ladder. Changing it invalidates the derivative cache. DERIVATIVE_WIDTHS=480,960,1440,2048 # Required. Primary output format. A fallback is always emitted alongside. DERIVATIVE_FORMAT=avif # Required. Used for canonical links, the sitemap and redirect targets. SITE_URL=https://example.com # Required. How many previous builds stay on disk for rollback. BUILDS_KEPT=3
You are building a lean indie version of Carbonmade. 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 ===== # Carbonmade · indie build A static portfolio generator whose hard problems are an image pipeline that respects the originals and the phone, a publish that either completes or does not happen, and a URL you can keep sending to clients after you rename a project. 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 | Projects as folders on disk, one metadata file each, images beside them | the content has to outlive whatever generator you get bored of | | Generator | A local static build producing plain files, nothing running in production | a portfolio should not have a server you are responsible for patching | | Images | One derivative ladder generated from untouched originals | originals are the only thing in this project you cannot regenerate | | Editor | A local admin that edits metadata and ordering, never raw markup | an editor that writes HTML becomes the only thing that can read it | | Publish | Build into a fresh directory, verify, then swap, keeping the previous build | a half uploaded portfolio is worse than an old one | ## 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 generator, the admin and the image pipeline. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **An image processing library that reads orientation metadata** · free - Why: The whole derivative pipeline rests on it, and the ones that quietly drop orientation are the reason portfolios launch sideways. - Get it: Choose a maintained native library for your runtime and read its documentation on metadata handling before you build the ladder. - Verify: A deliberately rotated fixture comes out upright with no orientation tag - [ ] **Somewhere to put static files, with a custom domain** · usually free at portfolio traffic; the domain is the real cost - Why: Publishing is a file upload, and the domain is the thing that makes the portfolio yours rather than a tenant of someone else's brand. - Get it: Any static host or object storage with a CDN in front, plus your own domain pointed at it. - Verify: A published build is reachable at your domain over https - [ ] **Your actual originals, including the awkward ones** · free - Why: A pipeline tested on three tidy exports fails on the panorama, the portrait scan and the thirty megabyte camera file that are already in your archive. - Get it: Copy a representative selection into the content directory, including at least one rotated photograph and one very large file. - Verify: A full build completes and reports derivative sizes per project - [ ] **What counts as above the fold on your layout** · free - Why: The priority image rule needs a definition, and deciding it per page after the fact means deciding it inconsistently. - Get it: Write down the rule, for example the project cover on a project page and the first grid item on the index. - Verify: The rule is in the README and enforced by the build ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - A general purpose visual editor. - Client collaboration and approvals. - Hosted forms and analytics. - A template marketplace. - general-purpose visual editing - large template and component libraries - CMS collaboration and client handoff - managed hosting, forms, and analytics - high-fidelity color, format, and export handling If one of those is essential to you, that is the reason to keep paying for Carbonmade, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Carbonmade 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 publisher that turns project folders into a static site you own to replace Carbonmade. 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: Projects as folders on disk, one metadata file each, images beside them · the content has to outlive whatever generator you get bored of - Generator: A local static build producing plain files, nothing running in production · a portfolio should not have a server you are responsible for patching - Images: One derivative ladder generated from untouched originals · originals are the only thing in this project you cannot regenerate - Editor: A local admin that edits metadata and ordering, never raw markup · an editor that writes HTML becomes the only thing that can read it - Publish: Build into a fresh directory, verify, then swap, keeping the previous build · a half uploaded portfolio is worse than an old one ### Data model (create this before Phase 1) - projects(slug, title, year, client, summary, body_md, cover_media_id, ordinal, status) · status is draft or published - media(id, project_id, original_path, original_bytes, width, height, exif_orientation, sha256, alt_text, ordinal) - derivatives(media_id, width, format, path, bytes) · regenerated from the original, never edited in place, safe to delete - pages(slug, title, body_md, ordinal) · the about and contact pages every portfolio has - builds(id, started_at, finished_at, output_dir, content_hash, status, bytes, page_count, lcp_ms) - redirects(from_path, to_path, code, created_at) · written automatically whenever a published slug changes - alt_text is a column rather than an optional front matter key, and a published image without one fails the build. ### Phase 1 · Content on disk that outlives the generator Build: One directory per project holding a metadata file and the original images. The database is a cache built from the directory, not the source of truth: deleting it and re-scanning produces the same site. Slugs come from a field in the metadata file, not from the folder name, so a folder can be renamed without changing a URL. Trap: Keeping the canonical content in a database and the images in an uploads folder means the day you abandon this generator your portfolio is trapped in a schema, which is precisely the situation you left the paid product to avoid. Keep the folders canonical, treat the database as derived, and prove it by deleting the database and rebuilding byte-identical output. Done when: Deleting the database and re-scanning the content directory produces a build with the same content hash, tested. A project folder renamed on disk keeps its URL because the slug is in the metadata, tested. Every metadata file is valid against a schema and the scan reports the file and line on failure. An image sitting in a project folder but absent from the metadata is reported rather than silently ignored. Do not build yet: No image pipeline, page templates or publishing yet. ### Phase 2 · Derivatives from originals nobody touches Build: Read each original's dimensions and orientation tag once. Generate a fixed set of widths in a modern format plus a fallback, writing the orientation into the pixels and stripping the tag from the output. Cache by the original's hash plus the ladder settings so a rebuild does not redo the work, and never write back into the original file. Trap: Resizing with a library that ignores the orientation tag turns a third of a photographer's portfolio sideways, and nobody notices before launch because the operating system's viewer applies the tag and shows the originals correctly. Read the tag, bake the rotation into the derivative, strip the tag from the output so the browser does not rotate it a second time, and put a deliberately rotated photograph in the fixtures. Trap: Writing the resized version over the original saves disk and destroys the one artefact you cannot regenerate, usually in a script somebody ran to fix something else. Make the originals directory read only in the pipeline and assert it in a test. Done when: A fixture photograph with a non-default orientation tag renders upright in every derivative and the output carries no orientation tag, tested by reading the output metadata. Original files are byte-identical before and after a full build, asserted by hash. Changing the ladder settings regenerates derivatives and changing nothing regenerates none, both tested. Derivative total size per project is reported at build time. Do not build yet: No templates, editing or publishing yet. ### Phase 3 · The page that loads fast on a phone Build: Emit width, height, srcset and sizes on every image so nothing reflows. Mark exactly one image per page as the priority image: no lazy attribute, fetchpriority high, and preloaded. Everything below gets lazy loading. Fail the build if a page declares two priority images or none, and record a measured load figure per build so a regression is visible. Trap: Adding loading lazy to every image because it is the obvious optimisation puts the hero, which on a portfolio is almost always the largest contentful paint element, behind layout and the published guidance is explicit that an above the fold image with lazy loading will likely have a large negative effect on that metric. Choose one priority image per page, give it fetchpriority high and no lazy attribute, and lazy load only what is genuinely below the fold. Trap: Omitting width and height means the page reflows as each image arrives, which on a slow phone connection is the difference between a portfolio and a slideshow of jumping boxes. Emit intrinsic dimensions from the stored values on every single image, and fail the build when one is missing. Done when: Every image element in the output carries width, height, srcset and sizes, asserted by parsing the built HTML. Exactly one image per page carries the priority attributes and none of them carries lazy loading, asserted. A page with zero or two priority images fails the build with the page named. A measured load figure on a throttled connection is recorded per build and the previous build's figure is shown alongside it. Do not build yet: No editor, publishing or redirects yet. ### Phase 4 · Editing without breaking the site Build: Edit project metadata and body text, drag to reorder projects and images, set the cover, and write alt text. Preview builds against the same renderer as production so preview and live cannot diverge. Validation runs on save and blocks publishing rather than blocking editing, so a half-finished project can sit in draft. Trap: A preview served by a different code path from the build is the reason things look right locally and wrong live, and you only find out after you have sent the link. Run the real build into a temporary directory and serve that, even though it is slower, because the alternative is a class of bug you cannot reproduce. Trap: Making alt text optional means it is never written, and a portfolio is the one kind of site that is entirely images. Block publishing on a missing alt text, name the image, and let the project stay in draft as long as it needs to. Done when: Preview is produced by the same build code as publishing, verified by review and by comparing output for a fixture project. Publishing a project with an image missing alt text is refused and the image is named. Reordering in the admin writes the ordinal back into the metadata file on disk, not only into the database. A draft project appears in no built output, asserted by searching the build directory for its slug. Do not build yet: No publishing, redirects or rollback yet. ### Phase 5 · Publish, roll back, keep the links working Build: Build into a fresh directory, verify it, then swap it into place and keep the previous build for rollback. Record a redirect automatically whenever a published slug changes, and refuse to publish when a previously published slug has neither a page nor a redirect. Ship a useful 404 and a rollback command that swaps the previous build back in one step. Trap: Uploading file by file over the live site means a visitor during the upload gets new HTML pointing at images that are not there yet, and a failure halfway leaves that state permanently. Build complete, verify complete, swap complete, and keep the previous directory so rollback is a rename rather than a rebuild. Trap: Renaming a project to fix a typo changes its URL and every link in every email you sent a client goes to a 404, which you discover when one of them mentions it months later. Diff published slugs against the last build, write the redirect yourself, and block a publish that would orphan a slug with no redirect. Done when: A publish interrupted part way leaves the previously published site serving unchanged, tested by killing the process mid-swap. Renaming a published project writes a redirect automatically and the old URL resolves after publishing, tested end to end. Rollback restores the previous build and is exercised in the suite rather than documented. The build refuses to publish when a previously published slug has no page and no redirect, naming the slug. Do not build yet: Nothing after this. ### Out of scope (and why) - A general purpose visual editor, which is a different product with a much larger surface and is what the paid tools actually sell. - Client collaboration and approvals, which need accounts, permissions and a hosted service you would then have to keep running. - Hosted contact forms and analytics, which turn a static site into a service with data obligations; link to something else or run a separate small service. - A template marketplace, which is content work rather than engineering and is where the polish you are comparing against comes from. ### README must contain - That the project folders are canonical and the database is a cache you can delete and rebuild - That originals are never modified and every displayed image is a generated derivative - That exactly one image per page is the priority image and why lazy loading the hero is the wrong default - That publishing builds, verifies and swaps, that the previous build is kept, and the rollback command - That renaming a project writes a redirect automatically and that publishing is blocked if one is missing ===== AGENTS.md ===== # Agent instructions · Carbonmade indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Projects as folders on disk, one metadata file each, images beside them, A local static build producing plain files, nothing running in production, One derivative ladder generated from untouched originals, A local admin that edits metadata and ordering, never raw markup, Build into a fresh directory, verify, then swap, keeping the previous build. 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 - Keeping the canonical content in a database and the images in an uploads folder means the day you abandon this generator your portfolio is trapped in a schema, which is precisely the situation you left the paid product to avoid. Keep the folders canonical, treat the database as derived, and prove it by deleting the database and rebuilding byte-identical output. - Resizing with a library that ignores the orientation tag turns a third of a photographer's portfolio sideways, and nobody notices before launch because the operating system's viewer applies the tag and shows the originals correctly. Read the tag, bake the rotation into the derivative, strip the tag from the output so the browser does not rotate it a second time, and put a deliberately rotated photograph in the fixtures. - Writing the resized version over the original saves disk and destroys the one artefact you cannot regenerate, usually in a script somebody ran to fix something else. Make the originals directory read only in the pipeline and assert it in a test. - Adding loading lazy to every image because it is the obvious optimisation puts the hero, which on a portfolio is almost always the largest contentful paint element, behind layout and the published guidance is explicit that an above the fold image with lazy loading will likely have a large negative effect on that metric. Choose one priority image per page, give it fetchpriority high and no lazy attribute, and lazy load only what is genuinely below the fold. - Omitting width and height means the page reflows as each image arrives, which on a slow phone connection is the difference between a portfolio and a slideshow of jumping boxes. Emit intrinsic dimensions from the stored values on every single image, and fail the build when one is missing. - A preview served by a different code path from the build is the reason things look right locally and wrong live, and you only find out after you have sent the link. Run the real build into a temporary directory and serve that, even though it is slower, because the alternative is a class of bug you cannot reproduce. - Making alt text optional means it is never written, and a portfolio is the one kind of site that is entirely images. Block publishing on a missing alt text, name the image, and let the project stay in draft as long as it needs to. ===== BUILD_PLAN.md ===== # Build plan · Carbonmade A static portfolio generator whose hard problems are an image pipeline that respects the originals and the phone, a publish that either completes or does not happen, and a URL you can keep sending to clients after you rename a project. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Content on disk that outlives the generator Everything the site is made of lives in plain files you could edit with any text editor. ### Steps 1. Make folders canonical and the database derived Your content should not need this program to be readable. Files: `src/content/scan.ts`, `tests/rebuild.test.ts` 2. Put the slug in the metadata, not the folder name Folders get tidied; URLs you sent people do not. Files: `src/content/schema.ts` 3. Report unreferenced files instead of ignoring them A missing image is usually a typo, not a decision. Files: `src/content/scan.ts` ### Done when - [ ] A rebuild from scratch matches the content hash - [ ] Renaming a folder does not change a URL - [ ] Invalid metadata reports file and line - [ ] Unreferenced images in a project folder are reported ### Watch out - Keeping the canonical content in a database and the images in an uploads folder means the day you abandon this generator your portfolio is trapped in a schema, which is precisely the situation you left the paid product to avoid. Keep the folders canonical, treat the database as derived, and prove it by deleting the database and rebuilding byte-identical output. ## Phase 2 · Derivatives from originals nobody touches Every displayed image is generated from an untouched original at a fixed ladder of widths. ### Steps 1. Apply orientation once and strip the tag Otherwise the browser rotates an already rotated image. Files: `src/images/derive.ts`, `tests/orientation.test.ts` 2. Cache on original hash plus ladder settings A rebuild of an unchanged portfolio should be fast. Files: `src/images/cache.ts` 3. Assert the originals are never written to This is the file you cannot get back. Files: `tests/originals-immutable.test.ts` ### Done when - [ ] A rotated fixture renders upright with no tag in the output - [ ] Originals are byte-identical after a build - [ ] Unchanged content regenerates no derivatives - [ ] Derivative size per project is reported ### Watch out - Resizing with a library that ignores the orientation tag turns a third of a photographer's portfolio sideways, and nobody notices before launch because the operating system's viewer applies the tag and shows the originals correctly. Read the tag, bake the rotation into the derivative, strip the tag from the output so the browser does not rotate it a second time, and put a deliberately rotated photograph in the fixtures. - Writing the resized version over the original saves disk and destroys the one artefact you cannot regenerate, usually in a script somebody ran to fix something else. Make the originals directory read only in the pipeline and assert it in a test. ## Phase 3 · The page that loads fast on a phone Each page declares exactly one priority image and everything else defers, with the build enforcing it. ### Steps 1. Declare one priority image per page and enforce it The hero is the number that decides how the site feels. Files: `src/render/image.ts`, `tests/priority-image.test.ts` 2. Emit intrinsic dimensions on every image Reflow is the other half of a slow-feeling page. Files: `src/render/image.ts` 3. Record a measured load figure per build A number nobody records is a regression nobody notices. Files: `scripts/measure.mjs`, `src/build/record.ts` ### Done when - [ ] Every image has dimensions, srcset and sizes - [ ] Exactly one priority image per page, never lazy - [ ] Zero or two priority images fails the build - [ ] A measured load figure is recorded per build ### Watch out - Adding loading lazy to every image because it is the obvious optimisation puts the hero, which on a portfolio is almost always the largest contentful paint element, behind layout and the published guidance is explicit that an above the fold image with lazy loading will likely have a large negative effect on that metric. Choose one priority image per page, give it fetchpriority high and no lazy attribute, and lazy load only what is genuinely below the fold. - Omitting width and height means the page reflows as each image arrives, which on a slow phone connection is the difference between a portfolio and a slideshow of jumping boxes. Emit intrinsic dimensions from the stored values on every single image, and fail the build when one is missing. ## Phase 4 · Editing without breaking the site A local admin edits metadata, reorders images and previews, and cannot publish something invalid. ### Steps 1. Preview through the real build Two renderers means two behaviours and one surprise. Files: `src/admin/preview.ts` 2. Block publish on missing alt text, naming the image It is the only accessibility rule that matters here. Files: `src/build/validate.ts`, `tests/alt-text.test.ts` 3. Write ordering back to the metadata files The folders are canonical, including the order. Files: `src/admin/reorder.ts` ### Done when - [ ] Preview and publish share the build code - [ ] Publishing without alt text is refused by name - [ ] Reordering persists to the metadata files - [ ] Draft slugs appear nowhere in the build output ### Watch out - A preview served by a different code path from the build is the reason things look right locally and wrong live, and you only find out after you have sent the link. Run the real build into a temporary directory and serve that, even though it is slower, because the alternative is a class of bug you cannot reproduce. - Making alt text optional means it is never written, and a portfolio is the one kind of site that is entirely images. Block publishing on a missing alt text, name the image, and let the project stay in draft as long as it needs to. ## Not in this build - A general purpose visual editor. - Client collaboration and approvals. - Hosted forms and analytics. - A template marketplace. ## After v1, if you want it - Case study layouts with per-project template overrides, once the base renderer is stable enough to be worth overriding - A password-protected draft URL for work you cannot show publicly yet, which is a hosting feature more than a build one - An archive page generated from the builds table, so the portfolio's own history is a page rather than a folder ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Project folders and originals. This is the portfolio; back it up first. CONTENT_DIR=./content # Required. Numbered build outputs. The previous one is kept for rollback. BUILD_DIR=./builds # Required. The ladder. Changing it invalidates the derivative cache. DERIVATIVE_WIDTHS=480,960,1440,2048 # Required. Primary output format. A fallback is always emitted alongside. DERIVATIVE_FORMAT=avif # Required. Used for canonical links, the sitemap and redirect targets. SITE_URL=https://example.com # Required. How many previous builds stay on disk for rollback. BUILDS_KEPT=3
You are building a production product version of Carbonmade. 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 ===== # Carbonmade · product brief ## Problem The visible portfolio builder loop is buildable, but a credible replacement needs more than the first screen. Carbonmade 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 plain folders, whose images come from untouched originals in the right orientation, whose pages declare one priority image, and whose publish either completes or never happened. ## 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 - A general purpose visual editor. - Client collaboration and approvals. - Hosted forms and analytics. - A template marketplace. - general-purpose visual editing - large template and component libraries - CMS collaboration and client handoff - managed hosting, forms, and analytics - high-fidelity color, format, and export handling ## Success criteria - A rebuild from an empty cache is confirmed to match the content hash - Originals are confirmed byte-identical after a build - Exactly one priority image per page is confirmed and none is lazy loaded - An interrupted publish is confirmed to leave the live site unchanged - A renamed project's old URL is confirmed to resolve ===== BRIEF.md ===== # Build brief · Carbonmade 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 publisher that turns project folders into a static site you own to replace Carbonmade. 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: Projects as folders on disk, one metadata file each, images beside them · the content has to outlive whatever generator you get bored of - Generator: A local static build producing plain files, nothing running in production · a portfolio should not have a server you are responsible for patching - Images: One derivative ladder generated from untouched originals · originals are the only thing in this project you cannot regenerate - Editor: A local admin that edits metadata and ordering, never raw markup · an editor that writes HTML becomes the only thing that can read it - Publish: Build into a fresh directory, verify, then swap, keeping the previous build · a half uploaded portfolio is worse than an old one ### Data model (create this before Phase 1) - projects(slug, title, year, client, summary, body_md, cover_media_id, ordinal, status) · status is draft or published - media(id, project_id, original_path, original_bytes, width, height, exif_orientation, sha256, alt_text, ordinal) - derivatives(media_id, width, format, path, bytes) · regenerated from the original, never edited in place, safe to delete - pages(slug, title, body_md, ordinal) · the about and contact pages every portfolio has - builds(id, started_at, finished_at, output_dir, content_hash, status, bytes, page_count, lcp_ms) - redirects(from_path, to_path, code, created_at) · written automatically whenever a published slug changes - alt_text is a column rather than an optional front matter key, and a published image without one fails the build. ### Phase 1 · Content on disk that outlives the generator Build: One directory per project holding a metadata file and the original images. The database is a cache built from the directory, not the source of truth: deleting it and re-scanning produces the same site. Slugs come from a field in the metadata file, not from the folder name, so a folder can be renamed without changing a URL. Trap: Keeping the canonical content in a database and the images in an uploads folder means the day you abandon this generator your portfolio is trapped in a schema, which is precisely the situation you left the paid product to avoid. Keep the folders canonical, treat the database as derived, and prove it by deleting the database and rebuilding byte-identical output. Done when: Deleting the database and re-scanning the content directory produces a build with the same content hash, tested. A project folder renamed on disk keeps its URL because the slug is in the metadata, tested. Every metadata file is valid against a schema and the scan reports the file and line on failure. An image sitting in a project folder but absent from the metadata is reported rather than silently ignored. Do not build yet: No image pipeline, page templates or publishing yet. ### Phase 2 · Derivatives from originals nobody touches Build: Read each original's dimensions and orientation tag once. Generate a fixed set of widths in a modern format plus a fallback, writing the orientation into the pixels and stripping the tag from the output. Cache by the original's hash plus the ladder settings so a rebuild does not redo the work, and never write back into the original file. Trap: Resizing with a library that ignores the orientation tag turns a third of a photographer's portfolio sideways, and nobody notices before launch because the operating system's viewer applies the tag and shows the originals correctly. Read the tag, bake the rotation into the derivative, strip the tag from the output so the browser does not rotate it a second time, and put a deliberately rotated photograph in the fixtures. Trap: Writing the resized version over the original saves disk and destroys the one artefact you cannot regenerate, usually in a script somebody ran to fix something else. Make the originals directory read only in the pipeline and assert it in a test. Done when: A fixture photograph with a non-default orientation tag renders upright in every derivative and the output carries no orientation tag, tested by reading the output metadata. Original files are byte-identical before and after a full build, asserted by hash. Changing the ladder settings regenerates derivatives and changing nothing regenerates none, both tested. Derivative total size per project is reported at build time. Do not build yet: No templates, editing or publishing yet. ### Phase 3 · The page that loads fast on a phone Build: Emit width, height, srcset and sizes on every image so nothing reflows. Mark exactly one image per page as the priority image: no lazy attribute, fetchpriority high, and preloaded. Everything below gets lazy loading. Fail the build if a page declares two priority images or none, and record a measured load figure per build so a regression is visible. Trap: Adding loading lazy to every image because it is the obvious optimisation puts the hero, which on a portfolio is almost always the largest contentful paint element, behind layout and the published guidance is explicit that an above the fold image with lazy loading will likely have a large negative effect on that metric. Choose one priority image per page, give it fetchpriority high and no lazy attribute, and lazy load only what is genuinely below the fold. Trap: Omitting width and height means the page reflows as each image arrives, which on a slow phone connection is the difference between a portfolio and a slideshow of jumping boxes. Emit intrinsic dimensions from the stored values on every single image, and fail the build when one is missing. Done when: Every image element in the output carries width, height, srcset and sizes, asserted by parsing the built HTML. Exactly one image per page carries the priority attributes and none of them carries lazy loading, asserted. A page with zero or two priority images fails the build with the page named. A measured load figure on a throttled connection is recorded per build and the previous build's figure is shown alongside it. Do not build yet: No editor, publishing or redirects yet. ### Phase 4 · Editing without breaking the site Build: Edit project metadata and body text, drag to reorder projects and images, set the cover, and write alt text. Preview builds against the same renderer as production so preview and live cannot diverge. Validation runs on save and blocks publishing rather than blocking editing, so a half-finished project can sit in draft. Trap: A preview served by a different code path from the build is the reason things look right locally and wrong live, and you only find out after you have sent the link. Run the real build into a temporary directory and serve that, even though it is slower, because the alternative is a class of bug you cannot reproduce. Trap: Making alt text optional means it is never written, and a portfolio is the one kind of site that is entirely images. Block publishing on a missing alt text, name the image, and let the project stay in draft as long as it needs to. Done when: Preview is produced by the same build code as publishing, verified by review and by comparing output for a fixture project. Publishing a project with an image missing alt text is refused and the image is named. Reordering in the admin writes the ordinal back into the metadata file on disk, not only into the database. A draft project appears in no built output, asserted by searching the build directory for its slug. Do not build yet: No publishing, redirects or rollback yet. ### Phase 5 · Publish, roll back, keep the links working Build: Build into a fresh directory, verify it, then swap it into place and keep the previous build for rollback. Record a redirect automatically whenever a published slug changes, and refuse to publish when a previously published slug has neither a page nor a redirect. Ship a useful 404 and a rollback command that swaps the previous build back in one step. Trap: Uploading file by file over the live site means a visitor during the upload gets new HTML pointing at images that are not there yet, and a failure halfway leaves that state permanently. Build complete, verify complete, swap complete, and keep the previous directory so rollback is a rename rather than a rebuild. Trap: Renaming a project to fix a typo changes its URL and every link in every email you sent a client goes to a 404, which you discover when one of them mentions it months later. Diff published slugs against the last build, write the redirect yourself, and block a publish that would orphan a slug with no redirect. Done when: A publish interrupted part way leaves the previously published site serving unchanged, tested by killing the process mid-swap. Renaming a published project writes a redirect automatically and the old URL resolves after publishing, tested end to end. Rollback restores the previous build and is exercised in the suite rather than documented. The build refuses to publish when a previously published slug has no page and no redirect, naming the slug. Do not build yet: Nothing after this. ### Out of scope (and why) - A general purpose visual editor, which is a different product with a much larger surface and is what the paid tools actually sell. - Client collaboration and approvals, which need accounts, permissions and a hosted service you would then have to keep running. - Hosted contact forms and analytics, which turn a static site into a service with data obligations; link to something else or run a separate small service. - A template marketplace, which is content work rather than engineering and is where the polish you are comparing against comes from. ### README must contain - That the project folders are canonical and the database is a cache you can delete and rebuild - That originals are never modified and every displayed image is a generated derivative - That exactly one image per page is the priority image and why lazy loading the hero is the wrong default - That publishing builds, verifies and swaps, that the previous build is kept, and the rollback command - That renaming a project writes a redirect automatically and that publishing is blocked if one is missing ===== ARCHITECTURE.md ===== # Architecture · Carbonmade ## Stack | Part | Choice | Why | | --- | --- | --- | | Content | Projects as folders on disk, one metadata file each, images beside them | the content has to outlive whatever generator you get bored of | | Generator | A local static build producing plain files, nothing running in production | a portfolio should not have a server you are responsible for patching | | Images | One derivative ladder generated from untouched originals | originals are the only thing in this project you cannot regenerate | | Editor | A local admin that edits metadata and ordering, never raw markup | an editor that writes HTML becomes the only thing that can read it | | Publish | Build into a fresh directory, verify, then swap, keeping the previous build | a half uploaded portfolio is worse than an old one | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Content | Scanning folders, schema validation, slugs. | Any front matter format, provided the folders stay canonical. | | Images | Orientation, the width ladder and the cache. | Any library, provided originals are never written to. | | Render | Templates, image markup, priority rules. | Any templating, provided the build enforces one priority image. | | Admin | Editing, ordering, alt text and preview. | Any interface, provided preview runs the real build. | | Publish | Verify, swap, redirects and rollback. | Any host, provided the swap is atomic and the previous build is kept. | ## 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 · Project folders and originals. This is the portfolio; back it up first. - `BUILD_DIR` · required · Numbered build outputs. The previous one is kept for rollback. - `DERIVATIVE_WIDTHS` · required · The ladder. Changing it invalidates the derivative cache. - `DERIVATIVE_FORMAT` · required · Primary output format. A fallback is always emitted alongside. - `SITE_URL` · required · Used for canonical links, the sitemap and redirect targets. - `BUILDS_KEPT` · required · How many previous builds stay on disk for rollback. ## 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 · Carbonmade product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Projects as folders on disk, one metadata file each, images beside them, A local static build producing plain files, nothing running in production, One derivative ladder generated from untouched originals, A local admin that edits metadata and ordering, never raw markup, Build into a fresh directory, verify, then swap, keeping the previous build. - 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 - Keeping the canonical content in a database and the images in an uploads folder means the day you abandon this generator your portfolio is trapped in a schema, which is precisely the situation you left the paid product to avoid. Keep the folders canonical, treat the database as derived, and prove it by deleting the database and rebuilding byte-identical output. - Resizing with a library that ignores the orientation tag turns a third of a photographer's portfolio sideways, and nobody notices before launch because the operating system's viewer applies the tag and shows the originals correctly. Read the tag, bake the rotation into the derivative, strip the tag from the output so the browser does not rotate it a second time, and put a deliberately rotated photograph in the fixtures. - Writing the resized version over the original saves disk and destroys the one artefact you cannot regenerate, usually in a script somebody ran to fix something else. Make the originals directory read only in the pipeline and assert it in a test. - Adding loading lazy to every image because it is the obvious optimisation puts the hero, which on a portfolio is almost always the largest contentful paint element, behind layout and the published guidance is explicit that an above the fold image with lazy loading will likely have a large negative effect on that metric. Choose one priority image per page, give it fetchpriority high and no lazy attribute, and lazy load only what is genuinely below the fold. - Omitting width and height means the page reflows as each image arrives, which on a slow phone connection is the difference between a portfolio and a slideshow of jumping boxes. Emit intrinsic dimensions from the stored values on every single image, and fail the build when one is missing. - A preview served by a different code path from the build is the reason things look right locally and wrong live, and you only find out after you have sent the link. Run the real build into a temporary directory and serve that, even though it is slower, because the alternative is a class of bug you cannot reproduce. - Making alt text optional means it is never written, and a portfolio is the one kind of site that is entirely images. Block publishing on a missing alt text, name the image, and let the project stay in draft as long as it needs to. - Uploading file by file over the live site means a visitor during the upload gets new HTML pointing at images that are not there yet, and a failure halfway leaves that state permanently. Build complete, verify complete, swap complete, and keep the previous directory so rollback is a rename rather than a rebuild. - Renaming a project to fix a typo changes its URL and every link in every email you sent a client goes to a 404, which you discover when one of them mentions it months later. Diff published slugs against the last build, write the redirect yourself, and block a publish that would orphan a slug with no redirect. ===== MILESTONES.md ===== # Delivery milestones · Carbonmade Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Content on disk that outlives the generator Everything the site is made of lives in plain files you could edit with any text editor. ### Steps 1. Make folders canonical and the database derived Your content should not need this program to be readable. Files: `src/content/scan.ts`, `tests/rebuild.test.ts` 2. Put the slug in the metadata, not the folder name Folders get tidied; URLs you sent people do not. Files: `src/content/schema.ts` 3. Report unreferenced files instead of ignoring them A missing image is usually a typo, not a decision. Files: `src/content/scan.ts` ### Done when - [ ] A rebuild from scratch matches the content hash - [ ] Renaming a folder does not change a URL - [ ] Invalid metadata reports file and line - [ ] Unreferenced images in a project folder are reported ### Watch out - Keeping the canonical content in a database and the images in an uploads folder means the day you abandon this generator your portfolio is trapped in a schema, which is precisely the situation you left the paid product to avoid. Keep the folders canonical, treat the database as derived, and prove it by deleting the database and rebuilding byte-identical output. ## M2 · Derivatives from originals nobody touches Every displayed image is generated from an untouched original at a fixed ladder of widths. ### Steps 1. Apply orientation once and strip the tag Otherwise the browser rotates an already rotated image. Files: `src/images/derive.ts`, `tests/orientation.test.ts` 2. Cache on original hash plus ladder settings A rebuild of an unchanged portfolio should be fast. Files: `src/images/cache.ts` 3. Assert the originals are never written to This is the file you cannot get back. Files: `tests/originals-immutable.test.ts` ### Done when - [ ] A rotated fixture renders upright with no tag in the output - [ ] Originals are byte-identical after a build - [ ] Unchanged content regenerates no derivatives - [ ] Derivative size per project is reported ### Watch out - Resizing with a library that ignores the orientation tag turns a third of a photographer's portfolio sideways, and nobody notices before launch because the operating system's viewer applies the tag and shows the originals correctly. Read the tag, bake the rotation into the derivative, strip the tag from the output so the browser does not rotate it a second time, and put a deliberately rotated photograph in the fixtures. - Writing the resized version over the original saves disk and destroys the one artefact you cannot regenerate, usually in a script somebody ran to fix something else. Make the originals directory read only in the pipeline and assert it in a test. ## M3 · The page that loads fast on a phone Each page declares exactly one priority image and everything else defers, with the build enforcing it. ### Steps 1. Declare one priority image per page and enforce it The hero is the number that decides how the site feels. Files: `src/render/image.ts`, `tests/priority-image.test.ts` 2. Emit intrinsic dimensions on every image Reflow is the other half of a slow-feeling page. Files: `src/render/image.ts` 3. Record a measured load figure per build A number nobody records is a regression nobody notices. Files: `scripts/measure.mjs`, `src/build/record.ts` ### Done when - [ ] Every image has dimensions, srcset and sizes - [ ] Exactly one priority image per page, never lazy - [ ] Zero or two priority images fails the build - [ ] A measured load figure is recorded per build ### Watch out - Adding loading lazy to every image because it is the obvious optimisation puts the hero, which on a portfolio is almost always the largest contentful paint element, behind layout and the published guidance is explicit that an above the fold image with lazy loading will likely have a large negative effect on that metric. Choose one priority image per page, give it fetchpriority high and no lazy attribute, and lazy load only what is genuinely below the fold. - Omitting width and height means the page reflows as each image arrives, which on a slow phone connection is the difference between a portfolio and a slideshow of jumping boxes. Emit intrinsic dimensions from the stored values on every single image, and fail the build when one is missing. ## M4 · Editing without breaking the site A local admin edits metadata, reorders images and previews, and cannot publish something invalid. ### Steps 1. Preview through the real build Two renderers means two behaviours and one surprise. Files: `src/admin/preview.ts` 2. Block publish on missing alt text, naming the image It is the only accessibility rule that matters here. Files: `src/build/validate.ts`, `tests/alt-text.test.ts` 3. Write ordering back to the metadata files The folders are canonical, including the order. Files: `src/admin/reorder.ts` ### Done when - [ ] Preview and publish share the build code - [ ] Publishing without alt text is refused by name - [ ] Reordering persists to the metadata files - [ ] Draft slugs appear nowhere in the build output ### Watch out - A preview served by a different code path from the build is the reason things look right locally and wrong live, and you only find out after you have sent the link. Run the real build into a temporary directory and serve that, even though it is slower, because the alternative is a class of bug you cannot reproduce. - Making alt text optional means it is never written, and a portfolio is the one kind of site that is entirely images. Block publishing on a missing alt text, name the image, and let the project stay in draft as long as it needs to. ## M5 · Publish, roll back, keep the links working (production only) Publishing either completes or leaves the old site untouched, and no URL you have ever sent someone breaks. ### Steps 1. Build, verify, swap, keep the previous Nobody should ever see a half published portfolio. Files: `scripts/publish.sh`, `tests/publish.test.ts` 2. Write redirects from the slug diff automatically You will not remember to do this by hand. Files: `src/build/redirects.ts` 3. Exercise rollback in the suite A rollback you have never run is a plan, not a command. Files: `tests/rollback.test.ts` ### Done when - [ ] An interrupted publish leaves the live site unchanged - [ ] A renamed project's old URL still resolves - [ ] Rollback is a single command exercised in tests - [ ] An orphaned published slug blocks the publish ### Watch out - Uploading file by file over the live site means a visitor during the upload gets new HTML pointing at images that are not there yet, and a failure halfway leaves that state permanently. Build complete, verify complete, swap complete, and keep the previous directory so rollback is a rename rather than a rebuild. - Renaming a project to fix a typo changes its URL and every link in every email you sent a client goes to a 404, which you discover when one of them mentions it months later. Diff published slugs against the last build, write the redirect yourself, and block a publish that would orphan a slug with no redirect. ===== OPERATIONS.md ===== # Operations · Carbonmade ## Backup The content directory, and nothing else matters as much. Derivatives and builds regenerate; originals and the words you wrote about the work do not. Keep an offsite copy, because a portfolio is also an archive. ## Restore Restore the content directory, run a full build from an empty cache, and compare the content hash against the last recorded build. If it matches, the restore is complete, which is a cleaner test than checking whether the site looks right. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Build success, the measured load figure per build against the previous one, output size, and an external check that the domain serves the current build. Portfolios break quietly because nobody visits their own site. ## Incident checklist The incidents here are a broken link somebody was sent and a site half replaced by a failed upload. Redirects handle the first and the build-verify-swap handles the second, which is why both are enforced 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 - [ ] A rebuild from an empty cache is confirmed to match the content hash - [ ] Originals are confirmed byte-identical after a build - [ ] Exactly one priority image per page is confirmed and none is lazy loaded - [ ] An interrupted publish is confirmed to leave the live site unchanged - [ ] A renamed project's old URL is confirmed to resolve ## Launch constraint Do not market omitted Carbonmade 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. Project folders and originals. This is the portfolio; back it up first. CONTENT_DIR=./content # Required. Numbered build outputs. The previous one is kept for rollback. BUILD_DIR=./builds # Required. The ladder. Changing it invalidates the derivative cache. DERIVATIVE_WIDTHS=480,960,1440,2048 # Required. Primary output format. A fallback is always emitted alongside. DERIVATIVE_FORMAT=avif # Required. Used for canonical links, the sitemap and redirect targets. SITE_URL=https://example.com # Required. How many previous builds stay on disk for rollback. BUILDS_KEPT=3
# Carbonmade · indie build A static portfolio generator whose hard problems are an image pipeline that respects the originals and the phone, a publish that either completes or does not happen, and a URL you can keep sending to clients after you rename a project. 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 | Projects as folders on disk, one metadata file each, images beside them | the content has to outlive whatever generator you get bored of | | Generator | A local static build producing plain files, nothing running in production | a portfolio should not have a server you are responsible for patching | | Images | One derivative ladder generated from untouched originals | originals are the only thing in this project you cannot regenerate | | Editor | A local admin that edits metadata and ordering, never raw markup | an editor that writes HTML becomes the only thing that can read it | | Publish | Build into a fresh directory, verify, then swap, keeping the previous build | a half uploaded portfolio is worse than an old one | ## 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 generator, the admin and the image pipeline. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **An image processing library that reads orientation metadata** · free - Why: The whole derivative pipeline rests on it, and the ones that quietly drop orientation are the reason portfolios launch sideways. - Get it: Choose a maintained native library for your runtime and read its documentation on metadata handling before you build the ladder. - Verify: A deliberately rotated fixture comes out upright with no orientation tag - [ ] **Somewhere to put static files, with a custom domain** · usually free at portfolio traffic; the domain is the real cost - Why: Publishing is a file upload, and the domain is the thing that makes the portfolio yours rather than a tenant of someone else's brand. - Get it: Any static host or object storage with a CDN in front, plus your own domain pointed at it. - Verify: A published build is reachable at your domain over https - [ ] **Your actual originals, including the awkward ones** · free - Why: A pipeline tested on three tidy exports fails on the panorama, the portrait scan and the thirty megabyte camera file that are already in your archive. - Get it: Copy a representative selection into the content directory, including at least one rotated photograph and one very large file. - Verify: A full build completes and reports derivative sizes per project - [ ] **What counts as above the fold on your layout** · free - Why: The priority image rule needs a definition, and deciding it per page after the fact means deciding it inconsistently. - Get it: Write down the rule, for example the project cover on a project page and the first grid item on the index. - Verify: The rule is in the README and enforced by the build ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - A general purpose visual editor. - Client collaboration and approvals. - Hosted forms and analytics. - A template marketplace. - general-purpose visual editing - large template and component libraries - CMS collaboration and client handoff - managed hosting, forms, and analytics - high-fidelity color, format, and export handling If one of those is essential to you, that is the reason to keep paying for Carbonmade, and the README should say so rather than pretend.
# Build brief · Carbonmade 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 publisher that turns project folders into a static site you own to replace Carbonmade. 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: Projects as folders on disk, one metadata file each, images beside them · the content has to outlive whatever generator you get bored of - Generator: A local static build producing plain files, nothing running in production · a portfolio should not have a server you are responsible for patching - Images: One derivative ladder generated from untouched originals · originals are the only thing in this project you cannot regenerate - Editor: A local admin that edits metadata and ordering, never raw markup · an editor that writes HTML becomes the only thing that can read it - Publish: Build into a fresh directory, verify, then swap, keeping the previous build · a half uploaded portfolio is worse than an old one ### Data model (create this before Phase 1) - projects(slug, title, year, client, summary, body_md, cover_media_id, ordinal, status) · status is draft or published - media(id, project_id, original_path, original_bytes, width, height, exif_orientation, sha256, alt_text, ordinal) - derivatives(media_id, width, format, path, bytes) · regenerated from the original, never edited in place, safe to delete - pages(slug, title, body_md, ordinal) · the about and contact pages every portfolio has - builds(id, started_at, finished_at, output_dir, content_hash, status, bytes, page_count, lcp_ms) - redirects(from_path, to_path, code, created_at) · written automatically whenever a published slug changes - alt_text is a column rather than an optional front matter key, and a published image without one fails the build. ### Phase 1 · Content on disk that outlives the generator Build: One directory per project holding a metadata file and the original images. The database is a cache built from the directory, not the source of truth: deleting it and re-scanning produces the same site. Slugs come from a field in the metadata file, not from the folder name, so a folder can be renamed without changing a URL. Trap: Keeping the canonical content in a database and the images in an uploads folder means the day you abandon this generator your portfolio is trapped in a schema, which is precisely the situation you left the paid product to avoid. Keep the folders canonical, treat the database as derived, and prove it by deleting the database and rebuilding byte-identical output. Done when: Deleting the database and re-scanning the content directory produces a build with the same content hash, tested. A project folder renamed on disk keeps its URL because the slug is in the metadata, tested. Every metadata file is valid against a schema and the scan reports the file and line on failure. An image sitting in a project folder but absent from the metadata is reported rather than silently ignored. Do not build yet: No image pipeline, page templates or publishing yet. ### Phase 2 · Derivatives from originals nobody touches Build: Read each original's dimensions and orientation tag once. Generate a fixed set of widths in a modern format plus a fallback, writing the orientation into the pixels and stripping the tag from the output. Cache by the original's hash plus the ladder settings so a rebuild does not redo the work, and never write back into the original file. Trap: Resizing with a library that ignores the orientation tag turns a third of a photographer's portfolio sideways, and nobody notices before launch because the operating system's viewer applies the tag and shows the originals correctly. Read the tag, bake the rotation into the derivative, strip the tag from the output so the browser does not rotate it a second time, and put a deliberately rotated photograph in the fixtures. Trap: Writing the resized version over the original saves disk and destroys the one artefact you cannot regenerate, usually in a script somebody ran to fix something else. Make the originals directory read only in the pipeline and assert it in a test. Done when: A fixture photograph with a non-default orientation tag renders upright in every derivative and the output carries no orientation tag, tested by reading the output metadata. Original files are byte-identical before and after a full build, asserted by hash. Changing the ladder settings regenerates derivatives and changing nothing regenerates none, both tested. Derivative total size per project is reported at build time. Do not build yet: No templates, editing or publishing yet. ### Phase 3 · The page that loads fast on a phone Build: Emit width, height, srcset and sizes on every image so nothing reflows. Mark exactly one image per page as the priority image: no lazy attribute, fetchpriority high, and preloaded. Everything below gets lazy loading. Fail the build if a page declares two priority images or none, and record a measured load figure per build so a regression is visible. Trap: Adding loading lazy to every image because it is the obvious optimisation puts the hero, which on a portfolio is almost always the largest contentful paint element, behind layout and the published guidance is explicit that an above the fold image with lazy loading will likely have a large negative effect on that metric. Choose one priority image per page, give it fetchpriority high and no lazy attribute, and lazy load only what is genuinely below the fold. Trap: Omitting width and height means the page reflows as each image arrives, which on a slow phone connection is the difference between a portfolio and a slideshow of jumping boxes. Emit intrinsic dimensions from the stored values on every single image, and fail the build when one is missing. Done when: Every image element in the output carries width, height, srcset and sizes, asserted by parsing the built HTML. Exactly one image per page carries the priority attributes and none of them carries lazy loading, asserted. A page with zero or two priority images fails the build with the page named. A measured load figure on a throttled connection is recorded per build and the previous build's figure is shown alongside it. Do not build yet: No editor, publishing or redirects yet. ### Phase 4 · Editing without breaking the site Build: Edit project metadata and body text, drag to reorder projects and images, set the cover, and write alt text. Preview builds against the same renderer as production so preview and live cannot diverge. Validation runs on save and blocks publishing rather than blocking editing, so a half-finished project can sit in draft. Trap: A preview served by a different code path from the build is the reason things look right locally and wrong live, and you only find out after you have sent the link. Run the real build into a temporary directory and serve that, even though it is slower, because the alternative is a class of bug you cannot reproduce. Trap: Making alt text optional means it is never written, and a portfolio is the one kind of site that is entirely images. Block publishing on a missing alt text, name the image, and let the project stay in draft as long as it needs to. Done when: Preview is produced by the same build code as publishing, verified by review and by comparing output for a fixture project. Publishing a project with an image missing alt text is refused and the image is named. Reordering in the admin writes the ordinal back into the metadata file on disk, not only into the database. A draft project appears in no built output, asserted by searching the build directory for its slug. Do not build yet: No publishing, redirects or rollback yet. ### Phase 5 · Publish, roll back, keep the links working Build: Build into a fresh directory, verify it, then swap it into place and keep the previous build for rollback. Record a redirect automatically whenever a published slug changes, and refuse to publish when a previously published slug has neither a page nor a redirect. Ship a useful 404 and a rollback command that swaps the previous build back in one step. Trap: Uploading file by file over the live site means a visitor during the upload gets new HTML pointing at images that are not there yet, and a failure halfway leaves that state permanently. Build complete, verify complete, swap complete, and keep the previous directory so rollback is a rename rather than a rebuild. Trap: Renaming a project to fix a typo changes its URL and every link in every email you sent a client goes to a 404, which you discover when one of them mentions it months later. Diff published slugs against the last build, write the redirect yourself, and block a publish that would orphan a slug with no redirect. Done when: A publish interrupted part way leaves the previously published site serving unchanged, tested by killing the process mid-swap. Renaming a published project writes a redirect automatically and the old URL resolves after publishing, tested end to end. Rollback restores the previous build and is exercised in the suite rather than documented. The build refuses to publish when a previously published slug has no page and no redirect, naming the slug. Do not build yet: Nothing after this. ### Out of scope (and why) - A general purpose visual editor, which is a different product with a much larger surface and is what the paid tools actually sell. - Client collaboration and approvals, which need accounts, permissions and a hosted service you would then have to keep running. - Hosted contact forms and analytics, which turn a static site into a service with data obligations; link to something else or run a separate small service. - A template marketplace, which is content work rather than engineering and is where the polish you are comparing against comes from. ### README must contain - That the project folders are canonical and the database is a cache you can delete and rebuild - That originals are never modified and every displayed image is a generated derivative - That exactly one image per page is the priority image and why lazy loading the hero is the wrong default - That publishing builds, verifies and swaps, that the previous build is kept, and the rollback command - That renaming a project writes a redirect automatically and that publishing is blocked if one is missing
# Agent instructions · Carbonmade indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Projects as folders on disk, one metadata file each, images beside them, A local static build producing plain files, nothing running in production, One derivative ladder generated from untouched originals, A local admin that edits metadata and ordering, never raw markup, Build into a fresh directory, verify, then swap, keeping the previous build. 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 - Keeping the canonical content in a database and the images in an uploads folder means the day you abandon this generator your portfolio is trapped in a schema, which is precisely the situation you left the paid product to avoid. Keep the folders canonical, treat the database as derived, and prove it by deleting the database and rebuilding byte-identical output. - Resizing with a library that ignores the orientation tag turns a third of a photographer's portfolio sideways, and nobody notices before launch because the operating system's viewer applies the tag and shows the originals correctly. Read the tag, bake the rotation into the derivative, strip the tag from the output so the browser does not rotate it a second time, and put a deliberately rotated photograph in the fixtures. - Writing the resized version over the original saves disk and destroys the one artefact you cannot regenerate, usually in a script somebody ran to fix something else. Make the originals directory read only in the pipeline and assert it in a test. - Adding loading lazy to every image because it is the obvious optimisation puts the hero, which on a portfolio is almost always the largest contentful paint element, behind layout and the published guidance is explicit that an above the fold image with lazy loading will likely have a large negative effect on that metric. Choose one priority image per page, give it fetchpriority high and no lazy attribute, and lazy load only what is genuinely below the fold. - Omitting width and height means the page reflows as each image arrives, which on a slow phone connection is the difference between a portfolio and a slideshow of jumping boxes. Emit intrinsic dimensions from the stored values on every single image, and fail the build when one is missing. - A preview served by a different code path from the build is the reason things look right locally and wrong live, and you only find out after you have sent the link. Run the real build into a temporary directory and serve that, even though it is slower, because the alternative is a class of bug you cannot reproduce. - Making alt text optional means it is never written, and a portfolio is the one kind of site that is entirely images. Block publishing on a missing alt text, name the image, and let the project stay in draft as long as it needs to.
# Build plan · Carbonmade A static portfolio generator whose hard problems are an image pipeline that respects the originals and the phone, a publish that either completes or does not happen, and a URL you can keep sending to clients after you rename a project. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Content on disk that outlives the generator Everything the site is made of lives in plain files you could edit with any text editor. ### Steps 1. Make folders canonical and the database derived Your content should not need this program to be readable. Files: `src/content/scan.ts`, `tests/rebuild.test.ts` 2. Put the slug in the metadata, not the folder name Folders get tidied; URLs you sent people do not. Files: `src/content/schema.ts` 3. Report unreferenced files instead of ignoring them A missing image is usually a typo, not a decision. Files: `src/content/scan.ts` ### Done when - [ ] A rebuild from scratch matches the content hash - [ ] Renaming a folder does not change a URL - [ ] Invalid metadata reports file and line - [ ] Unreferenced images in a project folder are reported ### Watch out - Keeping the canonical content in a database and the images in an uploads folder means the day you abandon this generator your portfolio is trapped in a schema, which is precisely the situation you left the paid product to avoid. Keep the folders canonical, treat the database as derived, and prove it by deleting the database and rebuilding byte-identical output. ## Phase 2 · Derivatives from originals nobody touches Every displayed image is generated from an untouched original at a fixed ladder of widths. ### Steps 1. Apply orientation once and strip the tag Otherwise the browser rotates an already rotated image. Files: `src/images/derive.ts`, `tests/orientation.test.ts` 2. Cache on original hash plus ladder settings A rebuild of an unchanged portfolio should be fast. Files: `src/images/cache.ts` 3. Assert the originals are never written to This is the file you cannot get back. Files: `tests/originals-immutable.test.ts` ### Done when - [ ] A rotated fixture renders upright with no tag in the output - [ ] Originals are byte-identical after a build - [ ] Unchanged content regenerates no derivatives - [ ] Derivative size per project is reported ### Watch out - Resizing with a library that ignores the orientation tag turns a third of a photographer's portfolio sideways, and nobody notices before launch because the operating system's viewer applies the tag and shows the originals correctly. Read the tag, bake the rotation into the derivative, strip the tag from the output so the browser does not rotate it a second time, and put a deliberately rotated photograph in the fixtures. - Writing the resized version over the original saves disk and destroys the one artefact you cannot regenerate, usually in a script somebody ran to fix something else. Make the originals directory read only in the pipeline and assert it in a test. ## Phase 3 · The page that loads fast on a phone Each page declares exactly one priority image and everything else defers, with the build enforcing it. ### Steps 1. Declare one priority image per page and enforce it The hero is the number that decides how the site feels. Files: `src/render/image.ts`, `tests/priority-image.test.ts` 2. Emit intrinsic dimensions on every image Reflow is the other half of a slow-feeling page. Files: `src/render/image.ts` 3. Record a measured load figure per build A number nobody records is a regression nobody notices. Files: `scripts/measure.mjs`, `src/build/record.ts` ### Done when - [ ] Every image has dimensions, srcset and sizes - [ ] Exactly one priority image per page, never lazy - [ ] Zero or two priority images fails the build - [ ] A measured load figure is recorded per build ### Watch out - Adding loading lazy to every image because it is the obvious optimisation puts the hero, which on a portfolio is almost always the largest contentful paint element, behind layout and the published guidance is explicit that an above the fold image with lazy loading will likely have a large negative effect on that metric. Choose one priority image per page, give it fetchpriority high and no lazy attribute, and lazy load only what is genuinely below the fold. - Omitting width and height means the page reflows as each image arrives, which on a slow phone connection is the difference between a portfolio and a slideshow of jumping boxes. Emit intrinsic dimensions from the stored values on every single image, and fail the build when one is missing. ## Phase 4 · Editing without breaking the site A local admin edits metadata, reorders images and previews, and cannot publish something invalid. ### Steps 1. Preview through the real build Two renderers means two behaviours and one surprise. Files: `src/admin/preview.ts` 2. Block publish on missing alt text, naming the image It is the only accessibility rule that matters here. Files: `src/build/validate.ts`, `tests/alt-text.test.ts` 3. Write ordering back to the metadata files The folders are canonical, including the order. Files: `src/admin/reorder.ts` ### Done when - [ ] Preview and publish share the build code - [ ] Publishing without alt text is refused by name - [ ] Reordering persists to the metadata files - [ ] Draft slugs appear nowhere in the build output ### Watch out - A preview served by a different code path from the build is the reason things look right locally and wrong live, and you only find out after you have sent the link. Run the real build into a temporary directory and serve that, even though it is slower, because the alternative is a class of bug you cannot reproduce. - Making alt text optional means it is never written, and a portfolio is the one kind of site that is entirely images. Block publishing on a missing alt text, name the image, and let the project stay in draft as long as it needs to. ## Not in this build - A general purpose visual editor. - Client collaboration and approvals. - Hosted forms and analytics. - A template marketplace. ## After v1, if you want it - Case study layouts with per-project template overrides, once the base renderer is stable enough to be worth overriding - A password-protected draft URL for work you cannot show publicly yet, which is a hosting feature more than a build one - An archive page generated from the builds table, so the portfolio's own history is a page rather than a folder
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Project folders and originals. This is the portfolio; back it up first. CONTENT_DIR=./content # Required. Numbered build outputs. The previous one is kept for rollback. BUILD_DIR=./builds # Required. The ladder. Changing it invalidates the derivative cache. DERIVATIVE_WIDTHS=480,960,1440,2048 # Required. Primary output format. A fallback is always emitted alongside. DERIVATIVE_FORMAT=avif # Required. Used for canonical links, the sitemap and redirect targets. SITE_URL=https://example.com # Required. How many previous builds stay on disk for rollback. BUILDS_KEPT=3
# Carbonmade · product brief ## Problem The visible portfolio builder loop is buildable, but a credible replacement needs more than the first screen. Carbonmade 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 plain folders, whose images come from untouched originals in the right orientation, whose pages declare one priority image, and whose publish either completes or never happened. ## 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 - A general purpose visual editor. - Client collaboration and approvals. - Hosted forms and analytics. - A template marketplace. - general-purpose visual editing - large template and component libraries - CMS collaboration and client handoff - managed hosting, forms, and analytics - high-fidelity color, format, and export handling ## Success criteria - A rebuild from an empty cache is confirmed to match the content hash - Originals are confirmed byte-identical after a build - Exactly one priority image per page is confirmed and none is lazy loaded - An interrupted publish is confirmed to leave the live site unchanged - A renamed project's old URL is confirmed to resolve
# Build brief · Carbonmade 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 publisher that turns project folders into a static site you own to replace Carbonmade. 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: Projects as folders on disk, one metadata file each, images beside them · the content has to outlive whatever generator you get bored of - Generator: A local static build producing plain files, nothing running in production · a portfolio should not have a server you are responsible for patching - Images: One derivative ladder generated from untouched originals · originals are the only thing in this project you cannot regenerate - Editor: A local admin that edits metadata and ordering, never raw markup · an editor that writes HTML becomes the only thing that can read it - Publish: Build into a fresh directory, verify, then swap, keeping the previous build · a half uploaded portfolio is worse than an old one ### Data model (create this before Phase 1) - projects(slug, title, year, client, summary, body_md, cover_media_id, ordinal, status) · status is draft or published - media(id, project_id, original_path, original_bytes, width, height, exif_orientation, sha256, alt_text, ordinal) - derivatives(media_id, width, format, path, bytes) · regenerated from the original, never edited in place, safe to delete - pages(slug, title, body_md, ordinal) · the about and contact pages every portfolio has - builds(id, started_at, finished_at, output_dir, content_hash, status, bytes, page_count, lcp_ms) - redirects(from_path, to_path, code, created_at) · written automatically whenever a published slug changes - alt_text is a column rather than an optional front matter key, and a published image without one fails the build. ### Phase 1 · Content on disk that outlives the generator Build: One directory per project holding a metadata file and the original images. The database is a cache built from the directory, not the source of truth: deleting it and re-scanning produces the same site. Slugs come from a field in the metadata file, not from the folder name, so a folder can be renamed without changing a URL. Trap: Keeping the canonical content in a database and the images in an uploads folder means the day you abandon this generator your portfolio is trapped in a schema, which is precisely the situation you left the paid product to avoid. Keep the folders canonical, treat the database as derived, and prove it by deleting the database and rebuilding byte-identical output. Done when: Deleting the database and re-scanning the content directory produces a build with the same content hash, tested. A project folder renamed on disk keeps its URL because the slug is in the metadata, tested. Every metadata file is valid against a schema and the scan reports the file and line on failure. An image sitting in a project folder but absent from the metadata is reported rather than silently ignored. Do not build yet: No image pipeline, page templates or publishing yet. ### Phase 2 · Derivatives from originals nobody touches Build: Read each original's dimensions and orientation tag once. Generate a fixed set of widths in a modern format plus a fallback, writing the orientation into the pixels and stripping the tag from the output. Cache by the original's hash plus the ladder settings so a rebuild does not redo the work, and never write back into the original file. Trap: Resizing with a library that ignores the orientation tag turns a third of a photographer's portfolio sideways, and nobody notices before launch because the operating system's viewer applies the tag and shows the originals correctly. Read the tag, bake the rotation into the derivative, strip the tag from the output so the browser does not rotate it a second time, and put a deliberately rotated photograph in the fixtures. Trap: Writing the resized version over the original saves disk and destroys the one artefact you cannot regenerate, usually in a script somebody ran to fix something else. Make the originals directory read only in the pipeline and assert it in a test. Done when: A fixture photograph with a non-default orientation tag renders upright in every derivative and the output carries no orientation tag, tested by reading the output metadata. Original files are byte-identical before and after a full build, asserted by hash. Changing the ladder settings regenerates derivatives and changing nothing regenerates none, both tested. Derivative total size per project is reported at build time. Do not build yet: No templates, editing or publishing yet. ### Phase 3 · The page that loads fast on a phone Build: Emit width, height, srcset and sizes on every image so nothing reflows. Mark exactly one image per page as the priority image: no lazy attribute, fetchpriority high, and preloaded. Everything below gets lazy loading. Fail the build if a page declares two priority images or none, and record a measured load figure per build so a regression is visible. Trap: Adding loading lazy to every image because it is the obvious optimisation puts the hero, which on a portfolio is almost always the largest contentful paint element, behind layout and the published guidance is explicit that an above the fold image with lazy loading will likely have a large negative effect on that metric. Choose one priority image per page, give it fetchpriority high and no lazy attribute, and lazy load only what is genuinely below the fold. Trap: Omitting width and height means the page reflows as each image arrives, which on a slow phone connection is the difference between a portfolio and a slideshow of jumping boxes. Emit intrinsic dimensions from the stored values on every single image, and fail the build when one is missing. Done when: Every image element in the output carries width, height, srcset and sizes, asserted by parsing the built HTML. Exactly one image per page carries the priority attributes and none of them carries lazy loading, asserted. A page with zero or two priority images fails the build with the page named. A measured load figure on a throttled connection is recorded per build and the previous build's figure is shown alongside it. Do not build yet: No editor, publishing or redirects yet. ### Phase 4 · Editing without breaking the site Build: Edit project metadata and body text, drag to reorder projects and images, set the cover, and write alt text. Preview builds against the same renderer as production so preview and live cannot diverge. Validation runs on save and blocks publishing rather than blocking editing, so a half-finished project can sit in draft. Trap: A preview served by a different code path from the build is the reason things look right locally and wrong live, and you only find out after you have sent the link. Run the real build into a temporary directory and serve that, even though it is slower, because the alternative is a class of bug you cannot reproduce. Trap: Making alt text optional means it is never written, and a portfolio is the one kind of site that is entirely images. Block publishing on a missing alt text, name the image, and let the project stay in draft as long as it needs to. Done when: Preview is produced by the same build code as publishing, verified by review and by comparing output for a fixture project. Publishing a project with an image missing alt text is refused and the image is named. Reordering in the admin writes the ordinal back into the metadata file on disk, not only into the database. A draft project appears in no built output, asserted by searching the build directory for its slug. Do not build yet: No publishing, redirects or rollback yet. ### Phase 5 · Publish, roll back, keep the links working Build: Build into a fresh directory, verify it, then swap it into place and keep the previous build for rollback. Record a redirect automatically whenever a published slug changes, and refuse to publish when a previously published slug has neither a page nor a redirect. Ship a useful 404 and a rollback command that swaps the previous build back in one step. Trap: Uploading file by file over the live site means a visitor during the upload gets new HTML pointing at images that are not there yet, and a failure halfway leaves that state permanently. Build complete, verify complete, swap complete, and keep the previous directory so rollback is a rename rather than a rebuild. Trap: Renaming a project to fix a typo changes its URL and every link in every email you sent a client goes to a 404, which you discover when one of them mentions it months later. Diff published slugs against the last build, write the redirect yourself, and block a publish that would orphan a slug with no redirect. Done when: A publish interrupted part way leaves the previously published site serving unchanged, tested by killing the process mid-swap. Renaming a published project writes a redirect automatically and the old URL resolves after publishing, tested end to end. Rollback restores the previous build and is exercised in the suite rather than documented. The build refuses to publish when a previously published slug has no page and no redirect, naming the slug. Do not build yet: Nothing after this. ### Out of scope (and why) - A general purpose visual editor, which is a different product with a much larger surface and is what the paid tools actually sell. - Client collaboration and approvals, which need accounts, permissions and a hosted service you would then have to keep running. - Hosted contact forms and analytics, which turn a static site into a service with data obligations; link to something else or run a separate small service. - A template marketplace, which is content work rather than engineering and is where the polish you are comparing against comes from. ### README must contain - That the project folders are canonical and the database is a cache you can delete and rebuild - That originals are never modified and every displayed image is a generated derivative - That exactly one image per page is the priority image and why lazy loading the hero is the wrong default - That publishing builds, verifies and swaps, that the previous build is kept, and the rollback command - That renaming a project writes a redirect automatically and that publishing is blocked if one is missing
# Architecture · Carbonmade ## Stack | Part | Choice | Why | | --- | --- | --- | | Content | Projects as folders on disk, one metadata file each, images beside them | the content has to outlive whatever generator you get bored of | | Generator | A local static build producing plain files, nothing running in production | a portfolio should not have a server you are responsible for patching | | Images | One derivative ladder generated from untouched originals | originals are the only thing in this project you cannot regenerate | | Editor | A local admin that edits metadata and ordering, never raw markup | an editor that writes HTML becomes the only thing that can read it | | Publish | Build into a fresh directory, verify, then swap, keeping the previous build | a half uploaded portfolio is worse than an old one | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Content | Scanning folders, schema validation, slugs. | Any front matter format, provided the folders stay canonical. | | Images | Orientation, the width ladder and the cache. | Any library, provided originals are never written to. | | Render | Templates, image markup, priority rules. | Any templating, provided the build enforces one priority image. | | Admin | Editing, ordering, alt text and preview. | Any interface, provided preview runs the real build. | | Publish | Verify, swap, redirects and rollback. | Any host, provided the swap is atomic and the previous build is kept. | ## 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 · Project folders and originals. This is the portfolio; back it up first. - `BUILD_DIR` · required · Numbered build outputs. The previous one is kept for rollback. - `DERIVATIVE_WIDTHS` · required · The ladder. Changing it invalidates the derivative cache. - `DERIVATIVE_FORMAT` · required · Primary output format. A fallback is always emitted alongside. - `SITE_URL` · required · Used for canonical links, the sitemap and redirect targets. - `BUILDS_KEPT` · required · How many previous builds stay on disk for rollback. ## 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 · Carbonmade product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Projects as folders on disk, one metadata file each, images beside them, A local static build producing plain files, nothing running in production, One derivative ladder generated from untouched originals, A local admin that edits metadata and ordering, never raw markup, Build into a fresh directory, verify, then swap, keeping the previous build. - 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 - Keeping the canonical content in a database and the images in an uploads folder means the day you abandon this generator your portfolio is trapped in a schema, which is precisely the situation you left the paid product to avoid. Keep the folders canonical, treat the database as derived, and prove it by deleting the database and rebuilding byte-identical output. - Resizing with a library that ignores the orientation tag turns a third of a photographer's portfolio sideways, and nobody notices before launch because the operating system's viewer applies the tag and shows the originals correctly. Read the tag, bake the rotation into the derivative, strip the tag from the output so the browser does not rotate it a second time, and put a deliberately rotated photograph in the fixtures. - Writing the resized version over the original saves disk and destroys the one artefact you cannot regenerate, usually in a script somebody ran to fix something else. Make the originals directory read only in the pipeline and assert it in a test. - Adding loading lazy to every image because it is the obvious optimisation puts the hero, which on a portfolio is almost always the largest contentful paint element, behind layout and the published guidance is explicit that an above the fold image with lazy loading will likely have a large negative effect on that metric. Choose one priority image per page, give it fetchpriority high and no lazy attribute, and lazy load only what is genuinely below the fold. - Omitting width and height means the page reflows as each image arrives, which on a slow phone connection is the difference between a portfolio and a slideshow of jumping boxes. Emit intrinsic dimensions from the stored values on every single image, and fail the build when one is missing. - A preview served by a different code path from the build is the reason things look right locally and wrong live, and you only find out after you have sent the link. Run the real build into a temporary directory and serve that, even though it is slower, because the alternative is a class of bug you cannot reproduce. - Making alt text optional means it is never written, and a portfolio is the one kind of site that is entirely images. Block publishing on a missing alt text, name the image, and let the project stay in draft as long as it needs to. - Uploading file by file over the live site means a visitor during the upload gets new HTML pointing at images that are not there yet, and a failure halfway leaves that state permanently. Build complete, verify complete, swap complete, and keep the previous directory so rollback is a rename rather than a rebuild. - Renaming a project to fix a typo changes its URL and every link in every email you sent a client goes to a 404, which you discover when one of them mentions it months later. Diff published slugs against the last build, write the redirect yourself, and block a publish that would orphan a slug with no redirect.
# Delivery milestones · Carbonmade Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Content on disk that outlives the generator Everything the site is made of lives in plain files you could edit with any text editor. ### Steps 1. Make folders canonical and the database derived Your content should not need this program to be readable. Files: `src/content/scan.ts`, `tests/rebuild.test.ts` 2. Put the slug in the metadata, not the folder name Folders get tidied; URLs you sent people do not. Files: `src/content/schema.ts` 3. Report unreferenced files instead of ignoring them A missing image is usually a typo, not a decision. Files: `src/content/scan.ts` ### Done when - [ ] A rebuild from scratch matches the content hash - [ ] Renaming a folder does not change a URL - [ ] Invalid metadata reports file and line - [ ] Unreferenced images in a project folder are reported ### Watch out - Keeping the canonical content in a database and the images in an uploads folder means the day you abandon this generator your portfolio is trapped in a schema, which is precisely the situation you left the paid product to avoid. Keep the folders canonical, treat the database as derived, and prove it by deleting the database and rebuilding byte-identical output. ## M2 · Derivatives from originals nobody touches Every displayed image is generated from an untouched original at a fixed ladder of widths. ### Steps 1. Apply orientation once and strip the tag Otherwise the browser rotates an already rotated image. Files: `src/images/derive.ts`, `tests/orientation.test.ts` 2. Cache on original hash plus ladder settings A rebuild of an unchanged portfolio should be fast. Files: `src/images/cache.ts` 3. Assert the originals are never written to This is the file you cannot get back. Files: `tests/originals-immutable.test.ts` ### Done when - [ ] A rotated fixture renders upright with no tag in the output - [ ] Originals are byte-identical after a build - [ ] Unchanged content regenerates no derivatives - [ ] Derivative size per project is reported ### Watch out - Resizing with a library that ignores the orientation tag turns a third of a photographer's portfolio sideways, and nobody notices before launch because the operating system's viewer applies the tag and shows the originals correctly. Read the tag, bake the rotation into the derivative, strip the tag from the output so the browser does not rotate it a second time, and put a deliberately rotated photograph in the fixtures. - Writing the resized version over the original saves disk and destroys the one artefact you cannot regenerate, usually in a script somebody ran to fix something else. Make the originals directory read only in the pipeline and assert it in a test. ## M3 · The page that loads fast on a phone Each page declares exactly one priority image and everything else defers, with the build enforcing it. ### Steps 1. Declare one priority image per page and enforce it The hero is the number that decides how the site feels. Files: `src/render/image.ts`, `tests/priority-image.test.ts` 2. Emit intrinsic dimensions on every image Reflow is the other half of a slow-feeling page. Files: `src/render/image.ts` 3. Record a measured load figure per build A number nobody records is a regression nobody notices. Files: `scripts/measure.mjs`, `src/build/record.ts` ### Done when - [ ] Every image has dimensions, srcset and sizes - [ ] Exactly one priority image per page, never lazy - [ ] Zero or two priority images fails the build - [ ] A measured load figure is recorded per build ### Watch out - Adding loading lazy to every image because it is the obvious optimisation puts the hero, which on a portfolio is almost always the largest contentful paint element, behind layout and the published guidance is explicit that an above the fold image with lazy loading will likely have a large negative effect on that metric. Choose one priority image per page, give it fetchpriority high and no lazy attribute, and lazy load only what is genuinely below the fold. - Omitting width and height means the page reflows as each image arrives, which on a slow phone connection is the difference between a portfolio and a slideshow of jumping boxes. Emit intrinsic dimensions from the stored values on every single image, and fail the build when one is missing. ## M4 · Editing without breaking the site A local admin edits metadata, reorders images and previews, and cannot publish something invalid. ### Steps 1. Preview through the real build Two renderers means two behaviours and one surprise. Files: `src/admin/preview.ts` 2. Block publish on missing alt text, naming the image It is the only accessibility rule that matters here. Files: `src/build/validate.ts`, `tests/alt-text.test.ts` 3. Write ordering back to the metadata files The folders are canonical, including the order. Files: `src/admin/reorder.ts` ### Done when - [ ] Preview and publish share the build code - [ ] Publishing without alt text is refused by name - [ ] Reordering persists to the metadata files - [ ] Draft slugs appear nowhere in the build output ### Watch out - A preview served by a different code path from the build is the reason things look right locally and wrong live, and you only find out after you have sent the link. Run the real build into a temporary directory and serve that, even though it is slower, because the alternative is a class of bug you cannot reproduce. - Making alt text optional means it is never written, and a portfolio is the one kind of site that is entirely images. Block publishing on a missing alt text, name the image, and let the project stay in draft as long as it needs to. ## M5 · Publish, roll back, keep the links working (production only) Publishing either completes or leaves the old site untouched, and no URL you have ever sent someone breaks. ### Steps 1. Build, verify, swap, keep the previous Nobody should ever see a half published portfolio. Files: `scripts/publish.sh`, `tests/publish.test.ts` 2. Write redirects from the slug diff automatically You will not remember to do this by hand. Files: `src/build/redirects.ts` 3. Exercise rollback in the suite A rollback you have never run is a plan, not a command. Files: `tests/rollback.test.ts` ### Done when - [ ] An interrupted publish leaves the live site unchanged - [ ] A renamed project's old URL still resolves - [ ] Rollback is a single command exercised in tests - [ ] An orphaned published slug blocks the publish ### Watch out - Uploading file by file over the live site means a visitor during the upload gets new HTML pointing at images that are not there yet, and a failure halfway leaves that state permanently. Build complete, verify complete, swap complete, and keep the previous directory so rollback is a rename rather than a rebuild. - Renaming a project to fix a typo changes its URL and every link in every email you sent a client goes to a 404, which you discover when one of them mentions it months later. Diff published slugs against the last build, write the redirect yourself, and block a publish that would orphan a slug with no redirect.
# Operations · Carbonmade ## Backup The content directory, and nothing else matters as much. Derivatives and builds regenerate; originals and the words you wrote about the work do not. Keep an offsite copy, because a portfolio is also an archive. ## Restore Restore the content directory, run a full build from an empty cache, and compare the content hash against the last recorded build. If it matches, the restore is complete, which is a cleaner test than checking whether the site looks right. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Build success, the measured load figure per build against the previous one, output size, and an external check that the domain serves the current build. Portfolios break quietly because nobody visits their own site. ## Incident checklist The incidents here are a broken link somebody was sent and a site half replaced by a failed upload. Redirects handle the first and the build-verify-swap handles the second, which is why both are enforced 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 - [ ] A rebuild from an empty cache is confirmed to match the content hash - [ ] Originals are confirmed byte-identical after a build - [ ] Exactly one priority image per page is confirmed and none is lazy loaded - [ ] An interrupted publish is confirmed to leave the live site unchanged - [ ] A renamed project's old URL is confirmed to resolve ## Launch constraint Do not market omitted Carbonmade 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. Project folders and originals. This is the portfolio; back it up first. CONTENT_DIR=./content # Required. Numbered build outputs. The previous one is kept for rollback. BUILD_DIR=./builds # Required. The ladder. Changing it invalidates the derivative cache. DERIVATIVE_WIDTHS=480,960,1440,2048 # Required. Primary output format. A fallback is always emitted alongside. DERIVATIVE_FORMAT=avif # Required. Used for canonical links, the sitemap and redirect targets. SITE_URL=https://example.com # Required. How many previous builds stay on disk for rollback. BUILDS_KEPT=3
$ choose a build depth, inspect the files, then open the complete pack in your agent
Carbonmade: A single site is easy to code. People pay for safe ongoing edits, templates, publishing, domains, collaboration, and no-maintenance hosting.
xgeneral-purpose visual editing
xlarge template and component libraries
xCMS collaboration and client handoff
xmanaged hosting, forms, and analytics
xhigh-fidelity color, format, and export handling
Don't feel like building it? These folks already made it free.
all 4 free alternatives to Carbonmade →· no votes, no pay-to-list · just what's real
Carbonmade pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| beginner | $9.99 | — | 8 projects; unlimited image/video uploads; 3 navigation options; SD video |
| professional | $14.99 | — | 50 projects; unlimited image/video uploads; 5 navigation options; 4K video/audio; custom pages and private portfolio |
| unlimited | $24.99 | — | Unlimited projects and pages; includes all Professional features |
free tierno free tier; unlimited-duration private trial, but payment is required to publish the portfolio live
billingmonthly + annual (advertised as about 20% off); exact annual prices were not exposed in the accessible page text
hidden costsA custom domain itself is not included; Carbonmade hosts the site and lets you connect a domain you already own.
verified 2026-08-14 · source ↗
Vibecode Carbonmade
Kinda. The core of Carbonmade is buildable in a weekend with the prompt on this page, but there are real gaps: general-purpose visual editing, large template and component libraries. Read the honest list above before committing.
How much does Carbonmade cost?
Carbonmade'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 Carbonmade?
Honestly: general-purpose visual editing; large template and component libraries; CMS collaboration and client handoff; managed hosting, forms, and analytics; high-fidelity color, format, and export handling. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Carbonmade?
Yes: Webstudio (Open-source visual design with real CSS control; self-hosting replaces Carbonmade's calm with operations.) Publii (A desktop portfolio publisher that produces a fast static site; hosting is your problem, server maintenance is not.) Plasmic (A hosted visual portfolio with full layout freedom and a custom domain free; the Plasmic badge stays.) All 4 curated free alternatives are at vibecodeit.com/carbonmade/alternatives. The prompt is for when you want it exactly your way.