Added plan.md with complete architecture for format-agnostic content generation: - Support for Markdown, HTML, Plain Text, JSON formats - New FormatExporter module with neutral data structure - Integration strategy with existing ContentAssembly and ArticleStorage - Bonus features: SEO metadata generation, readability scoring, WordPress Gutenberg format - Implementation roadmap with 4 phases (6h total estimated) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
15 KiB
15 KiB
🚀 Plan Technique - Système Multi-Format d'Export
📋 Vue d'ensemble
Objectif: Ajouter un système d'export multi-format au générateur SEO pour produire du contenu dans différents formats (Markdown, HTML, Plain Text, JSON).
Principe: Séparer la génération de contenu de sa présentation en stockant le contenu dans une structure de données neutre, puis en fournissant des convertisseurs vers chaque format.
🎯 Formats à supporter
1. Markdown (.md) - PRIORITÉ #1
- Format le plus demandé pour les blogs
- Compatible WordPress, Ghost, Jekyll, Hugo
- Lisible et éditable facilement
2. Plain Text (.txt) - PRIORITÉ #2
- Format universel
- Pas de formatage, copier-coller direct
3. HTML (.html) - PRIORITÉ #3
- Format déjà généré par le système actuel
- Nécessaire pour certains CMS
4. JSON - Pour intégration API
- Format structuré
- Utile pour les intégrations programmatiques
🏗️ Architecture proposée
Structure de données neutre
Au lieu de générer directement du XML/HTML, stocker le contenu dans une structure intermédiaire:
{
title: "Titre principal",
meta_description: "Meta description SEO",
intro: "Texte d'introduction",
sections: [
{
heading: "Titre section 1",
level: 2, // h2
content: "Contenu de la section..."
},
{
heading: "Sous-section 1.1",
level: 3, // h3
content: "Contenu de la sous-section..."
}
],
conclusion: "Texte de conclusion",
metadata: {
word_count: 1247,
keywords: ["mot-clé1", "mot-clé2"],
generated_at: "2025-09-06T..."
}
}
Nouveau module: lib/FormatExporter.js
class FormatExporter {
constructor(contentStructure) {
this.content = contentStructure;
}
toMarkdown() {
let md = `# ${this.content.title}\n\n`;
md += `${this.content.intro}\n\n`;
for (const section of this.content.sections) {
const hashes = '#'.repeat(section.level);
md += `${hashes} ${section.heading}\n\n`;
md += `${section.content}\n\n`;
}
if (this.content.conclusion) {
md += `## Conclusion\n\n${this.content.conclusion}\n`;
}
return md;
}
toHTML() {
let html = `<h1>${this.content.title}</h1>\n`;
html += `<p>${this.content.intro}</p>\n`;
for (const section of this.content.sections) {
html += `<h${section.level}>${section.heading}</h${section.level}>\n`;
html += `<p>${section.content}</p>\n`;
}
if (this.content.conclusion) {
html += `<h2>Conclusion</h2>\n<p>${this.content.conclusion}</p>\n`;
}
return html;
}
toPlainText() {
let txt = `${this.content.title}\n\n`;
txt += `${this.content.intro}\n\n`;
for (const section of this.content.sections) {
txt += `${section.heading}\n`;
txt += `${section.content}\n\n`;
}
if (this.content.conclusion) {
txt += `Conclusion\n${this.content.conclusion}\n`;
}
return txt;
}
toJSON() {
return JSON.stringify(this.content, null, 2);
}
export(format = 'markdown') {
const exporters = {
'markdown': this.toMarkdown.bind(this),
'md': this.toMarkdown.bind(this),
'html': this.toHTML.bind(this),
'text': this.toPlainText.bind(this),
'txt': this.toPlainText.bind(this),
'json': this.toJSON.bind(this)
};
if (format.toLowerCase() in exporters) {
return exporters[format.toLowerCase()]();
}
throw new Error(`Format ${format} non supporté`);
}
}
module.exports = { FormatExporter };
🔧 Intégration au système existant
Modification de lib/ContentAssembly.js
Actuellement, ContentAssembly génère directement du XML. Il faudra:
- Extraire la structure du contenu généré
- Créer l'objet neutre au lieu de retourner directement le XML
- Appeler FormatExporter pour générer le format souhaité
// Avant
function assembleContent(elements, xmlTemplate) {
let result = xmlTemplate;
// Remplace les balises dans le XML
return result; // Retourne XML
}
// Après
function assembleContent(elements, xmlTemplate, options = {}) {
// Construire la structure neutre
const contentStructure = buildContentStructure(elements);
// Exporter au format demandé
const exporter = new FormatExporter(contentStructure);
const format = options.outputFormat || 'markdown';
return exporter.export(format);
}
function buildContentStructure(elements) {
// Parser les éléments générés et construire l'objet
return {
title: elements.find(e => e.tag === 'h1')?.content || '',
intro: elements.find(e => e.tag === 'intro')?.content || '',
sections: buildSections(elements),
conclusion: elements.find(e => e.tag === 'conclusion')?.content || '',
metadata: {
word_count: calculateWordCount(elements),
generated_at: new Date().toISOString()
}
};
}
Modification de lib/ArticleStorage.js
Ajouter un paramètre pour sauvegarder dans différents formats:
async function saveGeneratedArticle(contentStructure, options = {}) {
const exporter = new FormatExporter(contentStructure);
// Sauvegarder en markdown par défaut pour Google Sheets
const markdownContent = exporter.toMarkdown();
// Sauvegarder dans Google Sheets
await appendToSheet('Generated_Articles', [..., markdownContent]);
// Optionnel: sauvegarder aussi en HTML, JSON, etc.
if (options.saveAllFormats) {
const html = exporter.toHTML();
const json = exporter.toJSON();
// Sauvegarder dans des fichiers locaux ou autres sheets
}
}
📋 Plan d'implémentation
Phase 1: Création du module FormatExporter (2h)
- Créer
lib/FormatExporter.js - Implémenter
toMarkdown() - Implémenter
toHTML() - Implémenter
toPlainText() - Implémenter
toJSON() - Tests unitaires
Phase 2: Adaptation de ContentAssembly (2h)
- Créer
buildContentStructure(elements) - Parser les éléments XML existants en structure neutre
- Intégrer FormatExporter dans le workflow
- Tests d'intégration
Phase 3: Mise à jour ArticleStorage (1h)
- Adapter la sauvegarde Google Sheets pour Markdown
- Option pour sauvegarder en multi-formats
- Tests de sauvegarde
Phase 4: Tests et validation (1h)
- Test génération complète avec différents formats
- Validation import WordPress (Markdown)
- Vérification préservation du contenu
- Tests de performance
✅ Critères de validation
- ✅ Tous les formats génèrent le même contenu (structure équivalente)
- ✅ Markdown s'importe correctement dans WordPress
- ✅ HTML est identique à la version XML actuelle
- ✅ Plain text est lisible sans artefacts
- ✅ JSON est bien formé et parsable
- ✅ Aucune perte d'information entre formats
- ✅ Performance: < 50ms pour conversion de format
🔬 Tests à effectuer
// Test 1: Génération multi-format
const content = await generateContent(...);
const exporter = new FormatExporter(content);
const md = exporter.toMarkdown();
const html = exporter.toHTML();
const txt = exporter.toPlainText();
const json = exporter.toJSON();
// Vérifier que tous contiennent le même titre
assert(md.includes(content.title));
assert(html.includes(content.title));
assert(txt.includes(content.title));
// Test 2: Préservation du formatage
// Vérifier que les balises h2, h3 sont correctement converties
// Test 3: Import WordPress
// Copier le Markdown généré dans WordPress et vérifier le rendu
🔥 Features bonus (optionnel)
1. Génération automatique de métadonnées SEO
class FormatExporter {
// ... méthodes existantes ...
generateSEOMeta() {
const slug = this.content.title
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '') // Enlever accents
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
return {
title: this.content.title.substring(0, 60), // Max 60 caractères
description: this.content.meta_description?.substring(0, 160) ||
this.content.intro?.substring(0, 160), // Max 160 caractères
keywords: this.content.metadata?.keywords?.join(', ') || '',
slug: slug,
canonical: `https://example.com/${slug}`,
og_title: this.content.title,
og_description: this.content.meta_description || this.content.intro?.substring(0, 160),
og_type: 'article',
twitter_card: 'summary_large_image'
};
}
toHTMLWithMeta() {
const meta = this.generateSEOMeta();
return `<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${meta.title}</title>
<meta name="description" content="${meta.description}">
<meta name="keywords" content="${meta.keywords}">
<link rel="canonical" href="${meta.canonical}">
<!-- Open Graph -->
<meta property="og:title" content="${meta.og_title}">
<meta property="og:description" content="${meta.og_description}">
<meta property="og:type" content="${meta.og_type}">
<!-- Twitter Card -->
<meta name="twitter:card" content="${meta.twitter_card}">
<meta name="twitter:title" content="${meta.title}">
<meta name="twitter:description" content="${meta.description}">
</head>
<body>
${this.toHTML()}
</body>
</html>`;
}
}
2. Score de lisibilité (Flesch-Kincaid)
class FormatExporter {
// ... méthodes existantes ...
calculateReadability() {
const text = this.toPlainText();
// Compter mots
const words = text.split(/\s+/).filter(w => w.length > 0);
const wordCount = words.length;
// Compter phrases
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0);
const sentenceCount = sentences.length;
// Compter syllabes (approximation pour le français)
const syllables = this.countSyllables(text);
// Formule Flesch adaptée pour le français
const score = 207 - 1.015 * (wordCount / sentenceCount) - 73.6 * (syllables / wordCount);
let grade;
if (score >= 60) grade = 'Facile';
else if (score >= 30) grade = 'Moyen';
else grade = 'Difficile';
return {
score: Math.round(score),
grade: grade,
word_count: wordCount,
sentence_count: sentenceCount,
avg_words_per_sentence: Math.round(wordCount / sentenceCount),
reading_time_minutes: Math.ceil(wordCount / 200) // ~200 mots/min
};
}
countSyllables(text) {
// Approximation: compter les voyelles
const vowels = text.match(/[aeiouyàéèêëïîôùû]/gi);
return vowels ? vowels.length : 0;
}
}
3. Export avec statistiques complètes
class FormatExporter {
// ... méthodes existantes ...
exportWithStats(format = 'markdown') {
const content = this.export(format);
const seo = this.generateSEOMeta();
const readability = this.calculateReadability();
return {
content: content,
format: format,
seo: seo,
readability: readability,
metadata: {
...this.content.metadata,
exported_at: new Date().toISOString(),
content_hash: this.generateHash(content) // Pour déduplication
}
};
}
generateHash(content) {
// Simple hash pour identifier le contenu
const crypto = require('crypto');
return crypto.createHash('sha256').update(content).digest('hex').substring(0, 16);
}
}
4. Format WordPress Gutenberg (avec blocs)
class FormatExporter {
// ... méthodes existantes ...
toWordPressGutenberg() {
let wp = `<!-- wp:heading {"level":1} -->\n`;
wp += `<h1>${this.content.title}</h1>\n`;
wp += `<!-- /wp:heading -->\n\n`;
wp += `<!-- wp:paragraph -->\n`;
wp += `<p>${this.content.intro}</p>\n`;
wp += `<!-- /wp:paragraph -->\n\n`;
for (const section of this.content.sections) {
wp += `<!-- wp:heading {"level":${section.level}} -->\n`;
wp += `<h${section.level}>${section.heading}</h${section.level}>\n`;
wp += `<!-- /wp:heading -->\n\n`;
// Séparer les paragraphes
const paragraphs = section.content.split('\n\n');
for (const para of paragraphs) {
if (para.trim()) {
wp += `<!-- wp:paragraph -->\n`;
wp += `<p>${para.trim()}</p>\n`;
wp += `<!-- /wp:paragraph -->\n\n`;
}
}
}
if (this.content.conclusion) {
wp += `<!-- wp:heading {"level":2} -->\n`;
wp += `<h2>Conclusion</h2>\n`;
wp += `<!-- /wp:heading -->\n\n`;
wp += `<!-- wp:paragraph -->\n`;
wp += `<p>${this.content.conclusion}</p>\n`;
wp += `<!-- /wp:paragraph -->\n`;
}
return wp;
}
}
5. Intégration dans le workflow
Mise à jour de lib/Main.js pour utiliser les nouvelles fonctionnalités:
async function handleFullWorkflow(data, options = {}) {
// ... workflow existant ...
// Après génération du contenu
const contentStructure = buildContentStructure(generatedElements);
const exporter = new FormatExporter(contentStructure);
// Export multi-format
const results = {
markdown: exporter.export('markdown'),
html: exporter.export('html'),
json: exporter.export('json'),
wordpress: exporter.toWordPressGutenberg(),
seo: exporter.generateSEOMeta(),
readability: exporter.calculateReadability()
};
// Sauvegarder dans Google Sheets
await saveGeneratedArticle(contentStructure, {
format: options.outputFormat || 'markdown',
includeStats: true
});
// Retourner résultat complet
return {
success: true,
slug: results.seo.slug,
formats: {
markdown: results.markdown,
html: results.html,
wordpress: results.wordpress
},
stats: {
word_count: results.readability.word_count,
reading_time: results.readability.reading_time_minutes,
readability_score: results.readability.score,
seo_meta: results.seo
}
};
}
📊 Exemple de sortie complète
{
"success": true,
"slug": "comment-creer-une-plaque-personnalisee",
"formats": {
"markdown": "# Comment Créer une Plaque Personnalisée\n\n...",
"html": "<h1>Comment Créer une Plaque Personnalisée</h1>\n...",
"wordpress": "<!-- wp:heading {\"level\":1} -->..."
},
"stats": {
"word_count": 1247,
"reading_time": 7,
"readability_score": 65,
"seo_meta": {
"title": "Comment Créer une Plaque Personnalisée",
"description": "Découvrez comment créer facilement une plaque personnalisée...",
"keywords": "plaque personnalisée, gravure, décoration",
"slug": "comment-creer-une-plaque-personnalisee",
"canonical": "https://example.com/comment-creer-une-plaque-personnalisee"
}
}
}