gigadrive/markdown-wiki-bundle
A Symfony bundle that turns Markdown files into a browsable wiki, handling routing, parsing, and templating for you. Drop .md docs into a folder and get a navigable knowledge base that fits into existing Symfony apps. Lightweight and easy to set up, though docs and community support are limited.
Reality check before you install anything. gigadrive/markdown-wiki-bundle is a Symfony 5 bundle — it ships a Bundle class, a DI extension, service definitions in Resources/config, and Twig templates in Resources/views. It is not a Laravel package, and composer require gigadrive/markdown-wiki-bundle in a Laravel app will install dependencies you never boot. It is also archived (last release 2022-03-16, 0 dependents, 3 stars), so treat it as a reference implementation, not a dependency.
If you are actually in a Symfony 5 app (the only supported path):
composer require gigadrive/markdown-wiki-bundleGigadrive\MarkdownWikiBundle\MarkdownWikiBundle in config/bundles.php.md filesIf you are in Laravel (the likely case), don't install it. Spend 15 minutes reading the source instead:
README.md — config keys and route shapesrc/ — the controller and the service that walks the content directory and converts MarkdownResources/config/ — what the service actually needs injected (this tells you how thin the wrapper is)Resources/views/ — the page/index template structure, which maps almost 1:1 to BladeThen reimplement the ~100 lines of logic with league/commonmark (the same underlying converter most of these bundles use) plus a controller and a Blade view. That is the fastest path to a working help/wiki section in Laravel.
First use case: a resources/docs/*.md tree rendered at /help/{page}, with /help as an auto-generated index. Drop three markdown files in, get a routable, version-controlled help section with zero database.
Content-as-files, slug-as-route. The bundle's core pattern is a catch-all route mapping a slug to a file path. In Laravel:
Route::get('/help/{page?}', HelpController::class)
->where('page', '[A-Za-z0-9\-_/]+')
->name('help');
The where constraint is not optional — it's your path-traversal defense (see Gotchas).
Resolve, don't file_get_contents blindly. Build an explicit resolver that maps a slug to a path under a fixed root, normalizes it, and asserts the result is still inside the root before reading. This is the one piece of logic worth copying faithfully from the bundle's source.
Cache the parsed HTML, keyed by mtime. Markdown parsing on every request is wasteful and the nav/index scan is worse. In Laravel:
$key = "help:{$page}:" . filemtime($path);
return Cache::rememberForever($key, fn () => $converter->convert(file_get_contents($path))->getContent());
The mtime in the key gives you automatic invalidation on edit without a deploy step.
Build the sidebar from the directory tree once, cache it separately. The bundle's index view enumerates files; do the same in a HelpIndex service and cache the resulting nav array. Recompute it when any file's mtime changes, or just on a short TTL (Cache::remember(..., 300, ...)) — docs change rarely and a 5-minute stale nav is harmless.
Render with {!! !!} and a hardened converter. Configure CommonMark with html_input => 'strip' and allow_unsafe_links => false unless you fully trust every author. If you need raw HTML in docs, swap strip for an allowlist sanitizer, not 'allow'.
Blade over Twig. The bundle's Twig templates are simple enough to transliterate: one layout, one page template, one index template. Keep the same variables (content, title, nav) so the mental model transfers.
Integration points that pay off day-to-day:
auth middleware for internal wikis; leave public help docs open.throttle: if docs are public — file reads plus parsing are cheap but not free./sitemap.xml entries from the same index service.where-filtered array — you almost certainly don't need a search engine for a few hundred pages..., leading /, null bytes, and backslashes explicitly; realpath() + str_starts_with($resolved, $root) is the reliable check. Don't rely on the route regex alone if slugs can contain /.html_input deliberately and test with a <script> in a scratch file..md extension assumption in the resolver.league/commonmark directly (best control), spatie/laravel-markdown (caching + Blade components), or a static docs generator if the wiki doesn't need runtime rendering at all.How can I help you explore Laravel packages today?