Changer le thème

Génération de pages par modèle : la voie technique du SEO programmatique

Easton editorial illustration: SEO-to-publishing automation line

La semaine dernière, un ami m’a parlé de SEO programmatique. Sa matrice de mots-clés était prête : plus de deux mille requêtes longue traîne dans un fichier Excel — mais il bloquait à l’étape suivante.

« Je comprends la logique, mais dès que je passe à l’implémentation, plein de questions surgissent. Next.js ou Astro ? Quelle base de données ? Comment structurer les URL ? Et si je génère cinq mille pages d’un coup, le serveur tiendra-t-il le coup ? »

J’étais dans le même cas il y a deux ans. Je montais un annuaire de services juridiques et visais trois mille pages ville en SEO programmatique. Résultat : deux mois pour mettre en ligne la première version, avec des pièges dont je me souviens encore.

Cet article comble ces trous. Trois voies techniques complètes : génération statique, rendu dynamique, approche hybride. Chaque voie avec pistes de code, cas d’usage et exemples réels. À la fin, vous pourrez vous lancer — sans repartir de zéro comme moi à l’époque.


D’abord : quelle voie vous convient ?

L’implémentation technique ne se choisit pas au hasard. Il faut d’abord regarder vos données.

La génération statique (SSG) est-elle faite pour vous ?

Si vos données changent peu — une fois par semaine, voire par mois — la génération statique est la plus sûre.

Exemple concret : j’ai aidé un ami sur un site de guides touristiques par ville. Le contenu (sites, transports, restauration) bouge à peine tous les six mois. On a utilisé les Content Collections d’Astro : deux mille villes en JSON, puis génération en masse de pages statiques. Build d’environ dix minutes ; une fois en ligne, TTFB autour de 80 ms et taux de hit CDN ~95 %.

Avantages : chargement rapide, SEO naturellement favorable, faible charge serveur. Limites : toute mise à jour impose un rebuild ; au-delà de cinq mille pages, le build devient long.

Quand passer au rendu dynamique (SSR) ?

Dès que les données évoluent en temps réel, le statique ne suffit plus.

Wise (transferts internationaux) en est l’illustration : les pages de conversion de devises voient le taux changer chaque minute. En statique pur, l’utilisateur verrait peut-être un taux vieux de dix minutes — inacceptable pour une décision de transfert. Ils utilisent le SSR Next.js et tirent le taux à jour à chaque requête.

Le prix : charge serveur. Wise enregistre des millions de requêtes de conversion par jour ; le coût n’est pas négligeable. Le TTFB est aussi plus lent, souvent 200-500 ms.

L’approche hybride comme compromis

Données à la fois peu et très fréquentes ? L’hybride peut être le bon choix.

Zapier fait ainsi pour ses pages d’intégration « App A + App B » (ex. Slack et Gmail). Les infos de base (description, étapes) sont statiques ; l’état réel de l’intégration (connecté ou non, dernière synchro) est dynamique.

Zapier s’appuie sur l’ISR (Incremental Static Regeneration) de Next.js : première vue en statique, mécanisme en arrière-plan pour rafraîchir. Contenu à la fois rapide et à jour.

Conseil : répondez à ces trois questions avant de choisir.

  1. À quelle fréquence les données changent-elles ? (quotidien ? horaire ? temps réel ?)
  2. Quelle échelle de pages ? (moins de cinq mille ? cinq à vingt mille ? plus ?)
  3. Quelle exigence SEO ? (TTFB < 100 ms obligatoire ? 300 ms acceptable ?)

Une fois ces points clairs, le choix technique l’est aussi.

<100ms
TTFB génération statique
Hit cache CDN
200-500ms
TTFB rendu dynamique
Génération côté serveur
Compromis
Schéma ISR
Équilibre perf / temps réel
Source: Comparaison d’indicateurs techniques

Génération statique : pistes Astro

Si vous partez sur le statique, je recommande Astro : pensé pour les sites statiques, Content Collections idéal pour le SEO programmatique.

Structure des données

Définissez d’abord le schéma. Imaginons un annuaire d’avocats, une page par ville :

// src/content/config.ts
import { defineCollection, z } from 'astro:content';

const lawyersCollection = defineCollection({
  type: 'content',
  schema: z.object({
    city: z.string(),
    citySlug: z.string(),
    province: z.string(),
    lawyerCount: z.number(),
    topFirms: z.array(z.string()),
    avgPrice: z.string(),
    specialties: z.array(z.string()),
  }),
});

export const collections = {
  'lawyers': lawyersCollection,
};

Puis un fichier JSON par ville dans src/content/lawyers/ :

// src/content/lawyers/beijing.json
{
  "city": "北京",
  "citySlug": "beijing",
  "province": "北京市",
  "lawyerCount": 12500,
  "topFirms": ["金杜律师事务所", "中伦律师事务所", "大成律师事务所"],
  "avgPrice": "2000-5000元/小时",
  "specialties": ["刑事", "民事", "商事", "知识产权"]
}

Deux mille villes, deux mille JSON. En pratique, un script Python ou Node exporte la base et écrit les fichiers en lot.

Modèle de route dynamique

Données prêtes, place au modèle. Le routage dynamique d’Astro est souple :

// src/pages/[citySlug].astro
---
import { getCollection } from 'astro:content';

export async function getStaticPaths() {
  const lawyers = await getCollection('lawyers');
  return lawyers.map(lawyer => ({
    params: { citySlug: lawyer.data.citySlug },
    props: { lawyer },
  }));
}

const { lawyer } = Astro.props;
---

<!DOCTYPE html>
<html>
<head>
  <title>{lawyer.data.city}律师服务指南 | 律师事务所推荐与收费标准</title>
  <meta name="description" content={`${lawyer.data.city}律师服务完整指南,涵盖${lawyer.data.specialties.join('、')}等领域,推荐${lawyer.data.topFirms.join('、')}等顶级律所,平均收费标准${lawyer.data.avgPrice}`} />
</head>
<body>
  <h1>{lawyer.data.city}律师服务指南</h1>

  <section>
    <h2>核心数据</h2>
    <p>律师数量:{lawyer.data.lawyerCount}名</p>
    <p>主要领域:{lawyer.data.specialties.join('、')}</p>
    <p>平均收费:{lawyer.data.avgPrice}</p>
  </section>

  <section>
    <h2>推荐律所</h2>
    <ul>
      {lawyer.data.topFirms.map(firm => <li>{firm}</li>)}
    </ul>
  </section>

  <!-- 内链:相关城市 -->
  <section>
    <h2>周边城市律师服务</h2>
    <!-- 这里可以根据省份或地理位置推荐 -->
  </section>
</body>
</html>

getStaticPaths génère toutes les pages ville : deux mille villes, deux mille HTML.

Build et déploiement

npm run build

Le dossier dist/ contient les HTML statiques. Déployez sur Cloudflare Pages ou Vercel : le CDN met en cache automatiquement.

Sur le site de guides touristiques cité plus haut, ~dix minutes de build pour deux mille pages. Astro est nettement plus rapide que le SSG Next.js sur ce type de volume.

Astuce : au-delà de cinq mille pages, envisagez des builds par lots. Astro permet l’incrémental : ne regénérer que les pages liées aux nouvelles données.


Rendu dynamique : SSR Next.js

Données en temps réel : passez au SSR Next.js.

Configuration de base

Le cœur du SSR, c’est getServerSideProps :

// pages/currency/[pair].tsx
import { GetServerSideProps } from 'next';

export const getServerSideProps: GetServerSideProps = async (context) => {
  const { pair } = context.params;
  const [from, to] = pair.split('-to-');

  // 实时获取汇率
  const exchangeRate = await fetchExchangeRate(from, to);

  return {
    props: {
      from,
      to,
      rate: exchangeRate.rate,
      lastUpdate: exchangeRate.timestamp,
    },
  };
};

export default function CurrencyPage({ from, to, rate, lastUpdate }) {
  return (
    <div>
      <h1>{from} to {to} 货币转换</h1>
      <p>当前汇率:{rate}</p>
      <p>更新时间:{new Date(lastUpdate).toLocaleString()}</p>

      {/* 转换计算器 */}
      <input type="number" placeholder="输入金额" />
      <button>转换</button>

      {/* 历史走势图 */}
      <div>过去30天汇率走势</div>
    </div>
  );
}

Chaque visite sur /currency/usd-to-eur interroge l’API de taux : la page reste à jour.

Cache : ne pas épuiser le serveur

Le risque du dynamique, c’est la charge. Des millions de requêtes/jour avec un appel API à chaque fois, le coût explose.

La stale-while-revalidate aide : servir d’abord le cache (éventuellement vieux de quelques minutes), mettre à jour en arrière-plan, servir la version fraîche à la requête suivante.

export const getServerSideProps: GetServerSideProps = async (context) => {
  const { pair } = context.params;

  // 检查缓存
  const cached = await checkCache(pair);

  if (cached && !isExpired(cached)) {
    return { props: cached.data };
  }

  // 缓存过期,后台更新
  fetchExchangeRate(pair).then(data => updateCache(pair, data));

  // 先返回旧数据
  return { props: cached?.data || await fetchExchangeRate(pair) };
};

L’utilisateur voit vite du contenu ; le serveur n’appelle pas l’API externe à chaque fois.

Données structurées injectées dynamiquement

En SSR, le JSON-LD aussi doit être dynamique :

// 在页面组件中生成 JSON-LD
const jsonLd = {
  "@context": "https://schema.org",
  "@type": "FinancialService",
  "name": `${from} to ${to} Currency Conversion`,
  "offers": {
    "@type": "Offer",
    "price": rate,
    "priceCurrency": to,
  },
};

// 在 HTML 中注入
<script type="application/ld+json">
  {JSON.stringify(jsonLd)}
</script>

Chaque page a des données structurées exactes ; Google peut afficher le taux à jour dans les résultats.


Approche hybride : ISR Next.js

Scénario « en partie statique, en partie dynamique » : l’ISR est souvent le meilleur choix.

Logique ISR

Principe : page générée en statique au départ, avec une durée de validité. Après expiration, la prochaine visite déclenche une régénération en arrière-plan.

// pages/integrations/[app1]-and-[app2].tsx
export async function getStaticPaths() {
  const integrations = await fetchAllIntegrations();
  return integrations.map(int => ({
    params: { app1: int.app1, app2: int.app2 },
  }));
}

export async function getStaticProps({ params }) {
  const integration = await fetchIntegration(params.app1, params.app2);

  return {
    props: integration,
    revalidate: 3600, // 1小时后过期
  };
}

revalidate: 3600 : pendant une heure, les visites reçoivent le HTML statique. Après une heure, la première visite voit encore l’ancienne page pendant que le serveur régénère ; la suivante reçoit la nouvelle.

Mise à jour à la demande : on-demand revalidation

Parfois attendre l’expiration ne suffit pas. Une intégration Zapier tombe en panne : vous voulez mettre à jour tout de suite, pas une heure plus tard.

// API 路径:触发更新
// pages/api/revalidate.ts
export default async function handler(req, res) {
  const { app1, app2 } = req.query;

  try {
    await res.revalidate(`/integrations/${app1}-and-${app2}`);
    return res.json({ revalidated: true });
  } catch (err) {
    return res.status(500).send('Error revalidating');
  }
}

Un script de monitoring peut appeler cette API dès qu’un problème est détecté.

Limites de l’ISR

L’ISR n’est pas universel. Cours boursiers, flash sales : il faut du SSR pur.

L’ISR convient quand les données changent peu souvent (heure, jour) mais doivent se propager vite. Les pages d’intégration Zapier en sont l’exemple : le fond change rarement, mais une panne doit se refléter rapidement.


Structure d’URL : les fondations SEO

Stack choisi, place aux URL — souvent sous-estimées, pourtant elles pèsent sur le classement.

Trois principes d’URL SEO-friendly

1. Inclure le mot-clé cible. L’URL compte pour Google ; un mot-clé naturel dans le chemin aide.

Pour « avocat divorce Pékin », /beijing/divorce-lawyer place « beijing » et « divorce-lawyer » dans le chemin.

2. Pas plus de trois niveaux. Trop profond, mauvais pour l’utilisateur et le crawler.

/service/legal/lawyer/divorce/beijing en six niveaux perd presque tout le monde — Google peut aussi le traiter comme page de faible qualité.

3. Tirets, pas de paramètres de requête.

/lawyer?type=divorce&city=beijing indexe moins bien que /beijing/divorce-lawyer et peut être vu comme URL dynamique peu efficace.

Trois modèles d’URL courants

Modèle 1 — mot-clé métier en premier

/lawyer/beijing/divorce

Sites orientés marque : « lawyer » en tête renforce la catégorie.

Modèle 2 — géographie en premier

/beijing/divorce-lawyer

Services locaux : la requête « Pékin avocat divorce » colle au chemin.

Modèle 3 — URL aplatie

/beijing-divorce-lawyer

Grands volumes de pages : un seul segment, simple à gérer.

Choisissez selon la façon dont les gens cherchent : « ville + service » → modèle 2 ; requêtes dispersées → modèle 3.

Automatiser les liens internes

Une force du SEO programmatique : le maillage interne automatique.

Annuaire juridique, trois mille pages ville : chaque page doit pointer vers des villes ou services proches.

Hiérarchie géographique : page Pékin → Hebei, Tianjin, etc.

Type de service : page divorce Pékin → pénal, civil sur la même ville.

Exemple de code :

---
// 在页面模板中
const { lawyer } = Astro.props;
const nearbyCities = await getNearbyCities(lawyer.data.province);
const relatedSpecialties = lawyer.data.specialties;
---

<section>
  <h2>周边城市律师</h2>
  {nearbyCities.map(city => (
    <a href={`/${city.slug}/${lawyer.data.specialties[0]}-lawyer`}>
      {city.name}{lawyer.data.specialties[0]}律师
    </a>
  ))}
</section>

<section>
  <h2>{lawyer.data.city}其他法律服务</h2>
  {relatedSpecialties.map(spec => (
    <a href={`/${lawyer.data.citySlug}/${spec}-lawyer`}>
      {lawyer.data.city}{spec}律师
    </a>
  ))}
</section>

Des dizaines de liens par page, graphe dense : crawl efficace, navigation fluide pour l’utilisateur.


Choix de base de données : ne pas s’y perdre

Structure définie, où stocker ? Moins dramatique qu’on le croit.

Quatre options

PostgreSQL : données structurées, requêtes complexes.

Champs stables et besoin du type « tous les avocats divorce à Pékin sous 3000 ¥/h » : PostgreSQL, ACID, transactions, recherche full-text.

MongoDB : schéma flexible, itération rapide.

Structure encore mouvante : MongoDB sans schéma rigide à l’avance. J’ai souvent commencé ainsi sur des projets en phase d’exploration.

Airtable / Google Sheets : petit volume, collaboration.

Quelques dizaines à centaines de lignes, équipe non technique : édition visuelle, coédition. Un ami tenait ~200 lignes dans Airtable avec peu de maintenance.

CSV / JSON : génération 100 % statique.

Données figées, moins de mille pages : fichiers plats, pas de serveur de BDD. C’est le modèle Content Collections d’Astro.

Conseil selon le volume

Moins de mille enregistrements : CSV/JSON ou Airtable.
De mille à dix mille : PostgreSQL ou MongoDB.
Plus de dix mille : PostgreSQL + cache Redis.

Ne bloquez pas des semaines sur le choix. Migrez plus tard si le schéma évolue — ce n’est pas une montagne.


Développement de modèles : la différenciation du contenu

Architecture en place, le vrai défi est le modèle. Beaucoup d’échecs viennent de pages trop identiques.

Piège mortel : ne changer que le mot-clé

Un site « ville + hôtel », vingt mille pages : seul le nom de ville change, le corps est identique. Pékin, Shanghai, Canton — même texte.

Sanction Google, trafic -70 %, huit mois pour récupérer.

Cause : aucune valeur unique. Qui cherche un hôtel à Pékin et lit la même chose qu’à Shanghai voit du contenu généré en masse sans intérêt.

Trois leviers de différenciation

1. Injection de données dynamiques

Chaque page doit porter des données propres. Sur l’annuaire : effectifs, cabinets, tarifs moyens différents par ville — tout sort de la BDD.

2. Intégration UGC

Le contenu utilisateur différencie le mieux. TripAdvisor : avis uniques par hôtel. Sur un annuaire, un bloc « avis clients » :

<section>
  <h2>用户评价</h2>
  {lawyer.data.reviews.map(review => (
    <div>
      <p>{review.content}</p>
      <span>评分:{review.rating}/5</span>
    </div>
  ))}
</section>

3. Extension assistée par IA

Sans données dynamiques ni UGC, l’IA peut enrichir des paragraphes descriptifs — avec relecture humaine, jamais comme cœur de page.

Test personnel : données cœur (effectifs, cabinets) en BDD ; paragraphe « spécificités du marché juridique local » généré puis relu. L’IA complète, elle ne remplace pas les faits uniques.

Automatisation des données structurées

JSON-LD par page, entièrement automatisable :

const jsonLd = {
  "@context": "https://schema.org",
  "@type": "LegalService",
  "name": `${lawyer.data.city}律师服务`,
  "areaServed": {
    "@type": "City",
    "name": lawyer.data.city,
  },
  "provider": lawyer.data.topFirms.map(firm => ({
    "@type": "Organization",
    "name": firm,
  })),
};

Résultats enrichis (ville, cabinets) → CTR souvent meilleur.


Performance : ne pas faire attendre l’utilisateur

Grand volume de pages : la perf compte.

Trois indicateurs clés

TTFB : vitesse de réponse serveur.

Statique souvent < 100 ms, dynamique 200-500 ms. Au-delà de 500 ms, l’utilisateur part avant d’avoir lu.

LCP : affichage du contenu principal.

Objectif < 2,5 s. Beaucoup d’images ou de composants lourds dégradent le LCP.

FID (ou INP selon la métrique actuelle) : réactivité à la première interaction.

Objectif < 100 ms. Trop de JavaScript = boutons qui ne répondent pas.

CDN pour le statique

Pages statiques : le cache CDN est décisif. Cloudflare Pages et Vercel l’intègrent.

// astro.config.mjs
export default defineConfig({
  output: 'static',
  build: {
    assets: 'assets/',
  },
  vite: {
    build: {
      rollupOptions: {
        output: {
          assetFileNames: 'assets/[hash][extname]',
        },
      },
    },
  },
});

Fichiers avec hash unique → cache CDN efficace.

Images

Hero en JPG brut = parfois plusieurs Mo. Astro optimise :

---
import { Image } from 'astro:assets';
import heroImage from '../images/lawyer-hero.jpg';
---

<Image src={heroImage} alt="律师服务" width={1200} height={675} />

Conversion WebP et compression : 2 Mo → ~200 Ko typiquement.

Budget de crawl

Au-delà de cinq mille pages, Google peut ne pas tout crawler.

Sitemap fragmenté :

// sitemap-index.xml
<sitemapindex>
  <sitemap><loc>https://example.com/sitemap-1.xml</loc></sitemap>
  <sitemap><loc>https://example.com/sitemap-2.xml</loc></sitemap>
  <sitemap><loc>https://example.com/sitemap-3.xml</loc></sitemap>
</sitemapindex>

~500 URL par fichier sitemap.

Priorité des liens internes : plus de liens depuis l’accueil et la nav vers les pages à fort volume de recherche ; moins vers les pages secondaires pour orienter le crawl.


Études de cas

TripAdvisor : hybride à grande échelle

Des millions de pages hôtel.

Architecture : statique + mises à jour dynamiques. Infos de base (nom, adresse, équipements) en statique ; avis et notes via API.

URL : /hotel/[city]/[hotel-name], ex. /hotel/beijing/grand-hyatt — trois niveaux.

Points clés : UGC en temps réel, comparaison de prix dynamique, schéma Review automatisé. Sans UGC, le classement massif par modèle seul est illusoire.

Zapier : ISR de référence

Cinq mille+ pages « intégration App A + App B ».

Architecture : ISR Next.js — fond statique, état de connexion dynamique.

URL : /integrations/[app1]/[app2], deux niveaux.

Points clés : revalidation à la demande, tests Playwright par page, hub d’apps + recommandations. Leur blog technique sur l’ISR à cette échelle vaut la lecture.

Wise : SSR + cache

Taux de change minute par minute → pas de pur statique.

Architecture : SSR + stale-while-revalidate.

URL : /currency/[from]-to-[to].

Points clés : cache intelligent, edge CDN, JSON-LD avec taux live. Bon modèle pour toute donnée quasi temps réel avec maîtrise des coûts.


Pièges à éviter

Piège 1 : URL incohérentes

Mon premier annuaire : /service?id=lawyer&city=beijing&type=divorce. Flexible en dev, mauvais pour le crawl et opaque pour l’utilisateur.

Leçon : figer la structure d’URL avant le lancement. Changer après = refonte des liens internes, externes et sitemaps.

Piège 2 : contenu dupliqué

Vingt mille pages, seule la ville change → sanction, -70 % de trafic.

Leçon : pas de page sans donnée unique. Mille pages solides valent mieux que dix mille pages vides.

Piège 3 : goulot de performance

Cinq mille pages en PHP dynamique pur : serveur qui tombe régulièrement chez moi au début.

Leçon : au-delà de ~cinq mille pages, statique ou ISR ; le SSR pur sur tout le catalogue tient mal.

Piège 4 : absence de monitoring

Six mois sans suivi : la moitié des pages non indexées à cause d’un sitemap mal configuré.

Leçon : première semaine — Google Search Console (indexation), Screaming Frog (technique), Ahrefs (positions).


Passer à l’action

Étape 1 : petit périmètre

Pas cinq mille pages d’emblée : cinquante villes, par exemple.

Étape 2 : stack

Données peu fréquentes → Astro. Temps réel → SSR Next.js.

Étape 3 : données et URL

Une demi-journée pour les champs et les chemins — ne pas zapper.

Étape 4 : modèle et test

Trois pages pilotes, contrôle manuel de la différenciation, puis batch.

Étape 5 : lancement progressif

Cinquante pages, une semaine d’observation (indexation, rebond, durée). Si c’est sain, montez à cinq cents.

Étape 6 : itération

Ajustez modèle, maillage, URL selon les métriques. Le SEO programmatique est un cycle continu, pas un one-shot.


FAQ

Comment choisir entre génération statique, rendu dynamique et approche hybride ?
Regardez la fréquence de mise à jour des données et l’échelle des pages. Données peu fréquentes (hebdo/mensuel) + moins de cinq mille pages : génération statique (Astro). Données en temps réel : rendu dynamique (SSR Next.js). Entre les deux : approche hybride ISR.
Quelle est la limite supérieure du nombre de pages en SEO programmatique ?
Pas de plafond rigide, mais pensez au budget de crawl. Sous cinq mille pages, Google peut en général tout crawler. De cinq à vingt mille : sitemap fragmenté + optimisation des liens internes. Au-delà de vingt mille : filtrage qualité strict, ne générer que les pages avec volume de recherche.
Les pages modélisées seront-elles pénalisées par Google ?
Non, si chaque page apporte une valeur réelle. Trois points clés : données uniques par page (pas seulement un swap de mot-clé), participation utilisateur (UGC ou interaction), relecture humaine. Seules les pages qui ne changent que le mot-clé risquent une pénalité.
PostgreSQL ou MongoDB pour la base de données ?
Structure de données fixe : PostgreSQL, ACID et requêtes complexes plus stables. Structure encore en évolution : MongoDB, schéma flexible. Petit projet (quelques centaines d’enregistrements) : Airtable ou fichiers JSON suffisent.
Combien de niveaux d’URL au maximum ?
Trois niveaux recommandés au plus. Par exemple `/city/service/type` est la limite. Des chemins trop profonds nuisent aux utilisateurs et aux crawlers. URL aplaties (un niveau) conviennent aux sites à très grand volume de pages.
Comment automatiser les liens internes ?
Deux stratégies : hiérarchie géographique (pages ville liées aux villes voisines), regroupement thématique (pages même service liées entre elles). Côté code, dans le modèle de page, faire correspondre les pages liées via tags ou champ location, puis générer automatiquement la liste de liens.
Astro ou Next.js pour le SEO programmatique ?
Scénario purement statique : Astro, Content Collections conçu pour les pages en masse, builds rapides. Besoin de rendu dynamique : Next.js, SSR et ISR matures. Les deux conviennent — l’essentiel est d’aligner la fréquence de mise à jour des données.

15 min de lecture · Publié le: 4 avr. 2026 · Mis à jour le: 27 juil. 2026

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog