# `@orderlyshop/web-components`

Headless native web components for Orderly storefronts.

The package depends on `@orderlyshop/core-client` and wraps Core contracts such as `SearchObject`, `SearchQuery`, and `DraftOrder` in reusable browser components. Components render practical light-DOM defaults and expose typed properties, events, controllers, stores, and template overrides so host shops keep full control over CSS, layout, routing, and copy.

## Agent Prompt Template

Copy this prompt into a coding agent when you want it to install the package and build a finished Orderly shop. Fill in known values first; if a value is filled in and is not `ASK`, the agent should use it without asking again.

```text
Build a production-ready Orderly storefront with @orderlyshop/web-components.

Inputs:
- Project directory: ASK
- Shop name: ASK
- Orderly account id: ASK
- Legal webshop owner name: ASK
- Legal webshop owner CVR: ASK
- Visual style reference: ASK
- Deployment target: ASK
- Google Tag Manager container id: NONE
- Google Tag Manager data layer name: dataLayer
- Analytics consent categories: statistics
- Cookie policy URL: /information/cookies/

Use any provided input above without asking again. Ask only for values that are still `ASK`. Treat Google Tag Manager container id `NONE` or an empty value as an explicit choice to leave GTM/analytics disabled.

Workflow:
1. Install @orderlyshop/core-client and @orderlyshop/web-components if needed. Run npx orderly-init-shop for a new shop, pass --account-id when available, and preserve existing files unless I approve overwrite.
2. Read node_modules/@orderlyshop/web-components/AGENTS.md, DESIGN.md, README.md, docs/components/README.md, docs/search-syntax.md, and docs/shop-best-practices.md before changing shop files. Treat docs/shop-best-practices.md as a completion checklist, not optional inspiration.
3. Scope the shop with the Orderly account id in src/shop-query.ts and set `defaultShop.legalOwnerName` plus `defaultShop.legalOwnerCvr` from the legal owner inputs. When product sampling is possible, run npx orderly-init-navigation --suggest --account-id <account-id>, read querySyntax and navigationSuggestions, then build src/navigation.ts manually with stable unique slugs, at most two category levels, and plain SearchQueryInput objects.
4. Use package-owned components and configuration first: orderly-home-page, orderly-category-page, orderly-product-detail-page, orderly-checkout-page, configureShop(...), templates, generated Vite setup, generated category URLs, and package SSR helpers.
5. Use the visual style reference as design direction, not as copy material. Start src/style.css from DESIGN.md semantic tokens, then add scoped component selectors only where tokens are insufficient.
6. Do not ship the package default look and feel with minor color changes. Inspect the catalog, research the reference site and comparable modern shops, and create homepage/category/product context that is visually and editorially specific to this shop.
7. Aim for premium modern frontend craft: strong art direction, responsive composition, high-quality imagery, clear interaction states, appropriate motion, performance, and accessibility. Do not copy proprietary assets, text, logos, or brand marks unless I explicitly confirm they may be used.
8. Google Tag Manager is optional. Treat Google Tag Manager container id `NONE` or an empty value as an explicit choice to leave GTM/analytics disabled. If a container id is provided and is not `ASK`, wire it through `VITE_ORDERLY_GOOGLE_TAG_MANAGER_ID` and package config rather than raw GTM snippets. Use `configureShop({ consent, analytics })`, Cookie policy URL, Google Tag Manager data layer name, and Analytics consent categories from the inputs.
9. Configure the build/publish flow for the deployment target: npm run build for static hosting, npx orderly-publish-site --target ftp for FTP, package server helpers for Apache/PHP or Nginx/PHP, or explicit documented assumptions for other targets.
10. Run the nearest relevant build/test command. Start the dev server when a browser is available and visually inspect homepage, category, product, basket, and checkout on desktop and mobile before declaring the shop done. Report command and visual verification plus any missing backend, credential, research, analytics, browser, or deployment prerequisite.
```

## Install

```sh
npm install @orderlyshop/core-client @orderlyshop/web-components
```

## Create A Shop

For a new vanilla storefront, run the scaffold command from the shop project:

```sh
npx orderly-init-shop
npm install
npm run dev
```

Agents and developers should use `npx orderly-init-shop` before hand-writing shop files. It creates `src/index.html`, `src/category.html`, `src/product.html`, `src/checkout.html`, `src/payment-success.html`, `src/payment-failure.html`, `src/includes/head.html`, `src/navigation.ts`, `src/shop-query.ts`, `src/templates/*.html`, `src/style.css`, `vite.config.mjs`, and package scripts. The payment pages are physical callback URLs for payment providers and use the package `orderly-payment-success-page` and `orderly-payment-failure-page` components. Pass an account id when the shop-wide default query should be tenant-scoped from the start:

```sh
npx orderly-init-shop --account-id 00000000-0000-0000-0000-000000000000
```

For agent-assisted shop creation, ask for the Orderly account id, legal webshop owner name, legal owner CVR, and a reference website URL for visual style before customizing the scaffold. Use the account id for `src/shop-query.ts`, use the legal owner values for `defaultShop.legalOwnerName` and `defaultShop.legalOwnerCvr`, use the reference website and catalog research to shape `src/style.css`, and follow [`docs/shop-best-practices.md`](./docs/shop-best-practices.md) as the completion checklist. The first pass should already feel visually distinct, editorially useful, and technically polished; it should not look like the default package scaffold with small token changes.

Re-running `npx orderly-init-shop` in an existing shop is non-destructive by default. Existing scaffold files are left unchanged with warnings, while missing new scaffold files are added. Use `--force` only when you explicitly want scaffold files overwritten.

The scaffold uses Vite for local development and static builds, but Vite is added to the generated shop's `devDependencies`. Dev mode SSR for product/category URLs is enabled by default, so View Source on local category/product URLs shows semantic server-rendered markup. Consumers should test locally from `http://localhost:61677` or `https://localhost:61677` because those are the expected local development origins for backend CORS. Do not switch to `127.0.0.1`, `0.0.0.0`, or a random Vite fallback port when debugging API calls; close the conflicting process and keep the origin on `localhost:61677`. The generated Vite setup defaults to `http://localhost:61677`; if a local reverse proxy or TLS terminator is added, keep the same `localhost:61677` origin over HTTPS. `@orderlyshop/web-components` does not depend on Vite at runtime.

## Browser Standalone Bundle

The package build emits optimized ESM output and browser-ready standalone bundles with `@orderlyshop/core-client`, Connect, and protobuf runtime code included. JavaScript output is minified, and `npm run build` writes `.br` and `.gz` files next to every generated `dist/**/*.js` file for hosts that can serve precompressed assets. The standalone browser files are shipped in the npm package and are what the `unpkg` and `jsdelivr` package fields point at.

Use the configurable bundle when a vanilla HTML site needs to set shop options before registering elements:

```html
<script src="/node_modules/@orderlyshop/web-components/dist/browser/orderly-web-components.global.js"></script>
<script>
  const { configureShop } = OrderlyWebComponents;

  configureShop({
    uiLanguage: "DA",
    defaultShop: {
      baseUrl: "https://service.orderly.shop",
      navigationDefinitions: [
        {
          label: "Sko",
          slug: "sko",
          query: { query: "sko | sneakers" },
          children: [
            { label: "Dame sko", slug: "dame-sko", query: { query: "damesko | sneakers" } }
          ]
        }
      ]
    }
  });
</script>
```

Use the auto-register bundle for the built-in default shop or pages that configure everything declaratively:

```html
<script src="/node_modules/@orderlyshop/web-components/dist/browser/orderly-web-components.define.global.js"></script>
```

The normal package entrypoints remain ESM for bundlers. The standalone files are classic browser scripts and expose `window.OrderlyWebComponents`.

`configureShop()` is the recommended one-call setup for HTML head scripts. Its parameter is strongly typed as `ShopConfig`, so VS Code/TypeScript can provide completions directly on the object literal. All fields are optional: `uiLanguage`, `defaultShop`, `storefrontRouter`, `pageLayout`, `responsiveTemplates`, `shopFooter`, `storedImageUrls`, `consent`, `analytics`, and `components`. It applies configuration first and then registers custom elements. The built-in SPA storefront router is enabled by default; pass `storefrontRouter: { enabled: false }` only when a shop needs traditional full-page navigation for every internal link. Pass `components: false` only when elements are registered elsewhere.

Built-in component copy is Danish by default (`uiLanguage: "DA"`). Set `uiLanguage: "EN"` to switch built-in labels, empty states, checkout copy, navigation controls, and page defaults to English. Shop-specific attributes, templates, and `defaultShop` label fields still take precedence over language defaults.

## Cookie Consent

Use `configureShop({ consent })` when the shop uses optional cookies, local storage identifiers, Google Tag Manager, analytics, campaign measurement, personalization, or similar tracking. The package-owned consent runtime gives visitors a first-layer banner with accept, reject, and customize actions, optional categories that are off by default, a persistent "Cookieindstillinger" button, versioned localStorage persistence, and first-party cookie cleanup when consent is rejected or withdrawn:

```ts
configureShop({
  consent: {
    enabled: true,
    version: "2026-04-30",
    policyHref: "/information/cookies/",
    managedCookies: [
      {
        name: "_ga",
        category: "statistics",
        provider: "Google Analytics",
        purpose: "Skelner mellem besøgende i statistikmåling.",
        duration: "Typisk op til 2 år"
      },
      {
        name: "_ga_*",
        category: "statistics",
        provider: "Google Analytics",
        purpose: "Bevarer sessionsstatus for statistikmåling.",
        duration: "Typisk op til 2 år"
      }
    ]
  }
});
```

Default categories are `necessary`, `preferences`, `statistics`, and `marketing`. `necessary` is always on; all other categories require an affirmative choice. Override `categories` and `texts` when a shop needs different purpose wording, and change `version` when purposes, vendors, or the cookie policy materially change so visitors are asked again. `managedCookies` accepts exact first-party cookie names or simple wildcard patterns such as `_ga_*`; include `provider`, `purpose`, and `duration` so the preferences layer can disclose the cookies it manages.

Programmatic helpers are exported for custom code: `hasCookieConsent(category)`, `currentCookieConsent()`, `saveCookieConsent(categories)`, `openCookieConsentPreferences()`, `setOrderlyCookie(name, value, { category })`, `deleteOrderlyCookie(name)`, and `clearCookiesForCategory(category)`. Use `setOrderlyCookie` for shop-owned optional cookies so they are only written when the category has consent.

The package can enforce consent for package-owned browser behavior, analytics hooks, and configured first-party cookies. The shop owner still has to keep the cookie policy/vendor list accurate, configure any server-side or third-party deletion outside the package, and ensure any custom scripts also check consent before reading or writing optional cookies.

## Analytics And Google Tag Manager

Use `configureShop({ analytics })` to install Google Tag Manager and publish GA4-compatible navigation and ecommerce events to `dataLayer`:

```ts
configureShop({
  consent: {
    enabled: true,
    policyHref: "/information/cookies/"
  },
  analytics: {
    googleTagManagerId: import.meta.env.VITE_ORDERLY_GOOGLE_TAG_MANAGER_ID,
    defaultCurrency: "DKK",
    affiliation: "My Shop"
  }
});
```

When `googleTagManagerId` is set, the package initializes `dataLayer` and injects the GTM script. The generated starter shop wires this to `VITE_ORDERLY_GOOGLE_TAG_MANAGER_ID`; omit the env var to leave tracking off. When package cookie consent is configured, analytics defaults to requiring the `statistics` category before GTM loads or package analytics events are pushed. Set `analytics.consentCategories` to another category, an array such as `["statistics", "marketing"]`, or `false` only when the shop handles consent outside the package.

With package cookie consent enabled, analytics also queues Google Consent Mode v2 `default` and `update` commands. The default command denies optional storage until the visitor has chosen; the update command is pushed immediately when the visitor saves or changes consent.

The analytics integration listens to package-owned DOM events and pushes these GA4 event names: `page_view`, `view_item_list`, `select_item`, `view_item`, `add_to_cart`, `remove_from_cart`, `view_cart`, `begin_checkout`, `add_shipping_info`, `add_payment_info`, and `purchase`. Ecommerce events clear the previous `ecommerce` object before pushing the new event, so GTM GA4 tags can read the current event payload cleanly. Purchase tracking fires from `orderly-payment-success-page` with `transaction_id`; configure the GA4 tag in GTM to use the data layer ecommerce object and avoid also firing a separate duplicate purchase tag on the same page.

For custom analytics, keep `analytics.enabled: true` without `googleTagManagerId` to use the same hooks with an existing GTM/dataLayer install, or pass `onEvent` to forward package events to another destination.

## API Discovery

Developers and coding agents should start with these package-owned docs:

- [`DESIGN.md`](./DESIGN.md) defines the package-wide design tokens and the recommended theming workflow for consistent styling across all components.
- [`docs/components/README.md`](./docs/components/README.md) lists every custom element, common attributes, typed properties, events, and template hooks.
- [`docs/shop-best-practices.md`](./docs/shop-best-practices.md) is the practical checklist for turning the package scaffold into a branded, editorial storefront without replacing package-owned component behavior.
- [`docs/search-syntax.md`](./docs/search-syntax.md) explains `SearchQuery.Query`, `|` OR queries, tag narrowing, and the recommended category query workflow.
- [`docs/components/product-grid.md`](./docs/components/product-grid.md) documents search loading, sorting, paging, default query merging, and declarative query markup.
- [`docs/components/product-rail.md`](./docs/components/product-rail.md) documents homepage/category rails and their query configuration.
- [`AGENTS.md`](./AGENTS.md) gives coding agents the conventions for building shops with this package.
- [`custom-elements.json`](./custom-elements.json) is the Custom Elements Manifest for tools that understand web component metadata.
- [`html-custom-data.json`](./html-custom-data.json) is VS Code compatible custom data for HTML autocomplete and hover docs.

These docs are written for GPT-5.5-class coding agents as well as humans: start with the outcome the shop needs, use the package-owned API surface, follow `docs/shop-best-practices.md` as the shop quality bar, and verify with both the nearest package/shop script and visual browser inspection before calling a shop finished.

Enable HTML editor hints in a consuming project by adding the package custom data file to `.vscode/settings.json`:

```json
{
  "html.customData": [
    "./node_modules/@orderlyshop/web-components/html-custom-data.json"
  ]
}
```

The most important convention is that Core contracts use typed JavaScript properties, while simple search use cases can be declared in HTML. For example:

```html
<orderly-product-rail
  title="Sko"
  cta-label="Se alle"
  cta-href="/categories/sko/"
  keywords="sneakers | støvler | sandaler"
  order-by="CreatedTime desc">
</orderly-product-rail>
```

`keywords` maps to `SearchQuery.Query`. Spaces narrow the text query; they are not OR. Use `|` for broad alternatives, for example `lego | matchbox | playmobil`. `tags="a,b"` maps to multiple `SearchQuery.Tags` and should be treated as an AND-style narrowing filter, not as category alternatives. For the full guidance, read [`docs/search-syntax.md`](./docs/search-syntax.md).

## Default Shop

The package can render a working default storefront with the built-in backend, navigation, sorting, basket, product detail, and footer defaults:

```ts
import "@orderlyshop/web-components/define";
```

```html
<orderly-category-page></orderly-category-page>
```

The default backend is `https://service.orderly.shop`. Category links use real `/categories/<category-path>/` URLs by default so production builds can ship server-rendered category HTML. Use `categoryUrlMode: "hash"` only when a shop explicitly wants the older `/category.html#id=<category>` routing.

## SPA Storefront Navigation

SPA-style storefront navigation is the default when a shop calls `configureShop()`. It is implemented as progressive enhancement, not as a replacement for normal URLs:

- Links remain normal same-origin `<a href>` links, so Google, no-JS browsers, new tabs, copied links, and server-rendered entry pages still work.
- Ordinary left-click navigation between known storefront routes is intercepted in the browser and rendered without a physical reload.
- The router uses `history.pushState`, `popstate`, scroll restoration, canonical URLs, meta description updates, document title updates, and optional View Transitions.
- A shared `BasketController` is kept alive across home, category, product, checkout, payment result, and configured static routes.
- Unknown routes, external links, modified clicks, downloads, `_blank` links, and hash-only links fall back to the browser.

The default route coverage is:

- home: `defaultShop.homeHref`, default `/`
- categories: real category paths generated from `navigationDefinitions`, default `/categories/<category-path>/`
- product details: either hash or path product URLs, controlled by `defaultShop.productUrlMode`
- checkout: `defaultShop.checkoutHref`, default `/checkout.html`
- payment result pages: `/payment-success.html?orderid=...` and `/payment-failure.html?orderid=...`
- static pages: routes explicitly supplied in `storefrontRouter.staticRoutes`

The simplest setup needs no router block:

```ts
configureShop({
  defaultShop: {
    homeHref: "/",
    legalOwnerName: "Example Shop ApS",
    legalOwnerCvr: "12345678",
    productHref: "/products/",
    productUrlMode: "path",
    productPathRoot: "/products/",
    checkoutHref: "/checkout.html",
    navigationDefinitions
  }
});
```

Use `storefrontRouter` only when the shop needs custom static pages, custom page factories, View Transition control, or an explicit opt-out:

```ts
configureShop({
  defaultShop: {
    homeHref: "/",
    productHref: "/products/",
    productUrlMode: "path",
    productPathRoot: "/products/",
    checkoutHref: "/checkout.html"
  },
  storefrontRouter: {
    product: {
      mode: "path",
      pathRoot: "/products/"
    },
    staticRoutes: [
      {
        path: "/forretningsbetingelser.html",
        title: "Forretningsbetingelser | Example Shop",
        description: "Køb, levering, betaling, retur og reklamation.",
        create: () => document.querySelector("#termsPage")!.cloneNode(true) as HTMLElement
      }
    ]
  }
});
```

Disable SPA navigation explicitly when a host application owns routing or when every internal link must perform a full document navigation:

```ts
configureShop({
  storefrontRouter: {
    enabled: false
  }
});
```

If a shop still uses hash-based product URLs, set `productUrlMode: "hash"` or `storefrontRouter.product.mode: "hash"`. The package supports both `#url=<encoded-share-url>` and real product paths; path URLs are preferred for SEO, while hash URLs remain a fallback for shops that cannot add server-side product routing yet.

For SEO, keep physical pages or static generated pages available for every URL the router can render dynamically. The router improves navigation after the first load; it does not remove the need for server/static support for direct requests to category, product, checkout, and content URLs.

Customize the default once per shop from the HTML head:

```ts
import { create } from "@bufbuild/protobuf";
import { AccountIdSchema, SearchQuerySchema, UUIDSchema } from "@orderlyshop/core-client";
import { configureShop } from "@orderlyshop/web-components";
import { navigationDefinitions } from "./navigation";

const defaultQuery = create(SearchQuerySchema, {
  SourceAccountId: create(AccountIdSchema, {
    Id: create(UUIDSchema, { Value: "00000000-0000-0000-0000-000000000000" })
  }),
  HiddenQuery: "published"
});

configureShop({
  uiLanguage: "DA",
  defaultShop: {
    baseUrl: "https://service.orderly.shop",
    defaultQuery,
    brandLabel: "Example Shop",
    homeHref: "/",
    productHref: "/product.html",
    navigationDefinitions,
    sortOptions: [
      { label: "Newest", orderBy: ["CreatedTime desc"] },
      { label: "Price: low to high", orderBy: ["Price asc"] }
    ]
  },
  pageLayout: {
    logoSrc: "https://orderly.shop/home/App_Icon.svg",
    logoAlt: "Example Shop",
    logoHref: "/"
  },
  responsiveTemplates: {
    mobileMediaQuery: "(max-width: 860px)"
  }
});
```

Use English built-in copy by changing only the top-level language:

```ts
configureShop({ uiLanguage: "EN" });
```

`defaultQuery` is a full Core `SearchQuery` and is merged into every `orderly-product-grid` request, including grids inside `orderly-category-page`, `orderly-collection-page`, and `orderly-product-rail`. A tenant account id is just one possible field on that query. Advanced shops can also assign `defaultQuery` directly on a specific component from JavaScript. User-facing category and search text stays in `SearchQuery.Query`; any text from the default query is moved to `HiddenQuery` so it still narrows results without replacing the visible search term.

`configureShop()` validates `navigationDefinitions`. Every category needs an explicit globally unique `slug` using lowercase letters, numbers, and hyphens. If navigation is invalid, category pages render a visible configuration error that points at the broken item.

```ts
categoryPage.defaultQuery = defaultQuery;
productGrid.defaultQuery = defaultQuery;
productRail.defaultQuery = defaultQuery;
```

After that, category pages can stay declarative:

```html
<orderly-category-page slug="shoes/women"></orderly-category-page>
<orderly-category-page slug="women"></orderly-category-page>
<orderly-category-page></orderly-category-page>
```

When no category attribute is set, `orderly-category-page` resolves the category from the current URL. It supports `/category.html#id=shoes%2Fwomen` and generated path pages such as `/categories/shoes/women/`. Hash ids are matched against both the generated category id and each navigation item's slug.

## Example Shop

A complete vanilla HTML storefront example with strongly typed TypeScript navigation data is included in the npm package source at `examples/shop`.

After installing from the repository workspace, run it with:

```sh
npm run dev --workspace @orderlyshop/example-shop
```

Open the local shop from `http://localhost:61677` during normal development, or `https://localhost:61677` when a local TLS proxy is in front of the dev server. Consumers should keep that exact localhost origin because the backend CORS development policy expects it. If live API requests work from one local URL but fail from another, use `localhost:61677` and stop any process that forced Vite onto a different port; avoid `127.0.0.1` for package shop development.

Inside an installed package, inspect the same reference implementation at:

```text
node_modules/@orderlyshop/web-components/examples/shop
```

The example demonstrates how to customize the package default with Danish navigation, shop copy, declarative home/category/product/checkout pages, and the default SPA router. Its router config only adds static content routes and page factories; `enabled: true` is not required. It intentionally avoids local CSS and templates while the package default look and feel is developed. Shared component configuration lives in `src/includes/head.html`; the strongly typed navigation data lives in `src/navigation.ts`. It uses the package-level `orderly-generate-category-pages` and `orderly-build-category-pages` commands for simple real-URL category pages. It is included as readable source code and is not exported as part of the runtime API.

## Navigation Setup

Installing `@orderlyshop/web-components` installs explicit shop and navigation helper tools. They do not run automatically during `npm install`.

For a new shop, start with the full scaffold:

```sh
npx orderly-init-shop
```

Create only a default strongly typed `navigation.ts` in the current project when the shop scaffold already exists:

```sh
npx orderly-init-navigation
```

Create the file somewhere else:

```sh
npx orderly-init-navigation --path src/navigation.ts
```

Agent workflow for a new shop:

1. Ask for the Orderly account id and a reference website URL for the desired visual style, or a short style description when no reference site exists.
2. Run the navigation helper when product sampling is possible:

```sh
npx orderly-init-navigation --suggest --account-id <account-id>
```

3. Create `navigation.ts` manually from `navigation-products.json`. Read `querySyntax` and `navigationSuggestions` first. Export `navigationDefinitions: NavigationDefinition[]`; each item needs `label`, a globally unique `slug`, optional hero/content fields, optional `children`, and plain `SearchQueryInput` values such as `{ query: "sko | sneakers" }` or `{ query: "kjole | sommerkjole", tags: ["dametøj"] }`. Keep navigation to at most two category levels and avoid empty or tiny categories.
4. Configure shop scope with a Core `SearchQuery` through `configureShop({ defaultShop: { defaultQuery } })`.
5. Read and follow `docs/shop-best-practices.md`, translate the reference into `DESIGN.md` tokens plus scoped CSS, and add homepage/category content from catalog and research.
6. Run the dev server when a browser is available and visually inspect homepage, category, product, basket, and checkout on desktop and mobile. Fix visible issues before reporting completion; visual QA is required for finished shop work.

The helper is intentionally an explicit tool for developers and coding agents. It is not a postinstall action. Agents should keep user-owned navigation edits, ask before overwriting files, and treat the product dump as source material for human-reviewable navigation.

Show tool help:

```sh
npx orderly-init-navigation --help
```

Optional flags:

```sh
npx orderly-init-navigation --suggest --account-id <account-id> --base-url https://service.orderly.shop --max-results 1000 --product-dump-path src/navigation-products.json
```

Use TypeScript output paths when the shop source is TypeScript:

```sh
npx orderly-init-navigation --suggest --account-id <account-id> --path src/navigation.ts --product-dump-path src/navigation-products.json
```

The product dump is sampled so large search indexes are not copied wholesale. It starts with every second product, then every third, fifth, eighth, and thirteenth product as the source result set grows. `--max-results` caps the number of product records written and defaults to `1000`.

Navigation data has three layers:

- `NavigationDefinition` is the authored data in `navigation.ts`. It is the only type most shops need to write. Every item needs an explicit stable `slug`; labels are display text and can change without changing URLs.
- `ResolvedNavigationItem` is created by `createCategoryNavigation()` and consumed by default page components. It adds stable ids, hrefs, normalized Core `SearchQuery` values, and derived metadata.
- `NavigationMetadata` contains those derived page values: slug, parent id, path segments, normalized query, search text, description, hero image, hero layout, hero cover count, and page root.

The older `CategoryDefinition`, `CategoryNavigationItem`, and `CategoryMetadata` names remain as compatibility aliases only.

## Real Category URLs

Installing `@orderlyshop/web-components` also installs generic category page tools. Real path category URLs are the default because they are the best fit for SEO and static/server-rendered category pages.

Generate pages from the current shop's `src/navigation.ts` or `src/navigation.js` and category page template. If `src/category.html` exists, the generator uses it and writes generated source pages under `src/categories`; otherwise it falls back to `category.html` and `categories`:

```sh
npx orderly-generate-category-pages
```

Preview what would be generated:

```sh
npx orderly-generate-category-pages --dry-run
```

Remove generated pages:

```sh
npx orderly-generate-category-pages --clean
```

Build with generated category pages and then clean the generated source files. Scaffolded shops use this flow from `npm run build` by default:

```sh
npx orderly-build-category-pages
```

`orderly-build-category-pages` also hydrates the built `dist/index.html` and generated `dist/categories/**/index.html` pages with SEO-friendly fallback HTML. The hydration step embeds semantic product cards per category using `SearchService.Search`; it reads `--base-url`, `VITE_ORDERLY_BASE_URL`, or `ORDERLY_BASE_URL`, and otherwise uses the default `https://service.orderly.shop` backend. It warns loudly when a category page or homepage rail query matches 0 products, because that almost always means `src/navigation.ts` needs a broader OR query or fewer tag filters. Live web components read the semantic fallback as initial visible state, hide the raw fallback markup, and then continue as normal client components. Product grids still run their first live `SearchService.Search()` after hydration when a client or backend URL is configured, so fallback products are replaced by current backend data and continuation-token paging starts from the live result set.

Hydrate an already-built site manually, for example in a nightly job:

```sh
npx orderly-hydrate-static-pages --base-url https://service.orderly.shop --products-per-category 12
```

Use `--skip-products` for metadata-only hydration, `--strict-products` when a nightly job should fail on backend search errors, `--strict-empty-categories` when CI should fail on empty category pages/homepage rails, and `--no-hydrate` on `orderly-build-category-pages` to keep the older unhydrated build behavior.

The category generator expects `src/navigation.ts` or `src/navigation.js` to export `navigationDefinitions` and the template page to contain `<orderly-category-page></orderly-category-page>`. TypeScript navigation files use the current shop project's `typescript` dependency for one-off transpilation. Useful options include `--navigation`, `--template`, `--categories-dir`, `--site-title`, `--component-tag`, and `--export`. The older `--taxonomy` flag and `categoryDefinitions` export are still accepted as compatibility aliases.

## Static Vanilla Deployment

For static hosting, keep the generated shop as a normal Vite site and upload the built `dist` directory, not `src` or `node_modules`.

Recommended path-based setup:

1. Keep `productUrlMode: "path"` or configure `storefrontRouter.product.mode = "path"` when the host can serve `/products/.../` through the provided server helper or a static fallback.
2. Use real category URLs by exporting `navigationDefinitions` and running `npx orderly-build-category-pages` from the shop project. This generates `src/categories/**/index.html` during build, hydrates `dist/index.html` plus `dist/categories/**/index.html`, and cleans generated source files afterwards.
3. Upload every file in `dist`, including hashed assets under `dist/assets` and precompressed `.br`/`.gz` files when the host supports them.
4. Configure the host to serve `index.html`, generated category `index.html` files, checkout/payment callback pages, and product fallback routing. Apache/PHP, Nginx/PHP-FPM, and Node helpers live in `node_modules/@orderlyshop/web-components/server`.
5. Use hash product URLs only when the deployment target cannot route unknown product paths. Hash URLs still work with the SPA router, but path URLs are clearer for static/server-rendered SEO entry points.

For local dry runs use `npm run build`, then the host's static preview command or `npm run preview` from the generated shop. For one-off publish flows, use `npx orderly-publish-site --target local --dir published-site` or `npx orderly-publish-site --target ftp`.

## Local SSR Dev Mode

Scaffolded shops use SSR in dev mode by default through the package Vite middleware:

```js
import { orderlySsrDevServer } from "@orderlyshop/web-components/server/vite";

export default defineConfig({
  plugins: [htmlIncludes(), orderlySsrDevServer(), rootHtmlOutput()]
});
```

When you run `npm run dev`, product and category URLs such as `http://localhost:61677/products/example.html/` and `http://localhost:61677/categories/sko/` are served from the normal `src/product.html` and `src/category.html` templates with semantic SSR HTML injected before Vite transforms the page. Use browser View Source to inspect the server-rendered markup.

SSR responses include a small inline critical CSS block for the semantic fallback markup. Normal browser requests hide that fallback and route component light DOM, such as utility banners, until the web components hydrate, so users do not see an unstyled intermediate layout. Googlebot user agents receive the visible fallback layout instead. The HTML source is identical in both cases; only fallback visibility changes.

After the first SSR entry render, the default storefront router intercepts same-origin category, product, checkout, payment-result, and configured static-page links. Navigation then stays virtual with `pushState` and persistent app-shell state, while the anchors still keep real `href` values for SEO, no-JS clients, copied links, and direct entry URLs. Set `storefrontRouter: { enabled: false }` only if the host app deliberately wants physical navigation after every click.

Set `ORDERLY_DEV_SSR=0` or run `npm run dev -- --no-ssr` to debug pure client-side rendering. Dev SSR uses `VITE_ORDERLY_BASE_URL`, `ORDERLY_BASE_URL`, or `https://service.orderly.shop`, defaults to `grpc-web`, and keeps a short per-URL in-memory cache. Override the cache with `ORDERLY_DEV_SSR_CACHE_TTL_MS`.

## Publish Static Sites

Installing `@orderlyshop/web-components` also installs a publish command for the consuming shop project. Run it from the project directory. The command generates category pages, builds, hydrates the built homepage/category pages, and publishes the resulting `dist` folder as vanilla HTML/JS/CSS.

Publish the current project to a local folder:

```sh
npx orderly-publish-site --target local --dir published-site
```

Opt out of generated category pages when a project has its own server-side category routing:

```sh
npx orderly-publish-site --target local --dir published-site --no-category-pages
```

Publish the current project over FTP:

```sh
npx orderly-publish-site --target ftp
```

The FTP target prompts for server URL, remote folder, username, and password when values are missing. Values are saved in `.orderly-publish.local.json` in the current project directory and reused on the next publish. Keep that file out of git; it is written with owner-only file permissions where the OS supports it.

Re-enter saved FTP settings:

```sh
npx orderly-publish-site --target ftp --configure
```

## Server Side Product And Category URLs

Production SSR for Apache/PHP is generated into the built site:

```sh
npm run build
npx orderly-generate-server-renderers --site-title "My Shop"
```

Scaffolded shops run that generator from `npm run build` by default. It writes `dist/product.php`, `dist/category.php`, `dist/.htaccess`, and `dist/orderly-ssr-manifest.json`. The PHP renderers call `SearchService.Search` over gRPC-web, inject semantic HTML into the built `product.html` or `category.html`, and preserve the normal web-component scripts and styles. The generated `.htaccess` sets long immutable cache headers for fingerprinted static assets and keeps HTML/PHP revalidatable; configure equivalent cache headers manually when deploying to hosts that ignore `.htaccess`.

Runtime options:

- `ORDERLY_BASE_URL` overrides the backend URL used by PHP.
- `ORDERLY_SSR_TIMEOUT` controls PHP backend timeout in seconds.
- `ORDERLY_SSR_HEADERS` can add internal server-side request headers. Do not expose API keys or bearer tokens to browser JavaScript.
- `ORDERLY_SSR_MANIFEST` can point PHP to a manifest outside the web root.

The semantic fallback uses real headings, links, images, schema.org Product/Offer metadata, and small `data-orderly-*` attributes only for behavior-critical values. Components hydrate from that HTML, then hide the fallback and continue as normal interactive web components. Product grids treat fallback products as temporary visual content and replace them with the first live search result when a backend client is available; without a live search target, the fallback remains visible. Generated SSR output keeps the same HTML for users and crawlers, hides fallback visibility for normal browser hydration, and serves a visible fallback layout to Googlebot. See [`server/README.md`](./server/README.md) for details.

## Define Elements

```ts
import { configureShop } from "@orderlyshop/web-components";

configureShop({
  responsiveTemplates: { mobileMediaQuery: "(max-width: 860px)" },
  storedImageUrls: {
    baseUrl: "https://static.example-cdn.com/",
    roles: {
      "product-tile": { width: 500, height: 500 },
      "product-detail": { width: 900, height: 900 },
      fullsize: false
    }
  }
});
```

Use a custom prefix when a shop already owns the default tag names:

```ts
configureShop({ components: { prefix: "shop" } });
```

The side-effect entrypoint registers the default `orderly-*` tags:

```ts
import "@orderlyshop/web-components/define";
```

The package installs baseline storefront CSS and structural lazy-load CSS when components are registered. The baseline CSS gives the default light-DOM components usable layout, spacing, responsive behavior, and product presentation without requiring a shop-owned stylesheet. Host shops can override it with normal CSS variables and `orderly-*` class selectors.

The lazy-load CSS can also be included as early as possible in the document head when pages contain slotted light-DOM content and need to avoid pre-hydration flashes:

```ts
import { ORDERLY_LAZY_LOAD_CSS } from "@orderlyshop/web-components";

const style = document.createElement("style");
style.textContent = ORDERLY_LAZY_LOAD_CSS;
document.head.append(style);
```

`defineOrderlyWebComponents()` installs the same lazy-load CSS at runtime as a fallback, but head inclusion is what prevents raw slotted links from flashing before JavaScript has loaded. The optional hydration fade is one-shot: it runs only for the first rendered component tree after a real page load, then the package removes `data-orderly-hydration-fade` so SPA navigation, browser back, product overlays, and other later renders do not fade or visually re-layout. The default component CSS is exported as `ORDERLY_DEFAULT_CSS` and installed automatically by `defineOrderlyWebComponents()`.

## Global Templates

Templates can live next to a specific element or in a shared page-level registry. Local templates still win, but components also look for document-level templates with `data-orderly-for`, `data-orderly-component`, or `data-orderly-template-scope`.

```html
<template data-orderly-template="product" data-orderly-for="product-tile">
  <article class="product-card">
    <h3 data-orderly-bind="title"></h3>
    <orderly-stored-image data-orderly-bind="image" fit="contain"></orderly-stored-image>
    <p data-orderly-bind="price"></p>
    <button type="button" data-orderly-action="add-to-basket">
      <span data-orderly-bind="basket-action-icon"></span>
    </button>
  </article>
</template>
```

This is useful for vanilla shops that include shared snippets such as `body-start.html`; all shop-specific templates can be included there instead of being repeated on every category page. A shop can keep each template in `src/templates/*.html` and use build-time comments such as `<!-- orderly-include-template: ../templates/product-tile.html -->` from `body-start.html`. `data-orderly-for` can target a full tag name such as `orderly-product-tile`, a prefix-independent component name such as `product-tile`, or `*`. Responsive global templates support the same `data-orderly-viewport` and `data-orderly-media` attributes as local templates.

JavaScript can also register templates:

```ts
import { registerGlobalTemplate } from "@orderlyshop/web-components";

registerGlobalTemplate({
  name: "product",
  for: "product-tile",
  template: `<article><h3 data-orderly-bind="title"></h3></article>`
});
```

## Collection Page

```html
<orderly-collection-page
  id="collection"
  title="Summer collection"
  description="Light layers and one-off pieces"
  hero-image="/images/summer.jpg">
  <template data-orderly-template="product">
    <article class="product-card">
      <orderly-product-tile></orderly-product-tile>
    </article>
  </template>
</orderly-collection-page>
```

```ts
import { create } from "@bufbuild/protobuf";
import { SearchQuerySchema } from "@orderlyshop/core-client";
import { createOrderlyWebClient } from "@orderlyshop/core-client/web";

const client = createOrderlyWebClient({ baseUrl: "https://api.orderly.example" });
const searchQuery = create(SearchQuerySchema, {
  Term: "summer"
});

collection.client = client;
collection.query = searchQuery;
```

## Content and Brand Cookbook

The package defaults are a foundation, not the desired finished look for a branded shop. Start with the reference website and catalog, then make deliberate content and layout choices before replacing package templates.

For a fuller real-shop checklist, read [Shop Best Practices](./docs/shop-best-practices.md) before the first implementation pass.

- Homepage: combine `orderly-home-page` or product rails with shop-owned editorial sections, seasonal/category rails, trust copy, store context, and strong imagery from the catalog or approved brand assets.
- Category pages: use `NavigationDefinition.description`, `heroImage`, `heroLayout: "covers"` or `heroLayout: "split"`, and `heroCoverCount` to create category headers with context and product-cover previews before adding custom HTML.
- Product pages: use `defaultShop.productDetail.trustNote`, `secondaryCta`, and detail slots for store visits, secondhand curation notes, return policy, pickup information, or other shop-specific confidence builders.
- Navigation: use `slot="menu-before"` and `slot="menu-after"` for store finder, donation, volunteer, campaign, or brand links; use `persist-state` for vertical desktop menus with nested categories.
- Basket: use `basket-mode="inline-desktop"` on `orderly-home-page`, `orderly-category-page`, or `orderly-product-detail-page` when the desktop design needs a right rail that only appears after products are added. Use `<orderly-basket-icon href="/checkout.html" placement="mobile-fixed" hide-when-empty>` for a package-owned mobile cart/checkout button with count.
- Checkout: keep platform copy out of the user-facing flow. Configure labels through `defaultShop.checkoutLabels` and `basketLabels`, then use scoped CSS and the checkout responsive override rules rather than replacing the checkout template.

## Components

- `orderly-page-layout` provides reusable page regions for header, header actions, responsive primary navigation placement, left/right sidebars, content, footer, and overlay content. It includes a default logo image using `https://orderly.shop/home/App_Icon.svg`; override it with `logo-src`, `logo-alt`, and `logo-href`. It always appends the package-owned legal ownership notice after footer content using `defaultShop.legalOwnerName` and `defaultShop.legalOwnerCvr`; shop templates cannot replace that notice. Use `primary-nav-mobile-placement="header"` for the common mobile header burger pattern without a duplicate mobile layout template.
- `orderly-home-page` composes page layout, responsive primary navigation, one product rail per top-level category, basket icon, basket drawer, optional inline desktop basket rail, product detail dialog, and footer. Configure it with `configureShop({ defaultShop })` plus attributes such as `title`, `eyebrow`, `rail-cta-label`, `product-href`, and `basket-mode="inline-desktop"`. Use the `hero` slot or `hero` template for a custom premium homepage hero while keeping package-owned rails, basket, product overlay, and generated URLs. The product dialog is fullscreen on mobile, keeps a bounded image column on desktop through `--orderly-product-dialog-image-size`, and pushes a same-URL history entry when opened, so browser back closes the overlay before leaving the homepage. On mobile the product dialog includes a sticky top action row with package-owned search, basket, and close controls; the separate expand button is hidden there. The expand link follows the configured product URL mode, and compact `https://orderly.shop/...` product URLs are preserved when it builds hash or path routes.
- `orderly-category-page` composes page layout, navigation, search, product grid, product detail, basket icon, and basket around a navigation category. Without properties it uses the package default shop configuration. Override with `configureShop({ defaultShop })` once per shop, set `base-url` or `product-href` in markup, or assign `category`, `navigationItems`, `sortOptions`, `client`, `defaultQuery`, and `basketController` from JavaScript for advanced cases. Configure `defaultShop.navigationLayout` or top-level `navigationLayout` to switch the package-owned category page from desktop side navigation to top horizontal category navigation while keeping the mobile burger in the header. Set `basket-mode="inline-desktop"` when the desktop category page should show a right basket rail only after the basket has lines. The product dialog is fullscreen on mobile, keeps a bounded image column on desktop through `--orderly-product-dialog-image-size`, and pushes a same-URL history entry when opened, so browser back closes the overlay before leaving the category. On mobile the product dialog includes a sticky top action row with package-owned search, basket, and close controls; the separate expand button is hidden there. The expand link uses the configured product URL mode and still supports explicit per-page `product-href` overrides when a shop wants a different physical product page URL.
- `orderly-checkout-page` composes page layout, responsive primary navigation, checkout form, basket order summary, footer, shared basket state, delivery loading, and order-created cleanup. Delivery methods render as radio cards after address entry, service points render as radio-card pickup choices when required, and delivery changes verify the draft so totals update. If a persisted draft already contains a complete address, delivery methods are restored on mount; if it also contains a delivery choice, the draft is verified so the basket summary can show backend shipping and total prices. Configure copy through `configureShop({ defaultShop: { checkoutPageTitle, checkoutPageDescription, checkoutOrderTitle, checkoutTermsHref, checkoutLabels, basketLabels } })`, or assign `client`, `basketController`, `navigationItems`, `checkoutLabels`, and `basketLabels` from JavaScript.
- `orderly-product-detail-page` composes page layout, navigation, product detail, basket drawer, optional inline desktop basket rail, and footer for a product route. Set `share-url`, the `shareUrl` property, or pass `#url=<encoded-share-url>` in the page URL; compact Orderly slugs are restored to `https://orderly.shop/...` before it calls `SearchService.Search` with `SearchQuery.ShareUrl` and renders the resolved `SearchObject`. It forwards `add-label`, `in-basket-label`, `details-before`, `purchase-note`, `secondary-cta`, and `details-after` to the nested product page. Set `basket-mode="inline-desktop"` when a product page should keep the basket visible as a desktop right rail after add-to-basket. Shops can tune outer gutters with `--orderly-product-detail-page-padding`, `--orderly-product-detail-page-mobile-padding`, `--orderly-product-detail-page-inline-padding`, and `--orderly-product-detail-page-mobile-inline-padding`.
- `orderly-stored-image` accepts `image: StoredImage`, resolves image URLs from the configured prefix, and applies `RotationDeg`, crop, fit, and role-based CDN sizing in the browser. Default roles include `product-tile` 500x500, `product-detail` 900x900, 180x180 thumbnails, and `fullsize` without size params.
- `orderly-credit` accepts `credit: Credit` or declarative `amount` and `currency` attributes, then renders stylable money markup. DKK values are formatted as `kr. <pris>`, for example `kr. 100,00`.
- `orderly-product-tile` accepts `product: SearchObject` and emits `orderly-product-selected`, `orderly-add-to-basket`, and `orderly-remove-from-basket`. Templates can bind the product share URL with `data-orderly-bind="share-url"`; anchors without an existing `href` keep the raw share URL, while anchors that already point at a product page are rewritten to the configured physical product route using hash or path mode. Default tiles, generated starter-shop tile templates, and statically hydrated category tiles include `schema.org/Product`, nested `schema.org/Brand`, and `schema.org/Offer` microdata for Google product snippets: name, image, URL, description, SKU, brand, price, currency, and in-stock availability are emitted when those fields are present. Custom tile templates should preserve the `data-orderly-bind="schema-*"` placeholders plus the `data-orderly-schema-brand` and `data-orderly-schema-offer` wrappers. When a `BasketController` is assigned through `basketController`, the default action button toggles between add and remove, using an icon-only button next to the price. Keyboard focus is indicated on the product title instead of a frame around the whole tile; custom tile templates should keep a title element with `data-orderly-bind="title"` or `.orderly-product-tile__title`.
- `orderly-product-page` accepts `product: SearchObject`, renders all product image thumbnails, switches the selected image on thumbnail click, and exposes product details plus add-to-basket behavior. It emits `orderly-product-viewed` once per displayed product, so analytics integrations can track `view_item` without replacing the template. Product-page templates bind `title`, `description`, `brand`, `size`, `color`, `condition`, `price`, `image`, `addLabel`, and `closeLabel`; they also support `basket-action-label`, `basket-action-icon`, and `basket-action-state` so product-page add-to-basket markup can share the same binding names as product tiles. The default add button switches to `in-basket-label` with a confirmed visual state after the current product is added, and custom templates can use the same state through `basket-action-label` and `data-orderly-in-basket`. The default product template has extension slots `details-before`, `purchase-note`, `secondary-cta`, and `details-after`; use these for shop-specific notices or links without replacing the whole product template. For simple default content, configure `defaultShop.productDetail.trustNote` and `defaultShop.productDetail.secondaryCta`. The fullscreen image viewer is package-owned, so custom product templates only need inline image markup plus optional thumbnail markup; the overlay, click/wheel zoom behavior, labels, focus handling, square image viewport, overlay thumbnails, and browser history entry are injected by the component itself unless `image-viewer="false"` is set. Browser back closes the image viewer before leaving the product page or product dialog. When opened inside an existing product dialog, the viewer is portaled into that open `<dialog>` so it stays above the product overlay; otherwise it is portaled to `document.body`. Main image frame styling lives on the default wrapper, not the inner `orderly-stored-image`, and shops can tune spacing/image framing with `--orderly-product-page-gap`, `--orderly-product-page-mobile-gap`, `--orderly-product-page-media-gap`, `--orderly-product-page-padding`, `--orderly-product-page-mobile-padding`, `--orderly-product-page-details-padding`, `--orderly-product-page-mobile-details-padding`, `--orderly-product-page-image-border`, `--orderly-product-page-image-radius`, `--orderly-product-page-image-background`, and `--orderly-product-page-thumbnail-border`.
- `orderly-search-box` binds to a target `orderly-product-grid`. Use `mode="textbox"` for an inline search input or `mode="icon"` for a header icon that opens a full-page search overlay. Icon mode searches automatically with a 500ms debounce and renders matching product tiles below the search field. Default home and category pages use icon mode next to the basket icon.
- `orderly-product-grid` accepts `query: SearchQuery`, merges the configured shop query scope, calls `SearchService.Search`, renders its own sort control from `sortOptions`, supports opaque `ContinuationToken` paging, manual paging, dynamic/infinite scroll, and default loading placeholders while the first page is pending.
- `orderly-product-rail` accepts `query: SearchQuery`, title, CTA label, and CTA href, then renders the search as a horizontal scroll list by composing `orderly-product-grid`. It is useful for homepages and editorial rows where several category searches should be stacked vertically.
- `orderly-collection-page` accepts `query: SearchQuery`, title, description, hero image, `hero-layout`, and `hero-cover-count`, then delegates fetching to `orderly-product-grid`. Use `hero-layout="covers"` to show product-cover previews from the loaded category products without custom category header HTML.
- `orderly-shop-footer` renders configurable logo, about text, address, contact information, opening hours, and information links.
- `orderly-basket-icon` and `orderly-basket` share a `BasketController` backed by `DraftOrder` persistence. Basket state events such as `orderly-basket-open`, `orderly-basket-change`, and `orderly-basket-verified` use the current `DraftOrder` directly as `event.detail`, so consumers can derive counts and totals from Core contracts instead of an internal basket data shape. Product add/remove and in-basket UI state are matched only by `SearchObject.SKU.Value` and `DraftOrderLine.SKU.Value`; titles, barcodes, target ids, and share URLs are not basket identity. `orderly-basket-icon` can also render a checkout/cart link with `href`, `placement="mobile-fixed"`, and `hide-when-empty` for a package-owned mobile button with count. `orderly-basket-checkout` includes `{ draft, count, href }` when the checkout action is triggered. `orderly-basket` renders an item overview, verifies non-empty persisted drafts on load plus later basket mutations through `OrderService.VerifyDraft` when a client or `base-url` is configured, includes a configurable checkout link through `checkout-href` and `checkout-label`, and only exposes quantity selection for lines where `MaxQuantity > 1`. Summary rows now render only backend-provided `DraftOrder` values, never local subtotal/total math, and the component renders both `DraftOrder.Errors` and `DraftOrderLine.Errors`. Custom basket templates can project order-level errors through `data-orderly-slot="errors"`.
- `orderly-checkout` persists checkout contact and address fields directly on `DraftOrder.Transport`, defaults the phone country picker to `+45`, validates email and phone values with built-in component logic, looks up Danish city names from the entered postal code through Dataforsyningen's `https://api.dataforsyningen.dk/postnumre/{postnr}` endpoint, loads delivery methods and service points, verifies the draft, and calls `OrderService.Create`. On mount it restores delivery methods for a persisted complete address and verifies a persisted delivery/service-point choice once, so shared basket summaries receive backend shipping and total prices without waiting for another address edit.
- `orderly-navigation` uses `NavigationController` and can be subclassed with site-specific `NavigationItem[]`. It is sticky by default, including when placed in `left`, `right`, or `primary-nav`; set `sticky="false"` to opt out. Use `layout="vertical"` for disclosure side navigation, `layout="horizontal"` for the tiered desktop bar, or `layout="burgermenu"` for an icon trigger that expands into the same nested disclosure menu inside a floating panel. Use `layout-desktop="horizontal" layout-mobile="burgermenu"` with `orderly-page-layout primary-nav-mobile-placement="header"` for desktop horizontal nav plus inline mobile burger. Add `persist-state state-key="main-menu"` to remember expanded category branches across navigation, and use `slot="menu-before"`/`slot="menu-after"` for shop-owned links such as store finder, campaigns, or volunteer pages.
- `orderly-search-box`, `orderly-sort-select`, `orderly-filter-panel`, and `orderly-load-more` bind to `orderly-product-grid`.

## Navigation Patterns

There are two different mobile navigation patterns:

- `layout="horizontal"` means the navigation owns its responsive fallback where it is placed. It renders the desktop horizontal category bar and automatically falls back to drawer-style mobile navigation on narrow viewports, but it does not move the trigger into the page header action row.
- `layout-desktop="horizontal" layout-mobile="burgermenu"` plus `primary-nav-mobile-placement="header"` means desktop horizontal navigation and an inline mobile burger in the page header next to search, basket, or other icon buttons. This is the recommended setup for the common storefront header pattern.
- Custom `data-orderly-viewport="mobile"` page-layout templates are only needed when a shop replaces the whole page layout and wants manual control over where `data-orderly-slot="primary-nav"` is rendered.

Package-owned home, category, product, checkout, and payment pages read the same pattern from configuration:

```ts
configureShop({
  navigationLayout: {
    primaryNavDesktopPlacement: "primary-nav",
    primaryNavMobilePlacement: "header",
    layoutDesktop: "horizontal",
    layoutMobile: "burgermenu"
  }
});
```

```html
<orderly-page-layout primary-nav-mobile-placement="header">
  <a slot="header" href="/">Shop name</a>
  <orderly-search-box slot="header-actions" mode="icon"></orderly-search-box>
  <orderly-basket-icon slot="header-actions"></orderly-basket-icon>
  <orderly-navigation
    slot="primary-nav"
    layout-desktop="horizontal"
    layout-mobile="burgermenu"
  ></orderly-navigation>
  <main slot="content">...</main>
</orderly-page-layout>
```

## Basket Layout And Count

Use the package basket primitives before writing custom state glue:

```html
<orderly-category-page basket-mode="inline-desktop"></orderly-category-page>
<orderly-basket-icon href="/checkout.html" placement="mobile-fixed" hide-when-empty label="Kurv"></orderly-basket-icon>
```

`basket-mode="inline-desktop"` is supported by `orderly-home-page`, `orderly-category-page`, and `orderly-product-detail-page`. It keeps the existing drawer behavior on mobile, but on desktop it shows a right rail only when `BasketController.count > 0`.

Basket count contract:

- `BasketController.count` is derived from `DraftOrder.OrderLines[].Quantity`.
- Product identity in the basket is `SKU.Value` only. Products that share titles, barcodes, target ids, or share URLs must still have distinct SKUs to avoid being treated as the same basket line.
- `orderly-basket-icon`, `orderly-basket`, product tiles/grids/rails, product pages, and checkout should share the same `BasketController` instance.
- `orderly-basket-change` and `orderly-basket-verified` expose the current `DraftOrder` as `event.detail`; custom UI can derive counts from that draft without copying package state.
- Verified and persisted drafts still use the same `DraftOrder`; backend-provided prices and errors are rendered by `orderly-basket`.

## Checkout Responsive Overrides

When customizing checkout layout, do not override the package mobile collapse accidentally:

- Keep desktop-only grid changes inside `@media (min-width: 768px)` or the same breakpoint configured through `configureShop({ responsiveTemplates })`.
- Avoid global `grid-template-columns` overrides on `.orderly-checkout-page__grid`, `.orderly-checkout__field-grid`, `.orderly-checkout__delivery-grid`, and `.orderly-checkout__option-grid--delivery` without a matching mobile reset.
- Prefer CSS variables and scoped spacing/color rules on `.orderly-checkout-page`, `.orderly-checkout-page__card`, `.orderly-checkout`, and `.orderly-checkout__option`.
- Keep `.orderly-checkout-page__card--summary` as a normal layout container; the package moves submit/terms controls there for the responsive checkout summary.

## Rendering And Styling

The package ships baseline default CSS and uses light DOM. Default markup uses stable `orderly-*` class names and CSS variables, so host CSS can style or replace the default look directly.

For consistent theming across the whole package, start with the semantic design contract in [`DESIGN.md`](./DESIGN.md). In practice that means overriding `--orderly-color-primary`, `--orderly-color-primary-soft`, `--orderly-color-primary-contrast`, the `--orderly-action-primary-*` and `--orderly-action-secondary-*` tokens, plus shared tokens such as `--orderly-link-accent-color`, `--orderly-selection-*`, `--orderly-badge-*`, `--orderly-product-tile-*`, `--orderly-product-rail-*`, `--orderly-collection-hero-*`, and `--orderly-checkout-*`. `--orderly-color-accent` is kept as a compatibility alias, but new themes should treat `--orderly-color-primary` as the source of truth.

The package declares its fallback tokens with `:where(:root)`, which has zero selector specificity. If your shop sets tokens on `:root`, those values win even when `defineOrderlyWebComponents()` injects the package CSS after your stylesheet. You should not need to duplicate token overrides on `body` unless you intentionally want a scoped override.

Example:

```css
:root {
  --orderly-color-primary: #1f7a43;
  --orderly-color-primary-soft: #e7f4eb;
  --orderly-color-primary-contrast: #ffffff;
  --orderly-link-accent-color: var(--orderly-color-primary);
  --orderly-selection-border: var(--orderly-color-primary);
  --orderly-selection-background: var(--orderly-color-primary-soft);
  --orderly-badge-background: var(--orderly-color-primary);
  --orderly-badge-color: var(--orderly-color-primary-contrast);
  --orderly-action-primary-background: var(--orderly-color-primary);
  --orderly-action-primary-border: var(--orderly-color-primary);
  --orderly-action-primary-color: var(--orderly-color-primary-contrast);
  --orderly-action-secondary-border: var(--orderly-color-primary);
  --orderly-action-secondary-color: var(--orderly-color-primary);
}
```

Rendering can be replaced with templates:

```html
<orderly-page-layout>
  <template data-orderly-template="layout">
    <section class="shop-layout">
      <header data-orderly-slot="header"></header>
      <aside data-orderly-slot="left"></aside>
      <main data-orderly-slot="content"></main>
      <footer data-orderly-slot="footer"></footer>
    </section>
  </template>

  <a slot="header" href="/">Shop</a>
  <shop-navigation slot="left"></shop-navigation>
  <orderly-collection-page slot="content"></orderly-collection-page>
</orderly-page-layout>

<orderly-page-layout
  logo-src="/brand/logo.svg"
  logo-alt="Example shop"
  logo-href="/">
  <a slot="header" href="/">Example shop</a>
  <orderly-shop-footer slot="footer"></orderly-shop-footer>
</orderly-page-layout>

<orderly-product-grid id="grid" paging="dynamic">
  <template data-orderly-template="layout">
    <section class="catalog">
      <div data-orderly-slot="sort"></div>
      <p data-orderly-bind="status"></p>
      <ol data-orderly-slot="items"></ol>
      <span data-orderly-sentinel></span>
    </section>
  </template>
  <template data-orderly-template="empty">
    <p class="empty-state">No matches.</p>
  </template>
  <template data-orderly-template="product">
    <li class="catalog-item">
      <orderly-product-tile></orderly-product-tile>
    </li>
  </template>
</orderly-product-grid>
```

For dynamic paging, `orderly-product-grid` keeps the returned `PageResult.Continuation` as an opaque `ContinuationToken` and sends it as `SearchQuery.Continuation` when the sentinel enters the viewport. Dynamic and infinite paging do not render a load-more button. Use `paging="button"` only when a visible load-more button is desired, `paging="manual"` when another control calls `loadNextPage()`, and `paging="dynamic"` or `paging="infinite"` for automatic loading.

Every component has a default light-DOM implementation and matching template hooks. Use `data-orderly-bind` for values, `data-orderly-action` for behavior, and `data-orderly-slot` for repeated or projected content. Product tile templates can bind `basket-action-icon`, `basket-action-label`, and `basket-action-state` and use `data-orderly-action="add-to-basket"` for the add/remove toggle. Product page templates keep the existing `addLabel` binding and also support `basket-action-label`, `basket-action-icon`, and `basket-action-state` for the same add-to-basket concept; `basket-action-label` switches from `add-label` to `in-basket-label` after add, and `data-orderly-in-basket`/`data-orderly-basket-action` expose the current state for styling. Use `data-orderly-action="add-to-basket"` for the action. Product page galleries use `data-orderly-slot="thumbnails"` and `data-orderly-action="select-image"` for custom thumbnail markup, while the fullscreen viewer overlay stays package-owned; shops can optionally mark a custom trigger with `data-orderly-image-viewer-trigger`, otherwise the inline `data-orderly-bind="image"` element becomes the trigger automatically. The viewer overlay may be moved outside the product-page host while open, either to `document.body` or to the nearest open `<dialog>`, so custom CSS or tests should target `[data-orderly-image-overlay]` instead of assuming the overlay remains inside `orderly-product-page`. Basket templates can add `data-orderly-slot="errors"` to place order-level `DraftOrder.Errors` exactly where a shop wants them. Product rail templates can replace the outer `layout`, inner `grid`, product item, loading state, and empty state. The main template names are `layout`, `grid`, `footer`, `product`, `thumbnail`, `image`, `basket`, `basket-icon`, `line`, `checkout`, `navigation`, `item`, `search-box`, `sort`, `sort-select`, `filter-panel`, `load-more`, `address-line`, `contact-item`, `opening-hour`, and `information-link`.

Use product page detail slots for small shop-specific content without replacing the package product template:

```html
<orderly-product-detail-page>
  <section slot="purchase-note">
    <strong>Kurateret secondhand</strong>
    <p>Alle varer er udvalgt og pakket i butikken.</p>
  </section>
  <a slot="secondary-cta" href="/butik/">Besøg butikken på Nørrebrogade</a>
</orderly-product-detail-page>
```

Product sorting belongs to `orderly-product-grid`: assign `grid.sortOptions = [{ label, orderBy }]` or pass sort options through `orderly-collection-page.sortOptions`. The default grid layout renders the sort select above the products. Custom grid layouts should include `data-orderly-slot="sort"` where the control should appear, and custom sort templates can use `data-orderly-template="sort"` with a `<select data-orderly-action="sort">`.

Simple product queries can be declared in markup on `orderly-product-grid`, `orderly-product-rail`, and `orderly-collection-page`:

```html
<orderly-product-rail
  title="Sko"
  cta-label="Se alle"
  cta-href="/categories/sko/"
  keywords="sneakers | støvler | sandaler"
  tags="sko"
  order-by="CreatedTime desc">
</orderly-product-rail>
```

The same query can be grouped as child markup:

```html
<orderly-product-rail
  title="Sko"
  cta-label="Se alle"
  cta-href="/categories/sko/">
  <query>
    <tags>sko</tags>
    <keywords>sneakers | støvler | sandaler</keywords>
    <order-by>CreatedTime desc</order-by>
  </query>
</orderly-product-rail>
```

Declarative query fields map to `SearchQuery.Query`, `SearchQuery.Tags`, `SearchQuery.HiddenQuery`, `SearchQuery.OrderBy`, `SearchQuery.StoreId`, and `SearchQuery.Featured`. Use `|` for OR text and avoid multiple tags for alternatives. For the full Core contract, assign the typed `query` property from JavaScript.

Templates can be viewport-specific on every component:

```html
<orderly-product-page>
  <template data-orderly-template="product" data-orderly-viewport="desktop">
    <section class="product-detail product-detail--desktop">
      <div data-orderly-slot="thumbnails"></div>
      <orderly-stored-image data-orderly-bind="image" variant="object"></orderly-stored-image>
    </section>
  </template>
  <template data-orderly-template="product" data-orderly-viewport="mobile">
    <section class="product-detail product-detail--mobile">
      <orderly-stored-image data-orderly-bind="image" variant="object"></orderly-stored-image>
      <div data-orderly-slot="thumbnails"></div>
    </section>
  </template>
  <template data-orderly-template="thumbnail">
    <button type="button" data-orderly-action="select-image">
      <orderly-stored-image data-orderly-bind="thumbnail"></orderly-stored-image>
    </button>
  </template>
</orderly-product-page>
```

Use `data-orderly-viewport="mobile"` or `data-orderly-viewport="desktop"` for the package breakpoint, or `data-orderly-media="(max-width: 540px)"` for a component-specific media query. Components rerender when the viewport crosses the configured breakpoint. The default mobile breakpoint is `767px` and can be changed once per app:

```ts
import { configureShop } from "@orderlyshop/web-components";

configureShop({ responsiveTemplates: { mobileMediaQuery: "(max-width: 860px)" } });
```

`orderly-page-layout` can also be changed once per shop without touching every category page:

```ts
import { configureShop } from "@orderlyshop/web-components";

configureShop({
  pageLayout: {
    template: `
      <section class="custom-shop-layout">
        <header data-orderly-slot="header"></header>
        <main data-orderly-slot="content"></main>
        <footer>
          <orderly-shop-footer></orderly-shop-footer>
          <div data-orderly-slot="footer"></div>
        </footer>
        <aside data-orderly-slot="right"></aside>
        <div data-orderly-slot="overlay"></div>
      </section>
    `
  }
});
```

Shop-specific footer content is usually clearer as a global template in shared HTML, for example `body-start.html`:

```html
<template data-orderly-template="footer" data-orderly-for="shop-footer">
  <div class="orderly-shop-footer">
    <div class="orderly-shop-footer__brand">
      <a class="orderly-shop-footer__logo" href="/" aria-label="Example shop">
        <img class="orderly-shop-footer__logo-image" src="/brand/logo.svg" alt="Example shop">
      </a>
      <p class="orderly-shop-footer__about">Independent shop copy.</p>
    </div>
    <section class="orderly-shop-footer__section" aria-labelledby="footerInfo">
      <h2 id="footerInfo" class="orderly-shop-footer__heading">Information</h2>
      <ul class="orderly-shop-footer__list">
        <li class="orderly-shop-footer__link-item">
          <a class="orderly-shop-footer__link" href="/delivery/">Delivery</a>
        </li>
      </ul>
    </section>
  </div>
</template>
```

Typed JavaScript properties are used for Core contracts and clients. Attributes are limited to primitive configuration such as `base-url`, `paging`, `title`, `description`, `hero-image`, `logo-src`, `logo-alt`, `logo-href`, `fit`, `variant`, and `for`.

Basket checkout navigation is configured in markup:

```html
<orderly-basket checkout-href="/checkout.html" checkout-label="Go to checkout"></orderly-basket>
```

Stored images can be rendered directly:

```html
<orderly-stored-image id="productImage" fit="cover" variant="thumbnail"></orderly-stored-image>
```

```ts
productImage.image = searchObject.Images[0];
```

`orderly-stored-image` uses `DEFAULT_IMAGE_BASE_URL`, currently `https://orderlyproduction.azureedge.net/`, when no custom image URL configuration is supplied. `configureStoredImageUrls({ baseUrl })` maps normal product images to `baseUrl/thumbnails/<StoredImage.Name>` and full/object variants to `baseUrl/objects/<StoredImage.Name>`. The component also supports `ImageDataBase64`, absolute URLs, and a custom resolver through `configureStoredImageUrls({ resolver })`. Image URLs use configured roles rather than runtime layout measurement: default roles are `product-tile` 500x500, `product-detail` 900x900, `thumbnail`, `basket-line`, and `checkout-line` 180x180, and `fullsize` with no `width`/`height` parameters. Set `image-role` on the component, pass `{ role }` to `storedImageUrl()`, or override role sizes with `configureShop({ storedImageUrls: { roles } })`. Custom resolvers receive `{ variant, role, width, height }` as the second argument and should preserve those dimensions in the returned URL when the target CDN supports resizing. Use `loading`, `decoding`, and `fetchpriority` on custom image templates when a shop knows which image is above the fold; the default product detail main image is eager/high priority, while normal stored images stay lazy by default.

By default, `orderly-stored-image` does not render a frame border. Add the boolean `border` attribute when a bordered image is wanted:

```html
<orderly-stored-image border fit="contain"></orderly-stored-image>
```

## State And Security

Basket drafts are persisted to localStorage by default. Checkout contact and address data are stored directly on `DraftOrder.Transport`, so basket items, delivery selection, and checkout form state all share the same persisted draft. That draft can contain PII such as name, address, email, and phone. Provide a custom basket store to change or disable that behavior. This package never stores tokens, API keys, OAuth responses, or cookie values.

Persistence uses `LocalStorageBasketStore` and stores the full `DraftOrder`:

```ts
import { createBasketController } from "@orderlyshop/web-components";

basket.basketController = createBasketController({ store: myBasketStore });
```

Authentication remains owned by `@orderlyshop/core-client`. Browser cookie-backed sessions work through the core browser client and credentialed gRPC-web requests.
