Blog IA

Le module Blog du Labo du Yeti est un système de publication éditorial complet, conçu autour d'un template article global unique et d'une génération IA orchestrée (bundle data-ai) couvrant titre, paragraphes, FAQ, CTA, image vedette, sources vérifiées et JSON-LD. Il s'appuie sur les tables SQLite articles, categories et tags introduites en Phase 2, et s'expose via la route /api/blog documentée dans la référence API.

Capability requise : blog. Activée par défaut pour owner, admin, webmaster et editor. L'owner peut la désactiver pour n'importe quel rôle via la matrice RBAC.

Philosophie : un template global, des articles infinis

Contrairement aux pages classiques qui peuvent chacune utiliser un template distinct, tous les articles du blog partagent un seul template HTML configuré globalement dans Settings → Blog sous la clé config.article_template. Ce template est un fragment HTML balisé data-ai="*" que la skill template-article-blog génère typiquement (titre, intro, sections, FAQ, stats, table, testimonial, quiz multi-étapes, formulaire data-cms-form, JSON-LD). Le moteur de rendu injecte ensuite les contenus IA dans les emplacements data-ai au moment de la publication.

Ce choix garantit une cohérence éditoriale parfaite sur l'ensemble du blog (typographie, hiérarchie, blocs disponibles), simplifie radicalement la maintenance (on met à jour un template et tous les articles suivent au prochain build), et permet à la skill IA de raisonner sur une structure connue.

Conséquence : changer config.article_template impacte tous les articles existants. Un build complet est requis (buildSite()) pour propager le nouveau squelette.

Modèle de données

Trois entités SQLite structurent le blog. Voir la page Base de données pour les schémas complets.

Table articles

ChampTypeDescription
idTEXT (PK)UUID de l'article
titleTEXTTitre éditorial (sert au slug)
slugTEXTSlug URL (/blog/<slug>.html)
excerptTEXTRésumé court (méta description, listing)
category_idsTEXT (JSON)Multi-sélection de catégories
tag_idsTEXT (JSON)Tags associés
featured_imageTEXTChemin local /uploads/blog/<id>.webp
overridesTEXT (JSON)Bundle IA data-ai ou JSON data-cms legacy
sourcesTEXT (JSON)Sources vérifiées (HEAD 200) utilisées pour la rédaction
statusTEXTdraft · scheduled · published
publish_atTEXT (ISO)Date de publication programmée (si scheduled)
created_atTEXTHorodatage création
updated_atTEXTOptimistic locking (if_updated_at → 409)

Catégories et tags

Les tables categories et tags partagent une structure simple : id, name, slug, description (catégories uniquement), created_at. Un article peut appartenir à plusieurs catégories (multi-sélect) et porter plusieurs tags, ce qui alimente les pages d'archive générées par le blog-index-service.

Cycle de création d'un article

createArticle() : étape 1, créer le squelette

La fonction createArticle() du backend/src/services/blog-service.js valide les entrées, génère un slug unique, télécharge automatiquement une image vedette via le module image-providers (voir plus bas), persiste l'article en base avec le statut draft par défaut, et trace l'événement dans la table logs.

POST /api/blog/articles
{
  "title": "Comment optimiser son SEO local en 2026",
  "category_ids": ["cat-seo", "cat-local"],
  "tag_ids": ["tag-google", "tag-bing"],
  "status": "draft"
}
// → 201 { id, slug, featured_image, status: 'draft', ... }

generateAndStoreArticle() : étape 2, remplir le contenu

L'endpoint POST /api/blog/articles/:id/generate (rate-limité 10/min via aiGenLimiter) déclenche generateAndStoreArticle(). Cette fonction aiguille intelligemment selon la nature du template configuré :

Sources fiables : avant tout appel IA rédactionnel, searchSources() interroge le search-service (SerpAPI ou custom slot configuré dans Settings → API) pour rapporter 5 à 10 URL pertinentes, puis chaque URL est validée par verifyUrl() (requête HEAD anti-404, garde SSRF rejetant les IP privées et les schémas non-HTTP). Seules les URL répondant 2xx alimentent le prompt comme contexte vérifiable.

regenerateFeaturedImage() et le module image-providers

L'image vedette est cherchée automatiquement à la création (et regénérable manuellement à tout moment) via POST /api/blog/articles/:id/regenerate-image. Le module image-providers consulte le provider actif (unsplash, pexels ou pixabay, configuré dans Settings → API → Banques d'images) avec une requête déduite du titre, télécharge le fichier choisi dans uploads/blog/<id>.<ext> puis met à jour article.featured_image. Si l'extension webp-auto-converter est active, l'image est convertie en WebP via le hook media.before_save.

ProviderClé requiseNotes
Unsplashsettings.images.unsplash.access_keyDemo : 50 req/h ; Production : 5000 req/h
Pexelssettings.images.pexels.api_key200 req/h gratuit, illimité approuvé
Pixabaysettings.images.pixabay.api_key5000 req/h, attribution non requise

Statuts et publication

Un article transite par trois états contrôlés par la colonne status :

StatutVisible publiquementInclus au buildDescription
draftNonNonBrouillon, accessible via preview interne
scheduledNonNon (jusqu'à publish_at)Programmé, auto-promu par le scheduler
publishedOuiOuiPublié, rendu dans public/blog/<slug>.html

blog-scheduler : la boucle de promotion automatique

Le service backend/src/services/blog-scheduler.js est démarré au boot de l'application. Il exécute une tâche toutes les 60 secondes :

  1. Sélectionne les articles avec status = 'scheduled' et publish_at <= now().
  2. Bascule chaque article à status = 'published' en transaction.
  3. Déclenche buildSite() (voir Build & Publication) une seule fois si au moins un article a été promu.
  4. Trace chaque promotion dans la table logs avec type = 'blog_published'.
Latence maximale : un article programmé pour 14:00:00 sera publié au plus tard à 14:00:59. La précision est suffisante pour un blog éditorial. Pour un besoin temps-réel, l'API PUT /api/blog/articles/:id peut forcer status = 'published' immédiatement.

Sourcing et vérification des URL

La crédibilité d'un article IA repose sur la qualité de ses sources. Le pipeline blog suit une discipline stricte :

  1. Recherche : searchSources(query) appelle le search-service qui interroge le provider actif (SerpAPI, Brave, Tavily) configuré dans Settings → API → Recherche web. La sélection du slot custom permet de pointer vers un endpoint maison si besoin.
  2. Vérification : verifyUrl(url) émet une requête HEAD avec timeout 5 s. Le code retour doit être 2xx ou 3xx (suivi redirect 1 niveau). Toute URL 404, 410 ou réseau-injoignable est rejetée.
  3. Garde SSRF : avant toute requête sortante, verifyUrl() rejette les schémas non-HTTP, résout le DNS et bannit les plages privées (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, IPv6 link-local).
  4. Persistance : seules les URL validées sont stockées dans article.sources et injectées dans le prompt IA comme contexte de rédaction.
// backend/src/services/blog-service.js (extrait)
const candidates = await searchSources(article.title);
const verified = [];
for (const url of candidates) {
  if (await verifyUrl(url)) verified.push(url);
}
article.sources = verified.slice(0, 10);

blog-index-service : index, catégories et tags

Le service backend/src/services/blog-index-service.js est invoqué par buildSite() à chaque build complet. Il génère trois types de pages d'archive entièrement statiques :

Route publiqueContenuPagination
/blog.htmlTous les articles published12 articles/page
/blog/categorie/<slug>.htmlArticles d'une catégorie12 articles/page
/blog/tag/<slug>.htmlArticles d'un tag12 articles/page

Chaque page d'archive utilise le template blog-listing (skill template-blog-listing) qui contient un placeholder {{blog.list}} remplacé dynamiquement par les cartes d'articles, ainsi qu'un bloc pagination calculé automatiquement. Les pages 2..N suivent le pattern /blog/page-2.html, /blog/categorie/seo/page-3.html, etc.

Variable globale {{blog.url}}

Le moteur de rendu expose la variable {{blog.url}} dans tous les templates (pages, cocon, layout global) afin de permettre des liens cohérents vers l'index du blog où qu'on soit dans le site. Elle est résolue par render-service au build et pointe vers /blog.html ou la valeur surchargée dans Settings → Blog → URL personnalisée.

<a href="{{blog.url}}">Voir tous les articles</a>
<!-- rendu : <a href="/blog.html">Voir tous les articles</a> -->

Endpoints REST récapitulatifs

Tous les endpoints requièrent requireAuth + requireCapability('blog'). Mutations limitées aux rôles indiqués. Voir la référence API complète.

MéthodeRouteRôlesDescription
GET/api/blog/configTousLire la config blog (template global, URL)
PUT/api/blog/configowner, admin, webmasterModifier la config
GET/api/blog/articlesTousLister articles (filtres : status, category, tag)
POST/api/blog/articles+ editorCréer (upload template optionnel)
PUT/api/blog/articles/:id+ editorModifier
DELETE/api/blog/articles/:id+ editorSupprimer
POST/api/blog/articles/:id/generate+ editorLancer génération IA (10/min)
POST/api/blog/articles/:id/regenerate-image+ editorRe-télécharger image vedette
GET/api/blog/preview/indexTousPrévisualiser l'index
GET/api/blog/preview/category/:slugTousPrévisualiser une catégorie

UI frontend

Le module Blog injecte dynamiquement deux entrées dans la sidebar React (section CONTENU) :

L'éditeur d'article propose : modification des méta (titre, catégories, tags, excerpt), prévisualisation en iframe (/api/blog/articles/:id/preview), bouton "Générer le contenu IA", bouton "Re-générer l'image", changement de statut avec date-picker pour scheduled, et lien direct vers le builder visuel (voir Builder visuel) pour ajuster les blocs au cas par cas.

Helpers extension cms.blog.*

Le contexte ext.cms exposé aux extensions actives prévoit les helpers suivants (voir Système d'extensions) — certains sont en cours d'implémentation dans la roadmap :

Ces helpers requièrent la permission blog.write ou blog.read déclarée dans manifest.permissions de l'extension.

Audit et journalisation

Chaque action significative du module blog produit une entrée dans la table logs (cf. Base de données) avec un type dédié :

TypeDéclencheurMéta JSON
blog_article_createdPOST /articles{ article_id, slug, title }
blog_article_updatedPUT /articles/:id{ article_id, fields_changed }
blog_article_generatedPOST /articles/:id/generate{ article_id, provider, model, input_tokens, output_tokens }
blog_article_publishedscheduler + PUT{ article_id, publish_at, scheduled }
blog_image_regeneratedPOST /regenerate-image{ article_id, provider, query }

Couplé à la table ai_usage, ce journal permet de reconstituer précisément l'historique éditorial et la consommation IA imputable au blog. Voir la page Reporting pour les agrégats cross-sites.

Bonnes pratiques

Toujours définir au moins une catégorie par article : l'archive /blog/categorie/<slug>.html ne sera pas générée sans, et le SEO interne perd un cocon naturel.
Éviter de modifier config.article_template en production sans rebuild manuel : les nouveaux articles utiliseront le nouveau template, mais les anciens articles publiés resteront figés tant qu'un buildSite() n'a pas été déclenché.
Ne jamais stocker les clés des providers d'images ou de recherche dans le template HTML : elles vivent exclusivement dans Settings → API (table stores) et sont filtrées du contexte extension par le système de permissions (cf. Sécurité).

Ressources