← Website Deploy ToolkitCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Website Deploy Toolkit
Snapshot Sep 30, 2026 · 23:15 UTC · version 0.9.4
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"description": "Plan what to build on Website Deploy (simple-host.app). Helps a user decide whether their idea fits the static + light-backend model, maps it to concrete patterns (Page info the owner writes, Submissions visitors send, Personal records each visitor keeps, Shared boards a group edits, localStorage, public APIs), and produces a focused prompt for an implementation agent. Knows the planning rules that matter - every site lives at its own address, https://<site>.<handle>.simple-host.app/, where visitors sign in (Google or an emailed code) before saving from a page; saved data nobody declared is Shared (public), and Page info or Submissions are declared once; anything personal (orders, RSVPs, sign-ups) goes in Submissions, private to the owner by default; a free <name>.simple-host.app or a custom domain is an optional nicer address. Use when a user is starting a new site or describes a feature idea and needs help mapping it to what the platform can do.",
"included_files": [],
"name": "website-deploy-builder",
"skill_md_contents": "---\nname: website-deploy-builder\ndescription: Plan what to build on Website Deploy (simple-host.app). Helps a user decide whether their idea fits the static + light-backend model, maps it to concrete patterns (Page info the owner writes, Submissions visitors send, Personal records each visitor keeps, Shared boards a group edits, localStorage, public APIs), and produces a focused prompt for an implementation agent. Knows the planning rules that matter - every site lives at its own address, https://<site>.<handle>.simple-host.app/, where visitors sign in (Google or an emailed code) before saving from a page; saved data nobody declared is Shared (public), and Page info or Submissions are declared once; anything personal (orders, RSVPs, sign-ups) goes in Submissions, private to the owner by default; a free <name>.simple-host.app or a custom domain is an optional nicer address. Use when a user is starting a new site or describes a feature idea and needs help mapping it to what the platform can do.\n---\n\n# Website Deploy Builder\n\n**First rule: use the Simple Host tools when you have them.** If the Simple Host\nconnector's tools are available in this session (`who_am_i`, `list_sites`,\n`create_site` / `update_site` (`deploy_site` on older connections), `get_state`,\n`connect_domain`, …), use them for everything and never ask the person for an\nemail, a code or an API key — the connector is already signed in as them. Sign-in\nitself is unchanged: when the person connects Simple Host in their AI app, a\nSimple Host sign-in window opens, they sign in with Google or the emailed code,\nthen choose Allow, and every chat after that is signed in. If a tool reports the\nconnection is not signed in, ask them to reconnect Simple Host in their app's\nsettings. Only when those tools are not available (e.g. a coding agent without\nthe connector) use the email-code and API-key flow the `website-deploy` skill describes.\n\nUse this skill when a user wants help deciding what to build on Website Deploy, or how to scope an idea they already have. After the user picks an approach, hand off to the `website-deploy` skill for deploy.\n\n## What Website Deploy gives you\n\nWebsite Deploy is a static-file host at `https://simple-host.app`. Each site lives at its own address, `https://<sitename>.<handle>.simple-host.app/` (`handle` is the owner's URL-safe handle from GET `/v1/me`; `https://<handle>.simple-host.app/` lists the person's public sites). Always hand the person the `site_url`/`url` the deploy returned — for a brand-new account it is briefly `https://<handle>.simple-host.app/<sitename>/` until the site's certificate is issued. The dashboard/API stay on `https://simple-host.app` (a separate origin). Old `<handle>.simple-host.app/<site>/` and `sites.simple-host.app/<handle>/<site>/` links redirect to the site's address. There is no server-side execution — but the API gives each site a real, server-backed backend:\n\n| Capability | How |\n|---|---|\n| HTML / CSS / JS / images / fonts served as a site | Deploy files inline as JSON (`/files`) or upload a `.tar.gz`/`.zip`. With the connector: `create_site` / `update_site` (`deploy_site` on older connections) |\n| **What is this data?** A name nobody declared is **Shared** (public: anyone reads it, anyone signed in adds to it). Declare anything else once before the page saves to it; personal details are always private Submissions | `declare_data` or `PUT /v1/sites/<sitename>/data/<name>/kind`. **Page info** `{\"kind\":\"content\"}`: you write it (`update_data`), everyone reads it (`SH.data(name).get()`) — a menu, hours, prices. **Submissions** `{\"kind\":\"entries\"}`: visitors send them (`SH.data(name).add(...)`) — RSVPs, orders, sign-ups, votes, comments; private to the owner unless `\"visibility\":\"public\"`; each visitor sees, changes and withdraws their own (`mine` / `update` / `remove`); `\"one_per_person\": true` for votes; the owner gets a daily email about new private ones (`notify`). **Personal** `{\"kind\":\"mine\"}`: one private record per signed-in visitor that follows them to any device (`SH.data(name,'personal').get()/set()/inc()`) — a habit tracker, saved progress, preferences; the owner's tools never show it (only how many people have one), but the site's own pages read it for that visitor, so only use Personal on sites you trust, and never write a page that sends it anywhere else. **Shared board** `{\"kind\":\"board\"}`: a list anyone reads and signed-in visitors add to, change and delete item by item (`SH.data(name,'board').add()/update(id, fields, {version})/remove()/watch()`) — a shared shopping list, a kanban, a potluck sign-up; only the owner clears it; changes show up by polling, not instantly |\n| Who may save here | Anyone who signs in (default), or only listed emails and whole `@domains`, plus a block list: `set_who_can_save`, `block_person` |\n| Per-site JSON state (≤ 1 MB, shared across all visitors; older sites) | `GET / PUT /v1/sites/<sitename>/state` (same-origin from the page; agents can also use `/v1/u/<handle>/sites/<sitename>/state` on the apex). Reads public; a page write needs the visitor signed in first (`auth.js`) |\n| Atomic state updates (concurrent-safe counters, lists, votes) | `PATCH .../state` with `{ops:[inc/append/set/remove/removeWhere]}`; `If-None-Match` ETag for cheap polling. A write — same rule as above |\n| Collections (signups / RSVPs / submissions) | `POST/GET /v1/sites/<sitename>/collections/<name>`. GET public; POST is a write. The owner removes entries (one, or the whole list); in declared Submissions each visitor also changes and withdraws their own |\n| Private collections (orders, RSVPs, anything personal) | `set_collection_privacy` (or `PUT .../collections/<name>/privacy` `{\"private\":true}`). Signed-in visitors add; only the site owner — and the Simple Host operator, for moderation — can read it. The owner can edit or delete items (`update` / `remove`). In a public list the owner can delete (spam) but not edit; in declared Submissions each visitor also changes and withdraws their own |\n| A nicer address (optional) | Free `<name>.simple-host.app`: one call (`connect_domain`), active at once, no DNS. Or a custom domain via the `connect-domain` skill (two DNS records: the address and a TXT ownership record). The site moves there and its old address redirects |\n| Agent writing for the site owner (no browser) | The connector (`update_state`, `add_to_collection`) if present; otherwise the owner's own API key, obtained by email code, as `X-API-Key` — works only on sites that account owns (another account's key gets 404). Anyone else saves on the page as a signed-in visitor. See \"Saving from an agent\" in the `website-deploy` skill's `references/backend.md` |\n| Per-visitor state | `localStorage`, `sessionStorage`, `IndexedDB` (in the browser), or **Personal** (`mine`) when it must follow the visitor to another device |\n| External APIs | `fetch()` from the page to any public CORS-enabled API |\n| Routing | Static files only — path-relative directories with `index.html`; SPA routing via the framework's hash router or `404.html` fallback |\n\nIf your idea needs a server you control, a shared SQL database, your own user accounts and roles, or anything that runs server-side, Website Deploy is not the right host. Say so and stop.\n\n**Anyone can read; saving from a page needs sign-in.** Visitors sign in with Google or an emailed code on the site's own address (a sign-in covers that site only); every save from a page needs a signed-in visitor. Agents save with the API key (or the connector). Say this up front, before the page is written, so the form gets its sign-in box.\n\n**What is this data? One line decides it.** Open data anyone may read and add to (a guestbook, a counter): **Shared** — what a name is when nobody declares it. You write it and everyone reads it: **Page info**. Visitors send it: **Submissions**. Each visitor's own, private, on any device: **Personal**. A list the group edits together: **Shared board**. Plan the kind of every piece of data before the page is written, and declare it first (`declare_data`). Anything with personal details is private Submissions, never Shared; when unsure, choose the stricter kind. It does not fit — and you say so instead of approximating: roles, per-field rules, joins, search, live co-editing of one object, or instant updates.\n\n**Anything personal goes in private Submissions** (the default). Orders, RSVPs, survey answers, sign-ups, or anything with names, emails, phone numbers or addresses: only signed-in visitors can submit, only the owner reads them all, and each visitor sees, changes and withdraws their own. Plan it in this order:\n\n1. Declare it (`declare_data` with `kind: \"entries\"`) before the form goes live.\n2. The form page calls `await SH.requireSignIn()` before `SH.data('orders', 'entries').add({...})`, and shows the saved item from the answer as the visitor's receipt (and `.mine()` for what they sent before).\n3. An owner page on the site (e.g. `orders.html`) that signs in, lists them (`SH.data('orders').list()`), and has \"Mark done\" (`.update(id, {status:'done'})`) and \"Delete\" (`.remove(id)`) buttons. It works only for the owner's account. The owner also has their sites page (every entry with who sent it, a daily email, a spreadsheet download); the agent reads it with `read_collection`.\n\nPublic Submissions (a guestbook, public comments) are `\"visibility\": \"public\"`; say so plainly. Pages are always public; only private Submissions are closed, readable in full by the site owner and the Simple Host operator (for moderation).\n\n**Always pair a form with a viewer.** Any site that COLLECTS data (a signup, RSVP, guestbook, contact form, order) MUST also ship a second page — e.g. `admin.html` — that reads the same collection back (`GET .../collections/<name>?limit=200` → `{items:[{id,data,created_at},…]}`) and lists every entry for the owner, newest first, plus the live total from state. Link it quietly from the main page (a small \"Organizer view →\" in the footer). A form with nowhere to read the results is only half the feature — and the person you're building for will not think to ask for the viewer, so add it by default. Mark the viewer `<meta name=\"robots\" content=\"noindex\">`. A public collection is readable by anyone with the link, so don't fake a password; if the entries are personal, make the collection private and the viewer becomes the owner page above.\n\n## How to use this skill\n\n1. Ask the user what they're trying to build, in plain language. Don't push capabilities at them — let them describe the idea.\n2. Decide whether it can run as a static site. If parts of it can't, name those parts and either propose a static-friendly substitute or recommend a different host for that piece.\n3. If visitors will save anything, say now that they sign in first, and name the kind of each piece of data (Shared, Page info, Submissions, Personal or Shared board). If the saves hold personal details, plan private Submissions and an owner page.\n4. For the part that can run statically, give them: (a) a one-paragraph explanation of how to structure it, (b) any relevant snippet (storage, routing, external API call), (c) the gotchas.\n5. If they're starting from scratch, finish with a \"ready to deploy\" handoff: tell them to use the `website-deploy` skill, which handles registration (only without the connector), framework-aware build, packaging, and upload.\n6. If they want to wire a capability into a site they've already deployed, generate a focused prompt they can paste into a fresh agent chat (in their site's repo). Include the pattern, the storage shape, and any gotcha — nothing else. If the change deletes data, makes private data public or changes who can see or save, the prompt says to confirm that step with the person first.\n\n## Capability tree\n\n### 1. Static hosting (the baseline)\n\nWhat it is: any folder of HTML/CSS/JS/assets served as-is. Build any framework's normal production output (`dist/`, `build/`, `out/`, `public/`, `.output/public/`) and upload.\n\nWhen to choose: every Website Deploy site starts here. Deploy first, then layer storage and external calls.\n\nGotchas: use relative links (`style.css`, not `/style.css`, and `about.html`, not `/about`) so previews and a new site's first minutes work too; root-relative links work only at the live address. For framework builds, set the base/public path so output uses relative URLs (e.g. Vite `base: './'`, Next `basePath` / relative assets, etc.). Don't ship `node_modules/` or `.env`. Each archive is capped at 100 MB.\n\n### 2. Per-site JSON state (shared across visitors)\n\nWhat it is: a single JSON document (up to 1 MB) scoped to your site. The server stores it in Postgres; your site reads and writes it from the browser. The document is shared across **everyone** who visits — use `PATCH` ops so concurrent writers don't clobber each other.\n\nWhen to choose: anything you'd want a tiny key-value store for — a shared note, a counter, a vote tally, content the page generated, configuration. Reading is public. **Writing from a page needs sign-in**: the page loads `https://simple-host.app/auth.js`, sets `window.SH_CONFIG = { site: \"<sitename>\" }` (needed on a custom domain, harmless everywhere), and calls `await SH.requireSignIn()` before each write — the visitor signs in with Google or an emailed code. The session is site-scoped and is not an API key. Sign-in gates writing only — it does not make the page private. If you need per-visitor data, store it under different keys inside the document, keyed on something like `crypto.randomUUID()` saved in `localStorage`.\n\nHow to use, from a page on the site:\n\n```html\n<div id=\"sh-auth\"></div>\n<form id=\"f\"><textarea name=\"draft\"></textarea><button>Save</button></form>\n<p id=\"status\"></p>\n<script>window.SH_CONFIG = { site: \"<sitename>\" };</script>\n<script src=\"https://simple-host.app/auth.js\" defer></script>\n<script>\nwindow.addEventListener('DOMContentLoaded', async function () {\n SH.mount('#sh-auth'); // Google sign-in + email-code form\n const status = document.getElementById('status');\n const form = document.getElementById('f');\n\n // load — public, no sign-in\n const { data } = await SH.state.get();\n form.draft.value = data.draft || '';\n\n // save — sign in first, then write; keep the form on failure\n form.onsubmit = async function (e) {\n e.preventDefault();\n await SH.requireSignIn(); // signs the visitor in if needed\n try {\n await SH.state.patch([{ op: 'set', path: 'draft', value: form.draft.value }]);\n status.textContent = 'Saved';\n } catch (err) {\n status.textContent = 'Not saved: ' + (err.code || err.status); // never claim success\n }\n };\n});\n</script>\n```\n\nFull `SH` API (`SH.data(name, kind)`, and on older sites `SH.state` and `SH.collection(name)`; `SH.me`, `SH.signOut`) and the error bodies are in the `website-deploy` skill's `references/backend.md`.\n\nGotchas: state is public to anyone with the link; never keep personal details in it (use a private collection). Body cap is 1 MB; sending more returns 413.\n\n### 3. Per-visitor state with `localStorage`\n\nWhat it is: small JSON blobs stored in the visitor's browser, scoped to the page's origin (each site's own `<sitename>.<handle>.simple-host.app`, or its custom domain).\n\nWhen to choose: anything you'd want a tiny key-value store for in a single-visitor experience — drafts, settings, app state, the user's progress. Per-visitor only; there is no sharing across browsers or devices.\n\n```js\n// save\nlocalStorage.setItem('myapp.state', JSON.stringify(state));\n\n// load\nconst raw = localStorage.getItem('myapp.state');\nconst state = raw ? JSON.parse(raw) : {};\n```\n\nGotchas: typical browser quota is ~5 MB per origin. Cleared by the user at any time. Prefix your keys with the site name: on the `<handle>.simple-host.app/<sitename>/` fallback address a person's sites share one origin. For multi-megabyte structured data, use `IndexedDB` instead.\n\n### 4. Larger per-visitor state with `IndexedDB`\n\nWhen `localStorage`'s ~5 MB cap is too small or you have a lot of small records, use `IndexedDB` directly. It is built into every browser, so no library or CDN import is needed.\n\n```js\nfunction openDB() {\n return new Promise((resolve, reject) => {\n const req = indexedDB.open('myapp', 1);\n req.onupgradeneeded = () => req.result.createObjectStore('items', { keyPath: 'id' });\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n}\nfunction run(db, mode, fn) {\n return new Promise((resolve, reject) => {\n const tx = db.transaction('items', mode);\n const req = fn(tx.objectStore('items'));\n tx.oncomplete = () => resolve(req.result);\n tx.onerror = () => reject(tx.error);\n });\n}\nconst db = await openDB();\nawait run(db, 'readwrite', s => s.put({ id: 'a', text: 'hello' }));\nconst item = await run(db, 'readonly', s => s.get('a'));\n```\n\nGotchas: same per-origin / per-visitor scoping as `localStorage`. Cleared if the user clears site data.\n\n### 5. Calling external APIs from the browser\n\nWhat it is: `fetch()` from your page directly to any public HTTPS API that returns CORS-friendly responses.\n\nWhen to choose: pulling in public data (weather, Wikipedia, public LLM APIs the user provides their own key for, etc.).\n\n```js\nconst r = await fetch('https://api.example.com/v1/things');\nconst data = await r.json();\n```\n\nGotchas:\n- **CORS** — the upstream API must include `Access-Control-Allow-Origin`. If it doesn't, the browser blocks the response and there's nothing Website Deploy can do; you need a server-side proxy that you control elsewhere.\n- **Keys** — anything in your client-side code is visible to anyone who opens DevTools. Don't bake in API keys. If the API requires a key, have the user paste it into a small input field and save it to `localStorage` with a \"paste a fresh key\" hint when it's missing.\n- **Rate limits** — public APIs throttle by IP. If your site is on a shared machine, that quota is shared too.\n\n### 6. Static reports from generated exports\n\nWhat it is: a static HTML/JS dashboard or report built from data that was exported before deploy. The export becomes ordinary site data (`.json`, `.csv`, or pre-rendered HTML) and Website Deploy only serves the finished files.\n\nWhen to choose: public reports, class projects, research notes, and read-only dashboards where the private work already happened in another tool. For example, a user can turn a sanitized analytics or social export (a follower list, a search result, a CSV of metrics) into a static report, then deploy the report output here.\n\nGotchas:\n- Every deployed file is public. Remove keys, cookies, and anything the user did not explicitly approve for publication.\n- Prefer small, pre-filtered exports. Large raw datasets can exceed archive limits and make the page slow.\n- Do not fetch private APIs from the browser unless the user supplies a key at runtime. If the report needs server-side refresh, Website Deploy is only the static front-end, not the refresh worker.\n\n### 7. Routing patterns\n\nWebsite Deploy serves files. There is no rewrite layer. Because sites live under `/<sitename>/`, keep links **relative** so navigation stays inside the site path.\n\n- **Multi-page static site**: every page is a real `index.html` under a directory. `about/` resolves to `about/index.html` under the site path.\n- **SPA with framework router**: build for static export (see the `website-deploy` skill's framework section) **with a relative base**. Use the framework's hash-router mode or generate a `404.html` that bootstraps the app.\n- **Pretty URLs for plain HTML**: put each \"page\" in its own folder with an `index.html` (`about/index.html`, `pricing/index.html`).\n\n### 8. A nicer address: free name or custom domain\n\nOptional — every site already has its own `https://<sitename>.<handle>.simple-host.app/`, where sign-in and private collections work. The quickest nicer address is a free `<name>.simple-host.app`: `connect_domain` (or `POST /v1/sites/<sitename>/domain`) with `{\"domain\":\"clay-studio.simple-host.app\"}` answers `active` at once, no DNS. First come, first served. A user can instead serve a site from their own domain (e.g. `recipes.brand.com`) — use the `connect-domain` skill (`simple-host-website/skills/connect-domain`). Summary: `POST /v1/sites/<sitename>/domain` with `{domain}` → user adds two DNS records (the address and a TXT ownership record) → poll `GET /v1/sites/<sitename>/domain` until `active` (with the connector: `connect_domain`, then `domain_status`). Either one changes the address; sign-in and private collections carry over. Pages stay public. Once connected, the site lives only at that address: its `<sitename>.<handle>.simple-host.app` URL 302s there and takes no writes for it (agents keep writing through the apex `https://simple-host.app/v1/...`).\n\n## Picking a capability mix\n\n| User says | Capabilities |\n|---|---|\n| \"a landing page / portfolio / CV\" | static only |\n| \"a guestbook\" | static + public Submissions (`visibility: public`) + `auth.js` sign-in |\n| \"a waitlist / event RSVP / signup form\" | static + private Submissions (the default; `count()` for a live total) + owner page |\n| \"take orders / bookings / a survey\" | private Submissions + form with `auth.js` sign-in + owner page (`orders.html`) |\n| \"a poll / a vote\" | static + Submissions with `one_per_person: true` (public to show the tally) + `auth.js` sign-in |\n| \"a menu / opening hours / prices I update\" | static + Page info (you write it with `update_data`; the page reads `SH.data(name).get()`) |\n| \"a habit tracker / saved progress / my reading list, on any device\" | static + Personal (`kind: mine`; `SH.data(name, 'personal')`) + `auth.js` sign-in |\n| \"a shared shopping list / kanban / potluck sign-up\" | static + Shared board (`kind: board`; `SH.data(name, 'board')`, `watch()` to refresh) + `auth.js` sign-in |\n| \"a tool that runs entirely in the browser\" (calculator, drawing app, game) | static + `localStorage` for settings/saves |\n| \"a journal / notes app\" | static + `IndexedDB` (single-visitor scope) |\n| \"a dashboard pulling from a public API\" | static + external `fetch()` |\n| \"a report from an exported dataset (analytics, social, etc.)\" | static export + optional client-side filtering |\n| \"a multi-page site\" | static only — each page is its own folder + `index.html` (relative links) |\n| \"my own domain / brand.com\" | static + `connect-domain` skill |\n| \"a shorter address, but no domain\" | static + free `<name>.simple-host.app` (one `connect_domain` call) |\n| \"a slide deck I want to share a link to\" | build with Slidev, Reveal.js, or similar and deploy the output |\n\nIf the user wants something Website Deploy can't host — per-user accounts that span devices, server-side execution, or a shared SQL database — say so explicitly and stop. Suggest they pair Website Deploy (for the static front-end) with a separate backend host (Vercel functions, Cloudflare Workers, Supabase, etc.) where their server-side logic lives. Nicer addresses *are* supported (free `<name>.simple-host.app`, or a custom domain via `connect-domain`). Private or password-locked pages are not — every deployed page is public. Sign-in (Google or email code) gates *writing* to the backend; the only thing gated for reading is a private collection, which only the site owner — and the Simple Host operator, for moderation — can read. Never present \"sign in to save\" as a private page.\n\n## Generating a prompt for another agent\n\nWhen the user wants to wire a capability into an existing site, generate a focused prompt to paste into a fresh agent chat. Keep it short.\n\nExample prompt for \"save drafts in localStorage\":\n\n> Add draft autosave to this site. On every change to the text input, write `{text, updatedAt}` to `localStorage['mysite.draft']`. On page load, restore the input value from that key if present. Show a small \"Draft saved\" indicator that fades out after 1 second when the save runs. No external dependencies. Use relative asset links only (a site can also be served under a path).\n\nExample prompt for \"let visitors sign the guestbook\" (entries belong to signed-in visitors; this site also has a custom domain, so `SH_CONFIG` is required):\n\n> Add a guestbook to this site (deployed on simple-host, custom domain `guests.example.com`, sitename `guestbook`). First declare the data: `declare_data` with name `entries`, kind `entries`, visibility `public`. Load `https://simple-host.app/auth.js` with `window.SH_CONFIG = { site: \"guestbook\" }` set before the tag, mount `SH.mount('#sh-auth')` next to the form, and call `await SH.requireSignIn()` before `SH.data('entries', 'entries').add({name, message})`. On a non-2xx keep the form and show \"Not saved\". Add `admin.html` (noindex) that lists the collection newest-first. Relative asset links only.\n\nMirror this shape for `IndexedDB`, external API calls, routing, etc.\n\n## Handoff: deploy\n\nOnce the user has decided what to build, they need to deploy. Tell them to use the `website-deploy` skill, which handles registration (only when the Simple Host connector is not available), framework-aware build (with a relative base path), packaging, and upload. Before a new site goes online for the first time, it asks the person once (name, address, public to anyone with the link). The site will be live at `https://<sitename>.<handle>.simple-host.app/` (give them the `site_url` the deploy returned). If they want a nicer address, offer the free `<name>.simple-host.app` or the `connect-domain` skill for their own domain; it is optional.\n"
}SHA-256 of public snapshot: a555cbae2eb5bf455f018ff0c34fd56e2cc1900749bc60956f96756a9e6086f9