SEO Advanced

SEO for JavaScript Frameworks

Imran Nadwi
251 views 75 min read

Introduction to JavaScript SEO

Modern JavaScript frameworks like React, Vue, and Angular create unique SEO challenges. Understanding how search engines process JavaScript is essential for modern web development.

JavaScript Rendering Challenges

  • Search engines must execute JavaScript to see content
  • Rendering delays can impact crawl efficiency
  • Client-side routing complicates indexing
  • Dynamic content may not be discovered

1. How Google Renders JavaScript

Understanding Google rendering pipeline is fundamental to JavaScript SEO.

Google's Two-Wave Indexing

# Google JavaScript Processing Pipeline

Wave 1: Initial Crawl
├── Fetch HTML
├── Parse HTML content
├── Extract links from HTML
├── Index HTML content
└── Queue for rendering

Rendering Queue (Hours to Days)
├── Chromium-based renderer
├── Execute JavaScript
├── Wait for network requests
└── Capture rendered DOM

Wave 2: Post-Render Indexing
├── Process rendered content
├── Extract dynamic links
├── Update index with full content
└── Re-evaluate rankings

# Key Insight: Critical content should be 
# available without JavaScript when possible

Testing JavaScript Rendering

# Testing Tools

1. Google Search Console URL Inspection
   - Shows rendered HTML
   - Identifies rendering issues
   - Displays screenshot

2. Mobile-Friendly Test
   - Quick rendering check
   - Shows rendered HTML
   - Flags JavaScript errors

3. Chrome DevTools
   - Disable JavaScript: Settings > Debugger > Disable JavaScript
   - View what Googlebot sees without JS

4. View Source vs. Inspect Element
   - View Source = Initial HTML (Wave 1)
   - Inspect Element = Rendered DOM (Wave 2)

2. Server-Side Rendering (SSR)

SSR delivers fully rendered HTML to search engines and users.

Next.js SSR Implementation

// pages/product/[slug].js - Next.js SSR

export async function getServerSideProps(context) {
    const { slug } = context.params;
    
    // Fetch data on server
    const product = await fetchProduct(slug);
    
    if (!product) {
        return {
            notFound: true // Returns 404
        };
    }
    
    return {
        props: {
            product,
            // SEO data available immediately
            seoData: {
                title: product.name,
                description: product.description,
                image: product.image
            }
        }
    };
}

export default function ProductPage({ product, seoData }) {
    return (
        <>
            <Head>
                <title>{seoData.title}</title>
                <meta name="description" 
                      content={seoData.description} />
                <meta property="og:image" 
                      content={seoData.image} />
            </Head>
            <ProductDisplay product={product} />
        </>
    );
}

Nuxt.js SSR Implementation

// pages/product/_slug.vue - Nuxt.js SSR

<template>
    <div>
        <h1>{{ product.name }}</h1>
        <p>{{ product.description }}</p>
    </div>
</template>

<script>
export default {
    async asyncData({ params, $axios }) {
        const product = await $axios.$get(
            `/api/products/${params.slug}`
        );
        return { product };
    },
    
    head() {
        return {
            title: this.product.name,
            meta: [
                {
                    hid: "description",
                    name: "description",
                    content: this.product.description
                },
                {
                    hid: "og:title",
                    property: "og:title",
                    content: this.product.name
                }
            ]
        };
    }
};
</script>

3. Static Site Generation (SSG)

Pre-rendering pages at build time for optimal performance and SEO.

Next.js Static Generation

// pages/blog/[slug].js - Next.js SSG

// Generate paths at build time
export async function getStaticPaths() {
    const posts = await getAllPosts();
    
    return {
        paths: posts.map(post => ({
            params: { slug: post.slug }
        })),
        fallback: "blocking" // SSR for new paths
    };
}

// Generate page content at build time
export async function getStaticProps({ params }) {
    const post = await getPostBySlug(params.slug);
    
    return {
        props: { post },
        revalidate: 3600 // ISR: regenerate hourly
    };
}

export default function BlogPost({ post }) {
    return (
        <article>
            <Head>
                <title>{post.title}</title>
                <script 
                    type="application/ld+json"
                    dangerouslySetInnerHTML={{
                        __html: JSON.stringify(post.schema)
                    }}
                />
            </Head>
            <h1>{post.title}</h1>
            <div dangerouslySetInnerHTML={{ 
                __html: post.content 
            }} />
        </article>
    );
}

4. Dynamic Rendering

Serving different content to users vs. search engine bots.

Dynamic Rendering Setup

// middleware/dynamic-render.js

const BOTS = [
    "googlebot",
    "bingbot",
    "yandex",
    "baiduspider",
    "facebookexternalhit",
    "twitterbot",
    "linkedinbot"
];

function isBot(userAgent) {
    const ua = userAgent.toLowerCase();
    return BOTS.some(bot => ua.includes(bot));
}

module.exports = async (req, res, next) => {
    if (isBot(req.headers["user-agent"] || "")) {
        // Render with Puppeteer/Rendertron
        const renderedHtml = await prerenderPage(req.url);
        return res.send(renderedHtml);
    }
    
    // Serve SPA to users
    next();
};

// Rendertron service configuration
async function prerenderPage(url) {
    const rendertronUrl = process.env.RENDERTRON_URL;
    const response = await fetch(
        `${rendertronUrl}/render/${encodeURIComponent(url)}`
    );
    return response.text();
}

Rendertron Docker Setup

# docker-compose.yml

version: "3"
services:
    rendertron:
        image: "ammobindotca/rendertron"
        ports:
            - "3000:3000"
        environment:
            - CACHE_MAX_ENTRIES=100
            - CACHE_EXPIRY_SECONDS=3600
        restart: unless-stopped

# nginx.conf - Bot detection and routing

map $http_user_agent $is_bot {
    default 0;
    ~*(googlebot|bingbot|yandex) 1;
}

server {
    location / {
        if ($is_bot) {
            proxy_pass http://rendertron:3000/render/$scheme://$host$request_uri;
        }
        
        try_files $uri $uri/ /index.html;
    }
}

5. Client-Side SEO Optimization

When SSR is not possible, optimize client-side rendering for SEO.

React Helmet for Meta Tags

// components/SEO.jsx - React Helmet

import { Helmet } from "react-helmet-async";

export default function SEO({ 
    title, 
    description, 
    image, 
    url,
    type = "website"
}) {
    const siteTitle = "Your Site Name";
    const fullTitle = `${title} | ${siteTitle}`;
    
    return (
        <Helmet>
            <title>{fullTitle}</title>
            <meta name="description" content={description} />
            <link rel="canonical" href={url} />
            
            {/* Open Graph */}
            <meta property="og:type" content={type} />
            <meta property="og:title" content={fullTitle} />
            <meta property="og:description" content={description} />
            <meta property="og:image" content={image} />
            <meta property="og:url" content={url} />
            
            {/* Twitter */}
            <meta name="twitter:card" content="summary_large_image" />
            <meta name="twitter:title" content={fullTitle} />
            <meta name="twitter:description" content={description} />
            <meta name="twitter:image" content={image} />
        </Helmet>
    );
}

SPA Routing Best Practices

// Proper client-side routing for SEO

// 1. Use history mode (not hash mode)
// React Router
<BrowserRouter>
    <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/products/:slug" element={<Product />} />
    </Routes>
</BrowserRouter>

// Vue Router
const router = createRouter({
    history: createWebHistory(), // NOT createWebHashHistory()
    routes: [...]
});

// 2. Server configuration for SPA
// All routes should return index.html

# nginx.conf
location / {
    try_files $uri $uri/ /index.html;
}

# Apache .htaccess
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]

Key Terms

Server-Side Rendering (SSR)
Rendering JavaScript on the server and sending fully formed HTML to the client
Static Site Generation (SSG)
Pre-rendering pages at build time into static HTML files
Incremental Static Regeneration (ISR)
Updating static pages after deployment without rebuilding the entire site
Dynamic Rendering
Serving pre-rendered content to bots while serving the SPA to users

Practical Exercise

  1. Test your JavaScript site using Google's URL Inspection tool
  2. Implement SSR for your most important pages
  3. Set up ISR for content that changes periodically
  4. Configure proper meta tag management for your framework
  5. Create a sitemap that includes all JavaScript-rendered routes