How we moved a storefront from a Lighthouse SEO score of 67 to 100: SSR, semantic HTML, metadata, crawlability, and the mistakes that keep product pages invisible.
On the legacy Vue 2 app, product content lived behind client rendering. Crawlers got a shell. Lighthouse SEO sat at 67: missing descriptions, missing alts, and pages that were hard to crawl.
After the Next.js rebuild we hit 100 on Lighthouse SEO for key templates. Projected organic visibility uplift was 30–40%. The work was not keyword stuffing. It was making the HTML tell the truth.
If the title, price, and description are not in the first HTML, you are negotiating with crawlers.
// app/products/[slug]/page.tsx
export default async function ProductPage({ params }: { params: { slug: string } }) {
const product = await getProduct(params.slug);
return (
<article>
<h1>{product.name}</h1>
<p>{product.shortDescription}</p>
<AddToCart productId={product.id} />
</article>
);
}
Client widgets (add to cart, reviews) can hydrate. The copy that should rank cannot wait for JavaScript.
SSG/ISR for catalog pages is even better: fast TTFB and stable HTML for crawlers.
import type { Metadata } from 'next';
export async function generateMetadata({
params,
}: {
params: { slug: string };
}): Promise<Metadata> {
const product = await getProduct(params.slug);
return {
title: `${product.name} | Lab Store`,
description: product.metaDescription,
alternates: { canonical: `https://example.com/products/${product.slug}` },
openGraph: {
title: product.name,
description: product.metaDescription,
images: [{ url: product.ogImage, width: 1200, height: 630 }],
},
};
}
Rules we enforced in CMS validation:
description shipping to productionMissing meta descriptions were a large chunk of the old 67 score. They are also the easiest fix once metadata is generated on the server.
Lighthouse flags what users with assistive tech also need.
<header>
<nav aria-label="Primary">...</nav>
</header>
<main>
<article>
<h1>{product.name}</h1>
<figure>
<Image src={product.hero} alt={product.heroAlt} width={1200} height={800} />
<figcaption>{product.name} product photo</figcaption>
</figure>
</article>
</main>
h1 per templatealt=""This doubled as an accessibility pass. SEO and a11y often fail for the same structural reasons.
E-commerce search creates infinite URLs: ?color=red&sort=price.
const indexable = new Set(['page']);
export function robotsForSearch(searchParams: URLSearchParams) {
const extra = [...searchParams.keys()].filter((key) => !indexable.has(key));
return extra.length ? { robots: { index: false, follow: true } } : {};
}
Also:
activerobots.txt that does not block /_next static assets needed to renderLighthouse 100 is a hygiene score. Organic lift needs Search Console:
/products/*We treated SEO as a regression suite: CI failed if a product template shipped without title, description, canonical, or h1. That is how 67 became 100 and stayed there.