Skip to content

Commit aa542c0

Browse files
harlan-zwclaudevercel[bot]
authored
feat: add SSR social media embeds for X and Instagram (#590)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
1 parent c5e8c84 commit aa542c0

File tree

21 files changed

+1686
-14
lines changed

21 files changed

+1686
-14
lines changed

docs/app/pages/index.vue

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,76 @@ const contributors = useRuntimeConfig().public.contributors
445445
</div>
446446
</UPageSection>
447447

448+
<UPageSection :ui="{ wrapper: 'pt-0 py-6 sm:py-14' }">
449+
<div class="xl:flex items-center justify-between gap-12">
450+
<div class="max-w-lg">
451+
<UIcon name="i-ph-share-network-duotone" class="h-[100px] w-[100px] text-primary" />
452+
<h2 class="text-xl xl:text-4xl font-bold mb-4">
453+
Privacy-first Social Embeds
454+
</h2>
455+
<p class="text-gray-500 dark:text-gray-400 mb-3">
456+
Embed X (Twitter) and Instagram posts without loading third-party scripts. All content is fetched server-side and proxied through your domain.
457+
</p>
458+
<p class="text-gray-500 dark:text-gray-400 mb-3">
459+
Zero client-side API calls, no cookies, no tracking. Your users' privacy is protected while still displaying rich social content.
460+
</p>
461+
<div class="flex gap-3 mt-6">
462+
<UButton to="/scripts/content/x-embed" variant="soft">
463+
X Embed Docs
464+
</UButton>
465+
<UButton to="/scripts/content/instagram-embed" variant="soft">
466+
Instagram Embed Docs
467+
</UButton>
468+
</div>
469+
</div>
470+
<div class="flex-1 mt-8 xl:mt-0">
471+
<ScriptXEmbed tweet-id="1829496926842368288" class="max-w-md mx-auto">
472+
<template #default="{ userName, userHandle, userAvatar, text, datetime, likesFormatted, tweetUrl, isVerified }">
473+
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 shadow-sm">
474+
<div class="flex items-start gap-3 mb-3">
475+
<img :src="userAvatar" :alt="userName" class="w-10 h-10 rounded-full">
476+
<div class="flex-1 min-w-0">
477+
<div class="flex items-center gap-1">
478+
<span class="font-semibold text-gray-900 dark:text-white truncate">{{ userName }}</span>
479+
<svg v-if="isVerified" class="w-4 h-4 text-blue-500 flex-shrink-0" viewBox="0 0 24 24" fill="currentColor">
480+
<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" />
481+
</svg>
482+
</div>
483+
<span class="text-gray-500 text-sm">@{{ userHandle }}</span>
484+
</div>
485+
<a :href="tweetUrl" target="_blank" rel="noopener noreferrer" aria-label="Share on X" class="text-gray-400 hover:text-gray-600 flex-shrink-0">
486+
<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>
487+
</a>
488+
</div>
489+
<p class="text-gray-900 dark:text-white text-sm mb-3 line-clamp-3">
490+
{{ text }}
491+
</p>
492+
<div class="flex items-center gap-4 text-gray-500 text-xs">
493+
<span>{{ datetime }}</span>
494+
<span>{{ likesFormatted }} likes</span>
495+
</div>
496+
</div>
497+
</template>
498+
<template #loading>
499+
<div class="bg-gray-100 dark:bg-gray-800 rounded-xl p-4 max-w-md mx-auto animate-pulse">
500+
<div class="flex items-center gap-3 mb-3">
501+
<div class="w-10 h-10 bg-gray-300 dark:bg-gray-600 rounded-full" />
502+
<div class="space-y-2">
503+
<div class="h-3 w-24 bg-gray-300 dark:bg-gray-600 rounded" />
504+
<div class="h-2 w-16 bg-gray-300 dark:bg-gray-600 rounded" />
505+
</div>
506+
</div>
507+
<div class="space-y-2">
508+
<div class="h-3 bg-gray-300 dark:bg-gray-600 rounded w-full" />
509+
<div class="h-3 bg-gray-300 dark:bg-gray-600 rounded w-3/4" />
510+
</div>
511+
</div>
512+
</template>
513+
</ScriptXEmbed>
514+
</div>
515+
</div>
516+
</UPageSection>
517+
448518
<UPageSection :ui="{ wrapper: 'pt-0 py-6 sm:py-14' }">
449519
<UPageCTA
450520
description="Learn all of the fundamentals of Nuxt Scripts in the fun interactive confetti tutorial."
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
---
2+
title: CORS and Security Attributes
3+
description: Understanding how Nuxt Scripts handles cross-origin security.
4+
---
5+
6+
## Background
7+
8+
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:
9+
10+
- Whether cookies are sent with requests
11+
- Access to error details for debugging
12+
- Subresource Integrity (SRI) validation
13+
14+
## Default Behavior
15+
16+
Nuxt Scripts applies privacy-focused defaults to all scripts:
17+
18+
```html
19+
<script
20+
src="https://example.com/script.js"
21+
crossorigin="anonymous"
22+
referrerpolicy="no-referrer"
23+
></script>
24+
```
25+
26+
These defaults:
27+
- **`crossorigin="anonymous"`** - Prevents the script from sending cookies to third-party servers
28+
- **`referrerpolicy="no-referrer"`** - Prevents sharing the page URL with third-party servers
29+
30+
This improves user privacy but may break scripts that require cookies or referrer information.
31+
32+
## Common CORS Errors
33+
34+
### Script Fails to Load
35+
36+
```text
37+
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource
38+
```
39+
40+
This occurs when a server doesn't return proper CORS headers but `crossorigin="anonymous"` is set. Some third-party scripts don't support CORS.
41+
42+
### Script Loads but Functions Fail
43+
44+
The script loads but functionality is broken because it expected cookies or session data.
45+
46+
### Error Details Hidden
47+
48+
```js
49+
window.onerror = (msg) => console.log(msg)
50+
// Shows: "Script error." instead of actual error
51+
```
52+
53+
Without `crossorigin`, browsers hide error details from external scripts for security.
54+
55+
## Configuring CORS Attributes
56+
57+
### Per-Script Configuration
58+
59+
Disable CORS attributes for scripts that don't support them:
60+
61+
```ts
62+
useScript({
63+
src: 'https://example.com/script.js',
64+
crossorigin: false, // Remove crossorigin attribute
65+
referrerpolicy: false, // Remove referrerpolicy attribute
66+
})
67+
```
68+
69+
Or use a different `crossorigin` value:
70+
71+
```ts
72+
useScript({
73+
src: 'https://example.com/script.js',
74+
crossorigin: 'use-credentials', // Send cookies with request
75+
})
76+
```
77+
78+
### Global Configuration
79+
80+
Change defaults for all scripts:
81+
82+
```ts [nuxt.config.ts]
83+
export default defineNuxtConfig({
84+
scripts: {
85+
defaultScriptOptions: {
86+
crossorigin: false,
87+
referrerpolicy: false,
88+
}
89+
}
90+
})
91+
```
92+
93+
## Crossorigin Values
94+
95+
| Value | Cookies Sent | Error Details | Use Case |
96+
|-------|-------------|---------------|----------|
97+
| `anonymous` | No | Yes (if server supports) | Privacy-focused default |
98+
| `use-credentials` | Yes | Yes | Scripts requiring auth |
99+
| `false` | Yes | No | Scripts without CORS support |
100+
101+
## Registry Scripts
102+
103+
Many registry scripts already disable CORS attributes because the third-party doesn't support them:
104+
105+
```ts
106+
// From useScriptStripe
107+
scriptInput: {
108+
src: 'https://js.stripe.com/basil/stripe.js',
109+
crossorigin: false,
110+
referrerpolicy: false,
111+
}
112+
```
113+
114+
Scripts with `crossorigin: false` include:
115+
- Stripe
116+
- YouTube Player
117+
- Google Sign-In
118+
- Google reCAPTCHA
119+
- Meta Pixel
120+
- TikTok Pixel
121+
- X (Twitter) Pixel
122+
- Snapchat Pixel
123+
- Cloudflare Web Analytics
124+
- Lemon Squeezy
125+
- Matomo Analytics
126+
127+
If a registry script fails, check if CORS configuration needs adjustment.
128+
129+
## Subresource Integrity
130+
131+
When using [bundled scripts with SRI](/docs/guides/bundling#subresource-integrity-sri), `crossorigin="anonymous"` is required and automatically added:
132+
133+
```ts [nuxt.config.ts]
134+
export default defineNuxtConfig({
135+
scripts: {
136+
assets: {
137+
integrity: true, // Automatically sets crossorigin="anonymous"
138+
}
139+
}
140+
})
141+
```
142+
143+
## Troubleshooting
144+
145+
### Script Won't Load
146+
147+
1. Check browser console for CORS errors
148+
2. Set `crossorigin: false` to disable CORS mode
149+
3. Verify the third-party server supports CORS headers
150+
151+
### Script Loads but Broken
152+
153+
1. The script may require cookies - try `crossorigin: 'use-credentials'`
154+
2. The script may need the referrer - set `referrerpolicy: false`
155+
3. Check if the script expects to be loaded without CORS attributes
156+
157+
### Debugging External Script Errors
158+
159+
To see full error messages from external scripts:
160+
161+
1. Ensure the script has `crossorigin="anonymous"`
162+
2. Verify the server returns `Access-Control-Allow-Origin` header
163+
3. If the server doesn't support CORS, you won't get detailed errors
164+
165+
### Bundling as Alternative
166+
167+
If CORS issues persist, consider [bundling the script](/docs/guides/bundling) to serve it from your own domain, eliminating CORS entirely.

docs/content/scripts/analytics/google-analytics.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,3 +213,92 @@ function sendConversion() {
213213
```
214214

215215
::
216+
217+
## Custom Dimensions and User Properties
218+
219+
```ts
220+
const { proxy } = useScriptGoogleAnalytics()
221+
222+
// User properties (persist across sessions)
223+
proxy.gtag('set', 'user_properties', {
224+
user_tier: 'premium',
225+
account_type: 'business'
226+
})
227+
228+
// Event with custom dimensions (register in GA4 Admin > Custom Definitions)
229+
proxy.gtag('event', 'purchase', {
230+
transaction_id: 'T12345',
231+
value: 99.99,
232+
payment_method: 'credit_card', // custom dimension
233+
discount_code: 'SAVE10' // custom dimension
234+
})
235+
236+
// Default params for all future events
237+
proxy.gtag('set', { country: 'US', currency: 'USD' })
238+
```
239+
240+
## Manual Page View Tracking (SPAs)
241+
242+
GA4 auto-tracks page views. To disable and track manually:
243+
244+
```ts
245+
const { proxy } = useScriptGoogleAnalytics()
246+
247+
// Disable automatic page views
248+
proxy.gtag('config', 'G-XXXXXXXX', { send_page_view: false })
249+
250+
// Track on route change
251+
const router = useRouter()
252+
router.afterEach((to) => {
253+
proxy.gtag('event', 'page_view', { page_path: to.fullPath })
254+
})
255+
```
256+
257+
## Proxy Queuing
258+
259+
The proxy queues all `gtag` calls until the script loads. Calls are SSR-safe, adblocker-resilient, and order-preserved.
260+
261+
```ts
262+
const { proxy, onLoaded } = useScriptGoogleAnalytics()
263+
264+
// Fire-and-forget (queued until GA loads)
265+
proxy.gtag('event', 'cta_click', { button_id: 'hero-signup' })
266+
267+
// Need return value? Wait for load
268+
onLoaded(({ gtag }) => {
269+
gtag('get', 'G-XXXXXXXX', 'client_id', (id) => console.log(id))
270+
})
271+
```
272+
273+
## Common Event Patterns
274+
275+
```ts
276+
const { proxy } = useScriptGoogleAnalytics()
277+
278+
// E-commerce
279+
proxy.gtag('event', 'purchase', {
280+
transaction_id: 'T_12345',
281+
value: 59.98,
282+
currency: 'USD',
283+
items: [{ item_id: 'SKU_12345', item_name: 'Widget', price: 29.99, quantity: 2 }]
284+
})
285+
286+
// Engagement
287+
proxy.gtag('event', 'login', { method: 'Google' })
288+
proxy.gtag('event', 'search', { search_term: 'nuxt scripts' })
289+
290+
// Custom
291+
proxy.gtag('event', 'feature_used', { feature_name: 'dark_mode' })
292+
```
293+
294+
## Debugging
295+
296+
Enable debug mode via config or URL param `?debug_mode=true`:
297+
298+
```ts
299+
proxy.gtag('config', 'G-XXXXXXXX', { debug_mode: true })
300+
```
301+
302+
View events in GA4: **Admin > DebugView**. Install [GA Debugger extension](https://chrome.google.com/webstore/detail/google-analytics-debugger/jnkmfdileelhofjcijamephohjechhna) for console logging.
303+
304+
For consent mode setup, see the [Consent Guide](/docs/guides/consent).

0 commit comments

Comments
 (0)