Build Siter.io

KINDAback to the verdict

0%0 of 16 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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 weekendthe files for this build are in the project pack

RuntimeNext.js 15 with TypeScript for the editor, running locally or on one small boxContentTyped section records, never HTML stringsOutputA static build written to a versioned directory, then a pointer flipAssetsContent-hashed filenames with long cache lifetimesStorageSQLite for sites, pages and sections; files on disk for media

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the editor and the static build.

    Get it nodejs.org, or nvm install 22. open ↗

    Verify node --version prints v22 or higher

  2. have readyfree 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

  3. have readyaround 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

  4. decidefree 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

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- 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.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
DATABASE_PATHrequired./data/sites.dbSites, pages, sections, versions and domains.
OUTPUT_ROOTrequired./data/publishedOne directory per version. The live pointer sits here too.
ASSET_DIRrequired./data/assetsUploaded media, referenced by content hash.
KEEP_VERSIONSrequired10How many published versions to retain for rollback.
EDITOR_BINDrequired127.0.0.1:3000The editor is private. Only the output is public.
FORM_ENDPOINToptionalhttps://forms.example.com/f/abc123Where contact sections post. Unset means the contact section shows an address instead.

The build, in order

  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.

    1. The schema is what lets the renderer change independently.

      Files src/sections/schema.tssrc/sections/hero.tsx

    2. An allow list of marks is a format; arbitrary HTML is a liability.

      Files src/richtext.ts

    3. The cheapest guard against the shortcut creeping back in.

      Files tests/no-stored-markup.test.ts

    done when · tick each as it passes
    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.
  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.

    1. The viewport width is the only thing breakpoints listen to.

      Files src/editor/Preview.tsx

    2. Show the number so a wrong preview is visible, not suspected.

      Files src/editor/Preview.tsx

    3. One manual check that the simulation is honest.

      Files docs/preview-verification.md

    done when · tick each as it passes
    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.
  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.

    1. Immutable output is what makes rollback trivial.

      Files src/publish/build.ts

    2. A broken link found in a directory beats one found by a visitor.

      Files src/publish/verify.tstests/link-crawl.test.ts

    3. The whole atomicity story in one operation.

      Files src/publish/activate.ts

    done when · tick each as it passes
    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.
  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.

    1. They are different problems with different records.

      Files src/domains/instructions.ts

    2. DNS advice without a lookup is astrology.

      Files src/domains/verify.ts

    3. The simplest correct answer for most people.

      Files src/publish/redirects.ts

    done when · tick each as it passes
    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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more website builders.