The biggest concern about headless commerce is SEO. If your storefront is a JavaScript application, will Google index it properly? The short answer: yes, if you implement SSR/SSG correctly. The long answer involves meta tags, structured data, sitemaps, and a few headless-specific challenges.
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.
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
United States / English
Slovensko / Slovenčina
Canada / Français
Türkiye / Türkçe