Files
wevia-brain/s89-ai-apis/kb-ssr-generator.php
2026-04-12 23:01:36 +02:00

335 lines
15 KiB
PHP
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* WEVIA KB SSR GENERATOR v1.0
*
* Generates static HTML pages from KB content for SEO indexation.
* Google can't crawl SPA JS-only pages → this creates server-rendered HTML.
*
* USAGE:
* ?action=generate Generate all static pages
* ?action=sitemap Generate sitemap.xml
* ?action=status Show current SSR state
* ?action=serve&slug=xxx Serve a specific KB page (for nginx proxy)
*
* OUTPUT: /opt/wevads/public/wevia-kb/ (static HTML files)
*
* NGINX CONFIG (add to vhost):
* location /wevia-kb/ {
* try_files $uri $uri/ /api/kb-ssr-generator.php?action=serve&slug=$uri;
* }
*/
header('Content-Type: application/json; charset=utf-8');
$pdo = new PDO('pgsql:host=localhost;dbname=adx_system', 'admin', 'admin123');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("SET search_path TO admin,affiliate,public");
$pdo->exec("SET client_encoding TO 'UTF8'");
$action = $_GET['action'] ?? 'status';
$slug = $_GET['slug'] ?? null;
$OUTPUT_DIR = '/opt/wevads/public/wevia-kb/';
$BASE_URL = 'https://weval-consulting.com/wevia-kb';
// Ensure output dir exists
if (!is_dir($OUTPUT_DIR)) mkdir($OUTPUT_DIR, 0755, true);
function slugify(string $text): string {
$text = strtolower(trim($text));
$text = preg_replace('/[àáâãäå]/u', 'a', $text);
$text = preg_replace('/[èéêë]/u', 'e', $text);
$text = preg_replace('/[ìíîï]/u', 'i', $text);
$text = preg_replace('/[òóôõö]/u', 'o', $text);
$text = preg_replace('/[ùúûü]/u', 'u', $text);
$text = preg_replace('/[ç]/u', 'c', $text);
$text = preg_replace('/[^a-z0-9]+/', '-', $text);
return trim($text, '-');
}
function generatePage(array $entry, string $outputDir, string $baseUrl): string {
$slug = slugify($entry['title']);
$title = htmlspecialchars($entry['title'], ENT_QUOTES, 'UTF-8');
$category = htmlspecialchars($entry['category'] ?? 'General', ENT_QUOTES, 'UTF-8');
$content = $entry['content'] ?? '';
// Convert markdown-like content to HTML
$htmlContent = nl2br(htmlspecialchars($content, ENT_QUOTES, 'UTF-8'));
$htmlContent = preg_replace('/^### (.+)$/m', '<h3>$1</h3>', $htmlContent);
$htmlContent = preg_replace('/^## (.+)$/m', '<h2>$1</h2>', $htmlContent);
$htmlContent = preg_replace('/^# (.+)$/m', '<h1>$1</h1>', $htmlContent);
$htmlContent = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $htmlContent);
$htmlContent = preg_replace('/`(.+?)`/', '<code>$1</code>', $htmlContent);
$excerpt = htmlspecialchars(substr(strip_tags($content), 0, 160), ENT_QUOTES, 'UTF-8');
$date = date('Y-m-d', strtotime($entry['created_at'] ?? 'now'));
$wordCount = str_word_count($content);
$html = <<<HTML
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{$title} — WEVIA Knowledge Base | WEVAL Consulting</title>
<meta name="description" content="{$excerpt}">
<meta name="author" content="WEVAL Consulting">
<meta property="og:title" content="{$title}">
<meta property="og:description" content="{$excerpt}">
<meta property="og:type" content="article">
<meta property="og:url" content="{$baseUrl}/{$slug}.html">
<meta property="og:site_name" content="WEVIA Knowledge Base">
<link rel="canonical" href="{$baseUrl}/{$slug}.html">
<meta name="robots" content="index, follow">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "{$title}",
"description": "{$excerpt}",
"author": {"@type": "Organization", "name": "WEVAL Consulting"},
"publisher": {"@type": "Organization", "name": "WEVAL Consulting", "url": "https://weval-consulting.com"},
"datePublished": "{$date}",
"articleSection": "{$category}",
"wordCount": {$wordCount}
}
</script>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; background:#0a0f1a; color:#e2e8f0; line-height:1.7; }
.header { background:linear-gradient(135deg, #0c1220 0%, #1a1040 100%); padding:40px 20px; text-align:center; border-bottom:1px solid #1e293b; }
.header h1 { color:#22d3ee; font-size:28px; margin-bottom:8px; }
.header .meta { color:#64748b; font-size:13px; }
.header .meta span { margin:0 8px; }
.breadcrumb { padding:12px 20px; color:#64748b; font-size:12px; max-width:900px; margin:0 auto; }
.breadcrumb a { color:#22d3ee; text-decoration:none; }
.content { max-width:900px; margin:0 auto; padding:30px 20px; }
.content h2 { color:#a78bfa; font-size:20px; margin:24px 0 12px; border-bottom:1px solid #1e293b; padding-bottom:6px; }
.content h3 { color:#34d399; font-size:16px; margin:18px 0 8px; }
.content p, .content br+br { margin-bottom:12px; }
.content code { background:#111827; padding:2px 6px; border-radius:4px; color:#fbbf24; font-size:13px; }
.content strong { color:#f8fafc; }
.sidebar { max-width:900px; margin:20px auto; padding:20px; background:#0c1220; border-radius:8px; border:1px solid #1e293b; }
.sidebar h3 { color:#22d3ee; margin-bottom:10px; }
.sidebar a { color:#60a5fa; text-decoration:none; display:block; padding:4px 0; font-size:13px; }
.sidebar a:hover { color:#22d3ee; }
.footer { text-align:center; padding:30px; color:#475569; font-size:12px; border-top:1px solid #1e293b; margin-top:40px; }
.footer a { color:#22d3ee; text-decoration:none; }
.tag { display:inline-block; background:#1e293b; color:#94a3b8; padding:2px 10px; border-radius:12px; font-size:11px; margin:2px; }
</style>
</head>
<body>
<div class="header">
<h1>{$title}</h1>
<div class="meta">
<span class="tag">{$category}</span>
<span>📅 {$date}</span>
<span>📝 {$wordCount} mots</span>
</div>
</div>
<div class="breadcrumb">
<a href="/">WEVAL Consulting</a>
<a href="/wevia-kb/">WEVIA Knowledge Base</a>
<a href="/wevia-kb/?cat={$category}">{$category}</a>
{$title}
</div>
<div class="content">
{$htmlContent}
</div>
<div class="footer">
<p>WEVIA Knowledge Base — <a href="https://weval-consulting.com">WEVAL Consulting</a></p>
<p>Expert en transformation digitale, IA, Cloud, Blockchain & Email Marketing</p>
<p>Casablanca, Maroc | Paris, France</p>
</div>
</body>
</html>
HTML;
$filepath = $outputDir . $slug . '.html';
file_put_contents($filepath, $html);
return $slug;
}
function generateIndex(PDO $pdo, string $outputDir, string $baseUrl): void {
$entries = $pdo->query("SELECT id, title, category, LEFT(content, 200) as excerpt, created_at
FROM admin.knowledge_base WHERE content IS NOT NULL AND content != ''
ORDER BY category, title")->fetchAll(PDO::FETCH_ASSOC);
$grouped = [];
foreach ($entries as $e) {
$grouped[$e['category']][] = $e;
}
$catBlocks = '';
foreach ($grouped as $cat => $items) {
$catHtml = htmlspecialchars($cat, ENT_QUOTES, 'UTF-8');
$count = count($items);
$catBlocks .= "<div class='cat-section'><h2>{$catHtml} <span class='count'>({$count})</span></h2><div class='articles'>";
foreach ($items as $item) {
$slug = slugify($item['title']);
$title = htmlspecialchars($item['title'], ENT_QUOTES, 'UTF-8');
$excerpt = htmlspecialchars(substr(strip_tags($item['excerpt']), 0, 120), ENT_QUOTES, 'UTF-8');
$catBlocks .= "<a href='{$slug}.html' class='article-card'><div class='article-title'>{$title}</div><div class='article-excerpt'>{$excerpt}...</div></a>";
}
$catBlocks .= "</div></div>";
}
$totalEntries = count($entries);
$totalCats = count($grouped);
$indexHtml = <<<HTML
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WEVIA Knowledge Base — WEVAL Consulting | IA, Cloud, Blockchain, Cybersécurité</title>
<meta name="description" content="Base de connaissances WEVIA par WEVAL Consulting. {$totalEntries} articles sur l'IA, le Cloud, la Blockchain, la Cybersécurité, l'Email Marketing et plus.">
<meta property="og:title" content="WEVIA Knowledge Base — WEVAL Consulting">
<meta property="og:type" content="website">
<meta property="og:url" content="{$baseUrl}/">
<link rel="canonical" href="{$baseUrl}/">
<meta name="robots" content="index, follow">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "WEVIA Knowledge Base",
"url": "{$baseUrl}/",
"publisher": {"@type": "Organization", "name": "WEVAL Consulting"},
"description": "Base de connaissances sur l'IA, Cloud, Blockchain, Cybersécurité et transformation digitale"
}
</script>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:'Segoe UI',system-ui,sans-serif; background:#0a0f1a; color:#e2e8f0; }
.hero { background:linear-gradient(135deg,#0c1220,#1a1040 50%,#0a2020); padding:60px 20px; text-align:center; }
.hero h1 { color:#22d3ee; font-size:36px; margin-bottom:10px; }
.hero p { color:#94a3b8; font-size:16px; }
.hero .stats { margin-top:16px; color:#64748b; font-size:14px; }
.hero .stats span { color:#34d399; font-weight:700; }
.container { max-width:1000px; margin:0 auto; padding:30px 20px; }
.cat-section { margin-bottom:30px; }
.cat-section h2 { color:#a78bfa; font-size:20px; border-bottom:1px solid #1e293b; padding-bottom:8px; margin-bottom:14px; }
.cat-section .count { color:#64748b; font-size:14px; font-weight:400; }
.articles { display:grid; grid-template-columns:repeat(auto-fill, minmax(280px, 1fr)); gap:12px; }
.article-card { display:block; background:#0c1220; border:1px solid #1e293b; border-radius:8px; padding:14px; text-decoration:none; transition:border-color 0.2s; }
.article-card:hover { border-color:#22d3ee; }
.article-title { color:#e2e8f0; font-weight:600; font-size:14px; margin-bottom:6px; }
.article-excerpt { color:#64748b; font-size:12px; line-height:1.5; }
.footer { text-align:center; padding:30px; color:#475569; font-size:12px; border-top:1px solid #1e293b; }
.footer a { color:#22d3ee; text-decoration:none; }
</style>
</head>
<body>
<div class="hero">
<h1>🧠 WEVIA Knowledge Base</h1>
<p>Base de connaissances WEVAL Consulting — Intelligence Artificielle, Cloud, Blockchain, Cybersécurité</p>
<div class="stats"><span>{$totalEntries}</span> articles · <span>{$totalCats}</span> catégories</div>
</div>
<div class="container">
{$catBlocks}
</div>
<div class="footer">
<p><a href="https://weval-consulting.com">WEVAL Consulting</a> — Expert en transformation digitale</p>
<p>Casablanca, Maroc | Paris, France</p>
</div>
</body>
</html>
HTML;
file_put_contents($outputDir . 'index.html', $indexHtml);
}
function generateSitemap(PDO $pdo, string $outputDir, string $baseUrl): int {
$entries = $pdo->query("SELECT title, updated_at FROM admin.knowledge_base WHERE content IS NOT NULL AND content != '' ORDER BY updated_at DESC")->fetchAll(PDO::FETCH_ASSOC);
$xml = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
$xml .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
// Index page
$xml .= " <url><loc>{$baseUrl}/</loc><changefreq>weekly</changefreq><priority>1.0</priority></url>\n";
foreach ($entries as $e) {
$slug = slugify($e['title']);
$date = date('Y-m-d', strtotime($e['updated_at'] ?? 'now'));
$xml .= " <url><loc>{$baseUrl}/{$slug}.html</loc><lastmod>{$date}</lastmod><changefreq>monthly</changefreq><priority>0.7</priority></url>\n";
}
$xml .= '</urlset>';
file_put_contents($outputDir . 'sitemap.xml', $xml);
return count($entries);
}
// ============================================================
// ACTIONS
// ============================================================
switch ($action) {
case 'generate':
$entries = $pdo->query("SELECT * FROM admin.knowledge_base WHERE content IS NOT NULL AND content != '' AND LENGTH(content) > 50 ORDER BY category, title")->fetchAll(PDO::FETCH_ASSOC);
$generated = [];
foreach ($entries as $e) {
$slug = generatePage($e, $OUTPUT_DIR, $BASE_URL);
$generated[] = ['id' => $e['id'], 'slug' => $slug, 'title' => $e['title'], 'category' => $e['category']];
}
// Generate index
generateIndex($pdo, $OUTPUT_DIR, $BASE_URL);
// Generate sitemap
$sitemapCount = generateSitemap($pdo, $OUTPUT_DIR, $BASE_URL);
echo json_encode([
'action' => 'generate',
'pages_generated' => count($generated),
'index' => true,
'sitemap' => $sitemapCount . ' URLs',
'output_dir' => $OUTPUT_DIR,
'pages' => array_slice($generated, 0, 20),
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
break;
case 'sitemap':
$count = generateSitemap($pdo, $OUTPUT_DIR, $BASE_URL);
header('Content-Type: application/xml; charset=utf-8');
echo file_get_contents($OUTPUT_DIR . 'sitemap.xml');
exit;
case 'serve':
if (!$slug) die('Missing slug');
$slug = basename($slug, '.html');
$file = $OUTPUT_DIR . $slug . '.html';
if (file_exists($file)) {
header('Content-Type: text/html; charset=utf-8');
echo file_get_contents($file);
} else {
http_response_code(404);
echo '<h1>Page not found</h1>';
}
exit;
case 'status':
default:
$pageCount = count(glob($OUTPUT_DIR . '*.html'));
$sitemapExists = file_exists($OUTPUT_DIR . 'sitemap.xml');
$kbCount = (int)$pdo->query("SELECT COUNT(*) FROM admin.knowledge_base WHERE content IS NOT NULL AND content != '' AND LENGTH(content) > 50")->fetchColumn();
echo json_encode([
'ssr_pages' => $pageCount,
'sitemap' => $sitemapExists,
'kb_entries_eligible' => $kbCount,
'output_dir' => $OUTPUT_DIR,
'needs_generation' => ($pageCount < $kbCount),
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
break;
}