BlogNow.Tech logoBlogNow.Tech
DocsPricing

    Getting Started

    • Introduction
    • Quick Start
    • Authentication

    Integrations

    • Next.js
    • React
    • Vite
    • Vue.js
    • Nuxt
    • Astro

    SDK

    • Overview

    API Reference

    • Overview

    Vue.js Integration Guide

    Integrate BlogNow CMS with Vue 3 projects using Composition API. Includes Pinia state management, Vue Router, and production-ready examples.

    💡 Quick Start: Copy this entire guide and paste it into Claude, Cursor, or any AI coding assistant to automatically set up BlogNow in your Vue 3 project.

    BlogNow SDK Integration Prompt for Vue 3 Projects

    Context

    You are an expert Vue.js developer tasked with integrating BlogNow SDK into a Vue 3 project to create a complete blog system. Based on the analysis of production implementations, create all necessary files and configurations for a fully functional blog with the following requirements:

    Pre-Implementation Analysis

    1. Check Existing Project Setup

    Before implementing, analyze the existing project to understand:

    Build Tool:

    • Check if the project uses Vite, Vue CLI, or Webpack
    • Vite is the recommended and modern approach for Vue 3
    • Check vite.config.js/ts or vue.config.js

    State Management Library:

    • Check if the project uses Pinia (recommended for Vue 3), Vuex, or another state management solution
    • Look for existing stores in src/stores, src/store, or similar directories
    • Check package.json for state management dependencies
    • If no state management exists, ask the user: "No state management library detected. Would you like to proceed with Pinia (recommended for Vue 3), or do you prefer Vuex?"

    Icon Library:

    • Check if the project uses any icon library (lucide-vue-next, @iconify/vue, heroicons-vue, etc.)
    • Look for existing icon imports in components
    • Check package.json for icon library dependencies
    • If no icon library exists, proceed with lucide-vue-next

    UI Component Library:

    • Check if the project uses Vuetify, PrimeVue, Naive UI, Element Plus, or other component libraries
    • Look for existing component patterns and styling approaches
    • Adapt the implementation to match the existing UI patterns

    Router:

    • Confirm Vue Router is installed and configured
    • Check the router mode (history, hash)
    • Look for existing routing patterns

    Styling:

    • Check if the project uses Tailwind CSS, SCSS, CSS modules, or plain CSS
    • Adapt the styling approach to match the existing setup

    Project Setup Requirements

    1. Package Installation

    Install the required dependencies:

    # Core BlogNow SDK (always required) npm install @blognow/sdk # State management (install only if not already present) npm install pinia # recommended for Vue 3 # or npm install vuex # if preferred # Icons (install only if not already present) npm install lucide-vue-next # Router (install only if not already present) npm install vue-router # If using Tailwind CSS (only if not already in project): npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p

    2. Environment Configuration

    Create/update .env:

    VITE_BLOGNOW_API_KEY=your_public_api_key_here VITE_BLOGNOW_BASE_URL=https://api.blognow.tech

    Security Note: BlogNow SDK only requires the public API key (VITE_BLOGNOW_API_KEY) which is safe for client-side use. No private API key or server-side routes are needed.

    3. Vite Configuration

    Update vite.config.ts to add path alias:

    import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; import { fileURLToPath, URL } from 'node:url'; export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } } });

    Core Implementation Files

    4. BlogNow Client Configuration (src/lib/blognow.ts)

    import { BlogNowClient } from '@blognow/sdk'; let blogClient: BlogNowClient | null = null; const apiKey = import.meta.env.VITE_BLOGNOW_API_KEY; const baseUrl = import.meta.env.VITE_BLOGNOW_BASE_URL; export const createBlogNowClient = async () => { if (!blogClient) { if (!apiKey) { throw new Error( 'VITE_BLOGNOW_API_KEY is not configured in environment variables' ); } blogClient = new BlogNowClient({ apiKey, baseUrl }); } return blogClient; };

    5. Blog Store with Pinia (src/stores/blog.ts)

    Note: Adapt this implementation to match the project's existing state management library.

    If using Pinia (recommended for Vue 3):

    import { defineStore } from 'pinia'; import { ref, computed } from 'vue'; import { createBlogNowClient } from '@/lib/blognow'; import type { Post, PaginatedResponse } from '@blognow/sdk'; export const useBlogStore = defineStore('blog', () => { // State const posts = ref<Post[]>([]); const currentPost = ref<Post | null>(null); const loading = ref(false); const error = ref<string | null>(null); const pagination = ref({ total: 0, page: 1, size: 10, pages: 0 }); const searchQuery = ref(''); const selectedTag = ref<string | null>(null); // Getters const filteredPosts = computed(() => { let result = posts.value; if (searchQuery.value) { result = result.filter(post => post.title.toLowerCase().includes(searchQuery.value.toLowerCase()) ); } if (selectedTag.value) { result = result.filter(post => post.tags?.some(tag => tag.name === selectedTag.value) ); } return result; }); // Actions const fetchPosts = async ( page = 1, size = 10, query = '', sortBy: 'created_at' | 'updated_at' | 'published_at' = 'published_at', sortOrder: 'asc' | 'desc' = 'desc' ) => { try { loading.value = true; error.value = null; const blogClient = await createBlogNowClient(); const result: PaginatedResponse<Post> = await blogClient.posts.getPublishedPosts({ page, size, query, sort_by: sortBy, sort_order: sortOrder, }); if (page === 1) { posts.value = result.items; } else { posts.value = [...posts.value, ...result.items]; } pagination.value = { page: result.page || 1, size: result.size || 10, total: result.total || 0, pages: result.pages || 0, }; } catch (err) { error.value = err instanceof Error ? err.message : 'Failed to fetch posts'; } finally { loading.value = false; } }; const fetchPostBySlug = async (slug: string) => { try { loading.value = true; error.value = null; const blogClient = await createBlogNowClient(); const result = await blogClient.posts.getPost(slug); currentPost.value = result; } catch (err) { error.value = err instanceof Error ? err.message : 'Failed to fetch post'; } finally { loading.value = false; } }; const filterByTag = (tag: string | null) => { selectedTag.value = tag; }; const setSearchQuery = (query: string) => { searchQuery.value = query; }; const clearCurrentPost = () => { currentPost.value = null; }; const resetPagination = () => { pagination.value = { page: 1, size: 10, total: 0, pages: 0 }; }; return { // State posts, currentPost, loading, error, pagination, searchQuery, selectedTag, // Getters filteredPosts, // Actions fetchPosts, fetchPostBySlug, filterByTag, setSearchQuery, clearCurrentPost, resetPagination, }; });

    If using Vuex:

    import { createStore } from 'vuex'; import { createBlogNowClient } from '@/lib/blognow'; import type { Post } from '@blognow/sdk'; interface BlogState { posts: Post[]; currentPost: Post | null; loading: boolean; error: string | null; pagination: { total: number; page: number; size: number; pages: number; }; searchQuery: string; selectedTag: string | null; } export default createStore<BlogState>({ state: { posts: [], currentPost: null, loading: false, error: null, pagination: { total: 0, page: 1, size: 10, pages: 0 }, searchQuery: '', selectedTag: null, }, mutations: { SET_POSTS(state, posts: Post[]) { state.posts = posts; }, APPEND_POSTS(state, posts: Post[]) { state.posts = [...state.posts, ...posts]; }, SET_CURRENT_POST(state, post: Post | null) { state.currentPost = post; }, SET_LOADING(state, loading: boolean) { state.loading = loading; }, SET_ERROR(state, error: string | null) { state.error = error; }, SET_PAGINATION(state, pagination: BlogState['pagination']) { state.pagination = pagination; }, SET_SEARCH_QUERY(state, query: string) { state.searchQuery = query; }, SET_SELECTED_TAG(state, tag: string | null) { state.selectedTag = tag; }, }, actions: { async fetchPosts({ commit }, { page = 1, size = 10, query = '' } = {}) { commit('SET_LOADING', true); commit('SET_ERROR', null); try { const blogClient = await createBlogNowClient(); const result = await blogClient.posts.getPublishedPosts({ page, size, query, sort_by: 'published_at', sort_order: 'desc', }); if (page === 1) { commit('SET_POSTS', result.items); } else { commit('APPEND_POSTS', result.items); } commit('SET_PAGINATION', { page: result.page || 1, size: result.size || 10, total: result.total || 0, pages: result.pages || 0, }); } catch (error) { commit('SET_ERROR', error instanceof Error ? error.message : 'Failed to fetch posts'); } finally { commit('SET_LOADING', false); } }, async fetchPostBySlug({ commit }, slug: string) { commit('SET_LOADING', true); commit('SET_ERROR', null); try { const blogClient = await createBlogNowClient(); const post = await blogClient.posts.getPost(slug); commit('SET_CURRENT_POST', post); } catch (error) { commit('SET_ERROR', error instanceof Error ? error.message : 'Failed to fetch post'); } finally { commit('SET_LOADING', false); } }, }, });

    6. Router Configuration (src/router/index.ts)

    import { createRouter, createWebHistory } from 'vue-router'; import BlogList from '@/views/BlogList.vue'; import BlogPost from '@/views/BlogPost.vue'; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', redirect: '/blog' }, { path: '/blog', name: 'blog', component: BlogList, meta: { title: 'Blog', description: 'Insights, updates, and stories from our team' } }, { path: '/blog/:slug', name: 'blog-post', component: BlogPost, meta: { title: 'Blog Post' } } ], scrollBehavior(to, from, savedPosition) { if (savedPosition) { return savedPosition; } else { return { top: 0 }; } } }); export default router;

    7. SEO Composable (src/composables/useSEO.ts)

    import { watch, onMounted } from 'vue'; import type { Post } from '@blognow/sdk'; interface SEOConfig { title: string; description: string; image?: string; url?: string; type?: 'website' | 'article'; publishedTime?: string; author?: string; } export function useSEO(config: SEOConfig | (() => SEOConfig)) { const updateMetaTags = () => { const seoConfig = typeof config === 'function' ? config() : config; // Update document title document.title = seoConfig.title; const updateMetaTag = (name: string, content: string, property = false) => { const selector = property ? `meta[property="${name}"]` : `meta[name="${name}"]`; let meta = document.querySelector(selector) as HTMLMetaElement; if (!meta) { meta = document.createElement('meta'); meta.setAttribute(property ? 'property' : 'name', name); document.head.appendChild(meta); } meta.setAttribute('content', content); }; // Basic meta tags updateMetaTag('description', seoConfig.description); // Open Graph tags updateMetaTag('og:title', seoConfig.title, true); updateMetaTag('og:description', seoConfig.description, true); updateMetaTag('og:type', seoConfig.type || 'website', true); if (seoConfig.image) { updateMetaTag('og:image', seoConfig.image, true); } if (seoConfig.url) { updateMetaTag('og:url', seoConfig.url, true); } // Twitter Card tags updateMetaTag('twitter:card', 'summary_large_image'); updateMetaTag('twitter:title', seoConfig.title); updateMetaTag('twitter:description', seoConfig.description); if (seoConfig.image) { updateMetaTag('twitter:image', seoConfig.image); } // Article specific tags if (seoConfig.type === 'article') { if (seoConfig.publishedTime) { updateMetaTag('article:published_time', seoConfig.publishedTime, true); } if (seoConfig.author) { updateMetaTag('article:author', seoConfig.author, true); } } }; onMounted(() => { updateMetaTags(); }); // If config is reactive, watch for changes if (typeof config === 'function') { watch(() => config(), updateMetaTags, { deep: true }); } } // Helper composable for blog posts export function useBlogPostSEO(post: Post | null) { useSEO(() => { if (!post) { return { title: 'Blog | Your Company Name', description: 'Insights, updates, and stories from our team', type: 'website' as const }; } return { title: post.title, description: post.excerpt, image: post.og_image_url, url: `${window.location.origin}/blog/${post.slug}`, type: 'article' as const, publishedTime: post.published_at, author: post.author ? `${post.author.first_name} ${post.author.last_name}` : undefined, }; }); }

    Blog Components Implementation

    8. Blog List View (src/views/BlogList.vue)

    <template> <div class="min-h-screen bg-gray-50"> <!-- Header --> <div class="bg-white border-b"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12"> <div class="text-center"> <h1 class="text-4xl font-bold text-gray-900 mb-4">Our Blog</h1> <p class="text-xl text-gray-600 max-w-2xl mx-auto"> Insights, updates, and stories from our team </p> </div> </div> </div> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12"> <!-- Search and Filters --> <div class="mb-8"> <form @submit.prevent="handleSearch" class="flex gap-4 mb-6"> <div class="flex-1 relative"> <Search class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" /> <input v-model="searchInput" type="text" placeholder="Search articles..." class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" /> </div> <button type="submit" class="bg-blue-600 text-white px-8 py-2 rounded-lg hover:bg-blue-700 transition-colors" > Search </button> </form> </div> <!-- Error State --> <div v-if="blogStore.error" class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6" > {{ blogStore.error }} </div> <!-- Loading State --> <div v-if="blogStore.loading && blogStore.posts.length === 0" class="flex justify-center items-center py-12"> <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div> </div> <template v-else> <!-- Empty State --> <div v-if="displayPosts.length === 0" class="text-center py-12"> <p class="text-gray-600 text-lg">No articles found.</p> </div> <!-- Posts Grid --> <div v-else class="grid gap-8 md:grid-cols-1 lg:grid-cols-2"> <article v-for="post in displayPosts" :key="post.id" class="bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow overflow-hidden" > <!-- Featured Image --> <div v-if="post.og_image_url" class="relative overflow-hidden"> <img :src="post.og_image_url" :alt="post.title" class="w-full h-64 object-cover hover:scale-105 transition-transform duration-300" /> </div> <div class="p-6"> <!-- Meta Information --> <div class="flex items-center gap-4 text-sm text-gray-500 mb-3"> <span>{{ formatDate(post.published_at) }}</span> </div> <!-- Title --> <h2 class="text-xl font-bold text-gray-900 mb-3 line-clamp-2"> {{ post.title }} </h2> <!-- Excerpt --> <p class="text-gray-600 mb-4 line-clamp-3"> {{ post.excerpt }} </p> <!-- Footer --> <div class="flex items-center justify-between"> <div class="flex items-center gap-3"> <img v-if="post.author?.avatar_url" :src="post.author.avatar_url" :alt="`${post.author.first_name} ${post.author.last_name}`" class="h-8 w-8 rounded-full object-cover" /> <span class="text-sm font-medium text-gray-900"> {{ post.author?.first_name }} {{ post.author?.last_name }} </span> </div> <router-link :to="`/blog/${post.slug}`" class="inline-flex items-center gap-1 text-blue-600 hover:text-blue-800 font-medium text-sm" > Read more <ChevronRight class="h-4 w-4" /> </router-link> </div> </div> </article> </div> <!-- Load More Button --> <div v-if="blogStore.pagination.page < blogStore.pagination.pages" class="text-center mt-12" > <button @click="loadMorePosts" :disabled="blogStore.loading" class="bg-blue-600 text-white px-8 py-3 rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" > {{ blogStore.loading ? 'Loading...' : 'Load More Articles' }} </button> </div> </template> </div> </div> </template> <script setup lang="ts"> import { ref, computed, onMounted } from 'vue'; import { useBlogStore } from '@/stores/blog'; import { Search, ChevronRight } from 'lucide-vue-next'; import { useSEO } from '@/composables/useSEO'; const blogStore = useBlogStore(); const searchInput = ref(''); const displayPosts = computed(() => { if (!searchInput.value) { return blogStore.posts; } return blogStore.posts.filter(post => post.title.toLowerCase().includes(searchInput.value.toLowerCase()) ); }); const handleSearch = () => { // Search is reactive through computed property }; const loadMorePosts = () => { if (blogStore.pagination.page < blogStore.pagination.pages) { blogStore.fetchPosts(blogStore.pagination.page + 1); } }; const formatDate = (dateString: string | undefined) => { if (!dateString) return ''; return new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', }); }; onMounted(() => { blogStore.fetchPosts(); }); // SEO useSEO({ title: 'Blog | Your Company Name', description: 'Insights, updates, and stories from our team', type: 'website' }); </script>

    9. Individual Blog Post View (src/views/BlogPost.vue)

    <template> <div v-if="blogStore.loading" class="min-h-screen flex items-center justify-center"> <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div> </div> <div v-else-if="blogStore.error" class="min-h-screen flex items-center justify-center"> <div class="text-center"> <h1 class="text-2xl font-bold text-gray-900 mb-4">Error</h1> <p class="text-gray-600 mb-4">{{ blogStore.error }}</p> <router-link to="/blog" class="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors inline-block" > Back to Blog </router-link> </div> </div> <div v-else-if="blogStore.currentPost" class="min-h-screen flex flex-col justify-between bg-white"> <div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <!-- Back Button --> <router-link to="/blog" class="inline-flex items-center gap-2 text-blue-600 hover:text-blue-800 mb-8" > <ArrowLeft class="h-4 w-4" /> Back to Blog </router-link> <article> <!-- Header --> <header class="mb-8"> <h1 class="text-2xl md:text-3xl font-bold text-gray-900 mb-6"> {{ blogStore.currentPost.title }} </h1> <div class="flex flex-wrap items-center gap-6 text-gray-600 mb-6"> <div class="flex items-center gap-2"> <Calendar class="h-4 w-4" /> <span>{{ formatDate(blogStore.currentPost.published_at) }}</span> </div> <div class="flex items-center gap-2"> <User class="h-4 w-4" /> <span> {{ blogStore.currentPost.author?.first_name }} {{ blogStore.currentPost.author?.last_name }} </span> </div> <ShareButton :title="blogStore.currentPost.title" :excerpt="blogStore.currentPost.excerpt" :url="`/blog/${blogStore.currentPost.slug}`" /> </div> </header> <!-- Featured Image --> <div v-if="blogStore.currentPost.og_image_url" class="mb-8 relative rounded-lg overflow-hidden"> <img :src="blogStore.currentPost.og_image_url" :alt="blogStore.currentPost.title" class="w-full object-cover" /> </div> <!-- Content --> <div v-html="blogStore.currentPost.content" class="blog-content flex flex-col gap-4 text-gray-800 text-lg leading-10 tracking-normal" /> </article> <!-- Author Footer --> <footer class="mt-12 pt-8 border-t border-gray-200"> <div class="flex items-center justify-between"> <div class="flex items-center gap-4"> <img v-if="blogStore.currentPost.author?.avatar_url" :src="blogStore.currentPost.author.avatar_url" :alt="`${blogStore.currentPost.author.first_name} ${blogStore.currentPost.author.last_name}`" class="h-12 w-12 rounded-full object-cover" /> <div> <p class="font-semibold text-gray-900"> {{ blogStore.currentPost.author?.first_name }} {{ blogStore.currentPost.author?.last_name }} </p> <p class="text-gray-600 text-sm"> Published on {{ formatDate(blogStore.currentPost.published_at) }} </p> </div> </div> <ShareButton :title="blogStore.currentPost.title" :excerpt="blogStore.currentPost.excerpt" :url="`/blog/${blogStore.currentPost.slug}`" variant="button" /> </div> </footer> </div> <!-- CTA Section --> <div class="bg-gray-100 py-12"> <div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center"> <h2 class="text-2xl font-bold text-gray-900 mb-4">Want to read more?</h2> <p class="text-gray-600 mb-6">Explore more articles on our blog</p> <router-link to="/blog" class="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors inline-block" > View All Articles </router-link> </div> </div> </div> </template> <script setup lang="ts"> import { onMounted, onUnmounted, watch } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { useBlogStore } from '@/stores/blog'; import ShareButton from '@/components/blog/ShareButton.vue'; import { ArrowLeft, Calendar, User } from 'lucide-vue-next'; import { useBlogPostSEO } from '@/composables/useSEO'; const route = useRoute(); const router = useRouter(); const blogStore = useBlogStore(); const formatDate = (dateString: string | undefined) => { if (!dateString) return ''; return new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', }); }; onMounted(async () => { const slug = route.params.slug as string; if (slug) { await blogStore.fetchPostBySlug(slug); // Redirect to blog list if post not found if (!blogStore.currentPost && !blogStore.loading) { router.push('/blog'); } } }); onUnmounted(() => { blogStore.clearCurrentPost(); }); // Watch for route changes watch(() => route.params.slug, async (newSlug) => { if (newSlug) { await blogStore.fetchPostBySlug(newSlug as string); } }); // SEO useBlogPostSEO(blogStore.currentPost); </script>

    10. Share Button Component (src/components/blog/ShareButton.vue)

    <template> <button v-if="variant === 'button'" @click="sharePost" class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors flex items-center gap-2" > <Share2 class="h-4 w-4" /> Share Article </button> <button v-else @click="sharePost" class="flex items-center gap-2 text-blue-600 hover:text-blue-800" > <Share2 class="h-4 w-4" /> <span>Share</span> </button> </template> <script setup lang="ts"> import { Share2 } from 'lucide-vue-next'; interface Props { title: string; excerpt?: string; url: string; variant?: 'link' | 'button'; } const props = withDefaults(defineProps<Props>(), { variant: 'link' }); const sharePost = async () => { const fullUrl = `${window.location.origin}${props.url}`; if (navigator.share) { try { await navigator.share({ title: props.title, text: props.excerpt, url: fullUrl, }); } catch (error) { console.log('Error sharing:', error); navigator.clipboard.writeText(fullUrl); } } else { navigator.clipboard.writeText(fullUrl); } }; </script>

    11. Main App Entry (src/main.ts)

    import { createApp } from 'vue'; import { createPinia } from 'pinia'; // or import your Vuex store import App from './App.vue'; import router from './router'; import './assets/main.css'; // Your global styles const app = createApp(App); app.use(createPinia()); // or app.use(store) for Vuex app.use(router); app.mount('#app');

    12. Styling for Blog Content (src/assets/main.css)

    Add these styles to your main CSS file:

    @tailwind base; @tailwind components; @tailwind utilities; div.blog-content { h1, h2, h3, h4, h5, h6 { @apply font-bold tracking-normal; } h1 { @apply text-3xl/12 md:text-5xl/14 lg:text-6xl/16; } h2 { @apply text-2xl/9 md:text-4xl/12; } h3 { @apply text-2xl md:text-3xl; } h4 { @apply text-xl md:text-2xl; } section { @apply py-16 md:py-24; } a { @apply underline text-blue-600 hover:text-blue-800 visited:text-purple-600; } p { @apply mb-4; } ul, ol { @apply ml-6 mb-4; } ul { @apply list-disc; } ol { @apply list-decimal; } li { @apply mb-2; } img { @apply rounded-lg my-4; } blockquote { @apply border-l-4 border-gray-300 pl-4 italic my-4; } code { @apply bg-gray-100 px-2 py-1 rounded text-sm; } pre { @apply bg-gray-100 p-4 rounded-lg overflow-x-auto my-4; } pre code { @apply bg-transparent p-0; } }

    Implementation Notes

    Key Features Implemented:

    1. Vue 3 Composition API with <script setup> syntax
    2. Pinia state management (recommended for Vue 3)
    3. Vue Router with proper route guards and navigation
    4. SEO optimization with composables for dynamic meta tags
    5. Responsive design with Tailwind CSS
    6. TypeScript support throughout
    7. Reactive state management with computed properties
    8. Search functionality and filtering
    9. Progressive loading with pagination
    10. Social sharing capabilities

    Customization Options:

    • Update company name and URLs in SEO configuration
    • Modify styling and layout to match brand
    • Add newsletter signup component
    • Include related posts suggestions
    • Add reading time calculation
    • Implement comment system using Vue components
    • Add category/tag filtering with reactive filters

    Performance Optimizations:

    • Use v-show vs v-if for frequently toggled elements
    • Implement lazy loading for images with Intersection Observer
    • Add virtual scrolling for large lists using libraries like vue-virtual-scroller
    • Use defineAsyncComponent for code splitting
    • Leverage Vue 3's improved reactivity system

    Security Considerations:

    • BlogNow public API keys are safe for client-side use
    • Use v-html carefully (BlogNow content is already sanitized)
    • Implement proper error boundaries

    Vue 3 Best Practices:

    • Use Composition API for better code organization and reusability
    • Leverage composables for shared logic (like useSEO)
    • Use TypeScript for type safety
    • Follow Vue 3 naming conventions
    • Implement proper lifecycle hooks

    This implementation provides a production-ready blog system for Vue 3 projects that can be deployed immediately with minimal configuration changes.