Release Notes
Release Notes
This page covers what shipped in v0.3.5 → v0.6.0 (the most recent development cycle), plus the breaking change landing in the next minor. For the full changelog of every version since v0.3.0, see CHANGELOG.md in the repo.
Next minor (unreleased) — 2026-07 — BREAKING: dashboard family folder consolidation
The 10 dashboard-family components moved from app/components/X/AF/ to app/components/XAF/. All 99 components now live flat in a single folder.
Who is affected: only consumers deep-importing component file paths — e.g.
// before
import XAFDashboard from '@xenterprises/nuxt-x-affiliate/app/components/X/AF/Dashboard.vue'
// after
import XAFDashboard from '@xenterprises/nuxt-x-affiliate/app/components/XAF/Dashboard.vue'
Who is not affected: everyone using the components normally. Auto-import names (XAFBanner, XAFDashboard, XAFSignupForm, …) are derived from the XAF prefix and are unchanged — no template, page, or config changes are needed for the standard consumption path. The dual-folder layout existed only because the two families were built at different times; path-prefix merging meant both already auto-imported as XAF*, so the consolidation is purely a repository-layout cleanup.
BREAKING: useConsentTracking → useXAFConsentTracking
The consent composable useConsentTracking() is renamed to useXAFConsentTracking(), and its client plugin consent-tracking.client.ts to xaf-consent-tracking.client.ts. nuxt-x-marketing ships an identically-named composable + plugin, and when a consuming site extends both layers (the standard affiliate stack is marketing + affiliate + schema), Nuxt's auto-import collision resolution silently shadowed one of them. Affiliate defers to marketing.
Who is affected: anyone calling useConsentTracking() from the affiliate layer directly. Update call sites to useXAFConsentTracking(). Behavior, the xAffiliate.consent storage key, the consent:updated window event, and the xAffiliate.tracking config namespace are all unchanged. Sites using only <XAFCookieConsent /> need no changes — the component calls the composable internally.
v0.6.0 — 2026-06-29 — image support for content cards + page heroes
The headline change in v0.6.0 is that <XAFContentCard> and <XAFPageHero> now ship first-class image support. Before v0.6.0 both components were intentionally text-only / flat-color — every consuming site that wanted a thumbnail on a card or a photo behind a hero had to fork the component locally or wrap it in a per-site shim. v0.6.0 fixes that with a single image prop on each component, so a <XAFContentCard image="/covers/foo.webp"> or <XAFPageHero image="/heroes/foo.webp"> opt-in gives the component the photo treatment without any per-site work.
What shipped
| Component | New props |
|---|---|
<XAFContentCard> | image, imageAlt, imageAspect (video / square / auto), imageLoading (lazy / eager) |
<XAFPageHero> | image, imageAlt |
Why this matters
- One change ships to all 50–100 planned consuming sites. v0.6.0 closes the single biggest visual gap on every consuming site: card grids and hero blocks can now show real photos without per-site forks.
- Card padding collapses to
0whenimageis present. The thumbnail bleeds to the rounded border edge (no white frame around the photo). Text-only path is unchanged — sites that don't passimageget the same card as before. - Hero
overlaycolor is still rendered behindimage. Consumers don't have to pick a "with image" vs. "without image" color scheme — the gradient darkens the bottom of the photo for legible white text on top. imageAltfalls back totitle. Safer than an empty alt (which screen readers treat as decorative — wrong for editorial imagery). Sites with non-decorative photos should always pass an explicitimageAlt.imageAspect: "auto"for non-thumbnail imagery. Editorial illustrations and infographics preserve their natural aspect ratio withobject-containso nothing is cropped.
Wire-up
<!-- Article card with cover image -->
<XAFContentCard
kind="article"
title="How we test mechanical keyboards"
href="/articles/how-we-test-keyboards"
date="2026-01-15"
image="/covers/how-we-test.webp"
image-alt="Hands on a keyboard during a typing test"
description="A behind-the-scenes look at our testing methodology."
/>
<!-- Review card with square product thumbnail -->
<XAFContentCard
kind="review"
title="Logitech MX Keys S review"
href="/reviews/mx-keys-s"
category="Logitech"
:rating="4.6"
:price="99.99"
image="/covers/mx-keys-s.webp"
image-alt="Logitech MX Keys S on a desk"
image-aspect="square"
description="The best low-profile wireless keyboard for typists."
/>
<!-- Hero with full-bleed background photo -->
<XAFPageHero
eyebrow="Reviews"
title="The best kitchen gear of 2026"
subtitle="After 200+ hours of testing, here are our top picks."
image="/heroes/reviews-cover.webp"
image-alt="Coffee gear arranged on a wooden countertop"
/>
Tests + types
- 196/196 tests passing (no change in test count — both components are visual and verified in the playground, not via vitest DOM mounting per the layer's logic-only test policy).
- 0 typecheck errors. New props are typed in the
Propsinterface so IntelliSense coversimage,imageAlt,imageAspect, andimageLoading.
Upgrade notes for consumers
No breaking changes. v0.6.0 is purely additive. Sites that don't pass image to either component get the same behavior as v0.5.2 — text-only cards, flat-gradient heroes.
To opt in to the new visual treatment, drop image="/covers/your-image.webp" on the card or hero, an image-alt="..." describing the photo, and (for content cards) optionally image-aspect="square" if you want a product-thumbnail grid instead of the default blog-thumbnail 16:9. For LCP performance on listing pages, pass :image-loading="eager" on the first 1–3 cards so the above-the-fold images aren't lazy-loaded.
v0.5.0 — 2026-06-28 — consent-aware cookie banner with auto-tracking injection
The headline change in v0.5.0 is a complete rewrite of the consent system. The old <XAFCookieConsent> was raw HTML buttons + cookie storage. The new one ships on Nuxt UI v4 primitives (UModal, UToggle, UButton, UBadge), persists to localStorage, and gates every tracking script on the matching consent category via a new composable — so GTM / GA4 / Clarity / Meta Pixel / Hotjar / custom scripts only fire after the visitor grants the right category.
What shipped
| New | Purpose |
|---|---|
<XAFCookieConsent> (rewritten) | Banner + preferences modal in one, on Nuxt UI v4. Default 4 categories: necessary (required), analytics, marketing, preferences. Override labels, descriptions, and category list. |
useConsentTracking() | Reads app.config.xAffiliate.tracking and dynamically injects GTM / GA4 / Clarity / Meta Pixel / Hotjar / custom scripts only after the matching consent category is granted. |
app/plugins/consent-tracking.client.ts | Auto-loads stored consent on revisit so re-visitors get scripts immediately (no flash of banner). |
xAffiliate.tracking.scripts[] escape hatch | Per-script entries with id, src, inline, category, attrs. Default category is analytics. |
Wire-up
// app.config.ts
export default defineAppConfig({
xAffiliate: {
tracking: {
gtmId: 'GTM-XXXXXXX', // GA4 + Clarity also managed here when set
scripts: [
{ id: 'meta-pixel', src: 'https://connect.facebook.net/en_US/fbevents.js', category: 'marketing' },
],
},
},
})
<!-- app.vue -->
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<XAFCookieConsent />
</template>
That's it. The banner handles the UI; the composable handles the wire-level side. They share state via the xAffiliate.consent localStorage key.
Why this matters
- GDPR / CCPA compliant by default. Default categories match GDPR's required/opt-in split. Reject All is offered (not buried in Customize).
- No double-counting. When
gtmIdis set, the standalone GA4 + Clarity snippets are skipped (GTM owns them). One source of truth. - Aligned with
nuxt-x-marketing. TheXMarkPrivacyCookieConsentfamily innuxt-x-marketinguses the samelocalStoragekey + event shape, so a site that extends both layers gets a single consent UI, not two. - Decoupled.
<XAFCookieConsent>handles the UI;useConsentTracking()handles the scripts. You can use the composable without the banner (e.g. with a TCF CMP) — just dispatchconsent:updatedevents yourself.
Tests + types
- 22 new vitest tests in
test/useConsentTracking.test.tscover: storage round-trip, default state, accept-all / reject-all, partial grants, GTM-takes-priority-over-standalone-GA4, custom script injection, dedupe, SSR safety, event broadcasting. - 181/181 tests passing (up from 138 in v0.3.5).
- 0 typecheck errors.
XAffiliateTracking.scripts[]typed viaXAffiliateTrackingScriptso consumers get IntelliSense forid,src,inline,category,attrs.
Upgrade notes for consumers
- No breaking changes for sites that don't ship the consent UI yet. The composable + plugin are opt-in via the existing
<XAFCookieConsent>mount. - Sites that shipped the old
<XAFCookieConsent>(raw HTML) get the new component transparently — the props API is a superset of the old one. Existing prop names (consentDays,cookieName) are renamed (consentDaysremoved since storage is nowlocalStorage;cookieName→storageKey). Update those two props if you used them. - Storage moved cookie → localStorage. Visitors who had consent stored in the old
xaf_consentcookie will see the banner once more. One-time UX hit, then they're back to a smooth experience.
v0.4.1 — 2026-06-28 — multi-language support
The headline change in v0.4.1 is that the layer now ships multi-language support out of the box. Up until now, every consuming site that wanted i18n had to roll their own locale state, persistence, and switcher — v0.4.1 provides a provider + composable that covers the common case.
What shipped
| New | Purpose |
|---|---|
<XAFLocaleProvider> | Wrapper component that provides locale state, persists choice in a cookie, dispatches xaf:locale:changed window event on switch, sets <html lang> and dir (for RTL), and offers an optional built-in footer switcher |
useXafLocale() | Reactive locale state composable. Reads shared state from a parent <XAFLocaleProvider> via inject; falls back to a local singleton outside a provider. Returns { locale, locales, setLocale, resolved, t, isReady } |
t(map, fallback?) | Tiny translator for the small strings the layer renders itself (switcher labels, "no results" copy). For full i18n, use vue-i18n on top |
Default locale list
XAFLocaleProvider ships with a default list of 9 common locales:
| Code | Label | Flag | Dir |
|---|---|---|---|
en | English | 🇺🇸 | ltr |
es | Español | 🇪🇸 | ltr |
fr | Français | 🇫🇷 | ltr |
de | Deutsch | 🇩🇪 | ltr |
pt | Português | 🇵🇹 | ltr |
it | Italiano | 🇮🇹 | ltr |
ja | 日本語 | 🇯🇵 | ltr |
zh | 中文 | 🇨🇳 | ltr |
ar | العربية | 🇸🇦 | rtl |
Override with the locales prop — pass any subset, or extend with custom ones (e.g. fr-CA, pt-BR).
Tests + types
- 159/159 tests passing (was 146; added 13 logic tests for
useXafLocalecovering default behavior, setLocale, RTL fallback, translator fallbacks, default locales, etc.). - 0 typecheck errors.
- Nuxt prepare: 0 warnings.
Upgrade notes for consumers
No breaking changes. Purely additive.
If you weren't using i18n before, nothing changes — don't add <XAFLocaleProvider> to your app. If you want multi-language support, wrap your app's root with <XAFLocaleProvider v-model="locale"> and call useXafLocale() from any component that needs to render or switch locale.
For full i18n (translation files, pluralization, ICU format), use vue-i18n on top — <XAFLocaleProvider> plays well with it because the locale ref can drive vue-i18n's locale too.
v0.4.0 — 2026-06-28 — full schema coverage for articles + reviews
The headline change in v0.4.0 is that the schema composables now cover the full Schema.org article shape that consuming sites have been hand-rolling on articles/[slug].vue and reviews/[slug].vue. The composable was previously emitting a bare @type: "Article" with just headline / image / datePublished / publisher. Now it supports BlogPosting, articleSection, keywords, wordCount, articleBody, inLanguage, and isAccessibleForFree — everything Google's article parser cares about.
What changed
useArticleSchema()enhanced for full BlogPosting coverage. New fields:type: "Article" | "BlogPosting" | "NewsArticle"— explicit discriminator; defaults to"Article".articleSection?: string— category / section.keywords?: string[]— joined with commas per Schema.org spec.wordCount?: number— drives Google's "x min read" annotation.articleBody?: string— full or excerpt body text.inLanguage?: string— BCP-47 tag, defaults toxAffiliate.brand.locale.isAccessibleForFree?: boolean— defaults totrue; SubscribeWithGoogle signal.
useSiteMeta()extended withkeywordsandalternate.keywords?: string[]— emits<meta name="keywords">. Distinct fromtagsso you can have separate lists for meta vs article schema.alternate?: { hreflang: string; href: string }[]— emits<link rel="alternate" hreflang="..." href="...">for international SEO.
- Head output tightened.
useSiteMeta()no longer emits empty<meta property="og:..." content="">tags when there's no image / twitter handle / publish date to put in them. Cuts 5–8 empty meta tags per page. SchemaArticletype extended. The new fields are part of the typed shape, so consumers get IntelliSense.ArticleSchemaTypeexported.import type { ArticleSchemaType } from "@xenterprises/nuxt-x-affiliate"for consumers that want to discriminate on the schema kind.
Next-round consumer migration
These new fields unblock a one-PR migration on each consuming site that replaces ~150 lines of hand-rolled JSON-LD per page with two composable calls:
<!-- articles/[slug].vue — before: ~80 lines of JSON-LD → after: 1 call -->
useArticleSchema({
type: 'BlogPosting',
headline: article.title,
description: article.excerpt,
image: article.heroImage,
datePublished: article.publishedAt,
dateModified: article.updatedAt,
articleSection: article.category,
keywords: article.tags,
wordCount: article.wordCount,
articleBody: article.bodyText,
inLanguage: 'en-US',
})
<!-- reviews/[slug].vue — before: ~150 lines (BlogPosting + Review + Product) → after: 2 calls -->
useArticleSchema({ type: 'BlogPosting', headline, articleBody, ... })
useProductSchema({ product: {...}, ratingValue, ratingCount, offer: {...}, reviewBody, ... })
The useSiteMeta() additions similarly consolidate the 8-line useSeoMeta + useHead boilerplate that lives on every standard page (/, /about, /contact, /category, /search, etc.).
Tests + types
- 146/146 tests passing (up from 138; added 8 new tests).
- 0 typecheck errors.
- Tarball: 122 files, 106.1 kB unpacked.
Upgrade notes for consumers
No breaking changes. v0.4.0 is purely additive. Existing useArticleSchema() and useSiteMeta() calls work unchanged. New fields are optional.
v0.3.5 — 2026-06-28 — typecheck gate
The headline change in v0.3.5 is that typecheck is now a hard gate. Every push and MR must pass vue-tsc --noEmit with zero errors before the playground can be built, before tests run, and before the package can be published.
What changed
- Strict typecheck wired into CI and
prepublishOnly. Three jobs now run on every MR:test:unit(vitest, 131/131 tests),typecheck(vue-tsc, 0 errors), andbuild:playground(consumer-side smoke test). - 47 pre-existing typecheck errors fixed. Surfaced when we first wired
vue-tsc --noEmitproperly. They broke down into six buckets:- Schema.org contract gaps.
SchemaOrganization.logo,SchemaPerson.worksFor,SchemaReview.dateModified,AffiliateLink.productNamewere all missing from the type definitions — the components used them but the type was incomplete. All added. - Wrong relative imports. Six
X/AF/*components imported from'../../types'instead of'../../../types'. Worked at runtime (Nuxt auto-imports + types are erased) but typecheck caught it.XAF/Bundle.vuehad a similar off-by-one in a composables import. @types/nodemissing.nuxt.config.tsandvitest.config.tscouldn't resolvenode:url/path. Added as a devDependency.noUncheckedIndexedAccessstrict-mode issues.Byline.vue,DecisionMatrix.vue,Gallery.vue,Quiz.vue,SearchBar.vue,StickyCTA.vue— array index access andIntersectionObservercallback destructuring needed optional chaining or guards.- Cookie regex match results.
CookieConsent.vue,ExitIntent.vue,OfferBanner.vue,useReferralTracking.ts,useReferralTracking.test.ts—match[1]was being non-null-asserted; now guarded withmatch && match[1] ? ... : null. - Type cast tightness.
AuthorBox.vueanduseAffiliateContent.tswere castingappConfigdirectly to a narrower type; TS rejected as insufficient overlap. Now useas unknown as Xper the official TS migration guidance.
- Schema.org contract gaps.
Upgrade notes for consumers
No breaking changes. v0.3.5 is purely an infrastructure + correctness release. Every component prop, every composable signature, every schema shape is the same as v0.3.4.
If you're using TypeScript strict mode in your consuming site, you'll benefit from the tightened SchemaOrganization.logo type (now accepts the ImageObject shape Google prefers). Update your app config accordingly if you had type assertions pinned to string.
v0.3.4 — 2026-06-28 — CI + publish hardening
The headline change in v0.3.4 is the CI pipeline + publish gate. Before v0.3.4, npm test was a manual step someone had to remember before npm publish. Now it's automated.
What changed
.gitlab-ci.ymlintroduced. Runs four jobs:test:unit(required) —vitest runagainst the 131-test suite.typecheck(required, on MRs + tags) —vue-tsc --noEmitvia Nuxt's generated tsconfig. Promoted from "advisory" to "required" in v0.3.5.build:playground(required) —nuxt build .playground. The playground extends the layer the same way a consuming site would, so this is the consumer-side smoke test.publish:verify(tag-only) —npm pack --dry-runto confirm the tarball is clean.
prepublishOnlyhook added.npm publishnow runsnpm test && npm run typecheck && npm run lint:packfirst. If any step fails, publish aborts before pushing to the registry.- Typecheck script added.
npm run typecheckrunsnuxt prepare && vue-tsc --noEmit -p tsconfig.json. Useful for humans iterating locally before pushing. - Fix for
useSchema.tsshadowing Nuxt'scomputed. The composable was doingimport { computed } from "vue"+export { computed }, which shadowed Nuxt's auto-importedcomputedand produced a Nuxt prepare warning. Removed both. Components that previously gotcomputedvia the leaky re-export now get it via Nuxt's auto-import, which is what they should have been doing all along. SchemaThingno longer needs index signature. The relaxedcleanSchema<T>(input: T): Tgeneric means Schema.org types don't need[key: string]: unknownto satisfy the helper.- Duplicate
xAffiliateaugmentation deleted. Was inapp/types/affiliate.ts, conflicted with the canonical one inapp/app.config.ts. Nowapp.config.tsis the single source of truth for the augmentation. .gitlab-ci.ymlexcluded from npm pack. Tarball stays at 113 files / 88.9 kB.
Upgrade notes for consumers
No breaking changes. v0.3.4 is infrastructure-only. If you're consuming the layer via extends: [['@xenterprises/nuxt-x-affiliate', { install: true }]], no changes needed.
v0.3.3 — 2026-06-27 — XAF content family v3 expansion
The headline change in v0.3.3 is 21 new XAF components (53 → 78 total), bringing the layer to feature parity with most niche review-site templates you can buy or hire out.
What shipped
| Group | New components |
|---|---|
| Site chrome | <XAFSiteHeader>, <XAFSiteFooter>, <XAFMegaMenu>, <XAFNotFound> |
| Discovery | <XAFLatestReviews>, <XAFCategoryIndex>, <XAFTrending>, <XAFAuthorArchive>, <XAFSearchResults> |
| Engagement loops | <XAFAskQuestion>, <XAFCommentList>, <XAFCommentForm>, <XAFPoll>, <XAFQuiz> |
| Conversion | <XAFBundle>, <XAFCountdown>, <XAFOfferBanner>, <XAFShippingBar> |
| Stats / decision | <XAFStatHighlight>, <XAFDecisionFlow> |
| Retention | <XAFNewsletterArchive> |
Also in v0.3.3
- Agent team (
.harness/). Multi-agent orchestration: developer, tester, docs-writer, publisher. The orchestrator routes work between them. - Memory layer.
.harness/memory/MEMORY.mdfor shared team state — open issues, architectural invariants, conventions.
Upgrade notes for consumers
No breaking changes. v0.3.3 is purely additive. All 78 components coexist; nothing was renamed or removed.
If you were using <XAFSearchBar> (a pre-v0.3.3 single-page widget), v0.3.3 added <XAFSearchResults> for the search results page. Pair them: <XAFSearchBar> in your header → navigate to /search?q=... → <XAFSearchResults> on the results page.
CI + publish workflow (new in v0.3.4)
┌──────────────────────────────────────────────────────────┐
│ push to main / MR │
└──────────────────────────────────────────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
test:unit typecheck build:playground
(vitest) (vue-tsc) (nuxt build)
131 tests 0 errors consumer smoke
│ │ │
└───────────────┼───────────────┘
▼
all required jobs
│
▼
┌─────── MR can merge ───────┐
│ │
▼ ▼
tag (v0.3.5+) npm publish
│ │
▼ ▼
publish:verify prepublishOnly
(npm pack --dry-run) (test + typecheck + pack)
What prepublishOnly actually does
npm test # 131 vitest assertions
npm run typecheck # vue-tsc, must be 0 errors
npm run lint:pack # npm pack --dry-run, must be 113 files
If any of those fails, npm publish aborts before anything hits the registry.
v0.3.5 (second cut) — 2026-06-28 — feature expansion
The first v0.3.5 cut shipped the typecheck gate + Nuxt 4 config plumbing fixes (above). This second cut adds the missing backlog items and brings the docs site up to date.
What shipped
5 new components (84 → 89 total in the XAF* review-site family):
| Component | Purpose |
|---|---|
<XAFPagination> | Numbered page nav with prev/next, ellipsis gaps, query or path-style URLs |
<XAFSearchFilters> | Faceted filter sidebar (checkbox / radio / range) with v-model state and clear-all |
<XAFTaxonomyArchive> | Category/tag/topic archive grid with cover images, list/grid layouts, auto-emitted CollectionPage schema |
<XAFCurrencySwitcher> | Currency picker (dropdown or compact pill) with cookie persistence + xaf:currency:changed window event |
<XAFPageHero> (backfilled) | Full-viewport marketing hero — distinct from the per-review <XAFHero> |
<XAFPageSection> (backfilled) | Layout primitive with bg + padding variants for in-page sections |
1 new composable:
| Composable | Purpose |
|---|---|
useXafExperiment() | Sticky A/B testing hook with weighted random pick, deterministic bucketing by userId, TTL re-randomization, xaf:experiment:exposed event tracking |
Docus docs backfill — 16 new pages in this site, plus updates:
| Pages | What's documented |
|---|---|
22.pagination, 23.search-filters, 24.taxonomy-archive, 25.currency-switcher | New components from this cut |
26.site-header, 27.site-footer, 28.mega-menu, 29.faq, 30.hero, 31.latest-reviews, 32.category-index, 33.search-bar, 34.search-results, 35.newsletter, 36.trending, 37.author-archive | Backfill for the v0.3.3 expansion (the most-consulted 12) |
composables/6.use-xaf-experiment | New composable |
release-notes.md (this page) | Updated with the expansion section |
Docus coverage went from 21 → 38 component pages (still missing ~50 of the 89 — track in next milestone).
Tests + types
- 138/138 tests passing (up from 131; added 7 logic tests for
useXafExperiment). - 0 typecheck errors.
- New components emit their own schemas (
XAFTaxonomyArchive→CollectionPage,XAFPaginationis SEO-friendly by always linking page 1 to the bare URL).
Upgrade notes for consumers
No breaking changes. Purely additive.
If you're using useAffiliateContent()'s config.currency, you can now wire the value into <XAFCurrencySwitcher v-model> to give visitors a currency picker — the switcher emits xaf:currency:changed so any price-rendering code can listen and re-format.
Open follow-up
- Multi-language support (
<XAFLocaleProvider>) — the last backlog item. Tracked in CHANGELOG.md "Unreleased" section. - Docus backfill: still missing ~50 component pages (the v0.3.3 expansion had 21 components, of which the top 12 are now documented; the lower-priority 9 + ~30 v0.3.1/v0.3.2 components still need pages). Tracked for next milestone.
See also
- Configuration — full
app.config.tsreference - Composables —
useAffiliate,useAffiliateContent,useReferralTracking,useAffiliateImpression,useSchema,useXafExperiment - Components — full 89-component index
- GitLab repo
Configuration
app.config.ts reference for @xenterprises/nuxt-x-affiliate — covers xAffiliate (dashboard + brand + author + legal + tracking) and xAffiliateContent (merchants + currency).
nuxt-x-tenancy
Multi-tenant resolution layer for Nuxt 4 — bridges the better-auth active organization into a tenant vocabulary with path/subdomain/header resolution strategies, X-Tenant-ID request injection, and per-tenant useXCrud cache scoping with flush-on-switch.
