⌘K

Routing

Every unknown URL is rewritten to index.php by the root .htaccess. parseRequestUri() (in core/data-functions.php) then turns the URI into a routing decision.

ℹ️

parseRequestUri() reads $GLOBALS['data']['page'] directly and its position in the bootstrap order is fixed — index.php also writes its results back into $_GET. Treat $_GET as untrusted in this request cycle regardless of where a value appears to originate.


parseRequestUri(): array

Loops every registered content type (sl_all_type_slugs() — built-ins and any custom type), resolves each one's localized singular/plural URL segment via url_slug(), and builds a localized segment → internal type map before matching the request path against it. There is no fixed article/page/project case list — a custom content type routes through exactly the same cases a built-in does, with two narrow exceptions called out below.

SegmentsPatternResult
(empty)—Homepage
{slug}/Single segment, not a known type/category/tag prefixRoot-level page (checked against live pages, and — for a logged-in admin — unpublished/draft pages too)
{plural_slug}/e.g. articles/, or a custom type's pluralContent-type list page
{plural_slug}/page/{n}/—Paginated list page
{cat_slug}/…2+ segments, prefix = the localized "category" slugCategory page (leaf segment is the category)
{tag_slug}/{name}/—Tag page
{type_slug}/{slug}/Any registered type, no categorySingle content item
{type_slug}/{cat…}/{slug}/Any type except article/page — keeps its type prefix even when categorizedSingle item, category-aware
{cat…}/{slug}/ (1–4 segments, no type prefix)article/page onlySingle item via the bare hierarchical-category form

Returns a normalised ['type' => ..., 'slug' => ..., 'page' => ..., 'category' => ..., 'tag' => ...] array.

article and page are deliberately the only two types with a bare, un-prefixed category-chain route ({parent}/{child}/{slug}/, no article/ or page/ segment) — this is a legacy URL shape kept for backward compatibility, not something a new custom type gets. Every other type, built-in or custom, always keeps its url_slug() prefix, categorized or not, so a new type never opens a second, redirect-only route to the same item.

An article and a page can share the same slug — the router disambiguates by URL shape (category-prefixed vs. not), never by warning you. Not something to guard against in theme code, just don't be surprised by it while testing with demo content.


Localized URL slugs

"url_slug_article":  "article",
"url_slug_articles": "articles",
"url_slug_project":  "project",
"url_slug_projects": "projects",
"url_slug_page":     "page",
"url_slug_pages":    "pages",
"url_slug_category": "category",
"url_slug_tag":      "tag"

url_slug(string $type) (core/functions.php) is the single function that resolves a type's URL segment, in this priority order:

  1. Settings → Reading's renamed label for the type (if the site owner renamed "Article" to "Recipe", the URL segment follows)
  2. For a custom type only, its registered url_base (data/content-types.json) — this is what lets a type's identifier and its public URL differ, e.g. an internal slug recettes with url_base set to recipes
  3. The raw url_slug_{type} locale key above, for built-ins

cleanUrl(), parseRequestUri(), and admin content links all resolve through this same function — one source of truth, no separate URL-building logic to keep in sync.


Bootstrap load order

index.php
  ├── core/llms.php or llms-full.php   ← short-circuits before functions.php if ?_llms is set
  └── core/functions.php
        ├── core-functions.php          → core/content/{render-helpers,dispatch,galleries}.php
        ├── data-functions.php
        ├── render/theme-api.php  →  tf-markdown, tf-shortcodes, tf-cards, tf-navigation, tf-page (→ render/page/*)
        ├── data-layer.php              → core/data/{cache,page-cache,item-store,taxonomy-store,type-registry,misc}.php
        ├── plugin-api.php           ← pl_load_active_plugins()
        └── lang-cache.php
  ↓
loadConfig()
sl_build_data_array(null, false)  → every registered type → $GLOBALS['data']
  ↓
parseRequestUri()  → routing
  ↓
if single item:  sl_load_item_by_slug()  →  $data[$type] = [$fullItem]
  ↓
processContent() / renderCategoryPage() / renderTagPage()
  ↓
loadThemeTemplate('header', …) → echo $pageContent → loadThemeTemplate('footer', …)

Plugin hooks fire at two points in this cycle: early_request right after functions.php finishes loading (before routing or output — for blocking/redirecting the whole request), and after_routing once the route is resolved, with a flag for whether it's a genuine 404.