K KodePressdocumentation
Laravel 11 · MySQL 8 Specification Built 2026-10-10

02 — Architecture#

2.1 Stack#

Layer Choice Notes
Framework Laravel 11 PHP 8.3. See ADR-0001
Database MySQL 8.0 utf8mb4_0900_ai_ci, InnoDB, JSON columns used deliberately
Views Blade Public output is Blade components rendered from the block tree
Admin interactivity Livewire 3 Editor state, lists, settings screens
Client-side glue Alpine.js Local UI state only (toggles, dropdowns, drag handles)
Drag and drop SortableJS Sections, blocks, menu tree
Rich text TipTap richtext field type; output sanitised server-side
Images intervention/image Compression, WebP, size variants
Permissions spatie/laravel-permission Roles, permissions, model_has_roles
Auth scaffolding Laravel Breeze (Blade) Login and password reset only; the admin UI is ours
Asset build Vite Runs locally; public/build is committed
Tests Pest (on PHPUnit) Feature tests dominate

Deliberately absent#

Redis, Horizon, queue workers, Node at runtime, Docker, websockets, Inertia, React/Vue, a public content API. Each is unavailable or unsupportable on the target host.

2.2 Runtime constraints and the choices they force#

The target is shared cPanel hosting with SSH access, one cron entry, no root, no daemons.

Constraint Consequence in KodePress
No Redis CACHE_STORE=file, SESSION_DRIVER=database
No queue workers QUEUE_CONNECTION=sync. Anything slow is either done inline with progress UI, or chunked across cron ticks
One cron entry * * * * * php artisan schedule:run drives scheduled publishing, cache warming, backup chunks, cleanup
No Node on the server Vite output is committed; npm is never run on the server
Low memory, short max_execution_time Image processing, backups and cache rebuilds are chunked and resumable
No root No system packages and no php.ini edits assumed. .user.ini is ignored by mod_lsapi on some hosts, so never depend on it

Image processing note. imagick is not guaranteed on shared hosting. The image pipeline detects the available driver at install time (imagick, else gd) and records it in config/kodepress.php. WebP conversion needs GD compiled with WebP support (PHP 8.3 normally has it); if it is missing the installer warns and the pipeline stores originals plus resized JPEG/PNG variants instead.

2.3 Request lifecycle — public page#

Visitor GET /about
        |
        v
 (web middleware: session, CSRF on writes, locale resolution)
        |
        v
 ResolveSite            -> site by host, else the single default site
        |
        v
 SetLocale              -> /bn/.. , /en/.. , or the site default   (see doc 12)
        |
        v
 PageController@show    (catch-all route, registered LAST)
        |
        +-- HtmlCache::get(site, locale, path) --> HIT --> return cached HTML (200)
        |
        +-- MISS
              |-- Redirects::match(path)            -> 301/302 if a rule matches
              |-- Page::publishedByPath(site, path) -> 404 view if none
              |-- load pages.current_version_id -> page_versions.content (the block tree)
              |-- PartResolver: header + footer for this page        (see doc 08)
              |-- PageRenderer::render(tree) -> Blade component per block (see doc 04)
              |-- ThemeCss::url() -> token-generated stylesheet       (see doc 09)
              |-- HtmlCache::put(...) -> write the file
              +-- return HTML (200)

Key properties:

2.4 Request lifecycle — publish#

Editor clicks Publish
        |
        v
 PublishPage action (one transaction)
        |-- validate the tree against BlockRegistry schemas
        |-- sanitise every richtext / html field value
        |-- create a page_versions row (content snapshot, author, label)
        |-- pages.current_version_id = the new version
        |-- pages.status = published | scheduled, published_at set
        |-- slug changed? -> insert a 301 in `redirects` from the old path
        +-- audit_logs row
        |
        v  (after commit)
 PageWasPublished event
        |-- HtmlCache::forgetPage(page)   -> clear this page, every locale
        |-- HtmlCache::warm(page)         -> re-render immediately (configurable)
        +-- Sitemap::markDirty()

Draft autosave is not a publish: it writes pages.draft_content and never touches the cache or the public site.

2.5 Rendering model#

Three renderers share one tree format:

Renderer Input Output Used by
PageRenderer page_versions.content full page HTML public pages, cache warm
PartRenderer template_parts.content header / footer HTML wrapper around page HTML
PanelRenderer menu_items.mega mega-menu panel HTML menu rendering

All three walk the same sections -> columns -> blocks structure and resolve each block through BlockRegistry to app/Blocks/<Name>/view.blade.php. There is exactly one code path that turns a block into HTML; the renderers differ only in the wrapper they emit.

Grid: a section renders a 12-column grid. column.width is the desktop span; tablet and mobile spans come from section settings, defaulting to stacking below 768 px. Columns never use absolute positioning — that is what keeps the output responsive by construction.

2.6 Folder layout#

app/
├── Actions/              single-purpose writes (PublishPage, DuplicatePage, RestoreVersion, ...)
├── Blocks/               ONE FOLDER PER BLOCK — the extension point
│   ├── Heading/{schema.php, view.blade.php, preview.png}
│   ├── Text/ Image/ Button/ Video/ Gallery/ Form/ PostList/ Accordion/ Html/
│   ├── MenuSlot/ Logo/ Search/ LanguageSwitcher/ SocialIcons/
│   └── AnnouncementBar/ Newsletter/ BackToTop/
├── Console/Commands/     kodepress:install, kodepress:cache-warm, kodepress:backup, cms:* aliases
├── Http/
│   ├── Controllers/      thin: Public\PageController, Public\FeedController, Admin\*
│   ├── Middleware/       ResolveSite, SetLocale, AdminArea, EnsureInstalled
│   └── Requests/         all validation
├── Livewire/
│   ├── Editor/           PageEditor, OutlineTree, SettingsPanel, SectionGallery, MediaPicker
│   ├── Menus/            MenuBuilder, MegaPanelEditor
│   ├── Design/           ThemeSettings, PartEditor, TemplateGallery
│   └── Settings/         SiteInfo, Users, Redirects, Backup, Forms
├── Models/               one per table in doc 03
├── Policies/             PagePolicy, MenuPolicy, TemplatePartPolicy, ThemePolicy, FormPolicy, ...
├── Services/
│   ├── Blocks/           BlockRegistry, SchemaValidator, FieldTypes/
│   ├── Rendering/        PageRenderer, PartRenderer, PanelRenderer, PartResolver
│   ├── Cache/            HtmlCache, CacheInvalidator
│   ├── Media/            ImagePipeline, MediaStorage
│   ├── Seo/              SitemapBuilder, RedirectMatcher, MetaBuilder
│   ├── Theme/            TokenCompiler
│   └── Backup/           BackupRunner, RestoreRunner
└── Support/              helpers: fmt_date(), Sanitizer, Slug, Tree

config/kodepress.php            block paths, cache dir, image driver, locales, admin path, limits
database/migrations/            exactly the tables in doc 03
database/seeders/               RoleSeeder, ThemeSeeder, TemplateSeeder, DemoContentSeeder
lang/{bn,en}/                   admin.php, blocks.php, validation.php, public.php, emails.php
resources/
├── views/
│   ├── admin/                  layout + screens (Livewire renders inside)
│   ├── public/                 layout, page shell, 404, feed templates
│   ├── components/             block wrappers: section, column, block
│   └── parts/                  header / footer presets
├── css/ js/                    Vite sources (app, editor, admin)
└── fonts/                      Noto Sans Bengali, Inter (self-hosted)
public/build/                   COMMITTED Vite output
storage/app/kodepress-cache/    rendered public HTML
storage/app/public/media/       uploads (symlinked to public/storage)
docs/                           this documentation
tests/Feature/ tests/Unit/      Pest

2.7 Data boundaries#

2.8 What is stored as JSON, and why#

JSON is used where the shape is user-defined and never queried relationally:

Column Holds Why JSON
page_versions.content, pages.draft_content the block tree arbitrary user structure; read whole, written whole
template_parts.content header / footer tree same
menu_items.mega mega-panel tree same
menu_items.visibility, menu_items.settings per-item rules and styling sparse and evolving
template_parts.conditions assignment rules evaluated in PHP, never in SQL
pages.seo title, description, OG image, canonical, noindex sparse metadata
theme_settings.tokens design tokens one row per site, compiled to CSS
forms.fields form field definitions user-defined
media.conversions generated variant paths derived data

Anything that must be filtered, sorted or joined — status, slug, dates, taxonomy, menu location — is a real column with a real index.

2.9 Service responsibilities at a glance#

Service Owns Never does
BlockRegistry discovering blocks, caching the manifest, exposing schemas render HTML
SchemaValidator validating a tree against schemas, coercing defaults sanitise HTML
Sanitizer HTML allow-listing for richtext and the HTML block decide permissions
PageRenderer tree -> HTML read the cache or write it
HtmlCache read, write, forget, warm cached HTML decide when to invalidate
CacheInvalidator mapping a model change to what must be cleared render
PartResolver picking the header/footer for a given page render
TokenCompiler tokens -> one CSS file, with a content hash in the name know about pages
ImagePipeline compression, WebP, variants, metadata authorise uploads
SitemapBuilder sitemap.xml and robots.txt handle redirects
RedirectMatcher matching a path to a redirect rule create rules (actions do)

KodePress documentation · generated from the Markdown sources by tools/build-docs-site.py · internal preview, not indexed.