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

# Nuxt Module

The `@frontic/nuxt` module wires your Nuxt application to the Frontic backend. It ships composables for data fetching, automatic proxy configuration, and locale management.

## Installation

<CodeGroup>
  ```bash npm theme={"theme":"css-variables"}
  npx nuxi@latest module add @frontic/nuxt
  ```

  ```bash pnpm theme={"theme":"css-variables"}
  pnpm dlx nuxi@latest module add @frontic/nuxt
  ```

  ```bash yarn theme={"theme":"css-variables"}
  yarn dlx nuxi@latest module add @frontic/nuxt
  ```
</CodeGroup>

This installs the module and adds it to your `nuxt.config.ts` automatically.

```ts nuxt.config.ts theme={"theme":"css-variables"}
export default defineNuxtConfig({
  modules: ["@frontic/nuxt"],
});
```

<Check>
  The module works out of the box with sensible defaults: CORS proxy enabled,
  all composables registered, and TypeScript paths configured.
</Check>

***

## Configuration

All options are optional. Customize behavior as needed:

```ts nuxt.config.ts theme={"theme":"css-variables"}
export default defineNuxtConfig({
  modules: ["@frontic/nuxt"],
  frontic: {
    // All options below are optional
    contextDomain: "www.your-shop.com",
    proxy: "/api/custom-path",
    redirectOn301: false,
    throwOn404: false,
    // ... other options
  },
});
```

### Options

#### API

<ParamField path="proxy" type="boolean | string" default="true">
  Enable the built-in CORS proxy at `/api/frontic`, or set a custom path.

  <Expandable title="Options">
    <ParamField path="true" type="boolean">
      Enable proxy at `/api/frontic`.
    </ParamField>

    <ParamField path="false" type="boolean">
      Disable proxy (use for external proxy setups).
    </ParamField>

    <ParamField path="string" type="string">
      Custom proxy path (e.g., `/api/custom-frontic`).
    </ParamField>
  </Expandable>
</ParamField>

<ParamField path="fetchApiSecret" type="string">
  API secret for protected environments. **Server-only** - never exposed to the browser.

  <Expandable title="Usage">
    ```ts theme={"theme":"css-variables"}
    frontic: {
      fetchApiSecret: process.env.FRONTIC_FETCH_SECRET,
    }
    ```

    The secret is injected into requests via the server proxy, keeping it secure.
  </Expandable>
</ParamField>

#### Routing

<ParamField path="redirectOn301" type="boolean" default="true">
  Automatically redirect on 301 responses in `useFronticPage`.
</ParamField>

<ParamField path="throwOn404" type="boolean" default="true">
  Throw a 404 error when page is not found in `useFronticPage`.
</ParamField>

#### Context

<ParamField path="disableContext" type="boolean" default="false">
  Disable automatic context fetching and cookie management in `useFronticContext`.

  <Expandable title="When enabled">
    * The context endpoint will **not** be called automatically on mount
    * No context cookie will be set automatically
    * You must manually call `refresh()` to fetch contexts
    * You are responsible for storing and managing the context token

    Use this when you want full control over context management, or when integrating with an existing locale/region system.
  </Expandable>
</ParamField>

<ParamField path="contextCookieName" type="string" default="fs-context">
  Cookie name for storing the context token.
</ParamField>

<ParamField path="contextCookieMaxAge" type="number" default="31536000">
  Cookie max age in seconds. Default is 1 year.
</ParamField>

<ParamField path="contextDomain" type="string | boolean">
  Domain for page slug construction and API context resolution. Used by all composables.

  <Expandable title="Options">
    <ParamField path="string" type="string">
      Static domain (e.g., `'shop.com'`). Used for page slug construction and injected into block/listing requests.
    </ParamField>

    <ParamField path="true" type="boolean">
      Read `contextDomain` from the current `@nuxtjs/i18n` locale object. Requires `@nuxtjs/i18n` to be installed.
    </ParamField>
  </Expandable>

  When not set, `useFronticPage` derives the domain from the request URL host (`useRequestURL().host`). This works in production where the host *is* the Frontic domain, but not when the host doesn't match — for example in **local development** (`localhost:3000`), **sandbox / preview environments**, or **staging** where only the production domain is configured in Frontic.

  You can configure a **domain alias** in your [admin app](https://docs.frontic.com/domains) as an alternative. If your dev environment uses a host that isn't configured as an alias, set `contextDomain` to override it:

  ```ts theme={"theme":"css-variables"}
  export default defineNuxtConfig({
    modules: ["@frontic/nuxt"],
    frontic: {
      contextDomain: import.meta.dev ? "www.your-shop.com" : undefined,
    },
  });
  ```
</ParamField>

#### Frontic UI

The module configures [Frontic UI](/reference/frontic-ui) — a set of ready-to-use, customizable components designed for Frontic storefronts. When enabled, the module auto-imports your Frontic UI components so they're available everywhere in your app without manual imports, mirroring Nuxt's built-in component auto-import.

<Info>
  Frontic UI is currently in **open beta**. We'd love to hear your feedback! If
  you have questions or suggestions, please [get in touch with
  us](mailto:support@frontic.com).
</Info>

<ParamField path="componentsPrefix" type="string" default="">
  Prefix for auto-imported Frontic UI components from your component directory. For example, setting `'Ui'` would make `Button.vue` available as `<UiButton />`.
</ParamField>

<ParamField path="componentDir" type="string" default="@/components/ui">
  Directory path for Frontic UI components to auto-import.
</ParamField>

#### Composables

<ParamField path="composables" type="boolean | FronticComposable[]" default="true">
  Control which composables are auto-imported during the Nuxt build.

  <Expandable title="Options">
    <ParamField path="true" type="boolean">
      Import all composables.
    </ParamField>

    <ParamField path="false" type="boolean">
      Disable all composable imports.
    </ParamField>

    <ParamField path="FronticComposable[]" type="array">
      Import only specific composables. Valid values:

      * `'block'` → useFronticBlock
      * `'listing'` → useFronticListing
      * `'search'` → useFronticSearch
      * `'tree'` → useFronticTree
      * `'page'` → useFronticPage
      * `'context'` → useFronticContext
      * `'client'` → useFronticClient
    </ParamField>
  </Expandable>

  <Note>
    When a composable is **disabled**, it will not be auto-imported during the Nuxt build. You won't get type errors or warnings — the composable won't exist. Use this to reduce bundle size by excluding composables you don't need.
  </Note>
</ParamField>

***

## Composables

The Frontic composables provide a **smart data layer** for your Nuxt application. Built on [Pinia Colada](https://pinia-colada.esm.dev/), they handle caching, deduplication, and SSR hydration automatically — instant UI updates via stale-while-revalidate, shared requests across components, and server-to-client data transfer without extra configuration.

<CardGroup cols={2}>
  <Card title="useFronticBlock" icon="cube" href="#usefronticblock">
    Fetch single blocks (products, categories) with smart caching
  </Card>

  <Card title="useFronticListing" icon="list" href="#usefronticlisting">
    Fetch listings with automatic caching and SSR support
  </Card>

  <Card title="useFronticSearch" icon="magnifying-glass" href="#usefronticsearch">
    Full-featured search with filters, sorting, and smart state management
  </Card>

  <Card title="useFronticTree" icon="sitemap" href="#usefrontictree">
    Hierarchical menu trees with automatic caching and SSR support
  </Card>

  <Card title="useFronticPage" icon="file" href="#usefronticpage">
    Dynamic page routing with redirects and 404 handling
  </Card>

  <Card title="useFronticContext" icon="globe" href="#usefronticcontext">
    Locale and region switching with cookie persistence
  </Card>

  <Card title="useFronticClient" icon="code" href="#usefronticclient">
    Low-level client for direct API access
  </Card>
</CardGroup>

***

### `useFronticBlock`

Fetch a single block (product, category, brand) by key with automatic caching and SSR support. See [Caching](#caching) for details.

#### Parameters

<ParamField path="name" type="keyof Blocks" required>
  The name of the block to fetch. Provides full autocomplete for your generated
  block types.
</ParamField>

<ParamField path="key" type="string | Ref<string>" required>
  The key identifier for the block. Can be reactive for dynamic fetching.
</ParamField>

<ParamField path="options" type="object">
  Configuration options.

  <Expandable title="Request Options">
    <ParamField path="contextKey" type="string | Ref<string>">
      Context token for locale/region selection.
    </ParamField>

    <ParamField path="contextDomain" type="string | Ref<string>">
      Context domain to override the global setting for this request.
    </ParamField>

    <ParamField path="requestUrl" type="string | Ref<string>">
      Request URL for analytics/tracking purposes.
    </ParamField>

    <ParamField path="staleTime" type="number" default="300000">
      Cache duration in milliseconds. Default: 5 minutes.
    </ParamField>
  </Expandable>
</ParamField>

#### Returns

<ResponseField name="block" type="Ref<Responses[T] | undefined>">
  The block data, fully typed based on the block name.
</ResponseField>

<ResponseField name="status" type="Ref<'pending' | 'error' | 'success'>">
  Current query status for loading states.
</ResponseField>

<ResponseField name="refresh" type="() => Promise<void>">
  Refresh data using cache if still valid.
</ResponseField>

<ResponseField name="refetch" type="() => Promise<void>">
  Force a fresh fetch, bypassing cache entirely.
</ResponseField>

#### Example

```ts theme={"theme":"css-variables"}
<script setup lang="ts">
const route = useRoute()
const { block: product, status } = useFronticBlock('ProductFull', route.params.id)
</script>

<template>
  <div v-if="status === 'pending'">Loading...</div>
  <div v-else-if="product">
    <h1>{{ product.name }}</h1>
    <p>{{ product.price.formatted }}</p>
  </div>
</template>
```

<Accordion title="More Examples">
  ```ts theme={"theme":"css-variables"}
  // With options
  const { block } = useFronticBlock("ProductCard", productId, {
    contextKey: contextToken,
    staleTime: 1000 * 60 * 10, // 10 minutes
  });

  // Reactive key - automatically refetches when key changes
  const productKey = ref("product-123");
  const { block } = useFronticBlock("ProductCard", productKey);
  ```
</Accordion>

<Accordion title="Type Signature">
  ```ts theme={"theme":"css-variables"}
  import type { Ref, MaybeRef } from "vue";
  import type { Blocks, Responses } from "@frontic/stack/generated-types";

  function useFronticBlock<T extends keyof Blocks>(
    name: T,
    key: MaybeRef<string>,
    options?: {
      contextKey?: MaybeRef<string | undefined>;
      contextDomain?: MaybeRef<string | undefined>;
      requestUrl?: MaybeRef<string | undefined>;
      staleTime?: number;
    },
  ): {
    block: Ref<Responses[T] | undefined>;
    status: Ref<"pending" | "error" | "success">;
    refresh: () => Promise<void>;
    refetch: () => Promise<void>;
  };
  ```
</Accordion>

***

### `useFronticListing`

Fetch a listing by name and parameters with automatic caching and SSR support. Mirrors `client.listing(name, params, { query })` — including the optional `query` for `filter`, `sort`, `search`, `limit`, and `page`. See [Caching](#caching) for details.

<Note>
  Building a page with **interactive** filter/sort/search controls or
  pagination bound to user input? Use [`useFronticSearch`](#usefronticsearch).
  `useFronticListing` is the right pick when the query is fixed at call time —
  homepage carousels with `{ limit: 10 }`, server-rendered grids with a static
  filter, etc.
</Note>

#### Parameters

<ParamField path="name" type="keyof Listings" required>
  The name of the listing to fetch.
</ParamField>

<ParamField path="params" type="ListingParameters[T] | Ref" required>
  Parameters to pass to the listing endpoint. Type-safe based on listing name.
</ParamField>

<ParamField path="options" type="object">
  Configuration options.

  <Expandable title="Query Options">
    <ParamField path="query" type="ListingQuery<T> | Ref">
      Query forwarded directly to the Fetch API as `client.listing(name, params, { query })`. Fully typed against your generated listing schema — `filter` field keys and `sort` field/order resolve to the autocomplete-typed shape for the specific listing.

      ```ts theme={"theme":"css-variables"}
      query: {
        filter: [{ type: 'equals', field: 'properties.color', value: 'red' }],
        sort: { field: 'price.amount', order: 'asc' },
        limit: 20,
      }
      ```

      Accepts a reactive ref — the cache key includes a JSON-serialised form of `query`, so distinct values cache independently and the listing refetches when the ref changes.
    </ParamField>
  </Expandable>

  <Expandable title="Request Options">
    <ParamField path="contextKey" type="string | Ref<string>">
      Context token for locale/region selection.
    </ParamField>

    <ParamField path="contextDomain" type="string | Ref<string>">
      Context domain to override the global setting for this request.
    </ParamField>

    <ParamField path="requestUrl" type="string | Ref<string>">
      Request URL for analytics/tracking purposes.
    </ParamField>

    <ParamField path="staleTime" type="number" default="300000">
      Cache duration in milliseconds.
    </ParamField>
  </Expandable>
</ParamField>

#### Returns

<ResponseField name="listing" type="Ref<Responses[T] | undefined>">
  The listing data with items and metadata.
</ResponseField>

<ResponseField name="status" type="Ref<'pending' | 'error' | 'success'>">
  Current query status.
</ResponseField>

<ResponseField name="refresh" type="() => Promise<void>">
  Refresh using cache if valid.
</ResponseField>

<ResponseField name="refetch" type="() => Promise<void>">
  Force fresh fetch.
</ResponseField>

#### Example

```ts theme={"theme":"css-variables"}
<script setup lang="ts">
const { listing, status } = useFronticListing('CategoryProducts', {
  key: 'shoes',
})
</script>

<template>
  <div v-if="status === 'pending'">Loading...</div>
  <div v-else>
    <div v-for="item in listing?.items" :key="item.key">{{ item.name }}</div>
  </div>
</template>
```

<Accordion title="More Examples">
  ```ts theme={"theme":"css-variables"}
  // Reactive params - refetches when category changes
  const categoryKey = ref("shoes");

  const { listing } = useFronticListing(
    "CategoryProducts",
    computed(() => ({ key: categoryKey.value })),
  );

  // Static query - featured-products carousel with a fixed limit
  const { listing } = useFronticListing(
    "ProductSearch",
    {},
    { query: { limit: 10 } },
  );

  // Typed filter + sort
  const { listing } = useFronticListing(
    "ProductSearch",
    {},
    {
      query: {
        filter: [{ type: "equals", field: "properties.color", value: "red" }],
        sort: { field: "price.amount", order: "asc" },
        limit: 20,
      },
    },
  );

  // Reactive query - refetches when page changes
  const page = ref(1);
  const { listing } = useFronticListing(
    "ProductSearch",
    {},
    { query: computed(() => ({ limit: 20, page: page.value })) },
  );
  ```
</Accordion>

<Accordion title="Type Signature">
  ```ts theme={"theme":"css-variables"}
  import type { Ref, MaybeRef } from "vue";
  import type {
    Listings,
    ListingParameters,
    ListingQueryFilters,
    ListingQuerySorts,
    Responses,
  } from "@frontic/stack/generated-types";
  import type { Query } from "@frontic/stack/query-types";

  // Typed Query<filter, sort> for a given listing name — exposed for convenience
  type ListingQuery<T extends keyof Listings = keyof Listings> = Query<
    T extends keyof ListingQueryFilters ? ListingQueryFilters[T] : unknown,
    T extends keyof ListingQuerySorts ? ListingQuerySorts[T] : unknown
  >;

  function useFronticListing<T extends keyof Listings>(
    name: T,
    params: MaybeRef<ListingParameters[T]>,
    options?: {
      query?: MaybeRef<ListingQuery<T> | undefined>;
      contextKey?: MaybeRef<string | undefined>;
      contextDomain?: MaybeRef<string | undefined>;
      requestUrl?: MaybeRef<string | undefined>;
      staleTime?: number;
    },
  ): {
    listing: Ref<Responses[T] | undefined>;
    status: Ref<"pending" | "error" | "success">;
    refresh: () => Promise<void>;
    refetch: () => Promise<void>;
  };
  ```
</Accordion>

***

### `useFronticSearch`

A ready-to-use backend for building stateful search and filter UIs. This composable handles all the interaction logic - filtering, sorting, pagination, and text search - with automatic caching and SSR support, so you can focus on crafting the perfect UI. See [Caching](#caching) for details.

It follows best practices to reduce logic overhead in your templates, provides pre-processed filter and sort options with labels and counts, and seamlessly integrates with Frontic UI components.

#### Parameters

<ParamField path="name" type="keyof Listings" required>
  The name of the listing to fetch.
</ParamField>

<ParamField path="params" type="ListingParameters[T] | Ref" required>
  Parameters to pass to the listing endpoint.
</ParamField>

<ParamField path="options" type="object">
  Configuration options.

  <Expandable title="Search Options">
    <ParamField path="disableSearch" type="boolean" default="false">
      When `true`, the `searchTerm` ref is ignored and no search parameter is sent
      to the API. Use this when your listing doesn't support text search.
    </ParamField>

    <ParamField path="searchTermThreshold" type="number" default="2">
      Minimum number of characters required before a search request is triggered.
      Prevents API calls for very short, likely meaningless queries.
    </ParamField>

    <ParamField path="searchDebounce" type="number" default="200">
      Debounce delay in milliseconds for search input. Controls how long to wait
      after the user stops typing before triggering a search request. Increase for
      slower networks or to reduce API calls.
    </ParamField>
  </Expandable>

  <Expandable title="Filter Options">
    <ParamField path="disableFilter" type="boolean" default="false">
      When `true`, filter methods (`addFilter`, `removeFilter`, etc.) are disabled and `state.available.filter` remains empty. Use this when your UI doesn't need filtering controls.
    </ParamField>

    <ParamField path="orFilter" type="string[]">
      A list of filter field names that should use OR logic, allowing users to select multiple values (e.g., "red OR blue"). Fields not listed use AND logic by default.

      ```ts theme={"theme":"css-variables"}
      orFilter: ['properties.color', 'properties.size']
      ```
    </ParamField>

    <ParamField path="filter.select" type="string[]">
      A list of filter field keys to control which filters appear in `state.available.filter`. Combined with `mode` to include or exclude specific filters.

      ```ts theme={"theme":"css-variables"}
      filter: {
        select: ['properties.color', 'properties.size', 'properties.brand']
      }
      ```
    </ParamField>

    <ParamField path="filter.mode" type="'include' | 'exclude'" default="include">
      Controls how `select` is applied. With `'include'`, only the specified fields are shown. With `'exclude'`, all filters except the specified fields are shown.
    </ParamField>

    <ParamField path="filter.label" type="Record<string, string>">
      Human-readable labels for filter fields. These appear in `state.available.filter` for building filter UI headings.

      ```ts theme={"theme":"css-variables"}
      filter: {
        label: {
          'properties.color': 'Color',
          'properties.size': 'Size',
          'properties.brand': 'Brand'
        }
      }
      ```
    </ParamField>

    <ParamField path="filter.sort" type="string[]">
      Define the display order for filters. Keys listed here appear first in `state.available.filter` in the specified order. Any filters not in this array appear after in their original order.

      ```ts theme={"theme":"css-variables"}
      filter: {
        sort: ['properties.color', 'properties.size', 'properties.brand']
      }
      ```
    </ParamField>
  </Expandable>

  <Expandable title="Sort Options">
    <ParamField path="disableSorting" type="boolean" default="false">
      When `true`, sorting methods (`sortResult`, `resetSorting`) are disabled and `state.available.sorting` remains empty. Use this when your UI doesn't need sorting controls.
    </ParamField>

    <ParamField path="sorting.select" type="string[]">
      A list of sort options to control which sorts appear in `state.available.sorting`. Use the format `'field:order'` (e.g., `'name:asc'`). Use `'default'` for the backend's default sorting.

      ```ts theme={"theme":"css-variables"}
      sorting: {
        select: ['default', 'name:asc', 'name:desc', 'price.amount:asc']
      }
      ```
    </ParamField>

    <ParamField path="sorting.mode" type="'include' | 'exclude'" default="include">
      Controls how `select` is applied. With `'include'`, only the specified sort options are shown. With `'exclude'`, all sorts except the specified options are shown.
    </ParamField>

    <ParamField path="sorting.label" type="Record<string, string>">
      Human-readable labels for sort options. These appear in `state.available.sorting` for building sort dropdowns. Use `'default'` key for the backend's default sorting.

      ```ts theme={"theme":"css-variables"}
      sorting: {
        label: {
          'default': 'Relevance',
          'name:asc': 'Name A-Z',
          'name:desc': 'Name Z-A',
          'price.amount:asc': 'Price: Low to High',
          'price.amount:desc': 'Price: High to Low'
        }
      }
      ```
    </ParamField>

    <ParamField path="sorting.sort" type="string[]">
      Define the display order for sort options. Keys listed here appear first in `state.available.sorting` in the specified order. Any sorts not in this array appear after in their original order.

      ```ts theme={"theme":"css-variables"}
      sorting: {
        sort: ['default', 'price.amount:asc', 'price.amount:desc', 'name:asc']
      }
      ```
    </ParamField>
  </Expandable>

  <Expandable title="Pagination Options">
    <ParamField path="limit" type="number">
      Items per page (sent as `limit` to the Frontic API). When omitted, the backend default is used.
    </ParamField>

    <ParamField path="infinite" type="boolean" default="false">
      When `true`, `loadNext()` / `loadPrev()` append/prepend items from adjacent pages (infinite-scroll mode). When `false` (default), the same calls replace the current page.
    </ParamField>

    <ParamField path="initialPage" type="number" default="1">
      Starting page number. Useful in infinite-scroll mode when deep-linking to a specific page from the URL.
    </ParamField>
  </Expandable>

  <Expandable title="Request Options">
    <ParamField path="contextKey" type="string | Ref">
      Context token for locale/region.
    </ParamField>

    <ParamField path="contextDomain" type="string | Ref">
      Context domain to override the global setting for this request.
    </ParamField>

    <ParamField path="requestUrl" type="string | Ref">
      Request URL for analytics/tracking purposes.
    </ParamField>

    <ParamField path="staleTime" type="number" default="300000">
      Cache duration in milliseconds.
    </ParamField>

    <ParamField path="cacheKey" type="string">
      Custom cache key for state sharing across components.
    </ParamField>
  </Expandable>
</ParamField>

#### Returns

##### State & Cache Control

<ResponseField name="result" type="Ref<Responses[T] | undefined>">
  The full listing response from the API, containing items, pagination metadata, and filter facets.

  ```ts theme={"theme":"css-variables"}
  // Access pagination info
  const hasMore = result.value?.page?.next !== undefined
  // Access filter facets
  result.value?.filter
  // Iterate over items
  result.value?.items.forEach(item => console.log(item.name))
  ```
</ResponseField>

<ResponseField name="status" type="Ref<'pending' | 'error' | 'success'>">
  Current query status. Use this to show loading spinners or error states in your UI.

  ```ts theme={"theme":"css-variables"}
  <div v-if="status === 'pending'">Loading...</div>
  <div v-else-if="status === 'error'">Something went wrong</div>
  <div v-else>{{ result?.length }} products found</div>
  ```
</ResponseField>

<ResponseField name="state" type="Ref<SearchState<TFilters, TSorts>>">
  Pre-processed search state ready for building your UI. Fully typed based on your listing's filter and sort schema - with IDE autocomplete for filter keys, sort fields, and more. Contains everything needed to render filter sidebars, sort dropdowns, and pagination controls.

  <Expandable title="SearchState Properties">
    <ResponseField name="active" type="object">
      Currently active query parameters.

      <Expandable title="Properties">
        <ResponseField name="filter" type="Partial<Record<keyof TFilters, unknown>>">
          Active filter values by field. Keys are typed based on your listing's filter schema -get full autocomplete.

          ```ts theme={"theme":"css-variables"}
          // Keys are typed - autocomplete available!
          state.value.active.filter['properties.color']  // ✅ autocomplete
          state.value.active.filter['invalid.field']     // ❌ type error
          ```
        </ResponseField>

        <ResponseField name="sorting" type="{ field: keyof TSorts; order: 'asc' | 'desc' } | undefined">
          Currently applied sort (typed based on listing schema), or `undefined` when using default sorting.
        </ResponseField>

        <ResponseField name="search" type="string">
          Current search term (empty string when not searching).
        </ResponseField>

        <ResponseField name="page" type="number">
          Current page number (1-indexed).
        </ResponseField>

        <ResponseField name="count" type="object">
          Counts for active state.

          <Expandable title="Properties">
            <ResponseField name="filter" type="number">
              Number of filter fields with active selections. Use this to show a badge on your "Filters" button.

              ```ts theme={"theme":"css-variables"}
              <button>Filters <span v-if="state.active.count.filter">({{ state.active.count.filter }})</span></button>
              ```
            </ResponseField>

            <ResponseField name="sorting" type="number">
              Number of active sorts (0 or 1).
            </ResponseField>
          </Expandable>
        </ResponseField>
      </Expandable>
    </ResponseField>

    <ResponseField name="available" type="object">
      Available options for UI controls.

      <Expandable title="Properties">
        <ResponseField name="filter" type="UiFilter<TFilters>[]">
          Pre-processed filter options ready for rendering. Each filter's `key` is typed based on your listing schema.

          ```ts theme={"theme":"css-variables"}
          interface UiFilter<TFilters> {
            key: keyof TFilters  // Typed! e.g., 'properties.color'
            label: string        // e.g., 'Color' (from your config)
            options: Array<{
              option: string   // Display name, e.g., 'Red'
              value: string    // API value, e.g., 'red'
              count: number    // Number of matching items
              selected: boolean
              disabled: boolean
            }>
          }
          ```
        </ResponseField>

        <ResponseField name="sorting" type="UiSort<TSorts>[]">
          Pre-processed sort options ready for rendering dropdowns. Keys are typed based on your listing schema.

          ```ts theme={"theme":"css-variables"}
          interface UiSort<TSorts> {
            key: keyof TSorts | 'default'  // Typed!
            label: string  // e.g., 'Name A-Z' (from your config)
            value: keyof TSorts | 'default'
          }
          ```
        </ResponseField>

        <ResponseField name="nextPage" type="boolean">
          Whether a next page is available. Use for pagination controls.
        </ResponseField>

        <ResponseField name="prevPage" type="boolean">
          Whether a previous page is available. Use for pagination controls.
        </ResponseField>

        <ResponseField name="count" type="object">
          Counts for available options.

          <Expandable title="Properties">
            <ResponseField name="filter" type="number">
              Number of filter fields available.
            </ResponseField>

            <ResponseField name="sorting" type="number">
              Number of sort options available.
            </ResponseField>

            <ResponseField name="result" type="number">
              Total number of items matching the current query.

              ```ts theme={"theme":"css-variables"}
              <p>{{ state.available.count.result }} products found</p>
              ```
            </ResponseField>

            <ResponseField name="page" type="number">
              Total number of pages. Use this to display "Page X of Y".

              ```ts theme={"theme":"css-variables"}
              <p>Page {{ state.active.page }} of {{ state.available.count.page }}</p>
              ```
            </ResponseField>
          </Expandable>
        </ResponseField>
      </Expandable>
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="searchTerm" type="Ref<string>">
  Two-way bindable search term. Connect this directly to your search input -
  typing automatically triggers debounced API requests when the term exceeds
  `searchTermThreshold`.
</ResponseField>

<ResponseField name="refresh" type="() => Promise<void>">
  Re-fetch data, using cache if still valid within `staleTime`. Use this when
  you want to ensure fresh data but don't need to bypass the cache.
</ResponseField>

<ResponseField name="refetch" type="() => Promise<void>">
  Force a fresh fetch, completely bypassing the cache. Use this when you know
  data has changed and need guaranteed fresh results.
</ResponseField>

<ResponseField name="reset" type="() => Promise<void>">
  Reset all state (search term, filters, and sorting) to initial values and refresh results. Equivalent to calling `resetSearch()`, `resetFilter()`, and `resetSorting()` together. Use this for a "Clear All" button.

  ```ts theme={"theme":"css-variables"}
  <button @click="reset">Clear All Filters & Search</button>
  ```
</ResponseField>

##### Filter Actions

<ResponseField name="addFilter" type="(field, value) => Promise<void>">
  Add a single filter value while keeping existing selections. Use this for checkbox-style filters where users can select multiple options.

  ```ts theme={"theme":"css-variables"}
  // User clicks "Red" checkbox
  await addFilter('properties.color', 'red')

  // User also selects "Blue" - both are now active
  await addFilter('properties.color', 'blue')

  ```
</ResponseField>

<ResponseField name="removeFilter" type="(field, value?) => Promise<void>">
  Remove a specific filter value, or clear all values for a field if no value is provided. Use this when users uncheck options or click "clear" on a filter group.

  ```ts theme={"theme":"css-variables"}
  // User unchecks "Red"
  await removeFilter('properties.color', 'red')

  // User clicks "Clear all colors"
  await removeFilter('properties.color')
  ```
</ResponseField>

<ResponseField name="filterResult" type="(field, values) => Promise<void>">
  Replace all values for a filter field at once. Use this for single-select filters or when setting multiple values programmatically.

  ```ts theme={"theme":"css-variables"}
  // Single-select dropdown changed to "Large"
  await filterResult('properties.size', ['large'])

  // Set multiple values at once
  await filterResult('properties.color', ['red', 'blue', 'green'])

  ```
</ResponseField>

<ResponseField name="resetFilter" type="(field?) => Promise<void>">
  Clear filters for a specific field, or all filters if no field is provided. Use this for "Reset" buttons.

  ```ts theme={"theme":"css-variables"}
  // Clear just color filters
  await resetFilter('properties.color')

  // Clear ALL filters
  await resetFilter()
  ```
</ResponseField>

##### Sort Actions

<ResponseField name="sortResult" type="(sortBy?: string) => Promise<void>">
  Apply a sort order using the `'field:order'` format. Call without arguments to reset to default sorting.

  ```ts theme={"theme":"css-variables"}
  // Sort by name ascending
  await sortResult('name:asc')

  // Sort by price descending
  await sortResult('price.amount:desc')

  // Reset to default (backend's default order)
  await sortResult()

  ```
</ResponseField>

<ResponseField name="resetSorting" type="() => Promise<void>">
  Reset to the backend's default sort order. Equivalent to calling `sortResult()` without arguments.

  ```ts theme={"theme":"css-variables"}
  await resetSorting()
  ```
</ResponseField>

##### Pagination Actions

<ResponseField name="loadNext" type="() => Promise<void>">
  Load the next page. In **standard mode** (the default), the current page is replaced with the next one. In **infinite mode** (`infinite: true`), items from the next page are appended to the existing list — use this for "Load More" buttons or infinite scroll.

  ```ts theme={"theme":"css-variables"}
  <button v-if="result?.page?.next" @click="loadNext">
    Load More Products
  </button>
  ```
</ResponseField>

<ResponseField name="loadPrev" type="() => Promise<void>">
  Load the previous page. In **standard mode**, the current page is replaced with the previous one. In **infinite mode**, items from the previous page are prepended to the existing list.
</ResponseField>

<ResponseField name="loadPage" type="(page: number) => Promise<void>">
  Jump to a specific page number. In **standard mode**, the current page is replaced with the requested page. In **infinite mode**, all loaded pages are reset and the requested page becomes the new starting point.
</ResponseField>

##### Search Actions

<ResponseField name="resetSearch" type="() => Promise<void>">
  Clear the search term and refresh results. Use this for a "clear search" button.

  ```ts theme={"theme":"css-variables"}
  <button v-if="searchTerm" @click="resetSearch">✕</button>
  ```
</ResponseField>

#### Example

```ts theme={"theme":"css-variables"}
<script setup lang="ts">
const { result, state, searchTerm, addFilter, removeFilter, sortResult, loadNext, reset } = useFronticSearch(
  'CategoryProducts',
  { key: 'shoes' },
  {
    orFilter: ['properties.color', 'properties.size'],
    filter: {
      select: ['properties.color', 'properties.size', 'properties.brand'],
      label: { 'properties.color': 'Color', 'properties.size': 'Size' },
      sort: ['properties.color', 'properties.size'], // Display order for filters
    },
    sorting: {
      label: {
        default: 'Relevance',
        'name:asc': 'Name A-Z',
        'price.amount:asc': 'Price: Low to High',
      },
      sort: ['default', 'price.amount:asc', 'name:asc'], // Display order for sorts
    },
  }
)
</script>

<template>
  <input v-model="searchTerm" placeholder="Search..." />
  <button v-if="state.active.count.filter > 0" @click="reset">Clear All</button>
  <div v-for="item in result?.items" :key="item.key">{{ item.name }}</div>
  <button v-if="result?.page?.next" @click="loadNext">Load More</button>
</template>
```

<Accordion title="Full Example with Filters">
  ```ts theme={"theme":"css-variables"}
  <script setup lang="ts">
  const { result, state, searchTerm, addFilter, removeFilter, sortResult, loadNext } = useFronticSearch(
    'CategoryProducts',
    { key: 'shoes' },
    {
      orFilter: ['properties.color', 'properties.size'],
      filter: {
        select: ['properties.color', 'properties.size', 'properties.brand'],
        label: {
          'properties.color': 'Color',
          'properties.size': 'Size',
          'properties.brand': 'Brand',
        },
      },
      sorting: {
        label: {
          default: 'Relevance',
          'name:asc': 'Name A-Z',
          'name:desc': 'Name Z-A',
          'price.amount:asc': 'Price: Low to High',
          'price.amount:desc': 'Price: High to Low',
        },
      },
    }
  )

  function toggleFilter(field: string, value: string, isSelected: boolean) {
    if (isSelected) {
      removeFilter(field, value)
    } else {
      addFilter(field, value)
    }
  }
  </script>

  <template>
    <div class="search-page">
      <!-- Search Input -->
      <input v-model="searchTerm" type="search" placeholder="Search products..." />

      <!-- Sort Dropdown -->
      <select @change="sortResult(($event.target as HTMLSelectElement).value)">
        <option v-for="sort in state.available.sorting" :key="sort.key" :value="sort.key" :selected="state.active.sorting?.field === sort.key.split(':')[0]">
          {{ sort.label }}
        </option>
      </select>

      <!-- Filter Sidebar -->
      <aside>
        <div v-for="filter in state.available.filter" :key="filter.key">
          <h4>{{ filter.label }}</h4>
          <label v-for="option in filter.options" :key="option.value">
            <input type="checkbox" :checked="option.selected" @change="toggleFilter(filter.key, option.value, option.selected)" />
            {{ option.label }} ({{ option.count }})
          </label>
        </div>
      </aside>

      <!-- Results -->
      <main>
        <p>{{ state.available.count.result }} products found</p>
        <div class="product-grid">
          <article v-for="item in result?.items" :key="item.key">
            <h3>{{ item.name }}</h3>
            <p>{{ item.price.formatted }}</p>
          </article>
        </div>
        <button v-if="result?.page?.next" @click="loadNext">Load More</button>
      </main>
    </div>
  </template>
  ```
</Accordion>

<Accordion title="Shared State with Wrapper Composable">
  <CodeGroup>
    ```ts composables/useMySearch.ts theme={"theme":"css-variables"}
    /**
     * Wrapper composable for product search.
     *
     * By using a fixed cacheKey, all components calling this composable
     * share the same search state automatically via Nuxt's useState.
     */
    export function useMySearch() {
      return useFronticSearch(
        "CategoryProducts",
        { key: "shoes" },
        {
          // This cacheKey enables state sharing across components!
          cacheKey: "product-search",

          // Search configuration
          orFilter: ["properties.color", "properties.size"],
          filter: {
            select: ["properties.color", "properties.size", "properties.brand"],
            label: {
              "properties.color": "Color",
              "properties.size": "Size",
              "properties.brand": "Brand",
            },
          },
          sorting: {
            label: {
              default: "Relevance",
              "name:asc": "Name A-Z",
              "price.amount:asc": "Price: Low to High",
            },
          },
        },
      );
    }
    ```

    ```vue pages/index.vue theme={"theme":"css-variables"}
    <script setup lang="ts">
    // Both this page and the Filters component share the same search state
    const { result, status, searchTerm, state, loadNext } = useMySearch();
    </script>

    <template>
      <div class="search-page">
        <!-- Search input - shared with Filters component -->
        <input v-model="searchTerm" placeholder="Search products..." />

        <!-- Filter sidebar - uses same state -->
        <Filters />

        <!-- Results count reflects active filters from Filters component -->
        <p>{{ state.available.count.result }} products found</p>

        <!-- Product grid -->
        <div v-if="status === 'pending'">Loading...</div>
        <div v-else class="product-grid">
          <article v-for="item in result?.items" :key="item.key">
            <h3>{{ item.name }}</h3>
            <p>{{ item.price.formatted }}</p>
          </article>
        </div>

        <button v-if="state.available.nextPage" @click="loadNext">Load More</button>
      </div>
    </template>
    ```

    ```vue components/Filters.vue theme={"theme":"css-variables"}
    <script setup lang="ts">
    // Same composable = same state! Changes here update index.vue automatically
    const { state, addFilter, removeFilter, resetFilter } = useMySearch();

    function toggleFilter(field: string, value: string, isSelected: boolean) {
      if (isSelected) {
        removeFilter(field, value);
      } else {
        addFilter(field, value);
      }
    }
    </script>

    <template>
      <aside class="filters">
        <div class="filter-header">
          <h3>Filters</h3>
          <!-- Active filter count updates when filters change -->
          <button v-if="state.active.count.filter > 0" @click="resetFilter()">
            Clear all ({{ state.active.count.filter }})
          </button>
        </div>

        <!-- Filter groups from shared state -->
        <div
          v-for="filter in state.available.filter"
          :key="filter.key"
          class="filter-group"
        >
          <h4>{{ filter.label }}</h4>
          <label v-for="option in filter.options" :key="option.value">
            <input
              type="checkbox"
              :checked="option.selected"
              :disabled="option.disabled"
              @change="toggleFilter(filter.key, option.value, option.selected)"
            />
            {{ option.option }} ({{ option.count }})
          </label>
        </div>
      </aside>
    </template>
    ```
  </CodeGroup>

  <Info>
    **Why use a wrapper composable?**

    1. **Centralized configuration** - All your filter options, sort labels, and search settings live in one file. Need to add a new filter? Update it once.

    2. **Shared state** - The `cacheKey` option enables automatic state sharing. When multiple components call the same wrapper, they share the reactive `searchTerm`, filter/sort state, `result` data, and pagination.

    3. **Clean components** - Your page and filter components stay focused on rendering, not configuration.

    Any action (like `addFilter`) called from one component automatically updates all other components using the same `cacheKey`.
  </Info>
</Accordion>

<Accordion title="Type Signature">
  ```ts theme={"theme":"css-variables"}
  import type { Ref, MaybeRef } from "vue";
  import type {
    Listings,
    ListingParameters,
    Responses,
  } from "@frontic/stack/generated-types";

  // Typed filter for UI rendering
  interface UiFilter<TFilters = Record<string, unknown>> {
    key: keyof TFilters & string;
    label: string;
    options: FilterOption[];
  }

  // Typed sort for UI rendering
  interface UiSort<TSorts = Record<string, unknown>> {
    key: (keyof TSorts & string) | "default";
    label: string;
    value: (keyof TSorts & string) | "default";
  }

  // Fully typed search state
  interface SearchState<
    TFilters = Record<string, unknown>,
    TSorts = Record<string, unknown>,
  > {
    active: {
      filter: Partial<Record<keyof TFilters & string, unknown>>;
      sorting:
        | { field: keyof TSorts & string; order: "asc" | "desc" }
        | undefined;
      search: string;
      page: number;
      count: {
        filter: number;
        sorting: number;
      };
    };
    available: {
      filter: UiFilter<TFilters>[];
      sorting: UiSort<TSorts>[];
      nextPage: boolean;
      prevPage: boolean;
      count: {
        filter: number;
        sorting: number;
        result: number;
        page: number;
      };
    };
  }

  function useFronticSearch<
    TSearch extends keyof Listings,
    TFilters extends Record<string, unknown>,
    TSorts extends Record<string, unknown>,
  >(
    name: TSearch,
    params: MaybeRef<ListingParameters[TSearch]>,
    options?: {
      contextKey?: MaybeRef<string | undefined>;
      contextDomain?: MaybeRef<string | undefined>;
      requestUrl?: MaybeRef<string | undefined>;
      staleTime?: number;
      cacheKey?: string;
      searchTermThreshold?: number;
      searchDebounce?: number;
      orFilter?: Array<keyof TFilters & string>;
      disableSearch?: boolean;
      disableFilter?: boolean;
      disableSorting?: boolean;
      limit?: number;
      infinite?: boolean;
      initialPage?: number;
      filter?: {
        select?: Array<keyof TFilters & string>;
        mode?: "include" | "exclude";
        label?: Partial<Record<keyof TFilters & string, string>>;
        sort?: Array<keyof TFilters & string>; // Display order for filters
      };
      sorting?: {
        select?: Array<keyof TSorts & string>;
        mode?: "include" | "exclude";
        label?: Record<string, string>;
        sort?: Array<keyof TSorts & string>; // Display order for sorts
      };
    },
  ): {
    result: Ref<Responses[TSearch] | undefined>; // Full listing response
    status: Ref<"pending" | "error" | "success">;
    state: Ref<SearchState<TFilters, TSorts>>; // Fully typed state
    searchTerm: Ref<string>;
    loadNext: () => Promise<void>;
    loadPrev: () => Promise<void>;
    loadPage: (page: number) => Promise<void>;
    refresh: () => Promise<void>;
    refetch: () => Promise<void>;
    sortResult: (sortBy?: string) => Promise<void>;
    resetSorting: () => Promise<void>;
    filterResult: <K extends keyof TFilters>(
      field: K,
      values: TFilters[K][],
    ) => Promise<void>;
    addFilter: <K extends keyof TFilters>(
      field: K,
      value: TFilters[K],
    ) => Promise<void>;
    removeFilter: <K extends keyof TFilters>(
      field: K,
      value?: TFilters[K],
    ) => Promise<void>;
    resetFilter: (field?: keyof TFilters & string) => Promise<void>;
    resetSearch: () => Promise<void>;
    reset: () => Promise<void>; // Reset all state (search, filters, sorting)
  };
  ```
</Accordion>

***

### `useFronticTree`

Fetch a [Menu Tree](/api-builder/trees) — a hierarchical collection of records assembled from a Data Storage and rendered through a Detail Block — with automatic caching and SSR support. See [Caching](#caching) for details.

#### Parameters

<ParamField path="name" type="keyof Trees" required>
  The name of the tree to fetch. Provides full autocomplete for your generated tree types.
</ParamField>

<ParamField path="options" type="object">
  Configuration options.

  <Expandable title="Tree Options">
    <ParamField path="key" type="string | Ref<string | undefined>">
      Optional starting node key. When provided, the response contains the subtree rooted at this node. When omitted, all root-level nodes are returned.
    </ParamField>

    <ParamField path="depth" type="number | Ref<number | undefined>">
      Optional level limit. Controls how many levels of `$items` are included. When omitted, all levels are returned (subject to the API's 1,000-node response cap).
    </ParamField>
  </Expandable>

  <Expandable title="Request Options">
    <ParamField path="contextKey" type="string | Ref<string>">
      Context token for locale/region selection.
    </ParamField>

    <ParamField path="contextDomain" type="string | Ref<string>">
      Context domain to override the global setting for this request.
    </ParamField>

    <ParamField path="requestUrl" type="string | Ref<string>">
      Request URL for analytics/tracking purposes.
    </ParamField>

    <ParamField path="staleTime" type="number" default="300000">
      Cache duration in milliseconds. Default: 5 minutes.
    </ParamField>
  </Expandable>
</ParamField>

#### Returns

<ResponseField name="tree" type="Ref<Responses[T] | undefined>">
  The full tree response (`{ items?: [...] }`), fully typed based on the tree name.
</ResponseField>

<ResponseField name="items" type="ComputedRef<TreeItems<Responses[T]> | undefined>">
  Shortcut for `tree.value?.items`. Fully typed based on the tree name — when your generated client is in place, `items` resolves to the concrete node array, so iterating gives you autocomplete on `$items`, `key`, and the block fields defined by the tree's Detail Block (e.g. `name`, `link`).
</ResponseField>

<ResponseField name="status" type="Ref<'pending' | 'error' | 'success'>">
  Current query status for loading states.
</ResponseField>

<ResponseField name="refresh" type="() => Promise<void>">
  Refresh data using cache if still valid.
</ResponseField>

<ResponseField name="refetch" type="() => Promise<void>">
  Force a fresh fetch, bypassing cache entirely.
</ResponseField>

#### Example

```ts theme={"theme":"css-variables"}
<script setup lang="ts">
const { items, status } = useFronticTree('CategoryNavigation', { depth: 2 })
</script>

<template>
  <nav v-if="status === 'success'">
    <ul>
      <li v-for="node in items" :key="node.key">
        <NuxtLink :to="node.link?.path">{{ node.name }}</NuxtLink>
        <ul v-if="node.$items.length">
          <li v-for="child in node.$items" :key="child.key">
            <NuxtLink :to="child.link?.path">{{ child.name }}</NuxtLink>
          </li>
        </ul>
      </li>
    </ul>
  </nav>
</template>
```

<Accordion title="More Examples">
  ```ts theme={"theme":"css-variables"}
  // Subtree rooted at a specific node
  const { items } = useFronticTree("CategoryNavigation", { key: "shop" });

  // Reactive starting key — refetches whenever the user expands a different branch
  const expandedKey = ref<string | undefined>(undefined);
  const { items } = useFronticTree("CategoryNavigation", {
    key: expandedKey,
    depth: 2,
  });

  // With custom cache duration
  const { tree } = useFronticTree("CategoryNavigation", {
    staleTime: 1000 * 60 * 30, // 30 minutes
  });
  ```
</Accordion>

<Accordion title="Type Signature">
  ```ts theme={"theme":"css-variables"}
  import type { Ref, ComputedRef, MaybeRef } from "vue";
  import type { Trees, Responses } from "@frontic/stack/generated-types";

  // Internal helper — extracts the typed `items` array from a tree response
  type TreeItems<TResponse> = [TResponse] extends [{ items?: infer I }]
    ? (unknown extends I ? unknown[] : Exclude<I, undefined>)
    : unknown[];

  function useFronticTree<T extends keyof Trees>(
    name: T,
    options?: {
      key?: MaybeRef<string | undefined>;
      depth?: MaybeRef<number | undefined>;
      contextKey?: MaybeRef<string | undefined>;
      contextDomain?: MaybeRef<string | undefined>;
      requestUrl?: MaybeRef<string | undefined>;
      staleTime?: number;
    },
  ): {
    tree: Ref<Responses[T] | undefined>;
    items: ComputedRef<TreeItems<Responses[T]> | undefined>;
    status: Ref<"pending" | "error" | "success">;
    refresh: () => Promise<void>;
    refetch: () => Promise<void>;
  };
  ```
</Accordion>

<Info>
  The cache key includes `key`, `depth`, and `contextKey`, so different
  subtrees, depth caps, and contexts each cache independently — calling
  `useFronticTree('CategoryNavigation')` and
  `useFronticTree('CategoryNavigation', { key: 'shop' })` from different
  components will not collide.
</Info>

***

### `useFronticPage`

Dynamic page routing with automatic slug detection, redirect handling, and 404 errors. Includes automatic caching and SSR support. See [Caching](#caching) for details.

#### Parameters

<ParamField path="slug" type="string | Ref<string>">
  The page slug. If omitted, auto-detected from current URL.
</ParamField>

<ParamField path="options" type="object">
  Configuration options.

  <Expandable title="Properties">
    <ParamField path="redirectOn301" type="boolean" default="true">
      Automatically redirect on 301 responses. Uses module config default.
    </ParamField>

    <ParamField path="throwOn404" type="boolean" default="true">
      Throw a 404 error when page is not found. Uses module config default.
    </ParamField>

    <ParamField path="contextKey" type="string | Ref<string>">
      Context token for locale/region selection.
    </ParamField>

    <ParamField path="contextDomain" type="string | Ref<string>">
      Context domain to override the global setting for this request.
    </ParamField>

    <ParamField path="requestUrl" type="string | Ref<string>">
      Request URL for analytics/tracking purposes.
    </ParamField>

    <ParamField path="staleTime" type="number" default="300000">
      Cache duration in milliseconds.
    </ParamField>
  </Expandable>
</ParamField>

#### Returns

<ResponseField name="page" type="Ref<Page | undefined>">
  The full page response object.
</ResponseField>

<ResponseField name="data" type="ComputedRef<Page['data'] | undefined>">
  The page data payload for rendering.
</ResponseField>

<ResponseField name="type" type="ComputedRef<string | undefined>">
  The page type for conditional rendering (`'ProductCategory'`,
  `'ProductDetail'`, etc.).
</ResponseField>

<ResponseField name="block" type="ComputedRef<string | undefined>">
  The block name to render for this page.
</ResponseField>

<ResponseField name="route" type="ComputedRef<PageRoute | undefined>">
  Route information including redirect and context data.

  <Expandable title="PageRoute Properties">
    <ResponseField name="code" type="number">
      HTTP status code (200, 301, 404).
    </ResponseField>

    <ResponseField name="redirect" type="{ path: string }">
      Redirect destination with target path.
    </ResponseField>

    <ResponseField name="context" type="{ suggested?: string }">
      Suggested context for locale switching.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="alternates" type="ComputedRef<AlternateRoute[] | undefined>">
  Alternate language URLs for SEO hreflang tags.
</ResponseField>

<ResponseField name="suggested" type="ComputedRef<AlternateRoute | undefined>">
  Suggested route when context changes (e.g., for locale switching).
</ResponseField>

<ResponseField name="status" type="Ref<'pending' | 'error' | 'success'>">
  Current query status.
</ResponseField>

<ResponseField name="refresh" type="() => Promise<void>">
  Refresh using cache if valid.
</ResponseField>

<ResponseField name="refetch" type="() => Promise<void>">
  Force fresh fetch.
</ResponseField>

#### Example

```ts pages/[...slug].vue theme={"theme":"css-variables"}
<script setup lang="ts">
const { data, block, alternates, status } = useFronticPage()

useHead({
  link:
    alternates.value?.map((alt) => ({
      rel: 'alternate',
      hreflang: alt.locale,
      href: alt.href,
    })) ?? [],
})
</script>

<template>
  <div v-if="status === 'pending'">Loading...</div>
  <component v-else :is="resolveComponent(block)" :data="data" />
</template>
```

<Accordion title="More Examples">
  ```ts theme={"theme":"css-variables"}
  // With explicit slug
  const { page } = useFronticPage("demo-shop.com/uk/products/shoes");

  // Disable redirect for manual handling
  const { page, route } = useFronticPage(undefined, { redirectOn301: false });

  if (route.value?.redirect) {
    navigateTo(route.value.redirect.path);
  }

  // Disable 404 throwing for custom error handling
  const { page, route } = useFronticPage(undefined, { throwOn404: false });

  if (route.value?.code === 404) {
    // Custom 404 handling
  }
  ```
</Accordion>

<Accordion title="Type Signature">
  ```ts theme={"theme":"css-variables"}
  import type { Ref, ComputedRef, MaybeRef } from "vue";

  interface Page {
    id?: string;
    slug?: string;
    title?: string;
    meta?: Record<string, unknown>;
    data?: Record<string, unknown>;
    route?: PageRoute;
  }

  interface PageRoute {
    code?: number;
    redirect?: { path: string };
    alternates?: AlternateRoute[];
    context?: { suggested?: string };
  }

  interface AlternateRoute {
    locale?: string;
    lang?: string;
    href: string;
  }

  function useFronticPage(
    slug?: MaybeRef<string>,
    options?: {
      redirectOn301?: boolean;
      throwOn404?: boolean;
      contextKey?: MaybeRef<string | undefined>;
      contextDomain?: MaybeRef<string | undefined>;
      requestUrl?: MaybeRef<string | undefined>;
      staleTime?: number;
    },
  ): {
    page: Ref<Page | undefined>;
    data: ComputedRef<Page["data"] | undefined>;
    type: ComputedRef<string | undefined>;
    block: ComputedRef<string | undefined>;
    route: ComputedRef<PageRoute | undefined>;
    alternates: ComputedRef<AlternateRoute[] | undefined>;
    status: Ref<"pending" | "error" | "success">;
    refresh: () => Promise<void>;
    refetch: () => Promise<void>;
  };
  ```
</Accordion>

<Info>
  The composable automatically constructs the page slug from the current request
  URL (host + pathname). This works correctly in both SSR and client-side
  navigation.
</Info>

***

### `useFronticContext`

Manage locale and region switching with cookie persistence.

#### Parameters

<ParamField path="options" type="object">
  Configuration options.

  <Expandable title="Properties">
    <ParamField path="disableContext" type="boolean" default="false">
      Disable automatic context fetching and cookie management. When `true`, you
      must manually call `refresh()` to fetch contexts and manage the token
      yourself. Overrides the module-level `disableContext` setting.
    </ParamField>

    <ParamField path="cookieName" type="string" default="fs-context">
      Cookie name for persisting the context token.
    </ParamField>

    <ParamField path="cookieMaxAge" type="number" default="31536000">
      Cookie max age in seconds. Default: 1 year.
    </ParamField>
  </Expandable>
</ParamField>

#### Returns

<ResponseField name="contexts" type="Readonly<Ref<ContextOption[]>>">
  Available context options with regions and locales.

  <Expandable title="ContextOption Properties">
    <ResponseField name="region" type="string">
      Region code (e.g., `'uk'`, `'de'`).
    </ResponseField>

    <ResponseField name="currency" type="string">
      Currency code (e.g., `'GBP'`, `'EUR'`).
    </ResponseField>

    <ResponseField name="locales" type="Array<{ key: string; url: string }>">
      Available locales with their URLs.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="current" type="Readonly<Ref<Context | null>>">
  Current active context.

  <Expandable title="Context Properties">
    <ResponseField name="region" type="string">
      Current region (e.g., `'uk'`).
    </ResponseField>

    <ResponseField name="locale" type="string">
      Current locale (e.g., `'en-gb'`).
    </ResponseField>

    <ResponseField name="scope" type="string">
      Current scope (e.g., `'b2c'`).
    </ResponseField>

    <ResponseField name="token" type="string">
      Context identifier token.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="token" type="Readonly<Ref<string | null>>">
  The current context token.
</ResponseField>

<ResponseField name="update" type="(context: { region: string; locale: string }) => Promise<void>">
  Switch to a different region/locale combination.
</ResponseField>

<ResponseField name="isLoading" type="Readonly<Ref<boolean>>">
  Loading state during context operations.
</ResponseField>

<ResponseField name="refresh" type="() => Promise<void>">
  Manually refresh available contexts.
</ResponseField>

#### Example

```ts theme={"theme":"css-variables"}
<script setup lang="ts">
const { contexts, current, update, isLoading } = useFronticContext()

function getDefaultLocale(region: string) {
  return contexts.value.find((c) => c.region === region)?.locales[0]?.key ?? 'en'
}
</script>

<template>
  <select
    :value="current?.region"
    :disabled="isLoading"
    @change="
      update({
        region: $event.target.value,
        locale: getDefaultLocale($event.target.value),
      })
    "
  >
    <option v-for="ctx in contexts" :key="ctx.region" :value="ctx.region">{{ ctx.region.toUpperCase() }} ({{ ctx.currency }})</option>
  </select>
</template>
```

<Accordion title="Type Signature">
  ```ts theme={"theme":"css-variables"}
  import type { Ref, Readonly } from "vue";

  interface Context {
    region: string;
    locale: string;
    scope: string;
    token: string;
  }

  interface ContextOption {
    region: string;
    currency: string;
    locales: Array<{ key: string; url: string }>;
  }

  function useFronticContext(options?: {
    cookieName?: string;
    cookieMaxAge?: number;
    disableContext?: boolean;
  }): {
    contexts: Readonly<Ref<ContextOption[]>>;
    current: Readonly<Ref<Context | null>>;
    token: Readonly<Ref<string | null>>;
    update: (context: { region: string; locale: string }) => Promise<void>;
    isLoading: Readonly<Ref<boolean>>;
    refresh: () => Promise<void>;
  };
  ```
</Accordion>

***

### `useFronticClient`

Low-level client for direct API access. All other composables use this internally.

#### Parameters

<ParamField path="options" type="object">
  Configuration options for client behavior.

  <Expandable title="Properties">
    <ParamField path="proxy" type="boolean | string">
      Override proxy behavior: `true` (force proxy), `false` (direct API), or
      custom path.
    </ParamField>

    <ParamField path="contextDomain" type="false | string">
      Override context domain: `false` (disable) or specific domain.
    </ParamField>

    <ParamField path="requestUrl" type="false | string">
      Override request URL tracking: `false` (disable) or specific URL.
    </ParamField>

    <ParamField path="secret" type="string">
      API secret for server-side requests.
    </ParamField>
  </Expandable>
</ParamField>

#### Returns

Returns a `FronticClient` instance with type-safe methods:

<ResponseField name="block" type="<T>(name, key, config?) => Promise<Responses[T]>">
  Fetch a block by name and key.
</ResponseField>

<ResponseField name="listing" type="<T>(name, params, config?) => Promise<Responses[T]>">
  Fetch a listing with parameters and query options.
</ResponseField>

<ResponseField name="tree" type="<T>(name, config?) => Promise<Responses[T]>">
  Fetch a menu tree by name. Pass `key` and/or `depth` under `config.query` to fetch a subtree or cap the depth.
</ResponseField>

<ResponseField name="page" type="(slug, config?) => Promise<Page>">
  Fetch a page by its slug.
</ResponseField>

<ResponseField name="context" type="(token, config?) => Promise<Context>">
  Get context by token.
</ResponseField>

<ResponseField name="contextList" type="(token?, config?) => Promise<[ContextOption[], string]>">
  Get available contexts.
</ResponseField>

<ResponseField name="contextUpdate" type="(context, token, config?) => Promise<Context>">
  Update context with new region/locale.
</ResponseField>

#### Example

```ts theme={"theme":"css-variables"}
const client = useFronticClient();

// Fetch a product block
const product = await client.block("ProductFull", "product-123");

// Fetch a listing with filters
const listing = await client.listing(
  "CategoryProducts",
  { key: "shoes" },
  {
    query: {
      filter: [{ type: "equals", field: "properties.color", value: "Red" }],
      sort: { field: "price.amount", order: "asc" },
      limit: 20,
    },
  },
);
```

<Accordion title="More Examples">
  ```ts theme={"theme":"css-variables"}
  // Fetch page data
  const page = await client.page("demo-shop.com/uk/products");

  // Client with custom configuration
  const directClient = useFronticClient({ proxy: false });
  const customProxyClient = useFronticClient({ proxy: "/api/custom-frontic" });
  ```
</Accordion>

<Accordion title="Type Signature">
  ```ts theme={"theme":"css-variables"}
  import type {
    Blocks,
    Listings,
    ListingParameters,
    Trees,
    TreeQuery,
    Responses,
  } from "@frontic/stack/generated-types";

  interface RequestOptions {
    requestUrl?: string;
    contextKey?: string;
    contextDomain?: string;
    proxyUrl?: string;
  }

  interface FronticClient {
    block: <T extends keyof Blocks>(
      name: T,
      key: string,
      config?: RequestOptions,
    ) => Promise<Responses[T]>;
    listing: <T extends keyof Listings>(
      name: T,
      parameters: ListingParameters[T],
      config?: {
        query?: {
          filter?: Filter[];
          sort?: Sort | Sort[];
          search?: string;
          limit?: number;
          page?: number;
        };
      } & RequestOptions,
    ) => Promise<Responses[T]>;
    tree: <T extends keyof Trees>(
      name: T,
      config?: { query?: TreeQuery } & RequestOptions,
    ) => Promise<Responses[T]>;
    page: (slug: string, config?: RequestOptions) => Promise<Page>;
    context: (token: string, config?: RequestOptions) => Promise<Context>;
    contextList: (
      token?: string,
      config?: RequestOptions,
    ) => Promise<[ContextOption[], string]>;
    contextUpdate: (
      context: { region: string; locale: string },
      token: string,
      config?: RequestOptions,
    ) => Promise<Context>;
  }

  function useFronticClient(options?: {
    proxy?: boolean | string;
    contextDomain?: false | string;
    requestUrl?: false | string;
    secret?: string;
  }): FronticClient;
  ```
</Accordion>

***

## Proxy

The module includes a built-in proxy to prevent CORS issues on client-side requests.

<Steps>
  <Step title="Browser Request">
    Client sends request to your server at `/api/frontic`
  </Step>

  <Step title="Server Forwards">
    Your Nuxt server forwards the request to the Fetch API
  </Step>

  <Step title="Response Returns">
    Response flows back through your server to the browser
  </Step>
</Steps>

<Info>
  Server-side requests (SSR) go directly to the Fetch API without using the
  proxy.
</Info>

***

## TypeScript

The module configures a path alias so you can import from your generated Frontic client:

```ts theme={"theme":"css-variables"}
import type { ProductCard, ProductFull } from "@frontic/stack/generated-types";
import { createClient } from "@frontic/stack/generated-client";
```

<Note>
  This maps `@frontic/stack/*` to `.frontic/*` in your project root, where the
  Frontic CLI generates your typed client.
</Note>

All composables are fully typed with generics that preserve the specific block/listing types through to the return values, providing **full IDE autocomplete** for response data.

***

## Caching

All composables use [Pinia Colada](https://pinia-colada.esm.dev/) for intelligent caching:

<CardGroup cols={3}>
  <Card title="Stale-While-Revalidate" icon="clock">
    Shows cached data immediately while fetching fresh data in the background
  </Card>

  <Card title="Automatic Deduplication" icon="copy">
    Multiple components requesting the same data share a single request
  </Card>

  <Card title="SSR Hydration" icon="server">
    Data fetched on server transfers to client without duplicate requests
  </Card>
</CardGroup>

### Configure Cache Duration

```ts theme={"theme":"css-variables"}
const { block } = useFronticBlock("ProductFull", id, {
  staleTime: 1000 * 60 * 10, // 10 minutes
});
```

### Manual Cache Control

All composables return two methods for cache control:

| Method      | Description                                                 |
| ----------- | ----------------------------------------------------------- |
| `refresh()` | Refresh data, using cache if still valid within `staleTime` |
| `refetch()` | Force a fresh fetch, completely bypassing the cache         |
