Skip to content

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 JSON

The 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);
}
StagePriorityWhat goes there
Stage::SETUP1000Site-wide data: options, theme settings
Stage::CONTENT900The page's own data
Stage::BREADCRUMBS800Breadcrumbs
Stage::NAVIGATION700Menus
Stage::WIDGETS600Sidebar widgets
Stage::META500Head meta — built from the content above
Stage::LAYOUT400Page chrome and the theme component that renders the page
Stage::SCHEMA300schema.org markup — needs everything collected before it
Stage::OUTPUT100Last 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) or kitloom/blade — for a browser;
  • JsonRenderer for a request with Accept: 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:

RegistryProvider contractTag
PostType\PostTypeRegistryContracts\PostTypeProviderInterfacePostTypeRegistry::PROVIDER_TAG
Taxonomy\TaxonomyRegistryContracts\TaxonomyProviderInterfaceTaxonomyRegistry::PROVIDER_TAG
Settings\SettingsPagesRegistryContracts\SettingsPagesProviderInterfaceSettingsPagesRegistry::PROVIDER_TAG
ModelRegistryconfig('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 ​

LayerPackagesMay depend on
Corekitloom/coreLaravel only
Schemakitloom/wp-schema, kitloom/routing, kitloom/settingscore
Modules (domain)kitloom/seo, kitloom/redirects, …core, schema, other domain packages
Adminkitloom/*-filament, kitloom/*-novatheir domain package, the panel
Themesengines (kitloom/inertia, kitloom/blade) and theme packageskitloom/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.