How Express pre-rendering works
Express.js can run Encited before application routes so crawler requests receive rendered HTML without changing responses for normal visitors.
The Express middleware handles eligible crawler GET requests for HTML. Browser visits and unsuccessful render calls continue through next() to the existing route stack.
Prerequisites
- Node.js 18 or later so the middleware can use the native fetch API.
- Access to register middleware before authentication, static files, and application routers.
- LOVABLEHTML_API_KEY configured in the production process environment.
Set up pre-rendering with Express
Create the middleware file
Save the snippet as encited-prerender.js. It uses the native fetch API available in Node.js 18 and later.
Register it before your routes
Import encitedPrerender and call app.use(encitedPrerender) before authentication, static-file, and application routes.
Deploy or restart Express
Set LOVABLEHTML_API_KEY in your production environment, then restart the process.
npm start// encited-prerender.js - register before your routesconst 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;const ENCITED_FORWARD_HEADERS = ['accept-language', 'sec-fetch-mode', 'sec-fetch-site', 'sec-fetch-dest','sec-fetch-user', 'upgrade-insecure-requests', 'referer', 'user-agent',];const ENCITED_RESPONSE_HEADERS = ['x-lovablehtml-render-cache', 'x-lovablehtml-snapshot-key', 'cache-control','etag', 'content-digest', 'signature-input', 'signature',];export async function encitedPrerender(req, res, next) {const accept = req.get('accept') || '';const isHtml = !accept || accept === '*/*' || accept.includes('text/html');if (req.method !== 'GET' || !isHtml || !CRAWLER.test(req.get('user-agent') || '')) {return next();}const publicUrl = req.protocol + '://' + req.get('host') + req.originalUrl;try {const headers = {'x-lovablehtml-api-key': process.env.LOVABLEHTML_API_KEY,accept: 'text/html',};for (const name of ENCITED_FORWARD_HEADERS) {const value = req.get(name);if (value) headers[name] = value;}const rendered = await fetch('https://encited.com/api/prerender/render?url=' + encodeURIComponent(publicUrl),{ headers, redirect: 'manual' },);if (rendered.status === 301 && rendered.headers.get('location')) {return res.redirect(301, rendered.headers.get('location'));}if (rendered.status === 304) return next();if (rendered.status === 200 && (rendered.headers.get('content-type') || '').includes('text/html')) {for (const name of ENCITED_RESPONSE_HEADERS) {const value = rendered.headers.get(name);if (value) res.set(name, value);}res.status(200).type('html').send(Buffer.from(await rendered.arrayBuffer()));return;}} catch {// Fail open: keep serving the application from your existing route.}return next();}// app.use(encitedPrerender);
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
Crawler requests reach Express routes before Encited
Register encitedPrerender with app.use before static files, authentication middleware, and every application router.
The render call returns unauthorized
Set LOVABLEHTML_API_KEY in the same production process that starts Express, then restart the process.
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 Express.js middleware?
The middleware checks public HTML GET requests, calls Encited for recognized crawlers, and calls next() for normal visitors and fallback responses.
Where should Express.js pre-rendering middleware be registered?
Register it before authentication, static-file middleware, API routers, and SPA fallback routes so eligible crawler requests are intercepted first.
Will Express.js pre-rendering affect signed-in users?
No. Browser-classified requests continue through the normal Express middleware stack, and visitor cookies are not forwarded to Encited.
