Back to Blog
SEO & Web
July 4, 2026
7 min read

SEO Guide for Single Page Applications with React and Pre-rendering

Tomás Ledesma
Tomás Ledesma
Founder & Software Architect
SEO Guide for Single Page Applications with React and Pre-rendering

Single Page Applications (SPAs) built with React, Vue, or Svelte offer a superb user experience and instant transitions. However, their Achilles heel has historically been SEO. Since content is dynamically rendered in the browser via JavaScript, basic search engine web crawlers and social media bots often only see an empty HTML document: <div id="root"></div>.

Although Googlebot executes JavaScript nowadays, it does so in a deferred queue which delays indexing. Furthermore, platforms like LinkedIn, Slack, and Twitter do not interpret JS when generating Open Graph preview cards. To address this, static pre-rendering (SSG) is the ideal strategy.

What is Pre-rendering with Playwright?

Instead of migrating your project to heavy frameworks like Next.js (with all the complexity of running a Node.js server or high Vercel hosting costs), you can pre-render your React app at build time using Playwright or Puppeteer.

The process involves starting a temporary local production preview server with your Vite build, launching a headless (invisible) Chromium browser, visiting each path on your site, capturing the final rendered HTML DOM, and saving it into its corresponding static HTML file.

Implementing the Prerender Script

Want to optimize your technology?

We design and build bespoke AI automation pipelines and cloud architectures.

Using Node.js and Playwright, we can automate this step in our production build pipeline. Here is a basic workflow:

javascript
// scripts/prerender.js
import { chromium } from 'playwright';
import { preview } from 'vite';
import { writeFileSync, mkdirSync } from 'fs';
import { dirname, resolve } from 'path';

// Start the preview server on a local port
const previewServer = await preview({ preview: { port: 4999 } });
const browser = await chromium.launch();
const page = await browser.newPage();

const routes = ['/', '/blog', '/blog/how-to-automate-business-processes-with-ai'];

for (const route of routes) {
  await page.goto('http://localhost:4999' + route, { waitUntil: 'networkidle' });
  await page.waitForTimeout(1000); // Wait for hydration and animations
  const html = await page.content();
  
  // Save static file (e.g. /blog -> dist/blog/index.html)
  const targetPath = resolve(process.cwd(), 'dist', route === '/' ? 'index.html' : `${route.replace(/^\//, '')}/index.html`);
  mkdirSync(dirname(targetPath), { recursive: true });
  writeFileSync(targetPath, html, 'utf-8');
}

await browser.close();
previewServer.httpServer.close();

Hosting Configuration: Firebase, Netlify, or Vercel

The trick is storing the static files at physical paths like `dist/blog/how-to-automate-business-processes-with-ai/index.html`. Hostings like Firebase prioritize these static files over wildcard redirect rules. Thus, crawlers from Google or LinkedIn receive the precompiled HTML with specific meta tags instantly without executing JS, while actual human visitors enjoy instant React hydration and a fast SPA experience.

Related Articles

Frontend

Why Zustand is the best alternative to Redux in React

We compare Zustand against Redux Toolkit for state management in React, analyzing simplicity, performance, and the massive reduction of boilerplate.

Read post
Backend

Guide to structuring efficient microservices with FastAPI and Docker

Learn how to containerize your Python APIs with Docker, optimizing image sizes using multi-stage builds and accelerating your deployments.

Read post
Tomás Ledesma
Written by

Tomás Ledesma

Tomás Ledesma is a software architect and technical consultant specializing in intelligent process automation, scalable cloud architectures, and cross-platform app engineering.