Headless browser rendering SPA pages as static HTML for search engines

    Playwright SSG Tutorial: How to Make Lovable Apps Visible to Google

    14. April 20266 min read
    Till Freitag

    TL;DR:Playwright crawls your SPA routes at build time and saves fully rendered HTML as static files. Result: Google sees everything, Lighthouse scores go up, and you don't have to switch frameworks."

    Till Freitag

    The Problem: Google Can't See Your Lovable App

    You've built an amazing app with Lovable. Design is on point, features work, users love it. But Google? Google sees an empty page.

    That's because Lovable – like all Vibe Coding tools – generates Single Page Applications (SPAs). The server delivers an empty index.html, and JavaScript renders the content in the browser. Works for humans, not for crawlers.

    Background: Our Vibe Coding SEO Guide explains the problem in detail. This article is the practical deep-dive for the Playwright solution.

    What Is Playwright SSG?

    SSG stands for Static Site Generation. The idea: instead of hoping Googlebot executes JavaScript, we render all pages at build time ourselves – with a real browser.

    Playwright is Microsoft's browser automation framework. It launches a real Chromium browser, navigates to every route of your app, waits until everything is rendered, and saves the finished HTML.

    The result:

    • Every route has a complete HTML file
    • Google immediately sees all content
    • First Contentful Paint becomes dramatically faster
    • Social previews work out of the box

    Prerequisites

    Before you start, you need:

    1. Your Lovable app on GitHub – Export code via Lovable's GitHub integration
    2. Node.js 18+ on your build system
    3. Basic command line knowledge

    Not on GitHub yet? Our Lovable → GitHub → Vercel Flow Guide walks you through the entire process.

    Step 1: Install Playwright

    npm install -D playwright-core @playwright/test
    npx playwright install chromium

    You only need Chromium – Firefox and WebKit aren't necessary for SSG.

    Step 2: Create a Route List

    Playwright needs to know which pages to render. You have two options:

    Option A: Static Route List

    // scripts/routes.ts
    export const routes = [
      '/',
      '/about',
      '/blog',
      '/blog/my-first-article',
      '/contact',
      // ... all your routes
    ];

    Option B: Automatic Crawling

    // scripts/discover-routes.ts
    import { chromium } from 'playwright-core';
    
    async function discoverRoutes(baseUrl: string): Promise<string[]> {
      const browser = await chromium.launch();
      const page = await browser.newPage();
      const discovered = new Set<string>(['/']);
      const queue = ['/'];
    
      while (queue.length > 0) {
        const route = queue.shift()!;
        await page.goto(`${baseUrl}${route}`, { waitUntil: 'networkidle' });
        
        const links = await page.$eval('a[href]', anchors =>
          anchors
            .map(a => new URL(a.href, window.location.origin).pathname)
            .filter(href => href.startsWith('/') && !href.includes('.'))
        );
    
        for (const link of links) {
          if (!discovered.has(link)) {
            discovered.add(link);
            queue.push(link);
          }
        }
      }
    
      await browser.close();
      return [...discovered];
    }

    We recommend Option B for websites with many dynamic routes (e.g., blog articles).

    Step 3: The SSG Script

    Here's the core – the script that renders all routes and saves them as HTML:

    // scripts/ssg.ts
    import { chromium } from 'playwright-core';
    import { writeFileSync, mkdirSync } from 'fs';
    import { join, dirname } from 'path';
    
    const DIST_DIR = './dist';
    const BASE_URL = 'http://localhost:4173'; // Vite preview port
    
    async function renderRoutes(routes: string[]) {
      const browser = await chromium.launch();
      const context = await browser.newContext();
    
      for (const route of routes) {
        const page = await context.newPage();
        
        await page.goto(`${BASE_URL}${route}`, {
          waitUntil: 'networkidle',
          timeout: 30000,
        });
    
        // Wait for dynamic content
        await page.waitForTimeout(1000);
    
        const html = await page.content();
        
        // Save as index.html in the route directory
        const filePath = route === '/'
          ? join(DIST_DIR, 'index.html')
          : join(DIST_DIR, route, 'index.html');
        
        mkdirSync(dirname(filePath), { recursive: true });
        writeFileSync(filePath, html, 'utf-8');
        
        console.log(`✅ Rendered: ${route}`);
        await page.close();
      }
    
      await browser.close();
      console.log(`\n🎉 ${routes.length} pages rendered!`);
    }

    Step 4: Assemble the Build Script

    Add an ssg script to your package.json:

    {
      "scripts": {
        "build": "vite build",
        "preview": "vite preview",
        "ssg": "npm run build && npm run preview & sleep 3 && tsx scripts/ssg.ts"
      }
    }

    What happens:

    1. vite build creates the production build
    2. vite preview starts a local server
    3. tsx scripts/ssg.ts renders all routes and overwrites the HTML files in the dist directory

    Step 5: GitHub Actions CI/CD

    Automate the SSG process with a GitHub Action:

    # .github/workflows/ssg.yml
    name: Build & SSG
    on:
      push:
        branches: [main]
    
    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-node@v4
            with:
              node-version: 20
    
          - run: npm ci
          - run: npx playwright install chromium --with-deps
          - run: npm run build
    
          # Start preview server in background
          - run: npm run preview &
          - run: sleep 5
    
          # Render all pages
          - run: npx tsx scripts/ssg.ts
    
          # Deploy to Vercel (or other host)
          - uses: amondnet/vercel-action@v25
            with:
              vercel-token: ${{ secrets.VERCEL_TOKEN }}
              vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
              vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
              working-directory: ./dist

    Step 6: Verify Results

    After the first SSG build, you can verify the results:

    Lighthouse Score

    npx lighthouse https://your-domain.com --output html --output-path ./lighthouse.html

    Typical improvements:

    MetricBefore SSGAfter SSG
    First Contentful Paint2.8s0.4s
    Largest Contentful Paint3.5s0.8s
    Time to Interactive4.2s1.1s
    SEO Score60-7095-100

    Google Search Console

    After deployment, check in Google Search Console:

    1. URL Inspection → Shows the rendered page
    2. Coverage → No more "Not indexed" errors
    3. Core Web Vitals → Green values

    Common Problems and Solutions

    Problem: Dynamic content isn't rendered

    Solution: Increase the waitForTimeout or wait for specific selectors:

    await page.waitForSelector('[data-loaded="true"]', { timeout: 10000 });

    Problem: Browser API calls fail

    Solution: Ensure your preview server has access to all APIs. Alternatively: mock data for the SSG build.

    Problem: Hash routing (#) instead of path routing (/)

    Solution: Lovable uses React Router with path routing. If you have hash routing, switch to BrowserRouter.

    Problem: Build is too slow

    Solution: Parallelize rendering:

    // Render 5 pages simultaneously
    const CONCURRENCY = 5;
    const chunks = [];
    for (let i = 0; i < routes.length; i += CONCURRENCY) {
      chunks.push(routes.slice(i, i + CONCURRENCY));
    }
    
    for (const chunk of chunks) {
      await Promise.all(chunk.map(route => renderRoute(route)));
    }

    What's Still Missing: Schema & Meta Tags

    SSG solves the rendering problem. But for complete SEO you also need:

    • JSON-LD Schema for structured data → Automate JSON-LD Schema for SPAs
    • Meta tags for social previews (og:title, og:image, etc.)
    • Sitemap and robots.txt
    • Canonical URLs for multilingual content

    Conclusion: SSG Is the Game Changer for Vibe Coding SEO

    Playwright SSG is the missing bridge between Vibe Coding and search engines. You keep all the advantages of SPA architecture – interactive UI, fast development, AI-generated code – and get full SEO visibility on top.

    The complete stack:

    1. Lovable → Generates the app
    2. GitHub → Versions the code
    3. Playwright → Renders static HTML (this article)
    4. Vercel → Delivers via Edge CDN

    This is exactly the stack we used to bring till-freitag.com to Lighthouse 100.


    Don't want to set this up yourself? We'll do it for you →

    Our SEO audit shows you within 48 hours where your Vibe Coding app stands – and what's needed to make it visible.

    TeilenLinkedInWhatsAppE-Mail

    Related Articles

    Prerendering pipeline visualization: SPA, Playwright, Schema.org and edge deploy
    April 29, 20263 min

    Prerendering: How to Turn a React SPA Into a Google-Friendly Site

    React SPAs are invisible to crawlers. Prerendering fixes that – without Next.js, without an SSR server. How our Playwrig

    Read more
    Rocket made of code elements launching through search result pages with Lighthouse Score 100
    April 14, 20266 min

    Vibe Coding & SEO: Why AI-Generated Apps Stay Invisible – And How We Fix It

    Lovable, Bolt, v0 – Vibe Coding tools produce SPAs that Google can't see. Our playbook makes them SEO-ready: SSG, Schema

    Read more
    Pipeline diagram with three stations: Lovable, GitHub, Vercel
    April 14, 20264 min

    Lovable → GitHub → Vercel: The Complete Deployment Flow for SEO-Ready Apps

    Lovable generates the app, GitHub versions the code, Vercel delivers with < 50ms TTFB. This guide shows the complete flo

    Read more
    Google Search Console dashboard with performance graphs and coverage reports
    April 14, 20265 min

    Google Search Console for Vibe Coding Projects: Setup, Debugging & Indexing

    Your Lovable app is live on Vercel – but Google isn't indexing anything? How to set up Search Console, debug crawling is

    Read more
    Social media preview cards with OG-Image meta tags floating in dark space
    April 14, 20264 min

    OG-Image Best Practices for SPAs: Making Your Vibe Coding Projects Shareable

    When you share a link from your Lovable app, LinkedIn shows an empty box. Why SPAs don't deliver social previews – and h

    Read more
    Lovable app with structured HTML, visible to Google, ChatGPT and Perplexity
    May 13, 20267 min

    Lovable SEO/AEO: Every App Discoverable by Google and ChatGPT From Day 1

    Lovable ships server-side rendering, pre-rendering for existing apps, Semrush directly in the builder chat, and an on-de

    Read more
    SaaS analytics dashboard with KPI cards, line charts and data tables, built in Lovable
    March 8, 20265 min

    Build a SaaS Dashboard with Lovable: From Prompt to Production

    A complete SaaS dashboard with charts, auth and database – built with Lovable in an afternoon. Step-by-step tutorial wit

    Read more
    Minimalist illustration of a developer with a ponytail and oval glasses skeptically reviewing code on a screen
    June 14, 20265 min

    Ponytail: The Best Code Is the Code You Never Wrote

    A dev built Ponytail because his AI agents wrote 500 lines for a 5-line problem. The result: 80-94% less code, 47-77% ch

    Read more
    Classic drag-and-drop website builder blocks compared to an AI-native website stack
    June 13, 20263 min

    Website Builders vs. AI-Native Stack: Which Path Wins in 2026?

    Wix, Squarespace, Jimdo & co. vs. an AI-native stack with Lovable, Cloud and edge hosting – when does each really pay of

    Read more