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.
site
origin | scheme://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. |
|---|---|
searchPath | Where a search with no filters lives. Also the fallback address when the path shape a filter needs isn't there. |
language | en, 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.
label | What a person calls it out loud: "brand", "marka". |
|---|---|
aliases | What else they call it. Label and aliases are matched with the same fuzzy pass as values, so a mistranscribed field name still lands. |
type | choice (needs values), range (min and max numbers), text (whatever was said, unvalidated), flag (on or off). |
apply.via | Where the filter lands in the address. See below. |
requires | Filter 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.param | segment 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.canonical | Required for choice. The closed dictionary — see below. |
apply.via
param | One query parameter, named in param: ?cat=Mice. The commonest and the most stable. |
|---|---|
range | minParam and maxParam. Either may be absent; at least one must be present, or the filter is refused when the map is saved. |
slug | Part 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. |
segment | Its 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. |
facet | Not 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.
token | What the SITE calls this value, when it differs from the canonical name: lpg → benzin-lpg. Absent, the canonical name is used as-is. |
|---|---|
aliases | Spoken forms edit-distance won't reach: "mazot" → dizel. |
paramToken | What 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. |
under | Canonical 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
container | A 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.selectors | Where the count sentence lives. |
count.pattern | The 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.selectors | One row each, tried in order, so a redesign degrades to the next selector rather than breaking outright. |
rows.title / price / link | Searched 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.meta | Year, kilometres, colour — whatever else a row shows beside the price. Every match of every selector is collected, in document order. |
facets
group / title / link | How 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. |
|---|---|
container | Opts 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
search | A regular expression matched against the pathname; a match is a result list the engine may navigate. |
|---|---|
detail | A regular expression marking a single listing — never navigated to in place, only opened. |
openIn | page (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.