Repository navigation
docs(skills): add @nuxt/scripts Skill #950
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
2db1769
docs(skills): add @nuxt/scripts Skill
harlan-zw f108018
docs(skills): target @nuxt/scripts v2 in its Skill
harlan-zw a658a61
docs(skills): apply @nuxt/scripts Skill review
harlan-zw f7a2a15
docs(skills): simplify the @nuxt/scripts Skill description
harlan-zw File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,192 @@ | ||
| --- | ||
| name: nuxt-scripts | ||
| description: Loads and controls third-party scripts in Nuxt with @nuxt/scripts v2, covering the scripts.registry config key, useScript, registry composables such as useScriptGoogleAnalytics and useScriptMetaPixel, consent triggers, bundling, and the first-party proxy. Use when adding analytics, pixels, or chat scripts to Nuxt, upgrading @nuxt/scripts from v1 to v2, or when a script never loads, loads before consent, or fails the build while downloading. | ||
| --- | ||
|
|
||
| # @nuxt/scripts | ||
|
|
||
| Tested against `@nuxt/scripts` 2.0.0-beta.14 (npm `beta`) with Nuxt 4.6.0. | ||
| v2 needs Nuxt `^4.6.0 || ^5.0.0`, `@unhead/vue` and `unhead` `^3.4.2`, and Node.js `^22.22.3 || ^24.15.0 || >=26.0.0`. | ||
| Composables and components are auto-imported. The examples import from `#imports` only to stay complete modules. Paths assume the Nuxt 4 `app/` source directory. | ||
|
|
||
| ## Setup | ||
|
|
||
| Install with `npx nuxi module add @nuxt/scripts@2.0.0-beta.14`. It also adds the `unhead` and `@unhead/vue` peers. Keep the version: without it, nuxi installs the npm `latest` tag, which can be another major. | ||
|
|
||
| A registry entry is a flat object, `'mock'`, or `false`; any other shape throws. Its `trigger` decides whether it loads globally: | ||
|
|
||
| | `scripts.registry` entry | Global script | Composable called with no options | | ||
| |---|---|---| | ||
| | `{ id, trigger: 'onNuxtReady' }` | loads on every page | returns the global instance | | ||
| | `{ id }` or `{}` | none | loads with the default `onNuxtReady` trigger | | ||
| | `{ id, trigger: false }` | none | never loads: it inherits `trigger: false` | | ||
| | `'mock'` | none | stays `awaitingLoad`; proxy calls do nothing | | ||
|
|
||
| ```ts | ||
| // nuxt.config.ts | ||
| import { defineNuxtConfig } from 'nuxt/config' | ||
|
|
||
| export default defineNuxtConfig({ | ||
| modules: ['@nuxt/scripts'], | ||
| scripts: { | ||
| registry: { | ||
| // Cookieless, so it loads on every page. The script ID comes from | ||
| // NUXT_PUBLIC_SCRIPTS_PLAUSIBLE_ANALYTICS_SCRIPT_ID and must be real | ||
| // at build time (see Deploy). | ||
| plausibleAnalytics: { trigger: 'onNuxtReady' }, | ||
| // No trigger: app.vue loads it after consent. The ID comes from | ||
| // NUXT_PUBLIC_SCRIPTS_GOOGLE_ANALYTICS_ID. | ||
| googleAnalytics: {}, | ||
| }, | ||
| }, | ||
| }) | ||
| ``` | ||
|
|
||
| Each registry key reads `NUXT_PUBLIC_SCRIPTS_<KEY>_<FIELD>` (camelCase key and field in SCREAMING_SNAKE_CASE) without a `runtimeConfig` declaration. | ||
| The key must be in `scripts.registry`; otherwise the module warns and ignores the variable. These values are public. | ||
|
|
||
| ## Automatic behaviour | ||
|
|
||
| - SSR HTML contains no script tags. Scripts load on the client after hydration (`onNuxtReady`). | ||
| - SSR adds `<link rel="preload" fetchpriority="low">` for `onNuxtReady` and `client` triggers. `warmupStrategy: false` stops it for `onNuxtReady`; a `client` trigger always preloads. | ||
| - Cross-origin tags get `crossorigin="anonymous"` and `referrerpolicy="no-referrer"`. | ||
| - Registry scripts that support bundling are downloaded at build and served from `/_scripts/assets/<hash>.js`. Set `bundle: false` on an entry to load from the vendor. For most scripts this also stops the proxy, because the proxy rewrites the bundle. | ||
| - Collection requests of supported scripts go through `/_scripts/p/<host>/...`. Each script has a privacy tier, and every tier anonymises the IP. Set `proxy: false` on an entry to send them direct. | ||
| - Calls with the same `key`, else `src`, share one script. A second call returns the shared instance and ignores its input, `use`, and `resolve`. Its `trigger` still runs, and a call with no trigger uses `onNuxtReady`. | ||
| - Each call gets its own consumer scope. Unmounting the component releases that call's callbacks and triggers; the shared script stays. | ||
|
|
||
| ## Common tasks | ||
|
|
||
| Load consent-gated scripts. A module-level trigger in `app/utils/` is shared by every component: | ||
|
|
||
| ```ts | ||
| // app/utils/consent.ts | ||
| import { useScriptTriggerConsent } from '#imports' | ||
|
|
||
| export const scriptsConsent = useScriptTriggerConsent() | ||
| ``` | ||
|
|
||
| ```vue | ||
| <!-- app/app.vue --> | ||
| <script setup lang="ts"> | ||
| import { scriptsConsent, useScriptGoogleAnalytics } from '#imports' | ||
|
|
||
| const { consent } = useScriptGoogleAnalytics({ | ||
| // Pushed before gtag('js') and gtag('config'). | ||
| defaultConsent: { ad_storage: 'denied', analytics_storage: 'denied' }, | ||
| scriptOptions: { trigger: scriptsConsent }, | ||
| }) | ||
|
|
||
| function acceptAll() { | ||
| scriptsConsent.accept() | ||
| // `consent` is undefined during SSR, so its type is optional. | ||
| consent?.update({ ad_storage: 'granted', analytics_storage: 'granted' }) | ||
| } | ||
| </script> | ||
|
|
||
| <template> | ||
| <button @click="acceptAll"> | ||
| Accept cookies | ||
| </button> | ||
| <NuxtPage /> | ||
| </template> | ||
| ``` | ||
|
|
||
| Pass `scriptOptions: { trigger: scriptsConsent }` to every call of a consent-gated script, such as a component that tracks an event. One call without it loads the script before consent. | ||
| `scriptsConsent.revoke()` sets `consented` to `false` but does not unload a loaded script. Call the vendor API through `consent` instead. | ||
|
|
||
| Call a vendor API. Proxy calls queue until load and return `undefined`; use `onLoaded` for a return value: | ||
|
|
||
| ```vue | ||
| <!-- app/pages/widget.vue --> | ||
| <script setup lang="ts"> | ||
| import { useScript } from '#imports' | ||
|
|
||
| interface Widget { open: () => boolean } | ||
|
|
||
| const { proxy, load, onLoaded } = useScript<Widget>('https://cdn.example.com/widget.js', { | ||
| trigger: 'manual', | ||
| // Without `use`, the proxy has no API to call. | ||
| use: () => (window as unknown as { Widget?: Widget }).Widget, | ||
| }) | ||
|
|
||
| onLoaded((widget) => { | ||
| console.log('opened:', widget.open()) | ||
| }) | ||
| </script> | ||
|
|
||
| <template> | ||
| <button @click="load(); proxy.open()"> | ||
| Open widget | ||
| </button> | ||
| </template> | ||
| ``` | ||
|
|
||
| If the SDK signals readiness through a callback, use `resolve({ waitFor })`. `load()` then resolves only when the callback fires. The returned cleanup runs when the callback fires, or earlier if the script lifecycle ends: | ||
|
|
||
| ```vue | ||
| <!-- app/pages/sdk.vue --> | ||
| <script setup lang="ts"> | ||
| import { onMounted, useScript } from '#imports' | ||
|
|
||
| interface Sdk { version: string } | ||
| type SdkWindow = Window & { onSdkReady?: (sdk: Sdk) => void } | ||
|
|
||
| const sdk = useScript<Sdk>('https://cdn.example.com/sdk.js', { | ||
| trigger: 'manual', | ||
| resolve: ({ waitFor }) => waitFor<Sdk>((done) => { | ||
| (window as SdkWindow).onSdkReady = done | ||
| return () => delete (window as SdkWindow).onSdkReady | ||
| }), | ||
| }) | ||
|
|
||
| onMounted(() => { | ||
| sdk.load().then(api => console.log('sdk ready:', api.version)) | ||
| }) | ||
| </script> | ||
|
|
||
| <template> | ||
| <p>{{ sdk.status }}</p> | ||
| </template> | ||
| ``` | ||
|
|
||
| Give each instance a unique `key` to load one script twice with different options. | ||
|
|
||
| ## Integrations | ||
|
|
||
| - `partytown: true` needs `@nuxtjs/partytown`. It writes `<script type="text/partytown" src>` into SSR HTML at once. It ignores `trigger`, so consent does not gate it. It keeps only `src`, so other attributes are dropped. | ||
| - `<ScriptXEmbed>`, `<ScriptInstagramEmbed>`, and `<ScriptBlueskyEmbed>` need `scripts.registry.xEmbed: {}` (or the matching key). Without it, SSR fails with "requires `scripts.registry.instagramEmbed` to be enabled". Embed routes are unsigned public proxies. | ||
| - The DevTools panel needs `@nuxt/scripts-devtools` at the same version. If npm has no matching version, skip the panel; loading and proxies work without it. | ||
|
|
||
| ## Traps | ||
|
|
||
| - The "config provided without a `trigger`" warning offers `trigger: false` for infrastructure only. That value also blocks every composable call without a trigger. Use `{ id }` with no trigger key, or pass `scriptOptions: { trigger }` at the call. | ||
| - A registry entry with a `trigger` loads before consent code runs. A later call with a consent trigger gets that loaded instance. Keep consent-gated entries trigger-free. | ||
| - `remove()` removes the shared script for every consumer. To release one component's use, call `dispose()`, or let unmount do it. | ||
| - In `scripts.globals`, put `trigger` in the tuple's second item: `[{ src }, { trigger: { interaction: ['click'] } }]`. Beside `src` it is ignored, and the script loads on `onNuxtReady`. `'manual'` holds the script at runtime but fails `nuxt typecheck`. | ||
| - Option validation runs only in dev, and not for bundled scripts. When it runs, it holds the script and logs to the browser console, not the terminal. Production loads invalid options. | ||
| - The missing-trigger warning fires only for fields with no environment variable. `googleAnalytics: { id }` gets no warning. | ||
|
|
||
| ### Deploy | ||
|
|
||
| - Bundling fetches each vendor script during `nuxt build`. One failed download fails the build, such as a blocked host or an offline CI. Set `assets.fallbackOnSrcOnBundleFail: true` to load that script from its vendor instead. | ||
| - If `plausibleAnalytics` has a trigger or a composable call and no `scriptId`, the build fails with "plausibleAnalytics requires scriptId". A placeholder ID fails the download. | ||
| - Bundles are cached for 7 days in `node_modules/.cache/nuxt/scripts`. If a vendor script is stale, delete that folder. | ||
| - `nuxt generate`, `nitro.static`, and static presets disable the proxy with a build warning. Requests go direct and are not anonymised. | ||
|
|
||
| ## Upgrade from v1 | ||
|
|
||
| Run `npx @nuxt/scripts-cli@2.0.0-beta.14 migrate v2 --dry-run`, then the same command without `--dry-run`. For what it misses and every v1 call with its v2 replacement, read [references/migrate-v1.md](references/migrate-v1.md). | ||
|
|
||
| ## Config | ||
|
|
||
| Reference: <https://scripts.nuxt.com/docs/api/nuxt-config>. Options that change behaviour: | ||
|
|
||
| - `defaultScriptOptions`: default `{ trigger: 'onNuxtReady' }`, inherited by every script. | ||
| - `privacy`: `true`, `false`, or per-flag (`ip`, `userAgent`, `language`, `screen`, `timezone`, `hardware`) for proxied traffic. | ||
|
|
||
| ## Debug | ||
|
|
||
| - Read the returned `status` ref: `awaitingLoad`, `loading`, `loaded`, `error`, or `removed`. `awaitingLoad` means the trigger never fired. | ||
| - Set `scripts.debug: true` to log each script's `registered`, `loading`, and `loaded` steps in the browser console. | ||
| - A `/_scripts/p/` 403 with `Domain not allowed` means the host is not a configured script domain. `Local network targets are not allowed` means the server's DNS resolves the vendor to a local address, such as an ad-blocking resolver. | ||
40 changes: 40 additions & 0 deletions
40
packages/script/skills/nuxt-scripts/references/migrate-v1.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| # Migrate @nuxt/scripts v1 to v2 | ||
|
|
||
| Tested against `@nuxt/scripts` 2.0.0-beta.14 and `@nuxt/scripts-cli` 2.0.0-beta.14. | ||
|
|
||
| ## Migration CLI | ||
|
|
||
| Run it from the project root. Keep the version: the CLI's npm `latest` tag can be an older beta. | ||
|
|
||
| ```bash | ||
| npx @nuxt/scripts-cli@2.0.0-beta.14 migrate v2 --dry-run | ||
| npx @nuxt/scripts-cli@2.0.0-beta.14 migrate v2 | ||
| ``` | ||
|
|
||
| It rewrites static registry config and the renamed APIs it can match in `.vue` and `.ts` files, then lists manual follow-ups. Review its diff: | ||
|
|
||
| - It leaves `scripts.security` in place. v2 ignores that key silently. | ||
| - It leaves `remove()` calls in place. | ||
| - It misses a renamed API when the proxy has another name, such as `rybbit.rybbit.pageview()` after `const { proxy: rybbit } = ...`. | ||
| - It keeps Plausible `domain` and asks for a `scriptId`. Until then, the build fails. | ||
|
|
||
| ## v1 calls and their v2 replacements | ||
|
|
||
| | v1 | v2 | | ||
| |---|---| | ||
| | `googleAnalytics: true` | `googleAnalytics: { trigger: 'onNuxtReady' }` | | ||
| | `x: [{ id }, { trigger }]` or `x: { id, scriptOptions: { bundle: false } }` | `x: { id, trigger, bundle: false }` | | ||
| | `reverseProxyIntercept: false`, `'proxy-only'` | `proxy: false`, `{}` | | ||
| | `xEmbed: true` | `xEmbed: {}` | | ||
| | `globals: [...]` array | `globals: { name: ... }` object | | ||
| | `plausibleAnalytics: { domain }` | `plausibleAnalytics: { scriptId }` | | ||
| | `remove()` as component cleanup | `dispose()`, or nothing in Vue components | | ||
| | `use: () => readyPromise.then(...)` | `resolve: ({ waitFor }) => waitFor(...)` | | ||
| | `scripts.security`, `NUXT_SCRIPTS_PROXY_SECRET`, `generate-secret` | removed | | ||
| | `googleStaticMapsProxy`, Google Maps server proxies | removed; Static Maps loads from Google with the browser key | | ||
| | `<ScriptGoogleMaps :center :zoom>`, ref `googleMaps`, `overlay` | `:map-options="{ center, zoom }"`, `mapsApi`, `overlayView` | | ||
| | `<ScriptGoogleMapsAdvancedMarkerElement>`, `PinElement`, `HeatmapLayer` | `<ScriptGoogleMapsMarker>` with the `#content` slot; no heatmap | | ||
| | `proxy.ttq('track', ...)`, `proxy.rybbit.pageview()` | `proxy.ttq.track(...)`, `proxy.pageview()` | | ||
| | Matomo `trackPageView` | `watch` (default `true`) | | ||
|
|
||
| v2 also needs Nuxt `^4.6.0 || ^5.0.0` and `unhead` and `@unhead/vue` `^3.4.2`. If either Unhead package is missing or out of range, module setup stops with an error that names it and suggests `npx nuxi@latest upgrade --force`. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Call
widget.open()only once per click.When a user clicks the button before the script loads,
proxy.open()queues a call. TheonLoadedcallback also callswidget.open(), so one click opens the widget twice. Keep the queued proxy call and remove theopen()call fromonLoaded, or useload().then(...)instead of the proxy call. Unhead documents that it replays queued proxy calls after the API resolves. (unhead.unjs.io)🧰 Tools
🪛 SkillSpector (2.11.2)
[error] 70: [P2] Hidden Instructions: Hidden instructions were detected in comments or invisible text. These could contain malicious directives. Manual review is recommended.
Remediation: Audit all comments and invisible characters. Remove any instructions that direct the agent to perform unauthorized actions. Use plain, reviewable content.
(Prompt Injection (P2))
[error] 168: [AR2] Anti-Refusal Statement: Skill instructs the agent to omit warnings, disclaimers, or ethical commentary. Stripping safety caveats hides risk from the user and is a common jailbreak preamble.
Remediation: Remove instructions that suppress warnings, disclaimers, or ethical commentary. Let the agent surface safety-relevant caveats to the user.
(Anti-Refusal (AR2))
🤖 Prompt for AI Agents