Skip to content
Prerender Base44

Putting a pre-rendering proxy in front of Base44 with a Cloudflare Worker

How a Cloudflare Worker can route crawlers to a pre-renderer and visitors to your Base44 app: the code, the canonical and redirect fixes, and what to test before switching DNS.

10 min read

A Cloudflare Worker on your custom domain can split traffic: crawler requests for pages go to a pre-renderer, and everything else goes straight to your Base44 app at its base44.app address. Crawlers get fully rendered HTML on every visit, including crawlers Base44 doesn’t pre-render for, and visitors see your app as normal. The routing code is short. The details that matter are rewriting canonical tags and redirects, and knowing which Base44 features depend on a connected custom domain.

This is the do-it-yourself version of the managed option in the options guide.

How the traffic flows

  1. Your domain’s DNS points at Cloudflare, with the Worker attached as a custom domain.
  2. A request comes in. The Worker checks the method, the path and the user agent.
  3. Crawler asking for a page: the Worker fetches rendered HTML from your pre-renderer and returns it.
  4. Anyone else, or any asset: the Worker forwards the request to yourapp.base44.app and returns Base44’s response.

The pre-renderer loads your page through the same domain with a headless browser. Its browser user agent doesn’t match the crawler list, so its requests pass straight through to Base44, and there’s no loop.

The Worker

A minimal version, with placeholders for your app and renderer. It follows the same structure as the Worker Encited publishes for Base44.

// worker.js
const BASE44_ORIGIN = 'https://yourapp.base44.app'   // your published Base44 URL
const PUBLIC_HOST = 'yourdomain.com'
const RENDERER = 'https://renderer.example.com/render?url='  // your pre-renderer

const BOTS = /googlebot|bingbot|gptbot|oai-searchbot|chatgpt-user|claudebot|claude-user|perplexitybot|perplexity-user|applebot|duckduckbot|bravebot|meta-externalagent|amazonbot|ccbot|facebookexternalhit|twitterbot|linkedinbot|slackbot|discordbot|whatsapp/i
const ASSET = /\.(js|css|png|jpe?g|gif|svg|webp|ico|woff2?|ttf|map|json|xml|txt)$/i

export default {
  async fetch(req, env) {
    const url = new URL(req.url)
    const ua = req.headers.get('user-agent') || ''

    if (req.method === 'GET' && !ASSET.test(url.pathname) && BOTS.test(ua)) {
      const rendered = await fetch(RENDERER + encodeURIComponent(url.toString()), {
        headers: { authorization: `Bearer ${env.RENDERER_TOKEN}` },
      })
      if (rendered.ok) {
        return new Response(rendered.body, {
          headers: { 'content-type': 'text/html; charset=utf-8', 'x-prerendered': '1' },
        })
      }
      // If the renderer fails, fall through and serve Base44's own response.
    }
    return forward(req, url)
  },
}

async function forward(req, url) {
  const upstream = new URL(url.pathname + url.search, BASE44_ORIGIN)
  const resp = await fetch(upstream, {
    method: req.method,
    headers: req.headers,
    body: ['GET', 'HEAD'].includes(req.method) ? undefined : req.body,
    redirect: 'manual',
  })
  // Keep visitors on your domain when Base44 redirects.
  const loc = resp.headers.get('location')
  if (loc) {
    const headers = new Headers(resp.headers)
    headers.set('location', loc.replaceAll(new URL(BASE44_ORIGIN).host, PUBLIC_HOST))
    return new Response(resp.body, { status: resp.status, headers })
  }
  return resp
}

Store the renderer token as a Worker secret (wrangler secret put RENDERER_TOKEN), and deploy with wrangler deploy.

Fix the canonical tags

Base44 writes canonical and og:url tags for the address that served the page. Proxied from yourapp.base44.app, those tags point at base44.app, and Google may index that address instead of yours. Either have the renderer rewrite them, or rewrite in the Worker with Cloudflare’s HTMLRewriter:

const fixHost = (el, attr) => {
  const v = el.getAttribute(attr)
  if (v) el.setAttribute(attr, v.replace(new URL(BASE44_ORIGIN).host, PUBLIC_HOST))
}
const rewriter = new HTMLRewriter()
  .on('link[rel="canonical"]', { element: (el) => fixHost(el, 'href') })
  .on('meta[property="og:url"]', { element: (el) => fixHost(el, 'content') })
  .on('meta[name="twitter:url"]', { element: (el) => fixHost(el, 'content') })
// return rewriter.transform(rendered) for crawler responses

Then check the upstream’s headers: if the base44.app address sends x-robots-tag: noindex, crawlers must never see that header on your domain.

What depends on a connected custom domain

With this setup your domain points at Cloudflare, so it isn’t connected as a custom domain inside Base44. Base44 ties some features to a connected, verified custom domain:

  • Emailing people who haven’t signed up to your app with SendEmail.
  • Sending from a custom email domain.
  • Base44’s own custom-domain SEO features, such as its managed files on your domain.

Check which of these your app uses. Your Worker can serve robots.txt and a sitemap itself if needed.

The renderer is the hard part

The Worker is the easy half. The renderer needs a pool of headless browsers, waiting until each page’s data has loaded, caching, invalidation when you publish, retries, and monitoring. That’s where most of the cost and maintenance goes, and why a managed service is usually cheaper once everything is counted. A managed service such as Encited supplies the renderer, the post-processing and crawl logs, and its Base44 setup gives you this Worker ready-made, or a DNS-only setup with no Worker at all.

What to do next

  1. Decide whether you’ll run a renderer or use a managed one.
  2. Deploy the Worker to a test subdomain and run the end-to-end tests.
  3. Check canonicals and headers with a crawler user agent before moving your main domain.

Found something wrong or out of date? Base44 ships changes every week and we'd rather fix a guide than let it rot.

Disclosure: the team behind this guide also builds Encited, mentioned above.

Frequently asked questions

Can I put Cloudflare in front of a Base44 custom domain?
Not with Base44's own custom domain connection, which needs its DNS records set to DNS only. A Worker proxy instead serves your domain from Cloudflare and forwards visitors to your app's base44.app address, so the domain isn't connected in Base44 itself.
What do I lose by proxying a Base44 app through a Worker?
Features that depend on a custom domain being connected and verified in Base44, such as emailing people who haven't signed up to your app and sending from a custom email domain. Check which of those your app uses before switching.
Do I have to rewrite canonical tags when proxying Base44?
Yes. Pages served from your base44.app address declare that address in canonical and og:url tags. The proxy or renderer must rewrite them to your custom domain, or Google may index the base44.app version.
Is there a ready-made Worker for Base44 pre-rendering?
Encited publishes a Cloudflare Worker for Base44 in its documentation, which forwards visitors to your base44.app URL and sends crawler requests to its renderer. It also offers a DNS-only setup with no Worker.

Read next