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 GitHub

What'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