Skip to content

Commit 120cfa6

Browse files
authored
blog: A first look at local-first Feathers (#8)
1 parent f0ac524 commit 120cfa6

File tree

10 files changed

+391
-8
lines changed

10 files changed

+391
-8
lines changed

app/app.vue

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
<script setup lang="ts">
2+
const siteUrl = 'https://feathers.dev'
3+
const siteName = 'feathers.dev'
4+
const defaultTitle = 'feathers.dev - Identity. Data. Realtime. Beyond the Cloud.'
5+
const defaultDescription = 'Modern web application development with secure user logins, local-first data synchronization and real-time updates. All in one place.'
6+
const defaultImage = `${siteUrl}/img/moon-surface.svg`
7+
28
useHead({
3-
title: 'feathers.dev',
9+
title: defaultTitle,
410
htmlAttrs: {
511
lang: 'en',
612
'data-theme': 'feathers',
@@ -12,6 +18,21 @@ useHead({
1218
},
1319
],
1420
})
21+
22+
useSeoMeta({
23+
title: defaultTitle,
24+
description: defaultDescription,
25+
ogTitle: defaultTitle,
26+
ogDescription: defaultDescription,
27+
ogImage: defaultImage,
28+
ogUrl: siteUrl,
29+
ogType: 'website',
30+
ogSiteName: siteName,
31+
twitterCard: 'summary_large_image',
32+
twitterTitle: defaultTitle,
33+
twitterDescription: defaultDescription,
34+
twitterImage: defaultImage,
35+
})
1536
</script>
1637

1738
<template>

app/components/BlogPostTile.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ defineProps<{
99

1010
<template>
1111
<NuxtLink :to="post.path" class="block h-full">
12-
<Card :class="['h-full hover:shadow-xl transition-shadow cursor-pointer', cardClasses]">
12+
<Card :class="['h-full cursor-pointer', cardClasses]">
1313
<figure class="h-52 !rounded-box md:m-4 overflow-hidden">
1414
<img :src="post.meta.imgSrc" :alt="post.title" class="object-cover h-full w-full object-center" />
1515
</figure>
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<script setup lang="ts">
2+
interface Props {
3+
uri: string
4+
cid: string
5+
}
6+
7+
const props = defineProps<Props>()
8+
const embedContainer = ref<HTMLElement | null>(null)
9+
10+
// Extract the post URL from the AT URI
11+
// Format: at://did:plc:xxx/app.bsky.feed.post/postId
12+
const getPostUrl = () => {
13+
const parts = props.uri.split('/')
14+
const did = parts[2]
15+
const postId = parts[parts.length - 1]
16+
return `https://bsky.app/profile/${did}/post/${postId}`
17+
}
18+
19+
const postUrl = getPostUrl()
20+
21+
onMounted(() => {
22+
// Load the Bluesky embed script
23+
const scriptId = 'bluesky-embed-script'
24+
25+
const initializeEmbed = () => {
26+
// Wait a bit longer and check multiple times
27+
let attempts = 0
28+
const maxAttempts = 10
29+
30+
const checkAndScan = () => {
31+
attempts++
32+
33+
if ((window as any).bluesky?.scan) {
34+
console.log('Bluesky scan function found, triggering...')
35+
;(window as any).bluesky.scan()
36+
} else if (attempts < maxAttempts) {
37+
console.log(`Waiting for Bluesky script... attempt ${attempts}`)
38+
setTimeout(checkAndScan, 200)
39+
} else {
40+
console.error('Bluesky embed script did not load properly')
41+
}
42+
}
43+
44+
setTimeout(checkAndScan, 100)
45+
}
46+
47+
if (!document.getElementById(scriptId)) {
48+
const script = document.createElement('script')
49+
script.id = scriptId
50+
script.src = 'https://embed.bsky.app/static/embed.js'
51+
script.async = true
52+
script.charset = 'utf-8'
53+
54+
script.onload = () => {
55+
console.log('Bluesky embed script loaded')
56+
initializeEmbed()
57+
}
58+
59+
script.onerror = () => {
60+
console.error('Failed to load Bluesky embed script')
61+
}
62+
63+
document.head.appendChild(script)
64+
} else {
65+
initializeEmbed()
66+
}
67+
})
68+
</script>
69+
70+
<template>
71+
<ClientOnly>
72+
<div class="bluesky-embed-wrapper my-6 rounded-lg" ref="embedContainer">
73+
<blockquote
74+
class="bluesky-embed"
75+
:data-bluesky-uri="uri"
76+
:data-bluesky-cid="cid"
77+
data-bluesky-embed-color-mode="dark"
78+
>
79+
<p lang="en">
80+
Loading Bluesky post...
81+
<a :href="postUrl" target="_blank" rel="noopener noreferrer">View on Bluesky</a>
82+
</p>
83+
</blockquote>
84+
</div>
85+
</ClientOnly>
86+
</template>
87+
88+
<style scoped>
89+
.bluesky-embed-wrapper :deep(iframe) {
90+
border-radius: 0.7rem !important;
91+
}
92+
</style>

app/components/content/ProseImg.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,8 @@ const refinedSrc = computed(() => {
3030
</script>
3131

3232
<template>
33-
<img :src="refinedSrc" :alt="alt" :width="width" :height="height">
33+
<div class="prose-img text-center mb-4">
34+
<img :src="refinedSrc" :alt="alt" :width="width" :height="height" class="mb-1 rounded-md" />
35+
<small v-if="alt">{{ alt }}</small>
36+
</div>
3437
</template>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<script setup lang="ts">
2+
interface Props {
3+
src?: string
4+
async?: boolean
5+
defer?: boolean
6+
type?: string
7+
}
8+
9+
const props = defineProps<Props>()
10+
11+
// Only allow specific whitelisted scripts for security
12+
const allowedScripts = [
13+
'https://embed.bsky.app/static/embed.js'
14+
]
15+
16+
const isAllowed = props.src && allowedScripts.includes(props.src)
17+
18+
if (isAllowed && props.src) {
19+
useHead({
20+
script: [
21+
{
22+
src: props.src,
23+
async: props.async ?? true,
24+
defer: props.defer,
25+
type: props.type || 'text/javascript',
26+
}
27+
]
28+
})
29+
}
30+
</script>
31+
32+
<template>
33+
<!-- Empty template - script is loaded via useHead -->
34+
<div v-if="!isAllowed && src" class="border border-yellow-200 rounded-lg p-4 bg-yellow-50 text-yellow-800">
35+
<p class="text-sm">Script from "{{ src }}" is not in the allowlist and cannot be loaded.</p>
36+
</div>
37+
</template>

app/pages/blog/posts/[...slug].vue

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,21 @@ watch(
2828
{ immediate: false },
2929
)
3030
31+
const siteUrl = 'https://feathers.dev'
32+
const pageUrl = computed(() => `${siteUrl}/blog/posts/${slug.value}`)
33+
3134
useSeoMeta({
3235
title: post.value?.title,
33-
description: post.value?.description,
36+
description: post.value?.meta?.tagline || post.value?.description,
37+
ogTitle: post.value?.title,
38+
ogDescription: post.value?.meta?.tagline || post.value?.description,
39+
ogImage: post.value?.meta?.imgSrc,
40+
ogUrl: pageUrl.value,
41+
ogType: 'article',
42+
twitterCard: 'summary_large_image',
43+
twitterTitle: post.value?.title,
44+
twitterDescription: post.value?.meta?.tagline || post.value?.description,
45+
twitterImage: post.value?.meta?.imgSrc,
3446
})
3547
3648
const { data: allRecentPosts } = await useAsyncData(() =>
@@ -60,7 +72,11 @@ const recentPosts = computed(() => {
6072
<div class="bg-base-200 min-h-screen max-w-[82rem] mx-auto -mt-64 rounded-4xl p-6 pt-12 lg:p-12">
6173
<div v-if="post" class="prose mx-auto mb-24">
6274
<figure class="aspect-video">
63-
<img :src="post?.meta?.imgSrc" :alt="post?.title!" class="object-cover h-full w-full object-center" />
75+
<img
76+
:src="post?.meta?.imgSrc"
77+
:alt="post?.title!"
78+
class="object-cover h-full w-full object-center rounded-lg"
79+
/>
6480
</figure>
6581

6682
<Titles :title="post?.title!" :sub-title="post?.meta?.tagline!" />

app/pages/index.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ definePageMeta({
33
layout: 'page',
44
})
55
6-
const { data: posts } = await useAsyncData(() => queryCollection('blogPosts').order('date', 'DESC').all())
6+
const { data: posts } = await useAsyncData(() => queryCollection('blogPosts').order('date', 'DESC').limit(3).all())
77
const { data: products } = await useAsyncData(() => queryCollection('products').where('published', '=', true).all())
88
99
useSeoMeta({

content/blog/posts/2024-08-19-dwebcamp-2024.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ author: The Feathers Crew
77
category: DWeb
88
imgSrc: https://imagedelivery.net/9JPgw8SmnowT-UlbCrbUxw/1b3d43ed-2400-4f42-6afb-4eaf7b968100/public
99
imgContainerClasses: 'h-64 sm:h-96 md:h-120'
10-
pinned: false
1110
tags:
1211
- dweb
1312
---

content/blog/posts/2025-01-03-lowfiwknd.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ author: David Luecke
77
category: DWeb
88
imgSrc: https://imagedelivery.net/9JPgw8SmnowT-UlbCrbUxw/0b83d606-e110-4fd8-f0c6-88266dfc1600/public
99
imgContainerClasses: 'h-64 sm:h-96 md:h-120'
10-
pinned: true
1110
tags:
1211
- dweb
1312
---

0 commit comments

Comments
 (0)