@herazgo/varykit-nuxt
v0.3.2
Published
Nuxt 4 module for VaryKit: file-based page testing plus SSR-safe element-level targeting.
Maintainers
Readme
@herazgo/varykit-nuxt
Nuxt 4 module for VaryKit: file-based page testing plus SSR-safe element-level targeting.
This module depends on @herazgo/varykit-core as a real dependency — you never need to install it separately. For plain Vue (no Nuxt), use @herazgo/varykit-vue, which has the same API shape with an app-level createVaryKit() provider.
The experiment model
Tests are organized as named experiments. Each experiment defines its own
segment universe and gets its own cookie (vary:<experiment>), so assignments
are independent: a visitor can be hero: b and pricing: control at the same
time. One experiment is designated as the router experiment; its segments
drive file-based page testing.
Install
pnpm add @herazgo/varykit-nuxtSetup
Add @herazgo/varykit-nuxt to the modules array of your nuxt.config.ts:
export default defineNuxtConfig({
modules: ['@herazgo/varykit-nuxt'],
varykit: {
experiments: {
landing: {
segments: { a: 0.2, b: 0.5 }
},
hero: {
segments: { a: 0.5, b: 0.5 },
endWith: 'b',
endsAt: '2026-10-14T00:00:00Z'
}
},
routerExperiment: 'landing'
}
})File-based page testing
Create variant files using the __variant naming convention (flat or folder-based):
pages/
contact.__a.vue
contact.__b.vue
contact.__c.vueAll variants collapse into one route (/contact). The visitor's segment in the router experiment is used directly as the variant suffix, so with the config above (landing: { a, b }) only contact.__a.vue and contact.__b.vue are served — contact.__c.vue is never shown because there is no c segment in landing.
All page tests share the router experiment: it's the one knob driving page variants. Element-level experiments are independent (see below), so they don't affect which page variant is served.
Element-level targeting
@herazgo/varykit-nuxt provides useVary, <VaryShow> and <VaryHide> (auto-imported) with the cookie mechanism swapped to Nuxt's SSR-safe useCookie so element targeting is also SSR-consistent (no flash of the wrong variant).
Experiments and segments are defined in config, and both the composable and the components take the experiment name:
<template>
<VaryShow experiment="hero" segment="a">
<p>A content</p>
</VaryShow>
<VaryShow experiment="hero" segment="b">
<p>B content</p>
</VaryShow>
<!-- inverse: shown for every segment except "b" -->
<VaryHide experiment="hero" segment="b">
<p>Shown unless the visitor is in b</p>
</VaryHide>
</template><script setup>
const { segment } = useVary('hero')
</script>Segments always resolve from the experiment's config entry — there is no per-use override. Each experiment has its own cookie holding its own segment universe; redefining it per call would reassign the visitor and corrupt cross-page tests. Config is validated at build time: routerExperiment must reference a defined experiment, and variant files require routerExperiment to be set.
Exposure events
The module emits a varykit:exposure Nuxt app hook whenever a variant actually renders: the resolver serving a variant page, or a matching <VaryShow> / <VaryHide> slot rendering. Payload: { experiment, segment }.
Wire your analytics in a client plugin:
// plugins/varykit.client.ts
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hooks.hook('varykit:exposure', ({ experiment, segment }) => {
$analytics.track('experiment_viewed', { experiment, segment })
})
})Listeners live in client-only plugins, so SSR responses and crawlers without JS produce no exposure events. There is no built-in dedup — count distinct users in your analytics tool.
On pages without variant files (manual branching via useVary), nothing emits automatically — a read is not exposure. When you decide content was seen, emit explicitly:
<script setup>
const { segment } = useVary('landing')
onMounted(() => {
emitExposure('landing', segment.value)
})
</script>emitExposure(experiment, segment) is auto-imported, fires the same hook, and is a no-op on the server. Pass the resolved segment from useVary, and fire per instance — not in loops or watchers that re-run.
For conversions, attach the segment to your own events:
<script setup>
const { segment } = useVary('hero')
// e.g. $analytics.track('signup', { experiment: 'hero', segment })
</script>The module also sets data-varykit-<experiment>="<segment>" on <html>, usable from GTM triggers or CSS selectors.
Typed experiment and segment names
The module generates a type augmentation from your actual varykit config, so typos are compile errors — no extra setup:
<VaryShow experiment="hero" segment="typo">
<!-- vue-tsc: Type '"typo"' is not assignable to type '"a" | "b"' -->
</VaryShow>The generated file lives at .nuxt/types/varykit.d.ts. useVary('hero') returns a segment narrowed to the experiment's segment union, and unknown experiment names are compile errors. Works when your app runs vue-tsc/nuxt typecheck.
Lifecycle
Experiments support startsAt (default: immediate), endsAt (default: forever) and endWith — the terminal segment everyone gets outside the window:
| Config | Behavior |
| ------ | -------- |
| no endWith | runs forever (until removed from config) |
| endWith only | pinned immediately at deploy |
| endWith + endsAt | runs until the date, then pinned |
| endsAt without endWith | build error |
The pin overrides existing sticky cookies and rewrites them on the next request, so "ended" means everyone — including previously assigned visitors — resolves to the declared segment. Exposure keeps firing with the pinned segment, so post-period conversions attribute cleanly. Lifecycle is evaluated at resolution time (once per request); endWith must name a defined segment, and dates must parse as ISO.
Cookies are always persistent: Max-Age of 400 days (the browser cap), refreshed on every resolution — a visitor who keeps returning never loses their segment. There are no cookie-duration knobs; an assignment lasts as long as the experiment does.
Configuration
experiments
Map of experiment name to its definition:
| Option | Type | Description |
| ------ | ---- | ----------- |
| segments | Record<string, number> | Segment weights between 0 and 1. Omitted segments auto-split the remainder. |
| startsAt | string | ISO date; the experiment accepts traffic from this moment. Default: immediate. |
| endsAt | string | ISO date; the experiment stops splitting traffic at this moment. Requires endWith. |
| endWith | string | Terminal segment everyone gets outside the [startsAt, endsAt] window. |
routerExperiment
Name of the experiment that drives file-based page variants. Its segment name is matched against the __variant file suffix. Required when variant pages exist.
License
MIT
