Skip to content
Open
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
94 changes: 94 additions & 0 deletions app/components/EmbeddableBlueskyPost.client.vue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you get the anonymous render warnings when you mark this component as a client?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that was a merge conflict fail, I already removed it in my current PR

Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<script setup lang="ts">
import { BLUESKY_EMBED_BASE_ROUTE } from '#shared/utils/constants'
import type { BlueskyOEmbedResponse } from '#shared/schemas/atproto'

const { url } = defineProps<{
url: string
}>()

const embeddedId = String(Math.random()).slice(2)
const iframeHeight = ref(300)

// INFO: Strictly eager client-side fetch (server: false & lazy: true)
const { data: embedData, status } = useLazyAsyncData<BlueskyOEmbedResponse>(
`bluesky-embed-${embeddedId}`,
() =>
$fetch('/api/atproto/bluesky-oembed', {
query: { url, colorMode: 'system' },
}),
{
// INFO: Redundant with .client.vue but included for surety that SSR is not attempted
server: false,
immediate: true,
},
)

// INFO: Computed URL with Unique ID appended for postMessage handshake, must be stable per component instance
const embedUrl = computed<string | null>(() => {
if (!embedData.value?.embedUrl) return null
return `${embedData.value.embedUrl}&id=${embeddedId}`
})

const isLoading = computed(() => status.value === 'pending')

// INFO: REQUIRED - listener must attach after mount b/c window.postMessage only exists in the browser and the random ID must match between hydration and mount
onMounted(() => {
window.addEventListener('message', onPostMessage)
})

onUnmounted(() => {
window.removeEventListener('message', onPostMessage)
})

function onPostMessage(event: MessageEvent) {
if (event.origin !== BLUESKY_EMBED_BASE_ROUTE) return
if (event.data?.id !== embeddedId) return
if (typeof event.data?.height === 'number') {
iframeHeight.value = event.data.height
}
}
</script>

<template>
<article class="bluesky-embed-container">
<!-- Loading state -->
<LoadingSpinner
v-if="isLoading"
:text="$t('blog.atproto.loading_bluesky_post')"
aria-label="Loading Bluesky post..."
class="loading-spinner"
/>

<!-- Success state -->
<div v-else-if="embedUrl" class="bluesky-embed-container">
<iframe
:data-bluesky-id="embeddedId"
:src="embedUrl"
width="100%"
:height="iframeHeight"
frameborder="0"
scrolling="no"
/>
</div>

<!-- Fallback state -->
<a v-else :href="url" target="_blank" rel="noopener noreferrer">
{{ $t('blog.atproto.view_on_bluesky') }}
</a>
</article>
</template>

<style scoped>
.bluesky-embed-container {
/* INFO: Matches Bluesky's internal max-width */
max-width: 37.5rem;
width: 100%;
margin: 1.5rem 0;
/* INFO: Necessary to remove the white 1px line at the bottom of the embed. Also sets border-radius */
clip-path: inset(0 0 1px 0 round 0.75rem);
}

.bluesky-embed-container > .loading-spinner {
margin: 0 auto;
}
</style>
28 changes: 28 additions & 0 deletions app/pages/blog/atproto.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
authors:
- name: Daniel Roe
blueskyHandle: danielroe.dev
- name: Salma Alam-Naylor
blueskyHandle: whitep4nth3r.com
- name: Matias Capeletto
blueskyHandle: patak.dev
title: 'ATProto'
tags: ['OpenSource', 'Nuxt']
excerpt: 'ATProto is very cool'
date: '2026-01-28T14:30:00Z'
slug: 'atproto'
description: 'ATProto Adjacency Agenda'
draft: false
---

# Atmosphere Apps

All the cool kids are doing Software Decentralization.

This post is all about atmosphere. How it's something that we need to live. Without atmosphere we may end up like Arnie in Total Recall. We don't want that.
But thankfully, we have atmosphere. This beautiful concept is used for other things as well. Atmos is a Fellow product that is a vacuum canister used to store coffee.
This keeps your coffee fresh. But arguably, if you drink a lot of coffee you don't need to store it in a vacuum canister. But if you like to be super fancy. There is an
automated vacuum canister that sucks out the air for you. You don't need to twist and turn like a human machine. You press a button and it sucks the air out. Automation.
It's a wonderful thing. We use automation on this blog post. One automation is getting the Bluesky comments. These are fetched during build time and also run time. This means
posts will always have bluesky comments. Whether you like it or not. Under the hood we do fancy ATProto stuff. And that brings us back to Atmosphere. Because it's something
we need to live.
2 changes: 1 addition & 1 deletion app/pages/blog/first-post.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ authors:
title: 'Hello World'
tags: ['OpenSource', 'Nuxt']
excerpt: 'My first post'
date: '2026-01-28'
date: '2026-01-28T15:30:00Z'
slug: 'first-post'
description: 'My first post on the blog'
draft: true
Expand Down
18 changes: 18 additions & 0 deletions app/pages/blog/nuxt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
authors:
- name: Daniel Roe
blueskyHandle: danielroe.dev
title: 'Nuxted'
tags: ['OpenSource', 'Nuxt']
excerpt: 'Nuxting'
date: '2026-01-28T13:30:00Z'
slug: 'nuxt'
description: 'Nuxter'
draft: false
---

# Nuxt

What a great meta-framework!!

<EmbeddableBlueskyPost url="https://bsky.app/profile/danielroe.dev/post/3md3cmrg56k2r" />
16 changes: 16 additions & 0 deletions app/pages/blog/open-source.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
authors:
- name: Daniel Roe
blueskyHandle: danielroe.dev
title: 'OSS'
tags: ['OpenSource', 'Nuxt']
excerpt: 'OSS Things'
date: '2026-01-28T16:30:00Z'
slug: 'open-source'
description: 'Talking about Open Source Software'
draft: false
---

# OSS

This is about Open Source Software.
16 changes: 16 additions & 0 deletions app/pages/blog/package-registries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
authors:
- name: Daniel Roe
blueskyHandle: danielroe.dev
title: 'Package Registries'
tags: ['OpenSource', 'Nuxt']
excerpt: 'Package Registries need fixing'
date: '2026-01-28T12:30:00Z'
slug: 'package-registries'
description: 'Package Registries Reimagined'
draft: false
---

# Package Registries

Shortest explanation: Production grade JavaScript is weird.
15 changes: 15 additions & 0 deletions app/pages/blog/server-components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
authors:
- name: Daniel Roe
blueskyHandle: danielroe.dev
title: 'Server Components'
date: '2026-01-28T11:30:00Z'
slug: 'server-components'
description: 'My first post on the blog'
excerpt: 'Zero JS'
draft: false
---

# Server components

Here is some server component razzle dazzle. Hello there!
14 changes: 14 additions & 0 deletions app/pages/blog/test-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
authors:
- name: Daniel Roe
blueskyHandle: danielroe.dev
title: 'TEST FAIL'
tags: ['OpenSource', 'Nuxt']
excerpt: 'My first post'
date: '2026-01-28T10:30:00Z'
slug: 'first-post'
description: 'I was made to test this nuxt module'
draft: false
---

# TEST FAIL
10 changes: 10 additions & 0 deletions app/plugins/bluesky-embed.client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import EmbeddableBlueskyPost from '~/components/EmbeddableBlueskyPost.client.vue'

/**
* INFO: .md files are transformed into Vue SFCs by unplugin-vue-markdown during the Vite transform pipeline
* That transformation happens before Nuxt's component auto-import scanning can inject the proper imports
* Global registration ensures the component is available in the Vue runtime regardless of how the SFC was generated
*/
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp.component('EmbeddableBlueskyPost', EmbeddableBlueskyPost)
})
4 changes: 4 additions & 0 deletions lunaria/files/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@
"title": "Blog",
"heading": "blog",
"meta_description": "Insights and updates from the npmx community",
"atproto": {
"loading_bluesky_post": "Loading Bluesky post...",
"view_on_bluesky": "View this post on Bluesky"
},
"author": {
"view_profile": "View {name}'s profile on Bluesky"
},
Expand Down
Loading