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.
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.
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
| Champ | Type | Description |
|---|---|---|
id | TEXT (PK) | UUID de l'article |
title | TEXT | Titre éditorial (sert au slug) |
slug | TEXT | Slug URL (/blog/<slug>.html) |
excerpt | TEXT | Résumé court (méta description, listing) |
category_ids | TEXT (JSON) | Multi-sélection de catégories |
tag_ids | TEXT (JSON) | Tags associés |
featured_image | TEXT | Chemin local /uploads/blog/<id>.webp |
overrides | TEXT (JSON) | Bundle IA data-ai ou JSON data-cms legacy |
sources | TEXT (JSON) | Sources vérifiées (HEAD 200) utilisées pour la rédaction |
status | TEXT | draft · scheduled · published |
publish_at | TEXT (ISO) | Date de publication programmée (si scheduled) |
created_at | TEXT | Horodatage création |
updated_at | TEXT | Optimistic 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é :
- Chemin
data-ai(recommandé) : si le template contient des balisesdata-ai="*", l'orchestrateur appellegenerateArticleAiBundle(). Ce dernier interroge le provider IA (Anthropic ou OpenAI selonsettings.ai.provider) avec un prompt structuré demandant un bundle JSON complet : titre H1, paragraphes, items FAQ, items de table, citations, JSON-LD Article, etc. Le bundle est stocké dansarticle.overrideset rendu au build. - Chemin
data-cms(legacy) : si le template utilise l'ancien balisagedata-cms, la génération suit deux formats au choix :json: retour structuré simple{ titre, intro, sections: [...] }html: retour brut HTML inséré tel quel
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.
| Provider | Clé requise | Notes |
|---|---|---|
| Unsplash | settings.images.unsplash.access_key | Demo : 50 req/h ; Production : 5000 req/h |
| Pexels | settings.images.pexels.api_key | 200 req/h gratuit, illimité approuvé |
| Pixabay | settings.images.pixabay.api_key | 5000 req/h, attribution non requise |
Statuts et publication
Un article transite par trois états contrôlés par la colonne status :
| Statut | Visible publiquement | Inclus au build | Description |
|---|---|---|---|
| draft | Non | Non | Brouillon, accessible via preview interne |
| scheduled | Non | Non (jusqu'à publish_at) | Programmé, auto-promu par le scheduler |
| published | Oui | Oui | Publié, 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 :
- Sélectionne les articles avec
status = 'scheduled'etpublish_at <= now(). - Bascule chaque article à
status = 'published'en transaction. - Déclenche
buildSite()(voir Build & Publication) une seule fois si au moins un article a été promu. - Trace chaque promotion dans la table
logsavectype = 'blog_published'.
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 :
- Recherche :
searchSources(query)appelle lesearch-servicequi interroge le provider actif (SerpAPI, Brave, Tavily) configuré dansSettings → API → Recherche web. La sélection du slot custom permet de pointer vers un endpoint maison si besoin. - 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. - 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). - Persistance : seules les URL validées sont stockées dans
article.sourceset 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 publique | Contenu | Pagination |
|---|---|---|
/blog.html | Tous les articles published | 12 articles/page |
/blog/categorie/<slug>.html | Articles d'une catégorie | 12 articles/page |
/blog/tag/<slug>.html | Articles d'un tag | 12 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éthode | Route | Rôles | Description |
|---|---|---|---|
| GET | /api/blog/config | Tous | Lire la config blog (template global, URL) |
| PUT | /api/blog/config | owner, admin, webmaster | Modifier la config |
| GET | /api/blog/articles | Tous | Lister articles (filtres : status, category, tag) |
| POST | /api/blog/articles | + editor | Créer (upload template optionnel) |
| PUT | /api/blog/articles/:id | + editor | Modifier |
| DELETE | /api/blog/articles/:id | + editor | Supprimer |
| POST | /api/blog/articles/:id/generate | + editor | Lancer génération IA (10/min) |
| POST | /api/blog/articles/:id/regenerate-image | + editor | Re-télécharger image vedette |
| GET | /api/blog/preview/index | Tous | Prévisualiser l'index |
| GET | /api/blog/preview/category/:slug | Tous | Prévisualiser une catégorie |
UI frontend
Le module Blog injecte dynamiquement deux entrées dans la sidebar React (section CONTENU) :
- Ajouter un article → ouvre le formulaire de création (titre, catégories, tags, statut, date programmée)
- Articles publiés → liste paginée filtrable par statut, catégorie, tag, recherche plein texte
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 :
cms.blog.write(article)— créer ou mettre à jour un article depuis une extensioncms.blog.listCategories()— lister les catégories disponiblescms.blog.listTags()— lister les tags disponiblescms.blog.countArticles({ status, category })— compteur agrégé
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é :
| Type | Déclencheur | Méta JSON |
|---|---|---|
blog_article_created | POST /articles | { article_id, slug, title } |
blog_article_updated | PUT /articles/:id | { article_id, fields_changed } |
blog_article_generated | POST /articles/:id/generate | { article_id, provider, model, input_tokens, output_tokens } |
blog_article_published | scheduler + PUT | { article_id, publish_at, scheduled } |
blog_image_regenerated | POST /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
/blog/categorie/<slug>.html ne sera pas générée sans, et le SEO interne perd un cocon naturel.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é.Settings → API (table stores) et sont filtrées du contexte extension par le système de permissions (cf. Sécurité).Ressources
backend/src/services/blog-service.js— CRUD, génération, sourcesbackend/src/services/blog-scheduler.js— boucle 60 s de promotionbackend/src/services/blog-index-service.js— index, catégories, tags, paginationbackend/src/services/search-service.js— recherche web pour sourcingbackend/src/services/image-providers.js— Unsplash/Pexels/Pixabaybackend/src/routes/blog.js— routes Express + middlewares- Skill
template-article-blog— génération du template global - Skill
template-blog-listing— génération du template d'archive