How Laravel pre-rendering works
Laravel can prepend Encited to its middleware pipeline so crawler requests receive rendered HTML before route middleware and controllers execute.
The Laravel middleware calls Encited only for eligible crawler HTML GET requests. Visitors and failed render calls continue through the normal request handler.
Prerequisites
- A Laravel application with access to bootstrap/app.php middleware configuration.
- The built-in Laravel HTTP client available in the deployed application.
- LOVABLEHTML_API_KEY set in the production environment.
Set up pre-rendering with Laravel
Create the Laravel middleware
Save the class at app/Http/Middleware/EncitedPrerender.php. It uses Laravel's built-in HTTP client.
Register it before route middleware
Prepend EncitedPrerender in bootstrap/app.php so public crawler GET requests are intercepted first.
Deploy Laravel
Set LOVABLEHTML_API_KEY in the production environment, clear cached configuration, and deploy.
php artisan config:clear<?php// app/Http/Middleware/EncitedPrerender.phpnamespace App\Http\Middleware;use Closure;use Illuminate\Http\Request;use Illuminate\Support\Facades\Http;class EncitedPrerender{private const CRAWLER = '/(?:(OAI-SearchBot|ChatGPT-User(?:\/\d+\.\d+)?|GPTBot|ChatGPT|OpenAI-))|(?:(anthropic-ai|ClaudeBot|claude-web|Claude-Web|Claude-User|Claude-SearchBot))|(?:(GrokBot|xAI-Grok))|(?:(PerplexityBot|Perplexity-User))|(?:(Google-InspectionTool|GoogleOther(?:-Image|-Video)?|Googlebot|Google-CloudVertexBot|Storebot-Google))|(?:(Microsoft-Copilot|CopilotBot|Copilot-Chat|Microsoft-CopilotPlugin))|(?:(BingBot|bingbot|BingPreview))|(?:facebookexternalhit.*Twitterbot|Twitterbot.*facebookexternalhit)|(?:(Meta-ExternalAgent|meta-externalagent|meta-externalfetcher|MetaBot|FacebookBot|facebookexternalhit))|(?:LinkedInBot)|(?:Amazonbot)|(?:(Applebot-Extended|Applebot))|(?:Bytespider)|(?:(DuckAssistBot|DuckDuckBot))|(?:(cohere-training-data-crawler|cohere-ai|CohereAI))|(?:MistralAI-User)|(?:AI2Bot)|(?:CCBot)|(?:Diffbot)|(?:omgili)|(?:TimpiBot)|(?:YouBot)|(?:(Bravebot|Brave-Search))|(?:Yandex)|(?:Twitterbot)|(?:Discordbot)|(?:(Slackbot|Slack-ImgProxy))|(?:TelegramBot)|(?:(Pinterest|Pinterestbot))|(?:Snapchat)|(?:Redditbot)|(?:Tumblr)|(?:(Mastodon|http\.rb))|(?:Viber)|(?:^Line\/)|(?:WhatsApp)|(?:(SkypeUriPreview|Skype))|(?:(Teams|MicrosoftPreview))|(?:Zoom)|(?:Notion)|(?:(Quora|QuoraBot|Quora-Bot|Poe-Bot))|(?:Medium)|(?:(Pocket|PocketParser|PocketImageCache))|(?:(Flipboard|FlipboardProxy))|(?:(Embedly|embed\.ly))|(?:(Baiduspider|Baidu))|(?:(Sogou|sogou))|(?:(Yeti|NaverBot|Naver))|(?:SeznamBot)|(?:(Qwantify|Qwant))|(?:Ecosia)|(?:MojeekBot)|(?:Mail\.RU_Bot)|(?:Yahoo! Slurp)|(?:(SemrushBot|SEMrush))|(?:(AhrefsBot|AhrefsSiteAudit))|(?:(DotBot|Rogerbot|moz\.com))|(?:MJ12bot)|(?:Screaming Frog)|(?:SISTRIX)|(?:SEOkicks)|(?:serpstatbot)|(?:RSiteAuditor)|(?:LvHTML-SEOAuditBot)|(?:Barkrowler)|(?:HubSpot Crawler)|(?:(AwarioSmartBot|AwarioBot))/i';private const FORWARD_HEADERS = ['accept-language', 'sec-fetch-mode', 'sec-fetch-site', 'sec-fetch-dest','sec-fetch-user', 'upgrade-insecure-requests', 'referer', 'user-agent',];private const RESPONSE_HEADERS = ['x-lovablehtml-render-cache', 'x-lovablehtml-snapshot-key', 'cache-control','etag', 'content-digest', 'signature-input', 'signature',];public function handle(Request $request, Closure $next){$accept = $request->header('accept', '');$userAgent = $request->userAgent() ?? '';$isHtml = $accept === '' || $accept === '*/*' || str_contains($accept, 'text/html');if (!$request->isMethod('GET') || !$isHtml || !preg_match(self::CRAWLER, $userAgent)) {return $next($request);}try {$headers = ['x-lovablehtml-api-key' => (string) getenv('LOVABLEHTML_API_KEY'),'accept' => 'text/html',];foreach (self::FORWARD_HEADERS as $name) {$value = $request->header($name);if ($value) $headers[$name] = $value;}$rendered = Http::timeout(30)->connectTimeout(2)->withoutRedirecting()->withHeaders($headers)->get('https://encited.com/api/prerender/render', ['url' => $request->fullUrl(),]);if ($rendered->status() === 301 && $rendered->header('location')) {return redirect()->away($rendered->header('location'), 301);}if ($rendered->status() === 304) return $next($request);$contentType = $rendered->header('content-type') ?? '';if ($rendered->status() === 200 && str_contains($contentType, 'text/html')) {$responseHeaders = ['content-type' => 'text/html; charset=utf-8'];foreach (self::RESPONSE_HEADERS as $name) {$value = $rendered->header($name);if ($value) $responseHeaders[$name] = $value;}return response($rendered->body(), 200, $responseHeaders);}} catch (\Throwable $error) {// Fail open.}return $next($request);}}// Register before route/auth middleware in bootstrap/app.php.
Test the integration
1. Send a crawler request
curl -sS -D - -o /dev/null \
-A "Googlebot" \
-H "Accept: text/html" \
"https://your-domain.com/"Look for a successful HTML response and the x-lovablehtml-render-cache header.
2. Confirm browser passthrough
curl -sS -D - -o /dev/null \
-A "Mozilla/5.0" \
-H "Accept: text/html" \
-H "Accept-Language: en-US" \
-H "Sec-Fetch-Mode: navigate" \
-H "Sec-Fetch-Dest: document" \
-H "Upgrade-Insecure-Requests: 1" \
"https://your-domain.com/"This request should be served by your existing website without an Encited render-cache header.
Common errors
Laravel still uses an old or missing API key
Update the production environment, run php artisan config:clear, and restart long-running queue or application workers.
Crawler requests are redirected by route middleware
Prepend EncitedPrerender in bootstrap/app.php so it runs before authentication and route middleware.
Request and security behavior
- Only eligible GET requests for public HTML pages are considered for pre-rendering.
- Along with the public page URL, only the API key and crawler-classification headers are sent to Encited.
- Cookie and Authorization headers are never forwarded to Encited.
- If Encited is unavailable or does not return rendered HTML, the request falls through to your website.
Common questions
How does pre-rendering work with Laravel middleware?
Laravel middleware identifies eligible crawler requests, returns rendered HTML from Encited, and passes normal browser traffic to the application.
Where should the Laravel pre-rendering middleware be registered?
Prepend it in bootstrap/app.php so it executes before authentication, redirects, and route-specific middleware.
How do I verify Laravel crawler pre-rendering?
Send a Googlebot HTML request to a public route and confirm that the response includes x-lovablehtml-render-cache.
