Skip to main content
mvp development11 mars 202618 min de lecture

Construire un SaaS d'analyse SEO avec Nuxt et les LLMs

Comment j'ai construit HeySeo - une plateforme d'analyse SEO propulsée par l'IA qui permet aux utilisateurs de discuter avec leurs données Google Search Console. Architecture, intégration LLM et retours d'expérience.

Loic Bachellerie

Senior Product Engineer

Introduction

La plupart des fondateurs traitent le SEO comme une boîte noire. Ils scrutent les tableaux de bord de Google Search Console, remplis d'impressions et de clics, et peinent à répondre à la seule question qui compte vraiment : que dois-je faire ensuite ?

J'ai vécu ce problème pendant des années en construisant des sites clients et mes propres produits SaaS. Les données étaient là - des milliers de lignes de performances par mot-clé, de classements de pages, de tendances de CTR - mais en extraire des insights actionnables nécessitait soit une expertise SEO approfondie, soit des heures de manipulation de tableurs. J'ai décidé de combler ce fossé en construisant HeySeo : une plateforme d'analyse SEO propulsée par l'IA qui vous permet de converser avec vos données de recherche.

Voici le récit complet de la construction. Décisions d'architecture, ingénierie de prompts LLM, intégration des API Google, conception du serveur MCP, stratégie tarifaire, et ce que j'ai appris en mettant le produit en production. Je partagerai le vrai code et les vraies erreurs.

Le problème : les données SEO sont difficiles à exploiter

Google Search Console est gratuit, complet, et presque totalement impraticable pour la plupart des propriétaires de sites web.

L'interface vous donne :

  • Un tableau plat de requêtes classées par volume d'impressions
  • Des performances par page ventilées par appareil
  • Des rapports de couverture remplis de codes d'erreur obscurs
  • Des données Core Web Vitals et PageSpeed enfouies derrière plusieurs clics

Ce qu'elle ne vous donne pas, ce sont des réponses. Il faut encore avoir suffisamment de connaissances en SEO pour regarder un CTR de 0,8 % sur un mot-clé en position 4 et raisonner : "Ce title tag sous-performe pour cette intention de recherche. Il faut le réécrire."

Les outils SEO professionnels, Ahrefs, Semrush, Moz, résolvent une partie du problème en ajoutant l'analyse des backlinks, la recherche concurrentielle et les scores de difficulté des mots-clés. Mais ils coûtent entre 99 et 500 $ par mois, ont des courbes d'apprentissage abruptes, et nécessitent toujours une interprétation manuelle importante.

Il existe un fossé entre "données brutes" et "prochaine action claire" qu'aucun outil ne comblait correctement pour les développeurs indépendants, les petites équipes marketing et les créateurs de contenu. C'est ce fossé que HeySeo a été conçu pour combler.

L'idée : discuter avec ses données

L'insight déclencheur était simple. Les LLMs sont véritablement performants pour raisonner sur des données structurées quand on leur fournit le bon contexte. Et si au lieu de construire un énième tableau de bord SEO, je laissais les utilisateurs simplement poser des questions ?

  • "Quelles pages ont le plus perdu en classement ces 30 derniers jours ?"
  • "Sur quels mots-clés suis-je classé entre la 6e et la 15e position et que je devrais prioriser ?"
  • "Pourquoi le CTR de ma page d'accueil est-il si bas comparé à mes articles de blog ?"
  • "Donne-moi une liste de tâches SEO à traiter cette semaine, triées par impact attendu."

Cette dernière est le cas d'usage décisif. Pas seulement de l'analyse, mais des recommandations priorisées. Le LLM peut raisonner sur les bonnes pratiques SEO, examiner les données, et produire une liste de tâches concrètes. Aucune expertise requise du côté de l'utilisateur.

À partir de cette idée centrale, l'ensemble des fonctionnalités s'est étoffé :

  • Interface de chat en langage naturel sur les données Search Console
  • Rapports SEO hebdomadaires automatisés envoyés par email ou Slack
  • Surveillance PageSpeed avec suivi des tendances et commentaires générés par LLM
  • Gestion de l'indexation - vérifier quelles pages Google a indexées, soumettre des URLs pour le crawl
  • Tableau Kanban pour les tâches SEO, pré-rempli par l'analyse IA
  • Serveur MCP pour connecter HeySeo à Claude, Cursor et Windsurf

Vue d'ensemble de l'architecture

Architecture système de HeySeo

Flux de requête du navigateur au LLM et retour

FRONTEND
Nuxt 3 (Vue 3 + Vite)
Tailwind CSS + shadcn/vue
Pinia state management
Chat UI + Kanban board
COUCHE API
Nuxt server routes (H3)
Supabase (Postgres + Auth)
Redis (cache données GSC)
BullMQ (tâches en arrière-plan)
INTÉGRATIONS
Google Search Console API
Google Indexing API
PageSpeed Insights API
Anthropic Claude API
Slack webhooks
Serveur MCP - expose tous les outils à Claude / Cursor / Windsurf

J'ai choisi Nuxt 3 comme framework full-stack parce que je le connais bien, qu'il gère le SSR et les routes API dans un seul codebase de manière propre, et que la couche serveur Nitro (H3) est véritablement rapide. Pas besoin d'un backend Express séparé.

La base de données est Supabase. Les données Google Search Console sont mises en cache de manière agressive dans Redis parce que l'API GSC est limitée en débit et relativement lente - on ne veut pas que la latence de réponse du LLM inclue un appel GSC à froid à chaque message.

Construction de l'interface de chat

L'interface de chat est la première chose que j'ai construite parce que c'était le différenciateur principal. Trouver le bon modèle d'interaction dès le départ a façonné tout le reste.

Les décisions de conception clés :

Réponses en streaming. Attendre 5 à 10 secondes pour une réponse complète du LLM tue la sensation de conversation. Je diffuse les tokens depuis la route API directement vers le navigateur en utilisant les Server-Sent Events.

// server/api/chat.post.ts
import { streamText } from 'ai'
import { anthropic } from '@ai-sdk/anthropic'
 
export default defineEventHandler(async (event) => {
  const { messages, siteId } = await readBody(event)
  const user = await requireAuth(event)
 
  const context = await buildSeoContext(siteId, user.id)
 
  const result = streamText({
    model: anthropic('claude-3-5-sonnet-20241022'),
    system: buildSystemPrompt(context),
    messages,
    maxTokens: 2048,
  })
 
  return result.toDataStreamResponse()
})

Construction composable du contexte. Avant chaque message de chat, je construis un objet de contexte SEO structuré contenant les données récentes du site. Celui-ci est sérialisé dans le prompt système. Je détaillerai l'ingénierie de prompts plus bas.

Historique des messages. Je persiste la conversation dans Supabase pour que les utilisateurs puissent revenir à un fil de discussion et continuer. Chaque site a son propre historique de conversation indexé par site_id + user_id.

Côté Vue, le composant de chat utilise le composable useChat du Vercel AI SDK :

<!-- components/SeoChat.vue -->
<script setup lang="ts">
import { useChat } from '@ai-sdk/vue'
 
const props = defineProps<{ siteId: string }>()
 
const { messages, input, handleSubmit, isLoading } = useChat({
  api: '/api/chat',
  body: { siteId: props.siteId },
  initialMessages: await loadConversationHistory(props.siteId),
})
</script>
 
<template>
  <div class="flex h-full flex-col">
    <ChatMessageList :messages="messages" :is-loading="isLoading" />
    <ChatInput
      v-model="input"
      :disabled="isLoading"
      @submit="handleSubmit"
    />
  </div>
</template>

Le composant ChatMessageList rend le markdown de l'assistant - c'est important car le LLM formate naturellement les recommandations SEO sous forme de listes à puces, de tableaux et de titres. J'ai utilisé @nuxtjs/mdc pour le rendu markdown à l'intérieur du flux.

Connexion à Google Search Console

OAuth 2.0 avec Google est simple en théorie et pénible en pratique. Le point de douleur spécifique avec GSC est que les tokens d'accès expirent au bout d'une heure et qu'il faut gérer correctement les tokens de rafraîchissement à travers les sessions longues.

L'API Google Search Console renvoie les données sous forme de lignes de séries temporelles. Pour un site et une plage de dates donnés, on peut interroger par :

  • query (le terme de recherche)
  • page (l'URL qui s'est classée)
  • country
  • device

Chaque ligne renvoie clicks, impressions, ctr et position. C'est la matière première.

// server/utils/gsc.ts
import { google } from 'googleapis'
 
export async function fetchSearchAnalytics(
  accessToken: string,
  siteUrl: string,
  dateRange: { startDate: string; endDate: string },
  dimensions: string[],
): Promise<SearchAnalyticsRow[]> {
  const auth = new google.auth.OAuth2()
  auth.setCredentials({ access_token: accessToken })
 
  const searchconsole = google.searchconsole({ version: 'v1', auth })
 
  const response = await searchconsole.searchanalytics.query({
    siteUrl,
    requestBody: {
      startDate: dateRange.startDate,
      endDate: dateRange.endDate,
      dimensions,
      rowLimit: 1000,
    },
  })
 
  return response.data.rows ?? []
}

La stratégie de mise en cache est cruciale ici. Les données GSC pour les dates passées ne changent jamais, donc je mets en cache au niveau de la date avec un TTL de 24 heures pour le jour en cours et indéfiniment pour tous les jours précédents. Cela signifie que la plupart des messages de chat résolvent le contexte depuis Redis en moins de 50 ms plutôt que de faire des appels API en direct.

// server/utils/gsc-cache.ts
export async function getCachedAnalytics(
  siteId: string,
  dateRange: DateRange,
): Promise<SearchAnalyticsRow[]> {
  const cacheKey = `gsc:${siteId}:${dateRange.startDate}:${dateRange.endDate}`
  const cached = await redis.get(cacheKey)
 
  if (cached) return JSON.parse(cached)
 
  const rows = await fetchFromGSC(siteId, dateRange)
  const ttl = isCurrentDay(dateRange.endDate) ? 3600 : 0
  await redis.set(cacheKey, JSON.stringify(rows), ttl > 0 ? { ex: ttl } : undefined)
 
  return rows
}

Un piège à connaître : l'API GSC renvoie les données avec un décalage de 3 à 4 jours. Je l'ai appris à mes dépens quand des utilisateurs demandaient "qu'est-il arrivé à mon trafic hier" et que le LLM répondait avec assurance que rien n'avait changé - parce que les données n'étaient tout simplement pas encore là. J'inclus désormais une note sur ce décalage dans le prompt système et je l'affiche dans l'interface.

Ingénierie de prompts LLM pour les données SEO

C'est là que la majeure partie des itérations a eu lieu. La qualité des réponses de HeySeo dépend entièrement du fait de fournir au LLM le bon contexte dans le bon format.

L'objet de contexte. Avant chaque tour de conversation, je construis un instantané de contexte contenant :

interface SeoContext {
  site: {
    url: string
    verified: boolean
    addedAt: string
  }
  summary: {
    last28Days: PerformanceSummary
    previous28Days: PerformanceSummary
    deltaClicksPct: number
    deltaImpressionsPct: number
  }
  topQueries: SearchAnalyticsRow[]        // top 50 par clics
  decliningQueries: SearchAnalyticsRow[]  // plus fortes baisses de clics en glissement annuel
  topPages: SearchAnalyticsRow[]          // top 20 pages par clics
  quickWins: SearchAnalyticsRow[]         // pos 4-15, CTR sous la moyenne
  pagespeed: PagespeedSummary | null
  pendingTasks: SeoTask[]
  recentInsights: Insight[]
}

Je limite délibérément les données transmises au LLM. Envoyer les 1000 lignes GSC dépasserait les limites de contexte et ajouterait du bruit. L'étape de pré-traitement - calculer les deltas, faire remonter les quick wins, signaler les baisses - fait le gros du travail pour que le LLM puisse se concentrer sur le raisonnement et les recommandations.

Le prompt système. J'ai passé des semaines à itérer dessus. La version finale comporte quatre sections :

  1. Rôle et cadrage - ce qu'est HeySeo, quel est le rôle de l'assistant
  2. Base de connaissances SEO - principes fondamentaux que le LLM doit appliquer (benchmarks de CTR par position, logique d'identification des quick wins, bonnes pratiques d'indexation)
  3. Contexte des données actuelles - l'objet SeoContext sérialisé
  4. Directives de réponse - comment formater les réponses, quand suggérer des tâches, le ton
function buildSystemPrompt(context: SeoContext): string {
  return `You are an expert SEO analyst assistant for HeySeo. Your job is to help the user understand their search performance and take concrete actions to improve it.
 
## SEO Principles You Apply
 
- Position 1-3: expected CTR 15-30%. Below 10% suggests a title/meta issue.
- Position 4-15: high-opportunity zone. These keywords rank but don't convert to clicks.
- Quick wins: keywords in positions 4-15 with above-average impressions and below-average CTR.
- A click decline with stable impressions usually means a CTR problem (title, meta description).
- A click decline with impression decline usually means a ranking drop (content, backlinks, technical).
 
## Current Site Data
 
Site: ${context.site.url}
Last 28 days: ${context.summary.last28Days.clicks} clicks, ${context.summary.last28Days.impressions} impressions
Change vs prior period: ${context.summary.deltaClicksPct > 0 ? '+' : ''}${context.summary.deltaClicksPct.toFixed(1)}% clicks
 
Top queries by clicks:
${formatQueryTable(context.topQueries)}
 
Quick win opportunities (pos 4-15, low CTR):
${formatQueryTable(context.quickWins)}
 
Pages with biggest click declines:
${formatPageTable(context.decliningQueries)}
 
## Response Guidelines
 
- Be specific. Reference actual keywords, URLs, and numbers from the data.
- When suggesting tasks, format them as a numbered list with estimated impact (High / Medium / Low).
- If the user asks something you cannot answer from the available data, say so clearly.
- Keep answers concise. Use markdown tables for data-heavy responses.
- Current data lag: GSC reports with a 3-4 day delay. Mention this when relevant.`
}

Le helper formatQueryTable convertit les lignes en tableaux markdown compacts que le LLM gère bien sans gaspiller de tokens sur du JSON verbeux.

Sortie structurée pour la génération de tâches. Quand l'utilisateur demande à l'assistant de générer une liste de tâches, je bascule vers un appel en sortie structurée qui renvoie des objets SeoTask[] typés, écrits directement dans le tableau Kanban :

import { generateObject } from 'ai'
import { z } from 'zod'
 
const SeoTaskSchema = z.object({
  tasks: z.array(z.object({
    title: z.string(),
    description: z.string(),
    category: z.enum(['content', 'technical', 'onpage', 'indexing']),
    impact: z.enum(['high', 'medium', 'low']),
    effort: z.enum(['high', 'medium', 'low']),
    targetUrl: z.string().optional(),
    targetKeyword: z.string().optional(),
  })),
})
 
export async function generateSeoTasks(
  context: SeoContext,
): Promise<SeoTask[]> {
  const { object } = await generateObject({
    model: anthropic('claude-3-5-sonnet-20241022'),
    schema: SeoTaskSchema,
    prompt: buildTaskGenerationPrompt(context),
  })
 
  return object.tasks.map((task) => ({
    ...task,
    id: crypto.randomUUID(),
    status: 'todo' as const,
    createdAt: new Date().toISOString(),
  }))
}

Le tableau Kanban pour les tâches SEO

L'une des fonctionnalités sur lesquelles les utilisateurs se sont immédiatement greffés est le tableau Kanban. L'interface de chat est idéale pour l'exploration et les questions-réponses, mais il faut un endroit pour capturer et suivre le travail réel.

Le tableau comporte quatre colonnes : À faire, En cours, Terminé et Écarté. Les tâches peuvent être générées par l'IA, ajoutées manuellement, ou créées depuis une conversation de chat (l'assistant peut dire "J'ai ajouté ceci à votre tableau de tâches" et le faire réellement via un appel d'outil).

<!-- components/SeoKanban.vue -->
<script setup lang="ts">
import { useSeoTasks } from '~/composables/useSeoTasks'
 
const { tasks, moveTask, updateTask, deleteTask } = useSeoTasks()
 
const columns = [
  { id: 'todo', label: 'To Do' },
  { id: 'in_progress', label: 'In Progress' },
  { id: 'done', label: 'Done' },
  { id: 'dismissed', label: 'Dismissed' },
] as const
 
function onDrop(taskId: string, targetStatus: SeoTask['status']) {
  moveTask(taskId, targetStatus)
}
</script>

La fonction moveTask dans le composable appelle une RPC Supabase qui met à jour le statut de la tâche et enregistre une entrée d'historique horodatée. Cet historique est renvoyé au LLM comme contexte pour qu'il puisse voir quelles tâches l'utilisateur a déjà terminées lors de la génération de futures recommandations.

Rapports automatisés et intégration Slack

Tous les utilisateurs ne veulent pas ouvrir l'application pour rester informés. Les rapports automatisés livrent un résumé hebdomadaire directement dans leur boîte mail ou leur canal Slack.

La génération de rapports s'exécute comme une tâche BullMQ chaque lundi matin :

// server/jobs/weekly-report.ts
export async function generateWeeklyReport(siteId: string): Promise<void> {
  const [context, user, reportSettings] = await Promise.all([
    buildSeoContext(siteId, { days: 28 }),
    getUserBySiteId(siteId),
    getReportSettings(siteId),
  ])
 
  const { text: reportMarkdown } = await generateText({
    model: anthropic('claude-3-5-sonnet-20241022'),
    system: REPORT_SYSTEM_PROMPT,
    prompt: buildReportPrompt(context),
  })
 
  const reportRecord = await saveReport(siteId, reportMarkdown)
 
  if (reportSettings.emailEnabled) {
    await sendReportEmail(user.email, reportMarkdown, reportRecord.id)
  }
 
  if (reportSettings.slackWebhookUrl) {
    await sendSlackReport(reportSettings.slackWebhookUrl, reportMarkdown)
  }
}

L'intégration Slack utilise des webhooks entrants, ce qui maintient la friction de configuration au minimum - les utilisateurs collent une URL de webhook dans la page de paramètres et ça fonctionne. J'ai envisagé de construire une véritable application Slack avec OAuth, mais pour le cas d'usage (livraison de rapports en sens unique) les webhooks sont plus simples et plus fiables.

Le prompt de rapport demande au LLM de structurer la sortie ainsi :

  1. Résumé des performances (évolution du trafic, principaux mouvements)
  2. Top 3 des succès de la dernière semaine
  3. Top 3 des préoccupations ou baisses
  4. Axes de concentration recommandés pour la semaine à venir

Court, engagé et actionnable. Les utilisateurs m'ont dit lors des premiers retours que les rapports d'outils qu'ils recevaient auparavant étaient trop longs et trop chargés en données. Ils voulaient un briefing de type directeur financier, pas un tableur.

Surveillance PageSpeed

PageSpeed est la métrique SEO que la plupart des propriétaires de sites ignorent jusqu'à ce que Google les pénalise. HeySeo interroge l'API PageSpeed Insights chaque semaine pour chaque URL enregistrée et stocke les données de séries temporelles dans Supabase.

Le tableau de bord de surveillance affiche les tendances des Core Web Vitals (LCP, CLS, FID/INP) avec un graphique sparkline et un badge de statut coloré. Quand un score passe en dessous d'un seuil, le système génère une explication par LLM de la cause probable et des corrections à apporter.

// server/utils/pagespeed.ts
export async function fetchPagespeedScore(
  url: string,
  strategy: 'mobile' | 'desktop' = 'mobile',
): Promise<PagespeedResult> {
  const apiUrl = new URL('https://www.googleapis.com/pagespeedonline/v5/runPagespeed')
  apiUrl.searchParams.set('url', url)
  apiUrl.searchParams.set('strategy', strategy)
  apiUrl.searchParams.set('key', process.env.GOOGLE_PAGESPEED_API_KEY!)
 
  const response = await $fetch<PagespeedApiResponse>(apiUrl.toString())
 
  return {
    url,
    strategy,
    score: Math.round((response.lighthouseResult.categories.performance.score ?? 0) * 100),
    lcp: response.lighthouseResult.audits['largest-contentful-paint'].numericValue,
    cls: response.lighthouseResult.audits['cumulative-layout-shift'].numericValue,
    fid: response.lighthouseResult.audits['total-blocking-time'].numericValue,
    fetchedAt: new Date().toISOString(),
  }
}

Un apprentissage : l'API PageSpeed a ses propres limites de débit et renvoie parfois des erreurs 500 pour des URLs parfaitement valides. J'ai ajouté un backoff exponentiel avec 3 tentatives et je passe silencieusement les échecs plutôt que de faire remonter des erreurs parasites aux utilisateurs.

Gestion de l'indexation

La section d'indexation intègre à la fois le rapport de couverture de Google Search Console et l'API Google Indexing.

Les utilisateurs peuvent voir quelles pages sont indexées, lesquelles ont des erreurs (404, chaînes de redirections, bloquées par robots.txt), et soumettre des URLs directement pour le recrawl. Pour la plupart des petits sites, le workflow de soumission manuelle dans GSC est lourd - il faut naviguer vers l'Inspection d'URL, coller l'URL, cliquer sur Demander l'indexation, et répéter pour chaque page.

Le gestionnaire d'indexation de HeySeo permet aux utilisateurs de coller ou d'importer une liste d'URLs, de voir leur statut d'indexation actuel en masse, et de mettre en file d'attente la soumission en un clic.

// server/api/indexing/submit.post.ts
export default defineEventHandler(async (event) => {
  const { urls, siteId } = await readBody<{ urls: string[]; siteId: string }>(event)
  const user = await requireAuth(event)
 
  const accessToken = await getGoogleAccessToken(user.id)
  const results = await Promise.allSettled(
    urls.map((url) => submitUrlForIndexing(accessToken, url)),
  )
 
  const succeeded = results
    .filter((r): r is PromiseFulfilledResult<IndexingResult> => r.status === 'fulfilled')
    .map((r) => r.value)
 
  const failed = results
    .filter((r): r is PromiseRejectedResult => r.status === 'rejected')
    .map((_, i) => urls[i])
 
  await recordIndexingSubmissions(siteId, succeeded)
 
  return { succeeded: succeeded.length, failed: failed.length, failedUrls: failed }
})

L'API Indexing a un quota de 200 soumissions d'URLs par jour et par propriété. J'affiche cette limite dans l'interface et je suis les soumissions par rapport à ce quota pour que les utilisateurs ne rencontrent pas d'erreurs API inattendues.

Intégration du serveur MCP

C'est la fonctionnalité qui m'enthousiasme le plus. Le Model Context Protocol (MCP) permet aux assistants de codage IA comme Claude Desktop, Cursor et Windsurf d'appeler des outils externes. J'ai construit un serveur MCP HeySeo qui expose l'ensemble des fonctionnalités de la plateforme sous forme d'un jeu d'outils.

Le résultat concret : les développeurs peuvent rester dans leur éditeur, demander à Claude "quels problèmes SEO mon site a-t-il aujourd'hui ?" et obtenir une réponse en direct tirée de Google Search Console - sans ouvrir de navigateur.

Le serveur MCP est un processus Node.js autonome qui encapsule l'API HeySeo :

// mcp/index.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { z } from 'zod'
 
const server = new McpServer({
  name: 'heyseo',
  version: '1.0.0',
})
 
server.tool(
  'get_search_performance',
  'Fetch search performance data for a site from Google Search Console',
  {
    siteUrl: z.string().describe('The site URL registered in HeySeo'),
    days: z.number().default(28).describe('Number of days to look back'),
  },
  async ({ siteUrl, days }) => {
    const data = await heyseoClient.getSearchPerformance(siteUrl, { days })
    return {
      content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
    }
  },
)
 
server.tool(
  'get_seo_recommendations',
  'Get AI-generated SEO recommendations for a site',
  { siteUrl: z.string() },
  async ({ siteUrl }) => {
    const recommendations = await heyseoClient.getRecommendations(siteUrl)
    return {
      content: [{ type: 'text', text: recommendations.markdown }],
    }
  },
)
 
server.tool(
  'create_seo_task',
  'Add a task to the HeySeo Kanban board',
  {
    siteUrl: z.string(),
    title: z.string(),
    description: z.string(),
    impact: z.enum(['high', 'medium', 'low']),
  },
  async (params) => {
    const task = await heyseoClient.createTask(params)
    return {
      content: [{ type: 'text', text: `Task created: ${task.id}` }],
    }
  },
)
 
const transport = new StdioServerTransport()
await server.connect(transport)

Les utilisateurs installent le serveur MCP via npx heyseo-mcp et l'ajoutent à leur configuration Claude/Cursor avec leur clé API. L'onboarding prend environ 2 minutes.

C'est la fonctionnalité ayant le plus fort impact sur la rétention que j'ai livrée. Les utilisateurs qui configurent l'intégration MCP ont un taux de churn 40 % inférieur à ceux qui n'utilisent que l'application web. La friction du "ouvrir le navigateur, naviguer vers l'outil, poser une question" est réelle, et la supprimer change la fréquence à laquelle les gens interagissent réellement avec leurs données SEO.

Stratégie tarifaire

J'ai passé plus de temps sur la tarification que sur n'importe quelle fonctionnalité technique individuelle. Voici ce sur quoi j'ai atterri après avoir testé plusieurs modèles.

Offre gratuite : Un site, 30 jours d'historique, 20 messages de chat par mois, résumé du rapport hebdomadaire uniquement. Suffisant pour démontrer la valeur sans offrir le produit.

Starter (19 $/mois) : Trois sites, 12 mois d'historique, chat illimité, rapports hebdomadaires automatisés, surveillance PageSpeed. C'est le palier pour les projets personnels et les petites entreprises.

Growth (49 $/mois) : Dix sites, historique complet, rapports automatisés à n'importe quelle fréquence, intégration Slack, accès au serveur MCP, gestion de l'indexation. C'est le palier cible pour les équipes marketing et les agences.

Agency (149 $/mois) : Sites illimités, rapports en marque blanche, sièges d'équipe, support prioritaire.

L'insight clé était que le serveur MCP et l'intégration Slack devaient être des fonctionnalités payantes. Ce sont les fonctionnalités avec le plus fort engagement et elles servent des utilisateurs qui tirent clairement une valeur professionnelle récurrente de l'outil. Les placer derrière un paywall crée aussi un moment de mise à niveau clair : quand un utilisateur utilise activement le produit dans son environnement de développement, il comprend concrètement la proposition de valeur.

Je propose également une réduction annuelle de 20 %, ce qui améliore la trésorerie et réduit le churn simultanément. Environ 35 % des utilisateurs payants choisissent la facturation annuelle.

Lancement et traction initiale

J'ai lancé HeySeo sur Product Hunt en janvier 2026 après environ 3 mois de développement. Les résultats ont dépassé les attentes pour un outil B2B de niche :

  • 847 votes positifs, #3 produit du jour
  • 312 inscriptions dans les 48 premières heures
  • 23 conversions payantes la première semaine
  • 1 100 $ de MRR à la fin du mois de lancement

Le lancement Product Hunt a généré la notoriété initiale, mais la croissance soutenue provient de deux sources :

Marketing de contenu. J'ai publié des guides détaillés sur l'utilisation de l'IA pour l'analyse SEO, l'intégration de Search Console avec des outils IA, et la compréhension des Core Web Vitals. Ceux-ci se positionnent sur des mots-clés SEO de longue traîne et génèrent des inscriptions de personnes recherchant activement le problème exact que HeySeo résout.

Visibilité dans l'écosystème MCP. Quand j'ai soumis le serveur MCP HeySeo au registre MCP de Claude.ai et à la page des plugins Cursor, la découverte organique a sensiblement augmenté. Les développeurs cherchant des outils MCP trouvent HeySeo sans aucune promotion payante.

Ce qui n'a pas fonctionné : la prospection à froid auprès des agences marketing. Le cycle d'achat est trop long et le décideur est rarement la personne qui évalue les outils techniques. J'ai mis de côté la prospection agences au profit du contenu entrant.

Leçons apprises

Les coûts LLM sont prévisibles avec le bon cache. J'étais inquiet des coûts API incontrôlés avant le lancement. En pratique, la combinaison du cache agressif des données GSC et des objets de contexte pré-calculés fait que l'appel LLM est la seule étape coûteuse, et il n'est déclenché que sur les messages réels des utilisateurs. Mon coût LLM actuel est d'environ 0,08 $ par utilisateur actif par mois à des niveaux d'utilisation typiques.

Les limites de débit des API Google nécessitent un code défensif dès le premier jour. Pas seulement le backoff exponentiel - il faut aussi des circuit breakers, un suivi des limites de débit par utilisateur, et une dégradation gracieuse quand l'API est indisponible. J'ai été trop optimiste sur la fiabilité de l'API au début et j'ai eu plusieurs incidents avant d'ajouter une résilience appropriée.

Le tableau Kanban m'a surpris. Je l'ai construit comme une fonctionnalité secondaire pour donner aux sorties du chat un endroit où atterrir. Il s'est avéré être la raison principale pour laquelle de nombreux utilisateurs restent abonnés - le tableau leur donne une liste de tâches persistante qui conserve son état entre les sessions et prend de la valeur au fil du temps.

Le streaming compte plus que la qualité brute des réponses. Les utilisateurs qui voient les tokens apparaître en moins de 500 ms perçoivent le produit comme rapide et réactif même quand la réponse complète prend 8 secondes. Les utilisateurs qui attendent une réponse complète perçoivent même des réponses de 3 secondes comme lentes. Investissez dans le streaming avant d'investir dans l'optimisation de la latence.

Construisez d'abord pour vos meilleurs utilisateurs. L'intégration MCP a nécessité un effort d'ingénierie conséquent et ne sert qu'une minorité d'utilisateurs. Mais ces utilisateurs sont des power users qui recommandent le produit avec enthousiasme, ont un faible taux de churn, et passent aux paliers supérieurs. Le retour sur investissement est bien meilleur que celui de fonctionnalités qui améliorent légèrement l'expérience médiane.

La suite

HeySeo est à 3 800 $ de MRR en mars 2026 et croît régulièrement. La feuille de route pour le prochain trimestre :

  • Suivi des concurrents (comparer vos classements avec ceux de concurrents identifiés)
  • Suggestions de meta title et description générées par IA avec support de tests A/B
  • Alertes automatiques quand les classements chutent significativement
  • Intégration GA4 pour corréler le trafic de recherche avec les données de conversion
  • Rapports en marque blanche pour le palier Agency

L'insight fondamental, selon lequel la plupart des propriétaires de sites web possèdent des données qu'ils ne savent pas exploiter, et que les LLMs peuvent combler ce fossé, se vérifie à chaque étape du produit. La plateforme gagne en valeur à mesure que Google ajoute des données à Search Console et que les LLMs s'améliorent dans le raisonnement sur les données structurées.

Si vous construisez quelque chose de similaire ou souhaitez discuter de l'architecture, je serai ravi d'échanger avec vous.


Envie de travailler ensemble sur un projet comme celui-ci ? Je construis des produits SaaS propulsés par l'IA et des outils orientés données pour les fondateurs et les équipes de développement. Si vous avez un problème qui mérite d'être résolu, parlons-en.

Share:

Recevez des perspectives d'ingénierie pratiques

Agents vocaux IA, workflows d'automatisation et livraison rapide. Pas de spam, désabonnement à tout moment.