Trouvez les entreprises locales dont le site est absent, cassé ou dépassé — et arrivez au rendez-vous avec le brief déjà rédigé.
Quand on vend des refontes de sites, le difficile n'est pas le pitch — c'est de trouver les vingt entreprises du secteur dont le site justifie vraiment l'appel. Concrètement : ouvrir des centaines d'onglets, plisser les yeux sur chaque site depuis un téléphone, et deviner.
Opportunity plisse les yeux à votre place. Il balaie un rayon, confronte la présence web de chaque entreprise à une liste fixe de défauts, les classe de 0 à 100, et rédige un brief Markdown lisible dans la voiture avant le rendez-vous.
Tout tourne sur votre machine. Pas de compte, pas de backend hébergé, pas de LLM. Le diagnostic vient d'heuristiques déterministes, de données publiques et d'un cache SQLite local qui vous appartient.
Important
Opportunity est centré sur la France. Le géocodage passe par l'API BAN de
l'État (api-adresse.data.gouv.fr) et l'enrichissement par
recherche-entreprises.api.gouv.fr — les deux ne couvrent que la France.
L'interface est en français. Tout le reste (score, analyse de site, Google
Places) est indépendant du pays : brancher un autre géocodeur est l'essentiel
du travail pour s'en servir ailleurs. Voir la feuille de
route.
- Balayer autour d'une ville ou d'une adresse précise, avec un rayon configurable.
- Couvrir plusieurs métiers d'un coup : plombiers, électriciens, menuisiers, restaurants, coiffeurs, garages — la liste tient dans un seul fichier éditable.
- Noter chaque entreprise sur l'opportunité commerciale : pas de site, site mort, pas de viewport mobile, bases SEO faibles, technologie obsolète, pas de formulaire de contact, balises Open Graph manquantes, pas de favicon, pages lourdes ou lentes.
- Afficher les prospects sur une carte avec des pastilles colorées par score et une liste triable synchronisée.
- Ouvrir un prospect sans quitter la carte, inspecter le détail du score, puis exporter un brief Markdown prêt pour la prise de contact.
- Mettre en cache géocodage, Places, récupérations de sites et enrichissement dans SQLite : une recherche relancée ne consomme jamais deux fois du quota.
- Respecter les refus de démarchage du type
pas de démarchage pour un siteavant de noter ou de briefer une entreprise.
npm install
cp .env.local.example .env.local
npm run devOuvrez http://localhost:3000 et cliquez sur Lancer le balayage.
.env.local.example positionne MOCK_EXTERNAL=1. Dans ce mode, l'application
lit fixtures/ et ne fait aucun appel sortant côté serveur —
vous obtenez une démo complète sur Tours, avec dix entreprises fictives, sans
clé Google et sans dépenser un centime. Tout ce qui suit à propos de Google
Places ne compte qu'une fois ce drapeau désactivé.
Seule exception, et elle est côté navigateur : les tuiles du fond de carte sont
chargées depuis CARTO. Elles ne passent pas par cachedFetch() et ne sont donc
pas coupées par le mode mock. C'est gratuit et sans clé, mais ce n'est pas hors
ligne.
Le tout est fait pour produire ce fichier. Un clic sur Brief Markdown le génère pour n'importe quel prospect :
Exemple de brief — Restaurant Ancien, 85/100 (généré depuis les fixtures de démo)
# Restaurant Ancien
**Score d'opportunité : 85/100** — Prioritaire
## Identité
- **Secteur** : Restaurant
- **Adresse** : 21 rue Nationale, 37000 Tours, France
- **Téléphone** : 01 00 00 00 00
- **Site web** : http://www.restaurant-ancien.fr/
- **Réputation Google** : 4.4/5 sur 213 avis
## Interlocuteur
_Non identifié._ Lancer l'enrichissement depuis la fiche, ou appeler en demandant « le responsable » à l'accueil.
## Diagnostic
| Critère | Points | Constat |
| --- | ---: | --- |
| Pas adapté au mobile | 14 | Aucune balise viewport : le site s'affiche en version bureau sur téléphone, illisible pour la majorité des visiteurs. |
| Pas de HTTPS | 12 | Connexion non chiffrée : les navigateurs affichent « Non sécurisé » et Google déclasse la page. |
| Technologie obsolète | 10 | Détecté : WordPress 4.9.8 (obsolète, non maintenu) ; jQuery 1.7.2 (version de 2013 ou antérieure) ; Mise en page en tableaux HTML ; Contenu Flash (plus lu par aucun navigateur). |
| Référencement incomplet | 7 | Éléments manquants : meta description, titre h1, sitemap.xml, robots.txt. |
| Site visiblement abandonné | 7 | Le pied de page affiche encore 2014 : 12 ans sans mise à jour visible. |
| Pas de formulaire de contact | 6 | Aucun moyen de laisser une demande depuis le site : les prospects du soir et du week-end sont perdus. |
| Pas de balises Open Graph | 4 | Partagé sur Facebook ou WhatsApp, le lien s'affiche sans titre ni image. |
| Pas de mesure d'audience | 4 | Aucun outil de statistiques : impossible de savoir ce que le site rapporte. |
| Pas de favicon | 3 | Onglet et favoris affichent une icône vide : détail visible, effet amateur. |
| Pas de réseaux sociaux | 3 | Aucun lien vers un réseau social depuis le site. |
| Visibilité Google | +5 | 213 avis Google : l'établissement est déjà cherché en ligne. |
| Forte notoriété locale | +3 | Plus de 50 avis : audience suffisante pour rentabiliser un site. |
| Bonne réputation | +4 | Note de 4.4/5 : la qualité est là, la vitrine ne suit pas. |
| Joignable directement | +3 | Téléphone public : prise de contact immédiate possible. |
**Total : 85/100**
## Recommandations
1. Refonte responsive en priorité absolue : sur ce type d'activité, la majorité des visites viennent du mobile. Montrer le site sur son propre téléphone pendant le rendez-vous.
2. Installer un certificat TLS (gratuit via Let's Encrypt) et forcer la redirection. Faire constater l'avertissement « Non sécurisé » du navigateur.
3. Reconstruire sur une base maintenue. Insister sur le risque : un CMS non mis à jour est la première porte d'entrée des piratages de sites vitrines.
4. Reprendre les balises de base (title, meta description, h1) et publier sitemap.xml + robots.txt.
5. Le site donne l'impression d'une entreprise à l'arrêt. Argument fort auprès d'un dirigeant qui, lui, sait que son activité tourne.
### Angle d'attaque
- L'établissement est déjà cherché en ligne : la demande existe, seule la vitrine manque.
- Audience suffisante pour rentabiliser rapidement une refonte.
- La qualité perçue est excellente — le site ne lui rend pas justice.Chaque défaut porte son propre argument commercial, parce que le chiffre seul ne survit pas au contact d'un dirigeant.
Une entreprise sans site, ou dont le site est injoignable, démarre haut — c'est la conversation de refonte la plus probable. Un site existant est noté à partir de ses défauts visibles et de signaux commerciaux.
Principaux défauts : pas de HTTPS ; pas de viewport mobile ; title, meta description, H1, sitemap ou robots manquants ; technologie obsolète ou constructeurs gratuits (pages Facebook, Wix, e-monsite) ; pas de formulaire de contact ; pas de balises Open Graph ni de favicon ; année de copyright figée ; pages lourdes ou lentes.
Les bonus d'attractivité tiennent compte de la réputation, du volume d'avis et de la disponibilité d'un téléphone — une entreprise que personne ne cherche est un moins bon prospect qu'une entreprise très demandée avec un mauvais site.
Tous les poids vivent dans lib/scoring.ts. Les défauts sont
plafonnés collectivement (MAX_DEFECTS) pour qu'aucun signal isolé ne domine un
score.
Rien en mode mock. Avec MOCK_EXTERNAL=0, Opportunity sollicite deux SKU Google
facturés, déterminés par ses masques de champs figés dans
lib/places/client.ts :
| Appel | SKU | Prix (0–100k/mois) | Gratuit chaque mois |
|---|---|---|---|
| Recherche par secteur | Text Search Pro | 32,00 $ / 1 000 | 5 000 |
| Détail d'un prospect | Place Details Enterprise | 20,00 $ / 1 000 | 1 000 |
Les détails tombent en Enterprise parce que le masque demande rating,
userRatingCount et regularOpeningHours — les signaux dont le score a besoin.
La recherche reste délibérément en Pro : aucun champ Enterprise n'est jamais
autorisé dans le masque de recherche.
Un balayage qui retient 100 prospects coûte environ 2,50 $ — une centaine d'appels Details plus une douzaine d'appels Text Search. L'enveloppe gratuite mensuelle couvre environ 1 000 prospects avant la moindre facturation.
Trois garde-fous maintiennent ce niveau :
- des valeurs
X-Goog-FieldMaskstrictes — les modifier modifie votre facture ; - un plafond quotidien local via
PLACES_DAILY_CAP(300 par défaut) ; - le cache SQLite, qui rend gratuite une recherche relancée.
Les prix sont les tarifs publics de Google en dollars, vérifiés le 7 août 2026 sur la page de tarification Google Maps Platform ; les comptes européens sont facturés en euros au taux de Google. Vérifiez avant de vous y fier.
Opportunity regarde des entreprises, pas des personnes, et uniquement ce que ces entreprises publient elles-mêmes. Aucune donnée personnelle dans le pipeline, aucune collecte d'e-mails, aucun scraping au-delà de pages publiques qu'un navigateur récupérerait de toute façon.
Deux règles sont structurantes :
- Le refus de démarchage est respecté. Une fiche qui signale ne pas vouloir
être démarchée à propos d'un site — dans son nom ou sur son site — est exclue
du score et de la génération de brief (
lib/opt-out.ts). C'est visible dans la démo : deux des douze entreprises sont barrées. - Un brief est fait pour être lu par un humain. La sortie est un document que vous relisez avant de décider de contacter quelqu'un. En faire une machine à e-mailing automatique est un hors périmètre explicite.
Nécessaire uniquement pour passer à MOCK_EXTERNAL=0.
- Créez un projet Google Cloud et rattachez-y une facturation.
- Activez Places API (New). N'activez pas l'ancienne Places API.
- Créez une clé d'API restreinte à Places API (New).
- Ajoutez-la dans
.env.local:
GOOGLE_PLACES_API_KEY=votre_cle_google_places
MOCK_EXTERNAL=0
PLACES_DAILY_CAP=300Validez la clé avec un seul appel avant de lancer un vrai balayage :
npm run places:smoke -- "plombier à Tours"Données OpenStreetMap, rendues par CARTO Voyager — gratuit, sans compte, sans clé d'API. Aucune configuration.
Le fond est volontairement en palette sourde : les pastilles de score sont rouges et orangées, et un fond coloré entre en concurrence chromatique avec elles dès qu'un balayage est dense. Voyager conserve malgré tout des routes contrastées, ce qui permet de suivre une rue sous un amas de pins — c'est ce qui l'a fait préférer à Positron, plus épuré mais trop délavé pour ça.
Un fond vectoriel donnerait un contrôle complet du style et un zoom continu.
Le travail est commencé sur la branche carte/fond-vectoriel mais ne rend
pas : MapLibre s'initialise sans erreur puis ne demande jamais ses tuiles. Le
diagnostic est consigné dans le message de commit de cette branche.
| Commande | Rôle |
|---|---|
npm run dev |
Démarrer l'application en local |
npm run build |
Construire pour la production |
npm start |
Démarrer le build de production |
npm test |
Tests unitaires, sans accès réseau |
npm run typecheck |
Vérifications TypeScript |
npm run lint |
ESLint |
npm run db:check |
Vérifier le schéma SQLite, le WAL, le cache et les TTL |
npm run places:smoke |
Un vrai appel Places search + details |
La base vit dans data/opportunity.db et est ignorée par git. Supprimez-la pour
remettre à zéro recherches, réponses en cache et compteurs journaliers.
Définissez OPPORTUNITY_DB_PATH pour la placer ailleurs.
app/ routes Next.js et handlers d'API
components/ UI : recherche, carte, liste, barre d'outils, fiches prospect
config/ secteurs éditables et réglages de recherche par défaut
fixtures/ données simulées pour les démos et les tests
lib/ logique métier, cache, analyseurs, score, enrichissement
scripts/ vérifications smoke et validation de la base
tests/ couverture node:test du score, de l'analyseur, de la mémoire, des filtres
Les frontières qui gardent le projet peu coûteux et testable :
lib/n'importe jamais React.- Les routes d'API valident leur entrée et délèguent à
lib/. - Les composants ne parlent qu'aux routes d'API locales.
cachedFetch()danslib/cache.tsest le seul endroit qui effectue une requête externe — c'est ce qui fait deMOCK_EXTERNAL=1un mode hors-ligne complet.
| Source | Rôle | TTL du cache |
|---|---|---|
api-adresse.data.gouv.fr |
Géocodage français (BAN) | 365 jours |
| Google Places Text Search | Entreprises locales par secteur | 7 jours |
| Google Places Details | Site web, téléphone, note, horaires | 30 jours |
| Sites des prospects | Signaux techniques et de contenu | 7 jours |
recherche-entreprises.api.gouv.fr |
Enrichissement entreprise (SIREN, dirigeants) | 90 jours |
basemaps.cartocdn.com |
Tuiles du fond de carte | — (chargé par le navigateur) |
Pas des promesses — les directions qui amélioreraient le plus l'outil, dans un ordre approximatif :
- Un géocodeur enfichable, pour que l'application fonctionne hors de France. C'est de loin la plus grosse limite aujourd'hui.
- Plus de signaux de score. C'est la façon la plus simple de contribuer — voir le gabarit d'issue dédié.
- Exporter un balayage entier d'un coup, plutôt qu'un brief à la fois.
- Suivre l'état de la prise de contact par prospect, d'un balayage à l'autre.
Énoncé pour que personne ne gaspille une pull request dessus :
- Pas de SaaS, pas de version hébergée, pas de comptes. Ça tourne sur votre machine, sur vos données, avec votre clé d'API.
- Pas de LLM. Le diagnostic est déterministe et reproductible : le même site obtient deux fois le même score.
- Pas de CRM. L'outil trouve des prospects ; il n'est pas là où vous les gérez.
- Pas de prospection automatisée. Ni e-mailing de masse, ni appels automatiques, ni séquences.
- Pas de scraping au-delà des pages publiques. Pas d'authentification, pas de contenu payant, pas de données personnelles.
npm run lint
npm run typecheck
npm test
npm run db:checkLe CI exécute ces quatre commandes plus npm run build sur chaque pull request.
npm run places:smoke en est volontairement exclu : il exige une vraie clé et
consomme du quota.
Rapports de bugs, signaux de score et pull requests sont les bienvenus — voir CONTRIBUTING.md pour l'installation, les invariants d'architecture qu'un relecteur vérifiera, et les recettes pour ajouter un secteur ou un signal de score.
GNU AGPL-3.0. Vous pouvez l'utiliser, le modifier et le redistribuer librement ; si vous exécutez une version modifiée comme service en réseau, vous devez publier vos modifications.


