Skip to content
Docent Docs

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:

php
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:

markdown
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:

markdown
You have used {{ value:usage.transactions 30d }} transactions this period.

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.

php
Docent::link('repository', fn () => 'https://github.com/stechstudio/laravel-docent');
markdown
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:

markdown
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:

php
// 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.

markdown
:::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.

php
Docent::audience('billing-admin', fn ($context) => $context->user?->can('billing.manage') ?? false);
markdown
:::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.

php
Docent::condition('advanced-exports', fn ($context) => $context->user?->account->allowsAdvancedExports() ?? false);
markdown
:::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.

php
use App\Docs\PlanUsageComponent;

Docent::component('plan-usage', PlanUsageComponent::class);
markdown
<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.

markdown
:::include name="support-note"

The fragment below is included live from _partials/support-note.md:

Included fragment

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:

php
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:

php
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.