📍 Article principal de la série : Meilisearch 2026 : le guide pratique. Lisez le guide général pour la vue d’ensemble.
Vous avez un Meilisearch en production (voir tutoriel précédent) et une application Next.js 15 avec App Router. Ce tutoriel branche les deux pour livrer une expérience de recherche instantanée digne d’Algolia : autocomplete dès le 2e caractère, facettes filtrables, surlignage des termes, pagination infinie. La méthode marche pour un blog, une marketplace, un annuaire ou une documentation technique.
Prérequis
- Application Next.js 15 avec App Router opérationnelle.
- Meilisearch v1.10 accessible en HTTPS avec une API key search-only.
- Au moins un index avec quelques centaines de documents pour tester.
- Niveau : intermédiaire (React, hooks, TypeScript).
- Temps estimé : 1 à 2 heures.
Étape 1 — Installer les dépendances
npm install meilisearch instantsearch.js react-instantsearch react-instantsearch-nextjs
npm install @meilisearch/instant-meilisearch
Le package @meilisearch/instant-meilisearch fait le pont entre l’API Meilisearch et le protocole InstantSearch (compatible Algolia), permettant de réutiliser tous les composants react-instantsearch sans réécriture.
Étape 2 — Variables d’environnement
Dans .env.local :
NEXT_PUBLIC_MEILI_HOST=https://search.votre-entreprise.com
NEXT_PUBLIC_MEILI_SEARCH_KEY=votre-api-key-search-only
Important : seule la clé search-only doit être préfixée NEXT_PUBLIC_. Le master key reste côté serveur uniquement, dans MEILI_MASTER_KEY sans préfixe public.
Étape 3 — Créer le client InstantSearch
Fichier lib/search-client.ts :
'use client';
import { instantMeiliSearch } from '@meilisearch/instant-meilisearch';
export const { searchClient } = instantMeiliSearch(
process.env.NEXT_PUBLIC_MEILI_HOST!,
process.env.NEXT_PUBLIC_MEILI_SEARCH_KEY!,
{
finitePagination: true,
primaryKey: 'id',
}
);
Étape 4 — Page de recherche complète
Fichier app/recherche/page.tsx :
'use client';
import { InstantSearchNext } from 'react-instantsearch-nextjs';
import {
SearchBox, Hits, RefinementList, Pagination, Stats, Highlight,
} from 'react-instantsearch';
import { searchClient } from '@/lib/search-client';
function Hit({ hit }: { hit: any }) {
return (
<article className="border rounded p-4 hover:shadow">
<h3 className="font-bold">
<Highlight attribute="title" hit={hit} />
</h3>
<p className="text-sm text-gray-600">
<Highlight attribute="description" hit={hit} />
</p>
<p className="font-mono">{hit.price.toLocaleString()} FCFA</p>
</article>
);
}
export default function SearchPage() {
return (
<InstantSearchNext indexName="products" searchClient={searchClient} routing>
<div className="grid grid-cols-4 gap-6">
<aside className="col-span-1">
<h4>Catégorie</h4>
<RefinementList attribute="category" />
<h4>Marque</h4>
<RefinementList attribute="brand" />
</aside>
<main className="col-span-3">
<SearchBox placeholder="Rechercher..." />
<Stats />
<Hits hitComponent={Hit} />
<Pagination />
</main>
</div>
</InstantSearchNext>
);
}
Étape 5 — Configurer les attributes côté Meilisearch
Avant que les facettes fonctionnent, déclarez côté Meilisearch quels attributs sont filterables :
curl -X PATCH 'https://search.votre-entreprise.com/indexes/products/settings' \
-H 'Authorization: Bearer MASTER_KEY' \
-H 'Content-Type: application/json' \
--data '{
"filterableAttributes": ["category", "brand", "price"],
"sortableAttributes": ["price", "created_at"],
"searchableAttributes": ["title", "description", "brand"],
"displayedAttributes": ["*"]
}'
Étape 6 — Autocomplete dans le header
Pour une expérience comme Amazon, ajoutez un autocomplete miniature dans le header global. Composant components/HeaderSearch.tsx :
'use client';
import { useState, useEffect } from 'react';
import { MeiliSearch } from 'meilisearch';
import Link from 'next/link';
import { useDebouncedCallback } from 'use-debounce';
const client = new MeiliSearch({
host: process.env.NEXT_PUBLIC_MEILI_HOST!,
apiKey: process.env.NEXT_PUBLIC_MEILI_SEARCH_KEY!,
});
export default function HeaderSearch() {
const [q, setQ] = useState('');
const [results, setResults] = useState<any[]>([]);
const debouncedSearch = useDebouncedCallback(async (query: string) => {
if (!query) { setResults([]); return; }
const r = await client.index('products').search(query, { limit: 8 });
setResults(r.hits);
}, 250);
useEffect(() => debouncedSearch(q), [q]);
return (
<div className="relative">
<input value={q} onChange={(e) => setQ(e.target.value)}
placeholder="Rechercher..." className="border rounded px-3 py-2 w-72" />
{results.length > 0 && (
<ul className="absolute top-full bg-white shadow-lg w-full">
{results.map((h) => (
<li key={h.id} className="p-2 hover:bg-gray-100">
<Link href={`/produits/${h.slug}`}>{h.title}</Link>
</li>
))}
</ul>
)}
</div>
);
}
Étape 7 — Server-Side Rendering pour le SEO
Avec App Router et React Server Components, vous pouvez pre-render la première page de résultats côté serveur. Cela améliore le SEO et la latence perçue.
// app/recherche/[query]/page.tsx
import { MeiliSearch } from 'meilisearch';
const serverClient = new MeiliSearch({
host: process.env.MEILI_HOST!,
apiKey: process.env.MEILI_ADMIN_KEY!,
});
export default async function Page({ params }: { params: { query: string } }) {
const initial = await serverClient.index('products')
.search(decodeURIComponent(params.query), { limit: 20 });
return <ResultsClient initial={initial} query={params.query} />;
}
Étape 8 — Tracking analytics côté client
Trackez les recherches sans résultat pour améliorer la pertinence. Plausible self-hosted ou Umami suffisent :
useEffect(() => {
if (results.length === 0 && q.length > 2) {
plausible('NoResults', { props: { query: q } });
}
}, [results, q]);
Erreurs fréquentes
| Erreur | Cause | Solution |
|---|---|---|
| CORS error en local | Meilisearch refuse l’origine localhost | Configurer Caddy reverse proxy avec headers CORS adaptés |
| Hits sans surlignage | Composant Highlight mal configuré | Vérifier l’attribut attribute exact |
| Facettes vides | Attribut non filterable côté serveur | PATCH settings + waitForTask |
| Autocomplete laggy | Pas de debounce | useDebouncedCallback avec 250 ms |
| Master key dans le bundle | NEXT_PUBLIC_ sur la mauvaise clé | Régénérer une search-only et révoquer le master leaké |
| Pagination infinie cassée | routing=false | Activer routing dans InstantSearchNext |
Adaptation à l’écosystème Sonatel, Orange et Mixx by Yas
Trois ajustements concrets. Mobile first : 75% des utilisateurs e-commerce africains naviguent sur mobile. Le design de la page recherche doit privilégier une seule colonne sur mobile, avec un bouton « Filtres » qui ouvre une drawer. Tester sur des Android 6 à 8 (encore courants au Mali, Burkina, Niger). Connexion intermittente : cacher localement les 50 derniers résultats avec localStorage, retourner le cache si offline. Le service worker peut intercepter les requêtes Meilisearch et servir un cache JSON. Données locales : pour un annuaire, utiliser des slugs descriptifs (/recherche/restaurants-dakar-plateau) plutôt que des paramètres URL pour le SEO Google et les partages WhatsApp.
Articles connexes
- Déployer Meilisearch sur Hetzner avec Coolify — le pré-requis serveur.
- Synchronisation Postgres → Meilisearch via Drizzle
FAQ
Pourquoi react-instantsearch et pas un client custom ? Pour gagner 2 à 3 jours de développement. La librairie gère pagination, routing URL, état partagé, et 25+ composants prêts. Custom client si vous avez des besoins très spécifiques.
Compatibilité Server Components ? Le composant InstantSearchNext de react-instantsearch-nextjs supporte le streaming et l’hydratation App Router depuis 2024.
Comment gérer plusieurs index dans une seule page ? Utiliser <Index indexName="..."/> imbriqué dans InstantSearchNext pour rechercher dans plusieurs index simultanément (federated search).
Performance sur mobile bas de gamme ? Le bundle react-instantsearch ajoute environ 50 Ko gzippé. Acceptable pour 99% des usages. Pour ultra-léger, écrire un client custom direct avec meilisearch-js seul (15 Ko).
Comment ajouter du tracking de clic sur résultat ? Composant Hit personnalisé qui appelle onClick avec l’ID du document, envoyé à Plausible/Umami pour analyser quels résultats convertissent réellement.
Pour étoffer le tableau
- 🔝 Retour au guide général : guide pratique Meilisearch 2026
- Documentation react-instantsearch : algolia.com/doc/api-reference/widgets/react
- Tutoriel suivant : Synchronisation Postgres avec Drizzle
Étape 1 — Lancer une instance Meilisearch en local pour développer
Avant de toucher à Next.js, vous avez besoin d’un Meilisearch fonctionnel sur votre poste. Pour un développeur basé à Dakar avec une connexion intermittente, Docker est la voie la plus rapide. Une fois l’image téléchargée (≈ 60 Mo), vous travaillerez offline. Installez Docker Desktop puis lancez le conteneur.
docker run -d --name meili \
-p 7700:7700 \
-e MEILI_MASTER_KEY='masterKeyDevLocal' \
-v $(pwd)/meili_data:/meili_data \
getmeili/meilisearch:v1.11
Vérifiez la santé de l’instance avec curl http://localhost:7700/health. Vous devez recevoir {« status »: »available »}. Si curl échoue, vérifiez que le port 7700 n’est pas occupé par un autre service. Le volume meili_data persistera vos index entre redémarrages.
Étape 2 — Initialiser un projet Next.js 15 avec App Router
Démarrez un nouveau projet Next.js avec App Router et TypeScript. Cette version supporte React 19 et offre les Server Components nécessaires pour le rendu SSR de la première page de résultats, ce qui améliore le SEO et le LCP — point critique en 3G à Cotonou ou Bamako.
npx create-next-app@latest meili-search-demo \
--typescript --app --tailwind --eslint --src-dir
cd meili-search-demo
npm install meilisearch @meilisearch/instant-meilisearch react-instantsearch
Vérifiez que le projet démarre avec npm run dev sur http://localhost:3000. Indicateur que tout est en place : la page d’accueil Next.js par défaut s’affiche en moins de 2 secondes. Vous avez maintenant un terrain de jeu propre pour brancher Meilisearch.
Étape 3 — Indexer un jeu de données via la SDK officielle
Créez un script d’indexation indépendant pour ne pas polluer le code applicatif. Stockez votre clé d’API dans .env.local (jamais dans le repo). Utilisez la clé admin pour l’indexation, jamais la masterKey en production.
// scripts/seed.ts
import { MeiliSearch } from 'meilisearch';
import docs from './products.json';
const client = new MeiliSearch({
host: process.env.MEILI_HOST,
apiKey: process.env.MEILI_ADMIN_KEY
});
const index = client.index('products');
await index.addDocuments(docs);
console.log('Indexed', docs.length, 'documents');
Lancez avec npx tsx scripts/seed.ts. Meilisearch retourne une taskUid. Vérifiez son statut via curl http://localhost:7700/tasks/UID. Vous devez voir status: succeeded en moins d’une seconde pour 1 000 documents.
Étape 4 — Configurer les attributs recherchables et filtrables
Par défaut, Meilisearch indexe tous les champs en recherche fulltext mais ne filtre rien. Pour des facets (catégorie, prix, ville), vous devez déclarer explicitement les attributs filtrables. C’est l’étape oubliée par 80 % des débutants.
await index.updateSettings({
searchableAttributes: ['title', 'description', 'tags'],
filterableAttributes: ['category', 'price', 'city'],
sortableAttributes: ['price', 'createdAt'],
displayedAttributes: ['*']
});
Relancez le script et vérifiez via curl http://localhost:7700/indexes/products/settings. Vous devez voir vos attributs listés. Sans cette config, les facets retourneront 0 résultats même si les documents existent.
Étape 5 — Brancher InstantSearch côté client React
react-instantsearch fournit les composants pré-faits (SearchBox, Hits, RefinementList, Pagination). Combiné à @meilisearch/instant-meilisearch, vous avez un autocomplete instantané en moins de 50 lignes. Créez la page src/app/search/page.tsx.
'use client';
import { InstantSearch, SearchBox, Hits, RefinementList } from 'react-instantsearch';
import { instantMeiliSearch } from '@meilisearch/instant-meilisearch';
const { searchClient } = instantMeiliSearch(
process.env.NEXT_PUBLIC_MEILI_HOST,
process.env.NEXT_PUBLIC_MEILI_SEARCH_KEY
);
export default function Search() {
return (
<InstantSearch indexName="products" searchClient={searchClient}>
<SearchBox />
<RefinementList attribute="category" />
<Hits />
</InstantSearch>
);
}
Rechargez /search dans le navigateur. Tapez 3 caractères : les résultats apparaissent en moins de 100 ms même sur un Lenovo entrée de gamme. C’est l’expérience « Algolia-like » en self-hosted, sans facture mensuelle à 500 USD (≈ 327 980 FCFA).
Étape 6 — Activer typo-tolérance et mots vides
Meilisearch tolère par défaut 1 typo pour 5 caractères et 2 typos pour 9 caractères. Pour un public francophone (Aïcha, Mamadou, Sénégal), élargissez la tolérance et ajoutez les stop words français pour éviter de matcher sur « le », « la », « de ».
await index.updateSettings({
typoTolerance: {
minWordSizeForTypos: { oneTypo: 4, twoTypos: 8 }
},
stopWords: ['le', 'la', 'les', 'de', 'du', 'des', 'et', 'ou']
});
Testez avec une recherche « ordnateur » (au lieu de « ordinateur ») : Meilisearch doit retourner les ordinateurs. Sans typo-tolérance ajustée, les fautes courantes des utilisateurs mobiles (clavier 4 pouces) tuent vos taux de conversion.
Étape 7 — Déployer Meilisearch sur un VPS Hetzner Falkenstein ou Scaleway Paris
Pour la production, oubliez Heroku. Un VPS Hetzner CX22 à 4,51 EUR/mois (≈ 2 960 FCFA) supporte 100 000 documents et 50 req/s sans broncher. Privilégiez Falkenstein ou Helsinki pour la latence Europe-Afrique de l’Ouest (≈ 80 ms vs 200 ms US East).
ssh root@VOTRE_IP
curl -L https://install.meilisearch.com | sh
sudo mv ./meilisearch /usr/local/bin/
sudo useradd -d /var/lib/meilisearch -b /bin/false -m -r meilisearch
# Service systemd : voir doc officielle meilisearch.com/docs
Configurez systemd pour redémarrage auto, puis placez Caddy ou Nginx en reverse-proxy avec HTTPS Let’s Encrypt. Vérifiez via curl https://search.votredomaine.io/health depuis Dakar : la latence aller-retour doit rester sous 250 ms.
Étape 8 — Sécuriser avec des clés API restreintes
N’exposez JAMAIS la masterKey côté client. Générez une clé publique restreinte au search uniquement, scopée à un index. C’est l’équivalent du searchOnlyApiKey d’Algolia.
curl -X POST 'https://search.votredomaine.io/keys' \
-H 'Authorization: Bearer MASTER_KEY' \
-H 'Content-Type: application/json' \
--data-binary '{
"description": "Search only key for products",
"actions": ["search"],
"indexes": ["products"],
"expiresAt": "2027-01-01T00:00:00Z"
}'
Stockez cette clé dans NEXT_PUBLIC_MEILI_SEARCH_KEY. Elle peut fuiter dans le bundle JS sans risque, l’attaquant ne pourra que lire l’index produits, jamais écrire ni accéder à d’autres index.
Étape 9 — Monitorer et indexer en continu
En production, vos données changent. Ne réindexez pas tout chaque nuit. Utilisez un webhook depuis votre CMS (WordPress, Strapi, Sanity) qui appelle l’endpoint Meilisearch à chaque création/édition. Pour WordPress, le plugin officiel meilisearch-wordpress fait le pont.
Côté monitoring, exposez la métrique /metrics au format Prometheus, scraper avec Grafana Cloud (free tier 10k metrics). Surveillez search_count_total et search_duration_seconds. Une p95 supérieure à 200 ms = il faut grossir le VPS ou activer le sharding.
Sur un angle proche, lisez notre guide principal sur Next.js 15 et le tutoriel déploiement Hetzner pour développeurs africains. Vous avez maintenant un moteur de recherche pro, self-hosted, à coût marginal — l’avantage compétitif que les startups dakaroises ne peuvent plus ignorer.
Étape 10 — Optimiser les coûts et la sauvegarde quotidienne
Un index Meilisearch en production sans backup, c’est un incident qui devient catastrophe le jour où le disque du VPS lâche. Configurez un dump quotidien automatique vers un stockage objet Backblaze B2 (6 USD/To/mois, soit ≈ 3 940 FCFA) ou Scaleway Glacier. La commande est triviale et le coût marginal sur un index de 200 Mo reste sous 0,01 USD/mois.
# /etc/cron.daily/meili-dump
#!/bin/bash
curl -X POST 'http://127.0.0.1:7700/dumps' \
-H 'Authorization: Bearer MASTER_KEY'
sleep 60
rclone copy /var/lib/meilisearch/dumps/ b2:meili-backups/ --max-age 24h
find /var/lib/meilisearch/dumps/ -mtime +3 -delete
Vérifiez chaque lundi matin que le bucket Backblaze contient bien 7 dumps datés. Restaurez périodiquement sur un VPS de test : un backup jamais testé n’est pas un backup. Pour une boutique e-commerce dakaroise, perdre l’index produit signifie perdre la recherche pendant 24 h, donc 30 % du chiffre d’affaires en ligne sur la journée.