Features

Hooks

Extend frontend-core and orchestr behavior using Nuxt runtime hooks

Frontend-core and orchestr expose Nuxt runtime hooks that let you extend or modify the complete behaviour of your Laioutr Frontend. Register hooks inside a Nuxt plugin for client-side hooks, or a Nitro plugin for server-side hooks.

Hook mechanics

Every hook uses one of four mechanics, which decide when your handler runs and how it shapes the result:

  • Filter — runs after the default logic with result.value pre-seeded. Transform it, replace it, or leave it untouched. Chained across plugins: each handler receives the previous one's output.
  • Override — runs before the default with result.value empty. Set it to take over; leave it unset to fall back to the default.
  • Modify — mutates the payload object in place. There is no result slot.
  • Lifecycle — a before / success / error / finally sequence around an operation.

Each hook also has a dispatch, shown on its card. Synchronous handlers run inline and are not awaited — set values immediately, since a returned promise is ignored. Asynchronous handlers may await. Dispatch is a property of the individual hook, not of its mechanic.

A throwing handler does not fail the caller

Every result-returning hook guards each handler on its own. One that throws is skipped with a warning, the remaining handlers still run, and the value keeps threading, so the caller falls back at worst to what it seeded. This matters most inside a render, where SSR has no error boundary: before, a handler an app installed could fail the page. A handler that mutates result.value in place and then throws keeps that mutation.

Implementing a result-returning hook of your own

getHookResult and useHookResult are auto-imported, so a package outside Frontend Core can offer this same pattern without restating the synchronous-caller trick that makes a handler's result.value readable on the next line:

app/composables/useBadgeLabel.ts
export const useBadgeLabel = (product: Product) =>
  useHookResult('acme:badge:label', { product }, defaultLabel(product));

Pass a seed and the hook behaves as a filter, each handler receiving the previous one's output. Omit it and it behaves as an override, with result.value starting undefined until a handler sets it. useHookResult reads the Nuxt app for you; getHookResult takes one explicitly, for code that already holds it.

Frontend Core Hooks

These hooks run on the client. Register them in a Nuxt plugin with nuxtApp.hook().

Three hooks let you customize how linkResolver resolves links, switches locale paths, and switches market URLs.

frontend-core:link-resolver:resolve
Transform, replace, or pass through a resolved link before it's used — for example, point product references at an external catalog.

When it fires: After every call to linkResolver.resolve(), once the default resolution has produced a value.

Runs onClientRegister inNuxt pluginDispatchSynchronous
link

The link being resolved.

result
{ value: string }

Mutate result.value to transform the output. It arrives pre-seeded with the resolved URL or path — transform it, replace it, or leave it untouched. Threads across plugins, so each handler receives the previous output.

Switch locale path

frontend-core:link-resolver:switch-locale-path
Take over how the current page's URL is rebuilt when switching to another language, including cases the default can't resolve.

When it fires: Before the default logic, when switching the current page to another language.

Runs onClientRegister inNuxt pluginDispatchSynchronous
targetLanguageId
string

The language being switched to.

result
{ value?: string }

Set result.value to take over locale switching, including cases the default cannot resolve. It starts empty; leave it unset to fall back to the default. The first handler to set a value wins.

Switch market URL

frontend-core:link-resolver:switch-market-url
Take over the URL used when switching to a different market, which may include a change of host.

When it fires: Before the default logic, when switching to a different market.

Runs onClientRegister inNuxt pluginDispatchSynchronous
targetMarketId
string

The market being switched to.

targetLanguageId
stringoptional

The target language

result
{ value?: string }

Set result.value to take over market switching, which may include a host change. It starts empty; leave it unset to fall back to the default.

frontend-core:link:intent
Warm what the next page will need. Frontend Core already fetches that page's render config on this signal, so opening the link can cost no request at all; use the hook to load your own work alongside it.market and language are resolved for the target, which a link may cross, so a link into another market reports where it lands rather than where the visitor is.Nothing reports the intent ending and a request already sent is never cancelled, so keep the handler cheap.

When it fires: When a visitor rests on or tabs to an in-app link, after a short delay a passing cursor does not survive. Once per link per intent.

Runs onClientRegister inNuxt pluginDispatchSynchronous
path
string

The resolved in-app path the visitor is heading to. Never an external URL.

page
MetaPage

The page matched for that path.

market
RenderMarket

The market resolved for the target path

language
RenderLanguage

The language resolved for the target path.

Page Renderer

Select page variant

frontend-core:page-renderer:select-page-variant
Choose which variant of a page is rendered — for A/B testing, personalization, or conditional layouts.

When it fires: When the PageRenderer component selects a variant.

Runs onClientRegister inNuxt pluginDispatchSynchronous
page
RenderPage

Contains id, type, path, and a variants array.

result
{ value?: RenderPageVariant }

Set result.value to a variant to render it instead of the default. It starts empty; leave it unset to keep the default.

Resolve page head

frontend-core:page-head:resolve
Read and rewrite the SEO and locale tags Frontend Core writes to the document head on every page.

When it fires: When the PageRenderer applies the page head via useHead, on every page.

Runs onClientRegister inNuxt pluginDispatchSynchronous
page
RenderPage

The page being rendered.

pageVariant
RenderPageVariant

The selected variant.

metaPage
MetaPage

The page's SEO meta.

currentDomain
stringoptional

The resolved market domain — undefined in Studio preview.

result
{ value: { seo, locale } }

Mutate result.value to change the head; it is pre-seeded with the computed head. seo is a flat useSeoMeta object (title, description, robots, og:/twitter:); locale is { htmlAttrs, meta, link } — html lang, og:locale, canonical, and hreflang alternates.

Content Preview

Two hooks around content preview: one decides where the preview token comes from, the other tells you when preview turned on or off.

Resolve preview token

frontend-core:content-preview:resolve-token
Take over where the content-preview token comes from — a CMS cookie, a header injected by a gateway — instead of the query parameter.Your plugin must use enforce: 'pre': frontend-core evaluates the token source during its own plugin setup, so a handler registered later is never consulted. Handlers must also be synchronous — you may read cookies or useRequestHeaders(), but you must not await.

When it fires: Before frontend-core reads the preview_token query parameter, on every evaluation of the preview token source.

Runs onClientRegister inNuxt pluginDispatchSynchronous
route
RouteLocation

The current route.

result
{ value?: string }

Set result.value to a token to drive content preview from your own source. It starts empty; leave it unset to fall back to the ?preview_token= query parameter. The first handler to set a value wins.

Content preview changed

frontend-core:content-preview:changed
Invalidate your own caches when content preview turns on or off. Fire-and-forget: there is no result slot, and a returned promise is ignored.Frontend Core already calls refreshNuxtData() and invalidateOrchestrQueries() for you, so Nuxt's own data and every stored orchestr query result are handled. Use this hook for anything else you hold — a hydrated Pinia store, a memoized CMS client.

When it fires: After a content-preview transition — entering preview, leaving preview, or swapping to a different token.

Runs onClientRegister inNuxt pluginDispatchSynchronous
enabled
boolean

The new state — true once the server has verified the token

Analytics

Four hooks along the emission pipeline. Every event passes through the first three in order: :emit can veto it, :enrich shapes the whole event, and :project shapes each orchestr entity found in the payload. An event that waited for a consent decision passes through :redeliver as well, once, on its way out.

Emit analytics event

frontend-core:analytics:emit
Veto or pre-transform an event. Setting result.value to null drops it entirely — no enrichment, no buffering, no destination. Frontend Core registers no handlers here, so it is yours alone.

When it fires: At the start of every track() call, before enrichment, buffering and delivery.

Runs onClientRegister inNuxt pluginDispatchSynchronous
result
{ value: AnalyticsEvent | null }

Pre-seeded with the event as tracked. Transform it, or set it to null to drop the event before anything else sees it.

Enrich analytics event

frontend-core:analytics:enrich
Add to or rewrite the whole event — extra payload fields, a context of your own, a redacted value.Frontend Core registers two handlers here, in this order: it projects the orchestr entities in the payload (firing :project once per entity), then attaches the registered ambient contexts. Handlers run in registration order, so a handler that needs to see — or redact — a projected entity or an attached context must run after core's. That means enforce: 'post' or a dependsOn on the frontend-core plugin, not enforce: 'pre'.Redaction here is durable: :enrich runs before the event enters the replay buffer, so the redacted form is what gets buffered. The unredacted event is never retained and never replayed.

When it fires: After the :emit veto and before the event enters the replay buffer, on every event that survives.

Runs onClientRegister inNuxt pluginDispatchSynchronous
result
{ value: AnalyticsEvent }

Pre-seeded with the vetted event. Core handlers project payload entities and attach registered contexts before yours run; reassign result.value to change what is buffered and delivered.

Project analytics entity

frontend-core:analytics:project
Shape the wire snapshot a single entity turns into. Use augmentProjection for a typed handler bound to one entity type.

When it fires: Once per orchestr entity in the event payload, during :enrich.

Runs onClientRegister inNuxt pluginDispatchSynchronous
entity
ClientEntity

The orchestr entity found in the payload

overrides
Record<string, unknown>

The literal fields written alongside the entity at the track() call site, e.g. quantity.

result
{ value: unknown }

Pre-seeded with the base projection for the entity type, or { id } when no projector is registered for it. Reassign to add, remove or replace fields.

Redeliver held analytics event

frontend-core:analytics:redeliver
Transform an event that waited for a consent decision. Frontend Core registers one handler here: it re-reads the contexts marked refreshOnDelivery, which are session and consent, so the event carries the identity minted at the grant rather than the empty one it was enriched with. Every other context keeps its emit-time value, so a held page view still reports the page it was raised on.The bus stamps the delivery context with { deferred: true }after your handlers run, so you cannot set or override it from here. Read it in the destination's track() instead.

When it fires: Once per held event, when a destination becomes eligible for it and before that event is delivered. Never on a live emission.

Runs onClientRegister inNuxt pluginDispatchSynchronous
result
{ value: AnalyticsEvent }

Pre-seeded with the event as it was enriched, before the visitor answered. Core re-reads the consent-scoped contexts before your handlers run; reassign result.value to change what the destination receives.

Shape an analytics event for one destination

frontend-core:analytics:deliver
Give one destination a different event from every other. This is the only seam between enrichment and an individual destination.Copy rather than mutate. Every destination holds the same object and the transport batches it for a later request, so an in-place edit reaches recipients you did not mean to change.

When it fires: Once per destination, per delivery, immediately before the destination is called. Never for the transport that carries events to the project's own ingest endpoint.

Runs onClientRegister inNuxt pluginDispatchSynchronous
destination
{ id: string }

The destination that is about to receive this event.

consent
'granted' | 'denied'

Whether the visitor consented to this destination. denied reaches only a destination that declares onDenied.

result
{ value: AnalyticsEvent }

Pre-seeded with the event that destination is about to receive. Reassign result.value to change it for this destination alone.

Redaction: global or per destination

:emit, :enrich and :project all run once per event, before the fan-out. Whatever they change, every destination sees. Use them for a change that belongs to the event itself.

To give one destination different data, use frontend-core:analytics:deliver. It fires per destination, immediately before that destination is called, and result.value starts as the event that destination would otherwise have received.

One destination is exempt: the transport that carries events to the project's own ingest endpoint. The recipient fan-out for server-side subscribers happens there, and those subscribers declare their own consent requirements, so a handler must not be able to reshape what they receive. A destination declares this role with stage: 'transport', which only Frontend Core's own transport sets.

A destination author can also shape their own output inside track(), since that code owns what it sends.

Redirects

Send a project redirect

frontend-core:redirect:send
Add response headers to a redirect from the project's redirect list. Frontend Core sets no cache header on these redirects by default. On Laioutr Cloud and on Vercel, the hosting integration uses this hook to let the CDN keep them — see Caching at the edge.A project redirect depends only on the request URL, and it changes only with a deploy. So a shared cache may keep it, as long as the next deploy clears that cache. The hook does not fire for other redirects: a locale redirect or a redirect after login can depend on the visitor.

When it fires: When a request matches a redirect from the project's redirect list, before the redirect is sent.

Runs onServerRegister inNitro pluginDispatchAsynchronous
event
H3Event

The request being redirected. Set response headers on it.

location
string

The target, with the request query string already appended.

statusCode
301 | 302

Whether the redirect is permanent or temporary.

Orchestr Client Hooks

These hooks fire during client-side action execution. All receive a token string that identifies the action (e.g. ecommerce/cart/add-items).

Fetch Action Hooks

Fetch action lifecycle

Four hooks fire around every fetchAction request. finally always runs, whether the action resolved or errored.

Fired byfetchActionuseFetchActionuseQueryActionuseMutationAction
  1. beforeorchestr:action:fetch:before

    Before the request is sent.

    { token, input }
  2. successorchestr:action:fetch:success

    After the action resolves.

    { token, output }
  3. errororchestr:action:fetch:error

    After the action rejects.

    { token, error }
  4. finallyorchestr:action:fetch:finally

    Always

    { token, output?, error?, input }

Mutation Action Hooks

Mutation action lifecycle

useMutationAction fires these around the mutation.

Fired byuseMutationAction
  1. beforeorchestr:action:mutation:before

    Before the mutation runs.

    { token, input }
  2. successorchestr:action:mutation:success

    After the mutation resolves.

    { token, output, input, context }
  3. errororchestr:action:mutation:error

    After the mutation rejects.

    { token, error, context }
  4. finallyorchestr:action:mutation:finally

    Always

    { token, output?, error?, input, context }

The context value comes from Pinia Colada's mutation context and is set by the onMutate callback.

app/plugins/action-error-tracking.ts
export default defineNuxtPlugin((nuxtApp) => {
  // Track all failed actions (both fetch and mutation)
  nuxtApp.hook('orchestr:action:fetch:error', ({ token, error }) => {
    errorTracker.capture(error, { action: token, type: 'fetch' });
  });

  nuxtApp.hook('orchestr:action:mutation:error', ({ token, error }) => {
    errorTracker.capture(error, { action: token, type: 'mutation' });
  });
});

Action Request Hooks

Two hooks around each HTTP request that fetchAction sends. :prepare adds request headers, and :retry decides whether a failed request goes out once more. Bot protection uses both: :prepare attaches the provider's proof, and :retry sends a step-up proof after the server asks for one.

Unlike the fetch:* hooks, which fire once per fetchAction call, :prepare fires once per request. A call that is retried fires it twice.

Prepare an action request

orchestr:action:request:prepare
Attach headers an action request needs, such as a CSRF token or a proof from a bot-protection vendor. Handlers are awaited, so you can load a script or fetch a token first.A handler that throws cancels the call: no request is sent, and fetchAction rejects with that error. This is different from the result-returning hooks, where a throwing handler is skipped.

When it fires: Before every action request fetchAction sends, including the retry, and during server-side rendering as well as in the browser.

Runs onClientRegister inNuxt pluginDispatchAsynchronous
token
ActionToken

The action being called.

input
unknown

The action input.

headers
Record<string, string>

Starts empty for each request. Add entries in place; they are sent with the request.

previousError
unknownoptional

The error of the first request. Set only on the retry that orchestr:action:request:retry asked for.

fetchActionuseFetchActionuseQueryActionuseMutationAction

Retry a failed action request

orchestr:action:request:retry
Send a failed action request once more, with fresh headers from :prepare. A call is retried at most once: when the retry fails too, its error reaches the caller and this hook is not asked again.Decide from the error alone, synchronously. Ask for a retry only for an error the server raised before the action's side effects, such as a rejection from an orchestr:action:handler:guard handler. Retrying a request that failed halfway through a handler can run its side effects twice, for example adding an item to the cart twice.

When it fires: Once per fetchAction call, after its first request fails. Never after the retry fails.

Runs onClientRegister inNuxt pluginDispatchSynchronous
token
ActionToken

The action being called.

input
unknown

The action input.

error
unknown

The error of the failed request, or the error a :prepare handler threw.

result
{ value: boolean | undefined }

Starts empty. Set result.value = true to send the request once more; leave it unset to let the error reach the caller.

fetchActionuseFetchActionuseQueryActionuseMutationAction

URL Query Parameters

Two hooks control how Orchestr reads and writes URL query parameters (pagination, sorting, filters). See URL Query Parameters for the full reference with examples.

Parse query params

orchestr:query-params:parsed
Adjust the parsed URL query params before Orchestr reads pagination, sort, and filter from them.

When it fires: After parsing the URL, before reading pagination, sort, and filter.

Runs onClientRegister inNuxt pluginDispatchSynchronous
params
QueryParams

The parsed query params — mutate directly.

queryPrefixes
QueryPrefixes

The active query prefixes.

route
RouteLocation

The current route.

orchestr:navigate-query:build
Rewrite the assembled query object before Orchestr serializes it into a navigation URL.

When it fires: At the end of buildQueryUrl(), before returning the URL.

Runs onClientRegister inNuxt pluginDispatchSynchronous
params
QueryParams

The params being written.

query
QueryObject

The assembled query object — mutate directly.

path
string

The target path.

queryString
string

The serialized query string.

Client Environment

Client environment

orchestr:client-env:modify
Shape the wire client environment before every request is sent. This is the browser's payload, not what handlers receive: the server validates every field against the project's own configuration and resolves it into a ClientEnv. Nothing you set here is trusted.Frontend Core already sets marketId, languageId, and the content-preview fields. Put your own data under custom, and validate it server-side — a shopper can set it to anything.

When it fires: Synchronously, every time orchestr builds the wire clientEnv before sending a query or action request.

Runs onClientRegister inNuxt pluginDispatchSynchronous
clientEnv
WireClientEnv

{ isPreview, previewToken?, marketId?, languageId?, custom? }, imported from @laioutr-core/core-types/orchestr — mutate directly, do not replace.

Orchestr Server Hooks

These hooks fire during server-side action handler execution. They are Nitro runtime hooks and must be registered in a Nitro plugin, not a Nuxt plugin.

Guard an action request

orchestr:action:handler:guard
Refuse an action request before it costs anything. Throw an h3 error to end the request: the client receives Action failed with the error's status code, and the error as data. Frontend Core registers one handler here, which runs the bot-protection check for the listed actions.Handlers run one after another, and the first one that throws ends the request. A rejection fires orchestr:action:handler:error and :finally, but not :before.Call readInput() only when the decision depends on the input. A guard that never calls it rejects a request without parsing its body. When the body fails validation, readInput() throws the same 400 the route would.

When it fires: After orchestr has found the action handler, and before the body is read, initwares run or orchestr:action:handler:before fires.

Runs onServerRegister inNitro pluginDispatchAsynchronous
token
ActionToken

The action being requested.

event
H3Event

The incoming request.

readInput
() => Promise<unknown>

Reads the request body and validates it against the action input schema. The body is read once, however often this is called, and the route reuses that read.

Server handler lifecycle

Four hooks fire around the server-side action handler. Register them in a Nitro plugin with nitroApp.hooks.hook().

  1. beforeorchestr:action:handler:before

    Before the handler runs.

    { token, input, clientEnv }
  2. successorchestr:action:handler:success

    After the handler resolves.

    { token, output }
  3. errororchestr:action:handler:error

    After the handler throws.

    { token, error }
  4. finallyorchestr:action:handler:finally

    Always

    { token, output?, error?, input }

A request whose body fails validation fires :error and :finally too, with input undefined, and reaches the client as Action failed with status 400 and the validation error as data.

server/plugins/action-logging.ts
export default defineNitroPlugin((nitroApp) => {
  const pending = new Map<string, number>();

  nitroApp.hooks.hook('orchestr:action:handler:before', ({ token }) => {
    pending.set(token, Date.now());
  });

  nitroApp.hooks.hook('orchestr:action:handler:error', ({ token, error }) => {
    const startedAt = pending.get(token);
    const duration = startedAt ? Date.now() - startedAt : undefined;
    console.error(`[orchestr] ${token} failed after ${duration}ms`, error);
    pending.delete(token);
  });
});
Copyright © 2026 Laioutr GmbH