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.
| Segments | Pattern | Result |
|---|---|---|
| (empty) | — | Homepage |
{slug}/ | Single segment, not a known type/category/tag prefix | Root-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 plural | Content-type list page |
{plural_slug}/page/{n}/ | — | Paginated list page |
{cat_slug}/… | 2+ segments, prefix = the localized "category" slug | Category page (leaf segment is the category) |
{tag_slug}/{name}/ | — | Tag page |
{type_slug}/{slug}/ | Any registered type, no category | Single content item |
{type_slug}/{cat…}/{slug}/ | Any type except article/page — keeps its type prefix even when categorized | Single item, category-aware |
{cat…}/{slug}/ (1–4 segments, no type prefix) | article/page only | Single 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:
- Settings → Reading's renamed label for the type (if the site owner renamed "Article" to "Recipe", the URL segment follows)
- 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 slugrecetteswithurl_baseset torecipes - 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.
