Appearance
Architecture
Introduction
kitloom is small at the centre and grows through packages. The core knows only the shape of a page request; everything a site actually shows — content, menus, head meta, breadcrumbs — is added by modules that do not know about each other.
text
request → PageController → PageKernel
├─ resolve the model (post, term, author…)
├─ run the page stack: module steps by stage
└─ render: active theme engine, or JSONThe page lifecycle
1. Routing
Your application's own routes always win. Every other URL reaches Kitloom\Core\Http\PageController through Route::fallback().
2. Resolving the model
kitloom/routing turns the path into a model following the site's permalink settings, the way WordPress does: /hello-world/ → a post, /category/news/ → a term, /author/jane/ → a user. When the URL is not canonical (an old slug, a missing trailing slash), the kernel answers with a 301 to the canonical URL.
3. The page stack
The page data is collected by a pipeline of steps — pipes. A module adds its pipe from its own service provider and chooses a stage — a fixed place in the stack — instead of a position relative to other modules:
php
use Kitloom\Core\Pipeline\PipelineRegistry;
use Kitloom\Core\Pipeline\Stage;
public function boot(): void
{
$this->app->make(PipelineRegistry::class)
->add(Stage::WEB, ReadingTimeStage::class, Stage::CONTENT);
}| Stage | Priority | What goes there |
|---|---|---|
Stage::SETUP | 1000 | Site-wide data: options, theme settings |
Stage::CONTENT | 900 | The page's own data |
Stage::BREADCRUMBS | 800 | Breadcrumbs |
Stage::NAVIGATION | 700 | Menus |
Stage::WIDGETS | 600 | Sidebar widgets |
Stage::META | 500 | Head meta — built from the content above |
Stage::LAYOUT | 400 | Page chrome and the theme component that renders the page |
Stage::SCHEMA | 300 | schema.org markup — needs everything collected before it |
Stage::OUTPUT | 100 | Last touches: shortcode data |
Higher priority runs earlier; the gaps between stages leave room for steps in between (Stage::CONTENT - 10). A pipe sits in a stack once — adding it again moves it.
A pipe receives the PageContext — the model, the path and a bag of parameters that becomes the page payload:
php
use Closure;
use Kitloom\Core\Contracts\PipeInterface;
use Kitloom\Core\Page\PageContext;
final class ReadingTimeStage implements PipeInterface
{
public function handle(PageContext $context, Closure $next): mixed
{
$post = $context->getModel();
$words = str_word_count(strip_tags((string) $post?->post_content));
$context->setParams('readingTime', (int) ceil($words / 200));
return $next($context);
}
}A site adds its own steps in config/kitloom-page.php, by stage, between the modules' steps:
php
use Kitloom\Core\Pipeline\Stage;
return [
'pipelines' => [
'web' => [
App\Page\ThemeOptions::class => Stage::SETUP,
App\Page\Footer::class => Stage::LAYOUT,
],
],
];There are two stacks: Stage::WEB for pages and Stage::SEARCH for the search results page, which has a query instead of a model.
4. Rendering
The kernel hands the collected PageContext to a renderer (Kitloom\Core\Contracts\RendererInterface):
- the active theme's engine —
kitloom/inertia(Vue) orkitloom/blade— for a browser; JsonRendererfor a request withAccept: application/json, or when no theme is active.
That is why the same URL serves both the page and its data: a decoupled front end, a mobile app or a theme's client-side navigation can ask for /hello-world/ as JSON.
5. Events
Kitloom\Core\Events\PageResolved and PageNotFound are dispatched after the page is built. Side effects — view counters, the 404 log of the Redirections module — live in listeners, never in the pipeline.
Registries
Modules often need to know what exists on the site — which post types, which taxonomies, which settings pages. Instead of a contract every module must implement, kitloom/wp-schema keeps registries that collect answers from tagged providers:
| Registry | Provider contract | Tag |
|---|---|---|
PostType\PostTypeRegistry | Contracts\PostTypeProviderInterface | PostTypeRegistry::PROVIDER_TAG |
Taxonomy\TaxonomyRegistry | Contracts\TaxonomyProviderInterface | TaxonomyRegistry::PROVIDER_TAG |
Settings\SettingsPagesRegistry | Contracts\SettingsPagesProviderInterface | SettingsPagesRegistry::PROVIDER_TAG |
ModelRegistry | config('wp-schema.models') | — |
For example, the Post Types module tells everyone about the types created in the admin:
php
use Kitloom\WpSchema\PostType\PostTypeRegistry;
use Kitloom\WpSchema\Taxonomy\TaxonomyRegistry;
$this->app->tag([AcfPostTypeProvider::class], PostTypeRegistry::PROVIDER_TAG);
$this->app->tag([AcfTaxonomyProvider::class], TaxonomyRegistry::PROVIDER_TAG);From then on the SEO module offers title templates for those types, the sitemap lists them and the admin shows their sections — without any of those modules knowing that ACF exists.
Package boundaries
| Layer | Packages | May depend on |
|---|---|---|
| Core | kitloom/core | Laravel only |
| Schema | kitloom/wp-schema, kitloom/routing, kitloom/settings | core |
| Modules (domain) | kitloom/seo, kitloom/redirects, … | core, schema, other domain packages |
| Admin | kitloom/*-filament, kitloom/*-nova | their domain package, the panel |
| Themes | engines (kitloom/inertia, kitloom/blade) and theme packages | kitloom/themes |
The rule that matters most: domain packages never import Filament or Nova. Anything an admin needs from the domain — a list of options, a slug rule, a tree of terms — is a domain class both admins use.