Building a modern blog platform requires a systematic approach combining Laravel 11's robust backend with Next.js 14's App Router framework. This solution delivers exceptional performance, SEO optimization, and a seamless user experience.
Architecture Overview
Backend (Laravel 11)
- Admin panel powered by Laravel Nova
- RESTful API endpoints
- Authentication and authorization
- Database management
- Content management
Frontend (Next.js 14)
- Server-side rendering
- Static site generation
- Dynamic routing
- SEO optimization
- Responsive design
Backend Implementation
Laravel Installation and Setup
composer create-project laravel/laravel:^11.0 blog-backend
cd blog-backend
composer require laravel/nova
php artisan nova:install
Blog Post Model
// app/Models/BlogPost.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class BlogPost extends Model
{
protected $fillable = [
'title',
'slug',
'content',
'published_at'
];
protected $casts = [
'published_at' => 'datetime'
];
}
Nova Resource
// app/Nova/BlogPost.php
namespace App\Nova;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Fields\Textarea;
use Laravel\Nova\Fields\DateTime;
class BlogPost extends Resource
{
public static $model = \App\Models\BlogPost::class;
public function fields(Request $request)
{
return [
ID::make()->sortable(),
Text::make('Title')
->sortable()
->rules('required', 'max:255'),
Text::make('Slug')
->sortable()
->rules('required', 'unique:blog_posts,slug'),
Textarea::make('Content')
->rules('required'),
DateTime::make('Published At'),
];
}
}
Frontend Implementation
Project Setup
npx create-next-app@latest blog-frontend --typescript --tailwind --app
cd blog-frontend
Blog List Page
// app/blog/page.tsx
import { Metadata } from 'next';
interface Post {
id: string;
title: string;
slug: string;
excerpt: string;
publishedAt: string;
}
export const metadata: Metadata = {
title: 'Blog Posts',
description: 'Latest articles and updates'
};
async function fetchPosts(): Promise<Post[]> {
const res = await fetch('${process.env.API_URL}/posts', {
next: { revalidate: 3600 }
});
return res.json();
}
export default async function BlogPage() {
const posts = await fetchPosts();
return (
<main className="max-w-4xl mx-auto py-8 px-4">
<h1 className="text-3xl font-bold mb-8">Latest Posts</h1>
<div className="grid gap-6">
{posts.map(post => (
<article key={post.id} className="border rounded-lg p-6">
<h2 className="text-2xl font-semibold mb-2">
{post.title}
</h2>
<p className="text-gray-600">{post.excerpt}</p>
<time className="text-sm text-gray-500">
{new Date(post.publishedAt).toLocaleDateString()}
</time>
</article>
))}
</div>
</main>
);
}
Single Post Page
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';
import { Metadata } from 'next';
interface Post {
title: string;
content: string;
publishedAt: string;
}
export async function generateMetadata({
params
}: {
params: { slug: string }
}): Promise<Metadata> {
const post = await fetchPost(params.slug);
return {
title: post.title,
description: post.excerpt
};
}
async function fetchPost(slug: string): Promise<Post> {
const res = await fetch(`${process.env.API_URL}/posts/${slug}`, {
next: { revalidate: 3600 }
});
if (!res.ok) notFound();
return res.json();
}
export default async function PostPage({
params
}: {
params: { slug: string }
}) {
const post = await fetchPost(params.slug);
return (
<article className="max-w-4xl mx-auto py-8 px-4">
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<time className="text-gray-500 mb-8 block">
{new Date(post.publishedAt).toLocaleDateString()}
</time>
<div className="prose max-w-none"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
</article>
);
}
API Integration
Laravel API Controller
// app/Http/Controllers/Api/BlogController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\BlogPost;
use Illuminate\Http\Request;
class BlogController extends Controller
{
public function index()
{
return BlogPost::published()
->latest('published_at')
->paginate(12);
}
public function show($slug)
{
$post = BlogPost::where('slug', $slug)
->whereNotNull('published_at')
->firstOrFail();
return response()->json($post);
}
}
Performance Optimizations
Next.js Caching Strategy
// lib/cache.ts
export const postCacheConfig = {
next: {
revalidate: 3600, // Revalidate every hour
tags: ['posts'],
},
};
export async function revalidatePost(slug: string) {
await fetch(`/api/revalidate?tag=post-${slug}`);
}
Laravel Query Optimization
// app/Models/BlogPost.php
public function scopePublished($query)
{
return $query->whereNotNull('published_at')
->where('published_at', '<=', now())
->latest('published_at');
}
Error Handling
// app/blog/error.tsx
'use client';
export default function ErrorBoundary({
error,
reset
}: {
error: Error;
reset: () => void;
}) {
return (
<div className="text-center py-10">
<h2 className="text-2xl font-bold mb-4">
Something went wrong!
</h2>
<button
onClick={reset}
className="bg-blue-500 text-white px-4 py-2 rounded"
>
Try again
</button>
</div>
);
}
Let's create a comprehensive blog system with Next.js 14 App Router and Laravel 11.
Project Structure
Next.js Directory Structure
app/
├── blog/
│ ├── [slug]/
│ │ ├── page.tsx
│ │ ├── loading.tsx
│ │ └── error.tsx
├── categories/
│ ├── [category]/
│ │ └── page.tsx
└── layout.tsx
Backend Implementation
Laravel Blog Post Model
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Post extends Model
{
protected $fillable = [
'title',
'slug',
'content',
'excerpt',
'category_id',
'published_at',
'featured_image'
];
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
public function scopePublished($query)
{
return $query->whereNotNull('published_at')
->where('published_at', '<=', now());
}
}
API Controllers
// app/Http/Controllers/Api/PostController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index()
{
return Post::with('category')
->published()
->latest('published_at')
->paginate(12);
}
public function show($slug)
{
return Post::with('category')
->where('slug', $slug)
->published()
->firstOrFail();
}
public function byCategory($category)
{
return Post::whereHas('category', function($query) use ($category) {
$query->where('slug', $category);
})
->published()
->latest('published_at')
->paginate(12);
}
}
Frontend Implementation
Blog List Page
// app/blog/page.tsx
import { Metadata } from 'next';
interface Post {
id: number;
title: string;
slug: string;
excerpt: string;
featured_image: string;
published_at: string;
category: {
name: string;
slug: string;
};
}
export const metadata: Metadata = {
title: 'Blog Posts',
description: 'Latest articles from our blog'
};
async function getPosts(page = 1): Promise<{
data: Post[];
meta: { total: number; current_page: number; last_page: number; }
}> {
const res = await fetch(`${process.env.API_URL}/posts?page=${page}`, {
next: { revalidate: 3600 }
});
if (!res.ok) throw new Error('Failed to fetch posts');
return res.json();
}
export default async function BlogPage({
searchParams
}: {
searchParams: { page: string }
}) {
const page = parseInt(searchParams.page) || 1;
const { data: posts, meta } = await getPosts(page);
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{posts.map(post => (
<article key={post.id} className="bg-white rounded-lg shadow">
<img
src={post.featured_image}
alt={post.title}
className="w-full h-48 object-cover rounded-t-lg"
/>
<div className="p-6">
<span className="text-blue-600 text-sm">
{post.category.name}
</span>
<h2 className="text-xl font-semibold mt-2">
{post.title}
</h2>
<p className="mt-2 text-gray-600">
{post.excerpt}
</p>
</div>
</article>
))}
</div>
</div>
);
}
Single Post Page
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';
import { Metadata } from 'next';
interface Post {
title: string;
content: string;
published_at: string;
category: {
name: string;
slug: string;
};
}
async function getPost(slug: string): Promise<Post> {
const res = await fetch(`${process.env.API_URL}/posts/${slug}`, {
next: { revalidate: 3600 }
});
if (!res.ok) notFound();
return res.json();
}
export async function generateMetadata({
params
}: {
params: { slug: string }
}): Promise<Metadata> {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt
};
}
export default async function PostPage({
params
}: {
params: { slug: string }
}) {
const post = await getPost(params.slug);
return (
<article className="max-w-4xl mx-auto px-4 py-12">
<header className="mb-8">
<h1 className="text-4xl font-bold">{post.title}</h1>
<div className="mt-4 flex items-center text-gray-600">
<span>{post.category.name}</span>
<span className="mx-2">•</span>
<time>{new Date(post.published_at).toLocaleDateString()}</time>
</div>
</header>
<div
className="prose max-w-none"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
</article>
);
}
Category Page
// app/categories/[category]/page.tsx
import { Metadata } from 'next';
interface CategoryPost {
id: number;
title: string;
slug: string;
excerpt: string;
}
async function getCategoryPosts(category: string, page = 1): Promise<{
data: CategoryPost[];
meta: { total: number; current_page: number; }
}> {
const res = await fetch(
`${process.env.API_URL}/categories/${category}/posts?page=${page}`,
{ next: { revalidate: 3600 } }
);
if (!res.ok) throw new Error('Failed to fetch category posts');
return res.json();
}
export async function generateMetadata({
params
}: {
params: { category: string }
}): Promise<Metadata> {
return {
title: `${params.category.charAt(0).toUpperCase() + params.category.slice(1)} Posts`,
description: `Articles in the ${params.category} category`
};
}
export default async function CategoryPage({
params,
searchParams
}: {
params: { category: string };
searchParams: { page: string }
}) {
const page = parseInt(searchParams.page) || 1;
const { data: posts } = await getCategoryPosts(params.category, page);
return (
<div className="max-w-7xl mx-auto px-4 py-12">
<h1 className="text-3xl font-bold mb-8 capitalize">
{params.category} Posts
</h1>
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{posts.map(post => (
<article key={post.id} className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold">
{post.title}
</h2>
<p className="mt-2 text-gray-600">
{post.excerpt}
</p>
</article>
))}
</div>
</div>
);
}
Data Fetching Utility
// lib/api.ts
export async function fetchAPI<T>(
endpoint: string,
options?: RequestInit
): Promise<T> {
const defaultOptions = {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
next: { revalidate: 3600 },
};
const res = await fetch(`${process.env.API_URL}${endpoint}`, {
...defaultOptions,
...options,
});
if (!res.ok) {
throw new Error(`API error: ${res.statusText}`);
}
return res.json();
}
This implementation provides a complete blog system with category support, pagination, and proper error handling. The Laravel backend serves as a robust API while Next.js handles the frontend with server-side rendering and static generation capabilities.
About Prateeksha Web Design
Prateeksha Web Design offers expert services for creating a robust blogging platform utilizing Laravel Nova and Next.js. Their team specializes in building customizable, scalable solutions that enhance user experience and performance. They focus on seamless integration between backend and frontend, ensuring dynamic content delivery. With a commitment to modern design principles, they tailor each project to meet client needs. Clients can expect ongoing support and maintenance for optimal platform functionality.
Interested in learning more? Contact us today.
