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

Markdown Docs Bundle Laravel Package

mobizel/markdown-docs-bundle

A Symfony bundle that turns plain Markdown files into a browsable documentation site. It handles routing, navigation menus, and Twig rendering, with configurable source paths and templates, so you can write docs in Markdown and let the bundle generate the pages.

View on GitHub
Deep Wiki
Context7

Getting Started

The first thing to establish is that mobizel/markdown-docs-bundle is a Symfony bundle, not a Laravel package. Its composer requirements pull in symfony/framework-bundle, symfony/yaml, twig/twig and league/commonmark, and it registers itself through DependencyInjection/MobizelMarkdownDocsExtension.php plus a Resources/config/services.yaml — there is no MarkdownDocsServiceProvider, no Blade views, and no config file under config/. So composer require mobizel/markdown-docs-bundle into a Laravel app gets you dead weight, not routes.

v0.13.0 does not change that premise. It is a maintenance/housekeeping release — PHP 8.1 in the test matrix, a Symfony 6 test-kernel fix, removal of yarn.lock/package.json, and one small feature ("Print action"). Nothing in it introduces a Laravel integration path.

Where to look first, before deciding anything:

  1. README.md — the config block (mobizel_markdown_docs: directories: [...]) and the default route it exposes are the whole surface area.
  2. DependencyInjection/Configuration.php — the real list of options; the README drifts from it.
  3. Model/Documentation.php and Model/Document.php — this is where the directory→tree→slug mapping lives, and it is the genuinely reusable part.
  4. Resources/views/ — the Twig layout, nav partials and (new in v0.13.0) the print template you'd mirror in Blade.
  5. composer.json — check the branch alias and Symfony constraints; v0.13.0 is the first release since April 2022, so the version range has finally moved but the project is still pre-1.0.

The realistic first use case, regardless of framework, is docs-as-code: a docs/ folder in the repo containing ADRs, onboarding notes and internal API guides, rendered at /docs with a sidebar generated from the folder tree. If you're on Symfony, the minimal path is: require the bundle, enable it in config/bundles.php, point directories at %kernel.project_dir%/docs, and hit /docs. The new print action gives you a printer-friendly rendering of the same page for free. If you're on Laravel — which is the premise here — the correct move is still to read it as a reference implementation and rebuild the ~150 lines that matter on top of league/commonmark, which you almost certainly already have transitively.

Implementation Patterns

Port the model, not the bundle. The valuable logic is: scan configured directories → build a nested tree of files → derive a slug from the relative path → render the matched file. That's a recursive Finder/glob plus a small value object. Everything else (routing, templating, DI) is framework glue you'd rewrite anyway.

A Laravel-shaped version of the core:

// app/Support/Docs/DocRepository.php
public function tree(string $root): array {
    $files = collect(File::allFiles($root))
        ->filter(fn ($f) => $f->getExtension() === 'md')
        ->map(fn ($f) => [
            'slug'  => str_replace(['\\', '.md'], ['/', ''], $f->getRelativePathname()),
            'title' => $this->titleFrom($f),
            'order' => $this->frontMatter($f)['order'] ?? 999,
        ])
        ->sortBy('order')
        ->values();
    // group by first path segment for the sidebar
}

Route it narrowly. One route with a constrained parameter beats a catch-all:

Route::get('/docs/{path?}', DocsController::class)
    ->where('path', '[A-Za-z0-9_\-\/]+')
    ->name('docs.show');

Constrain the pattern, then still resolve the path against a pre-built whitelist of known slugs rather than concatenating it onto a base directory. The bundle resolves files from a request-supplied path; that is the one place you must not copy its looseness.

Front matter for ordering and titles. The bundle leans on Symfony YAML for this. In Laravel use spatie/yaml-front-matter (or parse the --- block yourself with Symfony\Component\Yaml\Yaml, which is a tiny standalone dependency). Conventions that pay off: title, order, updated, and a folder-level index.md as the section landing page.

Render once, cache hard. Markdown parsing is the only expensive step and it's pure. Cache the HTML keyed on md5($path . filemtime($file)):

$html = Cache::remember("docs:{$slug}:".md5_file($file), now()->addDay(),
    fn () => (new CommonMarkConverter([
        'html_input' => 'strip',
        'allow_unsafe_links' => false,
    ]))->convert(Str::after($raw, "---\n".explode("---\n", $raw, 3)[1]."---\n"))
        ->getContent()
);

Configure CommonMark once, not per request. Bind a singleton converter in a service provider with the GitHub-flavoured extension set (league/commonmark's CommonMarkCoreExtension, TableExtension, AutolinkExtension, HeadingPermalinkExtension for anchor links), and add AttributesExtension if you want {#custom-id} on headings.

Templating. Blade layout + Tailwind prose classes gets you 90% of the bundle's Twig output. Use {!! $html !!} only after html_input: 'strip' — and note that strips raw HTML, so if your docs rely on <details> blocks you'll need html_input: 'escape' plus an allowlist pass.

Print view (new in v0.13.0). The bundle now exposes a print action. The cheap Laravel equivalent is a sibling route (/docs/{path}/print, or ?print=1 on the same controller) that renders the identical HTML through a layouts/print.blade.php with expanded nav, no chrome, and @media print CSS. This is the single most useful thing in the release for internal docs — authors get "save as PDF" without a second render pipeline.

Access control. Internal docs belong behind middleware: ->middleware(['auth', 'can:view-docs']). The bundle assumes a public route — including for the new print action — you don't have to.

Static export. If you'd rather not serve at runtime, an artisan command that walks docs/ and writes public/docs/*.html gives you GitHub Pages deployability and zero request-time cost. This is often the better answer than mounting a route.

Gotchas and Tips

  • Framework mismatch is the headline risk. Twig templates, services.yaml, bundle tags and Symfony's routing don't translate. Budget for a rewrite, not an integration. v0.13.0 did nothing to narrow this gap.
  • Maintenance signal — improved, not resolved. v0.13.0 is the first release since April 2022 and adds PHP 8.1 coverage plus a Symfony 6 test-kernel fix, which is genuinely better news than "abandoned". But it's still 0.x, low-star, near-zero dependents, and the release is housekeeping plus one action — not a modernisation. Treat it as "lightly alive": fine as a reference, still a risky production dependency for a Laravel app.
  • Version drift after a long gap. Jumping from a 2022 release to v0.13.0 means your composer.lock (or an old constraint) may resolve very differently than the README implies. Re-read Configuration.php against your installed version rather than trusting cached docs — options may have been added or renamed silently in the intervening commits.
  • The Symfony 6 fix is app-side, not library-side. PR #111 fixes the test app's kernel. If you were relying on the bundle in a real Symfony 6 project, verify the actual bundle code path, not just the green CI badge.
  • Path traversal. Any bundle that maps a URL segment to a filesystem path needs scrutiny. Validate against a slug whitelist derived from an actual directory scan; never realpath() a user segment and trust it. Check whether the new print action shares the same resolution code.
  • XSS in docs. Markdown is user-editable content. Default to html_input: 'strip' and allow_unsafe_links: false. Authors who need raw HTML should get an explicit, reviewed escape hatch.
  • Front matter silently poisons pages. An unbalanced --- or a bad YAML indent turns the whole block into page body or throws. Wrap the parse in a try/catch, fall back to the raw content, and log the file path.
  • Filename casing. Getting-Started.md and getting-started.md are different files on Linux and the same on macOS. Normalise slugs to lowercase on generation or your links will 404 only in production. Your print route needs the same normalisation or the two views will disagree on URLs.
  • Route ordering. A docs/{path?} route registered before other /docs/* routes swallows them — including a /docs/{path}/print route if you add one. Register the catch-all last, or scope under a prefix like /docs/{section}/{page?}.
  • filemtime on deploy. git clone sets mtimes to checkout time, so mtime-keyed caches all invalidate at once after every deploy — usually fine, but under a warm-up it means a thundering herd. Prefer hashing file contents for the cache key if docs are small.
  • Extension points worth knowing if you do stay on Symfony: override templates under templates/bundles/MobizelMarkdownDocsBundle/ (now including the print template), decorate the documentation service rather than editing the bundle, and register custom CommonMark extensions through the converter service. All three are clean; the rest of the bundle is thin enough that forking the model classes is often simpler than decorating.
  • When to skip it entirely. If your need is "show markdown on a web page," league/commonmark + a controller is a 30-minute job and has no abandonment risk. Reach for a docs bundle only when you need multi-directory trees, search, print/export and versioned sections — and at that point, compare against a static generator or a hosted docs product before committing.
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.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels