Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament Knowledge Base Laravel Package

guava/filament-knowledge-base

A Filament plugin that adds a searchable knowledge base to your admin panel. Create articles and categories with familiar Filament resources, write in Markdown, and help users find answers without leaving the dashboard.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish before you register the plugin on a panel. Order matters — registering the plugin first gives you nav items and routes pointing at tables that don't exist yet, and the resulting 500s look like a broken package rather than a missing migration.

composer require guava/filament-knowledge-base
php artisan vendor:publish --tag="filament-knowledge-base-migrations"
php artisan vendor:publish --tag="filament-knowledge-base-config"
php artisan migrate

Pin tightly in composer.json — this package has essentially no public dependents and a fast-moving minor line, so "guava/filament-knowledge-base": "3.3.*" (not ^3) until you've done an upgrade drill at least once.

Read these four things before writing any code, in this order:

  1. The plugin class. Open vendor/guava/filament-knowledge-base/src/ and find the class implementing Filament\Contracts\Plugin. Read register() and boot(): they tell you exactly which resources, pages, widgets and routes get attached, and under what IDs. Copy the FQCN from the file, never from a blog post — the namespace is the single most common source of "class not found" in AI-generated snippets for low-adoption packages.
  2. config/filament-knowledge-base.php. Navigation group/label/icon, route prefix, resource class overrides, possibly a public-reader toggle. This file answers 80% of "can I change X without forking?" questions.
  3. The published migration. Confirms the actual schema: is it category_id on articles, or a pivot? Are there published_at / is_published / status columns? Does a tenant_id exist? Your content model decisions hang off this.
  4. php artisan route:list --name=knowledge (adjust the filter). Get the real route names and prefix now — you'll be generating URLs from them for the rest of the project's life, and the docs' assumed prefix is frequently wrong.

Register per panel, in the panel provider:

// app/Providers/Filament/AdminPanelProvider.php
use GuavaCZ\FilamentKnowledgeBase\KnowledgeBasePlugin; // verify against vendor/

public function panel(Panel $panel): Panel
{
    return $panel
        ->plugin(KnowledgeBasePlugin::make());
}

First use case — one end-to-end pass that validates the whole chain. Log into /admin, create a category ("Getting Started") and one article with a body, publish it, then hit the reader route in a private window as a low-privilege user. That single loop exercises migrations, route registration, policy wiring and the renderer. If it passes, the rest of the work is content and ergonomics; if it fails, you've found the problem while the blast radius is still zero.

Implementation Patterns

Treat the resources as scaffolding you own, not a black box. Before forking, check the config for a resources (or similarly named) map. Guava plugins generally let you substitute your own resource subclass:

// config/filament-knowledge-base.php — verify key names in your version
'resources' => [
    'article'  => \App\Filament\Resources\KnowledgeBase\ArticleResource::class,
    'category' => \App\Filament\Resources\KnowledgeBase\CategoryResource::class,
],

Extend, then override form() / table() / getNavigationGroup() in your subclass. That survives a composer update; a patched vendor view does not.

Keep article bodies in version control, seed them on slug. This is the pattern that makes the package feel like part of your codebase rather than an external CMS. Bodies live as markdown in database/kb/*.md, a seeder upserts by slug, and the whole thing reviews in a PR like any other change.

// database/seeders/KnowledgeBaseSeeder.php
foreach (File::glob(database_path('kb/*.md')) as $path) {
    $slug = basename($path, '.md');

    Article::updateOrCreate(
        ['slug' => $slug],
        [
            'title'       => Str::headline($slug),
            'category_id' => $categories[Str::before($slug, '/')] ?? null,
            'body'        => File::get($path),
            'is_published' => true,
        ],
    );
}

Run it unguarded in local and staging; in production wrap it behind an explicit --class invocation or an env check so a stray db:seed can't clobber an article a support lead hand-edited. If hand-editing in production is the actual workflow, invert it: seed once, then treat the DB as the source of truth and export back to markdown on a schedule.

Deep-link contextual help from the exact field that confuses people. This is the highest-leverage thing the package does and the reason to adopt it over a Notion page. Put a hint action next to the input, not a link in a sidebar:

TextInput::make('webhook_url')
    ->hintIcon('heroicon-m-question-mark-circle', tooltip: 'Outbound webhook setup')
    ->hintAction(
        Action::make('help')
            ->icon('heroicon-o-arrow-top-right-on-square')
            ->url(fn () => Help::url('integrations/webhook-setup'))
            ->openUrlInNewTab()
    );

Wrap it in one helper so a missing or unpublished article degrades to a hidden action instead of a 404 in a modal:

final class Help
{
    /** @var array<string, string>|null */
    private static ?array $published = null;

    public static function url(string $slug): ?string
    {
        self::$published ??= Article::query()
            ->where('is_published', true)
            ->pluck('slug')
            ->flip()
            ->all();

        return isset(self::$published[$slug])
            ? route('filament.knowledge-base.articles.show', ['article' => $slug]) // verify route name
            : null;
    }
}

Reference slugs from a HelpArticle enum or a constants class rather than raw strings scattered through resources — that gives you one place to grep when a slug changes, and IDE rename support.

Split authoring from reading across panels with policies. The plugin registers per panel with no global switch, and a read-only factory may not exist. The reliable lever is a policy, which also gives you the audit-shaped answer stakeholders ask for:

public function viewAny(User $user): bool
{
    return $user->can('knowledge-base.read');
}

public function create(User $user): bool
{
    return $user->can('knowledge-base.author');
}

Register the full plugin on AdminPanelProvider; register it on operator/customer panels only if you want the nav there, and let the policy hide create/edit. If even the nav item is noise for readers, skip the plugin on that panel entirely and just deep-link out to the reader routes.

Decide the body format once, up front. If you use Filament's RichEditor the body is HTML, and RichEditor output from a trusted author is not automatically safe for other users — purify on render (Purifier::clean($body) or equivalent) in a view override. If you use markdown, Str::markdown($body) in your own view gives you control over heading anchors, tables and link handling. Switching later means rewriting every article, so make this call during the spike.

Handle draft/publish as a workflow, not a boolean. If the schema has a published flag, treat "publish" as a deliberate action with a review step: author as draft, editor flips it, and the reader query filters on it. Keep a published_at you can sort by so the KB index can show "recently updated" — users trust docs more when they can see recency.

Test the surface you actually depend on. Two cheap tests pay for themselves:

it('renders every seeded article', function () {
    $this->seed(KnowledgeBaseSeeder::class);

    Article::where('is_published', true)->pluck('slug')->each(function (string $slug) {
        $this->get(route('filament.knowledge-base.articles.show', ['article' => $slug]))
            ->assertOk();
    });
});

it('only links to published articles', function () {
    expect(Help::url('integrations/webhook-setup'))->not->toBeNull();
});

The first catches route-prefix and rendering regressions on upgrade; the second catches the "help action points at a deleted article" bug that otherwise surfaces as a support ticket about a broken help link.

Gotchas and Tips

  • Migrate before publish is a no-op. The migration lives in the package; php artisan migrate before vendor:publish --tag=...-migrations does nothing and the plugin then fails on missing tables. Publish, migrate, then register.
  • Per-panel means per-panel. Registering on admin and expecting the nav item elsewhere is the most common confusion with this plugin family. If you want uniformity, loop over your panels; if not, make the split explicit in a comment so the next dev doesn't "fix" it.
  • Route prefix collisions are silent. A catch-all route, an existing page at the configured path, or a conflicting prefix wins and you get a 404 on a route that route:list says exists. Check the order in route:list, not just presence.
  • Nav visible + 403 on click (or the reverse) is a policy problem, not a plugin bug. Debug it directly instead of guessing: Gate::forUser($user)->allows('viewAny', Article::class) in tinker. The plugin will almost never be the owner of that decision.
  • Multi-tenancy is not handled for you. If the migration has no tenant_id, adding one also means scoping the slug unique index — otherwise two tenants can hold the same slug and route-model binding quietly resolves the wrong article. Use a compound unique (tenant_id, slug) and scoped bindings.
  • Slug renames break every deep link you've shipped. Treat slugs as permanent identifiers. If you must rename, add a redirect row or a small lookup table mapping old→new; otherwise every hintAction you placed in the app starts 404ing.
  • Cache will lie to you during authoring. If the reader caches articles, edits won't appear for readers immediately. Disable it in local/staging, and in production hook Article::saved → Cache::forget("kb.article.{$article->slug}"). Nothing eats an afternoon faster than "my edit didn't save" that was actually a stale cache.
  • Never edit published vendor views in place. Prefer config: navigation group, label, icon, route prefix, resource overrides. If you truly need a view change, publish it into resources/views/vendor/... and note in the PR that it must be re-diffed on every upgrade — with a package this young, that's a recurring tax.
  • Run an upgrade drill before every bump. In a throwaway branch: re-publish config and migrations with --force, then git diff. Renamed config keys and new columns show up as a readable diff instead of a staging incident. 3.3.2 was a Polish translation fix, so patch releases are low risk; the minor line is where behaviour moves.
  • Media uploads need php artisan storage:link. Rich-text image uploads write to a disk; if the symlink isn't there, authors see broken images and assume the editor is broken.
  • Keep the article model thin and the exit ramp short. Slug, title, category, body, published. Combined with bodies-in-git, migrating to a forked resource, a static docs generator, or your own CRUD later is a data copy plus changing one route helper — not a rewrite. With zero dependents and a 0.3 score, that insurance is the difference between a contained pilot and a load-bearing liability.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings