First-Party Mode
Background
When third-party scripts load directly from external servers, they expose your users' data:
- IP address exposure - Every request reveals your users' IP addresses to third parties
- Third-party cookies - External scripts can set cookies for cross-site tracking
- Ad blocker interference - Privacy tools block requests to known tracking domains
- Connection overhead - Extra DNS lookups and TLS handshakes slow page loads
How First-Party Mode Helps
First-party mode routes all script traffic through your domain:
- User IPs anonymized - IPs are anonymized to subnet level before forwarding; third parties can't identify individual users
- Device fingerprinting reduced - Screen resolution, User-Agent, and hardware info are generalized to common buckets
- No third-party cookies - Requests are same-origin, eliminating cross-site tracking
- Works with ad blockers - Requests appear first-party. Google's server-side tagging documentation notes that this approach provides more reliable data collection and better control over user privacy.
- Faster loads - No extra DNS lookups for external domains
How it Works
When you enable first-party mode:
- Build time: Nuxt downloads scripts and rewrites URLs to local paths (e.g.,
https://www.google-analytics.com/g/collect→/_scripts/c/ga/g/collect) - Runtime: Nitro route rules proxy requests from local paths back to original endpoints
User Browser → Your Server (/_scripts/c/ga/...) → Google Analytics
Your users never connect directly to third-party servers.
Usage
Enable Globally
Enable first-party mode for all supported scripts:
export default defineNuxtConfig({
scripts: {
firstParty: true,
registry: {
googleAnalytics: { id: 'G-XXXXXX' },
metaPixel: { id: '123456' },
}
}
})
Privacy Controls
Each script in the registry declares its own privacy defaults based on what data it needs. Privacy is controlled by six flags:
| Flag | What it does |
|---|---|
ip | Anonymizes IP addresses to subnet level in headers and payload params |
userAgent | Normalizes User-Agent to browser family + major version (e.g. Mozilla/5.0 (compatible; Chrome/131.0)) |
language | Normalizes Accept-Language to primary language tag |
screen | Generalizes screen resolution, viewport, hardware concurrency, and device memory to common buckets |
timezone | Generalizes timezone offset and IANA timezone names |
hardware | Anonymizes canvas/webgl/audio fingerprints, plugin/font lists, browser versions, and device info |
Sensitive headers (cookie, authorization) are always stripped regardless of privacy settings.
Per-Script Defaults
Scripts declare the privacy that makes sense for their use case:
| Script | ip | userAgent | language | screen | timezone | hardware | Rationale |
|---|---|---|---|---|---|---|---|
| Google Analytics | ✓ | - | ✓ | - | - | ✓ | UA/screen/timezone needed for device, time, and OS reports |
| Google Tag Manager | - | - | - | - | - | - | Container script loading - no user data in requests |
| Meta Pixel | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Untrusted ad network - full anonymization |
| TikTok Pixel | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Untrusted ad network - full anonymization |
| X/Twitter Pixel | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Untrusted ad network - full anonymization |
| Snapchat Pixel | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Untrusted ad network - full anonymization |
| Reddit Pixel | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Untrusted ad network - full anonymization |
| Segment | - | - | - | - | - | - | Trusted data pipeline - full fidelity required |
| PostHog | - | - | - | - | - | - | Trusted, open-source - full fidelity required |
| Microsoft Clarity | ✓ | - | ✓ | - | - | ✓ | UA/screen/timezone needed for heatmaps and device filtering |
| ✓ = anonymized, - = passed through |
| Hotjar | ✓ | - | ✓ | - | - | ✓ | UA/screen/timezone needed for heatmaps and device filtering |
Global Override
Override all per-script defaults at once:
export default defineNuxtConfig({
scripts: {
firstParty: {
privacy: true, // Full anonymize for ALL scripts
}
}
})
Or selectively override specific flags:
export default defineNuxtConfig({
scripts: {
firstParty: {
privacy: { ip: true }, // Anonymize IP for all scripts, rest uses per-script defaults
}
}
})
1440x900 becomes 1920x1080 (desktop bucket) and User-Agent is normalized to Mozilla/5.0 (compatible; Chrome/131.0), while hardware fingerprints like canvas, WebGL, plugins, and fonts are zeroed or cleared.Custom Paths
Customize the proxy endpoint paths:
export default defineNuxtConfig({
scripts: {
firstParty: {
collectPrefix: '/_analytics', // Default: /_scripts/c
}
}
})
Opt-out Per Script
Disable first-party routing for a specific script:
useScriptGoogleAnalytics({
id: 'G-XXXXXX',
scriptOptions: {
firstParty: false, // Load directly from Google
}
})
Supported Scripts
First-party mode supports the following scripts:
| Script | Endpoints Proxied |
|---|---|
| Google Analytics | google-analytics.com, analytics.google.com, stats.g.doubleclick.net, pagead2.googlesyndication.com |
| Google Tag Manager | www.googletagmanager.com |
| Meta Pixel | connect.facebook.net, www.facebook.com/tr, pixel.facebook.com |
| TikTok Pixel | analytics.tiktok.com |
| Segment | api.segment.io, cdn.segment.com |
| PostHog | us.i.posthog.com, eu.i.posthog.com, us-assets.i.posthog.com, eu-assets.i.posthog.com |
| Microsoft Clarity | www.clarity.ms, scripts.clarity.ms, d.clarity.ms, e.clarity.ms |
| Hotjar | static.hotjar.com, script.hotjar.com, vars.hotjar.com, in.hotjar.com |
| X/Twitter Pixel | analytics.twitter.com, t.co |
| Snapchat Pixel | tr.snapchat.com |
| Reddit Pixel | alb.reddit.com, pixel-config.reddit.com |
| Plausible Analytics | plausible.io |
| Cloudflare Web Analytics | static.cloudflareinsights.com, cloudflareinsights.com |
| Rybbit Analytics | app.rybbit.io |
| Umami Analytics | cloud.umami.is |
| Databuddy Analytics | cdn.databuddy.cc, basket.databuddy.cc |
| Fathom Analytics | cdn.usefathom.com |
| Vercel Analytics | va.vercel-scripts.com |
| Intercom | widget.intercom.io, api-iam.intercom.io |
| Crisp | client.crisp.chat |
Requirements
First-party mode requires a server runtime. It won't work with fully static hosting (e.g., nuxt generate to GitHub Pages) because the proxy endpoints need a server to forward requests.
For static deployments, you can still enable first-party mode - Nuxt bundles scripts with rewritten URLs, but you'll need to configure your hosting platform's rewrite rules manually.
Static Hosting Rewrites
If deploying statically, configure your platform to proxy these paths:
/_scripts/c/ga/* → https://www.google.com/*
/_scripts/c/gtm/* → https://www.googletagmanager.com/*
/_scripts/c/meta/* → https://connect.facebook.net/*
First-Party vs Bundle
First-party mode supersedes the bundle option:
| Feature | bundle: true | firstParty: true |
|---|---|---|
| Downloads script at build | ✅ | ✅ |
| Serves from your domain | ✅ | ✅ |
| Rewrites collection URLs | ❌ | ✅ |
| Proxies API requests | ❌ | ✅ |
| Hides user IPs | ❌ | ✅ |
| Blocks third-party cookies | ❌ | ✅ |
The bundle option only self-hosts the script file. First-party mode also rewrites and proxies all collection/tracking endpoints, providing complete first-party routing.
bundle option is deprecated. Use firstParty: true for new projects.Default Behavior
First-party mode is enabled by default. For static hosting (GitHub Pages, etc.), Nuxt still bundles scripts but you'll need to configure platform rewrites for the proxy endpoints to work.
To disable first-party mode:
export default defineNuxtConfig({
scripts: {
firstParty: false
}
})
Platform Rewrites
When deploying to static hosting or edge platforms, configure these rewrites to proxy collection endpoints.
Vercel
{
"rewrites": [
{ "source": "/_scripts/c/ga/:path*", "destination": "https://www.google.com/:path*" },
{ "source": "/_scripts/c/ga-legacy/:path*", "destination": "https://www.google-analytics.com/:path*" },
{ "source": "/_scripts/c/gtm/:path*", "destination": "https://www.googletagmanager.com/:path*" },
{ "source": "/_scripts/c/meta/:path*", "destination": "https://connect.facebook.net/:path*" },
{ "source": "/_scripts/c/tiktok/:path*", "destination": "https://analytics.tiktok.com/:path*" },
{ "source": "/_scripts/c/segment/:path*", "destination": "https://api.segment.io/:path*" },
{ "source": "/_scripts/c/clarity/:path*", "destination": "https://www.clarity.ms/:path*" },
{ "source": "/_scripts/c/hotjar/:path*", "destination": "https://static.hotjar.com/:path*" },
{ "source": "/_scripts/c/x/:path*", "destination": "https://analytics.twitter.com/:path*" },
{ "source": "/_scripts/c/snap/:path*", "destination": "https://tr.snapchat.com/:path*" },
{ "source": "/_scripts/c/reddit/:path*", "destination": "https://alb.reddit.com/:path*" }
]
}
Netlify
[[redirects]]
from = "/_scripts/c/ga/*"
to = "https://www.google.com/:splat"
status = 200
[[redirects]]
from = "/_scripts/c/ga-legacy/*"
to = "https://www.google-analytics.com/:splat"
status = 200
[[redirects]]
from = "/_scripts/c/gtm/*"
to = "https://www.googletagmanager.com/:splat"
status = 200
[[redirects]]
from = "/_scripts/c/meta/*"
to = "https://connect.facebook.net/:splat"
status = 200
# Add more as needed...
Cloudflare Pages
Create a _redirects file in your public directory:
/_scripts/c/ga/* https://www.google.com/:splat 200
/_scripts/c/ga-legacy/* https://www.google-analytics.com/:splat 200
/_scripts/c/gtm/* https://www.googletagmanager.com/:splat 200
/_scripts/c/meta/* https://connect.facebook.net/:splat 200
Or use Cloudflare Workers for more control:
export const onRequest: PagesFunction = async (context) => {
const url = new URL(context.request.url)
const path = url.pathname.replace('/_scripts/c/', '')
// Route based on prefix
const routes: Record<string, string> = {
'ga/': 'https://www.google.com/',
'gtm/': 'https://www.googletagmanager.com/',
'meta/': 'https://connect.facebook.net/',
}
for (const [prefix, target] of Object.entries(routes)) {
if (path.startsWith(prefix)) {
const targetUrl = target + path.slice(prefix.length) + url.search
return fetch(targetUrl, context.request)
}
}
return new Response('Not found', { status: 404 })
}
Architecture Diagram
┌─────────────────────────────────────────────────────────────────────┐
│ BUILD TIME │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Download script from third-party │
│ https://www.googletagmanager.com/gtag/js?id=G-XXX │
│ ↓ │
│ 2. Rewrite URLs in script content │
│ "www.google.com/g/collect" → "/_scripts/c/ga/g/collect" │
│ ↓ │
│ 3. Save rewritten script to build output │
│ .output/public/_scripts/abc123.js │
│ │
└─────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────┐
│ RUNTIME │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ User Browser │
│ │ │
│ │ 1. Request script │
│ │ GET /_scripts/abc123.js │
│ ↓ │
│ Your Server (Nitro) │
│ │ │
│ │ 2. Serve bundled script (rewritten URLs) │
│ ↓ │
│ User Browser │
│ │ │
│ │ 3. Script sends analytics │
│ │ POST /_scripts/c/ga/g/collect │
│ ↓ │
│ Your Server (Nitro route rule) │
│ │ │
│ │ 4. Proxy to third-party │
│ │ POST https://www.google.com/g/collect │
│ ↓ │
│ Third-Party Server │
│ │ │
│ │ 5. Sees YOUR server's IP, not user's IP │
│ ↓ │
│ Response proxied back to user │
│ │
└─────────────────────────────────────────────────────────────────────┘
Troubleshooting
Analytics Not Tracking
Symptoms: Events not appearing in analytics dashboard.
Check these:
- Verify proxy routes are active - In dev mode, check
/_scripts/status.jsonor the DevTools Scripts panel - Check browser Network tab - Look for requests to
/_scripts/c/paths and verify they return 200 - Check server logs - Look for proxy errors in Nitro logs
- Verify route rules - Run
npx nuxi buildand check.output/server/chunks/routes/rules.mjs
// Debug: Log all requests in server middleware
export default defineEventHandler((event) => {
if (event.path.startsWith('/_scripts/c/')) {
console.log('Proxying:', event.path)
}
})
CORS Errors
Symptoms: Browser console shows CORS errors for analytics requests.
Solutions:
- Ensure you correctly configure route rules in
nuxt.config.ts - For static hosting, verify platform rewrites are set up
- Check that the target URLs in rewrites match exactly
Cached Old Script
Symptoms: Script content is stale or URLs aren't rewritten.
Solutions:
- Clear the build cache:
rm -rf .nuxt/cache/scripts npx nuxi build - Force re-download during development:
useScriptGoogleAnalytics({ id: 'G-XXX', scriptOptions: { bundle: 'force' } })
Static Build Not Proxying
Symptoms: Scripts load but analytics don't track on static hosting.
This behavior is normal - Static builds cannot proxy requests. Configure platform rewrites (see Platform Rewrites section above) or switch to server-rendered mode.
Script Download Fails
Symptoms: Build fails with network timeout errors.
Solutions:
- Enable fallback mode:nuxt.config.ts
export default defineNuxtConfig({ scripts: { firstParty: true, assets: { fallbackOnSrcOnBundleFail: true } } }) - Increase timeout:nuxt.config.ts
export default defineNuxtConfig({ scripts: { assets: { fetchOptions: { timeout: 30000 // 30 seconds } } } })
FAQ
Does first-party mode bypass GDPR consent requirements?
No. First-party mode changes where requests are routed, not whether tracking occurs. You still need user consent before loading tracking scripts. Use consent triggers:
const { accept } = useScriptTriggerConsent()
// Only load after user consents
function onConsentGiven() {
accept()
}
Will analytics still work if the third-party changes their script?
Yes, with automatic updates. Nuxt caches scripts for 7 days by default. When the cache expires, the script is re-downloaded and URLs are rewritten again. You can customize cache duration:
export default defineNuxtConfig({
scripts: {
assets: {
cacheMaxAge: 86400000 // 1 day in ms
}
}
})
Can I customize proxy paths?
Yes. Use the collectPrefix option:
export default defineNuxtConfig({
scripts: {
firstParty: {
collectPrefix: '/_tracking' // Instead of /_scripts/c
}
}
})
Does this work with Server-Side Tracking?
First-party mode is for client-side scripts. For server-side tracking (Measurement Protocol, etc.), send requests directly from your server endpoints without the proxy layer.
How do I debug what's being proxied?
- DevTools: Open Nuxt DevTools → Scripts → First-Party tab
- Status endpoint: Visit
/_scripts/status.jsonin dev mode - Console logs: Enable debug mode:
nuxt.config.ts
export default defineNuxtConfig({ scripts: { debug: true } })
Is there a performance impact?
Minimal. The proxy adds a small latency (~10-50ms) as requests go through your server. However, this is often offset by:
- Eliminating DNS lookups for third-party domains
- Bypassing ad blockers that would otherwise block requests
- Reduced connection overhead (reusing existing connections)
Which scripts can I add first-party support to?
Currently, first-party mode supports all scripts listed in the Supported Scripts section. For other scripts, you can:
- Request support by opening an issue
- Use the
bundleoption for self-hosting without proxy (deprecated) - Configure custom route rules manually
Hybrid Rendering
First-party mode works with Nuxt's hybrid rendering features:
Route-Level SSR
When using routeRules to disable SSR for specific routes, first-party mode still works because:
- Nuxt bundles scripts at build time (not runtime)
- Proxy route rules are global Nitro configuration
- Collection requests still go through your server
export default defineNuxtConfig({
routeRules: {
'/dashboard/**': { ssr: false }, // SPA mode for dashboard
},
scripts: {
firstParty: true, // Still works for all routes
}
})
ISR (Incremental Static Regeneration)
ISR pages work with first-party mode. The bundled scripts are served from static assets, while collection requests are proxied through Nitro at runtime.
Edge Rendering
First-party mode works with edge deployments (Cloudflare Workers, Vercel Edge, etc.). The proxy routes use Nitro's native proxy support which works across all deployment targets.
Consent Integration
First-party mode and consent management are complementary features:
- First-party mode controls where requests go (through your server vs direct)
- Consent triggers control when scripts load (before vs after consent)
Using Both Together
<script setup>
// First-party mode is configured globally in nuxt.config
// Consent is handled per-script
const { status, accept } = useScriptTriggerConsent()
const { gtag } = useScriptGoogleAnalytics({
id: 'G-XXXXXX',
scriptOptions: {
trigger: status, // Don't load until consent given
}
})
function onConsentGiven() {
accept() // Script loads through first-party proxy
}
</script>
Consent Banner Example
<script setup>
const hasConsent = ref(false)
const { accept: acceptGA } = useScriptTriggerConsent()
const { accept: acceptMeta } = useScriptTriggerConsent()
// Configure scripts with consent triggers
useScriptGoogleAnalytics({
id: 'G-XXXXXX',
scriptOptions: { trigger: useScriptTriggerConsent().status }
})
useScriptMetaPixel({
id: '123456',
scriptOptions: { trigger: useScriptTriggerConsent().status }
})
function acceptAll() {
hasConsent.value = true
acceptGA()
acceptMeta()
// Scripts now load through first-party proxy
}
function rejectAll() {
hasConsent.value = true
// Scripts never load
}
</script>
<template>
<div v-if="!hasConsent" class="cookie-banner">
<p>We use analytics to improve your experience.</p>
<button @click="acceptAll">
Accept
</button>
<button @click="rejectAll">
Reject
</button>
</div>
</template>
Privacy Flow
User visits site
│
↓
Scripts registered but NOT loaded (consent pending)
│
↓
User accepts cookies
│
↓
Scripts load from YOUR server (/_scripts/...)
│
↓
Analytics sent through YOUR server (/_scripts/c/...)
│
↓
Third-party sees YOUR server's IP, not user's
This gives you both GDPR compliance (consent before tracking) and enhanced privacy (first-party routing after consent).
Health Check
To verify first-party mode is working correctly:
1. Check DevTools
Open Nuxt DevTools → Scripts → First-Party tab to see:
- Enabled status
- Configured scripts
- Active proxy routes
2. Check Status Endpoint
In development, visit /_scripts/status.json:
{
"enabled": true,
"scripts": ["googleAnalytics", "metaPixel"],
"routes": {
"/_scripts/c/ga/**": "https://www.google.com/**",
"/_scripts/c/meta/**": "https://connect.facebook.net/**"
},
"collectPrefix": "/_scripts/c"
}
3. Verify in Browser
- Open browser DevTools → Network tab
- Filter by
/_scripts - Trigger an analytics event
- Verify requests go to
/_scripts/c/...paths (not third-party domains) - Check response status is 200
4. CLI Status
Run the CLI command to check cache status:
npx nuxt-scripts status
This shows cached scripts and their sizes.