335 lines
15 KiB
PHP
Executable File
335 lines
15 KiB
PHP
Executable File
<?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;
|
||
}
|