From 5a4eea6f639a311e38c3a84c916c80eab14e07d4 Mon Sep 17 00:00:00 2001 From: Klement Gunndu Date: Fri, 6 Mar 2026 01:31:44 -0700 Subject: [PATCH 1/2] docs: add custom script integration patterns guide Adds a new guide covering 5 common patterns for building custom script integrations with useScript(): 1. Basic integration - wrapping IIFE scripts 2. API wrapping - strongly-typed method exposure 3. Consent management - coordinating with useScriptTriggerConsent() 4. Error handling - graceful degradation for blocked/failed scripts 5. Registry contribution checklist - file structure and steps Also adds a "See Also" section to the useScript() API docs linking to the new guide. Closes #636 --- .../docs/1.guides/2.custom-script-patterns.md | 266 ++++++++++++++++++ docs/content/docs/3.api/1.use-script.md | 6 + 2 files changed, 272 insertions(+) create mode 100644 docs/content/docs/1.guides/2.custom-script-patterns.md diff --git a/docs/content/docs/1.guides/2.custom-script-patterns.md b/docs/content/docs/1.guides/2.custom-script-patterns.md new file mode 100644 index 00000000..e470f422 --- /dev/null +++ b/docs/content/docs/1.guides/2.custom-script-patterns.md @@ -0,0 +1,266 @@ +--- + +title: Custom Script Patterns +description: Common integration patterns for loading and wrapping third-party scripts with useScript(). + +--- + +When the [Script Registry](/scripts) doesn't cover your use case, you can build custom integrations with [`useScript()`{lang="ts"}](/docs/api/use-script). This guide walks through the most common patterns, from basic loading to strongly-typed API wrappers. + +## Basic Integration + +The simplest pattern wraps a third-party IIFE script and waits for its global to become available. + +```ts [composables/useMyAnalytics.ts] +export function useMyAnalytics() { + return useScript<{ myAnalytics: { send: (event: string) => void } }>( + 'https://cdn.example.com/analytics.iife.js', + { + // Load only after Nuxt is fully hydrated (default behavior) + trigger: 'onNuxtReady', + use: () => ({ myAnalytics: window.myAnalytics }), + }, + ) +} +``` + +The `use` option tells Nuxt Scripts how to resolve the API from the global scope once the script loads. See [Understanding proxied functions](/docs/guides/key-concepts) for how the returned object stays SSR-safe before the script is ready. + +You can then call methods through the proxy without worrying about load order: + +```vue [app.vue] + +``` + +If you need the return value of a function call, await `onLoaded` instead: + +```ts +const { onLoaded } = useMyAnalytics() + +onLoaded(({ myAnalytics }) => { + const result = myAnalytics.send('page_view') + console.log(result) +}) +``` + +## API Wrapping + +For scripts with a larger API surface, define a typed interface and expose only the methods you need. This pattern mirrors how registry scripts like `useScriptSegment` wrap third-party APIs. + +```ts [composables/useMyChat.ts] +interface MyChatApi { + open: () => void + close: () => void + sendMessage: (text: string) => void + onMessage: (callback: (msg: { text: string, from: string }) => void) => void +} + +declare global { + interface Window { + MyChat: MyChatApi + } +} + +export function useMyChat(token: string) { + return useScript( + { + src: `https://cdn.example.com/chat-widget.js?token=${token}`, + defer: true, + }, + { + use: () => window.MyChat, + // Initialize the widget after the script loads + stub: import.meta.client + ? undefined + : ({ fn }) => { + // Return a no-op during SSR to prevent errors + return fn === 'open' || fn === 'close' ? () => {} : undefined + }, + }, + ) +} +``` + +Using the wrapper in a component: + +```vue [components/ChatButton.vue] + + + +``` + +::callout{icon="i-heroicons-light-bulb" color="blue"} +Because [`useScript()`{lang="ts"}](/docs/api/use-script) is a singleton, calling `useMyChat()` in multiple components returns the same instance. You don't need to worry about loading the script twice. +:: + +## Consent Management + +Scripts that set tracking cookies need to wait for user consent. Use [`useScriptTriggerConsent()`{lang="ts"}](/docs/api/use-script-trigger-consent) to coordinate loading with a consent banner. + +For full details on consent workflows, see the [Consent Management](/docs/guides/consent) guide. Here is the pattern applied to a custom script: + +```ts [composables/useCustomTracker.ts] +export function useCustomTracker(consent: Ref) { + const consentTrigger = useScriptTriggerConsent({ consent }) + + return useScript<{ tracker: { track: (event: string) => void } }>( + 'https://cdn.example.com/tracker.js', + { + trigger: consentTrigger, + use: () => ({ tracker: window.tracker }), + }, + ) +} +``` + +```vue [app.vue] + + + +``` + +To revoke consent and remove the script, call `remove()` on the script instance: + +```ts +const { $script } = useCustomTracker(hasConsent) + +function revokeCookies() { + hasConsent.value = false + $script.then(instance => instance.remove()) +} +``` + +## Error Handling + +Third-party scripts can fail to load due to network issues, ad blockers, or CSP restrictions. Use the `status` ref and `onError` callback for graceful degradation. + +```ts [composables/usePaymentForm.ts] +export function usePaymentForm(publishableKey: string) { + const { $script, ...rest } = useScript<{ PaymentSDK: { init: (key: string) => void } }>( + 'https://cdn.example.com/payment-sdk.js', + { + use: () => ({ PaymentSDK: window.PaymentSDK }), + }, + ) + + const error = ref(null) + + $script.catch(() => { + error.value = 'Payment form unavailable. Please try again later.' + }) + + return { ...rest, $script, error } +} +``` + +```vue [components/PaymentForm.vue] + + + +``` + +::callout{icon="i-heroicons-light-bulb" color="blue"} +Proxied function calls are silently dropped when a script fails to load. This means your app won't throw errors, but the functionality simply won't be available. Always provide a fallback UI for critical features. +:: + +## Registry Contribution Checklist + +If your custom integration is useful to others, consider contributing it to the [Script Registry](/scripts). Each registry entry follows a consistent structure. + +### Required Files + +For a script called `my-service`, create the following files: + +| File | Purpose | +| --- | --- | +| `src/runtime/registry/my-service.ts` | Composable using `useRegistryScript()` | +| `src/runtime/registry/schemas/my-service.ts` | Valibot schema for options validation | +| `src/runtime/components/ScriptMyService.vue` | (Optional) Facade component | +| `docs/content/scripts/analytics/my-service.md` | Documentation page | + +### Composable Structure + +Registry composables use `useRegistryScript()` instead of `useScript()` directly. This helper handles runtime config, schema validation, and the initialization lifecycle: + +```ts [src/runtime/registry/my-service.ts] +import type { RegistryScriptInput } from '#nuxt-scripts/types' +import { useRegistryScript } from '../utils' +import { MyServiceOptions } from './schemas' + +export type MyServiceInput = RegistryScriptInput + +export interface MyServiceApi { + track: (event: string, properties?: Record) => void +} + +export function useScriptMyService( + _options?: MyServiceInput, +) { + return useRegistryScript('my-service', options => ({ + scriptInput: { + src: `https://cdn.example.com/sdk.js?key=${options?.apiKey}`, + }, + schema: import.meta.dev ? MyServiceOptions : undefined, + scriptOptions: { + use: () => window.myService as T, + }, + }), _options) +} +``` + +### Contribution Steps + +1. **Check for existing requests** -- Search the [issues](https://github.com/nuxt/scripts/issues) for your script. If there's an open request (like [#604](https://github.com/nuxt/scripts/issues/604)), comment that you're working on it. +2. **Create the schema** -- Define a [Valibot](https://valibot.dev/) schema in `src/runtime/registry/schemas/`. Validate required fields like API keys or token lengths. +3. **Implement the composable** -- Follow the structure above. Use `clientInit` for any global setup code that needs to run before the script loads. +4. **Write documentation** -- Add a docs page under `docs/content/scripts/` in the appropriate category. Include usage examples and a list of available options. +5. **Add tests** -- Registry scripts have end-to-end tests in `test/`. Check an existing test like `google-analytics.test.ts` for the pattern. +6. **Open a PR** -- Reference the issue, describe the integration, and include a screenshot of the docs page if applicable. diff --git a/docs/content/docs/3.api/1.use-script.md b/docs/content/docs/3.api/1.use-script.md index 76237405..e1279df4 100644 --- a/docs/content/docs/3.api/1.use-script.md +++ b/docs/content/docs/3.api/1.use-script.md @@ -114,3 +114,9 @@ watch(() => route.path, () => reload()) ::callout{icon="i-heroicons-light-bulb" color="blue"} Many third-party scripts have their own SPA support (e.g., `_iub.cs.api.activateSnippets()`{lang="ts"} for iubenda). Check the script's documentation before using `reload()`{lang="ts"} - their built-in methods are usually more efficient. :: + +## See Also + +- [Custom Script Patterns](/docs/guides/custom-script-patterns) - Common integration patterns for wrapping third-party scripts, including API typing, consent management, and error handling. +- [Key Concepts](/docs/guides/key-concepts) - How proxied functions and script singletons work. +- [Registry Scripts](/docs/guides/registry-scripts) - Pre-configured integrations for popular third-party scripts. From a52360e3b65a38112db3284bff7e779db26b910a Mon Sep 17 00:00:00 2001 From: Klement Gunndu Date: Fri, 6 Mar 2026 05:32:36 -0700 Subject: [PATCH 2/2] docs: simplify ChatButton example, remove redundant useMyChat call Consolidate two useMyChat() calls into one and remove the unused onMessage destructure. The onMessage parameter comes from the onLoaded callback, not the outer scope. Addresses CodeRabbit nitpick: redundant destructuring on line 93. --- docs/content/docs/1.guides/2.custom-script-patterns.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/content/docs/1.guides/2.custom-script-patterns.md b/docs/content/docs/1.guides/2.custom-script-patterns.md index e470f422..f7371abb 100644 --- a/docs/content/docs/1.guides/2.custom-script-patterns.md +++ b/docs/content/docs/1.guides/2.custom-script-patterns.md @@ -90,10 +90,9 @@ Using the wrapper in a component: ```vue [components/ChatButton.vue]