spatie/laravel-markdown-response
Serve clean markdown versions of your Laravel HTML pages for AI agents and bots. Detects requests via Accept: text/markdown, known user agents, or .md URLs. Driver-based conversion (local PHP or Cloudflare Workers AI), caching, and HTML preprocessing included.
Install the package:
composer require spatie/laravel-markdown-response
Publish the config (optional):
php artisan vendor:publish --provider="Spatie\MarkdownResponse\MarkdownResponseServiceProvider"
Apply middleware to routes in routes/web.php:
use Spatie\MarkdownResponse\Middleware\ProvideMarkdownResponse;
Route::middleware(ProvideMarkdownResponse::class)->group(function () {
Route::get('/about', [PageController::class, 'show']);
});
Test immediately:
/about.md or set Accept: text/markdown in headers.GPTBot) will auto-trigger conversion.Convert a static documentation page to markdown for AI crawlers:
// routes/web.php
Route::get('/docs/{slug}', [DocsController::class, 'show'])
->middleware(ProvideMarkdownResponse::class);
Result: /docs/getting-started.md returns clean markdown for AI agents.
/docs, /blog).bootstrap/app.php for app-wide coverage:
->withMiddleware(function (Middleware $middleware) {
$middleware->append(ProvideMarkdownResponse::class);
});
DoNotProvideMarkdownResponse middleware for sensitive routes (e.g., /dashboard).#[ProvideMarkdown] on controllers/methods:
use Spatie\MarkdownResponse\Attributes\ProvideMarkdown;
#[ProvideMarkdown]
public function show() { ... }
#[DoNotProvideMarkdown] to opt-out:
#[DoNotProvideMarkdown]
public function admin() { ... }
Convert HTML strings programmatically (e.g., in jobs or commands):
use Spatie\MarkdownResponse\Facades\Markdown;
$markdown = Markdown::convert($htmlString);
MARKDOWN_RESPONSE_DRIVER=league
MARKDOWN_RESPONSE_DRIVER=cloudflare
CLOUDFLARE_ACCOUNT_ID=your_id
CLOUDFLARE_API_TOKEN=your_token
MarkdownDriver interface for specialized needs (e.g., Pandoc).MARKDOWN_RESPONSE_CACHE_ENABLED.MARKDOWN_RESPONSE_CACHE_TTL (default: 3600s).MARKDOWN_RESPONSE_CACHE_STORE=redis
php artisan markdown-response:clear.use Spatie\MarkdownResponse\Facades\Markdown;
beforeEach(function () {
Markdown::fake();
});
it('converts to markdown', function () {
$this->get('/about.md')->assertOk();
Markdown::assertConverted();
});
Cache Key Collisions:
/about?utm_source=x and /about share the same cache key by default.ignored_query_parameters in config/markdown-response.php or extend GeneratesCacheKey.Non-HTML Responses:
404 pages won’t convert. Ensure your routes return text/html responses.#[DoNotProvideMarkdown] for API routes.JavaScript Rendering:
Attribute Precedence:
#[ProvideMarkdown]
class PageController {
#[DoNotProvideMarkdown]
public function sensitive() { ... } // Won't convert
}
Performance:
MARKDOWN_RESPONSE_CACHE_ENABLED=false) for testing, but re-enable in production.MarkdownDriver wrapper to log HTML inputs/outputs:
public function convert(string $html): string {
\Log::debug('Markdown conversion input:', ['html' => substr($html, 0, 500)]);
return parent::convert($html);
}
dd($request->headers) to verify Accept: text/markdown or AI user agents.GPTBot.Preprocess HTML:
use Spatie\MarkdownResponse\Facades\Markdown;
$cleanHtml = preg_replace('/<nav[^>]*>.*<\/nav>/', '', $html);
$markdown = Markdown::convert($cleanHtml);
Markdown::convert() in a service provider.Custom Cache Keys:
GeneratesCacheKey to include dynamic segments (e.g., locale):
public function __invoke(Request $request): string {
return "markdown:{$request->locale}:{$request->path}";
}
Driver-Specific Options:
'driver_options' => [
'league' => [
'options' => [
'strip_tags' => ['script', 'style'], // Remove unwanted tags
],
],
],
Post-Conversion Processing:
use Spatie\MarkdownResponse\Facades\Markdown;
$markdown = Markdown::convert($html)
->replace('# Title', '## Modified Title');
/admin*:
Route::middleware(ProvideMarkdownResponse::class)
->where('path', '!=admin*')
->group(...);
.md URLs for public docs and Accept headers for AI agents:
Route::get('/docs/{slug}.md', [DocsController::class, 'show'])
->middleware(ProvideMarkdownResponse::class);
public function handle(Request $request, Closure $next) {
if ($request->wantsMarkdown()) {
\Log::info('Markdown request', ['path' => $request->path()]);
}
return $next($request);
}
try-catch in middleware to return HTML if conversion fails:
try {
return $this->convertResponse($response);
} catch (\Exception $e) {
\Log::error($e);
return $response; // Fallback to HTML
}
How can I help you explore Laravel packages today?