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

    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 and nuxt.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:

    1. Server-Side Rendering (SSR) with Nuxt 3 for optimal SEO and performance
    2. useAsyncData for efficient data fetching with automatic hydration
    3. File-based routing with automatic route generation
    4. Auto-imports for components, composables, and utilities
    5. SEO optimization with useHead and useSeoMeta
    6. NuxtImg for automatic image optimization
    7. Plugin system for BlogNow client initialization
    8. State management with Pinia or useState
    9. TypeScript support throughout
    10. 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.