Encited logo
Pricing

How to Integrate Pre-rendering with Hapi.js

Integrate pre-rendering with Hapi.js using a request lifecycle extension that serves crawler-ready HTML before route handlers.

How Hapi pre-rendering works

Hapi.js can register an onPreHandler extension that serves crawler-ready HTML before the selected route handler executes.

The lifecycle extension handles eligible crawler HTML requests. Browser visits and render fallbacks return h.continue and proceed to the normal Hapi route.

Prerequisites

  • Node.js 18 or later for the native fetch API.
  • Access to register the Encited lifecycle extension before application routes start serving traffic.
  • LOVABLEHTML_API_KEY configured in the Node.js process environment.

Set up pre-rendering with Hapi

1

Create the Hapi extension

Save the snippet as encited-prerender.js. It uses the native fetch API available in Node.js 18 and later.

2

Register it before routes

Call registerEncitedPrerender(server) before server.route so onPreHandler can intercept crawler requests.

3

Deploy or restart Hapi

Set LOVABLEHTML_API_KEY and restart the Node.js process.

npm start
encited-prerender.js
CopyDownload
// encited-prerender.js - register before routes
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;
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 function registerEncitedPrerender(server) {
server.ext('onPreHandler', async (request, h) => {
const accept = request.headers.accept || '';
const userAgent = request.headers['user-agent'] || '';
const isHtml = !accept || accept === '*/*' || accept.includes('text/html');
if (request.method !== 'get' || !isHtml || !CRAWLER.test(userAgent)) {
return h.continue;
}
try {
const headers = {
'x-lovablehtml-api-key': process.env.LOVABLEHTML_API_KEY,
accept: 'text/html',
};
for (const name of ENCITED_FORWARD_HEADERS) {
const value = request.headers[name];
if (value) headers[name] = value;
}
const publicUrl =
request.server.info.protocol + '://' + request.info.host +
request.url.pathname + request.url.search;
const rendered = await fetch(
'https://encited.com/api/prerender/render?url=' + encodeURIComponent(publicUrl),
{ headers, redirect: 'manual' },
);
const location = rendered.headers.get('location');
if (rendered.status === 301 && location) return h.redirect(location).permanent();
if (rendered.status === 304) return h.continue;
if (rendered.status === 200 && (rendered.headers.get('content-type') || '').includes('text/html')) {
const response = h.response(Buffer.from(await rendered.arrayBuffer())).code(200).type('text/html');
for (const name of ENCITED_RESPONSE_HEADERS) {
const value = rendered.headers.get(name);
if (value) response.header(name, value);
}
return response;
}
} catch {
// Fail open.
}
return h.continue;
});
}

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

Hapi starts without registering the extension

Call registerEncitedPrerender(server) during server setup before server.start and before relying on route traffic.

Crawler requests return the normal SPA shell

Confirm the extension is registered for the relevant routes and that LOVABLEHTML_API_KEY is present in the running 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 Hapi.js?

An onPreHandler extension detects eligible crawlers, returns rendered HTML from Encited, and uses h.continue for normal requests.

When should the Hapi.js pre-rendering extension be registered?

Register it during server initialization before server.start so it applies consistently to public page routes.

Will Hapi.js API routes be sent to Encited?

No. Only eligible GET requests for HTML pages call the render API; other methods and content types continue normally.

Avatar
How can we help?
Get instant answers to your questions or leave a message for an engineer will reach out
Ask AI about Encited
See our docs
Contact support
Leave a message
We'll get back to you soon
Avatar
Ask AI about Encited
Team is also here to help
Thinking
Preview
Drop an image to attach
Powered by ReplyMaven