Files
site_aegisone/blog/index.php
T
2026-05-17 05:22:06 +03:00

350 lines
19 KiB
PHP

<?php
/**
* AegisOne Engineering — Блог
*
* Два режима: список статей (/?cat=X&page=N&search=) или отдельная статья (/?slug=article-name)
* Использует собственную вёрстку (тёмный градиентный хедер, боковая панель).
*/
require_once __DIR__ . '/../config.php';
require_once __DIR__ . '/../data/db-config.php';
function getDB(): PDO {
static $pdo = null;
if ($pdo === null) {
if (DB_TYPE === 'pgsql') {
$dsn = 'pgsql:host=' . DB_HOST . ';port=' . DB_PORT . ';dbname=' . DB_NAME;
} else {
$dsn = 'mysql:host=' . DB_HOST . ';port=' . DB_PORT . ';dbname=' . DB_NAME . ';charset=' . DB_CHARSET;
}
$pdo = new PDO($dsn, DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
}
return $pdo;
}
$pdo = getDB();
$categories = [
'audit' => 'Аудит',
'sla' => 'SLA',
'incident' => 'Инциденты',
'supervision' => 'Технадзор',
'documentation' => 'Документация',
'risk' => 'Риск-инжиниринг',
'cases' => 'Кейсы',
];
$slug = trim($_GET['slug'] ?? '');
$search = trim($_GET['search'] ?? '');
$category = trim($_GET['cat'] ?? '');
$page = max(1, (int)($_GET['page'] ?? 1));
$perPage = 12;
// Single post
if ($slug !== '') {
$stmt = $pdo->prepare("SELECT bp.*, u.full_name AS author_name FROM blog_posts bp LEFT JOIN users u ON u.id=bp.author_id WHERE bp.slug=? AND bp.status='published' LIMIT 1");
$stmt->execute([$slug]);
$post = $stmt->fetch();
$pageTitle = $post ? ($post['title'] . ' — ' . SITE_NAME) : 'Пост не найден — ' . SITE_NAME;
$pageDesc = $post ? ($post['excerpt'] ?: mb_substr(strip_tags($post['content']), 0, 200)) : '';
$robots = $post ? 'index, follow' : 'noindex, nofollow';
$canonical = $post ? '/blog/' . $post['slug'] : '/blog/';
} else {
$pageTitle = 'Блог — ' . SITE_NAME;
$pageDesc = 'Инженерный блог AegisOne Engineering: риск-инжиниринг, SLA, инциденты, аудит систем безопасности.';
$robots = 'index, follow';
$canonical = '/blog/';
}
?>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $pageTitle ?></title>
<meta name="description" content="<?= $pageDesc ?>">
<link rel="canonical" href="https://<?= $_SERVER['HTTP_HOST'] ?><?= $canonical ?>">
<meta name="robots" content="<?= $robots ?>">
<link rel="icon" type="image/x-icon" href="/assets/img/favicon.ico">
<link rel="stylesheet" href="/style.php">
<?php if (file_exists(__DIR__ . '/../inc/analytics.php')) require __DIR__ . '/../inc/analytics.php'; ?>
<style>
:root { --bg-body:#0a0e1a; --bg-card:#111827; --bg-input:#1a1f2e; --text-primary:#e5e7eb; --text-secondary:#9ca3af; --text-muted:#6b7280; --border:#1f2937; --radius:10px; --font:'Open Sans',Arial,sans-serif; }
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:var(--font); background:var(--bg-body); color:var(--text-primary); min-height:100vh; }
a { color:inherit; text-decoration:none; }
.blog-header { background:linear-gradient(135deg,#0a0e1a 0%,#0d1a30 100%); padding:48px 24px; text-align:center; border-bottom:1px solid var(--border); }
.blog-header h1 { font-size:32px; font-weight:800; margin-bottom:8px; }
.blog-header p { color:var(--text-secondary); max-width:600px; margin:0 auto; }
.blog-layout { max-width:1100px; margin:0 auto; padding:32px 24px; display:grid; grid-template-columns:200px 1fr; gap:32px; }
.blog-sidebar { position:sticky; top:24px; align-self:start; }
.blog-sidebar a { display:block; padding:6px 0; font-size:13px; color:var(--text-secondary); }
.blog-sidebar a:hover,.blog-sidebar a.active { color:var(--accent); }
.blog-sidebar h3 { font-size:11px; text-transform:uppercase; color:var(--text-muted); margin-bottom:8px; letter-spacing:1px; }
.blog-grid { display:grid; gap:20px; }
.blog-card { background:var(--bg-card); border:1px solid var(--border); border-radius:var(--radius); padding:24px; transition:border-color 0.2s; }
.blog-card:hover { border-color:var(--accent); }
.blog-card .cat { font-size:11px; font-weight:700; text-transform:uppercase; color:var(--accent); letter-spacing:0.5px; margin-bottom:6px; }
.blog-card h2 { font-size:18px; font-weight:700; margin-bottom:8px; }
.blog-card h2 a:hover { color:var(--accent); }
.blog-card p { font-size:13px; color:var(--text-secondary); line-height:1.6; }
.blog-card .meta { font-size:11px; color:var(--text-muted); margin-top:12px; }
.search-form { display:flex; gap:8px; margin-bottom:20px; }
.search-form input { flex:1; padding:8px 14px; background:var(--bg-input); border:1px solid var(--border); border-radius:6px; color:var(--text-primary); font-size:13px; font-family:var(--font); }
.search-form input:focus { outline:none; border-color:var(--accent); }
.search-form button { padding:8px 16px; background:var(--accent); color:#fff; border:none; border-radius:6px; cursor:pointer; }
.blog-post { max-width:800px; margin:0 auto; padding:40px 0; }
.blog-post h1 { font-size:28px; margin-bottom:16px; }
.blog-meta { display:flex; gap:16px; font-size:13px; color:var(--text-muted); margin-bottom:24px; }
.blog-category { color:var(--accent); font-weight:600; }
.blog-content { line-height:1.8; font-size:15px; }
.blog-content p { margin-bottom:16px; }
.blog-share { margin:32px 0; padding:16px 0; border-top:1px solid var(--border); display:flex; gap:12px; align-items:center; font-size:13px; }
.blog-share a { color:var(--accent); }
.blog-nav { display:flex; justify-content:space-between; gap:16px; margin-top:24px; }
.blog-nav a { font-size:13px; color:var(--accent); }
.empty-state { text-align:center; padding:60px 20px; }
.empty-state h2 { font-size:24px; margin-bottom:12px; }
.empty-state p { color:var(--text-secondary); margin-bottom:24px; }
.pagination { display:flex; justify-content:center; gap:8px; margin-top:32px; }
.pagination a { padding:8px 14px; background:var(--bg-card); border:1px solid var(--border); border-radius:6px; font-size:13px; }
.pagination a.current { background:var(--accent); color:#fff; border-color:var(--accent); }
.filter-bar { display:flex; gap:12px; align-items:center; margin-bottom:20px; }
.btn-secondary { background:var(--bg-card); color:var(--text-secondary); padding:6px 14px; border-radius:6px; font-size:12px; }
.btn-secondary:hover { border-color:var(--accent); color:var(--accent); }
.blog-footer { text-align:center; padding:24px; border-top:1px solid var(--border); margin-top:40px; }
.blog-footer a { color:var(--text-muted); font-size:13px; }
.blog-footer a:hover { color:var(--accent); }
@media (max-width:768px) { .blog-layout { grid-template-columns:1fr; } .blog-sidebar { position:static; } .blog-post h1 { font-size:22px; } }
.blog-grid.loading { opacity:0.5; pointer-events:none; }
.blog-grid.loading::after { content:'Загрузка...'; position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); color:var(--text-muted); font-size:14px; }
</style>
</head>
<body>
<?php if (file_exists(__DIR__ . '/../inc/analytics-body.php')) require __DIR__ . '/../inc/analytics-body.php'; ?>
<header class="blog-header">
<h1>Инженерный блог AegisOne</h1>
<p>Управление рисками, SLA, инциденты и инженерная экспертиза</p>
<p style="margin-top:16px;"><a href="/" style="color:var(--text-muted);font-size:13px;">← На главную</a></p>
</header>
<?php if ($slug !== '' && isset($post) && $post): ?>
<!-- Single post view -->
<main class="blog-post">
<article>
<h1><?= htmlspecialchars($post['title']) ?></h1>
<div class="blog-meta">
<span><?= date('d.m.Y', strtotime($post['published_at'])) ?></span>
<span class="blog-category"><?= htmlspecialchars($categories[$post['category']] ?? $post['category']) ?></span>
<?php if ($post['author_name']): ?><span><?= htmlspecialchars($post['author_name']) ?></span><?php endif; ?>
</div>
<div class="blog-content"><?= $post['content'] ?></div>
</article>
<div class="blog-share">
<span>Поделиться:</span>
<a href="https://t.me/share/url?url=<?= urlencode('https://' . $_SERVER['HTTP_HOST'] . '/blog/' . $post['slug']) ?>" target="_blank">Telegram</a>
<a href="https://wa.me/?text=<?= urlencode(htmlspecialchars($post['title']) . ' — https://' . $_SERVER['HTTP_HOST'] . '/blog/' . $post['slug']) ?>" target="_blank">WhatsApp</a>
</div>
<div class="blog-nav">
<?php
$prev = $pdo->prepare("SELECT slug,title FROM blog_posts WHERE published_at<? AND status='published' ORDER BY published_at DESC LIMIT 1");
$prev->execute([$post['published_at']]); $p = $prev->fetch();
$next = $pdo->prepare("SELECT slug,title FROM blog_posts WHERE published_at>? AND status='published' ORDER BY published_at ASC LIMIT 1");
$next->execute([$post['published_at']]); $n = $next->fetch();
?>
<?php if ($p): ?><a href="/blog/<?= $p['slug'] ?>">← <?= htmlspecialchars($p['title']) ?></a><?php endif; ?>
<?php if ($n): ?><a href="/blog/<?= $n['slug'] ?>"><?= htmlspecialchars($n['title']) ?> →</a><?php endif; ?>
</div>
</main>
<?php elseif ($slug !== '' && (!isset($post) || !$post)): ?>
<!-- Post not found -->
<div class="empty-state">
<h2>Пост не найден</h2>
<p>Возможно, он был удалён или ещё не опубликован.</p>
<a href="/blog/" class="btn-secondary" style="display:inline-block;padding:10px 24px;">← Все записи</a>
</div>
<?php else: ?>
<!-- Listing view -->
<?php
$where = "status='published'";
$params = [];
if ($category !== '' && isset($categories[$category])) {
$where .= " AND category=?";
$params[] = $category;
}
if ($search !== '') {
$where .= " AND (title LIKE ? OR excerpt LIKE ? OR content LIKE ?)";
$searchTerm = '%' . $search . '%';
$params[] = $searchTerm;
$params[] = $searchTerm;
$params[] = $searchTerm;
}
$countStmt = $pdo->prepare("SELECT COUNT(*) FROM blog_posts WHERE $where");
$countStmt->execute($params);
$totalPosts = (int)$countStmt->fetchColumn();
$totalPages = max(1, (int)ceil($totalPosts / $perPage));
$offset = ($page - 1) * $perPage;
$stmt = $pdo->prepare("SELECT bp.*, u.full_name AS author_name FROM blog_posts bp LEFT JOIN users u ON u.id=bp.author_id WHERE $where ORDER BY bp.published_at DESC LIMIT $perPage OFFSET $offset");
$stmt->execute($params);
$posts = $stmt->fetchAll();
?>
<div class="blog-layout">
<aside class="blog-sidebar">
<h3>Категории</h3>
<a href="#" data-cat="" class="cat-link <?= $category === '' ? 'active' : '' ?>">Все статьи</a>
<?php foreach ($categories as $key => $label): ?>
<a href="#" data-cat="<?= $key ?>" class="cat-link <?= $category === $key ? 'active' : '' ?>"><?= htmlspecialchars($label) ?></a>
<?php endforeach; ?>
<h3 style="margin-top:20px;">Поиск</h3>
<form method="get" class="search-form" style="flex-direction:column;" id="blogSearchForm">
<input type="text" name="search" placeholder="Поиск..." value="<?= htmlspecialchars($search) ?>" id="blogSearchInput">
<button type="submit">Найти</button>
</form>
</aside>
<main>
<?php if ($category !== ''): ?>
<div class="filter-bar">
<span style="color:var(--text-muted);">Категория:</span>
<a href="/blog/" class="btn-secondary">Все записи</a>
</div>
<?php endif; ?>
<?php if (empty($posts)): ?>
<div class="empty-state">
<h2>Записей пока нет</h2>
<p>Следите за обновлениями — мы регулярно публикуем новые статьи.</p>
</div>
<?php else: ?>
<div class="blog-grid" id="blogGrid">
<?php foreach ($posts as $p): ?>
<article class="blog-card">
<div class="cat"><?= htmlspecialchars($categories[$p['category']] ?? $p['category']) ?></div>
<h2><a href="/blog/<?= htmlspecialchars($p['slug']) ?>"><?= htmlspecialchars($p['title']) ?></a></h2>
<?php if ($p['excerpt']): ?><p><?= htmlspecialchars($p['excerpt']) ?></p><?php endif; ?>
<div class="meta">
<?= $p['published_at'] ? date('d.m.Y', strtotime($p['published_at'])) : date('d.m.Y', strtotime($p['created_at'])) ?>
<?= $p['author_name'] ? ' · ' . htmlspecialchars($p['author_name']) : '' ?>
</div>
</article>
<?php endforeach; ?>
</div>
<?php if ($totalPages > 1): ?>
<div class="pagination" id="blogPagination">
<?php if ($page > 1): ?>
<a href="#" data-page="<?= $page - 1 ?>" data-cat="<?= htmlspecialchars($category) ?>" data-search="<?= htmlspecialchars($search) ?>">← Назад</a>
<?php endif; ?>
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
<a href="#" data-page="<?= $i ?>" data-cat="<?= htmlspecialchars($category) ?>" data-search="<?= htmlspecialchars($search) ?>" class="<?= $i === $page ? 'current' : '' ?>"><?= $i ?></a>
<?php endfor; ?>
<?php if ($page < $totalPages): ?>
<a href="#" data-page="<?= $page + 1 ?>" data-cat="<?= htmlspecialchars($category) ?>" data-search="<?= htmlspecialchars($search) ?>">Вперёд →</a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endif; ?>
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
<a href="/blog/?page=<?= $i ?><?= $category ? '&cat=' . $category : '' ?><?= $search ? '&search=' . urlencode($search) : '' ?>" class="<?= $i === $page ? 'current' : '' ?>"><?= $i ?></a>
<?php endfor; ?>
<?php if ($page < $totalPages): ?>
<a href="/blog/?page=<?= $page + 1 ?><?= $category ? '&cat=' . $category : '' ?><?= $search ? '&search=' . urlencode($search) : '' ?>">Вперёд →</a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</main>
</div>
<?php endif; ?>
<div class="blog-footer">
<a href="/">← AegisOne Engineering — главная</a>
</div>
<script>
(function() {
var grid = document.getElementById('blogGrid');
var pagination = document.getElementById('blogPagination');
var catLinks = document.querySelectorAll('.cat-link');
var searchForm = document.getElementById('blogSearchForm');
var searchInput = document.getElementById('blogSearchInput');
function loadPosts(cat, page, search) {
if (!grid) return;
grid.classList.add('loading');
var url = '/blog/api.php?cat=' + encodeURIComponent(cat) + '&page=' + page + '&search=' + encodeURIComponent(search);
fetch(url)
.then(function(res) { return res.json(); })
.then(function(data) {
var container = grid.parentElement;
container.innerHTML = data.html;
// Rebind pagination clicks
container.querySelectorAll('.pagination a').forEach(function(a) {
a.addEventListener('click', function(e) {
e.preventDefault();
loadPosts(this.dataset.cat || '', this.dataset.page, this.dataset.search || '');
});
});
// Update URL without reload
var newUrl = '/blog/';
var params = [];
if (cat) params.push('cat=' + cat);
if (page > 1) params.push('page=' + page);
if (search) params.push('search=' + encodeURIComponent(search));
if (params.length) newUrl += '?' + params.join('&');
history.pushState(null, '', newUrl);
// Update active category
catLinks.forEach(function(link) {
link.classList.toggle('active', link.dataset.cat === cat);
});
grid.classList.remove('loading');
})
.catch(function() {
grid.classList.remove('loading');
});
}
// Category clicks
catLinks.forEach(function(link) {
link.addEventListener('click', function(e) {
e.preventDefault();
loadPosts(this.dataset.cat, 1, searchInput ? searchInput.value : '');
});
});
// Pagination clicks (initial)
if (pagination) {
pagination.querySelectorAll('a').forEach(function(a) {
a.addEventListener('click', function(e) {
e.preventDefault();
loadPosts(this.dataset.cat, this.dataset.page, this.dataset.search);
});
});
}
// Search submit
if (searchForm) {
searchForm.addEventListener('submit', function(e) {
e.preventDefault();
loadPosts('', 1, searchInput.value);
});
}
})();
</script>
<?php if (file_exists(__DIR__ . '/../inc/analytics.php')) require __DIR__ . '/../inc/analytics.php'; ?>
</body>
</html>