Serving Open Graph to Bots at hey.xyz: Our Client-Side SPA Approach

At hey.xyz, we built a client-side SPA using Vite and React Router 7. But this created a challenge - how do you serve proper metadata to social media crawlers and search engines when your app renders client-side? Our solution was to create a dual-architecture system: Main Web App: CSR app built with Vite + React Router 7 hosted on Cloudflare Pages OG Service: Minimal SSR Next.js app hosted on Railway that only renders metadata, no UI The secret sauce is a Cloudflare Worker that acts as our traffic cop. It checks incoming requests for bot user agents, and: If it's a regular user: Pass them to our slick CSR app If it's a bot: Redirect to our specialized OG service This is the Cloudflare Interface where we connect the Worker with the Web app We've set a generous cache policy (30 days) for bot responses since our metadata doesn't change often. This approach lets us keep our main app fast and interactive for actual users while still having proper social sharing and SEO. No need to sacrifice the benefits of a modern CSR app just to satisfy crawlers. Here is the flow diagram of how things work Honestly, this pattern is so clean and efficient - we're never going back to server-rendering our entire app just for bots. This strategy helps us deliver millions of requests in a cost-efficient way, keeping our infrastructure costs low while providing the best experience for both users and search engines. PS: Here is our Cloudflare worker code the checks for all requests const botRegex = /(Bot|Twitterbot|facebookexternalhit|LinkedInBot|Slackbot|Discordbot|TelegramBot|WhatsApp|Googlebot|Bingbot|Applebot)/i; export default { async fetch(request) { const userAgent = request.headers.get("user-agent") || ""; // If not a bot, pass through if (!botRegex.test(userAgent)) { return fetch(request); } // If bot, rewrite to og.hey.xyz const url = new URL(request.url); const targetUrl = `https://og.hey.xyz${url.pathname}${url.search}`; const rewrittenRequest = new Request(targetUrl, { method: request.method, headers: request.headers, body: request.method !== "GET" && request.method !== "HEAD" ? await request.text() : null, redirect: "follow" }); const response = await fetch(rewrittenRequest); const newHeaders = new Headers(response.headers); newHeaders.set("Cache-Control", "public, max-age=2592000, immutable"); return new Response(response.body, { status: response.status, statusText: response.statusText, headers: newHeaders }); } }; Original version can be found here: https://world.hey.com/yoginth/serving-open-graph-to-bots-at-hey-xyz-our-client-side-spa-approach-8b4af2a3

Apr 20, 2025 - 07:47
 0
Serving Open Graph to Bots at hey.xyz: Our Client-Side SPA Approach

At hey.xyz, we built a client-side SPA using Vite and React Router 7. But this created a challenge - how do you serve proper metadata to social media crawlers and search engines when your app renders client-side?

Our solution was to create a dual-architecture system:

  1. Main Web App: CSR app built with Vite + React Router 7 hosted on Cloudflare Pages
  2. OG Service: Minimal SSR Next.js app hosted on Railway that only renders metadata, no UI

The secret sauce is a Cloudflare Worker that acts as our traffic cop. It checks incoming requests for bot user agents, and:

  • If it's a regular user: Pass them to our slick CSR app
  • If it's a bot: Redirect to our specialized OG service

This is the Cloudflare Interface where we connect the Worker with the Web app

Image description

We've set a generous cache policy (30 days) for bot responses since our metadata doesn't change often.

This approach lets us keep our main app fast and interactive for actual users while still having proper social sharing and SEO. No need to sacrifice the benefits of a modern CSR app just to satisfy crawlers.

Here is the flow diagram of how things work

Image description

Honestly, this pattern is so clean and efficient - we're never going back to server-rendering our entire app just for bots.

This strategy helps us deliver millions of requests in a cost-efficient way, keeping our infrastructure costs low while providing the best experience for both users and search engines.

PS: Here is our Cloudflare worker code the checks for all requests

const botRegex = /(Bot|Twitterbot|facebookexternalhit|LinkedInBot|Slackbot|Discordbot|TelegramBot|WhatsApp|Googlebot|Bingbot|Applebot)/i;

export default {
  async fetch(request) {
    const userAgent = request.headers.get("user-agent") || "";

    // If not a bot, pass through
    if (!botRegex.test(userAgent)) {
      return fetch(request);
    }

    // If bot, rewrite to og.hey.xyz
    const url = new URL(request.url);
    const targetUrl = `https://og.hey.xyz${url.pathname}${url.search}`;

    const rewrittenRequest = new Request(targetUrl, {
      method: request.method,
      headers: request.headers,
      body: request.method !== "GET" && request.method !== "HEAD" ? await request.text() : null,
      redirect: "follow"
    });

    const response = await fetch(rewrittenRequest);

    const newHeaders = new Headers(response.headers);
    newHeaders.set("Cache-Control", "public, max-age=2592000, immutable");

    return new Response(response.body, {
      status: response.status,
      statusText: response.statusText,
      headers: newHeaders
    });
  }
};

Original version can be found here: https://world.hey.com/yoginth/serving-open-graph-to-bots-at-hey-xyz-our-client-side-spa-approach-8b4af2a3