-
-
Notifications
You must be signed in to change notification settings - Fork 225
feat: blog #1094
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?
feat: blog #1094
Changes from all commits
651fe07
c5fcd2b
0878f16
9d077fd
81868e0
1b253b2
6ec8568
79e933e
012472e
c1b4ffc
6fc3a8f
7e87ee1
9d3714a
1f74b4b
030df0f
a4039c2
7835ef2
127ad59
9f6bc48
ed96391
6774771
391c883
b76ba2a
3ad742b
669063c
71f5cca
45f9bc6
2052c21
1ca8d10
1d112b4
cfe9d55
d58ae02
33ad33d
8424fa4
2a46555
2a17a53
6cb0feb
1cb8dbd
87353fe
1ec98d4
3d8f7df
3748d51
31fafbd
8beb8e6
38158f3
73ea341
56f1223
6f4e63e
129d94b
e588f5b
c96a5f8
5440302
b9d217f
9085d4a
4b0b850
c506c73
0d1f837
ac1baf9
d70b4a8
5077c69
277d83b
a27cea0
9a70159
9da9597
6b5d438
21c8b0b
0ec67fb
233f6a5
86c91ad
99ecd5b
9f2858d
499868a
c02fd9c
4a9f6a0
815de27
ae47eb9
18d1bf0
824ac6f
9db3aca
35d0a85
78beae7
04acd6d
f8662ca
84dbf26
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| <script setup lang="ts"> | ||
| import type { ResolvedAuthor } from '#shared/schemas/blog' | ||
|
|
||
| const props = defineProps<{ | ||
| author: ResolvedAuthor | ||
| size?: 'sm' | 'md' | 'lg' | ||
| }>() | ||
|
|
||
| const sizeClasses = computed(() => { | ||
| switch (props.size ?? 'md') { | ||
| case 'sm': | ||
| return 'w-8 h-8 text-sm' | ||
| case 'lg': | ||
| return 'w-12 h-12 text-xl' | ||
| default: | ||
| return 'w-10 h-10 text-lg' | ||
| } | ||
| }) | ||
|
|
||
| const initials = computed(() => | ||
| props.author.name | ||
| .split(' ') | ||
| .map(n => n[0]) | ||
| .join('') | ||
| .toUpperCase() | ||
| .slice(0, 2), | ||
| ) | ||
| </script> | ||
|
|
||
| <template> | ||
| <div | ||
| class="shrink-0 flex items-center justify-center border border-border rounded-full bg-bg-muted overflow-hidden" | ||
| :class="[sizeClasses]" | ||
| > | ||
| <img | ||
| v-if="author.avatar" | ||
| :src="author.avatar" | ||
| :alt="author.name" | ||
| class="w-full h-full object-cover" | ||
| /> | ||
| <span v-else class="text-fg-subtle font-mono" aria-hidden="true"> | ||
| {{ initials }} | ||
| </span> | ||
| </div> | ||
| </template> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| <script setup lang="ts"> | ||
| import type { Author } from '#shared/schemas/blog' | ||
|
|
||
| const props = defineProps<{ | ||
| authors: Author[] | ||
| variant?: 'compact' | 'expanded' | ||
| }>() | ||
|
|
||
| const { resolvedAuthors } = useAuthorProfiles(props.authors) | ||
| </script> | ||
|
|
||
| <template> | ||
| <!-- Expanded variant: vertical list with larger avatars --> | ||
| <div v-if="variant === 'expanded'" class="flex flex-wrap items-center gap-4"> | ||
| <div v-for="author in resolvedAuthors" :key="author.name" class="flex items-center gap-2"> | ||
| <AuthorAvatar :author="author" size="md" disable-link /> | ||
| <div class="flex flex-col"> | ||
| <span class="text-sm font-medium text-fg">{{ author.name }}</span> | ||
| <a | ||
| v-if="author.blueskyHandle && author.profileUrl" | ||
| :href="author.profileUrl" | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| :aria-label="$t('blog.author.view_profile', { name: author.name })" | ||
| class="text-xs text-fg-muted hover:text-primary transition-colors" | ||
| > | ||
| @{{ author.blueskyHandle }} | ||
| </a> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <!-- Compact variant: no avatars --> | ||
| <div v-else class="flex items-center gap-2 min-w-0"> | ||
| <div class="flex items-center"> | ||
| <AuthorAvatar | ||
| v-for="(author, index) in resolvedAuthors" | ||
| :key="author.name" | ||
| :author="author" | ||
| size="md" | ||
| class="ring-2 ring-bg" | ||
| :class="index > 0 ? '-ms-3' : ''" | ||
| /> | ||
| </div> | ||
| <span class="text-xs text-fg-muted font-mono truncate"> | ||
| {{ resolvedAuthors.map(a => a.name).join(', ') }} | ||
| </span> | ||
| </div> | ||
| </template> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| <script setup lang="ts"> | ||
| import type { Author } from '#shared/schemas/blog' | ||
|
|
||
| defineProps<{ | ||
| /** Authors of the blog post */ | ||
| authors: Author[] | ||
| /** Blog Title */ | ||
| title: string | ||
| /** Tags such as OpenSource, Architecture, Community, etc. */ | ||
| topics: string[] | ||
| /** Brief line from the text. */ | ||
| excerpt: string | ||
| /** The datetime value (ISO string or Date) */ | ||
| published: string | ||
| /** Path/Slug of the post */ | ||
| path: string | ||
| /** For keyboard nav scaffold */ | ||
| index: number | ||
| }>() | ||
| </script> | ||
|
|
||
| <template> | ||
| <article | ||
| class="group relative hover:bg-bg-subtle transition-colors duration-150 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-bg focus-within:ring-offset-2 focus-within:ring-fg/50 -mx-4 px-4 -my-2 py-2 sm:-mx-6 sm:px-6 sm:-my-3 sm:py-3 sm:rounded-md" | ||
| > | ||
| <NuxtLink | ||
| :to="`/blog/${path}`" | ||
| :data-suggestion-index="index" | ||
| class="flex items-center gap-4 focus-visible:outline-none after:content-[''] after:absolute after:inset-0" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This trick with |
||
| > | ||
| <!-- Text Content --> | ||
| <div class="flex-1 min-w-0 text-start gap-2"> | ||
| <span class="text-xs text-fg-muted font-mono">{{ published }}</span> | ||
| <h2 | ||
| class="font-mono text-xl font-medium text-fg group-hover:text-primary transition-colors hover:underline" | ||
| > | ||
| {{ title }} | ||
| </h2> | ||
| <p v-if="excerpt" class="text-fg-muted leading-relaxed line-clamp-2 no-underline"> | ||
| {{ excerpt }} | ||
| </p> | ||
| <div class="flex flex-wrap items-center gap-2 text-xs text-fg-muted font-mono mt-4"> | ||
| <AuthorList :authors="authors" /> | ||
| </div> | ||
| </div> | ||
|
|
||
| <span | ||
| class="i-carbon:arrow-right w-4 h-4 text-fg-subtle group-hover:text-fg relative inset-is-0 group-hover:inset-is-1 transition-all duration-200 shrink-0" | ||
| aria-hidden="true" | ||
| /> | ||
| </NuxtLink> | ||
| </article> | ||
| </template> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| <script setup lang="ts"> | ||
| import type { BlogPostFrontmatter } from '#shared/schemas/blog' | ||
|
|
||
| const props = defineProps<{ | ||
| frontmatter: BlogPostFrontmatter | ||
| }>() | ||
|
|
||
| useSeoMeta({ | ||
| title: props.frontmatter.title, | ||
| description: props.frontmatter.description || props.frontmatter.excerpt, | ||
| ogTitle: props.frontmatter.title, | ||
| ogDescription: props.frontmatter.description || props.frontmatter.excerpt, | ||
| ogType: 'article', | ||
| }) | ||
|
|
||
| const slug = computed(() => props.frontmatter.slug) | ||
|
|
||
| // Use Constellation to find the Bluesky post linking to this blog post | ||
| const { data: blueskyLink } = await useBlogPostBlueskyLink(slug) | ||
| const blueskyPostUri = computed(() => blueskyLink.value?.postUri ?? null) | ||
| </script> | ||
|
|
||
| <template> | ||
| <main class="container w-full py-8"> | ||
| <div v-if="frontmatter.authors" class="mb-12 max-w-prose mx-auto"> | ||
| <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> | ||
| <AuthorList :authors="frontmatter.authors" variant="expanded" /> | ||
| </div> | ||
| </div> | ||
| <article class="max-w-prose mx-auto p-2 border-b border-border prose dark:prose-invert"> | ||
| <div class="text-sm text-fg-muted font-mono mb-4"> | ||
| <DateTime :datetime="frontmatter.date" year="numeric" month="short" day="numeric" /> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't we respect the user's settings here in terms of date format? |
||
| </div> | ||
| <slot /> | ||
| </article> | ||
|
|
||
| <!-- | ||
| - Only renders if Constellation found a Bluesky post linking to this slug | ||
| - Cached API route avoids rate limits during build | ||
| --> | ||
| <LazyBlueskyComments v-if="blueskyPostUri" :post-uri="blueskyPostUri" /> | ||
| </main> | ||
| </template> | ||
|
|
||
| <style scoped> | ||
| :deep(.markdown-body) { | ||
| @apply prose dark:prose-invert; | ||
| } | ||
| </style> | ||
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.
@danielroe should we repeat the logic with typed paths here? I'm a bit confused about what the params should be in this case