Mixo's solo core is compact: build a focused AI website builder generator that edits structured page sections, previews responsive layouts, and publishes static files. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on editor, CMS, hosting, polish.
You are building a lean indie version of Mixo. 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 ===== # Mixo · indie build A landing page builder whose hard problems are keeping a page as data so regeneration cannot eat hand edits, publishing static files within a host's real limits with a rollback, and collecting email addresses from a static site without pretending to be a mailing list provider. 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 | | --- | --- | --- | | Runtime | Next.js 15 and TypeScript for the editor, run locally | the editor is a tool you use, not a service you operate | | Storage | SQLite holding pages as structured sections | a page is data, and HTML is one rendering of it | | Generation | One model behind a single interface, filling named fields only | a model that can emit markup can emit anything | | Output | Static HTML, CSS and images written to a folder, no client framework | a landing page that needs a runtime to show a headline has failed | | Publishing | Push the folder to a static host with a version tag and a rollback | the first bad deploy arrives sooner than you expect | ## 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 API key for one text model** · pennies per page, and it is worth setting a hard cap - Why: Generation fills section fields. One provider behind one interface is enough, and the app must stay usable with the key absent. - Get it: Create a key with your chosen provider and set a spending limit on it. - Verify: A section generates and validates against its schema, and removing the key leaves the editor working - [ ] **A static host and its published limits** · free tiers are common for a personal site - Why: Publishing is the phase where guessed limits cause silent failures, so the numbers come from the host's own documentation. - Get it: Choose a static host, read its current limits page, and record the figures with the date. - Verify: The README carries the limits and the date, and the publish step checks against them - [ ] **A domain you control** (optional) · roughly ten to fifteen pounds a year - Why: A landing page on a host's subdomain cannot be moved without losing every link to it, and moving is the point of owning the output. - Get it: Register a domain and point it at the static host per its documentation. - Verify: The published site answers on your domain over HTTPS - [ ] **The claims you are willing to publish** · free - Why: The generator checks numbers against this list, so writing it first is what makes the check meaningful rather than decorative. - Get it: Write down every figure and proof point that is true about the thing you are launching, with where each came from. - Verify: Each appears in the facts table with its evidence ## 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: - Sending email to collected addresses. - A free-form visual editor. - A template marketplace and asset library. - Hosted analytics and visitor tracking. - sender reputation, deliverability, and suppression handling - 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 Mixo, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Mixo 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 landing page generator that stores pages as structured sections and publishes static files to replace Mixo. 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) - Runtime: Next.js 15 and TypeScript for the editor, run locally · the editor is a tool you use, not a service you operate - Storage: SQLite holding pages as structured sections · a page is data, and HTML is one rendering of it - Generation: One model behind a single interface, filling named fields only · a model that can emit markup can emit anything - Output: Static HTML, CSS and images written to a folder, no client framework · a landing page that needs a runtime to show a headline has failed - Publishing: Push the folder to a static host with a version tag and a rollback · the first bad deploy arrives sooner than you expect ### Data model (create this before Phase 1) - sites(id, name, domain, created_at, published_version) - pages(id, site_id, path, title, meta_description, created_at, updated_at) - sections(id, page_id, ordinal, kind, fields_json, locked, source) · source is generated, edited or imported - facts(id, site_id, key, value, evidence) · every number that may appear on the page, with where it came from - versions(id, site_id, built_at, manifest_json, bytes_total, file_count, published_at, rolled_back_at) - signups(id, site_id, email, created_at, consent_text, source_page) · plain storage, with a documented export - A section is typed fields, never a blob of markup, which is what makes regenerating one section safe for the others. ### Phase 1 · A page that is data before it is HTML Build: Define a small set of section kinds with named typed fields: hero, feature list, quote, pricing, call to action, footer. Write one renderer from sections to HTML, and no other path to output. Render twice from the same data and assert byte identical results. Trap: Storing each section as a blob of HTML because the model produced HTML means every later edit is a string operation, two sections can define the same class name with different meanings, and nothing can be restyled without a find and replace across every page you ever generated. Type the fields, keep markup out of the database, and let the renderer own every tag. Done when: No row in the sections table contains markup, asserted by a test that scans for angle brackets in field values. Rendering the same page twice produces byte identical output. Changing the site stylesheet restyles every existing page with no edit to stored data. An unknown section kind fails the build with the page and ordinal named rather than rendering as empty. Do not build yet: No generation, publishing, forms or domains. ### Phase 2 · Generation that fills fields and invents no facts Build: Prompt the model to return values for a section's fields as structured data, validated against the section schema before anything is stored. Keep a facts table of the numbers and claims you are willing to publish, and reject generated text containing a number that is not in it. Mark hand edited sections locked so regeneration skips them. Trap: Asking a model for a landing page and storing whatever comes back gets you 'trusted by over 2,000 teams' on the site of a product with four customers, and because it reads exactly like the rest of the copy nobody deletes it before the page goes live. Gate every numeric claim on a recorded fact, and refuse the section rather than publishing an invented one. Done when: A generated section containing a number absent from the facts table is rejected with the offending phrase shown, tested with a crafted response. Generated output that does not validate against the section schema is discarded and retried a bounded number of times, never stored partially. A locked section is unchanged by a regeneration of its page, tested. Every section records whether it was generated, edited or imported. Do not build yet: No building, publishing or email capture yet. ### Phase 3 · A build that a browser can load on a bad connection Build: Build to a folder: HTML, one stylesheet, images resized to the widths the layout actually uses with a srcset, and no client JavaScript required for anything the visitor must read. Write a manifest of every file with its size, and fail the build on a broken internal link or a missing image. Trap: Shipping the original photographs straight from the upload puts four megabytes of hero image on a page whose entire text is two hundred words, and the person on a train sees nothing for eight seconds. Resize at build time to the widths the layout uses, record the total in the manifest, and fail the build over a stated page weight budget. Done when: The built folder contains no JavaScript that the visible content depends on, checked by rendering with scripting disabled. Every image is emitted at the widths used by the layout and referenced through a srcset. A broken internal link or missing image fails the build naming the file. The manifest records file count and total bytes, and exceeding the stated page weight budget fails the build. Do not build yet: No publishing, custom domain or email capture yet. ### Phase 4 · Publishing inside the host's real limits, with a way back Build: Publish the built folder to a static host. Read that host's published limits and check the manifest against them before pushing: GitHub Pages, for example, documents that a published site may be no larger than one gigabyte, with soft limits of one hundred gigabytes of bandwidth a month and ten builds an hour, the build limit not applying to a custom Actions workflow. Keep the previous version so a rollback is one command. Trap: Treating publish as a file copy means the first bad deploy is live until you rebuild, and a host's soft build limit turns an afternoon of small fixes into a site that silently stops updating while you keep pressing publish. Tag every version, check the manifest against the host's stated limits first, and make rollback a single command that does not rebuild anything. Done when: Publishing records a version with its manifest and the time, and rollback republishes a previous version without rebuilding, tested. A build exceeding the host's documented size limit is refused before any upload, with the limit and the measured size shown. The number of publishes in the last hour is displayed beside the publish button. The README names the host, its current limits and the date they were checked. Do not build yet: No email capture or analytics yet. ### Phase 5 · Email capture that is honest about where the address goes Build: Add a form that posts to one small endpoint you run, storing address, time, the exact consent text shown and the page it came from. Rate limit it and reject obvious automated submissions without fingerprinting the visitor. Export to CSV. State plainly that this is a list, not a sending service, and that sending to it is a separate decision with its own obligations. Trap: Collecting addresses with no record of what the person agreed to leaves you, a year later, with a list you cannot lawfully or honestly send to, because nobody can say what was on the page when each address was typed. Store the consent text verbatim with every signup, and version it when the wording changes. Done when: Every signup row stores the consent wording shown on that page at that time, verified by changing the wording and checking old rows are unchanged. The endpoint rate limits by address and by origin without storing a visitor fingerprint. CSV export includes the consent text and source page. The README states that no email is sent by this app and that bulk sending has requirements of its own to check before the first send. Do not build yet: Nothing after this. ### Out of scope (and why) - Sending email to the addresses you collect, which is a deliverability and reputation problem large enough to be its own product. - A free-form visual editor, because dragging arbitrary boxes is what turns structured sections back into blobs of markup. - A template marketplace and stock asset library, which is inventory work rather than software work. - Hosted analytics, which would put a tracker on every page you publish for numbers a static host's logs already approximate. ### README must contain - That a page is stored as typed sections and HTML exists only as build output - That generated copy cannot contain a number absent from the facts table, and what happens when it tries - That the built site needs no client JavaScript to display its content - The static host in use, its documented limits, the date you checked them, and how to roll back - That signups are stored with the exact consent wording and that this app sends no email ===== AGENTS.md ===== # Agent instructions · Mixo indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js 15 and TypeScript for the editor, run locally, SQLite holding pages as structured sections, One model behind a single interface, filling named fields only, Static HTML, CSS and images written to a folder, no client framework, Push the folder to a static host with a version tag and a rollback. 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 - Storing each section as a blob of HTML because the model produced HTML means every later edit is a string operation, two sections can define the same class name with different meanings, and nothing can be restyled without a find and replace across every page you ever generated. Type the fields, keep markup out of the database, and let the renderer own every tag. - Asking a model for a landing page and storing whatever comes back gets you 'trusted by over 2,000 teams' on the site of a product with four customers, and because it reads exactly like the rest of the copy nobody deletes it before the page goes live. Gate every numeric claim on a recorded fact, and refuse the section rather than publishing an invented one. - Shipping the original photographs straight from the upload puts four megabytes of hero image on a page whose entire text is two hundred words, and the person on a train sees nothing for eight seconds. Resize at build time to the widths the layout uses, record the total in the manifest, and fail the build over a stated page weight budget. - Treating publish as a file copy means the first bad deploy is live until you rebuild, and a host's soft build limit turns an afternoon of small fixes into a site that silently stops updating while you keep pressing publish. Tag every version, check the manifest against the host's stated limits first, and make rollback a single command that does not rebuild anything. ===== BUILD_PLAN.md ===== # Build plan · Mixo A landing page builder whose hard problems are keeping a page as data so regeneration cannot eat hand edits, publishing static files within a host's real limits with a rollback, and collecting email addresses from a static site without pretending to be a mailing list provider. 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 page that is data before it is HTML Sections are typed fields and one deterministic renderer turns them into a page. ### Steps 1. Define section kinds with typed fields Six kinds that cover a landing page beats forty that cover everything. Files: `src/lib/sections.ts`, `src/lib/sections.test.ts` 2. One renderer, one output path Two ways to produce HTML means two ways to produce different HTML. Files: `src/render/page.ts` 3. Assert determinism in the suite A build you cannot reproduce is a build you cannot roll back to. Files: `tests/deterministic-render.test.ts` ### Done when - [ ] No stored field contains markup - [ ] Rendering twice is byte identical - [ ] A stylesheet change restyles every page - [ ] Unknown section kinds fail the build loudly ### Watch out - Storing each section as a blob of HTML because the model produced HTML means every later edit is a string operation, two sections can define the same class name with different meanings, and nothing can be restyled without a find and replace across every page you ever generated. Type the fields, keep markup out of the database, and let the renderer own every tag. ## Phase 2 · Generation that fills fields and invents no facts The model writes into named fields, and any number it produces must match a recorded fact. ### Steps 1. Validate generated output against the schema Structured fields in, structured fields out, or nothing. Files: `src/lib/generate.ts` 2. Gate numeric claims on the facts table The one check that keeps a demo site from lying about a real business. Files: `src/lib/claims.ts`, `tests/claims.test.ts` 3. Lock what a person edited Nobody should lose a rewritten headline to a regeneration. Files: `src/lib/regenerate.ts` ### Done when - [ ] Unsupported numbers are rejected with the phrase shown - [ ] Invalid generations are discarded, never partially stored - [ ] Locked sections survive regeneration - [ ] Every section records its source ### Watch out - Asking a model for a landing page and storing whatever comes back gets you 'trusted by over 2,000 teams' on the site of a product with four customers, and because it reads exactly like the rest of the copy nobody deletes it before the page goes live. Gate every numeric claim on a recorded fact, and refuse the section rather than publishing an invented one. ## Phase 3 · A build that a browser can load on a bad connection The output is static files, sized and linked correctly, with no runtime requirement. ### Steps 1. Emit images at the widths the layout uses A hero image is not a photograph archive. Files: `src/build/images.ts` 2. Fail the build on a broken link Publishing is the wrong time to find out. Files: `src/build/links.ts` 3. Write a manifest with sizes It is what makes the next phase's limits checkable. Files: `src/build/manifest.ts` ### Done when - [ ] Content renders with scripting disabled - [ ] Images are emitted per layout width with a srcset - [ ] Broken links and missing images fail the build - [ ] The manifest records count and total bytes ### Watch out - Shipping the original photographs straight from the upload puts four megabytes of hero image on a page whose entire text is two hundred words, and the person on a train sees nothing for eight seconds. Resize at build time to the widths the layout uses, record the total in the manifest, and fail the build over a stated page weight budget. ## Phase 4 · Publishing inside the host's real limits, with a way back A deploy is a tagged version that can be rolled back, and the limits are checked before the push. ### Steps 1. Check the manifest against published limits Read them from the host's own documentation, and record when you read them. Files: `src/publish/limits.ts` 2. Tag versions and keep the previous one Rollback should not depend on your build still being reproducible today. Files: `src/publish/versions.ts` 3. Show the recent publish count Soft limits fail quietly, which is the worst way to fail. Files: `src/app/publish/page.tsx` ### Done when - [ ] Rollback republishes without rebuilding - [ ] An oversized build is refused before upload - [ ] Recent publish count is visible - [ ] The README records the host's limits and the date checked ### Watch out - Treating publish as a file copy means the first bad deploy is live until you rebuild, and a host's soft build limit turns an afternoon of small fixes into a site that silently stops updating while you keep pressing publish. Tag every version, check the manifest against the host's stated limits first, and make rollback a single command that does not rebuild anything. ## Not in this build - Sending email to collected addresses. - A free-form visual editor. - A template marketplace and asset library. - Hosted analytics and visitor tracking. ## After v1, if you want it - A second page type such as a changelog, which is the same sections against a different layout - Multilingual pages, easy while sections are data and painful once they are markup - A scheduled rebuild that refreshes only pages whose facts have changed ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Sites, pages, sections, facts and versions. DATABASE_PATH=./data/sites.db # Optional · secret. Generation only. Without it the editor still builds and publishes. MODEL_API_KEY=sk-...redacted # Optional. Recorded on every generated section. MODEL_NAME=a current small text model # Required. Static output. What gets published, and nothing else. BUILD_DIR=./out # Required. A page over this fails the build. PAGE_WEIGHT_BUDGET_KB=500 # Optional. Where the published form posts. Empty means the form is not rendered. SIGNUP_ENDPOINT_URL=https://forms.example.com/signup
You are building a lean indie version of Mixo. 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 ===== # Mixo · indie build A landing page builder whose hard problems are keeping a page as data so regeneration cannot eat hand edits, publishing static files within a host's real limits with a rollback, and collecting email addresses from a static site without pretending to be a mailing list provider. 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 | | --- | --- | --- | | Runtime | Next.js 15 and TypeScript for the editor, run locally | the editor is a tool you use, not a service you operate | | Storage | SQLite holding pages as structured sections | a page is data, and HTML is one rendering of it | | Generation | One model behind a single interface, filling named fields only | a model that can emit markup can emit anything | | Output | Static HTML, CSS and images written to a folder, no client framework | a landing page that needs a runtime to show a headline has failed | | Publishing | Push the folder to a static host with a version tag and a rollback | the first bad deploy arrives sooner than you expect | ## 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 API key for one text model** · pennies per page, and it is worth setting a hard cap - Why: Generation fills section fields. One provider behind one interface is enough, and the app must stay usable with the key absent. - Get it: Create a key with your chosen provider and set a spending limit on it. - Verify: A section generates and validates against its schema, and removing the key leaves the editor working - [ ] **A static host and its published limits** · free tiers are common for a personal site - Why: Publishing is the phase where guessed limits cause silent failures, so the numbers come from the host's own documentation. - Get it: Choose a static host, read its current limits page, and record the figures with the date. - Verify: The README carries the limits and the date, and the publish step checks against them - [ ] **A domain you control** (optional) · roughly ten to fifteen pounds a year - Why: A landing page on a host's subdomain cannot be moved without losing every link to it, and moving is the point of owning the output. - Get it: Register a domain and point it at the static host per its documentation. - Verify: The published site answers on your domain over HTTPS - [ ] **The claims you are willing to publish** · free - Why: The generator checks numbers against this list, so writing it first is what makes the check meaningful rather than decorative. - Get it: Write down every figure and proof point that is true about the thing you are launching, with where each came from. - Verify: Each appears in the facts table with its evidence ## 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: - Sending email to collected addresses. - A free-form visual editor. - A template marketplace and asset library. - Hosted analytics and visitor tracking. - sender reputation, deliverability, and suppression handling - 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 Mixo, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Mixo 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 landing page generator that stores pages as structured sections and publishes static files to replace Mixo. 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) - Runtime: Next.js 15 and TypeScript for the editor, run locally · the editor is a tool you use, not a service you operate - Storage: SQLite holding pages as structured sections · a page is data, and HTML is one rendering of it - Generation: One model behind a single interface, filling named fields only · a model that can emit markup can emit anything - Output: Static HTML, CSS and images written to a folder, no client framework · a landing page that needs a runtime to show a headline has failed - Publishing: Push the folder to a static host with a version tag and a rollback · the first bad deploy arrives sooner than you expect ### Data model (create this before Phase 1) - sites(id, name, domain, created_at, published_version) - pages(id, site_id, path, title, meta_description, created_at, updated_at) - sections(id, page_id, ordinal, kind, fields_json, locked, source) · source is generated, edited or imported - facts(id, site_id, key, value, evidence) · every number that may appear on the page, with where it came from - versions(id, site_id, built_at, manifest_json, bytes_total, file_count, published_at, rolled_back_at) - signups(id, site_id, email, created_at, consent_text, source_page) · plain storage, with a documented export - A section is typed fields, never a blob of markup, which is what makes regenerating one section safe for the others. ### Phase 1 · A page that is data before it is HTML Build: Define a small set of section kinds with named typed fields: hero, feature list, quote, pricing, call to action, footer. Write one renderer from sections to HTML, and no other path to output. Render twice from the same data and assert byte identical results. Trap: Storing each section as a blob of HTML because the model produced HTML means every later edit is a string operation, two sections can define the same class name with different meanings, and nothing can be restyled without a find and replace across every page you ever generated. Type the fields, keep markup out of the database, and let the renderer own every tag. Done when: No row in the sections table contains markup, asserted by a test that scans for angle brackets in field values. Rendering the same page twice produces byte identical output. Changing the site stylesheet restyles every existing page with no edit to stored data. An unknown section kind fails the build with the page and ordinal named rather than rendering as empty. Do not build yet: No generation, publishing, forms or domains. ### Phase 2 · Generation that fills fields and invents no facts Build: Prompt the model to return values for a section's fields as structured data, validated against the section schema before anything is stored. Keep a facts table of the numbers and claims you are willing to publish, and reject generated text containing a number that is not in it. Mark hand edited sections locked so regeneration skips them. Trap: Asking a model for a landing page and storing whatever comes back gets you 'trusted by over 2,000 teams' on the site of a product with four customers, and because it reads exactly like the rest of the copy nobody deletes it before the page goes live. Gate every numeric claim on a recorded fact, and refuse the section rather than publishing an invented one. Done when: A generated section containing a number absent from the facts table is rejected with the offending phrase shown, tested with a crafted response. Generated output that does not validate against the section schema is discarded and retried a bounded number of times, never stored partially. A locked section is unchanged by a regeneration of its page, tested. Every section records whether it was generated, edited or imported. Do not build yet: No building, publishing or email capture yet. ### Phase 3 · A build that a browser can load on a bad connection Build: Build to a folder: HTML, one stylesheet, images resized to the widths the layout actually uses with a srcset, and no client JavaScript required for anything the visitor must read. Write a manifest of every file with its size, and fail the build on a broken internal link or a missing image. Trap: Shipping the original photographs straight from the upload puts four megabytes of hero image on a page whose entire text is two hundred words, and the person on a train sees nothing for eight seconds. Resize at build time to the widths the layout uses, record the total in the manifest, and fail the build over a stated page weight budget. Done when: The built folder contains no JavaScript that the visible content depends on, checked by rendering with scripting disabled. Every image is emitted at the widths used by the layout and referenced through a srcset. A broken internal link or missing image fails the build naming the file. The manifest records file count and total bytes, and exceeding the stated page weight budget fails the build. Do not build yet: No publishing, custom domain or email capture yet. ### Phase 4 · Publishing inside the host's real limits, with a way back Build: Publish the built folder to a static host. Read that host's published limits and check the manifest against them before pushing: GitHub Pages, for example, documents that a published site may be no larger than one gigabyte, with soft limits of one hundred gigabytes of bandwidth a month and ten builds an hour, the build limit not applying to a custom Actions workflow. Keep the previous version so a rollback is one command. Trap: Treating publish as a file copy means the first bad deploy is live until you rebuild, and a host's soft build limit turns an afternoon of small fixes into a site that silently stops updating while you keep pressing publish. Tag every version, check the manifest against the host's stated limits first, and make rollback a single command that does not rebuild anything. Done when: Publishing records a version with its manifest and the time, and rollback republishes a previous version without rebuilding, tested. A build exceeding the host's documented size limit is refused before any upload, with the limit and the measured size shown. The number of publishes in the last hour is displayed beside the publish button. The README names the host, its current limits and the date they were checked. Do not build yet: No email capture or analytics yet. ### Phase 5 · Email capture that is honest about where the address goes Build: Add a form that posts to one small endpoint you run, storing address, time, the exact consent text shown and the page it came from. Rate limit it and reject obvious automated submissions without fingerprinting the visitor. Export to CSV. State plainly that this is a list, not a sending service, and that sending to it is a separate decision with its own obligations. Trap: Collecting addresses with no record of what the person agreed to leaves you, a year later, with a list you cannot lawfully or honestly send to, because nobody can say what was on the page when each address was typed. Store the consent text verbatim with every signup, and version it when the wording changes. Done when: Every signup row stores the consent wording shown on that page at that time, verified by changing the wording and checking old rows are unchanged. The endpoint rate limits by address and by origin without storing a visitor fingerprint. CSV export includes the consent text and source page. The README states that no email is sent by this app and that bulk sending has requirements of its own to check before the first send. Do not build yet: Nothing after this. ### Out of scope (and why) - Sending email to the addresses you collect, which is a deliverability and reputation problem large enough to be its own product. - A free-form visual editor, because dragging arbitrary boxes is what turns structured sections back into blobs of markup. - A template marketplace and stock asset library, which is inventory work rather than software work. - Hosted analytics, which would put a tracker on every page you publish for numbers a static host's logs already approximate. ### README must contain - That a page is stored as typed sections and HTML exists only as build output - That generated copy cannot contain a number absent from the facts table, and what happens when it tries - That the built site needs no client JavaScript to display its content - The static host in use, its documented limits, the date you checked them, and how to roll back - That signups are stored with the exact consent wording and that this app sends no email ===== AGENTS.md ===== # Agent instructions · Mixo indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js 15 and TypeScript for the editor, run locally, SQLite holding pages as structured sections, One model behind a single interface, filling named fields only, Static HTML, CSS and images written to a folder, no client framework, Push the folder to a static host with a version tag and a rollback. 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 - Storing each section as a blob of HTML because the model produced HTML means every later edit is a string operation, two sections can define the same class name with different meanings, and nothing can be restyled without a find and replace across every page you ever generated. Type the fields, keep markup out of the database, and let the renderer own every tag. - Asking a model for a landing page and storing whatever comes back gets you 'trusted by over 2,000 teams' on the site of a product with four customers, and because it reads exactly like the rest of the copy nobody deletes it before the page goes live. Gate every numeric claim on a recorded fact, and refuse the section rather than publishing an invented one. - Shipping the original photographs straight from the upload puts four megabytes of hero image on a page whose entire text is two hundred words, and the person on a train sees nothing for eight seconds. Resize at build time to the widths the layout uses, record the total in the manifest, and fail the build over a stated page weight budget. - Treating publish as a file copy means the first bad deploy is live until you rebuild, and a host's soft build limit turns an afternoon of small fixes into a site that silently stops updating while you keep pressing publish. Tag every version, check the manifest against the host's stated limits first, and make rollback a single command that does not rebuild anything. ===== BUILD_PLAN.md ===== # Build plan · Mixo A landing page builder whose hard problems are keeping a page as data so regeneration cannot eat hand edits, publishing static files within a host's real limits with a rollback, and collecting email addresses from a static site without pretending to be a mailing list provider. 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 page that is data before it is HTML Sections are typed fields and one deterministic renderer turns them into a page. ### Steps 1. Define section kinds with typed fields Six kinds that cover a landing page beats forty that cover everything. Files: `src/lib/sections.ts`, `src/lib/sections.test.ts` 2. One renderer, one output path Two ways to produce HTML means two ways to produce different HTML. Files: `src/render/page.ts` 3. Assert determinism in the suite A build you cannot reproduce is a build you cannot roll back to. Files: `tests/deterministic-render.test.ts` ### Done when - [ ] No stored field contains markup - [ ] Rendering twice is byte identical - [ ] A stylesheet change restyles every page - [ ] Unknown section kinds fail the build loudly ### Watch out - Storing each section as a blob of HTML because the model produced HTML means every later edit is a string operation, two sections can define the same class name with different meanings, and nothing can be restyled without a find and replace across every page you ever generated. Type the fields, keep markup out of the database, and let the renderer own every tag. ## Phase 2 · Generation that fills fields and invents no facts The model writes into named fields, and any number it produces must match a recorded fact. ### Steps 1. Validate generated output against the schema Structured fields in, structured fields out, or nothing. Files: `src/lib/generate.ts` 2. Gate numeric claims on the facts table The one check that keeps a demo site from lying about a real business. Files: `src/lib/claims.ts`, `tests/claims.test.ts` 3. Lock what a person edited Nobody should lose a rewritten headline to a regeneration. Files: `src/lib/regenerate.ts` ### Done when - [ ] Unsupported numbers are rejected with the phrase shown - [ ] Invalid generations are discarded, never partially stored - [ ] Locked sections survive regeneration - [ ] Every section records its source ### Watch out - Asking a model for a landing page and storing whatever comes back gets you 'trusted by over 2,000 teams' on the site of a product with four customers, and because it reads exactly like the rest of the copy nobody deletes it before the page goes live. Gate every numeric claim on a recorded fact, and refuse the section rather than publishing an invented one. ## Phase 3 · A build that a browser can load on a bad connection The output is static files, sized and linked correctly, with no runtime requirement. ### Steps 1. Emit images at the widths the layout uses A hero image is not a photograph archive. Files: `src/build/images.ts` 2. Fail the build on a broken link Publishing is the wrong time to find out. Files: `src/build/links.ts` 3. Write a manifest with sizes It is what makes the next phase's limits checkable. Files: `src/build/manifest.ts` ### Done when - [ ] Content renders with scripting disabled - [ ] Images are emitted per layout width with a srcset - [ ] Broken links and missing images fail the build - [ ] The manifest records count and total bytes ### Watch out - Shipping the original photographs straight from the upload puts four megabytes of hero image on a page whose entire text is two hundred words, and the person on a train sees nothing for eight seconds. Resize at build time to the widths the layout uses, record the total in the manifest, and fail the build over a stated page weight budget. ## Phase 4 · Publishing inside the host's real limits, with a way back A deploy is a tagged version that can be rolled back, and the limits are checked before the push. ### Steps 1. Check the manifest against published limits Read them from the host's own documentation, and record when you read them. Files: `src/publish/limits.ts` 2. Tag versions and keep the previous one Rollback should not depend on your build still being reproducible today. Files: `src/publish/versions.ts` 3. Show the recent publish count Soft limits fail quietly, which is the worst way to fail. Files: `src/app/publish/page.tsx` ### Done when - [ ] Rollback republishes without rebuilding - [ ] An oversized build is refused before upload - [ ] Recent publish count is visible - [ ] The README records the host's limits and the date checked ### Watch out - Treating publish as a file copy means the first bad deploy is live until you rebuild, and a host's soft build limit turns an afternoon of small fixes into a site that silently stops updating while you keep pressing publish. Tag every version, check the manifest against the host's stated limits first, and make rollback a single command that does not rebuild anything. ## Not in this build - Sending email to collected addresses. - A free-form visual editor. - A template marketplace and asset library. - Hosted analytics and visitor tracking. ## After v1, if you want it - A second page type such as a changelog, which is the same sections against a different layout - Multilingual pages, easy while sections are data and painful once they are markup - A scheduled rebuild that refreshes only pages whose facts have changed ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Sites, pages, sections, facts and versions. DATABASE_PATH=./data/sites.db # Optional · secret. Generation only. Without it the editor still builds and publishes. MODEL_API_KEY=sk-...redacted # Optional. Recorded on every generated section. MODEL_NAME=a current small text model # Required. Static output. What gets published, and nothing else. BUILD_DIR=./out # Required. A page over this fails the build. PAGE_WEIGHT_BUDGET_KB=500 # Optional. Where the published form posts. Empty means the form is not rendered. SIGNUP_ENDPOINT_URL=https://forms.example.com/signup
You are building a production product version of Mixo. 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 ===== # Mixo · product brief ## Problem Mixo's solo core is compact: build a focused AI website builder generator that edits structured page sections, previews responsive layouts, and publishes static files. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on editor, CMS, hosting, polish. ## Product outcome A builder that keeps pages as structured data, refuses to publish a number nobody can back up, produces static files light enough to load on a train, and lets you roll back a bad deploy in one command. ## 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 - Sending email to collected addresses. - A free-form visual editor. - A template marketplace and asset library. - Hosted analytics and visitor tracking. - sender reputation, deliverability, and suppression handling - general-purpose visual editing - large template and component libraries - CMS collaboration and client handoff - managed hosting, forms, and analytics ## Success criteria - No stored section field is confirmed to contain markup - An unsupported numeric claim is confirmed rejected - Content is confirmed to render with scripting disabled - A rollback is confirmed to work without rebuilding - Consent text is confirmed stored verbatim with each signup ===== BRIEF.md ===== # Build brief · Mixo 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 landing page generator that stores pages as structured sections and publishes static files to replace Mixo. 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) - Runtime: Next.js 15 and TypeScript for the editor, run locally · the editor is a tool you use, not a service you operate - Storage: SQLite holding pages as structured sections · a page is data, and HTML is one rendering of it - Generation: One model behind a single interface, filling named fields only · a model that can emit markup can emit anything - Output: Static HTML, CSS and images written to a folder, no client framework · a landing page that needs a runtime to show a headline has failed - Publishing: Push the folder to a static host with a version tag and a rollback · the first bad deploy arrives sooner than you expect ### Data model (create this before Phase 1) - sites(id, name, domain, created_at, published_version) - pages(id, site_id, path, title, meta_description, created_at, updated_at) - sections(id, page_id, ordinal, kind, fields_json, locked, source) · source is generated, edited or imported - facts(id, site_id, key, value, evidence) · every number that may appear on the page, with where it came from - versions(id, site_id, built_at, manifest_json, bytes_total, file_count, published_at, rolled_back_at) - signups(id, site_id, email, created_at, consent_text, source_page) · plain storage, with a documented export - A section is typed fields, never a blob of markup, which is what makes regenerating one section safe for the others. ### Phase 1 · A page that is data before it is HTML Build: Define a small set of section kinds with named typed fields: hero, feature list, quote, pricing, call to action, footer. Write one renderer from sections to HTML, and no other path to output. Render twice from the same data and assert byte identical results. Trap: Storing each section as a blob of HTML because the model produced HTML means every later edit is a string operation, two sections can define the same class name with different meanings, and nothing can be restyled without a find and replace across every page you ever generated. Type the fields, keep markup out of the database, and let the renderer own every tag. Done when: No row in the sections table contains markup, asserted by a test that scans for angle brackets in field values. Rendering the same page twice produces byte identical output. Changing the site stylesheet restyles every existing page with no edit to stored data. An unknown section kind fails the build with the page and ordinal named rather than rendering as empty. Do not build yet: No generation, publishing, forms or domains. ### Phase 2 · Generation that fills fields and invents no facts Build: Prompt the model to return values for a section's fields as structured data, validated against the section schema before anything is stored. Keep a facts table of the numbers and claims you are willing to publish, and reject generated text containing a number that is not in it. Mark hand edited sections locked so regeneration skips them. Trap: Asking a model for a landing page and storing whatever comes back gets you 'trusted by over 2,000 teams' on the site of a product with four customers, and because it reads exactly like the rest of the copy nobody deletes it before the page goes live. Gate every numeric claim on a recorded fact, and refuse the section rather than publishing an invented one. Done when: A generated section containing a number absent from the facts table is rejected with the offending phrase shown, tested with a crafted response. Generated output that does not validate against the section schema is discarded and retried a bounded number of times, never stored partially. A locked section is unchanged by a regeneration of its page, tested. Every section records whether it was generated, edited or imported. Do not build yet: No building, publishing or email capture yet. ### Phase 3 · A build that a browser can load on a bad connection Build: Build to a folder: HTML, one stylesheet, images resized to the widths the layout actually uses with a srcset, and no client JavaScript required for anything the visitor must read. Write a manifest of every file with its size, and fail the build on a broken internal link or a missing image. Trap: Shipping the original photographs straight from the upload puts four megabytes of hero image on a page whose entire text is two hundred words, and the person on a train sees nothing for eight seconds. Resize at build time to the widths the layout uses, record the total in the manifest, and fail the build over a stated page weight budget. Done when: The built folder contains no JavaScript that the visible content depends on, checked by rendering with scripting disabled. Every image is emitted at the widths used by the layout and referenced through a srcset. A broken internal link or missing image fails the build naming the file. The manifest records file count and total bytes, and exceeding the stated page weight budget fails the build. Do not build yet: No publishing, custom domain or email capture yet. ### Phase 4 · Publishing inside the host's real limits, with a way back Build: Publish the built folder to a static host. Read that host's published limits and check the manifest against them before pushing: GitHub Pages, for example, documents that a published site may be no larger than one gigabyte, with soft limits of one hundred gigabytes of bandwidth a month and ten builds an hour, the build limit not applying to a custom Actions workflow. Keep the previous version so a rollback is one command. Trap: Treating publish as a file copy means the first bad deploy is live until you rebuild, and a host's soft build limit turns an afternoon of small fixes into a site that silently stops updating while you keep pressing publish. Tag every version, check the manifest against the host's stated limits first, and make rollback a single command that does not rebuild anything. Done when: Publishing records a version with its manifest and the time, and rollback republishes a previous version without rebuilding, tested. A build exceeding the host's documented size limit is refused before any upload, with the limit and the measured size shown. The number of publishes in the last hour is displayed beside the publish button. The README names the host, its current limits and the date they were checked. Do not build yet: No email capture or analytics yet. ### Phase 5 · Email capture that is honest about where the address goes Build: Add a form that posts to one small endpoint you run, storing address, time, the exact consent text shown and the page it came from. Rate limit it and reject obvious automated submissions without fingerprinting the visitor. Export to CSV. State plainly that this is a list, not a sending service, and that sending to it is a separate decision with its own obligations. Trap: Collecting addresses with no record of what the person agreed to leaves you, a year later, with a list you cannot lawfully or honestly send to, because nobody can say what was on the page when each address was typed. Store the consent text verbatim with every signup, and version it when the wording changes. Done when: Every signup row stores the consent wording shown on that page at that time, verified by changing the wording and checking old rows are unchanged. The endpoint rate limits by address and by origin without storing a visitor fingerprint. CSV export includes the consent text and source page. The README states that no email is sent by this app and that bulk sending has requirements of its own to check before the first send. Do not build yet: Nothing after this. ### Out of scope (and why) - Sending email to the addresses you collect, which is a deliverability and reputation problem large enough to be its own product. - A free-form visual editor, because dragging arbitrary boxes is what turns structured sections back into blobs of markup. - A template marketplace and stock asset library, which is inventory work rather than software work. - Hosted analytics, which would put a tracker on every page you publish for numbers a static host's logs already approximate. ### README must contain - That a page is stored as typed sections and HTML exists only as build output - That generated copy cannot contain a number absent from the facts table, and what happens when it tries - That the built site needs no client JavaScript to display its content - The static host in use, its documented limits, the date you checked them, and how to roll back - That signups are stored with the exact consent wording and that this app sends no email ===== ARCHITECTURE.md ===== # Architecture · Mixo ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js 15 and TypeScript for the editor, run locally | the editor is a tool you use, not a service you operate | | Storage | SQLite holding pages as structured sections | a page is data, and HTML is one rendering of it | | Generation | One model behind a single interface, filling named fields only | a model that can emit markup can emit anything | | Output | Static HTML, CSS and images written to a folder, no client framework | a landing page that needs a runtime to show a headline has failed | | Publishing | Push the folder to a static host with a version tag and a rollback | the first bad deploy arrives sooner than you expect | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Sections | Typed fields and section kinds. | Any schema, provided markup never enters the database. | | Generation | Structured filling and the claims gate. | Any model behind the same interface, with the app usable without one. | | Build | Deterministic rendering, image widths and the manifest. | Any static generator that produces the same manifest. | | Publish | Version tags, limit checks and rollback. | Any static host, with its own limits read from its documentation. | | Signups | Address storage with verbatim consent. | Any store, provided the consent text travels with the row. | ## 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 · Sites, pages, sections, facts and versions. - `MODEL_API_KEY` · optional, secret · Generation only. Without it the editor still builds and publishes. - `MODEL_NAME` · optional · Recorded on every generated section. - `BUILD_DIR` · required · Static output. What gets published, and nothing else. - `PAGE_WEIGHT_BUDGET_KB` · required · A page over this fails the build. - `SIGNUP_ENDPOINT_URL` · optional · Where the published form posts. Empty means the form is not rendered. ## 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 · Mixo product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Next.js 15 and TypeScript for the editor, run locally, SQLite holding pages as structured sections, One model behind a single interface, filling named fields only, Static HTML, CSS and images written to a folder, no client framework, Push the folder to a static host with a version tag and a rollback. - 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 - Storing each section as a blob of HTML because the model produced HTML means every later edit is a string operation, two sections can define the same class name with different meanings, and nothing can be restyled without a find and replace across every page you ever generated. Type the fields, keep markup out of the database, and let the renderer own every tag. - Asking a model for a landing page and storing whatever comes back gets you 'trusted by over 2,000 teams' on the site of a product with four customers, and because it reads exactly like the rest of the copy nobody deletes it before the page goes live. Gate every numeric claim on a recorded fact, and refuse the section rather than publishing an invented one. - Shipping the original photographs straight from the upload puts four megabytes of hero image on a page whose entire text is two hundred words, and the person on a train sees nothing for eight seconds. Resize at build time to the widths the layout uses, record the total in the manifest, and fail the build over a stated page weight budget. - Treating publish as a file copy means the first bad deploy is live until you rebuild, and a host's soft build limit turns an afternoon of small fixes into a site that silently stops updating while you keep pressing publish. Tag every version, check the manifest against the host's stated limits first, and make rollback a single command that does not rebuild anything. - Collecting addresses with no record of what the person agreed to leaves you, a year later, with a list you cannot lawfully or honestly send to, because nobody can say what was on the page when each address was typed. Store the consent text verbatim with every signup, and version it when the wording changes. ===== MILESTONES.md ===== # Delivery milestones · Mixo Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A page that is data before it is HTML Sections are typed fields and one deterministic renderer turns them into a page. ### Steps 1. Define section kinds with typed fields Six kinds that cover a landing page beats forty that cover everything. Files: `src/lib/sections.ts`, `src/lib/sections.test.ts` 2. One renderer, one output path Two ways to produce HTML means two ways to produce different HTML. Files: `src/render/page.ts` 3. Assert determinism in the suite A build you cannot reproduce is a build you cannot roll back to. Files: `tests/deterministic-render.test.ts` ### Done when - [ ] No stored field contains markup - [ ] Rendering twice is byte identical - [ ] A stylesheet change restyles every page - [ ] Unknown section kinds fail the build loudly ### Watch out - Storing each section as a blob of HTML because the model produced HTML means every later edit is a string operation, two sections can define the same class name with different meanings, and nothing can be restyled without a find and replace across every page you ever generated. Type the fields, keep markup out of the database, and let the renderer own every tag. ## M2 · Generation that fills fields and invents no facts The model writes into named fields, and any number it produces must match a recorded fact. ### Steps 1. Validate generated output against the schema Structured fields in, structured fields out, or nothing. Files: `src/lib/generate.ts` 2. Gate numeric claims on the facts table The one check that keeps a demo site from lying about a real business. Files: `src/lib/claims.ts`, `tests/claims.test.ts` 3. Lock what a person edited Nobody should lose a rewritten headline to a regeneration. Files: `src/lib/regenerate.ts` ### Done when - [ ] Unsupported numbers are rejected with the phrase shown - [ ] Invalid generations are discarded, never partially stored - [ ] Locked sections survive regeneration - [ ] Every section records its source ### Watch out - Asking a model for a landing page and storing whatever comes back gets you 'trusted by over 2,000 teams' on the site of a product with four customers, and because it reads exactly like the rest of the copy nobody deletes it before the page goes live. Gate every numeric claim on a recorded fact, and refuse the section rather than publishing an invented one. ## M3 · A build that a browser can load on a bad connection The output is static files, sized and linked correctly, with no runtime requirement. ### Steps 1. Emit images at the widths the layout uses A hero image is not a photograph archive. Files: `src/build/images.ts` 2. Fail the build on a broken link Publishing is the wrong time to find out. Files: `src/build/links.ts` 3. Write a manifest with sizes It is what makes the next phase's limits checkable. Files: `src/build/manifest.ts` ### Done when - [ ] Content renders with scripting disabled - [ ] Images are emitted per layout width with a srcset - [ ] Broken links and missing images fail the build - [ ] The manifest records count and total bytes ### Watch out - Shipping the original photographs straight from the upload puts four megabytes of hero image on a page whose entire text is two hundred words, and the person on a train sees nothing for eight seconds. Resize at build time to the widths the layout uses, record the total in the manifest, and fail the build over a stated page weight budget. ## M4 · Publishing inside the host's real limits, with a way back A deploy is a tagged version that can be rolled back, and the limits are checked before the push. ### Steps 1. Check the manifest against published limits Read them from the host's own documentation, and record when you read them. Files: `src/publish/limits.ts` 2. Tag versions and keep the previous one Rollback should not depend on your build still being reproducible today. Files: `src/publish/versions.ts` 3. Show the recent publish count Soft limits fail quietly, which is the worst way to fail. Files: `src/app/publish/page.tsx` ### Done when - [ ] Rollback republishes without rebuilding - [ ] An oversized build is refused before upload - [ ] Recent publish count is visible - [ ] The README records the host's limits and the date checked ### Watch out - Treating publish as a file copy means the first bad deploy is live until you rebuild, and a host's soft build limit turns an afternoon of small fixes into a site that silently stops updating while you keep pressing publish. Tag every version, check the manifest against the host's stated limits first, and make rollback a single command that does not rebuild anything. ## M5 · Email capture that is honest about where the address goes (production only) A static page can collect an address, and the person is told what happens to it. ### Steps 1. Store the consent text with the address A list without it is a list you cannot use. Files: `src/app/api/signup/route.ts` 2. Rate limit without fingerprinting Abuse control should not become tracking. Files: `src/lib/ratelimit.ts` 3. Export everything, including the consent column The export is the only thing that outlives this app. Files: `src/lib/export.ts` ### Done when - [ ] Consent text is stored verbatim per signup - [ ] Rate limiting stores no visitor fingerprint - [ ] Exports include consent text and source page - [ ] The README states no email is sent from here ### Watch out - Collecting addresses with no record of what the person agreed to leaves you, a year later, with a list you cannot lawfully or honestly send to, because nobody can say what was on the page when each address was typed. Store the consent text verbatim with every signup, and version it when the wording changes. ===== OPERATIONS.md ===== # Operations · Mixo ## Backup The database and the built versions directory. Sections are the source; a published version is only useful for rolling back to, which is exactly why it is worth keeping. ## Restore Restore and confirm a rollback to the previously published version still works without a rebuild, since a rebuild after a restore may not reproduce what was live. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Publishes in the last hour against the host's soft limit, the size of the last build against its size limit, and signup volume, where a sudden spike usually means automated submissions. ## Incident checklist A bad page live is the incident, and the response is rollback first and diagnosis afterwards. Keeping the previous version's files is what makes that a single command rather than a rebuild under pressure. 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 - [ ] No stored section field is confirmed to contain markup - [ ] An unsupported numeric claim is confirmed rejected - [ ] Content is confirmed to render with scripting disabled - [ ] A rollback is confirmed to work without rebuilding - [ ] Consent text is confirmed stored verbatim with each signup ## Launch constraint Do not market omitted Mixo 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. Sites, pages, sections, facts and versions. DATABASE_PATH=./data/sites.db # Optional · secret. Generation only. Without it the editor still builds and publishes. MODEL_API_KEY=sk-...redacted # Optional. Recorded on every generated section. MODEL_NAME=a current small text model # Required. Static output. What gets published, and nothing else. BUILD_DIR=./out # Required. A page over this fails the build. PAGE_WEIGHT_BUDGET_KB=500 # Optional. Where the published form posts. Empty means the form is not rendered. SIGNUP_ENDPOINT_URL=https://forms.example.com/signup
# Mixo · indie build A landing page builder whose hard problems are keeping a page as data so regeneration cannot eat hand edits, publishing static files within a host's real limits with a rollback, and collecting email addresses from a static site without pretending to be a mailing list provider. 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 | | --- | --- | --- | | Runtime | Next.js 15 and TypeScript for the editor, run locally | the editor is a tool you use, not a service you operate | | Storage | SQLite holding pages as structured sections | a page is data, and HTML is one rendering of it | | Generation | One model behind a single interface, filling named fields only | a model that can emit markup can emit anything | | Output | Static HTML, CSS and images written to a folder, no client framework | a landing page that needs a runtime to show a headline has failed | | Publishing | Push the folder to a static host with a version tag and a rollback | the first bad deploy arrives sooner than you expect | ## 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 API key for one text model** · pennies per page, and it is worth setting a hard cap - Why: Generation fills section fields. One provider behind one interface is enough, and the app must stay usable with the key absent. - Get it: Create a key with your chosen provider and set a spending limit on it. - Verify: A section generates and validates against its schema, and removing the key leaves the editor working - [ ] **A static host and its published limits** · free tiers are common for a personal site - Why: Publishing is the phase where guessed limits cause silent failures, so the numbers come from the host's own documentation. - Get it: Choose a static host, read its current limits page, and record the figures with the date. - Verify: The README carries the limits and the date, and the publish step checks against them - [ ] **A domain you control** (optional) · roughly ten to fifteen pounds a year - Why: A landing page on a host's subdomain cannot be moved without losing every link to it, and moving is the point of owning the output. - Get it: Register a domain and point it at the static host per its documentation. - Verify: The published site answers on your domain over HTTPS - [ ] **The claims you are willing to publish** · free - Why: The generator checks numbers against this list, so writing it first is what makes the check meaningful rather than decorative. - Get it: Write down every figure and proof point that is true about the thing you are launching, with where each came from. - Verify: Each appears in the facts table with its evidence ## 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: - Sending email to collected addresses. - A free-form visual editor. - A template marketplace and asset library. - Hosted analytics and visitor tracking. - sender reputation, deliverability, and suppression handling - 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 Mixo, and the README should say so rather than pretend.
# Build brief · Mixo 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 landing page generator that stores pages as structured sections and publishes static files to replace Mixo. 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) - Runtime: Next.js 15 and TypeScript for the editor, run locally · the editor is a tool you use, not a service you operate - Storage: SQLite holding pages as structured sections · a page is data, and HTML is one rendering of it - Generation: One model behind a single interface, filling named fields only · a model that can emit markup can emit anything - Output: Static HTML, CSS and images written to a folder, no client framework · a landing page that needs a runtime to show a headline has failed - Publishing: Push the folder to a static host with a version tag and a rollback · the first bad deploy arrives sooner than you expect ### Data model (create this before Phase 1) - sites(id, name, domain, created_at, published_version) - pages(id, site_id, path, title, meta_description, created_at, updated_at) - sections(id, page_id, ordinal, kind, fields_json, locked, source) · source is generated, edited or imported - facts(id, site_id, key, value, evidence) · every number that may appear on the page, with where it came from - versions(id, site_id, built_at, manifest_json, bytes_total, file_count, published_at, rolled_back_at) - signups(id, site_id, email, created_at, consent_text, source_page) · plain storage, with a documented export - A section is typed fields, never a blob of markup, which is what makes regenerating one section safe for the others. ### Phase 1 · A page that is data before it is HTML Build: Define a small set of section kinds with named typed fields: hero, feature list, quote, pricing, call to action, footer. Write one renderer from sections to HTML, and no other path to output. Render twice from the same data and assert byte identical results. Trap: Storing each section as a blob of HTML because the model produced HTML means every later edit is a string operation, two sections can define the same class name with different meanings, and nothing can be restyled without a find and replace across every page you ever generated. Type the fields, keep markup out of the database, and let the renderer own every tag. Done when: No row in the sections table contains markup, asserted by a test that scans for angle brackets in field values. Rendering the same page twice produces byte identical output. Changing the site stylesheet restyles every existing page with no edit to stored data. An unknown section kind fails the build with the page and ordinal named rather than rendering as empty. Do not build yet: No generation, publishing, forms or domains. ### Phase 2 · Generation that fills fields and invents no facts Build: Prompt the model to return values for a section's fields as structured data, validated against the section schema before anything is stored. Keep a facts table of the numbers and claims you are willing to publish, and reject generated text containing a number that is not in it. Mark hand edited sections locked so regeneration skips them. Trap: Asking a model for a landing page and storing whatever comes back gets you 'trusted by over 2,000 teams' on the site of a product with four customers, and because it reads exactly like the rest of the copy nobody deletes it before the page goes live. Gate every numeric claim on a recorded fact, and refuse the section rather than publishing an invented one. Done when: A generated section containing a number absent from the facts table is rejected with the offending phrase shown, tested with a crafted response. Generated output that does not validate against the section schema is discarded and retried a bounded number of times, never stored partially. A locked section is unchanged by a regeneration of its page, tested. Every section records whether it was generated, edited or imported. Do not build yet: No building, publishing or email capture yet. ### Phase 3 · A build that a browser can load on a bad connection Build: Build to a folder: HTML, one stylesheet, images resized to the widths the layout actually uses with a srcset, and no client JavaScript required for anything the visitor must read. Write a manifest of every file with its size, and fail the build on a broken internal link or a missing image. Trap: Shipping the original photographs straight from the upload puts four megabytes of hero image on a page whose entire text is two hundred words, and the person on a train sees nothing for eight seconds. Resize at build time to the widths the layout uses, record the total in the manifest, and fail the build over a stated page weight budget. Done when: The built folder contains no JavaScript that the visible content depends on, checked by rendering with scripting disabled. Every image is emitted at the widths used by the layout and referenced through a srcset. A broken internal link or missing image fails the build naming the file. The manifest records file count and total bytes, and exceeding the stated page weight budget fails the build. Do not build yet: No publishing, custom domain or email capture yet. ### Phase 4 · Publishing inside the host's real limits, with a way back Build: Publish the built folder to a static host. Read that host's published limits and check the manifest against them before pushing: GitHub Pages, for example, documents that a published site may be no larger than one gigabyte, with soft limits of one hundred gigabytes of bandwidth a month and ten builds an hour, the build limit not applying to a custom Actions workflow. Keep the previous version so a rollback is one command. Trap: Treating publish as a file copy means the first bad deploy is live until you rebuild, and a host's soft build limit turns an afternoon of small fixes into a site that silently stops updating while you keep pressing publish. Tag every version, check the manifest against the host's stated limits first, and make rollback a single command that does not rebuild anything. Done when: Publishing records a version with its manifest and the time, and rollback republishes a previous version without rebuilding, tested. A build exceeding the host's documented size limit is refused before any upload, with the limit and the measured size shown. The number of publishes in the last hour is displayed beside the publish button. The README names the host, its current limits and the date they were checked. Do not build yet: No email capture or analytics yet. ### Phase 5 · Email capture that is honest about where the address goes Build: Add a form that posts to one small endpoint you run, storing address, time, the exact consent text shown and the page it came from. Rate limit it and reject obvious automated submissions without fingerprinting the visitor. Export to CSV. State plainly that this is a list, not a sending service, and that sending to it is a separate decision with its own obligations. Trap: Collecting addresses with no record of what the person agreed to leaves you, a year later, with a list you cannot lawfully or honestly send to, because nobody can say what was on the page when each address was typed. Store the consent text verbatim with every signup, and version it when the wording changes. Done when: Every signup row stores the consent wording shown on that page at that time, verified by changing the wording and checking old rows are unchanged. The endpoint rate limits by address and by origin without storing a visitor fingerprint. CSV export includes the consent text and source page. The README states that no email is sent by this app and that bulk sending has requirements of its own to check before the first send. Do not build yet: Nothing after this. ### Out of scope (and why) - Sending email to the addresses you collect, which is a deliverability and reputation problem large enough to be its own product. - A free-form visual editor, because dragging arbitrary boxes is what turns structured sections back into blobs of markup. - A template marketplace and stock asset library, which is inventory work rather than software work. - Hosted analytics, which would put a tracker on every page you publish for numbers a static host's logs already approximate. ### README must contain - That a page is stored as typed sections and HTML exists only as build output - That generated copy cannot contain a number absent from the facts table, and what happens when it tries - That the built site needs no client JavaScript to display its content - The static host in use, its documented limits, the date you checked them, and how to roll back - That signups are stored with the exact consent wording and that this app sends no email
# Agent instructions · Mixo indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js 15 and TypeScript for the editor, run locally, SQLite holding pages as structured sections, One model behind a single interface, filling named fields only, Static HTML, CSS and images written to a folder, no client framework, Push the folder to a static host with a version tag and a rollback. 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 - Storing each section as a blob of HTML because the model produced HTML means every later edit is a string operation, two sections can define the same class name with different meanings, and nothing can be restyled without a find and replace across every page you ever generated. Type the fields, keep markup out of the database, and let the renderer own every tag. - Asking a model for a landing page and storing whatever comes back gets you 'trusted by over 2,000 teams' on the site of a product with four customers, and because it reads exactly like the rest of the copy nobody deletes it before the page goes live. Gate every numeric claim on a recorded fact, and refuse the section rather than publishing an invented one. - Shipping the original photographs straight from the upload puts four megabytes of hero image on a page whose entire text is two hundred words, and the person on a train sees nothing for eight seconds. Resize at build time to the widths the layout uses, record the total in the manifest, and fail the build over a stated page weight budget. - Treating publish as a file copy means the first bad deploy is live until you rebuild, and a host's soft build limit turns an afternoon of small fixes into a site that silently stops updating while you keep pressing publish. Tag every version, check the manifest against the host's stated limits first, and make rollback a single command that does not rebuild anything.
# Build plan · Mixo A landing page builder whose hard problems are keeping a page as data so regeneration cannot eat hand edits, publishing static files within a host's real limits with a rollback, and collecting email addresses from a static site without pretending to be a mailing list provider. 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 page that is data before it is HTML Sections are typed fields and one deterministic renderer turns them into a page. ### Steps 1. Define section kinds with typed fields Six kinds that cover a landing page beats forty that cover everything. Files: `src/lib/sections.ts`, `src/lib/sections.test.ts` 2. One renderer, one output path Two ways to produce HTML means two ways to produce different HTML. Files: `src/render/page.ts` 3. Assert determinism in the suite A build you cannot reproduce is a build you cannot roll back to. Files: `tests/deterministic-render.test.ts` ### Done when - [ ] No stored field contains markup - [ ] Rendering twice is byte identical - [ ] A stylesheet change restyles every page - [ ] Unknown section kinds fail the build loudly ### Watch out - Storing each section as a blob of HTML because the model produced HTML means every later edit is a string operation, two sections can define the same class name with different meanings, and nothing can be restyled without a find and replace across every page you ever generated. Type the fields, keep markup out of the database, and let the renderer own every tag. ## Phase 2 · Generation that fills fields and invents no facts The model writes into named fields, and any number it produces must match a recorded fact. ### Steps 1. Validate generated output against the schema Structured fields in, structured fields out, or nothing. Files: `src/lib/generate.ts` 2. Gate numeric claims on the facts table The one check that keeps a demo site from lying about a real business. Files: `src/lib/claims.ts`, `tests/claims.test.ts` 3. Lock what a person edited Nobody should lose a rewritten headline to a regeneration. Files: `src/lib/regenerate.ts` ### Done when - [ ] Unsupported numbers are rejected with the phrase shown - [ ] Invalid generations are discarded, never partially stored - [ ] Locked sections survive regeneration - [ ] Every section records its source ### Watch out - Asking a model for a landing page and storing whatever comes back gets you 'trusted by over 2,000 teams' on the site of a product with four customers, and because it reads exactly like the rest of the copy nobody deletes it before the page goes live. Gate every numeric claim on a recorded fact, and refuse the section rather than publishing an invented one. ## Phase 3 · A build that a browser can load on a bad connection The output is static files, sized and linked correctly, with no runtime requirement. ### Steps 1. Emit images at the widths the layout uses A hero image is not a photograph archive. Files: `src/build/images.ts` 2. Fail the build on a broken link Publishing is the wrong time to find out. Files: `src/build/links.ts` 3. Write a manifest with sizes It is what makes the next phase's limits checkable. Files: `src/build/manifest.ts` ### Done when - [ ] Content renders with scripting disabled - [ ] Images are emitted per layout width with a srcset - [ ] Broken links and missing images fail the build - [ ] The manifest records count and total bytes ### Watch out - Shipping the original photographs straight from the upload puts four megabytes of hero image on a page whose entire text is two hundred words, and the person on a train sees nothing for eight seconds. Resize at build time to the widths the layout uses, record the total in the manifest, and fail the build over a stated page weight budget. ## Phase 4 · Publishing inside the host's real limits, with a way back A deploy is a tagged version that can be rolled back, and the limits are checked before the push. ### Steps 1. Check the manifest against published limits Read them from the host's own documentation, and record when you read them. Files: `src/publish/limits.ts` 2. Tag versions and keep the previous one Rollback should not depend on your build still being reproducible today. Files: `src/publish/versions.ts` 3. Show the recent publish count Soft limits fail quietly, which is the worst way to fail. Files: `src/app/publish/page.tsx` ### Done when - [ ] Rollback republishes without rebuilding - [ ] An oversized build is refused before upload - [ ] Recent publish count is visible - [ ] The README records the host's limits and the date checked ### Watch out - Treating publish as a file copy means the first bad deploy is live until you rebuild, and a host's soft build limit turns an afternoon of small fixes into a site that silently stops updating while you keep pressing publish. Tag every version, check the manifest against the host's stated limits first, and make rollback a single command that does not rebuild anything. ## Not in this build - Sending email to collected addresses. - A free-form visual editor. - A template marketplace and asset library. - Hosted analytics and visitor tracking. ## After v1, if you want it - A second page type such as a changelog, which is the same sections against a different layout - Multilingual pages, easy while sections are data and painful once they are markup - A scheduled rebuild that refreshes only pages whose facts have changed
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Sites, pages, sections, facts and versions. DATABASE_PATH=./data/sites.db # Optional · secret. Generation only. Without it the editor still builds and publishes. MODEL_API_KEY=sk-...redacted # Optional. Recorded on every generated section. MODEL_NAME=a current small text model # Required. Static output. What gets published, and nothing else. BUILD_DIR=./out # Required. A page over this fails the build. PAGE_WEIGHT_BUDGET_KB=500 # Optional. Where the published form posts. Empty means the form is not rendered. SIGNUP_ENDPOINT_URL=https://forms.example.com/signup
# Mixo · product brief ## Problem Mixo's solo core is compact: build a focused AI website builder generator that edits structured page sections, previews responsive layouts, and publishes static files. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on editor, CMS, hosting, polish. ## Product outcome A builder that keeps pages as structured data, refuses to publish a number nobody can back up, produces static files light enough to load on a train, and lets you roll back a bad deploy in one command. ## 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 - Sending email to collected addresses. - A free-form visual editor. - A template marketplace and asset library. - Hosted analytics and visitor tracking. - sender reputation, deliverability, and suppression handling - general-purpose visual editing - large template and component libraries - CMS collaboration and client handoff - managed hosting, forms, and analytics ## Success criteria - No stored section field is confirmed to contain markup - An unsupported numeric claim is confirmed rejected - Content is confirmed to render with scripting disabled - A rollback is confirmed to work without rebuilding - Consent text is confirmed stored verbatim with each signup
# Build brief · Mixo 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 landing page generator that stores pages as structured sections and publishes static files to replace Mixo. 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) - Runtime: Next.js 15 and TypeScript for the editor, run locally · the editor is a tool you use, not a service you operate - Storage: SQLite holding pages as structured sections · a page is data, and HTML is one rendering of it - Generation: One model behind a single interface, filling named fields only · a model that can emit markup can emit anything - Output: Static HTML, CSS and images written to a folder, no client framework · a landing page that needs a runtime to show a headline has failed - Publishing: Push the folder to a static host with a version tag and a rollback · the first bad deploy arrives sooner than you expect ### Data model (create this before Phase 1) - sites(id, name, domain, created_at, published_version) - pages(id, site_id, path, title, meta_description, created_at, updated_at) - sections(id, page_id, ordinal, kind, fields_json, locked, source) · source is generated, edited or imported - facts(id, site_id, key, value, evidence) · every number that may appear on the page, with where it came from - versions(id, site_id, built_at, manifest_json, bytes_total, file_count, published_at, rolled_back_at) - signups(id, site_id, email, created_at, consent_text, source_page) · plain storage, with a documented export - A section is typed fields, never a blob of markup, which is what makes regenerating one section safe for the others. ### Phase 1 · A page that is data before it is HTML Build: Define a small set of section kinds with named typed fields: hero, feature list, quote, pricing, call to action, footer. Write one renderer from sections to HTML, and no other path to output. Render twice from the same data and assert byte identical results. Trap: Storing each section as a blob of HTML because the model produced HTML means every later edit is a string operation, two sections can define the same class name with different meanings, and nothing can be restyled without a find and replace across every page you ever generated. Type the fields, keep markup out of the database, and let the renderer own every tag. Done when: No row in the sections table contains markup, asserted by a test that scans for angle brackets in field values. Rendering the same page twice produces byte identical output. Changing the site stylesheet restyles every existing page with no edit to stored data. An unknown section kind fails the build with the page and ordinal named rather than rendering as empty. Do not build yet: No generation, publishing, forms or domains. ### Phase 2 · Generation that fills fields and invents no facts Build: Prompt the model to return values for a section's fields as structured data, validated against the section schema before anything is stored. Keep a facts table of the numbers and claims you are willing to publish, and reject generated text containing a number that is not in it. Mark hand edited sections locked so regeneration skips them. Trap: Asking a model for a landing page and storing whatever comes back gets you 'trusted by over 2,000 teams' on the site of a product with four customers, and because it reads exactly like the rest of the copy nobody deletes it before the page goes live. Gate every numeric claim on a recorded fact, and refuse the section rather than publishing an invented one. Done when: A generated section containing a number absent from the facts table is rejected with the offending phrase shown, tested with a crafted response. Generated output that does not validate against the section schema is discarded and retried a bounded number of times, never stored partially. A locked section is unchanged by a regeneration of its page, tested. Every section records whether it was generated, edited or imported. Do not build yet: No building, publishing or email capture yet. ### Phase 3 · A build that a browser can load on a bad connection Build: Build to a folder: HTML, one stylesheet, images resized to the widths the layout actually uses with a srcset, and no client JavaScript required for anything the visitor must read. Write a manifest of every file with its size, and fail the build on a broken internal link or a missing image. Trap: Shipping the original photographs straight from the upload puts four megabytes of hero image on a page whose entire text is two hundred words, and the person on a train sees nothing for eight seconds. Resize at build time to the widths the layout uses, record the total in the manifest, and fail the build over a stated page weight budget. Done when: The built folder contains no JavaScript that the visible content depends on, checked by rendering with scripting disabled. Every image is emitted at the widths used by the layout and referenced through a srcset. A broken internal link or missing image fails the build naming the file. The manifest records file count and total bytes, and exceeding the stated page weight budget fails the build. Do not build yet: No publishing, custom domain or email capture yet. ### Phase 4 · Publishing inside the host's real limits, with a way back Build: Publish the built folder to a static host. Read that host's published limits and check the manifest against them before pushing: GitHub Pages, for example, documents that a published site may be no larger than one gigabyte, with soft limits of one hundred gigabytes of bandwidth a month and ten builds an hour, the build limit not applying to a custom Actions workflow. Keep the previous version so a rollback is one command. Trap: Treating publish as a file copy means the first bad deploy is live until you rebuild, and a host's soft build limit turns an afternoon of small fixes into a site that silently stops updating while you keep pressing publish. Tag every version, check the manifest against the host's stated limits first, and make rollback a single command that does not rebuild anything. Done when: Publishing records a version with its manifest and the time, and rollback republishes a previous version without rebuilding, tested. A build exceeding the host's documented size limit is refused before any upload, with the limit and the measured size shown. The number of publishes in the last hour is displayed beside the publish button. The README names the host, its current limits and the date they were checked. Do not build yet: No email capture or analytics yet. ### Phase 5 · Email capture that is honest about where the address goes Build: Add a form that posts to one small endpoint you run, storing address, time, the exact consent text shown and the page it came from. Rate limit it and reject obvious automated submissions without fingerprinting the visitor. Export to CSV. State plainly that this is a list, not a sending service, and that sending to it is a separate decision with its own obligations. Trap: Collecting addresses with no record of what the person agreed to leaves you, a year later, with a list you cannot lawfully or honestly send to, because nobody can say what was on the page when each address was typed. Store the consent text verbatim with every signup, and version it when the wording changes. Done when: Every signup row stores the consent wording shown on that page at that time, verified by changing the wording and checking old rows are unchanged. The endpoint rate limits by address and by origin without storing a visitor fingerprint. CSV export includes the consent text and source page. The README states that no email is sent by this app and that bulk sending has requirements of its own to check before the first send. Do not build yet: Nothing after this. ### Out of scope (and why) - Sending email to the addresses you collect, which is a deliverability and reputation problem large enough to be its own product. - A free-form visual editor, because dragging arbitrary boxes is what turns structured sections back into blobs of markup. - A template marketplace and stock asset library, which is inventory work rather than software work. - Hosted analytics, which would put a tracker on every page you publish for numbers a static host's logs already approximate. ### README must contain - That a page is stored as typed sections and HTML exists only as build output - That generated copy cannot contain a number absent from the facts table, and what happens when it tries - That the built site needs no client JavaScript to display its content - The static host in use, its documented limits, the date you checked them, and how to roll back - That signups are stored with the exact consent wording and that this app sends no email
# Architecture · Mixo ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js 15 and TypeScript for the editor, run locally | the editor is a tool you use, not a service you operate | | Storage | SQLite holding pages as structured sections | a page is data, and HTML is one rendering of it | | Generation | One model behind a single interface, filling named fields only | a model that can emit markup can emit anything | | Output | Static HTML, CSS and images written to a folder, no client framework | a landing page that needs a runtime to show a headline has failed | | Publishing | Push the folder to a static host with a version tag and a rollback | the first bad deploy arrives sooner than you expect | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Sections | Typed fields and section kinds. | Any schema, provided markup never enters the database. | | Generation | Structured filling and the claims gate. | Any model behind the same interface, with the app usable without one. | | Build | Deterministic rendering, image widths and the manifest. | Any static generator that produces the same manifest. | | Publish | Version tags, limit checks and rollback. | Any static host, with its own limits read from its documentation. | | Signups | Address storage with verbatim consent. | Any store, provided the consent text travels with the row. | ## 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 · Sites, pages, sections, facts and versions. - `MODEL_API_KEY` · optional, secret · Generation only. Without it the editor still builds and publishes. - `MODEL_NAME` · optional · Recorded on every generated section. - `BUILD_DIR` · required · Static output. What gets published, and nothing else. - `PAGE_WEIGHT_BUDGET_KB` · required · A page over this fails the build. - `SIGNUP_ENDPOINT_URL` · optional · Where the published form posts. Empty means the form is not rendered. ## 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 · Mixo product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Next.js 15 and TypeScript for the editor, run locally, SQLite holding pages as structured sections, One model behind a single interface, filling named fields only, Static HTML, CSS and images written to a folder, no client framework, Push the folder to a static host with a version tag and a rollback. - 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 - Storing each section as a blob of HTML because the model produced HTML means every later edit is a string operation, two sections can define the same class name with different meanings, and nothing can be restyled without a find and replace across every page you ever generated. Type the fields, keep markup out of the database, and let the renderer own every tag. - Asking a model for a landing page and storing whatever comes back gets you 'trusted by over 2,000 teams' on the site of a product with four customers, and because it reads exactly like the rest of the copy nobody deletes it before the page goes live. Gate every numeric claim on a recorded fact, and refuse the section rather than publishing an invented one. - Shipping the original photographs straight from the upload puts four megabytes of hero image on a page whose entire text is two hundred words, and the person on a train sees nothing for eight seconds. Resize at build time to the widths the layout uses, record the total in the manifest, and fail the build over a stated page weight budget. - Treating publish as a file copy means the first bad deploy is live until you rebuild, and a host's soft build limit turns an afternoon of small fixes into a site that silently stops updating while you keep pressing publish. Tag every version, check the manifest against the host's stated limits first, and make rollback a single command that does not rebuild anything. - Collecting addresses with no record of what the person agreed to leaves you, a year later, with a list you cannot lawfully or honestly send to, because nobody can say what was on the page when each address was typed. Store the consent text verbatim with every signup, and version it when the wording changes.
# Delivery milestones · Mixo Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A page that is data before it is HTML Sections are typed fields and one deterministic renderer turns them into a page. ### Steps 1. Define section kinds with typed fields Six kinds that cover a landing page beats forty that cover everything. Files: `src/lib/sections.ts`, `src/lib/sections.test.ts` 2. One renderer, one output path Two ways to produce HTML means two ways to produce different HTML. Files: `src/render/page.ts` 3. Assert determinism in the suite A build you cannot reproduce is a build you cannot roll back to. Files: `tests/deterministic-render.test.ts` ### Done when - [ ] No stored field contains markup - [ ] Rendering twice is byte identical - [ ] A stylesheet change restyles every page - [ ] Unknown section kinds fail the build loudly ### Watch out - Storing each section as a blob of HTML because the model produced HTML means every later edit is a string operation, two sections can define the same class name with different meanings, and nothing can be restyled without a find and replace across every page you ever generated. Type the fields, keep markup out of the database, and let the renderer own every tag. ## M2 · Generation that fills fields and invents no facts The model writes into named fields, and any number it produces must match a recorded fact. ### Steps 1. Validate generated output against the schema Structured fields in, structured fields out, or nothing. Files: `src/lib/generate.ts` 2. Gate numeric claims on the facts table The one check that keeps a demo site from lying about a real business. Files: `src/lib/claims.ts`, `tests/claims.test.ts` 3. Lock what a person edited Nobody should lose a rewritten headline to a regeneration. Files: `src/lib/regenerate.ts` ### Done when - [ ] Unsupported numbers are rejected with the phrase shown - [ ] Invalid generations are discarded, never partially stored - [ ] Locked sections survive regeneration - [ ] Every section records its source ### Watch out - Asking a model for a landing page and storing whatever comes back gets you 'trusted by over 2,000 teams' on the site of a product with four customers, and because it reads exactly like the rest of the copy nobody deletes it before the page goes live. Gate every numeric claim on a recorded fact, and refuse the section rather than publishing an invented one. ## M3 · A build that a browser can load on a bad connection The output is static files, sized and linked correctly, with no runtime requirement. ### Steps 1. Emit images at the widths the layout uses A hero image is not a photograph archive. Files: `src/build/images.ts` 2. Fail the build on a broken link Publishing is the wrong time to find out. Files: `src/build/links.ts` 3. Write a manifest with sizes It is what makes the next phase's limits checkable. Files: `src/build/manifest.ts` ### Done when - [ ] Content renders with scripting disabled - [ ] Images are emitted per layout width with a srcset - [ ] Broken links and missing images fail the build - [ ] The manifest records count and total bytes ### Watch out - Shipping the original photographs straight from the upload puts four megabytes of hero image on a page whose entire text is two hundred words, and the person on a train sees nothing for eight seconds. Resize at build time to the widths the layout uses, record the total in the manifest, and fail the build over a stated page weight budget. ## M4 · Publishing inside the host's real limits, with a way back A deploy is a tagged version that can be rolled back, and the limits are checked before the push. ### Steps 1. Check the manifest against published limits Read them from the host's own documentation, and record when you read them. Files: `src/publish/limits.ts` 2. Tag versions and keep the previous one Rollback should not depend on your build still being reproducible today. Files: `src/publish/versions.ts` 3. Show the recent publish count Soft limits fail quietly, which is the worst way to fail. Files: `src/app/publish/page.tsx` ### Done when - [ ] Rollback republishes without rebuilding - [ ] An oversized build is refused before upload - [ ] Recent publish count is visible - [ ] The README records the host's limits and the date checked ### Watch out - Treating publish as a file copy means the first bad deploy is live until you rebuild, and a host's soft build limit turns an afternoon of small fixes into a site that silently stops updating while you keep pressing publish. Tag every version, check the manifest against the host's stated limits first, and make rollback a single command that does not rebuild anything. ## M5 · Email capture that is honest about where the address goes (production only) A static page can collect an address, and the person is told what happens to it. ### Steps 1. Store the consent text with the address A list without it is a list you cannot use. Files: `src/app/api/signup/route.ts` 2. Rate limit without fingerprinting Abuse control should not become tracking. Files: `src/lib/ratelimit.ts` 3. Export everything, including the consent column The export is the only thing that outlives this app. Files: `src/lib/export.ts` ### Done when - [ ] Consent text is stored verbatim per signup - [ ] Rate limiting stores no visitor fingerprint - [ ] Exports include consent text and source page - [ ] The README states no email is sent from here ### Watch out - Collecting addresses with no record of what the person agreed to leaves you, a year later, with a list you cannot lawfully or honestly send to, because nobody can say what was on the page when each address was typed. Store the consent text verbatim with every signup, and version it when the wording changes.
# Operations · Mixo ## Backup The database and the built versions directory. Sections are the source; a published version is only useful for rolling back to, which is exactly why it is worth keeping. ## Restore Restore and confirm a rollback to the previously published version still works without a rebuild, since a rebuild after a restore may not reproduce what was live. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Publishes in the last hour against the host's soft limit, the size of the last build against its size limit, and signup volume, where a sudden spike usually means automated submissions. ## Incident checklist A bad page live is the incident, and the response is rollback first and diagnosis afterwards. Keeping the previous version's files is what makes that a single command rather than a rebuild under pressure. 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 - [ ] No stored section field is confirmed to contain markup - [ ] An unsupported numeric claim is confirmed rejected - [ ] Content is confirmed to render with scripting disabled - [ ] A rollback is confirmed to work without rebuilding - [ ] Consent text is confirmed stored verbatim with each signup ## Launch constraint Do not market omitted Mixo 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. Sites, pages, sections, facts and versions. DATABASE_PATH=./data/sites.db # Optional · secret. Generation only. Without it the editor still builds and publishes. MODEL_API_KEY=sk-...redacted # Optional. Recorded on every generated section. MODEL_NAME=a current small text model # Required. Static output. What gets published, and nothing else. BUILD_DIR=./out # Required. A page over this fails the build. PAGE_WEIGHT_BUDGET_KB=500 # Optional. Where the published form posts. Empty means the form is not rendered. SIGNUP_ENDPOINT_URL=https://forms.example.com/signup
$ choose a build depth, inspect the files, then open the complete pack in your agent
Mixo: A single site is easy to code. People pay for safe ongoing edits, templates, publishing, domains, collaboration, and no-maintenance hosting.
xsender reputation, deliverability, and suppression handling
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.
no votes, no pay-to-list · just what's real
Mixo pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0 | $0 | 1 published page on a Mixo subdomain; Mixo branding; hosting and SSL included |
| basic website | $9 | $7.20 | 1-page website; custom domain connection; hosting and SSL included |
| growing website | $19 | $15.20 | Up to 5 pages; AI generation of additional pages; custom domain; hosting and SSL included |
| premium website | $39 | $31.20 | Up to 50 pages; faster-priority AI page generation; custom domain; hosting and SSL included |
free tier1 page, Mixo branding, Mixo subdomain, hosting and SSL; can be published indefinitely
billingmonthly + annual (-20%)
hidden costsA free standard domain is only included for the first year on qualifying Business/Premium subscriptions; normal renewal fees apply after year one, and premium/specialty domains are excluded.
verified 2026-08-14 · source ↗
Vibecode Mixo
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Mixo replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Mixo cost?
Mixo'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 Mixo?
Honestly: sender reputation, deliverability, and suppression handling; 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 Mixo?
Yes: Mobirise AI (Three AI-generated sites a month, code export and basic lead forms; the copy is as original as the prompt deserves.) The prompt is for when you want it exactly your way.