Vibecode Instapage
track this build4 phases, 12 steps, beginner friendly0%The visible enterprise landing pages loop is buildable, but a credible replacement needs more than the first screen. Instapage 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 Instapage. 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 ===== # Instapage · indie build A landing page builder whose hard problems are publishing static files without a broken minute, measuring speed from real visits rather than a lab score, and running split tests that are decided in advance instead of stopped when they look good. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Content | Pages as ordered sections in SQLite, rendered to static HTML at publish | a page that needs a server at request time is a page that can be slow at request time | | Rendering | One template per section kind, server-side, no client framework on the published page | the published page should ship almost no JavaScript because that is the whole advantage | | Measurement | First-party field collection from the published page using the standard web vitals library | a new page has no field data anywhere else, and lab numbers cannot produce two of the three metrics | | Experiments | Deterministic bucketing from a first-party visitor identifier, decided sample size | sticky assignment and a stopping rule are what make the result mean anything | | Publishing | Build into a new directory and swap, keeping previous versions | a half-uploaded page is a broken page on the only URL that matters | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the editor and the build. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **A small server and a domain you control** · a few pounds a month plus the domain - Why: The published pages need a stable address with a certificate, and the measurement endpoint has to be first-party to avoid being blocked. - Get it: Any small virtual server plus a domain, with automatic certificates. - Verify: The page loads over your domain with a valid certificate and a measurement reaches the endpoint - [ ] **The standard web vitals measurement library** · free - Why: Computing these metrics by hand from performance entries is subtly wrong, and the library is the reference implementation of the definitions. - Get it: Add the published library to the page bundle and report to your own endpoint. - Verify: A real page load produces values for all three metrics - [ ] **A slow device and a throttled connection** · free - Why: Every layout shift and every slow paint is invisible on a developer machine on a fast network, which is why pages ship broken on phones. - Get it: An old phone, or your browser's device and network throttling. - Verify: The page has been loaded once on a throttled slow connection and the shift measured - [ ] **Your sample size and stopping rule** · free - Why: These are the difference between an experiment and a scoreboard, and choosing them after the data arrives means choosing the answer. - Get it: Decide the minimum effect worth detecting, compute the sample size it needs, and write the stopping rule down. - Verify: Both are stored on the experiment record and shown in the report ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - A general-purpose drag and drop canvas. - Audience personalisation. - Heatmaps and session recording. - Multi-user editing and approvals. - general-purpose visual editing - large template and component libraries - CMS collaboration and client handoff - managed hosting, forms, and analytics If one of those is essential to you, that is the reason to keep paying for Instapage, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Instapage 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 publisher whose pages are static files, whose speed is measured on real visits, and whose split tests are decided before they start to replace Instapage. 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. ### Rule zero A landing page exists to be measured, and both of the measurements this category sells are easy to fake. So neither is allowed to be faked here. Speed is what your visitors experienced, not what your laptop scored. The Core Web Vitals thresholds are published: a good Largest Contentful Paint is 2.5 seconds or less, a good Interaction to Next Paint is 200 milliseconds or less, and a good Cumulative Layout Shift is 0.1 or less, each assessed at the 75th percentile of page loads and segmented between mobile and desktop. A lab tool cannot produce two of those, because nobody interacted with the page and the run stopped at load. Collect the numbers from the published page itself. A split test is a decision procedure, not a scoreboard. Fix the sample size and the stopping rule before the test starts, keep the assignment sticky per visitor, and report the interval and the count with every result. Stopping a test on the morning it looks good is how a variant with no effect gets rolled out, and this build will not have a button that does it. ### Stack (fixed, do not substitute) - Content: Pages as ordered sections in SQLite, rendered to static HTML at publish · a page that needs a server at request time is a page that can be slow at request time - Rendering: One template per section kind, server-side, no client framework on the published page · the published page should ship almost no JavaScript because that is the whole advantage - Measurement: First-party field collection from the published page using the standard web vitals library · a new page has no field data anywhere else, and lab numbers cannot produce two of the three metrics - Experiments: Deterministic bucketing from a first-party visitor identifier, decided sample size · sticky assignment and a stopping rule are what make the result mean anything - Publishing: Build into a new directory and swap, keeping previous versions · a half-uploaded page is a broken page on the only URL that matters ### Data model (create this before Phase 1) - pages(id, slug, title, status, current_version_id, created_at) - page_versions(id, page_id, ordinal, sections_json, theme_json, built_at, built_by, notes) · every publish is a version you can go back to - sections(kind, schema) · hero, features, testimonial, form, footer, each with a declared field schema - experiments(id, page_id, name, variant_a_version_id, variant_b_version_id, metric, planned_n, started_at, stopped_at, stop_reason) - assignments(visitor_id, experiment_id, variant, assigned_at) · deterministic from the identifier, written once - events(id, page_id, experiment_id, variant, kind, at, session_id) · view and conversion, nothing else - vitals(id, page_id, version_id, metric, value, rating, device, at) · one row per reported measurement from a real visit - submissions(id, page_id, version_id, fields_json, received_at, spam_score, ip_country) · form entries, kept separately from analytics - A published page is a directory of files. The database is the editor's state, not a dependency of the live page. ### Phase 1 · Sections in, static files out Build: Define each section kind with a field schema and one template. The editor is a form per section with reordering. The build writes HTML, one stylesheet, and resized images into a directory that can be served by anything. Every page version is kept. Trap: Storing the page as a blob of HTML from a rich editor means the day you change the heading style you cannot change it anywhere, because the markup is copied into every page and half of it was pasted from a document with its own inline styles. Store fields against a schema and render them, so a template change reaches every page that uses it. Done when: A page built twice from the same version produces byte-identical output, tested. Changing a section template changes every page using that section after a rebuild, tested with two pages. The built directory serves correctly from a plain static file server with no application running. Every publish creates a version row that can be rebuilt from stored fields alone. Do not build yet: No measurement, experiments, forms or domains yet. ### Phase 2 · Fast for the visitor, measured on the visit Build: Enforce a build budget: total bytes, image dimensions and the number of blocking requests. Give the largest element a preload, size every image so nothing reflows, and handle web fonts with a declared fallback plus the font metric overrides rather than hoping. Then collect Largest Contentful Paint, Interaction to Next Paint and Cumulative Layout Shift from real visits with a first-party endpoint, and show the 75th percentile split by device against the published thresholds of 2.5 seconds, 200 milliseconds and 0.1. Trap: A perfect lab score and a page that is not in any field dataset feels like proof, but a synthetic run cannot measure responsiveness because nobody clicked anything, and it only sees layout shifts above the fold during load, while a page with little traffic may never appear in a public field dataset at all because inclusion requires a minimum number of visitors that is deliberately undisclosed. Collect your own field measurements from the published page and treat the lab run as a smoke test. Done when: The build fails when total transferred bytes or the number of blocking requests passes the configured budget, tested with an oversized image. A web font swapping in does not move the text block, verified by a layout shift measurement of effectively zero on a throttled load with the metric overrides in place and a visible shift without them. Field measurements arrive from a real visit and appear at the 75th percentile split by device. Every vitals row is linked to the page version that was live when it was measured. Do not build yet: No experiments, forms or domains yet. ### Phase 3 · Publishing without a broken minute Build: Build into a new directory, verify it by requesting the page and the assets from it, then swap the served directory in one operation. Keep a stated number of previous builds and provide a one-command rollback. A build that fails its checks never becomes live. Trap: Publishing by writing files over the live directory means the page is half old and half new for the seconds it takes, and a build that dies partway leaves it broken until somebody notices, which on a campaign page means the traffic you paid for hit an unstyled page. Build elsewhere, verify, then swap, and keep the previous build to roll back to. Done when: A build killed partway leaves the live directory untouched, tested by interrupting it. The swap is a single operation and a request during the swap gets either the old page or the new one, never a mixture, tested under load. Rollback restores the previous version with one command and is exercised in the suite. A build failing a budget or a template error is never swapped in, and the failure names what went wrong. Do not build yet: No experiments, forms or domains yet. ### Phase 4 · Split tests decided in advance Build: Assign a variant by hashing a first-party visitor identifier with the experiment identifier, so assignment is deterministic and sticky without a lookup. Record the planned sample size and the metric when the experiment is created. Show progress towards that size, and report the difference with an interval and the counts. Stopping early is possible but requires a written reason stored on the experiment. Trap: Watching the dashboard and stopping the moment one variant looks ahead turns a coin-flip difference into a decision, because checking repeatedly gives you many chances to catch a random lead, and on a page with a few dozen conversions a week that lead appears constantly. Fix the sample size when you create the experiment, show progress towards it, and make any early stop record who stopped it and why. Done when: The same visitor identifier always resolves to the same variant, across restarts and without a database lookup, tested with a thousand identifiers. Assignment is balanced within a stated tolerance over ten thousand identifiers, tested. The experiment view shows progress towards the planned sample size and does not display a winner before it is reached. An early stop stores a reason and the report shows that the test was stopped early wherever the result appears. Do not build yet: No forms, domains or consent yet. ### Phase 5 · Forms, the domain, and what you keep Build: Accept form submissions to a small endpoint with size limits, a honeypot, a rate limit per address and server-side validation, and store them separately from analytics. Serve the pages over your own domain with automatic certificates. Keep the first-party measurement identifier cookie-free where you can, state a retention period for events and submissions, and implement deletion of one person's submissions on request. Trap: Putting form submissions in the same table as page events and then applying one retention rule deletes the leads along with the analytics, or keeps the analytics as long as the leads; either way somebody is unhappy and one of them is a data protection problem. Keep them apart, give each its own retention period, and write both in the README. Done when: A submission over the size limit, from a bot filling the honeypot, or beyond the rate limit is rejected with a clear response and without storing the payload, all three tested. Submissions and events have separate retention periods, both enforced by a job and both stated in the README. Deleting one person's submissions removes their fields and leaves aggregate counts intact, tested. The site serves over its own domain with a valid certificate that renews without manual work, rehearsed once. Do not build yet: Nothing after this. ### Out of scope (and why) - A general-purpose drag and drop canvas, which is a year of work and is why the templates in the paid product look consistent and yours would not. - Personalisation by audience segment, which needs visitor data you would then have to hold and justify holding. - Heatmaps and session recording, which capture more about a visitor than a landing page needs and carry their own consent question. - Multi-user editing with approvals, which turns every save into a conflict and a permission decision. ### README must contain - That published pages are static files with no runtime dependency on the editor - The Core Web Vitals thresholds used as the target, that the assessment is at the 75th percentile split by device, and that the numbers come from real visits rather than a lab run - That a new page may have no field data in any public dataset, which is why measurement is first-party - How an experiment's sample size is fixed in advance and what happens when someone stops one early - The retention period for events and for form submissions, separately, and how to delete one person's submissions ===== AGENTS.md ===== # Agent instructions · Instapage indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Pages as ordered sections in SQLite, rendered to static HTML at publish, One template per section kind, server-side, no client framework on the published page, First-party field collection from the published page using the standard web vitals library, Deterministic bucketing from a first-party visitor identifier, decided sample size, Build into a new directory and swap, keeping previous versions. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing the page as a blob of HTML from a rich editor means the day you change the heading style you cannot change it anywhere, because the markup is copied into every page and half of it was pasted from a document with its own inline styles. Store fields against a schema and render them, so a template change reaches every page that uses it. - A perfect lab score and a page that is not in any field dataset feels like proof, but a synthetic run cannot measure responsiveness because nobody clicked anything, and it only sees layout shifts above the fold during load, while a page with little traffic may never appear in a public field dataset at all because inclusion requires a minimum number of visitors that is deliberately undisclosed. Collect your own field measurements from the published page and treat the lab run as a smoke test. - Publishing by writing files over the live directory means the page is half old and half new for the seconds it takes, and a build that dies partway leaves it broken until somebody notices, which on a campaign page means the traffic you paid for hit an unstyled page. Build elsewhere, verify, then swap, and keep the previous build to roll back to. - Watching the dashboard and stopping the moment one variant looks ahead turns a coin-flip difference into a decision, because checking repeatedly gives you many chances to catch a random lead, and on a page with a few dozen conversions a week that lead appears constantly. Fix the sample size when you create the experiment, show progress towards it, and make any early stop record who stopped it and why. ===== BUILD_PLAN.md ===== # Build plan · Instapage A landing page builder whose hard problems are publishing static files without a broken minute, measuring speed from real visits rather than a lab score, and running split tests that are decided in advance instead of stopped when they look good. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Sections in, static files out A page is structured data that renders to HTML, CSS and images with no runtime. ### Steps 1. Declare a schema per section kind Fields you can validate, not markup you cannot change later. Files: `src/sections/schema.ts`, `src/sections/templates/` 2. Make the build deterministic Byte-identical output is how you know a rebuild is safe. Files: `src/build.ts`, `tests/build.test.ts` 3. Serve the output with no application That constraint is the entire performance story. Files: `scripts/serve-static.sh` ### Done when - [ ] Two builds of one version are byte-identical - [ ] A template change reaches every page using it - [ ] The output serves from a plain file server - [ ] Each publish stores a rebuildable version ### Watch out - Storing the page as a blob of HTML from a rich editor means the day you change the heading style you cannot change it anywhere, because the markup is copied into every page and half of it was pasted from a document with its own inline styles. Store fields against a schema and render them, so a template change reaches every page that uses it. ## Phase 2 · Fast for the visitor, measured on the visit The page meets the published thresholds where it counts, and the numbers come from real loads. ### Steps 1. Put a byte and request budget in the build A budget checked by a person is a budget that slips. Files: `src/budget.ts`, `tests/budget.test.ts` 2. Handle fonts with overrides and a real fallback The fallback relayout is the most common shift on a landing page. Files: `src/theme/fonts.css`, `src/build.ts` 3. Collect vitals first-party from the live page Two of the three metrics cannot come from a lab run. Files: `src/public/vitals.js`, `src/api/vitals.ts` ### Done when - [ ] The build fails on an oversized image - [ ] A font swap produces no measurable shift - [ ] Field measurements appear at the 75th percentile by device - [ ] Each measurement is tied to the live version ### Watch out - A perfect lab score and a page that is not in any field dataset feels like proof, but a synthetic run cannot measure responsiveness because nobody clicked anything, and it only sees layout shifts above the fold during load, while a page with little traffic may never appear in a public field dataset at all because inclusion requires a minimum number of visitors that is deliberately undisclosed. Collect your own field measurements from the published page and treat the lab run as a smoke test. ## Phase 3 · Publishing without a broken minute A publish is atomic and reversible, and a failed build never reaches the URL. ### Steps 1. Build to a new directory every time Never write into the directory being served. Files: `src/publish.ts` 2. Verify before swapping Request the page and its assets from the new directory first. Files: `src/publish.ts`, `tests/publish.test.ts` 3. Make rollback one command, and test it The rollback nobody has run is not a rollback. Files: `scripts/rollback.sh`, `tests/rollback.test.ts` ### Done when - [ ] An interrupted build leaves the live page untouched - [ ] The swap never serves a mixture - [ ] Rollback runs in one command and is tested - [ ] A failing build is never published ### Watch out - Publishing by writing files over the live directory means the page is half old and half new for the seconds it takes, and a build that dies partway leaves it broken until somebody notices, which on a campaign page means the traffic you paid for hit an unstyled page. Build elsewhere, verify, then swap, and keep the previous build to roll back to. ## Phase 4 · Split tests decided in advance An experiment has a sample size and a stopping rule before it starts, and the same visitor always sees the same variant. ### Steps 1. Bucket deterministically from the identifier Sticky assignment with no lookup and no cookie race. Files: `src/experiments/assign.ts`, `tests/assign.test.ts` 2. Store the planned sample size at creation Deciding it afterwards is deciding it from the data. Files: `src/experiments/create.ts` 3. Report an interval and the counts, never a bare winner And label any early stop wherever the number is shown. Files: `src/experiments/report.ts` ### Done when - [ ] One identifier always gets the same variant - [ ] Assignment is balanced over ten thousand identifiers - [ ] No winner is shown before the planned sample size - [ ] An early stop is recorded and labelled in the report ### Watch out - Watching the dashboard and stopping the moment one variant looks ahead turns a coin-flip difference into a decision, because checking repeatedly gives you many chances to catch a random lead, and on a page with a few dozen conversions a week that lead appears constantly. Fix the sample size when you create the experiment, show progress towards it, and make any early stop record who stopped it and why. ## Not in this build - A general-purpose drag and drop canvas. - Audience personalisation. - Heatmaps and session recording. - Multi-user editing and approvals. ## After v1, if you want it - A second metric per experiment, recorded but never used to declare a winner - A content import from an existing page, mapping its blocks onto your section schemas - A scheduled publish, which is the same swap with a timer and a check that the build still passes ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Editor state, versions, experiments and measurements. Not needed to serve a published page. DATABASE_PATH=./data/pages.db # Required. One directory per build. The live directory is a pointer to one of them. BUILD_DIR=./data/builds # Required. How many previous builds to retain for rollback. KEEP_BUILDS=10 # Required. Total transferred bytes for a page. The build fails above it. BUDGET_KB=180 # Required. First-party, same domain, so the measurement is not blocked as third-party. VITALS_ENDPOINT=https://yourdomain.example/v # Required. Page views and experiment events. Separate from submissions on purpose. EVENT_RETENTION_DAYS=180 # Required. Form submissions. Longer, and deleted per person on request. SUBMISSION_RETENTION_DAYS=730
You are building a lean indie version of Instapage. 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 ===== # Instapage · indie build A landing page builder whose hard problems are publishing static files without a broken minute, measuring speed from real visits rather than a lab score, and running split tests that are decided in advance instead of stopped when they look good. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Content | Pages as ordered sections in SQLite, rendered to static HTML at publish | a page that needs a server at request time is a page that can be slow at request time | | Rendering | One template per section kind, server-side, no client framework on the published page | the published page should ship almost no JavaScript because that is the whole advantage | | Measurement | First-party field collection from the published page using the standard web vitals library | a new page has no field data anywhere else, and lab numbers cannot produce two of the three metrics | | Experiments | Deterministic bucketing from a first-party visitor identifier, decided sample size | sticky assignment and a stopping rule are what make the result mean anything | | Publishing | Build into a new directory and swap, keeping previous versions | a half-uploaded page is a broken page on the only URL that matters | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the editor and the build. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **A small server and a domain you control** · a few pounds a month plus the domain - Why: The published pages need a stable address with a certificate, and the measurement endpoint has to be first-party to avoid being blocked. - Get it: Any small virtual server plus a domain, with automatic certificates. - Verify: The page loads over your domain with a valid certificate and a measurement reaches the endpoint - [ ] **The standard web vitals measurement library** · free - Why: Computing these metrics by hand from performance entries is subtly wrong, and the library is the reference implementation of the definitions. - Get it: Add the published library to the page bundle and report to your own endpoint. - Verify: A real page load produces values for all three metrics - [ ] **A slow device and a throttled connection** · free - Why: Every layout shift and every slow paint is invisible on a developer machine on a fast network, which is why pages ship broken on phones. - Get it: An old phone, or your browser's device and network throttling. - Verify: The page has been loaded once on a throttled slow connection and the shift measured - [ ] **Your sample size and stopping rule** · free - Why: These are the difference between an experiment and a scoreboard, and choosing them after the data arrives means choosing the answer. - Get it: Decide the minimum effect worth detecting, compute the sample size it needs, and write the stopping rule down. - Verify: Both are stored on the experiment record and shown in the report ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - A general-purpose drag and drop canvas. - Audience personalisation. - Heatmaps and session recording. - Multi-user editing and approvals. - general-purpose visual editing - large template and component libraries - CMS collaboration and client handoff - managed hosting, forms, and analytics If one of those is essential to you, that is the reason to keep paying for Instapage, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Instapage 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 publisher whose pages are static files, whose speed is measured on real visits, and whose split tests are decided before they start to replace Instapage. 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. ### Rule zero A landing page exists to be measured, and both of the measurements this category sells are easy to fake. So neither is allowed to be faked here. Speed is what your visitors experienced, not what your laptop scored. The Core Web Vitals thresholds are published: a good Largest Contentful Paint is 2.5 seconds or less, a good Interaction to Next Paint is 200 milliseconds or less, and a good Cumulative Layout Shift is 0.1 or less, each assessed at the 75th percentile of page loads and segmented between mobile and desktop. A lab tool cannot produce two of those, because nobody interacted with the page and the run stopped at load. Collect the numbers from the published page itself. A split test is a decision procedure, not a scoreboard. Fix the sample size and the stopping rule before the test starts, keep the assignment sticky per visitor, and report the interval and the count with every result. Stopping a test on the morning it looks good is how a variant with no effect gets rolled out, and this build will not have a button that does it. ### Stack (fixed, do not substitute) - Content: Pages as ordered sections in SQLite, rendered to static HTML at publish · a page that needs a server at request time is a page that can be slow at request time - Rendering: One template per section kind, server-side, no client framework on the published page · the published page should ship almost no JavaScript because that is the whole advantage - Measurement: First-party field collection from the published page using the standard web vitals library · a new page has no field data anywhere else, and lab numbers cannot produce two of the three metrics - Experiments: Deterministic bucketing from a first-party visitor identifier, decided sample size · sticky assignment and a stopping rule are what make the result mean anything - Publishing: Build into a new directory and swap, keeping previous versions · a half-uploaded page is a broken page on the only URL that matters ### Data model (create this before Phase 1) - pages(id, slug, title, status, current_version_id, created_at) - page_versions(id, page_id, ordinal, sections_json, theme_json, built_at, built_by, notes) · every publish is a version you can go back to - sections(kind, schema) · hero, features, testimonial, form, footer, each with a declared field schema - experiments(id, page_id, name, variant_a_version_id, variant_b_version_id, metric, planned_n, started_at, stopped_at, stop_reason) - assignments(visitor_id, experiment_id, variant, assigned_at) · deterministic from the identifier, written once - events(id, page_id, experiment_id, variant, kind, at, session_id) · view and conversion, nothing else - vitals(id, page_id, version_id, metric, value, rating, device, at) · one row per reported measurement from a real visit - submissions(id, page_id, version_id, fields_json, received_at, spam_score, ip_country) · form entries, kept separately from analytics - A published page is a directory of files. The database is the editor's state, not a dependency of the live page. ### Phase 1 · Sections in, static files out Build: Define each section kind with a field schema and one template. The editor is a form per section with reordering. The build writes HTML, one stylesheet, and resized images into a directory that can be served by anything. Every page version is kept. Trap: Storing the page as a blob of HTML from a rich editor means the day you change the heading style you cannot change it anywhere, because the markup is copied into every page and half of it was pasted from a document with its own inline styles. Store fields against a schema and render them, so a template change reaches every page that uses it. Done when: A page built twice from the same version produces byte-identical output, tested. Changing a section template changes every page using that section after a rebuild, tested with two pages. The built directory serves correctly from a plain static file server with no application running. Every publish creates a version row that can be rebuilt from stored fields alone. Do not build yet: No measurement, experiments, forms or domains yet. ### Phase 2 · Fast for the visitor, measured on the visit Build: Enforce a build budget: total bytes, image dimensions and the number of blocking requests. Give the largest element a preload, size every image so nothing reflows, and handle web fonts with a declared fallback plus the font metric overrides rather than hoping. Then collect Largest Contentful Paint, Interaction to Next Paint and Cumulative Layout Shift from real visits with a first-party endpoint, and show the 75th percentile split by device against the published thresholds of 2.5 seconds, 200 milliseconds and 0.1. Trap: A perfect lab score and a page that is not in any field dataset feels like proof, but a synthetic run cannot measure responsiveness because nobody clicked anything, and it only sees layout shifts above the fold during load, while a page with little traffic may never appear in a public field dataset at all because inclusion requires a minimum number of visitors that is deliberately undisclosed. Collect your own field measurements from the published page and treat the lab run as a smoke test. Done when: The build fails when total transferred bytes or the number of blocking requests passes the configured budget, tested with an oversized image. A web font swapping in does not move the text block, verified by a layout shift measurement of effectively zero on a throttled load with the metric overrides in place and a visible shift without them. Field measurements arrive from a real visit and appear at the 75th percentile split by device. Every vitals row is linked to the page version that was live when it was measured. Do not build yet: No experiments, forms or domains yet. ### Phase 3 · Publishing without a broken minute Build: Build into a new directory, verify it by requesting the page and the assets from it, then swap the served directory in one operation. Keep a stated number of previous builds and provide a one-command rollback. A build that fails its checks never becomes live. Trap: Publishing by writing files over the live directory means the page is half old and half new for the seconds it takes, and a build that dies partway leaves it broken until somebody notices, which on a campaign page means the traffic you paid for hit an unstyled page. Build elsewhere, verify, then swap, and keep the previous build to roll back to. Done when: A build killed partway leaves the live directory untouched, tested by interrupting it. The swap is a single operation and a request during the swap gets either the old page or the new one, never a mixture, tested under load. Rollback restores the previous version with one command and is exercised in the suite. A build failing a budget or a template error is never swapped in, and the failure names what went wrong. Do not build yet: No experiments, forms or domains yet. ### Phase 4 · Split tests decided in advance Build: Assign a variant by hashing a first-party visitor identifier with the experiment identifier, so assignment is deterministic and sticky without a lookup. Record the planned sample size and the metric when the experiment is created. Show progress towards that size, and report the difference with an interval and the counts. Stopping early is possible but requires a written reason stored on the experiment. Trap: Watching the dashboard and stopping the moment one variant looks ahead turns a coin-flip difference into a decision, because checking repeatedly gives you many chances to catch a random lead, and on a page with a few dozen conversions a week that lead appears constantly. Fix the sample size when you create the experiment, show progress towards it, and make any early stop record who stopped it and why. Done when: The same visitor identifier always resolves to the same variant, across restarts and without a database lookup, tested with a thousand identifiers. Assignment is balanced within a stated tolerance over ten thousand identifiers, tested. The experiment view shows progress towards the planned sample size and does not display a winner before it is reached. An early stop stores a reason and the report shows that the test was stopped early wherever the result appears. Do not build yet: No forms, domains or consent yet. ### Phase 5 · Forms, the domain, and what you keep Build: Accept form submissions to a small endpoint with size limits, a honeypot, a rate limit per address and server-side validation, and store them separately from analytics. Serve the pages over your own domain with automatic certificates. Keep the first-party measurement identifier cookie-free where you can, state a retention period for events and submissions, and implement deletion of one person's submissions on request. Trap: Putting form submissions in the same table as page events and then applying one retention rule deletes the leads along with the analytics, or keeps the analytics as long as the leads; either way somebody is unhappy and one of them is a data protection problem. Keep them apart, give each its own retention period, and write both in the README. Done when: A submission over the size limit, from a bot filling the honeypot, or beyond the rate limit is rejected with a clear response and without storing the payload, all three tested. Submissions and events have separate retention periods, both enforced by a job and both stated in the README. Deleting one person's submissions removes their fields and leaves aggregate counts intact, tested. The site serves over its own domain with a valid certificate that renews without manual work, rehearsed once. Do not build yet: Nothing after this. ### Out of scope (and why) - A general-purpose drag and drop canvas, which is a year of work and is why the templates in the paid product look consistent and yours would not. - Personalisation by audience segment, which needs visitor data you would then have to hold and justify holding. - Heatmaps and session recording, which capture more about a visitor than a landing page needs and carry their own consent question. - Multi-user editing with approvals, which turns every save into a conflict and a permission decision. ### README must contain - That published pages are static files with no runtime dependency on the editor - The Core Web Vitals thresholds used as the target, that the assessment is at the 75th percentile split by device, and that the numbers come from real visits rather than a lab run - That a new page may have no field data in any public dataset, which is why measurement is first-party - How an experiment's sample size is fixed in advance and what happens when someone stops one early - The retention period for events and for form submissions, separately, and how to delete one person's submissions ===== AGENTS.md ===== # Agent instructions · Instapage indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Pages as ordered sections in SQLite, rendered to static HTML at publish, One template per section kind, server-side, no client framework on the published page, First-party field collection from the published page using the standard web vitals library, Deterministic bucketing from a first-party visitor identifier, decided sample size, Build into a new directory and swap, keeping previous versions. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing the page as a blob of HTML from a rich editor means the day you change the heading style you cannot change it anywhere, because the markup is copied into every page and half of it was pasted from a document with its own inline styles. Store fields against a schema and render them, so a template change reaches every page that uses it. - A perfect lab score and a page that is not in any field dataset feels like proof, but a synthetic run cannot measure responsiveness because nobody clicked anything, and it only sees layout shifts above the fold during load, while a page with little traffic may never appear in a public field dataset at all because inclusion requires a minimum number of visitors that is deliberately undisclosed. Collect your own field measurements from the published page and treat the lab run as a smoke test. - Publishing by writing files over the live directory means the page is half old and half new for the seconds it takes, and a build that dies partway leaves it broken until somebody notices, which on a campaign page means the traffic you paid for hit an unstyled page. Build elsewhere, verify, then swap, and keep the previous build to roll back to. - Watching the dashboard and stopping the moment one variant looks ahead turns a coin-flip difference into a decision, because checking repeatedly gives you many chances to catch a random lead, and on a page with a few dozen conversions a week that lead appears constantly. Fix the sample size when you create the experiment, show progress towards it, and make any early stop record who stopped it and why. ===== BUILD_PLAN.md ===== # Build plan · Instapage A landing page builder whose hard problems are publishing static files without a broken minute, measuring speed from real visits rather than a lab score, and running split tests that are decided in advance instead of stopped when they look good. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Sections in, static files out A page is structured data that renders to HTML, CSS and images with no runtime. ### Steps 1. Declare a schema per section kind Fields you can validate, not markup you cannot change later. Files: `src/sections/schema.ts`, `src/sections/templates/` 2. Make the build deterministic Byte-identical output is how you know a rebuild is safe. Files: `src/build.ts`, `tests/build.test.ts` 3. Serve the output with no application That constraint is the entire performance story. Files: `scripts/serve-static.sh` ### Done when - [ ] Two builds of one version are byte-identical - [ ] A template change reaches every page using it - [ ] The output serves from a plain file server - [ ] Each publish stores a rebuildable version ### Watch out - Storing the page as a blob of HTML from a rich editor means the day you change the heading style you cannot change it anywhere, because the markup is copied into every page and half of it was pasted from a document with its own inline styles. Store fields against a schema and render them, so a template change reaches every page that uses it. ## Phase 2 · Fast for the visitor, measured on the visit The page meets the published thresholds where it counts, and the numbers come from real loads. ### Steps 1. Put a byte and request budget in the build A budget checked by a person is a budget that slips. Files: `src/budget.ts`, `tests/budget.test.ts` 2. Handle fonts with overrides and a real fallback The fallback relayout is the most common shift on a landing page. Files: `src/theme/fonts.css`, `src/build.ts` 3. Collect vitals first-party from the live page Two of the three metrics cannot come from a lab run. Files: `src/public/vitals.js`, `src/api/vitals.ts` ### Done when - [ ] The build fails on an oversized image - [ ] A font swap produces no measurable shift - [ ] Field measurements appear at the 75th percentile by device - [ ] Each measurement is tied to the live version ### Watch out - A perfect lab score and a page that is not in any field dataset feels like proof, but a synthetic run cannot measure responsiveness because nobody clicked anything, and it only sees layout shifts above the fold during load, while a page with little traffic may never appear in a public field dataset at all because inclusion requires a minimum number of visitors that is deliberately undisclosed. Collect your own field measurements from the published page and treat the lab run as a smoke test. ## Phase 3 · Publishing without a broken minute A publish is atomic and reversible, and a failed build never reaches the URL. ### Steps 1. Build to a new directory every time Never write into the directory being served. Files: `src/publish.ts` 2. Verify before swapping Request the page and its assets from the new directory first. Files: `src/publish.ts`, `tests/publish.test.ts` 3. Make rollback one command, and test it The rollback nobody has run is not a rollback. Files: `scripts/rollback.sh`, `tests/rollback.test.ts` ### Done when - [ ] An interrupted build leaves the live page untouched - [ ] The swap never serves a mixture - [ ] Rollback runs in one command and is tested - [ ] A failing build is never published ### Watch out - Publishing by writing files over the live directory means the page is half old and half new for the seconds it takes, and a build that dies partway leaves it broken until somebody notices, which on a campaign page means the traffic you paid for hit an unstyled page. Build elsewhere, verify, then swap, and keep the previous build to roll back to. ## Phase 4 · Split tests decided in advance An experiment has a sample size and a stopping rule before it starts, and the same visitor always sees the same variant. ### Steps 1. Bucket deterministically from the identifier Sticky assignment with no lookup and no cookie race. Files: `src/experiments/assign.ts`, `tests/assign.test.ts` 2. Store the planned sample size at creation Deciding it afterwards is deciding it from the data. Files: `src/experiments/create.ts` 3. Report an interval and the counts, never a bare winner And label any early stop wherever the number is shown. Files: `src/experiments/report.ts` ### Done when - [ ] One identifier always gets the same variant - [ ] Assignment is balanced over ten thousand identifiers - [ ] No winner is shown before the planned sample size - [ ] An early stop is recorded and labelled in the report ### Watch out - Watching the dashboard and stopping the moment one variant looks ahead turns a coin-flip difference into a decision, because checking repeatedly gives you many chances to catch a random lead, and on a page with a few dozen conversions a week that lead appears constantly. Fix the sample size when you create the experiment, show progress towards it, and make any early stop record who stopped it and why. ## Not in this build - A general-purpose drag and drop canvas. - Audience personalisation. - Heatmaps and session recording. - Multi-user editing and approvals. ## After v1, if you want it - A second metric per experiment, recorded but never used to declare a winner - A content import from an existing page, mapping its blocks onto your section schemas - A scheduled publish, which is the same swap with a timer and a check that the build still passes ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Editor state, versions, experiments and measurements. Not needed to serve a published page. DATABASE_PATH=./data/pages.db # Required. One directory per build. The live directory is a pointer to one of them. BUILD_DIR=./data/builds # Required. How many previous builds to retain for rollback. KEEP_BUILDS=10 # Required. Total transferred bytes for a page. The build fails above it. BUDGET_KB=180 # Required. First-party, same domain, so the measurement is not blocked as third-party. VITALS_ENDPOINT=https://yourdomain.example/v # Required. Page views and experiment events. Separate from submissions on purpose. EVENT_RETENTION_DAYS=180 # Required. Form submissions. Longer, and deleted per person on request. SUBMISSION_RETENTION_DAYS=730
You are building a production product version of Instapage. 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 ===== # Instapage · product brief ## Problem The visible enterprise landing pages loop is buildable, but a credible replacement needs more than the first screen. Instapage earns its keep through editor, CMS, hosting, polish, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A publisher whose pages are static and fast, whose speed numbers come from real visits at the percentile the thresholds are defined at, whose publishes are atomic and reversible, and whose split tests cannot be stopped quietly on a good morning. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node.js 22 - deploy target or static hosting - user-owned domain for publishing ## Explicit non-goals for v1 - A general-purpose drag and drop canvas. - Audience personalisation. - Heatmaps and session recording. - Multi-user editing and approvals. - general-purpose visual editing - large template and component libraries - CMS collaboration and client handoff - managed hosting, forms, and analytics ## Success criteria - Two builds of one version are confirmed byte-identical - The build is confirmed to fail on an over-budget page - An interrupted publish is confirmed to leave the live page untouched - One visitor identifier is confirmed to get one variant every time - Leads and events are confirmed to have separate enforced retention ===== BRIEF.md ===== # Build brief · Instapage 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 publisher whose pages are static files, whose speed is measured on real visits, and whose split tests are decided before they start to replace Instapage. 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. ### Rule zero A landing page exists to be measured, and both of the measurements this category sells are easy to fake. So neither is allowed to be faked here. Speed is what your visitors experienced, not what your laptop scored. The Core Web Vitals thresholds are published: a good Largest Contentful Paint is 2.5 seconds or less, a good Interaction to Next Paint is 200 milliseconds or less, and a good Cumulative Layout Shift is 0.1 or less, each assessed at the 75th percentile of page loads and segmented between mobile and desktop. A lab tool cannot produce two of those, because nobody interacted with the page and the run stopped at load. Collect the numbers from the published page itself. A split test is a decision procedure, not a scoreboard. Fix the sample size and the stopping rule before the test starts, keep the assignment sticky per visitor, and report the interval and the count with every result. Stopping a test on the morning it looks good is how a variant with no effect gets rolled out, and this build will not have a button that does it. ### Stack (fixed, do not substitute) - Content: Pages as ordered sections in SQLite, rendered to static HTML at publish · a page that needs a server at request time is a page that can be slow at request time - Rendering: One template per section kind, server-side, no client framework on the published page · the published page should ship almost no JavaScript because that is the whole advantage - Measurement: First-party field collection from the published page using the standard web vitals library · a new page has no field data anywhere else, and lab numbers cannot produce two of the three metrics - Experiments: Deterministic bucketing from a first-party visitor identifier, decided sample size · sticky assignment and a stopping rule are what make the result mean anything - Publishing: Build into a new directory and swap, keeping previous versions · a half-uploaded page is a broken page on the only URL that matters ### Data model (create this before Phase 1) - pages(id, slug, title, status, current_version_id, created_at) - page_versions(id, page_id, ordinal, sections_json, theme_json, built_at, built_by, notes) · every publish is a version you can go back to - sections(kind, schema) · hero, features, testimonial, form, footer, each with a declared field schema - experiments(id, page_id, name, variant_a_version_id, variant_b_version_id, metric, planned_n, started_at, stopped_at, stop_reason) - assignments(visitor_id, experiment_id, variant, assigned_at) · deterministic from the identifier, written once - events(id, page_id, experiment_id, variant, kind, at, session_id) · view and conversion, nothing else - vitals(id, page_id, version_id, metric, value, rating, device, at) · one row per reported measurement from a real visit - submissions(id, page_id, version_id, fields_json, received_at, spam_score, ip_country) · form entries, kept separately from analytics - A published page is a directory of files. The database is the editor's state, not a dependency of the live page. ### Phase 1 · Sections in, static files out Build: Define each section kind with a field schema and one template. The editor is a form per section with reordering. The build writes HTML, one stylesheet, and resized images into a directory that can be served by anything. Every page version is kept. Trap: Storing the page as a blob of HTML from a rich editor means the day you change the heading style you cannot change it anywhere, because the markup is copied into every page and half of it was pasted from a document with its own inline styles. Store fields against a schema and render them, so a template change reaches every page that uses it. Done when: A page built twice from the same version produces byte-identical output, tested. Changing a section template changes every page using that section after a rebuild, tested with two pages. The built directory serves correctly from a plain static file server with no application running. Every publish creates a version row that can be rebuilt from stored fields alone. Do not build yet: No measurement, experiments, forms or domains yet. ### Phase 2 · Fast for the visitor, measured on the visit Build: Enforce a build budget: total bytes, image dimensions and the number of blocking requests. Give the largest element a preload, size every image so nothing reflows, and handle web fonts with a declared fallback plus the font metric overrides rather than hoping. Then collect Largest Contentful Paint, Interaction to Next Paint and Cumulative Layout Shift from real visits with a first-party endpoint, and show the 75th percentile split by device against the published thresholds of 2.5 seconds, 200 milliseconds and 0.1. Trap: A perfect lab score and a page that is not in any field dataset feels like proof, but a synthetic run cannot measure responsiveness because nobody clicked anything, and it only sees layout shifts above the fold during load, while a page with little traffic may never appear in a public field dataset at all because inclusion requires a minimum number of visitors that is deliberately undisclosed. Collect your own field measurements from the published page and treat the lab run as a smoke test. Done when: The build fails when total transferred bytes or the number of blocking requests passes the configured budget, tested with an oversized image. A web font swapping in does not move the text block, verified by a layout shift measurement of effectively zero on a throttled load with the metric overrides in place and a visible shift without them. Field measurements arrive from a real visit and appear at the 75th percentile split by device. Every vitals row is linked to the page version that was live when it was measured. Do not build yet: No experiments, forms or domains yet. ### Phase 3 · Publishing without a broken minute Build: Build into a new directory, verify it by requesting the page and the assets from it, then swap the served directory in one operation. Keep a stated number of previous builds and provide a one-command rollback. A build that fails its checks never becomes live. Trap: Publishing by writing files over the live directory means the page is half old and half new for the seconds it takes, and a build that dies partway leaves it broken until somebody notices, which on a campaign page means the traffic you paid for hit an unstyled page. Build elsewhere, verify, then swap, and keep the previous build to roll back to. Done when: A build killed partway leaves the live directory untouched, tested by interrupting it. The swap is a single operation and a request during the swap gets either the old page or the new one, never a mixture, tested under load. Rollback restores the previous version with one command and is exercised in the suite. A build failing a budget or a template error is never swapped in, and the failure names what went wrong. Do not build yet: No experiments, forms or domains yet. ### Phase 4 · Split tests decided in advance Build: Assign a variant by hashing a first-party visitor identifier with the experiment identifier, so assignment is deterministic and sticky without a lookup. Record the planned sample size and the metric when the experiment is created. Show progress towards that size, and report the difference with an interval and the counts. Stopping early is possible but requires a written reason stored on the experiment. Trap: Watching the dashboard and stopping the moment one variant looks ahead turns a coin-flip difference into a decision, because checking repeatedly gives you many chances to catch a random lead, and on a page with a few dozen conversions a week that lead appears constantly. Fix the sample size when you create the experiment, show progress towards it, and make any early stop record who stopped it and why. Done when: The same visitor identifier always resolves to the same variant, across restarts and without a database lookup, tested with a thousand identifiers. Assignment is balanced within a stated tolerance over ten thousand identifiers, tested. The experiment view shows progress towards the planned sample size and does not display a winner before it is reached. An early stop stores a reason and the report shows that the test was stopped early wherever the result appears. Do not build yet: No forms, domains or consent yet. ### Phase 5 · Forms, the domain, and what you keep Build: Accept form submissions to a small endpoint with size limits, a honeypot, a rate limit per address and server-side validation, and store them separately from analytics. Serve the pages over your own domain with automatic certificates. Keep the first-party measurement identifier cookie-free where you can, state a retention period for events and submissions, and implement deletion of one person's submissions on request. Trap: Putting form submissions in the same table as page events and then applying one retention rule deletes the leads along with the analytics, or keeps the analytics as long as the leads; either way somebody is unhappy and one of them is a data protection problem. Keep them apart, give each its own retention period, and write both in the README. Done when: A submission over the size limit, from a bot filling the honeypot, or beyond the rate limit is rejected with a clear response and without storing the payload, all three tested. Submissions and events have separate retention periods, both enforced by a job and both stated in the README. Deleting one person's submissions removes their fields and leaves aggregate counts intact, tested. The site serves over its own domain with a valid certificate that renews without manual work, rehearsed once. Do not build yet: Nothing after this. ### Out of scope (and why) - A general-purpose drag and drop canvas, which is a year of work and is why the templates in the paid product look consistent and yours would not. - Personalisation by audience segment, which needs visitor data you would then have to hold and justify holding. - Heatmaps and session recording, which capture more about a visitor than a landing page needs and carry their own consent question. - Multi-user editing with approvals, which turns every save into a conflict and a permission decision. ### README must contain - That published pages are static files with no runtime dependency on the editor - The Core Web Vitals thresholds used as the target, that the assessment is at the 75th percentile split by device, and that the numbers come from real visits rather than a lab run - That a new page may have no field data in any public dataset, which is why measurement is first-party - How an experiment's sample size is fixed in advance and what happens when someone stops one early - The retention period for events and for form submissions, separately, and how to delete one person's submissions ===== ARCHITECTURE.md ===== # Architecture · Instapage ## Stack | Part | Choice | Why | | --- | --- | --- | | Content | Pages as ordered sections in SQLite, rendered to static HTML at publish | a page that needs a server at request time is a page that can be slow at request time | | Rendering | One template per section kind, server-side, no client framework on the published page | the published page should ship almost no JavaScript because that is the whole advantage | | Measurement | First-party field collection from the published page using the standard web vitals library | a new page has no field data anywhere else, and lab numbers cannot produce two of the three metrics | | Experiments | Deterministic bucketing from a first-party visitor identifier, decided sample size | sticky assignment and a stopping rule are what make the result mean anything | | Publishing | Build into a new directory and swap, keeping previous versions | a half-uploaded page is a broken page on the only URL that matters | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Content | Sections, schemas and versions. | Any store, provided pages stay structured fields rather than markup. | | Build | Deterministic output and the budget. | Any generator, provided two builds of a version match byte for byte. | | Publish | Verify then swap, and rollback. | Any deployment, provided nothing writes into the served directory. | | Experiments | Deterministic assignment and the stopping rule. | Not swappable for a dashboard that declares winners on demand. | | Measurement | First-party vitals and event retention. | Any collector, provided it is first-party and tied to a version. | ## 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 · Editor state, versions, experiments and measurements. Not needed to serve a published page. - `BUILD_DIR` · required · One directory per build. The live directory is a pointer to one of them. - `KEEP_BUILDS` · required · How many previous builds to retain for rollback. - `BUDGET_KB` · required · Total transferred bytes for a page. The build fails above it. - `VITALS_ENDPOINT` · required · First-party, same domain, so the measurement is not blocked as third-party. - `EVENT_RETENTION_DAYS` · required · Page views and experiment events. Separate from submissions on purpose. - `SUBMISSION_RETENTION_DAYS` · required · Form submissions. Longer, and deleted per person on request. ## 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 · Instapage product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Pages as ordered sections in SQLite, rendered to static HTML at publish, One template per section kind, server-side, no client framework on the published page, First-party field collection from the published page using the standard web vitals library, Deterministic bucketing from a first-party visitor identifier, decided sample size, Build into a new directory and swap, keeping previous versions. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Storing the page as a blob of HTML from a rich editor means the day you change the heading style you cannot change it anywhere, because the markup is copied into every page and half of it was pasted from a document with its own inline styles. Store fields against a schema and render them, so a template change reaches every page that uses it. - A perfect lab score and a page that is not in any field dataset feels like proof, but a synthetic run cannot measure responsiveness because nobody clicked anything, and it only sees layout shifts above the fold during load, while a page with little traffic may never appear in a public field dataset at all because inclusion requires a minimum number of visitors that is deliberately undisclosed. Collect your own field measurements from the published page and treat the lab run as a smoke test. - Publishing by writing files over the live directory means the page is half old and half new for the seconds it takes, and a build that dies partway leaves it broken until somebody notices, which on a campaign page means the traffic you paid for hit an unstyled page. Build elsewhere, verify, then swap, and keep the previous build to roll back to. - Watching the dashboard and stopping the moment one variant looks ahead turns a coin-flip difference into a decision, because checking repeatedly gives you many chances to catch a random lead, and on a page with a few dozen conversions a week that lead appears constantly. Fix the sample size when you create the experiment, show progress towards it, and make any early stop record who stopped it and why. - Putting form submissions in the same table as page events and then applying one retention rule deletes the leads along with the analytics, or keeps the analytics as long as the leads; either way somebody is unhappy and one of them is a data protection problem. Keep them apart, give each its own retention period, and write both in the README. ===== MILESTONES.md ===== # Delivery milestones · Instapage Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Sections in, static files out A page is structured data that renders to HTML, CSS and images with no runtime. ### Steps 1. Declare a schema per section kind Fields you can validate, not markup you cannot change later. Files: `src/sections/schema.ts`, `src/sections/templates/` 2. Make the build deterministic Byte-identical output is how you know a rebuild is safe. Files: `src/build.ts`, `tests/build.test.ts` 3. Serve the output with no application That constraint is the entire performance story. Files: `scripts/serve-static.sh` ### Done when - [ ] Two builds of one version are byte-identical - [ ] A template change reaches every page using it - [ ] The output serves from a plain file server - [ ] Each publish stores a rebuildable version ### Watch out - Storing the page as a blob of HTML from a rich editor means the day you change the heading style you cannot change it anywhere, because the markup is copied into every page and half of it was pasted from a document with its own inline styles. Store fields against a schema and render them, so a template change reaches every page that uses it. ## M2 · Fast for the visitor, measured on the visit The page meets the published thresholds where it counts, and the numbers come from real loads. ### Steps 1. Put a byte and request budget in the build A budget checked by a person is a budget that slips. Files: `src/budget.ts`, `tests/budget.test.ts` 2. Handle fonts with overrides and a real fallback The fallback relayout is the most common shift on a landing page. Files: `src/theme/fonts.css`, `src/build.ts` 3. Collect vitals first-party from the live page Two of the three metrics cannot come from a lab run. Files: `src/public/vitals.js`, `src/api/vitals.ts` ### Done when - [ ] The build fails on an oversized image - [ ] A font swap produces no measurable shift - [ ] Field measurements appear at the 75th percentile by device - [ ] Each measurement is tied to the live version ### Watch out - A perfect lab score and a page that is not in any field dataset feels like proof, but a synthetic run cannot measure responsiveness because nobody clicked anything, and it only sees layout shifts above the fold during load, while a page with little traffic may never appear in a public field dataset at all because inclusion requires a minimum number of visitors that is deliberately undisclosed. Collect your own field measurements from the published page and treat the lab run as a smoke test. ## M3 · Publishing without a broken minute A publish is atomic and reversible, and a failed build never reaches the URL. ### Steps 1. Build to a new directory every time Never write into the directory being served. Files: `src/publish.ts` 2. Verify before swapping Request the page and its assets from the new directory first. Files: `src/publish.ts`, `tests/publish.test.ts` 3. Make rollback one command, and test it The rollback nobody has run is not a rollback. Files: `scripts/rollback.sh`, `tests/rollback.test.ts` ### Done when - [ ] An interrupted build leaves the live page untouched - [ ] The swap never serves a mixture - [ ] Rollback runs in one command and is tested - [ ] A failing build is never published ### Watch out - Publishing by writing files over the live directory means the page is half old and half new for the seconds it takes, and a build that dies partway leaves it broken until somebody notices, which on a campaign page means the traffic you paid for hit an unstyled page. Build elsewhere, verify, then swap, and keep the previous build to roll back to. ## M4 · Split tests decided in advance An experiment has a sample size and a stopping rule before it starts, and the same visitor always sees the same variant. ### Steps 1. Bucket deterministically from the identifier Sticky assignment with no lookup and no cookie race. Files: `src/experiments/assign.ts`, `tests/assign.test.ts` 2. Store the planned sample size at creation Deciding it afterwards is deciding it from the data. Files: `src/experiments/create.ts` 3. Report an interval and the counts, never a bare winner And label any early stop wherever the number is shown. Files: `src/experiments/report.ts` ### Done when - [ ] One identifier always gets the same variant - [ ] Assignment is balanced over ten thousand identifiers - [ ] No winner is shown before the planned sample size - [ ] An early stop is recorded and labelled in the report ### Watch out - Watching the dashboard and stopping the moment one variant looks ahead turns a coin-flip difference into a decision, because checking repeatedly gives you many chances to catch a random lead, and on a page with a few dozen conversions a week that lead appears constantly. Fix the sample size when you create the experiment, show progress towards it, and make any early stop record who stopped it and why. ## M5 · Forms, the domain, and what you keep (production only) The page collects leads safely on your own domain, and every visitor record has a stated life. ### Steps 1. Separate leads from analytics at the table level One retention rule cannot serve both. Files: `src/forms.ts`, `src/retention.ts` 2. Reject bad submissions before storing them Size, honeypot, rate limit, then validation. Files: `src/forms.ts`, `tests/forms.test.ts` 3. Make certificate renewal automatic and check it A campaign page with an expired certificate converts at zero. Files: `docs/domain.md` ### Done when - [ ] Oversized, honeypot and rate-limited submissions are rejected unstored - [ ] Leads and events have separate enforced retention - [ ] Per-person deletion leaves aggregates intact - [ ] Certificates renew without manual work ### Watch out - Putting form submissions in the same table as page events and then applying one retention rule deletes the leads along with the analytics, or keeps the analytics as long as the leads; either way somebody is unhappy and one of them is a data protection problem. Keep them apart, give each its own retention period, and write both in the README. ===== OPERATIONS.md ===== # Operations · Instapage ## Backup The database and the current build directory, plus submissions, which are the only part nobody can regenerate. A page can be rebuilt; a lead cannot. ## Restore Restore the database, rebuild the current version, verify it matches the retained build byte for byte, then swap it in. That comparison is also the test that the build is deterministic. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring The 75th percentile of each metric per page and device, the age of the live build, failed builds, and submission volume against the rate limit. A sudden drop in submissions is usually a broken form rather than a bad week. ## Incident checklist A slow page is diagnosed from the vitals rows against the version that was live, which is why they carry a version. A broken publish is one rollback command, which is why the previous builds are retained. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Two builds of one version are confirmed byte-identical - [ ] The build is confirmed to fail on an over-budget page - [ ] An interrupted publish is confirmed to leave the live page untouched - [ ] One visitor identifier is confirmed to get one variant every time - [ ] Leads and events are confirmed to have separate enforced retention ## Launch constraint Do not market omitted Instapage 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. Editor state, versions, experiments and measurements. Not needed to serve a published page. DATABASE_PATH=./data/pages.db # Required. One directory per build. The live directory is a pointer to one of them. BUILD_DIR=./data/builds # Required. How many previous builds to retain for rollback. KEEP_BUILDS=10 # Required. Total transferred bytes for a page. The build fails above it. BUDGET_KB=180 # Required. First-party, same domain, so the measurement is not blocked as third-party. VITALS_ENDPOINT=https://yourdomain.example/v # Required. Page views and experiment events. Separate from submissions on purpose. EVENT_RETENTION_DAYS=180 # Required. Form submissions. Longer, and deleted per person on request. SUBMISSION_RETENTION_DAYS=730
# Instapage · indie build A landing page builder whose hard problems are publishing static files without a broken minute, measuring speed from real visits rather than a lab score, and running split tests that are decided in advance instead of stopped when they look good. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Content | Pages as ordered sections in SQLite, rendered to static HTML at publish | a page that needs a server at request time is a page that can be slow at request time | | Rendering | One template per section kind, server-side, no client framework on the published page | the published page should ship almost no JavaScript because that is the whole advantage | | Measurement | First-party field collection from the published page using the standard web vitals library | a new page has no field data anywhere else, and lab numbers cannot produce two of the three metrics | | Experiments | Deterministic bucketing from a first-party visitor identifier, decided sample size | sticky assignment and a stopping rule are what make the result mean anything | | Publishing | Build into a new directory and swap, keeping previous versions | a half-uploaded page is a broken page on the only URL that matters | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the editor and the build. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **A small server and a domain you control** · a few pounds a month plus the domain - Why: The published pages need a stable address with a certificate, and the measurement endpoint has to be first-party to avoid being blocked. - Get it: Any small virtual server plus a domain, with automatic certificates. - Verify: The page loads over your domain with a valid certificate and a measurement reaches the endpoint - [ ] **The standard web vitals measurement library** · free - Why: Computing these metrics by hand from performance entries is subtly wrong, and the library is the reference implementation of the definitions. - Get it: Add the published library to the page bundle and report to your own endpoint. - Verify: A real page load produces values for all three metrics - [ ] **A slow device and a throttled connection** · free - Why: Every layout shift and every slow paint is invisible on a developer machine on a fast network, which is why pages ship broken on phones. - Get it: An old phone, or your browser's device and network throttling. - Verify: The page has been loaded once on a throttled slow connection and the shift measured - [ ] **Your sample size and stopping rule** · free - Why: These are the difference between an experiment and a scoreboard, and choosing them after the data arrives means choosing the answer. - Get it: Decide the minimum effect worth detecting, compute the sample size it needs, and write the stopping rule down. - Verify: Both are stored on the experiment record and shown in the report ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - A general-purpose drag and drop canvas. - Audience personalisation. - Heatmaps and session recording. - Multi-user editing and approvals. - general-purpose visual editing - large template and component libraries - CMS collaboration and client handoff - managed hosting, forms, and analytics If one of those is essential to you, that is the reason to keep paying for Instapage, and the README should say so rather than pretend.
# Build brief · Instapage 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 publisher whose pages are static files, whose speed is measured on real visits, and whose split tests are decided before they start to replace Instapage. 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. ### Rule zero A landing page exists to be measured, and both of the measurements this category sells are easy to fake. So neither is allowed to be faked here. Speed is what your visitors experienced, not what your laptop scored. The Core Web Vitals thresholds are published: a good Largest Contentful Paint is 2.5 seconds or less, a good Interaction to Next Paint is 200 milliseconds or less, and a good Cumulative Layout Shift is 0.1 or less, each assessed at the 75th percentile of page loads and segmented between mobile and desktop. A lab tool cannot produce two of those, because nobody interacted with the page and the run stopped at load. Collect the numbers from the published page itself. A split test is a decision procedure, not a scoreboard. Fix the sample size and the stopping rule before the test starts, keep the assignment sticky per visitor, and report the interval and the count with every result. Stopping a test on the morning it looks good is how a variant with no effect gets rolled out, and this build will not have a button that does it. ### Stack (fixed, do not substitute) - Content: Pages as ordered sections in SQLite, rendered to static HTML at publish · a page that needs a server at request time is a page that can be slow at request time - Rendering: One template per section kind, server-side, no client framework on the published page · the published page should ship almost no JavaScript because that is the whole advantage - Measurement: First-party field collection from the published page using the standard web vitals library · a new page has no field data anywhere else, and lab numbers cannot produce two of the three metrics - Experiments: Deterministic bucketing from a first-party visitor identifier, decided sample size · sticky assignment and a stopping rule are what make the result mean anything - Publishing: Build into a new directory and swap, keeping previous versions · a half-uploaded page is a broken page on the only URL that matters ### Data model (create this before Phase 1) - pages(id, slug, title, status, current_version_id, created_at) - page_versions(id, page_id, ordinal, sections_json, theme_json, built_at, built_by, notes) · every publish is a version you can go back to - sections(kind, schema) · hero, features, testimonial, form, footer, each with a declared field schema - experiments(id, page_id, name, variant_a_version_id, variant_b_version_id, metric, planned_n, started_at, stopped_at, stop_reason) - assignments(visitor_id, experiment_id, variant, assigned_at) · deterministic from the identifier, written once - events(id, page_id, experiment_id, variant, kind, at, session_id) · view and conversion, nothing else - vitals(id, page_id, version_id, metric, value, rating, device, at) · one row per reported measurement from a real visit - submissions(id, page_id, version_id, fields_json, received_at, spam_score, ip_country) · form entries, kept separately from analytics - A published page is a directory of files. The database is the editor's state, not a dependency of the live page. ### Phase 1 · Sections in, static files out Build: Define each section kind with a field schema and one template. The editor is a form per section with reordering. The build writes HTML, one stylesheet, and resized images into a directory that can be served by anything. Every page version is kept. Trap: Storing the page as a blob of HTML from a rich editor means the day you change the heading style you cannot change it anywhere, because the markup is copied into every page and half of it was pasted from a document with its own inline styles. Store fields against a schema and render them, so a template change reaches every page that uses it. Done when: A page built twice from the same version produces byte-identical output, tested. Changing a section template changes every page using that section after a rebuild, tested with two pages. The built directory serves correctly from a plain static file server with no application running. Every publish creates a version row that can be rebuilt from stored fields alone. Do not build yet: No measurement, experiments, forms or domains yet. ### Phase 2 · Fast for the visitor, measured on the visit Build: Enforce a build budget: total bytes, image dimensions and the number of blocking requests. Give the largest element a preload, size every image so nothing reflows, and handle web fonts with a declared fallback plus the font metric overrides rather than hoping. Then collect Largest Contentful Paint, Interaction to Next Paint and Cumulative Layout Shift from real visits with a first-party endpoint, and show the 75th percentile split by device against the published thresholds of 2.5 seconds, 200 milliseconds and 0.1. Trap: A perfect lab score and a page that is not in any field dataset feels like proof, but a synthetic run cannot measure responsiveness because nobody clicked anything, and it only sees layout shifts above the fold during load, while a page with little traffic may never appear in a public field dataset at all because inclusion requires a minimum number of visitors that is deliberately undisclosed. Collect your own field measurements from the published page and treat the lab run as a smoke test. Done when: The build fails when total transferred bytes or the number of blocking requests passes the configured budget, tested with an oversized image. A web font swapping in does not move the text block, verified by a layout shift measurement of effectively zero on a throttled load with the metric overrides in place and a visible shift without them. Field measurements arrive from a real visit and appear at the 75th percentile split by device. Every vitals row is linked to the page version that was live when it was measured. Do not build yet: No experiments, forms or domains yet. ### Phase 3 · Publishing without a broken minute Build: Build into a new directory, verify it by requesting the page and the assets from it, then swap the served directory in one operation. Keep a stated number of previous builds and provide a one-command rollback. A build that fails its checks never becomes live. Trap: Publishing by writing files over the live directory means the page is half old and half new for the seconds it takes, and a build that dies partway leaves it broken until somebody notices, which on a campaign page means the traffic you paid for hit an unstyled page. Build elsewhere, verify, then swap, and keep the previous build to roll back to. Done when: A build killed partway leaves the live directory untouched, tested by interrupting it. The swap is a single operation and a request during the swap gets either the old page or the new one, never a mixture, tested under load. Rollback restores the previous version with one command and is exercised in the suite. A build failing a budget or a template error is never swapped in, and the failure names what went wrong. Do not build yet: No experiments, forms or domains yet. ### Phase 4 · Split tests decided in advance Build: Assign a variant by hashing a first-party visitor identifier with the experiment identifier, so assignment is deterministic and sticky without a lookup. Record the planned sample size and the metric when the experiment is created. Show progress towards that size, and report the difference with an interval and the counts. Stopping early is possible but requires a written reason stored on the experiment. Trap: Watching the dashboard and stopping the moment one variant looks ahead turns a coin-flip difference into a decision, because checking repeatedly gives you many chances to catch a random lead, and on a page with a few dozen conversions a week that lead appears constantly. Fix the sample size when you create the experiment, show progress towards it, and make any early stop record who stopped it and why. Done when: The same visitor identifier always resolves to the same variant, across restarts and without a database lookup, tested with a thousand identifiers. Assignment is balanced within a stated tolerance over ten thousand identifiers, tested. The experiment view shows progress towards the planned sample size and does not display a winner before it is reached. An early stop stores a reason and the report shows that the test was stopped early wherever the result appears. Do not build yet: No forms, domains or consent yet. ### Phase 5 · Forms, the domain, and what you keep Build: Accept form submissions to a small endpoint with size limits, a honeypot, a rate limit per address and server-side validation, and store them separately from analytics. Serve the pages over your own domain with automatic certificates. Keep the first-party measurement identifier cookie-free where you can, state a retention period for events and submissions, and implement deletion of one person's submissions on request. Trap: Putting form submissions in the same table as page events and then applying one retention rule deletes the leads along with the analytics, or keeps the analytics as long as the leads; either way somebody is unhappy and one of them is a data protection problem. Keep them apart, give each its own retention period, and write both in the README. Done when: A submission over the size limit, from a bot filling the honeypot, or beyond the rate limit is rejected with a clear response and without storing the payload, all three tested. Submissions and events have separate retention periods, both enforced by a job and both stated in the README. Deleting one person's submissions removes their fields and leaves aggregate counts intact, tested. The site serves over its own domain with a valid certificate that renews without manual work, rehearsed once. Do not build yet: Nothing after this. ### Out of scope (and why) - A general-purpose drag and drop canvas, which is a year of work and is why the templates in the paid product look consistent and yours would not. - Personalisation by audience segment, which needs visitor data you would then have to hold and justify holding. - Heatmaps and session recording, which capture more about a visitor than a landing page needs and carry their own consent question. - Multi-user editing with approvals, which turns every save into a conflict and a permission decision. ### README must contain - That published pages are static files with no runtime dependency on the editor - The Core Web Vitals thresholds used as the target, that the assessment is at the 75th percentile split by device, and that the numbers come from real visits rather than a lab run - That a new page may have no field data in any public dataset, which is why measurement is first-party - How an experiment's sample size is fixed in advance and what happens when someone stops one early - The retention period for events and for form submissions, separately, and how to delete one person's submissions
# Agent instructions · Instapage indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Pages as ordered sections in SQLite, rendered to static HTML at publish, One template per section kind, server-side, no client framework on the published page, First-party field collection from the published page using the standard web vitals library, Deterministic bucketing from a first-party visitor identifier, decided sample size, Build into a new directory and swap, keeping previous versions. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing the page as a blob of HTML from a rich editor means the day you change the heading style you cannot change it anywhere, because the markup is copied into every page and half of it was pasted from a document with its own inline styles. Store fields against a schema and render them, so a template change reaches every page that uses it. - A perfect lab score and a page that is not in any field dataset feels like proof, but a synthetic run cannot measure responsiveness because nobody clicked anything, and it only sees layout shifts above the fold during load, while a page with little traffic may never appear in a public field dataset at all because inclusion requires a minimum number of visitors that is deliberately undisclosed. Collect your own field measurements from the published page and treat the lab run as a smoke test. - Publishing by writing files over the live directory means the page is half old and half new for the seconds it takes, and a build that dies partway leaves it broken until somebody notices, which on a campaign page means the traffic you paid for hit an unstyled page. Build elsewhere, verify, then swap, and keep the previous build to roll back to. - Watching the dashboard and stopping the moment one variant looks ahead turns a coin-flip difference into a decision, because checking repeatedly gives you many chances to catch a random lead, and on a page with a few dozen conversions a week that lead appears constantly. Fix the sample size when you create the experiment, show progress towards it, and make any early stop record who stopped it and why.
# Build plan · Instapage A landing page builder whose hard problems are publishing static files without a broken minute, measuring speed from real visits rather than a lab score, and running split tests that are decided in advance instead of stopped when they look good. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Sections in, static files out A page is structured data that renders to HTML, CSS and images with no runtime. ### Steps 1. Declare a schema per section kind Fields you can validate, not markup you cannot change later. Files: `src/sections/schema.ts`, `src/sections/templates/` 2. Make the build deterministic Byte-identical output is how you know a rebuild is safe. Files: `src/build.ts`, `tests/build.test.ts` 3. Serve the output with no application That constraint is the entire performance story. Files: `scripts/serve-static.sh` ### Done when - [ ] Two builds of one version are byte-identical - [ ] A template change reaches every page using it - [ ] The output serves from a plain file server - [ ] Each publish stores a rebuildable version ### Watch out - Storing the page as a blob of HTML from a rich editor means the day you change the heading style you cannot change it anywhere, because the markup is copied into every page and half of it was pasted from a document with its own inline styles. Store fields against a schema and render them, so a template change reaches every page that uses it. ## Phase 2 · Fast for the visitor, measured on the visit The page meets the published thresholds where it counts, and the numbers come from real loads. ### Steps 1. Put a byte and request budget in the build A budget checked by a person is a budget that slips. Files: `src/budget.ts`, `tests/budget.test.ts` 2. Handle fonts with overrides and a real fallback The fallback relayout is the most common shift on a landing page. Files: `src/theme/fonts.css`, `src/build.ts` 3. Collect vitals first-party from the live page Two of the three metrics cannot come from a lab run. Files: `src/public/vitals.js`, `src/api/vitals.ts` ### Done when - [ ] The build fails on an oversized image - [ ] A font swap produces no measurable shift - [ ] Field measurements appear at the 75th percentile by device - [ ] Each measurement is tied to the live version ### Watch out - A perfect lab score and a page that is not in any field dataset feels like proof, but a synthetic run cannot measure responsiveness because nobody clicked anything, and it only sees layout shifts above the fold during load, while a page with little traffic may never appear in a public field dataset at all because inclusion requires a minimum number of visitors that is deliberately undisclosed. Collect your own field measurements from the published page and treat the lab run as a smoke test. ## Phase 3 · Publishing without a broken minute A publish is atomic and reversible, and a failed build never reaches the URL. ### Steps 1. Build to a new directory every time Never write into the directory being served. Files: `src/publish.ts` 2. Verify before swapping Request the page and its assets from the new directory first. Files: `src/publish.ts`, `tests/publish.test.ts` 3. Make rollback one command, and test it The rollback nobody has run is not a rollback. Files: `scripts/rollback.sh`, `tests/rollback.test.ts` ### Done when - [ ] An interrupted build leaves the live page untouched - [ ] The swap never serves a mixture - [ ] Rollback runs in one command and is tested - [ ] A failing build is never published ### Watch out - Publishing by writing files over the live directory means the page is half old and half new for the seconds it takes, and a build that dies partway leaves it broken until somebody notices, which on a campaign page means the traffic you paid for hit an unstyled page. Build elsewhere, verify, then swap, and keep the previous build to roll back to. ## Phase 4 · Split tests decided in advance An experiment has a sample size and a stopping rule before it starts, and the same visitor always sees the same variant. ### Steps 1. Bucket deterministically from the identifier Sticky assignment with no lookup and no cookie race. Files: `src/experiments/assign.ts`, `tests/assign.test.ts` 2. Store the planned sample size at creation Deciding it afterwards is deciding it from the data. Files: `src/experiments/create.ts` 3. Report an interval and the counts, never a bare winner And label any early stop wherever the number is shown. Files: `src/experiments/report.ts` ### Done when - [ ] One identifier always gets the same variant - [ ] Assignment is balanced over ten thousand identifiers - [ ] No winner is shown before the planned sample size - [ ] An early stop is recorded and labelled in the report ### Watch out - Watching the dashboard and stopping the moment one variant looks ahead turns a coin-flip difference into a decision, because checking repeatedly gives you many chances to catch a random lead, and on a page with a few dozen conversions a week that lead appears constantly. Fix the sample size when you create the experiment, show progress towards it, and make any early stop record who stopped it and why. ## Not in this build - A general-purpose drag and drop canvas. - Audience personalisation. - Heatmaps and session recording. - Multi-user editing and approvals. ## After v1, if you want it - A second metric per experiment, recorded but never used to declare a winner - A content import from an existing page, mapping its blocks onto your section schemas - A scheduled publish, which is the same swap with a timer and a check that the build still passes
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Editor state, versions, experiments and measurements. Not needed to serve a published page. DATABASE_PATH=./data/pages.db # Required. One directory per build. The live directory is a pointer to one of them. BUILD_DIR=./data/builds # Required. How many previous builds to retain for rollback. KEEP_BUILDS=10 # Required. Total transferred bytes for a page. The build fails above it. BUDGET_KB=180 # Required. First-party, same domain, so the measurement is not blocked as third-party. VITALS_ENDPOINT=https://yourdomain.example/v # Required. Page views and experiment events. Separate from submissions on purpose. EVENT_RETENTION_DAYS=180 # Required. Form submissions. Longer, and deleted per person on request. SUBMISSION_RETENTION_DAYS=730
# Instapage · product brief ## Problem The visible enterprise landing pages loop is buildable, but a credible replacement needs more than the first screen. Instapage earns its keep through editor, CMS, hosting, polish, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A publisher whose pages are static and fast, whose speed numbers come from real visits at the percentile the thresholds are defined at, whose publishes are atomic and reversible, and whose split tests cannot be stopped quietly on a good morning. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node.js 22 - deploy target or static hosting - user-owned domain for publishing ## Explicit non-goals for v1 - A general-purpose drag and drop canvas. - Audience personalisation. - Heatmaps and session recording. - Multi-user editing and approvals. - general-purpose visual editing - large template and component libraries - CMS collaboration and client handoff - managed hosting, forms, and analytics ## Success criteria - Two builds of one version are confirmed byte-identical - The build is confirmed to fail on an over-budget page - An interrupted publish is confirmed to leave the live page untouched - One visitor identifier is confirmed to get one variant every time - Leads and events are confirmed to have separate enforced retention
# Build brief · Instapage 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 publisher whose pages are static files, whose speed is measured on real visits, and whose split tests are decided before they start to replace Instapage. 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. ### Rule zero A landing page exists to be measured, and both of the measurements this category sells are easy to fake. So neither is allowed to be faked here. Speed is what your visitors experienced, not what your laptop scored. The Core Web Vitals thresholds are published: a good Largest Contentful Paint is 2.5 seconds or less, a good Interaction to Next Paint is 200 milliseconds or less, and a good Cumulative Layout Shift is 0.1 or less, each assessed at the 75th percentile of page loads and segmented between mobile and desktop. A lab tool cannot produce two of those, because nobody interacted with the page and the run stopped at load. Collect the numbers from the published page itself. A split test is a decision procedure, not a scoreboard. Fix the sample size and the stopping rule before the test starts, keep the assignment sticky per visitor, and report the interval and the count with every result. Stopping a test on the morning it looks good is how a variant with no effect gets rolled out, and this build will not have a button that does it. ### Stack (fixed, do not substitute) - Content: Pages as ordered sections in SQLite, rendered to static HTML at publish · a page that needs a server at request time is a page that can be slow at request time - Rendering: One template per section kind, server-side, no client framework on the published page · the published page should ship almost no JavaScript because that is the whole advantage - Measurement: First-party field collection from the published page using the standard web vitals library · a new page has no field data anywhere else, and lab numbers cannot produce two of the three metrics - Experiments: Deterministic bucketing from a first-party visitor identifier, decided sample size · sticky assignment and a stopping rule are what make the result mean anything - Publishing: Build into a new directory and swap, keeping previous versions · a half-uploaded page is a broken page on the only URL that matters ### Data model (create this before Phase 1) - pages(id, slug, title, status, current_version_id, created_at) - page_versions(id, page_id, ordinal, sections_json, theme_json, built_at, built_by, notes) · every publish is a version you can go back to - sections(kind, schema) · hero, features, testimonial, form, footer, each with a declared field schema - experiments(id, page_id, name, variant_a_version_id, variant_b_version_id, metric, planned_n, started_at, stopped_at, stop_reason) - assignments(visitor_id, experiment_id, variant, assigned_at) · deterministic from the identifier, written once - events(id, page_id, experiment_id, variant, kind, at, session_id) · view and conversion, nothing else - vitals(id, page_id, version_id, metric, value, rating, device, at) · one row per reported measurement from a real visit - submissions(id, page_id, version_id, fields_json, received_at, spam_score, ip_country) · form entries, kept separately from analytics - A published page is a directory of files. The database is the editor's state, not a dependency of the live page. ### Phase 1 · Sections in, static files out Build: Define each section kind with a field schema and one template. The editor is a form per section with reordering. The build writes HTML, one stylesheet, and resized images into a directory that can be served by anything. Every page version is kept. Trap: Storing the page as a blob of HTML from a rich editor means the day you change the heading style you cannot change it anywhere, because the markup is copied into every page and half of it was pasted from a document with its own inline styles. Store fields against a schema and render them, so a template change reaches every page that uses it. Done when: A page built twice from the same version produces byte-identical output, tested. Changing a section template changes every page using that section after a rebuild, tested with two pages. The built directory serves correctly from a plain static file server with no application running. Every publish creates a version row that can be rebuilt from stored fields alone. Do not build yet: No measurement, experiments, forms or domains yet. ### Phase 2 · Fast for the visitor, measured on the visit Build: Enforce a build budget: total bytes, image dimensions and the number of blocking requests. Give the largest element a preload, size every image so nothing reflows, and handle web fonts with a declared fallback plus the font metric overrides rather than hoping. Then collect Largest Contentful Paint, Interaction to Next Paint and Cumulative Layout Shift from real visits with a first-party endpoint, and show the 75th percentile split by device against the published thresholds of 2.5 seconds, 200 milliseconds and 0.1. Trap: A perfect lab score and a page that is not in any field dataset feels like proof, but a synthetic run cannot measure responsiveness because nobody clicked anything, and it only sees layout shifts above the fold during load, while a page with little traffic may never appear in a public field dataset at all because inclusion requires a minimum number of visitors that is deliberately undisclosed. Collect your own field measurements from the published page and treat the lab run as a smoke test. Done when: The build fails when total transferred bytes or the number of blocking requests passes the configured budget, tested with an oversized image. A web font swapping in does not move the text block, verified by a layout shift measurement of effectively zero on a throttled load with the metric overrides in place and a visible shift without them. Field measurements arrive from a real visit and appear at the 75th percentile split by device. Every vitals row is linked to the page version that was live when it was measured. Do not build yet: No experiments, forms or domains yet. ### Phase 3 · Publishing without a broken minute Build: Build into a new directory, verify it by requesting the page and the assets from it, then swap the served directory in one operation. Keep a stated number of previous builds and provide a one-command rollback. A build that fails its checks never becomes live. Trap: Publishing by writing files over the live directory means the page is half old and half new for the seconds it takes, and a build that dies partway leaves it broken until somebody notices, which on a campaign page means the traffic you paid for hit an unstyled page. Build elsewhere, verify, then swap, and keep the previous build to roll back to. Done when: A build killed partway leaves the live directory untouched, tested by interrupting it. The swap is a single operation and a request during the swap gets either the old page or the new one, never a mixture, tested under load. Rollback restores the previous version with one command and is exercised in the suite. A build failing a budget or a template error is never swapped in, and the failure names what went wrong. Do not build yet: No experiments, forms or domains yet. ### Phase 4 · Split tests decided in advance Build: Assign a variant by hashing a first-party visitor identifier with the experiment identifier, so assignment is deterministic and sticky without a lookup. Record the planned sample size and the metric when the experiment is created. Show progress towards that size, and report the difference with an interval and the counts. Stopping early is possible but requires a written reason stored on the experiment. Trap: Watching the dashboard and stopping the moment one variant looks ahead turns a coin-flip difference into a decision, because checking repeatedly gives you many chances to catch a random lead, and on a page with a few dozen conversions a week that lead appears constantly. Fix the sample size when you create the experiment, show progress towards it, and make any early stop record who stopped it and why. Done when: The same visitor identifier always resolves to the same variant, across restarts and without a database lookup, tested with a thousand identifiers. Assignment is balanced within a stated tolerance over ten thousand identifiers, tested. The experiment view shows progress towards the planned sample size and does not display a winner before it is reached. An early stop stores a reason and the report shows that the test was stopped early wherever the result appears. Do not build yet: No forms, domains or consent yet. ### Phase 5 · Forms, the domain, and what you keep Build: Accept form submissions to a small endpoint with size limits, a honeypot, a rate limit per address and server-side validation, and store them separately from analytics. Serve the pages over your own domain with automatic certificates. Keep the first-party measurement identifier cookie-free where you can, state a retention period for events and submissions, and implement deletion of one person's submissions on request. Trap: Putting form submissions in the same table as page events and then applying one retention rule deletes the leads along with the analytics, or keeps the analytics as long as the leads; either way somebody is unhappy and one of them is a data protection problem. Keep them apart, give each its own retention period, and write both in the README. Done when: A submission over the size limit, from a bot filling the honeypot, or beyond the rate limit is rejected with a clear response and without storing the payload, all three tested. Submissions and events have separate retention periods, both enforced by a job and both stated in the README. Deleting one person's submissions removes their fields and leaves aggregate counts intact, tested. The site serves over its own domain with a valid certificate that renews without manual work, rehearsed once. Do not build yet: Nothing after this. ### Out of scope (and why) - A general-purpose drag and drop canvas, which is a year of work and is why the templates in the paid product look consistent and yours would not. - Personalisation by audience segment, which needs visitor data you would then have to hold and justify holding. - Heatmaps and session recording, which capture more about a visitor than a landing page needs and carry their own consent question. - Multi-user editing with approvals, which turns every save into a conflict and a permission decision. ### README must contain - That published pages are static files with no runtime dependency on the editor - The Core Web Vitals thresholds used as the target, that the assessment is at the 75th percentile split by device, and that the numbers come from real visits rather than a lab run - That a new page may have no field data in any public dataset, which is why measurement is first-party - How an experiment's sample size is fixed in advance and what happens when someone stops one early - The retention period for events and for form submissions, separately, and how to delete one person's submissions
# Architecture · Instapage ## Stack | Part | Choice | Why | | --- | --- | --- | | Content | Pages as ordered sections in SQLite, rendered to static HTML at publish | a page that needs a server at request time is a page that can be slow at request time | | Rendering | One template per section kind, server-side, no client framework on the published page | the published page should ship almost no JavaScript because that is the whole advantage | | Measurement | First-party field collection from the published page using the standard web vitals library | a new page has no field data anywhere else, and lab numbers cannot produce two of the three metrics | | Experiments | Deterministic bucketing from a first-party visitor identifier, decided sample size | sticky assignment and a stopping rule are what make the result mean anything | | Publishing | Build into a new directory and swap, keeping previous versions | a half-uploaded page is a broken page on the only URL that matters | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Content | Sections, schemas and versions. | Any store, provided pages stay structured fields rather than markup. | | Build | Deterministic output and the budget. | Any generator, provided two builds of a version match byte for byte. | | Publish | Verify then swap, and rollback. | Any deployment, provided nothing writes into the served directory. | | Experiments | Deterministic assignment and the stopping rule. | Not swappable for a dashboard that declares winners on demand. | | Measurement | First-party vitals and event retention. | Any collector, provided it is first-party and tied to a version. | ## 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 · Editor state, versions, experiments and measurements. Not needed to serve a published page. - `BUILD_DIR` · required · One directory per build. The live directory is a pointer to one of them. - `KEEP_BUILDS` · required · How many previous builds to retain for rollback. - `BUDGET_KB` · required · Total transferred bytes for a page. The build fails above it. - `VITALS_ENDPOINT` · required · First-party, same domain, so the measurement is not blocked as third-party. - `EVENT_RETENTION_DAYS` · required · Page views and experiment events. Separate from submissions on purpose. - `SUBMISSION_RETENTION_DAYS` · required · Form submissions. Longer, and deleted per person on request. ## 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 · Instapage product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Pages as ordered sections in SQLite, rendered to static HTML at publish, One template per section kind, server-side, no client framework on the published page, First-party field collection from the published page using the standard web vitals library, Deterministic bucketing from a first-party visitor identifier, decided sample size, Build into a new directory and swap, keeping previous versions. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Storing the page as a blob of HTML from a rich editor means the day you change the heading style you cannot change it anywhere, because the markup is copied into every page and half of it was pasted from a document with its own inline styles. Store fields against a schema and render them, so a template change reaches every page that uses it. - A perfect lab score and a page that is not in any field dataset feels like proof, but a synthetic run cannot measure responsiveness because nobody clicked anything, and it only sees layout shifts above the fold during load, while a page with little traffic may never appear in a public field dataset at all because inclusion requires a minimum number of visitors that is deliberately undisclosed. Collect your own field measurements from the published page and treat the lab run as a smoke test. - Publishing by writing files over the live directory means the page is half old and half new for the seconds it takes, and a build that dies partway leaves it broken until somebody notices, which on a campaign page means the traffic you paid for hit an unstyled page. Build elsewhere, verify, then swap, and keep the previous build to roll back to. - Watching the dashboard and stopping the moment one variant looks ahead turns a coin-flip difference into a decision, because checking repeatedly gives you many chances to catch a random lead, and on a page with a few dozen conversions a week that lead appears constantly. Fix the sample size when you create the experiment, show progress towards it, and make any early stop record who stopped it and why. - Putting form submissions in the same table as page events and then applying one retention rule deletes the leads along with the analytics, or keeps the analytics as long as the leads; either way somebody is unhappy and one of them is a data protection problem. Keep them apart, give each its own retention period, and write both in the README.
# Delivery milestones · Instapage Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Sections in, static files out A page is structured data that renders to HTML, CSS and images with no runtime. ### Steps 1. Declare a schema per section kind Fields you can validate, not markup you cannot change later. Files: `src/sections/schema.ts`, `src/sections/templates/` 2. Make the build deterministic Byte-identical output is how you know a rebuild is safe. Files: `src/build.ts`, `tests/build.test.ts` 3. Serve the output with no application That constraint is the entire performance story. Files: `scripts/serve-static.sh` ### Done when - [ ] Two builds of one version are byte-identical - [ ] A template change reaches every page using it - [ ] The output serves from a plain file server - [ ] Each publish stores a rebuildable version ### Watch out - Storing the page as a blob of HTML from a rich editor means the day you change the heading style you cannot change it anywhere, because the markup is copied into every page and half of it was pasted from a document with its own inline styles. Store fields against a schema and render them, so a template change reaches every page that uses it. ## M2 · Fast for the visitor, measured on the visit The page meets the published thresholds where it counts, and the numbers come from real loads. ### Steps 1. Put a byte and request budget in the build A budget checked by a person is a budget that slips. Files: `src/budget.ts`, `tests/budget.test.ts` 2. Handle fonts with overrides and a real fallback The fallback relayout is the most common shift on a landing page. Files: `src/theme/fonts.css`, `src/build.ts` 3. Collect vitals first-party from the live page Two of the three metrics cannot come from a lab run. Files: `src/public/vitals.js`, `src/api/vitals.ts` ### Done when - [ ] The build fails on an oversized image - [ ] A font swap produces no measurable shift - [ ] Field measurements appear at the 75th percentile by device - [ ] Each measurement is tied to the live version ### Watch out - A perfect lab score and a page that is not in any field dataset feels like proof, but a synthetic run cannot measure responsiveness because nobody clicked anything, and it only sees layout shifts above the fold during load, while a page with little traffic may never appear in a public field dataset at all because inclusion requires a minimum number of visitors that is deliberately undisclosed. Collect your own field measurements from the published page and treat the lab run as a smoke test. ## M3 · Publishing without a broken minute A publish is atomic and reversible, and a failed build never reaches the URL. ### Steps 1. Build to a new directory every time Never write into the directory being served. Files: `src/publish.ts` 2. Verify before swapping Request the page and its assets from the new directory first. Files: `src/publish.ts`, `tests/publish.test.ts` 3. Make rollback one command, and test it The rollback nobody has run is not a rollback. Files: `scripts/rollback.sh`, `tests/rollback.test.ts` ### Done when - [ ] An interrupted build leaves the live page untouched - [ ] The swap never serves a mixture - [ ] Rollback runs in one command and is tested - [ ] A failing build is never published ### Watch out - Publishing by writing files over the live directory means the page is half old and half new for the seconds it takes, and a build that dies partway leaves it broken until somebody notices, which on a campaign page means the traffic you paid for hit an unstyled page. Build elsewhere, verify, then swap, and keep the previous build to roll back to. ## M4 · Split tests decided in advance An experiment has a sample size and a stopping rule before it starts, and the same visitor always sees the same variant. ### Steps 1. Bucket deterministically from the identifier Sticky assignment with no lookup and no cookie race. Files: `src/experiments/assign.ts`, `tests/assign.test.ts` 2. Store the planned sample size at creation Deciding it afterwards is deciding it from the data. Files: `src/experiments/create.ts` 3. Report an interval and the counts, never a bare winner And label any early stop wherever the number is shown. Files: `src/experiments/report.ts` ### Done when - [ ] One identifier always gets the same variant - [ ] Assignment is balanced over ten thousand identifiers - [ ] No winner is shown before the planned sample size - [ ] An early stop is recorded and labelled in the report ### Watch out - Watching the dashboard and stopping the moment one variant looks ahead turns a coin-flip difference into a decision, because checking repeatedly gives you many chances to catch a random lead, and on a page with a few dozen conversions a week that lead appears constantly. Fix the sample size when you create the experiment, show progress towards it, and make any early stop record who stopped it and why. ## M5 · Forms, the domain, and what you keep (production only) The page collects leads safely on your own domain, and every visitor record has a stated life. ### Steps 1. Separate leads from analytics at the table level One retention rule cannot serve both. Files: `src/forms.ts`, `src/retention.ts` 2. Reject bad submissions before storing them Size, honeypot, rate limit, then validation. Files: `src/forms.ts`, `tests/forms.test.ts` 3. Make certificate renewal automatic and check it A campaign page with an expired certificate converts at zero. Files: `docs/domain.md` ### Done when - [ ] Oversized, honeypot and rate-limited submissions are rejected unstored - [ ] Leads and events have separate enforced retention - [ ] Per-person deletion leaves aggregates intact - [ ] Certificates renew without manual work ### Watch out - Putting form submissions in the same table as page events and then applying one retention rule deletes the leads along with the analytics, or keeps the analytics as long as the leads; either way somebody is unhappy and one of them is a data protection problem. Keep them apart, give each its own retention period, and write both in the README.
# Operations · Instapage ## Backup The database and the current build directory, plus submissions, which are the only part nobody can regenerate. A page can be rebuilt; a lead cannot. ## Restore Restore the database, rebuild the current version, verify it matches the retained build byte for byte, then swap it in. That comparison is also the test that the build is deterministic. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring The 75th percentile of each metric per page and device, the age of the live build, failed builds, and submission volume against the rate limit. A sudden drop in submissions is usually a broken form rather than a bad week. ## Incident checklist A slow page is diagnosed from the vitals rows against the version that was live, which is why they carry a version. A broken publish is one rollback command, which is why the previous builds are retained. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Two builds of one version are confirmed byte-identical - [ ] The build is confirmed to fail on an over-budget page - [ ] An interrupted publish is confirmed to leave the live page untouched - [ ] One visitor identifier is confirmed to get one variant every time - [ ] Leads and events are confirmed to have separate enforced retention ## Launch constraint Do not market omitted Instapage 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. Editor state, versions, experiments and measurements. Not needed to serve a published page. DATABASE_PATH=./data/pages.db # Required. One directory per build. The live directory is a pointer to one of them. BUILD_DIR=./data/builds # Required. How many previous builds to retain for rollback. KEEP_BUILDS=10 # Required. Total transferred bytes for a page. The build fails above it. BUDGET_KB=180 # Required. First-party, same domain, so the measurement is not blocked as third-party. VITALS_ENDPOINT=https://yourdomain.example/v # Required. Page views and experiment events. Separate from submissions on purpose. EVENT_RETENTION_DAYS=180 # Required. Form submissions. Longer, and deleted per person on request. SUBMISSION_RETENTION_DAYS=730
$ choose a build depth, inspect the files, then open the complete pack in your agent
Instapage: A single site is easy to code. People pay for safe ongoing edits, templates, publishing, domains, collaboration, and no-maintenance hosting.
xgeneral-purpose visual editing
xlarge template and component libraries
xCMS collaboration and client handoff
xmanaged hosting, forms, and analytics
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Instapage pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| create | $99/workspace | $79/workspace | 15,000 unique visitors/month; unlimited pages/conversions/contacts; 10 team members; 2 subdomains; 1 workspace; 15,000 email sends/month |
| optimize | $199/workspace | $159/workspace | 30,000 unique visitors/month at base usage; unlimited pages/conversions/contacts; 10 team members; 2 subdomains; 1 workspace; 30,000 email sends/month |
| convert | custom | — | Custom visitors, team/workspace limits and enterprise features; root-domain publishing, heatmaps, global elements, SLA and services |
free tierno free tier; 14-day Create/Optimize trial capped at 2,500 unique visitors and requiring a credit card
billingmonthly + annual (up to 20% off); custom usage above the expanded Optimize tier is annual-contract-only
hidden costsOptimize pricing rises with visitor volume (30,000 base, 50,000 expanded, then custom); hitting the 2,500-visitor trial cap ends the trial and transitions to the selected paid plan.
verified 2026-08-14 · source ↗
Vibecode Instapage
Kinda. The core of Instapage is buildable in a weekend with the prompt on this page, but there are real gaps: general-purpose visual editing, large template and component libraries. Read the honest list above before committing.
How much does Instapage cost?
Instapage costs about $99/month (Create, checked 2026-07-31), which is $1188 per year.
What do I lose by replacing Instapage?
Honestly: general-purpose visual editing; large template and component libraries; CMS collaboration and client handoff; managed hosting, forms, and analytics. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Instapage?
Yes: Mautic (Landing pages, forms, dynamic content, analytics and real split tests for free; in return, you operate a marketing database.) systeme.io (Three funnels, a custom domain, forms and one split test for zero dollars; personalization is basic.) The prompt is for when you want it exactly your way.