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

    Astro Integration Guide

    Integrate BlogNow CMS with Astro projects for maximum performance. Features static site generation, minimal JavaScript, and SEO optimization.

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

    BlogNow SDK Integration Prompt for Astro Projects

    Context

    You are an expert Astro developer tasked with integrating BlogNow SDK into an Astro project to create a complete blog system. Based on the analysis of production implementations, create all necessary files and configurations for a fully functional, performance-optimized blog leveraging Astro's static site generation, partial hydration, and minimal JavaScript philosophy.

    Pre-Implementation Analysis

    1. Check Existing Project Setup

    Before implementing, analyze the existing project to understand:

    Astro Version:

    • Confirm the project uses Astro 4.x or later (check package.json and astro.config.mjs)
    • Newer versions have better TypeScript and content collection support

    UI Framework Integration:

    • Check if the project uses React, Vue, Svelte, or vanilla components
    • Look for existing integrations in astro.config.mjs
    • Adapt components to match the framework if one is already integrated
    • Default to vanilla Astro components for minimal JavaScript

    Icon Library:

    • Check if the project uses astro-icon, @iconify/astro, or similar
    • Check package.json for icon dependencies
    • If none exists, proceed with astro-icon

    Styling:

    • Check if the project uses Tailwind CSS (common with Astro), UnoCSS, or vanilla CSS
    • Check astro.config.mjs for CSS integrations
    • Adapt the styling approach to match the existing setup

    Rendering Strategy:

    • Check if using SSG (default), SSR, or hybrid rendering
    • Look for output configuration in astro.config.mjs
    • Adapt data fetching strategy accordingly

    Project Setup Requirements

    1. Package Installation

    Install the required dependencies:

    # Core BlogNow SDK (always required) npm install @blognow/sdk # Icons (recommended) npm install astro-icon # Tailwind CSS (if not already installed) npx astro add tailwind # Optional: For interactive components # npx astro add react # npx astro add vue # npx astro add svelte

    2. Environment Configuration

    Create/update .env:

    PUBLIC_BLOGNOW_API_KEY=your_public_api_key_here 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 PUBLIC_ prefix to make it available on the client side. Variables without this prefix are only available server-side.

    3. Astro Configuration

    Update astro.config.mjs:

    import { defineConfig } from 'astro/config'; import tailwind from '@astrojs/tailwind'; import icon from 'astro-icon'; // https://astro.build/config export default defineConfig({ integrations: [ tailwind(), icon() ], // For static site generation (default) output: 'static', // Optional: For SSR/hybrid // output: 'server', // output: 'hybrid', // Image optimization image: { domains: ['cdn.blognow.tech'], }, // Build configuration build: { inlineStylesheets: 'auto', }, });

    Core Implementation Files

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

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

    5. Type Definitions (src/types/blog.ts)

    import type { Post } from '@blognow/sdk'; export type { Post }; export interface PostListProps { posts: Post[]; totalPages: number; currentPage: number; }

    6. Blog List Page (src/pages/blog/index.astro)

    --- import { getBlogNowClient } from '@/lib/blognow'; import type { Post } from '@blognow/sdk'; import Layout from '@/layouts/Layout.astro'; import { Icon } from 'astro-icon/components'; import { Image } from 'astro:assets'; // Fetch posts at build time const blogClient = getBlogNowClient(); const page = 1; const size = 12; let posts: Post[] = []; let totalPages = 0; let error: string | null = null; try { const result = await blogClient.posts.getPublishedPosts({ page, size, sort_by: 'published_at', sort_order: 'desc', }); posts = result.items; totalPages = result.pages || 0; } catch (err) { error = err instanceof Error ? err.message : 'Failed to fetch posts'; console.error('Error fetching posts:', err); } const formatDate = (dateString: string | undefined) => { if (!dateString) return ''; return new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', }); }; --- <Layout title="Blog | Your Company Name" description="Insights, updates, and stories from our team" > <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 Form (Client-side enhanced) --> <div class="mb-8"> <form id="search-form" 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 id="search-input" 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 && ( <div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6"> {error} </div> )} {posts.length === 0 ? ( <div class="text-center py-12"> <p class="text-gray-600 text-lg">No articles found.</p> </div> ) : ( <> <!-- Posts Grid --> <div class="grid gap-8 md:grid-cols-1 lg:grid-cols-2" id="posts-grid"> {posts.map((post) => ( <article class="post-card bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow overflow-hidden" data-title={post.title.toLowerCase()} > <!-- Featured Image --> {post.og_image_url && ( <a href={`/blog/${post.slug}`}> <div 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" loading="lazy" width="800" height="600" /> </div> </a> )} <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"> {post.author?.avatar_url && ( <img 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" width="32" height="32" /> )} <span class="text-sm font-medium text-gray-900"> {post.author?.first_name} {post.author?.last_name} </span> </div> <a href={`/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" /> </a> </div> </div> </article> ))} </div> <!-- Pagination (if needed) --> {totalPages > 1 && ( <div class="flex justify-center mt-12 gap-2"> {Array.from({ length: totalPages }, (_, i) => i + 1).map((pageNum) => ( <a href={pageNum === 1 ? '/blog' : `/blog/page/${pageNum}`} class={`px-4 py-2 rounded-lg ${ pageNum === page ? 'bg-blue-600 text-white' : 'bg-white text-gray-700 hover:bg-gray-100' }`} > {pageNum} </a> ))} </div> )} </> )} </div> </div> </Layout> <script> // Client-side search functionality (progressive enhancement) const searchForm = document.getElementById('search-form') as HTMLFormElement; const searchInput = document.getElementById('search-input') as HTMLInputElement; const postsGrid = document.getElementById('posts-grid'); if (searchForm && searchInput && postsGrid) { searchForm.addEventListener('submit', (e) => { e.preventDefault(); const searchTerm = searchInput.value.toLowerCase().trim(); const postCards = postsGrid.querySelectorAll('.post-card'); postCards.forEach((card) => { const title = card.getAttribute('data-title') || ''; if (!searchTerm || title.includes(searchTerm)) { (card as HTMLElement).style.display = ''; } else { (card as HTMLElement).style.display = 'none'; } }); }); // Real-time search searchInput.addEventListener('input', () => { const searchTerm = searchInput.value.toLowerCase().trim(); const postCards = postsGrid.querySelectorAll('.post-card'); postCards.forEach((card) => { const title = card.getAttribute('data-title') || ''; if (!searchTerm || title.includes(searchTerm)) { (card as HTMLElement).style.display = ''; } else { (card as HTMLElement).style.display = 'none'; } }); }); } </script>

    7. Individual Blog Post Page (src/pages/blog/[slug].astro)

    --- import { getBlogNowClient } from '@/lib/blognow'; import type { Post } from '@blognow/sdk'; import Layout from '@/layouts/Layout.astro'; import ShareButton from '@/components/ShareButton.astro'; import { Icon } from 'astro-icon/components'; // Get the slug from the URL const { slug } = Astro.params; if (!slug) { return Astro.redirect('/blog'); } // Fetch the post at build time const blogClient = getBlogNowClient(); let post: Post | null = null; let error: string | null = null; try { post = await blogClient.posts.getPost(slug); } catch (err) { error = err instanceof Error ? err.message : 'Post not found'; console.error('Error fetching post:', err); } // Redirect to blog list if post not found if (!post) { return Astro.redirect('/blog'); } const formatDate = (dateString: string | undefined) => { if (!dateString) return ''; return new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', }); }; // Generate all possible post paths for static generation export async function getStaticPaths() { const blogClient = getBlogNowClient(); const posts: Post[] = []; try { // Fetch all published posts for static generation for await (const post of blogClient.posts.iteratePublishedPosts()) { posts.push(post); } } catch (error) { console.error('Error fetching posts for static paths:', error); } return posts.map((post) => ({ params: { slug: post.slug }, })); } --- <Layout title={post.title} description={post.excerpt} image={post.og_image_url} article={true} publishedTime={post.published_at} author={post.author ? `${post.author.first_name} ${post.author.last_name}` : undefined} > <div 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 --> <a href="/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 </a> <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> <ShareButton title={post.title} excerpt={post.excerpt} url={`/blog/${post.slug}`} /> </div> </header> <!-- Featured Image --> {post.og_image_url && ( <div class="mb-8 relative rounded-lg overflow-hidden"> <img src={post.og_image_url} alt={post.title} class="w-full object-cover" width="1200" height="630" /> </div> )} <!-- Content --> <div set: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 flex-wrap gap-4"> <div class="flex items-center gap-4"> {post.author?.avatar_url && ( <img src={post.author.avatar_url} alt={`${post.author.first_name} ${post.author.last_name}`} class="h-12 w-12 rounded-full object-cover" width="48" height="48" /> )} <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> <ShareButton 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> <a href="/blog" class="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors inline-block" > View All Articles </a> </div> </div> </div> </Layout>

    8. Share Button Component (src/components/ShareButton.astro)

    --- interface Props { title: string; excerpt?: string; url: string; variant?: 'link' | 'button'; } const { title, excerpt, url, variant = 'link' } = Astro.props; --- {variant === 'button' ? ( <button id="share-button" data-title={title} data-excerpt={excerpt} data-url={url} 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 id="share-link" data-title={title} data-excerpt={excerpt} data-url={url} 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> )} <script> import { Icon } from 'astro-icon/components'; // Client-side share functionality const setupShareButton = (button: HTMLButtonElement) => { const title = button.dataset.title || ''; const excerpt = button.dataset.excerpt || ''; const url = button.dataset.url || ''; const fullUrl = `${window.location.origin}${url}`; button.addEventListener('click', async () => { if (navigator.share) { try { await navigator.share({ title, text: excerpt, url: fullUrl, }); } catch (error) { console.log('Error sharing:', error); await navigator.clipboard.writeText(fullUrl); } } else { await navigator.clipboard.writeText(fullUrl); } }); }; // Setup all share buttons on page document.addEventListener('DOMContentLoaded', () => { const shareButtons = document.querySelectorAll('[id^="share-"]') as NodeListOf<HTMLButtonElement>; shareButtons.forEach(setupShareButton); }); // For view transitions document.addEventListener('astro:page-load', () => { const shareButtons = document.querySelectorAll('[id^="share-"]') as NodeListOf<HTMLButtonElement>; shareButtons.forEach(setupShareButton); }); </script>

    9. Base Layout (src/layouts/Layout.astro)

    --- interface Props { title: string; description: string; image?: string; article?: boolean; publishedTime?: string; author?: string; } const { title, description, image, article = false, publishedTime, author } = Astro.props; const canonicalURL = new URL(Astro.url.pathname, Astro.site); const socialImage = image || `${Astro.site}og-image.jpg`; // Default OG image --- <!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <meta name="generator" content={Astro.generator} /> <!-- Primary Meta Tags --> <title>{title}</title> <meta name="title" content={title} /> <meta name="description" content={description} /> <link rel="canonical" href={canonicalURL} /> <!-- Open Graph / Facebook --> <meta property="og:type" content={article ? 'article' : 'website'} /> <meta property="og:url" content={canonicalURL} /> <meta property="og:title" content={title} /> <meta property="og:description" content={description} /> <meta property="og:image" content={socialImage} /> {article && publishedTime && ( <meta property="article:published_time" content={publishedTime} /> )} {article && author && ( <meta property="article:author" content={author} /> )} <!-- Twitter --> <meta property="twitter:card" content="summary_large_image" /> <meta property="twitter:url" content={canonicalURL} /> <meta property="twitter:title" content={title} /> <meta property="twitter:description" content={description} /> <meta property="twitter:image" content={socialImage} /> </head> <body> <slot /> </body> </html> <style is:global> :root { --accent: 136, 58, 234; --accent-light: 224, 204, 250; --accent-dark: 49, 10, 101; } html { font-family: system-ui, sans-serif; } code { font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace; } </style>

    10. Styling for Blog Content (src/styles/blog.css)

    Create a blog-specific stylesheet:

    @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 w-full h-auto; } blockquote { @apply border-l-4 border-gray-300 pl-4 italic my-4 text-gray-700; } code { @apply bg-gray-100 px-2 py-1 rounded text-sm font-mono; } pre { @apply bg-gray-100 p-4 rounded-lg overflow-x-auto my-4; } pre code { @apply bg-transparent p-0; } table { @apply w-full border-collapse my-4; } th, td { @apply border border-gray-300 px-4 py-2; } th { @apply bg-gray-100 font-bold; } }

    Import this in your Layout:

    --- // In Layout.astro head import '../styles/blog.css'; ---

    11. Pagination Page (src/pages/blog/page/[page].astro)

    --- import { getBlogNowClient } from '@/lib/blognow'; import type { Post } from '@blognow/sdk'; import Layout from '@/layouts/Layout.astro'; // ... (similar imports as index.astro) const { page } = Astro.params; const pageNum = parseInt(page || '1'); const size = 12; const blogClient = getBlogNowClient(); let posts: Post[] = []; let totalPages = 0; try { const result = await blogClient.posts.getPublishedPosts({ page: pageNum, size, sort_by: 'published_at', sort_order: 'desc', }); posts = result.items; totalPages = result.pages || 0; } catch (err) { console.error('Error fetching posts:', err); } // Generate static paths for pagination export async function getStaticPaths() { const blogClient = getBlogNowClient(); const size = 12; try { const firstPage = await blogClient.posts.getPublishedPosts({ page: 1, size, }); const totalPages = firstPage.pages || 1; return Array.from({ length: totalPages }, (_, i) => ({ params: { page: String(i + 1) }, })); } catch (error) { console.error('Error generating pagination paths:', error); return []; } } // ... (rest of the page similar to index.astro with pagination controls) ---

    Advanced Features

    RSS Feed

    Create src/pages/blog/rss.xml.ts:

    import rss from '@astrojs/rss'; import { getBlogNowClient } from '@/lib/blognow'; import type { APIContext } from 'astro'; export async function GET(context: APIContext) { const blogClient = getBlogNowClient(); const posts = []; try { for await (const post of blogClient.posts.iteratePublishedPosts()) { posts.push(post); } } catch (error) { console.error('Error fetching posts for RSS:', error); } return rss({ title: 'Your Company Blog', description: 'Insights, updates, and stories from our team', site: context.site!, items: posts.map((post) => ({ title: post.title, pubDate: new Date(post.published_at || ''), description: post.excerpt, link: `/blog/${post.slug}/`, content: post.content, })), customData: `<language>en-us</language>`, }); }

    Install RSS package: npm install @astrojs/rss

    Sitemap

    Add to astro.config.mjs:

    import sitemap from '@astrojs/sitemap'; export default defineConfig({ site: 'https://yoursite.com', integrations: [ tailwind(), icon(), sitemap() ], });

    Install: npm install @astrojs/sitemap

    Implementation Notes

    Key Features Implemented:

    1. Static Site Generation (SSG) for optimal performance and SEO
    2. Zero JavaScript by default - only for progressive enhancement
    3. getStaticPaths() for pre-rendering all blog posts at build time
    4. SEO optimization with proper meta tags in Layout
    5. Responsive design with Tailwind CSS
    6. Image optimization with lazy loading
    7. Progressive enhancement for search and sharing
    8. TypeScript support throughout
    9. Astro's partial hydration for interactive components
    10. Performance-first architecture

    Astro-Specific Features:

    • File-based routing with automatic page generation
    • set:html for safe HTML rendering
    • <script> tags for client-side interactivity
    • Scoped styles and global CSS support
    • Component islands architecture
    • Built-in TypeScript support
    • View Transitions API support (optional)

    Performance Optimizations:

    • Pre-rendered pages at build time (0ms TTFB)
    • Minimal JavaScript shipped to client
    • Automatic image optimization
    • CSS scoping and tree-shaking
    • Progressive enhancement strategy
    • Lazy loading for images
    • Critical CSS inlined automatically

    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 (with client-side framework)
    • Add category/tag filtering
    • Use Astro Content Collections alongside BlogNow

    SEO Best Practices:

    • Proper semantic HTML structure
    • Canonical URLs for all pages
    • Open Graph and Twitter Card meta tags
    • Structured data (JSON-LD) can be added
    • XML sitemap generation
    • RSS feed for content syndication
    • Fast page loads (Core Web Vitals optimized)

    Deployment:

    • Works with Vercel, Netlify, Cloudflare Pages, GitHub Pages
    • Static output: npm run build → dist/
    • Can enable SSR with adapters for dynamic routes
    • Incremental regeneration with adapters
    • Edge function support with appropriate adapters

    Framework Integration:

    If you need interactive components, you can add React, Vue, or Svelte:

    # React npx astro add react # Vue npx astro add vue # Svelte npx astro add svelte

    Then use client:load, client:visible, or client:idle directives for partial hydration.

    This implementation provides a production-ready, performance-optimized blog system for Astro projects that leverages static site generation for maximum speed and minimal JavaScript for the best user experience.