🇹🇷 Türkçe: Bu yazının Türkçesini oku →

Whenever a client tells me they want to go headless, the first objection in the room is always SEO. If your storefront is a JavaScript application, will Google index it properly? Here is my honest answer after shipping a few of these: yes, it indexes fine — but only if you treat server rendering as a requirement, not a nice-to-have, and only if you accept that you are now responsible for the meta tags, structured data, and sitemap that a traditional WooCommerce plugin used to hand you for free. Headless does not break SEO. It moves the work onto your side of the wall, and that is the part teams underestimate.

The Problem: Client-Side Rendering

A purely client-side rendered (CSR) React app sends an empty HTML shell to the browser (and to Googlebot):



  

Google can render JavaScript, but it’s slower, less reliable, and uses a separate rendering queue. Critical ecommerce pages — products, categories, landing pages — should never depend on client-side rendering for their indexable content. Treat the empty shell above as what the crawler sees on a bad day, and design so that day never costs you a ranking.

I have watched a whole medium go dark before

Let me date myself, because it is the reason I am insistent about this. I spent years building sites in Macromedia Flash and ActionScript — rich, interactive things that looked years ahead of everything around them. And search engines could not read a single word inside them. The content lived in a compiled binary the crawler had no way into, exactly the way your product copy lives inside a JavaScript bundle the crawler may or may not choose to execute. We told ourselves Google would catch up. It did not. When Flash died, an enormous amount of beautiful, unindexable work died with it, and the sites that survived were the ones that had kept a plain, server-rendered HTML version alongside the fancy one.

Client-side rendering is the same bet with a friendlier face. The tooling is genuinely better now — Googlebot does run JavaScript, which Flash never got — but the lesson holds: never let the only copy of your indexable content live in something the crawler has to work to reach. Render it server-side and you are not making a bet at all.

The Solution: SSR and SSG

Next.js and Nuxt provide server-side rendering (SSR) and static site generation (SSG) out of the box. With these, your product pages are delivered as fully-rendered HTML:



  Blue Widget - YourStore
  
  


  

Blue Widget

$29.99

Premium blue widget with stainless steel construction...

Next.js: ISR (Incremental Static Regeneration)

The ideal approach for WooCommerce product pages:

// app/products/[slug]/page.js

export async function generateStaticParams() {

const products = await fetchAllProductSlugs();

return products.map(p => ({ slug: p.slug }));

}

export async function generateMetadata({ params }) {

const product = await fetchProduct(params.slug);

return {

title: ${product.name} | YourStore,

description: product.short_description,

openGraph: {

title: product.name,

description: product.short_description,

images: [{ url: product.images[0]?.src }],

type: 'website'

}

};

}

export default async function ProductPage({ params }) {

const product = await fetchProduct(params.slug);

return ;

}

// Revalidate every 5 minutes

export const revalidate = 300;

Pages are pre-rendered at build time and regenerated in the background when stale. Googlebot always gets fully-rendered HTML.

Meta Tag Management

In traditional WooCommerce, Yoast or RankMath handles meta tags. In headless, you manage them in your frontend framework.

Dynamic Meta Tags (Next.js App Router)

// lib/seo.js

export function generateProductMeta(product) {

const title = product.yoast_head_json?.title || ${product.name} | YourStore;

const description = product.yoast_head_json?.og_description ||

stripHtml(product.short_description).substring(0, 160);

return {

title,

description,

openGraph: {

title: product.yoast_head_json?.og_title || product.name,

description,

url: https://yourstore.com/products/${product.slug},

images: product.images.map(img => ({

url: img.src,

width: 800,

height: 800,

alt: img.alt || product.name

})),

type: 'website',

siteName: 'YourStore'

},

twitter: {

card: 'summary_large_image',

title: product.name,

description,

images: [product.images[0]?.src]

},

alternates: {

canonical: https://yourstore.com/products/${product.slug}

}

};

}

Leveraging Yoast Data

If Yoast SEO is installed on your WordPress backend, it exposes SEO data via the REST API:

// Fetch product with Yoast SEO data

const product = await fetch(

${WP_URL}/wp-json/wp/v2/product/${id}?_fields=yoast_head_json

);

const yoast = product.yoast_head_json;

// Use Yoast's pre-computed meta

const meta = {

title: yoast.title,

description: yoast.og_description,

canonical: yoast.canonical,

robots: yoast.robots

};

Structured Data (Schema.org)

Product schema is critical for rich snippets in search results. Generate it server-side:

function generateProductSchema(product) {

const schema = {

'@context': 'https://schema.org',

'@type': 'Product',

name: product.name,

description: stripHtml(product.short_description),

image: product.images.map(img => img.src),

sku: product.sku,

brand: {

'@type': 'Brand',

name: product.brands?.[0]?.name || 'YourStore'

},

offers: {

'@type': 'Offer',

url: https://yourstore.com/products/${product.slug},

priceCurrency: 'USD',

price: product.price,

availability: product.stock_status === 'instock'

? 'https://schema.org/InStock'

: 'https://schema.org/OutOfStock',

seller: {

'@type': 'Organization',

name: 'YourStore'

}

}

};

// Add reviews if available

if (product.rating_count > 0) {

schema.aggregateRating = {

'@type': 'AggregateRating',

ratingValue: product.average_rating,

reviewCount: product.rating_count

};

}

return schema;

}

// In your page component

Close Search Window