React Package
The @haroonwaves/blog-kit-react package offers a collection of production-ready React components
and hooks for building beautiful blog interfaces. From markdown rendering with syntax highlighting
to search functionality, these components are designed to work seamlessly with any React framework.
BlogRenderer
Render markdown content with syntax highlighting and beautiful styling:
import { BlogRenderer } from '@haroonwaves/blog-kit-react';
function BlogPost({ content }) {
return <BlogRenderer content={content} metadata={metadata} />;
}
Customizing Components
You can override any default component by passing custom components through the components prop:
import { BlogRenderer } from '@haroonwaves/blog-kit-react';
import type { ComponentProps } from 'react';
function BlogPost({ content, metadata }) {
// Custom component overrides
const customComponents = {
// Custom blockquote with a different style
blockquote: (props: ComponentProps<'blockquote'>) => (
<blockquote
className="my-6 border-l-4 border-purple-500 bg-purple-50 dark:bg-purple-950 p-4 rounded-r-lg italic"
{...props}
/>
),
};
return <BlogRenderer content={content} metadata={metadata} components={customComponents} />;
}
Props:
content(string, required): Blog content to rendermetadata(BlogMeta, required): Blog meta info to renderclassName(string, optional): Additional CSS classescomponents(object, optional): Custom component overridesshowCategory(boolean, optional): Show category badge (default: true)showReadingTime(boolean, optional): Show reading time (default: true)showDate(boolean, optional): Show publication date (default: true)
BlogCard
Display a single blog post card:
import { BlogCard } from '@haroonwaves/blog-kit-react';
function BlogCardExample({ blogMeta }) {
return <BlogCard metadata={blogMeta} basePath="/blog" />;
}
Props:
metadata(BlogMeta, required): Blog metadata objectbasePath(string, optional): Base path for blog links (default: '/blog')renderLink(function, optional): Custom link renderer (useful for Next.js Link)className(string, optional): Additional CSS classesshowCategory(boolean, optional): Show category badge (default: true)showReadingTime(boolean, optional): Show reading time (default: true)showDate(boolean, optional): Show publication date (default: true)
BlogList
Display a list of blog posts:
import { BlogList } from '@haroonwaves/blog-kit-react';
function BlogListExample({ blogsMeta }) {
return <BlogList metadata={blogsMeta} basePath="/blog" emptyMessage="No posts found." />;
}
Props:
metadata(BlogMeta[], required): Array of blog metadatabasePath(string, optional): Base path for blog links (default: '/blog')renderLink(function, optional): Custom link rendererclassName(string, optional): Additional CSS classesemptyMessage(string, optional): Message when no blogs (default: 'No blog posts found.')cardProps(object, optional): Props to pass to each BlogCard
BlogPlaceholder
Show loading placeholders while blogs are loading:
import { BlogPlaceholder } from '@haroonwaves/blog-kit-react';
function LoadingBlogs() {
return <BlogPlaceholder count={3} />;
}
Props:
count(number, optional): Number of placeholder cards (default: 3)className(string, optional): Additional CSS classes
useBlogs Hook
Filter and search through blog posts:
import { useBlogs } from '@haroonwaves/blog-kit-react';
function BlogSearch({ blogsMeta }) {
const { metadata, searchTerm, setSearchTerm, selectedCategory, setSelectedCategory, categories } =
useBlogs(blogsMeta);
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search blogs..."
/>
<select
value={selectedCategory || ''}
onChange={(e) => setSelectedCategory(e.target.value || null)}
>
<option value="">All Categories</option>
{categories.map((cat) => (
<option key={cat} value={cat}>
{cat}
</option>
))}
</select>
<BlogList metadata={metadata} />
</div>
);
}
Returns:
metadata(BlogMeta[]): Filtered blog posts metadatasearchTerm(string): Current search termsetSearchTerm(function): Update search termselectedCategory(string | null): Selected category filtersetSelectedCategory(function): Update category filtercategories(string[]): Available categories from blogs
Next.js Integration
For Next.js projects, use a custom link renderer:
import Link from 'next/link';
import { BlogCard } from '@haroonwaves/blog-kit-react';
function NextBlogCard({ blog }) {
return (
<BlogCard
blog={blog}
basePath="/blog"
renderLink={(href, children) => <Link href={href}>{children}</Link>}
/>
);
}
Next.js SSG Example (Static Site Generation)
For Next.js with static site generation, use server components and generateStaticParams:
Blog List Page (app/blog/page.tsx):
import { getAllBlogsMeta } from '@haroonwaves/blog-kit-core';
import { BlogList } from '@haroonwaves/blog-kit-react';
import Link from 'next/link';
export default function BlogListPage() {
const blogsMeta = getAllBlogsMeta({
contentDirectory: process.cwd(),
blogSubdirectory: 'content/blog',
});
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-7xl mx-auto px-4 py-12">
<h1 className="text-4xl font-bold mb-4">Blogs</h1>
<BlogList
metadata={blogsMeta}
basePath="/blog"
renderLink={(href, children) => <Link href={href}>{children}</Link>}
/>
</div>
</div>
);
}
Blog Post Page (app/blog/[slug]/page.tsx):
import { getAllBlogsMeta, getBlog } from '@haroonwaves/blog-kit-core';
import { BlogRenderer } from '@haroonwaves/blog-kit-react';
import { notFound } from 'next/navigation';
import Link from 'next/link';
import type { Metadata } from 'next';
const blogConfig = {
contentDirectory: process.cwd(),
blogSubdirectory: 'content/blog',
};
export function generateStaticParams() {
const blogsMeta = getAllBlogsMeta(blogConfig);
return blogsMeta.map((meta) => ({
slug: meta.slug,
}));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const blog = getBlog(slug, blogConfig);
if (!blog) {
return {
title: 'Blog Post Not Found',
};
}
return {
title: `${blog.metadata.title} | Blog Kit`,
description: blog.metadata.description,
openGraph: {
title: blog.metadata.title,
description: blog.metadata.description,
type: 'article',
publishedTime: blog.metadata.date,
},
};
}
export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const blog = getBlog(slug, blogConfig);
if (!blog) notFound();
const { metadata, content } = blog;
return (
<article>
<BlogRenderer content={content} metadata={metadata} />
</article>
);
}
Next.js SSR Example (Server-Side Rendering)
For server-side rendering, use the same functions but without generateStaticParams:
// app/blog/[slug]/page.tsx
import { getBlog } from '@haroonwaves/blog-kit-core';
import { BlogRenderer } from '@haroonwaves/blog-kit-react';
import { notFound } from 'next/navigation';
export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const blog = getBlog(slug, {
contentDirectory: process.cwd(),
blogSubdirectory: 'content/blog',
});
if (!blog) notFound();
return (
<article>
<BlogRenderer content={blog.content} metadata={blog.metadata} />
</article>
);
}
Note: SSG is recommended for blogs as it pre-renders pages at build time for better performance.
Pure React Example (Client-Side)
For pure React applications (Create React App, Vite, etc.), use the client-side functions with markdown content fetched from an API or imported:
import { useState, useEffect } from 'react';
import { extractBlogMeta, extractBlog, type BlogMeta, type Blog } from '@haroonwaves/blog-kit-core';
import { BlogRenderer, BlogList, useBlogs } from '@haroonwaves/blog-kit-react';
// Example: Fetch markdown content from an API
async function fetchBlogContent(slug: string): Promise<string> {
const response = await fetch(`/api/blogs/${slug}`);
return response.text();
}
async function fetchAllBlogs(): Promise<BlogMeta[]> {
const response = await fetch('/api/blogs');
const blogs = await response.json();
// If you receive raw markdown, extract metadata
return blogs.map((blog: { content: string; slug: string }) =>
extractBlogMeta(blog.content, blog.slug)
);
}
function BlogPage() {
const [blogsMeta, setBlogsMeta] = useState<BlogMeta[]>([]);
const { metadata, searchTerm, setSearchTerm } = useBlogs(blogsMeta);
useEffect(() => {
fetchAllBlogs().then(setBlogsMeta);
}, []);
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
<BlogList metadata={metadata} basePath="/blog" />
</div>
);
}
function BlogPostPage({ slug }: { slug: string }) {
const [blog, setBlog] = useState<Blog | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchBlogContent(slug).then((content) => {
const blogData = extractBlog(content, slug);
setBlog(blogData);
setLoading(false);
});
}, [slug]);
if (loading) return <div>Loading...</div>;
if (!blog) return <div>Blog not found</div>;
return (
<article>
<BlogRenderer content={blog.content} metadata={blog.metadata} />
</article>
);
}