X Enterprises
nuxt-x-affiliate

Release Notes

Detailed release history for @xenterprises/nuxt-x-affiliate — what shipped in v0.3.5, v0.4.0, v0.4.1, v0.5.0, and v0.6.0, plus the breaking dashboard-folder consolidation landing in the next minor.

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: useConsentTrackinguseXAFConsentTracking

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

ComponentNew 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 0 when image is present. The thumbnail bleeds to the rounded border edge (no white frame around the photo). Text-only path is unchanged — sites that don't pass image get the same card as before.
  • Hero overlay color is still rendered behind image. 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.
  • imageAlt falls back to title. Safer than an empty alt (which screen readers treat as decorative — wrong for editorial imagery). Sites with non-decorative photos should always pass an explicit imageAlt.
  • imageAspect: "auto" for non-thumbnail imagery. Editorial illustrations and infographics preserve their natural aspect ratio with object-contain so 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 Props interface so IntelliSense covers image, imageAlt, imageAspect, and imageLoading.

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.


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

NewPurpose
<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.tsAuto-loads stored consent on revisit so re-visitors get scripts immediately (no flash of banner).
xAffiliate.tracking.scripts[] escape hatchPer-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 gtmId is set, the standalone GA4 + Clarity snippets are skipped (GTM owns them). One source of truth.
  • Aligned with nuxt-x-marketing. The XMarkPrivacyCookieConsent family in nuxt-x-marketing uses the same localStorage key + 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 dispatch consent:updated events yourself.

Tests + types

  • 22 new vitest tests in test/useConsentTracking.test.ts cover: 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 via XAffiliateTrackingScript so consumers get IntelliSense for id, 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 (consentDays removed since storage is now localStorage; cookieNamestorageKey). Update those two props if you used them.
  • Storage moved cookie → localStorage. Visitors who had consent stored in the old xaf_consent cookie 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

NewPurpose
<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:

CodeLabelFlagDir
enEnglish🇺🇸ltr
esEspañol🇪🇸ltr
frFrançais🇫🇷ltr
deDeutsch🇩🇪ltr
ptPortuguês🇵🇹ltr
itItaliano🇮🇹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 useXafLocale covering 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 to xAffiliate.brand.locale.
    • isAccessibleForFree?: boolean — defaults to true; SubscribeWithGoogle signal.
  • useSiteMeta() extended with keywords and alternate.
    • keywords?: string[] — emits <meta name="keywords">. Distinct from tags so 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.
  • SchemaArticle type extended. The new fields are part of the typed shape, so consumers get IntelliSense.
  • ArticleSchemaType exported. 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), and build:playground (consumer-side smoke test).
  • 47 pre-existing typecheck errors fixed. Surfaced when we first wired vue-tsc --noEmit properly. They broke down into six buckets:
    • Schema.org contract gaps. SchemaOrganization.logo, SchemaPerson.worksFor, SchemaReview.dateModified, AffiliateLink.productName were 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.vue had a similar off-by-one in a composables import.
    • @types/node missing. nuxt.config.ts and vitest.config.ts couldn't resolve node:url / path. Added as a devDependency.
    • noUncheckedIndexedAccess strict-mode issues. Byline.vue, DecisionMatrix.vue, Gallery.vue, Quiz.vue, SearchBar.vue, StickyCTA.vue — array index access and IntersectionObserver callback destructuring needed optional chaining or guards.
    • Cookie regex match results. CookieConsent.vue, ExitIntent.vue, OfferBanner.vue, useReferralTracking.ts, useReferralTracking.test.tsmatch[1] was being non-null-asserted; now guarded with match && match[1] ? ... : null.
    • Type cast tightness. AuthorBox.vue and useAffiliateContent.ts were casting appConfig directly to a narrower type; TS rejected as insufficient overlap. Now use as unknown as X per the official TS migration guidance.

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.yml introduced. Runs four jobs:
    • test:unit (required) — vitest run against the 131-test suite.
    • typecheck (required, on MRs + tags) — vue-tsc --noEmit via 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-run to confirm the tarball is clean.
  • prepublishOnly hook added. npm publish now runs npm test && npm run typecheck && npm run lint:pack first. If any step fails, publish aborts before pushing to the registry.
  • Typecheck script added. npm run typecheck runs nuxt prepare && vue-tsc --noEmit -p tsconfig.json. Useful for humans iterating locally before pushing.
  • Fix for useSchema.ts shadowing Nuxt's computed. The composable was doing import { computed } from "vue" + export { computed }, which shadowed Nuxt's auto-imported computed and produced a Nuxt prepare warning. Removed both. Components that previously got computed via the leaky re-export now get it via Nuxt's auto-import, which is what they should have been doing all along.
  • SchemaThing no longer needs index signature. The relaxed cleanSchema<T>(input: T): T generic means Schema.org types don't need [key: string]: unknown to satisfy the helper.
  • Duplicate xAffiliate augmentation deleted. Was in app/types/affiliate.ts, conflicted with the canonical one in app/app.config.ts. Now app.config.ts is the single source of truth for the augmentation.
  • .gitlab-ci.yml excluded 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

GroupNew 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.md for 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):

ComponentPurpose
<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:

ComposablePurpose
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:

PagesWhat's documented
22.pagination, 23.search-filters, 24.taxonomy-archive, 25.currency-switcherNew 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-archiveBackfill for the v0.3.3 expansion (the most-consulted 12)
composables/6.use-xaf-experimentNew 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 (XAFTaxonomyArchiveCollectionPage, XAFPagination is 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

Copyright © 2026