Guides

First-Party Mode

Last updated by Harlan Wilton in chore: lint.

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:

  1. 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)
  2. 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:

nuxt.config.ts
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:

FlagWhat it does
ipAnonymizes IP addresses to subnet level in headers and payload params
userAgentNormalizes User-Agent to browser family + major version (e.g. Mozilla/5.0 (compatible; Chrome/131.0))
languageNormalizes Accept-Language to primary language tag
screenGeneralizes screen resolution, viewport, hardware concurrency, and device memory to common buckets
timezoneGeneralizes timezone offset and IANA timezone names
hardwareAnonymizes 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:

ScriptipuserAgentlanguagescreentimezonehardwareRationale
Google Analytics---UA/screen/timezone needed for device, time, and OS reports
Google Tag Manager------Container script loading - no user data in requests
Meta PixelUntrusted ad network - full anonymization
TikTok PixelUntrusted ad network - full anonymization
X/Twitter PixelUntrusted ad network - full anonymization
Snapchat PixelUntrusted ad network - full anonymization
Reddit PixelUntrusted 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:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    firstParty: {
      privacy: true, // Full anonymize for ALL scripts
    }
  }
})

Or selectively override specific flags:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    firstParty: {
      privacy: { ip: true }, // Anonymize IP for all scripts, rest uses per-script defaults
    }
  }
})
When a flag is active, data is either generalized (reduced precision) or redacted (emptied/zeroed) - analytics endpoints still receive valid data. For example, screen resolution 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:

nuxt.config.ts
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:

ScriptEndpoints Proxied
Google Analyticsgoogle-analytics.com, analytics.google.com, stats.g.doubleclick.net, pagead2.googlesyndication.com
Google Tag Managerwww.googletagmanager.com
Meta Pixelconnect.facebook.net, www.facebook.com/tr, pixel.facebook.com
TikTok Pixelanalytics.tiktok.com
Segmentapi.segment.io, cdn.segment.com
PostHogus.i.posthog.com, eu.i.posthog.com, us-assets.i.posthog.com, eu-assets.i.posthog.com
Microsoft Claritywww.clarity.ms, scripts.clarity.ms, d.clarity.ms, e.clarity.ms
Hotjarstatic.hotjar.com, script.hotjar.com, vars.hotjar.com, in.hotjar.com
X/Twitter Pixelanalytics.twitter.com, t.co
Snapchat Pixeltr.snapchat.com
Reddit Pixelalb.reddit.com, pixel-config.reddit.com
Plausible Analyticsplausible.io
Cloudflare Web Analyticsstatic.cloudflareinsights.com, cloudflareinsights.com
Rybbit Analyticsapp.rybbit.io
Umami Analyticscloud.umami.is
Databuddy Analyticscdn.databuddy.cc, basket.databuddy.cc
Fathom Analyticscdn.usefathom.com
Vercel Analyticsva.vercel-scripts.com
Intercomwidget.intercom.io, api-iam.intercom.io
Crispclient.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:

Featurebundle: truefirstParty: 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.

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

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    firstParty: false
  }
})

Platform Rewrites

When deploying to static hosting or edge platforms, configure these rewrites to proxy collection endpoints.

Vercel

vercel.json
{
  "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

netlify.toml
[[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:

public/_redirects
/_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:

functions/_scripts/c/[[path]].ts
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:

  1. Verify proxy routes are active - In dev mode, check /_scripts/status.json or the DevTools Scripts panel
  2. Check browser Network tab - Look for requests to /_scripts/c/ paths and verify they return 200
  3. Check server logs - Look for proxy errors in Nitro logs
  4. Verify route rules - Run npx nuxi build and 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:

  1. Ensure you correctly configure route rules in nuxt.config.ts
  2. For static hosting, verify platform rewrites are set up
  3. Check that the target URLs in rewrites match exactly

Cached Old Script

Symptoms: Script content is stale or URLs aren't rewritten.

Solutions:

  1. Clear the build cache:
    rm -rf .nuxt/cache/scripts
    npx nuxi build
    
  2. 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:

  1. Enable fallback mode:
    nuxt.config.ts
    export default defineNuxtConfig({
      scripts: {
        firstParty: true,
        assets: {
          fallbackOnSrcOnBundleFail: true
        }
      }
    })
    
  2. Increase timeout:
    nuxt.config.ts
    export default defineNuxtConfig({
      scripts: {
        assets: {
          fetchOptions: {
            timeout: 30000 // 30 seconds
          }
        }
      }
    })
    

FAQ

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:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    assets: {
      cacheMaxAge: 86400000 // 1 day in ms
    }
  }
})

Can I customize proxy paths?

Yes. Use the collectPrefix option:

nuxt.config.ts
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?

  1. DevTools: Open Nuxt DevTools → Scripts → First-Party tab
  2. Status endpoint: Visit /_scripts/status.json in dev mode
  3. 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:

  1. Request support by opening an issue
  2. Use the bundle option for self-hosting without proxy (deprecated)
  3. 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:

  1. Nuxt bundles scripts at build time (not runtime)
  2. Proxy route rules are global Nitro configuration
  3. Collection requests still go through your server
nuxt.config.ts
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.

For edge deployments, ensure your edge runtime supports outbound fetch requests to the analytics endpoints.

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

  1. Open browser DevTools → Network tab
  2. Filter by /_scripts
  3. Trigger an analytics event
  4. Verify requests go to /_scripts/c/... paths (not third-party domains)
  5. 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.