Projet complet
Créez une application blog complète avec authentification et API
Projet complet Next.js - Application de A à Z
Dans ce tutoriel final, nous allons créer une application blog complète avec Next.js en utilisant tous les concepts appris dans les tutoriels précédents. Notre application aura une authentification, une API complète, une base de données et sera déployée en production.
Vue d’ensemble du projet
Fonctionnalités de l’application
Notre blog aura les fonctionnalités suivantes :
- Authentification : Inscription, connexion, gestion des sessions
- Gestion des articles : CRUD complet (Create, Read, Update, Delete)
- Commentaires : Système de commentaires pour les articles
- Catégories : Organisation des articles par catégories
- Recherche : Recherche d’articles par titre et contenu
- Interface admin : Dashboard pour gérer le contenu
- API REST : API complète avec documentation Swagger
- Responsive : Design adaptatif pour tous les écrans
Stack technique
- Frontend : Next.js 14, React, TypeScript, Tailwind CSS
- Backend : API Routes Next.js
- Base de données : PostgreSQL avec Prisma ORM
- Authentification : NextAuth.js avec JWT
- Documentation : Swagger UI
- Déploiement : Vercel
- Stockage : Cloudinary pour les images
Phase 1 : Configuration du projet
Initialisation du projet
npx create-next-app@latest blog-nextjs --typescript --tailwind --eslint --app
cd blog-nextjs
Installation des dépendances
# Base de données et ORM
npm install prisma @prisma/client
# Authentification
npm install next-auth @next-auth/prisma-adapter
# Validation et utilitaires
npm install zod bcryptjs jsonwebtoken
npm install @types/bcryptjs @types/jsonwebtoken
# Documentation API
npm install swagger-ui-react swagger-jsdoc
# Upload d'images
npm install cloudinary multer
npm install @types/multer
# Interface utilisateur
npm install @headlessui/react @heroicons/react
# Développement
npm install --save-dev @types/node
Configuration de la base de données
Créez le fichier prisma/schema.prisma
:
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
name String?
password String
role Role @default(USER)
avatar String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posts Post[]
comments Comment[]
@@map("users")
}
model Category {
id String @id @default(cuid())
name String @unique
slug String @unique
description String?
color String @default("#3B82F6")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posts Post[]
@@map("categories")
}
model Post {
id String @id @default(cuid())
title String
slug String @unique
content String
excerpt String?
coverImage String?
published Boolean @default(false)
featured Boolean @default(false)
views Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
authorId String
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
categoryId String
category Category @relation(fields: [categoryId], references: [id])
comments Comment[]
tags Tag[]
@@map("posts")
}
model Comment {
id String @id @default(cuid())
content String
approved Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
authorId String
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
postId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
@@map("comments")
}
model Tag {
id String @id @default(cuid())
name String @unique
slug String @unique
color String @default("#10B981")
posts Post[]
@@map("tags")
}
enum Role {
USER
ADMIN
}
Variables d’environnement
Créez le fichier .env.local
:
# Base de données
DATABASE_URL="postgresql://username:password@localhost:5432/blog_nextjs"
# NextAuth
NEXTAUTH_SECRET="your-secret-key-here"
NEXTAUTH_URL="http://localhost:3000"
# Cloudinary (pour les images)
CLOUDINARY_CLOUD_NAME="your-cloud-name"
CLOUDINARY_API_KEY="your-api-key"
CLOUDINARY_API_SECRET="your-api-secret"
# JWT
JWT_SECRET="your-jwt-secret"
Phase 2 : Configuration de l’authentification
Configuration NextAuth.js
Créez lib/auth.ts
:
import { NextAuthOptions } from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'
import { PrismaAdapter } from '@next-auth/prisma-adapter'
import { prisma } from './prisma'
import bcrypt from 'bcryptjs'
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
providers: [
CredentialsProvider({
name: 'credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' }
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null
}
const user = await prisma.user.findUnique({
where: { email: credentials.email }
})
if (!user) {
return null
}
const isPasswordValid = await bcrypt.compare(
credentials.password,
user.password
)
if (!isPasswordValid) {
return null
}
return {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
}
}
})
],
session: {
strategy: 'jwt'
},
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = user.role
}
return token
},
async session({ session, token }) {
if (token) {
session.user.id = token.sub
session.user.role = token.role
}
return session
}
},
pages: {
signIn: '/auth/signin',
signUp: '/auth/signup'
}
}
API d’authentification
Créez app/api/auth/[...nextauth]/route.ts
:
import NextAuth from 'next-auth'
import { authOptions } from '@/lib/auth'
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }
Créez app/api/auth/register/route.ts
:
import { NextRequest, NextResponse } from 'next/server'
import bcrypt from 'bcryptjs'
import { prisma } from '@/lib/prisma'
import { z } from 'zod'
const registerSchema = z.object({
name: z.string().min(2, 'Le nom doit contenir au moins 2 caractères'),
email: z.string().email('Email invalide'),
password: z.string().min(6, 'Le mot de passe doit contenir au moins 6 caractères')
})
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { name, email, password } = registerSchema.parse(body)
// Vérifier si l'utilisateur existe déjà
const existingUser = await prisma.user.findUnique({
where: { email }
})
if (existingUser) {
return NextResponse.json(
{ error: 'Un utilisateur avec cet email existe déjà' },
{ status: 400 }
)
}
// Hasher le mot de passe
const hashedPassword = await bcrypt.hash(password, 12)
// Créer l'utilisateur
const user = await prisma.user.create({
data: {
name,
email,
password: hashedPassword
}
})
// Retourner l'utilisateur sans le mot de passe
const { password: _, ...userWithoutPassword } = user
return NextResponse.json(
{ user: userWithoutPassword },
{ status: 201 }
)
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Données invalides', details: error.errors },
{ status: 400 }
)
}
return NextResponse.json(
{ error: 'Erreur interne du serveur' },
{ status: 500 }
)
}
}
Phase 3 : API Routes pour les articles
API CRUD pour les posts
Créez app/api/posts/route.ts
:
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import { z } from 'zod'
const postSchema = z.object({
title: z.string().min(5, 'Le titre doit contenir au moins 5 caractères'),
content: z.string().min(10, 'Le contenu doit contenir au moins 10 caractères'),
excerpt: z.string().optional(),
categoryId: z.string(),
published: z.boolean().default(false),
featured: z.boolean().default(false),
coverImage: z.string().optional()
})
/**
* @swagger
* /api/posts:
* get:
* summary: Récupère la liste des articles
* tags: [Posts]
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* - in: query
* name: limit
* schema:
* type: integer
* default: 10
* - in: query
* name: search
* schema:
* type: string
* - in: query
* name: category
* schema:
* type: string
* responses:
* 200:
* description: Liste des articles
*/
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '10')
const search = searchParams.get('search')
const category = searchParams.get('category')
const where: any = { published: true }
if (search) {
where.OR = [
{ title: { contains: search, mode: 'insensitive' } },
{ content: { contains: search, mode: 'insensitive' } }
]
}
if (category) {
where.category = { slug: category }
}
const [posts, total] = await Promise.all([
prisma.post.findMany({
where,
include: {
author: {
select: { id: true, name: true, avatar: true }
},
category: true,
_count: {
select: { comments: true }
}
},
orderBy: { createdAt: 'desc' },
skip: (page - 1) * limit,
take: limit
}),
prisma.post.count({ where })
])
return NextResponse.json({
posts,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
})
} catch (error) {
return NextResponse.json(
{ error: 'Erreur lors de la récupération des articles' },
{ status: 500 }
)
}
}
/**
* @swagger
* /api/posts:
* post:
* summary: Crée un nouvel article
* tags: [Posts]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CreatePost'
* responses:
* 201:
* description: Article créé avec succès
*/
export async function POST(request: NextRequest) {
try {
const session = await getServerSession(authOptions)
if (!session?.user) {
return NextResponse.json(
{ error: 'Non autorisé' },
{ status: 401 }
)
}
const body = await request.json()
const data = postSchema.parse(body)
// Générer le slug à partir du titre
const slug = data.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '')
const post = await prisma.post.create({
data: {
...data,
slug,
authorId: session.user.id
},
include: {
author: {
select: { id: true, name: true, avatar: true }
},
category: true
}
})
return NextResponse.json(post, { status: 201 })
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Données invalides', details: error.errors },
{ status: 400 }
)
}
return NextResponse.json(
{ error: 'Erreur lors de la création de l\'article' },
{ status: 500 }
)
}
}
API pour un article spécifique
Créez app/api/posts/[slug]/route.ts
:
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
interface Params {
slug: string
}
/**
* @swagger
* /api/posts/{slug}:
* get:
* summary: Récupère un article par son slug
* tags: [Posts]
* parameters:
* - in: path
* name: slug
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Article trouvé
* 404:
* description: Article non trouvé
*/
export async function GET(
request: NextRequest,
{ params }: { params: Params }
) {
try {
const post = await prisma.post.findUnique({
where: { slug: params.slug },
include: {
author: {
select: { id: true, name: true, avatar: true }
},
category: true,
comments: {
where: { approved: true },
include: {
author: {
select: { id: true, name: true, avatar: true }
}
},
orderBy: { createdAt: 'desc' }
},
_count: {
select: { comments: true }
}
}
})
if (!post) {
return NextResponse.json(
{ error: 'Article non trouvé' },
{ status: 404 }
)
}
// Incrémenter le nombre de vues
await prisma.post.update({
where: { id: post.id },
data: { views: { increment: 1 } }
})
return NextResponse.json(post)
} catch (error) {
return NextResponse.json(
{ error: 'Erreur lors de la récupération de l\'article' },
{ status: 500 }
)
}
}
export async function PUT(
request: NextRequest,
{ params }: { params: Params }
) {
try {
const session = await getServerSession(authOptions)
if (!session?.user) {
return NextResponse.json(
{ error: 'Non autorisé' },
{ status: 401 }
)
}
const post = await prisma.post.findUnique({
where: { slug: params.slug }
})
if (!post) {
return NextResponse.json(
{ error: 'Article non trouvé' },
{ status: 404 }
)
}
// Vérifier que l'utilisateur est l'auteur ou admin
if (post.authorId !== session.user.id && session.user.role !== 'ADMIN') {
return NextResponse.json(
{ error: 'Non autorisé' },
{ status: 403 }
)
}
const body = await request.json()
const updatedPost = await prisma.post.update({
where: { slug: params.slug },
data: body,
include: {
author: {
select: { id: true, name: true, avatar: true }
},
category: true
}
})
return NextResponse.json(updatedPost)
} catch (error) {
return NextResponse.json(
{ error: 'Erreur lors de la mise à jour de l\'article' },
{ status: 500 }
)
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Params }
) {
try {
const session = await getServerSession(authOptions)
if (!session?.user) {
return NextResponse.json(
{ error: 'Non autorisé' },
{ status: 401 }
)
}
const post = await prisma.post.findUnique({
where: { slug: params.slug }
})
if (!post) {
return NextResponse.json(
{ error: 'Article non trouvé' },
{ status: 404 }
)
}
// Vérifier que l'utilisateur est l'auteur ou admin
if (post.authorId !== session.user.id && session.user.role !== 'ADMIN') {
return NextResponse.json(
{ error: 'Non autorisé' },
{ status: 403 }
)
}
await prisma.post.delete({
where: { slug: params.slug }
})
return NextResponse.json(
{ message: 'Article supprimé avec succès' },
{ status: 200 }
)
} catch (error) {
return NextResponse.json(
{ error: 'Erreur lors de la suppression de l\'article' },
{ status: 500 }
)
}
}
Phase 4 : Interface utilisateur
Page d’accueil du blog
Créez app/page.tsx
:
import { Suspense } from 'react'
import Link from 'next/link'
import Image from 'next/image'
import { prisma } from '@/lib/prisma'
async function getFeaturedPosts() {
return await prisma.post.findMany({
where: { published: true, featured: true },
include: {
author: {
select: { name: true, avatar: true }
},
category: true,
_count: {
select: { comments: true }
}
},
orderBy: { createdAt: 'desc' },
take: 3
})
}
async function getRecentPosts() {
return await prisma.post.findMany({
where: { published: true },
include: {
author: {
select: { name: true, avatar: true }
},
category: true,
_count: {
select: { comments: true }
}
},
orderBy: { createdAt: 'desc' },
take: 6
})
}
function PostCard({ post }: { post: any }) {
return (
<article className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow">
{post.coverImage && (
<div className="relative h-48">
<Image
src={post.coverImage}
alt={post.title}
fill
className="object-cover"
/>
</div>
)}
<div className="p-6">
<div className="flex items-center mb-2">
<span
className="px-2 py-1 text-xs font-medium rounded-full text-white"
style={{ backgroundColor: post.category.color }}
>
{post.category.name}
</span>
<span className="ml-2 text-sm text-gray-500">
{new Date(post.createdAt).toLocaleDateString('fr-FR')}
</span>
</div>
<h2 className="text-xl font-bold mb-2 hover:text-blue-600">
<Link href={`/posts/${post.slug}`}>
{post.title}
</Link>
</h2>
{post.excerpt && (
<p className="text-gray-600 mb-4 line-clamp-3">
{post.excerpt}
</p>
)}
<div className="flex items-center justify-between">
<div className="flex items-center">
{post.author.avatar && (
<Image
src={post.author.avatar}
alt={post.author.name}
width={32}
height={32}
className="rounded-full mr-2"
/>
)}
<span className="text-sm text-gray-700">
{post.author.name}
</span>
</div>
<div className="flex items-center text-sm text-gray-500">
<span>{post.views} vues</span>
<span className="mx-2">•</span>
<span>{post._count.comments} commentaires</span>
</div>
</div>
</div>
</article>
)
}
export default async function HomePage() {
const [featuredPosts, recentPosts] = await Promise.all([
getFeaturedPosts(),
getRecentPosts()
])
return (
<div className="min-h-screen bg-gray-50">
{/* Hero Section */}
<section className="bg-gradient-to-r from-blue-600 to-purple-600 text-white py-20">
<div className="container mx-auto px-4 text-center">
<h1 className="text-5xl font-bold mb-4">
Bienvenue sur notre Blog
</h1>
<p className="text-xl mb-8 max-w-2xl mx-auto">
Découvrez nos articles sur le développement web, les technologies modernes
et les meilleures pratiques de programmation.
</p>
<Link
href="/posts"
className="bg-white text-blue-600 px-8 py-3 rounded-lg font-semibold hover:bg-gray-100 transition-colors"
>
Voir tous les articles
</Link>
</div>
</section>
{/* Featured Posts */}
{featuredPosts.length > 0 && (
<section className="py-16">
<div className="container mx-auto px-4">
<h2 className="text-3xl font-bold text-center mb-12">
Articles en vedette
</h2>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{featuredPosts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
</div>
</section>
)}
{/* Recent Posts */}
<section className="py-16 bg-white">
<div className="container mx-auto px-4">
<h2 className="text-3xl font-bold text-center mb-12">
Articles récents
</h2>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{recentPosts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
<div className="text-center mt-12">
<Link
href="/posts"
className="bg-blue-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-blue-700 transition-colors"
>
Voir tous les articles
</Link>
</div>
</div>
</section>
</div>
)
}
Page de détail d’un article
Créez app/posts/[slug]/page.tsx
:
import { notFound } from 'next/navigation'
import Image from 'next/image'
import Link from 'next/link'
import { prisma } from '@/lib/prisma'
import { CommentForm } from '@/components/CommentForm'
import { CommentList } from '@/components/CommentList'
interface PageProps {
params: {
slug: string
}
}
async function getPost(slug: string) {
const post = await prisma.post.findUnique({
where: { slug },
include: {
author: {
select: { id: true, name: true, avatar: true }
},
category: true,
comments: {
where: { approved: true },
include: {
author: {
select: { id: true, name: true, avatar: true }
}
},
orderBy: { createdAt: 'desc' }
},
_count: {
select: { comments: true }
}
}
})
if (!post || !post.published) {
return null
}
// Incrémenter les vues
await prisma.post.update({
where: { id: post.id },
data: { views: { increment: 1 } }
})
return post
}
export default async function PostPage({ params }: PageProps) {
const post = await getPost(params.slug)
if (!post) {
notFound()
}
return (
<div className="min-h-screen bg-gray-50">
<article className="max-w-4xl mx-auto py-8 px-4">
{/* Header */}
<header className="mb-8">
<div className="flex items-center mb-4">
<Link
href={`/categories/${post.category.slug}`}
className="px-3 py-1 text-sm font-medium rounded-full text-white hover:opacity-80"
style={{ backgroundColor: post.category.color }}
>
{post.category.name}
</Link>
<span className="ml-4 text-gray-500">
{new Date(post.createdAt).toLocaleDateString('fr-FR', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</span>
</div>
<h1 className="text-4xl font-bold text-gray-900 mb-4">
{post.title}
</h1>
<div className="flex items-center justify-between">
<div className="flex items-center">
{post.author.avatar && (
<Image
src={post.author.avatar}
alt={post.author.name}
width={48}
height={48}
className="rounded-full mr-3"
/>
)}
<div>
<p className="font-medium text-gray-900">
{post.author.name}
</p>
<p className="text-sm text-gray-500">
Auteur
</p>
</div>
</div>
<div className="flex items-center text-sm text-gray-500">
<span>{post.views} vues</span>
<span className="mx-2">•</span>
<span>{post._count.comments} commentaires</span>
</div>
</div>
</header>
{/* Cover Image */}
{post.coverImage && (
<div className="relative h-96 mb-8 rounded-lg overflow-hidden">
<Image
src={post.coverImage}
alt={post.title}
fill
className="object-cover"
/>
</div>
)}
{/* Content */}
<div className="bg-white rounded-lg shadow-sm p-8 mb-8">
<div
className="prose prose-lg max-w-none"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
</div>
{/* Comments Section */}
<div className="bg-white rounded-lg shadow-sm p-8">
<h2 className="text-2xl font-bold mb-6">
Commentaires ({post._count.comments})
</h2>
<CommentForm postId={post.id} />
{post.comments.length > 0 && (
<CommentList comments={post.comments} />
)}
</div>
</article>
</div>
)
}
Phase 5 : Dashboard administrateur
Interface d’administration
Créez app/admin/page.tsx
:
import { getServerSession } from 'next-auth'
import { redirect } from 'next/navigation'
import { authOptions } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import { AdminStats } from '@/components/admin/AdminStats'
import { RecentPosts } from '@/components/admin/RecentPosts'
import { RecentComments } from '@/components/admin/RecentComments'
async function getAdminStats() {
const [
totalPosts,
publishedPosts,
totalComments,
pendingComments,
totalUsers
] = await Promise.all([
prisma.post.count(),
prisma.post.count({ where: { published: true } }),
prisma.comment.count(),
prisma.comment.count({ where: { approved: false } }),
prisma.user.count()
])
return {
totalPosts,
publishedPosts,
totalComments,
pendingComments,
totalUsers
}
}
export default async function AdminDashboard() {
const session = await getServerSession(authOptions)
if (!session?.user || session.user.role !== 'ADMIN') {
redirect('/auth/signin')
}
const stats = await getAdminStats()
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-7xl mx-auto py-8 px-4">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">
Dashboard Administrateur
</h1>
<p className="text-gray-600">
Gérez votre blog et surveillez les statistiques
</p>
</div>
<AdminStats stats={stats} />
<div className="grid lg:grid-cols-2 gap-8 mt-8">
<RecentPosts />
<RecentComments />
</div>
</div>
</div>
)
}
Phase 6 : Déploiement
Configuration pour Vercel
Créez vercel.json
:
{
"build": {
"env": {
"PRISMA_GENERATE_DATAPROXY": "true"
}
},
"functions": {
"app/api/**/*.ts": {
"maxDuration": 30
}
}
}
Scripts de déploiement
Ajoutez dans package.json
:
{
"scripts": {
"build": "prisma generate && next build",
"postinstall": "prisma generate",
"db:migrate": "prisma migrate deploy",
"db:seed": "tsx prisma/seed.ts"
}
}
Seed de la base de données
Créez prisma/seed.ts
:
import { PrismaClient } from '@prisma/client'
import bcrypt from 'bcryptjs'
const prisma = new PrismaClient()
async function main() {
// Créer un utilisateur admin
const hashedPassword = await bcrypt.hash('admin123', 12)
const admin = await prisma.user.upsert({
where: { email: 'admin@blog.com' },
update: {},
create: {
email: 'admin@blog.com',
name: 'Administrateur',
password: hashedPassword,
role: 'ADMIN'
}
})
// Créer des catégories
const categories = await Promise.all([
prisma.category.upsert({
where: { slug: 'web-development' },
update: {},
create: {
name: 'Développement Web',
slug: 'web-development',
description: 'Articles sur le développement web moderne',
color: '#3B82F6'
}
}),
prisma.category.upsert({
where: { slug: 'javascript' },
update: {},
create: {
name: 'JavaScript',
slug: 'javascript',
description: 'Tout sur JavaScript et ses frameworks',
color: '#F59E0B'
}
}),
prisma.category.upsert({
where: { slug: 'nextjs' },
update: {},
create: {
name: 'Next.js',
slug: 'nextjs',
description: 'Framework React pour la production',
color: '#10B981'
}
})
])
// Créer des articles d'exemple
await prisma.post.create({
data: {
title: 'Bienvenue sur notre blog Next.js',
slug: 'bienvenue-blog-nextjs',
content: `
<h2>Bienvenue sur notre blog !</h2>
<p>Ce blog a été créé avec Next.js, une application complète de A à Z.</p>
<p>Vous trouverez ici des articles sur le développement web, JavaScript, React, Next.js et bien plus encore.</p>
<h3>Fonctionnalités du blog :</h3>
<ul>
<li>Authentification complète</li>
<li>Gestion des articles et commentaires</li>
<li>Interface d'administration</li>
<li>API REST documentée avec Swagger</li>
<li>Design responsive</li>
</ul>
`,
excerpt: 'Découvrez notre nouveau blog créé avec Next.js et toutes ses fonctionnalités modernes.',
published: true,
featured: true,
authorId: admin.id,
categoryId: categories[2].id // Next.js
}
})
console.log('Base de données initialisée avec succès !')
}
main()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})
Conclusion
Félicitations ! Vous avez créé une application blog complète avec Next.js qui inclut :
✅ Fonctionnalités implémentées
- Authentification complète avec NextAuth.js
- API REST avec toutes les opérations CRUD
- Base de données PostgreSQL avec Prisma ORM
- Interface utilisateur moderne et responsive
- Dashboard administrateur pour la gestion
- Système de commentaires avec modération
- Upload d’images avec Cloudinary
- Documentation API avec Swagger
- Déploiement sur Vercel
🚀 Prochaines étapes
Pour aller plus loin, vous pourriez ajouter :
- Recherche avancée avec Elasticsearch
- Newsletter avec intégration email
- SEO avancé avec métadonnées dynamiques
- Analytics avec Google Analytics
- Cache avec Redis
- Tests avec Jest et Cypress
- Monitoring avec Sentry
Ce projet vous a permis de mettre en pratique tous les concepts Next.js appris dans les tutoriels précédents. Vous avez maintenant une base solide pour créer des applications web modernes et complètes !
Commentaires
Les commentaires sont alimentés par GitHub Discussions
Connectez-vous avec GitHub pour participer à la discussion