Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
651fe07
feat: barebones implementation of Blog Posts UI (WIP)
Kai-ros Jan 28, 2026
c5fcd2b
feat: generate blog posts content via markdown
jonathanyeong Jan 29, 2026
0878f16
feat: generate lexicons on postinstall & log out marshalling to stand…
jonathanyeong Jan 29, 2026
9d077fd
fix: clean up comments
jonathanyeong Jan 29, 2026
81868e0
fix: fix linting in markdown files
jonathanyeong Jan 29, 2026
1b253b2
feat: implemented BlogPostListCard component for the list page
Kai-ros Jan 30, 2026
6ec8568
feat: add unocss typography plugin for prose styling
jonathanyeong Jan 30, 2026
79e933e
fix: remove uneeded h1 style
jonathanyeong Jan 30, 2026
012472e
chore: Updated TODOs and added Docs page
Kai-ros Jan 30, 2026
c1b4ffc
fix: can't find lexicon types when running pnpm install
jonathanyeong Jan 30, 2026
6fc3a8f
fix: resolved styling issues
Kai-ros Jan 30, 2026
7e87ee1
feat: switch from nuxt/content to unplug-vue-markdown
jonathanyeong Jan 30, 2026
9d3714a
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 30, 2026
1f74b4b
feat: move blog link from app header to footer
jonathanyeong Jan 30, 2026
030df0f
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 30, 2026
a4039c2
docs: update todos
jonathanyeong Jan 30, 2026
7835ef2
Merge branch 'main' into feat/atproto-blog-fe
jonathanyeong Jan 31, 2026
127ad59
refactor: reworked nuxt module with validation, dedupe, and more
Kai-ros Jan 31, 2026
9f6bc48
fix: removed comment and extra line
Kai-ros Jan 31, 2026
ed96391
fix: add h1 to blog list page
abbeyperini Jan 31, 2026
6774771
feat: add hardcoded bluesky comment view
jonathanyeong Jan 31, 2026
391c883
feat: use constellation to fetch bluesky backlinks
jonathanyeong Jan 31, 2026
b76ba2a
fix: get backlinks working
jonathanyeong Feb 1, 2026
3ad742b
fix: add back in ttl and reverse order limiting to 1 record
jonathanyeong Feb 1, 2026
669063c
merge: resolve conflicts with main
danielroe Feb 1, 2026
71f5cca
feat: blog posts (wip)
Kai-ros Feb 6, 2026
45f9bc6
merge main
whitep4nth3r Feb 6, 2026
2052c21
merge main
whitep4nth3r Feb 6, 2026
1ca8d10
feat: add blog menu item to mobile menu (#1147)
colinscz Feb 7, 2026
1d112b4
fix(i18n): various rtl and arabic fixes (#760)
skaldebane Feb 6, 2026
cfe9d55
fix: pass name/version separately in comparison grid (#1098)
danielroe Feb 6, 2026
d58ae02
fix: badge label with empty string should not occupy width (#1095)
btea Feb 6, 2026
33ad33d
fix: the event key may be undefined (#1068)
btea Feb 6, 2026
8424fa4
refactor: share `encodePackageName` utility function (#1066)
bluwy Feb 6, 2026
2a46555
fix: add default theme for no-js users (#1060)
alexdln Feb 6, 2026
2a17a53
fix: update link path in search-suggestion-card (#1102)
alexdln Feb 6, 2026
6cb0feb
feat(i18n): add support for norwegian language (#1104)
bonsak Feb 6, 2026
1cb8dbd
fix: stabilise modal chart render after transition (#1034)
graphieros Feb 6, 2026
87353fe
docs(i18n): correct zh-CN translation of `copy` (#1108)
KazariEX Feb 6, 2026
1ec98d4
chore: enable experimental typescript plugin (#1109)
KazariEX Feb 6, 2026
3d8f7df
feat(i18n): add telugu support (#1106)
KirankumarAmbati Feb 6, 2026
3748d51
test: add atproto lock tests (#1105)
43081j Feb 6, 2026
31fafbd
fix(i18n): improve german translation (#1085)
essenmitsosse Feb 6, 2026
8beb8e6
chore: add RTL CSS Checker to autofix (#1077)
userquin Feb 6, 2026
38158f3
merge main
whitep4nth3r Feb 7, 2026
73ea341
feat: add copy readme as markdown button on package page (#1058)
carwack Feb 6, 2026
56f1223
fix: visible focus rendering of footer's privacy policy link (#1110)
julien-deramond Feb 6, 2026
6f4e63e
fix: prevent modal from taking full width on mobile screen (#1067)
iiio2 Feb 6, 2026
129d94b
docs: remove gitpod as a playground platform (#1057)
sarah11918 Feb 6, 2026
e588f5b
fix: improve package page skeleton loading state (#1115)
danielroe Feb 6, 2026
c96a5f8
feat: load package like data add loading effect (#982)
btea Feb 6, 2026
5440302
chore: store return URLs in session (#1117)
43081j Feb 6, 2026
b9d217f
chore: fix auth error message (#1024)
iiio2 Feb 6, 2026
9085d4a
fix(i18n): update Japanese translation (#1132)
shuuji3 Feb 7, 2026
4b0b850
chore: add `eslint-plugin-regexp` (#1123)
btea Feb 7, 2026
c506c73
fix: do not show back button when there is no history (#1133)
shuuji3 Feb 7, 2026
0d1f837
feat: add subtle gradient on modal chart area (#1127)
graphieros Feb 7, 2026
ac1baf9
docs(i18n): update zh-CN translation (#1131)
KazariEX Feb 7, 2026
d70b4a8
chore: add format settings (#1128)
KazariEX Feb 7, 2026
5077c69
feat: directly navigate to localized atproto website like `https://at…
shuuji3 Feb 7, 2026
277d83b
fix: set `org` to empty string to make sure its route param is omitte…
KazariEX Feb 7, 2026
a27cea0
chore: fix codecov config
danielroe Feb 7, 2026
9a70159
ci: update codecov test report ci upload action
danielroe Feb 7, 2026
9da9597
fix: align deprecated version icon and text (#1125)
iiio2 Feb 7, 2026
6b5d438
fix(ui): correct column dropdown positioning and mobile overflow (#1092)
NandkishorJadoun Feb 7, 2026
21c8b0b
fix: update npm username regex to support underscores and dots (#1134)
NandkishorJadoun Feb 7, 2026
0ec67fb
chore: update `compare-translations.ts` logic (#1063)
userquin Feb 7, 2026
233f6a5
feat: extract button and link component, unify and improve design (#1…
essenmitsosse Feb 7, 2026
86c91ad
docs: add npmx-weekly to README.md (#1142)
trueberryless Feb 7, 2026
99ecd5b
feat(ui): adds social likes to compare page (#940)
Sukiiu Feb 7, 2026
9f2858d
feat: remove space and `@` from user input atproto handle (#1137)
shuuji3 Feb 7, 2026
499868a
fix: opening package docs in a new tab doesn't work (#1145)
NandkishorJadoun Feb 7, 2026
c02fd9c
fix(ui): avoid README actions to be cropped with visible focus (#1144)
julien-deramond Feb 7, 2026
4a9f6a0
fix: clickable rows & mobile scrollbar overflow & dynamic line width …
RYGRIT Feb 7, 2026
815de27
chore: use `getEnv` environment abstraction in lunaria module (#1146)
serhalp Feb 7, 2026
ae47eb9
feat: update readme toc button to use new component (#1112)
IdrisGit Feb 7, 2026
18d1bf0
Merge branch 'main' into feat/atproto-blog-fe
43081j Feb 7, 2026
824ac6f
chore: clean up some leftovers
43081j Feb 7, 2026
9db3aca
merge main
whitep4nth3r Feb 8, 2026
35d0a85
replace text-left with text-start
whitep4nth3r Feb 8, 2026
78beae7
fix unused translation and move authors above article to match incomi…
whitep4nth3r Feb 8, 2026
04acd6d
Merge branch 'main' into feat/atproto-blog-fe
whitep4nth3r Feb 8, 2026
f8662ca
Merge branch 'main' into feat/atproto-blog-fe
whitep4nth3r Feb 8, 2026
84dbf26
Merge branch 'main' into feat/atproto-blog-fe
whitep4nth3r Feb 8, 2026
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
1 change: 1 addition & 0 deletions app/assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
--bg-subtle: var(--bg-subtle-color, oklch(0.198 0 0));
--bg-muted: var(--bg-muted-color, oklch(0.236 0 0));
--bg-elevated: var(--bg-elevated-color, oklch(0.266 0 0));
--bg-blog: oklch(0.195 0 0);

/* text colors */
--fg: oklch(0.982 0 0);
Expand Down
3 changes: 3 additions & 0 deletions app/components/AppFooter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ const showModal = () => modalRef.value?.showModal?.()
<LinkBase :to="{ name: 'about' }">
{{ $t('footer.about') }}
</LinkBase>
<LinkBase :to="{ name: 'blog' }">
{{ $t('footer.blog') }}
</LinkBase>
<LinkBase :to="{ name: 'privacy' }">
{{ $t('privacy_policy.title') }}
</LinkBase>
Expand Down
45 changes: 45 additions & 0 deletions app/components/AuthorAvatar.vue
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>
49 changes: 49 additions & 0 deletions app/components/AuthorList.vue
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>
53 changes: 53 additions & 0 deletions app/components/BlogPostListCard.vue
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}`"
Copy link
Contributor

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

:data-suggestion-index="index"
class="flex items-center gap-4 focus-visible:outline-none after:content-[''] after:absolute after:inset-0"
Copy link
Contributor

Choose a reason for hiding this comment

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

This trick with after was used on other pages to create a backdrop for the entire card even if it wrap only little part - to minimize the link content (better for SEO). If you want to focus on SEO, it's better to wrap only the title in the link with such styles. Otherwise, remove this attribute.

>
<!-- 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>
49 changes: 49 additions & 0 deletions app/components/BlogPostWrapper.vue
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" />
Copy link
Contributor

Choose a reason for hiding this comment

The 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>
Loading
Loading