The cacheable spelling of a query. The whole question lives in the URL, so this address has a representation fixed by it — it carries an ETag and answers a conditional request, which the QUERY spelling beside it cannot.
It answers the same envelope QUERY does, through the same grammar: the parameters below are assembled into a query and handed to the one validator, so the caps, the enums and the refusals are identical.
Reach for this one first. What it cannot spell is a boolean tree, a batch, a lookup by slug, or a pasted passage — those need the grammar at POST /api/pages.
A request naming neither a search nor a filter is refused rather than answered: the whole corpus is not a question.
This resource answers HEAD as well, with the same headers and no body.
It also answers OPTIONS, which returns Allow, a Link to this description, and — where the resource takes one — the query formats it reads, as Accept-Query. Because Allow names QUERY and the wire will not carry one, that answer also carries method-override-required, saying which method to send it as and under which header. This document already publishes that as a required parameter, so a caller reading it here does not need the field.
The text to search for. Optional, and absent is not the same as empty: a request carrying only filters is a question too — "everything under this tag" — and is answered as one, while ?q= names a clause with no text and is refused exactly as the same empty clause in a request body is.
No length is published here. The grammar at /api/query-schema.json states its own, and restating it would be the second rendering this document exists to prevent.
in?string
Which field q searches. Omitted means all.
Default"all"
Value in
"title"
"body"
"description"
"aliases"
"tags"
"all"
collection?string
Narrow to one division of the site.
Value in
"writing"
"glossary"
"demos"
tag?string
Narrow to the pages carrying one tag, by slug rather than by title. Tags are content rather than a fixed set, so a slug no tag carries is answered with an empty result rather than refused. The vocabulary is at /api/tags.
limit?integer
How many rows to return. Refused rather than clamped, which is the opposite of ?limit on the list resources: a value this grammar does not admit is answered with a refusal naming the dimension it exceeded.
No minimum or maximum is published here, and their absence is deliberate. The ceiling is not a bound on this field — it is a budget spent across every query in a request, which JSON Schema has no vocabulary for, so the published grammar at /api/query-schema.json states its own limits and nothing here restates them.
Default10
facets?string
Which dimensions to count, as one ,-separated value rather than a repeated key — one of collection or tags, or both.
---
title: "Search the site"
description: "The cacheable spelling of a query. The whole question lives in the URL, so this address has a representation fixed by it — it carries an `ETag` and answers a conditional request, which the `QUERY` spelling beside it cannot.\n\nIt answers the same envelope `QUERY` does, through the same grammar: the parameters below are assembled into a query and handed to the one validator, so the caps, the enums and the refusals are identical.\n\n**Reach for this one first.** What it cannot spell is a boolean tree, a batch, a lookup by slug, or a pasted passage — those need the grammar at `POST /api/pages`.\n\nA request naming neither a search nor a filter is refused rather than answered: the whole corpus is not a question.\n\nThis resource answers `HEAD` as well, with the same headers and no body.\n\nIt also answers `OPTIONS`, which returns `Allow`, a `Link` to this description, and — where the resource takes one — the query formats it reads, as `Accept-Query`. Because `Allow` names `QUERY` and the wire will not carry one, that answer also carries `method-override-required`, saying which method to send it as and under which header. This document already publishes that as a required parameter, so a caller reading it here does not need the field."
url: "https://lateano.com/developers/api/reference/reading/pages.get"
author: "Colin Lateano"
---
# Search the site
The cacheable spelling of a query. The whole question lives in the URL, so this address has a representation fixed by it — it carries an `ETag` and answers a conditional request, which the `QUERY` spelling beside it cannot.
It answers the same envelope `QUERY` does, through the same grammar: the parameters below are assembled into a query and handed to the one validator, so the caps, the enums and the refusals are identical.
**Reach for this one first.** What it cannot spell is a boolean tree, a batch, a lookup by slug, or a pasted passage — those need the grammar at `POST /api/pages`.
A request naming neither a search nor a filter is refused rather than answered: the whole corpus is not a question.
This resource answers `HEAD` as well, with the same headers and no body.
It also answers `OPTIONS`, which returns `Allow`, a `Link` to this description, and — where the resource takes one — the query formats it reads, as `Accept-Query`. Because `Allow` names `QUERY` and the wire will not carry one, that answer also carries `method-override-required`, saying which method to send it as and under which header. This document already publishes that as a required parameter, so a caller reading it here does not need the field.
```json
{
"openapi": "3.1.1",
"info": {
"title": "lateano.com",
"version": "1.0.0"
},
"paths": {
"/api/pages": {
"get": {
"operationId": "pages.get",
"summary": "Search the site",
"tags": [
"Reading"
],
"description": "The cacheable spelling of a query. The whole question lives in the URL, so this address has a representation fixed by it — it carries an `ETag` and answers a conditional request, which the `QUERY` spelling beside it cannot.\n\nIt answers the same envelope `QUERY` does, through the same grammar: the parameters below are assembled into a query and handed to the one validator, so the caps, the enums and the refusals are identical.\n\n**Reach for this one first.** What it cannot spell is a boolean tree, a batch, a lookup by slug, or a pasted passage — those need the grammar at `POST /api/pages`.\n\nA request naming neither a search nor a filter is refused rather than answered: the whole corpus is not a question.\n\nThis resource answers `HEAD` as well, with the same headers and no body.\n\nIt also answers `OPTIONS`, which returns `Allow`, a `Link` to this description, and — where the resource takes one — the query formats it reads, as `Accept-Query`. Because `Allow` names `QUERY` and the wire will not carry one, that answer also carries `method-override-required`, saying which method to send it as and under which header. This document already publishes that as a required parameter, so a caller reading it here does not need the field.",
"parameters": [
{
"name": "q",
"in": "query",
"required": false,
"schema": {
"type": "string"
},
"description": "The text to search for. **Optional, and absent is not the same as empty:** a request carrying only filters is a question too — \"everything under this tag\" — and is answered as one, while `?q=` names a clause with no text and is refused exactly as the same empty clause in a request body is.\n\nNo length is published here. The grammar at [`/api/query-schema.json`](/api/query-schema.json) states its own, and restating it would be the second rendering this document exists to prevent."
},
{
"name": "in",
"in": "query",
"required": false,
"schema": {
"type": "string",
"enum": [
"title",
"body",
"description",
"aliases",
"tags",
"all"
],
"default": "all"
},
"description": "Which field `q` searches. Omitted means `all`."
},
{
"name": "collection",
"in": "query",
"required": false,
"schema": {
"type": "string",
"enum": [
"writing",
"glossary",
"demos"
]
},
"description": "Narrow to one division of the site."
},
{
"name": "tag",
"in": "query",
"required": false,
"schema": {
"type": "string"
},
"description": "Narrow to the pages carrying one tag, **by slug rather than by title**. Tags are content rather than a fixed set, so a slug no tag carries is answered with an empty result rather than refused. The vocabulary is at [`/api/tags`](/api/tags)."
},
{
"name": "limit",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"default": 10
},
"description": "How many rows to return. **Refused rather than clamped**, which is the opposite of `?limit` on the list resources: a value this grammar does not admit is answered with a refusal naming the dimension it exceeded.\n\nNo `minimum` or `maximum` is published here, and their absence is deliberate. The ceiling is not a bound on this field — it is a budget spent across every query in a request, which JSON Schema has no vocabulary for, so the published grammar at [`/api/query-schema.json`](/api/query-schema.json) states its own limits and nothing here restates them."
},
{
"name": "facets",
"in": "query",
"required": false,
"schema": {
"type": "string"
},
"description": "Which dimensions to count, as one `,`-separated value rather than a repeated key — one of `collection` or `tags`, or both."
}
],
"responses": {
"200": {
"description": "Search the site",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/QueryBody"
}
}
}
},
"304": {
"description": "The representation this request already holds is still current, so it is not sent again. No body. Read the validator off `ETag` and send it back as `If-None-Match`.",
"headers": {
"etag": {
"schema": {
"type": "string"
}
},
"cache-control": {
"schema": {
"type": "string"
}
}
}
},
"default": {
"description": "Every other status this operation answers with is a refusal, and carries `problem+json` (RFC 9457).\n\nEvery refusal this API can make, whatever the status. Match on the `type` URI’s last segment — the slug below — rather than on the whole URI, which follows whichever deployment answered.\n\n| Status | Slug | Title |\n| --- | --- | --- |\n| 400 | [`invalid-idempotency-key`](https://lateano.com/api/problems/invalid-idempotency-key) | The Idempotency-Key header carries a value this resource will not accept |\n| 400 | [`malformed-body`](https://lateano.com/api/problems/malformed-body) | The request body did not parse |\n| 400 | [`missing-idempotency-key`](https://lateano.com/api/problems/missing-idempotency-key) | This resource requires an Idempotency-Key header |\n| 400 | [`missing-media-type`](https://lateano.com/api/problems/missing-media-type) | The request carried a body with no media type |\n| 404 | [`no-such-resource`](https://lateano.com/api/problems/no-such-resource) | There is nothing at this address |\n| 405 | [`method-not-allowed`](https://lateano.com/api/problems/method-not-allowed) | This resource does not answer that method |\n| 406 | [`not-acceptable`](https://lateano.com/api/problems/not-acceptable) | This resource cannot answer in any media type the request accepts |\n| 409 | [`conflicting-idempotency-key`](https://lateano.com/api/problems/conflicting-idempotency-key) | That Idempotency-Key already names a different submission |\n| 412 | [`precondition-failed`](https://lateano.com/api/problems/precondition-failed) | The condition the request carried does not hold |\n| 415 | [`unsupported-media-type`](https://lateano.com/api/problems/unsupported-media-type) | The request body is in a media type this resource does not read |\n| 422 | [`invalid-query`](https://lateano.com/api/problems/invalid-query) | The query parsed but does not describe a question this resource can answer |\n| 422 | [`invalid-submission`](https://lateano.com/api/problems/invalid-submission) | The submission parsed but is not something this resource can record |\n| 429 | [`too-many-requests`](https://lateano.com/api/problems/too-many-requests) | The courtesy quota for this caller is spent |\n| 500 | [`unexpected-failure`](https://lateano.com/api/problems/unexpected-failure) | The resource failed while answering |",
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/ProblemDocument"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"QueryBody": {
"$ref": "#/components/schemas/ListEnvelope_QueryAnswer",
"description": "The list holds one answer for each query in the request."
},
"ProblemDocument": {
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "§ 3.1.1. Absolute, on the origin that answered, and it resolves."
},
"title": {
"type": "string",
"description": "§ 3.1.3. The catalogue row's, unchanged from occurrence to occurrence."
},
"status": {
"type": "number",
"description": "§ 3.1.2. The same number as the status line, derived rather than passed."
},
"instance": {
"type": "string",
"description": "§ 3.1.5 RECOMMENDS an absolute URI here. This field carries a full path from the root, which is the fallback form the same section permits."
},
"detail": {
"type": "string",
"description": "§ 3.1.4. This occurrence only, bounded by `DETAIL_MAX`, absent when there is nothing to add."
}
},
"required": [
"type",
"title",
"status",
"instance"
],
"additionalProperties": false,
"description": "The schema generator publishes this declaration as the error schema, so a field here reaches the reference. RFC 9457 § 3.1 makes all five members optional, and this API narrows four of them to required: § 3.1.1 reads an absent `type` as `about:blank` and discards the resolvable URI. An optional member is absent, never present and `undefined`, because `JSON.stringify` drops the value and the wire cannot tell the two apart."
},
"ListEnvelope_QueryAnswer": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/QueryAnswer"
}
},
"count": {
"type": "number",
"description": "How many items this response carries."
},
"total": {
"type": "number",
"description": "How many items exist across every response."
}
},
"required": [
"items",
"count",
"total"
],
"additionalProperties": false,
"description": "The one body shape every list resource returns."
},
"QueryAnswer": {
"type": "object",
"properties": {
"results": {
"type": "array",
"items": {
"$ref": "#/components/schemas/QueryRow"
}
},
"matched": {
"type": "number",
"description": "How many documents the query matched, before any bound reduced them to `results`."
},
"facets": {
"type": "array",
"items": {
"$ref": "#/components/schemas/FacetGroup"
}
}
},
"required": [
"results",
"matched",
"facets"
],
"additionalProperties": false,
"description": "`facets` is empty rather than absent for a query that asked for none, and one group per dimension in the order asked. An empty group is a real answer, so \"did not ask\" and \"asked and nothing matched\" stay apart."
},
"QueryRow": {
"type": "object",
"properties": {
"path": {
"type": "string"
},
"canonical": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"collection": {
"$ref": "#/components/schemas/Collection"
},
"date": {
"type": [
"string",
"null"
]
},
"twin": {
"type": [
"string",
"null"
],
"description": "The markdown twin's address, and where the whole document is fetched from."
}
},
"required": [
"path",
"canonical",
"title",
"description",
"collection",
"date",
"twin"
],
"additionalProperties": false,
"description": "`date` is a post's publication or a term's last review, and `null` for a Demo, which has none. `twin` is `null` for a page that publishes no twin, rather than an address that 404s."
},
"FacetGroup": {
"type": "object",
"properties": {
"facet": {
"$ref": "#/components/schemas/Facet"
},
"counts": {
"type": "array",
"items": {
"$ref": "#/components/schemas/FacetCount"
}
}
},
"required": [
"facet",
"counts"
],
"additionalProperties": false,
"description": "Counts over every matched document, not over the returned rows; counts over a `limit`-sized page describe the page, which a caller can already derive. Ordered by count and then by value, so the same query answers the same way twice."
},
"Collection": {
"type": "string",
"enum": [
"writing",
"glossary",
"demos"
]
},
"Facet": {
"type": "string",
"enum": [
"collection",
"tags"
]
},
"FacetCount": {
"type": "object",
"properties": {
"value": {
"type": "string"
},
"count": {
"type": "number"
}
},
"required": [
"value",
"count"
],
"additionalProperties": false,
"description": "How many documents carried one value of a facet."
}
}
}
}
```