Construire un site moderne avec Sanity v3 et Next.js App Router

Sanity v3 est une plateforme de contenu composable qui vous offre un Studio entièrement personnalisable, un lac de contenu en temps réel et GROQ — un des langages de requête de contenu les plus puissants disponibles. Dans ce tutoriel, vous construirez un blog complet avec prévisualisation en direct, optimisation des images et une expérience d'édition personnalisée.
Ce que vous allez construire
Un DevJournal — une plateforme de blog pour développeurs avec :
- Sanity Studio v3 intégré dans votre application Next.js
- Des schémas de documents personnalisés pour les articles, auteurs et catégories
- Des requêtes GROQ pour une récupération flexible du contenu
- Une prévisualisation en direct montrant les changements de brouillon en temps réel
- Sanity Image avec optimisation automatique des images responsives
- Rendu Portable Text pour le contenu riche
- Édition visuelle avec fonctionnalité cliquer-pour-éditer
- Revalidation à la demande via webhooks
- Métadonnées SEO et génération d'images Open Graph
- Stylisation avec Tailwind CSS
- Déploiement en production sur Vercel
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 18+ installé
- npm ou pnpm comme gestionnaire de paquets
- Des connaissances de base en React, TypeScript et Next.js App Router
- Un compte Sanity.io (niveau gratuit disponible sur sanity.io)
- Un éditeur de code (VS Code recommandé)
- Une familiarité avec les concepts de gestion de contenu
Pourquoi Sanity v3 ?
Sanity adopte une approche fondamentalement différente de la gestion de contenu. Au lieu de stocker le contenu dans une base de données traditionnelle, il utilise un lac de contenu en temps réel — un stockage de données cloud sans schéma qui se synchronise en temps réel.
| Fonctionnalité | Sanity v3 | Strapi 5 | Payload CMS 3 | Contentful |
|---|---|---|---|---|
| Architecture | Lac de contenu hébergé | Serveur auto-hébergé | Dans Next.js | SaaS hébergé |
| Langage de requête | GROQ (personnalisé) | REST / GraphQL | REST / GraphQL | REST / GraphQL |
| Studio | App React personnalisable | Panneau admin | Admin Next.js | Dashboard cloud |
| Temps réel | Intégré | Configuration requise | Configuration requise | Webhooks uniquement |
| Pipeline images | CDN intégré + transformations | Plugin requis | Adaptateur upload | CDN intégré |
| Tarification | Niveau gratuit généreux | Gratuit (auto-hébergé) | Gratuit (auto-hébergé) | Niveau gratuit limité |
| Portable Text | Format texte riche natif | Blocks / Markdown | Éditeur Lexical | Rich text JSON |
| Prévisualisation | Support de première classe | Plugin communautaire | Intégrée | Limitée |
Les avantages clés de Sanity : collaboration en temps réel, un langage de requête GROQ incroyablement flexible, et un Studio 100% personnalisable car il s'agit simplement d'une application React.
Étape 1 : Créer le projet Next.js
Commencez par créer une nouvelle application Next.js :
npx create-next-app@latest devjournal --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd devjournalÉtape 2 : Installer les dépendances Sanity
Installez le client Sanity, le Studio et les paquets associés :
npm install next-sanity @sanity/image-url @sanity/vision @portabletext/react
npm install -D @sanity/typesLe paquet next-sanity est l'intégration officielle qui fournit :
- Un client Sanity configuré pour Next.js
- Des utilitaires de prévisualisation en direct
- Un constructeur d'URL d'images
- Des helpers d'intégration du Studio
Étape 3 : Créer un projet Sanity
Si vous n'avez pas encore de projet Sanity, créez-en un :
npx sanity@latest init --envLorsque vous y êtes invité :
- Nom du projet :
devjournal - Utiliser la configuration par défaut du dataset ? Oui
- Chemin de sortie du projet : Choisissez le répertoire actuel
- Sélectionner le modèle de projet : Projet propre sans schémas prédéfinis
Cela crée un fichier .env.local avec les identifiants de votre projet :
NEXT_PUBLIC_SANITY_PROJECT_ID="your-project-id"
NEXT_PUBLIC_SANITY_DATASET="production"
SANITY_API_READ_TOKEN="your-read-token"Vous pouvez également trouver votre ID de projet dans le tableau de bord Sanity sur sanity.io/manage.
Étape 4 : Configurer le client Sanity
Créez les fichiers de configuration Sanity :
// src/sanity/config.ts
export const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!
export const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET!
export const apiVersion = '2026-04-07'Maintenant créez le client Sanity :
// src/sanity/client.ts
import { createClient } from 'next-sanity'
import { projectId, dataset, apiVersion } from './config'
export const client = createClient({
projectId,
dataset,
apiVersion,
useCdn: true,
})
// Client de prévisualisation avec token pour le contenu brouillon
export const previewClient = createClient({
projectId,
dataset,
apiVersion,
useCdn: false,
token: process.env.SANITY_API_READ_TOKEN,
perspective: 'previewDrafts',
})
export function getClient(preview = false) {
return preview ? previewClient : client
}Étape 5 : Définir les schémas de contenu
Les schémas Sanity définissent la structure de votre contenu. Créez des schémas pour les articles, les auteurs et les catégories.
Schéma Auteur
// src/sanity/schemas/author.ts
import { defineField, defineType } from 'sanity'
export const author = defineType({
name: 'author',
title: 'Author',
type: 'document',
fields: [
defineField({
name: 'name',
title: 'Name',
type: 'string',
validation: (rule) => rule.required(),
}),
defineField({
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'name',
maxLength: 96,
},
validation: (rule) => rule.required(),
}),
defineField({
name: 'image',
title: 'Image',
type: 'image',
options: { hotspot: true },
}),
defineField({
name: 'bio',
title: 'Bio',
type: 'array',
of: [{ type: 'block' }],
}),
],
preview: {
select: {
title: 'name',
media: 'image',
},
},
})Schéma Catégorie
// src/sanity/schemas/category.ts
import { defineField, defineType } from 'sanity'
export const category = defineType({
name: 'category',
title: 'Category',
type: 'document',
fields: [
defineField({
name: 'title',
title: 'Title',
type: 'string',
validation: (rule) => rule.required(),
}),
defineField({
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'title',
maxLength: 96,
},
validation: (rule) => rule.required(),
}),
defineField({
name: 'description',
title: 'Description',
type: 'text',
}),
],
})Schéma Article
// src/sanity/schemas/post.ts
import { defineField, defineType } from 'sanity'
export const post = defineType({
name: 'post',
title: 'Post',
type: 'document',
fields: [
defineField({
name: 'title',
title: 'Title',
type: 'string',
validation: (rule) => rule.required(),
}),
defineField({
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'title',
maxLength: 96,
},
validation: (rule) => rule.required(),
}),
defineField({
name: 'author',
title: 'Author',
type: 'reference',
to: [{ type: 'author' }],
validation: (rule) => rule.required(),
}),
defineField({
name: 'mainImage',
title: 'Main Image',
type: 'image',
options: { hotspot: true },
fields: [
{
name: 'alt',
type: 'string',
title: 'Alternative Text',
},
],
}),
defineField({
name: 'categories',
title: 'Categories',
type: 'array',
of: [{ type: 'reference', to: [{ type: 'category' }] }],
}),
defineField({
name: 'publishedAt',
title: 'Published at',
type: 'datetime',
}),
defineField({
name: 'excerpt',
title: 'Excerpt',
type: 'text',
rows: 3,
}),
defineField({
name: 'body',
title: 'Body',
type: 'array',
of: [
{ type: 'block' },
{
type: 'image',
options: { hotspot: true },
fields: [
{ name: 'alt', type: 'string', title: 'Alternative Text' },
{ name: 'caption', type: 'string', title: 'Caption' },
],
},
{
type: 'code',
title: 'Code Block',
},
],
}),
defineField({
name: 'seo',
title: 'SEO',
type: 'object',
fields: [
{ name: 'metaTitle', type: 'string', title: 'Meta Title' },
{ name: 'metaDescription', type: 'text', title: 'Meta Description', rows: 3 },
{ name: 'ogImage', type: 'image', title: 'Open Graph Image' },
],
}),
],
preview: {
select: {
title: 'title',
author: 'author.name',
media: 'mainImage',
},
prepare(selection) {
const { author } = selection
return { ...selection, subtitle: author ? `par ${author}` : '' }
},
},
orderings: [
{
title: 'Date de publication, récent',
name: 'publishedAtDesc',
by: [{ field: 'publishedAt', direction: 'desc' }],
},
],
})Index des schémas
// src/sanity/schemas/index.ts
import { author } from './author'
import { category } from './category'
import { post } from './post'
export const schemaTypes = [author, category, post]Étape 6 : Configurer Sanity Studio
Créez le fichier de configuration du Studio :
// src/sanity/studio.ts
import { defineConfig } from 'sanity'
import { structureTool } from 'sanity/structure'
import { visionTool } from '@sanity/vision'
import { schemaTypes } from './schemas'
import { projectId, dataset } from './config'
export default defineConfig({
name: 'devjournal-studio',
title: 'DevJournal Studio',
projectId,
dataset,
plugins: [structureTool(), visionTool()],
schema: {
types: schemaTypes,
},
})Étape 7 : Intégrer le Studio dans Next.js
Créez une route pour le Sanity Studio dans votre application Next.js :
// src/app/studio/[[...tool]]/page.tsx
'use client'
import { NextStudio } from 'next-sanity/studio'
import config from '@/sanity/studio'
export default function StudioPage() {
return <NextStudio config={config} />
}Ajoutez un fichier layout pour empêcher le Studio d'être affecté par la mise en page de votre application :
// src/app/studio/[[...tool]]/layout.tsx
export const metadata = {
title: 'DevJournal Studio',
}
export default function StudioLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="fr">
<body>{children}</body>
</html>
)
}Visitez maintenant http://localhost:3000/studio pour accéder à votre Sanity Studio. Vous verrez l'interface d'édition complète avec vos schémas personnalisés.
Étape 8 : Écrire des requêtes GROQ
GROQ (Graph-Relational Object Queries) est le langage de requête de Sanity. Il est incroyablement puissant pour récupérer exactement les données dont vous avez besoin.
Créez un fichier de requêtes :
// src/sanity/queries.ts
import { groq } from 'next-sanity'
// Récupérer tous les articles publiés
export const postsQuery = groq`
*[_type == "post" && defined(publishedAt)] | order(publishedAt desc) {
_id,
title,
slug,
excerpt,
publishedAt,
mainImage,
"author": author->{name, slug, image},
"categories": categories[]->{ title, slug }
}
`
// Récupérer un article par slug
export const postBySlugQuery = groq`
*[_type == "post" && slug.current == $slug][0] {
_id,
title,
slug,
excerpt,
publishedAt,
mainImage,
body,
"author": author->{name, slug, image, bio},
"categories": categories[]->{ title, slug },
seo
}
`
// Récupérer tous les slugs pour la génération statique
export const postSlugsQuery = groq`
*[_type == "post" && defined(slug.current)][].slug.current
`
// Récupérer les articles par catégorie
export const postsByCategoryQuery = groq`
*[_type == "post" && $categorySlug in categories[]->slug.current] | order(publishedAt desc) {
_id,
title,
slug,
excerpt,
publishedAt,
mainImage,
"author": author->{name, slug, image},
"categories": categories[]->{ title, slug }
}
`
// Articles connexes
export const relatedPostsQuery = groq`
*[_type == "post" && _id != $currentId && count(categories[@._ref in $categoryIds]) > 0] | order(publishedAt desc) [0...3] {
_id,
title,
slug,
excerpt,
publishedAt,
mainImage
}
`Remarquez comment GROQ utilise l'opérateur -> pour déréférencer les références en ligne. C'est l'une de ses fonctionnalités les plus puissantes — vous pouvez joindre des documents liés sans écrire des requêtes complexes.
Étape 9 : Configurer la gestion des images
Sanity fournit un pipeline d'images puissant avec redimensionnement, recadrage et conversion de format automatiques. Créez un utilitaire d'images :
// src/sanity/image.ts
import createImageUrlBuilder from '@sanity/image-url'
import { projectId, dataset } from './config'
import type { Image } from 'sanity'
const imageBuilder = createImageUrlBuilder({ projectId, dataset })
export function urlForImage(source: Image | undefined) {
if (!source) return undefined
return imageBuilder.image(source).auto('format').fit('max')
}Créez un composant image réutilisable :
// src/components/sanity-image.tsx
import Image from 'next/image'
import { urlForImage } from '@/sanity/image'
import type { Image as SanityImageType } from 'sanity'
interface SanityImageProps {
image: SanityImageType & { alt?: string }
width?: number
height?: number
className?: string
priority?: boolean
}
export function SanityImage({
image,
width = 800,
height = 450,
className,
priority = false,
}: SanityImageProps) {
const imageUrl = urlForImage(image)?.url()
if (!imageUrl) return null
return (
<Image
src={imageUrl}
alt={image.alt || ''}
width={width}
height={height}
className={className}
priority={priority}
/>
)
}Étape 10 : Construire les pages du blog
Page de liste du blog
// src/app/blog/page.tsx
import Link from 'next/link'
import { client } from '@/sanity/client'
import { postsQuery } from '@/sanity/queries'
import { SanityImage } from '@/components/sanity-image'
export const revalidate = 60
export default async function BlogPage() {
const posts = await client.fetch(postsQuery)
return (
<main className="mx-auto max-w-6xl px-4 py-16">
<h1 className="mb-12 text-4xl font-bold">Blog DevJournal</h1>
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{posts.map((post: any) => (
<article
key={post._id}
className="group overflow-hidden rounded-xl border bg-white shadow-sm transition-shadow hover:shadow-md"
>
<Link href={`/blog/${post.slug.current}`}>
{post.mainImage && (
<SanityImage
image={post.mainImage}
width={600}
height={340}
className="aspect-video w-full object-cover transition-transform group-hover:scale-105"
/>
)}
<div className="p-6">
<h2 className="mb-2 text-xl font-semibold">{post.title}</h2>
<p className="mb-4 line-clamp-2 text-gray-600">{post.excerpt}</p>
<div className="flex items-center gap-3 text-sm text-gray-500">
<span>{post.author?.name}</span>
</div>
</div>
</Link>
</article>
))}
</div>
</main>
)
}Page d'article individuel
// src/app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
import { client } from '@/sanity/client'
import { postBySlugQuery, postSlugsQuery } from '@/sanity/queries'
import { SanityImage } from '@/components/sanity-image'
import { PortableTextRenderer } from '@/components/portable-text'
import type { Metadata } from 'next'
interface PageProps {
params: Promise<{ slug: string }>
}
export async function generateStaticParams() {
const slugs = await client.fetch(postSlugsQuery)
return slugs.map((slug: string) => ({ slug }))
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params
const post = await client.fetch(postBySlugQuery, { slug })
if (!post) return { title: 'Article non trouvé' }
return {
title: post.seo?.metaTitle || post.title,
description: post.seo?.metaDescription || post.excerpt,
}
}
export default async function PostPage({ params }: PageProps) {
const { slug } = await params
const post = await client.fetch(postBySlugQuery, { slug })
if (!post) notFound()
return (
<article className="mx-auto max-w-3xl px-4 py-16">
<header className="mb-12">
<h1 className="mb-4 text-4xl font-bold leading-tight lg:text-5xl">
{post.title}
</h1>
{post.excerpt && (
<p className="mb-6 text-xl text-gray-600">{post.excerpt}</p>
)}
</header>
{post.mainImage && (
<SanityImage
image={post.mainImage}
className="mb-12 w-full rounded-xl"
priority
/>
)}
<div className="prose prose-lg max-w-none">
<PortableTextRenderer value={post.body} />
</div>
</article>
)
}Étape 11 : Rendre le Portable Text
Sanity utilise Portable Text — un format de texte riche qui vous donne un contrôle total sur le rendu. Créez un moteur de rendu personnalisé :
// src/components/portable-text.tsx
import {
PortableText,
type PortableTextComponents,
} from '@portabletext/react'
import { SanityImage } from './sanity-image'
const components: PortableTextComponents = {
types: {
image: ({ value }) => (
<figure className="my-8">
<SanityImage image={value} className="w-full rounded-lg" />
{value.caption && (
<figcaption className="mt-2 text-center text-sm text-gray-500">
{value.caption}
</figcaption>
)}
</figure>
),
code: ({ value }) => (
<pre className="my-6 overflow-x-auto rounded-lg bg-gray-900 p-4">
<code className={`language-${value.language || 'text'}`}>
{value.code}
</code>
</pre>
),
},
marks: {
link: ({ children, value }) => (
<a
href={value?.href}
rel="noreferrer noopener"
className="text-blue-600 underline hover:text-blue-800"
>
{children}
</a>
),
code: ({ children }) => (
<code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm">
{children}
</code>
),
},
block: {
h2: ({ children }) => (
<h2 className="mb-4 mt-12 text-3xl font-bold">{children}</h2>
),
h3: ({ children }) => (
<h3 className="mb-3 mt-8 text-2xl font-semibold">{children}</h3>
),
blockquote: ({ children }) => (
<blockquote className="my-6 border-l-4 border-blue-500 pl-4 italic text-gray-700">
{children}
</blockquote>
),
},
}
export function PortableTextRenderer({ value }: { value: any }) {
if (!value) return null
return <PortableText value={value} components={components} />
}Étape 12 : Ajouter la prévisualisation en direct
La prévisualisation en direct est l'une des fonctionnalités phares de Sanity. Elle permet aux éditeurs de contenu de voir leurs modifications en temps réel avant la publication.
Créer un fournisseur de prévisualisation
// src/components/preview-provider.tsx
'use client'
import { LiveQueryProvider } from 'next-sanity/preview'
import { client } from '@/sanity/client'
export default function PreviewProvider({
children,
token,
}: {
children: React.ReactNode
token: string
}) {
return (
<LiveQueryProvider client={client} token={token}>
{children}
</LiveQueryProvider>
)
}Créer une route de prévisualisation
// src/app/api/preview/route.ts
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const slug = searchParams.get('slug')
const secret = searchParams.get('secret')
if (secret !== process.env.SANITY_PREVIEW_SECRET) {
return new Response('Token invalide', { status: 401 })
}
const draft = await draftMode()
draft.enable()
redirect(slug ? `/blog/${slug}` : '/blog')
}Route de sortie de prévisualisation
// src/app/api/exit-preview/route.ts
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
export async function GET() {
const draft = await draftMode()
draft.disable()
redirect('/blog')
}Étape 13 : Revalidation à la demande avec Webhooks
Au lieu de revalider sur un timer, configurez un webhook pour que Sanity déclenche la revalidation quand le contenu change :
// src/app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
const secret = request.headers.get('x-sanity-webhook-secret')
if (secret !== process.env.SANITY_WEBHOOK_SECRET) {
return NextResponse.json({ message: 'Non autorisé' }, { status: 401 })
}
const body = await request.json()
switch (body._type) {
case 'post':
revalidatePath('/blog')
if (body.slug?.current) {
revalidatePath(`/blog/${body.slug.current}`)
}
break
case 'author':
case 'category':
revalidatePath('/blog')
break
default:
revalidatePath('/')
}
return NextResponse.json({ revalidated: true, now: Date.now() })
}Pour configurer le webhook dans Sanity :
- Allez sur sanity.io/manage et sélectionnez votre projet
- Naviguez vers API puis Webhooks
- Cliquez sur Create Webhook
- Définissez l'URL sur
https://votre-domaine.com/api/revalidate - Ajoutez un en-tête personnalisé :
x-sanity-webhook-secretavec votre valeur secrète - Sélectionnez les types de documents à déclencher (post, author, category)
- Choisissez les événements Create, Update et Delete
Étape 14 : Types TypeScript
Ajoutez des types TypeScript pour une sécurité de type complète :
// src/sanity/types.ts
import type { Image, Slug, PortableTextBlock } from 'sanity'
export interface Author {
_id: string
name: string
slug: Slug
image?: Image
bio?: PortableTextBlock[]
}
export interface Category {
_id: string
title: string
slug: Slug
description?: string
postCount?: number
}
export interface Post {
_id: string
title: string
slug: Slug
author: Author
mainImage?: Image & { alt?: string }
categories?: Category[]
publishedAt: string
excerpt?: string
body?: PortableTextBlock[]
seo?: {
metaTitle?: string
metaDescription?: string
ogImage?: Image
}
}Étape 15 : Déployer en production
Déployer Next.js sur Vercel
git add .
git commit -m "feat: complete devjournal with Sanity v3"
git push origin main
npx vercelConfigurez les variables d'environnement dans Vercel :
NEXT_PUBLIC_SANITY_PROJECT_IDNEXT_PUBLIC_SANITY_DATASETSANITY_API_READ_TOKENSANITY_PREVIEW_SECRETSANITY_WEBHOOK_SECRET
Configurer CORS dans Sanity
Allez sur sanity.io/manage et ajoutez votre domaine de production aux origines CORS :
https://votre-domaine.com(avec identifiants autorisés)http://localhost:3000(pour le développement)
Tester votre implémentation
- Accès au Studio : Visitez
/studioet vérifiez que vous pouvez créer des auteurs, catégories et articles - Liste du blog : Visitez
/bloget confirmez que les articles s'affichent avec les images et métadonnées - Article individuel : Cliquez sur un article et vérifiez que le Portable Text s'affiche correctement
- Optimisation des images : Vérifiez que les images se chargent en WebP avec des tailles responsives
- Prévisualisation en direct : Activez le mode brouillon et vérifiez que les changements apparaissent en temps réel
- Revalidation : Publiez un changement dans le Studio et vérifiez que le site se met à jour
Dépannage
Erreurs "Projet non trouvé" ou CORS
Assurez-vous que votre ID de projet est correct et que vous avez ajouté localhost:3000 aux origines CORS dans le tableau de bord Sanity.
Les images ne se chargent pas
Vérifiez que votre dataset Sanity est configuré en public pour les assets images, ou que vous transmettez le bon token pour les datasets privés.
Le Studio affiche une page blanche
Vérifiez la console du navigateur. Les problèmes courants incluent des types de schéma manquants ou une configuration de plugins incorrecte.
La requête GROQ retourne vide
Utilisez l'outil Vision dans le Studio pour tester les requêtes. Les erreurs courantes incluent des filtres _type manquants ou une syntaxe de référence incorrecte.
Prochaines étapes
- Contenu structuré : Ajoutez plus de types de documents comme Projets, Membres d'équipe ou FAQ
- Édition visuelle : Intégrez l'édition visuelle de Sanity pour cliquer-pour-éditer sur le frontend
- Internationalisation : Utilisez le plugin i18n au niveau document de Sanity pour le contenu multilingue
- Recherche : Implémentez la recherche plein texte avec la correspondance textuelle GROQ ou l'intégration Algolia
- Analytiques : Ajoutez des compteurs de vues en utilisant les mutations Sanity depuis le frontend
Conclusion
Vous avez construit un site web complet axé sur le contenu avec Sanity v3 et Next.js App Router. Cette combinaison vous offre un Studio CMS entièrement personnalisable, un puissant langage de requête GROQ, une prévisualisation en direct en temps réel et un pipeline d'images optimisé — le tout en maintenant une sécurité de type complète avec TypeScript.
L'architecture du lac de contenu de Sanity signifie que votre contenu est toujours disponible via API, facilitant sa réutilisation sur les sites web, applications mobiles et autres plateformes. L'approche du Studio intégré signifie que les éditeurs de contenu bénéficient d'une expérience d'édition soignée sans avoir besoin d'une application séparée.
Les points clés de ce tutoriel :
- Les schémas Sanity sont orientés code et entièrement typés
- Les requêtes GROQ vous donnent un contrôle précis sur la récupération des données
- Le Portable Text fournit un contenu riche avec un rendu personnalisé
- La prévisualisation en direct et la revalidation à la demande créent un flux de travail d'édition fluide
- Le pipeline d'images gère l'optimisation automatiquement
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

Construire un site web axé sur le contenu avec Payload CMS 3 et Next.js
Apprenez à construire un site web complet avec Payload CMS 3, qui fonctionne nativement dans Next.js App Router. Ce tutoriel couvre les collections, le rich text, les uploads, l'authentification et le déploiement en production.

Strapi 5 et Next.js 15 : Créer une Application Full-Stack avec un CMS Headless
Apprenez à créer une application full-stack avec Strapi 5 comme CMS headless et Next.js 15 App Router. Ce tutoriel couvre la création de types de contenu, les API REST, le rendu côté serveur et le déploiement en production.

Medusa.js 2.0 — Construire une boutique e-commerce Headless avec Next.js (2026)
Apprenez à construire une boutique e-commerce complète avec Medusa.js 2.0 et Next.js. Du catalogue produits au paiement, ce tutoriel couvre tout en TypeScript.