moneo / markdown-for-agents
Laravel package for Cloudflare's Markdown conversion services.
Requires
- php: ^8.2
- guzzlehttp/guzzle: ^7.0
- illuminate/contracts: ^11.0|^12.0
- illuminate/http: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
- league/html-to-markdown: ^5.0
Requires (Dev)
- laravel/pint: ^1.0
- orchestra/testbench: ^9.0|^10.0
- phpstan/phpstan: ^1.0
- phpunit/phpunit: ^11.0
README
A Laravel package that unifies Cloudflare's three Markdown conversion services under one elegant API.
Services
| Service | Driver Key | Description |
|---|---|---|
| Markdown for Agents | agents |
Content negotiation via Accept: text/markdown on any Cloudflare-enabled URL. Free, no auth. |
| Workers AI toMarkdown | workers_ai |
Converts files (PDF, DOCX, images, etc.) to Markdown via REST API. Auth required. |
| Browser Rendering | browser |
Headless browser renders JS-heavy pages then converts to Markdown. Auth required. |
Requirements
- PHP 8.2+
- Laravel 11.x or 12.x
Installation
composer require moneo/markdown-for-agents
Publish the config file:
php artisan vendor:publish --tag=markdown-for-agents-config
Configuration
Add your Cloudflare credentials to .env:
# Required for workers_ai and browser drivers
CF_ACCOUNT_ID=your-account-id
CF_API_TOKEN=your-api-token
# Optional overrides
MFA_DRIVER=agents
MFA_CACHE=true
MFA_CACHE_STORE=redis
MFA_CACHE_TTL=3600
The agents driver does not require credentials.
Usage
Convert a URL
use Moneo\MarkdownForAgents\Facades\MarkdownForAgents;
$result = MarkdownForAgents::url('https://example.com')->convert();
$result->markdown; // string — the converted content
$result->tokens; // int — estimated token count
$result->contentSignals; // ?array — ['ai-train' => 'yes', ...]
$result->driver; // string — which driver was used
$result->fromCache; // bool — served from cache?
Convert a file
// Single file
$result = MarkdownForAgents::file('/path/to/document.pdf')->convert();
// Laravel UploadedFile
$result = MarkdownForAgents::file($request->file('document'))->convert();
// Batch conversion
$results = MarkdownForAgents::files([$pdf, $image, $spreadsheet])->convert();
foreach ($results as $result) {
echo "{$result->name}: {$result->tokens} tokens\n";
}
Convert raw HTML
$result = MarkdownForAgents::driver('browser')
->html('<div><h1>Hello</h1><p>World</p></div>')
->convert();
Choose a driver
// Use the browser driver for JS-heavy pages
$result = MarkdownForAgents::driver('browser')
->url('https://spa-app.com')
->waitUntil('networkidle0')
->convert();
Fallback
If the primary driver fails, automatically retry with another:
$result = MarkdownForAgents::url('https://example.com')
->withFallback('browser')
->convert();
Browser options
$result = MarkdownForAgents::driver('browser')
->url('https://example.com')
->waitUntil('networkidle0')
->userAgent('MyBot/1.0')
->rejectPatterns(['/^.*\.(css|font)/'])
->cookies([['name' => 'session', 'value' => 'abc', 'domain' => '.example.com']])
->authenticate('user', 'pass')
->convert();
Cache control
Caching is enabled by default for URL and HTML conversions:
// Skip cache for this request
$result = MarkdownForAgents::url($url)->noCache()->convert();
// Custom TTL (seconds)
$result = MarkdownForAgents::url($url)->cache(7200)->convert();
// Clear cache
MarkdownForAgents::clearCache('https://example.com');
MarkdownForAgents::flushCache();
Conditional fluent API
PendingConversion uses Laravel's Conditionable and Tappable traits:
$result = MarkdownForAgents::url($url)
->when($useBrowser, fn ($c) => $c->waitUntil('networkidle0'))
->unless($isAdmin, fn ($c) => $c->noCache())
->convert();
Supported formats
$formats = MarkdownForAgents::supportedFormats();
// Returns SupportedFormat[] — delegates to Workers AI
Driver Comparison
| Scenario | Recommended Driver |
|---|---|
| Cloudflare-enabled site | agents (fastest, free) |
| PDF, DOCX, XLSX, images | workers_ai (only option) |
| SPA / JS-heavy page | browser (full render) |
| Non-Cloudflare site | browser |
| Raw HTML string | browser |
| Make your app agent-ready | Middleware (local, no API) |
Middleware
Add the middleware to routes to make your Laravel app respond with Markdown when AI agents request it:
Route::get('/blog/{slug}', [BlogController::class, 'show'])
->middleware('markdown-for-agents');
When a request includes Accept: text/markdown, the middleware converts HTML responses to Markdown locally using league/html-to-markdown. No Cloudflare API calls are made.
Response headers added:
Content-Type: text/markdown; charset=utf-8Vary: acceptx-markdown-tokens: {count}Content-Signal: ai-train=yes, search=yes, ai-input=yes
Artisan Commands
# Convert a URL
php artisan markdown:convert https://example.com
php artisan markdown:convert https://example.com --driver=browser
php artisan markdown:convert https://example.com --save=output.md
# Convert a file
php artisan markdown:convert /path/to/document.pdf
# List supported formats
php artisan markdown:formats
# Clear cache
php artisan markdown:cache:clear
php artisan markdown:cache:clear --url=https://example.com
Events
Two events are dispatched during conversions:
MarkdownConverted— on success (includes result, source, duration)ConversionFailed— on failure (includes source, driver, exception)
use Moneo\MarkdownForAgents\Events\MarkdownConverted;
Event::listen(MarkdownConverted::class, function (MarkdownConverted $event) {
Log::info("Converted {$event->source} in {$event->duration}s");
});
Custom Drivers
Register custom drivers that implement MarkdownConverterInterface:
use Moneo\MarkdownForAgents\Facades\MarkdownForAgents;
MarkdownForAgents::extend('custom', function ($app) {
return new MyCustomDriver($app['config']);
});
$result = MarkdownForAgents::driver('custom')->url($url)->convert();
Testing
The package uses orchestra/testbench. All tests mock HTTP responses -- no real API calls.
composer test
When testing your own app's code that uses this package, mock the facade:
use Moneo\MarkdownForAgents\Facades\MarkdownForAgents;
use Moneo\MarkdownForAgents\DTOs\ConversionResult;
MarkdownForAgents::shouldReceive('url->convert')
->andReturn(new ConversionResult(...));
License
MIT