Улучшаем дизайн блога с использованием Bootstrap 5

⏱ 25 мин · 🧩 заданий: 5

Зачем Bootstrap

Писать CSS с нуля долго, а результат часто выглядит «самодельно». Bootstrap — самый популярный CSS-фреймворк: сетка, готовые компоненты (навигация, карточки, кнопки, формы, модальные окна), адаптивность для телефонов. Пятая версия не требует jQuery.

Подключение

Самый быстрый способ — CDN в base.html:

{% load static blog_tags %}
<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{% block title %}{% endblock %} · Мой блог</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
  <link href="{% static 'css/blog.css' %}" rel="stylesheet">
</head>
<body class="bg-light">
  ...
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

meta viewport обязателен для корректного отображения на телефонах. Свой blog.css подключаем после Bootstrap — чтобы переопределять его стили.

Навигация

<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
  <div class="container">
    <a class="navbar-brand" href="{% url 'blog:post_list' %}">Мой блог</a>
    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#nav">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="nav">
      <ul class="navbar-nav me-auto">
        <li class="nav-item"><a class="nav-link" href="{% url 'blog:post_list' %}">Посты</a></li>
        <li class="nav-item"><a class="nav-link" href="{% url 'blog:post_search' %}">Поиск</a></li>
      </ul>
      <ul class="navbar-nav">
        {% if user.is_authenticated %}
          <li class="nav-item"><a class="nav-link" href="{% url 'dashboard' %}">{{ user.username }}</a></li>
          <li class="nav-item">
            <form method="post" action="{% url 'logout' %}">{% csrf_token %}
              <button class="btn btn-link nav-link">Выйти</button>
            </form>
          </li>
        {% else %}
          <li class="nav-item"><a class="nav-link" href="{% url 'login' %}">Войти</a></li>
          <li class="nav-item"><a class="btn btn-primary ms-2" href="{% url 'register' %}">Регистрация</a></li>
        {% endif %}
      </ul>
    </div>
  </div>
</nav>

На узких экранах меню сворачивается в «бургер» — за это отвечает JS-бандл Bootstrap.

Сетка

<div class="container">
  <div class="row">
    <main class="col-lg-8">
      {% block content %}{% endblock %}
    </main>
    <aside class="col-lg-4">
      <div class="card mb-3">
        <div class="card-body">
          <h5 class="card-title">Последние посты</h5>
          {% show_latest_posts 3 %}
        </div>
      </div>
    </aside>
  </div>
</div>

Строка делится на 12 колонок. col-lg-8 — 8 из 12 на больших экранах; на телефонах колонки встают друг под другом.

Карточки постов

{% for post in posts %}
  <article class="card mb-4 shadow-sm">
    <div class="card-body">
      <h2 class="card-title h4"><a href="{{ post.get_absolute_url }}" class="text-decoration-none">{{ post.title }}</a></h2>
      <p class="text-muted small">{{ post.publish|date:"j E Y" }} · {{ post.author }}</p>
      <div class="card-text">{{ post.body|markdown|truncatewords_html:40 }}</div>
      {% for tag in post.tags.all %}
        <a href="{% url 'blog:post_list_by_tag' tag.slug %}" class="badge text-bg-secondary text-decoration-none">{{ tag.name }}</a>
      {% endfor %}
    </div>
  </article>
{% endfor %}

Формы

Вариант 1 — классы в виджетах:

class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ["name", "email", "body"]
        widgets = {
            "name": forms.TextInput(attrs={"class": "form-control"}),
            "email": forms.EmailInput(attrs={"class": "form-control"}),
            "body": forms.Textarea(attrs={"class": "form-control", "rows": 4}),
        }

Вариант 2 — для всех полей сразу:

class BootstrapFormMixin:
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs.setdefault("class", "form-control")

Вариант 3 — пакет django-crispy-forms:

pip install django-crispy-forms crispy-bootstrap5
INSTALLED_APPS += ["crispy_forms", "crispy_bootstrap5"]
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5"
{% load crispy_forms_tags %}
<form method="post">{% csrf_token %}
  {{ form|crispy }}
  <button class="btn btn-primary">Отправить</button>
</form>

Сообщения

Теги уровней Django (success, error) почти совпадают с классами Bootstrap. Поправим error → danger:

from django.contrib.messages import constants as messages

MESSAGE_TAGS = {messages.ERROR: "danger"}
{% for message in messages %}
  <div class="alert alert-{{ message.tags }} alert-dismissible fade show">
    {{ message }}
    <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
  </div>
{% endfor %}

Пагинация

<nav>
  <ul class="pagination">
    {% if page.has_previous %}
      <li class="page-item"><a class="page-link" href="?page={{ page.previous_page_number }}">«</a></li>
    {% endif %}
    {% for num in page.paginator.page_range %}
      <li class="page-item {% if num == page.number %}active{% endif %}">
        <a class="page-link" href="?page={{ num }}">{{ num }}</a>
      </li>
    {% endfor %}
    {% if page.has_next %}
      <li class="page-item"><a class="page-link" href="?page={{ page.next_page_number }}">»</a></li>
    {% endif %}
  </ul>
</nav>

Картинки

<img src="{{ user.profile.photo.url }}" class="img-fluid rounded-circle" alt="">

img-fluid — картинка не шире родителя, rounded-circle — круглая аватарка.

Итог

  • Bootstrap подключается двумя строками (CSS и JS-бандл), плюс meta viewport.
  • Сетка: container → row → col-* (12 колонок).
  • Компоненты: navbar, card, badge, alert, pagination.
  • Поля форм стилизуются через attrs, миксин или django-crispy-forms.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Написать код

Подключите в <head> CSS Bootstrap 5 с CDN jsDelivr (версия 5.3.3): https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css.

Задание 2 Один вариант ответа

Сетка Bootstrap делит строку на сколько колонок?

Задание 3 Написать код

Сверстайте двухколоночный макет: контейнер container, строка row, основная колонка col-lg-8 с блоком {% block content %}{% endblock %} и боковая col-lg-4 с тегом {% show_latest_posts 3 %}.

Задание 4 Несколько вариантов

Как добавить CSS-классы Bootstrap к полям Django-формы?

Задание 5 Ввод ответа

Какой класс Bootstrap делает картинку адаптивной (не шире родителя)?

Войдите, чтобы отмечать пройденные уроки.