> ## Documentation Index
> Fetch the complete documentation index at: https://dokumentation.websale.de/llms.txt
> Use this file to discover all available pages before exploring further.

# Migration of the Strapi data structure (version 5)

> Instructions for migrating existing shops to the new Strapi data structure: new JSON output format, differences from the old structure and adjusting the shop templates.

export const ComingSoon = () => <span style={{
  display: 'inline-block',
  padding: '1px 10px',
  borderRadius: '9999px',
  fontSize: '0.75rem',
  fontWeight: 600,
  letterSpacing: '0.02em',
  backgroundColor: 'rgba(245, 158, 11, 0.18)',
  color: '#D97706',
  border: '1px solid rgba(245, 158, 11, 0.5)',
  verticalAlign: 'middle',
  whiteSpace: 'nowrap'
}}>
    Coming Soon
  </span>;

This page describes how existing shops are migrated to the new Strapi data structure. Because the previous content sync is being switched to the new sync middleware, the format of the JSON files that Strapi passes on to the shop changes. Shop templates that are based on the old data structure have to be adjusted for this.

<Note>
  <ComingSoon />

  The new sync middleware is currently under development. This page describes the data structure that has already been confirmed.
</Note>

## Does the new data structure already apply to my shop?

The new sync middleware is introduced with the update to Strapi version 5 and is the standard from then on. It does not exist before that. This results in three cases:

| **Your shop**                                      | **Action required**                                             |
| -------------------------------------------------- | --------------------------------------------------------------- |
| Does not yet use Strapi                            | None.                                                           |
| Is newly set up with Strapi version 5              | None. The shop receives the new data structure from the outset. |
| Already uses Strapi with the previous content sync | One-time migration of the templates, as described on this page. |

## What this page covers and what it does not

This page covers the new output format of the JSON files, the differences from the old structure, and the template adjustments required for it.

**Not covered:**

* **Creating or extending content in Strapi.** That is independent of the migration. The basics are described under [Basics & architecture of Strapi](/en/strapi-cms/grundlagen-architektur-von-strapi).
* **Rendering new content building blocks.** A template only ever renders the input masks, fields, and components that are implemented in it. If an editor creates a new input mask or a new component, the template has to be extended for it. Both before and after the migration. The procedure is described under [Adapting templates for Strapi content](/en/strapi-cms/templates-fur-strapi-inhalte-anpassen).

## Structure of this guide

The guide consists of two parts. Part A explains the basics, part B is the actual working instruction.

**Part A – Basics (reference part):**

* [Terminology: Strapi in five sentences](#terminology-strapi-in-five-sentences)
* [Why the data format is changing](#why-the-data-format-is-changing)
* [The two new formats compared](#the-two-new-formats-compared)
* [The mapping format in detail](#the-mapping-format-in-detail) and [The schema format in detail](#the-schema-format-in-detail)
* [Changes compared to the old structure](#changes-compared-to-the-old-structure)

**Part B – [Migrating templates: the eight steps](#migrating-templates-the-eight-steps):**

| **Step** | **What happens**                         | **When needed**                              |
| -------- | ---------------------------------------- | -------------------------------------------- |
| 1        | Find the affected places in the template | Always                                       |
| 2        | Add a test-mode switch (safety net)      | Always, **before** the first change          |
| 3        | Adjust the file names in the load calls  | Always                                       |
| 4        | Update field accesses                    | Always                                       |
| 5        | Adjust content blocks (dynamic zone)     | If the template renders content blocks       |
| 6        | Adjust media accesses                    | If images or files from Strapi are output    |
| 7        | Read SEO fields from `meta`              | If the template outputs SEO data from Strapi |
| 8        | Verify in test mode and go live          | Always, as the last step                     |

## Terminology: Strapi in five sentences

These five terms are used throughout the page and are essential for understanding the guide.

| **Term**                                     | **Meaning**                                                                                                                                                                                                                                 |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Input mask** (in Strapi: content type)     | The template that defines which input fields a piece of content has. Example: a content page with the fields "Title" and "Content".                                                                                                         |
| **Input field** (field)                      | A single field of that mask. Each field has a technical field name (for example `title`) and a field type (text, image, yes/no …).                                                                                                          |
| **Document**                                 | A concretely filled-in piece of content, for example the page "Terms and conditions". Per document and language, a dedicated set of JSON files is created for the shop (see [The two new formats compared](#the-two-new-formats-compared)). |
| **Component**                                | A reusable building block made up of several fields, for example a button consisting of "label" and "link".                                                                                                                                 |
| **Content blocks** (in Strapi: dynamic zone) | A field into which the editor inserts any number of components in any order. The typical page layout of a content page.                                                                                                                     |

In the following, field access means: the place in the template that reads the value of an input field from the loaded JSON file, for example the page title.

These terms are described in more detail under [Basics & architecture of Strapi](/en/strapi-cms/grundlagen-architektur-von-strapi).

## Why the data format is changing

The shop templates read content from the JSON files via the technical field names. Until now, the format of these files was closely coupled to the internal REST structure of Strapi. With every major Strapi version jump (most recently from version 4 to 5), this structure could change, which required adjustments in the shop and templates.

The new sync middleware decouples the output format from the internal Strapi structure. WEBSALE thus specifies its own, fixed data structure that remains stable even after future Strapi updates. Existing shops must be migrated once to this new structure. After that, they are update-ready without the templates needing to be adjusted for each Strapi update.

A second reason lies in Strapi itself: through the [Content-Type Builder](/en/strapi-cms/grundlagen-architektur-von-strapi), authorized users can not only maintain content but also change the input masks. Fields can be created, renamed, or deleted.

## The two new formats compared

The new sync middleware writes every document into two files, in two different formats. Both contain the same content, only arranged differently. By default, both are generated in parallel so that a gradual switch is possible.

The same document ("Terms and conditions", fields `title` and `content`) looks like this in the two formats:

**mapping format** – each input field is directly a JSON key:

```json theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/websale.json"]}}
{
  "title": "Terms and conditions",
  "content": [ … ]
}
```

**schema format** – each input field is an entry in a list, always with the same three keys:

```json theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/websale.json"]}}
{
  "contentType": "api::contentpage.contentpage",
  "meta": { … },
  "fields": [
    { "name": "title",   "type": "string",      "value": "Terms and conditions" },
    { "name": "content", "type": "dynamiczone", "value": [ … ] }
  ]
}
```

Overview of the differences:

|                                 | **mapping format**                                                                                       | **schema format**                                                                                                      |
| ------------------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| File name                       | `<documentId>.json`                                                                                      | `<documentId>.schema.json`                                                                                             |
| Template access                 | directly via the field name (`$cCMSData.title`)                                                          | via the `fields` list, converted once into a name/value object                                                         |
| Migration effort                | low – essentially the `attributes` wrapper is removed                                                    | higher – the field accesses are rebuilt                                                                                |
| If a field in Strapi is renamed | The **structure of the file changes**: the JSON key has a different name. The template must be adjusted. | The **structure of the file stays the same**, only the value of `name` changes. The template must be adjusted as well. |
| If a field is added or removed  | A key is added or missing.                                                                               | A list entry is added or missing.                                                                                      |
| Advantage                       | simplest switch, shortest templates                                                                      | stable file structure, errors are easier to find, field type is included                                               |

<Info>
  Important: **In both formats, the template location must be adjusted when an input field is renamed.** No format protects against that. The only difference is whether the structure of the file changes (mapping) or just a value within it (schema).
</Info>

## The mapping format in detail

### Structure

A content page with the input fields "title" and "content" looks like this:

```json theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/websale.json"]}}
{
  "title": "Terms and conditions",
  "content": [
    {
      "component": "elemente.ws-markup",
      "Markup": "Terms content",
      "FullWidth": false
    }
  ],
  "_sync": { "contentType": "api::contentpage.contentpage" }
}
```

* Each input field is a JSON key at the top level. The value sits directly below it.
* The `_sync` key is not a content field but a technical addition of the sync middleware. `_sync.contentType` names the input mask the document comes from.
* Content blocks appear as an array under their field name (here `content`). Each block carries its component name in `component` – **no longer** in `__component` as in the old structure – and its fields directly next to it.
* Media are provided as a flat, normalized media object, identical to the schema format (see [Media](#media-in-the-schema-format)). The `data`/`attributes` wrapper of the old structure is removed.

The structure therefore largely corresponds to the previous structure, but without the `attributes` wrapper and with the other changes from the [comparison below](#changes-compared-to-the-old-structure).

### What to watch out for with the mapping format

* **Field names come through unfiltered.** If an input field is renamed in Strapi, the JSON key changes. The affected template location must then be adjusted. This corresponds to the behavior you know from custom product data fields in the shop.
* **No field type in the file.** Unlike the schema format, there is no indication whether a value is text, a number, or a media object. The template has to know that.
* **Reserved names.** A field named `_sync` in Strapi would collide with the technical key. Avoid leading underscores in field names.

<Note>
  Where the SEO data of the Meta-Info plugin sits in the mapping format has not yet been finally decided. Clarify this for your shop with your WEBSALE contact before you implement [step 7](#step-7-read-seo-fields-from-meta) based on the mapping format. In the schema format, they sit in `meta`.
</Note>

## The schema format in detail

Every file of the schema format describes exactly one document, for example the page "Terms and conditions". Each file consists of three parts:

| **Part**      | **Content**                                                                    | **Details**                                                                               |
| ------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
| `contentType` | The technical identifier of the input mask the document comes from.            | [The envelope](#the-envelope)                                                             |
| `meta`        | Administrative data such as IDs and timestamps, plus the SEO data of the page. | [Administrative data and SEO fields in meta](#administrative-data-and-seo-fields-in-meta) |
| `fields`      | The actual content, i.e. the values of all input fields.                       | [The content fields in fields](#the-content-fields-in-fields)                             |

These three parts together are called the "envelope". This is the outer frame of every file, which always looks the same. Even when input masks are changed in Strapi.

For the migration itself, it is enough to know this base structure. The following subsections are the reference part for the concrete template adjustment.

### The envelope

```json theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/websale.json"]}}
{
  "contentType": "api::contentpage.contentpage",
  "meta": { … },
  "fields": [
    { "name": "…", "type": "…", "value": "…" }
  ]
}
```

| **Key**       | **Type** | **Meaning**                                                                                                                                                               |
| ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `contentType` | string   | Source UID of the input mask, for example `api::contentpage.contentpage`. The UID has the form `api::<name>.<name>` and is visible in Strapi in the Content-Type Builder. |
| `meta`        | object   | Fields managed by Strapi or the sync middleware (see below).                                                                                                              |
| `fields`      | array    | The actual content fields, each as `{ name, type, value }`.                                                                                                               |

<Info>
  This envelope is identical for all input masks and documents. That also holds when fields are renamed, added, or deleted. Only the entries inside `fields` change as a result.
</Info>

### Administrative data and SEO fields in meta

`meta` contains the administrative fields of a document. They are deliberately kept separate from the content fields in `fields`:

| **Field**                 | **Type**          | **Note**                                                                                                                                                                                              |
| ------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                      | number            | Internal numeric ID (per language version).                                                                                                                                                           |
| `documentId`              | string            | Stable, cross-language document ID. Determines the file name by default.                                                                                                                              |
| `locale`                  | string            | Language of this file, for example `de`. The language code does not necessarily match the directory name in the shop (for example `Deutsch`); the mapping is determined by the subshop configuration. |
| `createdAt` / `updatedAt` | string (ISO-8601) | Timestamps from Strapi.                                                                                                                                                                               |
| `publishedAt`             | string or null    | `null` for drafts.                                                                                                                                                                                    |

If the Meta-Info plugin is active for a document (see [SEO meta data for CMS pages](/en/strapi-cms/grundlagen-architektur-von-strapi#seo-meta-daten-fur-cms-seiten-meta-info-plugin)), `meta` additionally contains the SEO fields of the page:

| **Field**         | **Type**  | **Note**                                                                                                                                                                                                                                                                    |
| ----------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `url`             | string    | SEO URL of the page.                                                                                                                                                                                                                                                        |
| `metaTitle`       | string    | Meta title.                                                                                                                                                                                                                                                                 |
| `metaDescription` | string    | Meta description.                                                                                                                                                                                                                                                           |
| `robots`          | string\[] | An array, not a comma-separated string. For example `["noindex", "nofollow"]`.                                                                                                                                                                                              |
| `hreflang`        | array     | References to the language equivalents of the same page, for the hreflang entries in the HTML head. Calculated automatically as soon as the Meta-Info plugin is active. Contains one entry per subshop that serves this language (`{ language, url, subshopId, default }`). |

```json theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/websale.json"]}}
"meta": {
  "id": 99,
  "documentId": "l9h00uvgblpjvlqqv1va9s44",
  "locale": "de",
  "createdAt": "…", "updatedAt": "…", "publishedAt": "…",
  "url": "agb",
  "metaTitle": "Terms and conditions",
  "metaDescription": "General terms and conditions",
  "robots": ["noindex", "nofollow"],
  "hreflang": [
    { "language": "de", "url": "agb", "subshopId": "deutsch", "default": true }
  ]
}
```

<Warning>
  An input mask itself can have content fields with names such as `url` or `robots`. These then appear as regular entries in `fields` and are something **different** from `meta.url` or `meta.robots` (same name, but different meaning and in some cases a different form). SEO values from the Meta-Info plugin always sit in `meta` and never in `fields`. Do not confuse the two when processing them.
</Warning>

### The content fields in fields

`fields` is a list of all input fields of the document. Each entry has exactly three keys:

| **Key** | **Type**          | **Meaning**                                                                                          |
| ------- | ----------------- | ---------------------------------------------------------------------------------------------------- |
| `name`  | string            | The technical field name, as it is currently called in the input mask (may change through renaming). |
| `type`  | string            | The Strapi field type (see [Field types](#the-field-types)).                                         |
| `value` | depends on `type` | The value. Its form depends on the type.                                                             |

Two orderings need to be distinguished:

* **Order of the `fields` list**: it follows the schema definition and is irrelevant for rendering. Therefore, always access fields via `name` and never via position.
* The **order within an array value** (content blocks, repeatable components, relation lists) is layout-critical. It exactly matches the editor's arrangement and must be preserved when rendering.

### The field types

**Scalar types**: `value` is the value directly.

| **type**                                       | **value**                                                                                     |
| ---------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `string` / `text`                              | string or null                                                                                |
| `richtext`                                     | string or null (Markdown or already converted HTML, depending on the sync middleware setting) |
| `enumeration`                                  | string or null                                                                                |
| `boolean`                                      | boolean or null                                                                               |
| `integer` / `biginteger` / `decimal` / `float` | number or null                                                                                |
| `date` / `datetime` / `time` / `timestamp`     | string or null                                                                                |
| `uid`                                          | string or null                                                                                |
| `json`                                         | arbitrary JSON                                                                                |
| `blocks`                                       | array of Strapi Blocks nodes or HTML string, depending on the sync middleware setting         |

<Info>
  Whether `richtext` and `blocks` values arrive as raw data (Markdown or Blocks JSON) or as finished HTML is a deployment setting of the sync middleware. Clarify with WEBSALE which variant is active for your shop before you adjust the template.
</Info>

#### Media in the schema format

**`media`**: `value` is a normalized media object with fixed keys (`url`, `alt`, `caption`, `name`, `mime`, `ext`, `width`, `height`, `size`, `formats`). Strapi internals such as IDs, hashes, provider data, and timestamps are deliberately removed so that Strapi-internal changes do not reach the shop. If a value is missing, it is `null`. For multi-media, `value` is an array of such objects.

```json theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/websale.json"]}}
{
  "name": "logo",
  "type": "media",
  "value": {
    "url": "/uploads/websale_logo_576a45762c.webp",
    "alt": "Websale company logo",
    "caption": null,
    "name": "websale_logo.webp",
    "mime": "image/webp",
    "ext": ".webp",
    "width": 500,
    "height": 75,
    "size": 4.26,
    "formats": {
      "thumbnail": { "url": "/uploads/thumbnail_…", "width": 245, "height": 37, "mime": "image/webp", "size": 3.07 }
    }
  }
}
```

#### Components and relations

**`component`**: `value` contains its own `fields` list. For repeatable components, `value` is an array of such objects.

```json theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/websale.json"]}}
{
  "name": "button",
  "type": "component",
  "value": {
    "fields": [
      { "name": "label", "type": "string", "value": "More" },
      { "name": "link",  "type": "string", "value": "/info" }
    ]
  }
}
```

**`dynamiczone`** (content blocks): `value` is an array of blocks. Each block carries its component name in `component`, its `id`, and its own `fields` list.

```json theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/websale.json"]}}
{
  "name": "content",
  "type": "dynamiczone",
  "value": [
    {
      "component": "elemente.ws-markup",
      "id": 96,
      "fields": [
        { "name": "Markup",    "type": "richtext", "value": "Terms content" },
        { "name": "FullWidth", "type": "boolean",  "value": false }
      ]
    }
  ]
}
```

**`relation`**: `value` contains the linked documents, recursively with their own `fields` list. A single relation returns an object, a multi-relation an array. The resolution depth is limited. Deeper relations that are no longer loaded appear as `value: null`. Self-referencing relations (for example nested navigations) are additionally cut off by a cycle protection. The same type is only resolved once per path.

<Note>
  Keep the load depth in mind in the template as well: `$wsExternalData.load` truncates deeply nested structures via the `maxDepth` option. In the examples on this page, `maxDepth: 20` is used. See [\$wsExternalData](/en/frontend/referenz/module/wsexternaldata).
</Note>

### Stability guarantee and its limit

**The structure is stable:** No matter what happens to the input masks, every file is `{ contentType, meta, fields[] }` and every field is `{ name, type, value }`. Templates can rely on that 100%.

**The meaning is not stable:** If an editor renames a field `title` to `titel`, that comes through cleanly. But in `name`, it now reads `titel`. A template that specifically searches for `title` no longer finds anything and outputs nothing at that place. The mapping of field names to their meaning is an agreement between editors and template owners. The format cannot enforce it. The only exception are the SEO fields: through the Meta-Info plugin, they always sit at the same place in `meta` in the schema format, regardless of how the input masks are structured.

## Changes compared to the old structure

The following overview compares the new structure with the old data structure of the previous content sync. It is the basis for the template adjustment in the next section. The "New structure" column shows the schema format. The changes to the input-mask identifier, the component identifier, the media, `localizations`, and the file names apply to **both** formats. For the SEO fields, only the storage in the schema format (`meta`) is confirmed; for the mapping format, it is still open.

| **Aspect**            | **Old structure**                                                                                                                                                  | **New structure (schema)**                                                                                                                                                                                                                                                |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Envelope              | `{ "id", "attributes": { …all fields… }, "content_type" }` <br />The fields sit inside an `attributes` wrapper.                                                    | `{ contentType, meta, fields[] }` <br />There is no more `attributes` wrapper.                                                                                                                                                                                            |
| Input-mask identifier | short slug, for example `"contentpage"`                                                                                                                            | full Strapi UID, for example `"api::contentpage.contentpage"`                                                                                                                                                                                                             |
| Component identifier  | `"__component"`                                                                                                                                                    | `"component"` (on the individual content blocks)                                                                                                                                                                                                                          |
| Media                 | REST wrapper `{ "data": { "id", "attributes": { … } } }` including Strapi internals                                                                                | flat, normalized media object with a fixed set of keys (see above)                                                                                                                                                                                                        |
| SEO fields            | dedicated content component (for example "Meta Information" with `MetaTitle`, `MetaDescription`, `SeoURL`, `MetaRobots`), maintained editorially in the input mask | no longer a content field. The values sit in `meta` via the Meta-Info plugin with new names: `SeoURL` → `url`, `MetaTitle` → `metaTitle`, `MetaDescription` → `metaDescription`, `MetaRobots` → `robots` (now an **array** instead of a string). Newly added: `hreflang`. |
| `localizations`       | always present, even if empty                                                                                                                                      | completely removed                                                                                                                                                                                                                                                        |
| File name             | derived from content and descriptive, for example `tpl_about.json`                                                                                                 | by default based on `documentId`, for example `l9h00uvgblpjvlqqv1va9s44.json`                                                                                                                                                                                             |

<Warning>
  The switch of file names to `documentId` affects every place in the template that loads a JSON file via a descriptive name. The `documentId` is the central, stable identity of a document. The numeric `id` is not suitable for that because it changes per language version.
</Warning>

## Migrating templates: the eight steps

Three notes up front:

1. **The code examples are patterns to transfer, not code to be copied unchanged.** The "after" code **replaces** the corresponding existing location in your template – but inside the test-mode switch from step 2, so that the previous code initially remains in place for the live output.
2. **The "before" examples show a typical pattern of the old structure.** Because the old structure was set up differently per shop, the actual locations in your shop may differ.
3. **Work behind the test-mode switch from the start** (step 2). Otherwise you change the live output before you have verified it.

The examples use the schema format throughout because it involves the larger rework. Where the mapping format differs, this is noted in the respective step.

<Note>
  **Prerequisite:** The new files must already be generated for your shop, otherwise the test mode has nothing to work with. Check in the JSON directory whether there are files with `documentId` names (`<documentId>.json` and `<documentId>.schema.json`). The old files with descriptive names remain alongside them during the transition phase. Activation and regeneration are initiated by WEBSALE.
</Note>

### Step 1: Find the affected locations

**When needed:** Always.

Search the template repository (GitLab) for all places where Strapi JSON is loaded or read:

* `$wsExternalData.load(` – loads a single file. Relevant are the calls with the option `source: "system"` and a path that points to the JSON directory.
* `$wsExternalData.read(` – reads a directory and returns a list of file names.
* the subsequent field accesses on the loaded data, typically recognizable by `attributes` and `__component`.

The result is a list of the templates that will be adjusted in the following steps. For each hit, note which Strapi document is loaded there – you will need the "file ↔ document" mapping in step 3.

### Step 2: Add a test-mode switch

**When needed:** Always, and specifically **before** the first content change.

No second Strapi instance is needed for the check. Because the old files remain in place during the transition phase, old and new files sit side by side in the JSON directory. You can therefore switch between old and new in the same template: in [test mode](/en/frontend/referenz/module/wstestmode), the template loads and renders the new files, while the live output continues to work unchanged with the old ones. This keeps the migration safe at all times. The live page remains untouched until step 8.<br /><br />The switch covers not just the load call but the entire output block\*\*.\*\* The field accesses of the new structure (steps 4 to 7) do not fit the old files. If you only switched the file name, the live output would immediately be empty.

**Before (old structure, without a switch):**

```html theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/websale.json"]}}
{{ var $cCMSLoadOptions = {"source": "system", "type": "json", "maxDepth": 20} }}
{{ var $cCMSFile = ["json/Deutsch/tpl_about.json"] | join }}
{{ var $cCMSData = $wsExternalData.load($cCMSFile, $cCMSLoadOptions) }}
{{# … previous output code … #}}
```

**After (with a switch – scaffolding that you fill in steps 3 to 7):**

```html theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/websale.json"]}}
{{ var $cCMSLoadOptions = {"source": "system", "type": "json", "maxDepth": 20} }}
{{ if $wsTestMode.active }}
    {{# Test mode: new structure #}}
    {{ var $cCMSFile = ["json/Deutsch/l9h00uvgblpjvlqqv1va9s44.schema.json"] | join }}
    {{ var $cCMSData = $wsExternalData.load($cCMSFile, $cCMSLoadOptions) }}
    {{# … new output code from steps 4 to 7 … #}}
{{ else }}
    {{# Live: previous structure, unchanged #}}
    {{ var $cCMSFile = ["json/Deutsch/tpl_about.json"] | join }}
    {{ var $cCMSData = $wsExternalData.load($cCMSFile, $cCMSLoadOptions) }}
    {{# … previous output code … #}}
{{ /if }}
```

All following steps work exclusively in the upper branch. How to invoke test mode in the shop is described under [Switching shop test modes on/off](/en/testmodi-des-shops-ein-ausschalten).

<Note>
  In larger templates, a switch per output location becomes hard to read. In that case, it is more practical to leave the old output template unchanged and maintain the new version as a dedicated file that is only included in test mode. The procedure is described under [Adapting templates for Strapi content](/en/strapi-cms/templates-fur-strapi-inhalte-anpassen).
</Note>

### Step 3: Adjust the file names in the load calls

**When needed:** Always.

**The path stays, only the file name changes.** The JSON files remain in the same directory as before: in the JSON directory of your shop, which the templates reach via the option `source: "system"` – for example `json/Deutsch/` for the German-language content. Both new formats sit there **side by side**, in the same directory as the old file did before.

Instead of a descriptive name, each file carries the `documentId` of the document by default:

|                | **Before**                    | **After**                                           |
| -------------- | ----------------------------- | --------------------------------------------------- |
| mapping format | `json/Deutsch/tpl_about.json` | `json/Deutsch/l9h00uvgblpjvlqqv1va9s44.json`        |
| schema format  | (did not exist)               | `json/Deutsch/l9h00uvgblpjvlqqv1va9s44.schema.json` |

**How to find the `documentId`:** Open the document in Strapi in the Content Manager. The `documentId` then appears in the browser's address bar. In addition, it is present in every generated file of the schema format under `meta.documentId`.

*Screenshot: Content Manager with an opened document, `documentId` highlighted in the address bar.*

**Where to adjust:**

* In every `$wsExternalData.load(...)` call that loads a Strapi file by its name – in the scaffolding from step 2, that is the value of `$cCMSFile` in the test-mode branch.
* **Not** in `$wsExternalData.read(...)` calls. These read a directory and remain unchanged. If your template further evaluates the read file names by content – for example, to derive the appropriate page from the name –, that logic must also be adjusted, because the names are no longer descriptive.

<Warning>
  The most commonly overlooked point: mapping files and self-built naming conventions. If your template assembles the file name only at runtime (for example from the called URL via its own mapping file), this mapping must be switched to `documentId` file names.
</Warning>

### Step 4: Update field accesses

**When needed:** Always. The scope differs depending on the format.

This refers to the place that reads the value of an input field from the loaded data – for example the page title.

**In the mapping format**, only the `attributes` wrapper is removed. `$cCMSData.attributes.title` becomes `$cCMSData.title`. Nothing more needs to be done on the field accesses.

**In the schema format**, the values are no longer directly under their field name but as entries in the `fields` list. Convert this list once into a name/value object. Afterwards you access it via the field name as usual:

The load call from step 2 remains unchanged. Only the output part below it is replaced:

**Before (old structure):**

```html theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/websale.json"]}}
<h1>{{= $cCMSData.attributes.title }}</h1>
```

**After (schema format):**

```html theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/websale.json"]}}
{{ if $cCMSData }}
    {{# convert the fields list once into a name/value object #}}
    {{ var $cFields = {} }}
    {{ foreach $cField in $cCMSData.fields }}
        {{ $cFields[$cField.name] = $cField.value }}
    {{ /foreach }}

    <h1>{{= $cFields.title }}</h1>
{{ /if }}
```

The check `{{ if $cCMSData }}` is not decorative: `$wsExternalData.load` returns `null` if the file is missing or invalid. Without this check, the page stays empty without any indication.

This pattern works identically at every level: for the document fields, inside components (`value.fields`), and for the blocks of the content blocks (see next step).

### Step 5: Adjust content blocks (dynamic zone)

**When needed:** If your template renders the content blocks of a page. That applies to practically every content page with freely combinable components.

Two things change:

* The component identifier is now called `component` instead of `__component` – that applies to **both** formats.
* In the schema format, the field values of a block sit in its own `fields` list.

Replace the existing render code according to this pattern:

**Before (old structure):**

```html theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/websale.json"]}}
{{ foreach $cItem in $cCMSData.attributes.content }}
    {{ if $cItem.__component == "elemente.ws-markup" }}
        <div>{{! $cItem.Markup }}</div>
    {{ /if }}
{{ /foreach }}
```

**After (mapping format):**

```html theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/websale.json"]}}
{{ foreach $cItem in $cCMSData.content }}
    {{ if $cItem.component == "elemente.ws-markup" }}
        <div>{{! $cItem.Markup }}</div>
    {{ /if }}
{{ /foreach }}
```

**After (schema format, `$cFields` from step 4):**

```html theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/websale.json"]}}
{{ foreach $cItem in $cFields.content }}
    {{ var $cItemFields = {} }}
    {{ foreach $cField in $cItem.fields }}
        {{ $cItemFields[$cField.name] = $cField.value }}
    {{ /foreach }}

    {{ if $cItem.component == "elemente.ws-markup" }}
        <div>{{! $cItemFields.Markup }}</div>
    {{ else }}
        {{# component not implemented: make visible in test mode #}}
        {{ if $wsTestMode.active }}
            <!-- Component "{{= $cItem.component }}" is not implemented in the template -->
        {{ /if }}
    {{ /if }}
{{ /foreach }}
```

Replace the component names and fields in the example with those of your shop. The order of the blocks matches the editor's arrangement and is output unchanged.

<Note>
  Keep a `{{ else }}` branch for unknown components, or add one. Otherwise a newly created block disappears silently from the page instead of standing out.
</Note>

### Step 6: Adjust media accesses

**When needed:** Everywhere images or files from Strapi are output. Applies to both formats.

The `data`/`attributes` wrapper around media is removed. Image URL, alt text, and responsive formats sit directly in the normalized media object. Additionally, the alt text is now called `alt` instead of `alternativeText`. Replace the existing image outputs according to this pattern:

**Before (old structure):**

```html theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/websale.json"]}}
<img src="{{= $cCMSData.attributes.logo.data.attributes.url }}"
     alt="{{= $cCMSData.attributes.logo.data.attributes.alternativeText }}">
```

**After (schema format, `$cFields` from step 4):**

```html theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/websale.json"]}}
<img src="{{= $cFields.logo.url }}" alt="{{= $cFields.logo.alt }}">
```

**After (mapping format):**

```html theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/websale.json"]}}
<img src="{{= $cCMSData.logo.url }}" alt="{{= $cCMSData.logo.alt }}">
```

Newly available directly are also `width`, `height`, and the responsive variants under `formats`, for example `$cFields.logo.formats.thumbnail.url`. Only output these values if they are populated: missing entries are `null`, and not the same formats are generated for every shop.

### Step 7: Read SEO fields from `meta`

**When needed:** If your template outputs meta title, meta description, robots, or the SEO URL from the Strapi data. The examples show the schema format (for the mapping format, see the note under [What to watch out for with the mapping format](#what-to-watch-out-for-with-the-mapping-format)).

These values previously came from a content component (for example "Meta Information") and in the future sit in the `meta` section of the file, with new field names (see [comparison](#changes-compared-to-the-old-structure)). Replace the existing accesses according to this pattern:

**Before (old structure):**

```html theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/websale.json"]}}
<title>{{= $cCMSData.attributes.MetaInformation.MetaTitle }}</title>
<meta name="description" content="{{= $cCMSData.attributes.MetaInformation.MetaDescription }}">
<meta name="robots" content="{{= $cCMSData.attributes.MetaInformation.MetaRobots }}">
```

**After (schema format):**

```html theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/websale.json"]}}
<title>{{= $cCMSData.meta.metaTitle }}</title>
<meta name="description" content="{{= $cCMSData.meta.metaDescription }}">
<meta name="robots" content="{{= join($cCMSData.meta.robots, ",") }}">
```

Two points stand out here in particular:

* **`robots` is now an array** and must be joined into a string for output in the meta tag. `join` is called as a function with a separator for that; the filter notation `| join` in the path examples on this page joins without a separator.
* **The SEO fields sit in `meta`, not in `fields`.** The name/value object `$cFields` from step 4 does not contain them.

Newly available is also `meta.hreflang` with one entry per subshop that serves this language. If you want to output hreflang tags, that is the data source for it.

### Step 8: Verify in test mode and go live

**When needed:** Always, as the last step.

Open the shop in test mode (see [Switching shop test modes on/off](/en/testmodi-des-shops-ein-ausschalten)) and check every migrated page.

*Screenshot: shop in test mode showing a migrated content page.*

Checklist:

* Are all content blocks output – and in the order the editor set in Strapi?
* Do all images appear including the responsive formats, with alt texts?
* Is the SEO output in the HTML head correct: meta title, meta description, robots?
* Are the pages reachable via their SEO URLs?
* Are there empty places where content used to be? That is the typical hint at a field name that no longer matches.

It is best to compare test-mode and live output of the same page directly side by side.

Only when all pages have been checked, remove the switch from step 2: the test-mode branch becomes the regular code, and the `{{ else }}` branch with the old load call and old output is removed.

## When something does not work

| **Symptom**                                                  | **Likely cause**                                                                                                       | **Check**                                                                                                                                                                              |
| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The page stays completely empty, no content from Strapi      | The file was not found or not loaded. `$wsExternalData.load` returns `null` in that case.                              | Check file name and `documentId` (step 3). For the error cause, `$wsExternalData.getLastError()` provides a code, see [\$wsExternalData](/en/frontend/referenz/module/wsexternaldata). |
| Individual values are missing, the rest of the page is there | The field name in the template no longer matches the field name in Strapi.                                             | In the schema format, compare the `name` values in `fields` with the field names in the template.                                                                                      |
| A content block is not output                                | The component identifier is still checked via `__component`, or the component is not implemented in the template.      | Step 5, especially the `{{ else }}` branch for unknown components.                                                                                                                     |
| Images are missing or have no alt text                       | The old `data`/`attributes` wrapper or `alternativeText` is still being used.                                          | Step 6.                                                                                                                                                                                |
| Robots tag contains something like `noindexnofollow`         | The `meta.robots` array was joined without a separator.                                                                | Step 7.                                                                                                                                                                                |
| Deeply nested content is truncated                           | `maxDepth` in the load call is too small, or a relation was no longer resolved by the sync middleware (`value: null`). | Load options and section [Components and relations](#components-and-relations).                                                                                                        |

For the switch date of your shop as well as the activation of the formats and the regeneration of the files for existing content (backfill), contact your WEBSALE contact.


## Related topics

- [Migration](/en/migration.md)
- [Template migration](/en/migration/migration-websale-v8s-neue-version/template-migration.md)
- [Data migration](/en/migration/daten-migration.md)
- [Migration WEBSALE V8s -> new version](/en/migration/migration-websale-v8s-neue-version.md)
- [Configuration migration](/en/migration/migration-websale-v8s-neue-version/konfiguration-migration.md)
