> Portal Navigation:
> 
> - Append `.md` to any URL under `https://dev.wix.com/docs/` to get its markdown version.
> - Pages are either content pages (article or reference text) or menu pages (a list of links to child pages).
> - To get a menu page, truncate any URL to a parent path and append `.md` (e.g. `https://dev.wix.com/docs/sdk.md`, `https://dev.wix.com/docs/sdk/core-modules.md`).
> - Top-level index of all portals: https://dev.wix.com/docs/llms.txt
> - Full concatenated docs: https://dev.wix.com/docs/llms-full.txt

## Resource: Add SEO Support to Item Pages

## Article: Add SEO Support to Item Pages

## Article Link: https://dev.wix.com/docs/go-headless/wix-managed-headless/seo/add-seo-support-to-item-pages.md

## Article Content:

# Add SEO Support to Item Pages

An item page renders one business-solution item from a parameterized route, such as a product at `/product/[handle]` or a blog post at `/blog/[slug]`. Unlike main pages, which get their SEO tags injected automatically with no code, item pages need code to resolve an item's tags.

This article explains how to add the necessary code so SEO tags a Wix user edits in the dashboard appear on your headless frontend. After a one-time layout change to expose a tag slot, you add support in 2 parts:

1. Register each item-page route so Wix knows it exists.
2. Render the Wix-resolved tags into the page at request time.

For how these parts fit together, and how main pages differ, see [About SEO Support](https://dev.wix.com/docs/go-headless/wix-managed-headless/seo/about-seo-support.md).

This guide uses placeholders for the values that differ per page type. For the full set, see [Values for each page type](#values-for-each-page-type).

## Before you begin

Before starting to code, make sure that you have:

- A Wix-managed headless project built with Astro. It should already include the `@wix/astro` and `@wix/astro-pages` packages.
- [`@wix/seo`](https://www.npmjs.com/package/@wix/seo) and [`@wix/essentials`](https://www.npmjs.com/package/@wix/essentials) installed. Use `@wix/essentials` version 1.0.10 or later:

  ```bash
  npm install @wix/seo @wix/essentials
  ```

- Server-rendered item-page routes, configured with `output: "server"`. The calls that fetch SEO tags depend on request context, so don't set `prerender = true` or use `getStaticPaths()` on these routes.

## Step 1 | Add the SEO tags slot to your layout

Expose a named `seo-tags` slot in the `<head>` of your shared layout. Item pages fill this slot with the Wix-resolved tags.

Main pages leave it empty and get their tags from [automatic SEO injection](https://dev.wix.com/docs/go-headless/wix-managed-headless/seo/manage-seo-for-main-pages.md), so don't add fallback `<title>` or `<meta name="description">` tags here, since they'd duplicate the injected ones.

```astro
<!-- src/components/layout/layout.astro -->
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <slot name="seo-tags" />
  </head>
  <body>
    <slot />
  </body>
</html>
```

## Step 2 | Register the route

In each item-page route file, export a `wixMetadata` object so the page appears in the registry at `/_wix/pages.json`. Source the app ID, page identifier, and slug token from `WIX_APPS` rather than hard-coding them:

```js
import { WIX_APPS } from "@wix/essentials";

export const wixMetadata = {
  appDefId: WIX_APPS.<solution>.id,
  pageIdentifier: WIX_APPS.<solution>.<pageMetadata>.pageIdentifier,
  identifiers: {
    <routeParam>: WIX_APPS.<solution>.<pageMetadata>.identifiers.<slugToken>,
  },
};
```

<blockquote class="caution">

__Caution:__ Reference `WIX_APPS` directly inside the `wixMetadata` object, as shown. Don't read it into a separate variable first, such as `const { id } = WIX_APPS.<solution>`. Because `wixMetadata` is a module-level `export`, Astro evaluates it in the module scope, where variables declared in the component body aren't available.

</blockquote>

<blockquote class="important">

__Important:__ The key inside `identifiers` must be the route parameter of your route file, not the token the SDK uses. A route file named `[handle].astro` uses the key `handle`; `[slug].astro` uses `slug`.

</blockquote>

### Values for each page type

Each SEO-supported page type pairs a page-metadata accessor and slug token from `WIX_APPS` in `@wix/essentials` with an item type from `seoTags.ItemType` in `@wix/seo`. Because the packages don't reference each other, use this list to pair them for the `wixMetadata` export in Step 2 and the `loadSEOTagsServiceConfig()` call in Step 3:

- **Stores product**:
  - **Page metadata**: `WIX_APPS.checkoutAndOrders.productPageMetadata`
  - **Slug token**: `handle`
  - **Item type**: `seoTags.ItemType.STORES_PRODUCT`
- **Stores category**:
  - **Page metadata**: `WIX_APPS.checkoutAndOrders.categoryPageMetadata`
  - **Slug token**: `collection`
  - **Item type**: `seoTags.ItemType.STORES_CATEGORY`
- **Bookings service**:
  - **Page metadata**: `WIX_APPS.bookings.servicePageMetadata`
  - **Slug token**: `slug`
  - **Item type**: `seoTags.ItemType.BOOKINGS_SERVICE`
- **Events page**:
  - **Page metadata**: `WIX_APPS.events.eventPageMetadata`
  - **Slug token**: `slug`
  - **Item type**: `seoTags.ItemType.EVENTS_PAGE`
- **Blog post**:
  - **Page metadata**: `WIX_APPS.blogs.postPageMetadata`
  - **Slug token**: `slug`
  - **Item type**: `seoTags.ItemType.BLOG_POST`
- **Blog category**:
  - **Page metadata**: `WIX_APPS.blogs.categoryPageMetadata`
  - **Slug token**: `slug`
  - **Item type**: `seoTags.ItemType.BLOG_CATEGORY`

The `WIX_APPS` type is the source of truth for the exact accessor strings, the `pageIdentifier` each one resolves to, and the current set of supported page types. This list exists to pair each type with its `seoTags.ItemType`.

> **Note**: For Stores, the accessor is `WIX_APPS.checkoutAndOrders`, not `WIX_APPS.stores`. `WIX_APPS.stores.id` is the catalog ID used for `catalogReference.appId` in cart operations.

## Step 3 | Fetch and render the SEO tags

In the route's frontmatter, import the SEO helpers and call `loadSEOTagsServiceConfig()` at request time, then render the result into the `seo-tags` slot. The function takes the following fields:

- `pageUrl`: The canonical URL of the current page. Use `Astro.url.href`.
- `itemType`: A `seoTags.ItemType` value that identifies the page type.
- `itemData`: The item's identifier, always as `{ slug: "<value>" }`. The key is literally `slug` for every page type; the value is your route parameter, such as `Astro.params.handle` on a `[handle].astro` route.

```astro
---
import { SEO } from "@wix/seo/components";
import { loadSEOTagsServiceConfig } from "@wix/seo/services";
import { seoTags } from "@wix/seo";

const seoTagsServiceConfig = await loadSEOTagsServiceConfig({
  pageUrl: Astro.url.href,
  itemType: seoTags.ItemType.<ITEM_TYPE>,
  itemData: { slug: Astro.params.<routeParam>! },
});
---

<Layout>
  <SEO.Tags seoTagsServiceConfig={seoTagsServiceConfig} slot="seo-tags" />
  <!-- your page markup -->
</Layout>
```

<blockquote class="tip">

__Tip:__ When the page also fetches item data, run `loadSEOTagsServiceConfig()` in parallel with that fetch using `Promise.all` to avoid an extra round trip. See the example below.

</blockquote>

`loadSEOTagsServiceConfig()` returns the tags Wix has already resolved for the item. A Wix user controls these values in the dashboard under **SEO & GEO** > [**SEO Settings**](https://www.wix.com/my-account/site-selector/?buttonText=Select%20Site&title=Select%20a%20Site&autoSelectOnSingleSite=true&actionUrl=https:%2F%2Fwww.wix.com%2Fdashboard%2F%7B%7BmetaSiteId%7D%7D%2Fseo-home%2Fseo-settings), where each page type, such as Blog posts, has its own settings, and individual items can override them in their own SEO fields. You don't set these values in code. Your code only renders the resolved result.

## Step 4 | Optional: Add structured data

`<SEO.Tags>` covers the tags managed in the dashboard. To add [schema.org](https://schema.org) structured data for rich results, render a JSON-LD script from the item you already fetched, using the schema type that fits the page, such as `Product` for store products or `Event` for events:

```astro
---
const eventJsonLd = {
  "@context": "https://schema.org",
  "@type": "Event",
  name: event.title,
  description: event.shortDescription,
  startDate: event.startDate,
};
---

<script
  type="application/ld+json"
  set:html={JSON.stringify(eventJsonLd)}
  is:inline
/>
```

## Step 5 | Optional: Update tags during client-side navigation

`<SEO.Tags>` resolves the tags once, on the server. If your frontend navigates between items on the client without a full page reload, such as moving from one product to another in a single-page flow, the `<head>` keeps the first item's tags unless you update them.

To re-resolve tags on the client, wrap the relevant content in `<SEO.Root>`, which provides the SEO service context, and use `<SEO.UpdateTagsTrigger>`, a render-prop component that exposes an `updateSeoTags` function. Call it with the new item's type and slug to swap the tags in place:

```tsx
import { SEO } from "@wix/seo/components";
import { seoTags } from "@wix/seo";

<SEO.Root seoTagsServiceConfig={seoTagsServiceConfig}>
  <SEO.UpdateTagsTrigger>
    {({ updateSeoTags }) => (
      <a
        href="/product/another-product"
        onClick={() =>
          updateSeoTags(seoTags.ItemType.STORES_PRODUCT, {
            slug: "another-product",
          })
        }
      >
        Go to another product
      </a>
    )}
  </SEO.UpdateTagsTrigger>
</SEO.Root>
```

`<SEO.UpdateTagsTrigger>` must be nested inside `<SEO.Root>`. For a server-rendered page that emits its tags once, `<SEO.Tags>` alone is enough; you don't need `<SEO.Root>`.

## Step 6 | Verify your setup

To verify that your code is bringing the SEO tags as expected:

1. Run the [build](https://dev.wix.com/docs/wix-cli/command-reference/project-commands/build.md) command to build the assets for your project:

   ```bash
   wix build
   ```

1. Run the [release](https://dev.wix.com/docs/wix-cli/command-reference/project-commands/release.md) command to publish your project:

   ```bash
   wix release
   ```

1. Fetch a real item URL that returns `200`, not `404`, and inspect the resolved `<title>` in its `<head>`:

   ```bash
   curl -s https://<your-domain>/blog/<slug> | grep -o '<title[^>]*>[^<]*</title>'
   ```

   Confirm the title is that item's real title from the dashboard, not a generic page-type fallback such as `Post | <site name>`. An unregistered or mistyped route still returns fallback tags, so matching the real item's values is what proves your `<SEO.Tags>` render resolved the item.

1. Open `/_wix/pages.json` and confirm your item-page routes are listed. If the list shows only your static pages, the route isn't registered, so recheck the `wixMetadata` export. This registry is what feeds the sitemap and the dashboard SEO editor.

1. Change a value in the dashboard for the relevant page type, and then fetch the page again to confirm the change reaches the live `<head>`.

> **Notes**:
>
> - A route registered in `/_wix/pages.json` but missing `<SEO.Tags>` still ships your layout's default tags.
> - A page that renders `<SEO.Tags>` but exports no `wixMetadata` serves correct tags yet stays invisible to the sitemap.

## Examples

Complete examples for each item-page type. Each uses the values from [Values for each page type](#values-for-each-page-type), sources them from `WIX_APPS`, and runs the item fetch in parallel with `loadSEOTagsServiceConfig()`. The fetch helpers, such as `getProduct`, and the `<Layout>` component stand in for your own code.

- [Stores product page](#stores-product-page)
- [Stores category page](#stores-category-page)
- [Bookings service page](#bookings-service-page)
- [Events page](#events-page)
- [Blog post page](#blog-post-page)
- [Blog category page](#blog-category-page)

### Stores product page

Route file: `src/pages/product/[handle].astro`.

**Register the route:**

```js
import { WIX_APPS } from "@wix/essentials";

export const wixMetadata = {
  appDefId: WIX_APPS.checkoutAndOrders.id,
  pageIdentifier: WIX_APPS.checkoutAndOrders.productPageMetadata.pageIdentifier,
  identifiers: {
    // `handle` is this route's parameter (product/[handle].astro).
    handle: WIX_APPS.checkoutAndOrders.productPageMetadata.identifiers.handle,
  },
};
```

**Fetch and render the tags:**

```astro
---
import { SEO } from "@wix/seo/components";
import { loadSEOTagsServiceConfig } from "@wix/seo/services";
import { seoTags } from "@wix/seo";

const handle = Astro.params.handle!;
const [product, seoTagsServiceConfig] = await Promise.all([
  getProduct(handle),
  loadSEOTagsServiceConfig({
    pageUrl: Astro.url.href,
    itemType: seoTags.ItemType.STORES_PRODUCT,
    itemData: { slug: handle },
  }),
]);

if (!product) return new Response(null, { status: 404 });
---

<Layout>
  <SEO.Tags seoTagsServiceConfig={seoTagsServiceConfig} slot="seo-tags" />
  <!-- product markup -->
</Layout>
```

### Stores category page

Route file: `src/pages/search/[collection].astro`. Category pages resolve tags from the route parameter alone, so there's no item to fetch.

**Register the route:**

```js
import { WIX_APPS } from "@wix/essentials";

export const wixMetadata = {
  appDefId: WIX_APPS.checkoutAndOrders.id,
  pageIdentifier: WIX_APPS.checkoutAndOrders.categoryPageMetadata.pageIdentifier,
  identifiers: {
    // `collection` is this route's parameter (search/[collection].astro).
    collection: WIX_APPS.checkoutAndOrders.categoryPageMetadata.identifiers.collection,
  },
};
```

**Fetch and render the tags:**

```astro
---
import { SEO } from "@wix/seo/components";
import { loadSEOTagsServiceConfig } from "@wix/seo/services";
import { seoTags } from "@wix/seo";

const seoTagsServiceConfig = await loadSEOTagsServiceConfig({
  pageUrl: Astro.url.href,
  itemType: seoTags.ItemType.STORES_CATEGORY,
  itemData: { slug: Astro.params.collection! },
});
---

<Layout>
  <SEO.Tags seoTagsServiceConfig={seoTagsServiceConfig} slot="seo-tags" />
  <!-- category / search results markup -->
</Layout>
```

### Bookings service page

Route file: `src/pages/bookings/[slug].astro`.

**Register the route:**

```js
import { WIX_APPS } from "@wix/essentials";

export const wixMetadata = {
  appDefId: WIX_APPS.bookings.id,
  pageIdentifier: WIX_APPS.bookings.servicePageMetadata.pageIdentifier,
  identifiers: {
    // `slug` is this route's parameter (bookings/[slug].astro).
    slug: WIX_APPS.bookings.servicePageMetadata.identifiers.slug,
  },
};
```

**Fetch and render the tags:**

```astro
---
import { SEO } from "@wix/seo/components";
import { loadSEOTagsServiceConfig } from "@wix/seo/services";
import { seoTags } from "@wix/seo";

const slug = Astro.params.slug!;
const [service, seoTagsServiceConfig] = await Promise.all([
  getServiceBySlug(slug),
  loadSEOTagsServiceConfig({
    pageUrl: Astro.url.href,
    itemType: seoTags.ItemType.BOOKINGS_SERVICE,
    itemData: { slug },
  }),
]);

if (!service) return new Response(null, { status: 404 });
---

<Layout>
  <SEO.Tags seoTagsServiceConfig={seoTagsServiceConfig} slot="seo-tags" />
  <!-- service detail markup -->
</Layout>
```

### Events page

Route file: `src/pages/events/[slug].astro`.

**Register the route:**

```js
import { WIX_APPS } from "@wix/essentials";

export const wixMetadata = {
  appDefId: WIX_APPS.events.id,
  pageIdentifier: WIX_APPS.events.eventPageMetadata.pageIdentifier,
  identifiers: {
    // `slug` is this route's parameter (events/[slug].astro).
    slug: WIX_APPS.events.eventPageMetadata.identifiers.slug,
  },
};
```

**Fetch and render the tags:**

```astro
---
import { SEO } from "@wix/seo/components";
import { loadSEOTagsServiceConfig } from "@wix/seo/services";
import { seoTags } from "@wix/seo";

const slug = Astro.params.slug!;
const [event, seoTagsServiceConfig] = await Promise.all([
  getEvent(slug),
  loadSEOTagsServiceConfig({
    pageUrl: Astro.url.href,
    itemType: seoTags.ItemType.EVENTS_PAGE,
    itemData: { slug },
  }),
]);

if (!event) return new Response(null, { status: 404 });
---

<Layout>
  <SEO.Tags seoTagsServiceConfig={seoTagsServiceConfig} slot="seo-tags" />
  <!-- event detail markup -->
</Layout>
```

### Blog post page

Route file: `src/pages/blog/[slug].astro`.

**Register the route:**

```js
import { WIX_APPS } from "@wix/essentials";

export const wixMetadata = {
  appDefId: WIX_APPS.blogs.id,
  pageIdentifier: WIX_APPS.blogs.postPageMetadata.pageIdentifier,
  identifiers: {
    // `slug` is this route's parameter (blog/[slug].astro).
    slug: WIX_APPS.blogs.postPageMetadata.identifiers.slug,
  },
};
```

**Fetch and render the tags:**

```astro
---
import { SEO } from "@wix/seo/components";
import { loadSEOTagsServiceConfig } from "@wix/seo/services";
import { seoTags } from "@wix/seo";

const slug = Astro.params.slug!;
const [post, seoTagsServiceConfig] = await Promise.all([
  getBlogPost(slug),
  loadSEOTagsServiceConfig({
    pageUrl: Astro.url.href,
    itemType: seoTags.ItemType.BLOG_POST,
    itemData: { slug },
  }),
]);

if (!post) return new Response(null, { status: 404 });
---

<Layout>
  <SEO.Tags seoTagsServiceConfig={seoTagsServiceConfig} slot="seo-tags" />
  <!-- post markup -->
</Layout>
```

### Blog category page

Route file: `src/pages/blog-category/[slug].astro`. Category pages resolve tags from the route parameter alone, so there's no item to fetch.

**Register the route:**

```js
import { WIX_APPS } from "@wix/essentials";

export const wixMetadata = {
  appDefId: WIX_APPS.blogs.id,
  pageIdentifier: WIX_APPS.blogs.categoryPageMetadata.pageIdentifier,
  identifiers: {
    // `slug` is this route's parameter (blog-category/[slug].astro).
    slug: WIX_APPS.blogs.categoryPageMetadata.identifiers.slug,
  },
};
```

**Fetch and render the tags:**

```astro
---
import { SEO } from "@wix/seo/components";
import { loadSEOTagsServiceConfig } from "@wix/seo/services";
import { seoTags } from "@wix/seo";

const seoTagsServiceConfig = await loadSEOTagsServiceConfig({
  pageUrl: Astro.url.href,
  itemType: seoTags.ItemType.BLOG_CATEGORY,
  itemData: { slug: Astro.params.slug! },
});
---

<Layout>
  <SEO.Tags seoTagsServiceConfig={seoTagsServiceConfig} slot="seo-tags" />
  <!-- category listing markup -->
</Layout>
```

## Troubleshooting

### The dashboard shows no pages, or `/_wix/pages.json` is empty

A route file is throwing when Wix imports it, and one failing route is enough to clear the whole list. The usual cause is reading `WIX_APPS` into a variable instead of referencing it directly inside the `wixMetadata` export (see [Step 2](#step-2--register-the-route)). Fix the export, then rebuild and redeploy.

### Item URLs are missing from the sitemap

The route's `wixMetadata` is missing, or the key in `identifiers` doesn't match the route's filename parameter: use `handle` for `[handle].astro` and `slug` for `[slug].astro`. Confirm the route and its `identifiers` key in `/_wix/pages.json`.

### An item page shows a generic title

`loadSEOTagsServiceConfig()` didn't resolve the item, so only the fallback tags render. Check that the URL returns `200` rather than `404`, that `itemType` matches the page type, and that `itemData.slug` is the item's real slug.

### Tags don't change during client-side navigation

Server-rendered tags resolve once. To re-resolve them on client navigation, wrap the content in `<SEO.Root>` and call `updateSeoTags` with `<SEO.UpdateTagsTrigger>`, as shown in [Step 5](#step-5--optional-update-tags-during-client-side-navigation).

## See also

- [About SEO Support](https://dev.wix.com/docs/go-headless/wix-managed-headless/seo/about-seo-support.md)
- [Elevate API Call Permissions](https://dev.wix.com/docs/go-headless/wix-managed-headless/elevate-api-call-permissions.md)