feat: enhance blog components, improve routing, and add clean script for build process

This commit is contained in:
mahdiarghyani
2025-12-14 13:37:51 +03:30
parent 25aa41bcb8
commit c6b09fabc9
11 changed files with 117 additions and 37 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
<template>
<UInput :model-value="modelValue" :placeholder="t('blog.searchPlaceholder')" icon="i-heroicons-magnifying-glass"
size="lg" :ui="{ icon: { trailing: { pointer: '' } } }" @update:model-value="handleInput">
size="lg" @update:model-value="handleInput">
<template v-if="modelValue" #trailing>
<UButton color="gray" variant="link" icon="i-heroicons-x-mark-20-solid" :padded="false" @click="clearSearch" />
</template>
@@ -26,6 +26,12 @@ const shouldShowToc = computed(() => {
return countLinks(props.toc.links) >= 3
})
const setHashWithoutNavigation = (id: string) => {
const url = new URL(window.location.href)
url.hash = id
window.history.replaceState(window.history.state, '', url.toString())
}
// Smooth scroll to heading with offset for sticky header
const scrollToHeading = (id: string) => {
const element = document.getElementById(id)
@@ -40,11 +46,28 @@ const scrollToHeading = (id: string) => {
})
activeId.value = id
setHashWithoutNavigation(id)
}
}
// Track active section with IntersectionObserver
onMounted(() => {
// Handle initial hash (avoid full page navigation / transition)
const initialId = window.location.hash.replace(/^#/, '')
if (initialId) {
setTimeout(() => {
const el = document.getElementById(initialId)
if (!el) return
const offset = 100
const elementPosition = el.getBoundingClientRect().top + window.pageYOffset
const offsetPosition = elementPosition - offset
window.scrollTo({ top: offsetPosition, behavior: 'auto' })
activeId.value = initialId
}, 0)
}
const headings = document.querySelectorAll('article h2, article h3, article h4')
const observer = new IntersectionObserver(
+3 -4
View File
@@ -3,11 +3,10 @@
<UContainer>
<div class="flex flex-col items-center gap-4 text-center text-sm text-gray-600 dark:text-gray-400">
<ClientOnly>
<NuxtImg :src="logoSrc" alt="Ali Arghyani logo" width="64" height="64" class="h-12 w-12" format="png"
loading="lazy" />
<img :src="logoSrc" alt="Ali Arghyani logo" width="64" height="64" class="h-12 w-12" loading="lazy" />
<template #fallback>
<NuxtImg src="/favicon/android-chrome-192x192.png" alt="Ali Arghyani logo" width="64" height="64"
class="h-12 w-12" format="png" loading="lazy" />
<img src="/favicon/android-chrome-192x192.png" alt="Ali Arghyani logo" width="64" height="64"
class="h-12 w-12" loading="lazy" />
</template>
</ClientOnly>
<p>&copy; {{ currentYear }}, <span class="font-semibold text-gray-900 dark:text-gray-100">AliArghyani</span> -
+2 -4
View File
@@ -1,9 +1,7 @@
<template>
<UCard :ui="{
base: 'my-6',
body: { padding: 'p-4 sm:p-5' },
ring: 'ring-2',
divide: ''
root: 'my-6 ring-2',
body: 'p-4 sm:p-5'
}" :class="cardClass">
<div class="flex items-start gap-3">
<UIcon :name="icon" :class="iconClass" class="mt-0.5 h-5 w-5 flex-shrink-0" />
+40 -24
View File
@@ -2,17 +2,28 @@
const { locale, t } = useI18n()
const localePath = useLocalePath()
const route = useRoute()
const slug = Array.isArray(route.params.slug) ? route.params.slug : [route.params.slug]
// Fetch current post
const { data: post } = await useAsyncData(`blog-post-${slug.join('/')}`, async () => {
const posts = await queryCollection('blog')
.where('path', '=', `/${locale.value}/blog/${slug.join('/')}`)
.first()
return posts
const slugParts = computed(() => {
const raw = route.params.slug
const parts = Array.isArray(raw) ? raw : [raw]
return parts.filter((p): p is string => typeof p === 'string' && p.length > 0)
})
if (!post.value) {
// Fetch current post
const { data: post, error: postError } = await useAsyncData(
() => `blog-post-${locale.value}-${slugParts.value.join('/')}`,
async () => {
return await queryCollection('blog')
.where('path', '=', `/${locale.value}/blog/${slugParts.value.join('/')}`)
.first()
},
{
watch: [locale, slugParts],
server: true
}
)
if (process.server && (!post.value || postError.value)) {
throw createError({
statusCode: 404,
message: 'Blog post not found',
@@ -21,18 +32,25 @@ if (!post.value) {
}
// Fetch all posts for prev/next navigation
const { data: allPosts } = await useAsyncData(`blog-posts-nav-${locale.value}`, async () => {
const posts = await queryCollection('blog')
.where('draft', '<>', true)
.order('date', 'DESC')
.all()
const { data: allPosts } = await useAsyncData(
() => `blog-posts-nav-${locale.value}`,
async () => {
const posts = await queryCollection('blog')
.where('draft', '<>', true)
.order('date', 'DESC')
.all()
// Filter by locale and posts without draft field
return posts.filter((p: any) =>
p.path?.startsWith(`/${locale.value}/blog/`) &&
(p.draft === false || p.draft === undefined)
)
})
// Filter by locale and posts without draft field
return posts.filter((p: any) =>
p.path?.startsWith(`/${locale.value}/blog/`) &&
(p.draft === false || p.draft === undefined)
)
},
{
watch: [locale],
server: true
}
)
// Calculate adjacent posts
const currentIndex = computed(() => {
@@ -158,11 +176,9 @@ if (post.value) {
<!-- Sidebar: Table of Contents (Desktop) -->
<aside v-if="(post as any).body?.toc?.links?.length" class="hidden lg:block">
<UContentToc :links="(post as any).body.toc.links" :title="t('blog.tableOfContents')" color="primary"
highlight :ui="{
root: 'sticky top-24',
container: 'bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 p-4'
}" />
<div class="sticky top-24">
<BlogTableOfContents :toc="(post as any).body.toc" />
</div>
</aside>
</div>
</div>
+1 -1
View File
@@ -29,7 +29,7 @@ const { extractUniqueTags, filterPostsBySearch, filterPostsByTag } = useBlog()
// Fetch posts using queryCollection (Nuxt Content v3 API)
const { data: posts } = await useAsyncData<any[]>(
`blog-posts-${locale.value}`,
() => `blog-posts-${locale.value}`,
async () => {
try {
// Use queryCollection for Nuxt Content v3