# Playbook — novice user on a web-chat client

Use this playbook when the user is a small-business owner (plumber, baker, wedding planner, freelancer) typing plain English, and you are running inside a client that cannot execute outbound HTTP requests like `curl` and cannot read or write local files — but can call MCP tools and see images the user drops into chat. Current examples of clients in this bucket: 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` or read a local build folder, switch to `guide(topic="novice_console")` instead.

## Orient the user, translate the jargon

The user thinks in terms of "my shop", "my customers", "my website" — not microsites, hosting, or subdomains. Translate before you speak.

> Say something like: _"We can put your shop online in a few minutes — it'll have its own web address you can text to customers and print on flyers."_

To yourself, be precise: call the REST endpoints and MCP tools by their real names (`POST /projects`, `project({op: "create"})`, `upload_text({...})`, `page({op: "update", metadata: {...}})`) when deciding what to do, then tell the user in plain words what just happened ("your site is live").

## Auth — start with a guest session, offer Google later

If you are already connected through MCP, skip this section — the connector has already authenticated the user. The shared reference you already loaded via `guide(topic="orient")` has a "Connected via MCP" block with the details.

Otherwise, begin with a guest session. Call `POST /auth/guest?token=true` and keep the `access_token` in memory. Do not mention the words "token", "guest", or "expiry" yet.

> Say something like: _"Let's get your site up on the screen first — you can sign in with Google later to keep it forever."_

Save the deferred Google sign-in for after the user has seen their site on a live URL. Pushing OAuth before the reveal is the single biggest reason a new user drops out of the conversation.

## Project — derive the subdomain silently from the business name

Ask the user for the business name and derive a reasonable lowercase-hyphenated subdomain yourself. _"Brighton Bakery"_ becomes `brighton-bakery`. Don't ask the user to pick one.

Call `POST /projects` with that derived value. If the platform rejects it — already taken, reserved, or invalid characters — silently try a second candidate (add the city, the trade, or a short suffix: `brighton-bakery-uk`, `brighton-bakery-shop`) and call again. Only surface the accepted choice to the user:

> Say something like: _"Your site will live at **brighton-bakery.uat-beam.page** — does that look right?"_

Don't say the word used internally for this lowercase-hyphenated name; talk about the web address.

## Review — the URL is the moment

The moment the project exists, hand the user the subdomain URL. That reveal is the point of the first minute of the conversation.

> Say something like: _"Here's your site — **https://brighton-bakery.uat-beam.page** — give it a moment to go live, then open it in a new tab."_

Eventual consistency (the "Things to know" block of the shared reference you already loaded) means the very first reload may still show the default template; mention that once so the user doesn't think something's broken.

The default stance for this profile is **static pages plus the email action** — covers nearly every small-business ask (restaurant, trade, portfolio, event). SPAs exist on uat-beam.page under `/app/` but are a builder-profile concern; don't introduce that path here unless the user explicitly asks for something only an app can do (filterable catalogue with saved favourites, a dashboard, a live-updating tool), in which case say once that _"that's a bigger piece of work — happy to build it if you want"_ and let them decide.

## First content — inline HTML only, no file uploads

On a web-chat client you cannot run `curl`, you cannot read the user's laptop, and you cannot build a ZIP. Stick to the inline-content path:

- Generate the HTML yourself in the chat.
- Upload it with `PUT /projects/<id>/pages/_root/files/index.html` and a `{"content": "..."}` body — or the MCP `upload_text({projectId, scope: "page", slug: "_root", filename: "index.html", content})` tool.
- Do not paste image bytes, PDF bytes, or anything binary into `content`. The endpoint does not decode base64; it treats `content` as raw UTF-8 text only.

For the full picture of the four upload methods and their trade-offs, the "Files" block of the shared reference you already loaded has the detail.

## Getting found on Google — fill in the metadata, don't ask

Before moving on to iterate, set up the search-engine basics without quizzing the user on copy. The user volunteered the business name, roughly what they do, and (often) a location; that is enough to fill in:

- A descriptive `<title>` (e.g. _"Brighton Bakery — fresh sourdough & cakes in Kemp Town"_).
- A one-line `<meta name="description">` written as a search-result snippet — one sentence, 120–160 characters.
- OpenGraph tags so link previews render in WhatsApp / Facebook / iMessage: `og:title`, `og:description`, `og:image`, `og:url`. If the user hasn't provided a hero image, point `og:image` at the default `/assets/logo.svg` the platform seeds on project creation, or generate a brand-coloured typographic card — never omit it.
- A `<script type="application/ld+json">` block with a schema.org `LocalBusiness` JSON-LD object. Pick the subtype that fits what they told you: restaurant / café / bakery → `Restaurant`; plumber / builder / electrician / locksmith → `HomeAndConstructionBusiness`; salon / barber / spa → `BeautySalon`; accountant / solicitor / financial adviser → `FinancialService` or `ProfessionalService`; anything else → the base `LocalBusiness`. Include `name`, `address`, `telephone`, `openingHours`, and `geo` (lat/lng) **only** if the user supplied them — a partial object is fine; a fabricated one is not.

Write the copy yourself. Do not ask the user _"what should I put in the meta description?"_ — they don't know and shouldn't have to.

Mention once, in one sentence, that the site automatically publishes a sitemap at `/sitemap.xml` and they can submit that URL to Google Search Console if they want to tell Google the site exists. Don't walk them through Search Console itself — that's Google's UI, not ours. Don't create a `robots.txt` either: the platform already synthesises a permissive one on every subdomain.

## Images — public URL, placeholder, or one-step hosting

A photo the user drops into chat is visible to you as multimodal input, but you cannot upload it anywhere. The inline-content upload is text-only; there is no `curl`, no presigned S3 PUT, no ZIP. Do **not** try to base64 the bytes into `content` — the endpoint does not decode base64 and the file will be corrupted or rejected.

Walk the user through this fallback, in order:

1. **Already-public URL.** Ask if the photo already lives on their website, Facebook page, Google Business listing, or Instagram. If it does, use URL-fetch: `PUT /.../files/hero.jpg {"url": "https://..."}` and the platform fetches it for you.
2. **Neutral placeholder.** If no public URL is available, generate a tasteful inline-SVG placeholder or a brand-matching typographic hero, or pull a free stock image from Unsplash or Pexels by its public URL. Tell the user you are using a placeholder for now.
3. **One-step public hosting — ImgBB, then URL-fetch.** If the user insists on their own photo and has no URL, walk them to **ImgBB** in one message:

   > Say something like: _"Open **imgbb.com** in a new tab and drag your photo onto the page — you don't need an account. When it finishes, there's a box labelled **'Direct links'** — copy the URL from that one (it'll look like `https://i.ibb.co/XXXX/your-photo.jpg`) and paste it back here."_

   Check the shape before you URL-fetch. If the pasted URL doesn't start with `https://i.ibb.co/` — e.g. they pasted a page URL like `https://ibb.co/...` — gently redirect: _"that's the share page; on that page, right-click the image itself and pick 'Copy image address' — it should end in `.jpg` or `.png`."_ Only call the URL-fetch once the shape is right.

   If ImgBB is down or the user can't reach it, **PostImages** (postimages.org, direct URL shape `https://i.postimg.cc/XXXX/your-photo.jpg`) is the backup — same drag-and-drop flow, no account needed.

Do not recommend Google Drive's default share link for images — it's a viewer page, not a direct image URL, so URL-fetch will fail. Other public hosts geoblocked in the UK have the same dead-end problem; stick to the two named above.

> Say something like: _"Perfect, I can see the photo — to actually put it on your site I need a web address for it. Do you already have it on Instagram or Google Business? Or would you like me to use a tasteful placeholder while we figure that out?"_

PDFs and video take the same shape: public URL first, a simple link or placeholder second, one-step public hosting (ImgBB for images, a host the user has for other formats) last.

## Email action — offer a form shaped to their business, not "a contact form"

The platform's `email` action takes any JSON payload from any form on the site and emails it straight to the owner. It isn't restricted to "contact us" — it's the general-purpose _something-happens-on-the-site → owner-gets-an-email_ mechanism. Use it for a baker's cake-reservation form, a plumber's quote request with a photo of the problem, a wedding planner's RSVP, an application form, a waitlist signup, a donation pledge.

The moment you know the business type — right after the live-URL reveal and the first content — offer the form by business shape, not generically:

> Say something like: _"Should customers be able to reserve cakes or request special orders through the site? When they fill it in, the details come straight to your inbox, like getting a WhatsApp message but through your site."_

Same pattern for other business types — plumber: _"should people be able to request a quote, maybe with a photo of the problem?"_; wedding planner: _"would you like an RSVP form for guests?"_. Don't ask _"do you want a contact form?"_ — that's the narrowing this reframing exists to fix.

The action **requires a Google account**. That makes it the strongest lever for the Google conversion below: frame the conversion as _"if you sign in with Google, your customers can email you when they fill in the booking form"_, not abstractly as _"keep it forever."_

Offer the form once. If the user declines, don't push it again. For the HTML pattern, the Alpine.js scaffold, the rate limit (10 emails per project per hour), pre-filling via query params, and the exact body shape, consult `contact-form.md` — and note the file name narrows the framing: **the same pattern applies to bookings, RSVPs, quotes, applications, surveys, and waitlist signups** — don't let the name fool you.

## Iterate — conversational change requests

Expect the user to describe changes in plain English — _"change the phone number", "add Sunday hours", "swap the hero photo"_. For small textual edits prefer `POST /edit` with an `{old, new}` pair rather than re-uploading the whole HTML (and remember the typographic-character pitfalls in the "Common pitfalls" section of the shared reference you already loaded — curly quotes and `&mdash;` silently fail to match).

A category of change that matters for this profile is **feel / taste iteration** — _"I don't like this font", "can the colours match my logo", "make it look more professional", "make it feel more elegant"_. When a style-tweak ask arrives, **offer 2 or 3 concrete directions before regenerating anything** rather than asking the user for fonts, weights, or hex codes:

> Say something like: _"Do you want warm and traditional (cream / brown, serif headlines), clean and modern (white / single accent, geometric sans-serif), or bold and contemporary (high-contrast, strong display type)?"_

Then regenerate the relevant HTML/CSS inline — same path as other web-chat iterations, no new primitive.

If the user asks for something that will change regularly — a daily menu, a weekly event list, a rotating set of listings — switch to the relevant scenario guide: `restaurant.md`, `event-page.md`, `job-board.md`, `property-listings.md`. Those patterns set up `metadata` + `cross_page_metadata.json` so the next LLM can edit one structured object instead of rewriting HTML each time.

## Durability — offer Google conversion after the reveal, then stop nagging

Once the user has seen their site load and asked for at least one change (or accepted the email-action offer above), lean on the email action as the conversion lever:

> Say something like: _"If you sign in with Google, your customers can email you when they fill in the booking form — and your site stays yours forever."_

If they accept, walk the conversion flow: `GET /auth/google` → hand the user the returned URL → ask for the pasted code → `POST /auth/convert`. If they decline, don't push. Note once — in one sentence, not a paragraph — that a guest session lasts about two hours, then move on. Custom domains are coming but not available yet (match the "coming soon" phrasing from the shared reference); don't promise one that doesn't exist.

## Do not

- _"I can't upload your image"_, _"paste a base64 string"_, _"base64-encode it"_, _"switch to Claude Code"_ — each dead-ends the conversation.
- Recommend any public image host that is geoblocked in the UK — the user will hit a "not viewable in your region" wall; the two named above are live in the UK today.
- Ask the user to write the `<meta description>`, OG copy, or JSON-LD fields — derive them from what they already told you.
- Say _"do you want a contact form?"_ — propose the form shape that fits their business instead.
- Re-call `guide(topic="orient")` or re-fetch `llm.txt` when you need depth — the shared reference is already in your context; pick the relevant section by name.

## Next steps — where to go for depth

When the user asks for something this playbook deliberately left out, reach for the right source by calling `guide(topic="...")` with the matching scenario topic:

- Daily-changing menu → `guide(topic="scenario_restaurant")` (https://uat-beam.page/llm/restaurant.md)
- Single event with RSVP → `guide(topic="scenario_event_page")` (https://uat-beam.page/llm/event-page.md)
- Photo gallery (still only via public URLs on web chat) → `guide(topic="scenario_photo_gallery")` (https://uat-beam.page/llm/photo-gallery.md)
- Booking / quote / RSVP / application / contact form (requires Google) → `guide(topic="scenario_contact_form")` (https://uat-beam.page/llm/contact-form.md)
- Copy an existing page as a starting point → `guide(topic="scenario_copy_page")` (https://uat-beam.page/llm/copy-page.md)
- General reference (errors, conventions, redirects, snapshots, edit semantics) → the `llm.txt` you already loaded via `guide(topic="orient")`.

Reach those via MCP tools — don't hand the user raw URLs.
