Skip to content
Merged
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, Ahrefs, Fathom, CF Web Analytics, Vercel, Matomo, Carbon Ads, Lemon Squeezy, Intercom, Gravatar, YouTube, Vimeo |
| `PRIVACY_IP_ONLY` | ip only | PostHog, Plausible, Umami, Rybbit, Databuddy, Ahrefs, 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 @@ -146,6 +146,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, Ahrefs Web Analytics, 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, Ahrefs Web Analytics, 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
169 changes: 169 additions & 0 deletions docs/content/scripts/calendly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
---
title: Calendly
description: Embed Calendly bookings in your Nuxt app with inline, popup, and badge widgets.
links:
- label: useScriptCalendly
icon: i-simple-icons-github
to: https://github.com/nuxt/scripts/blob/main/packages/script/src/runtime/registry/calendly.ts
size: xs
- label: "<ScriptCalendlyInlineWidget>"
icon: i-simple-icons-github
to: https://github.com/nuxt/scripts/blob/main/packages/script/src/runtime/components/ScriptCalendlyInlineWidget.vue
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) and a headless [`<ScriptCalendlyInlineWidget>`{lang="html"}](/scripts/calendly){lang="html"} component to integrate it in your Nuxt app.

::script-stats
::

::script-docs
::

The composable comes with the following defaults:
- **Trigger: Client** Script will load when Nuxt is hydrating.
- **Stylesheet: Inline** The widget stylesheet (and its close-icon SVG) is inlined on first use, so no IP leak to `assets.calendly.com` on render.

You can access the `Calendly` global as a proxy directly or await `onLoaded` to use it. Recommended to use the proxy for void calls; `onLoaded` is convenient when you need a stable DOM reference.

::code-group

```ts [Proxy]
const { proxy } = useScriptCalendly()
function openBooking() {
proxy.Calendly.initPopupWidget({
url: 'https://calendly.com/your-name/30min',
})
}
```

```ts [onLoaded]
const { onLoaded } = useScriptCalendly()
onLoaded(({ Calendly }) => {
Calendly.initInlineWidget({
url: 'https://calendly.com/your-name/30min',
parentElement: document.getElementById('calendly-inline')!,
})
})
```

::

## [`<ScriptCalendlyInlineWidget>`{lang="html"}](/scripts/calendly){lang="html"}

The [`<ScriptCalendlyInlineWidget>`{lang="html"}](/scripts/calendly){lang="html"} component wraps [`useScriptCalendly()`{lang="ts"}](/scripts/calendly){lang="ts"} for the most common embed shape: an inline booking flow mounted into a host element you control.

It's optimized for performance by using [Element Event Triggers](/docs/guides/script-triggers#element-event-triggers), only loading the Calendly widget script once the host element comes into view. By default the trigger is `'visible'`.

```vue
<script setup lang="ts">
const ready = ref(false)
</script>

<template>
<ScriptCalendlyInlineWidget
url="https://calendly.com/your-name/30min"
@ready="ready = true"
/>
</template>
```

### Above-the-fold loading

If the widget is above the fold and you want it to start loading on hydration rather than on visibility, set `above-the-fold` (adds a preconnect to `calendly.com`) and override the trigger.

```vue
<ScriptCalendlyInlineWidget
url="https://calendly.com/your-name/30min"
above-the-fold
trigger="onNuxtReady"
/>
```

### Prefill, UTM, and page settings

```vue
<ScriptCalendlyInlineWidget
url="https://calendly.com/your-name/30min"
:prefill="{ name: 'Ada Lovelace', email: 'ada@example.com' }"
:utm="{ utmSource: 'website', utmMedium: 'cta', utmCampaign: 'launch' }"
:page-settings="{ hideEventTypeDetails: true, hideGdprBanner: true }"
/>
```

### Slots

The component exposes `loading`, `awaitingLoad`, and `error` slots for placeholder UX while the script trigger waits or the script load fails. The default `loading` slot renders an accessible spinner.

## Popup and badge widgets

Popup and badge modes have no host element, so they're driven from the composable directly:

::code-group

```ts [Popup]
const { proxy } = useScriptCalendly()
function open() {
proxy.Calendly.initPopupWidget({
url: 'https://calendly.com/your-name/30min',
})
}
```

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

::

## Prefilling invitee details and UTM parameters

All four widget initialisers (`initInlineWidget`, `initPopupWidget`, `initBadgeWidget`, `initPopupWidgetWithText`) 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>
```

::script-types
::

## Example

Loading Calendly through `app.vue` when Nuxt is ready, with the inline widget rendered on a booking page.

```vue [app.vue]
<script setup lang="ts">
useScriptCalendly({
scriptOptions: {
trigger: 'onNuxtReady',
},
})
</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 && nuxt prepare test/fixtures/ahrefs-analytics && nuxt prepare test/fixtures/ahrefs-analytics-cdn && nuxt prepare test/fixtures/usercentrics",
"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 && nuxt prepare test/fixtures/ahrefs-analytics && nuxt prepare test/fixtures/ahrefs-analytics-cdn && nuxt prepare test/fixtures/usercentrics",
"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 @@ -54,6 +54,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 @@ -116,6 +116,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 @@ -2138,6 +2180,38 @@
"description": "Override the language displayed by the CMP UI (BCP-47 code, e.g. `'en'`, `'de'`)."
}
],
"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
Loading
Loading