JSON templates provide full control over your API responses in PHP. This is useful when KQL is not flexible enough or you need custom data structures.
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
]
];
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);
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();
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.
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();
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();
}
]
]
];
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
]
]
]
]
];
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();
}
]
]
];