Migration Guide

What's New in v1

v1 focuses on privacy and performance: route analytics through your own domain, move scripts off the main thread, and render social embeds server-side.

First-Party Mode: Privacy Focused Proxy (#577)

Third-party scripts expose data that enables fingerprinting users across sites. Every request shares the user's IP address, and scripts can set third-party cookies for cross-site tracking.

First-party mode acts as a reverse proxy: scripts are bundled at build time and served from your domain, while runtime collection requests are forwarded through Nitro server routes with automatic anonymisation. It is auto-enabled for scripts that support it, zero-config:

export default defineNuxtConfig({
  scripts: {
    registry: {
      googleAnalytics: { id: 'G-XXXXXX' },
      metaPixel: { id: '123456' },
    }
  }
})
  • User IPs stay private: third parties see your server's IP
  • No third-party cookies: requests are same-origin
  • Works with ad blockers: requests appear first-party
  • Per-script opt-out: proxy: false in the registry entry

See First-Party Mode Guide for details.

Partytown Support (#576)

Load scripts off the main thread using web workers. Set partytown: true per-script:

export default defineNuxtConfig({
  modules: ['@nuxtjs/partytown', '@nuxt/scripts'],
  scripts: {
    registry: {
      plausibleAnalytics: { domain: 'example.com', partytown: true },
      fathomAnalytics: { site: 'XXXXX', partytown: true },
    }
  }
})

Forward arrays are auto-configured for supported scripts.

GA4 has known issues with Partytown. GTM is not compatible. Consider Plausible, Fathom, or Umami instead.

SSR Social Embeds (#590)

Render X, Instagram, and Bluesky embeds server-side without loading third-party JavaScript.

Enable the embeds you need in your nuxt.config:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    registry: {
      xEmbed: { trigger: 'onNuxtReady' },
      instagramEmbed: { trigger: 'onNuxtReady' },
      blueskyEmbed: { trigger: 'onNuxtReady' },
    },
  },
})
<ScriptXEmbed tweet-id="1754336034228171055">
  <template #default="{ userName, text, likesFormatted }">
    <!-- Full styling control via scoped slots -->
  </template>
</ScriptXEmbed>

<ScriptInstagramEmbed post-url="https://instagram.com/p/ABC123/">
  <template #default="{ html }">
    <div v-html="html" />
  </template>
</ScriptInstagramEmbed>

<ScriptBlueskyEmbed post-url="https://bsky.app/profile/...">
  <template #default="{ html }">
    <div v-html="html" />
  </template>
</ScriptBlueskyEmbed>
  • Zero third-party JavaScript
  • No cookies set by X/Instagram/Bluesky
  • User IPs not shared
  • All content served from your domain

The useScriptTriggerConsent() composable now supports revoking consent at runtime:

const trigger = useScriptTriggerConsent()
useScriptGoogleTagManager({ id: 'GTM-XXX', scriptOptions: { trigger } })

trigger.accept() // Grant consent
trigger.revoke() // Revoke consent
trigger.consented // Ref<boolean>

Script Reload (77f853b)

Re-execute DOM-scanning scripts after SPA navigation:

const script = useScript('/third-party.js')
await script.reload()

SRI Integrity Hashes (#575)

export default defineNuxtConfig({
  scripts: {
    assets: {
      integrity: 'sha384' // or 'sha256', 'sha512'
    }
  }
})

Script Stats Export

New @nuxt/scripts/stats subpath export for auditing script privacy, performance, and security characteristics.

import { getScriptStats } from '@nuxt/scripts/stats'

const stats = await getScriptStats()
// Privacy ratings (A+ to F), performance ratings, CWV estimates
export default defineNuxtConfig({
  scripts: {
    registry: {
      googleTagManager: {
        id: 'GTM-XXXX',
        defaultConsent: {
          ad_storage: 'denied',
          analytics_storage: 'denied'
        }
      }
    }
  }
})

New Registry Scripts

  • PostHog (#568): Product analytics with feature flags
  • Google reCAPTCHA v3 (#567): Invisible bot protection
  • TikTok Pixel (#569): Conversion tracking
  • Google Sign-In (#573): One-tap authentication
  • Bing UET (#650): Microsoft Advertising conversion tracking
  • Mixpanel Analytics (#648): Product analytics and user tracking
  • Vercel Analytics (#605): Vercel Web Analytics integration
  • Gravatar (#606): Avatar service with privacy-preserving proxy

Vimeo Player Enhancements (#624)

New ratio prop for aspect ratio control, matching YouTube Player API.

Google Maps Color Mode (#587)

<ScriptGoogleMaps
  :map-ids="{ light: 'LIGHT_MAP_ID', dark: 'DARK_MAP_ID' }"
/>

Auto-switches with @nuxtjs/color-mode or manual color-mode prop.

Google Maps Geocode Proxy

Server-side geocoding proxy to reduce billing costs and hide API keys. Automatically enabled when googleMaps is in your registry.

Registry Config Changes

v1 redesigns how registry entries work. The key change: presence enables infrastructure, trigger enables auto-loading.

Scripts No Longer Auto-Load Without a Trigger

In v0, any configured script auto-loaded globally. In v1, scripts require an explicit trigger to auto-load. Without one, the script registers infrastructure (proxy routes, types, bundling) but does not load on the page.

If you had a config like this in v0:

v0 nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    registry: {
      googleAnalytics: { id: 'G-XXXXXX' },
    }
  }
})

You need to add a trigger in v1:

v1 nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    registry: {
      googleAnalytics: { id: 'G-XXXXXX', trigger: 'onNuxtReady' },
    }
  }
})

If you only need infrastructure without loading the script on the page, set trigger: false explicitly. This registers proxy routes, TypeScript types, and bundling config, but no <script> tag is injected. Useful when you load the script yourself via a component or composable.

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    registry: {
      googleAnalytics: { id: 'G-XXXXXX', trigger: false },
    }
  }
})
A build warning will appear if you provide config values without a trigger. Set trigger: 'onNuxtReady' to auto-load, or trigger: false for infrastructure only.

Config Migration

v0v1What changed
{ id: '...' }{ id: '...', trigger: 'onNuxtReady' }Add an explicit trigger to auto-load. true shorthand, bundle option, and array tuple syntax are also replaced by flat config with trigger.
'mock''mock'Unchanged; creates a stub for testing

Flat Config Syntax

v1 supports flat config syntax. trigger, proxy, bundle, and partytown can be set at the top level:

v0 nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    registry: {
      googleAnalytics: [{ id: 'G-xxx' }, { trigger: 'onNuxtReady' }],
      plausibleAnalytics: [{ domain: 'mysite.com' }, { bundle: true }],
    }
  }
})
v1 nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    registry: {
      googleAnalytics: { id: 'G-xxx', trigger: 'onNuxtReady' },
      plausibleAnalytics: { domain: 'mysite.com' },
    }
  }
})

Environment Variables

Registry config fields resolve through Nuxt's standard runtimeConfig.public mechanism. Add your script config under runtimeConfig.public.scripts and reference it via environment variables:

nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      scripts: {
        googleAnalytics: { id: '' }, // NUXT_PUBLIC_SCRIPTS_GOOGLE_ANALYTICS_ID
        posthog: { apiKey: '' }, // NUXT_PUBLIC_SCRIPTS_POSTHOG_API_KEY
      }
    }
  }
})

Testing with 'mock'

Use 'mock' to register a script stub without loading anything. This creates infrastructure (types, composable auto-imports) with trigger: 'manual' and validation disabled:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    registry: {
      googleAnalytics: 'mock',
    }
  }
})

Breaking Changes

PayPal SDK v6 (#628)

Migrated to PayPal JavaScript SDK v6. The component API has changed significantly:

  • SDK URLs now use v6 endpoints (/web-sdk/v6/core)
  • New authentication modes: clientId (with optional clientToken) or clientToken alone
  • Sandbox mode defaults to true in development

YouTube Player

Aspect Ratio

Use ratio prop instead of deriving from width/height:

<ScriptYouTubePlayer
  video-id="..."
-  :width="1280"
-  :height="720"
+  ratio="16/9"
/>

Default is 16/9.

Placeholder Image

Default object-fit changed from contain to cover:

<ScriptYouTubePlayer video-id="..." placeholder-object-fit="contain" />

Multiple Players

Player instances are now properly isolated. Remove any workarounds for multiple players.

Google Maps Component Consolidation

v1 consolidates the Google Maps marker components and removes the legacy google.maps.Marker API.

ScriptGoogleMapsAdvancedMarkerElementScriptGoogleMapsMarker

ScriptGoogleMapsAdvancedMarkerElement has been renamed to ScriptGoogleMapsMarker. The old name still works but emits a deprecation warning.

-<ScriptGoogleMapsAdvancedMarkerElement :position="{ lat: 0, lng: 0 }" />
+<ScriptGoogleMapsMarker :position="{ lat: 0, lng: 0 }" />

ScriptGoogleMapsPinElement Removed

Use the #content slot on ScriptGoogleMapsMarker instead:

-<ScriptGoogleMapsAdvancedMarkerElement :position="pos">
-  <ScriptGoogleMapsPinElement :options="{ background: 'red' }" />
-</ScriptGoogleMapsAdvancedMarkerElement>
+<ScriptGoogleMapsMarker :position="pos">
+  <template #content>
+    <div class="custom-pin" style="background: red;">📍</div>
+  </template>
+</ScriptGoogleMapsMarker>

markers and centerMarker Props Removed from ScriptGoogleMaps

Use child ScriptGoogleMapsMarker components instead:

-<ScriptGoogleMaps
-  :center="center"
-  :markers="[{ position: { lat: 0, lng: 0 } }]"
-  center-marker
-/>
+<ScriptGoogleMaps :center="center">
+  <ScriptGoogleMapsMarker :position="center" />
+  <ScriptGoogleMapsMarker :position="{ lat: 0, lng: 0 }" />
+</ScriptGoogleMaps>

Google Maps Static Placeholder (#673)

v1 extracts the built-in static map placeholder into a standalone <ScriptGoogleMapsStaticMap> component. This removes the following props from <ScriptGoogleMaps>:

  • placeholderOptions
  • placeholderAttrs
  • aboveTheFold

The #placeholder slot no longer passes a placeholder URL string. It is now empty by default.

<!-- Before -->
<ScriptGoogleMaps
  :center="center"
  :zoom="7"
  above-the-fold
  :placeholder-options="{ maptype: 'satellite' }"
  :placeholder-attrs="{ class: 'rounded' }"
/>

<!-- After -->
+<ScriptGoogleMaps :center="center" :zoom="7">
+  <template #placeholder>
+    <ScriptGoogleMapsStaticMap
+      :center="center"
+      :zoom="7"
+      loading="eager"
+      maptype="satellite"
+      :img-attrs="{ class: 'rounded' }"
+    />
+  </template>
+</ScriptGoogleMaps>

Use the new component standalone for store locators, contact pages, and directions previews without loading the interactive Maps API.

Google Tag Manager

onBeforeGtmStart Callback

Now fires for cached/pre-initialized scripts. Guard against multiple calls:

let initialized = false
useScriptGoogleTagManager({
  onBeforeGtmStart: (gtag) => {
    if (initialized)
      return
    initialized = true
    // your init code
  }
})

Type Augmentation

Templates reorganized. Run nuxi prepare after upgrading.