# Build and publish a Latchbell website, start to finish
Read https://latchbell.com/business.md first: it has the sign-in and the rules. This page is the whole path for a website you write yourself, with the business's AI chat on it, in order.

A new business shows customers nothing until it goes live: its setup finished (`POST update_front_desk {"complete": true}`) and a paid plan started (`GET receptionist_service` lists them, and the owner pays in Stripe: https://latchbell.com/docs/billing.md). Any plan will do: a website and its chat need no phone line. Until then you can upload, look at `preview_url` and publish, but `website_url` shows nothing and the chat doesn't answer. Tell the owner before you start.

Build only for a business whose owner asked you to, signed in as that business. A preview of somebody else's business, made to show its owner, is Latchbell's to make: don't sign up a business you don't run.

## 1. Know which business you're acting for
- One sign-in is one business on one Latchbell server. `GET my_business` names it (`business`, `business_id`). Every answer from `my_website`, `upload_site_pages` and `publish_website` carries `business`: `{ "id", "name", "slug", "server" }`. Check it before every write, and stop if it isn't the business you mean.
- Production is https://latchbell.com. A staging server is another server with its own businesses and sign-ins: set `LATCHBELL_BASE_URL` to it for every helper call.
- The helper keeps one sign-in per server. For a second business on the same machine, give each its own profile: `LATCHBELL_PROFILE=shop node latchbell.mjs signin new`, then `LATCHBELL_PROFILE=shop` on each call for it. `signin new` without a profile refuses to replace a sign-in that's already there.
- Each business has its own email: an email that already has a business signs in to it instead of creating another.

## 2. Create the business (if it's new)
`node latchbell.mjs signin new` opens **Create your business** in the owner's browser: business name, their name, email, mobile, ZIP and kind of business. The owner types these, never you in the chat; the mobile is where Latchbell reaches them, and customers never see it.
The page address comes from the business name: "Maple & Mop" is `maplemop`, at https://latchbell.com/b/maplemop and its website at https://maplemop.latchbell.com. Renaming with `POST update_business {"business": "…"}` moves the address too, as long as nothing has used it yet (no website draft, photo or published page, and no customer). After that the address stays, so settle the name first. `update_business` answers the address in `customer_page`.

## 3. Teach its AI
The chat on the website answers from what the AI knows. Add the owner's facts as in business.md step 3 (`add_receptionist_source` with a `url`, a `note` or a file), then `GET receptionist` every 20 seconds until `still_reading` is false, and check the `draft` against the owner's facts.

## 4. Decide what customers can do on it
- **Book or order:** set the catalog, hours and rules (business.md step 5). The pages must then have a working booking or ordering flow built as https://latchbell.com/docs/site-api says, or publishing refuses them. Pages without `/latchbell.js` also get a floating "Book <business>" button.
- **Read and ask questions only** (a showcase, a product that's sold elsewhere): `POST update_booking_rules {"mode": "off"}` and leave the catalog off. Then no page gets a Book button, the footer says "Made with Latchbell", and publishing asks for no booking flow. Link out to where things are sold with ordinary links.
`GET receptionist_readiness` shows what's on (`booking.enabled`).

## 5. Photos
`POST upload_site_photo` for each image (JPEG, PNG or WebP; over REST, the raw bytes up to 8 MB with `Content-Type` and `file_name` in the query) and use the `url` it returns. An image with a public https address can go in the page as is: it's copied into the business's photos when you upload or publish, up to 8 MB each. Never `data:` addresses.

## 6. Write the pages
Each page is a whole HTML document of up to 400 KB, at a path such as `/`, `/about` or `/gallery` (a website needs `/`).
- CSS and scripts are inline. Only `<script src="/latchbell.js"></script>` and Google Fonts stylesheets load from outside; uploads using anything else are refused with what to change. Frames: YouTube, Vimeo and Google Maps embeds only.
- Facts Latchbell fills in, so they stay current: `{{business}}`, `{{slug}}`, `{{phone}}` and `{{phone_url}}` (an element with `data-lb-phone` is hidden while the business has no number), `{{hours}}`, `{{area}}`, `{{booking_url}}`, `{{price.<item id>}}`.
- **The chat:** load `/latchbell.js` and add `<div data-lb-bell="{{slug}}"></div>` once per page: a bell in the corner opens the chat. `data-title="…"` on that div names the panel ("Chat" by default). Any element with `data-lb-open-chat` opens it too, such as a Chat link in the menu. `<section data-lb-chat></section>` puts the chat inside the page instead. Never use `chat_embed.script_embed` here: that one is for sites hosted somewhere else.
- Booking and ordering flows: https://latchbell.com/docs/site-api.

A complete page with the chat:

```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{{business}}</title>
<style>
  body { margin: 0; font: 17px/1.5 system-ui, sans-serif; color: #1d1d1f; }
  header, section { max-width: 960px; margin: 0 auto; padding: 24px 20px; }
  header { display: flex; gap: 16px; align-items: center; justify-content: space-between; flex-wrap: wrap; }
  img { max-width: 100%; height: auto; display: block; border-radius: 12px; }
  .btn { display: inline-block; padding: 10px 18px; border-radius: 999px; background: #1d1d1f; color: #fff; text-decoration: none; }
</style>
<script src="/latchbell.js"></script>
</head>
<body>
<header>
  <strong>{{business}}</strong>
  <nav><a href="/gallery">Gallery</a> · <a href="#" data-lb-open-chat>Chat</a> · <a data-lb-phone href="{{phone_url}}">Call {{phone}}</a></nav>
</header>
<section>
  <h1>Say what the business does, in the owner's words</h1>
  <p>One or two sentences from what the owner gave you.</p>
  <img src="PHOTO_URL" alt="Describe the photo"><!-- the url upload_site_photo returned -->
  <p><a class="btn" href="#" data-lb-open-chat>Ask us anything</a></p>
</section>
<div data-lb-bell="{{slug}}" data-title="Ask {{business}}"></div>
</body>
</html>
```

## 7. Upload, look, publish
1. `GET my_website` for `rev` (and `working`: uploads wait while Latchbell's designer is working).
2. `POST upload_site_pages {"pages": [{"path": "/", "title": "Home", "html": "…"}], "rev": <rev>}`. Pages you don't send stay; `"remove": ["/old"]` takes one off. The answer has `business`, `saved`, the new `rev` and `warnings`. A refusal says what to fix: fix that and keep the design, never send a simpler one.
3. `GET my_website`: `preview_url` shows the draft (good for an hour; read `my_website` again for a new one), and `check` lists problems the publish step will refuse. Show the owner `preview_url`.
4. When they approve: `POST publish_website {"rev": <the rev they saw>}`. Publishing runs the page checks and a real browser on every page. `"publishing": true` means the browser check is still running: read `my_website` every 20 seconds until `published` is true, or `browser` says what failed. A draft that changes before then isn't published.
5. Published, the answer has `website_url`, `rev`, and `shows`: `book_button` lists the pages with Latchbell's Book button and `chat` the pages with the chat.

To change it later, upload again and publish again. The owner can go back to an earlier draft in the Website studio (https://latchbell.com/app), then publish it.

## 8. Check it yourself, and say what you checked
Published means the checks passed, not that someone looked at it. Once the business is live (see the top of this page), before you tell the owner it works:
- Open `website_url` at phone width and at computer width: nothing cut off or wider than the screen, every image shows.
- Open the chat and ask a question the AI should know: it must answer from the owner's facts. It needs the AI switched on and AI credits (`GET receptionist_readiness`).
- Book or order once only if the owner agrees, and tell them which you tried.
Tell the owner what you checked and what you didn't.
