-
Notifications
You must be signed in to change notification settings - Fork 75
feat: add SSR social media embeds for X and Instagram #590
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Add privacy-first social media embed components that fetch data server-side and proxy all assets through your domain. Zero client-side API calls to third-party services. - ScriptXEmbed: Headless component with scoped slots for X/Twitter - ScriptInstagramEmbed: Renders proxied Instagram embed HTML - Server routes for data fetching and image/asset proxying - 10-minute server-side caching - Full documentation and e2e tests Resolves #335 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| const rewrittenHtml = html | ||
| // Rewrite scontent CDN images | ||
| .replace( | ||
| /https:\/\/scontent\.cdninstagram\.com([^"'\s)]+)/g, | ||
| '/api/_scripts/instagram-embed-image?url=' + encodeURIComponent('https://scontent.cdninstagram.com') + '$1', | ||
| ) | ||
| // Rewrite static CDN CSS/assets | ||
| .replace( | ||
| /https:\/\/static\.cdninstagram\.com([^"'\s)]+)/g, | ||
| '/api/_scripts/instagram-embed-asset?url=' + encodeURIComponent('https://static.cdninstagram.com') + '$1', | ||
| ) | ||
| // Remove Instagram's embed.js script (we don't need it) | ||
| .replace(/<script[^>]*src="[^"]*embed\.js"[^>]*><\/script>/gi, '') |
Check failure
Code scanning / CodeQL
Incomplete multi-character sanitization High
<script
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 18 hours ago
In general, this should be fixed by using a robust HTML sanitization strategy rather than a narrow regex that removes only a specific <script> tag. Since the problem is that <script content can remain after partial sanitization, the safest fix (without changing existing functional intent too much) is to ensure that all <script tags are removed from the returned HTML, not just the embed.js one. This aligns with the direction of the current code (explicitly removing one script) but makes it comprehensive instead of incomplete.
The single best fix here, without changing the overall behavior of proxying Instagramβs embed HTML, is to adjust the final .replace call so it strips all <script> elements rather than only those whose src contains embed.js. This keeps the path construction / proxying logic intact and simply broadens the sanitization. Concretely, in src/runtime/server/instagram-embed.ts around line 54, replace the regex /\<script[^>]*src="[^"]*embed\.js"[^>]*><\/script>/gi with a more general script-element removal regex, e.g. /\<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, which matches any <script> block, regardless of attributes or content. No new imports or helper methods are needed; we are just changing an existing regex in the string-processing chain.
-
Copy modified lines R53-R54
| @@ -50,8 +50,8 @@ | ||
| /https:\/\/static\.cdninstagram\.com([^"'\s)]+)/g, | ||
| (match) => `/api/_scripts/instagram-embed-asset?url=${encodeURIComponent(match)}`, | ||
| ) | ||
| // Remove Instagram's embed.js script (we don't need it) | ||
| .replace(/<script[^>]*src="[^"]*embed\.js"[^>]*><\/script>/gi, '') | ||
| // Remove all script tags to avoid executing arbitrary scripts | ||
| .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '') | ||
|
|
||
| // Cache for 10 minutes | ||
| setHeader(event, 'Content-Type', 'text/html') |
commit: |
Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
| const shortcode = extractInstagramShortcode(props.postUrl) | ||
|
|
||
| const { data: html, status, error } = useAsyncData<string>( | ||
| `instagram-embed-${shortcode}`, | ||
| () => $fetch(`${props.apiEndpoint}?url=${encodeURIComponent(props.postUrl)}&captions=${props.captions}`), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The shortcode is computed once at component initialization but never updated if the postUrl prop changes. Additionally, if an invalid Instagram URL is passed (one that doesn't match the extraction regex), the shortcode becomes undefined, causing multiple invalid URLs to share the same cache key instagram-embed-undefined, which will incorrectly return cached results from one invalid URL when fetching another.
View Details
π Patch Details
diff --git a/src/runtime/components/ScriptInstagramEmbed.vue b/src/runtime/components/ScriptInstagramEmbed.vue
index 0d25caa..7765864 100644
--- a/src/runtime/components/ScriptInstagramEmbed.vue
+++ b/src/runtime/components/ScriptInstagramEmbed.vue
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
+import { computed } from 'vue'
import { useAsyncData } from 'nuxt/app'
import { extractInstagramShortcode } from '../registry/instagram-embed'
@@ -28,10 +29,16 @@ const props = withDefaults(defineProps<{
apiEndpoint: '/api/_scripts/instagram-embed',
})
-const shortcode = extractInstagramShortcode(props.postUrl)
+const shortcode = computed(() => extractInstagramShortcode(props.postUrl))
+
+const cacheKey = computed(() => {
+ const code = shortcode.value
+ // Use shortcode if available, otherwise use a hash of the URL to avoid collisions
+ return `instagram-embed-${code || btoa(props.postUrl).substring(0, 16)}`
+})
const { data: html, status, error } = useAsyncData<string>(
- `instagram-embed-${shortcode}`,
+ cacheKey,
() => $fetch(`${props.apiEndpoint}?url=${encodeURIComponent(props.postUrl)}&captions=${props.captions}`),
)
Analysis
Non-reactive cache key and cache collision in ScriptInstagramEmbed
What fails: ScriptInstagramEmbed component has two related caching bugs:
-
Non-reactive cache key: When the
postUrlprop changes, the component fails to refetch data because the cache key is static. Theshortcodevariable is computed once at component initialization and never updates, causinguseAsyncDatato reuse the old cached result instead of fetching new data for a different Instagram post. -
Cache collision for invalid URLs: When an invalid Instagram URL is passed (one that doesn't match the extraction regex),
extractInstagramShortcode()returnsundefined. Multiple invalid URLs all receive the cache keyinstagram-embed-undefined, causing the first invalid URL's error response to be cached and incorrectly returned for all subsequent invalid URLs.
How to reproduce:
-
Problem 1 - Non-reactive cache key:
- Create a component instance with
postUrl="https://www.instagram.com/p/ABC123/" - Change the prop to
postUrl="https://www.instagram.com/p/XYZ789/" - Expected: Component refetches the new post
- Actual: Component returns the cached result from the first post
- Create a component instance with
-
Problem 2 - Cache collision:
- Create two component instances with different invalid URLs:
- Instance 1:
postUrl="https://example.com/invalid" - Instance 2:
postUrl="https://another-invalid.com/url"
- Instance 1:
- Both receive cache key
instagram-embed-undefined - First instance's error response is cached
- Second instance incorrectly returns the first instance's cached error
- Create two component instances with different invalid URLs:
Root cause:
At line 31, shortcode was computed once from props.postUrl using a regular variable assignment:
const shortcode = extractInstagramShortcode(props.postUrl)This is not a reactive computed property. According to Nuxt 4.x documentation, the cache key for useAsyncData can be reactive (using computed refs, refs, or getter functions). When a cache key doesn't change, useAsyncData doesn't refetch data. Additionally, when extractInstagramShortcode() returns undefined for invalid URLs, all invalid URLs collapse into a single cache entry.
Solution: Made shortcode a reactive computed property and created a reactive cacheKey computed property that includes a URL hash when the shortcode is undefined, ensuring:
- Cache key updates when
postUrlprop changes, triggering refetch - Each unique URL gets a unique cache key, eliminating collision for invalid URLs
Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
π Linked issue
Resolves #335
β Type of change
π Description
Third-party embed scripts (Twitter widgets, Instagram embeds) hurt performance and leak user data. Following the Cloudflare Zaraz approach, we now fetch embed data server-side and proxy all assets through your domain.
Added two headless components with scoped slots for full styling control:
What's included:
ScriptXEmbed- fetches from X syndication API, exposes tweet data via slotsScriptInstagramEmbed- fetches embed HTML, rewrites asset URLs to proxyPrivacy benefits: