Bluesky is a decentralized social media platform built on the AT Protocol.

Nuxt Scripts provides a <ScriptBlueskyEmbed> component that fetches post data server-side and exposes it via slots for complete styling control. All data is proxied through your server - no client-side API calls to Bluesky.

Script Stats
Loading
CDN
First-Party
No
Bundling
No
postUrlstring required

The Bluesky post URL to embed.

apiEndpointstring = '/_scripts/embed/bluesky'

Custom API endpoint for fetching post data.

imageProxyEndpointstring = '/_scripts/embed/bluesky-image'

Custom image proxy endpoint.

Setup

To use the Bluesky embed component, you must enable it in your nuxt.config:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    registry: {
      blueskyEmbed: true,
    },
  },
})

This registers the required server API routes (/_scripts/embed/bluesky and /_scripts/embed/bluesky-image) that handle fetching post data and proxying images.

<ScriptBlueskyEmbed>

The <ScriptBlueskyEmbed> component is a headless component that:

  • Fetches post data server-side via the Bluesky public API (AT Protocol)
  • Proxies all images through your server for privacy
  • Converts rich text facets (links, mentions, hashtags) to HTML
  • Exposes post data via scoped slots for custom rendering
  • Caches responses for 10 minutes
  • Respects author opt-out (!no-unauthenticated label)

Demo

<template>
  <ScriptBlueskyEmbed post-url="https://bsky.app/profile/bsky.app/post/3mgnwwvj3u22a">
    <template #default="{ displayName, handle, text, datetime, likesFormatted }">
      <div class="border rounded-lg p-4 max-w-md">
        <p class="font-bold">
          {{ displayName }} (@{{ handle }})
        </p>
        <p>{{ text }}</p>
        <p class="text-gray-500 text-sm">
          {{ datetime }} - {{ likesFormatted }} likes
        </p>
      </div>
    </template>
  </ScriptBlueskyEmbed>
</template>

Slot Props

The default slot receives the following props:

interface SlotProps {
  // Raw data
  post: BlueskyEmbedPostData
  // Author info
  displayName: string
  handle: string
  avatar: string // Proxied URL
  avatarOriginal: string // Original Bluesky CDN URL
  isVerified: boolean
  // Post content
  text: string // Plain text
  richText: string // HTML with links, mentions, and hashtags
  langs?: string[] // Language codes
  // Formatted values
  datetime: string // "12:47 PM · Feb 5, 2024"
  createdAt: Date
  likes: number
  likesFormatted: string // "1.2K"
  reposts: number
  repostsFormatted: string // "234"
  replies: number
  repliesFormatted: string // "42"
  quotes: number
  quotesFormatted: string // "12"
  // Media
  images?: Array<{
    thumb: string // Proxied thumbnail URL
    fullsize: string // Proxied full-size URL
    alt: string
    aspectRatio?: { width: number, height: number }
  }>
  externalEmbed?: {
    uri: string
    title: string
    description: string
    thumb?: string // Proxied URL
  }
  // Links
  postUrl: string
  authorUrl: string
  // Helpers
  proxyImage: (url: string) => string
}

Named Slots

SlotDescription
defaultMain content with slot props
loadingShown while fetching post data
errorShown if post fetch fails, receives { error }

How It Works

  1. Server-side fetch: The server fetches post data from public.api.bsky.app (AT Protocol) during SSR
  2. Handle resolution: The server resolves handles to DIDs for reliable post lookup
  3. Image proxying: The server rewrites all images to proxy through /_scripts/embed/bluesky-image
  4. Rich text: The component converts Bluesky facets (links, mentions, hashtags) to HTML
  5. Caching: The server caches responses for 10 minutes
  6. No client-side API calls: The user's browser never contacts Bluesky directly

Privacy Benefits

  • No third-party JavaScript loaded
  • No cookies set by Bluesky
  • No direct browser-to-Bluesky communication
  • User IP addresses not shared with Bluesky
  • All content served from your domain

Author Opt-Out

The component respects Bluesky's !no-unauthenticated label. If a post author has opted out of external embedding, the API returns a 403 error and the component shows the error slot.