Back to Documentation
Framework Guide
Next.js 14+
Next.js Quick Start Guide
Add a fully-featured blog to your Next.js application in minutes
Choose Your Setup Method
Use GitHub Template
Fastest way to get started. Download the complete blog implementation and drag into your project.
View on GitHubWhat's included: Complete /app/blog
directory with listing and article pages
Manual Setup
Follow the step-by-step guide below to implement the blog yourself. Full control over customization.
Complete code examples
Customizable styling
SEO optimized
1
Install Lightweight Client
Add the lightweight-client package to your Next.js project
npm install lightweight-client
2
Configure Environment Variables
Add your API key to the environment variables
# .env.local
LIGHTWEIGHT_API_KEY=your-api-key-here
3
Create Blog Structure
Set up the following folder structure in your Next.js app directory:
app/
blog/
page.tsx(Blog listing page)
blog.css(Optional styling)
[slug]/
page.tsx(Article page)
4
Create Blog Listing Page
Create app/blog/page.tsx
with the following code:
import Link from 'next/link';
import { type Metadata } from 'next';
import { LightweightClient } from 'lightweight-client';
export async function generateMetadata(): Promise<Metadata> {
const title = 'Blog';
const description = 'Read our latest articles and insights';
return {
title,
description,
openGraph: {
type: 'website',
title,
description,
},
};
}
async function getPosts(page: number) {
const key = process.env.LIGHTWEIGHT_API_KEY;
if (!key) throw Error('LIGHTWEIGHT_API_KEY environment variable must be set');
const client = new LightweightClient(key);
return client.getPosts(page, 10);
}
export const fetchCache = 'force-no-store';
export default async function Blog({ searchParams }: { searchParams: Promise<{ page: number }> }) {
const { page } = await searchParams;
const pageNumber = Math.max((page || 0) - 1, 0);
const { total, articles } = await getPosts(pageNumber);
const posts = articles || [];
const lastPage = Math.ceil(total / 10);
return (
<section className="max-w-6xl mx-auto px-4 md:px-8 py-16 lg:py-24">
{/* Hero Section */}
<div className="text-center mb-16">
<h1 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-6">
Blog
</h1>
<p className="text-lg text-gray-600 max-w-3xl mx-auto">
Explore our latest articles and insights
</p>
</div>
{/* Blog Posts Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{posts.map((article: any) => (
<Link
key={article.id}
href={`/blog/${article.slug}`}
className="block bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow"
>
{/* Image */}
<div className="relative h-48 bg-gradient-to-br from-purple-100 to-blue-100">
{article.image && (
<img
src={article.image}
alt={article.headline}
className="w-full h-full object-cover"
/>
)}
</div>
{/* Content */}
<div className="p-6">
{/* Category */}
{article.category && (
<span className="inline-block px-3 py-1 bg-blue-100 text-blue-800 text-xs font-medium rounded-full mb-3">
{article.category.title}
</span>
)}
{/* Title */}
<h3 className="text-lg font-semibold text-gray-900 line-clamp-2 mb-3">
{article.headline}
</h3>
{/* Date */}
<div className="text-sm text-gray-500">
{new Date(article.publishedAt).toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric"
})}
</div>
</div>
</Link>
))}
</div>
{/* Pagination */}
{lastPage > 1 && (
<div className="mt-12 flex justify-center">
<div className="flex items-center gap-4">
<a
className={`px-4 py-2 border rounded-md ${!pageNumber ? 'opacity-50 pointer-events-none' : ''}`}
href={pageNumber ? `/blog?page=${pageNumber}` : '#'}
>
← Previous
</a>
<span className="px-4 py-2">
Page {pageNumber + 1} of {lastPage}
</span>
<a
className={`px-4 py-2 border rounded-md ${pageNumber >= lastPage - 1 ? 'opacity-50 pointer-events-none' : ''}`}
href={pageNumber >= lastPage - 1 ? '#' : `/blog?page=${pageNumber + 2}`}
>
Next →
</a>
</div>
</div>
)}
</section>
);
}
5
Create Article Page
Create app/blog/[slug]/page.tsx
for individual articles:
import { type Metadata } from 'next';
import Link from 'next/link';
import { LightweightClient } from 'lightweight-client';
import '../blog.css'; // Optional: Add custom styles
async function getPost(slug: string) {
const key = process.env.LIGHTWEIGHT_API_KEY;
if (!key) throw Error('LIGHTWEIGHT_API_KEY environment variable must be set');
const client = new LightweightClient(key);
return client.getPost(slug);
}
export const fetchCache = 'force-no-store';
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
if (!post) return {};
return {
title: post.headline,
description: post.metaDescription,
openGraph: {
type: 'article',
title: post.headline,
description: post.metaDescription,
images: [post.image],
},
twitter: {
card: 'summary_large_image',
title: post.headline,
description: post.metaDescription,
images: [post.image],
},
};
}
export default async function Article({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4">404</h1>
<p className="text-gray-600 mb-6">Article not found</p>
<Link href="/blog" className="text-blue-600 hover:underline">
← Back to blog
</Link>
</div>
</div>
);
}
return (
<article className="max-w-4xl mx-auto px-4 py-16">
{/* Hero Section */}
<header className="mb-12">
{/* Breadcrumb */}
<nav className="flex items-center gap-2 text-sm mb-6">
<Link href="/" className="text-blue-600 hover:underline">Home</Link>
<span>/</span>
<Link href="/blog" className="text-blue-600 hover:underline">Blog</Link>
<span>/</span>
<span className="text-gray-500">{post.slug}</span>
</nav>
{/* Title */}
<h1 className="text-4xl md:text-5xl font-bold mb-4">
{post.headline}
</h1>
{/* Meta */}
<div className="flex items-center gap-4 text-gray-600">
<time>
{new Date(post.publishedAt).toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric"
})}
</time>
{post.readingTime && (
<>
<span>•</span>
<span>{post.readingTime} min read</span>
</>
)}
</div>
{/* Featured Image */}
{post.image && (
<img
src={post.image}
alt={post.headline}
className="w-full h-auto rounded-lg mt-8"
/>
)}
</header>
{/* Article Content */}
<div className="prose prose-lg max-w-none"
dangerouslySetInnerHTML={{ __html: post.html }}
/>
{/* Author Section */}
{post.author && (
<div className="mt-12 pt-8 border-t">
<div className="flex items-center gap-4">
{post.author.image && (
<img
src={post.author.image}
alt={post.author.name}
className="w-16 h-16 rounded-full"
/>
)}
<div>
<div className="font-semibold">{post.author.name}</div>
{post.author.title && (
<div className="text-gray-600">{post.author.title}</div>
)}
</div>
</div>
</div>
)}
{/* Tags */}
{post.tags && post.tags.length > 0 && (
<div className="mt-8 flex flex-wrap gap-2">
{post.tags.map((tag: any, index: number) => (
<span key={index} className="px-3 py-1 bg-gray-100 rounded-full text-sm">
{tag}
</span>
))}
</div>
)}
</article>
);
}
6
Add Custom Styling (Optional)
Create app/blog/blog.css
for better article typography:
/* app/blog/blog.css */
/* Prose styling for article content */
.article {
line-height: 1.75;
color: #374151;
}
.article h1,
.article h2,
.article h3,
.article h4 {
scroll-margin-top: 80px;
font-weight: 700;
color: #111827;
margin-top: 2rem;
margin-bottom: 1rem;
}
.article h1 { font-size: 2.25rem; }
.article h2 { font-size: 1.875rem; }
.article h3 { font-size: 1.5rem; }
.article h4 { font-size: 1.25rem; }
.article p {
margin-bottom: 1.5rem;
}
.article img {
border-radius: 0.5rem;
margin: 2rem auto;
max-width: 100%;
height: auto;
}
.article a {
color: #2563eb;
text-decoration: underline;
}
.article a:hover {
color: #1d4ed8;
}
.article ul,
.article ol {
margin: 1.5rem 0;
padding-left: 2rem;
}
.article li {
margin: 0.5rem 0;
}
.article blockquote {
border-left: 4px solid #e5e7eb;
padding-left: 1.5rem;
margin: 2rem 0;
font-style: italic;
color: #6b7280;
}
.article code {
background-color: #f3f4f6;
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-size: 0.875rem;
}
.article pre {
background-color: #1f2937;
color: #f9fafb;
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
margin: 2rem 0;
}
.article pre code {
background-color: transparent;
padding: 0;
}
What You Get
SEO Optimized
- Dynamic meta tags with generateMetadata()
- Open Graph and Twitter Card support
- Structured data for better indexing
Performance Focused
- Server-side rendering for fast initial load
- Built-in pagination for large datasets
- Optimized image loading
Fully Customizable
- Tailwind CSS for easy styling
- Component-based architecture
- Responsive design out of the box
Content Features
- Rich HTML content rendering
- Author profiles and metadata
- Categories, tags, and related posts
Next Steps
- 1.Test your implementation: Navigate to
/blog
in your browser - 2.Customize the design: Modify the Tailwind classes to match your brand
- 3.Add features: Implement search, filters, or newsletter signup
- 4.Deploy: Push to production with Vercel, Netlify, or your preferred platform
Need Help?
Check out our API reference or reach out to support