Файл read_post.php
<?php require 'db_connect.php'; $slug = trim($_GET['slug'] ?? ''); if ($slug === '') { http_response_code(404); echo 'Статья не найдена'; return; } // Подготовленный запрос — защита от SQL‑инъекций $sql = 'SELECT id, title, slug, content, meta_description, is_published, created_at FROM posts WHERE slug = ? AND is_published = 1'; $stmt = mysqli_prepare($connection, $sql); if (!$stmt) { http_response_code(500); echo 'Ошибка подготовки запроса'; return; } mysqli_stmt_bind_param($stmt, 's', $slug); mysqli_stmt_execute($stmt); $result = mysqli_stmt_get_result($stmt); $article = mysqli_fetch_assoc($result); mysqli_stmt_close($stmt); if (!$article) { http_response_code(404); echo 'Статья не найдена'; return; } ?> <title><?php echo htmlspecialchars($article['title']); ?></title> <h1><?php echo htmlspecialchars($article['title']); ?></h1> <small>Опубликовано: <?php echo date('d.m.Y H:i', strtotime($article['created_at'])); ?></small> <div class="article-content"> <?php echo $article['content']; ?> </div>
Файл read_post_loop.php
<?php require_once 'db_connect.php'; $sql = 'SELECT id, title, slug, meta_description, content, created_at FROM posts WHERE is_published = 1 ORDER BY created_at DESC'; $result = mysqli_query($connection, $sql); // Вместо die() делаем безопасный вывод ошибки, чтобы футер не пропал if (!$result) { error_log('Ошибка запроса: ' . mysqli_error($connection)); echo '<p style="color: red;">Ошибка загрузки списка статей.</p>'; return; // Возвращаемся в главный файл, чтобы он дорисовал футер } ?> <h2>Статьи</h2> <?php if (mysqli_num_rows($result) === 0): ?> <p>Статей пока нет.</p> <?php else: ?> <ul class="articles-list"> <?php while ($row = mysqli_fetch_assoc($result)): ?> <li class="article-item"> <h3> <!-- Относительная ссылка: без index.php, чтобы не ломалось при переименовании файла --> <a href="?slug=<?php echo htmlspecialchars(urlencode($row['slug'] ?? '')); ?>"> <?php echo htmlspecialchars($row['title'] ?? ''); ?> </a> </h3> <?php // Логика выбора описания $preview = ''; // 1. Если есть meta_description — берём его if (!empty($row['meta_description'])) { $preview = htmlspecialchars($row['meta_description']); } // 2. Если нет — делаем анонс из content else { $content = $row['content'] ?? ''; // Удаляем HTML-теги, чтобы не сломать верстку списка $plainText = strip_tags($content); // Обрезаем до 200 символов (UTF-8 для кириллицы) $preview = mb_substr($plainText, 0, 200, 'UTF-8'); // Добавляем многоточие, если текст был обрезан if (mb_strlen($plainText, 'UTF-8') > 200) { $preview .= '…'; } $preview = htmlspecialchars($preview); } ?> <!-- Выводим только один раз --> <?php if ($preview !== ''): ?> <p class="excerpt"><?php echo $preview; ?></p> <?php endif; ?> <small class="date"><?php echo date('d.m.Y', strtotime($row['created_at'] ?? 'now')); ?></small> </li> <?php endwhile; ?> </ul> <?php endif; ?>
Этот код решает две задачи:
- показать список опубликованных статей;
- показать одну статью по её slug (короткому адресу).
Разберём оба фрагмента по сути — где что делает и зачем.
Фрагмент 1: страница отдельной статьи
require 'db_connect.php'; $slug = trim($_GET['slug'] ?? ''); if ($slug === '') { http_response_code(404); echo 'Статья не найдена'; return; }
Суть: берём slug из URL (например, ?slug=my-first-post). Если его нет — сразу отдаём 404. Это защита от бессмысленных запросов и лишних обращений к базе.
$sql = 'SELECT id, title, slug, content, meta_description, is_published, created_at FROM posts WHERE slug = ? AND is_published = 1'; $stmt = mysqli_prepare($connection, $sql); if (!$stmt) { http_response_code(500); echo 'Ошибка подготовки запроса'; return; } mysqli_stmt_bind_param($stmt, 's', $slug); mysqli_stmt_execute($stmt); $result = mysqli_stmt_get_result($stmt); $article = mysqli_fetch_assoc($result); mysqli_stmt_close($stmt);
Суть: делаем безопасный запрос через подготовленный оператор (mysqli_prepare). Знак ? — место под значение, которое мы передаём отдельно (bind_param). Это защищает от SQL‑инъекций: даже если кто-то пришлёт злонамеренный slug, он не станет частью SQL‑команды.
Условие AND is_published = 1 гарантирует, что пользователь видит только опубликованные статьи.
if (!$article) { http_response_code(404); echo 'Статья не найдена'; return; } ?> <title><?php echo htmlspecialchars($article['title']); ?></title> <h1><?php echo htmlspecialchars($article['title']); ?></h1> <small>Опубликовано: <?php echo date('d.m.Y H:i', strtotime($article['created_at'])); ?></small> <div class="article-content"> <?php echo $article['content']; ?> </div>
Суть: если статьи нет — снова 404. Если есть — выводим заголовок и дату. htmlspecialchars экранирует спецсимволы: это защита от XSS. То есть если в заголовке окажется <script>, браузер покажет его как текст, а не выполнит.
Контент статьи выводится «как есть» — предполагается, что он уже хранится в безопасном формате (например, HTML без вредоносных скриптов).
Фрагмент 2: список статей
require_once 'db_connect.php'; $sql = 'SELECT id, title, slug, meta_description, content, created_at FROM posts WHERE is_published = 1 ORDER BY created_at DESC'; $result = mysqli_query($connection, $sql); if (!$result) { error_log('Ошибка запроса: ' . mysqli_error($connection)); echo '<p style="color: red;">Ошибка загрузки списка статей.</p>'; return; }
Суть: выбираем все опубликованные статьи и сортируем по дате — новые сверху. Если запрос не прошёл, ошибку пишем в лог сервера (error_log), а пользователю показываем нейтральное сообщение. Так не раскрывается внутренняя структура БД.
<h2>Статьи</h2> <?php if (mysqli_num_rows($result) === 0): ?> <p>Статей пока нет.</p> <?php else: ?> <ul class="articles-list"> <?php while ($row = mysqli_fetch_assoc($result)): ?> <li class="article-item"> <h3> <a href="?slug=<?php echo htmlspecialchars(urlencode($row['slug'] ?? '')); ?>"> <?php echo htmlspecialchars($row['title'] ?? ''); ?> </a> </h3>
Суть: формируем список. Ссылка ведёт на ту же страницу, но с параметром ?slug=…. urlencode делает slug безопасным для URL (пробелы, кириллица и т. п. превращаются в корректный формат). htmlspecialchars защищает заголовок в HTML.
<?php $preview = ''; if (!empty($row['meta_description'])) { $preview = htmlspecialchars($row['meta_description']); } else { $content = $row['content'] ?? ''; $plainText = strip_tags($content); $preview = mb_substr($plainText, 0, 200, 'UTF-8'); if (mb_strlen($plainText, 'UTF-8') > 200) { $preview .= '…'; } $preview = htmlspecialchars($preview); } ?> <?php if ($preview !== ''): ?> <p class="excerpt"><?php echo $preview; ?></p> <?php endif; ?> <small class="date"><?php echo date('d.m.Y', strtotime($row['created_at'] ?? 'now')); ?></small> </li> <?php endwhile; ?> </ul> <?php endif; ?>
Суть: логика формирования анонса:
- Если у статьи есть
meta_description— показываем его. - Если нет — берём
content, удаляем из него HTML‑теги (strip_tags), чтобы не сломать верстку списка. - Обрезаем до 200 символов с учётом UTF‑8 (
mb_substr,mb_strlen). Для кириллицы это обязательно: обычныйsubstrможет разрезать символ пополам. - Добавляем
…, если текст был обрезан. - Экранируем спецсимволы через
htmlspecialchars.
Ключевые моменты, где это уместно
- Подготовленные запросы — всегда для выборки по параметрам (slug, ID, фильтры). Это базовая защита от SQL‑инъекций.
- Проверка
is_published— везде, где контент видят пользователи. Черновики не должны быть доступны по прямым ссылкам. htmlspecialchars— при выводе любых данных, которые могут содержать спецсимволы или быть под контролем пользователя (заголовки, описания).urlencode— при вставке значений в URL.strip_tags+mb_*функции — когда нужно сделать анонс из HTML‑контента и корректно обрезать его для кириллицы.- Логирование ошибок вместо вывода деталей — чтобы не показывать пользователю внутреннюю структуру БД и не помогать злоумышленникам.
Как выводить статью или список статей
Файл index.php
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Блог: полезные статьи</title> <link rel="stylesheet" href="site.css"> </head> <body> <div class="site-wrapper"> <!-- Новая обёртка --> <header> <div class="container header-content"> <a href="./" class="logo">Website</a> <nav> <ul> <li><a href="./">Главная</a></li> <li><a href="/articles">Статьи</a></li> <li><a href="/about">О проекте</a></li> </ul> </nav> </div> </header> <div class="main-wrapper"> <aside class="sidebar-left"> <!-- Левая панель --> Левая панель </aside> <main class="container"> <nav><a href="add_form.php">Добавить статью</a></nav> <hr> <?php if (empty($_GET)) {require 'read_post_loop.php';}?> <?php if (isset($_GET['slug'])) {require 'read_post.php';}?> </main> <aside class="sidebar-right"> Правая панель <!-- Правая панель --> </aside> </div> <footer> <p>© 2026 Website. Все права защищены.</p> </footer> </div> </body> </html>
Вывод статьи или списка статей здесь целиком зависит от GET‑запроса — то есть от параметров в URL.
Как это работает на уровне запроса
- Если URL такой:
site.com/илиsite.com/articles(без параметров) →$_GETпустой → показываем список статей. - Если URL такой:
site.com/?slug=my-post→ в$_GET['slug']есть значение → показываем одну статью с этим slug.
Именно параметр slug в GET‑запросе говорит скрипту: «покажи не список, а конкретную запись».
Где в коде это реализовано
В HTML‑файле стоит такая логика (только она отвечает за выбор, что выводить):
<?php if (empty($_GET)) {require ‘read_post_loop.php’;}?><?php if (isset($_GET[‘slug’])) {require ‘read_post.php’;}?>
Что происходит:
empty($_GET)— проверяет, нет ли вообще GET‑параметров. Если нет — подключаетread_post_loop.php(список).isset($_GET['slug'])— проверяет, есть ли параметрslug. Если есть — подключаетread_post.php(одна статья).
Пример URL:
https://rcl-radio.ru/test/?slug=tablitsy-v-html-risuem-prostuyu-tablitsu
Что происходит при таком запросе
- Браузер отправляет GET‑запрос на адрес
https://rcl-radio.ru/test/и передаёт параметрslugсо значениемtablitsy-v-html-risuem-prostuyu-tablitsu. - PHP видит параметр: в суперглобальном массиве
$_GETпоявляется запись:php$_GET['slug'] === 'tablitsy-v-html-risuem-prostuyu-tablitsu'
- Срабатывает условие в твоём HTML‑файле:
php
if (isset($_GET['slug'])) { require 'read_post.php'; }
→ подключается файл, который выводит одну статью.
- Внутри
read_post.phpзначение берут из GET и используют в запросе к БД:php$slug = trim($_GET['slug'] ?? ''); // далее: WHERE slug = ? AND is_published = 1
Система ищет в таблице
postsстроку, гдеslugточно совпадает с тем, что пришёл в URL. Если находит и статья опубликована (is_published = 1) — показывает её. Если нет — отдаёт 404.
А если URL без slug (например, https://rcl-radio.ru/test/)
Тогда:
$_GETпустой → срабатывает:phpif (empty($_GET)) { require 'read_post_loop.php'; }
- Подключается файл, который делает
SELECTвсех опубликованных статей и выводит список.
Откуда в списке берутся такие ссылки
В read_post_loop.php ссылки формируют так:
<a href="?slug=<?php echo htmlspecialchars(urlencode($row['slug'] ?? '')); ?>"> <?php echo htmlspecialchars($row['title'] ?? ''); ?> </a>
Когда пользователь кликает, браузер берёт текущий адрес и добавляет к нему ?slug=… — получается ровно такой GET‑запрос, как в твоём примере.
Список статей:

Статья:

