# Playbook — builder / developer on a web-chat client

Use this playbook when the user is a developer or technically-literate solo builder — comfortable with HTML, slugs, and API semantics — and you are running in a client that cannot execute outbound HTTP requests like `curl`, cannot read or write local files on the user's machine, and cannot build a ZIP, but can call MCP tools and receive chat-attached images. Current examples: Claude.ai (web and mobile), ChatGPT (web and mobile), Gemini (web), Claude Desktop without a shell MCP server configured. If your client can run `curl`, switch to `guide(topic="builder_console")` instead.

This is the squeezed cell — a technical user on a non-technical platform. You work in the web-chat primitive set (inline content, URL-fetch, MCP `upload_text` with `content`) while speaking to the user in the register they already know.

## Orient

Facts you can state flatly, no hedging: static hosting per project, one subdomain each (`<slug>.uat-beam.page`), Tailwind + Alpine defaults, any browser-runnable stack fine, eventual consistency on writes, full reference loaded already via `guide(topic="orient")`. Imperatives, not wrappers — `call`, `PUT`, `fetch`, never _"your AI might be able to…"_ or _"if your tool supports…"_.

## Auth — Google up front

This profile generally wants a permanent tenant. The user is usually already logged into Google in a browser tab, and the refresh-token path is strictly easier than guest-then-convert. Go straight to `GET /auth/google` and hand the user the returned URL; ask for the pasted code; use it.

If you're running on MCP, the connector has already authenticated the user through their Google account — skip the auth step entirely; the "Connected via MCP" block of the shared reference you already loaded has the identity-propagation details.

## Project — user picks the slug

State the rules inline — 2–63 characters, `[a-z0-9-]`, no leading or trailing hyphen, globally unique — and let the user type one. Don't derive it for them; this profile has an opinion.

Call `POST /projects {"slug": "...", "context": "..."}`. On 409 (already taken), 400 (malformed), or 403 (reserved), surface the error message verbatim in one line and hand control back. Don't loop through candidates; the user wants explicit control.

Hand the subdomain URL over the instant the call returns 200.

## Review — URL first, lag warning second

The first action after a 200 is to give the user the subdomain URL — they'll want a browser tab open for iteration. Mention the eventual-consistency window once (it's in the "Things to know" block of the shared reference) so the first refresh showing the default template isn't mistaken for a failure.

## Static pages or SPA?

Before iterating, pick the shape silently:

- **Mostly read-only content that updates infrequently and needs to rank on Google** (marketing sites, restaurants, portfolios, agencies, most small-business sites) → **static pages** — one HTML file per page, SEO works by default, `POST /edit` and `PUT /files` cover every iteration.
- **User-interactive tool, dashboard, filterable catalogue, map with saved state, anything with meaningful client-side state** → **SPA under `/app/*`** — the shared reference has "Hosting a SPA under `/app/`" for the mechanics, plus `spa-react.md` / `spa-flutter.md` for end-to-end scripts. ZIP upload requires a console, so the user pushes the ZIP from their own machine.
- **Hybrid** (content marketing + app functionality) → **both**: static pages for `/`, `/menu`, `/about`; SPA at `/app/*`; a `/` → `/app/*` redirect is optional and usually not what you want.

**Static does not mean read-only.** The `email` action is a server-side primitive the platform provides, so a static site with a booking / quote / RSVP form works fine — the static-plus-action combo covers most small-business use cases without the SPA complexity.

## First content — inline content and URL-fetch only

On web chat you have two practical upload paths:

- `PUT /projects/<id>/pages/<slug>/files/<filename> {"content": "..."}` — text only (HTML, CSS, JS, JSON, XML, SVG, TXT). Content type is inferred from the filename. Does **not** decode base64; binary bytes will be rejected or corrupted.
- `PUT /projects/<id>/pages/<slug>/files/<filename> {"url": "https://..."}` — the platform fetches a public HTTPS URL and stores the bytes. HTTPS only, up to 4 MB, 10 s timeout, no redirects, private IPs rejected.

Presigned S3 PUT and presigned-ZIP upload both require a client that can execute outbound HTTPS PUTs. You don't have that. If the user has a folder of files or a SPA build, they need to push from their own machine — ask them to run the presigned PUT / ZIP flow there and come back when it's done. The "Files" (presigned S3 PUT) and "Bulk upload via ZIP" sections of the shared reference you already loaded have the depth; don't re-explain them in chat.

For surgical edits on files already on the platform, use `POST /edit` with `{edits: [{old, new}]}`. Byte-exact UTF-8 matching — the "Common pitfalls" section of the shared reference covers curly quotes, `&mdash;` vs `—`, non-breaking spaces.

**Worked `POST /edit` rhythm.** For a back-and-forth of small changes, model the loop:

1. **GET the current file** — `read_content({projectId, scope: "page", slug: "_root", filename: "index.html"})` (MCP) or `GET /projects/<slug>/pages/_root/files/index.html` — so you have the exact bytes.
2. **Show the user the relevant chunk** — paste the 3–10 lines around the change; let them confirm the intent.
3. **Propose `{old, new}`** — `old` copied verbatim from the bytes you just read, `new` the replacement.
4. **Apply** — `edit` (MCP) or `POST /projects/<slug>/edit` with `{edits: [{old, new}], page: "_root", file: "index.html"}`.
5. **Confirm on the live URL** — remind the user to refresh the browser tab.

Example: adding a CSS class to the `<header>`. Step 1: `read_content` returns `<header class="flex items-center">`. Step 2: confirm with user. Step 3: `{old: "<header class=\"flex items-center\">", new: "<header class=\"flex items-center bg-white shadow\">"}`. Step 4: `edit` call. Step 5: user refreshes.

Never retype `old` from what you _think_ the file says — cross-link the typographic-character pitfalls in "Common pitfalls" and copy exact bytes from the read.

## SEO — head tags, auto-sitemap, metadata distinction

Sitemap is auto-generated per project at `/sitemap.xml`; `robots.txt` is synthesised permissively by the platform on every subdomain — you don't create either. Search-engine metadata lives in the HTML: `<title>`, `<meta name="description">`, OpenGraph (`og:title`, `og:description`, `og:image`, `og:url`), and a `<script type="application/ld+json">` block with schema.org `LocalBusiness` (pick a subtype — `Restaurant`, `HomeAndConstructionBusiness`, `BeautySalon`, `FinancialService`, `ProfessionalService`, or the base `LocalBusiness`).

The platform's `PUT /pages/<slug>/metadata` primitive is a different concern — it's for **data-surface** metadata that the rest of the site (or `cross_page_metadata.json`) reads from client-side JS. Head tags are for search engines and link previews; metadata is for the site's own JavaScript. Don't conflate them.

## Email action — the default stance for forms

If the site has a form of any kind — contact, booking, RSVP, quote request, application, survey, waitlist, donation pledge — **default to wiring it to the `email` action** unless the user has an explicit reason to do otherwise (Slack webhook, custom handler, CRM integration). The primitive is `POST /actions/<project-slug>/email` with an arbitrary JSON body; the platform rate-limits to 10 emails per project per hour, SES delivers to the authenticated owner, and spam basics are handled.

The action is not restricted to "contact us" — RSVPs, bookings, applications, quotes, survey responses, and waitlist signups are all the same shape. The existing `contact-form.md` scenario guide documents the HTML pattern, Alpine.js scaffold, rate limit, and pre-filling via query params — but the file name narrows the framing: **the same pattern applies to every form on the site, not only contact forms.** Don't let the file name make you forget that.

Builder overrides: if the user says _"route this to Slack"_ or _"I'll handle it with my own webhook"_, do that — the `email` action is a default stance, not a mandate.

## Iterate — builder flow, within the web-chat constraints

The change requests you'll see on this profile — add a page, rename a slug and keep the old URL alive, redeploy the SPA, roll back to a point-in-time — map cleanly onto existing endpoints. Walk them via MCP tools (`page({op: "create"})`, `redirect({op: "create"})`, `snapshot({op: "take"})`, `snapshot({op: "restore"})`) and point at the relevant section of the shared reference for depth:

- Add a page → `POST /projects/<id>/pages` then upload the `index.html` via inline content.
- Rename with redirect → "Redirects" section of the shared reference.
- SPA shell under `/app/` → `spa-react.md` / `spa-flutter.md` cover the full shape, but the ZIP upload requires a console, so ask the user to push the ZIP from their machine, then resume here for the redirect and verification steps.
- Point-in-time restore → "Snapshots and point-in-time restore" section of the shared reference.

## Images — tighter fallback

You see the image via multimodal input but cannot PUT binary bytes; `{"content": "..."}` is raw UTF-8 only and does not decode base64. Prefer a public URL the user already has (their current site, Facebook / Instagram, Google Business, personal CDN) via URL-fetch; if none exists, offer a neutral placeholder (inline SVG, typographic hero, public-URL free-stock image); as a last resort, point the user at **ImgBB** (`imgbb.com`, drag-and-drop, no account, direct URL shape `https://i.ibb.co/XXXX/filename.jpg` — copy from the "Direct links" field) with **PostImages** (`postimages.org`, direct URL shape `https://i.postimg.cc/XXXX/filename.jpg`) as a backup, then URL-fetch. Google Drive's default share link is a viewer page, not a direct URL — not a first choice. Don't attempt base64 into `content`. PDFs and video follow the same shape.

## Durability — snapshots and the upgrade offer

If the user is authenticated with Google (strongly recommended for this profile), every project has nightly snapshots at 00:00 UTC retained for 30 days, plus an initial snapshot taken at project creation. Before a risky refactor, offer a manual one:

> "Take a snapshot first so we can roll back cleanly? `POST /projects/<slug>/snapshots {"label": "pre-refactor"}`."

For the mechanics — IDs, restore failure modes, rate limits — the "Snapshots and point-in-time restore" section of the shared reference you already loaded has the detail; don't restate it.

Custom domains are coming but not available yet; match the "coming soon" phrasing from the shared reference.

## Do not

- Promise presigned PUT or ZIP upload from this client — you can't execute outbound HTTPS PUTs; ask the user to run those flows from their own machine.
- Silently default the slug — this profile wants explicit control over the subdomain.
- Over-explain eventual consistency — mention it once at the reveal, then move on.
- Retype `old` for `POST /edit` from memory — `GET` the file and copy exact bytes; the typographic pitfalls in the shared reference are unforgiving.
- Re-call `guide(topic="orient")` or re-fetch `llm.txt` for depth — the reference is already in your context; pick the relevant section by name.

## Next steps

Reach depth via `guide(topic="...")` with the matching scenario or `orient` topic — don't guess at URLs:

- Scenario guides — the table at the bottom of the shared reference lists `simple-page.md`, `multi-page-site.md`, `restaurant.md`, `property-listings.md`, `map-with-pins.md`, `job-board.md`, `contact-form.md`, `photo-gallery.md`, `event-page.md`, `multi-language.md`, `portfolio.md`, `copy-page.md` with canonical URLs.
- SPA patterns — `https://uat-beam.page/llm/spa-react.md`, `https://uat-beam.page/llm/spa-flutter.md` (the ZIP step runs on a console).
- Platform reference (errors, conventions, redirects, snapshots, edit semantics) — the `llm.txt` you already loaded.
- Management UI — `https://uat-beam.page/projects` — point the user at it so they can see their projects in a browser.
