Nuxt 3 Integration Guide
Integrate BlogNow CMS with Nuxt 3 projects leveraging SSR/SSG. Features useAsyncData, auto-imports, NuxtImg, and comprehensive SEO support.
💡 Quick Start: Copy this entire guide and paste it into Claude, Cursor, or any AI coding assistant to automatically set up BlogNow in your Nuxt 3 project.
BlogNow SDK Integration Prompt for Nuxt 3 Projects
Context
You are an expert Nuxt developer tasked with integrating BlogNow SDK into a Nuxt 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 leveraging Nuxt's powerful features like auto-imports, server-side rendering, and SEO capabilities.
Pre-Implementation Analysis
1. Check Existing Project Setup
Before implementing, analyze the existing project to understand:
Nuxt Version:
- Confirm the project uses Nuxt 3 (check
package.json
andnuxt.config.ts
) - Nuxt 3 is required for this implementation
State Management:
- Check if the project uses Pinia (recommended for Nuxt 3), Vuex, or built-in useState
- Nuxt 3 has built-in
useState
for simple state management - Look for existing stores in
stores/
directory - If using complex state, Pinia is recommended
Icon Library:
- Check if the project uses @nuxt/icon, nuxt-icons, or icon libraries
- Check
package.json
for icon dependencies - If none exists, proceed with @nuxt/icon module
UI Component Library:
- Check if the project uses Nuxt UI, Vuetify, PrimeVue, or other component libraries
- Look for existing component patterns and styling approaches
- Adapt the implementation to match the existing UI patterns
Styling:
- Check if the project uses Tailwind CSS (common in Nuxt 3), UnoCSS, or CSS modules
- Check
nuxt.config.ts
for CSS configuration - 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 (only if using Pinia) npm install pinia @pinia/nuxt # Icons (recommended) npm install @nuxt/icon # Tailwind CSS (if not already installed) npm install -D @nuxtjs/tailwindcss
2. Environment Configuration
Create/update .env
:
NUXT_PUBLIC_BLOGNOW_API_KEY=your_public_api_key_here NUXT_PUBLIC_BLOGNOW_BASE_URL=https://api.blognow.tech
Security Note: BlogNow SDK only requires the public API key which is safe for client-side use. Use the NUXT_PUBLIC_
prefix to make it available on the client side.
3. Nuxt Configuration
Update nuxt.config.ts
:
// https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ devtools: { enabled: true }, modules: [ '@nuxtjs/tailwindcss', '@pinia/nuxt', // if using Pinia '@nuxt/icon', // if using @nuxt/icon ], runtimeConfig: { // Private keys (server-side only) // apiSecret: process.env.API_SECRET, public: { // Public keys (exposed to client) blognowApiKey: process.env.NUXT_PUBLIC_BLOGNOW_API_KEY, blognowBaseUrl: process.env.NUXT_PUBLIC_BLOGNOW_BASE_URL || 'https://api.blognow.tech', } }, app: { head: { title: 'Your Site Name', meta: [ { charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { name: 'description', content: 'Your site description' } ], } }, // Enable auto-imports (default in Nuxt 3) imports: { dirs: ['stores', 'composables'] } });
Core Implementation Files
4. BlogNow Client Plugin (plugins/blognow.ts
)
import { BlogNowClient } from '@blognow/sdk'; export default defineNuxtPlugin(() => { const config = useRuntimeConfig(); const blogClient = new BlogNowClient({ apiKey: config.public.blognowApiKey as string, baseUrl: config.public.blognowBaseUrl as string, }); return { provide: { blogClient } }; });
5. Blog Store with Pinia (stores/blog.ts
)
Option 1: Using Pinia (Recommended for Complex State):
import { defineStore } from 'pinia'; import type { Post, PaginatedResponse } from '@blognow/sdk'; export const useBlogStore = defineStore('blog', { state: () => ({ posts: [] as Post[], currentPost: null as Post | null, loading: false, error: null as string | null, pagination: { total: 0, page: 1, size: 10, pages: 0 }, searchQuery: '', selectedTag: null as string | null, }), getters: { filteredPosts: (state) => { let result = state.posts; if (state.searchQuery) { result = result.filter(post => post.title.toLowerCase().includes(state.searchQuery.toLowerCase()) ); } if (state.selectedTag) { result = result.filter(post => post.tags?.some(tag => tag.name === state.selectedTag) ); } return result; } }, actions: { async fetchPosts( page = 1, size = 10, query = '', sortBy: 'created_at' | 'updated_at' | 'published_at' = 'published_at', sortOrder: 'asc' | 'desc' = 'desc' ) { this.loading = true; this.error = null; try { const { $blogClient } = useNuxtApp(); const result: PaginatedResponse<Post> = await $blogClient.posts.getPublishedPosts({ page, size, query, sort_by: sortBy, sort_order: sortOrder, }); if (page === 1) { this.posts = result.items; } else { this.posts = [...this.posts, ...result.items]; } this.pagination = { page: result.page || 1, size: result.size || 10, total: result.total || 0, pages: result.pages || 0, }; } catch (err) { this.error = err instanceof Error ? err.message : 'Failed to fetch posts'; } finally { this.loading = false; } }, async fetchPostBySlug(slug: string) { this.loading = true; this.error = null; try { const { $blogClient } = useNuxtApp(); const result = await $blogClient.posts.getPost(slug); this.currentPost = result; } catch (err) { this.error = err instanceof Error ? err.message : 'Failed to fetch post'; } finally { this.loading = false; } }, filterByTag(tag: string | null) { this.selectedTag = tag; }, setSearchQuery(query: string) { this.searchQuery = query; }, clearCurrentPost() { this.currentPost = null; }, resetPagination() { this.pagination = { page: 1, size: 10, total: 0, pages: 0 }; } } });
Option 2: Using Nuxt's Built-in useState (Simple State):
// composables/useBlog.ts import type { Post, PaginatedResponse } from '@blognow/sdk'; export const useBlog = () => { const posts = useState<Post[]>('blog-posts', () => []); const currentPost = useState<Post | null>('blog-current-post', () => null); const loading = useState<boolean>('blog-loading', () => false); const error = useState<string | null>('blog-error', () => null); const pagination = useState('blog-pagination', () => ({ total: 0, page: 1, size: 10, pages: 0 })); const fetchPosts = async ( page = 1, size = 10, query = '', sortBy: 'created_at' | 'updated_at' | 'published_at' = 'published_at', sortOrder: 'asc' | 'desc' = 'desc' ) => { loading.value = true; error.value = null; try { const { $blogClient } = useNuxtApp(); 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) => { loading.value = true; error.value = null; try { const { $blogClient } = useNuxtApp(); 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 clearCurrentPost = () => { currentPost.value = null; }; return { posts, currentPost, loading, error, pagination, fetchPosts, fetchPostBySlug, clearCurrentPost, }; };
6. Blog List Page (pages/blog/index.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"> <Icon name="lucide: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="error" class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6" > {{ error }} </div> <!-- Loading State --> <div v-if="pending && 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 --> <NuxtLink v-if="post.og_image_url" :to="`/blog/${post.slug}`"> <div class="relative overflow-hidden"> <NuxtImg :src="post.og_image_url" :alt="post.title" class="w-full h-64 object-cover hover:scale-105 transition-transform duration-300" loading="lazy" /> </div> </NuxtLink> <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"> <NuxtImg 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" loading="lazy" /> <span class="text-sm font-medium text-gray-900"> {{ post.author?.first_name }} {{ post.author?.last_name }} </span> </div> <NuxtLink :to="`/blog/${post.slug}`" class="inline-flex items-center gap-1 text-blue-600 hover:text-blue-800 font-medium text-sm" > Read more <Icon name="lucide:chevron-right" class="h-4 w-4" /> </NuxtLink> </div> </div> </article> </div> <!-- Load More Button --> <div v-if="pagination.page < pagination.pages" class="text-center mt-12" > <button @click="loadMorePosts" :disabled="pending" 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" > {{ pending ? 'Loading...' : 'Load More Articles' }} </button> </div> </template> </div> </div> </template> <script setup lang="ts"> const searchInput = ref(''); // Using Pinia store const blogStore = useBlogStore(); const { posts, pagination, error } = storeToRefs(blogStore); // Or using useState composable // const { posts, pagination, error, fetchPosts } = useBlog(); // Fetch posts on mount const { pending } = await useAsyncData('blog-posts', () => blogStore.fetchPosts()); const displayPosts = computed(() => { if (!searchInput.value) { return posts.value; } return posts.value.filter(post => post.title.toLowerCase().includes(searchInput.value.toLowerCase()) ); }); const handleSearch = () => { // Search is reactive through computed property }; const loadMorePosts = async () => { if (pagination.value.page < pagination.value.pages) { await blogStore.fetchPosts(pagination.value.page + 1); } }; const formatDate = (dateString: string | undefined) => { if (!dateString) return ''; return new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', }); }; // SEO useHead({ title: 'Blog | Your Company Name', meta: [ { name: 'description', content: 'Insights, updates, and stories from our team' }, { property: 'og:title', content: 'Blog | Your Company Name' }, { property: 'og:description', content: 'Insights, updates, and stories from our team' }, { property: 'og:type', content: 'website' } ] }); </script>
7. Individual Blog Post Page (pages/blog/[slug].vue
)
<template> <div v-if="pending" 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="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">{{ error.message || 'Post not found' }}</p> <NuxtLink 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 </NuxtLink> </div> </div> <div v-else-if="post" 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 --> <NuxtLink to="/blog" class="inline-flex items-center gap-2 text-blue-600 hover:text-blue-800 mb-8" > <Icon name="lucide:arrow-left" class="h-4 w-4" /> Back to Blog </NuxtLink> <article> <!-- Header --> <header class="mb-8"> <h1 class="text-2xl md:text-3xl font-bold text-gray-900 mb-6"> {{ post.title }} </h1> <div class="flex flex-wrap items-center gap-6 text-gray-600 mb-6"> <div class="flex items-center gap-2"> <Icon name="lucide:calendar" class="h-4 w-4" /> <span>{{ formatDate(post.published_at) }}</span> </div> <div class="flex items-center gap-2"> <Icon name="lucide:user" class="h-4 w-4" /> <span> {{ post.author?.first_name }} {{ post.author?.last_name }} </span> </div> <BlogShareButton :title="post.title" :excerpt="post.excerpt" :url="`/blog/${post.slug}`" /> </div> </header> <!-- Featured Image --> <div v-if="post.og_image_url" class="mb-8 relative rounded-lg overflow-hidden"> <NuxtImg :src="post.og_image_url" :alt="post.title" class="w-full object-cover" /> </div> <!-- Content --> <div v-html="post.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"> <NuxtImg v-if="post.author?.avatar_url" :src="post.author.avatar_url" :alt="`${post.author.first_name} ${post.author.last_name}`" class="h-12 w-12 rounded-full object-cover" /> <div> <p class="font-semibold text-gray-900"> {{ post.author?.first_name }} {{ post.author?.last_name }} </p> <p class="text-gray-600 text-sm"> Published on {{ formatDate(post.published_at) }} </p> </div> </div> <BlogShareButton :title="post.title" :excerpt="post.excerpt" :url="`/blog/${post.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> <NuxtLink 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 </NuxtLink> </div> </div> </div> </template> <script setup lang="ts"> const route = useRoute(); const slug = route.params.slug as string; // Fetch post data with SSR const { data: post, pending, error } = await useAsyncData( `blog-post-${slug}`, async () => { const { $blogClient } = useNuxtApp(); try { return await $blogClient.posts.getPost(slug); } catch (err) { throw createError({ statusCode: 404, statusMessage: 'Post not found' }); } } ); const formatDate = (dateString: string | undefined) => { if (!dateString) return ''; return new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', }); }; // SEO with useHead useHead(() => { if (!post.value) { return { title: 'Post Not Found' }; } return { title: post.value.title, meta: [ { name: 'description', content: post.value.excerpt }, { property: 'og:title', content: post.value.title }, { property: 'og:description', content: post.value.excerpt }, { property: 'og:type', content: 'article' }, { property: 'og:image', content: post.value.og_image_url || '' }, { property: 'article:published_time', content: post.value.published_at || '' }, { property: 'article:author', content: post.value.author ? `${post.value.author.first_name} ${post.value.author.last_name}` : '' }, { name: 'twitter:card', content: 'summary_large_image' }, { name: 'twitter:title', content: post.value.title }, { name: 'twitter:description', content: post.value.excerpt }, { name: 'twitter:image', content: post.value.og_image_url || '' } ] }; }); // Alternative: Use useSeoMeta for simpler SEO (Nuxt 3.6+) useSeoMeta(() => { if (!post.value) return {}; return { title: post.value.title, description: post.value.excerpt, ogTitle: post.value.title, ogDescription: post.value.excerpt, ogType: 'article', ogImage: post.value.og_image_url, articlePublishedTime: post.value.published_at, articleAuthor: post.value.author ? `${post.value.author.first_name} ${post.value.author.last_name}` : undefined, twitterCard: 'summary_large_image', twitterTitle: post.value.title, twitterDescription: post.value.excerpt, twitterImage: post.value.og_image_url, }; }); </script>
8. Share Button Component (components/BlogShareButton.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" > <Icon name="lucide:share-2" 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" > <Icon name="lucide:share-2" class="h-4 w-4" /> <span>Share</span> </button> </template> <script setup lang="ts"> 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); await navigator.clipboard.writeText(fullUrl); } } else { await navigator.clipboard.writeText(fullUrl); } }; </script>
9. Styling for Blog Content (assets/css/main.css
)
Create or update 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; } }
10. App Entry Point (app.vue
)
<template> <div> <NuxtPage /> </div> </template> <script setup lang="ts"> // Global app setup if needed </script>
Advanced Features
Server-Side Data Fetching with API Routes
For better performance, you can create API routes in Nuxt:
Create server/api/blog/posts.get.ts
:
export default defineEventHandler(async (event) => { const query = getQuery(event); const config = useRuntimeConfig(); const { BlogNowClient } = await import('@blognow/sdk'); const blogClient = new BlogNowClient({ apiKey: config.public.blognowApiKey as string, }); return await blogClient.posts.getPublishedPosts({ page: Number(query.page) || 1, size: Number(query.size) || 10, query: query.query as string || '', }); });
Create server/api/blog/[slug].get.ts
:
export default defineEventHandler(async (event) => { const slug = getRouterParam(event, 'slug'); const config = useRuntimeConfig(); if (!slug) { throw createError({ statusCode: 400, statusMessage: 'Slug is required' }); } const { BlogNowClient } = await import('@blognow/sdk'); const blogClient = new BlogNowClient({ apiKey: config.public.blognowApiKey as string, }); try { return await blogClient.posts.getPost(slug); } catch (error) { throw createError({ statusCode: 404, statusMessage: 'Post not found' }); } });
Implementation Notes
Key Features Implemented:
- Server-Side Rendering (SSR) with Nuxt 3 for optimal SEO and performance
- useAsyncData for efficient data fetching with automatic hydration
- File-based routing with automatic route generation
- Auto-imports for components, composables, and utilities
- SEO optimization with
useHead
anduseSeoMeta
- NuxtImg for automatic image optimization
- Plugin system for BlogNow client initialization
- State management with Pinia or useState
- TypeScript support throughout
- Responsive design with Tailwind CSS
Nuxt 3 Specific Features:
- useAsyncData: Fetches data on server and hydrates on client
- useFetch: Shorthand for fetching data from API routes
- useState: Built-in state management for simple use cases
- useHead/useSeoMeta: SEO meta tag management
- NuxtLink: Automatic prefetching and optimized navigation
- NuxtImg: Automatic image optimization and lazy loading
- Auto-imports: No need to import components or composables
Performance Optimizations:
- SSR/SSG for initial page load performance
- Automatic code splitting per page
- Built-in image optimization with NuxtImg
- Route prefetching with NuxtLink
- Lazy loading for images and components
- Server API routes for better caching control
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
- Add category/tag filtering
- Use Nuxt Content alongside BlogNow for additional content
Security Considerations:
- BlogNow public API keys are safe for client-side use
- Use
NUXT_PUBLIC_
prefix for client-exposed variables - Server routes can use private keys if needed
- Built-in CSRF protection in Nuxt
Deployment:
- Works with Vercel, Netlify, Cloudflare Pages, etc.
- Supports static generation with
nuxt generate
- Supports SSR with
nuxt build
- Can use ISR (Incremental Static Regeneration) with adapters
This implementation provides a production-ready blog system for Nuxt 3 projects that leverages all of Nuxt's powerful features and can be deployed immediately with minimal configuration changes.