---
schema_version: "1"
id: ae0d43815b98471d3d231dba8865d5269538e08f2a2fb84b3f398b25bbba4b26
title: SEO and Metadata
collection: developer
version: current
locale: en-us
portal_url: https://docs.salesforce.com/developer/current/en-us/commerce-cloud/sfnext/guides/sfnext-seo-metadata.md
canonical_url: https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/sfnext-seo-metadata.html
source_site: developer.salesforce.com
content_type: documentation
content_hash: 2390c4509e0bd9b73a0b2b38370e9ade4352ba0060f3134e2dc6d76db1d7a32c
products:
  - Composable Storefront
  - Salesforce APIs
---
# SEO and Metadata

Storefront Next template’s built-in SEO features helps optimize your store’s search and product discoverability. The storefront template provides SEO features with `hreflang` alternate links, canonical URLs, page titles, meta tags, indexing control, and structured data (JSON-LD)

## Hreflang Alternate Links

The storefront can render locale-aware `<link rel="alternate" hreflang="...">` tags in the document `<head>`. These tags are generated centrally in [`src/utils/seo.ts`](https://github.com/SalesforceCommerceCloud/storefront-next-template/blob/latest/src/utils/seo.ts) and returned from the root [`meta` export in `src/root.tsx`](https://github.com/SalesforceCommerceCloud/storefront-next-template/blob/latest/src/root.tsx).

### Why It Matters

Search engines use `hreflang` to understand whether several pages are regional or language-specific versions of the same content.

For an ecommerce storefront, the alternate links feature is especially useful because:

- It helps search engines send shoppers to the right language or locale version of a product, category, or content page.
- It reduces confusion between similar pages that differ only by locale, currency, or URL prefix.
- It increases the likelihood that international shoppers land on the version of the storefront that matches their market.

### How It Works

The root loader calls `buildSeoMetaDescriptors(...)`, which returns:

1. A canonical `<link rel="canonical">`
2. One `<link rel="alternate" hreflang="...">` per supported locale
3. An `x-default` alternate pointing to the site's default locale

The alternate URLs are built from the current request path plus the configured multisite URL prefix. In practice, this means:

- Locale-specific URLs are generated automatically from `site.supportedLocales`.
- Locale aliases are respected when configured in the multisite setup.
- The current locale prefix is stripped before alternates are rebuilt, so switching locales keeps the same logical page path.
- Query parameters follow the same canonical normalization rules described in a section later.
- Each page includes a self-referencing hreflang tag pointing to itself, which Google requires for valid hreflang sets.

### Example Output

```html
<!-- When viewing the en-GB version of a page with en-GB (default) and fr-FR locales: -->
<link rel="canonical" href="https://www.example.com/global/en-GB/product/123" />
<link rel="alternate" hreflang="en-GB" href="https://www.example.com/global/en-GB/product/123" />
<!-- self-referencing -->
<link rel="alternate" hreflang="fr-FR" href="https://www.example.com/global/fr-FR/product/123" />
<link
  rel="alternate"
  hreflang="x-default"
  href="https://www.example.com/global/en-GB/product/123"
/>
```

### When You Need to Change `hreflang` Tags

You usually don't add `hreflang` tags in route components manually. Update this centralized logic only if you change:

- The multisite URL prefix format
- How locale aliases map into URLs
- Which query parameters are considered content-bearing for canonicalization

## Canonical URLs

Every page renders a `<link rel="canonical">` tag in the `<head>`. This tells search engines which URL should be treated as the preferred version of the page.

### Why It Matters

Search engines often discover multiple URLs that show the same page content. Canonical URLs tell them which version should receive SEO credit.

For an ecommerce storefront, canonical URLs are useful because:

- Product and category pages often collect tracking parameters, sort parameters, and filter parameters that can create many URL variations.
- It helps consolidate ranking signals instead of spreading them across duplicate URLs.
- It lowers the risk of duplicate-content problems on search, category, and product detail pages.

### How It Works

The canonical URL is built in [`src/utils/canonical-url.ts`](https://github.com/SalesforceCommerceCloud/storefront-next-template/blob/latest/src/utils/canonical-url.ts). Storefront Next adds it to the root-level SEO descriptors generated by [`src/utils/seo.ts`](https://github.com/SalesforceCommerceCloud/storefront-next-template/blob/latest/src/utils/seo.ts) and renders it from [`src/root.tsx`](https://github.com/SalesforceCommerceCloud/storefront-next-template/blob/latest/src/root.tsx).

It applies three normalizations:

1. **Allowlisted query parameters** — Only parameters that change page content are kept. Everything else (tracking params, analytics IDs, unknown params) is stripped.
2. **Sorted parameters** — Retained params are sorted alphabetically so that `?sort=price&q=jacket` and `?q=jacket&sort=price` produce the same canonical URL.
3. **Trailing slash removal** — Trailing slashes are removed from non-root paths (`/product/jacket/` → `/product/jacket`).

### Query Parameter Allowlist

The canonical URL uses an **allowlist** that specifies which query parameters are kept in canonical URLs. Storefront Next strips any parameter not explicitly listed, such as tracking parameters, analytics IDs, and other non-content parameters.

The current allowlist is defined in `src/utils/canonical-url.ts`:

| Parameter | Purpose            | Used by             |
| --------- | ------------------ | ------------------- |
| `q`       | Search query       | Search page         |
| `offset`  | Pagination offset  | Category, Search    |
| `sort`    | Sort order         | Category, Search    |
| `refine`  | Filter refinements | Category, Search    |
| `pid`     | Product variant ID | Product detail page |

### Adding a Custom Query Parameter

If you add a feature that uses a new query parameter to change the page's main content, add it to the allowlist so it's kept in the canonical URL and in locale alternates.

**Example:** You add a `?view=grid` parameter that switches between grid and list layouts with different product data.

1. Open `src/utils/canonical-url.ts`
2. Add the parameter to the `CONTENT_PARAMS` set:

```typescript
const CONTENT_PARAMS = new Set([
  "q",
  "offset",
  "sort",
  "refine",
  "pid",
  "view", // layout mode (grid/list)
]);
```

3. Add a corresponding test in `src/utils/canonical-url.test.ts`:

```typescript
it("preserves view param", () => {
  expect(buildCanonicalUrl(origin, "/category/mens", "?view=grid")).toBe(
    "https://www.example.com/category/mens?view=grid",
  );
});
```

**When not to add a parameter:** If the parameter doesn't change the page's main content, don't add it to the allowlist. Examples include analytics flags, modal triggers, and UI preferences.

## Page Titles and Meta Tags (`SeoMeta`)

`SeoMeta` is a React component defined in [`src/components/seo-meta/index.tsx`](https://github.com/SalesforceCommerceCloud/storefront-next-template/blob/latest/src/components/seo-meta/index.tsx). Route components render this component to add page-level `<title>` and `<meta>` tags. `SeoMeta` handles page titles, descriptions, robots directives, Open Graph tags, and X (formerly Twitter) Card tags.

Canonical and `hreflang` links are **not** rendered by `SeoMeta`. Those are generated centrally at the root level, as described above.

React hoists these tags into `<head>` automatically, so you can render the `SeoMeta` component inside a route component and the final tags still end up in the right place.

### Why It Matters

The page title and meta tags help search engines understand what a page is about and how it should appear in search results or link previews.

For an ecommerce storefront, these tags are especially useful because:

- Clear titles help product, category, and content pages compete for relevant searches.
- Good descriptions can improve click-through rate from search results.
- Consistent metadata makes it easier for search engines to understand the difference between product, category, cart, account, and content pages.

### What `SeoMeta` Renders

| Concern               | Tags                                                                          |
| --------------------- | ----------------------------------------------------------------------------- |
| Title                 | `<title>`                                                                     |
| Description           | `<meta name="description">`                                                   |
| Robots                | `<meta name="robots" content="noindex">` when `noIndex` is true               |
| Open Graph            | `og:title`, `og:description`, `og:type`, `og:url`, `og:image`, `og:site_name` |
| X Card / Twitter Card | `twitter:card`, `twitter:title`, `twitter:description`, `twitter:image`       |

### Basic Usage

```tsx
import { SeoMeta } from "@/components/seo-meta";

// `SeoMeta` is a component that you render in your route
// Standard page — site name is appended automatically
// Renders: <title>Classic Jacket | My Storefront Next Store</title>
<SeoMeta
  title="Classic Jacket"
  description="A premium leather jacket with a tailored fit."
  openGraph={{
    type: "product",
    url: "https://www.example.com/product/classic-jacket",
    image: "https://www.example.com/images/classic-jacket.jpg",
  }}
/>;
```

### Props

| Prop          | Type      | Default                       | Description                                                                                                                                                |
| ------------- | --------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `title`       | `string`  | —                             | Page title. By default it renders as `{title} \| {siteName}`. If omitted, the site name alone is used.                                                     |
| `rawTitle`    | `boolean` | `false`                       | Render `title` exactly as given, with no `\| {siteName}` suffix. See [Title Modes](#title-modes) below.                                                    |
| `description` | `string`  | —                             | Meta description. Also reused for Open Graph and X Card description tags when those are rendered.                                                          |
| `noIndex`     | `boolean` | `false`                       | Adds `<meta name="robots" content="noindex">`.                                                                                                             |
| `siteName`    | `string`  | `t('common:defaultSiteName')` | Override the site name used in the title suffix and `og:site_name`.                                                                                        |
| `twitter`     | `object`  | —                             | X Card metadata (`cardType`, `image`). The `twitter:` meta tag prefix is part of the Card spec and is still used by X.                                     |
| `openGraph`   | `object`  | —                             | Open Graph metadata (`type`, `url`, `image`) for social sharing. When provided without `twitter`, X Card tags are auto-derived from the Open Graph values. |

### Title Modes

`SeoMeta` supports two title modes through the `title` and `rawTitle` props:

| Mode                   | Props                      | `<title>` output                      | When to use                                                                              |
| ---------------------- | -------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------- |
| **Suffixed** (default) | `title="My Page"`          | `My Page \| My Storefront Next Store` | Most pages: product, category, search, account, and content pages.                       |
| **Raw**                | `rawTitle title="My Page"` | `My Page`                             | Pages that need full control, for example, the homepage that passes the full store name. |
| **Fallback**           | _(no title)_               | `My Storefront Next Store`            | Only the site name.                                                                      |

**Examples:**

```tsx
// Suffixed (default) — most pages use this
<SeoMeta title="Classic Jacket" />
// → <title>Classic Jacket | My Storefront Next Store</title>

// Raw — homepage or any page needing exact title control
<SeoMeta rawTitle title="My Storefront Next Store — Shop the Latest" />
// → <title>My Storefront Next Store — Shop the Latest</title>

// Fallback — no title provided
<SeoMeta />
// → <title>My Storefront Next Store</title>
```

### General Meta Tag Behavior

- **Title** comes from `title` and also contains the localized site name suffix unless `rawTitle` is set.
- **Description** renders `meta[name="description"]` and is reused for Open Graph and X Card descriptions.
- **Open Graph** is opt-in via the `openGraph` prop.
- **X Card / Twitter Card** is opt-in via the `twitter` prop, or auto-generated from `openGraph`.
- **Robots** only renders when `noIndex` is set for pages that should not be indexed.

### Why Each Meta Tag Helps

- **Title**: Search engines use the page title as a strong signal for what the page is about, and it is often shown as the main clickable headline in search results.
- **Description**: Search engines may use the meta description as the summary snippet in search results, which can help attract clicks.
- **Open Graph**: Social platforms use these tags when a page is shared, helping product and content links look more complete and trustworthy.
- **X Card / Twitter Card**: Similar to Open Graph, these tags improve how shared links appear on X.
- **Robots / `noIndex`**: This helps keep low-value or private pages out of search results so search engines focus on pages shoppers should actually land on.

### Open Graph and X Card Behavior

When `openGraph` is provided, `SeoMeta` renders:

- `og:title`
- `og:description` when `description` is present
- `og:type` (defaults to `website`)
- `og:url` when provided
- `og:image` when provided
- `og:site_name`

When `twitter` is provided, or when `openGraph` is provided without an explicit `twitter` prop, `SeoMeta` renders:

- `twitter:card`
- `twitter:title`
- `twitter:description` when `description` is present
- `twitter:image` when an image is available

If `openGraph.image` is present and `twitter.cardType` is not set, the card defaults to `summary_large_image`; otherwise it defaults to `summary`.

### Why Social Tags Matter for SEO

Open Graph and X Card tags aren't direct search ranking factors, but they're still valuable for storefront SEO. When someone shares a storefront URL on a platform such as Facebook, LinkedIn, X, Slack, or other messaging tools, that platform's crawler usually fetches the page and reads its social metadata.

The crawler uses tags such as:

- `og:title` or `twitter:title` for the preview headline
- `og:description` or `twitter:description` for the preview summary
- `og:image` or `twitter:image` for the preview image
- `twitter:card` to decide which X card layout to use

This metadata helps the platform build a richer preview card instead of showing a plain URL.

For an ecommerce storefront, this is useful because:

- Product, category, and campaign links look more polished and trustworthy when shared.
- Preview cards can show the product name, a useful summary, and a strong product image before the shopper even clicks.
- Better-looking previews can improve click-through from social, messaging, affiliate, and marketing channels.
- Consistent preview data reduces the chance that a platform picks incomplete or low-quality fallback content from the page.

### Canonical URL Consistency for Social Tags

For pages that supply `openGraph.url`, pass the same absolute URL that you want search engines to treat as canonical. In most routes, this is the canonicalized `pageUrl` built in the loader.

This keeps search and social signals aligned around the same preferred URL.

### Changing the Site Name

The site name appended to page titles (for example, `" | My Storefront Next Store"`) comes from the `common.defaultSiteName` translation key. To change it, update the value in each locale's `translations.json`:

```json
// src/locales/en-US/translations.json
{
  "common": {
    "defaultSiteName": "Your Store Name"
  }
}
```

You can also override it per-route via the `siteName` prop:

```tsx
<SeoMeta title="Sale" siteName="My Outlet Store" />
// Renders: "Sale | My Outlet Store"
```

### Route Patterns

The template uses two common patterns depending on whether a page is public or auth-protected.

**Public pages**: Include both title and description.

```tsx
<SeoMeta
  title={t("meta.title", { defaultValue: "About Us" })}
  description={t("meta.description", { defaultValue: "Learn more about our story." })}
/>
```

**Auth-protected or transactional pages**: Include a title for browser tab UX, but omit the description and set `noIndex`.

```tsx
<SeoMeta title={t("meta.title", { defaultValue: "Order History" })} noIndex />
```

### Adding Meta Tags to a New Page

1. Import `SeoMeta` and render it inside your route component:

```tsx
import { SeoMeta } from "@/components/seo-meta";

export default function MyNewPage() {
  const { t } = useTranslation("myPage");

  return (
    <>
      <SeoMeta
        title={t("meta.title", { defaultValue: "My Page" })}
        description={t("meta.description", { defaultValue: "Description of my page." })}
      />
      {/* page content */}
    </>
  );
}
```

2. If the page should be shared socially, pass `openGraph` and optionally `twitter`.
3. All title and description strings use i18n with `defaultValue` fallbacks. To localize them, add the corresponding keys to your translation files, for example `meta.title` and `meta.description`.

### Where `SeoMeta` Is Used

| Route                          | Title               | Description                       | Open Graph / Twitter | noIndex |
| ------------------------------ | ------------------- | --------------------------------- | -------------------- | ------- |
| `/` (Home)                     | Store name (raw)    | Welcome message                   | Yes                  | —       |
| `/category/:id`                | Category name       | Category page/general description | Yes                  | —       |
| `/product/:id`                 | Product name        | Product page/short description    | Yes                  | —       |
| `/search`                      | Search query        | Result count + query              | Yes                  | —       |
| `/about-us`                    | About Us            | Store mission description         | Yes                  | —       |
| `/login`                       | Sign In             | Sign in prompt                    | Yes                  | —       |
| `/cart`                        | Cart                | Cart review prompt                | Yes                  | —       |
| `/signup`                      | Sign Up             | Account creation prompt           | Yes                  | —       |
| `/checkout`                    | Checkout            | —                                 | No                   | Yes     |
| `/order-confirmation/:orderNo` | Order Confirmation  | —                                 | No                   | Yes     |
| `/account/*` (all sub-pages)   | Page-specific title | —                                 | No                   | Yes     |

**Note on search and cart indexability:** The template indexes `/search` and `/cart` by default. Some storefronts prefer to add `noIndex` to internal search results (to avoid thin/duplicate content) and to the cart page (session-specific content). Evaluate whether these pages provide value as search landing pages for your store and add noIndex if they do not.

### Why Some Pages Use `noIndex`

Pages behind authentication (account, wishlist, orders) and transactional pages (checkout, order confirmation) use `noIndex` because:

- **Account pages aren't useful landing pages for search**: Crawlers trying to visit account, wishlist, or order pages will usually be redirected to the login page instead of seeing the actual account content.
- **Checkout pages are often empty without shopper state**: A crawler visiting checkout without an active basket will usually see an empty or unusable checkout experience, which is not helpful in search results.
- **Some pages are session-specific**: An order confirmation page or another shopper-specific page has no value for a general search audience.

If your implementation exposes any of these pages publicly (for example, shared wishlists), remove `noIndex` from those routes.

### Why `noIndex` Helps

Search engines work best when they focus on pages that are useful landing pages for shoppers.

For an ecommerce storefront, `noIndex` helps by:

- Keeping checkout, account, and other private flows out of search results
- Preventing thin, empty, or session-specific pages from being indexed
- Helping search engines spend more attention on high-value pages such as product, category, brand, and content pages
