Skip to content

Commit b77e45a

Browse files
committed
feat: barebones implementation of Blog Posts UI (WIP)
1 parent 803a38f commit b77e45a

8 files changed

Lines changed: 175 additions & 0 deletions

File tree

app/components/AppHeader.vue

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ onKeyStroke(',', e => {
2323
e.preventDefault()
2424
router.push('/settings')
2525
})
26+
onKeyStroke('.', e => {
27+
const target = e.target as HTMLElement
28+
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
29+
return
30+
}
31+
32+
e.preventDefault()
33+
router.push('/blog')
34+
})
2635
</script>
2736

2837
<template>
@@ -72,6 +81,22 @@ onKeyStroke(',', e => {
7281
<li v-if="isConnected && npmUser" class="flex items-center">
7382
<HeaderOrgsDropdown :username="npmUser" />
7483
</li>
84+
85+
<li class="flex items-center">
86+
<NuxtLink
87+
to="/blog"
88+
class="link-subtle font-mono text-sm inline-flex items-center gap-2"
89+
aria-keyshortcuts="."
90+
>
91+
{{ $t('nav.blog') }}
92+
<kbd
93+
class="hidden sm:inline-flex items-center justify-center w-5 h-5 text-xs bg-bg-muted border border-border rounded"
94+
aria-hidden="true"
95+
>
96+
.
97+
</kbd>
98+
</NuxtLink>
99+
</li>
75100
</ul>
76101

77102
<!-- Right: User status + GitHub -->

app/components/BlogPost.server.vue

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<script setup lang="ts">
2+
const props = defineProps<{ title: string; htmlContent: string; date: string }>()
3+
</script>
4+
<template>
5+
<main class="container">
6+
<h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-2">
7+
{{ title }}
8+
</h1>
9+
<!-- <time class="text-sm text-gray-500 dark:text-gray-400"> -->
10+
11+
<DateTime
12+
:datetime="date"
13+
year="numeric"
14+
month="short"
15+
day="numeric"
16+
class="text-xs text-fg-subtle"
17+
/>
18+
19+
<div class="" v-html="htmlContent" />
20+
</main>
21+
</template>
22+
23+
<style>
24+
.container {
25+
background: var(--bg-blue);
26+
}
27+
</style>

app/pages/blog/[...path].vue

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<script setup lang="ts">
2+
const route = useRoute('blog-post')
3+
const router = useRouter()
4+
5+
interface Post {
6+
title: string
7+
date: string
8+
body: string
9+
}
10+
11+
const fakePosts: Record<string, Post> = {
12+
'hello-world': {
13+
title: 'Hello World',
14+
date: 'Jan 28, 2026',
15+
body: '<p>This is the first post.</p>',
16+
},
17+
'server-components': {
18+
title: 'Server Components',
19+
date: 'Jan 29, 2026',
20+
body: '<p>Zero JS is great.</p>',
21+
},
22+
}
23+
24+
const slug = computed<string>(() => {
25+
const pathParam = route.params.path
26+
return Array.isArray(pathParam) ? pathParam.join('/') : pathParam
27+
})
28+
29+
const post = computed(() => {
30+
const s = slug.value
31+
return s ? fakePosts[s] : null
32+
})
33+
34+
definePageMeta({
35+
name: 'blog-post',
36+
// alias: ['/:path(.*)*'],
37+
})
38+
39+
useSeoMeta({
40+
title: () => $t('blog.post.title'),
41+
42+
// description: () => `Blog Article ${post.value}@${post.value}`,
43+
})
44+
</script>
45+
46+
<template>
47+
<main class="container py-8 sm:py-12 w-full">
48+
<!-- Header -->
49+
<header class="mb-8 pb-8 border-b border-border">
50+
<div class="">I AM A WEAK HEADER</div>
51+
</header>
52+
53+
<article v-if="post">
54+
<BlogPost :title="post.title" :date="post.date" :html-content="post.body" />
55+
</article>
56+
57+
<article v-else>
58+
<h1>Post Not Found</h1>
59+
<p>We couldn't find a post at /blog/{{ slug }}</p>
60+
</article>
61+
</main>
62+
</template>

app/pages/blog/index.vue

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<script setup lang="ts">
2+
const route = useRoute('blog')
3+
const router = useRouter()
4+
5+
// 1. Define your list of posts (This would eventually come from useAsyncData)
6+
const posts = [
7+
{ slug: 'hello-world', title: 'Hello World', excerpt: 'My first post' },
8+
{ slug: 'server-components', title: 'Server Components', excerpt: 'Zero JS' },
9+
]
10+
11+
definePageMeta({
12+
name: 'blog',
13+
// alias: ['/:path(.*)*'],
14+
})
15+
16+
useSeoMeta({
17+
title: () => $t('blog.title'),
18+
description: () => $t('blog.description'),
19+
})
20+
</script>
21+
22+
<template>
23+
<main class="container py-8 sm:py-12 w-full">
24+
<header class="mb-8 pb-8 border-b border-border">
25+
<div class="">I AM A MIGHTY HEADER</div>
26+
</header>
27+
28+
<article class="flex flex-col gap-6">
29+
<div v-for="post in posts" :key="post.slug" class="p-4 border border-border rounded-lg">
30+
<h2 class="text-xl font-semibold">
31+
<NuxtLink :to="`/blog/${post.slug}`" class="text-primary hover:underline">
32+
{{ post.title }}
33+
</NuxtLink>
34+
</h2>
35+
<p class="text-muted-foreground">{{ post.excerpt }}</p>
36+
</div>
37+
</article>
38+
</main>
39+
</template>
40+
41+
<style>
42+
.container {
43+
background: var(--bg-red);
44+
}
45+
</style>

i18n/locales/en.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,14 @@
3838
},
3939
"nav": {
4040
"popular_packages": "Popular packages",
41+
"blog": "blog",
4142
"search": "search",
4243
"settings": "settings"
4344
},
45+
"blog": {
46+
"title": "Blog",
47+
"description": "Insights and updates from the npmx community"
48+
},
4449
"settings": {
4550
"relative_dates": "Relative dates",
4651
"include_types": "Include {'@'}types in install",

i18n/locales/fr.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,14 @@
3737
},
3838
"nav": {
3939
"popular_packages": "Paquets populaires",
40+
"blog": "blog",
4041
"search": "recherche",
4142
"settings": "paramètres"
4243
},
44+
"blog": {
45+
"title": "Blog",
46+
"description": "Perspectives et actualités de la communauté npmx"
47+
},
4348
"settings": {
4449
"relative_dates": "Dates relatives",
4550
"include_types": "Inclure {'@'}types à la commande d'installation",

i18n/locales/it.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,14 @@
3030
},
3131
"nav": {
3232
"popular_packages": "Pacchetti popolari",
33+
"blog": "blog",
3334
"search": "cerca",
3435
"settings": "impostazioni"
3536
},
37+
"blog": {
38+
"title": "Blog",
39+
"description": "Approfondimenti e aggiornamenti dalla comunità npmx"
40+
},
3641
"settings": {
3742
"relative_dates": "Date relative",
3843
"include_types": "Includi {'@'}types durante l'installazione",

nuxt.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export default defineNuxtConfig({
7777
'/**': { isr: 60 },
7878
'/package/**': { isr: 60 },
7979
'/search': { isr: false, cache: false },
80+
'/blog/**': { isr: true, prerender: true },
8081
'/_v/script.js': { proxy: 'https://npmx.dev/_vercel/insights/script.js' },
8182
'/_v/view': { proxy: 'https://npmx.dev/_vercel/insights/view' },
8283
'/_v/event': { proxy: 'https://npmx.dev/_vercel/insights/event' },

0 commit comments

Comments
 (0)