← Back to Skills

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.md
Download .md

Coming soon via CLI:

tawa chaac install tawa-hypesite

Details

Format
Command
Category
platform
Version
1.0.88635
Tokens
~3,234
Updated
2026-10-13
platform