> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wristler.eu/llms.txt
> Use this file to discover all available pages before exploring further.

# Pages and Insights

> The filter pages the catalogue is organised into, and what we write about watches.

Four things live here, and the first three are built the same way — and, at the end, who to ask for help.

**Filter pages** are what the catalogue is organised into — "Rolex", "Rolex Air-King",
"Omega under €5,000". There are around 16,000 of them across nine languages, and they
are how most people arrive.

**Articles** are Insights: what we write, film and record. `/v1/article-tags` is every
word they are filed under, in one call — the chips an app draws, and what
`filter[tag]` takes.

**Pages** are everything else: how it works, who we are, the terms, the contact page.
`template` says which layout the website draws each one with, so an app can lay out a
contact page differently from a terms page without matching on the slug.

**The help centre** is `/v1/faq-categories` and `/v1/faq-articles`. An answer is a
question and one body of HTML — not blocks, because it is written in a single editor
field and a one-block list is a wrapper nobody wants to unwrap. A heading with
`?include=articles` is the whole screen in one call, which is why the headings are not
paged.

All of them need `content:read`, and a guest token is enough — somebody browsing
before they sign up is exactly who these pages are for.

## A filter page is a search with words around it

The field that matters is `search`. It is the listing the page stands for, written in
the catalogue's own vocabulary:

```json theme={null}
{
  "search": {
    "words": null,
    "filters": { "brand": ["Rolex"], "watchModel": ["Air-King", "Air-King Date"] }
  }
}
```

Hand it straight back to the catalogue and you have the page:

```http theme={null}
GET /v1/watches?filter[brand][]=Rolex&filter[watchModel][]=Air-King
```

> [!NOTE]
>
> `search.words` goes in `filter[q]`, `search.filters` are the facet filters. They are
> the same names the search's own facets come back under, so a page and a filter panel
> speak one language — [the search, in full →](https://docs.wristler.eu/app/search.md)

That is the whole reason the page does not return its own listings. There is one way
to ask "every Rolex", and it is the one an app already implements.

## Blocks

Filter pages, articles and pages carry their content as `blocks`: an ordered list, each with a `kind`
and named fields.

```json theme={null}
[
  { "kind": "content", "heading": "About the Air-King", "body": "<p>Introduced in…</p>" },
  { "kind": "links", "groups": [
    { "name": "Popular models", "links": [{ "title": "Submariner", "url": "…" }] }
  ]}
]
```

| `kind` | Carries | Where |
| - | - | - |
| `content` | `heading`, `title`, `subtitle`, `body` | Everywhere |
| `columns` | `heading`, `title`, `subtitle`, `left`, `right` | Filter pages, pages |
| `image` | `heading`, `title`, `subtitle`, `body`, `image`, `alt` | Everywhere |
| `links` | `groups`, each a `name` and its `links` | Filter pages |
| `hero` | `title`, `excerpt`, `image` | Articles |
| `video` | `title`, `description`, `youtubeId`, `duration` | Articles |

`body` is HTML written in our editor. Everything else is plain text.

> [!WARNING]
>
> A block whose `kind` you do not recognise should be skipped rather than guessed at.
> The list above will grow, and an app that skips an unfamiliar kind keeps working
> when it does.

Fields that are empty are left out rather than sent as `null`, so check for a key
before reading it. The one exception is a `links` group's `name`, which is `null`
when the links were written on the block itself rather than picked from a named list.

### Why not the raw page builder

Our editor stores what it needs: `content_left`, `list_3`, `image_alt`. Those mean
something standing next to one of our Blade templates and nothing on their own, and an
app built on them would break the day somebody renames one. So each component is
translated into a block, and a component kind the API has not been taught about is
left out entirely — a blank space beats a half-understood guess.

## In a list, `blocks` is null

```http theme={null}
GET /v1/filter-pages
GET /v1/articles
GET /v1/pages
```

Thousands of pages with every block loaded is the whole site in one response, and
nobody draws a block in a list. Ask for the page itself and it is filled in.

Filter pages come back biggest first (`-watchCount`); articles newest first
(`-publishedAt`).

## Autocomplete

```http theme={null}
GET /v1/filter-pages?filter[q]=air-k&page[size]=5
```

```http theme={null}
GET /v1/articles?filter[q]=air-k&page[size]=3
```

Ranked by the same index and the same relevance threshold as the search bar on the
website, so the same letters offer the same pages. **Ask both** — the website's
dropdown has two sections, pages and Insights, and one of them alone is half a
dropdown. Articles come back newest first among what matched.

A `sort` is refused (`400`, `search_only_sort`) while the filter pages are searching:
suggestions are ordered by how well they match, and nothing else would be useful.
While the articles are searching a `sort` is not applied — they come back newest
first among what matched, whatever it says. A suggestion row is
`autocompleteName` — the short name an editor gave the page, falling back to `name` —
with `watchCount` beside it.

Tapping one is a catalogue search with the `search` it already carries. Nothing has to
be looked up in between, and there is still only one way to say "every Rolex".

## Arriving from a link

A link carries a slug, so that is what they take:

```http theme={null}
GET /v1/filter-pages?filter[slug]=rolex/air-king
GET /v1/articles?filter[slug]=how-to-spot-a-fake
GET /v1/pages?filter[slug]=how-it-works
GET /v1/faq-articles?filter[category]=buying
```

Articles also filter on `tag` (by the tag's slug), `type` — `default`, `video` or
`podcast`, or several comma separated (`podcast,video`) — `featured` (`true` or `1` for
the ones we are featuring, `false` or `0` for the rest), and `author` (by the author's
slug).

## Who wrote it

`?include=author` puts the byline beside the article, as type `authors`: `name`,
`function`, `bio`, `linkedin`, `slug`, `image` (the square portrait) and `url`, their
page on the website. A byline, not an account — nothing else about the person is
served. Everything they wrote is `GET /v1/articles?filter[author]={slug}`.

## Personal shopper

`GET /v1/personal-shopper` is the account page's "Personal shopper" block: an `intro`
line, the `shoppers` (each a `name` and a square portrait `image`), and how to reach
them — `whatsapp` and `whatsappUrl`, `phone` and `phoneUrl` (`tel:`), `email`. The
website reads the same settings, so a new number or a new face reaches the app without a
release. The website's chat button is a live-chat widget that exists only there, and no
opening hours are kept: in the app, WhatsApp is the chat.

## The help centre has no search

There is no `filter[q]` on `/v1/faq-articles`, and no website page to link an answer to:
the help centre is the app's own screen. It is small enough to be one call —
`GET /v1/faq-categories?include=articles` — so search what came back, on the phone.

## Ask again cheaply

Every read here carries an `ETag`. Send it back as `If-None-Match` and an unchanged
page answers `304` with no body:

```http theme={null}
GET /v1/filter-pages/812
If-None-Match: W/"a3f1…"
```

Content barely changes and an app re-reads the same pages on every open, over a
connection it did not choose. Keep the tag beside whatever you cached, per URL — the
tag covers the whole answer, so a different `include` or language is a different tag.

> [!NOTE]
>
> It saves the network, not the work: the answer is still built. Do not use it as a
> cheap way to poll for changes on something that changes often.

## What else to read

```http theme={null}
GET /v1/articles/1188/related
```

Up to six articles sharing a tag, most tags in common first, in the same language,
never the one being read. The same ones the website puts at the bottom of a piece —
and the same shape as `/v1/watches/{id}/related` under a listing.

## One language at a time

Every page exists as a row per locale. `Accept-Language` picks one, and a list without
it would be the same page nine times over, so there is no way to ask for all of them.

```http theme={null}
GET /v1/filter-pages
Accept-Language: nl
```

## Nothing unpublished

A page still being written, or an article scheduled for next Tuesday, is not in a list
and answers `404` by id. There is no preview parameter: a draft reachable by id is a
draft published.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.