appstract/laravel-multisite
appstract/laravel-multisite is a Laravel package for serving multiple websites from one codebase. It resolves the current site by domain, adds site-aware models and middleware, and helps keep content and configuration separated across domains.
composer require appstract/laravel-multisite, then publish the provider assets (php artisan vendor:publish --provider="Appstract\Multisite\MultisiteServiceProvider"). You get a config/multisite.php and, depending on version, a middleware entry to register in app/Http/Kernel.php.config/multisite.php. Site registration lives there: a sites array keyed by a slug, each with at least a domain (or hostname) and optional per-site paths for config, views, routes, and database. This file is the package — everything else is resolution and swapping.web group (before StartSession, Authenticate, and SubstituteBindings) so the active site is known before sessions, auth, or routing read config. Verify in Kernel.php; if the package auto-registers, confirm via php artisan route:list -v.site-a.test and site-b.test at the same vhost (Valet/Homestead hosts file). Hit each domain; you should see the different view/route/config output. That two-site smoke test is your first use case and immediately tells you whether path swapping works before you invest in real content.AppServiceProvider::boot() to confirm domain, loaded config, and DB connection. This is faster than reasoning from the config file.config/multisite.php as the source of truth and keep each site's overrides in dedicated directories (sites/{slug}/config, sites/{slug}/routes, resources/views/{slug}). This mirrors the package's "drop-in" design and avoids a schema migration just to add a domain.routes/web.php for shared endpoints and load sites/{slug}/routes/web.php for site-specific ones. Wrap site routes in per-site middleware/prefixes if you need different auth guards.database connection name per site in config and reference it via config('database.default') after detection — models should never hardcode a connection. Use ->on(site()->connection) or resolve the connection from config so tests can swap it.array_merge the site's config values on top. Fully replacing config per site quietly breaks mail, cache, and filesystem drivers.env(). env() is cached at boot and won't vary per request.$this->get('http://site-b.test/...') (or set the Host header / SERVER_NAME) so Laravel's test client exercises the real detection path. Add one feature test per site hitting the home route.php artisan route:cache and config:cache run without an HTTP host, so site detection has nothing to resolve. Either disable caching for the multisite middleware or add a "current site" override the console can set.new SyncJob($siteSlug)) and call the package's site-switch API at the top of handle(); otherwise multi-tenant jobs silently write to the wrong database.StartSession, you'll get the default site's cookie domain and database, and debugging is miserable. Put it first, and log the resolved host + slug during development.storage/framework/views and cache prefixes are shared unless you namespace them per site (e.g. config(['cache.prefix' => site()->slug])). Two sites with the same view name but different content will render the wrong compiled Blade.asset() and url() depend on the current request root; if you generate links to another site's domain you must override the root yourself, since the package only resolves incoming hosts.*.test domains plus a single vhost with ServerAlias is the least painful setup; remember to clear config:cache after adding a new site to the array, or the new domain won't exist as far as the package is concerned.How can I help you explore Laravel packages today?