Files
portfolio/.kiro/specs/nuxt-content-blog/design.md
T
2025-11-09 13:56:03 +03:30

1322 lines
33 KiB
Markdown

# Blog System Design Document
## Overview
This document outlines the technical design for implementing a fully-featured blog system in the Nuxt 4 portfolio application using Nuxt Content v3. The design integrates seamlessly with the existing architecture, leveraging Nuxt UI components, i18n for bilingual support, and modern performance optimization techniques.
### Design Goals
1. **Content-First Architecture**: Use file-based markdown content management for simplicity and version control
2. **Seamless Integration**: Maintain consistency with existing portfolio design system and navigation
3. **Bilingual Support**: Full English and Persian (RTL) support with locale-specific content
4. **Performance**: Optimize for fast page loads, SEO, and Core Web Vitals
5. **Developer Experience**: Hot-reload, TypeScript safety, and intuitive content authoring
6. **Extensibility**: Support for MDC components, custom frontmatter, and future enhancements
### Technology Stack
- **Content Management**: @nuxt/content v3.x
- **UI Components**: Nuxt UI v4 (existing)
- **Styling**: Tailwind CSS v4 (existing)
- **Internationalization**: @nuxtjs/i18n (existing)
- **Image Optimization**: @nuxt/image (existing)
- **Syntax Highlighting**: Shiki (built-in with Nuxt Content)
- **Type Safety**: TypeScript with custom interfaces
## Architecture
### High-Level Architecture Diagram
```
┌─────────────────────────────────────────────────────────────┐
│ Nuxt 4 Application │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Pages │ │ Components │ │
│ │ │ │ │ │
│ │ /blog/ │─────▶│ BlogList │ │
│ │ index.vue │ │ BlogCard │ │
│ │ │ │ BlogSearch │ │
│ │ /blog/ │ │ TagFilter │ │
│ │ [...slug] │─────▶│ BlogPost │ │
│ │ .vue │ │ TableOfContents │
│ └──────────────┘ │ BlogNav │ │
│ │ └──────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ Nuxt Content Module │ │
│ │ - queryContent() API │ │
│ │ - ContentDoc component │ │
│ │ - ContentRenderer component │ │
│ │ - useContentHead() composable │ │
│ └─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ Content Directory │ │
│ │ content/ │ │
│ │ ├── en/blog/*.md │ │
│ │ └── fa/blog/*.md │ │
│ └─────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
### Directory Structure
```
project-root/
├── content/ # Content directory (new)
│ ├── en/
│ │ └── blog/
│ │ ├── my-first-post.md
│ │ ├── nuxt-tips.md
│ │ └── tutorials/
│ │ └── getting-started.md
│ └── fa/
│ └── blog/
│ ├── first-post-fa.md
│ └── nuxt-tips-fa.md
├── app/
│ ├── pages/
│ │ └── blog/
│ │ ├── index.vue # Blog listing page (existing, to be updated)
│ │ └── [...slug].vue # Blog detail page (existing, to be updated)
│ │
│ ├── components/
│ │ └── blog/ # Blog components (new)
│ │ ├── BlogCard.vue
│ │ ├── BlogList.vue
│ │ ├── BlogSearch.vue
│ │ ├── BlogTagFilter.vue
│ │ ├── BlogPost.vue
│ │ ├── BlogTableOfContents.vue
│ │ ├── BlogNavigation.vue
│ │ └── BlogEmpty.vue
│ │
│ ├── composables/
│ │ └── useBlog.ts # Blog utilities composable (new)
│ │
│ ├── types/
│ │ └── blog.ts # Blog TypeScript types (new)
│ │
│ └── public/
│ └── img/
│ └── blog/ # Blog images (new)
│ └── default-cover.jpg
├── server/
│ └── routes/
│ └── blog/
│ └── rss.xml.ts # RSS feed generator (new)
└── nuxt.config.ts # Updated with @nuxt/content
```
### Content Flow
1. **Content Creation**: Developer writes markdown files in `content/{locale}/blog/`
2. **Content Parsing**: Nuxt Content parses markdown and frontmatter on server start
3. **Content Query**: Pages use `queryContent()` to fetch filtered/sorted content
4. **Content Rendering**: `ContentRenderer` transforms markdown to HTML with Vue components
5. **Content Display**: Nuxt UI components style the rendered content
## Components and Interfaces
### Page Components
#### 1. Blog Listing Page (`app/pages/blog/index.vue`)
**Purpose**: Display all published blog posts with search and filtering capabilities
**Key Features**:
- Fetch posts using `queryContent()`
- Search functionality with debounce
- Tag filtering
- Responsive grid layout
- Empty state handling
- Locale-aware content fetching
**Component Structure**:
```vue
<template>
<UContainer>
<BlogSearch v-model="searchQuery" />
<BlogTagFilter v-model="selectedTag" :tags="allTags" />
<BlogList :posts="filteredPosts" />
<BlogEmpty v-if="filteredPosts.length === 0" />
</UContainer>
</template>
```
**Data Fetching Strategy**:
```typescript
const { locale } = useI18n()
const route = useRoute()
// Fetch posts for current locale
const { data: posts } = await useAsyncData('blog-posts', () =>
queryContent(`${locale.value}/blog`)
.where({ draft: { $ne: true } })
.sort({ date: -1 })
.only(['title', 'description', 'date', 'tags', '_path', 'image'])
.find()
)
```
#### 2. Blog Detail Page (`app/pages/blog/[...slug].vue`)
**Purpose**: Render individual blog post with full content and metadata
**Key Features**:
- Fetch single post by slug
- Render markdown content with syntax highlighting
- Display metadata (title, date, tags, reading time)
- Table of contents for long posts
- Previous/next post navigation
- SEO meta tags
- Breadcrumb navigation
**Component Structure**:
```vue
<template>
<UContainer>
<UBreadcrumb :links="breadcrumbs" />
<article>
<BlogPost :post="post" />
<ContentRenderer :value="post" />
<BlogTableOfContents v-if="post.body?.toc" :toc="post.body.toc" />
</article>
<BlogNavigation :prev="prevPost" :next="nextPost" />
</UContainer>
</template>
```
**Data Fetching Strategy**:
```typescript
const { locale } = useI18n()
const route = useRoute()
const slug = route.params.slug as string[]
// Fetch current post
const { data: post } = await useAsyncData(`blog-post-${slug.join('/')}`, () =>
queryContent(`${locale.value}/blog`)
.where({ _path: `/${locale.value}/blog/${slug.join('/')}` })
.findOne()
)
if (!post.value) {
throw createError({ statusCode: 404, message: 'Post not found' })
}
// Fetch adjacent posts for navigation
const { data: adjacentPosts } = await useAsyncData('adjacent-posts', () =>
queryContent(`${locale.value}/blog`)
.where({ draft: { $ne: true } })
.sort({ date: -1 })
.only(['title', '_path', 'date'])
.find()
)
```
### UI Components
#### 1. BlogCard Component
**Purpose**: Display blog post preview in listing page
**Props**:
```typescript
interface BlogCardProps {
post: BlogPost
}
```
**Features**:
- Cover image with lazy loading
- Title and description
- Formatted date
- Reading time estimate
- Tags as badges
- Hover effects
- Click to navigate
**Implementation Notes**:
- Use `UCard` from Nuxt UI as base
- Use `NuxtImg` for optimized images
- Use `UBadge` for tags
- Use `localePath()` for navigation
#### 2. BlogSearch Component
**Purpose**: Search input for filtering posts
**Props**:
```typescript
interface BlogSearchProps {
modelValue: string
}
interface BlogSearchEmits {
'update:modelValue': [value: string]
}
```
**Features**:
- Debounced input (300ms)
- Clear button
- Search icon
- Placeholder text (i18n)
**Implementation Notes**:
- Use `UInput` with icon slots
- Use `useDebounceFn` from VueUse
- Emit updates to parent
#### 3. BlogTagFilter Component
**Purpose**: Display and filter by tags
**Props**:
```typescript
interface BlogTagFilterProps {
tags: string[]
modelValue: string | null
}
```
**Features**:
- Display all unique tags
- Highlight active tag
- Clear filter option
- Responsive layout
**Implementation Notes**:
- Use `UButton` or `UBadge` for tags
- Use query parameters for state persistence
- Horizontal scroll on mobile
#### 4. BlogTableOfContents Component
**Purpose**: Display navigable table of contents for long posts
**Props**:
```typescript
interface TocLink {
id: string
text: string
depth: number
children?: TocLink[]
}
interface BlogTableOfContentsProps {
toc: {
links: TocLink[]
}
}
```
**Features**:
- Nested heading structure
- Active section highlighting
- Smooth scroll to sections
- Sticky positioning on desktop
- Collapsible on mobile
**Implementation Notes**:
- Use `IntersectionObserver` for active tracking
- Use `scrollIntoView({ behavior: 'smooth' })` for navigation
- Use `UAccordion` for mobile collapsible version
#### 5. BlogNavigation Component
**Purpose**: Previous/next post navigation
**Props**:
```typescript
interface BlogNavigationProps {
prev: BlogPost | null
next: BlogPost | null
}
```
**Features**:
- Previous and next post links
- Post titles
- Directional arrows
- Keyboard navigation support
**Implementation Notes**:
- Use `UButton` with icon slots
- Use `@keydown` for arrow key navigation
- Use flexbox for layout
#### 6. BlogPost Component
**Purpose**: Display post metadata header
**Props**:
```typescript
interface BlogPostProps {
post: BlogPost
}
```
**Features**:
- Post title (h1)
- Formatted publish date
- Reading time estimate
- Author info (if available)
- Tags
- Cover image
**Implementation Notes**:
- Use semantic HTML (`<article>`, `<header>`, `<time>`)
- Use `useDateFormat` from VueUse for date formatting
- Calculate reading time from word count
## Data Models
### BlogPost Interface
```typescript
import type { ParsedContent } from '@nuxt/content/dist/runtime/types'
export interface BlogPost extends ParsedContent {
// Required fields
title: string
description: string
date: string // ISO 8601 format
tags: string[]
// Optional fields
image?: string // Cover image path
author?: string
draft?: boolean
updatedAt?: string
// Custom SEO
head?: {
title?: string
description?: string
image?: string
}
// Computed by Nuxt Content
_path: string
_dir: string
_draft: boolean
_partial: boolean
_locale: string
_empty: boolean
body: {
type: string
children: any[]
toc?: {
title: string
searchDepth: number
depth: number
links: TocLink[]
}
}
}
```
### Frontmatter Example
```yaml
---
title: "Getting Started with Nuxt Content"
description: "Learn how to build a blog with Nuxt Content v3 and TypeScript"
date: "2024-11-09"
tags: ["nuxt", "vue", "typescript", "tutorial"]
image: "/img/blog/nuxt-content-cover.jpg"
author: "Ali Arghyani"
draft: false
---
```
## Composables
### useBlog Composable
**Purpose**: Centralize blog-related utilities and logic
**Location**: `app/composables/useBlog.ts`
**Exports**:
```typescript
export function useBlog() {
const { locale } = useI18n()
const route = useRoute()
/**
* Calculate reading time from word count
*/
const calculateReadingTime = (content: any): number => {
if (!content?.body?.children) return 0
const text = JSON.stringify(content.body.children)
const wordCount = text.split(/\s+/).length
return Math.ceil(wordCount / 200) // 200 words per minute
}
/**
* Format date for display
*/
const formatDate = (dateString: string): string => {
const date = new Date(dateString)
return new Intl.DateTimeFormat(locale.value, {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(date)
}
/**
* Get all unique tags from posts
*/
const extractUniqueTags = (posts: BlogPost[]): string[] => {
const tagSet = new Set<string>()
posts.forEach(post => {
post.tags?.forEach(tag => tagSet.add(tag))
})
return Array.from(tagSet).sort()
}
/**
* Get blog path for current locale
*/
const getBlogPath = (): string => {
return `${locale.value}/blog`
}
/**
* Filter posts by search query
*/
const filterPostsBySearch = (posts: BlogPost[], query: string): BlogPost[] => {
if (!query) return posts
const lowerQuery = query.toLowerCase()
return posts.filter(post =>
post.title?.toLowerCase().includes(lowerQuery) ||
post.description?.toLowerCase().includes(lowerQuery) ||
post.tags?.some(tag => tag.toLowerCase().includes(lowerQuery))
)
}
/**
* Filter posts by tag
*/
const filterPostsByTag = (posts: BlogPost[], tag: string | null): BlogPost[] => {
if (!tag) return posts
return posts.filter(post => post.tags?.includes(tag))
}
return {
calculateReadingTime,
formatDate,
extractUniqueTags,
getBlogPath,
filterPostsBySearch,
filterPostsByTag
}
}
```
## Nuxt Content Configuration
### nuxt.config.ts Updates
```typescript
export default defineNuxtConfig({
modules: [
'@nuxt/content', // Add before other modules
'@nuxt/fonts',
'@nuxt/ui',
'@nuxtjs/i18n',
'@nuxtjs/color-mode',
'@nuxt/image'
],
content: {
// Highlight code blocks with Shiki
highlight: {
theme: {
default: 'github-light',
dark: 'github-dark'
},
preload: ['typescript', 'javascript', 'vue', 'css', 'bash', 'json']
},
// Enable MDC syntax
markdown: {
mdc: true,
toc: {
depth: 3,
searchDepth: 3
}
},
// Document-driven mode disabled (we use custom pages)
documentDriven: false,
// Respect gitignore
respectPathCase: true
},
// Prerender blog routes
nitro: {
prerender: {
crawlLinks: true,
routes: ['/blog', '/fa/blog']
}
},
// Route rules for blog
routeRules: {
'/blog': { swr: 3600 },
'/fa/blog': { swr: 3600 },
'/blog/**': { swr: 3600 },
'/fa/blog/**': { swr: 3600 }
}
})
```
## Styling and Theming
### Prose Components Customization
Nuxt UI provides Prose components for styling markdown content. We'll customize them to match the portfolio design.
**Approach**:
1. Use Nuxt UI's built-in Prose components (ProseH1, ProseP, ProseCode, etc.)
2. Override styles via `app.config.ts` if needed
3. Apply consistent spacing and typography
**Example Prose Styling**:
```typescript
// app.config.ts additions
export default defineAppConfig({
ui: {
prose: {
// Customize prose components
h1: {
base: 'text-4xl font-semibold mb-4 mt-8',
color: 'text-gray-900 dark:text-gray-100'
},
h2: {
base: 'text-3xl font-semibold mb-3 mt-6',
color: 'text-gray-900 dark:text-gray-100'
},
p: {
base: 'text-lg leading-relaxed mb-4',
color: 'text-gray-700 dark:text-gray-300'
},
code: {
base: 'font-mono text-sm',
inline: 'px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-800'
}
}
}
})
```
### Blog-Specific Styles
**Content Width**:
- Max width: 65ch for optimal readability
- Wider for code blocks and images
**Typography**:
- Body text: 18px / 1.75 line-height
- Headings: Existing font stack (Fraunces for headings, Inter for body)
**Code Blocks**:
- Syntax highlighting with Shiki
- Copy button overlay
- Language label
- Line numbers for long blocks
**Images**:
- Responsive with `NuxtImg`
- Lazy loading
- Caption support via markdown
### RTL Support
**CSS Direction Handling**:
```vue
<template>
<div :dir="locale === 'fa' ? 'rtl' : 'ltr'" class="blog-content">
<ContentRenderer :value="post" />
</div>
</template>
<style scoped>
.blog-content[dir="rtl"] {
/* RTL-specific adjustments */
text-align: right;
}
.blog-content[dir="rtl"] code {
/* Keep code LTR even in RTL context */
direction: ltr;
text-align: left;
}
</style>
```
## SEO Implementation
### Meta Tags Strategy
**Page-Level SEO** (`app/pages/blog/[...slug].vue`):
```typescript
const { locale } = useI18n()
const config = useAppConfig()
// Use Nuxt Content's built-in SEO helper
useContentHead(post)
// Additional custom meta tags
useSeoMeta({
title: `${post.value.title} | Blog | ${config.siteName}`,
description: post.value.description,
ogTitle: post.value.title,
ogDescription: post.value.description,
ogImage: post.value.image || '/img/blog/default-cover.jpg',
ogType: 'article',
ogUrl: `${config.siteUrl}${post.value._path}`,
twitterCard: 'summary_large_image',
twitterTitle: post.value.title,
twitterDescription: post.value.description,
twitterImage: post.value.image || '/img/blog/default-cover.jpg',
articlePublishedTime: post.value.date,
articleModifiedTime: post.value.updatedAt || post.value.date,
articleAuthor: post.value.author || 'Ali Arghyani',
articleTag: post.value.tags
})
// JSON-LD structured data
useHead({
script: [
{
type: 'application/ld+json',
children: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.value.title,
description: post.value.description,
image: post.value.image,
datePublished: post.value.date,
dateModified: post.value.updatedAt || post.value.date,
author: {
'@type': 'Person',
name: post.value.author || 'Ali Arghyani'
},
publisher: {
'@type': 'Person',
name: 'Ali Arghyani'
}
})
}
]
})
```
## RSS Feed Implementation
### Server Route Design
**Location**: `server/routes/blog/rss.xml.ts`
**Implementation**:
```typescript
import { serverQueryContent } from '#content/server'
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const locale = event.node.req.url?.includes('/fa/') ? 'fa' : 'en'
// Fetch published posts
const posts = await serverQueryContent(event, `${locale}/blog`)
.where({ draft: { $ne: true } })
.sort({ date: -1 })
.limit(20)
.find()
// Generate RSS XML
const rss = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Ali Arghyani - Blog</title>
<link>${config.public.siteUrl}/${locale}/blog</link>
<description>Stories and notes from Ali Arghyani</description>
<language>${locale}</language>
<atom:link href="${config.public.siteUrl}/${locale}/blog/rss.xml" rel="self" type="application/rss+xml"/>
${posts.map(post => `
<item>
<title>${escapeXml(post.title)}</title>
<link>${config.public.siteUrl}${post._path}</link>
<guid>${config.public.siteUrl}${post._path}</guid>
<pubDate>${new Date(post.date).toUTCString()}</pubDate>
<description>${escapeXml(post.description)}</description>
</item>
`).join('')}
</channel>
</rss>`
event.node.res.setHeader('Content-Type', 'application/rss+xml')
return rss
})
function escapeXml(unsafe: string): string {
return unsafe.replace(/[<>&'"]/g, (c) => {
switch (c) {
case '<': return '&lt;'
case '>': return '&gt;'
case '&': return '&amp;'
case "'": return '&apos;'
case '"': return '&quot;'
default: return c
}
})
}
```
## Error Handling
### 404 Handling
When a blog post is not found:
```typescript
// In [...slug].vue
const { data: post } = await useAsyncData(`blog-post-${slug}`, () =>
queryContent(getBlogPath())
.where({ _path: `/${getBlogPath()}/${slug.join('/')}` })
.findOne()
)
if (!post.value) {
throw createError({
statusCode: 404,
message: 'Blog post not found',
fatal: true
})
}
```
This will trigger the existing `app/error.vue` page.
### Content Parsing Errors
Nuxt Content will show errors in development mode with:
- File path
- Line number
- Error description
In production, malformed markdown files will be skipped.
### Missing Translations
When a post exists in one language but not another:
```typescript
// In blog listing
const { data: posts } = await useAsyncData('blog-posts', async () => {
try {
return await queryContent(getBlogPath())
.where({ draft: { $ne: true } })
.sort({ date: -1 })
.find()
} catch (error) {
// Return empty array if content directory doesn't exist
return []
}
})
```
Display a message: "This post is not available in [language]. View in [other language]."
## Testing Strategy
### Manual Testing Checklist
**Content Creation**:
- [ ] Create markdown file with valid frontmatter
- [ ] Create markdown file with missing required fields
- [ ] Create draft post
- [ ] Create post with cover image
- [ ] Create post with code blocks
- [ ] Create post with MDC components
**Blog Listing**:
- [ ] View listing page in English
- [ ] View listing page in Persian
- [ ] Test search functionality
- [ ] Test tag filtering
- [ ] Test empty state (no posts)
- [ ] Test responsive layout (mobile, tablet, desktop)
**Blog Detail**:
- [ ] View post in English
- [ ] View post in Persian (RTL)
- [ ] Test table of contents navigation
- [ ] Test previous/next navigation
- [ ] Test code block copy functionality
- [ ] Test 404 for non-existent post
- [ ] Verify SEO meta tags in browser dev tools
**Performance**:
- [ ] Run Lighthouse audit
- [ ] Check image lazy loading
- [ ] Verify static generation
- [ ] Test hot-reload in development
**Accessibility**:
- [ ] Keyboard navigation
- [ ] Screen reader compatibility
- [ ] Color contrast
- [ ] Focus indicators
## Performance Optimization
### Image Optimization
**Strategy**:
1. Use `NuxtImg` for all blog images
2. Generate responsive srcsets
3. Lazy load images below fold
4. Use modern formats (WebP, AVIF)
**Implementation**:
```vue
<!-- In markdown or components -->
<NuxtImg
:src="post.image"
:alt="post.title"
width="1200"
height="630"
format="webp"
loading="lazy"
sizes="sm:100vw md:80vw lg:1200px"
/>
```
### Code Splitting
**Approach**:
- Blog components are auto-imported (lazy by default)
- Use `defineAsyncComponent` for heavy components like TOC
- Separate chunks for blog routes
**Example**:
```typescript
// In blog/index.vue
const BlogTableOfContents = defineAsyncComponent(
() => import('~/components/blog/BlogTableOfContents.vue')
)
```
### Content Query Optimization
**Best Practices**:
1. Use `.only()` to fetch minimal fields for listings
2. Use `.without()` to exclude heavy fields like body
3. Cache queries with `useAsyncData`
4. Limit results with `.limit()`
**Example**:
```typescript
// Listing page - only fetch necessary fields
const { data: posts } = await useAsyncData('blog-posts', () =>
queryContent(getBlogPath())
.where({ draft: { $ne: true } })
.sort({ date: -1 })
.only(['title', 'description', 'date', 'tags', '_path', 'image'])
.find()
)
// Detail page - fetch full content
const { data: post } = await useAsyncData(`blog-post-${slug}`, () =>
queryContent(getBlogPath())
.where({ _path: `/${getBlogPath()}/${slug}` })
.findOne()
)
```
### Static Generation
**Configuration**:
```typescript
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
prerender: {
crawlLinks: true,
routes: async () => {
// Dynamically generate routes for all blog posts
const { $content } = useNuxtApp()
const enPosts = await $content('en/blog').only(['_path']).find()
const faPosts = await $content('fa/blog').only(['_path']).find()
return [
'/blog',
'/fa/blog',
...enPosts.map(p => p._path),
...faPosts.map(p => p._path)
]
}
}
}
})
```
### Caching Strategy
**Route Rules**:
- Blog listing: SWR 1 hour
- Blog posts: SWR 1 hour
- RSS feed: SWR 1 hour
```typescript
routeRules: {
'/blog': { swr: 3600 },
'/fa/blog': { swr: 3600 },
'/blog/**': { swr: 3600 },
'/fa/blog/**': { swr: 3600 },
'/blog/rss.xml': { swr: 3600 },
'/fa/blog/rss.xml': { swr: 3600 }
}
```
## Internationalization Integration
### Locale-Based Content Fetching
**Pattern**:
```typescript
const { locale } = useI18n()
const { localePath } = useI18n()
// Fetch content for current locale
const posts = await queryContent(`${locale.value}/blog`).find()
// Generate localized links
const postLink = localePath(`/blog/${post.slug}`)
```
### Translation Keys
**New i18n keys to add**:
```json
{
"blog": {
"title": "Blog",
"explore": "Stories & notes",
"empty": "No posts yet. Check back soon!",
"readMore": "Read article",
"readingTime": "{minutes} min read",
"publishedOn": "Published on",
"updatedOn": "Updated on",
"backToBlog": "Back to Blog",
"previousPost": "Previous",
"nextPost": "Next",
"tableOfContents": "Table of Contents",
"searchPlaceholder": "Search posts...",
"filterByTag": "Filter by tag",
"allPosts": "All posts",
"noResults": "No posts found matching your search",
"copyCode": "Copy code",
"codeCopied": "Copied!",
"subscribe": "Subscribe via RSS"
}
}
```
### RTL Layout Adjustments
**CSS Strategy**:
```css
/* Automatic RTL with Tailwind */
.blog-content {
@apply text-start; /* Uses start instead of left */
}
/* Force LTR for code */
.blog-content pre,
.blog-content code {
direction: ltr;
text-align: left;
}
/* RTL-aware spacing */
.blog-content ul,
.blog-content ol {
@apply ps-6; /* padding-inline-start */
}
```
## Migration from Existing Blog Pages
### Current State
The existing blog pages (`app/pages/blog/index.vue` and `app/pages/blog/[...slug].vue`) have placeholder content indicating Nuxt Content is disabled.
### Migration Steps
1. **Backup existing files** (if needed)
2. **Replace page content** with new implementations
3. **Add i18n translations** for blog-specific keys
4. **Create sample blog posts** for testing
5. **Update navigation** to enable blog link in TopNav
### Backward Compatibility
No breaking changes expected since:
- Routes remain the same (`/blog`, `/fa/blog`)
- i18n structure is preserved
- Existing components are not affected
## MDC Component Examples
### Custom Components in Markdown
Nuxt Content's MDC syntax allows embedding Vue components in markdown.
**Example 1: Alert Component**
```markdown
::alert{type="info"}
This is an informational alert using MDC syntax.
::
```
**Example 2: Code Group**
```markdown
::code-group
```bash [npm]
npm install @nuxt/content
\```
```bash [pnpm]
pnpm add @nuxt/content
\```
::
```
**Example 3: Custom Blog Component**
```markdown
::blog-callout{title="Pro Tip"}
Use `queryContent()` with `.only()` to optimize performance.
::
```
**Component Implementation** (`app/components/content/BlogCallout.vue`):
```vue
<template>
<UCard class="my-6 border-l-4 border-primary-500">
<template #header>
<h4 class="font-semibold">{{ title }}</h4>
</template>
<div class="prose prose-sm">
<slot />
</div>
</UCard>
</template>
<script setup lang="ts">
defineProps<{
title: string
}>()
</script>
```
## Development Workflow
### Content Authoring Workflow
1. **Create markdown file** in `content/{locale}/blog/`
2. **Add frontmatter** with required fields
3. **Write content** using markdown and MDC syntax
4. **Preview in dev mode** (drafts visible)
5. **Test in both locales** if bilingual
6. **Set draft: false** when ready to publish
7. **Commit to git** (content is version controlled)
### Hot Reload Experience
Nuxt Content provides instant feedback:
- **Markdown changes**: Hot-reload without page refresh
- **Frontmatter changes**: Hot-reload
- **New files**: Detected automatically
- **Component changes**: Standard Nuxt HMR
### Debugging Tools
**1. Content API Endpoint**:
```
http://localhost:3000/api/_content/query
```
**2. Dev Tools**:
- Nuxt DevTools shows content queries
- Vue DevTools for component inspection
**3. Console Logging**:
```typescript
const posts = await queryContent('en/blog').find()
console.log('Fetched posts:', posts)
```
## Security Considerations
### Content Sanitization
Nuxt Content automatically sanitizes HTML in markdown to prevent XSS attacks.
**Safe by default**:
- User-provided markdown is parsed safely
- HTML tags are escaped unless explicitly allowed
- Script tags are stripped
### Draft Post Protection
```typescript
// Ensure drafts are hidden in production
const isDev = process.dev
const query = queryContent(getBlogPath())
.where({ draft: { $ne: true } })
if (isDev) {
// In dev, show all posts including drafts
query.where({}) // Remove draft filter
}
```
### Image Security
- Use `@nuxt/image` for automatic optimization
- Validate image paths
- Use CDN for external images
## Deployment Considerations
### Build Process
1. **Install dependencies**: `pnpm install`
2. **Build application**: `pnpm build`
3. **Content parsing**: Nuxt Content parses all markdown during build
4. **Static generation**: Blog routes are prerendered
5. **Deploy**: Upload `.output` directory
### Environment Variables
```bash
# .env
NUXT_PUBLIC_SITE_URL=https://aliarghyani.vercel.app
NUXT_PUBLIC_SITE_NAME=Ali Arghyani
```
### Vercel Configuration
```json
{
"buildCommand": "pnpm build",
"outputDirectory": ".output/public",
"framework": "nuxtjs"
}
```
### Content Updates
**Option 1: Git-based workflow**
- Commit markdown files to repository
- Trigger rebuild on push
- Vercel auto-deploys
**Option 2: CMS integration** (future enhancement)
- Use Nuxt Studio or other headless CMS
- Webhook triggers rebuild
- Content preview before publish
## Future Enhancements
### Phase 2 Features (Not in current scope)
1. **Comments System**
- Integration with Giscus or Utterances
- GitHub-based comments
2. **Related Posts**
- Algorithm based on tags
- Display at end of post
3. **Reading Progress Bar**
- Scroll-based progress indicator
- Sticky header with progress
4. **Social Sharing**
- Share buttons for Twitter, LinkedIn
- Copy link functionality
5. **Newsletter Subscription**
- Email capture form
- Integration with email service
6. **Blog Analytics**
- View count tracking
- Popular posts widget
7. **Content Series**
- Multi-part article support
- Series navigation
8. **Dark Mode Code Themes**
- Multiple syntax highlighting themes
- User preference storage
## Summary
This design provides a comprehensive, production-ready blog system that:
- ✅ Integrates seamlessly with existing Nuxt 4 portfolio
- ✅ Supports bilingual content (EN/FA) with RTL
- ✅ Uses Nuxt Content v3 best practices
- ✅ Optimizes for performance and SEO
- ✅ Maintains type safety with TypeScript
- ✅ Provides excellent developer experience
- ✅ Follows accessibility standards
- ✅ Scales for future enhancements
The implementation will be done incrementally following the task list in the next phase.