dniccum/nova-documentation
dniccum/nova-documentation adds a complete documentation system to Laravel Nova. Write, organize, and publish Markdown-based guides, help pages, and internal docs directly inside Nova with a built-in editor, navigation, and versioning—no separate CMS needed.
Install and publish
composer require dniccum/nova-documentation
php artisan vendor:publish --tag=nova-documentation
The publish tag gives you config/nova-documentation.php. Read it before writing any markdown — disk, path, home, and the menu label all live there, and every "it doesn't work" failure traces back to this file.
Register the tool in app/Providers/NovaServiceProvider.php:
public function tools()
{
return [
new \Dniccum\NovaDocumentation\NovaDocumentation,
];
}
Drop a landing document at {path}/{home}.md on the configured disk (default convention is home.md). One file is enough to confirm the tool boots and renders in /nova. Don't assume the path — resolve it concretely:
Storage::disk(config('nova-documentation.disk'))
->exists(config('nova-documentation.path').'/home.md');
If that returns false, you have your answer before you open a browser.
First realistic day-to-day use case: the internal admin handbook. Create docs/getting-started.md, docs/billing/refunds.md, docs/permissions.md in the repo, deploy, and you have a navigable runbook set for support and ops staff living inside the panel they already use. This is the pattern 90% of teams stick with — it's the package's sweet spot and the reason to adopt it at all.
Where to look first when something breaks: the published config (disk + path together), then php artisan route:list | grep -i document, then whether home.md exists at that resolved location, then whether config is cached. That order resolves nearly everything.
Version note: v3.6.0 is a maintenance release — Laravel 13.x compatibility plus markdown link-processing fixes (#45, #36). Start at 3.6.0 or later; there is no new feature surface versus 3.5.x, so the integration API you learn today is stable across the 3.x line.
The directory tree is the sidebar. Nested folders become nested nav. This is the primary organizing mechanism and it's worth respecting: keep nesting to two levels, group by audience or workflow (onboarding/, runbooks/, policies/), and let filenames be short and stable.
Front matter for display, filenames for identity. Control titles and ordering in YAML front matter, not by renaming files — filenames typically drive URLs, so a rename is a broken inbound link and a broken bookmark.
---
title: Refund Processing
order: 2
---
# Refund Processing
...
Use order aggressively once you pass ~10 pages; it's the only knob you get for nav sequencing across directories.
Multiple doc sets via multiple tool instances. Need "Developer Docs" and "Support Playbooks" as separate Nova sidebar entries? Register two instances pointed at different paths:
public function tools()
{
return [
(new \Dniccum\NovaDocumentation\NovaDocumentation)->withPath('docs/engineering'),
(new \Dniccum\NovaDocumentation\NovaDocumentation)->withPath('docs/support'),
];
}
(Check the actual method name in your installed version — the package is single-path per instance, so if there's no fluent setter, duplicate the config or subclass and override it.) Two thin instances beat faking sections inside one tree.
Gate access at the tool level with canSee. There is no per-document permission system. The only access control seam is whether the tool appears in Nova at all:
public function tools()
{
return array_filter([
auth()->user()?->isAdmin() ? new \Dniccum\NovaDocumentation\NovaDocumentation : null,
]);
}
For finer-grained scoping, split into multiple tool instances and gate each one on a different ability. Anything beyond that means overriding views or forking — don't fight it.
Relative cross-document links. Prefer [Invoices](../billing/invoices.md) over hardcoded /documentation/... URLs. Relative links survive base-URI changes; absolute ones don't. If you applied workarounds for broken links on 3.5.x, re-test them after upgrading to 3.6.0 — the link processor changed.
Ship docs with the code. Keep the markdown directory in the repo, deployed alongside the feature it describes. Add a PR checklist item ("updated the runbook") so the docs don't rot. The entire value proposition is proximity — if docs live in a separate wiki, you've paid the integration cost for nothing.
Seed content in local dev. Committing markdown means every developer gets the same docs on php artisan migrate --seed deploys or a git pull. No import step, no sync job. For a docs system, that's the correct amount of infrastructure.
Branding via published views. Publish views and edit under resources/views/vendor/nova-documentation/. Pin the package version in composer.json ("3.6.*" or exact) the moment you do this — view paths shift between releases and a composer update will silently break your layout.
Version by copying. "Current" vs "archived" docs: duplicate the tree, point a second tool instance at the copy. Cheap, obvious, works. The package has no versioning concept and bolting one on is more effort than it's worth.
path is a prefix within the disk, not a filesystem path. /var/www/docs silently resolves to nothing. Always test disk and path together, never in isolation.local ≠ storage/app/public. With disk => 'local', path is relative to storage/app. Putting files in storage/app/public and wondering why they don't render is the single most common integration failure.--- fence, or order: "3" instead of order: 3, can drop a page from the nav with no error. When a file "disappears," validate the front matter before anything else — the symptom doesn't point at the cause.php artisan cache:clear (and config:clear if config is cached). Locally, a hard refresh is usually enough — which is exactly why this bites in production and not in dev.index.md of links. Don't promise full-text search to stakeholders; it isn't there and won't be added by config..md files, and links inside fenced code blocks — both the previously broken cases and any workaround markup you wrapped around them.canSee on the tool instance. No events, no middleware, no per-document policies. Keep all customization at those three points; anything else is a fork.canSee → config resolves → path/home.md exists on that disk → config not stale → route in route:list. Work down it; almost every failure is in the first three.---, parses as YAML, and has a unique order will save you the silent-disappearance debugging session. Cheap insurance for a package with no validation.laravel-shift PR, so re-read composer.json constraints after any bump — the supported framework range can shift at either end.How can I help you explore Laravel packages today?