# Kibble installation contract: section 23 of 29 (guide version 3)

> This is **one section only**, not the whole contract. Complete document: https://www.usekibble.dev/AI/guide.md

## 23. Site catalog: name the ids your reports show {#site-catalog}

Reports show business subject ids, such as a SynXis hotel id or a product id. The site catalog maps those ids to readable names, per property. It is presentation only: counts, ids and published reports never change, names describe the current catalog (not the name at the time of activity), and no AI service is involved when data is collected or reports are read.

Pages are named differently. A page is shown by its page title once at least three browsers saw the same title on a day (account, checkout, confirmation and search pages never are); otherwise by its content id. Staff can name a page, or keep its page title as the name, in Catalog (open the page in Browse and edit its name in the drawer). Manifests (`kibble_catalog` 1) carry business entities only, not page names. To give pages stable identities, add [`kibble:content`](https://www.usekibble.dev/AI#markup).

### What a manifest may contain

Public business entities and their identifiers, and nothing else. **Never** private keys or tokens, staff credentials, people, guests, customers or visitors, form values, search phrases, private CMS content, prices meant for one customer, or instructions. Kibble treats every manifest as untrusted data: text is stored literally, never executed and never followed.

| Field | Rule |
| --- | --- |
| `kibble_catalog` | Required. Must be `1`. |
| `mode` | `patch` (default): adds and updates listed entries, leaves others alone. `replace`: also archives (never deletes) entries of each namespace + type pair present in the manifest that it omits. Synchronized manifests should use `patch`; see [Hosting and synchronizing](https://www.usekibble.dev/AI#manifest). |
| `report_namespaces` | Per entity type, the one catalog namespace reports use to name this property’s collected ids, for example `{"hotel":"synxis"}`. Collected events do not record which provider an id came from, so this is configuration, not proof: it applies to all retained history of that type. Without it nothing resolves for the type. Changing an existing value re-names history and needs an admin’s confirmation. |
| `generator` | Optional free text for humans (which script or person produced the file). Not stored. |
| `entities[].namespace` | Whose ids these are: 1–32 lowercase letters, digits, `-` or `_` (for example `synxis`, `shopify`, `site`). Distinct from the observed event source. |
| `entities[].type` | `hotel`, `destination`, `room` or `product`. Catalog `hotel` names the collected subject type `property` (a SynXis tag or dataLayer `hotel_id`); the others map one to one. |
| `entities[].id` | A JSON **string** (`"07421"`, never `7421`), exactly as the site sends it: leading zeros and case are kept as submitted. 1–128 characters, no leading or trailing spaces. It must be an id collection can record: letters, digits and `_ . : - /`, starting with a letter or digit, no email addresses, and a run of six or more digits needs a prefix with a colon (`hotel:123456`). |
| `entities[].name` | Required, 1–120 characters. |
| `short_name`, `locality` | Optional; 60 and 80 characters. Locality is a business location such as a city, never a visitor location. |
| `page_url`, `image_url` | Optional: https on one of the property’s registered hosts, no query string or fragment, at most 500 characters. Kibble stores them; it never fetches them. |
| `provenance.kind` | `site_owned` (read from authoritative site data), `operator` (confirmed by a person), or `inferred` (a guess: stored as a suggestion, not used in reports until an admin confirms it). |
| `provenance.note` | Optional, 200 characters: name the file, record or field the entry came from. |

- Limits: 256 KiB, at most 2000 entities per manifest, 5000 stored entries per property. Text must not contain emails, phone numbers or control characters. Unknown fields are refused.
- Ids that differ only by letter case collide (collection lowercases ids) and are refused; so is the same namespace, type and id twice.
- An `inferred` entry never replaces an approved one. Unresolved ids are left out and listed in your report, never filled with a guess.
- JSON Schema (structure): `/AI/site-manifest.schema.json`. The schema checks shape; Kibble’s validator also checks hosts, id rules and collisions, and reports every problem with its entry and field.

### Generate it from the site’s own data

1. Find the authoritative source: public CMS records, explicit booking configuration (a hotel `<select>` whose option values are booking-engine ids), a product catalog or export, or a known integration mapping.
2. Match each id to the value the tracking code actually sends to Kibble.
3. Where practical, add a small build step that writes `/kibble/site.json` from that source (for example a script run at build time), so names never drift from a hand-maintained copy. Keep it bounded and independent of visitor requests: no generation per page view, no crawling.
4. Validate it (the schema, then a Preview in Kibble) and list unresolved ids.
5. Say who maintains the source when hotels or products change.

GTM-only or CMS-only installation: without repository access, work from the public page source a person provides, mark entries `inferred` until a person confirms them, and give the file to a Kibble admin to upload (Catalog → Import). Tags and the dataLayer never carry catalog data.

### Images

Reports show an image beside a page or a named hotel, room, product or destination only when the address is public and belongs to the property: https, on one of the property’s website hosts or on an **image host** an admin listed (for example a CDN, in Catalog → Settings → Learning), with no credentials, query string or fragment. Signed or expiring addresses are never kept. Booking and checkout connection hosts are never image hosts. Kibble never fetches an image: staff browsers load it directly, and a missing or failed image simply shows no picture.

- **Pages**: the page’s own `og:image`, once at least three browsers saw the same image on a day (like page titles). Account, checkout, confirmation and search pages never get one.
- **Named items**, in this order: the `image_url` in the site manifest; an image staff set in Kibble; a discovered image staff chose; otherwise, automatically, the item’s **single** discovered image once it was seen on two days. When the site shows a second, different image for the same id, automatic use stops until staff choose. A discovered image never replaces a manifest or staff image.
- New installation: list any CDN host first, then either add `image_url` to the manifest or turn on Image suggestions. Existing properties are never switched on automatically.

### Learning from pages (opt-in, optional)

A property’s admin can switch on learning in Catalog → Settings: one switch, **Learn from this site’s pages**, turns all four parts on or off together (it reads **Custom** when only some are on), and **Choose what Kibble learns** shows the four independent opt-ins: **Name suggestions**, **Image suggestions**, **Page details** and **Forms without Kibble markup**. Each is **off by default**; until one is on, nothing extra loads or is sent. When any is on, the SDK loads a small extra script (`/sdk/v1/observe.js`) from the same Kibble origin as the SDK, only after consent is granted, and sends only what the enabled opt-ins cover. Names, page details and form structures are shown **only to staff in Catalog** (page details and form structures only after Kibble has seen them on two different days) and never appear in reports or MCP; discovered images follow the Images rules above. Nothing carries a visitor, browser, visit or stream id. Name suggestions wait in Catalog → Review and can be accepted or dismissed; accepting creates a normal operator catalog entry.

**Name suggestions** read four bounded, structured sources only: JSON-LD (at most 5 scripts of 20,000 characters each, 50 nodes; hotel, lodging, room, product and destination types with an `identifier`, `sku` or `productID`; never `@id` or `url`), the markup below, the `item_id` and `item_name` of items in recent GA4 dataLayer ecommerce events, and the options of booking-form `property` and `destination` selects (see [Booking engines and booking widgets](https://www.usekibble.dev/AI#booking)). Ids follow the collector id rules: a run of six or more digits needs a prefix with a colon (`hotel:123456`), and names containing an email address or phone number are dropped.

**Image suggestions** read, from the same JSON-LD items, the image an item declares for itself (`image`, the first of an `image` list, or `image.url` / `image.contentUrl`) beside the item’s `identifier`, `sku` or `productID`. Other images on the page are never read, and an image is never inferred to show an item because both appear on one page.

**Page details** read, once per page: the `kibble:content` meta (when it is a valid content id), the `description` meta (up to 300 characters), the text of the first `h1` (up to 160), the canonical link (only its path when on the same host, otherwise just the words `other-host`; never a query string), whether the robots or googlebot meta says `noindex`, and the `lang` attribute of the `html` element. Text containing an email address or phone number is dropped. No other page text is read.

**Forms without Kibble markup** look at up to 10 forms per page that lack `data-kibble-form`, and read only structure: the form’s `id` or `name` (when it is a plain identifier without a long run of digits), its `action` (a path on the same host, or just the host of another site), the method, how many controls of each kind it has (text, email, tel, select, textarea, checkbox, hidden and so on; up to 100 controls) and the `autocomplete` tokens present. They never read labels, placeholders, field values, option text or button text. Site search forms (`role="search"`, inside a `role="search"` region, or with a search field) are never listed: search is measured with the [search hook](https://www.usekibble.dev/AI#search), not form markup. Marking a form with `data-kibble-form` ([Forms](https://www.usekibble.dev/AI#forms)) is still how to get its starts and errors measured; this only shows staff which forms exist without markup.

None of the three reads form values, other dataLayer fields or objects, visitor ids, or query strings, and none sends a page title or timestamp. Account, checkout, confirmation, search and sign-in pages are skipped (by path, by `kibble:type`, or when the page has a password field). Evidence that stops appearing is removed: pending names and page details after 13 months without being seen, forms after 90 days; accepted and dismissed names are kept.

Name a hotel, destination, room or product on the page itself:

```html
<div data-kibble-entity="58308" data-kibble-entity-type="hotel" data-kibble-name="Times Square">...</div>   <!-- hotel|destination|room|product -->
```

- Use the attributes on an element that is on the page whenever the id and name are known; the name is read from `data-kibble-name`, never from the element’s text.
- Content-Security-Policy: the module loads from the same Kibble origin as the SDK and posts to the same origin, so a policy that already allows the SDK needs nothing more.
- Not available in Shopify’s sandboxed custom pixel: it has no page DOM to read.
- This is a hint for staff, not a source of truth. For authoritative names use a manifest.

### Examples (validated against Kibble’s validator)

Hotel group (/AI/examples/hotel-group.json):

```json
{
  "kibble_catalog": 1,
  "mode": "patch",
  "report_namespaces": {
    "hotel": "synxis",
    "destination": "site"
  },
  "generator": "scripts/kibble-manifest.mjs from config/hotels.json",
  "entities": [
    {
      "namespace": "synxis",
      "type": "hotel",
      "id": "58308",
      "name": "Harbor Hotel, Times Square",
      "short_name": "Times Square",
      "locality": "New York",
      "page_url": "https://www.example.com/hotels/times-square",
      "image_url": "https://www.example.com/img/hotels/times-square.jpg",
      "provenance": {
        "kind": "site_owned",
        "note": "config/hotels.json: synxisId 58308, name, slug"
      }
    },
    {
      "namespace": "synxis",
      "type": "hotel",
      "id": "07421",
      "name": "Harbor Hotel, Boston Seaport",
      "short_name": "Boston Seaport",
      "locality": "Boston",
      "page_url": "https://www.example.com/hotels/boston-seaport",
      "provenance": {
        "kind": "site_owned",
        "note": "config/hotels.json: synxisId 07421 (leading zero kept)"
      }
    },
    {
      "namespace": "site",
      "type": "destination",
      "id": "nyc",
      "name": "New York City",
      "provenance": {
        "kind": "site_owned",
        "note": "Booking widget destination select: value nyc"
      }
    }
  ]
}
```

Online store (/AI/examples/store-products.json):

```json
{
  "kibble_catalog": 1,
  "report_namespaces": {
    "product": "shopify"
  },
  "generator": "Shopify product export, published fields only",
  "entities": [
    {
      "namespace": "shopify",
      "type": "product",
      "id": "gid:shopify/product:8123456789",
      "name": "Cedar Bath Oil",
      "page_url": "https://shop.example.com/products/cedar-bath-oil",
      "provenance": {
        "kind": "site_owned",
        "note": "Product export: id and title"
      }
    },
    {
      "namespace": "shopify",
      "type": "product",
      "id": "bath-salts-01",
      "name": "Sea Bath Salts",
      "provenance": {
        "kind": "inferred",
        "note": "Product page heading; confirm in Kibble"
      }
    }
  ]
}
```

The second example shows an `inferred` entry: it becomes a suggestion that an admin confirms in Catalog. Hosts and names in these examples are fictional.

END OF SECTION site-catalog
