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
70 changes: 70 additions & 0 deletions docs/app/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,76 @@ const contributors = useRuntimeConfig().public.contributors
</div>
</UPageSection>

<UPageSection :ui="{ wrapper: 'pt-0 py-6 sm:py-14' }">
<div class="xl:flex items-center justify-between gap-12">
<div class="max-w-lg">
<UIcon name="i-ph-share-network-duotone" class="h-[100px] w-[100px] text-primary" />
<h2 class="text-xl xl:text-4xl font-bold mb-4">
Privacy-first Social Embeds
</h2>
<p class="text-gray-500 dark:text-gray-400 mb-3">
Embed X (Twitter) and Instagram posts without loading third-party scripts. All content is fetched server-side and proxied through your domain.
</p>
<p class="text-gray-500 dark:text-gray-400 mb-3">
Zero client-side API calls, no cookies, no tracking. Your users' privacy is protected while still displaying rich social content.
</p>
<div class="flex gap-3 mt-6">
<UButton to="/scripts/content/x-embed" variant="soft">
X Embed Docs
</UButton>
<UButton to="/scripts/content/instagram-embed" variant="soft">
Instagram Embed Docs
</UButton>
</div>
</div>
<div class="flex-1 mt-8 xl:mt-0">
<ScriptXEmbed tweet-id="1829496926842368288" class="max-w-md mx-auto">
<template #default="{ userName, userHandle, userAvatar, text, datetime, likesFormatted, tweetUrl, isVerified }">
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 shadow-sm">
<div class="flex items-start gap-3 mb-3">
<img :src="userAvatar" :alt="userName" class="w-10 h-10 rounded-full">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-1">
<span class="font-semibold text-gray-900 dark:text-white truncate">{{ userName }}</span>
<svg v-if="isVerified" class="w-4 h-4 text-blue-500 flex-shrink-0" viewBox="0 0 24 24" fill="currentColor">
<path d="M22.5 12.5c0-1.58-.875-2.95-2.148-3.6.154-.435.238-.905.238-1.4 0-2.21-1.71-3.998-3.818-3.998-.47 0-.92.084-1.336.25C14.818 2.415 13.51 1.5 12 1.5s-2.816.917-3.437 2.25c-.415-.165-.866-.25-1.336-.25-2.11 0-3.818 1.79-3.818 4 0 .494.083.964.237 1.4-1.272.65-2.147 2.018-2.147 3.6 0 1.495.782 2.798 1.942 3.486-.02.17-.032.34-.032.514 0 2.21 1.708 4 3.818 4 .47 0 .92-.086 1.335-.25.62 1.334 1.926 2.25 3.437 2.25 1.512 0 2.818-.916 3.437-2.25.415.163.865.248 1.336.248 2.11 0 3.818-1.79 3.818-4 0-.174-.012-.344-.033-.513 1.158-.687 1.943-1.99 1.943-3.484zm-6.616-3.334l-4.334 6.5c-.145.217-.382.334-.625.334-.143 0-.288-.04-.416-.126l-.115-.094-2.415-2.415c-.293-.293-.293-.768 0-1.06s.768-.294 1.06 0l1.77 1.767 3.825-5.74c.23-.345.696-.436 1.04-.207.346.23.44.696.21 1.04z" />
</svg>
</div>
<span class="text-gray-500 text-sm">@{{ userHandle }}</span>
</div>
<a :href="tweetUrl" target="_blank" rel="noopener noreferrer" aria-label="Share on X" class="text-gray-400 hover:text-gray-600 flex-shrink-0">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" /></svg>
</a>
</div>
<p class="text-gray-900 dark:text-white text-sm mb-3 line-clamp-3">
{{ text }}
</p>
<div class="flex items-center gap-4 text-gray-500 text-xs">
<span>{{ datetime }}</span>
<span>{{ likesFormatted }} likes</span>
</div>
</div>
</template>
<template #loading>
<div class="bg-gray-100 dark:bg-gray-800 rounded-xl p-4 max-w-md mx-auto animate-pulse">
<div class="flex items-center gap-3 mb-3">
<div class="w-10 h-10 bg-gray-300 dark:bg-gray-600 rounded-full" />
<div class="space-y-2">
<div class="h-3 w-24 bg-gray-300 dark:bg-gray-600 rounded" />
<div class="h-2 w-16 bg-gray-300 dark:bg-gray-600 rounded" />
</div>
</div>
<div class="space-y-2">
<div class="h-3 bg-gray-300 dark:bg-gray-600 rounded w-full" />
<div class="h-3 bg-gray-300 dark:bg-gray-600 rounded w-3/4" />
</div>
</div>
</template>
</ScriptXEmbed>
</div>
</div>
</UPageSection>

<UPageSection :ui="{ wrapper: 'pt-0 py-6 sm:py-14' }">
<UPageCTA
description="Learn all of the fundamentals of Nuxt Scripts in the fun interactive confetti tutorial."
Expand Down
167 changes: 167 additions & 0 deletions docs/content/docs/1.guides/6.cors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
---
title: CORS and Security Attributes
description: Understanding how Nuxt Scripts handles cross-origin security.
---

## Background

When loading scripts from external domains, browsers enforce Cross-Origin Resource Sharing (CORS) policies. CORS controls how resources on one domain can be requested by scripts running on another domain. For third-party scripts, this affects:

- Whether cookies are sent with requests
- Access to error details for debugging
- Subresource Integrity (SRI) validation

## Default Behavior

Nuxt Scripts applies privacy-focused defaults to all scripts:

```html
<script
src="https://example.com/script.js"
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
```

These defaults:
- **`crossorigin="anonymous"`** - Prevents the script from sending cookies to third-party servers
- **`referrerpolicy="no-referrer"`** - Prevents sharing the page URL with third-party servers

This improves user privacy but may break scripts that require cookies or referrer information.

## Common CORS Errors

### Script Fails to Load

```text
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource
```

This occurs when a server doesn't return proper CORS headers but `crossorigin="anonymous"` is set. Some third-party scripts don't support CORS.

### Script Loads but Functions Fail

The script loads but functionality is broken because it expected cookies or session data.

### Error Details Hidden

```js
window.onerror = (msg) => console.log(msg)
// Shows: "Script error." instead of actual error
```

Without `crossorigin`, browsers hide error details from external scripts for security.

## Configuring CORS Attributes

### Per-Script Configuration

Disable CORS attributes for scripts that don't support them:

```ts
useScript({
src: 'https://example.com/script.js',
crossorigin: false, // Remove crossorigin attribute
referrerpolicy: false, // Remove referrerpolicy attribute
})
```

Or use a different `crossorigin` value:

```ts
useScript({
src: 'https://example.com/script.js',
crossorigin: 'use-credentials', // Send cookies with request
})
```

### Global Configuration

Change defaults for all scripts:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
scripts: {
defaultScriptOptions: {
crossorigin: false,
referrerpolicy: false,
}
}
})
```

## Crossorigin Values

| Value | Cookies Sent | Error Details | Use Case |
|-------|-------------|---------------|----------|
| `anonymous` | No | Yes (if server supports) | Privacy-focused default |
| `use-credentials` | Yes | Yes | Scripts requiring auth |
| `false` | Yes | No | Scripts without CORS support |

## Registry Scripts

Many registry scripts already disable CORS attributes because the third-party doesn't support them:

```ts
// From useScriptStripe
scriptInput: {
src: 'https://js.stripe.com/basil/stripe.js',
crossorigin: false,
referrerpolicy: false,
}
```

Scripts with `crossorigin: false` include:
- Stripe
- YouTube Player
- Google Sign-In
- Google reCAPTCHA
- Meta Pixel
- TikTok Pixel
- X (Twitter) Pixel
- Snapchat Pixel
- Cloudflare Web Analytics
- Lemon Squeezy
- Matomo Analytics

If a registry script fails, check if CORS configuration needs adjustment.

## Subresource Integrity

When using [bundled scripts with SRI](/docs/guides/bundling#subresource-integrity-sri), `crossorigin="anonymous"` is required and automatically added:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
scripts: {
assets: {
integrity: true, // Automatically sets crossorigin="anonymous"
}
}
})
```

## Troubleshooting

### Script Won't Load

1. Check browser console for CORS errors
2. Set `crossorigin: false` to disable CORS mode
3. Verify the third-party server supports CORS headers

### Script Loads but Broken

1. The script may require cookies - try `crossorigin: 'use-credentials'`
2. The script may need the referrer - set `referrerpolicy: false`
3. Check if the script expects to be loaded without CORS attributes

### Debugging External Script Errors

To see full error messages from external scripts:

1. Ensure the script has `crossorigin="anonymous"`
2. Verify the server returns `Access-Control-Allow-Origin` header
3. If the server doesn't support CORS, you won't get detailed errors

### Bundling as Alternative

If CORS issues persist, consider [bundling the script](/docs/guides/bundling) to serve it from your own domain, eliminating CORS entirely.
89 changes: 89 additions & 0 deletions docs/content/scripts/analytics/google-analytics.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,92 @@ function sendConversion() {
```

::

## Custom Dimensions and User Properties

```ts
const { proxy } = useScriptGoogleAnalytics()

// User properties (persist across sessions)
proxy.gtag('set', 'user_properties', {
user_tier: 'premium',
account_type: 'business'
})

// Event with custom dimensions (register in GA4 Admin > Custom Definitions)
proxy.gtag('event', 'purchase', {
transaction_id: 'T12345',
value: 99.99,
payment_method: 'credit_card', // custom dimension
discount_code: 'SAVE10' // custom dimension
})

// Default params for all future events
proxy.gtag('set', { country: 'US', currency: 'USD' })
```

## Manual Page View Tracking (SPAs)

GA4 auto-tracks page views. To disable and track manually:

```ts
const { proxy } = useScriptGoogleAnalytics()

// Disable automatic page views
proxy.gtag('config', 'G-XXXXXXXX', { send_page_view: false })

// Track on route change
const router = useRouter()
router.afterEach((to) => {
proxy.gtag('event', 'page_view', { page_path: to.fullPath })
})
```

## Proxy Queuing

The proxy queues all `gtag` calls until the script loads. Calls are SSR-safe, adblocker-resilient, and order-preserved.

```ts
const { proxy, onLoaded } = useScriptGoogleAnalytics()

// Fire-and-forget (queued until GA loads)
proxy.gtag('event', 'cta_click', { button_id: 'hero-signup' })

// Need return value? Wait for load
onLoaded(({ gtag }) => {
gtag('get', 'G-XXXXXXXX', 'client_id', (id) => console.log(id))
})
```

## Common Event Patterns

```ts
const { proxy } = useScriptGoogleAnalytics()

// E-commerce
proxy.gtag('event', 'purchase', {
transaction_id: 'T_12345',
value: 59.98,
currency: 'USD',
items: [{ item_id: 'SKU_12345', item_name: 'Widget', price: 29.99, quantity: 2 }]
})

// Engagement
proxy.gtag('event', 'login', { method: 'Google' })
proxy.gtag('event', 'search', { search_term: 'nuxt scripts' })

// Custom
proxy.gtag('event', 'feature_used', { feature_name: 'dark_mode' })
```

## Debugging

Enable debug mode via config or URL param `?debug_mode=true`:

```ts
proxy.gtag('config', 'G-XXXXXXXX', { debug_mode: true })
```

View events in GA4: **Admin > DebugView**. Install [GA Debugger extension](https://chrome.google.com/webstore/detail/google-analytics-debugger/jnkmfdileelhofjcijamephohjechhna) for console logging.

For consent mode setup, see the [Consent Guide](/docs/guides/consent).
Loading
Loading