yucca/prerender-bundle
Symfony bundle that detects crawler and bot user agents and serves prerendered HTML from Prerender.io, improving SEO and social link previews for JavaScript-heavy single-page apps while normal users continue to receive the standard response.
This is a Symfony2 bundle, not a Laravel package. For a Laravel codebase the practical move is to treat it as a reference implementation: the useful part is a small request listener that detects crawlers and proxies to prerender.io. Read it, then port the logic into a Laravel middleware rather than trying to install it.
composer.json first. It targets symfony/symfony ~2.x and PHP 5.x. On Laravel 10/11 (PHP 8.1+) it will not resolve; do not --ignore-platform-reqs it into production just to try it.composer require yucca/prerender-bundle, register the bundle in AppKernel.php, then add a yucca_prerender: block to config.yml with your prerender.io token (exact key names live in DependencyInjection/Configuration.php). Default service endpoint is https://service.prerender.io/.README.md — supported config keys and the intended listener behavior.DependencyInjection/Configuration.php — every knob the bundle actually exposes.Resources/config/services.xml — listener registration and its kernel.request priority.<div id="app">. Enable the listener (or your port), then compare:
curl -s https://example.com/pricing | head # normal user
curl -s -A "Googlebot/2.1 (+http://www.google.com/bot.html)" https://example.com/pricing | head
The second should return fully rendered HTML.The core pattern (port this to Laravel). Dynamic rendering proxy on kernel.request / HTTP middleware:
GET (optionally HEAD).User-Agent against a crawler list, or detect ?_escaped_fragment_=..js, .css, .png, .json, …), excluded path prefixes, and XHR/JSON requests.https://service.prerender.io/{full_url} including scheme and host.X-Prerender-Token header, return the HTML and short-circuit the normal pipeline.Laravel middleware sketch:
public function handle($request, Closure $next)
{
if (! $request->isMethod('GET') || ! $this->isCrawler($request) || $this->isIgnored($request)) {
return $next($request);
}
$url = $request->fullUrl(); // strip _escaped_fragment_ before sending
try {
$res = Http::withHeaders(['X-Prerender-Token' => config('services.prerender.token')])
->timeout(5)
->get('https://service.prerender.io/'.$url);
if ($res->successful()) {
return response($res->body(), $res->status())
->header('Content-Type', 'text/html; charset=UTF-8')
->header('Vary', 'User-Agent');
}
} catch (\Throwable $e) {
report($e); // fall through to normal response, never 500 a bot
}
return $next($request);
}
Registration order matters. Put it in the global stack (or prepend to the web group) after TrustProxies so fullUrl() reflects the real scheme/host, but before session/auth-heavy middleware. Exclude api/*, health checks, and anything returning JSON.
Caching is the main cost control. Prerender charges per render; wrap the call:
$html = Cache::remember('prerender:'.sha1($url), now()->addHours(6), fn () => ...);
Invalidate per-URL in model observers on publish/update, or tag by content type. Also honor prerender's status-code response header convention (it often returns HTTP 200 and puts the real status in a header) so 404s are seen as 404s by crawlers.
Bot detection. Reuse prerender's recommended UA regex (Googlebot, bingbot, Yandex, Baiduspider, facebookexternalhit, Twitterbot, Slackbot, etc.) and support _escaped_fragment_ for the deprecated AJAX crawling scheme. Log every match at debug level — it makes "why is Google seeing this?" trivial to answer.
Self-hosted alternative. The same API contract is served by prerender/prerender in Docker; point your service URL at it. Put the service URL and token behind config so you can switch without touching middleware.
Symfony-side extension tips (if you actually maintain one). The listener is a normal service: override it to customize UA lists or the client, and raise its kernel.request priority if it is firing after routing/firewall. Because it returns a response from kernel.request, the controller, session, and auth never run — which is the point, but also means no user-specific content.
Vary: User-Agent (or a separate cache key for crawlers), a CDN can serve prerendered HTML to humans or the SPA shell to Googlebot. Set Vary and prefer short TTLs or private caching for prerendered responses.$next($request). Set a 3–5s timeout; a bot waiting 30s on a hung proxy is worse than an unrendered page._escaped_fragment_ back to the #! URL, and do not forward tracking query params you don't need — they create duplicate cache entries and duplicate renders.Accept-header filtering you will pay to render .js, .css, and API responses, which returns garbage and pollutes your cache.PRERENDER_TOKEN in env/secrets, send it as a header only. Never put it in a URL or commit it to config.yml.services.xml and bump it.curl -s -H "X-Prerender-Token: $TOKEN" \
"https://service.prerender.io/https://example.com/pricing"
If that's fine but the app isn't, the bug is in UA matching or skip rules, not prerender.How can I help you explore Laravel packages today?