---
title: "Prerender API: Turn JavaScript Pages into Crawlable HTML | Encited Blog"
url: https://encited.com/blog/prerender-api
description: "What a prerender API is, how it compares to SSR, and the full reference for wiring Encited's render endpoint into Cloudflare Workers, Vercel, Netlify, or your DNS."
lang: en
---

Image: Prerender API: Turn JavaScript Pages into Crawlable HTML (https://encited.com/marketing/og/docs-pre-rendering-api-introduction.webp)

# Prerender API: Turn JavaScript Pages into Crawlable HTML

Oct 13, 2025 October 13, 2025 · Updated Jul 26, 2026 July 26, 2026 · by Aki from Encited

What a prerender API is, how it compares to SSR, and the full reference for wiring Encited's render endpoint into Cloudflare Workers, Vercel, Netlify, or your DNS.

**TL;DR** A prerender API renders your JavaScript pages in a real browser, caches the finished HTML, and serves that copy to search engines and AI crawlers. Your visitors keep getting the normal app. You don't rewrite anything. If you run a single-page application, a legacy frontend, dynamic pages, or a large storefront with heavy JavaScript content, this is usually the fastest way to make every page fully crawlable. Everything below, from the render endpoint to the middleware snippets, is copy-paste ready.

## What pre-rendering is

When a crawler requests a page from a single-page application, a legacy client-rendered frontend, or a storefront that builds its pages in the browser, it often gets a nearly empty HTML shell and a bundle of scripts. The content only appears after the JavaScript runs. Google can execute JavaScript, but it does so in a delayed second wave, and most AI crawlers (GPTBot, PerplexityBot, Claude) don't execute it at all. Whatever isn't in the initial HTML is invisible to them.

A pre-rendering API fixes this at the network layer. It loads your page in a headless browser, waits for the JavaScript to finish, and saves the fully rendered HTML as a snapshot. When a crawler shows up, the user agent gives it away, and it gets the snapshot. Real visitors pass straight through to your app.

This matters most for four kinds of sites:

- **Single-page applications.** React, Vue, or Angular apps where routing and content live entirely in JavaScript. The raw HTML contains almost nothing.
- **Legacy frontend systems.** Older client-rendered builds that nobody wants to re-architect just to fix indexing.
- **Dynamic pages.** Content assembled at request time from APIs: search results, listings, personalized catalogs. There's no static file for a crawler to read.
- **Large storefronts.** Thousands of product pages behind JavaScript-rendered templates, where render delays across the whole catalog add up to real indexing gaps.

The same snapshot fixes link previews too. Slack, X, and other social platforms read your HTML the way crawlers do, so pages that render in the browser often unfurl blank. Serve them the rendered copy and the preview fills in.

## Pre-rendering vs. server-side rendering

SSR solves the same problem from inside your codebase: the server builds the HTML on every request before sending it. It works well, and if you're starting a new project on a framework with SSR built in, use it.

The difference shows up on existing sites:

| | Pre-rendering | SSR |
| --- | --- | --- |
| Code changes | None. Sits in front of your site. | Rewrite rendering logic, often the whole data layer |
| Who gets rendered HTML | Crawlers (visitors get your normal app) | Everyone |
| Server load | Low. Pages render once and cache. | Every request renders |
| Works with legacy builds | Yes | Usually means a migration |
| Setup time | About 5 minutes with DNS | Weeks to months on a real codebase |

Pre-rendering is the practical choice when the site already exists and rebuilding isn't on the roadmap. Plenty of teams run both, SSR for the marketing pages and pre-rendering in front of the legacy app.

One more thing SSR doesn't give you: a prerender API can serve the same page as clean Markdown (https://encited.com/docs/pre-rendering/serve-pages-as-markdown) for AI agents that prefer it over HTML.

## How the Encited pre-render API works

The full reference lives in our API documentation (https://encited.com/docs), starting with authentication (https://encited.com/docs/api-reference/authentication) and the Render API (https://encited.com/docs/api-reference/render-api). Below is how it works at a high level, with enough detail to go live: one render endpoint your middleware calls, three response statuses to handle, and cache invalidation endpoints to keep snapshots fresh.

Pre-rendering is one of several Encited APIs. The same key also drives the SEO Audit API (https://encited.com/docs/api-reference/seo-audit-api), the AI Mentions API (https://encited.com/docs/api-reference/ai-mentions-api) for tracking your brand in AI answers, the Analytics API (https://encited.com/docs/api-reference/analytics-api) for crawler traffic, and the Domains API (https://encited.com/docs/api-reference/domains-api).

One requirement before anything renders: you must own the target domain (added to your Encited account). The API verifies domain ownership.

## Pre-render API auth header

Send your API key with one of these headers:

- `x-lovablehtml-api-key: <API_KEY>`
- `Authorization: Bearer <API_KEY>`

Create and manage keys in the dashboard.

## The pre-render endpoint

GET `/api/prerender/render?url=<ENCODED_URL>`

Headers (one of):

### Behavior

- If prerendering applies: returns `200 text/html` with the page HTML.
- If prerendering does not apply (static asset, non-HTML request, or browser navigation): returns `304` with `Location` header pointing to the target URL.
- If a configured redirect rule matches: returns `301` with `Location` header set to the redirect target. Middleware should forward the `301` to the end client.

Example middleware handling for all three statuses:

ts

Copy Download

const r = await fetch (

"https://encited.com/api/prerender/render?url=" + encodeURIComponent (req. url),

{ headers: { "x-lovablehtml-api-key": API_KEY, accept: "text/html" } },

);

// 301 = configured redirect rule matched, forward to the end client

if (r. status === 301) {

return new Response (null, {

status: 301,

headers: {

Location: r. headers. get ("location") ?? "/",

"Cache-Control": "no-store",

},

});

}

// 304 = not pre-rendered, pass through to origin

if (r. status === 304) return fetch (req);

// 200 = rendered HTML

return new Response (await r. text (), {

headers: { "content-type": "text/html; charset=utf-8" },

});

### Notes

- Static assets (e.g. `.css`, `.js`, images, fonts) are never prerendered. Follow the `Location` header or fetch directly.
- To ensure HTML rendering, send `Accept: text/html`. The endpoint classifies requests similarly to the built-in prerenderer.

## Wiring it into your stack

You have two paths. The no-code route points your DNS at Encited and needs none of the code below: set up pre-rendering with DNS (https://encited.com/docs/pre-rendering/setup-no-code-via-dns). That's the default for sites on AI builders and hosted platforms, and we keep step-by-step guides for Lovable (https://encited.com/docs/domain-setup/cloudflare-lovable), Base44 (https://encited.com/docs/domain-setup/cloudflare-base44), Bolt (https://encited.com/docs/domain-setup/cloudflare-bolt), Replit (https://encited.com/docs/domain-setup/cloudflare-replit), GHL AI Studio (https://encited.com/docs/domain-setup/cloudflare-ghl), Bubble (https://encited.com/docs/domain-setup/cloudflare-bubble), Webflow (https://encited.com/docs/domain-setup/cloudflare-webflow), Shopify (https://encited.com/docs/domain-setup/cloudflare-shopify), BigCommerce (https://encited.com/docs/domain-setup/cloudflare-bigcommerce), Manus (https://encited.com/docs/domain-setup/cloudflare-manus), and Emergent (https://encited.com/docs/domain-setup/cloudflare-emergent).

If you'd rather keep DNS where it is, run the detection yourself with the middleware below. Prefer it inside application code instead? We have framework guides for Next.js (https://encited.com/docs/domain-setup/nextjs), Nuxt (https://encited.com/docs/domain-setup/nuxt), Express (https://encited.com/docs/domain-setup/express-js), Koa (https://encited.com/docs/domain-setup/koa-js), Hapi (https://encited.com/docs/domain-setup/hapi-js), Django (https://encited.com/docs/domain-setup/django), Laravel (https://encited.com/docs/domain-setup/laravel), Ruby on Rails (https://encited.com/docs/domain-setup/ruby-on-rails), Spring Boot (https://encited.com/docs/domain-setup/spring-boot), and ASP.NET Core (https://encited.com/docs/domain-setup/aspnet-core).

## Pre-rendering with Cloudflare Workers (run for every request via Route)

### Standard pre-rendering setup

For custom managed servers, WordPress, Shopify, BigCommerce and others.

1. **Create a new Worker.** Open Cloudflare dashboard (https://dash.cloudflare.com/?to=/:account/workers-and-pages/create) → choose a "Hello World" Worker → Deploy → Edit Code.
2. **Paste the snippet below into the Worker editor.**

js

Copy Download

// encited.js (Cloudflare Worker)

export default {

async fetch (req, env) {

// Only handle public GET navigations

if (req. method !== "GET") return fetch (req);

// 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 = (req. headers. get ("accept") || ""). trim ();

const isHtmlRequest =

! accept || accept === "*/*" || accept. includes ("text/html");

if (! isHtmlRequest) return fetch (req);

const headers = new Headers ();

// Add LOVABLEHTML_API_KEY as a secret: Worker -> Settings -> Variables and Secrets.

headers. set ("x-lovablehtml-api-key", env. LOVABLEHTML_API_KEY);

headers. set ("accept", "text/html");

const forward = [

"accept-language",

"sec-fetch-mode",

"sec-fetch-site",

"sec-fetch-dest",

"sec-fetch-user",

"upgrade-insecure-requests",

"referer",

"user-agent",

];

for (const name of forward) {

const v = req. headers. get (name);

if (v) headers. set (name, v);

}

try {

const r = await fetch (

"https://encited.com/api/prerender/render?url=" +

encodeURIComponent (req. 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" },

});

}

}

if (r. status === 304) {

return fetch (req);

}

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 {

// Prerender unreachable → fall through so visitors still get the site

}

return fetch (req);

},

};

3. **Add a route to your Worker.** Go to your Worker → Settings → Domains & Routes → **Add Route** → enter `yourdomain.com/*`. If your site is reachable on both `yourdomain.com` and `www.yourdomain.com`, enter `*yourdomain.com/*` instead so both are covered. Hosting on a subdomain? Use `blog.yourdomain.com/*` for just that subdomain, or `*.yourdomain.com/*` to cover every subdomain.
4. **Deploy the Worker.** It can take a couple of minutes to start working.

**Common mistakes**

- DNS record on the gray cloud. Routes only fire on proxied traffic. In DNS → Records, the A or CNAME record for `yourdomain.com` must show the orange cloud (**Proxied**), not gray (DNS only).
- Failure mode left on the default. Set the route's Failure mode to **Fail open (proceed)** so a Worker error sends requests straight to your origin instead of failing.
- Bot Fight Mode blocking crawlers. Bot Fight Mode and AI crawler blocking (Security → Bots) run before your Worker, so a blocked crawler never reaches it. Allow the crawlers you want pre-rendered.

### Pre-rendering setup for BigCommerce

For BigCommerce stores on a custom domain.

BigCommerce manages your hostname through its own Cloudflare account (Cloudflare for SaaS). With BigCommerce's default DNS instructions, Cloudflare hands requests directly to BigCommerce, so they never reach your Worker even when a route is attached. The fix below works on any Cloudflare plan.

1. **Point your DNS at BigCommerce with a proxied CNAME.** In your Cloudflare zone, the store hostname (`www` or a subdomain) must be a CNAME to `shops.mybigcommerce.com`, set to **Proxied** (orange cloud). Cloudflare then routes traffic through your zone first, so your Worker runs before BigCommerce serves the page. This only works for www and subdomains; an A record on the apex bypasses your Worker. If your store runs on the apex domain, redirect the apex to www and attach the Worker route to http://www/.
2. **Follow the Standard setup.** Create the Worker, paste the snippet, and attach the route exactly as described in the standard pre-rendering setup above.
3. **Verify the Worker is in the path.** A crawler request to your store should come back with the `x-lovablehtml-render-cache` header. If it's missing, the DNS record is usually still an A record or not proxied, so traffic is skipping your zone.

### Pre-rendering setup for Lovable, Base44, GHL and others

For domains connected to a Lovable, GHL AI Studio, Base44 (or similar) hosted project. If this feels too complicated, our no-code setup (https://encited.com/docs/pre-rendering/setup-no-code-via-dns) can handle it for you with a couple of DNS records.

1. **Create a new Worker.** Open Cloudflare dashboard (https://dash.cloudflare.com/?to=/:account/workers-and-pages/create) → choose a "Hello World" Worker → Deploy → Edit Code.
2. **Paste the snippet below into the Worker editor.** Edit the two constants at the top: `LOVABLE_UPSTREAM` (your hosted URL) and `PUBLIC_HOST` (your custom domain).

js

Copy Download

// encited.js (Cloudflare Worker - Custom Domain mode)

// Use this when the Worker is attached as a Custom Domain in Cloudflare and

// you need to forward non-prerendered traffic to your Lovable hosted URL.

// CHANGE THIS: your Lovable hosted URL (e.g. https://yourapp.lovable.app)

const LOVABLE_UPSTREAM = "https://yourapp.lovable.app";

// CHANGE THIS: your public custom domain (e.g. yourdomain.com)

const PUBLIC_HOST = "yourdomain.com";

function isRedirect (status) {

return (

status === 301 ||

status === 302 ||

status === 303 ||

status === 307 ||

status === 308

);

}

async function forwardToUpstream (req) {

const upstreamBase = new URL (LOVABLE_UPSTREAM);

const upstreamUrl = new URL (req. url);

upstreamUrl. protocol = upstreamBase. protocol;

upstreamUrl. hostname = upstreamBase. hostname;

upstreamUrl. port = upstreamBase. port;

const h = new Headers (req. headers);

h. set ("Host", upstreamBase. hostname);

h. set ("X-Forwarded-Host", PUBLIC_HOST);

h. set ("X-Forwarded-Proto", "https");

h. delete ("cf-connecting-ip");

h. delete ("x-forwarded-for");

h. delete ("forwarded");

const isGetLike = req. method === "GET" || req. method === "HEAD";

const upstreamReq = new Request (upstreamUrl. toString (), {

method: req. method,

headers: h,

body: isGetLike? undefined: req. body,

redirect: "manual",

});

const resp = await fetch (upstreamReq);

// Rewrite redirects so users stay on your custom domain

if (isRedirect (resp. status)) {

const loc = resp. headers. get ("Location") || "";

let newLoc = loc. replaceAll (upstreamBase. hostname, PUBLIC_HOST);

newLoc = newLoc. replace (/ ^ http: \/ \/ / i, "https://");

const newHeaders = new Headers (resp. headers);

if (loc) newHeaders. set ("Location", newLoc);

return new Response (resp. body, {

status: resp. status,

headers: newHeaders,

});

}

return resp;

}

export default {

async fetch (req, env) {

// Only handle public GET navigations

if (req. method !== "GET") return forwardToUpstream (req);

// so they won't match.

const isHtmlRequest =

if (! isHtmlRequest) return forwardToUpstream (req);

const headers = new Headers ();

headers. set ("accept", "text/html");

const forward = [

"accept-language",

"sec-fetch-mode",

"sec-fetch-site",

"sec-fetch-dest",

"sec-fetch-user",

"upgrade-insecure-requests",

"referer",

"user-agent",

];

for (const name of forward) {

const v = req. headers. get (name);

if (v) headers. set (name, v);

}

try {

const r = await fetch (

encodeURIComponent (req. url),

{ headers, redirect: "manual" },

);

if (r. status === 301) {

if (loc) {

return new Response (null, {

status: 301,

});

}

}

// 304 = not pre-rendered, fall through to upstream

if (r. status === 304) {

return forwardToUpstream (req);

}

if (

r. status === 200 &&

) {

for (const name of [

"content-encoding",

"content-length",

"transfer-encoding",

"connection",

"keep-alive",

]) {

responseHeaders. delete (name);

}

}

} catch {

}

return forwardToUpstream (req);

},

};

3. **Deploy the Worker.**
4. **Attach the Worker as a Custom Domain.** Go to your Worker → Settings → Domains & Routes → Add → Custom domain → enter `yourdomain.com`. Add a second Custom Domain for `www.yourdomain.com` if you use http://www/.

## Pre-rendering with Vercel Middleware (run before every request)

1. **Create `middleware.js` at the project root.** Install `@vercel/functions`, then save the snippet at the same level as `package.json`.

js

Copy Download

// 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) {

// so they won't match.

const accept = (request. headers. get ("accept") || ""). trim ();

const isHtmlRequest =

// 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 = {

// Set LOVABLEHTML_API_KEY in your Vercel project's environment variables.

"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 (

encodeURIComponent (request. url),

{ headers, redirect: "manual" },

);

if (r. status === 301) {

if (loc) {

return new Response (null, {

status: 301,

});

}

}

// 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 &&

) {

for (const name of [

"content-encoding",

"content-length",

"transfer-encoding",

"connection",

"keep-alive",

]) {

responseHeaders. delete (name);

}

}

} catch {

// ignore

}

// Safety fallback: never block the request

return next ();

}

2. **Deploy to Vercel.** The middleware runs on the configured matcher for every request.

## Pre-rendering with Netlify Edge Functions (attach to /*)

1. **Create the Edge Function file** at `netlify/edge-functions/lovablehtml.js`.

js

Copy Download

// netlify/edge-functions/lovablehtml.js (Netlify Edge Function)

export default async (request, context) => {

// Only handle public GET navigations.

// so they won't match.

const isHtmlRequest =

if (request. method !== "GET" ||! isHtmlRequest) return context. next ();

const headers = {

// Set LOVABLEHTML_API_KEY in your Netlify site's environment variables (Functions scope).

"x-lovablehtml-api-key": Netlify. env. get ("LOVABLEHTML_API_KEY"),

accept: "text/html",

"upgrade-insecure-requests":

};

try {

const r = await fetch (

encodeURIComponent (request. url),

{ headers, redirect: "manual" },

);

if (r. status === 301) {

if (loc) {

return new Response (null, {

status: 301,

});

}

}

if (r. status === 304) {

return context. next ();

}

if (

r. status === 200 &&

) {

for (const name of [

"content-encoding",

"content-length",

"transfer-encoding",

"connection",

"keep-alive",

]) {

responseHeaders. delete (name);

}

}

} catch {

// Prerender unreachable → continue to the existing Netlify request chain

}

return context. next ();

};

export const config = {

path: "/*",

onError: "bypass",

};

2. **Deploy to Netlify.** The edge function runs on every request and lets Encited classify public HTML GET requests.

## Configuring pre-render redirects

Configure redirect rules from the `/config/routing` (https://encited.com/config/routing) page in the dashboard. Rules are evaluated by the `/render` endpoint; on a match it returns `301` with the resolved `Location` so your middleware can forward it directly to the client.

Behavior:

- `301` only. There is no internal-rewrite (`307`) mode; matched requests always redirect the end client.
- First-match-wins: rules are evaluated top-to-bottom and the first matching rule wins.
- Wildcards: `*` matches a single path segment, `**` matches the rest of the path.
- Query strings on the incoming request are preserved by default and appended to the redirect target.
- Destinations may be relative paths or full external `https://...` URLs.
- Redirect responses include `Cache-Control: no-store` so browsers won't cache them indefinitely.

## Pre-rendered cache invalidation endpoints

These endpoints purge prerendered snapshots of pages for domains you own. Optionally prewarm to immediately re-render. Authentication is the same as the render endpoint (API key header).

### POST `/api/prerender/cache/invalidate-page-cache`

Body:

json

Copy Download

{

"domain": "example.com",

"path": "/pricing",

"prewarm": true

}

Response:

json

Copy Download

{ "ok": true, "prewarmed": 1 }

Example:

bash

Copy Download

curl -sS \

-X POST \

-H "content-type: application/json" \

-H "x-lovablehtml-api-key: <API_KEY>" \

-d '{"domain":"example.com","path":"/pricing","prewarm":true}' \

https://<your-dashboard-host>/api/prerender/cache/invalidate-page-cache

### POST `/api/prerender/cache/invalidate-paths-cache`

Body:

json

Copy Download

{

"domain": "example.com",

"paths": [ "/", "/pricing", "/blog/post" ],

"prewarm": true

}

Response:

json

Copy Download

{ "ok": true, "prewarmed": 3 }

Example:

bash

Copy Download

curl -sS \

-X POST \

-H "content-type: application/json" \

-H "x-lovablehtml-api-key: <API_KEY>" \

-d '{"domain":"example.com","paths":["/","/pricing","/blog/post"],"prewarm":true}' \

https://<your-dashboard-host>/api/prerender/cache/invalidate-paths-cache

### POST `/api/prerender/cache/invalidate-site-cache`

Body:

json

Copy Download

{ "domain": "example.com" }

Response:

json

Copy Download

{ "ok": true, "accepted": true }

Example:

bash

Copy Download

curl -sS \

-X POST \

-H "content-type: application/json" \

-H "x-lovablehtml-api-key: <API_KEY>" \

-d '{"domain":"example.com","prewarm":true}' \

https://<your-dashboard-host>/api/prerender/cache/invalidate-site-cache

### Notes

- The API validates that the `domain` belongs to the authenticated user.
- `prewarm: true` deletes the old cache and immediately re-renders the path(s).
- Common variants (with/without trailing slash) are handled automatically.

## Pre-render API errors

- `401 missing_api_key` / `invalid_api_key`
- `403 domain_not_owned`
- `200 text/html` on success
- `301` with `Location` header when a configured redirect rule matches
- `304` with `Location` header when prerendering not applicable

## Pre-rendering best practices

- Keep API keys secret; rotate or revoke when compromised.
- Always send `Accept: text/html` for bots and crawlers to maximize prerender chance.

## FAQ

**Is pre-rendering cloaking?** No. Google explicitly allows dynamic rendering as long as crawlers and users see the same content. The snapshot is your page, fully rendered. Only the rendering location changes.

**How do snapshots stay fresh?** Pages are recached on a schedule, and you can force a refresh for any URL through the cache invalidation endpoints above whenever content changes.

**Do I need this if my framework already does SSR?** If every page that matters is server-rendered, no. Check what crawlers actually receive with the crawler simulator (https://encited.com/free-tools/crawler-simulator) before adding anything.

**Does it work for AI crawlers too?** Yes. The same snapshot is served to GPTBot, PerplexityBot, Claude, and Google's crawlers, and you can confirm each bot got your content (https://encited.com/docs/crawl-logs/confirm-a-bot-got-your-content) in your crawl log.

**Which plans include API access?** API authentication works on any active subscription. See usage and billing (https://encited.com/docs/billing/usage-and-billing) for how renders and forced recaches count toward your usage.

### See what crawlers get from your site

Paste a URL and we'll show you the HTML your pages return to search engines and AI crawlers right now.

Run the AI Crawler Checker: https://encited.com/free-tools/ai-crawl-checker

## Get discovered anywhere search happens

Readable, citable, outranking pages.

Book a demo: https://encited.com/book-a-demo
Get started: https://encited.com/?authModal=true

Image: Avatar (https://replymaven.com/api/uploads/Su6Q0nK63iZW0ZFJBRNWpmRuppVDLuab/98df5262-f992-45fa-a750-00a6c919baf9.png)

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 (https://lovablehtml.com/docs)

Contact support

Leave a message

We'll get back to you soon

Ask AI about Encited

Product questions, troubleshooting and

Thinking

Image: Preview

Drop an image to attach

Powered by ReplyMaven (https://replymaven.com/)

## Structured data

```json
[
  {
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    "itemListElement": [
      {
        "@type": "ListItem",
        "position": 1,
        "name": "Home",
        "item": " https://encited.com/"
      },
      {
        "@type": "ListItem",
        "position": 2,
        "name": "Blog",
        "item": " https://encited.com/blog"
      },
      {
        "@type": "ListItem",
        "position": 3,
        "name": "Prerender API: Turn JavaScript Pages into Crawlable HTML",
        "item": " https://encited.com/blog/prerender-api"
      }
    ]
  },
  {
    "@context": "https://schema.org",
    "@type": "Article",
    "headline": "Prerender API: Turn JavaScript Pages into Crawlable HTML",
    "description": "What a prerender API is, how it compares to SSR, and the full reference for wiring Encited's render endpoint into Cloudflare Workers, Vercel, Netlify, or your DNS.",
    "image": " https://encited.com/marketing/og/docs-pre-rendering-api-introduction.webp",
    "datePublished": "2025-10-13",
    "dateModified": "2026-07-26",
    "author": {
      "@type": "Person",
      "name": "Aki from Encited"
    },
    "publisher": {
      "@type": "Organization",
      "name": "Encited.com",
      "alternateName": "LovableHTML.com",
      "logo": {
        "@type": "ImageObject",
        "url": "https://encited.com/brand/logo.svg"
      }
    },
    "mainEntityOfPage": {
      "@type": "WebPage",
      "@id": " https://encited.com/blog/prerender-api"
    }
  }
]
```