JSON Templates
Use JSON templates when KQL can't shape the response you need – custom aggregations, computed fields, or data drawn from outside Kirby.
Global Routes
By default, Kirby Headless does not interfere with Kirby's routing. Enable global routes to automatically return JSON from all templates:
return [
'headless' => [
'globalRoutes' => true
]
];
Writing JSON Templates
Encode template data as JSON in your template files:
<?php
$data = [
'title' => $page->title()->value(),
'layout' => $page->layout()->toResolvedLayouts()->toArray(),
'address' => $page->address()->value(),
'email' => $page->email()->value(),
'phone' => $page->phone()->value(),
'social' => $page->social()->toStructure()->toArray()
];
echo \Kirby\Data\Json::encode($data);
Fetching Template Data
Fetch JSON template data with bearer token authentication:
const response = await fetch("https://example.com/about", {
headers: {
Authorization: `Bearer ${process.env.KIRBY_API_TOKEN}`,
},
});
const data = await response.json();
Template Routes
You can also fetch templates by name using the __template__ endpoint:
const response = await fetch("https://example.com/__template__/about", {
headers: {
Authorization: `Bearer ${process.env.KIRBY_API_TOKEN}`,
},
});
const data = await response.json();
This fetches the about template and returns its JSON output.
__template__ endpoint wraps the rendered template in the standard API envelope: { "code": 200, "status": "OK", "result": … }. The catch-all route above (e.g. /about) instead returns the template's raw JSON output.Sitemap Endpoint
Kirby Headless includes a built-in sitemap endpoint at __sitemap__ that returns all indexable pages:
const response = await fetch("https://example.com/__sitemap__", {
headers: {
Authorization: `Bearer ${process.env.KIRBY_API_TOKEN}`,
},
});
const sitemap = await response.json();
The response wraps the page list in the standard API envelope. On multi-language sites, each entry also gains a links array with one entry per language plus an x-default fallback:
{
"code": 200,
"status": "OK",
"result": [
{
"url": "/blog/first-post",
"modified": "2024-01-15",
"links": [
{ "lang": "en", "url": "/blog/first-post" },
{ "lang": "de", "url": "/de/blog/erster-beitrag" },
{ "lang": "x-default", "url": "/blog/first-post" }
]
}
]
}
modified field is omitted when a page has no resolvable modification date. The links array is only present on multi-language sites, and each lang value is derived from the language's locale (for example en_US.UTF-8 becomes en-us).Sitemap Configuration
Configure which pages appear in the sitemap:
return [
'headless' => [
'sitemap' => [
'exclude' => [
// Exclude pages by template name
'templates' => ['error', 'maintenance'],
// Exclude pages by ID (supports regex patterns)
'pages' => [
'home/draft-page',
'blog/.*-draft$' // Regex: exclude all blog drafts
]
],
// Custom indexability check
'isIndexable' => function ($page) {
// Only include listed pages that are marked as public
return $page->isListed() && $page->isPublic()->toBool();
}
]
]
];
Exclude Pages by Regex
The exclude.pages option supports regex patterns for flexible page exclusion:
return [
'headless' => [
'sitemap' => [
'exclude' => [
'pages' => [
'home/draft-.*', // All pages starting with "draft-"
'.*-internal$', // All pages ending with "-internal"
'projects/.*/archive/.*' // All archived project pages
]
]
]
]
];
Blueprint-Level Control
Control sitemap inclusion per page using blueprint fields:
fields:
sitemap:
type: toggle
label: Include in Sitemap
default: true
Then use it in your isIndexable callback:
return [
'headless' => [
'sitemap' => [
'isIndexable' => function ($page) {
return $page->sitemap()->toBool();
}
]
]
];