jeffersongoncalves / laravel-knowledge-base
A Laravel package for building knowledge bases with articles, categories, versioning, and feedback
Package info
github.com/jeffersongoncalves/laravel-knowledge-base
pkg:composer/jeffersongoncalves/laravel-knowledge-base
Requires
- php: ^8.2
- illuminate/contracts: ^11.0|^12.0|^13.0
- illuminate/database: ^11.0|^12.0|^13.0
- illuminate/events: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- spatie/laravel-package-tools: ^1.15
Requires (Dev)
- jeffersongoncalves/pest-plugin-database-migrations: ^1.0
- larastan/larastan: ^2.9|^3.0
- laravel/pint: ^1.0
- nunomaduro/collision: ^8.0
- orchestra/testbench: ^9.0|^10.0|^11.0
- pestphp/pest: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Laravel Knowledge Base
A Laravel package for building knowledge bases with articles, categories, versioning, feedback, and search.
See it in action
laravel-knowledge-base-demo is a help center built with this package and filament-knowledge-base, for Fernleaf, a made-up plant shop. It has public help pages in English and German (search, an FAQ, topics, notices for missing or outdated translations) and the Filament admin, with images and files per article and staff-only articles. It runs locally with Docker in a few minutes; screenshots are in its README.
Features
- Article Management — Create, update, publish, archive, and soft-delete articles with UUID and slug routing
- Hierarchical Categories — Nested parent/child categories with ordering and activation control
- Article Versioning — Automatic version history tracking with editor attribution and change notes
- User Feedback — Helpful/not helpful voting with optional comments, supports authenticated and anonymous users
- Related Articles — Many-to-many article relationships with sort ordering
- Full-Text Search — Database-powered, language-aware search across publicly visible articles by title and content
- SEO Fields — Built-in SEO title, description, and keywords per article
- Customizable Models — Override any model via config while maintaining contract compliance
- Table Prefix — Configurable table prefix to avoid naming collisions (default:
kb_) - Translations — 19 languages out of the box: ar, az, de, en, es, fa, fr, hi, it, ja, nl, pl, pt, pt_BR, ru, tr, uk, uz, zh_CN
- Public Visibility — One rule for what customers may see: published, public, and in a public, active category with public ancestors
- Content Localization — One version per configured language with fallback, per-language slugs and the default language as the authoritative one
- Article Assets — Images and files owned by an article, one folder per article, referenced by ID and resolved to current URLs at render time, with usage and broken-reference reporting and a prune command
- Private Assets — Stored on a private disk and served with the article's visibility: plain, cacheable URLs for public articles and expiring signed URLs for everything else
- Collections (FAQ) — Curated, ordered, translatable lists of articles such as an FAQ
- Help Center API — One public-only, language-aware read API for building help pages: category tree, category, article, collection and search
Requirements
- PHP 8.2+
- Laravel 11+
Installation
composer require jeffersongoncalves/laravel-knowledge-base
Publish and run migrations:
php artisan vendor:publish --tag="knowledge-base-migrations"
php artisan migrate
Upgrading an existing knowledge base: before running
migrate, setknowledge-base.default_localeto the language your existing content is written in (defaulten). The localization migration tags every existing article and category with that language; a wrong value mislabels all content.
Publish the config (optional):
php artisan vendor:publish --tag="knowledge-base-config"
Configuration
The config file (config/knowledge-base.php) covers:
Table Prefix
'table_prefix' => 'kb_',
Set to null to use table names without a prefix.
Custom Models
Override any model to extend the default behavior. Custom models must implement the corresponding contract:
'models' => [ 'article' => \App\Models\Article::class, 'category' => \App\Models\Category::class, 'article_version' => \App\Models\ArticleVersion::class, 'article_feedback' => \App\Models\ArticleFeedback::class, 'article_relation' => \App\Models\ArticleRelation::class, 'article_translation' => \App\Models\ArticleTranslation::class, 'category_translation' => \App\Models\CategoryTranslation::class, 'collection' => \App\Models\Collection::class, 'collection_translation' => \App\Models\CollectionTranslation::class, ],
Knowledge Base Settings
'default_visibility' => 'public', // default visibility for new articles 'versioning_enabled' => true, // track article version history 'feedback_enabled' => true, // allow user feedback 'track_views' => true, // track article view counts
Languages
'locales' => ['en'], // languages the knowledge base serves 'default_locale' => 'en', // always the authoritative language; must be in locales 'missing_translation' => 'fallback', // 'fallback' or 'hide' 'search_languages' => ['en' => 'english', 'de' => 'german'], // PostgreSQL text-search config per language
See Localization for details.
Search
'search_engine' => 'database', // search engine type 'search_results_limit' => 20, // max results per search
Attachments
'attachments' => [ 'disk' => env('KB_ATTACHMENTS_DISK', 'local'), // private: files are served by the route below 'directory' => env('KB_ATTACHMENTS_DIRECTORY', 'knowledge-base'), // all knowledge base files live here 'route' => [ 'enabled' => (bool) env('KB_ATTACHMENTS_ROUTE', true), 'prefix' => env('KB_ATTACHMENTS_ROUTE_PREFIX', 'knowledge-base/assets'), 'middleware' => [], 'signed_url_minutes' => (int) env('KB_ATTACHMENTS_SIGNED_URL_MINUTES', 30), 'cache_max_age' => (int) env('KB_ATTACHMENTS_CACHE_MAX_AGE', 3600), // seconds, public articles only ], 'images' => [ 'accepted_types' => ['image/jpeg', 'image/png', 'image/webp'], 'max_size' => (int) env('KB_ATTACHMENTS_IMAGES_MAX_SIZE', 5120), // kilobytes ], 'files' => [ 'accepted_types' => ['application/pdf'], 'max_size' => (int) env('KB_ATTACHMENTS_FILES_MAX_SIZE', 10240), // kilobytes ], ],
Where the images and files that belong to articles are stored, and how readers get them (see Article Assets and Serving assets). The default local disk is private; a private cloud disk such as s3 works the same way. Changing the disk or directory later does not move existing files: each asset remembers the disk and path it was stored on, and php artisan kb:move-assets moves them to the configured disk.
Usage
Using the Service
The KnowledgeBaseService is the recommended way to interact with the knowledge base:
use JeffersonGoncalves\KnowledgeBase\Services\KnowledgeBaseService; $service = app(KnowledgeBaseService::class);
Creating Categories
// Auto-generates slug from name $category = $service->createCategory([ 'name' => 'Getting Started', ]); // With custom slug and parent $child = $service->createCategory([ 'name' => 'Installation', 'slug' => 'installation-guide', 'parent_id' => $category->id, 'description' => 'How to install the application', 'icon' => 'heroicon-o-wrench', 'sort_order' => 1, ]); // Update $service->updateCategory($category, ['name' => 'Quick Start']); // Delete (soft delete) $service->deleteCategory($category);
Creating Articles
$article = $service->createArticle([ 'category_id' => $category->id, 'title' => 'How to Install', 'slug' => 'how-to-install', 'content' => 'Step 1: Run composer require...', 'excerpt' => 'Quick installation guide.', 'visibility' => 'public', 'seo_title' => 'Installation Guide - My App', 'seo_description' => 'Learn how to install My App step by step.', 'seo_keywords' => 'install, setup, getting started', 'metadata' => ['difficulty' => 'beginner'], ], $author); // $author is any Eloquent Model (User, Admin, etc.)
This automatically:
- Generates a UUID
- Creates version 1 (when versioning enabled)
- Dispatches
ArticleCreatedevent
Updating Articles
$updated = $service->updateArticle($article, [ 'title' => 'How to Install (Updated)', 'content' => 'Updated installation steps...', ], $editor, 'Fixed outdated instructions');
When versioning is enabled, this creates a new version entry with the editor and change notes.
Publishing & Archiving
// Publish — sets status to Published, sets published_at, dispatches ArticlePublished $service->publishArticle($article); // Archive — sets status to Archived $service->archiveArticle($article); // Delete — soft deletes $service->deleteArticle($article);
Feedback
// Authenticated helpful feedback $service->addFeedback($article, true, $user, 'Very clear, thank you!', '127.0.0.1'); // Anonymous not-helpful feedback $service->addFeedback($article, false, null, 'Needs more examples'); // Anonymous without comment $service->addFeedback($article, true);
Feedback automatically increments helpful_count or not_helpful_count on the article and dispatches ArticleFeedbackReceived.
Search
// Basic search (only articles customers may see, see "Public vs Internal Content") $results = $service->search('installation'); // With filters $results = $service->search('installation', [ 'category_id' => 1, 'visibility' => 'public', 'limit' => 10, ]); // Staff search $all = $service->search('installation', ['include_internal' => true]); // every published article $staffOnly = $service->search('installation', ['visibility' => 'internal']); // only staff-only articles
Results are ordered by view_count descending (most popular first).
Searching in a language. Pass locale to search that language's versions. Under the fallback setting, an article whose requested-language version is missing or not readable is matched through its authoritative version instead; hide returns nothing for it. Each result reports the language it was served in and carries that language version in served_translation; the article's own columns stay the authoritative version.
$results = $service->search('Passwort', ['locale' => 'de']); foreach ($results as $article) { $article->served_locale; // 'de', or 'en' when it fell back $article->served_translation->title; // the title in that language $article->served_translation->slug; // and its slug, for the link }
On PostgreSQL the German version is searched with German text-search rules (search_languages); MySQL, MariaDB and SQLite use the same LIKE search as above.
Using Models Directly
For queries outside the service, use ModelResolver:
use JeffersonGoncalves\KnowledgeBase\Support\ModelResolver; $articleClass = ModelResolver::article(); $categoryClass = ModelResolver::category(); // Article scopes $published = $articleClass::published()->get(); // staff only, see below $public = $articleClass::publiclyVisible()->get(); $staffOnly = $articleClass::staffOnly()->get(); $drafts = $articleClass::draft()->get(); $archived = $articleClass::archived()->get(); $internal = $articleClass::byVisibility(ArticleVisibility::Internal)->get(); // Category scopes $public = $categoryClass::publiclyVisible()->get(); $active = $categoryClass::active()->get(); $roots = $categoryClass::root()->get(); $ordered = $categoryClass::ordered()->get(); $tree = $categoryClass::active()->root()->ordered()->get(); // Relationships $article->category; $article->author; $article->versions; $article->feedback; $article->relatedArticles; $category->parent; $category->children; $category->articles; // View tracking $article->incrementViewCount();
Public vs Internal Content
Both articles and categories have a visibility of public or internal. An article may be shown to customers only when all of these hold:
- the article is published,
- the article's visibility is
public, - its category, and every parent category above it, is
public, active and not deleted.
Every other published article is staff-only. A public article inside an internal category is staff-only too, and a category whose parent is missing counts as hidden.
| Query | Returns | Use for |
|---|---|---|
publiclyVisible() |
Articles that pass the rule above | Help pages, FAQ, anything a customer can see |
staffOnly() |
Published articles that fail it | Staff-only lists |
published() |
Every published article, whatever its visibility | Admin and staff tools only |
publiclyVisible() and staffOnly() split the published articles: each one is returned by exactly one of them. Category::publiclyVisible() applies the same category rule to category menus. All of them combine with other constraints:
$article = $articleClass::publiclyVisible()->where('slug', $slug)->firstOrFail(); $articles = $category->articles()->publiclyVisible()->get(); $related = $article->relatedArticles()->publiclyVisible()->get();
Content meant for customers and staff alike is simply public: staff tools use published() and see it as well.
Note:
published()filters on status only. Do not use it on customer-facing pages, or internal content will show up there.
Localization
The knowledge base can serve its articles and categories in several languages. Configure them once:
// config/knowledge-base.php 'locales' => ['en', 'de'], // languages the knowledge base serves 'default_locale' => 'en', // always the authoritative language; must be in locales 'missing_translation' => 'fallback', // 'fallback' (default) or 'hide' 'search_languages' => [ // PostgreSQL text-search configuration per language 'en' => 'english', 'de' => 'german', ],
With a single configured language the knowledge base behaves exactly as before this change.
One version per language. Each article and category has at most one version per configured language, with its own title, slug, excerpt, content and SEO fields (articles) or name, slug and description (categories). Category, visibility, related articles, author and counters are shared by every version.
$german = $article->translations()->create([ 'locale' => 'de', 'title' => 'Passwort zurücksetzen', 'slug' => 'passwort-zuruecksetzen', 'content' => 'So setzt du dein Passwort zurück.', ]);
The authoritative language. Every article's authoritative language is always default_locale, exactly as for categories and collections: it cannot be chosen or changed per article, and every article always has a version in it. That version is mirrored onto the article's own title, slug, content, excerpt and SEO columns, in both directions: writing those columns updates the authoritative version, and editing the authoritative version updates those columns. This is what keeps existing code that reads $article->title working.
$article->authoritativeLocale(); // 'en' (the configured default) $article->authoritativeTranslation()->title; // the mirrored title
Because the default language is authoritative for every article, changing default_locale later requires every article to have a version in the new language before the site can show it. Make sure the content is translated first; the built-in upgrade migration refuses to run and names the articles that are missing a default-language version.
Categories and collections always have a version in default_locale too; their main row mirrors it.
Slugs. A slug belongs to one article or category: the same article may use the same slug in several of its languages, but two different articles may never share a slug in any language. Saving a conflicting slug or a second version in the same language is rejected.
Editing a language. updateArticle() edits the authoritative version; updateArticleTranslation() edits one other language. Both record a history entry tagged with the edited language.
$service->updateArticleTranslation($article, 'de', ['title' => 'Passwort ändern'], $editor, 'Wording'); $article->versions()->forLocale('de')->get(); // the history of one language only
Outdated versions. Saving the authoritative version can mark every other version as outdated (off by default, so typo fixes flag nothing); readers still see outdated versions.
$service->updateArticle($article, $data, $editor, 'Rewrote step 2', flagOtherLocalesOutdated: true); $article->flagOtherTranslationsOutdated(); // or mark the other languages directly $translation->markUpToDate(); // editing or confirming a version clears it $service->outdatedTranslations('de'); // the outdated versions of one language
Run php artisan kb:verify-translations to report any drift between a main row and its authoritative version.
Serving a language. TranslationResolver is the one place that decides what a reader gets, applying the article's status, the visibility rules and missing_translation (fallback serves the authoritative version, hide serves nothing). It reports which language was actually served, plus that version's slug, so a page can redirect.
use JeffersonGoncalves\KnowledgeBase\Support\TranslationResolver; $resolver = app(TranslationResolver::class); $resolved = $resolver->resolveArticleBySlug('reset', 'de'); // $resolved->locale // 'de', or 'en' when it fell back // $resolved->slug // the served version's slug (may differ from 'reset') // $resolved->translation // the translation that was served $resolved = $resolver->resolveArticle($article, 'de'); $resolved = $resolver->resolveCategory($category, 'de'); $resolved = $resolver->resolveCategoryBySlug($categorySlug, 'de');
Pass publicOnly: false for staff lookups that may see internal content.
Before migrating an existing knowledge base, set
default_localeto the language the existing content is written in. The upgrade tags every existing article and category with that language, so a wrong value mislabels all existing content. The migration stops before writing anything whendefault_localeis not one oflocales.
Article Assets
Every image or file an article uses is an asset of that article. Content never stores an asset's URL: it refers to the asset by its uuid, and the core builds the URL when the content is shown, so moving the site to another domain or port, or switching disk, never breaks an image.
use JeffersonGoncalves\KnowledgeBase\Enums\AssetKind; use JeffersonGoncalves\KnowledgeBase\Support\ArticleAssets; $assets = app(ArticleAssets::class); // Store an upload (or the path to a local file) for an article. Without a // language the asset is shared; the default language is stored as shared too. $image = $assets->store($article, $request->file('image'), AssetKind::Image); $german = $assets->store($article, $request->file('image'), AssetKind::Image, 'de'); $pdf = $assets->store($article, $request->file('file'), AssetKind::File); $assets->url($image); // the current URL: plain for a public article, signed otherwise $assets->usage($image); // ['en', 'de'] — languages whose current content uses it $assets->brokenReferences($article); // ['de' => ['<uuid>']] — deleted assets still referenced $assets->delete($image); // removes the file and the record (allowed while used)
Storage layout. Each article owns one folder, named after its never-changing uuid, under {directory}/articles/{uuid}/, with a shared/ subfolder for shared assets and one subfolder per language:
knowledge-base/articles/6f1c.../shared/fern-leaf-3fa2c1.jpg
knowledge-base/articles/6f1c.../de/farn-9b04e2.jpg
A file name is the original name in readable form plus a six-character random code and the extension of the detected type. The uuid keeps the folder stable when the title or slug changes; the random code keeps names unique and URLs unguessable. Copying knowledge-base/articles/{uuid} copies everything of that article, in every language.
Allowed types and sizes. Each kind has its own accepted types and maximum size (see Attachments). The type is checked on the file's detected content, not its name, so a PDF renamed to photo.jpg is still refused as an image. image/svg+xml is not offered by default because SVG can carry scripts; add it to attachments.images.accepted_types only if you trust your editors and sanitise the output. image/avif and image/gif are opt-in the same way. Width and height are read when PHP can read the format, and left empty otherwise.
Rendering. Turn stored content into displayable HTML with resolve(). For public readers (forPublic: true) a reference to an asset that was deleted, or that belongs to another article, is left out (images) or turned into plain text (file links), so a page never shows a broken image or a dead link.
$html = $assets->resolve($translation->content, $article, forPublic: true);
Pruning. php artisan kb:prune-assets removes files under {directory}/articles/ that no asset record knows, and folders whose article no longer exists at all; it reports asset records whose file is missing and keeps unused but valid assets. Add --dry-run to list what it would remove without removing anything. A soft-deleted article keeps its folder (it can be restored); the files go when the article is force-deleted.
Serving assets
Assets are stored on a private disk and served by a route of this package, GET /knowledge-base/assets/{asset uuid}/{file name} (named knowledge-base.assets.show). It looks at the article on every request, with the same rule as everything else (see Public vs Internal Content):
- Publicly visible article: anyone can open the file.
url()returns a plain URL that never changes, so pages and CDNs can cache it; the response is cacheable forcache_max_ageseconds. - Any other article (internal, draft, archived, in an internal or inactive category, trashed): only through a signed URL that
url()makes, valid forsigned_url_minutesand rounded up to the end of the hour. Without a valid signature the response is a 404, so nobody can tell the file exists.
Because the check happens on each request, nothing moves when visibility changes: once an article or one of its categories is made internal, the plain URLs of its files stop working at the server. Copies already cached by browsers or a CDN may still be served for up to cache_max_age seconds; lower cache_max_age, or purge your CDN, when revocation must be immediate. The URL ends in the file's stored name only for readable links and downloads; the asset is found by its uuid.
Set prefix and middleware to fit your app. Set enabled to false to link to the disk's own URLs instead, as in 2.0: then the disk must be public (public with php artisan storage:link, or a public cloud disk), and so are the files of internal articles.
Upgrading from 2.0. Assets stored by 2.0 on the public disk keep working through the route straight away, but they also stay reachable at their old /storage/... address. If you published the config, set disk to local (or a private cloud disk), then run:
php artisan kb:move-assets --dry-run # lists what would move php artisan kb:move-assets # copies each file, updates its record, removes the old file
Content saved with 2.0 needs no change: links and images are recognised by asset id or path and point at the served URL when shown.
Collections
A collection is a named, ordered, translatable list of articles, such as an FAQ. The FAQ is one collection; other lists ("Getting started", …) need no schema change.
use JeffersonGoncalves\KnowledgeBase\Models\Collection; $faq = Collection::create([ 'name' => 'Frequently asked questions', 'slug' => 'frequently-asked-questions', ]); $faq->translations()->create([ 'locale' => 'de', 'name' => 'Häufige Fragen', 'slug' => 'haeufige-fragen', ]); $faq->addArticle($article); // appended at the end $faq->addArticle($other, 0); // or at a given position $faq->reorderArticles([$other, $article]); $faq->removeArticle($article); $faq->articleIds(); // the ids, in order
Like categories, a collection always has a version in default_locale (its main row mirrors it), and a slug belongs to one collection in any language. Adding an article that is already in the collection is rejected; removing it, or deleting the collection, never touches the article itself.
Editors may put any article in a collection, public or not: the public view filters at read time, so an article whose visibility (or whose category's) changes later is handled without re-saving the collection. Collection::publicArticles($locale) returns only the publicly visible articles, in order, each resolved for the language (see Localization above): under hide, an article without a readable version in that language is left out; under fallback it is served in its authoritative language, at its position. An inactive or deleted collection is not found publicly.
Help Center API
Support\HelpCenter composes the visibility and language rules for public pages, so a help page is a few lines. It is public-only by construction: every result goes through Visibility and TranslationResolver, and anything not publicly visible is reported as not found.
use JeffersonGoncalves\KnowledgeBase\Support\HelpCenter; $help = app(HelpCenter::class); $tree = $help->categories('de'); // list<CategoryNode>, with article counts $category = $help->category('abrechnung', 'de'); // ?CategoryPage $article = $help->article($slug, 'de'); // ?ArticlePage $collection = $help->collection('haeufige-fragen', 'de'); // ?CollectionPage $results = $help->search('reset', 'de', 20); // public, language-aware search results
- An
ArticlePagecarries the served version (translation), the served language (locale),isFallback(served ≠ requested),redirectSlug(set when the requested slug is not the served version's slug),alternates(locale ⇒ slug of every language the article can actually be read in),breadcrumbandrelated. - A
CategoryPagecarries the resolved category, itschildrenand itsarticles. - A
CollectionPagecarries the resolved collection and its publicarticlesin order. - A
CategoryNodecarries the resolved category, a slug and name in the served language, its public article count (articlesCount, counting only the articles a reader of that language is served) and itschildren.
A minimal public article page built on the API:
Route::get('/{locale}/help/articles/{slug}', function (string $locale, string $slug) { abort_unless(in_array($locale, config('knowledge-base.locales'), true), 404); $page = app(HelpCenter::class)->article($slug, $locale); if ($page === null) { abort(404); } if ($page->redirectSlug !== null) { return redirect("/{$page->locale}/help/articles/{$page->redirectSlug}", 301); } return view('help.article', ['page' => $page]); });
The package registers no routes or views (it stays headless); the app renders the pages, including redirects, a fallback notice when isFallback is true, hreflang alternates from alternates, and 404s.
Extending Models
Create custom models that implement the required contract:
namespace App\Models; use JeffersonGoncalves\KnowledgeBase\Models\Article as BaseArticle; use JeffersonGoncalves\KnowledgeBase\Models\Contracts\ArticleContract; class Article extends BaseArticle implements ArticleContract { // Add custom relationships, scopes, methods... public function comments() { return $this->hasMany(Comment::class); } }
Then update the config:
// config/knowledge-base.php 'models' => [ 'article' => \App\Models\Article::class, // ... ],
Events
| Event | Payload | When |
|---|---|---|
ArticleCreated |
$article |
After article creation via service |
ArticlePublished |
$article |
After publishing via service |
ArticleFeedbackReceived |
$article, $feedback |
After feedback submission |
Listen to events in your EventServiceProvider or with listeners:
use JeffersonGoncalves\KnowledgeBase\Events\ArticlePublished; class SendArticleNotification { public function handle(ArticlePublished $event): void { // Notify subscribers about the new article $article = $event->article; } }
Enums
ArticleStatus
| Value | Label (en) | Label (pt_BR) |
|---|---|---|
draft |
Draft | Rascunho |
published |
Published | Publicado |
archived |
Archived | Arquivado |
ArticleVisibility
| Value | Label (en) | Label (pt_BR) |
|---|---|---|
public |
Public | Público |
internal |
Internal | Interno |
TranslationStatus
| Value | Label (en) | Label (pt_BR) |
|---|---|---|
draft |
Draft | Rascunho |
published |
Published | Publicado |
use JeffersonGoncalves\KnowledgeBase\Enums\ArticleStatus; use JeffersonGoncalves\KnowledgeBase\Enums\ArticleVisibility; $status = ArticleStatus::Published; $status->label(); // 'Published' or 'Publicado' $visibility = ArticleVisibility::Internal; $visibility->label(); // 'Internal' or 'Interno'
Database Tables
All tables use the configured prefix (default: kb_):
| Table | Description |
|---|---|
kb_categories |
Hierarchical article categories |
kb_category_translations |
Category name/slug/description per language |
kb_articles |
Knowledge base articles |
kb_article_translations |
Article title/slug/content per language |
kb_article_versions |
Article version history, per language |
kb_article_assets |
Images and files owned by an article |
kb_article_feedback |
User feedback on articles |
kb_article_relations |
Related articles (pivot) |
kb_collections |
Curated, ordered lists of articles (e.g. an FAQ) |
kb_collection_translations |
Collection name/slug/description per language |
kb_collection_article |
Ordered membership of articles in collections (pivot) |
Testing
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.
