Encited logo
Pricing

How to Integrate Pre-rendering with Vercel Middleware

Integrate pre-rendering with Vercel Middleware and route eligible crawler GET requests to Encited without changing normal visitor traffic.

How it works

Your edge middleware/worker calls /api/prerender/render?url=... for HTML document requests. If Encited returns 200 you serve rendered HTML. If it returns 304 with a Location header, prerendering didn't apply — fall back to your origin SPA.

Prerequisites

  • An Encited account and API key (Settings → API Keys)
  • A domain added + verified in your Encited dashboard
  • A Vercel project serving your SPA

Setup

Set LOVABLEHTML_API_KEY in the Vercel project environment. The snippet reads it at runtime; don't paste it into source control.

1

Create middleware.js at the project root

Install @vercel/functions, then save the snippet at the same level as package.json.

2

Set the LOVABLEHTML_API_KEY environment variable

Add the key in your Vercel project environment settings. The snippet reads it at runtime and does not store it in source control.

3

Deploy to Vercel

The middleware runs on the configured matcher for every request.

middleware.js
CopyDownload
// middleware.js (place at the project root next to package.json)
export const config = {
// Use Node.js runtime to access standard Request/Response
runtime: 'nodejs',
// Run on all paths except static assets (customize for your app)
matcher: [
'/((?!_some-static-path|favicon.ico).*)',
// You can also be explicit:
// '/:path*'
],
};
import { next } from "@vercel/functions"; // <- npm install @vercel/functions
export default async function middleware(request) {
// Treat missing/empty Accept and bare '*/*' as HTML so crawler tests
// (curl without -H, default fetch) still route through prerender.
// Asset requests from browsers send specific Accept (e.g. 'text/css,*/*;q=0.1')
// so they won't match.
const accept = (request.headers.get("accept") || "").trim();
const isHtmlRequest =
!accept || accept === "*/*" || accept.includes("text/html");
// 2. If it's not a GET request or not HTML, pass through (e.g. API routes)
if (request.method !== "GET" || !isHtmlRequest) {
return next();
}
try {
// Forward relevant headers and add custom ones
const headers = {
"x-lovablehtml-api-key": process.env.LOVABLEHTML_API_KEY,
accept: "text/html",
"accept-language": request.headers.get("accept-language") || "",
"sec-fetch-mode": request.headers.get("sec-fetch-mode") || "",
"sec-fetch-site": request.headers.get("sec-fetch-site") || "",
"sec-fetch-dest": request.headers.get("sec-fetch-dest") || "",
"sec-fetch-user": request.headers.get("sec-fetch-user") || "",
"upgrade-insecure-requests":
request.headers.get("upgrade-insecure-requests") || "",
referer: request.headers.get("referer") || "",
"user-agent": request.headers.get("user-agent") || "",
};
// Call Encited prerender service with the full URL
const r = await fetch(
"https://encited.com/api/prerender/render?url=" +
encodeURIComponent(request.url),
{ headers, redirect: 'manual' }
);
// 301 = configured redirect rule matched - forward to client
if (r.status === 301) {
const loc = r.headers.get("location");
if (loc) {
return new Response(null, {
status: 301,
headers: { location: loc, "cache-control": "no-store" },
});
}
}
// not pre-rendered, regular browser routing - pass through to SPA
if (r.status === 304) {
return next();
}
// Return HTML or fall through
if (r.status === 200 && (r.headers.get("content-type") || "").includes("text/html")) {
const responseHeaders = new Headers(r.headers);
for (const name of ["content-encoding", "content-length", "transfer-encoding", "connection", "keep-alive"]) {
responseHeaders.delete(name);
}
responseHeaders.set("content-type", "text/html; charset=utf-8");
return new Response(r.body, { status: 200, headers: responseHeaders });
}
} catch {
// ignore
}
// Safety fallback: never block the request
return next();
};

How to Test

  1. Call the render API directly and verify x-lovablehtml-render-cache: hit | miss
  2. Hit your site with Accept: text/html and verify you get HTML back
  3. Verify a normal browser request falls through to your existing origin
bash
CopyDownload
# 1) Call the Encited render API directly
curl -sS -D - -o /dev/null \
-H "x-lovablehtml-api-key: <API_KEY>" \
-H "Accept: text/html" \
"https://encited.com/api/prerender/render?url=https%3A%2F%2Fyour-domain.com%2Fyour-page"
# Look for:
# - HTTP/1.1 200
# - x-lovablehtml-render-cache: hit | miss
# - x-lovablehtml-snapshot-key: ...
bash
CopyDownload
# 2) Hit your site with an HTML Accept header
curl -sS -D - -o /dev/null \
-H "Accept: text/html" \
-A "Googlebot" \
"https://your-domain.com/your-page"
# Look for:
# - HTTP/1.1 200
# - content-type: text/html
bash
CopyDownload
# 3) Browser passthrough (Encited returns 304; middleware continues the request)
curl -sS -D - -o /dev/null \
-A "Mozilla/5.0" \
"https://your-domain.com/your-page"

Best Practices

  • Keep secrets out of git — Store keys in your platform's secret manager or env vars; rotate/revoke when compromised.
  • Don't proxy static assets — Only call Encited for HTML document requests. Always pass through JS/CSS/images/fonts.
  • Handle 304 passthrough — 304 means prerendering doesn't apply. Fall back to your origin and use the Location header as the target URL when needed.
  • Invalidate after content changes — Use the cache invalidation endpoints (optionally with prewarm) after deploys or CMS updates.

Need help? Check the full API reference for prerender, cache, and analytics endpoint docs, or jump directly to Analytics API, or contact us if you run into issues.

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