Делаем вывод статей и страницу отдельной статьи

Файл 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: страница отдельной статьи

php
require 'db_connect.php';
 
$slug = trim($_GET['slug'] ?? '');
if ($slug === '') {
    http_response_code(404);
    echo 'Статья не найдена';
    return;
}

Суть: берём slug из URL (например, ?slug=my-first-post). Если его нет — сразу отдаём 404. Это защита от бессмысленных запросов и лишних обращений к базе.

php
$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 гарантирует, что пользователь видит только опубликованные статьи.

php
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: список статей

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);
 
if (!$result) {
    error_log('Ошибка запроса: ' . mysqli_error($connection));
    echo '<p style="color: red;">Ошибка загрузки списка статей.</p>';
    return;
}

Суть: выбираем все опубликованные статьи и сортируем по дате — новые сверху. Если запрос не прошёл, ошибку пишем в лог сервера (error_log), а пользователю показываем нейтральное сообщение. Так не раскрывается внутренняя структура БД.

php
<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
                <?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; ?>

Суть: логика формирования анонса:

  1. Если у статьи есть meta_description — показываем его.
  2. Если нет — берём content, удаляем из него HTML‑теги (strip_tags), чтобы не сломать верстку списка.
  3. Обрезаем до 200 символов с учётом UTF‑8 (mb_substr, mb_strlen). Для кириллицы это обязательно: обычный substr может разрезать символ пополам.
  4. Добавляем , если текст был обрезан.
  5. Экранируем спецсимволы через 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>&copy; 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
<?php if (empty($_GET)) {require ‘read_post_loop.php’;}?>
<?php if (isset($_GET[‘slug’])) {require ‘read_post.php’;}?>

Что происходит:

  1. empty($_GET) — проверяет, нет ли вообще GET‑параметров. Если нет — подключает read_post_loop.php (список).
  2. isset($_GET['slug']) — проверяет, есть ли параметр slug. Если есть — подключает read_post.php (одна статья).

Пример URL:

https://rcl-radio.ru/test/?slug=tablitsy-v-html-risuem-prostuyu-tablitsu

Что происходит при таком запросе

  1. Браузер отправляет GET‑запрос на адрес https://rcl-radio.ru/test/ и передаёт параметр slug со значением tablitsy-v-html-risuem-prostuyu-tablitsu.
  2. PHP видит параметр: в суперглобальном массиве $_GET появляется запись:
    php
    $_GET['slug'] === 'tablitsy-v-html-risuem-prostuyu-tablitsu'
  3. Срабатывает условие в твоём HTML‑файле:
    php
    if (isset($_GET['slug'])) {
        require 'read_post.php';
    }

    → подключается файл, который выводит одну статью.

  4. Внутри 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 пустой → срабатывает:
    php
    if (empty($_GET)) {
        require 'read_post_loop.php';
    }
  • Подключается файл, который делает SELECT всех опубликованных статей и выводит список.

Откуда в списке берутся такие ссылки

В read_post_loop.php ссылки формируют так:

php
<a href="?slug=<?php echo htmlspecialchars(urlencode($row['slug'] ?? '')); ?>">
    <?php echo htmlspecialchars($row['title'] ?? ''); ?>
</a>

Когда пользователь кликает, браузер берёт текущий адрес и добавляет к нему ?slug=… — получается ровно такой GET‑запрос, как в твоём примере.

Список статей:

Статья:

Добавить комментарий