Site map reference

One JSON document per tenant, validated before it is ever stored. Every array and record in it is capped, because this is customer-edited data that ends up in a prompt and in a URL — a map is not a place to smuggle a paragraph into the model.

The demo store's map

This is the real map behind the demo store — the one the screenshots on this site come from. Two filters, a sort, and how to read the results.

{
  "version": 1,
  "site": { "origin": "https://katman-widget.vercel.app", "searchPath": "/demo/", "language": "en" },
  "filters": {
    "category": {
      "label": "category",
      "aliases": ["type", "section", "department"],
      "type": "choice",
      "apply": { "via": "param", "param": "cat" },
      "values": {
        "Headphones": { "aliases": ["headphone", "headset", "earphones"] },
        "Mice": { "aliases": ["mouse", "mice"] },
        "Keyboards": { "aliases": ["keyboard"] },
        "Monitors": { "aliases": ["monitor", "screen", "display"] }
      }
    },
    "price": {
      "label": "price",
      "aliases": ["budget", "dollars", "cost"],
      "type": "range",
      "apply": { "via": "range", "minParam": "min", "maxParam": "max" }
    }
  },
  "sort": { "param": "sort", "tokens": { "price_asc": "price_asc", "price_desc": "price_desc" }, "control": "#sort" },
  "facets": { "container": "aside" },
  "results": {
    "container": "#results",
    "count": { "selectors": ["#status"] },
    "rows": { "selectors": [".grid .card"], "title": ["h3"], "price": [".p"], "link": ["h3 a"], "meta": [".m"] }
  }
}

You rarely write this by hand: Setup drafts it from your search page's URL, and the panel's Form tab edits the common fields without touching JSON at all. This reference is for the fields the form doesn't reach, and for reading a map someone else wrote.

In the panel: draft it, or edit it as form and JSON

The Site map screen can draft a map from a search-page URL, load one of a few starter templates, or edit the document directly — as a Form covering everything most sites need, or as raw JSON for fields the form doesn't expose yet. Editing JSON and saving rebuilds the form from what was actually stored, so the two never drift against each other silently.

The top of the Site map screen: a Draft from your search page URL field, a Start from a template picker, and the Form and JSON tabs below
Draft from a URL, or load a template, before dropping into the form or the JSON below it.
The site map's Form tab: origin, search path, language, how a filter is applied, the two filters, and the results container, count and row selectors
The Form tab, on the demo tenant's map.
The site map's JSON tab, showing the raw document with a Save changes button and a Remove the map link
The same map, as JSON. "We normally write this document for you. If you edit it by hand, read the validation notes before saving."

site

originscheme://host[:port], nothing else. Every address the engine builds or follows must be on it; anything that resolves elsewhere is never navigated to. One origin per map.
searchPathWhere a search with no filters lives. Also the fallback address when the path shape a filter needs isn't there.
languageen, tr or de. The map's own default for the tool descriptions; a conversation's actual language (see Languages and the greeting) overrides it, so a tenant that speaks several still gets the same six tools worded in the visitor's own language.

navigation

soft (the default) or reload. This is the single most consequential field in the map — the whole mechanics, the refusal ladder, and what your results page needs to look like for soft to work are on How a filter is applied.

filters.name

name is what the model sends in field: lowercase, digits and underscores.

labelWhat a person calls it out loud: "brand", "marka".
aliasesWhat else they call it. Label and aliases are matched with the same fuzzy pass as values, so a mistranscribed field name still lands.
typechoice (needs values), range (min and max numbers), text (whatever was said, unvalidated), flag (on or off).
apply.viaWhere the filter lands in the address. See below.
requiresFilter names that must be set first (only meaningful for slug and segment). A model with no brand has no URL shape for a model filter; it is dropped and reported, never guessed at.
fallback.paramsegment filters only: where the value goes when there's nothing to hang it off (İstanbul with no brand slug → ?address_city=34). The value used is the matching value's paramToken; without one, the filter is dropped for that request.
values.canonicalRequired for choice. The closed dictionary — see below.

apply.via

paramOne query parameter, named in param: ?cat=Mice. The commonest and the most stable.
rangeminParam and maxParam. Either may be absent; at least one must be present, or the filter is refused when the map is saved.
slugPart of the FIRST path segment, dash-joined with the other slug filters at their order: brand at order 1 + model at order 2 gives /volkswagen-polo.
segmentIts own slash segment after the slug, at position order: /volkswagen-polo/istanbul/dizel. The order is load-bearing — a segment in the wrong place is a different page, not this filter.
facetNot in the address at all. Resolved against the site's own live filter panel at session time, and applied by following the href the site itself wrote — Gerry never constructs one of these.

filters.name.values.canonical

This is the whole answer to speech-to-text mistakes: the model hands over "folksvagen", and only a closed list turns that into "Volkswagen", or into an honest "I didn't catch that". A tie between two close values asks instead of picking.

tokenWhat the SITE calls this value, when it differs from the canonical name: lpgbenzin-lpg. Absent, the canonical name is used as-is.
aliasesSpoken forms edit-distance won't reach: "mazot" → dizel.
paramTokenWhat this value travels as through a segment filter's fallback parameter. No paramToken means the fallback can't express this value, so it's dropped when it would be needed.
underCanonical values of the filter this one requires, that this value belongs to. "Golf" is only a Volkswagen — without this a mis-scoped model builds /renault-golf, which is an error page, not a search.

sort

param plus tokens (canonical key → the site's own token), and an optional control — a selector for the site's own sort control, used only by the pointer layer to show where a sort landed (see What Gerry shows on the page). Sorting itself never touches the control; the order travels in the address. Omit the whole sort block and no sort_results tool is offered at all.

results

containerA selector for the region holding the count and the rows — the only part of the page a filter ever replaces. Absent, it's derived at runtime as the nearest common parent of the count element and the first row; if that comes out as <body> (or higher), it's refused. Name it explicitly if you can — a derived region is a guess.
count.selectorsWhere the count sentence lives.
count.patternThe number in the sentence, as a capture group: ([0-9][0-9.]*)\s*products. Anchor it on a noun — the first number in "2016 Volkswagen Polo — 99 results" is the year, not the count. Without a pattern, common wordings are tried; a line with no count in it is reported as unknown, never guessed.
rows.selectorsOne row each, tried in order, so a redesign degrades to the next selector rather than breaking outright.
rows.title / price / linkSearched inside a row. No title means no listing — that is how an advert row is told from a real one, and it's also how rows keep the numbering the SCREEN has, so "open the third one" opens the third one a visitor is actually looking at.
rows.metaYear, kilometres, colour — whatever else a row shows beside the price. Every match of every selector is collected, in document order.

facets

group / title / linkHow to find the live filter panel: one selector per filter group, the group's own heading, and the option links inside it. Optional — without it the engine falls back to cautious heuristics. Scope group tightly; a loose selector turns every link on the page into a filter option.
containerOpts the panel INTO being swapped alongside the results on a soft navigation, so its checkboxes and links show the state the address now describes. Opt-in on purpose — swapping it destroys every listener the site attached to its own controls. Leave it out and the panel goes stale instead, which is the safer of the two wrongs (more in How a filter is applied).

pages

searchA regular expression matched against the pathname; a match is a result list the engine may navigate.
detailA regular expression marking a single listing — never navigated to in place, only opened.
openInpage (default) opens a listing through the same soft navigation a filter uses, so the conversation survives and Gerry can read the page it just opened. newTab is for a detail page that can't be swapped in — its own layout, no results container — the visitor gets the listing beside the search, and the call keeps running where it started.

Reading order

Always most-stable-first: the map's own selectors, then schema.org JSON-LD (ItemList / Product / Offer), then a text pattern. The map's caps, for reference: up to 40 filters, 600 values per filter, 12 aliases per filter and 8 per value, 8 selectors per field, 200 characters per selector.

Questions: hello@heygerry.io. These pages describe what is built today; nothing here is a promise about what is not.