Every Next.js fix we catalog
SEOLZ catalogs 23 fixes for Next.js across 5 areas — SEO, answer-engine readiness, accessibility, security and site-health. Each lists the exact steps for Next.js, with a link to the official docs.
SEO · 13 fixes
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
- Identify every route that currently shares a meta description — common culprits are the home page (`app/page.tsx`), PDP (`app/products/[slug]/page.tsx`), PLP (`app/collections/[slug]/page.tsx`), blog post (`app/blog/[slug]/page.tsx`), and any static pages.
- For dynamic routes, export an async `generateMetadata()` function that fetches the page-specific data (product title, description, collection summary, etc.) from your headless CMS/commerce API and returns a unique `description` string derived from that data.
- For static routes (home, about, contact, etc.), export a static `metadata` object with a hand-crafted, page-specific `description` (150–160 characters, includes primary keyword, no duplicate of other pages).
- Centralise shared helpers — e.g. a `lib/seo.ts` utility that truncates copy to 155 characters and strips HTML tags — so every route builds its description the same way.
- Remove or stop inheriting any `defaultMetadata` / `metadataBase` description that was being re-used verbatim across all pages (a root `layout.tsx` description is fine only as an absolute last-resort fallback, never as the primary value).
- Verify uniqueness by running `next build` and scraping the rendered `<meta name='description'>` tags with a script or a tool like Screaming Frog; each URL must produce a distinct string.
<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
- Centralise your title strategy in a shared `lib/seo.ts` helper that builds a unique, descriptive title from page-specific data (product name, category, page type) plus a consistent brand suffix.
- For the App Router, export an async `generateMetadata()` function from every `app/**/page.tsx` that calls the helper with data fetched for that specific route — never hard-code a static string shared across pages.
- For the Pages Router, replace any shared/static `<title>` strings in `_app.tsx` or individual pages with per-page `<Head><title>` tags using `next/head`, driven by the same `lib/seo.ts` helper.
- Use Next.js `metadata.metadataBase` (App Router) or an absolute URL in Pages Router so Open Graph titles and canonical tags resolve correctly alongside the unique `<title>`.
- Add the `template` key inside a root `layout.tsx` `metadata` export as a fallback pattern (e.g. `'%s | Brand'`) so any page that only provides a short segment still gets a fully-formed, unique title.
- Audit with `generateStaticParams` or your sitemap to assert every slug produces a distinct title — add a CI test that fetches rendered HTML for key routes and asserts no two `<title>` values are identical.
<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.
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
- Replace every bare <img> tag in your Next.js project with Next.js's built-in <Image> component (from 'next/image'). The `alt` prop is required by the component's TypeScript types, so the compiler will surface any missing values at build time.
- For product images, pull `alt` text from your commerce API response — prefer a dedicated `altText` field, then fall back to the product/variant `name`, then a generic but descriptive string (e.g. `'Photo of {productName}'`). Never leave `alt` as an empty string unless the image is purely decorative.
- For decorative images that carry no informational value (e.g. background flourishes), explicitly pass `alt=""` AND `role="presentation"` so screen readers skip them intentionally — do NOT omit the prop.
- Create a shared `<ProductImage>` wrapper component (e.g. `components/ui/ProductImage.tsx`) that encapsulates the `alt` derivation logic and the `<Image>` component so the pattern is consistent across PDP, PLP, cart, and search pages.
- Add an ESLint rule (`jsx-a11y/alt-text`) via `eslint-plugin-jsx-a11y` to enforce alt text at lint time and prevent regressions in CI.
- Run `next build` to catch TypeScript and lint errors, then audit with Lighthouse or axe DevTools to confirm zero missing-alt violations before deploying.
<!-- 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.
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
- Locate every page file that sets a meta description — these will be either `generateMetadata()` exports (App Router: `app/*/page.tsx`) or `<Head>` components (Pages Router: `pages/*/*.tsx`).
- Create a shared helper utility at `lib/seo.ts` that hard-limits any description string to 160 characters, trimming cleanly on a word boundary so no word is split mid-word.
- App Router — replace or wrap your existing description value inside `generateMetadata()` with the new `truncateDescription()` helper so the limit is enforced automatically at build/request time.
- Pages Router — if you use `next/head`, pass the description through the same `truncateDescription()` helper before rendering the `<meta name='description'>` tag.
- For dynamic pages (PDP, PLP, blog) that pull descriptions from a CMS or commerce API, apply `truncateDescription()` to the raw API value before it reaches the metadata object, ensuring no runtime value can exceed the limit.
- Run `next build` and audit the output with a meta-tag checker (e.g. browser DevTools → Elements, or a Lighthouse run) to confirm every description is ≤ 160 characters.
<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
- Audit every route that exports `generateMetadata` (App Router) or uses `next/head` (Pages Router) and measure the current `description` string length — flag anything outside 120–160 characters.
- For App Router routes (`app/` directory), update the `description` field inside each `generateMetadata` function, pulling dynamic data (product name, short description, category) from your commerce API so the copy is unique and 120–160 chars.
- For Pages Router routes (`pages/` directory), update the `<meta name='description'>` tag inside the `<Head>` component from `next/head` with the same length-checked string.
- Add a shared helper `truncateDescription(text, min=120, max=160)` in `lib/seo.ts` that trims or pads the raw commerce API copy to always land in the valid range — import it in every route.
- Set a `metadataBase` in the root `app/layout.tsx` so Open Graph and canonical URLs resolve correctly alongside the description.
- Run `next build` and spot-check with a meta-tag preview tool (e.g. Ahrefs SERP Simulator or Google's Rich Results Test) to confirm every description is 120–160 characters before deploying.
export async function generateMetadata({ params }) {
return {
description:
"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 in the generateMetadata() export in your page.tsx or layout.tsx file.
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
- Open the page component for the template type that is missing an H1 (e.g. pages/products/[slug].tsx or app/products/[slug]/page.tsx).
- Ensure the product/page name is rendered inside an <h1> tag: <h1>{product.title}</h1>.
- Confirm no global CSS or Tailwind class hides the element.
- Run 'npm run build && npm start', open the page, and inspect with DevTools (Ctrl+F '<h1>') to confirm.
export default function ProductPage({ product }: { product: { title: string } }) {
return (
<main>
<h1>{product.title}</h1>
{/* rest of page content */}
</main>
);
}In your Next.js product page component (app/products/[slug]/page.tsx or pages/products/[slug].tsx), render the title inside an H1.
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
- App Router (Next.js 13+): In your page.tsx/page.jsx, export: `export const metadata = { description: 'Your unique description here.' };` — Next.js automatically renders the `<meta name='description'>` tag.
- Pages Router: Import `Head` from `next/head`, then inside your component return: `<Head><meta name='description' content='Your unique description here.' /></Head>`.
- For dynamic routes (e.g., /products/[slug]): use `generateMetadata` (App Router) or `getServerSideProps`/`getStaticProps` to fetch the product's SEO description from your data source and pass it to the metadata/Head.
// app/products/[slug]/page.jsx
export async function generateMetadata({ params }) {
const product = await getProduct(params.slug); // your data-fetch function
return {
description: product.seoDescription, // 120–160 chars
};
}
export default function ProductPage({ params }) {
return <main>{/* page content */}</main>;
}App Router (Next.js 13+): export metadata from your page file. Place in app/products/[slug]/page.jsx.
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
- Decide which router you use: App Router (app/) uses `generateMetadata` / a static `metadata` export; Pages Router (pages/) uses the `<Head>` component from `next/head`. Apply the matching fix below.
- App Router — open every page file (e.g. `app/page.tsx`, `app/products/[slug]/page.tsx`) and export a `generateMetadata` async function that reads the page-specific description from your CMS/API and returns it under `openGraph.description`. Fall back to a site-wide default stored in a shared constant so no page is ever left empty.
- 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:description' content={description} />` inside it, sourcing the value from `getStaticProps` / `getServerSideProps`.
- Create a shared helper `lib/seo.ts` that exports a `buildOgDescription(raw?: string): string` function. It trims HTML tags, truncates to ≤ 155 characters, and returns the site-wide fallback when `raw` is falsy — import this everywhere so the format is consistent.
- Add `NEXT_PUBLIC_SITE_DESCRIPTION` to your `.env.local` (and your CI/CD environment variables) as the site-wide fallback description; reference it inside the shared helper.
- After deploying, validate each key URL with the [Open Graph debugger](https://developers.facebook.com/tools/debug/) or `curl -s <url> | grep og:description` to confirm the tag is present and populated.
// app/products/[slug]/page.tsx (Next.js App Router)
import type { Metadata } from 'next';
interface Props {
params: { slug: string };
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const product = await fetchProduct(params.slug); // your data-fetching function
return {
openGraph: {
title: product.name,
description: product.shortDescription, // 100-160 chars recommended
url: `https://www.example.com/products/${params.slug}`,
images: [
{
url: product.imageUrl,
width: 1200,
height: 630,
alt: product.name,
},
],
type: 'website',
},
};
}App Router (Next.js 13+): export generateMetadata from your page.tsx / page.js file.
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
- Create a default fallback OG image (e.g. 1200×630 px) and place it at /public/og-default.jpg so Next.js can serve it as a static asset.
- In app/layout.tsx, export a root `metadata` object with an `openGraph.images` array that points to the default image — this acts as the site-wide fallback for every page that does not override it.
- For static pages (e.g. app/about/page.tsx), export their own `metadata` object with a page-specific `openGraph.images` entry to override the root fallback.
- For dynamic pages (e.g. app/products/[slug]/page.tsx), export an async `generateMetadata()` function that fetches the product data and returns the product's image URL in `openGraph.images`; fall back to the default image when no product image is available.
- Ensure every image entry includes `url`, `width`, `height`, and `alt` fields — these are required by the Open Graph spec and improve how platforms render the preview.
- Verify the tags are rendered correctly by running `next build && next start` and inspecting the page `<head>` with curl or browser DevTools, then validate with the Meta Sharing Debugger (developers.facebook.com/tools/debug) or opengraph.xyz.
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
- In the App Router (Next.js 13+): export a generateMetadata function from your category page (e.g. app/collections/[slug]/page.tsx). Inside, construct the canonical URL from params only (no searchParams) and return it as: return { alternates: { canonical: `https://www.example.com/collections/${params.slug}` } };
- In the Pages Router: use next/head inside your category page component. Compute the canonical URL by using router.pathname (not router.asPath, which includes query strings): <Head><link rel='canonical' href={`https://www.example.com${router.pathname.replace('[slug]', router.query.slug)}`} /></Head>.
- Verify by running next build && next start, then viewing source on a filtered URL to confirm the canonical is the clean path.
import type { Metadata } from 'next';
type Props = {
params: { slug: string };
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const canonicalUrl = `https://www.example.com/collections/${params.slug}`;
return {
alternates: {
canonical: canonicalUrl,
},
};
}
export default function CollectionPage({ params }: Props) {
return <main>{/* page content */}</main>;
}App Router: place this generateMetadata export in app/collections/[slug]/page.tsx. It sets the canonical to the clean path, ignoring any searchParams.
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
- If your storefront is built on Next.js (headless Shopify, headless BigCommerce, custom), use getStaticProps (SSG) for product pages that don't change in real-time, or getServerSideProps (SSR) for pages requiring live inventory/pricing.
- In your [product].js or [handle].js page file, fetch product data inside getStaticProps or getServerSideProps and pass it as props — Next.js will render this into the HTML before it reaches the browser.
- In the page component, render <p>{product.description}</p> directly from props (not from a client-side useEffect fetch) so the description is in the server-rendered HTML.
- Add Product JSON-LD using Next.js's <Head> component or the next-seo library's ProductJsonLd component — both render into the <head> server-side.
- For App Router (Next.js 13+): use async Server Components — fetch product data directly in the component body (no useEffect), which renders on the server. Use the metadata API for structured data if needed.
- Validate with 'curl https://yourstore.com/products/example | grep description' — both the text and JSON-LD must appear in the plain HTML output.
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
- Audit every page's title: search your codebase for `title:` inside `generateMetadata` (App Router) or `<title>` inside `<Head>` (Pages Router) and list any that exceed 60 characters.
- Create a shared utility helper `lib/seo.ts` that trims any title to ≤60 characters at the nearest word boundary — use this everywhere titles are composed.
- App Router (`app/**/page.tsx`): update each `generateMetadata` export to pipe its title string through the helper before returning it.
- Pages Router (`pages/*/.tsx`): update each `<Head><title>` or `defaultTitle` in `next/head` to pipe through the same helper.
- For dynamic routes (e.g. product/category pages), ensure the fetched name/brand combination is truncated before interpolation so the full string — including separators like ` | Brand` — still fits within 60 chars.
- Run `next build` and spot-check titles in the generated HTML (`curl` the page or open DevTools) to confirm no title exceeds 60 characters.
<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
- Audit every route that has a short title: check both `generateMetadata()` (App Router) and `<Head>` / `next/head` (Pages Router) for `<title>` tags or `title` metadata fields shorter than 50 characters.
- For App Router routes (`app/` directory), open the relevant `page.tsx` (or `layout.tsx` for site-wide defaults) and update the exported `generateMetadata` function so that `title` — or `title.absolute` — is a descriptive string between 50 and 60 characters, including your brand name, primary keyword, and a value proposition.
- For Pages Router routes (`pages/` directory), open the relevant page file, import `Head` from `next/head`, and set a `<title>` element whose text is 50–60 characters.
- Use the `title.template` field in a root `layout.tsx` to auto-append your brand name (e.g. `'%s | Brand Name'`) so individual pages only need to provide the descriptive prefix — keeping the combined string within 60 characters.
- Run `next build` and inspect the rendered HTML (`<title>` in `<head>`) or use a browser extension to verify the final character count is 50–60 before deploying.
<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 · 3 fixes
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
- Create a reusable `BreadcrumbJsonLd` component at `components/seo/BreadcrumbJsonLd.tsx`. It accepts a `items` prop (array of `{ name, url }`) and renders a `<script type='application/ld+json'>` tag with the full BreadcrumbList schema.
- Import and render `<BreadcrumbJsonLd>` in every page or layout that should carry breadcrumb data — e.g. `app/products/[slug]/page.tsx`, `app/collections/[slug]/page.tsx`, and any other category/product/content pages.
- Build the `items` array dynamically from your route segments (or from the commerce API response). Each item must have a human-readable `name` and an absolute `url`. The Home item is always index position 1.
- For the *App Router* (`app/`), place the component anywhere inside the RSC/page component's JSX return — it renders a `<script>` tag into `<head>` automatically via React's built-in support for `<script>` in RSC, or use Next.js's `<Script strategy='beforeInteractive'>` from `next/script` if you need fine-grained control.
- For the *Pages Router* (`pages/`), drop the component inside `<Head>` from `next/head` — or, because `<script>` is not allowed inside `<Head>`, place it directly in the page JSX body (outside `<Head>`) so Next.js hoists it correctly.
- Validate the output with Google's Rich Results Test (https://search.google.com/test/rich-results) before deploying.
// components/BreadcrumbJsonLd.jsx
import Head from 'next/head';
export default function BreadcrumbJsonLd({ crumbs }) {
// crumbs: [{ name: 'Home', url: 'https://example.com/' }, ...]
const schema = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: crumbs.map((crumb, index) => ({
'@type': 'ListItem',
position: index + 1,
name: crumb.name,
item: crumb.url,
})),
};
return (
<Head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
</Head>
);
}
// Usage in pages/products/[slug].jsx:
// <BreadcrumbJsonLd crumbs={[
// { name: 'Home', url: 'https://example.com/' },
// { name: 'Products', url: 'https://example.com/products/' },
// { name: product.title, url: `https://example.com/products/${product.slug}` },
// ]} />Place this component in your product/category page files. Uses next/head to inject into <head>.
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
- Identify your router: if you use the App Router (app/), you'll inject JSON-LD via a <Script> tag inside the page component or layout; if you use the Pages Router (pages/), you'll use next/head. The snippet below targets the App Router (app/page.tsx / app/layout.tsx) — the most common modern setup.
- Create (or open) app/page.tsx (your homepage). Pull Organization data from environment variables or your CMS/storefront API at build/request time so the values stay dynamic and are never hard-coded secrets.
- Define a typed organizationSchema object that follows the schema.org/Organization spec: @context, @type, name, url, logo (ImageObject), contactPoint, and sameAs (social profiles).
- Render a Next.js <Script id='schema-organization' type='application/ld+json'> component (from 'next/script') with strategy='afterInteractive' — or, for pure SSR/static output with no client JS cost, use a plain <script> tag inside a server component, which is perfectly valid in the App Router.
- Verify the output in 'View Source' (not DevTools Elements tab) and test with Google's Rich Results Test and schema.org's validator to confirm the JSON-LD is present and error-free.
- Optionally move the schema into app/layout.tsx if you want it on every page, or keep it scoped to app/page.tsx (homepage only) — the latter is the recommended placement for Organization schema.
// app/layout.tsx (App Router example)
import type { Metadata } from 'next';
const orgSchema = {
'@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',
],
contactPoint: {
'@type': 'ContactPoint',
contactType: 'customer service',
email: 'hello@acmestore.com',
},
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(orgSchema) }}
/>
</head>
<body>{children}</body>
</html>
);
}Add to your app/layout.tsx (App Router) or pages/_app.tsx (Pages Router) inside the <Head> component. Uses dangerouslySetInnerHTML to avoid escaping issues.
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
- Create a reusable `SchemaMarkup` component at `components/SchemaMarkup.tsx` that accepts `WebPage` and `Organization` JSON-LD props and renders them via a Next.js `<Script>` tag (or a plain `<script>` inside the component) with `type='application/ld+json'`.
- Populate the `Organization` schema once with your brand's canonical details (name, URL, logo, sameAs social profiles) — ideally sourced from a central `lib/config.ts` constants file so they never go out of sync.
- For the `WebPage` schema, pass per-page values (`name`, `description`, `url`, `breadcrumb`, `dateModified`) as props. In the App Router fetch these from `generateMetadata` data so they share the same fetch; in the Pages Router pass them as `getStaticProps`/`getServerSideProps` return values.
- In the *App Router* (`app/`): import `SchemaMarkup` inside every `page.tsx` (or in `app/layout.tsx` for the global `Organization` block) and render it directly in the JSX — Next.js serialises it into the `<head>` automatically.
- In the *Pages Router* (`pages/`): import `SchemaMarkup` into `pages/_app.tsx` for the `Organization` block and into each individual page component for the `WebPage` block, or use `next/head` inside the component if you prefer inline `<Head>` management.
- Validate the output with Google's Rich Results Test and Schema.org's validator, then confirm the `<script type='application/ld+json'>` tags appear in `view-source` before caching/CDN layers to ensure crawlers see them.
<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.
Accessibility (WCAG) · 1 fixes
Html has langQuick win
Add a valid `lang` attribute to the `<html>` element so browsers, screen readers, and search engines know what language your page is written in.
On Next.js
- Open pages/_document.js (Pages Router) or app/layout.js (App Router).
- For Pages Router: In your `<Html>` component, add the lang prop: `<Html lang="en">`.
- For App Router: In your root layout.js, set the lang attribute on the html element: `export default function RootLayout({ children }) { return <html lang="en">{children}</html>; }`
- For i18n sites using next.config.js internationalization, you can use the locale from the router: `const { locale } = useRouter();` and pass it as the lang value.
- Deploy and verify via View Source.
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}For App Router: place this in app/layout.js as your root layout. Replace 'en' with your locale or pass it dynamically.
Security (OWASP) · 5 fixes
Info disclosure x powered byQuick win
Remove or suppress the X-Powered-By HTTP response header so your server stops advertising which technology stack, hosting platform, or software version powers your store.
On Next.js
- In next.config.js, add a custom headers() export that removes X-Powered-By, or use the built-in poweredByHeader: false option.
- Set module.exports = { poweredByHeader: false }; in next.config.js — Next.js will stop sending 'X-Powered-By: Next.js' in responses.
- If using a custom Express server, also add app.disable('x-powered-by'); and install helmet (npm install helmet) with app.use(helmet());
- Redeploy and verify via curl -I or DevTools.
/** @type {import('next').NextConfig} */
const nextConfig = {
// Disable the X-Powered-By: Next.js response header
poweredByHeader: false,
};
module.exports = nextConfig;Add or edit next.config.js in your project root. This disables the 'X-Powered-By: Next.js' header.
Missing permissions policyQuick win
Add a Permissions-Policy HTTP response header to your store to explicitly restrict which browser features (camera, microphone, geolocation, etc.) pages and embedded third-party scripts are allowed to access.
On Next.js
- Open next.config.js in your project root.
- Add or extend the `headers()` async function to return the Permissions-Policy header for all routes: async headers() { return [{ source: '/(.*)', headers: [{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=(), payment=(), usb=()' }] }] }
- Save the file and restart your Next.js server (npm run build && npm start for production).
- Verify with Chrome DevTools → Network → Response Headers.
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()',
},
],
},
];
},
};
module.exports = nextConfig;Add or merge into next.config.js at the project root.
Missing x content type optionsQuick win
Add the `X-Content-Type-Options: nosniff` HTTP response header to every page and asset your store serves.
On Next.js
- Open next.config.js in your project root.
- Add or update the headers() async function to return the header for all routes: { source: '/(.*)', headers: [{ key: 'X-Content-Type-Options', value: 'nosniff' }] }
- Redeploy your Next.js app (e.g. via Vercel, which also lets you set headers in vercel.json under the 'headers' key).
- Verify the header appears on all responses in browser dev-tools.
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
],
},
];
},
};
module.exports = nextConfig;Add or merge into next.config.js at the root of your project. The header will be sent on every route.
Vulnerable component axiosModerate effort
Update the Axios JavaScript library to its latest stable release to eliminate known security vulnerabilities in your storefront code.
On Next.js
- In your Next.js project root, run `npm install axios@latest` to update the package.
- Check that package.json shows the latest version, then run `npm run build` to rebuild.
- Deploy via your normal pipeline (e.g. `vercel --prod` or your CI/CD system).
- Run `npm audit` post-deploy to confirm zero Axios-related advisories.
# 1. Update Axios
npm install axios@latest
# 2. Confirm the installed version
npm list axios
# 3. Audit for any remaining advisories
npm audit
# 4. Build for production
npm run build
# 5. Deploy (example: Vercel)
vercel --prodRun these commands in your Next.js project root to update Axios and rebuild.
X frame options weakQuick win
Replace the weak X-Frame-Options header value with DENY or SAMEORIGIN to prevent clickjacking attacks on your store.
On Next.js
- Open next.config.js and add a headers() export: async headers() { return [{ source: '/(.*)', headers: [{ key: 'X-Frame-Options', value: 'DENY' }] }]; }
- Restart your Next.js server (or redeploy to Vercel/your host) and verify with DevTools.
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'Content-Security-Policy',
value: "frame-ancestors 'none';",
},
],
},
];
},
};
module.exports = nextConfig;Add or update next.config.js in your project root. This applies headers to all routes.
Site Lifecycle · 1 fixes
Html langQuick win
Add a lang attribute to your site's <html> tag that matches the primary language of your store's content.
On Next.js
- If using the App Router (Next.js 13+), open app/layout.tsx (or app/layout.jsx) and add the lang prop to the <html> element: <html lang="en">.
- If using the Pages Router, open pages/_document.js and set <Html lang="en"> (using Next.js's built-in Html component).
- For internationalized routing, configure i18n in next.config.js and Next.js will automatically pass the detected locale; bind it: <html lang={params.lang}> using the dynamic segment.
export default function RootLayout({
children,
params,
}: {
children: React.ReactNode;
params: { lang: string };
}) {
return (
<html lang={params.lang ?? 'en'}>
<body>{children}</body>
</html>
);
}In app/layout.tsx (App Router, Next.js 13+) — set lang statically or bind to a locale param for i18n routes.