Skip to main content
Laravel, shipping fast.

Chapter 18

Shipping Templates to Live Sites

Julian Beaujardin

Every chapter before this one built one well-made API. One repo, one deploy target, one team reading one set of conventions. That's the easy case, and everything in this chapter breaks the moment it stops being true.

Here's the harder case: your platform doesn't just serve requests, it generates other running applications. Each one is a live Laravel site, cloned from a shared skeleton, sitting in its own git repo, deployed to its own box, edited by a real customer through a browser. You don't own one API anymore. You own a fleet, and every member of it has to keep behaving like it belongs to the same system, months after you shipped the code that created it. This chapter covers distributing a template system across independently-versioned tiers, why the installer that bakes a theme into a site matters more than the theme itself, why a version bump is a two-step operation, how a persistent runtime cache can make a real fix look like it did nothing, and why a live editing surface needs a credential of its own.

Distribution is a harder problem than construction. Building the thing once is the part every tutorial covers. Getting a change into every already-running copy of it, safely, on your schedule, is the part that decides whether your platform scales past a dozen customers.

Three Tiers, Three Cadences

Start with the shape of the problem. A generated site is not one artifact, it's the composition of three:

  1. A skeleton repo, cloned once per customer instance: the Laravel application itself, bootstrap/app.php, config/, routing. It changes rarely, because every change has to be re-cloned or manually reconciled into every instance that already exists.
  2. A package installed into that skeleton, delivering the actual product: the theme, the editable-content system, the Blade components a customer's site is built from. Versioned independently, pulled in through Composer like any other dependency.
  3. A static-asset host, serving the client-side code the installed package references, on its own release cadence entirely separate from either of the other two.

In this fleet, that's template-base (the skeleton), package-templates (published as webplo/templates), and a CDN serving compiled client-side JS. Here's why splitting it this way earns its complexity: each tier answers a different question, on a different timeline.

template-base (cloned once per customer)
    ↓ composer install
webplo/templates (theme + editable system, versioned package)
    ↓ ships client-side code hosted on
cdn (static assets, its own release cadence)

The skeleton answers "what is this application, structurally." The package answers "what does it look like and do today." The CDN answers "what browser code does the running page load right now." Bundle those three questions into one repo and every answer changes at the pace of the slowest one: a one-line CSS tweak now requires re-cloning a Laravel skeleton into a dozen customer repos. Split them, and each tier moves at exactly the speed its own question needs.

It's worth being honest about what "changes rarely" costs. template-base's bootstrap/app.php still hand-instantiates Illuminate\Foundation\Application and binds the HTTP kernel and exception handler as singletons, the pre-Laravel-11 skeleton shape, even though its composer.json pins the same laravel/framework: ^13.23.0 constraint every other service in this fleet runs on the modern Application::configure() skeleton with. Nobody's losing sleep over that. It's the predictable cost of a tier deliberately kept hard to touch: never worth the risk of a structural migration, because nothing about the product actually lives there anymore. A tradeoff working as designed, not a bug you'd find by grepping harder.