Skip to content
2 changes: 2 additions & 0 deletions guides/plugins/apps/app-scripts/custom-endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ Additionally, it is also possible to redirect to an existing route:

For a complete overview of the available data and services, refer to the [reference documentation](../../../../resources/references/app-reference/script-reference/script-hooks-reference.md#storefront-hook).

To make such a page reachable under a readable path like `/imprint` or `/blog/my-post`, declare an SEO URL for it in your manifest. See the [SEO URLs for App Routes](../storefront/seo-urls.md) guide.

## Manipulate HTTP-headers to API responses

::: info
Expand Down
145 changes: 145 additions & 0 deletions guides/plugins/apps/storefront/seo-urls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
---
nav:
title: SEO URLs for App Routes
position: 30

---

# SEO URLs for App Routes

## Overview

Storefront pages rendered by [app scripts](../app-scripts/custom-endpoints.md#storefront-endpoints) live under `/storefront/script/{hook}`. That path is technical and cannot carry an entity id. With `<seo-url>` and `<entity-seo-url>` elements in the `<storefront>` section of your `manifest.xml`, Shopware gives those pages SEO URLs, the same way it does for products and categories.

Check warning on line 12 in guides/plugins/apps/storefront/seo-urls.md

View workflow job for this annotation

GitHub Actions / LanguageTool

[LanguageTool] guides/plugins/apps/storefront/seo-urls.md#L12

This abbreviation for “identification” is spelled all-uppercase. (ID_CASING[2]) Suggestions: `ID` Rule: https://community.languagetool.org/rule/show/ID_CASING?lang=en-US&subId=2 Category: CASING
Raw output
guides/plugins/apps/storefront/seo-urls.md:12:190: This abbreviation for “identification” is spelled all-uppercase. (ID_CASING[2])
 Suggestions: `ID`
 Rule: https://community.languagetool.org/rule/show/ID_CASING?lang=en-US&subId=2
 Category: CASING

::: info
This feature was introduced in Shopware 6.7.16.0 and is not available in earlier versions.
:::

There are two kinds of SEO URLs:

- A **static** SEO URL (`<seo-url>`) maps a fixed path such as `/imprint` to one of your storefront scripts.
- An **entity-bound** SEO URL (`<entity-seo-url>`) generates one URL per entity from a Twig template, for example `/blog/{{ ceBlog.translated.title }}`, and passes the entity id to your script.

Check warning on line 21 in guides/plugins/apps/storefront/seo-urls.md

View workflow job for this annotation

GitHub Actions / LanguageTool

[LanguageTool] guides/plugins/apps/storefront/seo-urls.md#L21

This abbreviation for “identification” is spelled all-uppercase. (ID_CASING[2]) Suggestions: `ID` Rule: https://community.languagetool.org/rule/show/ID_CASING?lang=en-US&subId=2 Category: CASING
Raw output
guides/plugins/apps/storefront/seo-urls.md:21:175: This abbreviation for “identification” is spelled all-uppercase. (ID_CASING[2])
 Suggestions: `ID`
 Rule: https://community.languagetool.org/rule/show/ID_CASING?lang=en-US&subId=2
 Category: CASING

## Prerequisites

You need an app with at least one storefront script. Read the [App Base Guide](../app-base-guide.md) and the [Custom Endpoints](../app-scripts/custom-endpoints.md) guide first. Both kinds of SEO URLs point at scripts stored in `Resources/scripts/storefront-<hook>/`.

## Static SEO URLs

Declare a `<seo-url>` with one or more `<path>` elements. The `name` identifies the route inside your app and is also the default script hook.

```xml

Check warning on line 31 in guides/plugins/apps/storefront/seo-urls.md

View workflow job for this annotation

GitHub Actions / LanguageTool

[LanguageTool] guides/plugins/apps/storefront/seo-urls.md#L31

File types are normally capitalized. (FILE_EXTENSIONS_CASE[1]) Suggestions: `XML` URL: https://languagetool.org/insights/post/spelling-capital-letters/ Rule: https://community.languagetool.org/rule/show/FILE_EXTENSIONS_CASE?lang=en-US&subId=1 Category: CASING
Raw output
guides/plugins/apps/storefront/seo-urls.md:31:3: File types are normally capitalized. (FILE_EXTENSIONS_CASE[1])
 Suggestions: `XML`
 URL: https://languagetool.org/insights/post/spelling-capital-letters/ 
 Rule: https://community.languagetool.org/rule/show/FILE_EXTENSIONS_CASE?lang=en-US&subId=1
 Category: CASING
<?xml version="1.0" encoding="UTF-8"?>

Check warning on line 32 in guides/plugins/apps/storefront/seo-urls.md

View workflow job for this annotation

GitHub Actions / LanguageTool

[LanguageTool] guides/plugins/apps/storefront/seo-urls.md#L32

File types are normally capitalized. (FILE_EXTENSIONS_CASE[1]) Suggestions: `XML` URL: https://languagetool.org/insights/post/spelling-capital-letters/ Rule: https://community.languagetool.org/rule/show/FILE_EXTENSIONS_CASE?lang=en-US&subId=1 Category: CASING
Raw output
guides/plugins/apps/storefront/seo-urls.md:32:2: File types are normally capitalized. (FILE_EXTENSIONS_CASE[1])
 Suggestions: `XML`
 URL: https://languagetool.org/insights/post/spelling-capital-letters/ 
 Rule: https://community.languagetool.org/rule/show/FILE_EXTENSIONS_CASE?lang=en-US&subId=1
 Category: CASING
<manifest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware/shopware/trunk/src/Core/Framework/App/Manifest/Schema/manifest-3.0.xsd">
<meta>
<name>SwagCompanyPages</name>
...
</meta>
<storefront>
<seo-url name="imprint">
<path>imprint</path>
<path lang="de-DE">impressum</path>
</seo-url>
</storefront>
</manifest>
```

With this manifest, `/imprint` and `/impressum` run the scripts in `Resources/scripts/storefront-imprint/`. Shopware writes one SEO URL per storefront sales channel domain. It picks the `<path>` of the domain language and falls back to the shop's default language. Query parameters of the request stay available as `hook.query`.

Set the `hook` attribute when the script folder should differ from the route name:

```xml

Check warning on line 51 in guides/plugins/apps/storefront/seo-urls.md

View workflow job for this annotation

GitHub Actions / LanguageTool

[LanguageTool] guides/plugins/apps/storefront/seo-urls.md#L51

File types are normally capitalized. (FILE_EXTENSIONS_CASE[1]) Suggestions: `XML` URL: https://languagetool.org/insights/post/spelling-capital-letters/ Rule: https://community.languagetool.org/rule/show/FILE_EXTENSIONS_CASE?lang=en-US&subId=1 Category: CASING
Raw output
guides/plugins/apps/storefront/seo-urls.md:51:3: File types are normally capitalized. (FILE_EXTENSIONS_CASE[1])
 Suggestions: `XML`
 URL: https://languagetool.org/insights/post/spelling-capital-letters/ 
 Rule: https://community.languagetool.org/rule/show/FILE_EXTENSIONS_CASE?lang=en-US&subId=1
 Category: CASING
<seo-url name="imprint" hook="company-imprint">
<path>imprint</path>
</seo-url>
```

## Entity-bound SEO URLs

Declare an `<entity-seo-url>` with an `entity` attribute and a `<default-template>`. The entity can be one of your [custom entities](../custom-data/custom-entities.md) or a core entity such as `product`.

```xml

Check warning on line 61 in guides/plugins/apps/storefront/seo-urls.md

View workflow job for this annotation

GitHub Actions / LanguageTool

[LanguageTool] guides/plugins/apps/storefront/seo-urls.md#L61

File types are normally capitalized. (FILE_EXTENSIONS_CASE[1]) Suggestions: `XML` URL: https://languagetool.org/insights/post/spelling-capital-letters/ Rule: https://community.languagetool.org/rule/show/FILE_EXTENSIONS_CASE?lang=en-US&subId=1 Category: CASING
Raw output
guides/plugins/apps/storefront/seo-urls.md:61:3: File types are normally capitalized. (FILE_EXTENSIONS_CASE[1])
 Suggestions: `XML`
 URL: https://languagetool.org/insights/post/spelling-capital-letters/ 
 Rule: https://community.languagetool.org/rule/show/FILE_EXTENSIONS_CASE?lang=en-US&subId=1
 Category: CASING
<storefront>
<entity-seo-url name="blog-detail" entity="ce_blog">
<default-template>blog/{{ ceBlog.translated.title }}</default-template>
</entity-seo-url>
</storefront>
```

Shopware generates one SEO URL per entity and language from the template and keeps it up to date whenever the entity is written. Like the SEO URLs of products and categories, this follows the indexing behaviour of the write (for example the `indexing-behavior` header of the Sync API, or `indexing-skip: app_seo_url.seo-url` to skip it), and `bin/console dal:refresh:index` rebuilds them. The template context exposes the entity under its camel-cased name, so `ce_blog` becomes `ceBlog` and `product` becomes `product`. Every field of the entity is available, translated fields through `translated`.

Check warning on line 69 in guides/plugins/apps/storefront/seo-urls.md

View workflow job for this annotation

GitHub Actions / LanguageTool

[LanguageTool] guides/plugins/apps/storefront/seo-urls.md#L69

The verb ‘write’ does not usually follow articles like ‘the’. Check that ‘write’ is spelled correctly; using ‘write’ as a noun may be non-standard. (A_INFINITIVE[2]) Suggestions: `to write`, `the White`, `the trite`, `the white`, `the writ`, `they write` URL: https://languagetool.org/insights/post/verbs-as-nouns/ Rule: https://community.languagetool.org/rule/show/A_INFINITIVE?lang=en-US&subId=2 Category: GRAMMAR
Raw output
guides/plugins/apps/storefront/seo-urls.md:69:214: The verb ‘write’ does not usually follow articles like ‘the’. Check that ‘write’ is spelled correctly; using ‘write’ as a noun may be non-standard. (A_INFINITIVE[2])
 Suggestions: `to write`, `the White`, `the trite`, `the white`, `the writ`, `they write`
 URL: https://languagetool.org/insights/post/verbs-as-nouns/ 
 Rule: https://community.languagetool.org/rule/show/A_INFINITIVE?lang=en-US&subId=2
 Category: GRAMMAR

The generated URL resolves to `/storefront/script/blog-detail?id=<entity-id>`, so your script receives the id as `hook.query.id`:

Check warning on line 71 in guides/plugins/apps/storefront/seo-urls.md

View workflow job for this annotation

GitHub Actions / LanguageTool

[LanguageTool] guides/plugins/apps/storefront/seo-urls.md#L71

This abbreviation for “identification” is spelled all-uppercase. (ID_CASING[2]) Suggestions: `ID` Rule: https://community.languagetool.org/rule/show/ID_CASING?lang=en-US&subId=2 Category: CASING
Raw output
guides/plugins/apps/storefront/seo-urls.md:71:107: This abbreviation for “identification” is spelled all-uppercase. (ID_CASING[2])
 Suggestions: `ID`
 Rule: https://community.languagetool.org/rule/show/ID_CASING?lang=en-US&subId=2
 Category: CASING

```twig
// Resources/scripts/storefront-blog-detail/script.twig
{% set post = services.store.search('ce_blog', { 'ids': [hook.query.id] }).first %}

{% do hook.page.addExtension('post', post) %}

{% do hook.setResponse(
services.response.render('@SwagBlog/storefront/page/blog/detail.html.twig', { 'page': hook.page })

Check warning on line 80 in guides/plugins/apps/storefront/seo-urls.md

View workflow job for this annotation

GitHub Actions / LanguageTool

[LanguageTool] guides/plugins/apps/storefront/seo-urls.md#L80

If a new sentence starts here, add a space and start with an uppercase letter. (LC_AFTER_PERIOD[1]) Suggestions: ` Page`, ` page` Rule: https://community.languagetool.org/rule/show/LC_AFTER_PERIOD?lang=en-US&subId=1 Category: CASING
Raw output
guides/plugins/apps/storefront/seo-urls.md:80:95: If a new sentence starts here, add a space and start with an uppercase letter. (LC_AFTER_PERIOD[1])
 Suggestions: ` Page`, ` page`
 Rule: https://community.languagetool.org/rule/show/LC_AFTER_PERIOD?lang=en-US&subId=1
 Category: CASING
) %}
```

### Permissions

Your app needs `<read>` permission for the entity and for every association your `<default-template>` uses. A template that reads `product.manufacturer.name` needs both entities:

```xml

Check warning on line 88 in guides/plugins/apps/storefront/seo-urls.md

View workflow job for this annotation

GitHub Actions / LanguageTool

[LanguageTool] guides/plugins/apps/storefront/seo-urls.md#L88

File types are normally capitalized. (FILE_EXTENSIONS_CASE[1]) Suggestions: `XML` URL: https://languagetool.org/insights/post/spelling-capital-letters/ Rule: https://community.languagetool.org/rule/show/FILE_EXTENSIONS_CASE?lang=en-US&subId=1 Category: CASING
Raw output
guides/plugins/apps/storefront/seo-urls.md:88:3: File types are normally capitalized. (FILE_EXTENSIONS_CASE[1])
 Suggestions: `XML`
 URL: https://languagetool.org/insights/post/spelling-capital-letters/ 
 Rule: https://community.languagetool.org/rule/show/FILE_EXTENSIONS_CASE?lang=en-US&subId=1
 Category: CASING
<storefront>
<entity-seo-url name="product-detail" entity="product">
<default-template>{{ product.manufacturer.name }}/{{ product.translated.name }}</default-template>
</entity-seo-url>
</storefront>
<permissions>
<read>product</read>
<read>product_manufacturer</read>
</permissions>
```

Your own [custom entities](../custom-data/custom-entities.md#permissions) need no extra permission: your app has full access to them anyway. The generated paths are public, so only use fields in the template that may appear in a URL.

### Merchant configuration

Entity-bound routes appear in the Administration under *Settings > SEO* as `storefront.app.<app name>.<name>`, for example `storefront.app.SwagBlog.blog-detail`. Merchants can adjust the template per sales channel and override single URLs like they do for products. Changing a template regenerates the URLs automatically. Your `<default-template>` is only the initial value: app updates don't overwrite a template that already exists. Changing the `entity` of a route resets every template of that route, including sales channel overrides. Declare a new `name` if a route needs a fresh template for the same entity.

## Linking to your pages

Use the `seoUrl` Twig function with the technical route and parameters in your storefront templates. The placeholder is replaced with the SEO URL when the page is rendered:

```twig
<a href="{{ seoUrl('frontend.script_endpoint', { 'hook': 'blog-detail', 'id': post.id }) }}">

Check warning on line 111 in guides/plugins/apps/storefront/seo-urls.md

View workflow job for this annotation

GitHub Actions / LanguageTool

[LanguageTool] guides/plugins/apps/storefront/seo-urls.md#L111

If a new sentence starts here, add a space and start with an uppercase letter. (LC_AFTER_PERIOD[1]) Suggestions: ` Id`, ` id` Rule: https://community.languagetool.org/rule/show/LC_AFTER_PERIOD?lang=en-US&subId=1 Category: CASING
Raw output
guides/plugins/apps/storefront/seo-urls.md:111:83: If a new sentence starts here, add a space and start with an uppercase letter. (LC_AFTER_PERIOD[1])
 Suggestions: ` Id`, ` id`
 Rule: https://community.languagetool.org/rule/show/LC_AFTER_PERIOD?lang=en-US&subId=1
 Category: CASING
{{ post.translated.title }}
</a>

<a href="{{ seoUrl('frontend.script_endpoint', { 'hook': 'imprint' }) }}">
{{ 'swag-company-pages.imprint'|trans }}
</a>
```

## Lifecycle

- **Install and update**: Shopware stores the declared routes and creates the default SEO URL template for entity-bound routes that don't have one yet. Routes that an update no longer declares lose their SEO URLs and templates.
- **Activation and update**: The static SEO URLs are written right away. The entity-bound URLs are generated through the message queue, so make sure a [worker](../../../hosting/infrastructure/message-queue.md) processes messages.
- **Hook changes**: Changing the `hook` of a route keeps its SEO URLs, including the merchant's edits, and points them at the new script.
- **Deactivation and uninstall**: The app's SEO URLs are marked as deleted and stop resolving. Uninstalling also removes the SEO URL templates, unless the merchant keeps the app data.
- **New sales channel domains**: Like product URLs, the static SEO URLs for a new domain show up on the next activation or update of the app, or after `bin/console dal:refresh:index`.

## Validation rules

The manifest schema checks the structure of both elements:

- `name` and `hook` must match `[a-z0-9]+(-[a-z0-9]+)*`, and `name` must be unique across all `<seo-url>` and `<entity-seo-url>` elements of the app.
- A `<seo-url>` needs at least one non-empty `<path>`.
- An `<entity-seo-url>` needs an `entity` attribute and a non-empty `<default-template>`.

On app `install` and `update`, Shopware also checks:

- Static paths must not contain characters that aren't allowed in URLs and must not yet be used by a storefront route such as `/account`, by another app or by an existing SEO URL.
- A script hook belongs to the first app that declares it in an SEO URL. Within your app, a hook can be used by at most one `<seo-url>` and at most one `<entity-seo-url>`.
- The `entity` of an `<entity-seo-url>` must exist and must not be a mapping or translation entity, and your app needs the [permissions](#permissions) described above.

## Limitations

- SEO URLs are generated for storefront sales channels only. Headless sales channels are not supported yet.
- No `hreflang` links are generated for app routes.
2 changes: 1 addition & 1 deletion resources/references/app-reference/manifest-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Validates that the Shopware instance is publicly reachable, which is necessary f

## Storefront

Can be omitted if your app template needs higher load priority than other plugins/apps. For more details, follow the [storefront guide](../../../guides/plugins/apps/storefront/index.md).
Configure the template load priority of your app and declare SEO URLs for pages rendered by your storefront scripts. For more details, follow the [storefront guide](../../../guides/plugins/apps/storefront/index.md) and the [SEO URLs for App Routes](../../../guides/plugins/apps/storefront/seo-urls.md) guide.

<<< @/docs/snippets/config/app/storefront.xml

Expand Down
7 changes: 7 additions & 0 deletions snippets/config/app/storefront.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,12 @@
</meta>
<storefront>
<template-load-priority>100</template-load-priority>
<seo-url name="imprint">
<path>imprint</path>
<path lang="de-DE">impressum</path>
</seo-url>
<entity-seo-url name="blog-detail" entity="ce_blog">
<default-template>blog/{{ ceBlog.translated.title }}</default-template>
</entity-seo-url>
</storefront>
</manifest>
Loading