Output ONLY one fenced ```json code block for Simple Host, and nothing else. Use this exact schema: {"simpleHost":1,"name":"[YOUR-SITE-NAME]","files":{"index.html":""},"files_base64":{"assets/example.png":""}} Relative links only — never a leading slash. index.html must be one self-contained file (inline CSS/JS). Omit files_base64 when there are no binary files. A path must not appear in both files and files_base64. SITE: [Describe your site in plain language here — what it's for, the style/colors/vibe, the font, any headings, and all the real details like names, dates, times, and locations. Say what visitors should be able to do.] BACKEND AND SAVING Every site has state and collections; reads are public and Origin/Referer-gated. On the shared host (sites.simple-host.app) pages read and write freely: no sign-in, no key, and shared-host data can be changed by anyone. On a site's own custom domain writes need a signed-in visitor (Google or an emailed code) or an API key; connecting a domain is what adds sign-in: https://simple-host.app/v1/skills/connect-domain. A site with a custom domain lives only on that domain: its shared-host page URL https://sites.simple-host.app///... answers 302 to https:///... (same path and query), its shared-host API refuses ALL writes with 401 {"error":"this site saves on its own domain","code":"use_custom_domain","domain":""} — key or not; agents write through the apex https://simple-host.app/v1/... as usual, and reads there stay public — and /me there returns {"signed_in":false,"sign_in_available":false,"code":"use_custom_domain","domain":""} so the page can link to the same page on the domain — SH.mount() renders that link and SH.requireSignIn() rejects with code use_custom_domain and .domain. Write with plain fetch. Always send credentials:'include' and X-SH-CSRF: 1 so the same code keeps working once a domain is connected: const m=location.pathname.match(/^\/([^/]+)\/([^/]+)\//); const API=m?'/v1/u/'+m[1]+'/sites/'+m[2]:'/v1/sites/[YOUR-SITE-NAME]'; await fetch(API+'/state',{method:'PATCH',credentials:'include',headers:{'Content-Type':'application/json','X-SH-CSRF':'1'},body:JSON.stringify({ops:[{op:'inc',path:'total',by:1}]})}); await fetch(API+'/collections/entries',{method:'POST',credentials:'include',headers:{'Content-Type':'application/json','X-SH-CSRF':'1'},body:JSON.stringify(item)}); Read with GET API+'/state' and GET API+'/collections/entries?limit=50'. Check response.ok; a non-2xx is a failed save. On a custom domain also load , then SH.mount('#sh-auth') after DOMContentLoaded with a matching
, and await SH.requireSignIn() before each save. Configure window.SH_CONFIG={site:'your-site'} for a custom domain; the API stays same-origin. SH.requireSignIn() resolves immediately on the shared host (no sign-in there), so one page works on both hosts. SH.state.patch({ops:[...]}), SH.collection('entries').append(item), SH.state.get() and SH.collection('entries').list() do the fetches above for you, with the credentials and CSRF header included. Sign-in is Google or an emailed code. SH.email.request(email) sends a 6-digit code; SH.email.verify(email,code) signs in. Visitor verification never returns an API key. SH.me() returns the server body; on the shared host it is {"signed_in":false,"sign_in_available":false,"code":"custom_domain_required"} and SH.mount() shows one muted line: "Saves on this site are public. Connect a domain to add sign-in." Shared data belongs on the server, not in localStorage/sessionStorage. Reads return public data; do not store secrets. Keep forms visible on errors; never claim success on failure or repeat a successful append to retry a failed count update. Agents write with X-API-Key on any site through the apex https://simple-host.app/v1/... (the shared host sites.simple-host.app refuses writes for a site that has a domain bound, key or not). Invalid keys return 401 invalid_api_key. Keep keys out of page HTML. WRITE_AUTH_MODE=log/off and the owner's allow_anonymous_writes override are compatibility exceptions. If the page collects entries, include a matching admin.html that reads and renders them, linked from index.html; the data is public. Use a solid page background. ANALYTICS — INFORMATIONAL, BUILD NOTHING FOR IT. Traffic is counted server-side from the access log: never add a hit counter, beacon, or analytics script to the page. Two read-only owner endpoints, X-API-Key, owner-scoped, days defaults to 30 (clamped 1..365): GET /v1/sites//analytics?days=30 → {range_days, classified_from, totals, last_24h, daily:[{day,...}], hourly:[{hour,...}]}. daily is zero-filled dense over the window; hourly is always 24 buckets ending with the hour in progress. GET /v1/analytics/sites?days=30 → {range_days, sites:[{name,...}]} — every site you own, ordered by person.views descending. Admins only may add &all=1 for every site on the box; anyone else gets 403. PATCH /v1/sites/ with {"name":"new-name"} renames a site and moves its files. A custom domain stays attached. The old public URL returns 404; use site_url from the response. POST /v1/me/api-key/rotate returns a new api_key and invalidates the old key immediately. Update the agent or CLI with the new key. Every bucket (totals, last_24h, each daily, each hourly, each site) is the SAME four-class split — person, bot, infra, unknown — each {views, visitors}. There is NO top-level views or visitors. person = no automation signature; READ THIS AS THE AUDIENCE NUMBER. bot = crawlers, AI scrapers, SEO tools, security scanners, HTTP libraries. infra = uptime probes and health checks, usually the biggest number and never audience. unknown = days recorded before traffic was classified; report it separately, never folded into the other three. visitors = distinct hashed IPs over the window asked for, so totals.*.visitors is NOT the sum of the daily figures (one person on five days = 1 in totals, 5 in daily). Views count 200/304 GET document responses only, for every class alike. SELF-CHECK before you answer: page writes use fetch with credentials:'include' and X-SH-CSRF: 1 (or SH.state/SH.collection) and need no sign-in on the shared host; custom-domain saving loads auth.js and awaits SH.requireSignIn(), which resolves at once on the shared host; shared data uses state/collections; API keys never appear in page code; failed writes keep the form visible; successful appends are never repeated; collected entries have a public organizer view; the background is solid. Output only the JSON block. PUBLISHING — the JSON block above is the site; this is how it goes live. Pick a short lowercase name (letters, digits, hyphens). Create it once with POST, change it afterwards with PUT: POST https://simple-host.app/v1/sites//files with header X-API-Key: , body {"files":{"index.html":""}} PUT https://simple-host.app/v1/sites//files same body, for every later change A 409 on POST means that name is already yours: use PUT. Binary files go in files_base64 instead, base64-encoded. The response carries site_url, which is the link to share. Every deploy is versioned; GET /v1/sites//versions lists the versions this instance still holds and PUT /v1/sites//active-version switches to one of them. An event instance may keep only the live version to save disk, in which case that list has one entry and redeploying is how you go back. Take a copy any time with GET /v1/sites//export.tar.gz. AGENT SAVING — POST /v1/auth {"email":"person@example.com"}, then POST /v1/auth/verify with that email and the person's emailed code to receive api_key. Send X-API-Key on state/collections writes to any site, including on the shared host. Event instances: admins can POST /v1/admin/users with {"emails":["a@example.com"]} or {"count":30,"prefix":"team"} to issue participant keys without sign-in. There is no limit on how many. GET /v1/admin/usage reports what the disk is actually holding, what is free, and warns as it fills. DELETE /v1/admin/users/{id} removes a non-admin account and its files. PATCH /v1/me edits display_name (trimmed, max 100 characters) or handle (only with zero sites). GET /v1/auth/oauth/providers includes email_enabled as well as configured OAuth providers.