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

Gitdown Laravel Package

calebporzio/gitdown

Gitdown is a Laravel/PHP package that converts Markdown into HTML, giving developers a simple way to render READMEs, documentation, and other Markdown content directly in their applications. It integrates cleanly with Laravel and produces browser-ready output.

View on GitHub
Deep Wiki
Context7

Getting Started

Install, then verify what actually exists in vendor/ before trusting anything.

composer require calebporzio/gitdown

The published description and the actual source tell two different stories, and the difference determines your entire day one. The README pitches a database-free blog driven by GitHub Issues (fetch issues, filter by label, cache API responses, render bodies). The assessment of the source says the package is roughly ten lines delegating to cebe/markdown's GithubMarkdown::parse(). Those cannot both be true of the same release. Resolve it in the first five minutes:

ls vendor/calebporzio/gitdown/src/
cat vendor/calebporzio/gitdown/composer.json

Whichever it is, you now know your dependency. If it's the ten-line parser: no service provider, no facade registration, no publishable config — just a static method. If it's the issue-fetcher: find the config file, the API client, and the cache keys, because those are your operational surface (tokens, rate limits, stale content).

Establish determinism immediately. This is the single most important fact to lock down, because it decides whether the package is a pure function or a network adapter:

// tinker
GitDown::parse("# Hello\n\n| a | b |\n|---|---|\n| 1 | 2 |");

Run it once normally, then again with the network cut and any GITHUB_TOKEN unset. Identical HTML both times means a local, deterministic, rate-limit-free renderer — the good outcome. Different output or an exception means you're holding a network client, and every page view is now coupled to GitHub's availability and your quota. That changes caching from "nice to have" to "mandatory before you ship."

Your first real use case is pre-rendering in the controller, not the view.

return view('docs.show', [
    'html' => GitDown::parse(Storage::get('docs/getting-started.md')),
]);
<article class="prose">{!! $html !!}</article>

Do this once, then immediately do the thing you'll wish you'd done first: wrap it behind your own contract. Rendering inline in a Blade template is how a ten-line dependency becomes fifty call sites you can't migrate.

Read the transitive dependency, not the wrapper. composer show cebe/markdown — that's where the actual parsing behavior, the GFM version, and the abandonment risk live. The wrapper is never the thing that breaks.

Implementation Patterns

Bind behind your own contract on day one, not later. GitDown's only real advantage is that it's thin enough to be replaceable. Preserve that by making sure no controller, view, or service ever names the package directly.

// app/Markdown/MarkdownRenderer.php
interface MarkdownRenderer
{
    public function toHtml(string $markdown): string;
}
// app/Markdown/GitDownRenderer.php
final class GitDownRenderer implements MarkdownRenderer
{
    public function toHtml(string $markdown): string
    {
        return GitDown::parse($markdown);
    }
}
// AppServiceProvider::register()
$this->app->bind(MarkdownRenderer::class, GitDownRenderer::class);

Migrating to league/commonmark plus its GFM extension later becomes one new class and one line in the provider. Without this seam, every GitDown::parse() call site is a migration ticket — and given the package's status, migration is a when, not an if.

Cache on the content hash, not the model. Parsing is cheap per-call but wasteful per-page if you're rendering fifty comment bodies on every request for content that changes weekly.

public function toHtml(string $markdown): string
{
    return Cache::remember(
        'md:' . hash('xxh128', $markdown) . ':gitdown-1.0',
        now()->addDay(),
        fn () => GitDown::parse($markdown)
    );
}

Include a renderer version string in the key so an engine swap or a fork doesn't serve stale HTML. If you went the issue-blog route instead, the same rule applies one level up: cache the API response keyed on repo, label filter, and a TTL, and never let a page render wait on a live GitHub call.

Pick render-on-read or render-on-write and stay consistent. For user content — comments, tickets, pasted text, model output — store raw Markdown and render on read. That way you can re-render the entire corpus when you change engines, which you will. Storing a rendered HTML column only pays off when writes are rare and reads are constant; if you do it, store the source column alongside it or you've made the migration unrecoverable.

Sanitize in a decorator, not in templates. GFM permits raw inline and block HTML, and cebe/markdown passes it through — that's spec-compliant behavior, not a bug. For trusted content (staff docs, seeded fixtures, your own README) {!! $html !!} is fine. For anything user-supplied or model-generated, wrap the renderer:

final class SanitizingRenderer implements MarkdownRenderer
{
    public function __construct(private MarkdownRenderer $inner) {}

    public function toHtml(string $markdown): string
    {
        return Purifier::clean($this->inner->toHtml($markdown), 'markdown');
    }
}

One chokepoint, one place to audit, applies to every render path automatically. An archived renderer receives no security patches; the sanitizer is your compensation.

Lean on GFM fidelity where it's the actual requirement. Tables, fenced code, strikethrough, and task lists all render. That's the day-to-day value: user-authored content looks like the GitHub text the author typed. Fenced blocks come out as <pre><code class="language-php"> — pair them with highlight.js or Prism on the front end, because the package does no syntax highlighting.

Test with golden files, not per-tag assertions. Snapshot rendered HTML for a corpus of your nastiest real content — wide tables, nested lists, fences containing backticks and HTML, task lists.

public function test_renders_gfm_fixtures(): void
{
    foreach (glob(base_path('tests/fixtures/markdown/*.md')) as $file) {
        $expected = file_get_contents(str_replace('.md', '.html', $file));

        $this->assertSame($expected, app(MarkdownRenderer::class)->toHtml(file_get_contents($file)));
    }
}

This gives you a diffable baseline and turns the eventual engine swap into an output comparison you can review, rather than a leap of faith.

Where this fits comfortably day-to-day: internal wikis, admin previews of user input, changelogs and release notes, seeded demo content, CLI and build-step rendering where you need Markdown → HTML without a browser or a network call. Low-trust-exposure surfaces where the dependency's age isn't a live risk.

Gotchas and Tips

The package is archived; the transitive parser is the real dependency. GitDown is a thin wrapper over cebe/markdown, which shares the abandonment problem, and that's the tree your security review will object to — and the thing you cannot cleanly patch. Pin exact versions, commit the lock file, and accept that you are now maintainer of record for the whole path.

XSS is the failure mode that becomes an incident. The renderer is doing exactly what GFM specifies when it passes raw HTML through. The bug is always in an integration that skipped sanitization. Decide up front which render paths take untrusted input and enforce it in the decorator, not in individual Blade files — one missed {!! !!} is all it takes.

No config file, no publishable assets, no facade to hunt for. If the source is the ten-line parser, stop looking for config/gitdown.php. The extension point is the package boundary — your wrapper class. Need different behavior? Change the wrapper or replace the engine. Don't fork ten lines to bolt on a withX() method; you've just created a private dependency with none of the upstream benefits.

GFM has moved since 2019. Expect visible diffs against what GitHub renders today: no footnote syntax, no > [!NOTE] alert blocks, differing autolink behavior, edge cases in table parsing. If your product promise is "looks exactly like GitHub," you'll be explaining these diffs to users forever. If it's "renders Markdown nicely," you're fine.

Task lists render as disabled checkboxes. They inherit browser styling and aren't interactive. If users need to tick boxes, that's not the parser's job — parse the list items yourself or override the renderer.

Indentation is the most common support ticket. Four leading spaces is a code block, not a nested paragraph, and a tab is more aggressive still. This bites hardest with content pasted from rich editors or from AI output that indents for readability. When pasted content "looks broken," check leading whitespace before you blame the parser.

A table without a delimiter row silently isn't a table. |---|---| under the header is mandatory; omit it and you get a paragraph of pipes. Users paste tables missing it routinely — worth a preview pane or a lint pass.

Run it in CI on your target PHP version before committing to it. A 2019 cebe/markdown on PHP 8.2+ is exactly where deprecation notices and deprecation-to-exception conversions surface. Catching this on a branch is cheap; catching it after three features are built on the integration is not.

If you're on the issue-blog variant, cache unconditionally and handle failure paths. Unauthenticated GitHub API calls are rate-limited hard, tokens expire, and issues get edited or deleted from under you. Treat the API as a slow, unreliable origin: cache the response, degrade to the cached copy on error, and never let page render block on it.

Keep the exit ramp warm. When you migrate to league/commonmark + GFM, implement the same MarkdownRenderer contract, bind it, and run both renderers in shadow mode over your fixture corpus. Diff the HTML. Let the quantified differences — not the dependency's reputation — decide whether any divergence matters to your product. In most cases it won't, and the swap is an afternoon.

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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo