> For the complete documentation index, see [llms.txt](https://docs.carpose.de/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.carpose.de/misc-modules/08_url-structure.md).

# URL Structure

{% hint style="info" %}
The vehicle search writes the open vehicle and the current result page into the page URL, so every state is linkable, shareable and crawlable. Two URL forms are available; the query form is the default and works on every platform.
{% endhint %}

## Overview

The search keeps two pieces of state in the URL:

* **the open vehicle** — which detail view is shown
* **the result page** — which page of the listing is shown

Both are written with `pushState`, so browser back and forward move through them, and both are read back on a fresh page load. A crawler, a shared link and a bookmark therefore all open exactly what the sender saw.

## The two URL forms

|                   | Query form (default)      | Path form                    |
| ----------------- | ------------------------- | ---------------------------- |
| Vehicle           | `?vehicle={slug}--{uuid}` | `/fahrzeug/{slug}--{uuid}/`  |
| Result page       | `?cp-page=2`              | `/seite/2/`                  |
| Host requirements | none                      | sub-path routing (see below) |

Full examples on a search page at `https://your-site.example/fahrzeugsuche/`:

```
Query form
https://your-site.example/fahrzeugsuche/?vehicle=audi-q3-s-line--efbb7d07-0101-11f0-8de6-960000658564
https://your-site.example/fahrzeugsuche/?cp-page=2

Path form
https://your-site.example/fahrzeugsuche/fahrzeug/audi-q3-s-line--efbb7d07-0101-11f0-8de6-960000658564/
https://your-site.example/fahrzeugsuche/seite/2/
```

The vehicle value follows the `{slug}--{uuid}` pattern in both forms — a human-readable slug, two dashes, then the vehicle UUID. See [Addressing a vehicle](#addressing-a-vehicle) for the shorter slug-only form. Other query parameters already on the URL (such as `utm_*` tracking) and the hash are preserved.

Page 1 never carries a marker, so the first page keeps exactly one address. A URL carries at most one marker: opening a vehicle from page 2 leads to the vehicle, and a page link shown on a detail view leads back to the list.

{% hint style="success" %}
**Links written before a switch keep working.** Both forms are always *read*, whichever one is configured. Only newly written links follow the configured form, so URLs indexed under `?vehicle=` continue to open the right vehicle after a switch to the path form.
{% endhint %}

## Choosing the form

The form is an account setting, because three places have to agree on it: the search widget, the server-rendered SEO fallback of your CMS plugin, and the sitemap Carposé generates for your inventory. Set it on your Carposé account and all three follow.

The path form is strictly opt-in. An account without the setting, and an account where it is cleared again, both use the query form — so withdrawing the switch immediately returns every newly written link to the form that works without any routing on your side.

For an individual embed, the `data-url-mode` attribute overrides the account setting:

```html
<div
  data-carpose-component="vehicle-search-form"
  data-api-key="your-api-key"
  data-url-mode="path"
></div>
```

| Attribute            | Type    | Default | Description                                                                                                                                                                         |
| -------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data-url-mode`      | string  | `query` | URL form of the listing. Only an explicit `path` switches it; every other value — including an unset or cleared account setting — is the query form. Overrides the account setting. |
| `data-base-path`     | string  | derived | Base path the listing URLs are built on. Only needed for an embed whose links have to lead to a *different* search page than the one it sits on.                                    |
| `data-url-slug-only` | boolean | `false` | Address vehicles by their slug alone instead of `{slug}--{uuid}`. Overrides the account setting.                                                                                    |

### `data-base-path`

Normally you do not set this. The marker segments (`fahrzeug`, `seite`) make the base path self-describing: everything in front of the marker is the base, so the widget derives it from the URL it was opened with — the value that cannot disagree with where the widget actually sits.

Name a base path only when a second search is embedded on another page and its links should lead to your main search page instead:

```html
<div
  data-carpose-component="vehicle-search-form"
  data-api-key="your-api-key"
  data-url-mode="path"
  data-base-path="/fahrzeugsuche/"
></div>
```

## Addressing a vehicle <a href="#addressing-a-vehicle" id="addressing-a-vehicle"></a>

By default a vehicle is addressed as `{slug}--{uuid}`: a readable slug, two dashes, then the vehicle UUID. The UUID is what the vehicle is actually looked up by, and it never changes. The slug is derived from the vehicle title and carries no meaning for the lookup — it exists for readability and for search engines.

That split is deliberate. A vehicle title can change after an import, and with it the slug; because the UUID does the resolving, a link written under the old title keeps opening the right vehicle.

With `data-url-slug-only` — or the matching account setting — the UUID is left out and the slug becomes the address:

```
Default     /fahrzeugsuche/?vehicle=audi-q3-s-line--efbb7d07-0101-11f0-8de6-960000658564
Slug only   /fahrzeugsuche/?vehicle=audi-q3-s-line
```

**The two switches are independent.** Slug-only changes *what* addresses the vehicle, the URL form changes *where* it sits — either can be used without the other. Slug-only needs nothing from your website, so it works on the query form as shown above. Combining both gives the shortest URL available:

```
/fahrzeugsuche/fahrzeug/audi-q3-s-line/
```

{% hint style="warning" %}
**Only switch this on if former slugs stay resolvable.** Without the UUID the slug is the sole identifier, so a vehicle whose title changes gets a new address and the old one has to keep resolving on the server. Where that is not guaranteed, keep the default form — it is immune to title changes by construction.
{% endhint %}

Reading is unaffected by the setting: a `{slug}--{uuid}` link written before the switch keeps resolving through its UUID. Only newly written links use the short form. A value that is neither a UUID nor a valid slug is discarded and removed from the URL rather than being looked up.

## Host requirements for the path form

The query form needs nothing from your website. The path form does: your site has to answer `/fahrzeugsuche/fahrzeug/…/` and `/fahrzeugsuche/seite/2/` with the same page that holds the search widget. Without that routing every deep link is an ordinary 404 — which is why the query form stays the default.

### WordPress

Register the two segments as page endpoints and flush the rewrite rules once, on plugin activation and whenever the setting changes:

```php
add_action('init', function () {
    add_rewrite_endpoint('fahrzeug', EP_PAGES);
    add_rewrite_endpoint('seite', EP_PAGES);
});
```

{% hint style="warning" %}
`flush_rewrite_rules()` has to run once after registering the endpoints. Without it WordPress keeps answering 404 until someone re-saves the permalink settings — a failure that looks like a broken plugin.

If your site uses a German permalink setup that moves WordPress' own `pagination_base` to `seite`, the two collide. Keep the query form on those sites.
{% endhint %}

### Other platforms

Any host that can route sub-paths back to a page works the same way — a rewrite rule, a catch-all route, or a wildcard page. Website builders that do not allow arbitrary sub-paths (Wix among them) cannot serve the path form; use the query form there, which is fully indexable on its own.

## Switching an existing search over

Both switches change the addresses in your sitemap, so both follow the same order — prepare the target addresses first, flip the setting last.

**To the path form:**

1. Make the sub-path routing live on your website and confirm a deep link returns HTTP 200.
2. Add a permanent redirect (301) from the query form to the path form, so already-indexed URLs keep their history.
3. Flip the account setting. Widget, server-rendered fallback and sitemap change together.

**To slug-only:** no routing is involved, but the addresses still change. Confirm that a slug-only URL resolves, add the 301 from `{slug}--{uuid}` to the slug, then flip the setting.

Flipping the setting first leaves your sitemap listing URLs that still answer 404.

## Structured data

Whichever form is active, the `url` in the injected [schema.org](https://schema.org) JSON-LD matches the links on the page — the listing emits an `ItemList` of `Car` entries, and a detail view emits the vehicle itself. No configuration is required.
