Build a HypeSite
The procedure for turning a brief ("a landing page for Acme with a waitlist") into a published site. The always-on rules are in `tawa-hypesites`; this is the…
Commandplatform
About
The procedure for turning a brief ("a landing page for Acme with a waitlist") into a published site. The always-on rules are in `tawa-hypesites`; this is the order of work.
Skill Content
This is the raw markdown that gets installed as a Claude Code command.
---
description: Build and publish a HypeSite — a public website made from a portal project's artifact pages, with its tables as the database.
---
# Build a HypeSite
The procedure for turning a brief ("a landing page for Acme with a waitlist") into a
published site. The always-on rules are in `tawa-hypesites`; this is the order of work.
## 0. Settle these first
Ask only for what you cannot infer, in one message:
| | |
|---|---|
| **Project** | Which portal project the pages and tables live in (`find_projects`). Everything is scoped to it. |
| **Address** | The slug: `hypesites.tawa.pro/<slug>/`. Propose one from the name. |
| **Data** | Does a page show rows, collect rows, both, or neither? Name the table, or say you will create it. |
| **Domain** | Will it live on its own domain? If yes, do step 0.5 before writing a word. |
| **Message** | What the site says, in one line, when the domain alone does not tell you. Never invent a company's positioning for a public page. |
If a site with that slug already exists (`list_sites`), you are **updating** it, not making
a second one.
## 0.5 If it gets its own domain, look the domain up first
```
webmaster_property_get { domain: "acme.com" } # verified in Search Console? DNS on Cloudflare?
read_table { pageKey: "mkt-domain-map", filters: [{ columnId: "Domain", op: "eq", value: "acme.com" }] }
```
| What you find | What it means |
|---|---|
| `verified`, `dns: cloudflare` | The easy path. `tawa domain add --cloudflare` will write the record |
| Not verified | `webmaster_verify { domain }` first, so the sitemap has somewhere to go on publish |
| DNS not on Cloudflare | `--cloudflare` will not work. Plan a DNS move (`webmaster_dns move_plan`) or use `--direct` |
| A Domain Map row | Read its Family, Role and Notes. That row is updated at the end (step 8) |
| No Domain Map row | Say so. It is a domain nobody has catalogued |
## 1. Look at what the project already has
```
list_sites { slug: "<slug>" } # an existing site: its pages, grants, and everything it may use
find_pages { project: "<project>" }
```
`list_sites` with a slug returns the project's artifact pages and its tables **with their
columns** — the exact names `create_site` will accept. Never guess a column name.
## 2. Build the data first, if there is any
```
create_table { project, title: "Acme Waitlist",
columns: [{ name: "Email" }, { name: "Name" },
{ name: "Interest", type: "select", options: ["…"] }] }
```
Types earn their keep here: the site refuses a submission that does not fit the column, by
field name, before it reaches the table. A `select` becomes a real allow-list.
For a page that *shows* rows, seed enough real rows that the page has something to render.
## 3. Write the pages as artifacts
One `create_page { type: "artifact" }` per screen, each a complete HTML document. Give each
a `pageKey` so you can revise it later without hunting for its slug.
Things that are specific to a site page:
- **Links between pages** are written as if the site owned its domain: `href="/pricing"`.
- **Data** comes from `HypeSite.data(alias)` / `HypeSite.submit(alias, row)` — injected into
every published page. Guard with `if (window.HypeSite)` so the page still renders in the
portal's own preview.
- **No storage.** `localStorage` throws on the platform host.
- **Assets** (images, fonts) go up with `upload_page_asset` and are referenced as
`assets/<name>`. Do not inline large images; the whole site must stay under 12 MB.
- A form for visitors is an **append grant**, never `create_form`.
- Write `<title>`, a meta description and Open Graph tags on every page. Put `noindex` on a
thank-you page.
### Read the copy before it is public
Delegating the pages to another agent is fine. Publishing them unread is not. Before step 5,
`read_page { include: "text" }` each page and check:
- **No figures, names or terms from internal sources.** No investor or partner names, deal
terms, prices, returns or internal codenames, even when the brief's source pages have them.
- **No regulated claims we cannot make.** Do not say "licensed", "regulated", "insurer",
"reinsurer" or "captive manager" about us. Say who does that work ("we work with licensed …")
and carry a footer disclaimer when the subject is regulated.
- **No invented proof.** No testimonials, statistics, client logos or awards.
- **No promises the team has not agreed**, such as reply times or SLAs.
## 4. Make the site
```
create_site {
project, name, slug,
pages: [{ path: "/", page: "<page slug>" }, { path: "/pricing", page: "<page slug>" }],
description: "one sentence for search results and link previews",
data: [{ alias: "waitlist", table: "acme-waitlist", mode: "append", columns: ["Email", "Name", "Interest"] }]
}
```
- **A page at `/` is required** — publishing is refused without one.
- `columns` is the whole exposure. List what the page needs and nothing else; a read grant
is public data. Never grant an email or phone column on a `read`.
- Pages, tables, columns and views are named by slug or name.
- A visitor form should reach a person. Give the append grant an `onSubmit`:
`{ ticket: { project, assigneeBioId, title: "Enquiry — {Company} ({Name})" }, notify: ["…"] }`.
Add `crm: { mode: "candidate", emailColumn, nameColumn, companyColumn }` if leads belong in
the CRM. `list_sites` shows how the live sites do it.
## 5. Publish, then check it
```
publish_site { slug }
```
Then actually open the URL it returns and confirm:
1. The home page renders, and its fonts and images load.
2. Every link goes to a page of the site, not a 404.
3. A read-backed section shows real rows.
4. A form submission succeeds **and the row appears** (`read_table`) — the truthful check,
because a form that looks fine can still be refused by the table.
5. **Clean up the test.** Delete your test row (`delete_table_rows`), and close the ticket and
ignore the email an `onSubmit` sent for it. Say that you did.
Report the live URL and the version number.
## 6. Changes afterwards
```
update_site { slug, … } # settings; list fields (pages, data, domains) replace whole
publish_site { slug } # REQUIRED — a page edit is invisible until you publish
```
A site can be taken down instantly with `set_site_status { slug, status: "offline" }`, and
an earlier version restored from the HypeSites tile.
## 7. Launch on a custom domain
Do the apex and `www` together.
```bash
# in the hypesites repo — the domain is attached to the hypesites SERVICE, not to the site
tawa domain add acme.com --cloudflare
tawa domain add www.acme.com --cloudflare
```
`--cloudflare` writes the CNAME and patches the live ingress, so **no deploy is needed**. It
refuses with `409 DNS_RECORD_EXISTS` when the name already points somewhere else. Find out
what that record serves before you remove it. A parked domain usually serves nothing.
`--external` and `--direct` need `tawa domain verify` and `tawa deploy --prod` afterwards, and
the owner of the DNS adds the record.
Then put the hostnames on the site and **publish again**:
```
update_site { slug, domains: ["acme.com", "www.acme.com"] }
publish_site { slug }
```
The publish is what connects the site to Webmaster Tools. It calls iec-webmaster's
`hypesite-published` hook. That hook registers the pages and submits the sitemap to Search
Console. It also takes the property over from `legacy`. Adding the domain without
republishing leaves Search Console with nothing.
### Check the domain, not your machine's DNS cache
Your machine may still hold the domain's old records. If requests time out, resolve
against a public resolver and pin the address:
```bash
dig +short acme.com @1.1.1.1
curl -s -o /dev/null -w "%{http_code}\n" --resolve acme.com:443:<ip from dig> https://acme.com/
```
Confirm on the custom domain:
1. Every page returns 200. An unknown path returns 404. `www` serves the same site.
2. `/sitemap.xml` lists the indexable pages (not `/thanks`). `/robots.txt` points to it.
3. `webmaster_property_get { domain }` shows `source: "hypesite"`. That is the proof the hook
ran. Search numbers arrive two to three days behind Google.
## 7.5 Add a blog fed by the marketing engine (optional)
A site gets `/blog`, `/feed.xml` and `/<brand>/feed.xml` from the marketing engine
(iec-marketing) when it lists a **brand** in `feedBrands`. Posts are written, reviewed and
published under that brand; the site only serves them.
**Check whether a brand exists first:**
```
marketing_pieces { brand: "acme" } # "\"acme\" is not one of insureco's brands" → it does not
```
Serve an existing brand only if its posts belong on this site. A specific site showing another
brand's general posts is worse than no blog.
**Adding a brand is a code change in iec-marketing** (one PR, then `tawa deploy --prod`; it is
a Tawa service, so merging does not deploy it). cayman is the worked example (iec-marketing#141,
PRs #142 and #144):
| File | What to add |
|---|---|
| `src/brands/seed.ts` | The brand: key, display name, parent `insureco`, blog `{ site, path: "/blog" }`, styles, `counsel` (`always` for regulated subjects), `newsFeed` (`false` keeps it off the InsurEco news hub) |
| `src/drafting/generate.ts` | `BRAND_REGISTER.<key>`: audience, voice, and the guardrails the drafts must obey (no regulated claims, no figures/partner names, the disclaimer verbatim) |
| `src/publisher/targets.ts` | The brand → portal project the site lives in |
| `src/blog/themes.ts` | A `THEMES` entry + `BRAND_THEMES` mapping, with a `shell` (CSS, nav links incl. Blog, footer) so the template matches the site |
Without a theme the first published post regenerates `/blog-template` in the neutral theme
and replaces any hand-built template. Give every brand a theme.
**Then, in this order:**
1. Add the site's project to the portal's `JCI_SITE_SERVICE_ALLOWLIST`
(`iec-marketing-prod:marketing|…|<project>`) and `tawa deploy --prod` the portal. Without
it the publisher cannot write to the project. Config only reaches the pod on a deploy.
2. Deploy iec-marketing. At boot it reseeds brands and writes the themed `/blog-template` page.
3. Add a **Blog** link (`href="/blog"`) to every page's nav.
4. `update_site { slug, feedBrands: ["<key>"] }`, then `publish_site { slug }`.
5. Check on the custom domain: `/blog` (an empty state reads "No posts yet."), `/feed.xml` and
`/<key>/feed.xml` are 200 (Atom), and `marketing_pieces { brand }` succeeds.
The blog is empty until someone writes for the brand. Offer to draft first posts; they go
through the brand's counsel setting before anything is public.
## 8. Update the Domain Map
The Domain Map is the marketing record of the estate, and iec-webmaster reads it every hour.
Set the row so it matches what is live:
```
update_table_rows { pageKey: "mkt-domain-map", rowIds: ["row-…"], set: {
"Role": "Landing / campaign", "Live status": "Live", "Serving now": "<the page <title>>",
"Audience": "…", "Message": "<the hero line>", "Mission": "<what the site is for>",
"Keywords": "…", "Recommendation": "Keep & build", "Decided": true,
"Notes": "HypeSite '<slug>' (project <p>) live on <domain> + www, v<N>. <date>." } }
```
- Use the select values the other rows use. Read a live row first, such as `policyos.io`.
- Role, Recommendation and Decided record a decision. Set them only when the person asked
for the site. Otherwise update the facts (Live status, Serving now, Notes) and leave the
decision to them.
- While you are there, look for other rows that say `Dark` or `(no site)` for a domain that a
site already serves (`list_sites` shows every site's domains). Offer to correct them.
## 9. Report
Report these things:
- the live URL and the version;
- the project, pages, table and grant;
- what the form does when someone submits it;
- the webmaster state and the sitemap URL;
- the blog brand and feed URLs, if it has one;
- the Domain Map change;
- anything still open, such as the test ticket or unverified search data.
## Refusals worth reading
| What it says | What it means |
|---|---|
| `No artifact page "x" in this project` | The slug is wrong, or the page is in another project. `list_sites { slug }` lists what is available |
| `Mount a page at /` | Add a home page before publishing |
| `"X" is a formula column` | A site cannot write to a computed column |
| `The address "x" is taken` | Another site owns that slug |
| `You need edit access to this project` | The person you are acting for is not a writer there. Do not retry as somebody else |
## Do not
- Do not publish somebody else's content, or a page impersonating a company, to a public
address. Ask first.
- Do not put anything confidential in a page or an asset — both are public the moment the
site is live.
- Do not add a data grant "for later". A grant is live exposure the moment the site is.
Install
Copy the skill content and save it to:
~/.claude/commands/tawa-hypesite.mdComing soon via CLI:
tawa chaac install tawa-hypesiteDetails
- Format
- Command
- Category
- platform
- Version
- 1.0.88635
- Tokens
- ~3,234
- Updated
- 2026-10-13
platform