Vibecode Cargo
track this build4 phases, 12 steps, beginner friendly0%The visible creative website builder loop is buildable, but a credible replacement needs more than the first screen. Cargo 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 Cargo. 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 ===== # Cargo · indie build A site builder whose hard problems are a content model that survives a redesign, images that do not cost a visitor their data allowance, and a publish that is atomic and reversible rather than a file by file sync over a live site. 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 | | --- | --- | --- | | Editor | Next.js with TypeScript, one local user, no accounts | the editor is a tool you run, not a service you operate | | Content | SQLite plus a JSON export written on every save | the content has to be readable when this application is not running | | Output | A deterministic static build into a release folder | a folder of files can be hosted anywhere and outlives the builder | | Images | Derivatives generated at publish time from untouched originals | the original is the only thing you cannot regenerate later | | Publishing | Upload a whole release then switch, with the previous release kept | a visitor should never see half of an update | ## 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 editor and the build. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **An image processing library for your runtime** · free - Why: Derivative generation at several widths is the difference between a fast site and a slow one, and doing it by hand does not happen. - Get it: Any maintained library that resizes and re-encodes, installed as a dependency. - Verify: It produces a resized file at three widths from one original - [ ] **A static host you can upload a whole directory to** · free to a few pounds a month - Why: Atomic publishing needs a host where you can upload a release and then switch, rather than one that only offers a file by file sync. - Get it: Any static hosting or object storage you already have, plus its published limits page. - Verify: You can upload a directory and serve it, and you have read the limits page - [ ] **A domain you control** (optional) · around ten pounds a year - Why: Domain and certificate handling is the part that breaks on a Friday, and testing it against a real name is the only test that counts. - Get it: Any registrar, with access to the DNS records. - Verify: A record you add resolves within an hour - [ ] **A licence note for every font you ship** · free - Why: Web embedding is a separate permission from desktop use, and the build refuses to run without the note. - Get it: For each font, record the licence and one sentence on whether web embedding is permitted. - Verify: Every font file in the project has a note beside it and the build passes ## 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: - Forms and comments. - A template marketplace. - Multi user editing and client handoff. - Commerce. - general-purpose visual editing - large template and component libraries - CMS collaboration and client handoff - managed hosting, forms, and analytics If one of those is essential to you, that is the reason to keep paying for Cargo, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Cargo 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 small site builder whose output is a folder of plain static files you could host anywhere to replace Cargo. 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) - Editor: Next.js with TypeScript, one local user, no accounts · the editor is a tool you run, not a service you operate - Content: SQLite plus a JSON export written on every save · the content has to be readable when this application is not running - Output: A deterministic static build into a release folder · a folder of files can be hosted anywhere and outlives the builder - Images: Derivatives generated at publish time from untouched originals · the original is the only thing you cannot regenerate later - Publishing: Upload a whole release then switch, with the previous release kept · a visitor should never see half of an update ### Data model (create this before Phase 1) - sites(id, name, base_url, primary_domain, created_at) - pages(id, site_id, slug, title, description, status, ordinal, updated_at) · slug is unique per site and changing it records a redirect - sections(id, page_id, ordinal, kind, content_json, layout_json) · typed sections with layout options, never absolute pixel positions - assets(id, site_id, original_path, sha256, width, height, bytes, alt_text) · alt text lives with the asset, not with one use of it - derivatives(asset_id, width, format, path, bytes) · generated at publish, safe to delete, regenerated on the next build - releases(id, site_id, built_at, output_dir, file_count, bytes_total, published_at, rolled_back_at) - redirects(site_id, from_path, to_path, created_at) · because a renamed page that returns nothing is how a site loses its links ### Phase 1 · A content model, not a canvas Build: Define a small set of section kinds with their own fields: text, image, gallery, embed, index. A page is an ordered list of sections. Layout choices live beside the content as options, and the renderer turns sections into HTML with no positional coordinates anywhere. Trap: A freeform canvas that stores each element at an absolute pixel position looks like the product people want and produces a site that is unreadable on a phone, impossible to restyle, and stuck with whichever font metrics the editor had that day. Store typed sections with layout options and let the renderer decide where things go. Done when: No content row contains a pixel coordinate, verified by inspecting the schema and a saved page. Adding a new section kind requires a renderer change and a migration and nothing else, demonstrated by adding one. Changing the layout option on a section changes its rendering without touching its content. Every page renders at a narrow viewport without a horizontal scrollbar, checked at 360 pixels wide. Do not build yet: No images, build, publishing or domains. ### Phase 2 · Images that do not cost a visitor their data allowance Build: At publish, generate derivatives at a fixed set of widths and emit srcset and sizes. Write the intrinsic width and height on every image element. Keep originals untouched and outside the release. Require alt text on the asset, with an explicit decorative option. Trap: Shipping the original from a camera and constraining it with a hundred percent width rule sends a twelve megabyte file to a phone to be displayed at four hundred pixels, and without declared dimensions the page jumps as each one lands. Generate derivatives at publish, emit srcset, and always write width and height. Done when: The largest file in a built release is under a stated byte budget, asserted by the build. Every image element carries width, height, srcset and sizes, checked by parsing the output. An image with neither alt text nor a decorative flag fails the build with the page and section named. Originals are not present anywhere in the release directory. Do not build yet: No publishing, rollback, fonts or domains yet. ### Phase 3 · A build that is just a folder Build: Build every page to static HTML with content hashed asset filenames. Building twice from unchanged content produces identical files. Emit a sitemap, the redirects file for the host, and a manifest listing every file with its size and hash. Trap: Rendering pages from the database on request means the site is only alive while your builder is, so the moment the process dies or the disk fills the whole site is gone rather than merely un-editable. Build to files, and treat the database as a working copy of something that already exists on disk. Done when: Two consecutive builds from unchanged content produce byte identical output, tested by comparing hashes. The release directory serves correctly from any plain static file server with no other process running. Every asset filename contains a content hash, so a changed file never reuses a cached URL. The manifest lists file count and total bytes and the build prints both. Do not build yet: No publishing, rollback, fonts or domains yet. ### Phase 4 · Publishing that is all or nothing Build: Upload the release to a new directory, verify the file count and hashes against the manifest, then switch the pointer. Keep a stated number of previous releases and provide a rollback command. Check the release against the host's published limits before uploading. Trap: Syncing the build over the live site file by file leaves visitors during the upload with new HTML asking for stylesheets that are not there yet, and a failure halfway leaves the site permanently in that state with no way back. Upload a whole release, verify it, switch, and keep the previous one so rollback is one command. Done when: A publish interrupted halfway leaves the live site untouched, tested by killing the upload. Rollback restores the previous release and is exercised in the suite rather than described. The build refuses to publish a release that exceeds the size limit you configured for your host, with the limit and the actual size printed. If the target is GitHub Pages, the README records its published limits, checked on a stated date: sites no larger than 1 GB, a soft bandwidth limit of 100 GB per month and a soft limit of 10 builds per hour. Do not build yet: No fonts, domains or export yet. ### Phase 5 · Fonts, domains and the ability to walk away Build: Self host fonts and record the licence for each one, refusing to build if a font has no recorded licence note. Connect a custom domain with a certificate and verify it before switching. Export the entire site as content JSON plus assets plus the built release, and confirm the export alone is enough to rebuild. Trap: Converting a font bought for desktop use into a web format and serving it is a licensing decision most people make by accident, and it is the kind of mistake that arrives as a letter rather than a bug report. Record a licence note per font, check that web embedding is permitted before you ship it, and let the build fail when the note is missing. Done when: A font without a recorded licence note fails the build and names the font. The export contains content JSON, originals and the built release, and a rebuild from the export alone produces byte identical output, tested. The custom domain serves over a valid certificate and the previous address redirects rather than breaking. Renaming a page writes a redirect and the old path resolves. Do not build yet: Nothing after this. ### Out of scope (and why) - Forms and comments, because collecting anything from visitors turns a static site into a service you operate, and static hosts explicitly warn against using them for sensitive submissions. - A template marketplace and a component library, which is years of design work rather than a weekend of code and is a large part of what the subscription actually buys. - Multi user editing and client handoff, which needs a permission model, an audit trail and conflict handling on every field. - Commerce, which brings payment processing, tax and order state into what is otherwise a folder of files. ### README must contain - That the output is a folder of static files and the database is a working copy of content that also exists as JSON - That publishing uploads a whole release, verifies it against a manifest and then switches, with rollback as one command - The published limits of the host you chose, with the date you checked them - That images are generated at publish time at a fixed set of widths, and the byte budget the build enforces - That every font ships with a recorded licence note and the build fails without one ===== AGENTS.md ===== # Agent instructions · Cargo indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js with TypeScript, one local user, no accounts, SQLite plus a JSON export written on every save, A deterministic static build into a release folder, Derivatives generated at publish time from untouched originals, Upload a whole release then switch, with the previous release kept. 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 - A freeform canvas that stores each element at an absolute pixel position looks like the product people want and produces a site that is unreadable on a phone, impossible to restyle, and stuck with whichever font metrics the editor had that day. Store typed sections with layout options and let the renderer decide where things go. - Shipping the original from a camera and constraining it with a hundred percent width rule sends a twelve megabyte file to a phone to be displayed at four hundred pixels, and without declared dimensions the page jumps as each one lands. Generate derivatives at publish, emit srcset, and always write width and height. - Rendering pages from the database on request means the site is only alive while your builder is, so the moment the process dies or the disk fills the whole site is gone rather than merely un-editable. Build to files, and treat the database as a working copy of something that already exists on disk. - Syncing the build over the live site file by file leaves visitors during the upload with new HTML asking for stylesheets that are not there yet, and a failure halfway leaves the site permanently in that state with no way back. Upload a whole release, verify it, switch, and keep the previous one so rollback is one command. ===== BUILD_PLAN.md ===== # Build plan · Cargo A site builder whose hard problems are a content model that survives a redesign, images that do not cost a visitor their data allowance, and a publish that is atomic and reversible rather than a file by file sync over a live site. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A content model, not a canvas Pages are typed sections, so a redesign changes the renderer and not the content. ### Steps 1. Define section kinds with explicit fields Five kinds that fit is better than one kind that holds anything. Files: `src/schema/sections.ts` 2. Keep layout as options, not coordinates Options survive a redesign; coordinates are the redesign. Files: `src/render/section.tsx` 3. Write a JSON export on every save So the content is readable without the editor from day one. Files: `src/content-export.ts` ### Done when - [ ] No pixel coordinates exist in content rows - [ ] A new section kind needs only renderer plus migration - [ ] Layout options change rendering, not content - [ ] Pages render at 360 pixels without horizontal scroll ### Watch out - A freeform canvas that stores each element at an absolute pixel position looks like the product people want and produces a site that is unreadable on a phone, impossible to restyle, and stuck with whichever font metrics the editor had that day. Store typed sections with layout options and let the renderer decide where things go. ## Phase 2 · Images that do not cost a visitor their data allowance Every image ships at a sensible size with dimensions declared. ### Steps 1. Generate derivatives at publish time Not at request time, because there is no server to request from. Files: `src/build/images.ts` 2. Always write intrinsic dimensions The cheapest fix for a page that jumps while it loads. Files: `src/render/image.tsx` 3. Fail the build on missing alt text A warning nobody reads is how a site ends up with none of it. Files: `src/build/checks.ts`, `tests/alt-text.test.ts` ### Done when - [ ] The largest released file is under the byte budget - [ ] Every image has width, height, srcset and sizes - [ ] Missing alt text fails the build with a location - [ ] No originals appear in the release ### Watch out - Shipping the original from a camera and constraining it with a hundred percent width rule sends a twelve megabyte file to a phone to be displayed at four hundred pixels, and without declared dimensions the page jumps as each one lands. Generate derivatives at publish, emit srcset, and always write width and height. ## Phase 3 · A build that is just a folder The same content builds to the same bytes and needs nothing running. ### Steps 1. Make the build deterministic Sort everything, and keep timestamps out of the output. Files: `src/build/index.ts`, `tests/deterministic-build.test.ts` 2. Content hash every asset filename So caching is aggressive and still correct. Files: `src/build/assets.ts` 3. Emit a manifest with sizes and hashes It is what publishing, rollback and the size check all read. Files: `src/build/manifest.ts` ### Done when - [ ] Two builds produce identical bytes - [ ] The release serves from a plain static server - [ ] Asset filenames are content hashed - [ ] The manifest reports file count and total bytes ### Watch out - Rendering pages from the database on request means the site is only alive while your builder is, so the moment the process dies or the disk fills the whole site is gone rather than merely un-editable. Build to files, and treat the database as a working copy of something that already exists on disk. ## Phase 4 · Publishing that is all or nothing Visitors see the old site or the new one, never a mixture, and you can go back. ### Steps 1. Upload to a new directory, then switch The switch is the only moment anything changes for a visitor. Files: `src/publish/upload.ts` 2. Verify against the manifest before switching Count and hashes, because a truncated upload looks like a successful one. Files: `src/publish/verify.ts` 3. Make rollback a tested command The night you need it is not the night to find out it was never written. Files: `src/publish/rollback.ts`, `tests/rollback.test.ts` ### Done when - [ ] An interrupted publish leaves the live site untouched - [ ] Rollback is exercised in the suite - [ ] Oversized releases are refused with sizes printed - [ ] Host limits are recorded in the README with a check date ### Watch out - Syncing the build over the live site file by file leaves visitors during the upload with new HTML asking for stylesheets that are not there yet, and a failure halfway leaves the site permanently in that state with no way back. Upload a whole release, verify it, switch, and keep the previous one so rollback is one command. ## Not in this build - Forms and comments. - A template marketplace. - Multi user editing and client handoff. - Commerce. ## After v1, if you want it - A second theme sharing the same content, which is the test of whether the model really is separate from the layout - Scheduled publishing, which is the existing publish behind a timer and nothing more - A link checker in the build, so a broken internal link fails before visitors find it ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Pages, sections and assets. A working copy of the JSON export. DATABASE_PATH=./data/site.db # Required. JSON written on every save. This is the copy that outlives the editor. CONTENT_EXPORT_DIR=./content # Required. Untouched originals. Never included in a release. ASSETS_DIR=./assets/originals # Required. One directory per release, kept for rollback. BUILD_DIR=./releases # Required. Derivative widths emitted into srcset. IMAGE_WIDTHS=400,800,1200,2000 # Required. The build fails if any released file exceeds this. MAX_FILE_BYTES=1500000 # Required. Previous releases retained. This is what rollback uses. RELEASES_KEEP=5 # Optional · secret. Only if your host needs one to upload. Unset means you publish by hand. DEPLOY_TOKEN=sk-...redacted
You are building a lean indie version of Cargo. 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 ===== # Cargo · indie build A site builder whose hard problems are a content model that survives a redesign, images that do not cost a visitor their data allowance, and a publish that is atomic and reversible rather than a file by file sync over a live site. 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 | | --- | --- | --- | | Editor | Next.js with TypeScript, one local user, no accounts | the editor is a tool you run, not a service you operate | | Content | SQLite plus a JSON export written on every save | the content has to be readable when this application is not running | | Output | A deterministic static build into a release folder | a folder of files can be hosted anywhere and outlives the builder | | Images | Derivatives generated at publish time from untouched originals | the original is the only thing you cannot regenerate later | | Publishing | Upload a whole release then switch, with the previous release kept | a visitor should never see half of an update | ## 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 editor and the build. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **An image processing library for your runtime** · free - Why: Derivative generation at several widths is the difference between a fast site and a slow one, and doing it by hand does not happen. - Get it: Any maintained library that resizes and re-encodes, installed as a dependency. - Verify: It produces a resized file at three widths from one original - [ ] **A static host you can upload a whole directory to** · free to a few pounds a month - Why: Atomic publishing needs a host where you can upload a release and then switch, rather than one that only offers a file by file sync. - Get it: Any static hosting or object storage you already have, plus its published limits page. - Verify: You can upload a directory and serve it, and you have read the limits page - [ ] **A domain you control** (optional) · around ten pounds a year - Why: Domain and certificate handling is the part that breaks on a Friday, and testing it against a real name is the only test that counts. - Get it: Any registrar, with access to the DNS records. - Verify: A record you add resolves within an hour - [ ] **A licence note for every font you ship** · free - Why: Web embedding is a separate permission from desktop use, and the build refuses to run without the note. - Get it: For each font, record the licence and one sentence on whether web embedding is permitted. - Verify: Every font file in the project has a note beside it and the build passes ## 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: - Forms and comments. - A template marketplace. - Multi user editing and client handoff. - Commerce. - general-purpose visual editing - large template and component libraries - CMS collaboration and client handoff - managed hosting, forms, and analytics If one of those is essential to you, that is the reason to keep paying for Cargo, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Cargo 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 small site builder whose output is a folder of plain static files you could host anywhere to replace Cargo. 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) - Editor: Next.js with TypeScript, one local user, no accounts · the editor is a tool you run, not a service you operate - Content: SQLite plus a JSON export written on every save · the content has to be readable when this application is not running - Output: A deterministic static build into a release folder · a folder of files can be hosted anywhere and outlives the builder - Images: Derivatives generated at publish time from untouched originals · the original is the only thing you cannot regenerate later - Publishing: Upload a whole release then switch, with the previous release kept · a visitor should never see half of an update ### Data model (create this before Phase 1) - sites(id, name, base_url, primary_domain, created_at) - pages(id, site_id, slug, title, description, status, ordinal, updated_at) · slug is unique per site and changing it records a redirect - sections(id, page_id, ordinal, kind, content_json, layout_json) · typed sections with layout options, never absolute pixel positions - assets(id, site_id, original_path, sha256, width, height, bytes, alt_text) · alt text lives with the asset, not with one use of it - derivatives(asset_id, width, format, path, bytes) · generated at publish, safe to delete, regenerated on the next build - releases(id, site_id, built_at, output_dir, file_count, bytes_total, published_at, rolled_back_at) - redirects(site_id, from_path, to_path, created_at) · because a renamed page that returns nothing is how a site loses its links ### Phase 1 · A content model, not a canvas Build: Define a small set of section kinds with their own fields: text, image, gallery, embed, index. A page is an ordered list of sections. Layout choices live beside the content as options, and the renderer turns sections into HTML with no positional coordinates anywhere. Trap: A freeform canvas that stores each element at an absolute pixel position looks like the product people want and produces a site that is unreadable on a phone, impossible to restyle, and stuck with whichever font metrics the editor had that day. Store typed sections with layout options and let the renderer decide where things go. Done when: No content row contains a pixel coordinate, verified by inspecting the schema and a saved page. Adding a new section kind requires a renderer change and a migration and nothing else, demonstrated by adding one. Changing the layout option on a section changes its rendering without touching its content. Every page renders at a narrow viewport without a horizontal scrollbar, checked at 360 pixels wide. Do not build yet: No images, build, publishing or domains. ### Phase 2 · Images that do not cost a visitor their data allowance Build: At publish, generate derivatives at a fixed set of widths and emit srcset and sizes. Write the intrinsic width and height on every image element. Keep originals untouched and outside the release. Require alt text on the asset, with an explicit decorative option. Trap: Shipping the original from a camera and constraining it with a hundred percent width rule sends a twelve megabyte file to a phone to be displayed at four hundred pixels, and without declared dimensions the page jumps as each one lands. Generate derivatives at publish, emit srcset, and always write width and height. Done when: The largest file in a built release is under a stated byte budget, asserted by the build. Every image element carries width, height, srcset and sizes, checked by parsing the output. An image with neither alt text nor a decorative flag fails the build with the page and section named. Originals are not present anywhere in the release directory. Do not build yet: No publishing, rollback, fonts or domains yet. ### Phase 3 · A build that is just a folder Build: Build every page to static HTML with content hashed asset filenames. Building twice from unchanged content produces identical files. Emit a sitemap, the redirects file for the host, and a manifest listing every file with its size and hash. Trap: Rendering pages from the database on request means the site is only alive while your builder is, so the moment the process dies or the disk fills the whole site is gone rather than merely un-editable. Build to files, and treat the database as a working copy of something that already exists on disk. Done when: Two consecutive builds from unchanged content produce byte identical output, tested by comparing hashes. The release directory serves correctly from any plain static file server with no other process running. Every asset filename contains a content hash, so a changed file never reuses a cached URL. The manifest lists file count and total bytes and the build prints both. Do not build yet: No publishing, rollback, fonts or domains yet. ### Phase 4 · Publishing that is all or nothing Build: Upload the release to a new directory, verify the file count and hashes against the manifest, then switch the pointer. Keep a stated number of previous releases and provide a rollback command. Check the release against the host's published limits before uploading. Trap: Syncing the build over the live site file by file leaves visitors during the upload with new HTML asking for stylesheets that are not there yet, and a failure halfway leaves the site permanently in that state with no way back. Upload a whole release, verify it, switch, and keep the previous one so rollback is one command. Done when: A publish interrupted halfway leaves the live site untouched, tested by killing the upload. Rollback restores the previous release and is exercised in the suite rather than described. The build refuses to publish a release that exceeds the size limit you configured for your host, with the limit and the actual size printed. If the target is GitHub Pages, the README records its published limits, checked on a stated date: sites no larger than 1 GB, a soft bandwidth limit of 100 GB per month and a soft limit of 10 builds per hour. Do not build yet: No fonts, domains or export yet. ### Phase 5 · Fonts, domains and the ability to walk away Build: Self host fonts and record the licence for each one, refusing to build if a font has no recorded licence note. Connect a custom domain with a certificate and verify it before switching. Export the entire site as content JSON plus assets plus the built release, and confirm the export alone is enough to rebuild. Trap: Converting a font bought for desktop use into a web format and serving it is a licensing decision most people make by accident, and it is the kind of mistake that arrives as a letter rather than a bug report. Record a licence note per font, check that web embedding is permitted before you ship it, and let the build fail when the note is missing. Done when: A font without a recorded licence note fails the build and names the font. The export contains content JSON, originals and the built release, and a rebuild from the export alone produces byte identical output, tested. The custom domain serves over a valid certificate and the previous address redirects rather than breaking. Renaming a page writes a redirect and the old path resolves. Do not build yet: Nothing after this. ### Out of scope (and why) - Forms and comments, because collecting anything from visitors turns a static site into a service you operate, and static hosts explicitly warn against using them for sensitive submissions. - A template marketplace and a component library, which is years of design work rather than a weekend of code and is a large part of what the subscription actually buys. - Multi user editing and client handoff, which needs a permission model, an audit trail and conflict handling on every field. - Commerce, which brings payment processing, tax and order state into what is otherwise a folder of files. ### README must contain - That the output is a folder of static files and the database is a working copy of content that also exists as JSON - That publishing uploads a whole release, verifies it against a manifest and then switches, with rollback as one command - The published limits of the host you chose, with the date you checked them - That images are generated at publish time at a fixed set of widths, and the byte budget the build enforces - That every font ships with a recorded licence note and the build fails without one ===== AGENTS.md ===== # Agent instructions · Cargo indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js with TypeScript, one local user, no accounts, SQLite plus a JSON export written on every save, A deterministic static build into a release folder, Derivatives generated at publish time from untouched originals, Upload a whole release then switch, with the previous release kept. 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 - A freeform canvas that stores each element at an absolute pixel position looks like the product people want and produces a site that is unreadable on a phone, impossible to restyle, and stuck with whichever font metrics the editor had that day. Store typed sections with layout options and let the renderer decide where things go. - Shipping the original from a camera and constraining it with a hundred percent width rule sends a twelve megabyte file to a phone to be displayed at four hundred pixels, and without declared dimensions the page jumps as each one lands. Generate derivatives at publish, emit srcset, and always write width and height. - Rendering pages from the database on request means the site is only alive while your builder is, so the moment the process dies or the disk fills the whole site is gone rather than merely un-editable. Build to files, and treat the database as a working copy of something that already exists on disk. - Syncing the build over the live site file by file leaves visitors during the upload with new HTML asking for stylesheets that are not there yet, and a failure halfway leaves the site permanently in that state with no way back. Upload a whole release, verify it, switch, and keep the previous one so rollback is one command. ===== BUILD_PLAN.md ===== # Build plan · Cargo A site builder whose hard problems are a content model that survives a redesign, images that do not cost a visitor their data allowance, and a publish that is atomic and reversible rather than a file by file sync over a live site. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A content model, not a canvas Pages are typed sections, so a redesign changes the renderer and not the content. ### Steps 1. Define section kinds with explicit fields Five kinds that fit is better than one kind that holds anything. Files: `src/schema/sections.ts` 2. Keep layout as options, not coordinates Options survive a redesign; coordinates are the redesign. Files: `src/render/section.tsx` 3. Write a JSON export on every save So the content is readable without the editor from day one. Files: `src/content-export.ts` ### Done when - [ ] No pixel coordinates exist in content rows - [ ] A new section kind needs only renderer plus migration - [ ] Layout options change rendering, not content - [ ] Pages render at 360 pixels without horizontal scroll ### Watch out - A freeform canvas that stores each element at an absolute pixel position looks like the product people want and produces a site that is unreadable on a phone, impossible to restyle, and stuck with whichever font metrics the editor had that day. Store typed sections with layout options and let the renderer decide where things go. ## Phase 2 · Images that do not cost a visitor their data allowance Every image ships at a sensible size with dimensions declared. ### Steps 1. Generate derivatives at publish time Not at request time, because there is no server to request from. Files: `src/build/images.ts` 2. Always write intrinsic dimensions The cheapest fix for a page that jumps while it loads. Files: `src/render/image.tsx` 3. Fail the build on missing alt text A warning nobody reads is how a site ends up with none of it. Files: `src/build/checks.ts`, `tests/alt-text.test.ts` ### Done when - [ ] The largest released file is under the byte budget - [ ] Every image has width, height, srcset and sizes - [ ] Missing alt text fails the build with a location - [ ] No originals appear in the release ### Watch out - Shipping the original from a camera and constraining it with a hundred percent width rule sends a twelve megabyte file to a phone to be displayed at four hundred pixels, and without declared dimensions the page jumps as each one lands. Generate derivatives at publish, emit srcset, and always write width and height. ## Phase 3 · A build that is just a folder The same content builds to the same bytes and needs nothing running. ### Steps 1. Make the build deterministic Sort everything, and keep timestamps out of the output. Files: `src/build/index.ts`, `tests/deterministic-build.test.ts` 2. Content hash every asset filename So caching is aggressive and still correct. Files: `src/build/assets.ts` 3. Emit a manifest with sizes and hashes It is what publishing, rollback and the size check all read. Files: `src/build/manifest.ts` ### Done when - [ ] Two builds produce identical bytes - [ ] The release serves from a plain static server - [ ] Asset filenames are content hashed - [ ] The manifest reports file count and total bytes ### Watch out - Rendering pages from the database on request means the site is only alive while your builder is, so the moment the process dies or the disk fills the whole site is gone rather than merely un-editable. Build to files, and treat the database as a working copy of something that already exists on disk. ## Phase 4 · Publishing that is all or nothing Visitors see the old site or the new one, never a mixture, and you can go back. ### Steps 1. Upload to a new directory, then switch The switch is the only moment anything changes for a visitor. Files: `src/publish/upload.ts` 2. Verify against the manifest before switching Count and hashes, because a truncated upload looks like a successful one. Files: `src/publish/verify.ts` 3. Make rollback a tested command The night you need it is not the night to find out it was never written. Files: `src/publish/rollback.ts`, `tests/rollback.test.ts` ### Done when - [ ] An interrupted publish leaves the live site untouched - [ ] Rollback is exercised in the suite - [ ] Oversized releases are refused with sizes printed - [ ] Host limits are recorded in the README with a check date ### Watch out - Syncing the build over the live site file by file leaves visitors during the upload with new HTML asking for stylesheets that are not there yet, and a failure halfway leaves the site permanently in that state with no way back. Upload a whole release, verify it, switch, and keep the previous one so rollback is one command. ## Not in this build - Forms and comments. - A template marketplace. - Multi user editing and client handoff. - Commerce. ## After v1, if you want it - A second theme sharing the same content, which is the test of whether the model really is separate from the layout - Scheduled publishing, which is the existing publish behind a timer and nothing more - A link checker in the build, so a broken internal link fails before visitors find it ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Pages, sections and assets. A working copy of the JSON export. DATABASE_PATH=./data/site.db # Required. JSON written on every save. This is the copy that outlives the editor. CONTENT_EXPORT_DIR=./content # Required. Untouched originals. Never included in a release. ASSETS_DIR=./assets/originals # Required. One directory per release, kept for rollback. BUILD_DIR=./releases # Required. Derivative widths emitted into srcset. IMAGE_WIDTHS=400,800,1200,2000 # Required. The build fails if any released file exceeds this. MAX_FILE_BYTES=1500000 # Required. Previous releases retained. This is what rollback uses. RELEASES_KEEP=5 # Optional · secret. Only if your host needs one to upload. Unset means you publish by hand. DEPLOY_TOKEN=sk-...redacted
You are building a production product version of Cargo. 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 ===== # Cargo · product brief ## Problem The visible creative website builder loop is buildable, but a credible replacement needs more than the first screen. Cargo earns its keep through editor, CMS, hosting, polish, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A builder whose output is a folder anyone can host, whose images arrive at a sensible size, whose publish is atomic and reversible, and whose content can be rebuilt from an export after the builder is gone. ## 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 - Forms and comments. - A template marketplace. - Multi user editing and client handoff. - Commerce. - general-purpose visual editing - large template and component libraries - CMS collaboration and client handoff - managed hosting, forms, and analytics ## Success criteria - Two builds are confirmed byte identical - An interrupted publish is confirmed not to affect the live site - Rollback is confirmed in the suite - Every image is confirmed to have dimensions and srcset - A rebuild from the export alone is confirmed identical ===== BRIEF.md ===== # Build brief · Cargo 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 small site builder whose output is a folder of plain static files you could host anywhere to replace Cargo. 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) - Editor: Next.js with TypeScript, one local user, no accounts · the editor is a tool you run, not a service you operate - Content: SQLite plus a JSON export written on every save · the content has to be readable when this application is not running - Output: A deterministic static build into a release folder · a folder of files can be hosted anywhere and outlives the builder - Images: Derivatives generated at publish time from untouched originals · the original is the only thing you cannot regenerate later - Publishing: Upload a whole release then switch, with the previous release kept · a visitor should never see half of an update ### Data model (create this before Phase 1) - sites(id, name, base_url, primary_domain, created_at) - pages(id, site_id, slug, title, description, status, ordinal, updated_at) · slug is unique per site and changing it records a redirect - sections(id, page_id, ordinal, kind, content_json, layout_json) · typed sections with layout options, never absolute pixel positions - assets(id, site_id, original_path, sha256, width, height, bytes, alt_text) · alt text lives with the asset, not with one use of it - derivatives(asset_id, width, format, path, bytes) · generated at publish, safe to delete, regenerated on the next build - releases(id, site_id, built_at, output_dir, file_count, bytes_total, published_at, rolled_back_at) - redirects(site_id, from_path, to_path, created_at) · because a renamed page that returns nothing is how a site loses its links ### Phase 1 · A content model, not a canvas Build: Define a small set of section kinds with their own fields: text, image, gallery, embed, index. A page is an ordered list of sections. Layout choices live beside the content as options, and the renderer turns sections into HTML with no positional coordinates anywhere. Trap: A freeform canvas that stores each element at an absolute pixel position looks like the product people want and produces a site that is unreadable on a phone, impossible to restyle, and stuck with whichever font metrics the editor had that day. Store typed sections with layout options and let the renderer decide where things go. Done when: No content row contains a pixel coordinate, verified by inspecting the schema and a saved page. Adding a new section kind requires a renderer change and a migration and nothing else, demonstrated by adding one. Changing the layout option on a section changes its rendering without touching its content. Every page renders at a narrow viewport without a horizontal scrollbar, checked at 360 pixels wide. Do not build yet: No images, build, publishing or domains. ### Phase 2 · Images that do not cost a visitor their data allowance Build: At publish, generate derivatives at a fixed set of widths and emit srcset and sizes. Write the intrinsic width and height on every image element. Keep originals untouched and outside the release. Require alt text on the asset, with an explicit decorative option. Trap: Shipping the original from a camera and constraining it with a hundred percent width rule sends a twelve megabyte file to a phone to be displayed at four hundred pixels, and without declared dimensions the page jumps as each one lands. Generate derivatives at publish, emit srcset, and always write width and height. Done when: The largest file in a built release is under a stated byte budget, asserted by the build. Every image element carries width, height, srcset and sizes, checked by parsing the output. An image with neither alt text nor a decorative flag fails the build with the page and section named. Originals are not present anywhere in the release directory. Do not build yet: No publishing, rollback, fonts or domains yet. ### Phase 3 · A build that is just a folder Build: Build every page to static HTML with content hashed asset filenames. Building twice from unchanged content produces identical files. Emit a sitemap, the redirects file for the host, and a manifest listing every file with its size and hash. Trap: Rendering pages from the database on request means the site is only alive while your builder is, so the moment the process dies or the disk fills the whole site is gone rather than merely un-editable. Build to files, and treat the database as a working copy of something that already exists on disk. Done when: Two consecutive builds from unchanged content produce byte identical output, tested by comparing hashes. The release directory serves correctly from any plain static file server with no other process running. Every asset filename contains a content hash, so a changed file never reuses a cached URL. The manifest lists file count and total bytes and the build prints both. Do not build yet: No publishing, rollback, fonts or domains yet. ### Phase 4 · Publishing that is all or nothing Build: Upload the release to a new directory, verify the file count and hashes against the manifest, then switch the pointer. Keep a stated number of previous releases and provide a rollback command. Check the release against the host's published limits before uploading. Trap: Syncing the build over the live site file by file leaves visitors during the upload with new HTML asking for stylesheets that are not there yet, and a failure halfway leaves the site permanently in that state with no way back. Upload a whole release, verify it, switch, and keep the previous one so rollback is one command. Done when: A publish interrupted halfway leaves the live site untouched, tested by killing the upload. Rollback restores the previous release and is exercised in the suite rather than described. The build refuses to publish a release that exceeds the size limit you configured for your host, with the limit and the actual size printed. If the target is GitHub Pages, the README records its published limits, checked on a stated date: sites no larger than 1 GB, a soft bandwidth limit of 100 GB per month and a soft limit of 10 builds per hour. Do not build yet: No fonts, domains or export yet. ### Phase 5 · Fonts, domains and the ability to walk away Build: Self host fonts and record the licence for each one, refusing to build if a font has no recorded licence note. Connect a custom domain with a certificate and verify it before switching. Export the entire site as content JSON plus assets plus the built release, and confirm the export alone is enough to rebuild. Trap: Converting a font bought for desktop use into a web format and serving it is a licensing decision most people make by accident, and it is the kind of mistake that arrives as a letter rather than a bug report. Record a licence note per font, check that web embedding is permitted before you ship it, and let the build fail when the note is missing. Done when: A font without a recorded licence note fails the build and names the font. The export contains content JSON, originals and the built release, and a rebuild from the export alone produces byte identical output, tested. The custom domain serves over a valid certificate and the previous address redirects rather than breaking. Renaming a page writes a redirect and the old path resolves. Do not build yet: Nothing after this. ### Out of scope (and why) - Forms and comments, because collecting anything from visitors turns a static site into a service you operate, and static hosts explicitly warn against using them for sensitive submissions. - A template marketplace and a component library, which is years of design work rather than a weekend of code and is a large part of what the subscription actually buys. - Multi user editing and client handoff, which needs a permission model, an audit trail and conflict handling on every field. - Commerce, which brings payment processing, tax and order state into what is otherwise a folder of files. ### README must contain - That the output is a folder of static files and the database is a working copy of content that also exists as JSON - That publishing uploads a whole release, verifies it against a manifest and then switches, with rollback as one command - The published limits of the host you chose, with the date you checked them - That images are generated at publish time at a fixed set of widths, and the byte budget the build enforces - That every font ships with a recorded licence note and the build fails without one ===== ARCHITECTURE.md ===== # Architecture · Cargo ## Stack | Part | Choice | Why | | --- | --- | --- | | Editor | Next.js with TypeScript, one local user, no accounts | the editor is a tool you run, not a service you operate | | Content | SQLite plus a JSON export written on every save | the content has to be readable when this application is not running | | Output | A deterministic static build into a release folder | a folder of files can be hosted anywhere and outlives the builder | | Images | Derivatives generated at publish time from untouched originals | the original is the only thing you cannot regenerate later | | Publishing | Upload a whole release then switch, with the previous release kept | a visitor should never see half of an update | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Content | Typed sections and the JSON export. | Any editor, provided no coordinate ever becomes content. | | Images | Derivatives, srcset and the byte budget. | Any library, provided originals stay out of the release. | | Build | Deterministic output and the manifest. | Any generator whose two runs produce the same bytes. | | Publish | Whole release upload, verify, switch, rollback. | Not swappable for a file by file sync over the live site. | | Export | Everything needed to rebuild elsewhere. | Any archive format, provided a rebuild from it is tested. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · Pages, sections and assets. A working copy of the JSON export. - `CONTENT_EXPORT_DIR` · required · JSON written on every save. This is the copy that outlives the editor. - `ASSETS_DIR` · required · Untouched originals. Never included in a release. - `BUILD_DIR` · required · One directory per release, kept for rollback. - `IMAGE_WIDTHS` · required · Derivative widths emitted into srcset. - `MAX_FILE_BYTES` · required · The build fails if any released file exceeds this. - `RELEASES_KEEP` · required · Previous releases retained. This is what rollback uses. - `DEPLOY_TOKEN` · optional, secret · Only if your host needs one to upload. Unset means you publish by hand. ## 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 · Cargo product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Next.js with TypeScript, one local user, no accounts, SQLite plus a JSON export written on every save, A deterministic static build into a release folder, Derivatives generated at publish time from untouched originals, Upload a whole release then switch, with the previous release kept. - 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 - A freeform canvas that stores each element at an absolute pixel position looks like the product people want and produces a site that is unreadable on a phone, impossible to restyle, and stuck with whichever font metrics the editor had that day. Store typed sections with layout options and let the renderer decide where things go. - Shipping the original from a camera and constraining it with a hundred percent width rule sends a twelve megabyte file to a phone to be displayed at four hundred pixels, and without declared dimensions the page jumps as each one lands. Generate derivatives at publish, emit srcset, and always write width and height. - Rendering pages from the database on request means the site is only alive while your builder is, so the moment the process dies or the disk fills the whole site is gone rather than merely un-editable. Build to files, and treat the database as a working copy of something that already exists on disk. - Syncing the build over the live site file by file leaves visitors during the upload with new HTML asking for stylesheets that are not there yet, and a failure halfway leaves the site permanently in that state with no way back. Upload a whole release, verify it, switch, and keep the previous one so rollback is one command. - Converting a font bought for desktop use into a web format and serving it is a licensing decision most people make by accident, and it is the kind of mistake that arrives as a letter rather than a bug report. Record a licence note per font, check that web embedding is permitted before you ship it, and let the build fail when the note is missing. ===== MILESTONES.md ===== # Delivery milestones · Cargo Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A content model, not a canvas Pages are typed sections, so a redesign changes the renderer and not the content. ### Steps 1. Define section kinds with explicit fields Five kinds that fit is better than one kind that holds anything. Files: `src/schema/sections.ts` 2. Keep layout as options, not coordinates Options survive a redesign; coordinates are the redesign. Files: `src/render/section.tsx` 3. Write a JSON export on every save So the content is readable without the editor from day one. Files: `src/content-export.ts` ### Done when - [ ] No pixel coordinates exist in content rows - [ ] A new section kind needs only renderer plus migration - [ ] Layout options change rendering, not content - [ ] Pages render at 360 pixels without horizontal scroll ### Watch out - A freeform canvas that stores each element at an absolute pixel position looks like the product people want and produces a site that is unreadable on a phone, impossible to restyle, and stuck with whichever font metrics the editor had that day. Store typed sections with layout options and let the renderer decide where things go. ## M2 · Images that do not cost a visitor their data allowance Every image ships at a sensible size with dimensions declared. ### Steps 1. Generate derivatives at publish time Not at request time, because there is no server to request from. Files: `src/build/images.ts` 2. Always write intrinsic dimensions The cheapest fix for a page that jumps while it loads. Files: `src/render/image.tsx` 3. Fail the build on missing alt text A warning nobody reads is how a site ends up with none of it. Files: `src/build/checks.ts`, `tests/alt-text.test.ts` ### Done when - [ ] The largest released file is under the byte budget - [ ] Every image has width, height, srcset and sizes - [ ] Missing alt text fails the build with a location - [ ] No originals appear in the release ### Watch out - Shipping the original from a camera and constraining it with a hundred percent width rule sends a twelve megabyte file to a phone to be displayed at four hundred pixels, and without declared dimensions the page jumps as each one lands. Generate derivatives at publish, emit srcset, and always write width and height. ## M3 · A build that is just a folder The same content builds to the same bytes and needs nothing running. ### Steps 1. Make the build deterministic Sort everything, and keep timestamps out of the output. Files: `src/build/index.ts`, `tests/deterministic-build.test.ts` 2. Content hash every asset filename So caching is aggressive and still correct. Files: `src/build/assets.ts` 3. Emit a manifest with sizes and hashes It is what publishing, rollback and the size check all read. Files: `src/build/manifest.ts` ### Done when - [ ] Two builds produce identical bytes - [ ] The release serves from a plain static server - [ ] Asset filenames are content hashed - [ ] The manifest reports file count and total bytes ### Watch out - Rendering pages from the database on request means the site is only alive while your builder is, so the moment the process dies or the disk fills the whole site is gone rather than merely un-editable. Build to files, and treat the database as a working copy of something that already exists on disk. ## M4 · Publishing that is all or nothing Visitors see the old site or the new one, never a mixture, and you can go back. ### Steps 1. Upload to a new directory, then switch The switch is the only moment anything changes for a visitor. Files: `src/publish/upload.ts` 2. Verify against the manifest before switching Count and hashes, because a truncated upload looks like a successful one. Files: `src/publish/verify.ts` 3. Make rollback a tested command The night you need it is not the night to find out it was never written. Files: `src/publish/rollback.ts`, `tests/rollback.test.ts` ### Done when - [ ] An interrupted publish leaves the live site untouched - [ ] Rollback is exercised in the suite - [ ] Oversized releases are refused with sizes printed - [ ] Host limits are recorded in the README with a check date ### Watch out - Syncing the build over the live site file by file leaves visitors during the upload with new HTML asking for stylesheets that are not there yet, and a failure halfway leaves the site permanently in that state with no way back. Upload a whole release, verify it, switch, and keep the previous one so rollback is one command. ## M5 · Fonts, domains and the ability to walk away (production only) The site keeps working without the builder, and every asset in it is one you may use. ### Steps 1. Record a licence note per font One sentence per font, checked by the build, saves a legal conversation. Files: `src/build/fonts.ts` 2. Export everything needed to rebuild Content, originals and the release, in one archive. Files: `src/export.ts`, `tests/rebuild-from-export.test.ts` 3. Write redirects on slug changes A renamed page that returns nothing is how a site loses its inbound links. Files: `src/redirects.ts` ### Done when - [ ] A font with no licence note fails the build - [ ] A rebuild from the export alone is byte identical - [ ] The custom domain serves with a valid certificate - [ ] Renamed pages redirect from their old paths ### Watch out - Converting a font bought for desktop use into a web format and serving it is a licensing decision most people make by accident, and it is the kind of mistake that arrives as a letter rather than a bug report. Record a licence note per font, check that web embedding is permitted before you ship it, and let the build fail when the note is missing. ===== OPERATIONS.md ===== # Operations · Cargo ## Backup The content export directory and the originals; the database and the releases can both be regenerated from them. Keep the last export off the machine that runs the editor. ## Restore Restore content and originals, run a build, and compare the manifest against the last published release. A difference means either content was lost or the build is not deterministic, and both are worth knowing before you publish. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Release size against the host's published limit, the largest file in each release, and whether the live pointer matches the release you think you published. Check the host's limits page on a schedule, because those numbers change. ## Incident checklist The two incidents are a half published site, which the upload then switch order exists to prevent, and a site that is up but no longer editable, which is why the output is files and the export is tested. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Two builds are confirmed byte identical - [ ] An interrupted publish is confirmed not to affect the live site - [ ] Rollback is confirmed in the suite - [ ] Every image is confirmed to have dimensions and srcset - [ ] A rebuild from the export alone is confirmed identical ## Launch constraint Do not market omitted Cargo capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Pages, sections and assets. A working copy of the JSON export. DATABASE_PATH=./data/site.db # Required. JSON written on every save. This is the copy that outlives the editor. CONTENT_EXPORT_DIR=./content # Required. Untouched originals. Never included in a release. ASSETS_DIR=./assets/originals # Required. One directory per release, kept for rollback. BUILD_DIR=./releases # Required. Derivative widths emitted into srcset. IMAGE_WIDTHS=400,800,1200,2000 # Required. The build fails if any released file exceeds this. MAX_FILE_BYTES=1500000 # Required. Previous releases retained. This is what rollback uses. RELEASES_KEEP=5 # Optional · secret. Only if your host needs one to upload. Unset means you publish by hand. DEPLOY_TOKEN=sk-...redacted
# Cargo · indie build A site builder whose hard problems are a content model that survives a redesign, images that do not cost a visitor their data allowance, and a publish that is atomic and reversible rather than a file by file sync over a live site. 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 | | --- | --- | --- | | Editor | Next.js with TypeScript, one local user, no accounts | the editor is a tool you run, not a service you operate | | Content | SQLite plus a JSON export written on every save | the content has to be readable when this application is not running | | Output | A deterministic static build into a release folder | a folder of files can be hosted anywhere and outlives the builder | | Images | Derivatives generated at publish time from untouched originals | the original is the only thing you cannot regenerate later | | Publishing | Upload a whole release then switch, with the previous release kept | a visitor should never see half of an update | ## 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 editor and the build. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **An image processing library for your runtime** · free - Why: Derivative generation at several widths is the difference between a fast site and a slow one, and doing it by hand does not happen. - Get it: Any maintained library that resizes and re-encodes, installed as a dependency. - Verify: It produces a resized file at three widths from one original - [ ] **A static host you can upload a whole directory to** · free to a few pounds a month - Why: Atomic publishing needs a host where you can upload a release and then switch, rather than one that only offers a file by file sync. - Get it: Any static hosting or object storage you already have, plus its published limits page. - Verify: You can upload a directory and serve it, and you have read the limits page - [ ] **A domain you control** (optional) · around ten pounds a year - Why: Domain and certificate handling is the part that breaks on a Friday, and testing it against a real name is the only test that counts. - Get it: Any registrar, with access to the DNS records. - Verify: A record you add resolves within an hour - [ ] **A licence note for every font you ship** · free - Why: Web embedding is a separate permission from desktop use, and the build refuses to run without the note. - Get it: For each font, record the licence and one sentence on whether web embedding is permitted. - Verify: Every font file in the project has a note beside it and the build passes ## 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: - Forms and comments. - A template marketplace. - Multi user editing and client handoff. - Commerce. - general-purpose visual editing - large template and component libraries - CMS collaboration and client handoff - managed hosting, forms, and analytics If one of those is essential to you, that is the reason to keep paying for Cargo, and the README should say so rather than pretend.
# Build brief · Cargo 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 small site builder whose output is a folder of plain static files you could host anywhere to replace Cargo. 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) - Editor: Next.js with TypeScript, one local user, no accounts · the editor is a tool you run, not a service you operate - Content: SQLite plus a JSON export written on every save · the content has to be readable when this application is not running - Output: A deterministic static build into a release folder · a folder of files can be hosted anywhere and outlives the builder - Images: Derivatives generated at publish time from untouched originals · the original is the only thing you cannot regenerate later - Publishing: Upload a whole release then switch, with the previous release kept · a visitor should never see half of an update ### Data model (create this before Phase 1) - sites(id, name, base_url, primary_domain, created_at) - pages(id, site_id, slug, title, description, status, ordinal, updated_at) · slug is unique per site and changing it records a redirect - sections(id, page_id, ordinal, kind, content_json, layout_json) · typed sections with layout options, never absolute pixel positions - assets(id, site_id, original_path, sha256, width, height, bytes, alt_text) · alt text lives with the asset, not with one use of it - derivatives(asset_id, width, format, path, bytes) · generated at publish, safe to delete, regenerated on the next build - releases(id, site_id, built_at, output_dir, file_count, bytes_total, published_at, rolled_back_at) - redirects(site_id, from_path, to_path, created_at) · because a renamed page that returns nothing is how a site loses its links ### Phase 1 · A content model, not a canvas Build: Define a small set of section kinds with their own fields: text, image, gallery, embed, index. A page is an ordered list of sections. Layout choices live beside the content as options, and the renderer turns sections into HTML with no positional coordinates anywhere. Trap: A freeform canvas that stores each element at an absolute pixel position looks like the product people want and produces a site that is unreadable on a phone, impossible to restyle, and stuck with whichever font metrics the editor had that day. Store typed sections with layout options and let the renderer decide where things go. Done when: No content row contains a pixel coordinate, verified by inspecting the schema and a saved page. Adding a new section kind requires a renderer change and a migration and nothing else, demonstrated by adding one. Changing the layout option on a section changes its rendering without touching its content. Every page renders at a narrow viewport without a horizontal scrollbar, checked at 360 pixels wide. Do not build yet: No images, build, publishing or domains. ### Phase 2 · Images that do not cost a visitor their data allowance Build: At publish, generate derivatives at a fixed set of widths and emit srcset and sizes. Write the intrinsic width and height on every image element. Keep originals untouched and outside the release. Require alt text on the asset, with an explicit decorative option. Trap: Shipping the original from a camera and constraining it with a hundred percent width rule sends a twelve megabyte file to a phone to be displayed at four hundred pixels, and without declared dimensions the page jumps as each one lands. Generate derivatives at publish, emit srcset, and always write width and height. Done when: The largest file in a built release is under a stated byte budget, asserted by the build. Every image element carries width, height, srcset and sizes, checked by parsing the output. An image with neither alt text nor a decorative flag fails the build with the page and section named. Originals are not present anywhere in the release directory. Do not build yet: No publishing, rollback, fonts or domains yet. ### Phase 3 · A build that is just a folder Build: Build every page to static HTML with content hashed asset filenames. Building twice from unchanged content produces identical files. Emit a sitemap, the redirects file for the host, and a manifest listing every file with its size and hash. Trap: Rendering pages from the database on request means the site is only alive while your builder is, so the moment the process dies or the disk fills the whole site is gone rather than merely un-editable. Build to files, and treat the database as a working copy of something that already exists on disk. Done when: Two consecutive builds from unchanged content produce byte identical output, tested by comparing hashes. The release directory serves correctly from any plain static file server with no other process running. Every asset filename contains a content hash, so a changed file never reuses a cached URL. The manifest lists file count and total bytes and the build prints both. Do not build yet: No publishing, rollback, fonts or domains yet. ### Phase 4 · Publishing that is all or nothing Build: Upload the release to a new directory, verify the file count and hashes against the manifest, then switch the pointer. Keep a stated number of previous releases and provide a rollback command. Check the release against the host's published limits before uploading. Trap: Syncing the build over the live site file by file leaves visitors during the upload with new HTML asking for stylesheets that are not there yet, and a failure halfway leaves the site permanently in that state with no way back. Upload a whole release, verify it, switch, and keep the previous one so rollback is one command. Done when: A publish interrupted halfway leaves the live site untouched, tested by killing the upload. Rollback restores the previous release and is exercised in the suite rather than described. The build refuses to publish a release that exceeds the size limit you configured for your host, with the limit and the actual size printed. If the target is GitHub Pages, the README records its published limits, checked on a stated date: sites no larger than 1 GB, a soft bandwidth limit of 100 GB per month and a soft limit of 10 builds per hour. Do not build yet: No fonts, domains or export yet. ### Phase 5 · Fonts, domains and the ability to walk away Build: Self host fonts and record the licence for each one, refusing to build if a font has no recorded licence note. Connect a custom domain with a certificate and verify it before switching. Export the entire site as content JSON plus assets plus the built release, and confirm the export alone is enough to rebuild. Trap: Converting a font bought for desktop use into a web format and serving it is a licensing decision most people make by accident, and it is the kind of mistake that arrives as a letter rather than a bug report. Record a licence note per font, check that web embedding is permitted before you ship it, and let the build fail when the note is missing. Done when: A font without a recorded licence note fails the build and names the font. The export contains content JSON, originals and the built release, and a rebuild from the export alone produces byte identical output, tested. The custom domain serves over a valid certificate and the previous address redirects rather than breaking. Renaming a page writes a redirect and the old path resolves. Do not build yet: Nothing after this. ### Out of scope (and why) - Forms and comments, because collecting anything from visitors turns a static site into a service you operate, and static hosts explicitly warn against using them for sensitive submissions. - A template marketplace and a component library, which is years of design work rather than a weekend of code and is a large part of what the subscription actually buys. - Multi user editing and client handoff, which needs a permission model, an audit trail and conflict handling on every field. - Commerce, which brings payment processing, tax and order state into what is otherwise a folder of files. ### README must contain - That the output is a folder of static files and the database is a working copy of content that also exists as JSON - That publishing uploads a whole release, verifies it against a manifest and then switches, with rollback as one command - The published limits of the host you chose, with the date you checked them - That images are generated at publish time at a fixed set of widths, and the byte budget the build enforces - That every font ships with a recorded licence note and the build fails without one
# Agent instructions · Cargo indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js with TypeScript, one local user, no accounts, SQLite plus a JSON export written on every save, A deterministic static build into a release folder, Derivatives generated at publish time from untouched originals, Upload a whole release then switch, with the previous release kept. 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 - A freeform canvas that stores each element at an absolute pixel position looks like the product people want and produces a site that is unreadable on a phone, impossible to restyle, and stuck with whichever font metrics the editor had that day. Store typed sections with layout options and let the renderer decide where things go. - Shipping the original from a camera and constraining it with a hundred percent width rule sends a twelve megabyte file to a phone to be displayed at four hundred pixels, and without declared dimensions the page jumps as each one lands. Generate derivatives at publish, emit srcset, and always write width and height. - Rendering pages from the database on request means the site is only alive while your builder is, so the moment the process dies or the disk fills the whole site is gone rather than merely un-editable. Build to files, and treat the database as a working copy of something that already exists on disk. - Syncing the build over the live site file by file leaves visitors during the upload with new HTML asking for stylesheets that are not there yet, and a failure halfway leaves the site permanently in that state with no way back. Upload a whole release, verify it, switch, and keep the previous one so rollback is one command.
# Build plan · Cargo A site builder whose hard problems are a content model that survives a redesign, images that do not cost a visitor their data allowance, and a publish that is atomic and reversible rather than a file by file sync over a live site. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A content model, not a canvas Pages are typed sections, so a redesign changes the renderer and not the content. ### Steps 1. Define section kinds with explicit fields Five kinds that fit is better than one kind that holds anything. Files: `src/schema/sections.ts` 2. Keep layout as options, not coordinates Options survive a redesign; coordinates are the redesign. Files: `src/render/section.tsx` 3. Write a JSON export on every save So the content is readable without the editor from day one. Files: `src/content-export.ts` ### Done when - [ ] No pixel coordinates exist in content rows - [ ] A new section kind needs only renderer plus migration - [ ] Layout options change rendering, not content - [ ] Pages render at 360 pixels without horizontal scroll ### Watch out - A freeform canvas that stores each element at an absolute pixel position looks like the product people want and produces a site that is unreadable on a phone, impossible to restyle, and stuck with whichever font metrics the editor had that day. Store typed sections with layout options and let the renderer decide where things go. ## Phase 2 · Images that do not cost a visitor their data allowance Every image ships at a sensible size with dimensions declared. ### Steps 1. Generate derivatives at publish time Not at request time, because there is no server to request from. Files: `src/build/images.ts` 2. Always write intrinsic dimensions The cheapest fix for a page that jumps while it loads. Files: `src/render/image.tsx` 3. Fail the build on missing alt text A warning nobody reads is how a site ends up with none of it. Files: `src/build/checks.ts`, `tests/alt-text.test.ts` ### Done when - [ ] The largest released file is under the byte budget - [ ] Every image has width, height, srcset and sizes - [ ] Missing alt text fails the build with a location - [ ] No originals appear in the release ### Watch out - Shipping the original from a camera and constraining it with a hundred percent width rule sends a twelve megabyte file to a phone to be displayed at four hundred pixels, and without declared dimensions the page jumps as each one lands. Generate derivatives at publish, emit srcset, and always write width and height. ## Phase 3 · A build that is just a folder The same content builds to the same bytes and needs nothing running. ### Steps 1. Make the build deterministic Sort everything, and keep timestamps out of the output. Files: `src/build/index.ts`, `tests/deterministic-build.test.ts` 2. Content hash every asset filename So caching is aggressive and still correct. Files: `src/build/assets.ts` 3. Emit a manifest with sizes and hashes It is what publishing, rollback and the size check all read. Files: `src/build/manifest.ts` ### Done when - [ ] Two builds produce identical bytes - [ ] The release serves from a plain static server - [ ] Asset filenames are content hashed - [ ] The manifest reports file count and total bytes ### Watch out - Rendering pages from the database on request means the site is only alive while your builder is, so the moment the process dies or the disk fills the whole site is gone rather than merely un-editable. Build to files, and treat the database as a working copy of something that already exists on disk. ## Phase 4 · Publishing that is all or nothing Visitors see the old site or the new one, never a mixture, and you can go back. ### Steps 1. Upload to a new directory, then switch The switch is the only moment anything changes for a visitor. Files: `src/publish/upload.ts` 2. Verify against the manifest before switching Count and hashes, because a truncated upload looks like a successful one. Files: `src/publish/verify.ts` 3. Make rollback a tested command The night you need it is not the night to find out it was never written. Files: `src/publish/rollback.ts`, `tests/rollback.test.ts` ### Done when - [ ] An interrupted publish leaves the live site untouched - [ ] Rollback is exercised in the suite - [ ] Oversized releases are refused with sizes printed - [ ] Host limits are recorded in the README with a check date ### Watch out - Syncing the build over the live site file by file leaves visitors during the upload with new HTML asking for stylesheets that are not there yet, and a failure halfway leaves the site permanently in that state with no way back. Upload a whole release, verify it, switch, and keep the previous one so rollback is one command. ## Not in this build - Forms and comments. - A template marketplace. - Multi user editing and client handoff. - Commerce. ## After v1, if you want it - A second theme sharing the same content, which is the test of whether the model really is separate from the layout - Scheduled publishing, which is the existing publish behind a timer and nothing more - A link checker in the build, so a broken internal link fails before visitors find it
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Pages, sections and assets. A working copy of the JSON export. DATABASE_PATH=./data/site.db # Required. JSON written on every save. This is the copy that outlives the editor. CONTENT_EXPORT_DIR=./content # Required. Untouched originals. Never included in a release. ASSETS_DIR=./assets/originals # Required. One directory per release, kept for rollback. BUILD_DIR=./releases # Required. Derivative widths emitted into srcset. IMAGE_WIDTHS=400,800,1200,2000 # Required. The build fails if any released file exceeds this. MAX_FILE_BYTES=1500000 # Required. Previous releases retained. This is what rollback uses. RELEASES_KEEP=5 # Optional · secret. Only if your host needs one to upload. Unset means you publish by hand. DEPLOY_TOKEN=sk-...redacted
# Cargo · product brief ## Problem The visible creative website builder loop is buildable, but a credible replacement needs more than the first screen. Cargo earns its keep through editor, CMS, hosting, polish, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A builder whose output is a folder anyone can host, whose images arrive at a sensible size, whose publish is atomic and reversible, and whose content can be rebuilt from an export after the builder is gone. ## 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 - Forms and comments. - A template marketplace. - Multi user editing and client handoff. - Commerce. - general-purpose visual editing - large template and component libraries - CMS collaboration and client handoff - managed hosting, forms, and analytics ## Success criteria - Two builds are confirmed byte identical - An interrupted publish is confirmed not to affect the live site - Rollback is confirmed in the suite - Every image is confirmed to have dimensions and srcset - A rebuild from the export alone is confirmed identical
# Build brief · Cargo 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 small site builder whose output is a folder of plain static files you could host anywhere to replace Cargo. 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) - Editor: Next.js with TypeScript, one local user, no accounts · the editor is a tool you run, not a service you operate - Content: SQLite plus a JSON export written on every save · the content has to be readable when this application is not running - Output: A deterministic static build into a release folder · a folder of files can be hosted anywhere and outlives the builder - Images: Derivatives generated at publish time from untouched originals · the original is the only thing you cannot regenerate later - Publishing: Upload a whole release then switch, with the previous release kept · a visitor should never see half of an update ### Data model (create this before Phase 1) - sites(id, name, base_url, primary_domain, created_at) - pages(id, site_id, slug, title, description, status, ordinal, updated_at) · slug is unique per site and changing it records a redirect - sections(id, page_id, ordinal, kind, content_json, layout_json) · typed sections with layout options, never absolute pixel positions - assets(id, site_id, original_path, sha256, width, height, bytes, alt_text) · alt text lives with the asset, not with one use of it - derivatives(asset_id, width, format, path, bytes) · generated at publish, safe to delete, regenerated on the next build - releases(id, site_id, built_at, output_dir, file_count, bytes_total, published_at, rolled_back_at) - redirects(site_id, from_path, to_path, created_at) · because a renamed page that returns nothing is how a site loses its links ### Phase 1 · A content model, not a canvas Build: Define a small set of section kinds with their own fields: text, image, gallery, embed, index. A page is an ordered list of sections. Layout choices live beside the content as options, and the renderer turns sections into HTML with no positional coordinates anywhere. Trap: A freeform canvas that stores each element at an absolute pixel position looks like the product people want and produces a site that is unreadable on a phone, impossible to restyle, and stuck with whichever font metrics the editor had that day. Store typed sections with layout options and let the renderer decide where things go. Done when: No content row contains a pixel coordinate, verified by inspecting the schema and a saved page. Adding a new section kind requires a renderer change and a migration and nothing else, demonstrated by adding one. Changing the layout option on a section changes its rendering without touching its content. Every page renders at a narrow viewport without a horizontal scrollbar, checked at 360 pixels wide. Do not build yet: No images, build, publishing or domains. ### Phase 2 · Images that do not cost a visitor their data allowance Build: At publish, generate derivatives at a fixed set of widths and emit srcset and sizes. Write the intrinsic width and height on every image element. Keep originals untouched and outside the release. Require alt text on the asset, with an explicit decorative option. Trap: Shipping the original from a camera and constraining it with a hundred percent width rule sends a twelve megabyte file to a phone to be displayed at four hundred pixels, and without declared dimensions the page jumps as each one lands. Generate derivatives at publish, emit srcset, and always write width and height. Done when: The largest file in a built release is under a stated byte budget, asserted by the build. Every image element carries width, height, srcset and sizes, checked by parsing the output. An image with neither alt text nor a decorative flag fails the build with the page and section named. Originals are not present anywhere in the release directory. Do not build yet: No publishing, rollback, fonts or domains yet. ### Phase 3 · A build that is just a folder Build: Build every page to static HTML with content hashed asset filenames. Building twice from unchanged content produces identical files. Emit a sitemap, the redirects file for the host, and a manifest listing every file with its size and hash. Trap: Rendering pages from the database on request means the site is only alive while your builder is, so the moment the process dies or the disk fills the whole site is gone rather than merely un-editable. Build to files, and treat the database as a working copy of something that already exists on disk. Done when: Two consecutive builds from unchanged content produce byte identical output, tested by comparing hashes. The release directory serves correctly from any plain static file server with no other process running. Every asset filename contains a content hash, so a changed file never reuses a cached URL. The manifest lists file count and total bytes and the build prints both. Do not build yet: No publishing, rollback, fonts or domains yet. ### Phase 4 · Publishing that is all or nothing Build: Upload the release to a new directory, verify the file count and hashes against the manifest, then switch the pointer. Keep a stated number of previous releases and provide a rollback command. Check the release against the host's published limits before uploading. Trap: Syncing the build over the live site file by file leaves visitors during the upload with new HTML asking for stylesheets that are not there yet, and a failure halfway leaves the site permanently in that state with no way back. Upload a whole release, verify it, switch, and keep the previous one so rollback is one command. Done when: A publish interrupted halfway leaves the live site untouched, tested by killing the upload. Rollback restores the previous release and is exercised in the suite rather than described. The build refuses to publish a release that exceeds the size limit you configured for your host, with the limit and the actual size printed. If the target is GitHub Pages, the README records its published limits, checked on a stated date: sites no larger than 1 GB, a soft bandwidth limit of 100 GB per month and a soft limit of 10 builds per hour. Do not build yet: No fonts, domains or export yet. ### Phase 5 · Fonts, domains and the ability to walk away Build: Self host fonts and record the licence for each one, refusing to build if a font has no recorded licence note. Connect a custom domain with a certificate and verify it before switching. Export the entire site as content JSON plus assets plus the built release, and confirm the export alone is enough to rebuild. Trap: Converting a font bought for desktop use into a web format and serving it is a licensing decision most people make by accident, and it is the kind of mistake that arrives as a letter rather than a bug report. Record a licence note per font, check that web embedding is permitted before you ship it, and let the build fail when the note is missing. Done when: A font without a recorded licence note fails the build and names the font. The export contains content JSON, originals and the built release, and a rebuild from the export alone produces byte identical output, tested. The custom domain serves over a valid certificate and the previous address redirects rather than breaking. Renaming a page writes a redirect and the old path resolves. Do not build yet: Nothing after this. ### Out of scope (and why) - Forms and comments, because collecting anything from visitors turns a static site into a service you operate, and static hosts explicitly warn against using them for sensitive submissions. - A template marketplace and a component library, which is years of design work rather than a weekend of code and is a large part of what the subscription actually buys. - Multi user editing and client handoff, which needs a permission model, an audit trail and conflict handling on every field. - Commerce, which brings payment processing, tax and order state into what is otherwise a folder of files. ### README must contain - That the output is a folder of static files and the database is a working copy of content that also exists as JSON - That publishing uploads a whole release, verifies it against a manifest and then switches, with rollback as one command - The published limits of the host you chose, with the date you checked them - That images are generated at publish time at a fixed set of widths, and the byte budget the build enforces - That every font ships with a recorded licence note and the build fails without one
# Architecture · Cargo ## Stack | Part | Choice | Why | | --- | --- | --- | | Editor | Next.js with TypeScript, one local user, no accounts | the editor is a tool you run, not a service you operate | | Content | SQLite plus a JSON export written on every save | the content has to be readable when this application is not running | | Output | A deterministic static build into a release folder | a folder of files can be hosted anywhere and outlives the builder | | Images | Derivatives generated at publish time from untouched originals | the original is the only thing you cannot regenerate later | | Publishing | Upload a whole release then switch, with the previous release kept | a visitor should never see half of an update | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Content | Typed sections and the JSON export. | Any editor, provided no coordinate ever becomes content. | | Images | Derivatives, srcset and the byte budget. | Any library, provided originals stay out of the release. | | Build | Deterministic output and the manifest. | Any generator whose two runs produce the same bytes. | | Publish | Whole release upload, verify, switch, rollback. | Not swappable for a file by file sync over the live site. | | Export | Everything needed to rebuild elsewhere. | Any archive format, provided a rebuild from it is tested. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · Pages, sections and assets. A working copy of the JSON export. - `CONTENT_EXPORT_DIR` · required · JSON written on every save. This is the copy that outlives the editor. - `ASSETS_DIR` · required · Untouched originals. Never included in a release. - `BUILD_DIR` · required · One directory per release, kept for rollback. - `IMAGE_WIDTHS` · required · Derivative widths emitted into srcset. - `MAX_FILE_BYTES` · required · The build fails if any released file exceeds this. - `RELEASES_KEEP` · required · Previous releases retained. This is what rollback uses. - `DEPLOY_TOKEN` · optional, secret · Only if your host needs one to upload. Unset means you publish by hand. ## 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 · Cargo product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Next.js with TypeScript, one local user, no accounts, SQLite plus a JSON export written on every save, A deterministic static build into a release folder, Derivatives generated at publish time from untouched originals, Upload a whole release then switch, with the previous release kept. - 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 - A freeform canvas that stores each element at an absolute pixel position looks like the product people want and produces a site that is unreadable on a phone, impossible to restyle, and stuck with whichever font metrics the editor had that day. Store typed sections with layout options and let the renderer decide where things go. - Shipping the original from a camera and constraining it with a hundred percent width rule sends a twelve megabyte file to a phone to be displayed at four hundred pixels, and without declared dimensions the page jumps as each one lands. Generate derivatives at publish, emit srcset, and always write width and height. - Rendering pages from the database on request means the site is only alive while your builder is, so the moment the process dies or the disk fills the whole site is gone rather than merely un-editable. Build to files, and treat the database as a working copy of something that already exists on disk. - Syncing the build over the live site file by file leaves visitors during the upload with new HTML asking for stylesheets that are not there yet, and a failure halfway leaves the site permanently in that state with no way back. Upload a whole release, verify it, switch, and keep the previous one so rollback is one command. - Converting a font bought for desktop use into a web format and serving it is a licensing decision most people make by accident, and it is the kind of mistake that arrives as a letter rather than a bug report. Record a licence note per font, check that web embedding is permitted before you ship it, and let the build fail when the note is missing.
# Delivery milestones · Cargo Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A content model, not a canvas Pages are typed sections, so a redesign changes the renderer and not the content. ### Steps 1. Define section kinds with explicit fields Five kinds that fit is better than one kind that holds anything. Files: `src/schema/sections.ts` 2. Keep layout as options, not coordinates Options survive a redesign; coordinates are the redesign. Files: `src/render/section.tsx` 3. Write a JSON export on every save So the content is readable without the editor from day one. Files: `src/content-export.ts` ### Done when - [ ] No pixel coordinates exist in content rows - [ ] A new section kind needs only renderer plus migration - [ ] Layout options change rendering, not content - [ ] Pages render at 360 pixels without horizontal scroll ### Watch out - A freeform canvas that stores each element at an absolute pixel position looks like the product people want and produces a site that is unreadable on a phone, impossible to restyle, and stuck with whichever font metrics the editor had that day. Store typed sections with layout options and let the renderer decide where things go. ## M2 · Images that do not cost a visitor their data allowance Every image ships at a sensible size with dimensions declared. ### Steps 1. Generate derivatives at publish time Not at request time, because there is no server to request from. Files: `src/build/images.ts` 2. Always write intrinsic dimensions The cheapest fix for a page that jumps while it loads. Files: `src/render/image.tsx` 3. Fail the build on missing alt text A warning nobody reads is how a site ends up with none of it. Files: `src/build/checks.ts`, `tests/alt-text.test.ts` ### Done when - [ ] The largest released file is under the byte budget - [ ] Every image has width, height, srcset and sizes - [ ] Missing alt text fails the build with a location - [ ] No originals appear in the release ### Watch out - Shipping the original from a camera and constraining it with a hundred percent width rule sends a twelve megabyte file to a phone to be displayed at four hundred pixels, and without declared dimensions the page jumps as each one lands. Generate derivatives at publish, emit srcset, and always write width and height. ## M3 · A build that is just a folder The same content builds to the same bytes and needs nothing running. ### Steps 1. Make the build deterministic Sort everything, and keep timestamps out of the output. Files: `src/build/index.ts`, `tests/deterministic-build.test.ts` 2. Content hash every asset filename So caching is aggressive and still correct. Files: `src/build/assets.ts` 3. Emit a manifest with sizes and hashes It is what publishing, rollback and the size check all read. Files: `src/build/manifest.ts` ### Done when - [ ] Two builds produce identical bytes - [ ] The release serves from a plain static server - [ ] Asset filenames are content hashed - [ ] The manifest reports file count and total bytes ### Watch out - Rendering pages from the database on request means the site is only alive while your builder is, so the moment the process dies or the disk fills the whole site is gone rather than merely un-editable. Build to files, and treat the database as a working copy of something that already exists on disk. ## M4 · Publishing that is all or nothing Visitors see the old site or the new one, never a mixture, and you can go back. ### Steps 1. Upload to a new directory, then switch The switch is the only moment anything changes for a visitor. Files: `src/publish/upload.ts` 2. Verify against the manifest before switching Count and hashes, because a truncated upload looks like a successful one. Files: `src/publish/verify.ts` 3. Make rollback a tested command The night you need it is not the night to find out it was never written. Files: `src/publish/rollback.ts`, `tests/rollback.test.ts` ### Done when - [ ] An interrupted publish leaves the live site untouched - [ ] Rollback is exercised in the suite - [ ] Oversized releases are refused with sizes printed - [ ] Host limits are recorded in the README with a check date ### Watch out - Syncing the build over the live site file by file leaves visitors during the upload with new HTML asking for stylesheets that are not there yet, and a failure halfway leaves the site permanently in that state with no way back. Upload a whole release, verify it, switch, and keep the previous one so rollback is one command. ## M5 · Fonts, domains and the ability to walk away (production only) The site keeps working without the builder, and every asset in it is one you may use. ### Steps 1. Record a licence note per font One sentence per font, checked by the build, saves a legal conversation. Files: `src/build/fonts.ts` 2. Export everything needed to rebuild Content, originals and the release, in one archive. Files: `src/export.ts`, `tests/rebuild-from-export.test.ts` 3. Write redirects on slug changes A renamed page that returns nothing is how a site loses its inbound links. Files: `src/redirects.ts` ### Done when - [ ] A font with no licence note fails the build - [ ] A rebuild from the export alone is byte identical - [ ] The custom domain serves with a valid certificate - [ ] Renamed pages redirect from their old paths ### Watch out - Converting a font bought for desktop use into a web format and serving it is a licensing decision most people make by accident, and it is the kind of mistake that arrives as a letter rather than a bug report. Record a licence note per font, check that web embedding is permitted before you ship it, and let the build fail when the note is missing.
# Operations · Cargo ## Backup The content export directory and the originals; the database and the releases can both be regenerated from them. Keep the last export off the machine that runs the editor. ## Restore Restore content and originals, run a build, and compare the manifest against the last published release. A difference means either content was lost or the build is not deterministic, and both are worth knowing before you publish. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Release size against the host's published limit, the largest file in each release, and whether the live pointer matches the release you think you published. Check the host's limits page on a schedule, because those numbers change. ## Incident checklist The two incidents are a half published site, which the upload then switch order exists to prevent, and a site that is up but no longer editable, which is why the output is files and the export is tested. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Two builds are confirmed byte identical - [ ] An interrupted publish is confirmed not to affect the live site - [ ] Rollback is confirmed in the suite - [ ] Every image is confirmed to have dimensions and srcset - [ ] A rebuild from the export alone is confirmed identical ## Launch constraint Do not market omitted Cargo capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Pages, sections and assets. A working copy of the JSON export. DATABASE_PATH=./data/site.db # Required. JSON written on every save. This is the copy that outlives the editor. CONTENT_EXPORT_DIR=./content # Required. Untouched originals. Never included in a release. ASSETS_DIR=./assets/originals # Required. One directory per release, kept for rollback. BUILD_DIR=./releases # Required. Derivative widths emitted into srcset. IMAGE_WIDTHS=400,800,1200,2000 # Required. The build fails if any released file exceeds this. MAX_FILE_BYTES=1500000 # Required. Previous releases retained. This is what rollback uses. RELEASES_KEEP=5 # Optional · secret. Only if your host needs one to upload. Unset means you publish by hand. DEPLOY_TOKEN=sk-...redacted
$ choose a build depth, inspect the files, then open the complete pack in your agent
Cargo: 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
Don't feel like building it? These folks already made it free.
all 3 free alternatives to Cargo →· no votes, no pay-to-list · just what's real
Cargo pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| site builder | $19 | $14 | One account can design and publish one or many public sites; real-time multi-user editing; 1 custom-domain connection |
free tierno general free published-site tier; sites are free to build privately for any duration; verified students/teachers can receive free use
billingmonthly or annual ($19/month monthly; $14/month billed yearly)
hidden costsOnly 1 custom-domain connection is included; each additional domain costs $3/month on monthly billing or $2/month on yearly billing. Commerce payment processing still incurs the connected processor's fees.
verified 2026-08-14 · source ↗
Vibecode Cargo
Kinda. The core of Cargo 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 Cargo cost?
Cargo'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 Cargo?
Honestly: general-purpose visual editing; large template and component libraries; CMS collaboration and client handoff; managed hosting, forms, and analytics. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Cargo?
Yes: Webstudio (A visual canvas with honest CSS and CMS connections; the free custom-domain route means self-hosting.) Plasmic (Freeform hosted design, responsive layouts and custom domains free; Cargo's typography cult is not included.) Silex (Freeform visual publishing with static export; less art-school mystique, considerably more ownership.) All 3 curated free alternatives are at vibecodeit.com/cargo/alternatives. The prompt is for when you want it exactly your way.