Writing Docs
Dynamic Content
The app-aware directives: values, links, ability blocks, audiences, conditions, components, and includes.
This is where Docent stops being a static site generator. Documentation never
contains raw PHP or Blade. Instead, your app registers stable, allowlisted
integrations (usually in a service provider) and your Markdown references them
by name. Your internals can refactor freely; the identifiers your docs use stay
stable, and docent:check catches any drift.
The examples on this page are live. This site registers a handful of real
integrations in its AppServiceProvider, so the directives below render actual
values for you, the current viewer.
Dynamic values
A {{ value:name }} token renders a string resolved from your app. Register one
with the Docent facade:
use STS\Docent\Facades\Docent;
Docent::value('viewer.name', fn ($context) => $context->user?->name ?? 'a guest');
Docent::value('app.name', fn () => config('app.name'));
The resolver receives a context object with the current user, request,
audience, and site (its key and name). Reference the value inline:
Signed in as {{ value:viewer.name }}, reading the {{ value:app.name }} docs.
Rendered for you right now: Signed in as a guest, reading the Docent docs.
Values can take arguments, which are passed after the key and forwarded to your resolver:
You have used {{ value:usage.transactions 30d }} transactions this period.
App links
A {{ link:name }} token resolves to a URL from a named registration, and a
{{ route:name }} token resolves a Laravel named route directly. Use either
inline or as a Markdown link destination.
Docent::link('repository', fn () => 'https://github.com/stechstudio/laravel-docent');
Browse [the source]({{ link:repository }}) or open your
[billing settings]({{ route:billing.settings }}).
The link half is live: browse the source. Because links
resolve through named registrations and routes, a refactor that renames a route
never leaves a dead link in your docs. docent:check verifies every link and
route still resolves.
Tokens inside code
Tokens don't resolve inside backticks or fenced code blocks. That's deliberate, and it's the only reason this page can show you the syntax at all. The cost is that wrapping a token in backticks when you wanted its value prints the mustache syntax to the reader instead:
Your plan is `{{ value:account.plan }}`.
That renders the literal text rather than the plan name, and the page still
returns 200, so nothing about the result looks wrong until someone reads it.
docent:check catches it with the token-in-code rule, which fires only when
the key names something your application actually resolves. Generic examples
like {{ value:some.key }} name nothing registered and stay quiet, so
documenting the dialect costs you nothing. Fenced blocks are left alone
entirely, since an example is meant to be read literally.
When a resolver fails
Your closures run against real application state, and some reader states are
odd. A freshly invited user has no tenant selected; someone mid-account-switch
is briefly between them. A value or link closure that throws for one of those
substitutes nothing, is handed to report(), and the rest of the page renders
normally. A help center is where someone goes when something is already wrong
for them, so one broken token shouldn't cost them every paragraph around it.
The exception still reaches your error tracking, so the closure gets fixed
rather than quietly papered over. This covers your closure failing, not Docent
failing to call it: a resolver class that doesn't exist, a resolver returning
something that isn't a string, and a {{ route: }} token missing a parameter
all still raise, because those break for every reader alike and are defects to
fix rather than reader states to render around.
If you'd rather see the exception either way, ask for it:
// config/docent.php
'render' => [
'strict_tokens' => true,
],
Ability blocks
:::can and :::cannot show or hide content based on a gate ability, evaluated
against the current viewer. Content inside a hidden block is never sent to the
browser and never indexed for search.
:::can ability="billing.manage"
Head to billing settings to change the default card.
:::
:::cannot ability="billing.manage"
Only an account administrator can change the payment method.
:::
This site defines a read-docs ability that everyone passes, so the :::can
block below renders for you and the matching :::cannot block stays hidden:
You can read these docs, so this block is shown: you pass the read-docs
ability.
Audience blocks
An audience is a named predicate your app registers. It's useful when a rule is
more than a single gate ability. Gate an entire block on it with :::audience.
Docent::audience('billing-admin', fn ($context) => $context->user?->can('billing.manage') ?? false);
:::audience name="billing-admin"
As a billing administrator you can also download invoices in bulk.
:::
This site registers an everyone audience that always matches, so the block
below renders for every viewer:
Everyone is in the everyone audience, so you can see this.
Condition blocks
Conditions gate content on feature flags or any boolean your app computes.
:::when shows content when the condition is true; :::unless shows it when
false.
Docent::condition('advanced-exports', fn ($context) => $context->user?->account->allowsAdvancedExports() ?? false);
:::when condition="advanced-exports"
Your plan includes scheduled CSV exports.
:::
:::unless condition="advanced-exports"
Upgrade to unlock scheduled CSV exports.
:::
This site registers a search-enabled condition wired to config. Search is on,
so the :::when block renders and the :::unless block stays hidden:
Search is enabled on this site, so this block is visible.
Embedded components
Register a Blade-backed component and embed it with a self-closing
<docs-component> tag. Attributes beyond name are passed to the component.
use App\Docs\PlanUsageComponent;
Docent::component('plan-usage', PlanUsageComponent::class);
<docs-component name="plan-usage" plan="team" />
The component renders real, app-generated UI inside the page: a usage meter, a
live status badge, anything your app can produce. docent:check fails if a page
references a component name you haven't registered.
Includes and partials
Reusable fragments live in a _partials directory and are pulled into any page
with :::include. A partial is just a Markdown file, and it can itself contain
directives.
:::include name="support-note"
The fragment below is included live from _partials/support-note.md:
This callout lives in _partials/support-note.md and is pulled in with
:::include name="support-note". Edit it once and every page that includes it
updates.
Edit the partial once and every page that includes it updates. docent:check
reports a missing partial or an include cycle before you ship.
Registrations on a multi-site install
Everything registered on the Docent facade, values, links, conditions,
audiences, components, and widget suggestions alike, applies to every
configured site. When one site needs a different answer for the same
identifier, you have two options.
Register it on that site's manager, which wins over the global registration for that site only:
Docent::value('support.email', fn () => 'help@example.com');
Docent::site('admin')->value('support.email', fn () => 'ops@example.com');
Or keep one global registration and branch on the context's site:
Docent::value('support.portal', fn ($context) => match ($context->site?->key) {
'admin' => route('admin.support'),
default => route('support'),
});
Both approaches keep the identifier in your Markdown identical across sites,
which is the point: authors write {{ value:support.email }} and never care
which site the page lives on. See multiple sites
for the wider picture.
Ready to gate whole pages instead of blocks? See access control.