Embed Bluesky's and other services' contents

Nov 10, 2024

Everything started when I decided to be more active on Bluesky and needed a way to integrate it more seamlessly with my website. I wanted to embed my posts directly into my blog posts, and have a share/comment button that would open the post on Bluesky.

Everthing was supposed to be simple, as the team has very detailed documentation at their main site. But because of the setup of this website and the perfectionist in me, it turned out a bit more complicated than I expected.

The API format

Aparently, Bluesky uses the oEmbed API format to allow embedding posts on other websites. I thought it would be a great opportunity to create a more general solution that could be used to embed content from other services as well. So I started with an Astro component for oEmbed. It worked great at the first try, but then I realized I could set the theme as well. And yikes, the headache began. As I wanted it to be perfect, aside from got the theme right at the first load of the page, I want the component to be able to change the theme on the fly, meaning reload when the theme changes.

We need a proxy endpoint?

So I had to change the component to Vue to make use of prevous setup regarding theme management. But Vue component are client-side code, and most of these oEmbed endpoints won’t allow the app to fetch data from them because of CORS policy. So I had to create a Cloudflare Worker to act as a proxy to fetch data from these endpoints. And here is how it looks like:

worker.js
// Cache for providers list
let providersCache = null;
let providersCacheTime = 0;
const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
const ALLOWED_ORIGINS = [
// Add your allowed origins here
];
function isOriginAllowed(request) {
const origin = request.headers.get('Origin');
if (!origin) return false;
return ALLOWED_ORIGINS.some(allowed => origin.startsWith(allowed));
}
function corsHeaders(request) {
const origin = request.headers.get('Origin');
// Only return specific origin if it's allowed, otherwise no CORS headers
return isOriginAllowed(request) ? {
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Max-Age': '86400',
} : {};
}
async function getProviders() {
// Return cached providers if available and not expired
if (providersCache && (Date.now() - providersCacheTime < CACHE_DURATION)) {
return providersCache;
}
try {
const response = await fetch('https://oembed.com/providers.json');
if (!response.ok) {
throw new Error(`Failed to fetch providers: ${response.status}`);
}
providersCache = await response.json();
providersCacheTime = Date.now();
return providersCache;
} catch (error) {
// If we have a cached version, use it even if expired
if (providersCache) {
return providersCache;
}
throw error;
}
}
function findProvider(url, providers) {
for (const provider of providers) {
for (const endpoint of provider.endpoints) {
for (const scheme of endpoint.schemes || []) {
const pattern = new RegExp(
'^' + scheme.replace(/\*/g, '.*').replace(/\?/g, '\\?') + '$'
);
if (pattern.test(url)) {
return {
name: provider.provider_name,
endpoint: endpoint.url,
formats: endpoint.formats || ['json'],
};
}
}
// If no schemes defined but url matches provider url
if (!endpoint.schemes && url.startsWith(provider.provider_url)) {
return {
name: provider.provider_name,
endpoint: endpoint.url,
formats: endpoint.formats || ['json'],
};
}
}
}
return null;
}
async function fetchOembedData(provider, targetUrl, options = {}) {
const embedUrl = new URL(provider.endpoint);
embedUrl.searchParams.set('url', targetUrl);
// Set format preference (prefer json if available)
const format = provider.formats.includes('json') ? 'json' : provider.formats[0];
embedUrl.searchParams.set('format', format);
// Add additional parameters if provided
for (const [key, value] of Object.entries(options)) {
if (value) {
embedUrl.searchParams.set(key, value);
}
}
const response = await fetch(embedUrl);
if (!response.ok) {
throw new Error(`Failed to fetch oEmbed data: ${response.status}`);
}
return response.json();
}
export default {
async fetch(request, env, ctx) {
// Check if origin is allowed
if (!isOriginAllowed(request)) {
return new Response('Forbidden', {
status: 403,
statusText: 'Forbidden',
headers: {
'Content-Type': 'application/json',
}
});
}
// Handle CORS preflight requests
if (request.method === 'OPTIONS') {
return new Response(null, {
headers: corsHeaders(request),
});
}
// Parse URL and get parameters
const url = new URL(request.url);
const targetUrl = url.searchParams.get('url');
if (!targetUrl) {
return new Response('Missing URL parameter', {
status: 400,
headers: {
'Content-Type': 'application/json',
...corsHeaders(request),
},
});
}
try {
// Get providers list
const providers = await getProviders();
// Find the appropriate provider
const provider = findProvider(targetUrl, providers);
if (!provider) {
return new Response(JSON.stringify({
error: 'Unsupported URL format',
message: 'No oEmbed provider found for this URL'
}), {
status: 400,
headers: {
'Content-Type': 'application/json',
...corsHeaders(request),
},
});
}
// Get additional options from query parameters
const options = {};
const validOptions = ['maxwidth', 'maxheight', 'theme', 'format', 'lang'];
for (const option of validOptions) {
const value = url.searchParams.get(option);
if (value) {
options[option] = value;
}
}
const data = await fetchOembedData(provider, targetUrl, options);
return new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=3600',
'X-Provider': provider.name,
...corsHeaders(request),
},
});
} catch (error) {
console.error('Error:', error);
return new Response(JSON.stringify({
error: 'Error processing request',
message: error.message
}), {
status: 500,
headers: {
'Content-Type': 'application/json',
...corsHeaders(request),
},
});
}
},
};

The Vue component

src⠀›⠀components⠀›⠀OEmbed.vue
<script setup lang="ts">
import config from '@/config'
import { useDark } from '@vueuse/core'
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
const props = defineProps<{
url: string
maxWidth?: number
}>()
const isDark = useDark({ storageKey: 'color-scheme' })
const oembedData = ref<any>(null)
const containerRef = ref<HTMLElement>()
async function fetchOembedData() {
try {
const response = await fetch(
`${config.workerHost}/oembed?url=${encodeURIComponent(props.url)}&theme=${isDark.value ? 'dark' : 'light'}`,
)
oembedData.value = await response.json()
if (oembedData.value?.type === 'rich') {
nextTick(() => injectContent(oembedData.value.html))
}
} catch (error) {
console.error('Failed to fetch oembed data:', error)
}
}
function injectContent(html: string) {
if (!containerRef.value)
return
containerRef.value.innerHTML = ''
const temp = document.createElement('div')
temp.innerHTML = html
// Add data-bluesky-embed-color-mode attribute to blockquote elements for Bluesky embeds
const blockquotes = Array.from(temp.getElementsByTagName('blockquote'))
blockquotes.forEach((blockquote) => {
if (blockquote.classList.contains('bluesky-embed') ||
(blockquote.getAttribute('data-bluesky-uri') && blockquote.getAttribute('data-bluesky-uri')?.includes('bsky'))) {
blockquote.setAttribute('data-bluesky-embed-color-mode', isDark.value ? 'dark' : 'light')
}
})
const scripts = Array.from(temp.getElementsByTagName('script'))
scripts.forEach((oldScript) => {
oldScript.parentNode?.removeChild(oldScript)
})
containerRef.value.innerHTML = temp.innerHTML
scripts.forEach((oldScript) => {
const newScript = document.createElement('script')
Array.from(oldScript.attributes).forEach((attr) => {
newScript.setAttribute(attr.name, attr.value)
})
newScript.innerHTML = oldScript.innerHTML
containerRef.value?.appendChild(newScript)
})
}
onMounted(() => {
fetchOembedData()
})
onUnmounted(() => {
if (containerRef.value) {
containerRef.value.innerHTML = ''
}
})
watch(isDark, () => {
fetchOembedData()
})
</script>
<template>
<div v-if="oembedData" class="oembed-container">
<div
v-if="oembedData.type === 'rich'"
ref="containerRef"
:style="`max-width: ${maxWidth || 800}px;`"
/>
<img
v-else-if="oembedData.type === 'photo'"
:src="oembedData.url"
:alt="oembedData.title || ''"
:width="oembedData.width"
:height="oembedData.height"
:style="`max-width: ${maxWidth || 800}px;`"
>
<div
v-else-if="oembedData.type === 'video'"
class="video-container"
:style="`max-width: ${maxWidth || 800}px;`"
v-html="oembedData.html"
/>
<a
v-else-if="oembedData.type === 'link'"
:href="oembedData.url"
target="_blank"
rel="noopener noreferrer"
>
{{ oembedData.title || url }}
</a>
</div>
</template>
<style scoped>
.oembed-container {
margin: 1rem 0;
width: 100%;
}
.video-container {
position: relative;
padding-bottom: 56.25%;
height: 0;
overflow: hidden;
}
.video-container :deep(iframe) {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
img {
max-width: 100%;
height: auto;
}
</style>

Noticed that we need a injectContent function to inject the HTML content into the container? This is because the HTML content returned from the oEmbed endpoint being santized by v-html directive. And we need to extract the scripts from the HTML content and re-add them to the container.

Usage and results

Now we can use the OEmbed component to embed content from Bluesky and other services with the following code in MDX files:

import OEmbed from '@/components/OEmbed.vue'
<OEmbed client:only="vue" url="https://bsky.app/profile/vinh.dev/post/3lalirzsyuc2i" />

And we will got this:

Conclusion

This is just long story of mine to embed Bluesky’s posts and other services’ contents. You don’t have to do exact the same. Just like I said in the beginning, there are simpler ways to do this without all these fancy stuff. So find the one that suits you most and go with it. Have fun at coding and don’t forget to follow me on Bluesky / vinh.dev. Cheers!