Every Next.js (Headless) fix we catalog

SEOLZ catalogs 65 fixes for Next.js (Headless) across 2 areas — SEO, answer-engine readiness, accessibility, security and site-health. Each lists the exact steps for Next.js (Headless), with a link to the official docs.

SEO · 50 fixes

Catalog coverageModerate effort

Audit your XML sitemap and internal linking so that every product and category page is discoverable and crawlable by Google — not hidden behind JavaScript, pagination, or missing links.

On Next.js (Headless)

  1. Create `app/sitemap.ts` (App Router) to dynamically fetch every product and category slug from your commerce API and return a fully-typed `MetadataRoute.Sitemap` array — this file is automatically served at `/sitemap.xml` by Next.js with no extra configuration.
  2. Create `app/robots.ts` (App Router) to emit a `robots.txt` that explicitly allows all crawlers and points to your sitemap URL, overriding any accidental `Disallow: /` rules that may have been set in a static `public/robots.txt`.
  3. Audit `generateMetadata` (or `next/head`) in `app/products/[slug]/page.tsx` and `app/categories/[slug]/page.tsx` — ensure NO page sets `robots: 'noindex'` or `X-Robots-Tag: noindex` unless intentional (e.g. draft/out-of-stock pages that should remain hidden).
  4. Replace any JavaScript-only navigation (e.g. `router.push()` triggered by a `<div onClick>`) with real `<Link href='...'>` components from `next/link` so crawlers can follow internal links without executing JS event handlers.
  5. Verify `next.config.js` contains no `headers()` rule that adds `X-Robots-Tag: noindex` to product/category routes, and that no middleware (`middleware.ts`) redirects or blocks crawlers by user-agent.
  6. After deploying, submit the sitemap URL (`https://your-domain.com/sitemap.xml`) to Google Search Console and use the URL Inspection tool to confirm key product and category pages return HTTP 200 with `robots: index, follow`.
Code · Generic HTML (html)
<!-- CORRECT: Standard crawlable link -->
<a href="/products/blue-running-shoes">Blue Running Shoes</a>

<!-- WRONG: JavaScript-only — Googlebot cannot follow this -->
<span onclick="navigate('/products/blue-running-shoes')">Blue Running Shoes</span>

Standard crawlable product link — use plain <a href> tags, never JavaScript-only click handlers, for navigation links that Google must follow.

Cwv clsModerate effort

Eliminate unexpected layout shifts by reserving space for every image, video, embed, and dynamically injected element so nothing jumps when the page loads.

On Next.js (Headless)

  1. Use next/image for ALL images and always supply explicit `width` and `height` props (or use `fill` with a sized wrapper). Never let an `<img>` tag render without known dimensions — this is the single largest CLS source in headless storefronts.
  2. For above-the-fold hero/banner images, add the `priority` prop so they are preloaded and the space is claimed before first paint, preventing a late swap from a placeholder to the real image.
  3. For videos and third-party embeds (YouTube, Vimeo, ads, chat widgets), wrap them in an aspect-ratio container using Tailwind's `aspect-video` class or a CSS `aspect-ratio` rule so the browser reserves the exact rectangle before the iframe/script loads.
  4. For late-loading widgets (reviews, personalisation banners, cookie notices), set an explicit `min-height` on their mount-point element and use a matching skeleton/placeholder component rendered on the server so the DOM node already occupies space during hydration.
  5. Audit dynamic content injected via `useEffect` (cart drawer, announcement bars, sticky headers) — wrap each in a container with fixed or min-height so the rest of the page is not pushed when they mount.
  6. Run Lighthouse or the Vercel Speed Insights panel after each change to confirm the CLS score drops; target CLS < 0.1.
Code · Generic HTML (html)
<!-- CORRECT: explicit width and height prevent layout shift -->
<img
  src="/images/product-red-sneaker.jpg"
  alt="Red canvas sneaker, side view"
  width="800"
  height="800"
  loading="lazy"
>

Always declare width and height on every img element. The browser uses these to reserve space before the image file downloads.

Cwv inpLarger project

Reduce Interaction to Next Paint (INP) to under 200 ms by breaking up long JavaScript tasks, deferring non-critical third-party scripts, and lightening event handlers so the page feels fast and responsive after every tap or click.

On Next.js (Headless)

  1. Break up long tasks with scheduler.postTask / setTimeout(fn, 0): Audit your event handlers (add-to-cart, filter changes, modal opens) in components like `components/Cart.tsx`, `components/ProductFilter.tsx`, etc. Wrap any non-paint-critical work (analytics calls, state syncs, inventory re-fetches) inside `setTimeout(() => { heavyWork() }, 0)` or, where supported, `scheduler.postTask(() => { heavyWork() }, { priority: 'background' })` so the browser can commit the visual response first.
  2. Defer non-critical third-party scripts with `next/script`: In `app/layout.tsx` (App Router) or `pages/_app.tsx` (Pages Router), replace plain `<script>` tags for chat widgets, tag managers, analytics, and ad pixels with Next.js `<Script strategy='lazyOnload' …/>` (or `'afterInteractive'` for scripts that need the DOM but not immediate interaction). This removes their parse/execute cost from the critical path entirely.
  3. Optimise React event handlers with `useTransition` / `useDeferredValue`: For expensive state updates triggered by user input (e.g., faceted search, variant selectors), wrap the heavy `setState` call in `startTransition(() => { setResults(filtered) })`. React will yield to the browser between renders, keeping INP low. Import `useTransition` from `react`.
  4. Avoid layout-thrashing in handlers: Inside click/tap handlers, read all DOM measurements before writing. Replace patterns like `el.style.height = el.scrollHeight + 'px'` (which force synchronous layout) with CSS transitions or the View Transitions API so the browser's rendering pipeline is not blocked.
  5. Code-split heavy components with `dynamic()`: In `app/` or `pages/`, replace static imports of heavy UI (rich-text editors, map embeds, heavy carousels) with `const HeavyComp = dynamic(() => import('../components/HeavyComp'), { ssr: false })`. This keeps main-bundle parse time low, directly reducing the task length that precedes a user interaction.
  6. Measure and verify: Add the `web-vitals` package and report INP via `onINP` in `app/layout.tsx` or `pages/_app.tsx`. Use Chrome DevTools → Performance panel → 'Interactions' track and PageSpeed Insights to confirm all interactions are consistently below 200 ms after deploying.
Cwv lcpModerate effort

Reduce Largest Contentful Paint (LCP) to under 2.5 seconds by serving the hero image in a next-gen format (WebP/AVIF), preloading it, and eliminating render-blocking resources.

On Next.js (Headless)

  1. Replace any raw <img> tag used for the hero/banner with Next.js's built-in <Image> component (next/image). Set the `priority` prop so Next.js automatically emits a <link rel='preload'> for the image and skips lazy-loading — this is the single highest-impact LCP fix.
  2. Set the `sizes` prop to match the image's rendered CSS width at every breakpoint (e.g. `sizes='100vw'` for a full-width hero). This lets the browser download the smallest adequate srcset candidate instead of the full-resolution file.
  3. Enable modern format delivery: next/image serves WebP/AVIF automatically via its built-in Image Optimization API. Confirm `images.formats` in next.config.js includes `['image/avif', 'image/webp']` (this is the default but worth verifying). For remote CDN images also add the domain/remotePattern to `images.remotePatterns`.
  4. Eliminate render-blocking CSS/JS: audit `_app.tsx` / `app/layout.tsx` for synchronous third-party scripts and move them to next/script with `strategy='lazyOnload'` or `strategy='afterInteractive'`. Never use the default (blocking) strategy for non-critical scripts.
  5. Use `next/font` for any custom web font used in the LCP area instead of a <link> stylesheet. `next/font` inlines the @font-face and removes the render-blocking font request entirely.
  6. Run Lighthouse or `next build && next start` with `NEXT_TELEMETRY_DISABLED=1` to verify LCP drops below 2.5 s. Use the built-in `/_next/image` response headers (Cache-Control: public, max-age=31536000) to confirm the optimized image is being served.
Code · Generic HTML (html)
<!-- Step 1: Preload the LCP hero image -->
<link
  rel="preload"
  as="image"
  href="/images/hero-banner.webp"
  imagesrcset="/images/hero-banner-400.webp 400w,
               /images/hero-banner-800.webp 800w,
               /images/hero-banner-1500.webp 1500w"
  imagesizes="100vw"
  fetchpriority="high"
>

<!-- Step 2: Hero <img> tag — eager, high priority, no lazy-load -->
<img
  src="/images/hero-banner-1500.webp"
  srcset="/images/hero-banner-400.webp 400w,
          /images/hero-banner-800.webp 800w,
          /images/hero-banner-1500.webp 1500w"
  sizes="100vw"
  alt="[Descriptive alt text for your hero image]"
  width="1500"
  height="600"
  loading="eager"
  fetchpriority="high"
>

Add inside <head>, immediately after <meta charset>. Replace the href with your actual hero image CDN URL.

Duplicate meta descriptionModerate effort

Write a unique, page-specific meta description for every page on your store so Google can display a compelling, accurate snippet for each one in search results.

On Next.js (Headless)

  1. Identify every route segment that currently shares or omits a meta description (e.g. app/page.tsx, app/products/[slug]/page.tsx, app/collections/[handle]/page.tsx, app/blog/[slug]/page.tsx).
  2. For static/root pages (e.g. app/page.tsx), export a plain `metadata` object with a hand-written, unique `description` field that reflects that page's actual content.
  3. For dynamic routes (e.g. app/products/[slug]/page.tsx), export an async `generateMetadata({ params })` function that fetches the page-specific data (product title, excerpt, etc.) from your commerce API and returns a computed `description` — trim it to 150-160 characters.
  4. Create a shared `truncate(text, max)` helper (e.g. lib/seo.ts) so every route formats descriptions consistently and never accidentally duplicates the same fallback string.
  5. Add a root-level fallback in app/layout.tsx using the `metadata` export so any page that hasn't yet received a custom description gets a site-level default instead of nothing — but always override it at the leaf route.
  6. Run `next build` and audit with `<meta name='description'>` checks in the rendered HTML (or a tool like Screaming Frog) to confirm every URL now carries a distinct description.
Code · Generic HTML (html)
<head>
  <meta charset="UTF-8">
  <meta name="description" content="Shop women's running shoes at Acme Store — lightweight, cushioned designs for road and trail. Free shipping on orders over $50.">
  <title>Women's Running Shoes | Acme Store</title>
</head>

Place inside the <head> element of every page. Replace the content value with a unique 120–160 character description for that specific page.

Duplicate titleModerate effort

Give every page a unique, descriptive title tag that accurately reflects that page's specific content.

On Next.js (Headless)

  1. Identify every route segment in your `app/` directory (e.g. `app/page.tsx`, `app/products/[slug]/page.tsx`, `app/collections/[handle]/page.tsx`, `app/blog/[slug]/page.tsx`, etc.) that currently exports a static or duplicate `metadata` object or uses a shared `<title>` tag.
  2. For static pages (e.g. Home, About, Contact), export a unique `metadata` object directly from the route file with a page-specific `title` string — never reuse the same string across routes.
  3. For dynamic pages (e.g. Product, Collection, Blog Post), replace the static `metadata` export with an async `generateMetadata()` function that fetches the resource by its route param (`slug`, `handle`, etc.) and builds the title from real data (e.g. product name, collection title).
  4. Use Next.js's built-in `title.template` in your root `app/layout.tsx` `metadata` export to automatically append your brand name (e.g. `'%s | My Store'`), so every page only needs to supply the unique `%s` portion — eliminating accidental duplicates from copy-paste.
  5. Remove any legacy `<Head><title>…</title></Head>` blocks from `pages/` router files or custom `_app.tsx`/`_document.tsx` if you are migrating to the App Router, since mixing both systems causes duplicate or overriding titles.
  6. Run `next build` and review the build output for any route that still emits a generic or repeated title; fix each one until every route shows a distinct title in the terminal summary.
Code · Generic HTML (html)
<head>
  <title>Merino Wool Running Socks – Free UK Shipping | BrandName</title>
</head>

Place inside <head>. Make the title unique per page — include the specific item name and brand.

Faceted url indexableModerate effort

Point the rel=canonical on every faceted/filter URL to its clean parent category URL (or add noindex), so Google treats filtered pages as a single authoritative page instead of thousands of duplicates.

On Next.js (Headless)

  1. Identify your category/collection route file — typically `app/[...category]/page.tsx` (App Router) or `pages/[...category].tsx` (Pages Router). Faceted filter state is usually carried in URL search params (e.g. `?color=red&size=M`) or as extra path segments.
  2. In the App Router, export a `generateMetadata` function. Detect whether the current request URL contains any filter/facet params (or extra path segments beyond the base category slug). If it does, set `alternates.canonical` to the clean category URL (strip all query params and facet path segments). If it does not, set canonical to itself.
  3. In the Pages Router equivalent, use `next/head` inside the page component and render a `<link rel='canonical' href={canonicalUrl} />` using the same clean-URL logic.
  4. Optionally, for deeply-nested or purely parameter-driven faceted pages you never want indexed, add `robots: { index: false, follow: true }` (App Router) or `<meta name='robots' content='noindex,follow' />` (Pages Router) alongside the canonical — belt-and-suspenders protection.
  5. Ensure `generateStaticParams` (or `getStaticPaths`) does NOT enumerate faceted filter combinations — only emit paths for real category pages, so filtered URLs are never accidentally pre-rendered as standalone pages.
  6. Verify the fix with `curl -s https://your-domain/category/shirts?color=red | grep canonical` and confirm the canonical points to `/category/shirts`, not the filtered URL.
Code · Generic HTML (html)
<head>
  <!-- Canonical points to the CLEAN category URL, not the filtered variant -->
  <link rel="canonical" href="https://www.example.com/womens-pants">
</head>

Place this in the <head> of every filtered/faceted page. The href must be the clean parent category URL — never the filtered URL itself.

Images missing altModerate effort

Add descriptive alt text to every product and content image so search engines and screen readers can understand what the image shows.

On Next.js (Headless)

  1. Replace every bare <img> tag with Next.js's next/image <Image> component, which enforces the `alt` prop at the TypeScript/ESLint level.
  2. For product images, derive alt text from your commerce API data — prefer a dedicated `altText` field, then fall back to the product `title` plus a variant descriptor (e.g. `${product.title} – ${variant.color}`). Never leave alt empty unless the image is purely decorative.
  3. For decorative/background images that convey no information (e.g. a spacer or icon that has adjacent visible text), pass `alt=""` explicitly so screen readers skip them — do NOT omit the prop entirely.
  4. Create a shared utility `getImageAlt(image, fallback)` (see snippet) so alt-text logic is centralised and consistent across PDP, PLP, Cart, and CMS content components.
  5. Enable the `@next/next/no-img-element` ESLint rule (it ships with `eslint-config-next`) to surface any remaining raw <img> usages across the codebase.
  6. Add an integration check in CI (or a Playwright/Cypress test) that queries `document.querySelectorAll('img:not([alt])')` and fails if the list is non-empty.
Code · Generic HTML (html)
<!-- BEFORE (missing alt — bad) -->
<img src="/images/red-leather-bag.jpg">

<!-- AFTER (descriptive alt — correct) -->
<img src="/images/red-leather-bag.jpg" alt="Red leather crossbody bag with gold buckle clasp">

<!-- Decorative image (intentionally empty alt — correct) -->
<img src="/images/divider-line.png" alt="">

Place this wherever an <img> tag appears in your HTML. Replace the placeholder text with your real description.

Images missing alt bulkModerate effort

Add descriptive alt text to every product and content image at the template level so search engines and screen readers can understand all images on the page.

On Next.js (Headless)

  1. Replace every bare <img> tag in your component tree with Next.js's next/image <Image> component, which enforces the alt prop at the TypeScript/prop-types level.
  2. Create a shared ProductImage component (e.g. components/ProductImage.tsx) that accepts the product object and derives a meaningful alt string from structured data fields (e.g. product.title + variant color/size), with a non-empty fallback.
  3. Create a shared ContentImage component (e.g. components/ContentImage.tsx) for CMS/editorial images that reads an explicit alt field from the CMS payload and falls back to the image caption or filename-derived string — never an empty string.
  4. Audit your product listing page (e.g. app/products/page.tsx or pages/products/index.tsx), product detail page (app/products/[slug]/page.tsx or pages/products/[slug].tsx), and any content/blog pages — swap raw <img> / untyped <Image alt=''> usages for the new shared components.
  5. Add an ESLint rule (jsx-a11y/alt-text) to your .eslintrc to catch empty or missing alt props at build time so regressions are blocked in CI.
  6. Run next build and resolve any TypeScript or lint errors before deploying; verify rendered HTML with browser DevTools to confirm every <img> element in the output has a non-empty alt attribute.
Code · Generic HTML (html)
<!-- Informative product image — describe what the image shows -->
<img src="/images/red-running-shoes-side-view.jpg"
     alt="Red canvas running shoes, side view, white rubber sole"
     width="800" height="800">

<!-- Decorative image — empty alt tells screen readers to skip it -->
<img src="/images/divider-wave.svg"
     alt=""
     role="presentation"
     width="1200" height="40">

Correct alt text pattern for informative images vs. decorative images.

Low performance scoreLarger project

Improve your Lighthouse/PageSpeed performance score by reducing page weight, eliminating render-blocking resources, optimising images, and addressing Core Web Vitals so Google ranks your store higher in search results.

On Next.js (Headless)

  1. Replace every native <img> tag with Next.js <Image> from 'next/image', always supplying width, height, and a descriptive alt prop. For above-the-fold hero images set priority={true} to preload them; for everything else rely on the default lazy loading.
  2. Audit third-party and custom scripts: move any <script> tags into Next.js <Script> from 'next/script' and set strategy='lazyOnload' (analytics, chat widgets) or strategy='afterInteractive' (tag managers). Never use strategy='beforeInteractive' unless absolutely required.
  3. Enable the built-in font optimisation: import all Google/custom fonts via 'next/font' (next/font/google or next/font/local). This eliminates the render-blocking font request, self-hosts the font file, and injects an inline font-face declaration automatically.
  4. Reduce JavaScript bundle size: run 'next build && next analyze' (add @next/bundle-analyzer to next.config.js) to spot large dependencies, then code-split heavy components with dynamic(() => import('./HeavyComponent'), { ssr: false }) so they are not part of the initial bundle.
  5. Configure next.config.js for maximum asset optimisation: enable the 'swcMinify' option (default in Next 13+), add image 'formats' (['image/avif','image/webp']), set appropriate 'deviceSizes' and 'imageSizes', and add 'Cache-Control' headers via the 'headers()' function to maximise CDN caching.
  6. Improve Core Web Vitals (LCP, CLS, INP): ensure the LCP image uses priority={true} + explicit dimensions to avoid layout shift, wrap dynamically sized ad/embed containers in a fixed aspect-ratio shell to eliminate CLS, and defer all non-critical data fetching to avoid blocking the main thread.
Code · Generic HTML (html)
<!-- In <head>: preconnect to critical third-party origins to reduce connection latency -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

<!-- Preload your Largest Contentful Paint image (hero/banner) so it loads earlier -->
<link rel="preload" as="image" href="/images/hero.webp" fetchpriority="high">

<!-- Hero image: eager load, explicit dimensions to prevent CLS -->
<img
  src="/images/hero.webp"
  alt="Descriptive alt text for your hero image"
  width="1200"
  height="600"
  fetchpriority="high"
  decoding="async"
>

<!-- All other images: lazy load, explicit dimensions to prevent CLS -->
<img
  src="/images/product-shot.webp"
  alt="Descriptive product alt text"
  width="800"
  height="800"
  loading="lazy"
  decoding="async"
>

<!-- Defer non-critical third-party scripts (analytics, chat, pixels) -->
<script src="https://example-analytics.com/tracker.js" defer></script>

<!-- Load non-critical CSS asynchronously to unblock render -->
<link rel="preload" href="/css/non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/non-critical.css"></noscript>

Paste these patterns into your <head> and image markup to address the most common Lighthouse failures.

Meta description too longQuick win

Shorten your meta description to 150–160 characters so Google displays it in full instead of cutting it off with "…" in search results.

On Next.js (Headless)

  1. Locate every page that sets a meta description — this is either a `generateMetadata()` export (App Router: `app/*/page.tsx`) or a `<Head><meta name='description' .../></Head>` block (Pages Router: `pages/*/*.tsx`).
  2. Create a shared utility `lib/seo.ts` with a `truncateDescription(raw: string, max = 155)` helper that hard-trims the raw string at the last word boundary before `max` characters, so no description ever exceeds 155 characters.
  3. Replace every hard-coded or CMS-sourced description value with `truncateDescription(rawDescription)` before it is passed to `generateMetadata` or `next/head`.
  4. For the App Router, return the trimmed value inside the `description` field of the `Metadata` object from `generateMetadata()`; for the Pages Router, set it as the `content` prop of `<meta name='description' …/>` inside `<Head>`.
  5. Run `next build` and inspect the rendered HTML with `curl` or browser DevTools to confirm every `<meta name='description'>` is ≤ 155 characters.
  6. Optionally add a Jest/Vitest unit test for `truncateDescription` to prevent regressions when CMS copy changes.
Code · Generic HTML (html)
<meta name="description" content="Award-winning Bang & Olufsen headphones, speakers and TVs. Visit our Malaysia showroom at The Gardens Mall, Level 2. Free shipping on orders over RM500.">

Place this inside the <head> element of every page. Replace the content value with your page-specific description (max 160 characters).

Meta description too shortModerate effort

Expand your meta description to 120–160 characters so Google displays a compelling, complete snippet in search results that drives more clicks to your store.

On Next.js (Headless)

  1. Audit all route segments (app/ router) or page files (pages/ router) for missing or short `description` values — anything under 120 characters needs to be expanded to 120–160 characters.
  2. For the App Router: open (or create) each route's `layout.tsx` / `page.tsx` and export either a static `metadata` object or an async `generateMetadata()` function. Set the `description` field to a unique, keyword-rich sentence between 120–160 characters that summarises the page content.
  3. For the Pages Router: open each page file and use the `<Head>` component from `next/head`. Add or update the `<meta name='description' content='...' />` tag with a 120–160 character string.
  4. For dynamic routes (e.g. `/products/[slug]`), fetch the product/collection data inside `generateMetadata()` and build the description string programmatically — include the product name, key attributes, and a call-to-action to ensure uniqueness and adequate length.
  5. Add a shared `truncateDescription(text, max = 160)` utility so that any CMS/API-sourced description that exceeds 160 characters is safely trimmed, and a `padDescription()` guard logs a warning when the value is below 120 characters during development.
  6. Run `next build` and verify with browser DevTools or a meta-tag checker that every rendered `<meta name='description'>` is between 120–160 characters before deploying.
Code · Generic HTML (html)
<meta name="description" content="Shop the official Nike national team jerseys — advanced Dri-FIT heat tech meets bold heritage design. Free shipping on orders over $75. Explore the full collection now.">

Place this inside the <head> element of your HTML page. Replace the content value with your 120–160 character description.

Missing canonicalModerate effort

Add a self-referencing canonical tag to every page so Google knows which URL is the definitive version of that content.

On Next.js (Headless)

  1. Identify your router: App Router (app/) uses `generateMetadata` / `metadata` exports; Pages Router (pages/) uses the `<Head>` component from `next/head`. Apply the correct approach for each route type.
  2. Set a NEXT_PUBLIC_SITE_URL environment variable (e.g. https://www.example.com) in .env.local and in your hosting provider's env config — this is the base used to build every canonical URL.
  3. App Router — In every page.tsx (or layout.tsx for shared defaults), export a `generateMetadata` function that returns `alternates.canonical` set to the page's full absolute URL. For dynamic routes, receive `params` and construct the URL accordingly.
  4. Pages Router — In every page file, add a `<Head>` block containing `<link rel='canonical' href={canonicalUrl} />`, where `canonicalUrl` is built from `NEXT_PUBLIC_SITE_URL` + `useRouter().asPath` (strip query strings as needed).
  5. For the Pages Router, extract the logic into a reusable `<CanonicalTag />` component and drop it into your shared `<Layout>` component so every page gets it automatically without repetition.
  6. Verify the output with `curl -s <page-url> | grep canonical` or the browser DevTools Elements panel — each page must render exactly one `<link rel='canonical'>` pointing to its own clean, absolute URL.
Code · Generic HTML (html)
<link rel="canonical" href="https://www.yourstore.com/your-page/" />

Paste this inside the <head> element of every page. Replace the href value with the full, absolute preferred URL of that specific page.

Missing h1Quick win

Add a single, descriptive H1 tag to every page that currently lacks one, clearly stating the page's main topic.

On Next.js (Headless)

  1. Audit every route in your `app/` (or `pages/`) directory for a missing `<h1>`. Use browser DevTools or a script (`grep -r '<h1' ./app`) to find pages with zero H1 elements.
  2. For each page component (e.g. `app/products/[slug]/page.tsx`, `app/collections/[handle]/page.tsx`, `app/page.tsx`), add exactly ONE `<h1>` near the top of the rendered JSX. Pull the text from your data-fetch result (product title, collection name, CMS headline) so it is unique and descriptive per page.
  3. Create a shared `PageHeading` component in `components/PageHeading.tsx` to enforce consistent styling and guarantee a single H1 is never accidentally duplicated across layouts.
  4. Ensure your root `app/layout.tsx` does NOT contain an `<h1>` — headings in the shared layout would produce duplicate H1s on every page.
  5. For statically or dynamically rendered pages using `generateStaticParams` / `generateMetadata`, confirm the H1 text matches (or is derived from) the `title` metadata field so signal consistency is maintained for search engines.
  6. Run Lighthouse or an axe accessibility audit after deploying to verify each route now reports exactly one H1 with no empty text content.
Code · Generic HTML (html)
<h1>Women's Running Shoes</h1>

Place exactly one of these per page, as the first heading in the main content area.

Missing meta descriptionModerate effort

Write and add a unique meta description (120–160 characters) to every page that is missing one, so Google can display an accurate, compelling snippet in search results.

On Next.js (Headless)

  1. Decide which router you use: App Router (app/) uses `generateMetadata` or a static `metadata` export; Pages Router (pages/) uses `next/head` with a `<meta name='description'>` tag.
  2. For App Router: open (or create) each page/layout file (e.g. `app/page.tsx`, `app/products/[slug]/page.tsx`). Export a `generateMetadata` async function that fetches page-specific data and returns a `description` string of 120–160 characters.
  3. For Pages Router: open each page file (e.g. `pages/index.tsx`, `pages/products/[slug].tsx`). Inside the component, add a `<Head>` import from `next/head` and render `<meta name='description' content={description} />` using data already fetched in `getStaticProps` / `getServerSideProps`.
  4. Never hard-code the same description across pages. Derive it from real content: product short description, category intro text, or a CMS field — then trim/truncate to stay within 120–160 characters.
  5. Add a shared `truncateDescription(text: string, max = 155): string` utility (e.g. `lib/seo.ts`) so every page applies the same character-limit logic consistently.
  6. Audit with `next build` output or a tool like Screaming Frog / Lighthouse after deploying to confirm every route emits a unique, non-empty description tag.
Code · Generic HTML (html)
<head>
  <meta charset="UTF-8">
  <meta name="description" content="Shop our full range of handmade leather wallets. Free shipping on orders over $50. Slim, durable & personalised — order yours today.">
  <title>Handmade Leather Wallets | YourStoreName</title>
</head>

Place this tag inside the <head> element of every HTML page. Keep the content between 120–160 characters.

Missing og descriptionModerate effort

Add an og:description meta tag to every page so that social media platforms display a compelling, accurate preview when your pages are shared.

On Next.js (Headless)

  1. Identify whether your project uses the App Router (`app/`) or Pages Router (`pages/`). The fix differs slightly between them — both approaches are covered in the snippet below.
  2. App Router: Open (or create) the `generateMetadata` export in every `app/**/page.tsx` (and `app/layout.tsx` for site-wide defaults). Add an `openGraph.description` field that is populated from your page/product data fetched at build or request time.
  3. Pages Router: In every `pages/*/.tsx` file, import `next/head` and add a `<meta property='og:description' content={description} />` tag inside the `<Head>` component, using the page's fetched data for the value.
  4. Create a shared `getSeoDescription(text: string, maxLen = 160)` utility in `lib/seo.ts` that trims raw copy to a safe length, strips HTML tags, and falls back to a site-wide default — then call it from every page's metadata logic.
  5. For dynamic routes (e.g. `app/products/[slug]/page.tsx`), fetch the product/collection description inside `generateMetadata` and pass it through `getSeoDescription` before assigning it to `openGraph.description`.
  6. Run `npx next build` and inspect the rendered HTML (`<meta property='og:description'>`) or use the Open Graph debugger (https://developers.facebook.com/tools/debug/) to confirm the tag appears on every page type.
Code · Generic HTML (html)
<head>
  <!-- Open Graph / Social Sharing -->
  <meta property="og:type"        content="website" />
  <meta property="og:url"         content="https://www.example.com/page-url/" />
  <meta property="og:title"       content="Page Title – Brand Name" />
  <meta property="og:description" content="A concise, compelling summary of this page in 100–160 characters that encourages clicks." />
  <meta property="og:image"       content="https://www.example.com/images/social-preview.jpg" />
</head>

Place all Open Graph tags inside the <head> element of every page.

Missing og imageModerate effort

Add an og:image meta tag to every page so that social media platforms and messaging apps display a rich, branded image preview whenever someone shares your store's link.

On Next.js (Headless)

  1. Place a high-quality branded fallback image (e.g. og-image.jpg, 1200×630 px) inside the /public directory so it is served statically at /og-image.jpg.
  2. In your root layout (app/layout.tsx) export a `metadata` object (or `generateMetadata`) that sets `openGraph.images` with the absolute URL of that fallback image — use `metadataBase` so Next.js can resolve relative paths to absolute URLs automatically.
  3. Override `openGraph.images` inside every dynamic route's `generateMetadata` function (e.g. app/products/[slug]/page.tsx) to supply a product-specific image pulled from your commerce API, with the static fallback as a safe default.
  4. For any remaining static pages (e.g. app/about/page.tsx, app/collections/page.tsx), export a local `metadata` object with `openGraph.images` set to at least the fallback URL.
  5. Run `next build` and verify the rendered HTML contains `<meta property="og:image" content="https://…">` with a fully-qualified URL on every page type.
  6. Optionally add `twitter:image` via `metadata.twitter.images` in the same objects so Twitter/X card previews also display the image.
Missing og titleQuick win

Add an og:title meta tag to every page so your store's title appears correctly when shared on social media platforms like Facebook, LinkedIn, and Pinterest.

On Next.js (Headless)

  1. Determine which router you are using: App Router (app/) or Pages Router (pages/). The fix differs slightly between them — use `generateMetadata` for App Router and `next/head` for Pages Router.
  2. App Router — open (or create) your root layout at `app/layout.tsx` and add a top-level `metadata` export with an `openGraph.title` field. For dynamic pages (e.g. `app/products/[slug]/page.tsx`), export an async `generateMetadata()` function that fetches the product and returns a per-page `openGraph.title`.
  3. Pages Router — import `Head` from `next/head` in every page component (e.g. `pages/index.tsx`, `pages/products/[slug].tsx`) and add `<meta property="og:title" content={title} />` inside the `<Head>` block.
  4. Ensure the `og:title` value is dynamic and meaningful: use the product name on product pages, collection name on collection pages, and a branded fallback (e.g. 'My Store') on the homepage/404.
  5. Run `next build` and inspect the rendered HTML (or use a social-media debugger such as the Facebook Sharing Debugger) to confirm the `og:title` tag is present and correct on every page type.
  6. Optionally centralise defaults in `app/layout.tsx` using `metadataBase` + `openGraph` so every child page inherits a sensible fallback and only needs to override what is unique.
Code · Generic HTML (html)
<head>
  <!-- Other head tags -->
  <meta property="og:title" content="Amazing Product Name | Your Store Name" />
  <meta property="og:type" content="website" />
  <meta property="og:url" content="https://www.yourstore.com/page-url" />
  <meta property="og:description" content="Short description of this page for social sharing." />
  <!-- Other head tags -->
</head>

Place inside the <head> section of every HTML page. Replace the content value with your actual page title.

Missing titleQuick win

Add a unique, descriptive title tag (30–60 characters) to every page that is missing one.

On Next.js (Headless)

  1. Decide which router you are using: App Router (app/) uses `generateMetadata` or a static `metadata` export; Pages Router (pages/) uses the `<Head>` component from `next/head`.
  2. For the App Router, open (or create) every `page.tsx` / `layout.tsx` that is missing a title. Export a `generateMetadata` function (for dynamic titles) or a `metadata` object (for static titles). The title must be 30–60 characters, unique per page, and descriptive.
  3. Use the `template` field in a root `layout.tsx` `metadata` export so that every child page automatically appends your brand name — this prevents accidentally leaving a page untitled.
  4. For the Pages Router, import `Head` from `next/head` inside each page component and render `<title>` inside it. For dynamic pages (e.g. `pages/products/[slug].tsx`) derive the title from fetched data in `getStaticProps` / `getServerSideProps`.
  5. Run `next build` and check the build output or use a crawler (e.g. `npx source-map-explorer` or browser DevTools) to confirm every route's `<title>` is present, unique, and within the 30–60 character limit.
  6. Add a CI lint rule or a Playwright/Cypress smoke test that asserts `document.title` is non-empty on every critical route to prevent regressions.
Code · Generic HTML (html)
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Slim Bifold Leather Wallet | BrandName</title>
</head>
<body>
  <!-- page content -->
</body>
</html>

Place this inside the <head> element of every HTML page. Keep the text between 30–60 characters.

Multiple h1Moderate effort

Remove all but one H1 tag from each page so every page has exactly one clear, descriptive primary heading.

On Next.js (Headless)

  1. Audit every page and layout file in your `app/` (or `pages/`) directory for `<h1>` tags. Common offenders are shared layout components (e.g. `app/layout.tsx`, `components/Header.tsx`, `components/Hero.tsx`) that each render their own `<h1>` alongside a page-level `<h1>`. Use a global search: `grep -rn '<h1' ./app ./components ./pages`.
  2. Establish a single rule: *only the page-level component* (e.g. `app/products/[slug]/page.tsx`) is allowed to render an `<h1>`. Shared layout wrappers (`app/layout.tsx`), headers, hero banners, and section components must use `<h2>`–`<h6>` or a semantically neutral element instead.
  3. Create a reusable `<PageHeading>` component (see snippet below) that always renders an `<h1>`. Import and use it in every page component as the single primary heading, passing the page-specific title as a prop. Delete any existing inline `<h1>` tags from those page files.
  4. In shared/global components (`Header`, `Hero`, `Banner`, etc.) demote any existing `<h1>` tags to the appropriate level (`<h2>` for section titles, `<p>` for taglines, etc.) so they never compete with the page-level `<h1>`.
  5. For dynamic routes (e.g. PDP, PLP, blog posts), derive the `<PageHeading>` text from fetched data — for example the product name, category name, or article title — so every unique URL has a unique, descriptive primary heading.
  6. After deploying, validate each page type in a browser DevTools console with `document.querySelectorAll('h1').length` (should return `1`) and re-run your SEO audit tool to confirm the `multiple_h1` issue is resolved.
Code · Generic HTML (html)
<!-- CORRECT: One H1 per page -->
<h1>Blue Running Shoes – Men's Lightweight Trainers</h1>

<!-- Major sections use H2 -->
<h2>Product Details</h2>
<p>Breathable mesh upper, responsive foam midsole...</p>

<h2>Customer Reviews</h2>
<!-- Sub-sections within a major section use H3 -->
<h3>Top Review: 5 Stars</h3>
<p>Best shoes I've ever bought!</p>

<!-- WRONG: Multiple H1 tags — do NOT do this -->
<!--
<h1>Blue Running Shoes</h1>
<h1>Product Details</h1>
<h1>Customer Reviews</h1>
-->

Correct semantic heading structure for an ecommerce page — one H1, everything else H2 or lower.

Noindex detectedQuick win

Remove or replace the noindex directive on any page you want to appear in Google search results.

On Next.js (Headless)

  1. Audit every route for `noindex` signals: search the entire project for the strings `noindex`, `robots`, and `X-Robots-Tag` across all files in `app/` or `pages/`, `next.config.js`, middleware, and any HTTP-header configuration.
  2. For App Router routes (`app/`): open the relevant `page.tsx` (or `layout.tsx`) and remove or replace the `noindex` value in the `robots` field of the exported `metadata` object / `generateMetadata()` return value. Set `index: true, follow: true` instead.
  3. For Pages Router routes (`pages/`): open the relevant page file and remove the `<meta name='robots' content='noindex' />` (or any variant) tag inside the `<Head>` component imported from `next/head`.
  4. Check `next.config.js` for custom `headers()` that set `X-Robots-Tag: noindex` and delete or change those entries to `X-Robots-Tag: index, follow` for the affected paths.
  5. Check any middleware (`middleware.ts`) that injects `X-Robots-Tag` response headers and remove the `noindex` value for the affected routes.
  6. Deploy and verify with Google's Rich Results Test or a `curl -I` on the live URL that no `X-Robots-Tag: noindex` header and no `<meta name='robots' content='noindex'>` tag is present.
Code · Generic HTML (html)
<meta name="robots" content="index,follow">

Place inside <head>. This is the CORRECT tag that allows indexing and link-following.

Non self canonicalQuick win

Audit every page whose canonical tag points to a different URL, then either correct it to self-reference or confirm the cross-reference is intentional and correct.

On Next.js (Headless)

  1. 1. Create a canonical utility (`lib/canonical.ts`) that derives the canonical URL from the incoming request — using `NEXT_PUBLIC_SITE_URL` plus the current pathname — so every page always self-references unless you explicitly override it.
  2. 2. For the App Router: replace every hard-coded or missing `alternates.canonical` inside `generateMetadata()` with a call to this utility, ensuring the returned value always matches the page's own public URL.
  3. 3. For the Pages Router: in `_app.tsx` (or in each page), replace any `<link rel='canonical'>` inside `next/head` that points to a different URL with `<link rel='canonical' href={canonicalUrl(router.asPath)} />`.
  4. 4. Audit dynamic routes (e.g. `[slug]`, `[handle]`, `[...path]`) — these are the most common source of non-self canonicals. Confirm that the `params`-derived URL in `generateMetadata` exactly matches the final public URL (no trailing-slash mismatch, no www/non-www mismatch).
  5. 5. If a page intentionally cross-canonicalises to another URL (e.g. a paginated variant pointing to page 1), add a `// canonical: intentional cross-reference` comment and document the reason so future audits can skip it safely.
  6. 6. Run a post-deploy audit with a script (or a crawler) that fetches every page, extracts the `<link rel='canonical'>` value, and asserts it equals `location.href` — fail the CI build if any unintentional mismatch is found.
Code · Generic HTML (html)
<link rel="canonical" href="https://www.example.com/your-page-url/">

Place this tag inside the <head> element of every page. The href must exactly match the page's own preferred URL.

Page not foundModerate effort

Fix broken internal links pointing to pages that return a 404 error by either removing those links or setting up a 301 redirect from the missing URL to the correct replacement page.

On Next.js (Headless)

  1. Audit all internal `<Link href='...'>` and `<a href='...'>` usages across your components (e.g. `components/`, `app/`, `pages/`) — replace any broken destination with the correct live URL, or remove the link entirely if no replacement exists.
  2. For every broken URL that must keep working (e.g. old slugs, renamed routes), declare a permanent 301 redirect in `next.config.js` (or `next.config.ts`) using the `redirects()` async function — this is evaluated at the Edge/server before any page render.
  3. If broken URLs are data-driven (e.g. old product/category slugs stored in a CMS), fetch the redirect map at build time inside `redirects()` or generate them via `getStaticProps`/`generateStaticParams` and issue a `{ redirect: { destination, permanent: true } }` return value from the page itself.
  4. For catch-all runtime redirects (unknown slugs), add a custom `app/not-found.tsx` (App Router) or `pages/404.tsx` (Pages Router) that uses `useEffect` + `router.replace()` or a server-side lookup to forward the user to the nearest correct page.
  5. After deploying, re-crawl with a tool (e.g. Screaming Frog, `next build` output warnings) to confirm no internal link still resolves to a 404; also verify the `Location` header returns 301 (not 302) for all redirected paths.
Code · Generic HTML (html)
<!-- BEFORE: broken internal link -->
<a href="/old-products/discontinued-item">Shop Now</a>

<!-- AFTER: updated to correct URL -->
<a href="/products/replacement-item">Shop Now</a>

Update an internal anchor link to point to the correct URL instead of the broken one.

Page speed warningModerate effort

Reduce your page load time to under 1.5 seconds by optimizing images, eliminating render-blocking resources, and enabling caching and compression.

On Next.js (Headless)

  1. Use next/image for ALL product and hero images — set `priority` on above-the-fold images, always supply `width`/`height` or `fill`, and configure `remotePatterns` in `next.config.ts` so images are auto-converted to WebP/AVIF and lazy-loaded by default.
  2. Eliminate render-blocking fonts by switching to `next/font` (e.g. `next/font/google`). Call the font loader at the module level, apply the returned `className` to your root layout, and remove any manual `<link rel='stylesheet'>` font imports from `app/layout.tsx`.
  3. Enable HTTP response compression and long-lived caching headers in `next.config.ts`: set `compress: true`, add `Cache-Control` headers for static assets via the `headers()` async function, and leverage Next.js's built-in ISR (`revalidate`) to avoid full SSR on every request.
  4. Code-split and lazy-load heavy below-the-fold components (carousels, modals, review widgets) with `next/dynamic` and `{ ssr: false, loading: () => <Skeleton /> }` so their JS is not part of the critical bundle.
  5. Add a `<link rel='preconnect'>` and `<link rel='dns-prefetch'>` for every third-party origin (CDN, analytics, payment scripts) inside `app/layout.tsx` via the Next.js `<Head>` equivalent metadata exports or direct JSX in the root layout.
  6. Run `next build && next start` and audit with Lighthouse / `@next/bundle-analyzer` — set `ANALYZE=true` in `.env.local`, identify large chunks, and either tree-shake or dynamically import them.
Redirect chainModerate effort

Update every internal link that passes through a redirect chain to point directly at the final destination URL, eliminating all intermediate hops.

On Next.js (Headless)

  1. Audit every internal `<Link href='...'>` and plain `<a href='...'>` across your components (e.g. `components/`, `app/`, `pages/`) plus any `router.push()` / `router.replace()` calls to collect all internal URLs that currently resolve through a redirect chain.
  2. Centralise all internal hrefs in a single canonical-URL map file (e.g. `lib/internalLinks.ts`). Each key is the old/intermediate URL and each value is the resolved final destination URL. Pull the data from your CMS or commerce API at build time if the URLs are dynamic.
  3. Create a utility hook `useCanonicalHref` (or a pure helper `resolveHref`) that looks up any given href in the map and returns the final URL, so every component shares the same resolution logic.
  4. Replace every hard-coded internal `<Link href='...'>` with `<Link href={resolveHref('/old-path')}>` and every programmatic `router.push('/old-path')` with `router.push(resolveHref('/old-path'))`. Do the same for `<a href='...'>` anchors.
  5. In `next.config.js`, audit the `redirects()` array: any redirect whose `source` is still referenced by an internal link should be removed once all internal links are updated, preventing the chain from being re-introduced.
  6. Run `next build` and use a crawler (e.g. Screaming Frog or a custom script using `fetch` with `redirect: 'manual'`) against the build output to verify that no internal link returns a 3xx status code before deploying.
Code · Generic HTML (html)
<!-- BEFORE: link points to an intermediate URL that itself redirects -->
<a href="https://example.com/old-page">Our Story</a>

<!-- AFTER: link points directly to the final destination URL -->
<a href="https://example.com/about-us">Our Story</a>

Replace every intermediate redirect URL in your anchor href with the final destination URL — the one that returns HTTP 200 with no further redirects.

Search results indexableModerate effort

Add a noindex robots meta tag to all internal search result pages and block the search URL path in robots.txt to prevent Google from crawling and indexing thin, duplicate search results.

On Next.js (Headless)

  1. Identify your search results route. In the App Router it is typically `app/search/page.tsx` (or `app/search/[query]/page.tsx`); in the Pages Router it is `pages/search.tsx` (or `pages/search/[query].tsx`).
  2. App Router: Export a `generateMetadata` function from the search page that returns `robots: { index: false, follow: false }`. This renders `<meta name='robots' content='noindex,nofollow'>` automatically. Pages Router: Use `next/head` to inject `<meta name='robots' content='noindex,nofollow'>` inside the page component.
  3. Create or edit `public/robots.txt` (the static file served at `/robots.txt`) and add a `Disallow: /search` rule under the `User-agent: *` block to stop crawlers from even requesting the URL.
  4. If search is served from multiple paths (e.g. `/search`, `/catalogsearch/result`, `/s`), add a `Disallow` line for each path and mirror a `noindex` tag in every corresponding page file.
  5. Deploy and verify with Google Search Console's URL Inspection tool that the page returns `noindex` in the HTTP response and that Googlebot obeys the `robots.txt` disallow rule.
Code · Generic HTML (html)
<meta name="robots" content="noindex,follow">

Place this tag inside the <head> element of every search results page template.

Seo category description missingModerate effort

Write a 100–200 word description for every category page explaining what products it contains and who they are for, so Google can rank it for category-level keywords.

On Next.js (Headless)

  1. In your headless CMS or commerce API (e.g. Shopify, Contentful, BigCommerce), ensure every category/collection record has a `description` field of 100–200 words. Populate it for every category that is currently missing one.
  2. Open your category page route file — typically `app/[category]/page.tsx` (App Router) or `pages/category/[slug].tsx` (Pages Router) — and fetch the category description alongside the existing data in `generateStaticParams` / `getStaticProps`.
  3. For the App Router: export a `generateMetadata` function that maps the fetched description to `description` in the returned metadata object, so the `<meta name='description'>` tag is server-rendered for crawlers.
  4. Render the description text visibly on the page inside a semantic `<section>` or `<p>` element below the category heading and above the product grid. Visible body copy carries more ranking weight than meta tags alone.
  5. Add a `Category` JSON-LD block (using `@type: 'ItemList'` or `'CollectionPage'`) with a `description` property via a `<Script type='application/ld+json'>` component so the description is also machine-readable in structured data.
  6. Run `next build` and validate with Google's Rich Results Test and a `curl` of the rendered HTML to confirm the description appears in both the `<meta>` tag and the visible DOM before deploying.
Code · Generic HTML (html)
<section class="category-description">
  <h1>Women's Waterproof Hiking Boots</h1>
  <p>
    Our women's waterproof hiking boots are built for trail runners, weekend
    trekkers, and serious mountaineers who refuse to let wet weather slow them
    down. Every pair in this collection features a fully sealed waterproof
    membrane, aggressive rubber outsoles for grip on loose or muddy terrain,
    and reinforced ankle support to protect against sprains on uneven ground.
    Whether you're day-hiking a local forest trail or taking on a multi-day
    alpine route, you'll find boots here rated for every conditions and
    experience level. Lightweight options keep pack weight low on fast-and-light
    adventures, while insulated models keep feet warm below freezing. Browse the
    full range below to find your perfect fit.
  </p>
</section>

Place this block near the top of your category page, above the product grid.

Seo category description thinModerate effort

Expand your category page description to at least 100 words of genuinely helpful, keyword-rich content that explains what shoppers will find in that category.

On Next.js (Headless)

  1. Locate your category page component — typically `app/categories/[slug]/page.tsx` (App Router) or `pages/categories/[slug].tsx` (Pages Router). This is where category data is fetched and rendered.
  2. Fetch the full category object from your commerce API (e.g., via `generateStaticParams` + `generateMetadata` in App Router, or `getStaticProps` in Pages Router). Ensure the payload includes both a short `description` field AND any extended `longDescription` / `seoDescription` field your CMS/commerce platform exposes.
  3. In the page's JSX, add a visible `<CategoryDescription>` section below the hero/banner that renders the long-form description (≥ 100 words). Wrap it in a semantic `<section>` with an `aria-label` so it is meaningful to screen readers and crawlers.
  4. If the commerce platform only provides a short description, compose a richer fallback string server-side by combining the short description, a keyword-rich mid-section built from category metadata (name, breadcrumb, product count, top attributes), and a call-to-action sentence — all assembled in one template literal before rendering.
  5. Simultaneously update `generateMetadata` (App Router) or the `<Head>` block (Pages Router) so that `description` meta tag also uses a ≥ 150-character excerpt of the same rich content, ensuring on-page copy and meta description are consistent.
  6. Run `next build` and use a crawler (e.g., `npx @next/bundle-analyzer` or Screaming Frog) to verify the rendered HTML contains the full description text in the `<section>` and that the `<meta name='description'>` content is populated correctly on every category URL.
Code · Generic HTML (html)
<section class="category-description">
  <h2>Shop Our Women's Running Shoes</h2>
  <p>
    Discover our curated collection of women's running shoes, designed for every
    type of runner — from casual joggers to marathon enthusiasts. Whether you
    need extra cushioning for long-distance road running, a lightweight racer
    for speed training, or a grippy trail shoe for off-road adventures, you'll
    find the perfect fit here.
  </p>
  <p>
    We stock top brands including Brooks, ASICS, New Balance, and Hoka, all
    rigorously tested for comfort, durability, and performance. Each pair is
    available in a wide range of widths and sizes, so you never have to
    compromise on fit. Browse by surface type, cushion level, or pronation
    support to narrow down your ideal shoe in seconds.
  </p>
  <p>
    Not sure where to start? Check out our
    <a href="/guides/running-shoe-guide">Running Shoe Buying Guide</a> or
    explore our <a href="/collections/running-accessories">Running Accessories</a>
    collection to complete your kit.
  </p>
</section>

Paste this pattern into any category page template. Replace placeholder text with your real content.

Seo category emptyQuick win

Either populate the empty category with products, or add a noindex tag and remove it from your navigation so Google does not waste crawl budget on a page with no content.

On Next.js (Headless)

  1. In your category page route (e.g. `app/categories/[slug]/page.tsx` or `pages/categories/[slug].tsx`), fetch the product list for the category at build/request time and check whether `products.length === 0`.
  2. If using the App Router, export a `generateMetadata` function that returns `robots: { index: false, follow: false }` when the category is empty, and `robots: { index: true, follow: true }` when it has products.
  3. If using the Pages Router, use `next/head` inside the page component to conditionally render `<meta name='robots' content='noindex,nofollow' />` when the product list is empty.
  4. Remove empty category pages from your sitemap (e.g. `app/sitemap.ts` or a custom `pages/sitemap.xml.tsx`) by filtering out slugs whose product count is 0.
  5. Remove empty category links from your navigation component (e.g. `components/Navigation.tsx`) by filtering the category list to only those with `productCount > 0` before rendering `<Link>` elements.
  6. Optionally, redirect empty category URLs to the parent category or home page using Next.js `redirect()` (App Router) or `getServerSideProps` + `res.redirect` (Pages Router) to avoid soft 404s entirely.
Code · Generic HTML (html)
<meta name="robots" content="noindex, follow">

Place inside the <head> element of an empty category page to prevent Google from indexing it.

Seo category sparseModerate effort

Add more products to sparse category pages, or consolidate very thin categories into broader, related ones so every indexed category page delivers genuine value to shoppers and search engines.

On Next.js (Headless)

  1. In your category route (e.g. app/categories/[slug]/page.tsx or pages/categories/[slug].tsx), fetch the product count for the category at build/request time. If the count falls below your minimum threshold (e.g. < 6 products), mark the page as noindex and return a canonical pointing to the closest parent/related category so thin pages are de-indexed without being deleted.
  2. Add generateMetadata (App Router) or a next/head block (Pages Router) that emits <meta name='robots' content='noindex,follow'> and <link rel='canonical' href='/categories/[parent-slug]'> conditionally when the product count is below the threshold.
  3. In generateStaticParams (App Router) or getStaticPaths (Pages Router), filter out categories whose product count is below the threshold so they are never statically built and indexed in the first place — this is the primary fix.
  4. For categories just above the threshold, fetch and display related/cross-sell products from sibling or parent categories using your commerce API (e.g. Shopify Storefront API collectionByHandle + a second query for related collections) to pad content and increase page value.
  5. Add a canonical self-referencing tag to all healthy category pages (count >= threshold) to prevent duplicate-index issues when pagination or filters create variant URLs.
  6. Add a JSON-LD BreadcrumbList + ItemList structured-data block to every category page so search engines understand the taxonomy and can index rich results, reinforcing the page's topical authority.
Code · Generic HTML (html)
<meta name="robots" content="noindex, follow">

Add inside <head> of any sparse category page you want excluded from Google's index while still being followable by crawlers.

Seo facet url not canonicalizedModerate effort

Add a canonical tag to every filtered, sorted, and paginated collection/category URL pointing back to the clean (unfiltered) category URL, so Google consolidates duplicate pages and focuses crawl budget on your real content.

On Next.js (Headless)

  1. Locate (or create) your collection/category route. In the App Router this is typically `app/collections/[slug]/page.tsx`; in the Pages Router it is `pages/collections/[slug].tsx`. All canonical logic below targets the App Router — see the Pages Router note at the bottom of the snippet for the equivalent.
  2. Extract the clean canonical URL from the route segment alone (the `slug` param) — deliberately ignoring every search-param that represents a facet, sort order, or page number (e.g. `?color=red&sort=price&page=2`). Build the full absolute URL with your `NEXT_PUBLIC_SITE_URL` environment variable.
  3. In the App Router, export an async `generateMetadata` function from the same `page.tsx` file. Set `alternates.canonical` to the clean URL string — Next.js renders this as `<link rel='canonical'>` in `<head>` automatically.
  4. Ensure the `generateMetadata` function receives the `searchParams` prop but does NOT include any of those params in the canonical href — the canonical must always point to the unfiltered base URL regardless of which filters or page numbers are active.
  5. For the Pages Router, import `next/head` and render `<link rel='canonical' href={canonicalUrl} />` inside `<Head>` in the page component, applying the same logic: derive `canonicalUrl` from `router.query.slug` only, ignoring all other query params.
  6. Deploy and validate with 'Inspect URL' in Google Search Console or by viewing page source — confirm `<link rel='canonical'>` points to the clean category URL on filtered, sorted, and paginated variants.
Code · Generic HTML (html)
<head>
  <!-- All other head tags -->
  <link rel="canonical" href="https://www.example.com/collections/hoops">
</head>

Place this inside the <head> of every filtered/sorted/paginated category page. The href must be the full, absolute clean URL (no query parameters).

Seo product content client renderedLarger project

Server-render your product descriptions in the initial HTML so that all crawlers — including Google, AI search bots, and social media scrapers — can read your content without executing JavaScript.

On Next.js (Headless)

  1. Locate your product page component. In the App Router it lives at app/products/[slug]/page.tsx; in the Pages Router it lives at pages/products/[slug].tsx. Confirm the product description is currently fetched client-side (inside a useEffect, SWR call, or similar).
  2. App Router: Convert the page to an async React Server Component (RSC) and fetch product data directly inside the component body using your commerce API/SDK. Remove any 'use client' directive from the page file itself. Pages Router: Replace any client-side fetch with getStaticProps (preferred for SSG) or getServerSideProps (for SSR), returning the product description as a prop.
  3. Render the description directly in the JSX returned by the server component / page — e.g. <div dangerouslySetInnerHTML={{ __html: product.descriptionHtml }} /> or a plain <p>{product.description}</p>. This guarantees the text is present in the initial HTML payload served to crawlers.
  4. If rich/HTML descriptions are used, sanitize the markup server-side with a library such as `isomorphic-dompurify` before passing it to dangerouslySetInnerHTML, keeping XSS risk zero.
  5. Add structured data (JSON-LD) for the product in the same server component using a <script type='application/ld+json'> tag so AI and social scrapers also get machine-readable content.
  6. Verify the fix: run `curl -s https://your-domain.com/products/your-slug | grep -i 'description'` — the description text must appear in the raw HTML response, not be empty.
Seo product description minimalModerate effort

Expand your product descriptions to at least 300 words by adding specifications, use cases, and benefits so search engines have enough content to rank and shoppers have enough information to buy.

On Next.js (Headless)

  1. Locate your product detail page component — typically `app/products/[slug]/page.tsx` (App Router) or `pages/products/[slug].tsx` (Pages Router). This is where the product description is rendered.
  2. Audit your CMS/commerce API query: extend the product fetch (e.g. `getProductBySlug`) to also retrieve or accept structured fields for `specifications` (key/value pairs), `useCases` (array of strings), and `benefits` (array of strings). If the commerce backend (e.g. Shopify, Commercetools) stores them in metafields or custom attributes, map them into a typed `Product` interface.
  3. Create a reusable `<ProductDescription>` component at `components/product/ProductDescription.tsx` that composes the raw description with the structured sections (specifications table, use-cases list, benefits list) so the combined rendered text reliably exceeds 300 words.
  4. Replace the bare `<p>{product.description}</p>` (or `dangerouslySetInnerHTML`) in your PDP with `<ProductDescription product={product} />`. Because this is a Server Component the full text is present in the HTML payload — search engines index it without JavaScript execution.
  5. Verify the output with `curl` or View Source: confirm the rendered HTML contains all three sections and that a word-count check (`wc -w`) reports ≥ 300 words of visible copy.
  6. Update your JSON-LD Product schema (in the same page file) to populate the `description` field with the same expanded copy so AEO/rich-result parsers also see the richer content.
Code · Generic HTML (html)
<section class="product-description">
  <h2>About the [Product Name]</h2>
  <p>
    The [Product Name] is a [category] designed for [target audience]. Whether you need
    [use case 1] or [use case 2], this [product] delivers [core benefit] without
    [common pain point].
  </p>

  <h3>Key Features &amp; Specifications</h3>
  <ul>
    <li><strong>Material:</strong> [e.g. 100% organic cotton, BPA-free polypropylene]</li>
    <li><strong>Dimensions:</strong> [W × H × D] / Weight: [X kg / X lbs]</li>
    <li><strong>Colour options:</strong> [Colour 1], [Colour 2], [Colour 3]</li>
    <li><strong>Compatibility:</strong> [e.g. Works with iOS 14+ and Android 10+]</li>
    <li><strong>Warranty:</strong> [X-year manufacturer warranty]</li>
  </ul>

  <h3>Why Shoppers Choose [Product Name]</h3>
  <p>
    Unlike [generic alternatives], the [Product Name] [specific differentiator]. This means
    [tangible benefit for the shopper], saving you [time / money / effort].
  </p>
  <p>
    Ideal for [persona / scenario], the [Product Name] has been tested to [standard or
    certification] and ships ready to use — no assembly required.
  </p>

  <h3>How to Use</h3>
  <p>
    [Step-by-step or scenario-based usage instructions in 2–4 sentences. Include any
    important setup, safety, or care notes here.]
  </p>

  <h3>In the Box</h3>
  <ul>
    <li>1 × [Product Name]</li>
    <li>1 × [Accessory or manual]</li>
    <li>1 × [Charging cable / carry bag / etc.]</li>
  </ul>
</section>

Structural template for a product description section — paste inside your product page's main content area.

Seo product description missingModerate effort

Write and publish a 200–400 word product description covering what the product is, its key specs, who it's for, and how to use it.

On Next.js (Headless)

  1. Fetch the full product data (including a rich `description` field) from your headless commerce API inside `app/products/[slug]/page.tsx` using `generateMetadata` (for the `<meta name='description'>` tag) and the page's async Server Component (for the visible body copy).
  2. Ensure your CMS / commerce back-end stores a 200–400 word description covering: what the product is, key specs, target audience, and usage instructions. Map that field (e.g. `product.descriptionHtml` or `product.description`) in your GraphQL/REST query.
  3. Render the rich description in the product page JSX inside a semantic `<section>` with a visually-hidden or visible `<h2>` (e.g. 'About this product') so crawlers associate the copy with the product.
  4. Pass the plain-text version (strip HTML tags) into `generateMetadata` → `description` (keep it under 160 chars for the meta tag, but serve the full copy on-page).
  5. Add a JSON-LD `Product` schema block via a `<Script type='application/ld+json'>` component (next/script) in the same page file, embedding the full description in the `description` field so it surfaces in AI/AEO answer engines.
  6. Run `next build` and validate with Google's Rich Results Test and an `<meta>` inspector to confirm the description appears both in the rendered HTML and in the structured-data payload.
Code · Generic HTML (html)
<section class="product-description">
  <h2>About This Product</h2>
  <p>
    The <strong>Trailblazer 500 Waterproof Hiking Boot</strong> is a lightweight,
    full-grain leather boot built for all-day comfort on rugged mountain trails.
    Its Gore-Tex membrane keeps feet dry in rain and creek crossings while the
    Vibram outsole delivers confident grip on wet rock and loose scree.
  </p>
  <p>
    Designed for day hikers and weekend backpackers who want reliable protection
    without the weight of a mountaineering boot, the Trailblazer 500 strikes the
    ideal balance between support and agility. The padded collar and gusseted
    tongue prevent trail debris from entering the boot, and the cushioned EVA
    midsole absorbs impact mile after mile.
  </p>
  <h3>Key Specifications</h3>
  <ul>
    <li><strong>Upper:</strong> Full-grain leather with Gore-Tex waterproof lining</li>
    <li><strong>Outsole:</strong> Vibram Megagrip rubber</li>
    <li><strong>Weight:</strong> 680 g / pair (size UK 9)</li>
    <li><strong>Shaft height:</strong> Mid (ankle) cut</li>
    <li><strong>Available widths:</strong> Standard and Wide</li>
  </ul>
  <h3>How to Use</h3>
  <p>
    Lace firmly for ankle support on uneven terrain. Break the boot in over two
    or three shorter walks before a full-day hike. Apply a water-repellent spray
    to the leather upper every 20–30 hours of trail use to maintain waterproofing.
  </p>
</section>

Minimum viable product description markup — paste inside your product page's <main> or article element.

Seo product description thinModerate effort

Expand every product description to at least 300 words of unique, keyword-rich content that answers real shopper questions and gives Google enough text to understand and rank the page.

On Next.js (Headless)

  1. Locate your product page component — typically `app/products/[slug]/page.tsx` (App Router) or `pages/products/[slug].tsx` (Pages Router). This is where product data is fetched and rendered.
  2. Audit the description field coming from your commerce API (e.g. Shopify Storefront API `product.descriptionHtml`, BigCommerce `product.description`, or your custom CMS). If it is under 300 words, enrich it at the data layer OR supplement it in the component with structured editorial content sections (features, FAQs, use-cases).
  3. Create a reusable `<ProductDescription>` component at `components/product/ProductDescription.tsx`. It accepts the raw API description plus optional structured-content blocks (features list, FAQ pairs, usage tips). Render the raw HTML with `dangerouslySetInnerHTML` inside a semantic `<article>` and append the supplemental blocks below it.
  4. Ensure the enriched description is rendered in the server component so the full text is present in the initial HTML payload (no client-only fetch). Use `generateMetadata` to also pull the first ~160 characters into the `<meta name='description'>` tag.
  5. Add a word-count guard in development: if the combined visible text is still under 300 words, throw a warning to the console so content editors are alerted before shipping.
  6. After deploying, validate with Google's Rich Results Test and 'Inspect URL' in Search Console to confirm the expanded description text is indexable.
Code · Generic HTML (html)
<section class="product-description" aria-label="Product description">
  <h2>About This Product</h2>
  <p>
    Write your opening paragraph here (2–3 sentences introducing the product,
    its primary benefit, and who it is for). Aim for a natural, engaging tone
    that mirrors how your customers talk about this type of product.
  </p>

  <h3>Key Features</h3>
  <ul>
    <li><strong>Feature One:</strong> Explain the benefit, not just the spec.</li>
    <li><strong>Feature Two:</strong> Include the detail a shopper needs to decide.</li>
    <li><strong>Feature Three:</strong> Use the words your customers actually search for.</li>
    <li><strong>Feature Four:</strong> Keep each bullet to one clear point.</li>
  </ul>

  <h3>Materials &amp; Specifications</h3>
  <p>
    Describe materials, dimensions, weight, compatibility, or other technical
    details relevant to your product category. Specificity builds trust and
    captures long-tail searches (e.g. "100% organic cotton", "fits 13-inch laptops").
  </p>

  <h3>How to Use</h3>
  <p>
    Walk the customer through setup, application, or use in plain language.
    Answer the most common pre-purchase questions here so shoppers feel confident.
  </p>

  <h3>Why Our Customers Love It</h3>
  <p>
    Reinforce with a sentence or two of social proof context or a unique selling
    point that differentiates this product from generic alternatives.
  </p>
</section>

Ideal semantic structure for a product description section on any platform's product page template.

Seo product missing brandModerate effort

Add a `brand` property to your Product schema markup so search engines can match your listings to brand-specific queries.

On Next.js (Headless)

  1. Locate your product page component — typically `app/products/[slug]/page.tsx` (App Router) or `pages/products/[slug].tsx` (Pages Router).
  2. Ensure your product data fetch (via `generateMetadata`, `getStaticProps`, or a server component) retrieves the brand name from your headless commerce API (e.g. a `brand` or `vendor` field on the product object).
  3. In the same file, define a `productJsonLd` object that includes a `brand` property of type `Brand` with a `name` field populated from the fetched product data.
  4. Inject the JSON-LD into the page using a `<Script>` tag with `type='application/ld+json'` (App Router) or Next.js `<Script>` / a plain `<script>` inside `<Head>` (Pages Router), placing it inside the returned JSX.
  5. Verify the structured data is correct by pasting your page URL into Google's Rich Results Test (https://search.google.com/test/rich-results) and confirming the `brand` field appears under the Product entity.
Seo product missing gtinModerate effort

Add a GTIN (and/or MPN) field to your Product structured data so Google can match your listings to its product knowledge graph for rich results and Google Shopping.

On Next.js (Headless)

  1. Locate your product page file — either `app/products/[slug]/page.tsx` (App Router) or `pages/products/[slug].tsx` (Pages Router). This is where the JSON-LD block lives (or will be added).
  2. Ensure your storefront API/CMS query fetches GTIN fields (gtin13, gtin12, gtin8, gtin14) *and* MPN from the product data source. Add these fields to your GraphQL fragment, REST call, or SDK query if they are not already present.
  3. In the App Router, add or update a `<Script id='product-jsonld' type='application/ld+json'>` block inside the page component (or a dedicated `ProductJsonLd` component). In the Pages Router, place it inside `next/head` or use a `<script>` tag rendered inline.
  4. Inside the JSON-LD object, add the `gtin13` (or whichever GTIN length applies), `gtin` (generic fallback), and `mpn` properties directly on the top-level `Product` object *and* mirror them onto every `Offer` object within `offers`, as Google recommends both locations.
  5. Validate the updated markup with Google's Rich Results Test (https://search.google.com/test/rich-results) and the Schema Markup Validator (https://validator.schema.org) to confirm no errors before deploying.
  6. Deploy and monitor Google Search Console → Enhancements → Products for any new GTIN-related warnings or the appearance of rich result eligibility.
Seo product missing stock statusModerate effort

Add a visible stock availability label and an `availability` property to your Product structured data so Google can display rich results and shoppers know immediately whether a product is in stock.

On Next.js (Headless)

  1. Fetch stock status from your commerce API (e.g. availableForSale, stockLevel, or inventoryQuantity) inside the page's data-fetching layer — `generateStaticParams` + `generateMetadata` (App Router) or `getStaticProps`/`getServerSideProps` (Pages Router).
  2. Map the raw stock value to the correct schema.org URL string: `https://schema.org/InStock`, `https://schema.org/OutOfStock`, or `https://schema.org/PreOrder`, and store it alongside the product props.
  3. In the Product page component, render a visible stock-status badge (e.g. 'In Stock' / 'Out of Stock') in the JSX so both users and crawlers see it in the HTML — do NOT rely on client-only state alone.
  4. Inject a JSON-LD `<Script>` block (using `next/script` with `type='application/ld+json'`) that includes the `offers.availability` property set to the mapped schema.org URL.
  5. For App Router projects, place the `<Script>` inside the page Server Component (app/products/[slug]/page.tsx) so it is part of the SSR payload; for Pages Router, place it inside `<Head>` or directly in the component return.
  6. Validate the structured data with Google's Rich Results Test (https://search.google.com/test/rich-results) to confirm `availability` appears correctly in the parsed output.
Code · Generic HTML (html)
<!-- Product structured data with availability -->
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Your Product Name",
  "description": "A short description of your product.",
  "sku": "YOUR-SKU-123",
  "image": "https://www.example.com/images/product.jpg",
  "brand": {
    "@type": "Brand",
    "name": "Your Brand Name"
  },
  "offers": {
    "@type": "Offer",
    "url": "https://www.example.com/products/your-product",
    "priceCurrency": "USD",
    "price": "29.99",
    "availability": "https://schema.org/InStock",
    "itemCondition": "https://schema.org/NewCondition"
  }
}
</script>

<!-- Visible stock label near Add-to-Cart button -->
<p class="stock-status" style="font-weight:bold; color:#2a7a2a;">&#10003; In Stock</p>

Paste the complete block into the <head> section of your product page HTML. Change availability value to OutOfStock or PreOrder as needed.

Seo product no imagesModerate effort

Add at least one high-quality image to every product page so shoppers can see what they're buying and Google can surface your products in Image Search.

On Next.js (Headless)

  1. In your product page component (e.g. `app/products/[slug]/page.tsx` or `pages/products/[slug].tsx`), fetch the product's images array from your headless commerce API (Shopify Storefront API, BigCommerce, etc.) and guard against empty arrays — fall back to a placeholder only as a last resort, but always prefer a real image.
  2. Replace any raw `<img>` tags with Next.js `<Image>` from `next/image`. Set a descriptive, keyword-rich `alt` prop (e.g. product name + key attribute) so Google Image Search can index it correctly.
  3. Add the image host domain to `images.remotePatterns` (or `images.domains`) in `next.config.js` so Next.js is allowed to optimise the remote URL.
  4. Expose the primary product image inside the page-level `generateMetadata` (App Router) or `<Head>` (Pages Router) as an Open Graph `og:image` tag — this lets Google surface the image in rich results and social cards.
  5. Add an `ImageObject` node inside the existing Product JSON-LD structured data block so Google can associate the image with the product in its Knowledge Graph and Shopping surfaces.
  6. Verify with Next.js `<Image>` props: set explicit `width` / `height` (or `fill` + a sized wrapper), `priority` on the hero/first image to avoid LCP regressions, and `sizes` for responsive delivery.
Code · Generic HTML (html)
<figure class="product-gallery">
  <img
    src="/images/products/black-leather-wallet-front.jpg"
    alt="Black leather wallet with RFID blocking, front view"
    width="800"
    height="800"
    loading="eager"
  />
  <img
    src="/images/products/black-leather-wallet-open.jpg"
    alt="Black leather wallet open showing card slots and cash pocket"
    width="800"
    height="800"
    loading="lazy"
  />
</figure>

Basic responsive product image with descriptive alt text — place inside your product page HTML where the gallery/main image appears.

Seo product no spec tableModerate effort

Add a structured specifications table or list to every product page so shoppers, search engines, and AI tools can instantly find and use key product details.

On Next.js (Headless)

  1. Locate your product page component — typically `app/products/[slug]/page.tsx` (App Router) or `pages/products/[slug].tsx` (Pages Router). This is where the specification table and JSON-LD will be injected.
  2. Fetch the product's specification/attribute data from your headless commerce API (e.g. Shopify Storefront API `metafields`, BigCommerce `custom_fields`, or your PIM) inside `generateStaticParams` / `getStaticProps` / a server component's async fetch, and pass it as a prop or read it directly in the Server Component.
  3. Create a reusable `ProductSpecsTable` component in `components/ProductSpecsTable.tsx` that renders a semantically correct `<table>` (or `<dl>`) with spec name/value pairs — this is the visible, crawlable HTML that search engines and AI tools read.
  4. Render `<ProductSpecsTable specs={product.specifications} />` inside the product page template, placed below the main product description so it appears in the natural reading flow.
  5. Add a `Product` JSON-LD block via Next.js `<Script type='application/ld+json'>` (App Router) or inject it with `next/head` (Pages Router) to expose the same specifications as structured data — key properties include `name`, `description`, `sku`, and `additionalProperty` for each spec.
  6. Run `npx next build && npx next start` and validate both the rendered HTML table and the JSON-LD with Google's Rich Results Test and Schema.org validator to confirm specs are machine-readable.
Seo product price not in htmlModerate effort

Render your product price in the server-generated HTML so Google and other crawlers can read it directly — without executing JavaScript.

On Next.js (Headless)

  1. Locate your product page component — typically `app/products/[slug]/page.tsx` (App Router) or `pages/products/[slug].tsx` (Pages Router). Confirm that the price is currently fetched only on the client (e.g. inside a `useEffect` or a client component), making it invisible to crawlers.
  2. Move the price fetch to the server: in the App Router use an `async` Server Component and `fetch`/your commerce SDK directly in the component body; in the Pages Router use `getStaticProps` or `getServerSideProps` and pass `price` as a prop.
  3. Render the price value in JSX inside a semantic element (e.g. `<span>` with an appropriate `aria-label`) so it appears in the initial HTML payload — not injected after hydration.
  4. Add a `Product` JSON-LD block (using Next.js `<Script type='application/ld+json'>` in the App Router, or a `<script>` tag via `next/head` in the Pages Router) that includes `offers.price` and `offers.priceCurrency` so structured-data parsers also pick up the price.
  5. Run `curl -s 'https://your-domain/products/your-slug' | grep -i 'price'` or use Google's Rich Results Test to verify the price appears in raw HTML before any JS executes.
  6. If the price can change frequently, prefer `getServerSideProps` (Pages) or a short `revalidate` in `fetch()` (App Router) so the server-rendered price stays fresh.
Seo product single imageModerate effort

Add at least 4 product images (multiple angles, detail shots, and lifestyle/in-use photos) to every product page to improve click-through rates, conversions, and Google Images visibility.

On Next.js (Headless)

  1. Ensure your headless CMS / commerce API query fetches ALL product images (not just the first/featured one). Expand the images selection in your GraphQL query or REST call to return an array of objects each containing `url`, `altText`, and optionally `width`/`height`.
  2. In your product page component (e.g. `app/products/[slug]/page.tsx` for the App Router, or `pages/products/[slug].tsx` for Pages Router), destructure the full `images` array from the fetched product data and verify it contains at least 4 entries before rendering.
  3. Build a `<ProductGallery>` client component (e.g. `components/ProductGallery.tsx`) that accepts the `images` array, renders a primary large image and a row of thumbnail images, and uses Next.js `<Image>` for every image so that automatic optimisation, lazy-loading, and `alt` attributes are always present.
  4. Add structured data (JSON-LD `Product` schema) inside the page via a `<Script type='application/ld+json'>` tag and populate its `image` array property with all image URLs — this is what Google uses for rich results and Google Images indexing.
  5. Update `generateMetadata` (App Router) or `<Head>` (Pages Router) to emit `og:image` tags for each image so social-share and search previews surface multiple angles.
  6. Validate in Google's Rich Results Test that the `Product` schema lists ≥ 4 images, and confirm in Chrome DevTools that every `<img>` has a descriptive, keyword-rich `alt` attribute (e.g. 'Blue running shoe – side view').
Code · Generic HTML (html)
<figure class="product-gallery">
  <!-- Main hero image -->
  <img
    src="/images/red-leather-wallet-front.webp"
    alt="Red leather bifold wallet — front view"
    width="2000"
    height="2000"
    loading="eager"
    fetchpriority="high"
  />
</figure>

<!-- Thumbnail strip -->
<ul class="product-thumbnails" role="list">
  <li>
    <img
      src="/images/red-leather-wallet-front-thumb.webp"
      alt="Red leather bifold wallet — front view"
      width="200"
      height="200"
      loading="lazy"
    />
  </li>
  <li>
    <img
      src="/images/red-leather-wallet-back-thumb.webp"
      alt="Red leather bifold wallet — back view"
      width="200"
      height="200"
      loading="lazy"
    />
  </li>
  <li>
    <img
      src="/images/red-leather-wallet-interior-thumb.webp"
      alt="Red leather bifold wallet — interior card slots"
      width="200"
      height="200"
      loading="lazy"
    />
  </li>
  <li>
    <img
      src="/images/red-leather-wallet-lifestyle-thumb.webp"
      alt="Red leather bifold wallet shown in a shirt pocket"
      width="200"
      height="200"
      loading="lazy"
    />
  </li>
</ul>

Basic accessible product image gallery markup. Place inside your product detail page template.

Seo product title genericModerate effort

Rewrite every product page title tag to include the specific product name plus at least one differentiating attribute (variant, material, brand, or model) so Google and shoppers can instantly tell what the page is about.

On Next.js (Headless)

  1. Locate your product page route. In the App Router this is typically `app/products/[slug]/page.tsx`; in the Pages Router it is `pages/products/[slug].tsx`.
  2. Fetch the full product object (name, brand, primary variant label such as colour/size, material, model number) at build/request time — via `generateStaticParams` + `generateMetadata` (App Router) or `getStaticProps`/`getServerSideProps` + `next/head` (Pages Router).
  3. Build a descriptive title string that combines: *Product Name + Differentiating Attribute(s) + Brand*. Never fall back to a bare product name; always include at least one variant/material/model field.
  4. Export `generateMetadata` (App Router) or render a `<Head><title>` block (Pages Router) using that string — both the `title` and `openGraph.title` fields should be set so social shares also get the specific title.
  5. Add a site-name suffix (e.g. `| YourStore`) via the App Router's `metadata.title.template` in `app/layout.tsx` so every product page inherits it automatically without duplicating code.
  6. Validate the output with `next build && next start`, then inspect the rendered `<title>` tag in DevTools and confirm it matches the pattern `Product Name – Attribute – Brand | Site`.
Code · Generic HTML (html)
<head>
  <title>Magnetic Card Holder Phone Mount – MagSafe Compatible – BrandName</title>
  <meta name="description" content="Attach your cards securely to any MagSafe-compatible iPhone with BrandName's slim magnetic card holder mount. Holds 1–3 cards. Available in black and white.">
</head>

Place inside the <head> section of every product page. Follow the pattern: Product Name – Key Attribute – Brand.

Seo variant urls in sitemapModerate effort

Remove product variant URLs (e.g. ?variant= or ?sku= query strings) from your XML sitemap and keep only the canonical base product URL for each product.

On Next.js (Headless)

  1. Locate your sitemap source — either `app/sitemap.ts` (App Router) or a custom `pages/sitemap.xml.tsx` / `getServerSideProps` route (Pages Router). This is the ONLY place to make changes.
  2. In the function that builds sitemap entries, strip every query string (e.g. `?variant=`, `?sku=`, `?color=`) from product URLs before they are emitted. Use `new URL(rawUrl)` and read only `origin + pathname` — discarding `search` entirely.
  3. Deduplicate the resulting URL list with a `Set` or `Map` keyed on the clean pathname, so that multiple variants that share the same base product URL collapse into a single sitemap entry.
  4. Return (or render) only the deduplicated, query-string-free canonical URLs. For App Router this means returning the cleaned array from `sitemap.ts`; for Pages Router this means building the XML string from the cleaned array inside `getServerSideProps`.
  5. Verify the output by visiting `/sitemap.xml` locally and confirming no `?variant=` / `?sku=` parameters appear, and that each product path appears exactly once.
  6. Optionally set a `<link rel='canonical'>` on every product page (via `generateMetadata` or `next/head`) pointing to the same base URL, so crawlers that reach a variant URL are explicitly directed to the canonical version.
Code · Generic HTML (xml)
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">

  <!-- CORRECT: canonical base product URL -->
  <url>
    <loc>https://www.example.com/products/blue-sneaker</loc>
    <lastmod>2024-06-01</lastmod>
    <changefreq>weekly</changefreq>
    <priority>0.8</priority>
  </url>

  <!-- WRONG — never include variant query strings:
  <url>
    <loc>https://www.example.com/products/blue-sneaker?variant=12345</loc>
  </url>
  <url>
    <loc>https://www.example.com/products/blue-sneaker?sku=blue-large</loc>
  </url>
  -->

</urlset>

Correct sitemap entry — clean base product URL only, no query parameters.

Seo variant urls not canonicalizedModerate effort

Add a canonical tag to every product variant URL pointing back to the base product page, so Google consolidates ranking signals on one URL instead of splitting them across hundreds of duplicates.

On Next.js (Headless)

  1. Locate your product page route file — either `app/products/[slug]/page.tsx` (App Router) or `pages/products/[slug].tsx` (Pages Router). This is where the canonical must be set.
  2. Identify how variant parameters are passed — typically as query strings (e.g. `?color=red&size=M`) or as additional dynamic segments. The canonical must always point to the base product URL, stripping all variant params.
  3. App Router: Export an async `generateMetadata` function that receives `params` and `searchParams`, constructs the bare product URL (no query string), and returns it under `alternates.canonical`. This injects a `<link rel='canonical'>` in the `<head>` automatically.
  4. Pages Router: Import `next/head` in the page component, read the base slug from `router.query`, build the canonical URL, and render `<link rel='canonical' href={canonicalUrl} />` inside `<Head>`.
  5. Ensure the canonical URL is absolute (includes `https://` + your domain). Use an environment variable such as `NEXT_PUBLIC_SITE_URL` so it works across staging and production environments.
  6. Test by navigating to a variant URL (e.g. `/products/my-shirt?color=blue`) and inspecting the rendered `<head>` — the canonical should point to `https://yourdomain.com/products/my-shirt` with no query string.
Code · Generic HTML (html)
<head>
  <!-- All variant URLs (e.g. ?color=Red, ?size=M, ?variant=123) point canonical to the base product URL -->
  <link rel="canonical" href="https://www.example.com/products/my-product-name">
</head>

Place this inside the <head> of EVERY variant URL. The href must be the clean, base product URL — never the variant URL itself.

Server errorModerate effort

Identify and fix every page returning an HTTP 5xx server error so search engines can crawl and index those URLs again.

On Next.js (Headless)

  1. Audit 5xx sources: check Vercel/server logs, or add a global fetch wrapper that logs non-2xx responses from your commerce API (e.g. in lib/fetcher.ts). Identify which routes (App Router pages, Route Handlers, or getServerSideProps functions) are throwing unhandled errors.
  2. For App Router routes, add an `error.tsx` (client boundary) AND a `global-error.tsx` at the `app/` root so uncaught render errors return a recoverable UI instead of a 500. Pair each with a `not-found.tsx` to avoid conflating 404s with 5xxs.
  3. For Route Handlers (`app/api/*/route.ts`) and legacy `pages/api/*` handlers, wrap all logic in try/catch and return explicit `NextResponse.json({ error: '...' }, { status: 500 })` — never let an unhandled promise rejection propagate, which causes Next.js to emit a bare 500 with no body.
  4. For `getServerSideProps` pages (pages/ router), wrap the entire function body in try/catch. On a caught error, either `return { props: { error: true } }` (renders a degraded page with 200) OR use `res.status(500)` only when the page is genuinely unserviceable — prefer a degraded 200 over a 500 for SEO.
  5. Harden commerce-API calls: add retry logic with exponential back-off (e.g. using `p-retry`) and set explicit `next: { revalidate }` / `cache` options so a single upstream blip does not bust the entire render. For ISR pages, returning stale content is always better than a 500.
  6. Deploy, then validate with `curl -I https://<your-domain>/affected-path` and confirm HTTP 200 (or 404 where appropriate). Re-submit affected URLs in Google Search Console via the URL Inspection tool to trigger re-crawl.
Code · Generic HTML (html)
# Run this in your terminal to check the HTTP status code of any URL
curl -o /dev/null -s -w "HTTP Status: %{http_code}\n" https://www.example.com/your-page

# A healthy page returns:
# HTTP Status: 200

# A broken page returns (example):
# HTTP Status: 500

This is the correct HTTP 200 response header your server should return for a healthy page. Use curl on the command line to verify any URL's status code before and after your fix.

Slow pageModerate effort

Reduce your page load time to under 3 seconds by compressing images, eliminating render-blocking scripts and styles, and improving server response speed.

On Next.js (Headless)

  1. Replace every raw <img> tag with Next.js's built-in <Image> component (next/image). Set explicit width, height, and a descriptive alt prop. Add priority prop to above-the-fold hero images so they are preloaded. This enables automatic WebP/AVIF conversion, lazy-loading, and responsive srcsets out of the box.
  2. Eliminate render-blocking scripts by always using next/script with the correct strategy: strategy='lazyOnload' for analytics/chat widgets, strategy='afterInteractive' for tag managers, and strategy='beforeInteractive' ONLY for truly critical polyfills. Never use a plain <script> tag in _app.tsx / layout.tsx.
  3. Eliminate render-blocking CSS by importing third-party stylesheets inside the component that needs them (dynamic import) or inlining only the critical CSS. In the App Router, place global styles in app/layout.tsx and use CSS Modules or Tailwind (which purges unused rules at build time) for component styles.
  4. Enable HTTP caching and edge delivery by adding Cache-Control headers in next.config.js (headers() API) for static assets, and deploy on a CDN-edge runtime (Vercel Edge, Cloudflare, etc.). For API routes / Route Handlers that feed the page, use Next.js fetch() with { next: { revalidate: N } } to enable ISR and avoid blocking SSR round-trips.
  5. Reduce JavaScript bundle size: enable the SWC minifier (default in Next 13+), use dynamic(() => import('./HeavyComponent'), { ssr: false }) for large client-only widgets (carousels, map pickers), and audit your bundle with NEXT_ANALYZE=true next build via @next/bundle-analyzer.
  6. Measure before and after: run 'next build && next start' locally, profile with Lighthouse CI or WebPageTest, and verify LCP < 2.5 s, TBT < 200 ms, CLS < 0.1. Commit the next.config.js changes and re-run the audit in CI on every PR.
Code · Generic HTML (html)
<img
  src="product-image.webp"
  alt="Descriptive product alt text"
  width="800"
  height="600"
  loading="lazy"
  decoding="async"
>

Add these attributes to every image below the fold to enable native lazy loading and prevent layout shift.

Thin contentModerate effort

Add meaningful, original written content to pages that contain fewer than 300 words so Google has enough substance to rank and display them confidently.

On Next.js (Headless)

  1. Identify thin pages (product, collection, CMS, or landing pages) by auditing your route files in app/ (App Router) or pages/ (Pages Router) for components that render fewer than 300 words of visible text. Common culprits: app/products/[slug]/page.tsx, app/collections/[slug]/page.tsx.
  2. For each thin page, fetch rich editorial content from your headless CMS (e.g. Contentful, Sanity, Prismic) or extend your commerce API query (e.g. Shopify Storefront API metafields/metaobjects) to return a `description`, `longDescription`, `editorialBody`, or `richText` field alongside the existing product/collection data. Do this inside the server component's data-fetching function (async page component or generateStaticParams/getStaticProps).
  3. Create a reusable <PageContent> server component (e.g. components/PageContent.tsx) that accepts the rich text/HTML and renders it inside semantic HTML elements (<article>, <section>, <h2>, <p>) so crawlers see meaningful, crawlable DOM nodes — not JavaScript-gated content.
  4. Render the <PageContent> component below the hero/product information in each thin page file, passing the fetched editorial copy. Ensure the combined rendered text (product description + editorial body + features/specs) meets or exceeds 300 words.
  5. Add or update generateMetadata() in the same page file so the <title> and <meta name='description'> reflect the enriched content, reinforcing topical relevance signals for Google.
  6. Run `next build` and verify with a `curl` (or View Source) that the full text is present in the server-rendered HTML — not loaded client-side — so Googlebot receives it on the first HTTP response without executing JavaScript.
Code · Generic HTML (html)
<section class="product-description">
  <h2>About This Product</h2>
  <p>
    The Merino Wool Crew-Neck Sweater is crafted from 100% ethically sourced
    Grade-A Merino wool, making it naturally temperature-regulating, moisture-
    wicking, and soft enough to wear directly against the skin. Perfect for
    layering in autumn and winter, or as a standalone piece during mild spring
    evenings.
  </p>

  <h3>Key Features</h3>
  <ul>
    <li>100% Merino wool — no synthetic blends</li>
    <li>Available in sizes XS–3XL with a relaxed, unisex fit</li>
    <li>12 colours, each piece individually yarn-dyed for depth</li>
    <li>Ribbed cuffs and hem for a tailored finish</li>
    <li>Machine washable on a gentle 30°C wool cycle</li>
    <li>Certified by the Responsible Wool Standard (RWS)</li>
  </ul>

  <h3>Sizing &amp; Fit</h3>
  <p>
    This sweater is cut with a relaxed, slightly oversized silhouette. If you
    prefer a closer fit, we recommend sizing down. See our full <a href="/size-guide">size guide</a>
    for chest, waist, and sleeve measurements.
  </p>

  <h3>Care Instructions</h3>
  <p>
    Machine wash cold on a gentle/wool cycle. Lay flat to dry — do not tumble
    dry, iron at high heat, or dry-clean. With proper care, Merino wool retains
    its shape and softness for years.
  </p>

  <h3>Frequently Asked Questions</h3>
  <dl>
    <dt>Is Merino wool itchy?</dt>
    <dd>
      No. Merino fibres are finer than standard wool (under 19.5 microns),
      which means they bend against the skin rather than prick it, making this
      sweater comfortable even for people who normally find wool irritating.
    </dd>
    <dt>How does Merino wool compare to cashmere?</dt>
    <dd>
      Merino is more durable and easier to care for than cashmere, while still
      offering exceptional softness. It's an ideal everyday choice for those
      who want luxury without high maintenance.
    </dd>
    <dt>Can I return or exchange this item?</dt>
    <dd>
      Yes. We accept returns and exchanges within 30 days of delivery, provided
      the item is unworn and in its original packaging. See our
      <a href="/returns">returns policy</a> for full details.
    </dd>
  </dl>
</section>

Example of a well-structured, content-rich product description section with FAQ — paste into any HTML page template.

Title too longQuick win

Shorten your page title tag to 60 characters or fewer so it displays fully — without truncation — in Google search results.

On Next.js (Headless)

  1. Locate every page that sets a `<title>` — in the App Router this means any `generateMetadata()` export or a static `metadata` object; in the Pages Router it means any `<Head>` block using `next/head`.
  2. Create a shared utility (e.g. `lib/seo.ts`) that accepts a raw title string and hard-truncates it to 60 characters, appending an ellipsis only when the string is actually trimmed, so you never silently lose context.
  3. Replace every raw title string in `generateMetadata()` / static `metadata` objects (App Router) or `<title>` tags (Pages Router) with a call to `truncateTitle()`.
  4. For dynamic pages (products, collections, blog posts) that derive their title from an API/CMS, pipe the fetched title through `truncateTitle()` before passing it to `metadata.title`.
  5. Run `next build` and verify via the build output or a `<head>` inspection that no rendered `<title>` exceeds 60 characters.
  6. Optionally write a Jest/Vitest unit test for `truncateTitle()` to prevent regressions when copy is updated.
Code · Generic HTML (html)
<head>
  <meta charset="UTF-8">
  <title>Running Shoes for Men | YourBrand</title>
</head>

Place inside the <head> element of every HTML page. Keep the total character count at or under 60.

Title too shortQuick win

Expand your page title to between 30 and 60 characters so Google displays it fully in search results and shoppers can immediately understand what the page is about.

On Next.js (Headless)

  1. Identify every route that has a short title — check both the App Router (`app/*/page.tsx` → `generateMetadata`) and the Pages Router (`pages/*/*.tsx` → `next/head`) files.
  2. For App Router routes: update (or add) the exported `generateMetadata` function so the returned `title` string is between 30 and 60 characters, including your brand name and primary keyword.
  3. For Pages Router routes: update (or add) the `<title>` tag inside `<Head>` from `next/head`, applying the same 30–60 character rule.
  4. Use a shared `buildTitle` utility (see snippet) to enforce the character-length contract in one place and warn at build/runtime when a title falls outside the 30–60 character range.
  5. Run `console.log(buildTitle(...).length)` or add a Jest/Vitest unit test against `buildTitle` to confirm every title variant stays within the window before shipping.
  6. Deploy and verify in Google Search Console's URL Inspection tool or a third-party SEO crawler that the rendered `<title>` is between 30 and 60 characters.
Code · Generic HTML (html)
<head>
  <meta charset="UTF-8">
  <title>Men's Merino Wool Beanies – BrandName</title>
</head>

Place inside the <head> element of every page. Replace the example with your actual descriptive title (30–60 chars).

Answer Engine Optimization · 15 fixes

Aeo schema client renderedModerate effort

Move your JSON-LD structured data from client-side JavaScript injection to server-rendered HTML so that AI crawlers and search engines can read it in the raw page source.

On Next.js (Headless)

  1. Locate every place JSON-LD is currently injected client-side (e.g. inside a useEffect, a dangerouslySetInnerHTML block mounted in a Client Component, or a third-party tag-manager call) and delete those injections.
  2. For the App Router: open (or create) the relevant page or layout file (e.g. app/products/[slug]/page.tsx). Add a server-side `generateMetadata` export if you need <meta> tags, then drop a native Next.js `<Script>` (or a plain `<script>`) tag with `type='application/ld+json'` directly in the JSX return — no 'use client' directive on this file.
  3. For the Pages Router: open the page file (e.g. pages/products/[slug].tsx). Inside `getStaticProps` or `getServerSideProps`, fetch the product data and pass it as props. In the component, render a `<script type='application/ld+json'>` tag inside `<Head>` from 'next/head', using `dangerouslySetInnerHTML` with `JSON.stringify(jsonLd)`.
  4. Build the JSON-LD object on the server using the props/data already fetched for the page — never reconstruct it in a useEffect or onClick handler.
  5. Run `next build && next start`, then use 'View Page Source' (not DevTools Elements) to confirm the <script type='application/ld+json'> block appears in the raw HTML before any JS executes.
  6. Validate the rendered markup with Google's Rich Results Test and Schema.org Validator to confirm the structured data is parseable.
Code · Generic HTML (html)
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Your Product Name",
  "description": "A clear description of your product.",
  "sku": "YOUR-SKU-123",
  "brand": {
    "@type": "Brand",
    "name": "Your Brand Name"
  },
  "offers": {
    "@type": "Offer",
    "url": "https://www.example.com/products/your-product",
    "priceCurrency": "USD",
    "price": "49.99",
    "availability": "https://schema.org/InStock",
    "itemCondition": "https://schema.org/NewCondition"
  },
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "4.7",
    "reviewCount": "128"
  }
}
</script>

Place this block inside <head> or <body> in the server-rendered HTML. All values must be populated by the server, not by JavaScript.

Article missing authorQuick win

Add a structured-data author field (schema.org Person) to every article or blog post so AI engines and Google can identify and trust who wrote the content.

On Next.js (Headless)

  1. Locate (or create) your article/blog-post page component — typically `app/blog/[slug]/page.tsx` (App Router) or `pages/blog/[slug].tsx` (Pages Router). This is where the JSON-LD fix lives.
  2. Fetch the author data alongside the article payload from your headless CMS/API (name, URL, image). If the CMS doesn't expose a per-article author, define a sitewide fallback author object in a shared config file such as `lib/seo-config.ts`.
  3. In the App Router, add or extend the `generateMetadata` export for `<title>`/`<meta>` tags, then inject a separate `<Script id='article-jsonld' type='application/ld+json'>` component (from `next/script`) inside the page JSX that carries the full `Article` + `author` schema.
  4. Ensure the `author` field is typed as `{ '@type': 'Person', name: string, url?: string, image?: string }`. For multiple authors, pass an array. For an organisation byline, use `'@type': 'Organization'` instead.
  5. In the Pages Router alternative, place the same `<Script>` block (or a `<script>` tag inside `next/head`) inside the page component that already receives `getStaticProps` / `getServerSideProps` article data.
  6. Validate the output with Google's Rich Results Test (https://search.google.com/test/rich-results) and the Schema Markup Validator (https://validator.schema.org) before deploying.
Code · Generic HTML (html)
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "How to Choose the Best Running Shoes",
  "datePublished": "2024-03-10T09:00:00+00:00",
  "dateModified": "2024-06-20T14:00:00+00:00",
  "author": {
    "@type": "Person",
    "name": "Alex Johnson",
    "url": "https://www.yourstore.com/about/alex-johnson",
    "sameAs": [
      "https://www.linkedin.com/in/alexjohnson",
      "https://twitter.com/alexjohnson"
    ]
  },
  "publisher": {
    "@type": "Organization",
    "name": "Your Store Name",
    "logo": {
      "@type": "ImageObject",
      "url": "https://www.yourstore.com/assets/logo.png",
      "width": 300,
      "height": 60
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://www.yourstore.com/blog/how-to-choose-running-shoes"
  }
}
</script>

For any static site or custom CMS: paste inside the <head> of each article page and fill in real values.

Invalid rating valueModerate effort

Fix the AggregateRating.ratingValue in your structured data so it is a valid number between 1 and 5 (or your declared scale's minimum and maximum).

On Next.js (Headless)

  1. Locate the product page component that renders JSON-LD structured data — typically `app/products/[slug]/page.tsx` (App Router) or `pages/products/[slug].tsx` (Pages Router).
  2. Find the `AggregateRating` object inside your JSON-LD script and identify where `ratingValue` is sourced (e.g. raw API field, CMS field, or a manual constant).
  3. Clamp and sanitise the value before injecting it: parse it as a float, ensure it is a finite number, then clamp it between `bestRating` (max, default 5) and `worstRating` (min, default 1). Never pass `null`, `undefined`, `NaN`, or an out-of-range value.
  4. Ensure `bestRating`, `worstRating`, and `reviewCount` are also present in the `AggregateRating` block — Google requires all four fields for the rating to be valid.
  5. Inject the sanitised object via a `<script type='application/ld+json'>` tag: use Next.js `<Script id='...' type='application/ld+json'>` (from `next/script`) inside the component, or return it from `generateMetadata` / use the App Router's built-in JSON-LD pattern.
  6. Validate the output in Google's Rich Results Test (https://search.google.com/test/rich-results) to confirm the rating is parsed correctly and no warnings remain.
Low answer first scoreModerate effort

Restructure page content to lead with a direct, concise answer, use question-format headings, and add FAQ structured data so search engines and AI assistants can surface your answers prominently.

On Next.js (Headless)

  1. Identify the page(s) with low AEO answer-first scores (e.g. `app/products/[slug]/page.tsx` or `app/blog/[slug]/page.tsx`). Open that file — this is where all changes live.
  2. Restructure the JSX so the page's primary question is rendered as an `<h1>` in question format (e.g. 'What is [Product]?'), immediately followed by a short `<p>` (≤ 50 words) that directly answers it — before any other body content, CTAs, or media.
  3. Add additional `<h2>` or `<h3>` question-format headings throughout the page for supporting FAQs, each immediately followed by a concise answer paragraph. This trains crawlers and AI assistants to map questions → answers within your content.
  4. In the same file, define a `faqJsonLd` object containing an `FAQPage` schema using all question/answer pairs. Render it via Next.js's `<Script>` component (or a plain `<script>` tag inside the component) with `type='application/ld+json'`.
  5. Export a `generateMetadata` function from the same page file to set a descriptive `title` and `description` that mirror the primary question and its direct answer — this reinforces the AEO signal at the meta level.
  6. Run `next build` and validate the FAQ structured data in Google's Rich Results Test (https://search.google.com/test/rich-results) and the Schema Markup Validator (https://validator.schema.org) before deploying.
Code · Generic HTML (html)
<!-- Answer-first page structure template -->

<!-- 1. Direct-answer summary (≤160 characters) -->
<p class="answer-summary">
  [Product Name] does [core benefit] by [key mechanism] — ideal for [target customer].
</p>

<!-- 2. Question-format H2 subheading -->
<h2>What does [Product Name] do?</h2>
<p>Direct 1–2 sentence answer here. Then expand with supporting detail below.</p>

<!-- 3. Another question-format heading -->
<h2>Who is [Product Name] designed for?</h2>
<p>Direct answer. Then elaborate.</p>

<!-- 4. FAQ section (visible on page — required for FAQPage schema) -->
<section id="faq">
  <h2>Frequently Asked Questions</h2>

  <h3>How long does shipping take?</h3>
  <p>Standard shipping takes 3–5 business days. Express options are available at checkout.</p>

  <h3>What is your return policy?</h3>
  <p>We accept returns within 30 days of delivery on unused items in original packaging.</p>

  <h3>Is [Product Name] compatible with [common concern]?</h3>
  <p>Yes. [One-sentence direct answer with any important caveats.]</p>
</section>

<!-- 5. FAQPage JSON-LD (copy the JSON-LD snippet above and paste here or in <head>) -->

Page content structure template — place this as the opening content block on any product, category, or landing page.

Low review countModerate effort

Collect at least 5 published reviews so Google will display star ratings in search results alongside your product listings.

On Next.js (Headless)

  1. Fetch your product's reviews from your reviews API/CMS (e.g. Yotpo, Stamped, Judge.me, or your own endpoint) inside the Next.js page — use `generateMetadata` + a separate async data fetch in `app/products/[slug]/page.tsx` (App Router) or `getStaticProps`/`getServerSideProps` in `pages/products/[slug].tsx` (Pages Router).
  2. Ensure at least 5 reviews are published and returned by the API before injecting structured data — gate the JSON-LD block on `reviews.length >= 5` so you never emit an incomplete aggregate rating that Google may ignore.
  3. Build an `AggregateRating` + `Review` array JSON-LD object from the fetched review data and inject it via a `<Script id='product-jsonld' type='application/ld+json'>` component (next/script) placed inside the page's JSX return, below the fold but still in the document.
  4. Include every required field Google demands: `@type: 'Product'`, `name`, `aggregateRating` (with `ratingValue`, `reviewCount`, `bestRating`, `worstRating`), and an array of individual `Review` objects each with `author`, `datePublished`, `reviewRating`, and `reviewBody`.
  5. Run the output through Google's Rich Results Test (https://search.google.com/test/rich-results) and the Schema Markup Validator (https://validator.schema.org/) to confirm zero errors before deploying.
  6. Re-deploy; once Googlebot re-crawls the page the star ratings will become eligible to appear in SERPs (typically within days to a few weeks).
Code · Generic HTML (html)
<!-- Product structured data with AggregateRating -->
<!-- Note: Google will only show review stars when reviewCount >= 5 -->
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Your Product Name",
  "image": [
    "https://example.com/images/product-front.jpg",
    "https://example.com/images/product-side.jpg"
  ],
  "description": "A clear, accurate description of your product.",
  "sku": "SKU-12345",
  "mpn": "MPN-12345",
  "brand": {
    "@type": "Brand",
    "name": "Your Brand"
  },
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "4.5",
    "reviewCount": "12",
    "bestRating": "5",
    "worstRating": "1"
  },
  "offers": {
    "@type": "Offer",
    "url": "https://example.com/products/your-product",
    "priceCurrency": "USD",
    "price": "49.99",
    "priceValidUntil": "2026-12-31",
    "itemCondition": "https://schema.org/NewCondition",
    "availability": "https://schema.org/InStock",
    "seller": {
      "@type": "Organization",
      "name": "Your Store Name"
    }
  }
}
</script>

Drop this complete block into the <head> section of any product page. Swap in your real values for name, image, SKU, price, ratingValue, and reviewCount.

Missing schema articleModerate effort

Add Article schema markup (JSON-LD) to every blog post and editorial page so Google can identify and rich-result-enhance your content in search.

On Next.js (Headless)

  1. Create a reusable `ArticleJsonLd` component at `components/seo/ArticleJsonLd.tsx` that accepts article metadata props and renders a `<script type='application/ld+json'>` tag using Next.js's `<Script>` (or a plain `<script>` inside a Server Component).
  2. In your blog post page — `app/blog/[slug]/page.tsx` (App Router) or `pages/blog/[slug].tsx` (Pages Router) — fetch the article data (title, description, author, dates, images) from your headless CMS/API inside `generateMetadata` / `getStaticProps` / `getServerSideProps`.
  3. Import and render `<ArticleJsonLd>` inside the page component, passing the fetched article fields as props. For the App Router this sits directly in the Server Component; for the Pages Router place it inside the JSX return alongside `<Head>`.
  4. Ensure every required Article schema field is populated: `headline`, `image`, `datePublished`, `dateModified`, `author` (with `@type: 'Person'` or `'Organization'`), `publisher`, and `url`. Fall back gracefully when optional CMS fields are absent.
  5. Extend the same `ArticleJsonLd` component to editorial/landing pages by passing `@type: 'Article'` or the more specific `'BlogPosting'` / `'NewsArticle'` depending on the content type returned by your CMS.
  6. Validate the rendered output with Google's Rich Results Test (https://search.google.com/test/rich-results) and the Schema Markup Validator (https://validator.schema.org/) before deploying.
Code · Generic HTML (html)
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>How to Choose the Right Running Shoes</title>
  <script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "BlogPosting",
    "headline": "How to Choose the Right Running Shoes",
    "description": "A complete guide to selecting running shoes based on your gait, terrain, and training goals.",
    "image": {
      "@type": "ImageObject",
      "url": "https://www.example.com/images/running-shoes-guide.jpg",
      "width": 1200,
      "height": 630
    },
    "author": {
      "@type": "Person",
      "name": "Jane Smith",
      "url": "https://www.example.com/authors/jane-smith"
    },
    "publisher": {
      "@type": "Organization",
      "name": "Example Store",
      "logo": {
        "@type": "ImageObject",
        "url": "https://www.example.com/logo.png",
        "width": 250,
        "height": 60
      }
    },
    "datePublished": "2024-03-15",
    "dateModified": "2024-06-01",
    "mainEntityOfPage": {
      "@type": "WebPage",
      "@id": "https://www.example.com/blog/how-to-choose-running-shoes"
    },
    "url": "https://www.example.com/blog/how-to-choose-running-shoes"
  }
  </script>
</head>
<body>
  <!-- page content -->
</body>
</html>

Place this entire block inside the <head> element of any article or blog-post page. Fill in all values before publishing.

Missing schema breadcrumblistModerate effort

Add BreadcrumbList structured data (JSON-LD) to every page so Google can display your site's navigation path directly in search results.

On Next.js (Headless)

  1. Create a reusable `BreadcrumbJsonLd` component (e.g. `components/seo/BreadcrumbJsonLd.tsx`) that accepts a `items` prop — an ordered array of `{ name, url }` objects — and renders a `<Script id='breadcrumb-jsonld' type='application/ld+json'>` tag (Next.js `next/script` with `strategy='beforeInteractive'`, or a plain `<script>` inside the App Router's Server Component).
  2. In every page file that needs a breadcrumb trail (e.g. `app/products/[slug]/page.tsx`, `app/collections/[slug]/page.tsx`, `app/blog/[slug]/page.tsx`), build the breadcrumb `items` array dynamically from your route params / fetched data.
  3. Import and render `<BreadcrumbJsonLd>` inside the page's JSX *below* your `generateMetadata` export — the component outputs only a `<script>` tag and has no visual output, so it won't affect layout.
  4. For the App Router, place the `<script>` tag directly in the Server Component's return value (no `next/script` needed); for the Pages Router, use `next/head` or `next/script` with `strategy='beforeInteractive'` so the tag is present before hydration.
  5. Validate the output with Google's Rich Results Test (https://search.google.com/test/rich-results) and confirm the `BreadcrumbList` entity is parsed correctly for each page type.
  6. Optionally build a `useBreadcrumbs()` hook or a shared `buildBreadcrumbs(pathname)` utility (e.g. `lib/breadcrumbs.ts`) to derive the items array from `usePathname()` (App Router) or `useRouter().asPath` (Pages Router) so all pages share one source of truth.
Code · Generic HTML (json)
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "https://www.example.com/"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "Women's Clothing",
      "item": "https://www.example.com/womens-clothing/"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "Dresses",
      "item": "https://www.example.com/womens-clothing/dresses/"
    },
    {
      "@type": "ListItem",
      "position": 4,
      "name": "Blue Wrap Dress",
      "item": "https://www.example.com/womens-clothing/dresses/blue-wrap-dress/"
    }
  ]
}
</script>

Place this <script> block inside <head> or anywhere in <body> on any page with a breadcrumb trail. Extend itemListElement with one ListItem per level.

Missing schema faqpageQuick win

Add FAQPage schema markup to any page that contains a Frequently Asked Questions section so Google can display your answers as rich results directly in search.

On Next.js (Headless)

  1. Identify every page that renders an FAQ section (e.g. app/faq/page.tsx, app/product/[slug]/page.tsx). The JSON-LD block must live on the same page as the visible Q&A content.
  2. Create a reusable FaqJsonLd component at components/seo/FaqJsonLd.tsx that accepts a typed array of { question, answer } pairs and renders a <script type='application/ld+json'> tag via Next.js's built-in <Script> or a plain <script> inside the JSX (see snippet).
  3. In each page file that uses the App Router, import FaqJsonLd and drop it anywhere inside the returned JSX — it renders no visible UI, only the <script> tag that Google crawls.
  4. If you use the Pages Router instead, import FaqJsonLd the same way; Next.js will hoist the <script> tag correctly because it contains no document.write calls.
  5. Ensure every question and answer string matches the visible text on the page exactly — Google cross-validates the schema against the rendered DOM.
  6. Deploy and validate with the Google Rich Results Test (https://search.google.com/test/rich-results) and Schema Markup Validator (https://validator.schema.org/) to confirm FAQPage rich results are eligible.
Code · Generic HTML (html)
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Frequently Asked Questions | Your Store</title>
  <script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "FAQPage",
    "mainEntity": [
      {
        "@type": "Question",
        "name": "What is your return policy?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "We accept returns within 30 days of purchase. Items must be unused and in their original packaging."
        }
      },
      {
        "@type": "Question",
        "name": "How long does shipping take?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "Standard shipping takes 5–7 business days. Expedited options are available at checkout."
        }
      }
    ]
  }
  </script>
</head>
<body>
  <h1>Frequently Asked Questions</h1>
  <h2>What is your return policy?</h2>
  <p>We accept returns within 30 days of purchase. Items must be unused and in their original packaging.</p>
  <h2>How long does shipping take?</h2>
  <p>Standard shipping takes 5–7 business days. Expedited options are available at checkout.</p>
</body>
</html>

Full HTML page example showing exactly where to place the FAQPage script block inside the <head>.

Missing schema howtoModerate effort

Add HowTo structured data (JSON-LD) to any page that walks customers through a multi-step process, so Google can display your instructions as rich results in search.

On Next.js (Headless)

  1. Identify every page that contains a multi-step process (assembly guides, tutorials, recipe pages, setup flows, etc.) — these are the target routes in your `app/` or `pages/` directory.
  2. Create a reusable `HowToJsonLd` component (e.g. `components/seo/HowToJsonLd.tsx`) that accepts the schema props and renders a `<script type='application/ld+json'>` tag via Next.js's `<Script>` or a plain `<script>` inside a Server Component.
  3. In each target page file (e.g. `app/guides/[slug]/page.tsx`), fetch or import your step data, build the HowTo schema object, and render `<HowToJsonLd>` in the JSX return — no `<head>` manipulation needed in the App Router because Next.js hoists inline scripts correctly.
  4. If you are on the *Pages Router* (`pages/guides/[slug].tsx`), import `Head` from `next/head` and place the `<script type='application/ld+json'>` tag inside `<Head>` instead.
  5. Validate the output with Google's Rich Results Test (https://search.google.com/test/rich-results) and the Schema Markup Validator (https://validator.schema.org/) before deploying.
  6. Optionally extend `generateMetadata` (App Router) or the `<Head>` block with a descriptive `title` and `description` that matches the HowTo topic, reinforcing the AEO signal.
Code · Generic HTML (html)
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>How to Complete Your Task | Your Store</title>
  <script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "HowTo",
    "name": "How to Complete Your Task",
    "description": "Follow these steps to complete your task easily.",
    "totalTime": "PT10M",
    "step": [
      {
        "@type": "HowToStep",
        "position": 1,
        "name": "Prepare your materials",
        "text": "Gather everything you need before you start: list materials here."
      },
      {
        "@type": "HowToStep",
        "position": 2,
        "name": "Complete the main action",
        "text": "Describe exactly what the user should do in this core step."
      },
      {
        "@type": "HowToStep",
        "position": 3,
        "name": "Verify the result",
        "text": "Explain how the user confirms the task was completed successfully."
      }
    ]
  }
  </script>
</head>
<body>
  <!-- Your how-to page content -->
</body>
</html>

Place this block inside the <head> element of any static HTML page that contains step-by-step instructions.

Missing schema localbusinessQuick win

Add LocalBusiness JSON-LD structured data to your store so search engines and AI answer engines can surface your business name, address, phone number, and hours in rich results and direct answers.

On Next.js (Headless)

  1. Create a reusable `LocalBusinessJsonLd` component at `components/seo/LocalBusinessJsonLd.tsx` that accepts your business data as props and renders a `<script type='application/ld+json'>` tag using Next.js's `<Script>` component (or a plain `<script>` tag inside the component).
  2. Populate the JSON-LD object with all required and recommended LocalBusiness fields: `@context`, `@type`, `name`, `url`, `telephone`, `address` (PostalAddress), `openingHoursSpecification`, `image`, and `geo` (if available). Source these values from your CMS, environment variables, or a central site-config file (e.g. `lib/site-config.ts`).
  3. Import and render `<LocalBusinessJsonLd />` in your root layout file (`app/layout.tsx` for the App Router, or `pages/_app.tsx` / `pages/index.tsx` for the Pages Router) so the schema appears on every page — or at minimum on the homepage and contact page.
  4. For the App Router, place the component inside the `<body>` of `app/layout.tsx` (Next.js will hoist `<script>` tags correctly). For the Pages Router, wrap it with `next/head` using a `<script>` tag with `dangerouslySetInnerHTML`.
  5. Validate the output with Google's Rich Results Test (https://search.google.com/test/rich-results) and Schema.org Validator (https://validator.schema.org/) to confirm all required fields pass without errors.
  6. Extend the `@type` to a more specific subtype if applicable (e.g. `ClothingStore`, `FurnitureStore`, `ElectronicsStore`) so AI answer engines and Google can surface even richer results for your category.
Code · Generic HTML (html)
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Your Store</title>
  <script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "Store",
    "name": "Your Business Name",
    "url": "https://www.yourdomain.com",
    "logo": "https://www.yourdomain.com/logo.png",
    "address": {
      "@type": "PostalAddress",
      "streetAddress": "123 Main Street",
      "addressLocality": "Springfield",
      "addressRegion": "IL",
      "postalCode": "62701",
      "addressCountry": "US"
    },
    "telephone": "+1-217-555-0100",
    "openingHoursSpecification": [
      {
        "@type": "OpeningHoursSpecification",
        "dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
        "opens": "09:00",
        "closes": "18:00"
      }
    ]
  }
  </script>
</head>
<body>
  <!-- page content -->
</body>
</html>

Paste this block anywhere inside the <head> element of your HTML page. Replace all placeholder values before deploying.

Missing schema organizationQuick win

Add an Organization schema markup block to your store's homepage (and/or sitewide template) so search engines and AI assistants can definitively identify your brand.

On Next.js (Headless)

  1. Identify your router: if you use the App Router (app/), add the JSON-LD <Script> block in app/layout.tsx (sitewide) or app/page.tsx (homepage only). If you use the Pages Router (pages/), add it in pages/_app.tsx or pages/index.tsx using next/head.
  2. Create a reusable OrganizationSchema component at components/seo/OrganizationSchema.tsx that renders a <script type='application/ld+json'> tag with all required and recommended Organization fields (name, url, logo, sameAs social profiles, contactPoint, etc.).
  3. Import and place <OrganizationSchema /> in app/layout.tsx (App Router) so it renders on every page, or inside the <Head> component in pages/_app.tsx (Pages Router).
  4. Populate the schema constants (SITE_NAME, SITE_URL, LOGO_URL, SOCIAL_PROFILES, etc.) from your environment variables or a central site-config file (lib/config.ts) so values are maintainable and not hard-coded.
  5. After deploying, validate the output with Google's Rich Results Test (https://search.google.com/test/rich-results) and Schema.org's validator (https://validator.schema.org/) to confirm zero errors.
Code · Generic HTML (html)
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Organization",
  "name": "Acme Store",
  "url": "https://www.acmestore.com",
  "logo": {
    "@type": "ImageObject",
    "url": "https://www.acmestore.com/images/logo.png",
    "width": 250,
    "height": 250
  },
  "sameAs": [
    "https://www.facebook.com/acmestore",
    "https://www.instagram.com/acmestore",
    "https://twitter.com/acmestore",
    "https://www.linkedin.com/company/acmestore",
    "https://www.youtube.com/c/acmestore"
  ],
  "contactPoint": {
    "@type": "ContactPoint",
    "contactType": "customer service",
    "email": "hello@acmestore.com"
  }
}
</script>

Drop this complete block inside your <head>…</head> section on every page. Replace all placeholder values.

Missing schema productModerate effort

Add Product schema markup (JSON-LD) to every product page so search engines can display rich results — including price, availability, and ratings — directly in search.

On Next.js (Headless)

  1. Locate (or create) your product page file — either `app/products/[slug]/page.tsx` (App Router) or `pages/products/[slug].tsx` (Pages Router). All Product JSON-LD lives here.
  2. Fetch the product data server-side (via `generateMetadata` / `getStaticProps` / `getServerSideProps`) so every field in the schema is populated before the page is sent to the crawler.
  3. Build a typed `productJsonLd` object that maps your commerce data to the Schema.org `Product` type, including `name`, `image`, `description`, `sku`, `brand`, `offers` (price, currency, availability), and optionally `aggregateRating`.
  4. Inject the JSON-LD using a `<Script id='product-schema' type='application/ld+json'>` tag (App Router) or Next.js `<Script>` / a plain `<script>` inside `<Head>` (Pages Router) — *not* as a regular `<script>` without `type='application/ld+json'`, and never dangerouslySetInnerHTML in a client component.
  5. Validate the output in Google's Rich Results Test (https://search.google.com/test/rich-results) and Schema Markup Validator (https://validator.schema.org/) before deploying.
  6. For large catalogues, generate the schema inside `generateStaticParams` so every PDP is statically rendered with its own schema at build time, avoiding runtime latency.
Missing schema webpageModerate effort

Add WebPage schema markup to every page so search engines and AI answer engines can precisely understand and describe your content.

On Next.js (Headless)

  1. Create a reusable `WebPageJsonLd` component at `components/seo/WebPageJsonLd.tsx` that accepts page-level props (name, description, url, dateModified, breadcrumb) and renders a `<script type='application/ld+json'>` tag using Next.js's `<Script>` strategy or a plain `<script>` inside the JSX return.
  2. In the App Router, add the `<WebPageJsonLd>` component directly inside each page's default export (e.g. `app/page.tsx`, `app/products/[slug]/page.tsx`, `app/blog/[slug]/page.tsx`) so it is rendered server-side in the HTML response.
  3. For every page, also export a `generateMetadata()` function (or static `metadata` object) to keep the `<title>` and `<meta name='description'>` in sync with the same values passed to `WebPageJsonLd` — single source of truth via a shared `getSeoData()` helper.
  4. Derive the canonical `url` field from `headers().get('host')` + `pathname` (server component) or from a `NEXT_PUBLIC_SITE_URL` environment variable so the schema URL always matches the canonical link.
  5. For content pages (blog, landing pages) use `@type: 'WebPage'`; for product listing pages use `@type: 'CollectionPage'`; for product detail pages use `@type: 'ItemPage'`; for the homepage use `@type: 'WebPage'` with `isPartOf: { @type: 'WebSite', ... }` — pass `pageType` as a prop to the shared component.
  6. Validate the output in Google's Rich Results Test and the Schema.org validator after deployment to confirm every route emits a valid, crawlable JSON-LD block.
Code · Generic HTML (html)
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "WebPage",
  "name": "About Us | Acme Store",
  "description": "Learn about Acme Store, our mission, and the team behind our products.",
  "url": "https://www.example.com/about/",
  "inLanguage": "en-US",
  "datePublished": "2022-01-15",
  "dateModified": "2024-06-01",
  "isPartOf": {
    "@type": "WebSite",
    "name": "Acme Store",
    "url": "https://www.example.com/"
  },
  "breadcrumb": {
    "@type": "BreadcrumbList",
    "itemListElement": [
      {
        "@type": "ListItem",
        "position": 1,
        "name": "Home",
        "item": "https://www.example.com/"
      },
      {
        "@type": "ListItem",
        "position": 2,
        "name": "About Us",
        "item": "https://www.example.com/about/"
      }
    ]
  }
}
</script>

Paste this inside the <head> element of any HTML page. Replace all placeholder values with real page data.

Non iso dateQuick win

Change every datePublished (and dateModified) value in your structured data from a human-readable string to a strict ISO-8601 date — formatted as YYYY-MM-DD or YYYY-MM-DDThh:mm:ss+hh:mm — so search engines can reliably parse your content's freshness.

On Next.js (Headless)

  1. Locate every page or component that renders a JSON-LD <Script type='application/ld+json'> block (typically in app/[slug]/page.tsx, app/blog/[slug]/page.tsx, or a shared JsonLd component).
  2. Create a shared helper utility (e.g. lib/formatIsoDate.ts) that accepts any date value and always returns a strict ISO-8601 string with timezone offset (YYYY-MM-DDThh:mm:ss+hh:mm), throwing a clear error for invalid inputs so bad dates are caught at build time.
  3. Replace every raw date string literal or .toLocaleDateString() / human-readable format passed to datePublished and dateModified with a call to this helper — both in the JSON-LD structured-data object AND in any og:article:published_time / og:article:modified_time meta tags produced by generateMetadata().
  4. For dates that originate from a CMS or API, pipe the raw value (ISO string, Unix timestamp, or JS Date object) directly into the helper rather than formatting it for display first — keep display formatting separate (e.g. use Intl.DateTimeFormat only for visible UI text).
  5. Run next build and check the build output for any thrown date-format errors from the helper; then validate a rendered page's JSON-LD with Google's Rich Results Test to confirm the ISO-8601 format is accepted.
Code · Generic HTML (html)
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "Your Article Headline",
  "datePublished": "2025-03-12",
  "dateModified": "2025-03-12",
  "author": {
    "@type": "Person",
    "name": "Author Name"
  }
}
</script>

Minimum viable JSON-LD block to paste into any HTML <head> — replace values with your own.

Org missing sameasQuick win

Add a `sameAs` array to your Organization schema, linking to your verified social profiles, Wikipedia page, LinkedIn, and other authoritative sources so search engines can confidently identify your brand as a known entity.

On Next.js (Headless)

  1. Locate (or create) your site-wide layout file — `app/layout.tsx` for the App Router, or `pages/_app.tsx` / a shared `<Head>` wrapper for the Pages Router. The Organization JSON-LD should live here so it is emitted on every page.
  2. Create a dedicated component file `components/seo/OrganizationSchema.tsx` that renders a `<script type='application/ld+json'>` tag. Centralising the schema here makes the `sameAs` list easy to maintain.
  3. Populate the `sameAs` array with every authoritative, verified URL that represents your brand: social-media profile URLs (Facebook, X/Twitter, Instagram, LinkedIn, YouTube, Pinterest, TikTok), your Wikipedia or Wikidata page, your Crunchbase profile, and any other widely-recognised directory pages. Each URL must exactly match the canonical URL of the profile (check the address bar when logged out).
  4. Add all other required/recommended Organization fields (`@type`, `name`, `url`, `logo`, `contactPoint`, etc.) so the entity is fully described alongside the `sameAs` links.
  5. Import and render `<OrganizationSchema />` inside the root `<body>` of `app/layout.tsx` (App Router) or inside the `<Head>` equivalent in Pages Router, so it is server-rendered in the initial HTML on every route.
  6. Validate the output with Google's Rich Results Test (https://search.google.com/test/rich-results) and Schema.org's validator (https://validator.schema.org/) to confirm the `sameAs` URLs are parsed correctly and there are no errors.
Code · Generic HTML (html)
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Organization",
  "name": "Your Brand Name",
  "url": "https://www.yourdomain.com",
  "logo": "https://www.yourdomain.com/images/logo.png",
  "sameAs": [
    "https://www.linkedin.com/company/your-company",
    "https://www.facebook.com/yourpage",
    "https://twitter.com/yourhandle",
    "https://www.instagram.com/yourhandle",
    "https://www.youtube.com/@yourchannel",
    "https://en.wikipedia.org/wiki/Your_Brand",
    "https://www.wikidata.org/wiki/QXXXXXXX"
  ]
}
</script>

Paste this directly before the closing </head> tag in your HTML template. Remove any sameAs entries for platforms your brand does not have a presence on.