React Integration Guide
Integrate BlogNow CMS with React and React Router. Supports React Router v5 & v6, SEO optimization, and dynamic meta tags.
💡 Quick Start: Copy this entire guide and paste it into Claude, Cursor, or any AI coding assistant to automatically set up BlogNow in your React project.
BlogNow SDK Integration Prompt for React Router Projects
Context
You are an expert React developer tasked with integrating BlogNow SDK into a React project using React Router to create a complete blog system.
Pre-Implementation Analysis
Check existing project setup:
- Build Tool: CRA (
REACT_APP_
), Vite (VITE_
), or Webpack - State Management: Redux, Zustand, Jotai, etc. Ask user preference if none found
- Icons: lucide-react, react-icons, heroicons, etc. Default to lucide-react
- Router Version: v5 vs v6 (check package.json)
- Styling: Tailwind, styled-components, CSS modules, etc.
Setup
1. Install Dependencies
npm install @blognow/sdk zustand lucide-react react-router-dom
2. Environment Variables
# For CRA/Webpack: REACT_APP_BLOGNOW_API_KEY=your_public_api_key_here # For Vite: VITE_BLOGNOW_API_KEY=your_public_api_key_here
Core Files
3. BlogNow Client (src/lib/blognow.ts
)
import { BlogNowClient } from "@blognow/sdk"; let blogClient: BlogNowClient | null = null; // For CRA: process.env.REACT_APP_BLOGNOW_API_KEY // For Vite: import.meta.env.VITE_BLOGNOW_API_KEY const apiKey = process.env.REACT_APP_BLOGNOW_API_KEY || import.meta.env.VITE_BLOGNOW_API_KEY; export const createBlogNowClient = async () => { if (!blogClient) { if (!apiKey) throw new Error("BlogNow API key not configured"); blogClient = new BlogNowClient({ apiKey }); } return blogClient; };
4. Blog Store (src/stores/blogStore.ts
)
import { create } from "zustand"; import { createBlogNowClient } from "@/lib/blognow"; import { Post } from "@blognow/sdk"; interface BlogState { posts: Post[]; currentPost: Post | null; loading: boolean; error: string | null; pagination: { total: number; page: number; pages: number }; fetchPosts: (page?: number) => Promise<void>; fetchPostBySlug: (slug: string) => Promise<void>; clearCurrentPost: () => void; } const blogClient = await createBlogNowClient(); export const useBlogStore = create<BlogState>((set, get) => ({ posts: [], currentPost: null, loading: false, error: null, pagination: { total: 0, page: 1, pages: 0 }, fetchPosts: async (page = 1) => { set({ loading: true, error: null }); try { const result = await blogClient.posts.getPublishedPosts({ page, size: 10, }); set((state) => ({ posts: page === 1 ? result.items : [...state.posts, ...result.items], pagination: { total: result.total || 0, page: result.page || 1, pages: result.pages || 0, }, loading: false, })); } catch (error) { set({ error: error instanceof Error ? error.message : "Failed to fetch posts", loading: false, }); } }, fetchPostBySlug: async (slug: string) => { set({ loading: true, error: null }); try { const post = await blogClient.posts.getPost(slug); set({ currentPost: post, loading: false }); } catch (error) { set({ error: error instanceof Error ? error.message : "Failed to fetch post", loading: false, }); } }, clearCurrentPost: () => set({ currentPost: null }), }));
5. Router Setup (src/router/AppRouter.tsx
)
import React from "react"; import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; // v6 // import { BrowserRouter, Switch, Route, Redirect } from 'react-router-dom'; // v5 import Layout from "@/components/Layout"; import BlogList from "@/components/blog/BlogList"; import BlogPost from "@/components/blog/BlogPost"; // For React Router v6: export function AppRouter() { return ( <BrowserRouter> <Routes> <Route path="/" element={<Layout />}> <Route index element={<Navigate to="/blog" replace />} /> <Route path="blog" element={<BlogList />} /> <Route path="blog/:slug" element={<BlogPost />} /> </Route> </Routes> </BrowserRouter> ); } // For React Router v5: Use Switch, Redirect, and component props instead
6. Layout Component (src/components/Layout.tsx
)
import React from "react"; import { Outlet, Link } from "react-router-dom"; // v6 // For v5: import { Link } from 'react-router-dom'; and use {children} prop export default function Layout() { return ( <div className="min-h-screen bg-gray-50"> <nav className="bg-white shadow-sm border-b"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="flex justify-between items-center h-16"> <Link to="/" className="text-xl font-bold text-gray-900"> Your Company </Link> <Link to="/blog" className="text-gray-700 hover:text-gray-900"> Blog </Link> </div> </div> </nav> <main> <Outlet /> {/* For v5: {children} */} </main> </div> ); }
7. Blog List Component (src/components/blog/BlogList.tsx
)
import React, { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { useBlogStore } from "@/stores/blogStore"; import { Search, ChevronRight } from "lucide-react"; export default function BlogList() { const { posts, loading, error, pagination, fetchPosts } = useBlogStore(); const [searchInput, setSearchInput] = useState(""); const [filteredPosts, setFilteredPosts] = useState(posts); useEffect(() => { fetchPosts(); }, [fetchPosts]); useEffect(() => { setFilteredPosts(posts); }, [posts]); const handleSearch = (e: React.FormEvent) => { e.preventDefault(); const term = searchInput.trim(); setFilteredPosts( term ? posts.filter((p) => p.title.toLowerCase().includes(term.toLowerCase()) ) : posts ); }; const formatDate = (date: string) => new Date(date).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric", }); if (loading && posts.length === 0) return ( <div className="flex justify-center py-12"> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div> </div> ); return ( <div className="min-h-screen bg-gray-50"> <div className="bg-white border-b"> <div className="max-w-7xl mx-auto px-4 py-12 text-center"> <h1 className="text-4xl font-bold text-gray-900 mb-4">Our Blog</h1> <p className="text-xl text-gray-600"> Insights, updates, and stories from our team </p> </div> </div> <div className="max-w-7xl mx-auto px-4 py-12"> <form onSubmit={handleSearch} className="flex gap-4 mb-8"> <div className="flex-1 relative"> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" /> <input type="text" placeholder="Search articles..." value={searchInput} onChange={(e) => setSearchInput(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg" /> </div> <button type="submit" className="bg-blue-600 text-white px-8 py-2 rounded-lg hover:bg-blue-700" > Search </button> </form> {error && ( <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6"> {error} </div> )} {filteredPosts.length === 0 ? ( <div className="text-center py-12"> <p className="text-gray-600">No articles found.</p> </div> ) : ( <> <div className="grid gap-8 lg:grid-cols-2"> {filteredPosts.map((post) => ( <article key={post.id} className="bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow" > {post.og_image_url && ( <div className="relative"> <img src={post.og_image_url} alt={post.title} className="object-cover" /> </div> )} <div className="p-6"> <div className="text-sm text-gray-500 mb-3"> {formatDate(post.published_at!)} </div> <h2 className="text-xl font-bold text-gray-900 mb-3"> {post.title} </h2> <p className="text-gray-600 mb-4">{post.excerpt}</p> <div className="flex justify-between items-center"> <div className="flex items-center gap-3"> {post.author?.avatar_url && ( <img src={post.author.avatar_url} alt={post.author.first_name} className="w-8 h-8 rounded-full" /> )} <span className="text-sm font-medium"> {post.author?.first_name} {post.author?.last_name} </span> </div> <Link to={`/blog/${post.slug}`} className="flex items-center gap-1 text-blue-600 hover:text-blue-800" > Read more <ChevronRight className="h-4 w-4" /> </Link> </div> </div> </article> ))} </div> {pagination.page < pagination.pages && ( <div className="text-center mt-12"> <button onClick={() => fetchPosts(pagination.page + 1)} disabled={loading} className="bg-blue-600 text-white px-8 py-3 rounded-lg hover:bg-blue-700 disabled:opacity-50" > {loading ? "Loading..." : "Load More"} </button> </div> )} </> )} </div> </div> ); }
8. Blog Post Component (src/components/blog/BlogPost.tsx
)
import React, { useEffect } from "react"; import { useParams, Link, Navigate } from "react-router-dom"; // v6 // import { useParams, Link, Redirect } from 'react-router-dom'; // v5 import { useBlogStore } from "@/stores/blogStore"; import { ShareButton } from "./ShareButton"; import { ArrowLeft, Calendar, User } from "lucide-react"; export default function BlogPost() { const { slug } = useParams<{ slug: string }>(); const { currentPost, loading, error, fetchPostBySlug, clearCurrentPost } = useBlogStore(); useEffect(() => { if (slug) fetchPostBySlug(slug); return () => clearCurrentPost(); }, [slug, fetchPostBySlug, clearCurrentPost]); const formatDate = (date: string) => new Date(date).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric", }); if (loading) return ( <div className="min-h-screen flex items-center justify-center"> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div> </div> ); if (error) return ( <div className="min-h-screen flex items-center justify-center"> <div className="text-center"> <h1 className="text-2xl font-bold mb-4">Error</h1> <p className="text-gray-600 mb-4">{error}</p> <Link to="/blog" className="bg-blue-600 text-white px-6 py-3 rounded-lg" > Back to Blog </Link> </div> </div> ); if (!currentPost) return <Navigate to="/blog" replace />; // For v5: <Redirect to="/blog" /> return ( <div className="min-h-screen bg-white"> <div className="max-w-4xl mx-auto px-4 py-8"> <Link to="/blog" className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-800 mb-8" > <ArrowLeft className="h-4 w-4" /> Back to Blog </Link> <article> <header className="mb-8"> <h1 className="text-3xl font-bold text-gray-900 mb-6"> {currentPost.title} </h1> <div className="flex flex-wrap items-center gap-6 text-gray-600 mb-6"> <div className="flex items-center gap-2"> <Calendar className="h-4 w-4" /> <span>{formatDate(currentPost.published_at || "")}</span> </div> <div className="flex items-center gap-2"> <User className="h-4 w-4" /> <span> {currentPost.author?.first_name}{" "} {currentPost.author?.last_name} </span> </div> <ShareButton title={currentPost.title} excerpt={currentPost.excerpt} url={`/blog/${currentPost.slug}`} /> </div> </header> {currentPost.og_image_url && ( <div className="mb-8 rounded-lg relative"> <img src={currentPost.og_image_url} alt={currentPost.title} className="object-cover" /> </div> )} <div dangerouslySetInnerHTML={{ __html: currentPost.content }} className="blog-content flex flex-col gap-4 text-gray-800 text-lg leading-10 tracking-normal" /> </article> <footer className="mt-12 pt-8 border-t border-gray-200"> <div className="flex items-center justify-between"> <div className="flex items-center gap-4"> {currentPost.author?.avatar_url && ( <img src={currentPost.author.avatar_url} alt={currentPost.author.first_name} className="w-12 h-12 rounded-full" /> )} <div> <p className="font-semibold"> {currentPost.author?.first_name}{" "} {currentPost.author?.last_name} </p> <p className="text-sm text-gray-600"> Published on {formatDate(currentPost.published_at || "")} </p> </div> </div> <ShareButton title={currentPost.title} excerpt={currentPost.excerpt} url={`/blog/${currentPost.slug}`} variant="button" /> </div> </footer> </div> <div className="bg-gray-100 py-12"> <div className="max-w-4xl mx-auto px-4 text-center"> <h2 className="text-2xl font-bold mb-4">Want to read more?</h2> <p className="text-gray-600 mb-6"> Explore more articles on our blog </p> <Link to="/blog" className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 inline-block" > View All Articles </Link> </div> </div> </div> ); }
9. Share Button Component (src/components/blog/ShareButton.tsx
)
import React from "react"; import { Share2 } from "lucide-react"; interface ShareButtonProps { title: string; excerpt?: string; url: string; variant?: "link" | "button"; } export function ShareButton({ title, excerpt, url, variant = "link", }: ShareButtonProps) { const sharePost = async () => { const fullUrl = `${window.location.origin}${url}`; if (navigator.share) { try { await navigator.share({ title, text: excerpt, url: fullUrl }); } catch (error) { navigator.clipboard.writeText(fullUrl); } } else { navigator.clipboard.writeText(fullUrl); } }; if (variant === "button") { return ( <button onClick={sharePost} className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 flex items-center gap-2" > <Share2 className="h-4 w-4" /> Share Article </button> ); } return ( <button onClick={sharePost} className="flex items-center gap-2 text-blue-600 hover:text-blue-800" > <Share2 className="h-4 w-4" /> <span>Share</span> </button> ); }
10. Styling for the blog conent, append to (src/app/global.css
)
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; } }
11. SEO Hook (src/hooks/useSEO.ts
)
import { useEffect } from "react"; interface SEOConfig { title: string; description: string; image?: string; url?: string; type?: "website" | "article"; } export function useSEO(config: SEOConfig) { useEffect(() => { document.title = config.title; const updateMeta = (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); }; updateMeta("description", config.description); updateMeta("og:title", config.title, true); updateMeta("og:description", config.description, true); updateMeta("og:type", config.type || "website", true); if (config.image) updateMeta("og:image", config.image, true); if (config.url) updateMeta("og:url", config.url, true); }, [config]); }
12. App Entry Point (src/App.tsx
)
import React from "react"; import { AppRouter } from "./router/AppRouter"; import "./App.css"; function App() { return <AppRouter />; } export default App;
Implementation Notes
Key Features:
- Client-side routing (v5 & v6 support)
- SEO optimization with dynamic meta tags
- Responsive design with Tailwind CSS
- State management with Zustand
- Build tool flexibility (CRA, Vite, Webpack)
- Search functionality and pagination
- Social sharing capabilities
- TypeScript support
Customization:
- Update company name and URLs
- Modify styling to match brand
- Add newsletter signup, comments, categories
Performance:
- Use React.memo for expensive components
- Implement virtual scrolling for large lists
- Add image lazy loading and code splitting
This provides a production-ready blog system for React Router projects.