Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion FIRST_PARTY.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ Four presets in `proxy-configs.ts` cover all proxy-enabled scripts:
| `PRIVACY_NONE` | all false | (not currently assigned to any script) |
| `PRIVACY_FULL` | all true | Meta, TikTok, X, Snap, Reddit, LinkedIn |
| `PRIVACY_HEATMAP` | ip, language, hardware | GA, Clarity, Hotjar |
| `PRIVACY_IP_ONLY` | ip only | PostHog, Plausible, Umami, Rybbit, Databuddy, Fathom, CF Web Analytics, Vercel, Matomo, Carbon Ads, Lemon Squeezy, Intercom, Gravatar, YouTube, Vimeo |
| `PRIVACY_IP_ONLY` | ip only | PostHog, Plausible, Umami, Rybbit, Databuddy, Fathom, CF Web Analytics, Vercel, Matomo, Carbon Ads, Lemon Squeezy, Intercom, Gravatar, YouTube, Vimeo, Calendly |

Note: GTM, Segment, Crisp, Mixpanel, and Bing UET are bundle-only (no proxy capability), so no privacy transforms are applied.

Expand Down Expand Up @@ -145,6 +145,7 @@ Note: GTM, Segment, Crisp, Mixpanel, and Bing UET are bundle-only (no proxy capa
| `vimeoPlayer` | vimeoPlayer | `PRIVACY_IP_ONLY` | Path A |
| `intercom` | intercom | `PRIVACY_IP_ONLY` | Path A |
| `gravatar` | gravatar | `PRIVACY_IP_ONLY` | Path A |
| `calendly` | calendly | `PRIVACY_IP_ONLY` | Path A |
| `googleTagManager` | googleTagManager | n/a | Bundle only |
| `segment` | segment | n/a | Bundle only |
| `crisp` | crisp | n/a | Bundle only |
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/1.guides/2.first-party.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
|------|-------------------|---------|
| **Full** | IP, user agent, language, screen, timezone, hardware fingerprints | Meta Pixel, TikTok Pixel, X Pixel, Snapchat Pixel, Reddit Pixel, LinkedIn Insight Tag |
| **Heatmap-safe** | IP, language, hardware fingerprints (preserves screen and user agent for session replay) | Google Analytics, Microsoft Clarity, Hotjar |
| **IP only** | IP addresses anonymised to subnet level | Plausible, PostHog, Umami, Fathom, CF Web Analytics, Vercel Analytics, Rybbit, Databuddy, Matomo, Intercom, YouTube, Vimeo, Gravatar, Carbon Ads, Lemon Squeezy, Google AdSense |
| **IP only** | IP addresses anonymised to subnet level | Plausible, PostHog, Umami, Fathom, CF Web Analytics, Vercel Analytics, Rybbit, Databuddy, Matomo, Intercom, YouTube, Vimeo, Gravatar, Carbon Ads, Lemon Squeezy, Google AdSense, Calendly |

Sensitive headers (`cookie`, `authorization`) are **always** stripped regardless of tier.

Expand Down Expand Up @@ -180,7 +180,7 @@

### Static Hosting (SSG)

The reverse proxy requires a **server runtime**. For static deployments (`nuxt generate`), the proxy is automatically disabled. Scripts are still bundled and served from your domain, but runtime collection requests (analytics beacons, pixel fires) go directly to third-party servers.

Check warning on line 183 in docs/content/docs/1.guides/2.first-party.md

View workflow job for this annotation

GitHub Actions / lint

Passive voice: "is automatically disabled". Consider rewriting in active voice

Check warning on line 183 in docs/content/docs/1.guides/2.first-party.md

View workflow job for this annotation

GitHub Actions / lint

Passive voice: "is automatically disabled". Consider rewriting in active voice

If you need proxying with static hosting, configure platform-level rewrites manually. The pattern is `/_scripts/p/<domain>/:path*` β†’ `https://<domain>/:path*`:

Expand Down Expand Up @@ -461,7 +461,7 @@
| Problem | Fix |
|---------|-----|
| Analytics not tracking | Check DevTools β†’ Network for `/_scripts/p/` requests. Check Nitro server logs for proxy errors |
| Proxy not working on static site | The reverse proxy is automatically disabled for SSG. Use platform rewrites or switch to server mode. See [Static Hosting](#static-hosting-ssg) |

Check warning on line 464 in docs/content/docs/1.guides/2.first-party.md

View workflow job for this annotation

GitHub Actions / lint

Passive voice: "is automatically disabled". Consider rewriting in active voice

Check warning on line 464 in docs/content/docs/1.guides/2.first-party.md

View workflow job for this annotation

GitHub Actions / lint

Passive voice: "is automatically disabled". Consider rewriting in active voice
| Stale script | `rm -rf .nuxt/cache/scripts` and rebuild |
| Build download fails | Set `assets.fallbackOnSrcOnBundleFail: true`{lang="ts"} to fall back to direct loading |
| Debugging | Open Nuxt DevTools β†’ Scripts to see proxy routes and privacy status |
Expand Down
115 changes: 115 additions & 0 deletions docs/content/scripts/calendly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
---
title: Calendly
description: Embed Calendly bookings in your Nuxt app with inline, popup, and badge widgets.
links:
- label: Source
icon: i-simple-icons-github
to: https://github.com/nuxt/scripts/blob/main/packages/script/src/runtime/registry/calendly.ts
size: xs
---

[Calendly](https://calendly.com) is a scheduling tool that lets visitors book time on your calendar without back-and-forth emails. The Calendly embed widget renders the booking flow inline, in a popup, or behind a floating badge button.

Nuxt Scripts provides a registry script composable [`useScriptCalendly()`{lang="ts"}](/scripts/calendly) to integrate it in your Nuxt app.

::script-stats
::

::script-docs
::

::script-types
::

## Loading Calendly

`useScriptCalendly()`{lang="ts"} loads the official Calendly widget script and stylesheet, then exposes the `Calendly` global through a typed proxy. Method calls made before the real SDK is ready are queued, then replayed once the script finishes loading.

## Examples

### Inline widget

The inline widget mounts inside an element you control. The host element needs an explicit height (Calendly recommends at least 700px) so the iframe is fully visible.

```vue
<script setup lang="ts">
const { onLoaded } = useScriptCalendly()

onLoaded(({ Calendly }) => {
Calendly.initInlineWidget({
url: 'https://calendly.com/your-name/30min',
parentElement: document.getElementById('calendly-inline')!,
})
})
</script>

<template>
<div id="calendly-inline" style="min-width: 320px; height: 700px" />
</template>
```

### Popup widget

The popup widget overlays the booking flow on top of your page when triggered by a user action.

```vue
<script setup lang="ts">
const { proxy } = useScriptCalendly()

function openBooking() {
proxy.Calendly.initPopupWidget({
url: 'https://calendly.com/your-name/30min',
})
}
</script>

<template>
<UButton @click="openBooking">
Schedule a call
</UButton>
</template>
```

### Badge widget

The badge widget pins a floating "Schedule time with me" button to the corner of the page.

```vue
<script setup lang="ts">
const { onLoaded } = useScriptCalendly()

onLoaded(({ Calendly }) => {
Calendly.initBadgeWidget({
url: 'https://calendly.com/your-name/30min',
text: 'Schedule time with me',
color: '#0069ff',
textColor: '#ffffff',
})
})
</script>
```

### Prefilling invitee details and UTM parameters

All four widget initialisers accept `prefill` and `utm` options to pre-populate the booking form and tag the booking with marketing attribution.

```vue
<script setup lang="ts">
const { proxy } = useScriptCalendly()

function bookFromCampaign(user: { name: string, email: string }) {
proxy.Calendly.initPopupWidget({
url: 'https://calendly.com/your-name/30min',
prefill: {
name: user.name,
email: user.email,
},
utm: {
utmSource: 'website',
utmMedium: 'cta',
utmCampaign: 'launch',
},
})
}
</script>
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"dev": "nuxt dev playground",
"dev:ssl": "nuxt dev playground --https",
"dev:prepare": "pnpm -r dev:prepare && nuxt prepare && nuxt prepare playground && pnpm prepare:fixtures",
"prepare:fixtures": "nuxt prepare test/fixtures/basic && nuxt prepare test/fixtures/cdn && nuxt prepare test/fixtures/extend-registry && nuxt prepare test/fixtures/partytown && nuxt prepare test/fixtures/first-party && nuxt prepare test/fixtures/linkedin-insight && nuxt prepare test/fixtures/linkedin-insight-cdn",
"prepare:fixtures": "nuxt prepare test/fixtures/basic && nuxt prepare test/fixtures/cdn && nuxt prepare test/fixtures/extend-registry && nuxt prepare test/fixtures/partytown && nuxt prepare test/fixtures/first-party && nuxt prepare test/fixtures/linkedin-insight && nuxt prepare test/fixtures/linkedin-insight-cdn && nuxt prepare test/fixtures/calendly && nuxt prepare test/fixtures/calendly-cdn",
"typecheck": "nuxt typecheck",
"release": "pnpm build && bumpp -r --output=CHANGELOG.md",
"lint": "eslint .",
Expand Down
1 change: 1 addition & 0 deletions packages/script/src/registry-logos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const LOGOS = {
dark: `<svg height="30" width="35" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><filter id="a" height="138.7%" width="131.4%" x="-15.7%" y="-15.1%"><feMorphology in="SourceAlpha" operator="dilate" radius="1" result="shadowSpreadOuter1"/><feOffset dy="1" in="shadowSpreadOuter1" result="shadowOffsetOuter1"/><feGaussianBlur in="shadowOffsetOuter1" result="shadowBlurOuter1" stdDeviation="1"/><feComposite in="shadowBlurOuter1" in2="SourceAlpha" operator="out" result="shadowBlurOuter1"/><feColorMatrix in="shadowBlurOuter1" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.07 0"/></filter><path id="b" d="M14.23 20.46l-9.65 1.1L3 5.12 30.07 2l1.58 16.46-9.37 1.07-3.5 5.72-4.55-4.8z"/></defs><g fill="none" fill-rule="evenodd"><use fill="#000" filter="url(#a)" xlink:href="#b"/><use fill="#fff" stroke="#fff" stroke-width="2" xlink:href="#b"/></g></svg>`,
},
npm: `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><path fill="#C12127" d="M0 256V0h256v256z"/><path fill="#FFF" d="M48 48h160v160h-32V80h-48v128H48z"/></svg>`,
calendly: `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><path fill="#006BFF" d="M128 0C57.308 0 0 57.308 0 128s57.308 128 128 128s128-57.308 128-128S198.692 0 128 0m65.832 165.957l-15.226 13.157c-13.768 11.892-31.358 18.435-49.55 18.435h-2.36c-26.36 0-50.51-14.518-62.99-37.85l-7.392-13.823a71.32 71.32 0 0 1 0-67.752l7.391-13.822c12.48-23.333 36.63-37.851 62.99-37.851h2.361c18.193 0 35.782 6.543 49.55 18.435l15.226 13.157a8.93 8.93 0 0 1 .904 12.598a8.93 8.93 0 0 1-12.598.904l-15.225-13.157c-10.527-9.094-23.97-14.094-37.857-14.094h-2.361c-19.823 0-37.985 10.918-47.367 28.466l-7.39 13.822a53.49 53.49 0 0 0 0 50.83l7.39 13.822c9.382 17.548 27.544 28.466 47.367 28.466h2.361c13.886 0 27.33-5 37.857-14.094l15.225-13.157a8.93 8.93 0 0 1 12.598.904a8.93 8.93 0 0 1-.904 12.598"/></svg>`,
googleRecaptcha: `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 262"><path fill="#4285F4" d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"/><path fill="#34A853" d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"/><path fill="#FBBC05" d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z"/><path fill="#EB4335" d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"/></svg>`,
googleSignIn: `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 262"><path fill="#4285F4" d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"/><path fill="#34A853" d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"/><path fill="#FBBC05" d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z"/><path fill="#EB4335" d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"/></svg>`,
googleTagManager: `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><path fill="#8AB4F8" d="m150.262 245.516l-44.437-43.331l95.433-97.454l46.007 45.091z"/><path fill="#4285F4" d="M150.45 53.938L106.176 8.731L9.36 104.629c-12.48 12.48-12.48 32.713 0 45.207l95.36 95.986l45.09-42.182l-72.654-76.407z"/><path fill="#8AB4F8" d="m246.625 105.37l-96-96c-12.494-12.494-32.756-12.494-45.25 0c-12.495 12.495-12.495 32.757 0 45.252l96 96c12.494 12.494 32.756 12.494 45.25 0c12.495-12.495 12.495-32.757 0-45.251"/><circle cx="127.265" cy="224.731" r="31.273" fill="#246FDB"/></svg>`,
Expand Down
74 changes: 74 additions & 0 deletions packages/script/src/registry-types.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,48 @@
"code": "interface ScriptBlueskyEmbedSlots {\n default?: (props: object) => any\n loading?: () => any\n error?: (props: object) => any\n}"
}
],
"calendly": [
{
"name": "CalendlyOptions",
"kind": "const",
"code": "export const CalendlyOptions = object({\n /**\n * The Calendly event URL to embed.\n * Required for inline, popup, and badge widgets when called via the composable.\n * @example 'https://calendly.com/your-name/30min'\n * @see https://help.calendly.com/hc/en-us/articles/223147027\n */\n url: optional(string()),\n /**\n * Pre-fill invitee fields on the booking form.\n * @see https://help.calendly.com/hc/en-us/articles/360020052833\n */\n prefill: optional(object({\n name: optional(string()),\n email: optional(string()),\n firstName: optional(string()),\n lastName: optional(string()),\n /** Custom answers keyed by `a1`, `a2`, ... matching custom question order. */\n customAnswers: optional(record(string(), string())),\n })),\n /**\n * UTM parameters appended to the booking URL for marketing attribution.\n * @see https://help.calendly.com/hc/en-us/articles/360020052833\n */\n utm: optional(object({\n utmCampaign: optional(string()),\n utmSource: optional(string()),\n utmMedium: optional(string()),\n utmContent: optional(string()),\n utmTerm: optional(string()),\n })),\n /**\n * Theme and layout overrides applied to the booking page.\n * @see https://help.calendly.com/hc/en-us/articles/360020052833\n */\n pageSettings: optional(object({\n backgroundColor: optional(string()),\n hideEventTypeDetails: optional(boolean()),\n hideLandingPageDetails: optional(boolean()),\n primaryColor: optional(string()),\n textColor: optional(string()),\n })),\n /**\n * CSS selector for the element that hosts the inline widget.\n * Required when the widget is initialised inline; the element should have a\n * minimum height of around 700px so the booking iframe is fully visible.\n */\n parentElement: optional(string()),\n})"
},
{
"name": "CalendlyPrefill",
"kind": "interface",
"code": "interface CalendlyPrefill {\n name?: string\n email?: string\n firstName?: string\n lastName?: string\n customAnswers?: Record<string, string>\n}"
},
{
"name": "CalendlyUtm",
"kind": "interface",
"code": "interface CalendlyUtm {\n utmCampaign?: string\n utmSource?: string\n utmMedium?: string\n utmContent?: string\n utmTerm?: string\n}"
},
{
"name": "CalendlyPageSettings",
"kind": "interface",
"code": "interface CalendlyPageSettings {\n backgroundColor?: string\n hideEventTypeDetails?: boolean\n hideLandingPageDetails?: boolean\n primaryColor?: string\n textColor?: string\n}"
},
{
"name": "CalendlyInlineWidgetOptions",
"kind": "interface",
"code": "export interface CalendlyInlineWidgetOptions {\n url: string\n parentElement: HTMLElement\n prefill?: CalendlyPrefill\n utm?: CalendlyUtm\n pageSettings?: CalendlyPageSettings\n}"
},
{
"name": "CalendlyPopupWidgetOptions",
"kind": "interface",
"code": "export interface CalendlyPopupWidgetOptions {\n url: string\n rootElement?: HTMLElement\n text?: string\n color?: string\n textColor?: string\n branding?: boolean\n prefill?: CalendlyPrefill\n utm?: CalendlyUtm\n pageSettings?: CalendlyPageSettings\n}"
},
{
"name": "CalendlyBadgeWidgetOptions",
"kind": "interface",
"code": "export interface CalendlyBadgeWidgetOptions {\n url: string\n text?: string\n color?: string\n textColor?: string\n branding?: boolean\n prefill?: CalendlyPrefill\n utm?: CalendlyUtm\n pageSettings?: CalendlyPageSettings\n}"
},
{
"name": "CalendlyApi",
"kind": "interface",
"code": "export interface CalendlyApi {\n Calendly: {\n initInlineWidget: (options: CalendlyInlineWidgetOptions) => void\n initPopupWidget: (options: CalendlyPopupWidgetOptions) => void\n initBadgeWidget: (options: CalendlyBadgeWidgetOptions) => void\n showPopupWidget: (url: string) => void\n closePopupWidget: () => void\n initPopupWidgetWithText: (options: CalendlyPopupWidgetOptions) => void\n q?: unknown[]\n }\n}"
}
],
"clarity": [
{
"name": "ClarityOptions",
Expand Down Expand Up @@ -2060,6 +2102,38 @@
"defaultValue": "false"
}
],
"CalendlyOptions": [
{
"name": "url",
"type": "string",
"required": false,
"description": "The Calendly event URL to embed. Required for inline, popup, and badge widgets when called via the composable."
},
{
"name": "prefill",
"type": "object",
"required": false,
"description": "Pre-fill invitee fields on the booking form."
},
{
"name": "utm",
"type": "object",
"required": false,
"description": "UTM parameters appended to the booking URL for marketing attribution."
},
{
"name": "pageSettings",
"type": "object",
"required": false,
"description": "Theme and layout overrides applied to the booking page."
},
{
"name": "parentElement",
"type": "string",
"required": false,
"description": "CSS selector for the element that hosts the inline widget. Required when the widget is initialised inline; the element should have a minimum height of around 700px so the booking iframe is fully visible."
}
],
"SegmentOptions": [
{
"name": "writeKey",
Expand Down
16 changes: 16 additions & 0 deletions packages/script/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { LOGOS } from './registry-logos'
import {
BingUetOptions,
BlueskyEmbedOptions,
CalendlyOptions,
ClarityOptions,
CloudflareWebAnalyticsOptions,
CrispOptions,
Expand Down Expand Up @@ -162,6 +163,7 @@ export const registryMeta: RegistryScriptMeta[] = [
// cdn
m('npm', 'NPM', 'cdn', 'useScriptNpm', { bundle: true }, null),
// utility
m('calendly', 'Calendly', 'utility', 'useScriptCalendly', { bundle: true, proxy: true, partytown: true }, PRIVACY_IP_ONLY),
m('googleRecaptcha', 'Google reCAPTCHA', 'utility', 'useScriptGoogleRecaptcha', {}, null),
m('googleSignIn', 'Google Sign-In', 'utility', 'useScriptGoogleSignIn', {}, null),
m('gravatar', 'Gravatar', 'utility', 'useScriptGravatar', { bundle: true, proxy: true }, PRIVACY_IP_ONLY),
Expand Down Expand Up @@ -711,6 +713,20 @@ export async function registry(resolve?: (path: string) => Promise<string>): Pro
},
}),
// utility
def('calendly', {
schema: CalendlyOptions,
label: 'Calendly',
src: 'https://assets.calendly.com/assets/external/widget.js',
category: 'utility',
bundle: true,
proxy: {
// Booking iframes load from calendly.com directly (vendor-hosted) and
// are intentionally not proxied. Only the widget script + assets are.
domains: ['assets.calendly.com'],
privacy: PRIVACY_IP_ONLY,
},
partytown: { forwards: ['Calendly.initInlineWidget', 'Calendly.initPopupWidget', 'Calendly.initBadgeWidget'] },
}),
def('googleRecaptcha', {
schema: GoogleRecaptchaOptions,
label: 'Google reCAPTCHA',
Expand Down
Loading
Loading