2026 08 header en

Laioutr Release News - August 2026

August was about measurement. The new consent-aware analytics system gives the platform a typed event model: useAnalytics().track(Token, payload) emits an event whose token carries its own payload schema, and delivery to each individual recipient is judged against the visitor's consent at the moment of delivery rather than at emission. Laioutr UI wired a storefront's commerce events straight onto it — product lists, product pages, the cart and search all report with no configuration at all. Alongside that, consent moved from cookie categories to processing purposes, and the ConsentAdapter contract shrank to four members.

The second theme is speed. Orchestr now resolves a request's queries, a query's links and the component resolvers for one entity type concurrently: a storefront home page's server render fell from about 724 ms to about 322 ms. Add to that cache keys roughly 45% shorter, a declarative cache configuration that lets Orchestr build the whole key itself, and several corrections to keys that previously carried no market, offset or locale. In Laioutr UI, sliders and videos hydrate only near the viewport, and web fonts ship as woff2 only.

Third: observability. Frontend Core and Orchestr export OpenTelemetry traces of their server-side work — queries, links and component resolvers as nested spans, every cache read carrying a verdict. And fourth, operations: markets now have a status of Draft or Active, a project can name its default market explicitly, and Studio keeps where you were — page, selection, market, language, preview — in the URL.

Frontend Core v0.38.2 – v0.47.2

v0.38.2 was released on 31 July but only documented in August, so it did not run in the July post. It carries a breaking change and is covered here.

Highlights

  • A consent-aware analytics system: useAnalytics() returns { track, register, unregister }. track(Token, payload) emits a typed event whose token carries its own payload schema; @laioutr-core/core-types/analytics provides the token factories and the platform's own web/* events, @laioutr-core/canonical-types/analytics the ecommerce/* commerce vocabulary. Register a recipient with defineAnalyticsDestination, declaring what it needs as consent: { purposes: ['analytics'] }, or purposeSets for an OR-of-ANDs. Consent is evaluated per recipient at delivery, never at emission — so one emission fans out to exactly the destinations the visitor allowed. Events emitted before the visitor answers are held and replayed in order once they grant. A destination may instead declare onDenied to take denied events with the consent state attached, degrading rather than going silent; such an event counts as delivered and is not replayed on a later grant, and a revocation leaves that destination running rather than tearing it down.
  • Payloads carry Orchestr entities directly: track(AddToCart, { products: [{ entity: product, quantity: 2 }] }) — each entity is projected to a flat wire snapshot at emit time, selected by its entityType. An entity carrying a slug on its base component also gets an absolute url on the market's production host. @laioutr-core/core-types/orchestr gains getEntityComponent(entity, Token) and getLinkedEntities(entity, LinkToken). Every payload schema carries an optional customFields bag for site- or vendor-specific data; namespace the keys you put there ('acme:productLine').
  • Collection and extension: the v-track-click and v-track-impression directives, plus useTrackImpression, useTrackScrollDepth and useTrackVideoProgress. Ambient page, market, session, consent and experiment context attaches to every event; useAnalyticsContexts() adds a provider of your own or overrides one of the platform's. Three synchronous Nuxt hooks extend the pipeline: :emit as a veto, :enrich to enrich, and a per-entity :project filter, with augmentProjection typing a :project handler.
  • Server side and identity: the browser posts batches to POST /api/frontend/signals, overridable with analyticsIngestPath in the module's public runtime config, which moves the server route too. Every event in a batch is judged on its own, so one malformed or oversized event never costs the batch it rode in on. subscribeToAnalytics registers a recipient that must not run in the browser; handlers receive sentAt and receivedAt alongside the event. Visitor and session cookies are minted only under the analytics purpose and scoped to the market's registrable domain, so an identity survives a hop between subdomains; on platform hostnames they stay host-only. Inside the Studio preview they are written cross-site and partitioned. Withdrawal deletes both, and a later re-grant mints fresh ones without stitching activity retroactively.
  • Consent is reported as processing purposes (breaking): a ConsentAdapter now reports Partial<ConsentState> over necessary, functional, analytics, advertising and personalization, and owns the mapping from its own vendor's vocabulary. A purpose absent from a report counts as denied; an adapter grants a purpose when any of its own that map to it is granted. A visitor who allowed measurement but refused personalisation is now reported to Google Consent Mode as exactly that, rather than as allowing both ad purposes.
  • A new ConsentAdapter contract (breaking): an adapter is four members — name, setup, openConsentUi and an optional hasDecision — and the store installs one with setAdapter. init, getConsentState, onConsentChange, destroy and isActive collapse into setup(report): report the visitor's verdict at once and again on every change, and return a cleanup if you need one. It runs synchronously inside the installing plugin, so useHead and useCookie are available. On the storefront side, one openConsentUi replaces both former overlay calls, and adapterName replaces activeAdapter.
  • hasDecision() tells a refusal from silence: useConsentStore() reports whether the visitor has answered the consent prompt at all. A state of "denied" is otherwise indistinguishable from "never asked", which matters wherever that state is passed to a third party applying its own regional default. The Cookiebot and CCM19 apps implement it, each reporting a saved refusal as a decision rather than as silence. From 0.42.0 it returns boolean | undefined: undefined means no decision signal exists at all — no adapter, or one that cannot tell.
  • Every way a link can fail to resolve now has a name: linkResolver.resolve() still returns an in-page fallback rather than throwing, but that fallback has one shape — #<code>?<detail> — drawn from a closed set, and each carries what identifies the offending link (#unknown-route?pageId=pdp, #missing-required-params?params=brand). New is linkResolver.resolveOrThrow(link, options?) for a caller that can act on the failure — an analytics projector, a sitemap writer — throwing a LinkResolutionError carrying code and details. Pass { withOrigin: true } for an absolute URL rather than an absolute path; the origin comes from the market's own domain, never from location, so an address resolved inside the Studio preview or on a dev host still names the site a visitor would land on.
  • Markets can be set to Draft, and a project can name its default market: a draft market still serves its own host so it can be checked before launch, but it is excluded from hreflang alternates, og:locale:alternate, x-default and market switchers, and its pages are served with noindex, nofollow. switchMarketUrl returns '#market-not-active' for a draft target. Status is read through RenderMarket.isLinkable and isIndexable rather than the status member. RcProject.defaultMarketId replaces the implicit "first market in the configuration" rule for x-default, the primary route path, the unknown-host fallback and nuxt-i18n's defaultLocale — which market that was previously depended on the order Cockpit happened to return them in. Leaving the field unset preserves the previous behaviour.
  • OpenTelemetry tracing: a storefront that sets OTEL_EXPORTER_OTLP_ENDPOINT at build time exports traces of its server-side work through the standard OTEL_* configuration; without it nothing is installed. Queries, links and component resolvers each become a span nested under the Nitro request span, and an upstream API call made while a resolver runs nests under that resolver — so a trace attributes upstream time to the work that caused it. Attributes are counts and names a backend can group by: how many entities a resolver asked for, a token name, the app a handler comes from. Every cache read carries orchestr.cache.verdict (hit, miss, partial, skip, uncacheable), and a server-rendered request tags its span with the page type, market slug and locale, since a product page and a listing page share one wildcard route.
  • Tracing on Vercel is opt-in (breaking): a storefront deployed there first turned it on automatically (0.45.0), which cost every request a failed span export and two error log lines, because Vercel runs no collector for it to reach. From 0.46.0, set LAIOUTR_OTEL_ENABLED=1 on the project to keep tracing; a build configured with OTEL_EXPORTER_OTLP_ENDPOINT is unaffected. A traced request also joins the trace it arrives in, instead of starting a second, unrelated one.
  • A laioutr:// resource locator: @laioutr-core/core-types/locator exports formatLocator and parseLocator plus the supporting types and the STUDIO_CONTAINER_KINDS constant. A locator names a namespace (studio is the only one today), a container (pageVariant, section, sectionRef, globalSection or block) by id, a path of object-key or array-item-by-id steps into its props, and optional view coordinates (locale, market, ref) — for example laioutr://studio/block/blk_C3/slides[itm_E5]/heading?locale=de. Both directions also handle a relative form that omits the laioutr://studio/ base. parseLocator never throws: it returns { ok: true, value } | { ok: false, error } for every input.
  • useSectionContext() and useRenderPageContext() are auto-imported: both contexts were already provided at runtime but reachable only through a deep import path. A block never receives its own id, so the section context is the only stable list identity available to one.
  • Read the visitor's analytics identity server-side: readAnalyticsIdentity(event) returns the visitor and session tokens the browser is already reporting on its events. A server route or a connector can group its own work into the same visit without minting a second identity. The tokens exist only under the analytics purpose, so an empty result means the visitor has not granted it.
  • Development-only switches live under laioutr.dev, and a production build discards the whole object rather than each flag on its own. New is consentDebug: a debug CMP that grants every purpose without asking. Playgrounds install no consent management app, so nothing ever grants a purpose there and the consent-gated paths never run. It reports a decision the visitor never made, so it warns on install. analyticsDebug keeps logging canonical events through the built-in debug destination, and now only takes effect in dev.
  • Session cookies survive the Studio preview: cart and customer-session cookies are issued with SameSite=None; Secure; Partitioned when the request comes from the preview frame, so a cart built in the editor persists across reloads. The preview gets its own cookie partition, keeping it separate from your real session on the same shop in the same browser. App authors get two new server auto-imports, setManagedCookie and deleteManagedCookie, which apply this policy — deletions must go through deleteManagedCookie, since a delete that omits Partitioned addresses the wrong cookie jar and silently leaves the cookie in place. Secure is now derived from the request origin rather than set per connector.
  • menuTreeAtDepth(items, startLevel): auto-imported alongside buildMenuTree, it builds the menu tree and then descends startLevel levels so the nodes at that depth become the top level — for skipping a synthetic upstream root node (e.g. a Magento "Root" category) so the first business-facing level renders as the top level. 0 (the default) keeps the tree as fetched.
  • A page with a single variant fetches in one request: its page queries and its variant queries now go out together instead of in two round trips in series, so it renders sooner. Pages with several variants are unchanged — a frontend-core:page-renderer:select-page-variant handler still reads the resolved page queries before it chooses.

Fixes & Improvements

  • Fixed: the reflect endpoint served a previous deployment's section and block catalog. Its cached reflection is now keyed by build id, so a redeploy is a cache miss instead of inheriting whatever the last build left behind, and two frontends sharing one Redis no longer overwrite each other's entry. Cached entries expire after 12 hours.
  • Fixed: laioutrrc.json app config was merged into each app module twice. It was both assigned to nuxt.options[<appName>] and passed to installModule, so Nuxt merged it with itself and concatenated every array-valued option — a four-entry Shopify sortings list arrived as eight and failed the build with a duplicate-key error.
  • Fixed: linkResolver.resolve() dropped a link's query. Because every Laioutr page route carries localizedPaths, internal links always resolved through the localized-path branch — which ignored query and returned before the vue-router fallback that applied it. A header search landed on /search without ?q=. url and anchor links now carry their query too; it is merged into whatever the href already has (the link's own query wins on a key collision) and always lands before the fragment.
  • Fixed: the page endpoint's render cache grew without limit and could exhaust a storefront's memory. The endpoint memoised every rendered page by page, market and language in a map that was never evicted from — and module scope on a serverless host lives for the whole instance. It is now capped.
  • Fixed: the consent store is scoped to the Nuxt app rather than the module, so on the server each request gets its own. It was global to the module, which on a server is global to the process: every concurrent render shared one store, the CMP adapter installed by the first request kept serving all later ones with the cookie ref it captured then, and each subsequent request added another consent listener to it.
  • Fixed: switching market or language in Studio reloads the previewed content. The preview kept rendering whichever market it was opened with — switching from German to Dutch left the previous menus, product data and prices on screen, so the preview showed one market's content under another market's settings. Orchestr results are scoped to a market and a language, but the client cached them under a key that carried neither. Cached results are now dropped when the selection changes. Storefront rendering is unaffected.
  • Fixed: the canonical page types and analytics projectors now register in the browser. The plugin that pulled them used a dynamic import with @vite-ignore and a template-literal specifier, so Vite left the bare specifier alone and the browser could not resolve it. The failure went into an empty catch, so nothing registered and every entity in an analytics payload fell back to its id and address. Page types were unaffected, because they resolve on the server. The module now checks at build time whether the package is installed and, if it is, emits a plugin that imports it statically; the package stays optional.
  • Fixed: a page no longer runs a query that only an abandoned field still binds. When a section or block drops a field from its schema, the stored value stays in the project configuration — and that value kept the query alive, so every render fetched data no component reads. A header still holding a cart binding this way cost one upstream request on every page of the storefront. A query now survives as long as any live field, or an SEO placeholder, still references it; a section or block that no registry knows loses its queries as well. Each drop warns once and names the component, the field and the query token.
  • Fixed: the Studio preview failed when the handshake secret was absent from the URL. It now authorizes off the embed marker the server mints after validating that secret, so a reload or a client-side navigation inside the frame keeps working, and a storefront opened outside Studio renders normally instead of throwing No secret provided. The marker stays scoped to one project. The project secret no longer reaches client JavaScript, so it no longer appears in the hydration payload of an embedded page.
  • Fixed: the analytics identity cookies survive on a hosting platform's shared domain. A storefront served from a host such as example.vercel.app or example.pages.dev scoped laioutr_vid and laioutr_sid to the platform suffix itself, which a browser rejects as an invalid cookie domain — so neither cookie existed on those hosts and no visitor or session token reached an event. The cookie domain now resolves against the private section of the Public Suffix List, the same view a browser applies.
  • Fixed: a query's configured sorting is now applied. The value stored on the query was dropped while the request was built, so a sorting set in Studio — or returned as defaultSorting from a queryTemplateProvider — never reached the query handler. It is now sent as the query's sort; an s URL parameter still takes precedence, so the configured value sets the default order rather than a fixed one. Links are unaffected.
  • Fixed: the referrer on a page event is an absolute URL on every page view, not only the first. Within an SPA session it reported the previous route as a bare path, which a destination that classifies referrers reads as malformed rather than as a same-site visit.
  • Fixed: a property edit in Studio updates the preview again. Every edit re-rendered the page variant that PageRenderer captured when the preview opened, so nothing on the page moved until a reload.
  • A request span adopts an inbound trace context only when nothing upstream opened one. Where an HTTP instrumentation already started a server span — a node deployment shipping to an OTLP endpoint — that span stays the parent, instead of the request being reattached to the remote caller.
  • The startup banner no longer prints during nuxi prepare and nuxi typecheck, only when the app actually boots.

Notes for Developers (Breaking Changes)

  • @nuxtjs/robots is no longer installed (0.38.2): robots.txt, the X-Robots-Tag header and the route-rule robots value now come from the @laioutr/app-essentials-seo app — install it to keep them, and configure them through its app config instead of nuxt.options.robots. A frontend with neither that app nor its own @nuxtjs/robots install serves no /robots.txt (a 404 tells crawlers to crawl everything, which is what the previous default content said), and any robots key in nuxt.config or in a route rule is silently inert. Page-level robots meta tags are unaffected; they come from the page variant's SEO settings.
  • The specialized tracking composables are removed (0.42.0): instead of const { trackAddToCart } = useProductInteraction(); trackAddToCart(payload), now const { track } = useAnalytics(); track(AddToCart, payload).
  • The ConsentAdapter contract (0.42.0): upgrade the CMP apps alongside @laioutr-core/frontend-core — an adapter written against the old contract no longer installs. registerAdapter plus activateAdapter become one synchronous setAdapter, which returns the handle that drops the adapter; deactivateAdapter is gone. Throwing from setup makes the store warn and drop the adapter. On the storefront side, consentStore.openConsentUi() and consentStore.openConsentUi('preferences') replace showConsentOverlay() and renewConsent(). hasDecision() returns boolean | undefined — code that read a falsy result as a refusal has to tell the two apart.
  • Consent as purposes rather than cookie categories (0.42.0): ConsentManagementState and hasCategoryConsent are gone. { necessary, functional, statistics, marketing, unclassified } becomes { necessary, functional, analytics, advertising, personalization }.
  • fillParams returns undefined when a required param has no value, instead of filling it with a blank (0.42.0). The blank collapsed into the neighbouring separator — /:brand/p/:slug became /p/shoe, an address that looked resolvable and was not — so callers must now check for undefined. Finite-set defaults (/:page(a|b)) and optional params (:lang?, :rest*) are unaffected; missingRequiredParams(path, params) names the ones missing. Downstream: an hreflang or canonical link whose params cannot be filled is omitted rather than emitted truncated, and a language switcher offers the target domain's homepage rather than a broken path.
  • RenderI18nConfig.markets now contains only linkable markets (0.41.0). The complete list, including drafts, moved to RenderI18nConfig.allMarkets. Code that renders a market switcher needs no change and starts honouring status automatically; code that needs every configured market (routing, host resolution, preview) must switch to allMarkets.
  • Local dev hostnames (0.41.0): for projects hosted on *.app.laioutr.tech, that suffix is dropped instead of folded into the label. A market on example-shop.app.laioutr.tech is now reachable at example-shop.local.laioutr.tech rather than example-shop-app-laioutr-tech.local.laioutr.tech. The same host drives market resolution and the dev cookie domain, so all three stay in step — update any bookmark or allowedHosts entry that named the old form.
  • An /api or /.well-known path that nothing claims answers with a small plain HTML 404 (0.47.0), instead of the project's whole 404 page with its sections and queries. The response carries Cache-Control: public, s-maxage=3600, so a CDN answers a repeat probe without invoking the function. A registered server route and a file in public/ both still win, so neither needs changing. A /.well-known/… path served by a Nuxt page does not: move it to public/ or to a server route.
  • Tracing on Vercel is opt-in (0.46.0): set LAIOUTR_OTEL_ENABLED=1 on the Vercel project to keep it.

Orchestr v0.38.2 – v0.45.0

Orchestr and Frontend Core release in lockstep; this section carries only what appears exclusively in the Orchestr changelog. As with Frontend Core, v0.38.2 was released on 31 July and documented in August.

Highlights

  • Concurrent resolution: a request's queries, a query's links, and the component resolvers for one entity type now resolve concurrently instead of one after another. A page waits for its longest strand rather than for the sum of its work. A storefront home page's server render fell from about 724 ms to about 322 ms; a cold product page's six component resolvers finished in 378 ms — run one after another they total 937 ms. At the top level, up to six queries run at a time. Chunks now interleave: a client that routes them by path and merges entity chunks by id, as it always had to, is unaffected; one that depends on chunks arriving grouped by query in request order is not.
  • A failing link no longer discards its query: a link handler that threw collapsed the whole query into one error chunk, taking the query result and every entity chunk its sibling links had already streamed. It now reports an error at its own path, [queryId, linkToken], and the rest of the query completes. Where several parts of one query fail, each reports its own error rather than only the first.
  • A declarative cache: a query or link handler enables its cache with a TTL and a strategy, and nothing else — cache: { ttl: '1 day', strategy: 'ttl' }. Orchestr builds the whole key: the token, the environment, the requested slice, the sorting, the filters and the token's input. The first slice of a listing, unfiltered, is cached by default; the tail of a listing is cold and a filter combination unbounded, so both are opted into (pages: 'all', filters: ['filter.v.availability']) rather than out of. shouldBypassCache refuses a request the declarative options would admit; it runs after them and can only narrow them.
  • validate decides from the outcome: a query or link cache config accepts validate, called with the handler's result before it is written. Return false and the result is served but not stored. buildCacheKey already decides cacheability from the request, before the handler runs; this decides it from the outcome — a degraded fallback that must not be served for the rest of its TTL, or a result that cost nothing to produce. It gates the write only: an entry already in the cache keeps serving until its TTL lapses.
  • cacheKeys for hand-built keys: useUserlandCache hands back a bare storage with no prefixing at all. A key built there carries the environment itself or it serves one storefront's data to another, and it escapes an id holding a / or a : or unstorage rewrites it into a different key. cacheKeys.forClientEnv(clientEnv) and cacheKeys.escape(id) close that. cacheKeys.forEntityIds turns a set of entity ids into a short, fixed-length segment, so a hand-built key stops growing with the page size — a handler that joined ids instead produced over two thousand characters for 48 products, past the 255 bytes a filesystem allows for one path segment and into the request-size limit of a hosted Redis. Query and link handlers do not need it: Orchestr keys a link's source ids itself.
  • Cache keys are roughly 45% shorter: measured across two live storefronts, a component key fell from 144 bytes to 84 on one and from 121 to 66 on the other, a product-variants link key from 136 to 82, and a page-index key by 17%. The key was a third of what a component entry cost to store, and every key is sent again on each batched read, so the saving lands on stored size and on request payload alike. Three things got shorter: the namespace, the escaping (two bytes for the four characters unstorage would otherwise rewrite, rather than three bytes for every character encodeURIComponent recognises — a Shopify GID paid 15 bytes for 5 characters), and the environment segment, where locale, currency, market and preview stage become one 8-character digest. Entity type, id, component and page type stay readable.
  • A request-level cache report: an execution summary carries a cache report for the request next to its per-query summaries. It answers questions the query summaries could not — whether a component read arrives as one batched call or many single ones, what a given component's hit rate is, and whether a background write was lost when the isolate froze. The counters are gathered only for a request that set options.dev.enableSummary; every other request pays nothing, including the payload measurement.
  • Passthrough is scoped to the handler that writes it: every handler in a query used to share one passthrough store. A handler now reads every token its callers set, and writes where only the handlers beneath it can read. So a link handler can hand data to its own component resolvers without it reaching the rest of the query. In exchange, two links that set the same token no longer see each other's value.

Fixes & Improvements

  • Fixed: a TypeError: Cannot read properties of undefined (reading 'length') failed any query whose link handler answers with entity, entities or a single targetId. Telemetry added in 0.44.0 counted a link's targets by reading targetIds on the handler's raw response, which only the targetIds shape carries. The failing query returned no data and reported an error chunk, so a page built on it rendered without its products.
  • Fixed: query, link and entity-component caches now key on the market, and query and link caches also on the pagination limit. Two markets that share a language and a currency no longer read each other's cached results, and a request for 24 items no longer receives the slice a different page size cached.
  • Fixed: the requested offset was absent from every query and link key, so ?offset=5&limit=24 was served the offset=0 result. And a component resolver's getKeySuffix replaced the environment digest instead of extending it, which would have let two markets collide on one entry.
  • Fixed: a streamed query result renders once it has settled, rather than once per response chunk. On a server-rendered page, the first client-side navigation that ran a query re-rendered on every chunk — including the window where a link's entity ids have arrived but the entities themselves still report no components. Sections reading those components rendered against that half-loaded state and threw, which read as intermittent because a retry usually landed after the response had finished.
  • Fixed: a project that points its cache at a real backend now keeps it in development. Orchestr mounted an in-memory LRU over its own cache namespace on every dev boot, and that mount is more specific than the cache mount a project configures, so it won. The effect was that dev never ran the path production runs: no round trips, no serialization limits, and a configured Redis that received nothing. The LRU still mounts when no backend is configured.
  • Fixed: a pageIndex.locate lookup no longer serves one locale's page metadata to another. The cached result carries meta resolved in the locale the lookup was made in, but the entry was keyed on the market alone. Two locales of one market collide whenever they share a route param — which is every product whose slug is a SKU, a brand name or an untranslated model number. The key now covers the locale, and preview and published results no longer share an entry either.
  • Fixed: cache writes survive on Vercel. Orchestr writes to the cache after responding, through event.waitUntil, and on Vercel's Node runtime nothing was keeping the function alive to finish them — Nitro's Vercel preset never sets the event.context.waitUntil that Nitro itself looks for, so the write was left as a floating promise and died with the invocation. Measured against a deployed probe on a cold instance, the deferred write was lost in four trials out of four and the immediate write in two of them. A Nitro plugin now supplies that hook from Vercel's own request context; it needs no extra dependency, does nothing off Vercel, and defers to any platform that already provides one.
  • Fixed: devtools traces and execution summaries describe what actually happened. A completed query reported unknown for its id and token, a component resolver's missing entity ids were dropped, and spans started concurrently appeared as a chain — initwares running under Promise.all rendered as each one nested inside the last rather than side by side.
  • Fixed (0.38.2): listPagesFrom reports its endCursor at any stopping position, not only at the take boundary. A consumer that stopped iterating early — on a wall-clock budget, say — read endCursor as undefined and could not distinguish that from an exhausted enumeration, so a partial walk was recorded as complete. Two fields make the outcome of a pass unambiguous: exhausted is the termination signal for an accumulation loop, and progressed reports whether the pass durably advanced. A loop must stop on !progressed as well as on exhausted, or a pass that takes nothing repeats forever.

Notes for Developers (Breaking Changes)

  • The storage namespaces moved: a project mounting one driver at cache covers both and needs no change. A project mounting at the full path does — 'cache:orchestr:internal' becomes 'cache:orch:i'. A driver left at an old path receives nothing, and the cache falls back to whatever serves cache — in the worst case a per-isolate memory driver, which reports no error and holds nothing between requests.
  • buildCacheKey is now optional and supplies one trailing segment rather than the whole key. A handler that keeps it keeps working, and returning null from it still refuses the cache.
  • Every cache key changes shape, in 0.43.1 as in 0.45.0. Existing entries are never read again and expire under their own TTL — expect one cold-cache window after the deploy; nothing needs clearing by hand.
  • ExecutionSummary is a union now: narrow on type before reading a query's fields (if (summary.type === 'query') summary.linkSummaries;).
  • Chunk order: chunks from different queries now interleave. A client that reads chunks by path is unaffected; one that relies on grouping by query in request order is not.
  • Shared passthrough is gone: a handler that read a sibling link's token now gets its own default.

Laioutr UI v2.8.6 – v2.22.0

Highlights

  • Commerce events with no configuration: product lists, product pages, the cart and search now emit commerce events. The instrumentation is always on and has no Studio surface. Product sliders and the product grid report view_item_list once they are on screen, not merely rendered. A tile reports select_item on a navigation and add_to_cart after the mutation resolves. A product page reports view_item on load and again on every variant switch. The cart reports view_cart on open, begin_checkout from the call to action, and a removal or an add for every quantity change, sized to what moved rather than what remains. The header reports a submitted search, and a search results page reports view_search_results. Raw entities go into track() and the projector registry turns them into wire shapes, so no call site builds a payload by hand.
  • MediaFeed: a full-screen short-form media feed in the TikTok/Reels pattern — vertical scroll-snap, one item playing at a time, an action rail, a mute toggle and a read-more sheet. Only the item in view is interactive; the others are inert, so tabbing never reaches a control that is off screen, and each item carries data-state="active" | "inactive". Render it from props (items + v-model:open) or drive it from anywhere on the page with useMediaFeedStore(). Because browsers only allow autoplay while muted, the feed starts silent and the top bar carries a way back to sound. Set urlParam to sync the item in view to the URL — ?reel=<id> — so an item can be shared and the back button closes the feed instead of leaving the page; the parameter carries the item id, not its position, so a link survives the feed being reordered. The compound parts are exported for composition.
  • New in the UI Kit alongside MediaFeed: useOverlayHistory syncs any overlay's open state and current item to a query parameter — opening pushes one history entry, item changes replace it, and back closes the overlay rather than navigating away. AnimatedIcon reflects a boolean active state, cross-fading on the transition with a burst that flies up and outwards, sized in multiples of the icon so it holds at every size, and coloured through --animated-icon-active-color. AnimatedButton is Button with a continuous pulse, paused on hover, under prefers-reduced-motion, or via paused. $count(1234) gives compact number formatting for the current locale, honouring your own numberFormats; the default abbreviation is CLDR's and differs by language — English shortens from a thousand (1.2K), German only from a million. Badge gains a glass-black variant for badges sitting over arbitrary media, and Media and MediaVideo gain v-model:currentTime, the sibling of v-model:paused: assigning to it seeks, and the element reports its own progress back.
  • Four new locale bundles: French, Italian, Spanish and Polish — storefronts in those languages stop rendering their UI chrome in English. ui-kit shipped de, en and nl only, and any other language fell through to en: 257 messages across 33 components, including the search field, the cart, "Add to cart", "Sold Out", the filter drawer and the header's account, wishlist and cart labels. The page content was translated; the chrome around it was not. The new bundles cover all 257 keys, verified key-for-key against en.ts with every {placeholder} token preserved. Two pre-existing gaps are closed at the same time: de was missing sliderNavigation.play / .pause, and nl the whole mediaFeed block. Not a behaviour change for existing projects — de, en and nl resolve exactly as before. The Dutch bundle for the UI Kit had landed earlier in the month (2.11.0).
  • The product tile pins a variant in its link only when the connector authored it: a tile in a listing, a grid or a slider produced /<slug>?variant=<variant-id> for every product. The value came from defaultVariant, which most connectors derive from stock, so the same product changed its URL whenever one of its sizes sold out. A derived default now stays out of the link, and every listing and slider URL for those products changes. An authored default is a merchant's choice and does not move with stock, so the link still carries it. An explicit ?variant= in a shared link, an ad landing URL or a wishlist entry is untouched. In step with that, a product detail page opened without ?variant= now opens the product's default variant rather than its first, and a colour swatch is marked as selected only for an authored default variant. ProductDefaultVariant.origin carries the distinction.
  • Product tiles show the colour swatches of the product they render: the swatch row existed but was never filled, so it was always empty. Tiles read the product's option axes directly and no longer load every variant to build a listing, which cuts the data a product grid ships to the browser. A sold-out product still disables its add-to-cart button; that state now comes from the product's default variant instead of a loaded variant entity. ProductTileBasic.sizeVariants carries one entry per value on the product's size axis, each addressing the variant the connector names for that size and disabled when no purchasable variant carries it.
  • Web fonts are configurable: fonts on the @laioutr-app/ui module reaches the whole @nuxt/fonts configuration — families, defaults, providers and preload. A value you set replaces the default, arrays included; false skips the module, for a storefront that ships its own @font-face rules. Fonts are served as woff2 only: a provider answers one request per user agent and returns every format tier at once, and the legacy woff faces carry no unicode-range, so they match every character and are written last — which makes a modern browser download them in place of the woff2 files. On top of that, fonts.defaults.subsets now takes effect at all: previously every subset a provider returned reached the stylesheet, and an unused one still downloaded as soon as a single character claimed it — 36.7 KB of glyphs that never render, on every page, on one storefront.
  • Sliders and videos hydrate and load late: CommonSwiper hydrates once it comes near the viewport instead of during the initial hydration pass, so carousels far down a page no longer initialise Swiper on load. Server-rendered markup is unchanged — a slider looks the same before it hydrates, it just does not answer arrows, bullets or autoplay yet. Import CommonSwiperEager for the previous behaviour. Videos rendered through Media likewise wait until they are near the viewport before fetching; videos under a MediaAboveTheFold provider are unaffected, and elsewhere videoPreload overrides it per video. And an above-the-fold image emits its preload link with fetchpriority="high" — the <img> already carried the hint, but the preload link is what starts the fetch and carried none, so the browser fetched the LCP image at default priority.
  • Banners: height, background colour and two-dimensional alignment: all three CTA banner sections — Banner Basic, Banner Integrated and Banner Showcase — gain a Sizing control (fixed height, responsive height or aspect ratio) to give the banner a definite height. Banner Basic's Content Alignment becomes two-dimensional (vertical plus horizontal, e.g. bottom-left) and Banner Integrated gains a Vertical Alignment control, so content can sit at any edge — including the bottom. Vertical alignment only takes effect once the banner has a definite height. All three also gain, in their section as in their block variant, a Background Color for the inner banner tile, independent of the section's outer backdrop; text and icon contrast adapt to the chosen colour automatically. Existing sections render unchanged.
  • Container gains a tablet stage: columnsTablet and gapTablet add a stage between mobile and desktop. Column count and gap now resolve in three stages — mobile (below 600px), tablet (600–1279px), desktop (1280px and up) — one breakpoint scheme shared with the Sizer. Both fall back to their mobile value when unset. The Container section gains Tablet columns and Gap (Tablet) controls for it.
  • Fully clickable banners and hero slides: HeroSlide, BannerBasic, BannerIntegrated and BannerShowcase accept an href that turns the whole component into a single link, rendered as a transparent full-area overlay. It activates only when no CTA button is present, so a component is either button-driven or fully clickable — never both, which would nest interactive elements inside an anchor. The matching blocks and, from 2.14.0, SectionBannerBasic gain a Link field for it.
  • New blocks and controls for Studio: BlockBasicTable brings the BasicTable molecule to Studio as a block, with an authored array of label/value pairs and the primitive's outlined / plain variant as its Design style; it is standalone, so it is offered in every section. BlockProductSlider makes the Product Slider available as a standalone block so it can sit in a container slot — configuration, data binding and rendering are identical to SectionProductSlider. ProductDetailButtonGroup and BlockProductDetailButtonGroup stack full-width calls to action on the product detail page. The Mega Menu, Mobile Menu (Shop) and Side-by-Side Menu blocks gain a Start Level control that descends the fetched category tree by the configured number of levels — for skipping a synthetic upstream root node without changing the query or connector. BlockPagination hides itself when the result set fits on a single page, with a showOnSinglePage toggle to keep it. And BlockFooterMenu items gain a Visible in markets list: leaving it empty shows the item everywhere, which is what every menu authored before this release does. The two pieces behind it are exported for other components to adopt — visibleMarketsField for the schema and inMarket() for the render.
  • Captions gain a Text Size setting, wired the same way heading and subline already are. CaptionFlag had its size hardcoded; it is now optional, with the variant-derived value as the fallback. The option list is a deliberate subset of the shared text-size scale: the caption utilities come from the --caption-* tokens, which stop at xl, so 2xl and above are not offered rather than offered and silently ineffective. The setting lands on the shared captionVariant style decorator, so every section and block that already uses it gets the control.
  • MediaVideo reports milestones: a milestone event fires as playback crosses each quarter of the video, carrying { milestone, currentTime, duration } where milestone is the fraction reached — 0.25, 0.5, 0.75 or 1, each once per mounted player. useMediaMilestones(playback, onMilestone, config?) is the headless detector behind it, for wiring the same behaviour to a non-native player; it takes reactive currentTime and duration, knows nothing about media elements or analytics, and accepts { fractions } to replace the default quarters.
  • SocialShare reports the platform picked: it emits share with the platform a visitor chose, and BlockSocialShare reports it as a web/share event. The payload carries method alone — the platform id (facebook, x, linkedin, pinterest, email). The block binds to no entity, so what was shared is the page, and the ambient page context already puts its url, path and type on every event. Every button is an outbound link to the platform's own dialog, so the event marks the click rather than a completed share.
  • Login and account: a visitor who wants to write a product review is now sent to their account. The login prompt resolves through the ecommerce/auth/login-oauth action, so it works with whichever connector answers it — an OAuth authorization URL where accounts are hosted, or a link to the account page where the storefront renders its own. The account button and the review section's login prompt redirect to customer login instead of opening a popup, and carry the current path so the customer comes back to it; that needs a connector that reads AuthLoginOauthAction's returnTo input, and against one that ignores it login still works. The login popup also no longer fails silently in Safari and Firefox: it is opened while the click is still a trusted gesture, and falls back to a full-page redirect where a popup is blocked anyway. And a signed-in customer's cart now carries their identity, so checkout opens authenticated and the resulting order attaches to their account.
  • A link naming several option values opens the right variant: a product-detail URL that names several option values opens the variant carrying all of them. Any single matching value was previously enough, so ?variant=Red&variant=XL could open a red product in the wrong size. A URL naming one value still opens the first variant carrying it.
  • A Child Categories link, and category cards from a data source (2.8.6): new is the ChildCategoriesLink token (ecommerce/category/child-categories) in canonical-types. The Shopify connector implements it by reading the navigation menu (new categoryMenuHandle option, default main-menu), locating the source collection's node and returning its collection children as inline Category entities, localized via @inContext. SectionCategoryCardSlider gains an optional Category query whose entities render as cards; manual slot cards still take precedence.
  • MenuSideBySide gains a below-separator slot on the root level. The area under the root drawer's separator rendered MobileMenuListItems from the rootMenuItems prop and nothing else, so a storefront whose quick links are styled buttons rather than list rows had nowhere to put them. The slot is forwarded at every level between a consumer and it, each time only when a consumer actually provides it.
  • ProductListingGrid accepts a productTile scoped slot, bound as { product }, matching the one ProductSlider already exposes; its fallback is the tile it rendered before. CartSummaryBox emits checkout when its call to action is clicked, and CartSheet forwards it — the button keeps its href, so navigation is unchanged.
  • CartSheet can be told how many items its header pill should report, via the new optional cartContent.itemCount. The pill was hardcoded to the number of lines, which only equals the number of items while every line has quantity 1 — a storefront whose header cart badge counts units therefore showed two different numbers for the same cart: two products at quantity 3 read 6 in the header and 2 in the drawer. Without the value it falls back to the line count.

Fixes & Improvements

  • Fixed: QuantityPicker removed an item by mistake. It emitted delete when the quantity field lost focus at the minimum value, or when the minus button stepped down to it — so clicking the quantity input of a cart line at quantity 1 removed the line. delete now fires only for the delete button at the minimum, or a value typed below it.
  • Fixed: the product tiles in a Product Grid show their add-to-cart button, brand, description, flags and rating again. ConnectedProductTileBasic declared each toggle as an optional boolean without a default, so Vue cast an unset one to false and the component forwarded that false on to ProductTileBasic — beating the tile's own default: true. BlockProductsListing passes none of these toggles, so every tile in a grid rendered with all five switched off.
  • Fixed: a price in a commerce event is computed for the variant the event names, not the product it belongs to. A product's own price is a "from" price across its variants, and the projection already reported the active variant's sku and name — so pairing those with the product's price priced one variant as another, understating the amount by an order of magnitude on a product with a wide variant spread. projectAnalyticsProduct now reads the active variant's prices and falls back to the product's only where the variant has none. A variant price never carries the product's strike-through price, since carrying it across would invent a discount. A tile's add_to_cart value is computed from the variant actually being added.
  • Fixed: product tiles keep rendering when a variant is missing the components they read. A query result can legitimately carry a product variant whose components have not been resolved; the shared product-tile mapper read options.selected and availability.status off every variant, so a single such variant took the whole Product Grid, Product Slider or Product Slider – Showcase down behind its error boundary. Those variants are now skipped, and the tile renders with a momentarily shorter size list.
  • Fixed: the add-to-cart button on tiles in the Product Slider – Showcase section did nothing when clicked — no spinner, no toast, no cart request — while the same button in the plain Product Slider worked. Both now behave identically: per-tile loading state, success and error toasts, and the cart count updating. ProductSliderShowcase also gains an optional productTile slot.
  • Fixed: CategoryCardSlider and CategoryCardGrid render their nodes prop and their default slot together, nodes first. Previously the default slot replaced nodes entirely, and because the Category Card sections always pass one, nodes never rendered through them at all — so a Category Card Slider bound to a category data source showed an empty slider.
  • Fixed: the filter off-canvas accordion opened the wrong panel. FilterOffCanvasAccordionItem used activeValue (the display text) as its reka AccordionItem identity, so every unselected list and range filter shared value="" and clicking any trigger opened the first one, overlapping the others. The item now takes a dedicated, unique value prop for identity.
  • Fixed: the Filter Bar's header and quick-filter rows line up with the surrounding page content on large screens. Their horizontal padding was a fixed --spacing-ml, narrower than the page container's own, so both rows sat slightly inside the content column from lg upwards. They now read --container-padding.
  • Fixed: BlockMedia's Sizing setting applies to the image only, not to the image plus its description. Sizer wrapped the whole block, so an aspect ratio of 16/9 was the ratio of image and caption together: the image lost exactly the height the caption took, and a longer caption shrank it further. MediaPreview now takes an optional sizing and applies it to its media box, with the description below it and outside the sized box.
  • Fixed: a crash on a hero slide that shows a caption. SectionHeroSlider called resolveCaptionVariant without importing it, and ui-app registers no auto-import directory, so building the slide props threw a ReferenceError. Only a slide with at least one visible caption reached the call, which is why it stayed latent.
  • Fixed: BrandHero shows a background colour when no background image is set. fallbackColor was passed to MediaStage all along, but finalMedia fell back to the theme's decorative hero SVG whenever background was empty — so the colour was painted behind an opaque image. An explicit colour now takes precedence; the theme default still applies when no colour is set. The field is relabelled "Background Color" in SectionBrandHero. BrandHero's description also becomes rich text: the field was a textarea, which takes typed copy only — and on a brand page its whole point is to carry the brand's own description from the catalogue. As richtext, Studio offers a data source on it; the prop widens to string | HtmlFragment and callers passing a plain string keep working.
  • Fixed: the filled heart icon (essentials/heart-filled) rendering at size="s" and size="m".
  • Added: the review.logIn string in all seven shipped locales — the review login prompt's button was hardcoded English.
  • The Product Slider – Showcase now defaults Show Product Flags to on, matching the standard Product Slider (new sections only; existing configurations are unaffected). And productTileMapper emits a sizeVariants array ({ value, label, disabled }, keyed by variant id, disabled for sold-out sizes); the shared query's variant limit rises from 5 to 30 for it, so full size runs are no longer truncated.

Notes for Developers (Breaking Changes)

  • AuthLoginOauthAction (ecommerce/auth/login-oauth) no longer returns a bare authorizationUrl (2.11.0), but a discriminated union: the OAuth authorization URL when the customer has no session, and a Link to their account when they do. Narrow on type before reading either. The account destination is a Link rather than a URL string, so the frontend resolves it — Shopify returns an external link to its hosted account page, and a connector whose account is a storefront page returns a pageType link from the same contract.
  • Review reports the intent instead of handling it itself (2.17.0): it emits submit with the form data when the review form is sent, and login when the visitor confirms the login prompt. The login prompt's primary button no longer opens a confirmation dialog of its own — it emits login, and the consumer decides where the visitor goes. A Review rendered without a login listener shows the prompt but cannot log anyone in.
  • CartListItem marks a line whose merchandise cannot be bought (2.16.0): it takes a new isSoldOut prop, renders an "Out of stock" badge and carries data-unavailable on its root for styling; ConnectedCartSheet sets it from the cart item's availability component. A custom locale must supply the new cart.itemSoldOut key.
  • SectionCategoryCardSlider no longer pads itself by default (2.14.0): the section overrode the shared paddingField with default: 's', so every instance started with vertical section padding nobody asked for — and a section that pads itself cannot be placed flush against its neighbour. Behaviour change: existing instances that never touched the Padding field lose that padding. Set it to S explicitly to keep the old look. Seven other sections carry the same default: 's' override and are deliberately left alone here.
  • Container's desktop gap switches at 1280px instead of 800px (2.10.0): containers with an explicit desktop gap and no tablet gap now use the mobile gap between 800 and 1279px where they previously used the desktop gap — the same breakpoint unification the Sizer already applies. Column behaviour is unaffected.
  • BlockPagination hides itself on single-page result sets (2.13.0). Behaviour change: storefronts that showed the controls on single-page listings will stop doing so. Switch showOnSinglePage on in the block's Rules group to keep the old rendering — worth doing where a listing's height should not change as filters narrow the result set, since a control appearing and disappearing moves everything below it.
  • A BrandHero with a background colour and no image switches from the theme graphic to that colour (2.15.0). That is the reported bug, but the change is visible — clear the colour to keep the graphic.
  • Every listing and slider URL for products with a derived default variant changes (2.20.0), because the ?variant= leaves it. Explicitly shared links are untouched.

Cockpit (Studio) August 2026

Highlights

  • Studio keeps where you were in the URL: your open page, selection and preview live in the URL — reload or share a link to return exactly where you were. The selected market and language are kept there too, so a reload or a shared link opens the same market and language instead of the project default.
  • Markets have a status: Draft or Active, new markets start as Draft, and the market cards show Draft and Default badges. A project can choose its default market from its active markets, and the selector names the fallback used when none is chosen.
  • Sorting in the query editor: set a sorting on a query to change the default order of its results.
  • Link fields from a query result: a link field can now take its target from a query result, the same way media fields already can.
  • An Abandoned Props panel in the Studio Devtools: a new panel reports stored props whose field a section or block no longer defines, or whose value no longer fits it — and can prune them.
  • Redirects on the Vercel edge: a managed-Vercel deploy publishes its redirects to the Vercel edge, which answers them before any route in the deployment, so an old URL no longer costs a function invocation.
  • The preview opens at the desktop frame (1920) instead of stretching to the width of the editor window.

Fixes & Improvements

  • Fixed: importing a redirect CSV with more than 256 rows now works.
  • Fixed: projects with more than 1000 redirects deploy every redirect, export the full list to CSV, and re-import without creating duplicates.
  • Fixed: editing a query's limit, label, URL alias and input rules in Studio now saves.
  • Fixed: Studio reports a rejected project secret and names the project the host serves, with a link to that project.
  • Fixed: clearing a number field resets it to its default, and a field set to 0 shows the 0.
  • Fixed: billing settings are now always available, so you can open the billing portal before a payment method has been added.

Find the full changelogs with all technical details at docs.laioutr.io/getting-started/changelogs.

D'autres articles intéressants

Un savoir-faire concret pour le développement frontend, les agents intelligents et le headless

Shopify
Shopify ist eine Commerce-Plattform zum Verkaufen online und im stationären Handel.
Shopware
Shopware ist eine flexible E-Commerce-Plattform aus Europa für Produktkataloge und Omnichannel-Commerce.
Planned
Scayle
SCAYLE ist eine Commerce-Engine, mit der Marken und Händler ihr Geschäft skalieren.
Planned
Commerce Layer
Commerce Layer ist eine Headless-Commerce-Plattform, um Bestände und Kataloge online verfügbar zu machen.
Planned
Salesforce Commerce Cloud
Salesforce Commerce Cloud ist eine cloudbasierte Enterprise-Commerce-Plattform für Unternehmen jeder Größe.
Commercetools
Commercetools ist eine SaaS-basierte, headless E-Commerce-Plattform mit weltweitem Einsatz.
Sylius
Sylius ist ein entwicklerfreundliches E-Commerce-Framework für B2C- und B2B-Shopping-Erlebnisse.
OXID eShop
OXID eShop ist eine erweiterbare Commerce-Plattform für komplexe B2B- und B2C-Anforderungen.
Emporix
Emporix ist eine composable, API-first Commerce-Plattform für skalierbare B2B- und B2C-Szenarien.
Adobe Commerce
Adobe Commerce ist eine Enterprise-Commerce-Plattform für komplexe, globale B2C- und B2B-Szenarien.
Coming Soon
VTEX
Cloud-native, composable Commerce-Plattform für B2B und B2C im großen Maßstab.
Planned
Spryker
Composable Commerce-Plattform für anspruchsvolle B2B- und B2C-Geschäftsmodelle.
Planned
SAP Commerce Cloud
Enterprise-Commerce-Plattform für komplexe Kataloge, Preismodelle und Omnichannel-Journeys.
Planned
Websale
Stabiles, enterprise-taugliches Commerce-Backend für komplexe Handelsumgebungen.
Planned
Intershop
Enterprise-Commerce-Plattform für komplexe B2B- und B2C-Geschäftsmodelle.
Planned
Magento 2
Weit verbreitete, erweiterbare Commerce-Plattform für B2C- und B2B-Szenarien.
Planned
B2Bsellers
B2B-Suite für Shopware, die den Online-Shop zur professionellen B2B-Commerce-Plattform macht.
Planned
Saleor
Open-Source-, API-first-Commerce-Plattform auf GraphQL-Basis für Custom-Storefronts.
Planned
Prestashop
Open-Source-Commerce-Plattform für kleine und mittlere Händler in Europa und darüber hinaus.
Planned
Vendure
Vendure ist eine Headless-Commerce-Plattform für Unternehmen mit komplexen Anforderungen.
Planned
Patchworks
Patchworks ist eine Low-Code-iPaaS, die E-Commerce, ERP, WMS, 3PL und Marktplätze verbindet.
Planned
HCL Software
Enterprise-Suite für digitalen Commerce und Experience mit hoher Konfigurierbarkeit.
Book a demo mobile
Entretien stratégique

Prêt à faire de votre frontend une véritable couche de pilotage ?

Montrez-nous votre stack, votre roadmap, votre scénario de replatforming, et nous vous montrerons comment Laioutr s'intègre, ce que cela coûte et à quelle vitesse vous passez en production.

« Après 30 minutes, nous savions que Laioutr rendait notre replatforming réalisable. » - Daniel B., CEO, hygibox.de