Vibecode Swipe Pages
track this build4 phases, 12 steps, beginner friendly0%The visible landing-page builder loop is buildable, but a credible replacement needs more than the first screen. Swipe Pages earns its keep through editor, CMS, hosting, polish, so expect a weekend or multi-day build and a narrower personal scope.
You are building a lean indie version of Swipe Pages. 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 ===== # Swipe Pages · indie build A section-based page builder whose hard problems are publishing without ever serving a half-written page, keeping the editor's own code out of what visitors download, and refusing to publish a page that misses a stated performance budget. 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 with TypeScript for the editor only, never for the published output | the editor is an application and the landing page is a document, and mixing them is how these get slow | | Output | Static HTML, CSS and images written to a directory, no client framework | a page you can open from the filesystem is a page any host will serve | | Storage | SQLite holding sections, page versions and publish records | the page is data and the HTML is derived, so a bad publish is one row away from being undone | | Publish | Write to a new content-addressed directory, then swap a symlink | a publish that half lands is a broken page in front of paid traffic | | Budget | A measured budget check that blocks the publish, not a lint warning | a warning nobody reads is not a budget | ## 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 publish pipeline. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **A headless browser for the publish measurement** · free - Why: The budget gate needs a real measurement rather than an estimate, and this is the part people skip and then wonder why the page is slow. - Get it: Install a headless browser your measurement script can drive. - Verify: The measurement script prints paint and interaction figures for a sample page - [ ] **Somewhere to serve a directory of static files** · free to a few pounds a month - Why: The published output is plain files, so this can be a small server, object storage or a static host, and picking it first stops the output drifting towards one vendor's conventions. - Get it: Any host that serves a directory over HTTPS. - Verify: An exported folder serves correctly from it with no build step - [ ] **A domain you control the DNS for** (optional) · around ten pounds a year - Why: Custom domain verification and certificate issuance are the two steps that turn this from a demo into something you can point traffic at. - Get it: Any registrar; you need to be able to add a TXT and an A or CNAME record. - Verify: A TXT record you add is visible from a public resolver - [ ] **The byte and metric ceilings the gate enforces** · free - Why: Chosen before the first section is written, these constrain the design; chosen afterwards they get raised until they pass. - Get it: Write down the HTML, CSS and JavaScript byte ceilings, the third-party origin count, and the paint and interaction figures you will hold to. - Verify: The numbers appear in the configuration file and in the README ## Quick start ```sh ln -sfn out/<hash> out/current.tmp && mv -Tf out/current.tmp out/current ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Arbitrary visual positioning and a template library. - Split testing with a significance verdict. - Managed hosting with traffic allowances. - A visitor analytics script. - large template and component libraries - CMS collaboration and client handoff - managed hosting, forms, and analytics - general-purpose visual editing If one of those is essential to you, that is the reason to keep paying for Swipe Pages, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Swipe Pages 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 builder that edits a small set of section types and publishes plain static files you own to replace Swipe Pages. 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 with TypeScript for the editor only, never for the published output · the editor is an application and the landing page is a document, and mixing them is how these get slow - Output: Static HTML, CSS and images written to a directory, no client framework · a page you can open from the filesystem is a page any host will serve - Storage: SQLite holding sections, page versions and publish records · the page is data and the HTML is derived, so a bad publish is one row away from being undone - Publish: Write to a new content-addressed directory, then swap a symlink · a publish that half lands is a broken page in front of paid traffic - Budget: A measured budget check that blocks the publish, not a lint warning · a warning nobody reads is not a budget ### Data model (create this before Phase 1) - pages(id, slug, title, created_at, archived_at) - page_versions(id, page_id, version, sections_json, theme_json, created_at, published_at) · immutable once published - sections(version_id, ordinal, kind, props_json) · kind is one of a closed list, and an unknown kind fails validation rather than rendering empty - assets(id, page_id, path, sha256, width, height, bytes, mime) · dimensions are stored because the markup needs them - publishes(id, version_id, output_dir, started_at, finished_at, state, bytes_html, bytes_css, bytes_js, lcp_ms, inp_ms, rolled_back_from) - submissions(id, page_id, version_id, received_at, fields_json, source_ref, spam_reason) · written before anything else runs - domains(id, page_id, hostname, verified_at, cert_state) - Every published page maps to exactly one immutable version row, so rollback is pointing the symlink at the previous output directory rather than regenerating anything. ### Phase 1 · A page that is data Build: Define six or seven section kinds and no more: hero, feature list, testimonial slot, pricing table, FAQ, form and footer. Each has a typed props schema. The editor writes rows, and a pure function turns a version row into HTML and CSS with no runtime behind it. Trap: An open-ended section that accepts arbitrary HTML turns the builder into a text editor with extra steps, and the first time a client pastes an embed the page ships a third-party script you never budgeted for and cannot measure. Keep the list closed, validate props against a schema, and make adding a kind a code change. Done when: A version with an unknown section kind fails validation and cannot be saved, tested directly. Rendering a version twice from the same row produces byte-identical HTML, asserted in the suite. No section kind accepts raw HTML or a script tag, verified by review and by a test that attempts it. The renderer is a pure function of a version row with no database access inside it. Do not build yet: No publishing, responsiveness work, budget or forms yet. ### Phase 2 · Publishing that cannot half happen Build: Render a version into a new directory named by the hash of its output. Write every file, fsync, then point the served symlink at it in one operation. Keep the previous directories with a retention. Rollback repoints the symlink and records which publish it came from. Trap: Rendering straight into the served directory means a visitor arriving during the write gets a page with half its stylesheet, and because the write takes under a second you will never reproduce it and will blame the host. Render elsewhere, verify the directory is complete, then swap. Done when: A publish interrupted midway leaves the previously served directory untouched and live, tested by killing the process during a render. The served path is a symlink and switching it is the only step that makes a publish live, verified by inspection. Rollback restores the previous output in one action and records the publish it rolled back from. Output directories older than the configured retention are removed and the currently served one is never removed, tested at the boundary. Do not build yet: No responsive rules, budget gate, forms or domains yet. ### Phase 3 · Responsive without a breakpoint editor Build: Use fluid type and spacing scales clamped between a floor and a ceiling, a single column stack below a stated width, and grids that wrap rather than shrink. Store every asset's intrinsic width and height at upload and emit them in the markup along with a srcset. Trap: Letting the editor set values per breakpoint feels like control and produces four layouts that drift apart, so the phone version is six months behind the desktop one and nobody notices because nobody previews it. Author once with rules, and let the preview show three widths side by side rather than offering three editors. Done when: The preview shows the page at a phone, tablet and desktop width simultaneously from one set of section rows, checked by review. Every image tag carries width and height attributes taken from the stored asset, asserted in the suite. No horizontal scrollbar appears at 320 pixels wide on any section kind, tested per kind. Removing the stylesheet leaves the page readable in document order, which is the accessibility floor for a static page. Do not build yet: No budget gate, forms or domains yet. ### Phase 4 · A performance budget that blocks the publish Build: Measure the rendered output before the swap: total HTML, CSS and JavaScript bytes, the number of third-party origins, whether a font blocks rendering, and a headless run reporting largest contentful paint and interaction to next paint. Compare against configured ceilings and refuse the publish with the failing figure named. Trap: Shipping the editor's own framework to the visitor is the standard way a page builder produces slow pages, because the bundle that makes the editing experience pleasant is downloaded by every visitor who will never edit anything. Publish static files with a JavaScript ceiling measured in kilobytes and let the gate fail the publish when a section kind starts importing a component library. Done when: A page whose output exceeds any configured byte ceiling is refused at publish with the measured figure and the ceiling both shown, tested with a deliberately bloated section. The published output contains zero framework runtime, asserted by scanning the emitted files. A headless measurement runs as part of publish and its figures are stored on the publish row. Raising a ceiling requires editing the configuration and is recorded in the publish record, so a regression is traceable to a decision. Do not build yet: No forms, domains or export yet. ### Phase 5 · Forms, a domain, and the folder you can take away Build: Post the form to your own endpoint, write the submission in one transaction, then respond. Rate limit per address with a hashed key and add a timed honeypot, recording why anything was rejected. Verify a custom domain by a DNS record and document the certificate step. Export any page as a folder of files plus its version JSON, and import that folder back. Trap: Sending the submission to an email address or a webhook from inside the request handler means a slow receiver holds the visitor's browser open and a timeout loses the lead entirely, which on a landing page is the only thing of value the page produced. Write the row first, respond, and let anything else happen afterwards and be allowed to fail. Done when: A submission is stored before any notification is attempted, tested by making the notifier throw. A rejected submission records its reason and is retrievable, so a misconfigured filter is discoverable rather than invisible. Exporting a page produces a folder that opens correctly from the filesystem with no server, tested. Importing that folder into an empty database reproduces the same rendered output, compared by hash. Do not build yet: Nothing after this. ### Out of scope (and why) - A general-purpose visual editor with arbitrary positioning, which is a different and much larger product and is the reason the paid tools have template libraries to hide behind. - Split testing with a significance verdict, which needs traffic volumes a single page rarely has and a statistics decision you should make deliberately rather than inherit. - Managed hosting with traffic allowances, since the published output is static files and the whole point is that any host will take them. - A visitor analytics script, which is another origin, another consent question and the largest single item in most landing page budgets. ### README must contain - That the published output is static files with no framework runtime, and how to serve them anywhere - That going live is an atomic symlink swap and how to roll back to the previous publish - The exact byte and metric ceilings the publish gate enforces, and how to change one deliberately - That section kinds are a closed list and adding one is a code change, with the reason - That form submissions are written before any notification is attempted, and where rejected ones go ===== AGENTS.md ===== # Agent instructions · Swipe Pages indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js 15 with TypeScript for the editor only, never for the published output, Static HTML, CSS and images written to a directory, no client framework, SQLite holding sections, page versions and publish records, Write to a new content-addressed directory, then swap a symlink, A measured budget check that blocks the publish, not a lint warning. 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 - An open-ended section that accepts arbitrary HTML turns the builder into a text editor with extra steps, and the first time a client pastes an embed the page ships a third-party script you never budgeted for and cannot measure. Keep the list closed, validate props against a schema, and make adding a kind a code change. - Rendering straight into the served directory means a visitor arriving during the write gets a page with half its stylesheet, and because the write takes under a second you will never reproduce it and will blame the host. Render elsewhere, verify the directory is complete, then swap. - Letting the editor set values per breakpoint feels like control and produces four layouts that drift apart, so the phone version is six months behind the desktop one and nobody notices because nobody previews it. Author once with rules, and let the preview show three widths side by side rather than offering three editors. - Shipping the editor's own framework to the visitor is the standard way a page builder produces slow pages, because the bundle that makes the editing experience pleasant is downloaded by every visitor who will never edit anything. Publish static files with a JavaScript ceiling measured in kilobytes and let the gate fail the publish when a section kind starts importing a component library. ===== BUILD_PLAN.md ===== # Build plan · Swipe Pages A section-based page builder whose hard problems are publishing without ever serving a half-written page, keeping the editor's own code out of what visitors download, and refusing to publish a page that misses a stated performance budget. 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 Sections are typed rows validated against a closed list, and the HTML is derived from them. ### Steps 1. Close the section list and type the props Every open field is an unbudgeted script waiting to happen. Files: `src/sections/schema.ts`, `src/sections/index.ts` 2. Make rendering a pure function of a version row Deterministic output is what makes rollback and hashing meaningful. Files: `src/render/page.ts`, `tests/render.test.ts` 3. Refuse raw HTML anywhere in props Including in a field that looks harmless, like a footer note. Files: `src/sections/schema.ts`, `tests/no-raw-html.test.ts` ### Done when - [ ] An unknown section kind fails validation - [ ] Rendering twice is byte-identical - [ ] No prop accepts raw HTML or scripts - [ ] The renderer touches no database ### Watch out - An open-ended section that accepts arbitrary HTML turns the builder into a text editor with extra steps, and the first time a client pastes an embed the page ships a third-party script you never budgeted for and cannot measure. Keep the list closed, validate props against a schema, and make adding a kind a code change. ## Phase 2 · Publishing that cannot half happen A publish writes a complete output directory and becomes live in one atomic step, with rollback. ### Steps 1. Render to a content-addressed directory The hash of the output is the name, so identical input publishes nothing new. Files: `src/publish/render.ts` 2. Swap a symlink as the only go-live step One syscall, not a copy loop through a live directory. Files: `src/publish/swap.ts`, `tests/publish-atomic.test.ts` ```sh ln -sfn out/<hash> out/current.tmp && mv -Tf out/current.tmp out/current ``` 3. Keep previous outputs and implement rollback The cheapest possible fix at eleven at night. Files: `src/publish/rollback.ts` ### Done when - [ ] An interrupted publish leaves the live page untouched - [ ] Going live is a single symlink swap - [ ] Rollback restores the previous output and is recorded - [ ] Retention never removes the live directory ### Watch out - Rendering straight into the served directory means a visitor arriving during the write gets a page with half its stylesheet, and because the write takes under a second you will never reproduce it and will blame the host. Render elsewhere, verify the directory is complete, then swap. ## Phase 3 · Responsive without a breakpoint editor One authored layout adapts by rule, and images never move the page while it loads. ### Steps 1. Fluid scales with a floor and a ceiling Two numbers per scale beats four breakpoints per element. Files: `src/render/theme.ts` 2. Store intrinsic dimensions at upload Emitted into the markup so nothing shifts as images arrive. Files: `src/assets/upload.ts`, `tests/assets.test.ts` 3. Preview three widths at once Nobody checks the phone view if checking it is a separate click. Files: `src/app/editor/preview.tsx` ### Done when - [ ] One set of rows renders three widths in the preview - [ ] Every image carries width and height - [ ] No section scrolls horizontally at 320 pixels - [ ] The page is readable with the stylesheet removed ### Watch out - Letting the editor set values per breakpoint feels like control and produces four layouts that drift apart, so the phone version is six months behind the desktop one and nobody notices because nobody previews it. Author once with rules, and let the preview show three widths side by side rather than offering three editors. ## Phase 4 · A performance budget that blocks the publish A page that misses the stated budget cannot go live until it is fixed or the budget is changed on purpose. ### Steps 1. Measure the output bytes per type before the swap Before it is live, not in a report afterwards. Files: `src/publish/budget.ts`, `tests/budget.test.ts` 2. Run a headless measurement and store the figures Check the current published thresholds for the metrics you record. Files: `scripts/measure.mjs`, `src/publish/measure.ts` 3. Fail the publish, do not warn A warning in a build log is not a budget. Files: `src/publish/index.ts` ### Done when - [ ] An over-budget page is refused with the figure named - [ ] The output contains no framework runtime - [ ] Measured figures are stored on the publish row - [ ] Raising a ceiling is a recorded configuration change ### Watch out - Shipping the editor's own framework to the visitor is the standard way a page builder produces slow pages, because the bundle that makes the editing experience pleasant is downloaded by every visitor who will never edit anything. Publish static files with a JavaScript ceiling measured in kilobytes and let the gate fail the publish when a section kind starts importing a component library. ## Not in this build - Arbitrary visual positioning and a template library. - Split testing with a significance verdict. - Managed hosting with traffic allowances. - A visitor analytics script. ## After v1, if you want it - Field-measured metrics collected from real visits rather than a headless run, once a page has traffic - A second symlink for a staging hostname pointing at the next publish, so a page can be reviewed live before the swap - A per-section budget attribution, so the gate can say which section added the bytes ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Pages, versions, assets, publishes and submissions. DATABASE_PATH=./data/pages.db # Required. Content-addressed publish directories and the served symlink. OUTPUT_ROOT=./out # Required. How many previous publishes to retain for rollback. PUBLISH_KEEP=10 # Required. Zero is a reasonable starting value for a static landing page. BUDGET_JS_BYTES=0 # Required. Measured on the rendered output, not on source files. BUDGET_CSS_BYTES=30000 # Required. Check the current published threshold before setting this. BUDGET_LCP_MS=2500 # Required. Check the current published threshold before setting this. BUDGET_INP_MS=200 # Required. Per hashed address. Rejections are stored with a reason, not dropped. SUBMISSION_RATE_PER_HOUR=20
You are building a lean indie version of Swipe Pages. 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 ===== # Swipe Pages · indie build A section-based page builder whose hard problems are publishing without ever serving a half-written page, keeping the editor's own code out of what visitors download, and refusing to publish a page that misses a stated performance budget. 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 with TypeScript for the editor only, never for the published output | the editor is an application and the landing page is a document, and mixing them is how these get slow | | Output | Static HTML, CSS and images written to a directory, no client framework | a page you can open from the filesystem is a page any host will serve | | Storage | SQLite holding sections, page versions and publish records | the page is data and the HTML is derived, so a bad publish is one row away from being undone | | Publish | Write to a new content-addressed directory, then swap a symlink | a publish that half lands is a broken page in front of paid traffic | | Budget | A measured budget check that blocks the publish, not a lint warning | a warning nobody reads is not a budget | ## 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 publish pipeline. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **A headless browser for the publish measurement** · free - Why: The budget gate needs a real measurement rather than an estimate, and this is the part people skip and then wonder why the page is slow. - Get it: Install a headless browser your measurement script can drive. - Verify: The measurement script prints paint and interaction figures for a sample page - [ ] **Somewhere to serve a directory of static files** · free to a few pounds a month - Why: The published output is plain files, so this can be a small server, object storage or a static host, and picking it first stops the output drifting towards one vendor's conventions. - Get it: Any host that serves a directory over HTTPS. - Verify: An exported folder serves correctly from it with no build step - [ ] **A domain you control the DNS for** (optional) · around ten pounds a year - Why: Custom domain verification and certificate issuance are the two steps that turn this from a demo into something you can point traffic at. - Get it: Any registrar; you need to be able to add a TXT and an A or CNAME record. - Verify: A TXT record you add is visible from a public resolver - [ ] **The byte and metric ceilings the gate enforces** · free - Why: Chosen before the first section is written, these constrain the design; chosen afterwards they get raised until they pass. - Get it: Write down the HTML, CSS and JavaScript byte ceilings, the third-party origin count, and the paint and interaction figures you will hold to. - Verify: The numbers appear in the configuration file and in the README ## Quick start ```sh ln -sfn out/<hash> out/current.tmp && mv -Tf out/current.tmp out/current ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Arbitrary visual positioning and a template library. - Split testing with a significance verdict. - Managed hosting with traffic allowances. - A visitor analytics script. - large template and component libraries - CMS collaboration and client handoff - managed hosting, forms, and analytics - general-purpose visual editing If one of those is essential to you, that is the reason to keep paying for Swipe Pages, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Swipe Pages 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 builder that edits a small set of section types and publishes plain static files you own to replace Swipe Pages. 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 with TypeScript for the editor only, never for the published output · the editor is an application and the landing page is a document, and mixing them is how these get slow - Output: Static HTML, CSS and images written to a directory, no client framework · a page you can open from the filesystem is a page any host will serve - Storage: SQLite holding sections, page versions and publish records · the page is data and the HTML is derived, so a bad publish is one row away from being undone - Publish: Write to a new content-addressed directory, then swap a symlink · a publish that half lands is a broken page in front of paid traffic - Budget: A measured budget check that blocks the publish, not a lint warning · a warning nobody reads is not a budget ### Data model (create this before Phase 1) - pages(id, slug, title, created_at, archived_at) - page_versions(id, page_id, version, sections_json, theme_json, created_at, published_at) · immutable once published - sections(version_id, ordinal, kind, props_json) · kind is one of a closed list, and an unknown kind fails validation rather than rendering empty - assets(id, page_id, path, sha256, width, height, bytes, mime) · dimensions are stored because the markup needs them - publishes(id, version_id, output_dir, started_at, finished_at, state, bytes_html, bytes_css, bytes_js, lcp_ms, inp_ms, rolled_back_from) - submissions(id, page_id, version_id, received_at, fields_json, source_ref, spam_reason) · written before anything else runs - domains(id, page_id, hostname, verified_at, cert_state) - Every published page maps to exactly one immutable version row, so rollback is pointing the symlink at the previous output directory rather than regenerating anything. ### Phase 1 · A page that is data Build: Define six or seven section kinds and no more: hero, feature list, testimonial slot, pricing table, FAQ, form and footer. Each has a typed props schema. The editor writes rows, and a pure function turns a version row into HTML and CSS with no runtime behind it. Trap: An open-ended section that accepts arbitrary HTML turns the builder into a text editor with extra steps, and the first time a client pastes an embed the page ships a third-party script you never budgeted for and cannot measure. Keep the list closed, validate props against a schema, and make adding a kind a code change. Done when: A version with an unknown section kind fails validation and cannot be saved, tested directly. Rendering a version twice from the same row produces byte-identical HTML, asserted in the suite. No section kind accepts raw HTML or a script tag, verified by review and by a test that attempts it. The renderer is a pure function of a version row with no database access inside it. Do not build yet: No publishing, responsiveness work, budget or forms yet. ### Phase 2 · Publishing that cannot half happen Build: Render a version into a new directory named by the hash of its output. Write every file, fsync, then point the served symlink at it in one operation. Keep the previous directories with a retention. Rollback repoints the symlink and records which publish it came from. Trap: Rendering straight into the served directory means a visitor arriving during the write gets a page with half its stylesheet, and because the write takes under a second you will never reproduce it and will blame the host. Render elsewhere, verify the directory is complete, then swap. Done when: A publish interrupted midway leaves the previously served directory untouched and live, tested by killing the process during a render. The served path is a symlink and switching it is the only step that makes a publish live, verified by inspection. Rollback restores the previous output in one action and records the publish it rolled back from. Output directories older than the configured retention are removed and the currently served one is never removed, tested at the boundary. Do not build yet: No responsive rules, budget gate, forms or domains yet. ### Phase 3 · Responsive without a breakpoint editor Build: Use fluid type and spacing scales clamped between a floor and a ceiling, a single column stack below a stated width, and grids that wrap rather than shrink. Store every asset's intrinsic width and height at upload and emit them in the markup along with a srcset. Trap: Letting the editor set values per breakpoint feels like control and produces four layouts that drift apart, so the phone version is six months behind the desktop one and nobody notices because nobody previews it. Author once with rules, and let the preview show three widths side by side rather than offering three editors. Done when: The preview shows the page at a phone, tablet and desktop width simultaneously from one set of section rows, checked by review. Every image tag carries width and height attributes taken from the stored asset, asserted in the suite. No horizontal scrollbar appears at 320 pixels wide on any section kind, tested per kind. Removing the stylesheet leaves the page readable in document order, which is the accessibility floor for a static page. Do not build yet: No budget gate, forms or domains yet. ### Phase 4 · A performance budget that blocks the publish Build: Measure the rendered output before the swap: total HTML, CSS and JavaScript bytes, the number of third-party origins, whether a font blocks rendering, and a headless run reporting largest contentful paint and interaction to next paint. Compare against configured ceilings and refuse the publish with the failing figure named. Trap: Shipping the editor's own framework to the visitor is the standard way a page builder produces slow pages, because the bundle that makes the editing experience pleasant is downloaded by every visitor who will never edit anything. Publish static files with a JavaScript ceiling measured in kilobytes and let the gate fail the publish when a section kind starts importing a component library. Done when: A page whose output exceeds any configured byte ceiling is refused at publish with the measured figure and the ceiling both shown, tested with a deliberately bloated section. The published output contains zero framework runtime, asserted by scanning the emitted files. A headless measurement runs as part of publish and its figures are stored on the publish row. Raising a ceiling requires editing the configuration and is recorded in the publish record, so a regression is traceable to a decision. Do not build yet: No forms, domains or export yet. ### Phase 5 · Forms, a domain, and the folder you can take away Build: Post the form to your own endpoint, write the submission in one transaction, then respond. Rate limit per address with a hashed key and add a timed honeypot, recording why anything was rejected. Verify a custom domain by a DNS record and document the certificate step. Export any page as a folder of files plus its version JSON, and import that folder back. Trap: Sending the submission to an email address or a webhook from inside the request handler means a slow receiver holds the visitor's browser open and a timeout loses the lead entirely, which on a landing page is the only thing of value the page produced. Write the row first, respond, and let anything else happen afterwards and be allowed to fail. Done when: A submission is stored before any notification is attempted, tested by making the notifier throw. A rejected submission records its reason and is retrievable, so a misconfigured filter is discoverable rather than invisible. Exporting a page produces a folder that opens correctly from the filesystem with no server, tested. Importing that folder into an empty database reproduces the same rendered output, compared by hash. Do not build yet: Nothing after this. ### Out of scope (and why) - A general-purpose visual editor with arbitrary positioning, which is a different and much larger product and is the reason the paid tools have template libraries to hide behind. - Split testing with a significance verdict, which needs traffic volumes a single page rarely has and a statistics decision you should make deliberately rather than inherit. - Managed hosting with traffic allowances, since the published output is static files and the whole point is that any host will take them. - A visitor analytics script, which is another origin, another consent question and the largest single item in most landing page budgets. ### README must contain - That the published output is static files with no framework runtime, and how to serve them anywhere - That going live is an atomic symlink swap and how to roll back to the previous publish - The exact byte and metric ceilings the publish gate enforces, and how to change one deliberately - That section kinds are a closed list and adding one is a code change, with the reason - That form submissions are written before any notification is attempted, and where rejected ones go ===== AGENTS.md ===== # Agent instructions · Swipe Pages indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js 15 with TypeScript for the editor only, never for the published output, Static HTML, CSS and images written to a directory, no client framework, SQLite holding sections, page versions and publish records, Write to a new content-addressed directory, then swap a symlink, A measured budget check that blocks the publish, not a lint warning. 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 - An open-ended section that accepts arbitrary HTML turns the builder into a text editor with extra steps, and the first time a client pastes an embed the page ships a third-party script you never budgeted for and cannot measure. Keep the list closed, validate props against a schema, and make adding a kind a code change. - Rendering straight into the served directory means a visitor arriving during the write gets a page with half its stylesheet, and because the write takes under a second you will never reproduce it and will blame the host. Render elsewhere, verify the directory is complete, then swap. - Letting the editor set values per breakpoint feels like control and produces four layouts that drift apart, so the phone version is six months behind the desktop one and nobody notices because nobody previews it. Author once with rules, and let the preview show three widths side by side rather than offering three editors. - Shipping the editor's own framework to the visitor is the standard way a page builder produces slow pages, because the bundle that makes the editing experience pleasant is downloaded by every visitor who will never edit anything. Publish static files with a JavaScript ceiling measured in kilobytes and let the gate fail the publish when a section kind starts importing a component library. ===== BUILD_PLAN.md ===== # Build plan · Swipe Pages A section-based page builder whose hard problems are publishing without ever serving a half-written page, keeping the editor's own code out of what visitors download, and refusing to publish a page that misses a stated performance budget. 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 Sections are typed rows validated against a closed list, and the HTML is derived from them. ### Steps 1. Close the section list and type the props Every open field is an unbudgeted script waiting to happen. Files: `src/sections/schema.ts`, `src/sections/index.ts` 2. Make rendering a pure function of a version row Deterministic output is what makes rollback and hashing meaningful. Files: `src/render/page.ts`, `tests/render.test.ts` 3. Refuse raw HTML anywhere in props Including in a field that looks harmless, like a footer note. Files: `src/sections/schema.ts`, `tests/no-raw-html.test.ts` ### Done when - [ ] An unknown section kind fails validation - [ ] Rendering twice is byte-identical - [ ] No prop accepts raw HTML or scripts - [ ] The renderer touches no database ### Watch out - An open-ended section that accepts arbitrary HTML turns the builder into a text editor with extra steps, and the first time a client pastes an embed the page ships a third-party script you never budgeted for and cannot measure. Keep the list closed, validate props against a schema, and make adding a kind a code change. ## Phase 2 · Publishing that cannot half happen A publish writes a complete output directory and becomes live in one atomic step, with rollback. ### Steps 1. Render to a content-addressed directory The hash of the output is the name, so identical input publishes nothing new. Files: `src/publish/render.ts` 2. Swap a symlink as the only go-live step One syscall, not a copy loop through a live directory. Files: `src/publish/swap.ts`, `tests/publish-atomic.test.ts` ```sh ln -sfn out/<hash> out/current.tmp && mv -Tf out/current.tmp out/current ``` 3. Keep previous outputs and implement rollback The cheapest possible fix at eleven at night. Files: `src/publish/rollback.ts` ### Done when - [ ] An interrupted publish leaves the live page untouched - [ ] Going live is a single symlink swap - [ ] Rollback restores the previous output and is recorded - [ ] Retention never removes the live directory ### Watch out - Rendering straight into the served directory means a visitor arriving during the write gets a page with half its stylesheet, and because the write takes under a second you will never reproduce it and will blame the host. Render elsewhere, verify the directory is complete, then swap. ## Phase 3 · Responsive without a breakpoint editor One authored layout adapts by rule, and images never move the page while it loads. ### Steps 1. Fluid scales with a floor and a ceiling Two numbers per scale beats four breakpoints per element. Files: `src/render/theme.ts` 2. Store intrinsic dimensions at upload Emitted into the markup so nothing shifts as images arrive. Files: `src/assets/upload.ts`, `tests/assets.test.ts` 3. Preview three widths at once Nobody checks the phone view if checking it is a separate click. Files: `src/app/editor/preview.tsx` ### Done when - [ ] One set of rows renders three widths in the preview - [ ] Every image carries width and height - [ ] No section scrolls horizontally at 320 pixels - [ ] The page is readable with the stylesheet removed ### Watch out - Letting the editor set values per breakpoint feels like control and produces four layouts that drift apart, so the phone version is six months behind the desktop one and nobody notices because nobody previews it. Author once with rules, and let the preview show three widths side by side rather than offering three editors. ## Phase 4 · A performance budget that blocks the publish A page that misses the stated budget cannot go live until it is fixed or the budget is changed on purpose. ### Steps 1. Measure the output bytes per type before the swap Before it is live, not in a report afterwards. Files: `src/publish/budget.ts`, `tests/budget.test.ts` 2. Run a headless measurement and store the figures Check the current published thresholds for the metrics you record. Files: `scripts/measure.mjs`, `src/publish/measure.ts` 3. Fail the publish, do not warn A warning in a build log is not a budget. Files: `src/publish/index.ts` ### Done when - [ ] An over-budget page is refused with the figure named - [ ] The output contains no framework runtime - [ ] Measured figures are stored on the publish row - [ ] Raising a ceiling is a recorded configuration change ### Watch out - Shipping the editor's own framework to the visitor is the standard way a page builder produces slow pages, because the bundle that makes the editing experience pleasant is downloaded by every visitor who will never edit anything. Publish static files with a JavaScript ceiling measured in kilobytes and let the gate fail the publish when a section kind starts importing a component library. ## Not in this build - Arbitrary visual positioning and a template library. - Split testing with a significance verdict. - Managed hosting with traffic allowances. - A visitor analytics script. ## After v1, if you want it - Field-measured metrics collected from real visits rather than a headless run, once a page has traffic - A second symlink for a staging hostname pointing at the next publish, so a page can be reviewed live before the swap - A per-section budget attribution, so the gate can say which section added the bytes ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Pages, versions, assets, publishes and submissions. DATABASE_PATH=./data/pages.db # Required. Content-addressed publish directories and the served symlink. OUTPUT_ROOT=./out # Required. How many previous publishes to retain for rollback. PUBLISH_KEEP=10 # Required. Zero is a reasonable starting value for a static landing page. BUDGET_JS_BYTES=0 # Required. Measured on the rendered output, not on source files. BUDGET_CSS_BYTES=30000 # Required. Check the current published threshold before setting this. BUDGET_LCP_MS=2500 # Required. Check the current published threshold before setting this. BUDGET_INP_MS=200 # Required. Per hashed address. Rejections are stored with a reason, not dropped. SUBMISSION_RATE_PER_HOUR=20
You are building a production product version of Swipe Pages. 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 ===== # Swipe Pages · product brief ## Problem The visible landing-page builder loop is buildable, but a credible replacement needs more than the first screen. Swipe Pages earns its keep through editor, CMS, hosting, polish, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A builder whose output is a folder of static files with no framework in it, which goes live in one atomic step, rolls back in one action, and refuses to publish a page that misses the performance budget you wrote down first. ## 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 - Arbitrary visual positioning and a template library. - Split testing with a significance verdict. - Managed hosting with traffic allowances. - A visitor analytics script. - large template and component libraries - CMS collaboration and client handoff - managed hosting, forms, and analytics - general-purpose visual editing ## Success criteria - An interrupted publish is confirmed to leave the live page untouched - The published output is confirmed to contain no framework runtime - An over-budget page is confirmed refused with its figure named - A submission is confirmed stored before any notification runs - An exported folder is confirmed to open without a server ===== BRIEF.md ===== # Build brief · Swipe Pages 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 builder that edits a small set of section types and publishes plain static files you own to replace Swipe Pages. 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 with TypeScript for the editor only, never for the published output · the editor is an application and the landing page is a document, and mixing them is how these get slow - Output: Static HTML, CSS and images written to a directory, no client framework · a page you can open from the filesystem is a page any host will serve - Storage: SQLite holding sections, page versions and publish records · the page is data and the HTML is derived, so a bad publish is one row away from being undone - Publish: Write to a new content-addressed directory, then swap a symlink · a publish that half lands is a broken page in front of paid traffic - Budget: A measured budget check that blocks the publish, not a lint warning · a warning nobody reads is not a budget ### Data model (create this before Phase 1) - pages(id, slug, title, created_at, archived_at) - page_versions(id, page_id, version, sections_json, theme_json, created_at, published_at) · immutable once published - sections(version_id, ordinal, kind, props_json) · kind is one of a closed list, and an unknown kind fails validation rather than rendering empty - assets(id, page_id, path, sha256, width, height, bytes, mime) · dimensions are stored because the markup needs them - publishes(id, version_id, output_dir, started_at, finished_at, state, bytes_html, bytes_css, bytes_js, lcp_ms, inp_ms, rolled_back_from) - submissions(id, page_id, version_id, received_at, fields_json, source_ref, spam_reason) · written before anything else runs - domains(id, page_id, hostname, verified_at, cert_state) - Every published page maps to exactly one immutable version row, so rollback is pointing the symlink at the previous output directory rather than regenerating anything. ### Phase 1 · A page that is data Build: Define six or seven section kinds and no more: hero, feature list, testimonial slot, pricing table, FAQ, form and footer. Each has a typed props schema. The editor writes rows, and a pure function turns a version row into HTML and CSS with no runtime behind it. Trap: An open-ended section that accepts arbitrary HTML turns the builder into a text editor with extra steps, and the first time a client pastes an embed the page ships a third-party script you never budgeted for and cannot measure. Keep the list closed, validate props against a schema, and make adding a kind a code change. Done when: A version with an unknown section kind fails validation and cannot be saved, tested directly. Rendering a version twice from the same row produces byte-identical HTML, asserted in the suite. No section kind accepts raw HTML or a script tag, verified by review and by a test that attempts it. The renderer is a pure function of a version row with no database access inside it. Do not build yet: No publishing, responsiveness work, budget or forms yet. ### Phase 2 · Publishing that cannot half happen Build: Render a version into a new directory named by the hash of its output. Write every file, fsync, then point the served symlink at it in one operation. Keep the previous directories with a retention. Rollback repoints the symlink and records which publish it came from. Trap: Rendering straight into the served directory means a visitor arriving during the write gets a page with half its stylesheet, and because the write takes under a second you will never reproduce it and will blame the host. Render elsewhere, verify the directory is complete, then swap. Done when: A publish interrupted midway leaves the previously served directory untouched and live, tested by killing the process during a render. The served path is a symlink and switching it is the only step that makes a publish live, verified by inspection. Rollback restores the previous output in one action and records the publish it rolled back from. Output directories older than the configured retention are removed and the currently served one is never removed, tested at the boundary. Do not build yet: No responsive rules, budget gate, forms or domains yet. ### Phase 3 · Responsive without a breakpoint editor Build: Use fluid type and spacing scales clamped between a floor and a ceiling, a single column stack below a stated width, and grids that wrap rather than shrink. Store every asset's intrinsic width and height at upload and emit them in the markup along with a srcset. Trap: Letting the editor set values per breakpoint feels like control and produces four layouts that drift apart, so the phone version is six months behind the desktop one and nobody notices because nobody previews it. Author once with rules, and let the preview show three widths side by side rather than offering three editors. Done when: The preview shows the page at a phone, tablet and desktop width simultaneously from one set of section rows, checked by review. Every image tag carries width and height attributes taken from the stored asset, asserted in the suite. No horizontal scrollbar appears at 320 pixels wide on any section kind, tested per kind. Removing the stylesheet leaves the page readable in document order, which is the accessibility floor for a static page. Do not build yet: No budget gate, forms or domains yet. ### Phase 4 · A performance budget that blocks the publish Build: Measure the rendered output before the swap: total HTML, CSS and JavaScript bytes, the number of third-party origins, whether a font blocks rendering, and a headless run reporting largest contentful paint and interaction to next paint. Compare against configured ceilings and refuse the publish with the failing figure named. Trap: Shipping the editor's own framework to the visitor is the standard way a page builder produces slow pages, because the bundle that makes the editing experience pleasant is downloaded by every visitor who will never edit anything. Publish static files with a JavaScript ceiling measured in kilobytes and let the gate fail the publish when a section kind starts importing a component library. Done when: A page whose output exceeds any configured byte ceiling is refused at publish with the measured figure and the ceiling both shown, tested with a deliberately bloated section. The published output contains zero framework runtime, asserted by scanning the emitted files. A headless measurement runs as part of publish and its figures are stored on the publish row. Raising a ceiling requires editing the configuration and is recorded in the publish record, so a regression is traceable to a decision. Do not build yet: No forms, domains or export yet. ### Phase 5 · Forms, a domain, and the folder you can take away Build: Post the form to your own endpoint, write the submission in one transaction, then respond. Rate limit per address with a hashed key and add a timed honeypot, recording why anything was rejected. Verify a custom domain by a DNS record and document the certificate step. Export any page as a folder of files plus its version JSON, and import that folder back. Trap: Sending the submission to an email address or a webhook from inside the request handler means a slow receiver holds the visitor's browser open and a timeout loses the lead entirely, which on a landing page is the only thing of value the page produced. Write the row first, respond, and let anything else happen afterwards and be allowed to fail. Done when: A submission is stored before any notification is attempted, tested by making the notifier throw. A rejected submission records its reason and is retrievable, so a misconfigured filter is discoverable rather than invisible. Exporting a page produces a folder that opens correctly from the filesystem with no server, tested. Importing that folder into an empty database reproduces the same rendered output, compared by hash. Do not build yet: Nothing after this. ### Out of scope (and why) - A general-purpose visual editor with arbitrary positioning, which is a different and much larger product and is the reason the paid tools have template libraries to hide behind. - Split testing with a significance verdict, which needs traffic volumes a single page rarely has and a statistics decision you should make deliberately rather than inherit. - Managed hosting with traffic allowances, since the published output is static files and the whole point is that any host will take them. - A visitor analytics script, which is another origin, another consent question and the largest single item in most landing page budgets. ### README must contain - That the published output is static files with no framework runtime, and how to serve them anywhere - That going live is an atomic symlink swap and how to roll back to the previous publish - The exact byte and metric ceilings the publish gate enforces, and how to change one deliberately - That section kinds are a closed list and adding one is a code change, with the reason - That form submissions are written before any notification is attempted, and where rejected ones go ===== ARCHITECTURE.md ===== # Architecture · Swipe Pages ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js 15 with TypeScript for the editor only, never for the published output | the editor is an application and the landing page is a document, and mixing them is how these get slow | | Output | Static HTML, CSS and images written to a directory, no client framework | a page you can open from the filesystem is a page any host will serve | | Storage | SQLite holding sections, page versions and publish records | the page is data and the HTML is derived, so a bad publish is one row away from being undone | | Publish | Write to a new content-addressed directory, then swap a symlink | a publish that half lands is a broken page in front of paid traffic | | Budget | A measured budget check that blocks the publish, not a lint warning | a warning nobody reads is not a budget | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Sections | The closed kind list and prop validation. | Add kinds in code; never open the list to raw HTML. | | Render | The pure version-to-files function. | Any renderer whose output is deterministic for a given row. | | Publish | Content-addressed directories, the swap and rollback. | Any mechanism that is atomic at the point of going live. | | Budget | Measurement and the publish refusal. | Any measurement tool, provided it blocks rather than warns. | | Forms | Submission storage, filtering and rejection records. | Any notifier afterwards, provided the row is written first. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · Pages, versions, assets, publishes and submissions. - `OUTPUT_ROOT` · required · Content-addressed publish directories and the served symlink. - `PUBLISH_KEEP` · required · How many previous publishes to retain for rollback. - `BUDGET_JS_BYTES` · required · Zero is a reasonable starting value for a static landing page. - `BUDGET_CSS_BYTES` · required · Measured on the rendered output, not on source files. - `BUDGET_LCP_MS` · required · Check the current published threshold before setting this. - `BUDGET_INP_MS` · required · Check the current published threshold before setting this. - `SUBMISSION_RATE_PER_HOUR` · required · Per hashed address. Rejections are stored with a reason, not dropped. ## 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 · Swipe Pages product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Next.js 15 with TypeScript for the editor only, never for the published output, Static HTML, CSS and images written to a directory, no client framework, SQLite holding sections, page versions and publish records, Write to a new content-addressed directory, then swap a symlink, A measured budget check that blocks the publish, not a lint warning. - 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 - An open-ended section that accepts arbitrary HTML turns the builder into a text editor with extra steps, and the first time a client pastes an embed the page ships a third-party script you never budgeted for and cannot measure. Keep the list closed, validate props against a schema, and make adding a kind a code change. - Rendering straight into the served directory means a visitor arriving during the write gets a page with half its stylesheet, and because the write takes under a second you will never reproduce it and will blame the host. Render elsewhere, verify the directory is complete, then swap. - Letting the editor set values per breakpoint feels like control and produces four layouts that drift apart, so the phone version is six months behind the desktop one and nobody notices because nobody previews it. Author once with rules, and let the preview show three widths side by side rather than offering three editors. - Shipping the editor's own framework to the visitor is the standard way a page builder produces slow pages, because the bundle that makes the editing experience pleasant is downloaded by every visitor who will never edit anything. Publish static files with a JavaScript ceiling measured in kilobytes and let the gate fail the publish when a section kind starts importing a component library. - Sending the submission to an email address or a webhook from inside the request handler means a slow receiver holds the visitor's browser open and a timeout loses the lead entirely, which on a landing page is the only thing of value the page produced. Write the row first, respond, and let anything else happen afterwards and be allowed to fail. ===== MILESTONES.md ===== # Delivery milestones · Swipe Pages Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A page that is data Sections are typed rows validated against a closed list, and the HTML is derived from them. ### Steps 1. Close the section list and type the props Every open field is an unbudgeted script waiting to happen. Files: `src/sections/schema.ts`, `src/sections/index.ts` 2. Make rendering a pure function of a version row Deterministic output is what makes rollback and hashing meaningful. Files: `src/render/page.ts`, `tests/render.test.ts` 3. Refuse raw HTML anywhere in props Including in a field that looks harmless, like a footer note. Files: `src/sections/schema.ts`, `tests/no-raw-html.test.ts` ### Done when - [ ] An unknown section kind fails validation - [ ] Rendering twice is byte-identical - [ ] No prop accepts raw HTML or scripts - [ ] The renderer touches no database ### Watch out - An open-ended section that accepts arbitrary HTML turns the builder into a text editor with extra steps, and the first time a client pastes an embed the page ships a third-party script you never budgeted for and cannot measure. Keep the list closed, validate props against a schema, and make adding a kind a code change. ## M2 · Publishing that cannot half happen A publish writes a complete output directory and becomes live in one atomic step, with rollback. ### Steps 1. Render to a content-addressed directory The hash of the output is the name, so identical input publishes nothing new. Files: `src/publish/render.ts` 2. Swap a symlink as the only go-live step One syscall, not a copy loop through a live directory. Files: `src/publish/swap.ts`, `tests/publish-atomic.test.ts` ```sh ln -sfn out/<hash> out/current.tmp && mv -Tf out/current.tmp out/current ``` 3. Keep previous outputs and implement rollback The cheapest possible fix at eleven at night. Files: `src/publish/rollback.ts` ### Done when - [ ] An interrupted publish leaves the live page untouched - [ ] Going live is a single symlink swap - [ ] Rollback restores the previous output and is recorded - [ ] Retention never removes the live directory ### Watch out - Rendering straight into the served directory means a visitor arriving during the write gets a page with half its stylesheet, and because the write takes under a second you will never reproduce it and will blame the host. Render elsewhere, verify the directory is complete, then swap. ## M3 · Responsive without a breakpoint editor One authored layout adapts by rule, and images never move the page while it loads. ### Steps 1. Fluid scales with a floor and a ceiling Two numbers per scale beats four breakpoints per element. Files: `src/render/theme.ts` 2. Store intrinsic dimensions at upload Emitted into the markup so nothing shifts as images arrive. Files: `src/assets/upload.ts`, `tests/assets.test.ts` 3. Preview three widths at once Nobody checks the phone view if checking it is a separate click. Files: `src/app/editor/preview.tsx` ### Done when - [ ] One set of rows renders three widths in the preview - [ ] Every image carries width and height - [ ] No section scrolls horizontally at 320 pixels - [ ] The page is readable with the stylesheet removed ### Watch out - Letting the editor set values per breakpoint feels like control and produces four layouts that drift apart, so the phone version is six months behind the desktop one and nobody notices because nobody previews it. Author once with rules, and let the preview show three widths side by side rather than offering three editors. ## M4 · A performance budget that blocks the publish A page that misses the stated budget cannot go live until it is fixed or the budget is changed on purpose. ### Steps 1. Measure the output bytes per type before the swap Before it is live, not in a report afterwards. Files: `src/publish/budget.ts`, `tests/budget.test.ts` 2. Run a headless measurement and store the figures Check the current published thresholds for the metrics you record. Files: `scripts/measure.mjs`, `src/publish/measure.ts` 3. Fail the publish, do not warn A warning in a build log is not a budget. Files: `src/publish/index.ts` ### Done when - [ ] An over-budget page is refused with the figure named - [ ] The output contains no framework runtime - [ ] Measured figures are stored on the publish row - [ ] Raising a ceiling is a recorded configuration change ### Watch out - Shipping the editor's own framework to the visitor is the standard way a page builder produces slow pages, because the bundle that makes the editing experience pleasant is downloaded by every visitor who will never edit anything. Publish static files with a JavaScript ceiling measured in kilobytes and let the gate fail the publish when a section kind starts importing a component library. ## M5 · Forms, a domain, and the folder you can take away (production only) Submissions land before anything else runs, the domain is yours, and the whole site exports as files. ### Steps 1. Write the submission first, respond second The lead is the product; a notification is a convenience. Files: `src/app/api/submit/route.ts`, `tests/submit.test.ts` 2. Record rejections with their reason A silent spam filter eating real submissions is undetectable. Files: `src/forms/filter.ts` 3. Export and reimport a page as a folder The check that you are not trapped in your own builder. Files: `src/export/page.ts`, `tests/roundtrip.test.ts` ### Done when - [ ] The submission is stored before any notification - [ ] Rejections are stored with a reason - [ ] An exported folder opens without a server - [ ] Reimport reproduces identical output ### Watch out - Sending the submission to an email address or a webhook from inside the request handler means a slow receiver holds the visitor's browser open and a timeout loses the lead entirely, which on a landing page is the only thing of value the page produced. Write the row first, respond, and let anything else happen afterwards and be allowed to fail. ===== OPERATIONS.md ===== # Operations · Swipe Pages ## Backup The database and the output root. The database is the source and the output is derived, so a database backup plus the code reproduces every page; keep a few output directories anyway because rollback is faster than a rebuild. ## Restore Restore the database, republish each live page and compare the output hash to the publish row. A mismatch means the renderer changed, which is worth knowing before traffic arrives. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Publish success rate, the measured figures per publish over time, and the submission rejection rate by reason. A rejection rate that jumps usually means your own filter, not a spam wave. ## Incident checklist The incident is a live page that is broken or slow. Rollback is the first move and takes one action, and the stored measurements tell you which publish introduced the regression. 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 - [ ] An interrupted publish is confirmed to leave the live page untouched - [ ] The published output is confirmed to contain no framework runtime - [ ] An over-budget page is confirmed refused with its figure named - [ ] A submission is confirmed stored before any notification runs - [ ] An exported folder is confirmed to open without a server ## Launch constraint Do not market omitted Swipe Pages capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Pages, versions, assets, publishes and submissions. DATABASE_PATH=./data/pages.db # Required. Content-addressed publish directories and the served symlink. OUTPUT_ROOT=./out # Required. How many previous publishes to retain for rollback. PUBLISH_KEEP=10 # Required. Zero is a reasonable starting value for a static landing page. BUDGET_JS_BYTES=0 # Required. Measured on the rendered output, not on source files. BUDGET_CSS_BYTES=30000 # Required. Check the current published threshold before setting this. BUDGET_LCP_MS=2500 # Required. Check the current published threshold before setting this. BUDGET_INP_MS=200 # Required. Per hashed address. Rejections are stored with a reason, not dropped. SUBMISSION_RATE_PER_HOUR=20
# Swipe Pages · indie build A section-based page builder whose hard problems are publishing without ever serving a half-written page, keeping the editor's own code out of what visitors download, and refusing to publish a page that misses a stated performance budget. 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 with TypeScript for the editor only, never for the published output | the editor is an application and the landing page is a document, and mixing them is how these get slow | | Output | Static HTML, CSS and images written to a directory, no client framework | a page you can open from the filesystem is a page any host will serve | | Storage | SQLite holding sections, page versions and publish records | the page is data and the HTML is derived, so a bad publish is one row away from being undone | | Publish | Write to a new content-addressed directory, then swap a symlink | a publish that half lands is a broken page in front of paid traffic | | Budget | A measured budget check that blocks the publish, not a lint warning | a warning nobody reads is not a budget | ## 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 publish pipeline. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **A headless browser for the publish measurement** · free - Why: The budget gate needs a real measurement rather than an estimate, and this is the part people skip and then wonder why the page is slow. - Get it: Install a headless browser your measurement script can drive. - Verify: The measurement script prints paint and interaction figures for a sample page - [ ] **Somewhere to serve a directory of static files** · free to a few pounds a month - Why: The published output is plain files, so this can be a small server, object storage or a static host, and picking it first stops the output drifting towards one vendor's conventions. - Get it: Any host that serves a directory over HTTPS. - Verify: An exported folder serves correctly from it with no build step - [ ] **A domain you control the DNS for** (optional) · around ten pounds a year - Why: Custom domain verification and certificate issuance are the two steps that turn this from a demo into something you can point traffic at. - Get it: Any registrar; you need to be able to add a TXT and an A or CNAME record. - Verify: A TXT record you add is visible from a public resolver - [ ] **The byte and metric ceilings the gate enforces** · free - Why: Chosen before the first section is written, these constrain the design; chosen afterwards they get raised until they pass. - Get it: Write down the HTML, CSS and JavaScript byte ceilings, the third-party origin count, and the paint and interaction figures you will hold to. - Verify: The numbers appear in the configuration file and in the README ## Quick start ```sh ln -sfn out/<hash> out/current.tmp && mv -Tf out/current.tmp out/current ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Arbitrary visual positioning and a template library. - Split testing with a significance verdict. - Managed hosting with traffic allowances. - A visitor analytics script. - large template and component libraries - CMS collaboration and client handoff - managed hosting, forms, and analytics - general-purpose visual editing If one of those is essential to you, that is the reason to keep paying for Swipe Pages, and the README should say so rather than pretend.
# Build brief · Swipe Pages 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 builder that edits a small set of section types and publishes plain static files you own to replace Swipe Pages. 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 with TypeScript for the editor only, never for the published output · the editor is an application and the landing page is a document, and mixing them is how these get slow - Output: Static HTML, CSS and images written to a directory, no client framework · a page you can open from the filesystem is a page any host will serve - Storage: SQLite holding sections, page versions and publish records · the page is data and the HTML is derived, so a bad publish is one row away from being undone - Publish: Write to a new content-addressed directory, then swap a symlink · a publish that half lands is a broken page in front of paid traffic - Budget: A measured budget check that blocks the publish, not a lint warning · a warning nobody reads is not a budget ### Data model (create this before Phase 1) - pages(id, slug, title, created_at, archived_at) - page_versions(id, page_id, version, sections_json, theme_json, created_at, published_at) · immutable once published - sections(version_id, ordinal, kind, props_json) · kind is one of a closed list, and an unknown kind fails validation rather than rendering empty - assets(id, page_id, path, sha256, width, height, bytes, mime) · dimensions are stored because the markup needs them - publishes(id, version_id, output_dir, started_at, finished_at, state, bytes_html, bytes_css, bytes_js, lcp_ms, inp_ms, rolled_back_from) - submissions(id, page_id, version_id, received_at, fields_json, source_ref, spam_reason) · written before anything else runs - domains(id, page_id, hostname, verified_at, cert_state) - Every published page maps to exactly one immutable version row, so rollback is pointing the symlink at the previous output directory rather than regenerating anything. ### Phase 1 · A page that is data Build: Define six or seven section kinds and no more: hero, feature list, testimonial slot, pricing table, FAQ, form and footer. Each has a typed props schema. The editor writes rows, and a pure function turns a version row into HTML and CSS with no runtime behind it. Trap: An open-ended section that accepts arbitrary HTML turns the builder into a text editor with extra steps, and the first time a client pastes an embed the page ships a third-party script you never budgeted for and cannot measure. Keep the list closed, validate props against a schema, and make adding a kind a code change. Done when: A version with an unknown section kind fails validation and cannot be saved, tested directly. Rendering a version twice from the same row produces byte-identical HTML, asserted in the suite. No section kind accepts raw HTML or a script tag, verified by review and by a test that attempts it. The renderer is a pure function of a version row with no database access inside it. Do not build yet: No publishing, responsiveness work, budget or forms yet. ### Phase 2 · Publishing that cannot half happen Build: Render a version into a new directory named by the hash of its output. Write every file, fsync, then point the served symlink at it in one operation. Keep the previous directories with a retention. Rollback repoints the symlink and records which publish it came from. Trap: Rendering straight into the served directory means a visitor arriving during the write gets a page with half its stylesheet, and because the write takes under a second you will never reproduce it and will blame the host. Render elsewhere, verify the directory is complete, then swap. Done when: A publish interrupted midway leaves the previously served directory untouched and live, tested by killing the process during a render. The served path is a symlink and switching it is the only step that makes a publish live, verified by inspection. Rollback restores the previous output in one action and records the publish it rolled back from. Output directories older than the configured retention are removed and the currently served one is never removed, tested at the boundary. Do not build yet: No responsive rules, budget gate, forms or domains yet. ### Phase 3 · Responsive without a breakpoint editor Build: Use fluid type and spacing scales clamped between a floor and a ceiling, a single column stack below a stated width, and grids that wrap rather than shrink. Store every asset's intrinsic width and height at upload and emit them in the markup along with a srcset. Trap: Letting the editor set values per breakpoint feels like control and produces four layouts that drift apart, so the phone version is six months behind the desktop one and nobody notices because nobody previews it. Author once with rules, and let the preview show three widths side by side rather than offering three editors. Done when: The preview shows the page at a phone, tablet and desktop width simultaneously from one set of section rows, checked by review. Every image tag carries width and height attributes taken from the stored asset, asserted in the suite. No horizontal scrollbar appears at 320 pixels wide on any section kind, tested per kind. Removing the stylesheet leaves the page readable in document order, which is the accessibility floor for a static page. Do not build yet: No budget gate, forms or domains yet. ### Phase 4 · A performance budget that blocks the publish Build: Measure the rendered output before the swap: total HTML, CSS and JavaScript bytes, the number of third-party origins, whether a font blocks rendering, and a headless run reporting largest contentful paint and interaction to next paint. Compare against configured ceilings and refuse the publish with the failing figure named. Trap: Shipping the editor's own framework to the visitor is the standard way a page builder produces slow pages, because the bundle that makes the editing experience pleasant is downloaded by every visitor who will never edit anything. Publish static files with a JavaScript ceiling measured in kilobytes and let the gate fail the publish when a section kind starts importing a component library. Done when: A page whose output exceeds any configured byte ceiling is refused at publish with the measured figure and the ceiling both shown, tested with a deliberately bloated section. The published output contains zero framework runtime, asserted by scanning the emitted files. A headless measurement runs as part of publish and its figures are stored on the publish row. Raising a ceiling requires editing the configuration and is recorded in the publish record, so a regression is traceable to a decision. Do not build yet: No forms, domains or export yet. ### Phase 5 · Forms, a domain, and the folder you can take away Build: Post the form to your own endpoint, write the submission in one transaction, then respond. Rate limit per address with a hashed key and add a timed honeypot, recording why anything was rejected. Verify a custom domain by a DNS record and document the certificate step. Export any page as a folder of files plus its version JSON, and import that folder back. Trap: Sending the submission to an email address or a webhook from inside the request handler means a slow receiver holds the visitor's browser open and a timeout loses the lead entirely, which on a landing page is the only thing of value the page produced. Write the row first, respond, and let anything else happen afterwards and be allowed to fail. Done when: A submission is stored before any notification is attempted, tested by making the notifier throw. A rejected submission records its reason and is retrievable, so a misconfigured filter is discoverable rather than invisible. Exporting a page produces a folder that opens correctly from the filesystem with no server, tested. Importing that folder into an empty database reproduces the same rendered output, compared by hash. Do not build yet: Nothing after this. ### Out of scope (and why) - A general-purpose visual editor with arbitrary positioning, which is a different and much larger product and is the reason the paid tools have template libraries to hide behind. - Split testing with a significance verdict, which needs traffic volumes a single page rarely has and a statistics decision you should make deliberately rather than inherit. - Managed hosting with traffic allowances, since the published output is static files and the whole point is that any host will take them. - A visitor analytics script, which is another origin, another consent question and the largest single item in most landing page budgets. ### README must contain - That the published output is static files with no framework runtime, and how to serve them anywhere - That going live is an atomic symlink swap and how to roll back to the previous publish - The exact byte and metric ceilings the publish gate enforces, and how to change one deliberately - That section kinds are a closed list and adding one is a code change, with the reason - That form submissions are written before any notification is attempted, and where rejected ones go
# Agent instructions · Swipe Pages indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js 15 with TypeScript for the editor only, never for the published output, Static HTML, CSS and images written to a directory, no client framework, SQLite holding sections, page versions and publish records, Write to a new content-addressed directory, then swap a symlink, A measured budget check that blocks the publish, not a lint warning. 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 - An open-ended section that accepts arbitrary HTML turns the builder into a text editor with extra steps, and the first time a client pastes an embed the page ships a third-party script you never budgeted for and cannot measure. Keep the list closed, validate props against a schema, and make adding a kind a code change. - Rendering straight into the served directory means a visitor arriving during the write gets a page with half its stylesheet, and because the write takes under a second you will never reproduce it and will blame the host. Render elsewhere, verify the directory is complete, then swap. - Letting the editor set values per breakpoint feels like control and produces four layouts that drift apart, so the phone version is six months behind the desktop one and nobody notices because nobody previews it. Author once with rules, and let the preview show three widths side by side rather than offering three editors. - Shipping the editor's own framework to the visitor is the standard way a page builder produces slow pages, because the bundle that makes the editing experience pleasant is downloaded by every visitor who will never edit anything. Publish static files with a JavaScript ceiling measured in kilobytes and let the gate fail the publish when a section kind starts importing a component library.
# Build plan · Swipe Pages A section-based page builder whose hard problems are publishing without ever serving a half-written page, keeping the editor's own code out of what visitors download, and refusing to publish a page that misses a stated performance budget. 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 Sections are typed rows validated against a closed list, and the HTML is derived from them. ### Steps 1. Close the section list and type the props Every open field is an unbudgeted script waiting to happen. Files: `src/sections/schema.ts`, `src/sections/index.ts` 2. Make rendering a pure function of a version row Deterministic output is what makes rollback and hashing meaningful. Files: `src/render/page.ts`, `tests/render.test.ts` 3. Refuse raw HTML anywhere in props Including in a field that looks harmless, like a footer note. Files: `src/sections/schema.ts`, `tests/no-raw-html.test.ts` ### Done when - [ ] An unknown section kind fails validation - [ ] Rendering twice is byte-identical - [ ] No prop accepts raw HTML or scripts - [ ] The renderer touches no database ### Watch out - An open-ended section that accepts arbitrary HTML turns the builder into a text editor with extra steps, and the first time a client pastes an embed the page ships a third-party script you never budgeted for and cannot measure. Keep the list closed, validate props against a schema, and make adding a kind a code change. ## Phase 2 · Publishing that cannot half happen A publish writes a complete output directory and becomes live in one atomic step, with rollback. ### Steps 1. Render to a content-addressed directory The hash of the output is the name, so identical input publishes nothing new. Files: `src/publish/render.ts` 2. Swap a symlink as the only go-live step One syscall, not a copy loop through a live directory. Files: `src/publish/swap.ts`, `tests/publish-atomic.test.ts` ```sh ln -sfn out/<hash> out/current.tmp && mv -Tf out/current.tmp out/current ``` 3. Keep previous outputs and implement rollback The cheapest possible fix at eleven at night. Files: `src/publish/rollback.ts` ### Done when - [ ] An interrupted publish leaves the live page untouched - [ ] Going live is a single symlink swap - [ ] Rollback restores the previous output and is recorded - [ ] Retention never removes the live directory ### Watch out - Rendering straight into the served directory means a visitor arriving during the write gets a page with half its stylesheet, and because the write takes under a second you will never reproduce it and will blame the host. Render elsewhere, verify the directory is complete, then swap. ## Phase 3 · Responsive without a breakpoint editor One authored layout adapts by rule, and images never move the page while it loads. ### Steps 1. Fluid scales with a floor and a ceiling Two numbers per scale beats four breakpoints per element. Files: `src/render/theme.ts` 2. Store intrinsic dimensions at upload Emitted into the markup so nothing shifts as images arrive. Files: `src/assets/upload.ts`, `tests/assets.test.ts` 3. Preview three widths at once Nobody checks the phone view if checking it is a separate click. Files: `src/app/editor/preview.tsx` ### Done when - [ ] One set of rows renders three widths in the preview - [ ] Every image carries width and height - [ ] No section scrolls horizontally at 320 pixels - [ ] The page is readable with the stylesheet removed ### Watch out - Letting the editor set values per breakpoint feels like control and produces four layouts that drift apart, so the phone version is six months behind the desktop one and nobody notices because nobody previews it. Author once with rules, and let the preview show three widths side by side rather than offering three editors. ## Phase 4 · A performance budget that blocks the publish A page that misses the stated budget cannot go live until it is fixed or the budget is changed on purpose. ### Steps 1. Measure the output bytes per type before the swap Before it is live, not in a report afterwards. Files: `src/publish/budget.ts`, `tests/budget.test.ts` 2. Run a headless measurement and store the figures Check the current published thresholds for the metrics you record. Files: `scripts/measure.mjs`, `src/publish/measure.ts` 3. Fail the publish, do not warn A warning in a build log is not a budget. Files: `src/publish/index.ts` ### Done when - [ ] An over-budget page is refused with the figure named - [ ] The output contains no framework runtime - [ ] Measured figures are stored on the publish row - [ ] Raising a ceiling is a recorded configuration change ### Watch out - Shipping the editor's own framework to the visitor is the standard way a page builder produces slow pages, because the bundle that makes the editing experience pleasant is downloaded by every visitor who will never edit anything. Publish static files with a JavaScript ceiling measured in kilobytes and let the gate fail the publish when a section kind starts importing a component library. ## Not in this build - Arbitrary visual positioning and a template library. - Split testing with a significance verdict. - Managed hosting with traffic allowances. - A visitor analytics script. ## After v1, if you want it - Field-measured metrics collected from real visits rather than a headless run, once a page has traffic - A second symlink for a staging hostname pointing at the next publish, so a page can be reviewed live before the swap - A per-section budget attribution, so the gate can say which section added the bytes
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Pages, versions, assets, publishes and submissions. DATABASE_PATH=./data/pages.db # Required. Content-addressed publish directories and the served symlink. OUTPUT_ROOT=./out # Required. How many previous publishes to retain for rollback. PUBLISH_KEEP=10 # Required. Zero is a reasonable starting value for a static landing page. BUDGET_JS_BYTES=0 # Required. Measured on the rendered output, not on source files. BUDGET_CSS_BYTES=30000 # Required. Check the current published threshold before setting this. BUDGET_LCP_MS=2500 # Required. Check the current published threshold before setting this. BUDGET_INP_MS=200 # Required. Per hashed address. Rejections are stored with a reason, not dropped. SUBMISSION_RATE_PER_HOUR=20
# Swipe Pages · product brief ## Problem The visible landing-page builder loop is buildable, but a credible replacement needs more than the first screen. Swipe Pages earns its keep through editor, CMS, hosting, polish, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A builder whose output is a folder of static files with no framework in it, which goes live in one atomic step, rolls back in one action, and refuses to publish a page that misses the performance budget you wrote down first. ## 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 - Arbitrary visual positioning and a template library. - Split testing with a significance verdict. - Managed hosting with traffic allowances. - A visitor analytics script. - large template and component libraries - CMS collaboration and client handoff - managed hosting, forms, and analytics - general-purpose visual editing ## Success criteria - An interrupted publish is confirmed to leave the live page untouched - The published output is confirmed to contain no framework runtime - An over-budget page is confirmed refused with its figure named - A submission is confirmed stored before any notification runs - An exported folder is confirmed to open without a server
# Build brief · Swipe Pages 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 builder that edits a small set of section types and publishes plain static files you own to replace Swipe Pages. 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 with TypeScript for the editor only, never for the published output · the editor is an application and the landing page is a document, and mixing them is how these get slow - Output: Static HTML, CSS and images written to a directory, no client framework · a page you can open from the filesystem is a page any host will serve - Storage: SQLite holding sections, page versions and publish records · the page is data and the HTML is derived, so a bad publish is one row away from being undone - Publish: Write to a new content-addressed directory, then swap a symlink · a publish that half lands is a broken page in front of paid traffic - Budget: A measured budget check that blocks the publish, not a lint warning · a warning nobody reads is not a budget ### Data model (create this before Phase 1) - pages(id, slug, title, created_at, archived_at) - page_versions(id, page_id, version, sections_json, theme_json, created_at, published_at) · immutable once published - sections(version_id, ordinal, kind, props_json) · kind is one of a closed list, and an unknown kind fails validation rather than rendering empty - assets(id, page_id, path, sha256, width, height, bytes, mime) · dimensions are stored because the markup needs them - publishes(id, version_id, output_dir, started_at, finished_at, state, bytes_html, bytes_css, bytes_js, lcp_ms, inp_ms, rolled_back_from) - submissions(id, page_id, version_id, received_at, fields_json, source_ref, spam_reason) · written before anything else runs - domains(id, page_id, hostname, verified_at, cert_state) - Every published page maps to exactly one immutable version row, so rollback is pointing the symlink at the previous output directory rather than regenerating anything. ### Phase 1 · A page that is data Build: Define six or seven section kinds and no more: hero, feature list, testimonial slot, pricing table, FAQ, form and footer. Each has a typed props schema. The editor writes rows, and a pure function turns a version row into HTML and CSS with no runtime behind it. Trap: An open-ended section that accepts arbitrary HTML turns the builder into a text editor with extra steps, and the first time a client pastes an embed the page ships a third-party script you never budgeted for and cannot measure. Keep the list closed, validate props against a schema, and make adding a kind a code change. Done when: A version with an unknown section kind fails validation and cannot be saved, tested directly. Rendering a version twice from the same row produces byte-identical HTML, asserted in the suite. No section kind accepts raw HTML or a script tag, verified by review and by a test that attempts it. The renderer is a pure function of a version row with no database access inside it. Do not build yet: No publishing, responsiveness work, budget or forms yet. ### Phase 2 · Publishing that cannot half happen Build: Render a version into a new directory named by the hash of its output. Write every file, fsync, then point the served symlink at it in one operation. Keep the previous directories with a retention. Rollback repoints the symlink and records which publish it came from. Trap: Rendering straight into the served directory means a visitor arriving during the write gets a page with half its stylesheet, and because the write takes under a second you will never reproduce it and will blame the host. Render elsewhere, verify the directory is complete, then swap. Done when: A publish interrupted midway leaves the previously served directory untouched and live, tested by killing the process during a render. The served path is a symlink and switching it is the only step that makes a publish live, verified by inspection. Rollback restores the previous output in one action and records the publish it rolled back from. Output directories older than the configured retention are removed and the currently served one is never removed, tested at the boundary. Do not build yet: No responsive rules, budget gate, forms or domains yet. ### Phase 3 · Responsive without a breakpoint editor Build: Use fluid type and spacing scales clamped between a floor and a ceiling, a single column stack below a stated width, and grids that wrap rather than shrink. Store every asset's intrinsic width and height at upload and emit them in the markup along with a srcset. Trap: Letting the editor set values per breakpoint feels like control and produces four layouts that drift apart, so the phone version is six months behind the desktop one and nobody notices because nobody previews it. Author once with rules, and let the preview show three widths side by side rather than offering three editors. Done when: The preview shows the page at a phone, tablet and desktop width simultaneously from one set of section rows, checked by review. Every image tag carries width and height attributes taken from the stored asset, asserted in the suite. No horizontal scrollbar appears at 320 pixels wide on any section kind, tested per kind. Removing the stylesheet leaves the page readable in document order, which is the accessibility floor for a static page. Do not build yet: No budget gate, forms or domains yet. ### Phase 4 · A performance budget that blocks the publish Build: Measure the rendered output before the swap: total HTML, CSS and JavaScript bytes, the number of third-party origins, whether a font blocks rendering, and a headless run reporting largest contentful paint and interaction to next paint. Compare against configured ceilings and refuse the publish with the failing figure named. Trap: Shipping the editor's own framework to the visitor is the standard way a page builder produces slow pages, because the bundle that makes the editing experience pleasant is downloaded by every visitor who will never edit anything. Publish static files with a JavaScript ceiling measured in kilobytes and let the gate fail the publish when a section kind starts importing a component library. Done when: A page whose output exceeds any configured byte ceiling is refused at publish with the measured figure and the ceiling both shown, tested with a deliberately bloated section. The published output contains zero framework runtime, asserted by scanning the emitted files. A headless measurement runs as part of publish and its figures are stored on the publish row. Raising a ceiling requires editing the configuration and is recorded in the publish record, so a regression is traceable to a decision. Do not build yet: No forms, domains or export yet. ### Phase 5 · Forms, a domain, and the folder you can take away Build: Post the form to your own endpoint, write the submission in one transaction, then respond. Rate limit per address with a hashed key and add a timed honeypot, recording why anything was rejected. Verify a custom domain by a DNS record and document the certificate step. Export any page as a folder of files plus its version JSON, and import that folder back. Trap: Sending the submission to an email address or a webhook from inside the request handler means a slow receiver holds the visitor's browser open and a timeout loses the lead entirely, which on a landing page is the only thing of value the page produced. Write the row first, respond, and let anything else happen afterwards and be allowed to fail. Done when: A submission is stored before any notification is attempted, tested by making the notifier throw. A rejected submission records its reason and is retrievable, so a misconfigured filter is discoverable rather than invisible. Exporting a page produces a folder that opens correctly from the filesystem with no server, tested. Importing that folder into an empty database reproduces the same rendered output, compared by hash. Do not build yet: Nothing after this. ### Out of scope (and why) - A general-purpose visual editor with arbitrary positioning, which is a different and much larger product and is the reason the paid tools have template libraries to hide behind. - Split testing with a significance verdict, which needs traffic volumes a single page rarely has and a statistics decision you should make deliberately rather than inherit. - Managed hosting with traffic allowances, since the published output is static files and the whole point is that any host will take them. - A visitor analytics script, which is another origin, another consent question and the largest single item in most landing page budgets. ### README must contain - That the published output is static files with no framework runtime, and how to serve them anywhere - That going live is an atomic symlink swap and how to roll back to the previous publish - The exact byte and metric ceilings the publish gate enforces, and how to change one deliberately - That section kinds are a closed list and adding one is a code change, with the reason - That form submissions are written before any notification is attempted, and where rejected ones go
# Architecture · Swipe Pages ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js 15 with TypeScript for the editor only, never for the published output | the editor is an application and the landing page is a document, and mixing them is how these get slow | | Output | Static HTML, CSS and images written to a directory, no client framework | a page you can open from the filesystem is a page any host will serve | | Storage | SQLite holding sections, page versions and publish records | the page is data and the HTML is derived, so a bad publish is one row away from being undone | | Publish | Write to a new content-addressed directory, then swap a symlink | a publish that half lands is a broken page in front of paid traffic | | Budget | A measured budget check that blocks the publish, not a lint warning | a warning nobody reads is not a budget | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Sections | The closed kind list and prop validation. | Add kinds in code; never open the list to raw HTML. | | Render | The pure version-to-files function. | Any renderer whose output is deterministic for a given row. | | Publish | Content-addressed directories, the swap and rollback. | Any mechanism that is atomic at the point of going live. | | Budget | Measurement and the publish refusal. | Any measurement tool, provided it blocks rather than warns. | | Forms | Submission storage, filtering and rejection records. | Any notifier afterwards, provided the row is written first. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · Pages, versions, assets, publishes and submissions. - `OUTPUT_ROOT` · required · Content-addressed publish directories and the served symlink. - `PUBLISH_KEEP` · required · How many previous publishes to retain for rollback. - `BUDGET_JS_BYTES` · required · Zero is a reasonable starting value for a static landing page. - `BUDGET_CSS_BYTES` · required · Measured on the rendered output, not on source files. - `BUDGET_LCP_MS` · required · Check the current published threshold before setting this. - `BUDGET_INP_MS` · required · Check the current published threshold before setting this. - `SUBMISSION_RATE_PER_HOUR` · required · Per hashed address. Rejections are stored with a reason, not dropped. ## 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 · Swipe Pages product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Next.js 15 with TypeScript for the editor only, never for the published output, Static HTML, CSS and images written to a directory, no client framework, SQLite holding sections, page versions and publish records, Write to a new content-addressed directory, then swap a symlink, A measured budget check that blocks the publish, not a lint warning. - 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 - An open-ended section that accepts arbitrary HTML turns the builder into a text editor with extra steps, and the first time a client pastes an embed the page ships a third-party script you never budgeted for and cannot measure. Keep the list closed, validate props against a schema, and make adding a kind a code change. - Rendering straight into the served directory means a visitor arriving during the write gets a page with half its stylesheet, and because the write takes under a second you will never reproduce it and will blame the host. Render elsewhere, verify the directory is complete, then swap. - Letting the editor set values per breakpoint feels like control and produces four layouts that drift apart, so the phone version is six months behind the desktop one and nobody notices because nobody previews it. Author once with rules, and let the preview show three widths side by side rather than offering three editors. - Shipping the editor's own framework to the visitor is the standard way a page builder produces slow pages, because the bundle that makes the editing experience pleasant is downloaded by every visitor who will never edit anything. Publish static files with a JavaScript ceiling measured in kilobytes and let the gate fail the publish when a section kind starts importing a component library. - Sending the submission to an email address or a webhook from inside the request handler means a slow receiver holds the visitor's browser open and a timeout loses the lead entirely, which on a landing page is the only thing of value the page produced. Write the row first, respond, and let anything else happen afterwards and be allowed to fail.
# Delivery milestones · Swipe Pages Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A page that is data Sections are typed rows validated against a closed list, and the HTML is derived from them. ### Steps 1. Close the section list and type the props Every open field is an unbudgeted script waiting to happen. Files: `src/sections/schema.ts`, `src/sections/index.ts` 2. Make rendering a pure function of a version row Deterministic output is what makes rollback and hashing meaningful. Files: `src/render/page.ts`, `tests/render.test.ts` 3. Refuse raw HTML anywhere in props Including in a field that looks harmless, like a footer note. Files: `src/sections/schema.ts`, `tests/no-raw-html.test.ts` ### Done when - [ ] An unknown section kind fails validation - [ ] Rendering twice is byte-identical - [ ] No prop accepts raw HTML or scripts - [ ] The renderer touches no database ### Watch out - An open-ended section that accepts arbitrary HTML turns the builder into a text editor with extra steps, and the first time a client pastes an embed the page ships a third-party script you never budgeted for and cannot measure. Keep the list closed, validate props against a schema, and make adding a kind a code change. ## M2 · Publishing that cannot half happen A publish writes a complete output directory and becomes live in one atomic step, with rollback. ### Steps 1. Render to a content-addressed directory The hash of the output is the name, so identical input publishes nothing new. Files: `src/publish/render.ts` 2. Swap a symlink as the only go-live step One syscall, not a copy loop through a live directory. Files: `src/publish/swap.ts`, `tests/publish-atomic.test.ts` ```sh ln -sfn out/<hash> out/current.tmp && mv -Tf out/current.tmp out/current ``` 3. Keep previous outputs and implement rollback The cheapest possible fix at eleven at night. Files: `src/publish/rollback.ts` ### Done when - [ ] An interrupted publish leaves the live page untouched - [ ] Going live is a single symlink swap - [ ] Rollback restores the previous output and is recorded - [ ] Retention never removes the live directory ### Watch out - Rendering straight into the served directory means a visitor arriving during the write gets a page with half its stylesheet, and because the write takes under a second you will never reproduce it and will blame the host. Render elsewhere, verify the directory is complete, then swap. ## M3 · Responsive without a breakpoint editor One authored layout adapts by rule, and images never move the page while it loads. ### Steps 1. Fluid scales with a floor and a ceiling Two numbers per scale beats four breakpoints per element. Files: `src/render/theme.ts` 2. Store intrinsic dimensions at upload Emitted into the markup so nothing shifts as images arrive. Files: `src/assets/upload.ts`, `tests/assets.test.ts` 3. Preview three widths at once Nobody checks the phone view if checking it is a separate click. Files: `src/app/editor/preview.tsx` ### Done when - [ ] One set of rows renders three widths in the preview - [ ] Every image carries width and height - [ ] No section scrolls horizontally at 320 pixels - [ ] The page is readable with the stylesheet removed ### Watch out - Letting the editor set values per breakpoint feels like control and produces four layouts that drift apart, so the phone version is six months behind the desktop one and nobody notices because nobody previews it. Author once with rules, and let the preview show three widths side by side rather than offering three editors. ## M4 · A performance budget that blocks the publish A page that misses the stated budget cannot go live until it is fixed or the budget is changed on purpose. ### Steps 1. Measure the output bytes per type before the swap Before it is live, not in a report afterwards. Files: `src/publish/budget.ts`, `tests/budget.test.ts` 2. Run a headless measurement and store the figures Check the current published thresholds for the metrics you record. Files: `scripts/measure.mjs`, `src/publish/measure.ts` 3. Fail the publish, do not warn A warning in a build log is not a budget. Files: `src/publish/index.ts` ### Done when - [ ] An over-budget page is refused with the figure named - [ ] The output contains no framework runtime - [ ] Measured figures are stored on the publish row - [ ] Raising a ceiling is a recorded configuration change ### Watch out - Shipping the editor's own framework to the visitor is the standard way a page builder produces slow pages, because the bundle that makes the editing experience pleasant is downloaded by every visitor who will never edit anything. Publish static files with a JavaScript ceiling measured in kilobytes and let the gate fail the publish when a section kind starts importing a component library. ## M5 · Forms, a domain, and the folder you can take away (production only) Submissions land before anything else runs, the domain is yours, and the whole site exports as files. ### Steps 1. Write the submission first, respond second The lead is the product; a notification is a convenience. Files: `src/app/api/submit/route.ts`, `tests/submit.test.ts` 2. Record rejections with their reason A silent spam filter eating real submissions is undetectable. Files: `src/forms/filter.ts` 3. Export and reimport a page as a folder The check that you are not trapped in your own builder. Files: `src/export/page.ts`, `tests/roundtrip.test.ts` ### Done when - [ ] The submission is stored before any notification - [ ] Rejections are stored with a reason - [ ] An exported folder opens without a server - [ ] Reimport reproduces identical output ### Watch out - Sending the submission to an email address or a webhook from inside the request handler means a slow receiver holds the visitor's browser open and a timeout loses the lead entirely, which on a landing page is the only thing of value the page produced. Write the row first, respond, and let anything else happen afterwards and be allowed to fail.
# Operations · Swipe Pages ## Backup The database and the output root. The database is the source and the output is derived, so a database backup plus the code reproduces every page; keep a few output directories anyway because rollback is faster than a rebuild. ## Restore Restore the database, republish each live page and compare the output hash to the publish row. A mismatch means the renderer changed, which is worth knowing before traffic arrives. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Publish success rate, the measured figures per publish over time, and the submission rejection rate by reason. A rejection rate that jumps usually means your own filter, not a spam wave. ## Incident checklist The incident is a live page that is broken or slow. Rollback is the first move and takes one action, and the stored measurements tell you which publish introduced the regression. 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 - [ ] An interrupted publish is confirmed to leave the live page untouched - [ ] The published output is confirmed to contain no framework runtime - [ ] An over-budget page is confirmed refused with its figure named - [ ] A submission is confirmed stored before any notification runs - [ ] An exported folder is confirmed to open without a server ## Launch constraint Do not market omitted Swipe Pages capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Pages, versions, assets, publishes and submissions. DATABASE_PATH=./data/pages.db # Required. Content-addressed publish directories and the served symlink. OUTPUT_ROOT=./out # Required. How many previous publishes to retain for rollback. PUBLISH_KEEP=10 # Required. Zero is a reasonable starting value for a static landing page. BUDGET_JS_BYTES=0 # Required. Measured on the rendered output, not on source files. BUDGET_CSS_BYTES=30000 # Required. Check the current published threshold before setting this. BUDGET_LCP_MS=2500 # Required. Check the current published threshold before setting this. BUDGET_INP_MS=200 # Required. Per hashed address. Rejections are stored with a reason, not dropped. SUBMISSION_RATE_PER_HOUR=20
$ choose a build depth, inspect the files, then open the complete pack in your agent
Swipe Pages: A single site is easy to code. People pay for safe ongoing edits, templates, publishing, domains, collaboration, and no-maintenance hosting.
xlarge template and component libraries
xCMS collaboration and client handoff
xmanaged hosting, forms, and analytics
xgeneral-purpose visual editing
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Swipe Pages pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| startup | $39/workspace | $29/workspace | 1 custom domain, 20,000 visits/month, unlimited landing pages, funnels and conversions |
| marketer | $89/workspace | $69/workspace | 5 custom domains, 50,000 visits/month, 5 client subaccounts, 5 team members, unlimited landing pages and funnels |
| agency | $199/workspace | $149/workspace | Unlimited custom domains, 500,000 visits/month, unlimited client subaccounts, team members, landing pages and funnels |
| enterprise | custom | — | Custom traffic, account and support limits |
free tierno free tier; 14-day full-featured trial with no credit card
billingmonthly + annual; annual plans are billed upfront
hidden costs$5 for each additional 5,000 visits above the monthly traffic cap, billed in 5,000-visit blocks.
verified 2026-08-14 · source ↗
Vibecode Swipe Pages
Kinda. The core of Swipe Pages is buildable in a weekend with the prompt on this page, but there are real gaps: large template and component libraries, CMS collaboration and client handoff. Read the honest list above before committing.
How much does Swipe Pages cost?
Swipe Pages'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 Swipe Pages?
Honestly: large template and component libraries; CMS collaboration and client handoff; managed hosting, forms, and analytics; general-purpose visual editing. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Swipe Pages?
Yes: Mautic (Responsive pages, forms, campaign analytics and A/B testing; self-hosting is the least mobile-friendly part.) systeme.io (Mobile-ready landing pages, forms and one split test free; advanced integrations and agency polish are thinner.) The prompt is for when you want it exactly your way.