Encited

How to Integrate Pre-rendering with Laravel

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#

1. Create the Laravel middleware#

Save the class at app/Http/Middleware/EncitedPrerender.php. It uses Laravel's built-in HTTP client.

EncitedPrerender

<?php
// app/Http/Middleware/EncitedPrerender.php

namespace 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))|(?:\b(GrokBot|xAI-Grok|xAI-Bot|xAI-SearchBot)\b)|(?:(PerplexityBot|Perplexity-User))|(?:(Google-InspectionTool|GoogleOther(?:-Image|-Video)?|Googlebot|Google-CloudVertexBot|Storebot-Google|Google-NotebookLM|GoogleAgent-URLContext|GoogleAIOverviewRenderer|Google-Lens|Google-Jetski-Antigravity))|(?:(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|AppleNewsBot))|(?:Bytespider)|(?:(DuckAssistBot|DuckDuckBot))|(?:(cohere-training-data-crawler|cohere-ai|CohereAI))|(?:MistralAI-User)|(?:AI2Bot)|(?:CCBot)|(?:Diffbot)|(?:omgili)|(?:TimpiBot)|(?:YouBot)|(?:ExaSearchBot)|(?:(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))|(?:YelpBot)|(?:(Baiduspider|Baidu))|(?:(Sogou|sogou))|(?:(Yeti|NaverBot|Naver))|(?:SeznamBot)|(?:(Qwantify|Qwant))|(?:Ecosia)|(?:MojeekBot)|(?:Mail\.RU_Bot)|(?:(Yahoo! Slurp|Y!J-))|(?:(SemrushBot|SEMrush))|(?:(AhrefsBot|AhrefsSiteAudit))|(?:(DotBot|Rogerbot|moz\.com))|(?:MJ12bot)|(?:Screaming Frog)|(?:SISTRIX)|(?:SEOkicks)|(?:serpstatbot)|(?:RSiteAuditor)|(?:(?:Encited|LvHTML)-SEOAuditBot)|(?:Barkrowler)|(?:HubSpot Crawler)|(?:(AwarioSmartBot|AwarioBot))|(?:DataForSeoBot)|(?:SERanking)|(?:Seobility)/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 = [
                // Set the LOVABLEHTML_API_KEY environment variable before deploying.
                '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.

2. Register it before route middleware#

Prepend EncitedPrerender in bootstrap/app.php so public crawler GET requests are intercepted first.

3. Deploy Laravel#

Clear cached configuration and deploy.

php artisan config:clear

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.

If your site sits behind a firewall, bot protection, or rate limiting, those rules block us before this middleware helps. Set a render token and add one firewall rule so renders and audit crawls get through.

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.