Vibecode Siter.io
track this build4 phases, 12 steps, beginner friendly0%The visible collaborative website builder loop is buildable, but a credible replacement needs more than the first screen. Siter.io 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 Siter.io. 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 ===== # Siter.io · indie build A structured site builder whose hard problems are publishing a whole site atomically with a way back, connecting a bare apex domain that the DNS standard will not let you point with a CNAME, and never letting editable content become executable markup. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js 15 with TypeScript for the editor, running locally or on one small box | the editor is private, the output is not | | Content | Typed section records, never HTML strings | a stored HTML blob cannot be restyled and can be a script tag | | Output | A static build written to a versioned directory, then a pointer flip | half-published is worse than not published | | Assets | Content-hashed filenames with long cache lifetimes | the file whose name changes is the file that updates | | Storage | SQLite for sites, pages and sections; files on disk for media | one file to back up, plus a media folder | ## 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 static build. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **Somewhere to serve static files** · free to a few pounds a month - Why: The output is plain files, and the whole publish design assumes a host that serves a directory and lets you swap which directory is live. - Get it: Object storage with a static website mode, a small VPS with a web server, or any static host. - Verify: A directory of files you upload is reachable over HTTPS - [ ] **A domain you control the DNS for** · around ten pounds a year - Why: The apex versus subdomain problem is the phase that most needs a real zone, and a test domain with a provider you can log into is the only way to exercise it. - Get it: Any registrar, ideally one whose DNS interface you can reach quickly while testing. - Verify: You can add and remove records and see them resolve - [ ] **Where contact form submissions go** · free to a few pounds a month - Why: This app deliberately is not a form backend, so you need to decide the destination before writing the contact section rather than inventing a database table for it later. - Get it: Choose a mail address, a form service or an endpoint you already run, and write it down. - Verify: A test submission arrives at the chosen destination ## 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 freeform visual editor. - Figma import. - A template marketplace. - Certificate issuance and edge hosting. - 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 Siter.io, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Siter.io 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 section-based site builder that publishes static files to a domain you actually own to replace Siter.io. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Next.js 15 with TypeScript for the editor, running locally or on one small box · the editor is private, the output is not - Content: Typed section records, never HTML strings · a stored HTML blob cannot be restyled and can be a script tag - Output: A static build written to a versioned directory, then a pointer flip · half-published is worse than not published - Assets: Content-hashed filenames with long cache lifetimes · the file whose name changes is the file that updates - Storage: SQLite for sites, pages and sections; files on disk for media · one file to back up, plus a media folder ### Data model (create this before Phase 1) - sites(id, name, primary_host, www_redirect, created_at, published_version_id) - pages(site_id, id, path, title, description, noindex, ordinal) · path is the route, and uniqueness is enforced - sections(page_id, id, kind, ordinal, data_json) · kind names a renderer, data_json is typed per kind, and neither holds markup - assets(site_id, id, original_name, sha256, ext, width, height, bytes) · served as content-hashed filenames - versions(site_id, id, built_at, output_dir, file_count, bytes, built_from_revision) · every publish, kept for rollback - domains(site_id, host, kind, verification_token, dns_checked_at, dns_state, notes) · kind is apex or subdomain, which changes the records required - The section data is the document and the HTML is derived, so changing a renderer restyles every page that uses it without touching content. ### Phase 1 · Sections as data, renderers as code Build: Define six section kinds with a schema each: hero, text, image, gallery, features and contact. Store only their fields. Render each through a component that escapes every value. Rich text is a restricted structured format, not an HTML string, with an allow list of marks. Trap: Storing each section as a blob of HTML feels like a shortcut and costs you both halves of the product at once: you cannot change a heading style across the site without a find and replace over content, and a paste from a document carries markup straight into the published page. Store fields, render through components, and never persist markup. Done when: No table column contains HTML, verified by a test that scans stored section data for angle brackets in text fields. Changing a renderer's markup updates every page using that section kind without a content migration, demonstrated. A pasted value containing a script tag renders as visible text in the published output, tested. Adding a seventh section kind requires no change to the page, publish or asset code. Do not build yet: No preview, build, publish or domains yet. ### Phase 2 · A preview that is actually that width Build: Render the preview in an iframe set to the real pixel width of the device you are previewing, then scale the iframe visually to fit the editor. Never scale by shrinking the layout itself. Offer three widths and let the user type one. Trap: Simulating a narrow screen by squashing the preview element leaves the document's viewport width unchanged, so the mobile breakpoint never fires and the page looks perfect in the editor and wrong on a phone. Set the iframe's actual width to the target, then scale the whole iframe with a transform for display. Done when: A media query written at a stated breakpoint activates in the preview at exactly the width it will activate in a browser, tested by reading the reported viewport width inside the iframe. Switching preview widths does not re-mount the editor or lose unsaved edits. A page with an image gallery reflows at each preview width the same way it does in a real browser at that width, checked side by side once. Typing a custom width is honoured. Do not build yet: No build output, publishing or domains yet. ### Phase 3 · Publish all of it or none of it Build: Build the whole site into a fresh directory named by version. Write content-hashed asset filenames. Verify the output, then flip a single pointer to the new directory. Keep the previous versions and expose a one-click rollback. Trap: Publishing by overwriting the live directory file by file leaves visitors mid-publish seeing new pages referencing assets not yet uploaded, and a failure halfway leaves the site permanently in that state with no record of what changed. Build into a new directory, verify it, flip the pointer, and keep the old one so rollback is a flip rather than a rebuild. Done when: Interrupting a publish leaves the live site serving the previous version unchanged, tested by killing the process mid-build. Every internal link and asset reference in the built output resolves to a file that exists in that version, checked by a link crawl over the output directory before the pointer flips. Rolling back restores the previous version without rebuilding, and the version list shows what was live when. Asset filenames change when their contents change and never otherwise. Do not build yet: No domains, forms or export yet. ### Phase 4 · The apex domain problem, explained properly Build: Ask whether the host is a bare apex or a subdomain, because they need different records. RFC 1034 section 3.6.2 states that if a CNAME record is present at a node, no other data should be present (checked against the RFC), and an apex necessarily carries its zone's SOA and NS records, so a bare domain cannot be pointed with a plain CNAME. Give apex users address records or an alias record their provider supports, and offer the www host with a redirect as the simple path. Verify by resolving the records and show what you actually saw. Trap: Telling every user to add a CNAME works for www and fails silently for the bare domain, and because some providers accept the record and then break mail or the whole zone, the user's first symptom is unrelated and awful. Detect apex versus subdomain, give the right instruction for each, and refuse to claim the domain is connected until you have resolved the expected records yourself. Done when: Choosing a bare apex host produces apex-appropriate instructions and never suggests a plain CNAME, tested. The verification step resolves the records and displays what was found alongside what was expected, rather than a pass or fail with no detail. A domain that does not yet resolve stays in a pending state with the last check time shown and retries on a schedule. The www redirect option is offered and, when chosen, is generated in the published output. Do not build yet: No forms, export or error pages yet. ### Phase 5 · Leaving with everything Build: Export a published version as a folder of plain files that any static host will serve, plus a JSON dump of pages, sections and assets. Generate a 404 page and a sitemap. Handle contact forms by writing to a mail address or an endpoint the user names, with a plain statement that this app is not a form backend. Trap: A builder whose export is a rendered snapshot but not the content behind it leaves the user able to host their site once and never edit it again, which is exactly the trap a paid builder is accused of. Export both the built files and the structured content, and document the content format well enough that a different renderer could consume it. Done when: The exported folder serves correctly from a plain static file server with no rewrites configured, tested. The JSON dump contains every page, section and asset reference, and re-importing it into a fresh install reproduces the site, tested. A 404 page and a sitemap are generated for every published version. The README states plainly that form submissions go to a service the user controls and that this application does not store them. Do not build yet: Nothing after this. ### Out of scope (and why) - A freeform visual editor where anything can be dragged anywhere, which is a different and much larger product and is where the paid builders spend their years. - Figma import, because translating absolute frame coordinates into a layout that survives a narrow screen is guesswork dressed as a feature. - A template and component marketplace, which is licensed content and a review process rather than code. - Issuing TLS certificates and running the edge, which your static host already does better than you will. ### README must contain - That sections are stored as typed data and never as HTML, and what that buys you - That a publish builds a new version and flips a pointer, and how to roll back - Why a bare apex domain cannot take a plain CNAME, citing RFC 1034 section 3.6.2, and what to use instead - That domain verification resolves the records and shows what it found - That contact forms post to a service the user controls, and this app stores nothing ===== AGENTS.md ===== # Agent instructions · Siter.io indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js 15 with TypeScript for the editor, running locally or on one small box, Typed section records, never HTML strings, A static build written to a versioned directory, then a pointer flip, Content-hashed filenames with long cache lifetimes, SQLite for sites, pages and sections; files on disk for media. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing each section as a blob of HTML feels like a shortcut and costs you both halves of the product at once: you cannot change a heading style across the site without a find and replace over content, and a paste from a document carries markup straight into the published page. Store fields, render through components, and never persist markup. - Simulating a narrow screen by squashing the preview element leaves the document's viewport width unchanged, so the mobile breakpoint never fires and the page looks perfect in the editor and wrong on a phone. Set the iframe's actual width to the target, then scale the whole iframe with a transform for display. - Publishing by overwriting the live directory file by file leaves visitors mid-publish seeing new pages referencing assets not yet uploaded, and a failure halfway leaves the site permanently in that state with no record of what changed. Build into a new directory, verify it, flip the pointer, and keep the old one so rollback is a flip rather than a rebuild. - Telling every user to add a CNAME works for www and fails silently for the bare domain, and because some providers accept the record and then break mail or the whole zone, the user's first symptom is unrelated and awful. Detect apex versus subdomain, give the right instruction for each, and refuse to claim the domain is connected until you have resolved the expected records yourself. ===== BUILD_PLAN.md ===== # Build plan · Siter.io A structured site builder whose hard problems are publishing a whole site atomically with a way back, connecting a bare apex domain that the DNS standard will not let you point with a CNAME, and never letting editable content become executable markup. 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 as data, renderers as code A page is a list of typed section records, and no user input is ever stored as markup. ### Steps 1. Give each section kind a schema The schema is what lets the renderer change independently. Files: `src/sections/schema.ts`, `src/sections/hero.tsx` 2. Restrict rich text to marks you render An allow list of marks is a format; arbitrary HTML is a liability. Files: `src/richtext.ts` 3. Scan stored content for markup in tests The cheapest guard against the shortcut creeping back in. Files: `tests/no-stored-markup.test.ts` ### Done when - [ ] No stored field contains markup, asserted by a scan - [ ] A renderer change restyles existing pages - [ ] A pasted script tag renders as text - [ ] A new section kind touches only its own files ### Watch out - Storing each section as a blob of HTML feels like a shortcut and costs you both halves of the product at once: you cannot change a heading style across the site without a find and replace over content, and a paste from a document carries markup straight into the published page. Store fields, render through components, and never persist markup. ## Phase 2 · A preview that is actually that width The responsive preview reports the viewport width it claims, so breakpoints fire where they will in the browser. ### Steps 1. Set the iframe's real width, scale for display The viewport width is the only thing breakpoints listen to. Files: `src/editor/Preview.tsx` 2. Report the inner viewport width Show the number so a wrong preview is visible, not suspected. Files: `src/editor/Preview.tsx` 3. Compare once against a real browser One manual check that the simulation is honest. Files: `docs/preview-verification.md` ### Done when - [ ] Breakpoints fire at the true width in preview - [ ] Switching widths preserves unsaved edits - [ ] Gallery reflow matches a real browser at the same width - [ ] A custom width is honoured ### Watch out - Simulating a narrow screen by squashing the preview element leaves the document's viewport width unchanged, so the mobile breakpoint never fires and the page looks perfect in the editor and wrong on a phone. Set the iframe's actual width to the target, then scale the whole iframe with a transform for display. ## Phase 3 · Publish all of it or none of it A publish is a new immutable version and the live site flips between versions in one step. ### Steps 1. Build into a version directory Immutable output is what makes rollback trivial. Files: `src/publish/build.ts` 2. Crawl the output before flipping A broken link found in a directory beats one found by a visitor. Files: `src/publish/verify.ts`, `tests/link-crawl.test.ts` 3. Flip one pointer, keep the old version The whole atomicity story in one operation. Files: `src/publish/activate.ts` ### Done when - [ ] An interrupted publish leaves the live version untouched - [ ] The link crawl passes before the pointer flips - [ ] Rollback restores a previous version without rebuilding - [ ] Asset names change only when contents change ### Watch out - Publishing by overwriting the live directory file by file leaves visitors mid-publish seeing new pages referencing assets not yet uploaded, and a failure halfway leaves the site permanently in that state with no record of what changed. Build into a new directory, verify it, flip the pointer, and keep the old one so rollback is a flip rather than a rebuild. ## Phase 4 · The apex domain problem, explained properly Connecting a domain succeeds or explains exactly which record to add, including the case the DNS standard forbids. ### Steps 1. Branch on apex versus subdomain They are different problems with different records. Files: `src/domains/instructions.ts` 2. Resolve and show what you saw DNS advice without a lookup is astrology. Files: `src/domains/verify.ts` 3. Generate the www redirect when chosen The simplest correct answer for most people. Files: `src/publish/redirects.ts` ### Done when - [ ] An apex host never gets plain CNAME instructions - [ ] Verification shows found versus expected records - [ ] A pending domain shows its last check and retries - [ ] The www redirect appears in the published output ### Watch out - Telling every user to add a CNAME works for www and fails silently for the bare domain, and because some providers accept the record and then break mail or the whole zone, the user's first symptom is unrelated and awful. Detect apex versus subdomain, give the right instruction for each, and refuse to claim the domain is connected until you have resolved the expected records yourself. ## Not in this build - A freeform visual editor. - Figma import. - A template marketplace. - Certificate issuance and edge hosting. ## After v1, if you want it - A second theme applied to the same content, which the section model already makes possible - Scheduled publishing, which is a timer on an operation that is already atomic - A read-only share link for a version, useful for client review before the pointer flips ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Sites, pages, sections, versions and domains. DATABASE_PATH=./data/sites.db # Required. One directory per version. The live pointer sits here too. OUTPUT_ROOT=./data/published # Required. Uploaded media, referenced by content hash. ASSET_DIR=./data/assets # Required. How many published versions to retain for rollback. KEEP_VERSIONS=10 # Required. The editor is private. Only the output is public. EDITOR_BIND=127.0.0.1:3000 # Optional. Where contact sections post. Unset means the contact section shows an address instead. FORM_ENDPOINT=https://forms.example.com/f/abc123
You are building a lean indie version of Siter.io. 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 ===== # Siter.io · indie build A structured site builder whose hard problems are publishing a whole site atomically with a way back, connecting a bare apex domain that the DNS standard will not let you point with a CNAME, and never letting editable content become executable markup. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js 15 with TypeScript for the editor, running locally or on one small box | the editor is private, the output is not | | Content | Typed section records, never HTML strings | a stored HTML blob cannot be restyled and can be a script tag | | Output | A static build written to a versioned directory, then a pointer flip | half-published is worse than not published | | Assets | Content-hashed filenames with long cache lifetimes | the file whose name changes is the file that updates | | Storage | SQLite for sites, pages and sections; files on disk for media | one file to back up, plus a media folder | ## 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 static build. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **Somewhere to serve static files** · free to a few pounds a month - Why: The output is plain files, and the whole publish design assumes a host that serves a directory and lets you swap which directory is live. - Get it: Object storage with a static website mode, a small VPS with a web server, or any static host. - Verify: A directory of files you upload is reachable over HTTPS - [ ] **A domain you control the DNS for** · around ten pounds a year - Why: The apex versus subdomain problem is the phase that most needs a real zone, and a test domain with a provider you can log into is the only way to exercise it. - Get it: Any registrar, ideally one whose DNS interface you can reach quickly while testing. - Verify: You can add and remove records and see them resolve - [ ] **Where contact form submissions go** · free to a few pounds a month - Why: This app deliberately is not a form backend, so you need to decide the destination before writing the contact section rather than inventing a database table for it later. - Get it: Choose a mail address, a form service or an endpoint you already run, and write it down. - Verify: A test submission arrives at the chosen destination ## 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 freeform visual editor. - Figma import. - A template marketplace. - Certificate issuance and edge hosting. - 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 Siter.io, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Siter.io 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 section-based site builder that publishes static files to a domain you actually own to replace Siter.io. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Next.js 15 with TypeScript for the editor, running locally or on one small box · the editor is private, the output is not - Content: Typed section records, never HTML strings · a stored HTML blob cannot be restyled and can be a script tag - Output: A static build written to a versioned directory, then a pointer flip · half-published is worse than not published - Assets: Content-hashed filenames with long cache lifetimes · the file whose name changes is the file that updates - Storage: SQLite for sites, pages and sections; files on disk for media · one file to back up, plus a media folder ### Data model (create this before Phase 1) - sites(id, name, primary_host, www_redirect, created_at, published_version_id) - pages(site_id, id, path, title, description, noindex, ordinal) · path is the route, and uniqueness is enforced - sections(page_id, id, kind, ordinal, data_json) · kind names a renderer, data_json is typed per kind, and neither holds markup - assets(site_id, id, original_name, sha256, ext, width, height, bytes) · served as content-hashed filenames - versions(site_id, id, built_at, output_dir, file_count, bytes, built_from_revision) · every publish, kept for rollback - domains(site_id, host, kind, verification_token, dns_checked_at, dns_state, notes) · kind is apex or subdomain, which changes the records required - The section data is the document and the HTML is derived, so changing a renderer restyles every page that uses it without touching content. ### Phase 1 · Sections as data, renderers as code Build: Define six section kinds with a schema each: hero, text, image, gallery, features and contact. Store only their fields. Render each through a component that escapes every value. Rich text is a restricted structured format, not an HTML string, with an allow list of marks. Trap: Storing each section as a blob of HTML feels like a shortcut and costs you both halves of the product at once: you cannot change a heading style across the site without a find and replace over content, and a paste from a document carries markup straight into the published page. Store fields, render through components, and never persist markup. Done when: No table column contains HTML, verified by a test that scans stored section data for angle brackets in text fields. Changing a renderer's markup updates every page using that section kind without a content migration, demonstrated. A pasted value containing a script tag renders as visible text in the published output, tested. Adding a seventh section kind requires no change to the page, publish or asset code. Do not build yet: No preview, build, publish or domains yet. ### Phase 2 · A preview that is actually that width Build: Render the preview in an iframe set to the real pixel width of the device you are previewing, then scale the iframe visually to fit the editor. Never scale by shrinking the layout itself. Offer three widths and let the user type one. Trap: Simulating a narrow screen by squashing the preview element leaves the document's viewport width unchanged, so the mobile breakpoint never fires and the page looks perfect in the editor and wrong on a phone. Set the iframe's actual width to the target, then scale the whole iframe with a transform for display. Done when: A media query written at a stated breakpoint activates in the preview at exactly the width it will activate in a browser, tested by reading the reported viewport width inside the iframe. Switching preview widths does not re-mount the editor or lose unsaved edits. A page with an image gallery reflows at each preview width the same way it does in a real browser at that width, checked side by side once. Typing a custom width is honoured. Do not build yet: No build output, publishing or domains yet. ### Phase 3 · Publish all of it or none of it Build: Build the whole site into a fresh directory named by version. Write content-hashed asset filenames. Verify the output, then flip a single pointer to the new directory. Keep the previous versions and expose a one-click rollback. Trap: Publishing by overwriting the live directory file by file leaves visitors mid-publish seeing new pages referencing assets not yet uploaded, and a failure halfway leaves the site permanently in that state with no record of what changed. Build into a new directory, verify it, flip the pointer, and keep the old one so rollback is a flip rather than a rebuild. Done when: Interrupting a publish leaves the live site serving the previous version unchanged, tested by killing the process mid-build. Every internal link and asset reference in the built output resolves to a file that exists in that version, checked by a link crawl over the output directory before the pointer flips. Rolling back restores the previous version without rebuilding, and the version list shows what was live when. Asset filenames change when their contents change and never otherwise. Do not build yet: No domains, forms or export yet. ### Phase 4 · The apex domain problem, explained properly Build: Ask whether the host is a bare apex or a subdomain, because they need different records. RFC 1034 section 3.6.2 states that if a CNAME record is present at a node, no other data should be present (checked against the RFC), and an apex necessarily carries its zone's SOA and NS records, so a bare domain cannot be pointed with a plain CNAME. Give apex users address records or an alias record their provider supports, and offer the www host with a redirect as the simple path. Verify by resolving the records and show what you actually saw. Trap: Telling every user to add a CNAME works for www and fails silently for the bare domain, and because some providers accept the record and then break mail or the whole zone, the user's first symptom is unrelated and awful. Detect apex versus subdomain, give the right instruction for each, and refuse to claim the domain is connected until you have resolved the expected records yourself. Done when: Choosing a bare apex host produces apex-appropriate instructions and never suggests a plain CNAME, tested. The verification step resolves the records and displays what was found alongside what was expected, rather than a pass or fail with no detail. A domain that does not yet resolve stays in a pending state with the last check time shown and retries on a schedule. The www redirect option is offered and, when chosen, is generated in the published output. Do not build yet: No forms, export or error pages yet. ### Phase 5 · Leaving with everything Build: Export a published version as a folder of plain files that any static host will serve, plus a JSON dump of pages, sections and assets. Generate a 404 page and a sitemap. Handle contact forms by writing to a mail address or an endpoint the user names, with a plain statement that this app is not a form backend. Trap: A builder whose export is a rendered snapshot but not the content behind it leaves the user able to host their site once and never edit it again, which is exactly the trap a paid builder is accused of. Export both the built files and the structured content, and document the content format well enough that a different renderer could consume it. Done when: The exported folder serves correctly from a plain static file server with no rewrites configured, tested. The JSON dump contains every page, section and asset reference, and re-importing it into a fresh install reproduces the site, tested. A 404 page and a sitemap are generated for every published version. The README states plainly that form submissions go to a service the user controls and that this application does not store them. Do not build yet: Nothing after this. ### Out of scope (and why) - A freeform visual editor where anything can be dragged anywhere, which is a different and much larger product and is where the paid builders spend their years. - Figma import, because translating absolute frame coordinates into a layout that survives a narrow screen is guesswork dressed as a feature. - A template and component marketplace, which is licensed content and a review process rather than code. - Issuing TLS certificates and running the edge, which your static host already does better than you will. ### README must contain - That sections are stored as typed data and never as HTML, and what that buys you - That a publish builds a new version and flips a pointer, and how to roll back - Why a bare apex domain cannot take a plain CNAME, citing RFC 1034 section 3.6.2, and what to use instead - That domain verification resolves the records and shows what it found - That contact forms post to a service the user controls, and this app stores nothing ===== AGENTS.md ===== # Agent instructions · Siter.io indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js 15 with TypeScript for the editor, running locally or on one small box, Typed section records, never HTML strings, A static build written to a versioned directory, then a pointer flip, Content-hashed filenames with long cache lifetimes, SQLite for sites, pages and sections; files on disk for media. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing each section as a blob of HTML feels like a shortcut and costs you both halves of the product at once: you cannot change a heading style across the site without a find and replace over content, and a paste from a document carries markup straight into the published page. Store fields, render through components, and never persist markup. - Simulating a narrow screen by squashing the preview element leaves the document's viewport width unchanged, so the mobile breakpoint never fires and the page looks perfect in the editor and wrong on a phone. Set the iframe's actual width to the target, then scale the whole iframe with a transform for display. - Publishing by overwriting the live directory file by file leaves visitors mid-publish seeing new pages referencing assets not yet uploaded, and a failure halfway leaves the site permanently in that state with no record of what changed. Build into a new directory, verify it, flip the pointer, and keep the old one so rollback is a flip rather than a rebuild. - Telling every user to add a CNAME works for www and fails silently for the bare domain, and because some providers accept the record and then break mail or the whole zone, the user's first symptom is unrelated and awful. Detect apex versus subdomain, give the right instruction for each, and refuse to claim the domain is connected until you have resolved the expected records yourself. ===== BUILD_PLAN.md ===== # Build plan · Siter.io A structured site builder whose hard problems are publishing a whole site atomically with a way back, connecting a bare apex domain that the DNS standard will not let you point with a CNAME, and never letting editable content become executable markup. 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 as data, renderers as code A page is a list of typed section records, and no user input is ever stored as markup. ### Steps 1. Give each section kind a schema The schema is what lets the renderer change independently. Files: `src/sections/schema.ts`, `src/sections/hero.tsx` 2. Restrict rich text to marks you render An allow list of marks is a format; arbitrary HTML is a liability. Files: `src/richtext.ts` 3. Scan stored content for markup in tests The cheapest guard against the shortcut creeping back in. Files: `tests/no-stored-markup.test.ts` ### Done when - [ ] No stored field contains markup, asserted by a scan - [ ] A renderer change restyles existing pages - [ ] A pasted script tag renders as text - [ ] A new section kind touches only its own files ### Watch out - Storing each section as a blob of HTML feels like a shortcut and costs you both halves of the product at once: you cannot change a heading style across the site without a find and replace over content, and a paste from a document carries markup straight into the published page. Store fields, render through components, and never persist markup. ## Phase 2 · A preview that is actually that width The responsive preview reports the viewport width it claims, so breakpoints fire where they will in the browser. ### Steps 1. Set the iframe's real width, scale for display The viewport width is the only thing breakpoints listen to. Files: `src/editor/Preview.tsx` 2. Report the inner viewport width Show the number so a wrong preview is visible, not suspected. Files: `src/editor/Preview.tsx` 3. Compare once against a real browser One manual check that the simulation is honest. Files: `docs/preview-verification.md` ### Done when - [ ] Breakpoints fire at the true width in preview - [ ] Switching widths preserves unsaved edits - [ ] Gallery reflow matches a real browser at the same width - [ ] A custom width is honoured ### Watch out - Simulating a narrow screen by squashing the preview element leaves the document's viewport width unchanged, so the mobile breakpoint never fires and the page looks perfect in the editor and wrong on a phone. Set the iframe's actual width to the target, then scale the whole iframe with a transform for display. ## Phase 3 · Publish all of it or none of it A publish is a new immutable version and the live site flips between versions in one step. ### Steps 1. Build into a version directory Immutable output is what makes rollback trivial. Files: `src/publish/build.ts` 2. Crawl the output before flipping A broken link found in a directory beats one found by a visitor. Files: `src/publish/verify.ts`, `tests/link-crawl.test.ts` 3. Flip one pointer, keep the old version The whole atomicity story in one operation. Files: `src/publish/activate.ts` ### Done when - [ ] An interrupted publish leaves the live version untouched - [ ] The link crawl passes before the pointer flips - [ ] Rollback restores a previous version without rebuilding - [ ] Asset names change only when contents change ### Watch out - Publishing by overwriting the live directory file by file leaves visitors mid-publish seeing new pages referencing assets not yet uploaded, and a failure halfway leaves the site permanently in that state with no record of what changed. Build into a new directory, verify it, flip the pointer, and keep the old one so rollback is a flip rather than a rebuild. ## Phase 4 · The apex domain problem, explained properly Connecting a domain succeeds or explains exactly which record to add, including the case the DNS standard forbids. ### Steps 1. Branch on apex versus subdomain They are different problems with different records. Files: `src/domains/instructions.ts` 2. Resolve and show what you saw DNS advice without a lookup is astrology. Files: `src/domains/verify.ts` 3. Generate the www redirect when chosen The simplest correct answer for most people. Files: `src/publish/redirects.ts` ### Done when - [ ] An apex host never gets plain CNAME instructions - [ ] Verification shows found versus expected records - [ ] A pending domain shows its last check and retries - [ ] The www redirect appears in the published output ### Watch out - Telling every user to add a CNAME works for www and fails silently for the bare domain, and because some providers accept the record and then break mail or the whole zone, the user's first symptom is unrelated and awful. Detect apex versus subdomain, give the right instruction for each, and refuse to claim the domain is connected until you have resolved the expected records yourself. ## Not in this build - A freeform visual editor. - Figma import. - A template marketplace. - Certificate issuance and edge hosting. ## After v1, if you want it - A second theme applied to the same content, which the section model already makes possible - Scheduled publishing, which is a timer on an operation that is already atomic - A read-only share link for a version, useful for client review before the pointer flips ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Sites, pages, sections, versions and domains. DATABASE_PATH=./data/sites.db # Required. One directory per version. The live pointer sits here too. OUTPUT_ROOT=./data/published # Required. Uploaded media, referenced by content hash. ASSET_DIR=./data/assets # Required. How many published versions to retain for rollback. KEEP_VERSIONS=10 # Required. The editor is private. Only the output is public. EDITOR_BIND=127.0.0.1:3000 # Optional. Where contact sections post. Unset means the contact section shows an address instead. FORM_ENDPOINT=https://forms.example.com/f/abc123
You are building a production product version of Siter.io. 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 ===== # Siter.io · product brief ## Problem The visible collaborative website builder loop is buildable, but a credible replacement needs more than the first screen. Siter.io earns its keep through editor, CMS, hosting, polish, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A builder where content outlives its renderer, a publish is all or nothing with a rollback, a domain either verifies or tells you exactly which record is wrong, and the export includes the content and not just the pixels. ## 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 freeform visual editor. - Figma import. - A template marketplace. - Certificate issuance and edge hosting. - general-purpose visual editing - large template and component libraries - CMS collaboration and client handoff - managed hosting, forms, and analytics ## Success criteria - An interrupted publish is confirmed to leave the live version untouched - The link crawl is confirmed to run before every pointer flip - An apex domain is confirmed never to receive CNAME instructions - Stored content is confirmed free of markup by scan - The content export is confirmed to round trip ===== BRIEF.md ===== # Build brief · Siter.io 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 section-based site builder that publishes static files to a domain you actually own to replace Siter.io. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Next.js 15 with TypeScript for the editor, running locally or on one small box · the editor is private, the output is not - Content: Typed section records, never HTML strings · a stored HTML blob cannot be restyled and can be a script tag - Output: A static build written to a versioned directory, then a pointer flip · half-published is worse than not published - Assets: Content-hashed filenames with long cache lifetimes · the file whose name changes is the file that updates - Storage: SQLite for sites, pages and sections; files on disk for media · one file to back up, plus a media folder ### Data model (create this before Phase 1) - sites(id, name, primary_host, www_redirect, created_at, published_version_id) - pages(site_id, id, path, title, description, noindex, ordinal) · path is the route, and uniqueness is enforced - sections(page_id, id, kind, ordinal, data_json) · kind names a renderer, data_json is typed per kind, and neither holds markup - assets(site_id, id, original_name, sha256, ext, width, height, bytes) · served as content-hashed filenames - versions(site_id, id, built_at, output_dir, file_count, bytes, built_from_revision) · every publish, kept for rollback - domains(site_id, host, kind, verification_token, dns_checked_at, dns_state, notes) · kind is apex or subdomain, which changes the records required - The section data is the document and the HTML is derived, so changing a renderer restyles every page that uses it without touching content. ### Phase 1 · Sections as data, renderers as code Build: Define six section kinds with a schema each: hero, text, image, gallery, features and contact. Store only their fields. Render each through a component that escapes every value. Rich text is a restricted structured format, not an HTML string, with an allow list of marks. Trap: Storing each section as a blob of HTML feels like a shortcut and costs you both halves of the product at once: you cannot change a heading style across the site without a find and replace over content, and a paste from a document carries markup straight into the published page. Store fields, render through components, and never persist markup. Done when: No table column contains HTML, verified by a test that scans stored section data for angle brackets in text fields. Changing a renderer's markup updates every page using that section kind without a content migration, demonstrated. A pasted value containing a script tag renders as visible text in the published output, tested. Adding a seventh section kind requires no change to the page, publish or asset code. Do not build yet: No preview, build, publish or domains yet. ### Phase 2 · A preview that is actually that width Build: Render the preview in an iframe set to the real pixel width of the device you are previewing, then scale the iframe visually to fit the editor. Never scale by shrinking the layout itself. Offer three widths and let the user type one. Trap: Simulating a narrow screen by squashing the preview element leaves the document's viewport width unchanged, so the mobile breakpoint never fires and the page looks perfect in the editor and wrong on a phone. Set the iframe's actual width to the target, then scale the whole iframe with a transform for display. Done when: A media query written at a stated breakpoint activates in the preview at exactly the width it will activate in a browser, tested by reading the reported viewport width inside the iframe. Switching preview widths does not re-mount the editor or lose unsaved edits. A page with an image gallery reflows at each preview width the same way it does in a real browser at that width, checked side by side once. Typing a custom width is honoured. Do not build yet: No build output, publishing or domains yet. ### Phase 3 · Publish all of it or none of it Build: Build the whole site into a fresh directory named by version. Write content-hashed asset filenames. Verify the output, then flip a single pointer to the new directory. Keep the previous versions and expose a one-click rollback. Trap: Publishing by overwriting the live directory file by file leaves visitors mid-publish seeing new pages referencing assets not yet uploaded, and a failure halfway leaves the site permanently in that state with no record of what changed. Build into a new directory, verify it, flip the pointer, and keep the old one so rollback is a flip rather than a rebuild. Done when: Interrupting a publish leaves the live site serving the previous version unchanged, tested by killing the process mid-build. Every internal link and asset reference in the built output resolves to a file that exists in that version, checked by a link crawl over the output directory before the pointer flips. Rolling back restores the previous version without rebuilding, and the version list shows what was live when. Asset filenames change when their contents change and never otherwise. Do not build yet: No domains, forms or export yet. ### Phase 4 · The apex domain problem, explained properly Build: Ask whether the host is a bare apex or a subdomain, because they need different records. RFC 1034 section 3.6.2 states that if a CNAME record is present at a node, no other data should be present (checked against the RFC), and an apex necessarily carries its zone's SOA and NS records, so a bare domain cannot be pointed with a plain CNAME. Give apex users address records or an alias record their provider supports, and offer the www host with a redirect as the simple path. Verify by resolving the records and show what you actually saw. Trap: Telling every user to add a CNAME works for www and fails silently for the bare domain, and because some providers accept the record and then break mail or the whole zone, the user's first symptom is unrelated and awful. Detect apex versus subdomain, give the right instruction for each, and refuse to claim the domain is connected until you have resolved the expected records yourself. Done when: Choosing a bare apex host produces apex-appropriate instructions and never suggests a plain CNAME, tested. The verification step resolves the records and displays what was found alongside what was expected, rather than a pass or fail with no detail. A domain that does not yet resolve stays in a pending state with the last check time shown and retries on a schedule. The www redirect option is offered and, when chosen, is generated in the published output. Do not build yet: No forms, export or error pages yet. ### Phase 5 · Leaving with everything Build: Export a published version as a folder of plain files that any static host will serve, plus a JSON dump of pages, sections and assets. Generate a 404 page and a sitemap. Handle contact forms by writing to a mail address or an endpoint the user names, with a plain statement that this app is not a form backend. Trap: A builder whose export is a rendered snapshot but not the content behind it leaves the user able to host their site once and never edit it again, which is exactly the trap a paid builder is accused of. Export both the built files and the structured content, and document the content format well enough that a different renderer could consume it. Done when: The exported folder serves correctly from a plain static file server with no rewrites configured, tested. The JSON dump contains every page, section and asset reference, and re-importing it into a fresh install reproduces the site, tested. A 404 page and a sitemap are generated for every published version. The README states plainly that form submissions go to a service the user controls and that this application does not store them. Do not build yet: Nothing after this. ### Out of scope (and why) - A freeform visual editor where anything can be dragged anywhere, which is a different and much larger product and is where the paid builders spend their years. - Figma import, because translating absolute frame coordinates into a layout that survives a narrow screen is guesswork dressed as a feature. - A template and component marketplace, which is licensed content and a review process rather than code. - Issuing TLS certificates and running the edge, which your static host already does better than you will. ### README must contain - That sections are stored as typed data and never as HTML, and what that buys you - That a publish builds a new version and flips a pointer, and how to roll back - Why a bare apex domain cannot take a plain CNAME, citing RFC 1034 section 3.6.2, and what to use instead - That domain verification resolves the records and shows what it found - That contact forms post to a service the user controls, and this app stores nothing ===== ARCHITECTURE.md ===== # Architecture · Siter.io ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js 15 with TypeScript for the editor, running locally or on one small box | the editor is private, the output is not | | Content | Typed section records, never HTML strings | a stored HTML blob cannot be restyled and can be a script tag | | Output | A static build written to a versioned directory, then a pointer flip | half-published is worse than not published | | Assets | Content-hashed filenames with long cache lifetimes | the file whose name changes is the file that updates | | Storage | SQLite for sites, pages and sections; files on disk for media | one file to back up, plus a media folder | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Sections | Typed content records and their renderers. | Any renderer, provided content stays free of markup. | | Preview | True-width iframe rendering. | Any implementation, provided it does not scale the layout. | | Publish | Versioned builds, link crawl, pointer flip. | Any host, provided activation stays a single atomic step. | | Domains | Apex and subdomain instructions and verification. | Any resolver, provided apex never gets a plain CNAME. | | Export | Built files plus structured content. | Any format, provided the round trip test passes. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · Sites, pages, sections, versions and domains. - `OUTPUT_ROOT` · required · One directory per version. The live pointer sits here too. - `ASSET_DIR` · required · Uploaded media, referenced by content hash. - `KEEP_VERSIONS` · required · How many published versions to retain for rollback. - `EDITOR_BIND` · required · The editor is private. Only the output is public. - `FORM_ENDPOINT` · optional · Where contact sections post. Unset means the contact section shows an address instead. ## 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 · Siter.io product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Next.js 15 with TypeScript for the editor, running locally or on one small box, Typed section records, never HTML strings, A static build written to a versioned directory, then a pointer flip, Content-hashed filenames with long cache lifetimes, SQLite for sites, pages and sections; files on disk for media. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Storing each section as a blob of HTML feels like a shortcut and costs you both halves of the product at once: you cannot change a heading style across the site without a find and replace over content, and a paste from a document carries markup straight into the published page. Store fields, render through components, and never persist markup. - Simulating a narrow screen by squashing the preview element leaves the document's viewport width unchanged, so the mobile breakpoint never fires and the page looks perfect in the editor and wrong on a phone. Set the iframe's actual width to the target, then scale the whole iframe with a transform for display. - Publishing by overwriting the live directory file by file leaves visitors mid-publish seeing new pages referencing assets not yet uploaded, and a failure halfway leaves the site permanently in that state with no record of what changed. Build into a new directory, verify it, flip the pointer, and keep the old one so rollback is a flip rather than a rebuild. - Telling every user to add a CNAME works for www and fails silently for the bare domain, and because some providers accept the record and then break mail or the whole zone, the user's first symptom is unrelated and awful. Detect apex versus subdomain, give the right instruction for each, and refuse to claim the domain is connected until you have resolved the expected records yourself. - A builder whose export is a rendered snapshot but not the content behind it leaves the user able to host their site once and never edit it again, which is exactly the trap a paid builder is accused of. Export both the built files and the structured content, and document the content format well enough that a different renderer could consume it. ===== MILESTONES.md ===== # Delivery milestones · Siter.io Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Sections as data, renderers as code A page is a list of typed section records, and no user input is ever stored as markup. ### Steps 1. Give each section kind a schema The schema is what lets the renderer change independently. Files: `src/sections/schema.ts`, `src/sections/hero.tsx` 2. Restrict rich text to marks you render An allow list of marks is a format; arbitrary HTML is a liability. Files: `src/richtext.ts` 3. Scan stored content for markup in tests The cheapest guard against the shortcut creeping back in. Files: `tests/no-stored-markup.test.ts` ### Done when - [ ] No stored field contains markup, asserted by a scan - [ ] A renderer change restyles existing pages - [ ] A pasted script tag renders as text - [ ] A new section kind touches only its own files ### Watch out - Storing each section as a blob of HTML feels like a shortcut and costs you both halves of the product at once: you cannot change a heading style across the site without a find and replace over content, and a paste from a document carries markup straight into the published page. Store fields, render through components, and never persist markup. ## M2 · A preview that is actually that width The responsive preview reports the viewport width it claims, so breakpoints fire where they will in the browser. ### Steps 1. Set the iframe's real width, scale for display The viewport width is the only thing breakpoints listen to. Files: `src/editor/Preview.tsx` 2. Report the inner viewport width Show the number so a wrong preview is visible, not suspected. Files: `src/editor/Preview.tsx` 3. Compare once against a real browser One manual check that the simulation is honest. Files: `docs/preview-verification.md` ### Done when - [ ] Breakpoints fire at the true width in preview - [ ] Switching widths preserves unsaved edits - [ ] Gallery reflow matches a real browser at the same width - [ ] A custom width is honoured ### Watch out - Simulating a narrow screen by squashing the preview element leaves the document's viewport width unchanged, so the mobile breakpoint never fires and the page looks perfect in the editor and wrong on a phone. Set the iframe's actual width to the target, then scale the whole iframe with a transform for display. ## M3 · Publish all of it or none of it A publish is a new immutable version and the live site flips between versions in one step. ### Steps 1. Build into a version directory Immutable output is what makes rollback trivial. Files: `src/publish/build.ts` 2. Crawl the output before flipping A broken link found in a directory beats one found by a visitor. Files: `src/publish/verify.ts`, `tests/link-crawl.test.ts` 3. Flip one pointer, keep the old version The whole atomicity story in one operation. Files: `src/publish/activate.ts` ### Done when - [ ] An interrupted publish leaves the live version untouched - [ ] The link crawl passes before the pointer flips - [ ] Rollback restores a previous version without rebuilding - [ ] Asset names change only when contents change ### Watch out - Publishing by overwriting the live directory file by file leaves visitors mid-publish seeing new pages referencing assets not yet uploaded, and a failure halfway leaves the site permanently in that state with no record of what changed. Build into a new directory, verify it, flip the pointer, and keep the old one so rollback is a flip rather than a rebuild. ## M4 · The apex domain problem, explained properly Connecting a domain succeeds or explains exactly which record to add, including the case the DNS standard forbids. ### Steps 1. Branch on apex versus subdomain They are different problems with different records. Files: `src/domains/instructions.ts` 2. Resolve and show what you saw DNS advice without a lookup is astrology. Files: `src/domains/verify.ts` 3. Generate the www redirect when chosen The simplest correct answer for most people. Files: `src/publish/redirects.ts` ### Done when - [ ] An apex host never gets plain CNAME instructions - [ ] Verification shows found versus expected records - [ ] A pending domain shows its last check and retries - [ ] The www redirect appears in the published output ### Watch out - Telling every user to add a CNAME works for www and fails silently for the bare domain, and because some providers accept the record and then break mail or the whole zone, the user's first symptom is unrelated and awful. Detect apex versus subdomain, give the right instruction for each, and refuse to claim the domain is connected until you have resolved the expected records yourself. ## M5 · Leaving with everything (production only) The site, its content and its history can be taken elsewhere without this application. ### Steps 1. Export built files and structured content One of the two alone is a lock-in with good manners. Files: `src/export/site.ts`, `src/export/content.ts` 2. Round trip the dump into a fresh install The only proof that the export is complete. Files: `tests/export-roundtrip.test.ts` 3. Be honest about forms A form backend is a spam problem and an inbox, not a feature. Files: `src/sections/contact.tsx`, `README.md` ### Done when - [ ] The export serves from a plain static server - [ ] A content dump round trips into a fresh install - [ ] 404 and sitemap are generated per version - [ ] The forms limitation is stated in the README ### Watch out - A builder whose export is a rendered snapshot but not the content behind it leaves the user able to host their site once and never edit it again, which is exactly the trap a paid builder is accused of. Export both the built files and the structured content, and document the content format well enough that a different renderer could consume it. ===== OPERATIONS.md ===== # Operations · Siter.io ## Backup The database and the asset directory. Published version directories are derived and can be rebuilt, but keeping the last few is cheaper than discovering a rebuild no longer reproduces them. ## Restore Restore, rebuild the current version, and compare the file list against the retained version directory. A difference means a renderer changed without a version bump, which is exactly the drift this design is meant to make visible. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Publish duration, link crawl failures, domains stuck pending, and how long since each site last published. A domain pending for a week means the instructions were unclear, which is a product problem rather than a DNS one. ## Incident checklist A broken live site is a pointer flip back to the previous version, which takes seconds because the previous build still exists. A domain that stopped resolving is diagnosed from the stored record check, including what was seen at the time. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] An interrupted publish is confirmed to leave the live version untouched - [ ] The link crawl is confirmed to run before every pointer flip - [ ] An apex domain is confirmed never to receive CNAME instructions - [ ] Stored content is confirmed free of markup by scan - [ ] The content export is confirmed to round trip ## Launch constraint Do not market omitted Siter.io capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Sites, pages, sections, versions and domains. DATABASE_PATH=./data/sites.db # Required. One directory per version. The live pointer sits here too. OUTPUT_ROOT=./data/published # Required. Uploaded media, referenced by content hash. ASSET_DIR=./data/assets # Required. How many published versions to retain for rollback. KEEP_VERSIONS=10 # Required. The editor is private. Only the output is public. EDITOR_BIND=127.0.0.1:3000 # Optional. Where contact sections post. Unset means the contact section shows an address instead. FORM_ENDPOINT=https://forms.example.com/f/abc123
# Siter.io · indie build A structured site builder whose hard problems are publishing a whole site atomically with a way back, connecting a bare apex domain that the DNS standard will not let you point with a CNAME, and never letting editable content become executable markup. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js 15 with TypeScript for the editor, running locally or on one small box | the editor is private, the output is not | | Content | Typed section records, never HTML strings | a stored HTML blob cannot be restyled and can be a script tag | | Output | A static build written to a versioned directory, then a pointer flip | half-published is worse than not published | | Assets | Content-hashed filenames with long cache lifetimes | the file whose name changes is the file that updates | | Storage | SQLite for sites, pages and sections; files on disk for media | one file to back up, plus a media folder | ## 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 static build. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **Somewhere to serve static files** · free to a few pounds a month - Why: The output is plain files, and the whole publish design assumes a host that serves a directory and lets you swap which directory is live. - Get it: Object storage with a static website mode, a small VPS with a web server, or any static host. - Verify: A directory of files you upload is reachable over HTTPS - [ ] **A domain you control the DNS for** · around ten pounds a year - Why: The apex versus subdomain problem is the phase that most needs a real zone, and a test domain with a provider you can log into is the only way to exercise it. - Get it: Any registrar, ideally one whose DNS interface you can reach quickly while testing. - Verify: You can add and remove records and see them resolve - [ ] **Where contact form submissions go** · free to a few pounds a month - Why: This app deliberately is not a form backend, so you need to decide the destination before writing the contact section rather than inventing a database table for it later. - Get it: Choose a mail address, a form service or an endpoint you already run, and write it down. - Verify: A test submission arrives at the chosen destination ## 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 freeform visual editor. - Figma import. - A template marketplace. - Certificate issuance and edge hosting. - 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 Siter.io, and the README should say so rather than pretend.
# Build brief · Siter.io 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 section-based site builder that publishes static files to a domain you actually own to replace Siter.io. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Next.js 15 with TypeScript for the editor, running locally or on one small box · the editor is private, the output is not - Content: Typed section records, never HTML strings · a stored HTML blob cannot be restyled and can be a script tag - Output: A static build written to a versioned directory, then a pointer flip · half-published is worse than not published - Assets: Content-hashed filenames with long cache lifetimes · the file whose name changes is the file that updates - Storage: SQLite for sites, pages and sections; files on disk for media · one file to back up, plus a media folder ### Data model (create this before Phase 1) - sites(id, name, primary_host, www_redirect, created_at, published_version_id) - pages(site_id, id, path, title, description, noindex, ordinal) · path is the route, and uniqueness is enforced - sections(page_id, id, kind, ordinal, data_json) · kind names a renderer, data_json is typed per kind, and neither holds markup - assets(site_id, id, original_name, sha256, ext, width, height, bytes) · served as content-hashed filenames - versions(site_id, id, built_at, output_dir, file_count, bytes, built_from_revision) · every publish, kept for rollback - domains(site_id, host, kind, verification_token, dns_checked_at, dns_state, notes) · kind is apex or subdomain, which changes the records required - The section data is the document and the HTML is derived, so changing a renderer restyles every page that uses it without touching content. ### Phase 1 · Sections as data, renderers as code Build: Define six section kinds with a schema each: hero, text, image, gallery, features and contact. Store only their fields. Render each through a component that escapes every value. Rich text is a restricted structured format, not an HTML string, with an allow list of marks. Trap: Storing each section as a blob of HTML feels like a shortcut and costs you both halves of the product at once: you cannot change a heading style across the site without a find and replace over content, and a paste from a document carries markup straight into the published page. Store fields, render through components, and never persist markup. Done when: No table column contains HTML, verified by a test that scans stored section data for angle brackets in text fields. Changing a renderer's markup updates every page using that section kind without a content migration, demonstrated. A pasted value containing a script tag renders as visible text in the published output, tested. Adding a seventh section kind requires no change to the page, publish or asset code. Do not build yet: No preview, build, publish or domains yet. ### Phase 2 · A preview that is actually that width Build: Render the preview in an iframe set to the real pixel width of the device you are previewing, then scale the iframe visually to fit the editor. Never scale by shrinking the layout itself. Offer three widths and let the user type one. Trap: Simulating a narrow screen by squashing the preview element leaves the document's viewport width unchanged, so the mobile breakpoint never fires and the page looks perfect in the editor and wrong on a phone. Set the iframe's actual width to the target, then scale the whole iframe with a transform for display. Done when: A media query written at a stated breakpoint activates in the preview at exactly the width it will activate in a browser, tested by reading the reported viewport width inside the iframe. Switching preview widths does not re-mount the editor or lose unsaved edits. A page with an image gallery reflows at each preview width the same way it does in a real browser at that width, checked side by side once. Typing a custom width is honoured. Do not build yet: No build output, publishing or domains yet. ### Phase 3 · Publish all of it or none of it Build: Build the whole site into a fresh directory named by version. Write content-hashed asset filenames. Verify the output, then flip a single pointer to the new directory. Keep the previous versions and expose a one-click rollback. Trap: Publishing by overwriting the live directory file by file leaves visitors mid-publish seeing new pages referencing assets not yet uploaded, and a failure halfway leaves the site permanently in that state with no record of what changed. Build into a new directory, verify it, flip the pointer, and keep the old one so rollback is a flip rather than a rebuild. Done when: Interrupting a publish leaves the live site serving the previous version unchanged, tested by killing the process mid-build. Every internal link and asset reference in the built output resolves to a file that exists in that version, checked by a link crawl over the output directory before the pointer flips. Rolling back restores the previous version without rebuilding, and the version list shows what was live when. Asset filenames change when their contents change and never otherwise. Do not build yet: No domains, forms or export yet. ### Phase 4 · The apex domain problem, explained properly Build: Ask whether the host is a bare apex or a subdomain, because they need different records. RFC 1034 section 3.6.2 states that if a CNAME record is present at a node, no other data should be present (checked against the RFC), and an apex necessarily carries its zone's SOA and NS records, so a bare domain cannot be pointed with a plain CNAME. Give apex users address records or an alias record their provider supports, and offer the www host with a redirect as the simple path. Verify by resolving the records and show what you actually saw. Trap: Telling every user to add a CNAME works for www and fails silently for the bare domain, and because some providers accept the record and then break mail or the whole zone, the user's first symptom is unrelated and awful. Detect apex versus subdomain, give the right instruction for each, and refuse to claim the domain is connected until you have resolved the expected records yourself. Done when: Choosing a bare apex host produces apex-appropriate instructions and never suggests a plain CNAME, tested. The verification step resolves the records and displays what was found alongside what was expected, rather than a pass or fail with no detail. A domain that does not yet resolve stays in a pending state with the last check time shown and retries on a schedule. The www redirect option is offered and, when chosen, is generated in the published output. Do not build yet: No forms, export or error pages yet. ### Phase 5 · Leaving with everything Build: Export a published version as a folder of plain files that any static host will serve, plus a JSON dump of pages, sections and assets. Generate a 404 page and a sitemap. Handle contact forms by writing to a mail address or an endpoint the user names, with a plain statement that this app is not a form backend. Trap: A builder whose export is a rendered snapshot but not the content behind it leaves the user able to host their site once and never edit it again, which is exactly the trap a paid builder is accused of. Export both the built files and the structured content, and document the content format well enough that a different renderer could consume it. Done when: The exported folder serves correctly from a plain static file server with no rewrites configured, tested. The JSON dump contains every page, section and asset reference, and re-importing it into a fresh install reproduces the site, tested. A 404 page and a sitemap are generated for every published version. The README states plainly that form submissions go to a service the user controls and that this application does not store them. Do not build yet: Nothing after this. ### Out of scope (and why) - A freeform visual editor where anything can be dragged anywhere, which is a different and much larger product and is where the paid builders spend their years. - Figma import, because translating absolute frame coordinates into a layout that survives a narrow screen is guesswork dressed as a feature. - A template and component marketplace, which is licensed content and a review process rather than code. - Issuing TLS certificates and running the edge, which your static host already does better than you will. ### README must contain - That sections are stored as typed data and never as HTML, and what that buys you - That a publish builds a new version and flips a pointer, and how to roll back - Why a bare apex domain cannot take a plain CNAME, citing RFC 1034 section 3.6.2, and what to use instead - That domain verification resolves the records and shows what it found - That contact forms post to a service the user controls, and this app stores nothing
# Agent instructions · Siter.io indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js 15 with TypeScript for the editor, running locally or on one small box, Typed section records, never HTML strings, A static build written to a versioned directory, then a pointer flip, Content-hashed filenames with long cache lifetimes, SQLite for sites, pages and sections; files on disk for media. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing each section as a blob of HTML feels like a shortcut and costs you both halves of the product at once: you cannot change a heading style across the site without a find and replace over content, and a paste from a document carries markup straight into the published page. Store fields, render through components, and never persist markup. - Simulating a narrow screen by squashing the preview element leaves the document's viewport width unchanged, so the mobile breakpoint never fires and the page looks perfect in the editor and wrong on a phone. Set the iframe's actual width to the target, then scale the whole iframe with a transform for display. - Publishing by overwriting the live directory file by file leaves visitors mid-publish seeing new pages referencing assets not yet uploaded, and a failure halfway leaves the site permanently in that state with no record of what changed. Build into a new directory, verify it, flip the pointer, and keep the old one so rollback is a flip rather than a rebuild. - Telling every user to add a CNAME works for www and fails silently for the bare domain, and because some providers accept the record and then break mail or the whole zone, the user's first symptom is unrelated and awful. Detect apex versus subdomain, give the right instruction for each, and refuse to claim the domain is connected until you have resolved the expected records yourself.
# Build plan · Siter.io A structured site builder whose hard problems are publishing a whole site atomically with a way back, connecting a bare apex domain that the DNS standard will not let you point with a CNAME, and never letting editable content become executable markup. 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 as data, renderers as code A page is a list of typed section records, and no user input is ever stored as markup. ### Steps 1. Give each section kind a schema The schema is what lets the renderer change independently. Files: `src/sections/schema.ts`, `src/sections/hero.tsx` 2. Restrict rich text to marks you render An allow list of marks is a format; arbitrary HTML is a liability. Files: `src/richtext.ts` 3. Scan stored content for markup in tests The cheapest guard against the shortcut creeping back in. Files: `tests/no-stored-markup.test.ts` ### Done when - [ ] No stored field contains markup, asserted by a scan - [ ] A renderer change restyles existing pages - [ ] A pasted script tag renders as text - [ ] A new section kind touches only its own files ### Watch out - Storing each section as a blob of HTML feels like a shortcut and costs you both halves of the product at once: you cannot change a heading style across the site without a find and replace over content, and a paste from a document carries markup straight into the published page. Store fields, render through components, and never persist markup. ## Phase 2 · A preview that is actually that width The responsive preview reports the viewport width it claims, so breakpoints fire where they will in the browser. ### Steps 1. Set the iframe's real width, scale for display The viewport width is the only thing breakpoints listen to. Files: `src/editor/Preview.tsx` 2. Report the inner viewport width Show the number so a wrong preview is visible, not suspected. Files: `src/editor/Preview.tsx` 3. Compare once against a real browser One manual check that the simulation is honest. Files: `docs/preview-verification.md` ### Done when - [ ] Breakpoints fire at the true width in preview - [ ] Switching widths preserves unsaved edits - [ ] Gallery reflow matches a real browser at the same width - [ ] A custom width is honoured ### Watch out - Simulating a narrow screen by squashing the preview element leaves the document's viewport width unchanged, so the mobile breakpoint never fires and the page looks perfect in the editor and wrong on a phone. Set the iframe's actual width to the target, then scale the whole iframe with a transform for display. ## Phase 3 · Publish all of it or none of it A publish is a new immutable version and the live site flips between versions in one step. ### Steps 1. Build into a version directory Immutable output is what makes rollback trivial. Files: `src/publish/build.ts` 2. Crawl the output before flipping A broken link found in a directory beats one found by a visitor. Files: `src/publish/verify.ts`, `tests/link-crawl.test.ts` 3. Flip one pointer, keep the old version The whole atomicity story in one operation. Files: `src/publish/activate.ts` ### Done when - [ ] An interrupted publish leaves the live version untouched - [ ] The link crawl passes before the pointer flips - [ ] Rollback restores a previous version without rebuilding - [ ] Asset names change only when contents change ### Watch out - Publishing by overwriting the live directory file by file leaves visitors mid-publish seeing new pages referencing assets not yet uploaded, and a failure halfway leaves the site permanently in that state with no record of what changed. Build into a new directory, verify it, flip the pointer, and keep the old one so rollback is a flip rather than a rebuild. ## Phase 4 · The apex domain problem, explained properly Connecting a domain succeeds or explains exactly which record to add, including the case the DNS standard forbids. ### Steps 1. Branch on apex versus subdomain They are different problems with different records. Files: `src/domains/instructions.ts` 2. Resolve and show what you saw DNS advice without a lookup is astrology. Files: `src/domains/verify.ts` 3. Generate the www redirect when chosen The simplest correct answer for most people. Files: `src/publish/redirects.ts` ### Done when - [ ] An apex host never gets plain CNAME instructions - [ ] Verification shows found versus expected records - [ ] A pending domain shows its last check and retries - [ ] The www redirect appears in the published output ### Watch out - Telling every user to add a CNAME works for www and fails silently for the bare domain, and because some providers accept the record and then break mail or the whole zone, the user's first symptom is unrelated and awful. Detect apex versus subdomain, give the right instruction for each, and refuse to claim the domain is connected until you have resolved the expected records yourself. ## Not in this build - A freeform visual editor. - Figma import. - A template marketplace. - Certificate issuance and edge hosting. ## After v1, if you want it - A second theme applied to the same content, which the section model already makes possible - Scheduled publishing, which is a timer on an operation that is already atomic - A read-only share link for a version, useful for client review before the pointer flips
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Sites, pages, sections, versions and domains. DATABASE_PATH=./data/sites.db # Required. One directory per version. The live pointer sits here too. OUTPUT_ROOT=./data/published # Required. Uploaded media, referenced by content hash. ASSET_DIR=./data/assets # Required. How many published versions to retain for rollback. KEEP_VERSIONS=10 # Required. The editor is private. Only the output is public. EDITOR_BIND=127.0.0.1:3000 # Optional. Where contact sections post. Unset means the contact section shows an address instead. FORM_ENDPOINT=https://forms.example.com/f/abc123
# Siter.io · product brief ## Problem The visible collaborative website builder loop is buildable, but a credible replacement needs more than the first screen. Siter.io earns its keep through editor, CMS, hosting, polish, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A builder where content outlives its renderer, a publish is all or nothing with a rollback, a domain either verifies or tells you exactly which record is wrong, and the export includes the content and not just the pixels. ## 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 freeform visual editor. - Figma import. - A template marketplace. - Certificate issuance and edge hosting. - general-purpose visual editing - large template and component libraries - CMS collaboration and client handoff - managed hosting, forms, and analytics ## Success criteria - An interrupted publish is confirmed to leave the live version untouched - The link crawl is confirmed to run before every pointer flip - An apex domain is confirmed never to receive CNAME instructions - Stored content is confirmed free of markup by scan - The content export is confirmed to round trip
# Build brief · Siter.io 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 section-based site builder that publishes static files to a domain you actually own to replace Siter.io. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Next.js 15 with TypeScript for the editor, running locally or on one small box · the editor is private, the output is not - Content: Typed section records, never HTML strings · a stored HTML blob cannot be restyled and can be a script tag - Output: A static build written to a versioned directory, then a pointer flip · half-published is worse than not published - Assets: Content-hashed filenames with long cache lifetimes · the file whose name changes is the file that updates - Storage: SQLite for sites, pages and sections; files on disk for media · one file to back up, plus a media folder ### Data model (create this before Phase 1) - sites(id, name, primary_host, www_redirect, created_at, published_version_id) - pages(site_id, id, path, title, description, noindex, ordinal) · path is the route, and uniqueness is enforced - sections(page_id, id, kind, ordinal, data_json) · kind names a renderer, data_json is typed per kind, and neither holds markup - assets(site_id, id, original_name, sha256, ext, width, height, bytes) · served as content-hashed filenames - versions(site_id, id, built_at, output_dir, file_count, bytes, built_from_revision) · every publish, kept for rollback - domains(site_id, host, kind, verification_token, dns_checked_at, dns_state, notes) · kind is apex or subdomain, which changes the records required - The section data is the document and the HTML is derived, so changing a renderer restyles every page that uses it without touching content. ### Phase 1 · Sections as data, renderers as code Build: Define six section kinds with a schema each: hero, text, image, gallery, features and contact. Store only their fields. Render each through a component that escapes every value. Rich text is a restricted structured format, not an HTML string, with an allow list of marks. Trap: Storing each section as a blob of HTML feels like a shortcut and costs you both halves of the product at once: you cannot change a heading style across the site without a find and replace over content, and a paste from a document carries markup straight into the published page. Store fields, render through components, and never persist markup. Done when: No table column contains HTML, verified by a test that scans stored section data for angle brackets in text fields. Changing a renderer's markup updates every page using that section kind without a content migration, demonstrated. A pasted value containing a script tag renders as visible text in the published output, tested. Adding a seventh section kind requires no change to the page, publish or asset code. Do not build yet: No preview, build, publish or domains yet. ### Phase 2 · A preview that is actually that width Build: Render the preview in an iframe set to the real pixel width of the device you are previewing, then scale the iframe visually to fit the editor. Never scale by shrinking the layout itself. Offer three widths and let the user type one. Trap: Simulating a narrow screen by squashing the preview element leaves the document's viewport width unchanged, so the mobile breakpoint never fires and the page looks perfect in the editor and wrong on a phone. Set the iframe's actual width to the target, then scale the whole iframe with a transform for display. Done when: A media query written at a stated breakpoint activates in the preview at exactly the width it will activate in a browser, tested by reading the reported viewport width inside the iframe. Switching preview widths does not re-mount the editor or lose unsaved edits. A page with an image gallery reflows at each preview width the same way it does in a real browser at that width, checked side by side once. Typing a custom width is honoured. Do not build yet: No build output, publishing or domains yet. ### Phase 3 · Publish all of it or none of it Build: Build the whole site into a fresh directory named by version. Write content-hashed asset filenames. Verify the output, then flip a single pointer to the new directory. Keep the previous versions and expose a one-click rollback. Trap: Publishing by overwriting the live directory file by file leaves visitors mid-publish seeing new pages referencing assets not yet uploaded, and a failure halfway leaves the site permanently in that state with no record of what changed. Build into a new directory, verify it, flip the pointer, and keep the old one so rollback is a flip rather than a rebuild. Done when: Interrupting a publish leaves the live site serving the previous version unchanged, tested by killing the process mid-build. Every internal link and asset reference in the built output resolves to a file that exists in that version, checked by a link crawl over the output directory before the pointer flips. Rolling back restores the previous version without rebuilding, and the version list shows what was live when. Asset filenames change when their contents change and never otherwise. Do not build yet: No domains, forms or export yet. ### Phase 4 · The apex domain problem, explained properly Build: Ask whether the host is a bare apex or a subdomain, because they need different records. RFC 1034 section 3.6.2 states that if a CNAME record is present at a node, no other data should be present (checked against the RFC), and an apex necessarily carries its zone's SOA and NS records, so a bare domain cannot be pointed with a plain CNAME. Give apex users address records or an alias record their provider supports, and offer the www host with a redirect as the simple path. Verify by resolving the records and show what you actually saw. Trap: Telling every user to add a CNAME works for www and fails silently for the bare domain, and because some providers accept the record and then break mail or the whole zone, the user's first symptom is unrelated and awful. Detect apex versus subdomain, give the right instruction for each, and refuse to claim the domain is connected until you have resolved the expected records yourself. Done when: Choosing a bare apex host produces apex-appropriate instructions and never suggests a plain CNAME, tested. The verification step resolves the records and displays what was found alongside what was expected, rather than a pass or fail with no detail. A domain that does not yet resolve stays in a pending state with the last check time shown and retries on a schedule. The www redirect option is offered and, when chosen, is generated in the published output. Do not build yet: No forms, export or error pages yet. ### Phase 5 · Leaving with everything Build: Export a published version as a folder of plain files that any static host will serve, plus a JSON dump of pages, sections and assets. Generate a 404 page and a sitemap. Handle contact forms by writing to a mail address or an endpoint the user names, with a plain statement that this app is not a form backend. Trap: A builder whose export is a rendered snapshot but not the content behind it leaves the user able to host their site once and never edit it again, which is exactly the trap a paid builder is accused of. Export both the built files and the structured content, and document the content format well enough that a different renderer could consume it. Done when: The exported folder serves correctly from a plain static file server with no rewrites configured, tested. The JSON dump contains every page, section and asset reference, and re-importing it into a fresh install reproduces the site, tested. A 404 page and a sitemap are generated for every published version. The README states plainly that form submissions go to a service the user controls and that this application does not store them. Do not build yet: Nothing after this. ### Out of scope (and why) - A freeform visual editor where anything can be dragged anywhere, which is a different and much larger product and is where the paid builders spend their years. - Figma import, because translating absolute frame coordinates into a layout that survives a narrow screen is guesswork dressed as a feature. - A template and component marketplace, which is licensed content and a review process rather than code. - Issuing TLS certificates and running the edge, which your static host already does better than you will. ### README must contain - That sections are stored as typed data and never as HTML, and what that buys you - That a publish builds a new version and flips a pointer, and how to roll back - Why a bare apex domain cannot take a plain CNAME, citing RFC 1034 section 3.6.2, and what to use instead - That domain verification resolves the records and shows what it found - That contact forms post to a service the user controls, and this app stores nothing
# Architecture · Siter.io ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js 15 with TypeScript for the editor, running locally or on one small box | the editor is private, the output is not | | Content | Typed section records, never HTML strings | a stored HTML blob cannot be restyled and can be a script tag | | Output | A static build written to a versioned directory, then a pointer flip | half-published is worse than not published | | Assets | Content-hashed filenames with long cache lifetimes | the file whose name changes is the file that updates | | Storage | SQLite for sites, pages and sections; files on disk for media | one file to back up, plus a media folder | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Sections | Typed content records and their renderers. | Any renderer, provided content stays free of markup. | | Preview | True-width iframe rendering. | Any implementation, provided it does not scale the layout. | | Publish | Versioned builds, link crawl, pointer flip. | Any host, provided activation stays a single atomic step. | | Domains | Apex and subdomain instructions and verification. | Any resolver, provided apex never gets a plain CNAME. | | Export | Built files plus structured content. | Any format, provided the round trip test passes. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · Sites, pages, sections, versions and domains. - `OUTPUT_ROOT` · required · One directory per version. The live pointer sits here too. - `ASSET_DIR` · required · Uploaded media, referenced by content hash. - `KEEP_VERSIONS` · required · How many published versions to retain for rollback. - `EDITOR_BIND` · required · The editor is private. Only the output is public. - `FORM_ENDPOINT` · optional · Where contact sections post. Unset means the contact section shows an address instead. ## 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 · Siter.io product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Next.js 15 with TypeScript for the editor, running locally or on one small box, Typed section records, never HTML strings, A static build written to a versioned directory, then a pointer flip, Content-hashed filenames with long cache lifetimes, SQLite for sites, pages and sections; files on disk for media. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Storing each section as a blob of HTML feels like a shortcut and costs you both halves of the product at once: you cannot change a heading style across the site without a find and replace over content, and a paste from a document carries markup straight into the published page. Store fields, render through components, and never persist markup. - Simulating a narrow screen by squashing the preview element leaves the document's viewport width unchanged, so the mobile breakpoint never fires and the page looks perfect in the editor and wrong on a phone. Set the iframe's actual width to the target, then scale the whole iframe with a transform for display. - Publishing by overwriting the live directory file by file leaves visitors mid-publish seeing new pages referencing assets not yet uploaded, and a failure halfway leaves the site permanently in that state with no record of what changed. Build into a new directory, verify it, flip the pointer, and keep the old one so rollback is a flip rather than a rebuild. - Telling every user to add a CNAME works for www and fails silently for the bare domain, and because some providers accept the record and then break mail or the whole zone, the user's first symptom is unrelated and awful. Detect apex versus subdomain, give the right instruction for each, and refuse to claim the domain is connected until you have resolved the expected records yourself. - A builder whose export is a rendered snapshot but not the content behind it leaves the user able to host their site once and never edit it again, which is exactly the trap a paid builder is accused of. Export both the built files and the structured content, and document the content format well enough that a different renderer could consume it.
# Delivery milestones · Siter.io Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Sections as data, renderers as code A page is a list of typed section records, and no user input is ever stored as markup. ### Steps 1. Give each section kind a schema The schema is what lets the renderer change independently. Files: `src/sections/schema.ts`, `src/sections/hero.tsx` 2. Restrict rich text to marks you render An allow list of marks is a format; arbitrary HTML is a liability. Files: `src/richtext.ts` 3. Scan stored content for markup in tests The cheapest guard against the shortcut creeping back in. Files: `tests/no-stored-markup.test.ts` ### Done when - [ ] No stored field contains markup, asserted by a scan - [ ] A renderer change restyles existing pages - [ ] A pasted script tag renders as text - [ ] A new section kind touches only its own files ### Watch out - Storing each section as a blob of HTML feels like a shortcut and costs you both halves of the product at once: you cannot change a heading style across the site without a find and replace over content, and a paste from a document carries markup straight into the published page. Store fields, render through components, and never persist markup. ## M2 · A preview that is actually that width The responsive preview reports the viewport width it claims, so breakpoints fire where they will in the browser. ### Steps 1. Set the iframe's real width, scale for display The viewport width is the only thing breakpoints listen to. Files: `src/editor/Preview.tsx` 2. Report the inner viewport width Show the number so a wrong preview is visible, not suspected. Files: `src/editor/Preview.tsx` 3. Compare once against a real browser One manual check that the simulation is honest. Files: `docs/preview-verification.md` ### Done when - [ ] Breakpoints fire at the true width in preview - [ ] Switching widths preserves unsaved edits - [ ] Gallery reflow matches a real browser at the same width - [ ] A custom width is honoured ### Watch out - Simulating a narrow screen by squashing the preview element leaves the document's viewport width unchanged, so the mobile breakpoint never fires and the page looks perfect in the editor and wrong on a phone. Set the iframe's actual width to the target, then scale the whole iframe with a transform for display. ## M3 · Publish all of it or none of it A publish is a new immutable version and the live site flips between versions in one step. ### Steps 1. Build into a version directory Immutable output is what makes rollback trivial. Files: `src/publish/build.ts` 2. Crawl the output before flipping A broken link found in a directory beats one found by a visitor. Files: `src/publish/verify.ts`, `tests/link-crawl.test.ts` 3. Flip one pointer, keep the old version The whole atomicity story in one operation. Files: `src/publish/activate.ts` ### Done when - [ ] An interrupted publish leaves the live version untouched - [ ] The link crawl passes before the pointer flips - [ ] Rollback restores a previous version without rebuilding - [ ] Asset names change only when contents change ### Watch out - Publishing by overwriting the live directory file by file leaves visitors mid-publish seeing new pages referencing assets not yet uploaded, and a failure halfway leaves the site permanently in that state with no record of what changed. Build into a new directory, verify it, flip the pointer, and keep the old one so rollback is a flip rather than a rebuild. ## M4 · The apex domain problem, explained properly Connecting a domain succeeds or explains exactly which record to add, including the case the DNS standard forbids. ### Steps 1. Branch on apex versus subdomain They are different problems with different records. Files: `src/domains/instructions.ts` 2. Resolve and show what you saw DNS advice without a lookup is astrology. Files: `src/domains/verify.ts` 3. Generate the www redirect when chosen The simplest correct answer for most people. Files: `src/publish/redirects.ts` ### Done when - [ ] An apex host never gets plain CNAME instructions - [ ] Verification shows found versus expected records - [ ] A pending domain shows its last check and retries - [ ] The www redirect appears in the published output ### Watch out - Telling every user to add a CNAME works for www and fails silently for the bare domain, and because some providers accept the record and then break mail or the whole zone, the user's first symptom is unrelated and awful. Detect apex versus subdomain, give the right instruction for each, and refuse to claim the domain is connected until you have resolved the expected records yourself. ## M5 · Leaving with everything (production only) The site, its content and its history can be taken elsewhere without this application. ### Steps 1. Export built files and structured content One of the two alone is a lock-in with good manners. Files: `src/export/site.ts`, `src/export/content.ts` 2. Round trip the dump into a fresh install The only proof that the export is complete. Files: `tests/export-roundtrip.test.ts` 3. Be honest about forms A form backend is a spam problem and an inbox, not a feature. Files: `src/sections/contact.tsx`, `README.md` ### Done when - [ ] The export serves from a plain static server - [ ] A content dump round trips into a fresh install - [ ] 404 and sitemap are generated per version - [ ] The forms limitation is stated in the README ### Watch out - A builder whose export is a rendered snapshot but not the content behind it leaves the user able to host their site once and never edit it again, which is exactly the trap a paid builder is accused of. Export both the built files and the structured content, and document the content format well enough that a different renderer could consume it.
# Operations · Siter.io ## Backup The database and the asset directory. Published version directories are derived and can be rebuilt, but keeping the last few is cheaper than discovering a rebuild no longer reproduces them. ## Restore Restore, rebuild the current version, and compare the file list against the retained version directory. A difference means a renderer changed without a version bump, which is exactly the drift this design is meant to make visible. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Publish duration, link crawl failures, domains stuck pending, and how long since each site last published. A domain pending for a week means the instructions were unclear, which is a product problem rather than a DNS one. ## Incident checklist A broken live site is a pointer flip back to the previous version, which takes seconds because the previous build still exists. A domain that stopped resolving is diagnosed from the stored record check, including what was seen at the time. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] An interrupted publish is confirmed to leave the live version untouched - [ ] The link crawl is confirmed to run before every pointer flip - [ ] An apex domain is confirmed never to receive CNAME instructions - [ ] Stored content is confirmed free of markup by scan - [ ] The content export is confirmed to round trip ## Launch constraint Do not market omitted Siter.io capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Sites, pages, sections, versions and domains. DATABASE_PATH=./data/sites.db # Required. One directory per version. The live pointer sits here too. OUTPUT_ROOT=./data/published # Required. Uploaded media, referenced by content hash. ASSET_DIR=./data/assets # Required. How many published versions to retain for rollback. KEEP_VERSIONS=10 # Required. The editor is private. Only the output is public. EDITOR_BIND=127.0.0.1:3000 # Optional. Where contact sections post. Unset means the contact section shows an address instead. FORM_ENDPOINT=https://forms.example.com/f/abc123
$ choose a build depth, inspect the files, then open the complete pack in your agent
Siter.io: 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
Siter.io pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/workspace | $0/workspace | 10 pages and 3 forms; pricing page displays unlimited websites and teammates, with Figma plugin and SSL |
| solo | $7/workspace | — | 1 website, 2 editors included, 10 pages, 3 forms, Figma plugin and SSL |
| plus | $12/workspace | — | 3 websites, 4 editors included, 50 pages, 5 forms, Figma plugin and SSL |
| pro | $24/workspace | — | 5 websites, 6 editors included, unlimited pages and forms, Figma plugin and SSL |
free tier10 pages and 3 forms; the live table labels websites and teammates unlimited, but does not clearly state publication/domain limits
billingmonthly + annual; exact annual prices not exposed in accessible first-party HTML
hidden costsAdditional websites and teammates beyond a paid plan's allowance are billed monthly, but the public help page does not publish their unit prices.
verified 2026-08-14 · source ↗
Vibecode Siter.io
Kinda. The core of Siter.io 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 Siter.io cost?
Siter.io's pricing is usage-based or varies by plan · The official pricing URL is recorded, but a stable current amount was not safely recoverable in this pass. Do not publish a number until rechecked..
What do I lose by replacing Siter.io?
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 Siter.io?
Yes: Plasmic (A collaborative visual builder with Figma import, hosting and custom domains on the free plan; the badge stays.) The prompt is for when you want it exactly your way.