seogeneratorserver/plan.md
StillHammer dbf1a3de8c Add technical plan for multi-format export system
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>
2025-11-18 16:14:29 +08:00

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:

  1. Extraire la structure du contenu généré
  2. Créer l'objet neutre au lieu de retourner directement le XML
  3. 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"
    }
  }
}