LovableHTML is nowEncited.com
·Read the announcement →
encited logo
BlogAPI PlatformPricing

Setup with AWS CloudFront (Lambda@Edge)

Run a Lambda@Edge function on CloudFront's origin-request event that calls Encited (formerly LovableHTML) for HTML document requests and falls back to your origin for everything else.

How it works (SPAs)

Your Lambda@Edge function 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 (return the original request).

CloudFront Functions can't make network calls, so they can't reach the prerender service. Use Lambda@Edge on the origin-request event instead — it has a global fetch and a 1 MB generated-response body limit, which is enough for HTML.

Prerequisites

  • A Encited account and API key (Settings → API Keys)
  • A domain added + verified in your Encited dashboard
  • A CloudFront distribution serving your site (S3, a custom origin, or your SPA host)

Setup

Replace <your-api-key> in the snippet. Lambda@Edge has no environment variables, so the key is inlined into the source — keep the function private and rotate the key if it leaks.

1

Create a Lambda function in us-east-1

Lambda@Edge functions must live in us-east-1 (N. Virginia), regardless of where your distribution serves traffic. Use the Node.js 18+ runtime and name the file index.mjs so it's treated as an ES module (the snippet uses export const handler). Region restriction docs

2

Add the edge trust policy

Lambda@Edge runs under a different principal than normal Lambda. The execution role's trust policy must allow both services:

Trust both lambda and edgelambda

Add lambda.amazonaws.com and edgelambda.amazonaws.com to the role's trusted entities, or CloudFront won't be able to replicate the function to edge locations.

3

Edit the config and raise the timeout

There are no environment variables at the edge, so config is inlined. Set PUBLIC_HOST to yourdomain.com and paste your API key where the snippet expects it. Then set the function timeout to 30s — the maximum allowed on an origin-request trigger.

4

Publish a version and attach it to CloudFront

CloudFront can only reference a published, numbered version — not $LATEST and not an alias. Publish a new version, then add it to your cache behavior as an Origin request trigger. Every code change means a new version and re-associating it.

5

Forward viewer headers to the function

At origin-request, CloudFront strips viewer headers unless your policy forwards them, and replaces User-Agent with Amazon CloudFront unless forwarded — which would break crawler detection.

Use the AllViewerExceptHostHeader policy

Attach the managed AllViewerExceptHostHeader origin request policy. Forwarding the Host header to an S3 origin breaks it — which is why the snippet reconstructs the public URL from PUBLIC_HOST instead of the request's Host.

index.mjs.js
CopyDownload
// index.mjs (AWS Lambda@Edge — attach to an "Origin request" trigger)
//
// Why Lambda@Edge and not a CloudFront Function?
// CloudFront Functions cannot make network calls, so they can't reach the
// prerender service. Lambda@Edge (Node.js 18+) has a global fetch and a 1 MB
// generated-response body limit on the origin-request event — enough for HTML.
//
// Deploy notes:
// 1. Create the function in us-east-1 (Lambda@Edge requirement), then deploy
// it to your CloudFront distribution as an "Origin request" trigger.
// 2. This is an ES module — the file must be named index.mjs (or set
// "type": "module" in package.json). Use 'export const handler', NOT
// 'exports.handler', or you'll get "exports is not defined in ES module scope".
// 3. Lambda@Edge does NOT support environment variables — the API key is
// inlined below.
// 4. In the cache behavior, forward (and include in the cache key) the
// User-Agent, Accept, and Accept-Language headers so crawler vs. browser
// requests are routed and cached separately.
// CHANGE THIS: your canonical public domain. On an origin-request trigger
// CloudFront rewrites the Host header to the origin's hostname, so we can't
// derive the viewer's domain from the request — the prerender service must be
// queried with your real public URL or it won't find the page.
const PUBLIC_HOST = 'yourdomain.com';
export const handler = async (event) => {
const request = event.Records[0].cf.request;
const header = (name) => {
const h = request.headers[name.toLowerCase()];
return h && h[0] ? h[0].value : '';
};
// Only handle public GET navigations.
// 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 = (header('accept') || '').trim();
const isHtmlRequest = !accept || accept === '*/*' || accept.includes('text/html');
if (request.method !== 'GET' || !isHtmlRequest) return request;
// Reconstruct the full public URL. Use PUBLIC_HOST, not the (origin-rewritten)
// Host header.
const fullUrl =
'https://' + PUBLIC_HOST + request.uri + (request.querystring ? '?' + request.querystring : '');
try {
const headers = {
'x-lovablehtml-api-key': <your-api-key>,
accept: 'text/html',
'accept-language': header('accept-language'),
'sec-fetch-mode': header('sec-fetch-mode'),
'sec-fetch-site': header('sec-fetch-site'),
'sec-fetch-dest': header('sec-fetch-dest'),
'sec-fetch-user': header('sec-fetch-user'),
'upgrade-insecure-requests': header('upgrade-insecure-requests'),
referer: header('referer'),
'user-agent': header('user-agent'),
};
const r = await fetch('https://encited.com/api/prerender/render?url=' + encodeURIComponent(fullUrl), { headers });
// 301 = configured redirect rule matched — forward to client
if (r.status === 301) {
const loc = r.headers.get('location');
if (loc) {
return {
status: '301',
statusDescription: 'Moved Permanently',
headers: {
location: [{ key: 'Location', value: loc }],
'cache-control': [{ key: 'Cache-Control', value: 'no-store' }],
},
};
}
}
// 304 = not pre-rendered, pass through to origin
if (r.status === 304) return request;
if ((r.headers.get('content-type') || '').includes('text/html')) {
return {
status: '200',
statusDescription: 'OK',
headers: {
'content-type': [{ key: 'Content-Type', value: 'text/html; charset=utf-8' }],
},
body: await r.text(),
};
}
} catch {
// Prerender unreachable → fall through so visitors still get the site
}
// Safety fallback: never block the request
return request;
};

Caveats specific to Lambda@Edge

These are the things that trip people up on CloudFront. Read them before you deploy.

Deployment & limits

The function must live in us-east-1. Lambda@Edge functions are authored only in N. Virginia, regardless of where your distribution serves traffic. Create it there or CloudFront can't attach it. See the region restriction docs.

  • CloudFront references a published, numbered version — never $LATEST and never an alias. Every code change means publishing a new version and re-associating it with the distribution.
  • Trust policy needs two principals. The execution role must trust both lambda.amazonaws.com and edgelambda.amazonaws.com, or replication to edge locations fails. See the Lambda@Edge permissions docs.
  • No environment variables at the edge. Config (your API key, your public host) is inlined/bundled into the source. This is why the snippet hard-codes the key — see the security note below.
  • Set the function timeout to 30s — the maximum allowed on an origin-request trigger. See the function restrictions docs.
  • Use an ES module. Name the file index.mjs (or set "type": "module") and export with export const handler, not exports.handler, or you'll get "exports is not defined in ES module scope".

Header forwarding & URL reconstruction

At the origin-request event, CloudFront rewrites the Host header to your origin's hostname (e.g. your S3 bucket), not your public domain. Reconstruct the public URL from a constant (PUBLIC_HOST in the snippet) — never from the Host header — or the prerender service won't find the page.

  • Viewer headers are stripped at origin-request unless they're in the cache policy or an origin request policy. In particular, User-Agent is replaced with Amazon CloudFront unless you forward it — which would break crawler detection.
  • Attach the managed AllViewerExceptHostHeader origin request policy. Forwarding the Host header to an S3 origin breaks the S3 request, which is exactly why AllViewerExceptHostHeader (not AllViewer) is the right choice.

S3 origins & SPA routing

  • If your origin is a private S3 bucket, missing keys return 403, not 404. Your SPA routing / error-document configuration must account for that when deciding which paths fall through to index.html.

Security

  • Because there are no edge environment variables, your API key lives in the function source. Restrict who can read/update the function, and rotate the key from Settings → API Keys if it's ever exposed.

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 static assets (JS/CSS/images/fonts) are not proxied to Encited
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) Passthrough (no Accept: text/html → function should not call Encited)
curl -sS -D - -o /dev/null \
-A "Mozilla/5.0" \
"https://your-domain.com/your-page"

Changes take a few minutes to propagate to edge locations after you publish a new version and re-associate it. If a test still hits the old behaviour, wait for replication to finish (CloudFront shows the distribution as "Deploying").

Best Practices

  • Keep the function private — the API key is in the source. Limit IAM access and rotate the key if it leaks.
  • 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. Return the original request so it falls through to your origin.
  • Re-publish & re-associate on every change — there's no $LATEST at the edge; a numbered version is the only thing CloudFront will run.
  • 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
Powered by ReplyMaven
Avatar
Aki
Hi, need help with SEO, AI search or getting indexed?