Routing
Path resolution is RouteIndex-driven. Every published, routable entry has a row in RouteIndex; delivery looks up the row and dispatches to the right entry / redirect / 404.
Resolution flow
- Hostname → site. The delivery handler matches the request's
Hostheader againstSite.hostnames[]and resolves the project + site. - Path + locale → RouteIndex. Looks up the unique row keyed by
(projectId, environmentId, siteId, locale, path). - If found:
isRedirect=false→ return the entry. Frontend renders.isRedirect=true→ return{ kind: "redirect", target, status }. Frontend emits HTTP redirect.
- No match → 404. The delivery API resolves exact RouteIndex matches only. Regex redirect matching is not wired into the delivery path, so a path that only matches a regex redirect resolves to 404 at the delivery API.
RouteIndex shape
{
projectId: string;
environmentId: string;
siteId: string;
locale: string;
path: string; // normalized: "/about/team"
entryId: string | null; // null on regex redirects
contentTypeApiName: string | null;
isRedirect: boolean;
isRegex: boolean;
redirectTarget: string | null;
redirectStatus: number | null; // 301, 302, 307, 308
sortOrder: number; // for regex redirects only
status: "published" | "draft";
}
Path computation
For a published, routable entry:
- Has a tree node + slug not overridden →
TreeNode.path(materialized). - Tree node + slug overridden → tree path with the last segment swapped for the entry's slug.
- No tree node, has slug →
/{slug}. - No slug → entry isn't addressable, no RouteIndex row.
Redirects
Two flavors:
Plain (exact)
source is a literal path. RouteIndex.path stores the normalized form; lookup is a single keyed query.
Regex (V2)
source is a regex pattern. Stored verbatim with isRegex=true, ordered by sortOrder, with capture groups ($1, $2) substituting into the target. Note: regex redirect matching is not wired into the delivery resolution path — a path that only matches a regex redirect resolves to 404 at the delivery API.
source: ^/blog/(\d{4})/(.+)$
target: /articles/$2
status: 301
Auto-redirect on slug change
When a published entry's slug changes, Krios automatically creates a 301 redirect from the old path to the new one. Idempotent — re-publishing into the same path is a no-op. Cycling A → B → A refreshes existing redirect entries instead of accumulating duplicates.
Resolving a route
GET /api/delivery/projects/{slug}/sites/{siteSlug}/routes?path=/about/team
Response:
{ "data": { "kind": "entry", "entry": { ... } } }
{ "data": { "kind": "redirect", "target": "/articles/team", "status": 301 } }
A path with no match returns HTTP 404:
{ "error": "route_not_found", "message": "No route matches the requested path" }
The SDK's KriosClient.resolveRoute(siteSlug, path) wraps this:
const result = await krios.resolveRoute("main", "/about/team");
switch (result.kind) {
case "entry": return render(result.entry);
case "redirect": return Response.redirect(result.target, result.status);
case "notFound": return notFound();
}
Sitemaps
GET /api/delivery/projects/{slug}/sites/{siteSlug}/sitemap?locale=en-US&format=xml
XML sitemap with <xhtml:link rel="alternate" hreflang="..."> for every supported locale. JSON format available for programmatic crawlers. Capped at 50,000 URLs per response (sitemap protocol limit); sitemap-index splitting for larger sites is on the V3.x roadmap.