
Ma première extension Chrome avec WXT : retour d'expérience sur RecruitEasy
Pendant longtemps, j'ai évité de toucher aux extensions Chrome. Le souvenir d'un manifest.json écrit à la main, d'un build Webpack bricolé et d'un rechargement manuel à chaque modification m'avait suffi.
Puis est venu le besoin réel : permettre aux recruteurs utilisateurs de RecruitEasy de capturer un profil candidat directement depuis LinkedIn ou une page d'offre, sans copier-coller. Une extension navigateur était la seule réponse logique.
C'est là que j'ai découvert WXT. Premier projet d'extension de ma vie fait sérieusement, et le framework a fait toute la différence.
Pourquoi WXT plutôt que le boilerplate brut
Manifest V3 impose une architecture précise : un service worker en arrière-plan, des content scripts injectés dans la page, un popup, parfois une page d'options. En partant de zéro, on passe la première journée à câbler le build, le hot reload et le typage.
WXT règle tout ça :
- Build basé sur Vite : démarrage instantané, HMR sur le popup et les content scripts.
- Convention de fichiers :
entrypoints/génère le manifest automatiquement. - Cross-browser : la même base produit du Chrome (MV3) et du Firefox sans branchements manuels.
- TypeScript de bout en bout : les APIs
browser.*sont typées.
L'argument décisif : je voulais coder la feature, pas l'outillage.
La structure du projet
Après npx wxt@latest init, l'arborescence est minimaliste. Voici à quoi ressemblait celle de l'extension RecruitEasy :
recruiteasy-extension/
├── entrypoints/
│ ├── background.ts # service worker
│ ├── popup/ # UI du popup (React)
│ │ ├── App.tsx
│ │ └── index.html
│ └── content.ts # script injecté dans LinkedIn
├── components/
│ └── ProfileCard.tsx
├── utils/
│ └── parseProfile.ts
└── wxt.config.ts
Chaque fichier dans entrypoints/ devient une entrée du manifest. Pas de manifest.json à maintenir : WXT le génère à partir de la config et des conventions.
Le content script : capturer un profil
Le cœur de l'extension : lire le DOM d'une page de profil et en extraire les infos pertinentes. WXT expose defineContentScript avec le matches directement déclaré dans le fichier.
// entrypoints/content.ts
import { parseProfile } from "@/utils/parseProfile";
export default defineContentScript({
matches: ["*://*.linkedin.com/in/*"],
main() {
// On écoute la demande venant du popup
browser.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message.type === "CAPTURE_PROFILE") {
const profile = parseProfile(document);
sendResponse({ ok: true, profile });
}
return true; // réponse asynchrone
});
},
});defineContentScript n'est pas un import classique : c'est une macro que WXT comprend au build pour câbler le manifest. Le matches ici suffit à déclarer les permissions de host. Plus besoin de les dupliquer ailleurs.
Le popup : déclencher la capture
Le popup est une petite app React. Au clic, il envoie un message au content script de l'onglet actif, puis affiche le résultat.
// entrypoints/popup/App.tsx
import { useState } from "react";
export default function App() {
const [profile, setProfile] = useState<Profile | null>(null);
const [loading, setLoading] = useState(false);
async function capture() {
setLoading(true);
const [tab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
const res = await browser.tabs.sendMessage(tab.id!, {
type: "CAPTURE_PROFILE",
});
setProfile(res.profile);
setLoading(false);
}
return (
<div className="w-80 p-4">
<button onClick={capture} disabled={loading}>
{loading ? "Capture…" : "Importer ce candidat"}
</button>
{profile && <ProfileCard profile={profile} />}
</div>
);
}Avec le HMR de WXT, modifier ce composant rafraîchit le popup sans recharger l'extension manuellement dans chrome://extensions. C'est ce détail, plus que tout, qui m'a fait gagner des heures.
Les pièges rencontrés
Tout n'a pas été lisse. Trois écueils m'ont coûté du temps :
1. Le service worker meurt. En MV3, le background n'est plus persistant. Il se réveille sur événement et se rendort. Toute donnée en mémoire est perdue. J'ai dû déplacer l'état dans browser.storage.local au lieu de variables globales.
2. L'injection asynchrone du content script. Si l'utilisateur clique sur le popup avant que LinkedIn ait fini de charger sa SPA, sendMessage échoue. J'ai ajouté un retry avec un court délai, et une vérification que le content script répond.
async function sendWithRetry(tabId: number, msg: unknown, tries = 3) {
for (let i = 0; i < tries; i++) {
try {
return await browser.tabs.sendMessage(tabId, msg);
} catch {
await new Promise((r) => setTimeout(r, 300));
}
}
throw new Error("Content script injoignable");
}3. Les permissions de host. LinkedIn change régulièrement son markup. Mon parsing reposait sur des sélecteurs CSS fragiles. J'ai fini par cibler des attributs plus stables (data-*, balises sémantiques) plutôt que des classes générées.
Le bilan
WXT a transformé une corvée que je redoutais en un projet fluide. La génération du manifest, le HMR et le typage des APIs navigateur enlèvent toute la friction qui m'avait dégoûté des extensions par le passé.
Pour RecruitEasy, le résultat est concret : un recruteur capture un profil en un clic, et le candidat atterrit directement dans son pipeline. Le copier-coller manuel a disparu.
Si vous hésitez à vous lancer dans une extension Chrome, ne commencez pas par le boilerplate brut. Commencez par WXT.
Pour aller plus loin

Écrit par
Déto Jean-Luc GouahoDéveloppeur full-stack basé au Canada. J'écris sur le code, l'IA et les produits que je construis.
Autres articles

L'IA code mieux que moi, et pourquoi ça m'arrange
Mon avis (assumé) sur l'IA dans le dev : ce n'est ni un messie ni le grand remplaçant, c'est un outil. Une évolution qu'on n'a pas le choix d'adopter, et qui nous pousse vers un rôle d'architecte. Parce que oui, l'IA code bien, encore faut-il l'empêcher de partir en cacahuète.

Intégrer Hermes Agent dans mon workflow : pourquoi je le préfère à OpenClaw
J'ai testé plusieurs agents IA pour automatiser des tâches dans mes projets. Après avoir intégré Hermes Agent puis comparé à OpenClaw, mon choix est fait. Retour d'expérience honnête sur l'intégration, le contrôle, la transparence et le coût.

L'IA dans mes projets : ce que j'ai appris en intégrant des LLM en production
De l'ATS chez Royal Broker à FitTrack et RecruitEasy, j'ai intégré des LLM dans plusieurs produits réels. SDK OpenAI, clés API, quotas et rate limits, choix du modèle selon l'usage, inférence vs pertinence, OpenRouter : un retour concret pour intégrer l'IA sans transformer une démo magique en gouffre de coûts.