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..f7371abb --- /dev/null +++ b/docs/content/docs/1.guides/2.custom-script-patterns.md @@ -0,0 +1,265 @@ +--- + +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.