@xenterprises/nuxt-x-affiliate
v0.2.0
Published
A drop-in Nuxt layer for Amazon, Walmart, and multi-network affiliate sites.
Readme
@xenterprises/nuxt-x-affiliate
A drop-in Nuxt layer for Amazon, Walmart, and multi-network affiliate sites.
Hand a consuming site a single
extends: ['@xenterprises/nuxt-x-affiliate']line and it gets navbar, footer, FTC-compliant disclosure, product pages, category pages, search, deals, best-of, contact, disclosure, privacy, terms, and home — all driven byapp.config.tsoverrides andruntimeConfigenv vars.
Built for the 40–50 affiliate sites the team runs. Add the layer, set your network tags, override what you want. Everything else works.
Quick start
// nuxt.config.ts
export default defineNuxtConfig({
extends: ['@xenterprises/nuxt-x-affiliate'],
// ... your other config
});// app.config.ts
export default defineAppConfig({
xAffiliate: {
name: 'My Picky Site',
tagline: 'Only the best deals make the cut.',
domain: 'mypickysite.com',
defaultNetwork: 'amazon', // 'amazon' | 'walmart' | 'multi'
contactEmail: '[email protected]',
navbar: {
links: [
{ label: 'Home', to: '/', icon: 'i-lucide-home' },
{ label: 'Deals', to: '/deals', icon: 'i-lucide-tag' },
// ...
],
},
footer: { /* ... */ },
disclosure: { /* ... */ },
productCard: { /* ... */ },
},
});# env vars
NUXT_PUBLIC_AMAZON_TAG=yourtag-20
NUXT_PUBLIC_WALMART_AFFILIATE_ID=abc123
NUXT_PUBLIC_AFFILIATE_API_ENDPOINT=https://api.yoursite.comThat's it. Run nuxi dev and the site has a homepage, deals page, product
pages, category pages, search, contact, legal pages, FTC disclosure — all
working against the bundled mock data until you wire up a real API.
Public API
Composables
| Composable | What it does |
| --- | --- |
| useAffiliate() | Core data API. Returns { list, get, featured, deals, related, categories, category, categoryProducts }. Each call returns the standard useAsyncData shape ({ data, pending, error, refresh }). |
| useAffiliateConfig() | Resolved config with sensible fallbacks. Returns { config, productCard, productGrid, disclosure, navbar, footer, seo }. |
| useAffiliateContent() | v0.2.0 — XAC content API. Returns { config, getMerchant, hasTag, taggedUrl, taggedLink, formatPrice, trackClick }. |
| useAffiliateLink() | Build affiliate-tagged URLs. { build(url, network), forProduct(product) }. |
| useAffiliateTracker() | Fire-and-forget click tracking. Uses sendBeacon when available. |
Components
All components are auto-imported. Naming follows the team convention:
X/<Namespace>/<Component> → <X<Namespace><Component>>.
XAffNavbar
XAffFooter
XAffFooterLegal # bottom-bar legal footer with FTC disclosure band
XAffDisclosure
XAffSearchBar
XAffPrice
XAffStarRating
XAffNetworkBadge
XAffBuyButton
XAffProductCard
XAffProductGrid
XAffProductDetail
XAffCategoryCard
XAffCategoryGrid
XAffCategoryList
XAffHomeHero
XAffHomeFeaturedProducts
XAffHomeTopDeals
XAffHomeCategories
XAffHomeHowItWorks
XAffHomeNewsletter
XAffHomeFAQ
XAffHomeDisclosurePages
The layer ships the following pages. Consuming sites can override any of them
by creating a file with the same path in their own app/pages/.
/ Home (hero, featured, deals, categories, FAQ)
/categories All categories
/categories/[slug] Category detail
/product/[slug] Product detail
/search Search results
/deals Today's top deals
/best/[slug] Best-of roundup
/about About
/contact Contact form
/disclosure FTC affiliate disclosure
/privacy Privacy policy
/terms Terms of serviceLayouts
layouts/default.vue Navbar + slot + Footer (the default for every page)
layouts/blank.vue Slot only — for custom layouts in consuming sitesTypes
import type {
Product,
ProductListResponse,
Category,
AffiliateNetwork,
ProductCardConfig,
ProductGridConfig,
AffiliateDisclosure,
Money,
XAffiliateConfig,
} from '#affiliate/types'; // (also available from the global auto-imports)Configuration reference
app.config.ts — xAffiliate
export default defineAppConfig({
xAffiliate: {
// Identity
name: 'Best Picks',
tagline: 'Hand-picked deals, updated daily.',
domain: 'bestpicks.example.com',
contactEmail: '[email protected]',
logo: '/logos/light.webp',
logoDark: '/logos/dark.webp',
// Default network when a product has none specified
defaultNetwork: 'amazon' as AffiliateNetwork,
// Top-level navigation
navbar: {
links: [
{ label: 'Home', to: '/', icon: 'i-lucide-home' },
{ label: 'Deals', to: '/deals', icon: 'i-lucide-tag' },
// ...
],
},
// Footer columns + body copy
footer: {
body: 'As an Amazon Associate and Walmart Affiliate, we earn from qualifying purchases.',
columns: [
{ headerLabel: 'Shop', links: [{ label: 'Deals', to: '/deals' }] },
// ...
],
socials: [
{ name: 'Twitter', url: 'https://twitter.com/...', icon: 'i-simple-icons-twitter' },
],
},
// FTC disclosure (mandatory)
disclosure: {
shortText: 'Short banner text shown above the fold.',
longText: 'Full disclosure shown in /disclosure and the footer.',
dismissable: true,
},
// Product card defaults
productCard: {
showImage: true,
showRating: true,
showPrime: true,
showFreeShipping: true,
showBadges: true,
showNetwork: true,
showStrike: true,
imageAspect: 'video', // 'square' | 'video' | 'wide'
variant: 'default', // 'default' | 'compact' | 'horizontal' | 'list'
ctaLabel: 'View Deal',
ctaConfirm: false, // set true to show a confirm modal before navigating
},
// Product grid defaults
productGrid: {
cols: { base: 1, sm: 2, md: 3, lg: 4 },
gap: 'normal', // 'tight' | 'normal' | 'loose'
emptyText: 'No products found.',
loadingText: 'Loading products…',
},
// SEO defaults
seo: {
title: 'Best Picks — Hand-picked deals',
description: 'Hand-picked products from Amazon, Walmart, and more.',
ogImage: '/og.png',
},
},
});runtimeConfig.public — env vars
| Env var | Purpose |
| --- | --- |
| NUXT_PUBLIC_AFFILIATE_API_ENDPOINT | Backend API base. Empty = use bundled mock data. |
| NUXT_PUBLIC_AMAZON_TAG | Your Amazon Associates tag (e.g. yoursite-20). |
| NUXT_PUBLIC_WALMART_AFFILIATE_ID | Your Walmart affiliate ID. |
| NUXT_PUBLIC_TARGET_AFFILIATE_ID | Target affiliate ID. |
| NUXT_PUBLIC_BESTBUY_AFFILIATE_ID | Best Buy affiliate ID. |
| NUXT_PUBLIC_DEFAULT_NETWORK | Default network. |
| NUXT_PUBLIC_AFFILIATE_TRACK_ENDPOINT | Optional click-tracking beacon. |
Backend API contract
The data composable hits these endpoints (replace with your own backend):
GET /public/products
?category=<slug>
&network=<amazon|walmart|...>
&search=<q>
&sort=<price-asc|price-desc|rating|newest>
&page=<n>
&pageSize=<n>
→ ProductListResponse
GET /public/products/:slug → Product | null
GET /public/products/featured?limit → Product[]
GET /public/products/deals?limit → Product[]
GET /public/products/:slug/related? → Product[]
GET /public/categories → Category[]
GET /public/categories/:slug → Category | null
GET /public/categories/:slug/products → ProductListResponseType shapes
interface Product {
id: string;
slug: string;
externalId: string; // ASIN, Walmart item id, etc.
network: 'amazon' | 'walmart' | 'target' | 'bestbuy' | 'ebay' | 'etsy' | 'custom';
title: string;
description?: string;
brand?: string;
image: string;
images?: string[];
price: { amount: number; currency: string };
listPrice?: { amount: number; currency: string };
rating?: number; // 0-5
reviewCount?: number;
inStock?: boolean;
affiliateUrl: string; // base URL — useAffiliateLink() tags it
url?: string; // un-tagged fallback
categories?: string[];
features?: string[];
prime?: boolean;
freeShipping?: boolean;
badges?: Array<{ label: string; color?: string; variant?: string; icon?: string }>;
updatedAt?: string;
}
interface Category {
slug: string;
name: string;
description?: string;
image?: string;
icon?: string;
parentSlug?: string;
productCount?: number;
}Customization patterns
Override a page
<!-- app/pages/index.vue in your consuming site -->
<template>
<div>
<XAffHomeHero
:primary-cta="{ label: 'My Custom CTA', to: '/deals' }"
/>
<XAffHomeCategories />
<!-- ... -->
</div>
</template>Build a custom page using primitives
<script setup lang="ts">
const { list } = useAffiliate();
const { data: results } = list({ category: 'kitchen', sort: 'rating' });
</script>
<template>
<UContainer class="py-12">
<h1>Top kitchen picks</h1>
<XAffProductGrid :items="results?.items ?? []" />
</UContainer>
</template>Use only specific components
<script setup>
import { getNetworkMeta, formatPrice } from '@xenterprises/nuxt-x-affiliate/app/types/networks';
</script>Build a custom Buy button
<script setup lang="ts">
const { forProduct } = useAffiliateLink();
const product = ref(/* ... */);
const finalUrl = computed(() => forProduct(product.value));
</script>Compliance (FTC)
This layer is designed to make FTC compliance easy and consistent across all 40–50 consuming sites:
- In-content article disclosure —
<XACArticleDisclosure />belongs at the top of every review / article page (FTC 16 CFR §255.5 — "in close proximity" to the claim). Optional Published/Updated timestamps. - Disclosure banner —
<XAffDisclosure />renders a short banner at the top of every page by default. Dismissable is configurable. Toggle per-site viaxAffiliate.disclosure.banner: boolean(defaulttrue). See Hiding the top disclosure banner. - Disclosure page —
/disclosureships with full FTC text out of the box. - Legal footer —
<XAffFooterLegal />is the dedicated bottom-bar affiliate legal footer (FTC disclosure band + X Enterprises, LLC copyright + network chips + 5 off-site legal links tox.enterprises/legal/{slug}). Single source of truth for the copyright — the marketing footer's columns deliberately render no copyright of their own, and the redundant bottom-of-page<XFooterXLegal />is omitted, so there's no drift. - Copyright override — every consuming site renders the same
Copyright ©2016-{year} X Enterprises, LLC. All Rights Reserved.line by default. Override per-site via thecopyrightHolderconfig or the<XAffFooterLegal copyright="..." />prop. - Off-site legal home — legal links point to
https://x.enterprises/legal/{slug}by default (the canonical x.enterprises legal hub). OverridexAffiliate.legal.baseUrlper site to redirect to a local legal page. - Buy button —
<XAffBuyButton />can be configured to show a confirm modal (productCard.ctaConfirm: true) before navigating to the retailer. - Click tracking — every Buy button click can be sent to
NUXT_PUBLIC_AFFILIATE_TRACK_ENDPOINTviauseAffiliateTracker().
Override xAffiliate.disclosure in your app.config.ts to customize the text
per site.
Local development
npm install
npm run dev # starts playground on http://localhost:3000
npm run dev:prepare # regenerates .nuxt types
npm run build # builds playgroundThe playground runs against bundled mock data — no backend required.
Architecture
app/
├── app.config.ts # defaults (overridable per consuming site)
├── app.vue # UApp + NuxtLayout + NuxtPage
├── error.vue # 404 / error page
├── layouts/
│ ├── default.vue # Navbar + slot + Footer
│ └── blank.vue # slot only
├── pages/ # home, deals, search, product, category, legal...
├── components/
│ └── X/Aff/ # the public component surface
│ ├── *.vue # primitives
│ └── Home/*.vue # home page sections
├── composables/
│ ├── useAffiliate.ts # data API
│ ├── useAffiliateConfig.ts
│ ├── useAffiliateLink.ts # URL tagging
│ └── useAffiliateTracker.ts
├── types/ # shared TypeScript types
└── utils/mockData.ts # bundled fallback dataPlayground
The repo ships a .playground/ that demos the layer wired up with
@xenterprises/nuxt-x-marketing for a polished landing-page feel.
cd .playground
npm run devThe playground extends both layers:
// .playground/nuxt.config.ts
export default defineNuxtConfig({
extends: [
['..', { install: true }], // the affiliate layer
['@xenterprises/nuxt-x-marketing', { install: true }], // marketing chrome
],
modules: ['nuxt-shiki'],
});It combines xAffiliate (products, disclosure, navbar/footer) and
xMarketing (hero, sections, features, FAQ, CTA, footer columns) in a
single app.config.ts, and uses the marketing layer's transparent navbar
- footer as the site chrome.
Pages in the playground
| Route | Description |
| --- | --- |
| / | Marketing hero + affiliate home sections (categories, featured, deals, how-it-works, newsletter, FAQ, disclosure) |
| /deals | Marketing hero + all deals grid |
| /search | Search input + results grid |
| /categories | All categories (grid) |
| /categories/[slug] | Category detail with filters and sort |
| /product/[slug] | Product detail with related |
| /best/[slug] | Marketing hero + best-of grid |
| /about | Marketing hero + editorial standards + 4-step process + stats + FAQ + CTA |
| /blog | Featured post promo + post grid + newsletter CTA |
| /blog/[slug] | Blog post detail (placeholder) |
| /reviews/[slug] | Canonical review-page mockup — shows all 6 XAC* components wired together (XACArticleDisclosure + XACStarRating + XACBuyButton + XACAffiliateLink + XACProsCons) |
| /xac | XAC smoke test — exercises every variant of every XAC* component for visual QA |
| /contact | Contact form |
| /disclosure | FTC affiliate disclosure |
| /privacy | Privacy policy |
| /terms | Terms of service |
The marketing navbar uses <XMarkLayoutNavbar> (full path
X/Mark/Layout/Navbar.vue) — the marketing layer's own app.vue uses
XMarkNavbar and XMarkFooter, which don't actually exist. Use the
XMarkLayout* names (and XFooter / XFooterXLegal for the simpler
components in X/Footer/).
Versioning
This layer follows semver. Consuming sites should pin to a minor version:
{
"dependencies": {
"@xenterprises/nuxt-x-affiliate": "^0.2.0"
}
}v0.2.0 — XAC* component family for review sites
v0.2.0 ships a parallel component API aimed specifically at niche review sites
(coffee-mug-warmer reviews, ergonomic chair reviews, etc.) that are affiliates
of Amazon, Walmart, etc. The new XAC* family wraps the existing
useAffiliateLink / useAffiliateTracker plumbing with a cleaner review-site
API: smart defaults, half-star ratings, FTC disclosures baked in, multi-merchant
support, and slot-based links for custom designs.
The X/Aff/* family is still around for the broader affiliate product pages —
they're not being removed.
Install
// nuxt.config.ts
export default defineNuxtConfig({
extends: [
['@xenterprises/nuxt-x-marketing', { install: true }],
['@xenterprises/nuxt-x-schema', { install: true }],
['@xenterprises/nuxt-x-affiliate', { install: true }],
],
})// app.config.ts
export default defineAppConfig({
xAffiliateContent: {
currency: 'USD',
disclosure: {
text: 'Affiliate link. We may earn a commission at no cost to you.',
position: 'above', // 'above' | 'below' | 'none'
},
merchants: {
amazon: { tagValue: 'yourtag-20' },
walmart: { tagValue: 'TODO' },
},
},
})Components
All six XAC* components are auto-imported. See
docs/XAC.md for the full reference and migration guide.
| Component | Use for |
| --- | --- |
| <XACStarRating> | Half-star ratings with proper aria. |
| <XACBuyButton> | The moneymaker — image, price, CTA, disclosure, tagging, tracking. |
| <XACAffiliateLink> | Slot-based link when you want full visual control. |
| <XACProsCons> | 1- or 2-column pros / cons, auto-collapses if one side is empty. |
| <XACDisclosure> | FTC disclosure text in 4 visual variants. |
| <XACArticleDisclosure> | FTC 16 CFR §255.5 in-content snippet — sits at the top of every review / article page, in-flow with the article typography. Optional Published/Updated timestamps. |
Composable
const { taggedUrl, formatPrice, getMerchant, hasTag, trackClick } =
useAffiliateContent();taggedUrl(merchant, url)— appends the configured tag, idempotent.formatPrice(amount)— Intl-formatted with the configured currency / locale.getMerchant(id)/hasTag(id)— display metadata + tag presence.trackClick(payload)— pushesaffiliate_clicktowindow.dataLayerif GTM / GA4 is loaded. Harmless noop otherwise.
Testing
npm test # 93/93 should pass
npm run test:watch # watch modeTests live in tests/ and use Vitest + happy-dom + @vue/test-utils.
Migration: review pages
If your consuming site has app/pages/reviews/[slug].vue with inline markup
for stars, buy buttons, and disclosures, the migration is two diffs. See the
full guide at docs/XAC.md (or open the migration section in the playground's
internal docs).
