Skip to content
Docent Docs

Getting Started

Multiple sites

Run separate documentation sites, each with its own content, routes, branding, and access rules, from one install.

One Docent install can serve any number of documentation sites. The classic split is a public help center at /help next to internal admin docs at /admin/docs, each with its own content directory, middleware, branding, and admin panel. Every install already uses this machinery: the shipped config defines a single site keyed docs, and until you add a second entry you never notice it.

A site is one entry in the sites array of config/docent.php. The key is yours to choose (letters, numbers, underscores, and hyphens), and it shows up in route names, cache keys, and upload paths, so pick something you're happy to see in a URL helper.

Adding a second site

php
'default' => 'public',

'sites' => [
    'public' => [
        'name' => 'Help Center',
        'route' => ['prefix' => 'help', 'middleware' => ['web']],
        'filesystem' => ['path' => resource_path('docs-public')],
    ],

    'admin' => [
        'name' => 'Admin Docs',
        'route' => ['prefix' => 'admin/docs', 'middleware' => ['web', 'auth']],
        'filesystem' => ['path' => resource_path('docs-admin')],
        'admin' => ['enabled' => true, 'gate' => 'manageAdminDocs'],
        'theme' => ['accent' => '#e11d48'],
    ],
],

Two rules apply to every site beyond the shipped docs entry. It must set filesystem.path, because only docs gets the resource_path('docs') fallback. And it needs its own route.prefix or route.domain; docent:check warns when two sites would collide.

Each site owns its identity sections: name, description, route, filesystem, admin, navigation, and layouts. Everything else (theme, search, ai, insights, content, database, cache, authorization, widget) is inherited from the top level and overridable per site. The configuration page covers the cascade in detail.

Separate audiences, separate access

Access is decided per site twice over. The site's route.middleware gates the whole surface, so the admin site above requires a login before Docent renders anything, while public stays open. On top of that, each site's admin panel has its own gate, so the people who edit your help center and the people who edit internal runbooks can be entirely different groups.

Everything permission-aware stays inside its site. Search results, navigation, llms.txt feeds, Assistant answers, and widget suggestions all draw from the current site's corpus and nothing else. A page about internal tooling on the admin site cannot leak into a public search, even when both sites share a database.

What isolation looks like in practice

Database-backed pages, Assistant question logs, and insight events share tables, scoped by a site column. Page identity is (site, slug), so billing/refunds can exist once on each site without conflict.

Admin image uploads are stored under docent/{site}/ on the site's configured disk, and each site's _uploads route serves only its own directory. Two sites can share the default public disk without a private site's images becoming reachable through the public one.

Caches are namespaced per site, and console commands follow suit:

bash
php artisan docent:clear                 # every site
php artisan docent:clear --site=public   # just one
php artisan docent:check --site=admin

Route names

Every route name includes its site key: docent.public.home, docent.admin.show, and so on for the routes of any site, including the shipped docs site. Use the keyed name anywhere your application links into documentation:

php
route('docent.public.home');
route('docent.admin.show', ['slug' => 'internal/runbook']);

Integrations per site

Registrations on the Docent facade apply to every site. Register the same identifier through Docent::site() to override it for one:

php
Docent::value('support.email', fn () => 'help@example.com');

Docent::site('admin')->value('support.email', fn () => 'ops@example.com');

Every closure also receives the current site on its context, so one global registration can branch instead:

php
Docent::value('support.portal', fn ($context) => match ($context->site?->key) {
    'admin' => route('admin.support'),
    default => route('support'),
});

See dynamic content for the full integration reference.

Targeting a site from the widget

The in-app help widget takes a site attribute, and different widgets on the same host page can target different sites:

blade
<x-docent::widget site="public" />

Omit the attribute and the widget uses your default site.

One rule for early-running code

Docent selects the current site when the request's route is matched. Inside a Docent request you can inject DocentManager or any other Docent service and get the right site. Code that runs before routing, such as a global middleware, should call Docent::site('public') instead, because an injection that early holds the default site's instance.