HTML — Основи мови розмітки¶
HTML (HyperText Markup Language) — мова розмітки, яка описує структуру веб-сторінки. HTML не програмує логіку — він каже браузеру ЩО показати. CSS каже ЯК це виглядає. JavaScript — ЩО відбувається при взаємодії.
🧠 Ментальна модель: HTML — це РЕЦЕПТ, а не програма. Браузер — це КУХАРЬщо цей рецепт виконує. DOM — це ГОТОВА СТРАВА на столі.
Уяви, що Django — це кухня ресторану. Коли клієнт (браузер) робить замовлення (HTTP запит), кухня (Django) бере шаблон рецепту (HTML-шаблон), додає інгредієнти (дані з БД через context), і відправляє готовий рецепт (рядок HTML) на стіл. Але браузер — це не просто офіціант, який кладе рецепт на стіл: він сам готує страву (DOM) з цього рецепту, відображаючи пікселі на екрані.
📚 Чому це існує: Розподіл обов'язків — сервер відповідає за дані і структуру, браузер — за рендеринг. Django не знає і не повинен знати, яка ширина екрану у користувача, яка версія браузера, чи підтримує він CSS grid. Це все — справа браузера.
🌐 Повний шлях від URL до пікселів:
Користувач вводить URL у адресному рядку ↓ DNS-резолюція: example.com → 93.184.216.34 ↓ TCP-з'єднання з сервером (порт 80/443) ↓ HTTP GET запит: GET /notes/ HTTP/1.1 ↓ Nginx (reverse proxy) → Django WSGI/ASGI ↓ Django: urls.py → views.py → template render ↓ Django генерує HTML-рядок (усі {{ }} і {% %} замінено) ↓ HTTP Response: 200 OK + HTML байти по мережі ↓ Browser HTML Parser (токенізація → побудова дерева) ↓ DOM Tree у пам'яті браузера ↓ (паралельно завантажуються CSS) CSSOM Tree ↓ Render Tree = DOM + CSSOM ↓ Layout Engine (обчислює координати кожного елемента) ↓ Paint (кольори, тіні, borders) ↓ Composite (шари на GPU) ↓ Пікселі на екрані користувача❌ Типова помилка початківця: Думати, що HTML — це програма, яку браузер "виконує" рядок за рядком як Python-скрипт. Насправді браузер будує об'єктну модель (DOM) і рендерить її цілком. Через це браузер не "падає" на помилках — він мовчки виправляє і продовжує.
0. Як браузер інтерпретує HTML — Core Mechanism¶
Від байтів до пікселів: Pipeline¶
Django Template Render
↓ (генерує HTML-рядок)
HTTP Response (байти по мережі)
↓
Browser HTML Parser
↓ (Tokenization → Tree Construction)
DOM Tree (об'єктна модель у пам'яті)
↓ (разом з CSS → Render Tree)
Layout Engine (обчислює розміри і позиції)
↓
Paint (пікселі на екрані)
DOM — Document Object Model¶
Коли браузер отримує HTML-документ — він не виконує його як програму. Він парсить документ і будує в пам'яті дерево об'єктів — DOM.
Document
└── <html>
├── <head>
│ ├── <meta charset="UTF-8">
│ └── <title>"Мій сайт"</title>
└── <body>
├── <header>
│ └── <nav>
│ ├── <a href="/">"Головна"</a>
│ └── <a href="/about/">"Про нас"</a>
├── <main>
│ ├── <h1>"Привіт!"</h1>
│ └── <p>"Текст..."</p>
└── <footer>"© 2026"</footer>
- DOM — живе дерево об'єктів. JavaScript може змінювати його після завантаження.
- HTML Source (Ctrl+U) — статичний текст який надіслав Django. Може відрізнятись від DOM.
Правило для дебагінгу: Якщо верстка виглядає не так — відкрий DevTools (F12) → Elements. Там ти бачиш живий DOM, а не сирий HTML з сервера.
Браузерне прощення — Error Recovery¶
Браузер не крашиться на зламаному HTML. Він мовчки виправляє помилки:
<!-- Написано (без закриваючих тегів) -->
<ul><li>Пункт 1<li>Пункт 2</ul>
<!-- DOM після виправлення браузером -->
<ul>
<li>Пункт 1</li> ← браузер додав </li>
<li>Пункт 2</li> ← браузер додав </li>
</ul>
<!-- Неправильне вкладення -->
<b><i>текст</b></i>
<!-- Браузер виправить на правильне -->
<b><i>текст</i></b>
Проблема: браузер маскує помилки візуально, але зламана структура DOM: - Руйнує SEO (Google бачить інше дерево) - Ламає скрінрідери (accessibility) - Спричиняє дивні баги в JavaScript (querySelector знаходить не той елемент)
HTML та принципи дизайну¶
| HTML концепція | Принцип дизайну |
|---|---|
<div> групує елементи |
Наближеність (Proximity) — пов'язані елементи поруч |
<h1> → <h6> ієрархія |
Контраст — важливіше = більший/жирніший |
<ul> / <ol> списки |
Вирівнювання (Alignment) — єдина лінія ліворуч |
class="card" на всіх картках |
Повторення — однаковий стиль = єдина система |
<header>, <nav>, <main> |
Навігаційний дизайн — "Де я? Що тут є?" |
Ментальна модель: HTML — кістяк будівлі: де стіни, де кімнати. DOM — 3D-модель у CAD архітектора: жива, можна змінювати. CSS — інтер'єр і фасад: кольори, розміри, позиції. JavaScript — ліфт і двері: динамічна поведінка після завантаження.
Реальне значення для backend-розробника¶
- SSR (Server-Side Rendering): Django генерує повний HTML до відправки браузеру
- SEO: Google-краулер читає HTML з сервера — правильна структура = кращий рейтинг
- First Contentful Paint (FCP): Готовий HTML = користувач бачить контент одразу, ще до JS
- Accessibility: Правильна семантика = скрінрідери читають сторінку правильно
🧠 Ментальна модель: HTML-документ — це матрьошка (лялька в ляльці). DOCTYPE загортає все. Всередині —
<html>, яка містить дві ляльки:<head>(невидима, для браузера і пошукових систем) і<body>(видима, для людини). У<body>ще більше ляльок — sections, articles, divs. Кожна лялька знає тільки своїх прямих дітей, але вся ієрархія утворює DOM-дерево.📚 Чому DOCTYPE існує: До HTML5 (2008) браузери мали два режими роботи: "quirks mode" (сумісність зі старими глючними сайтами 90-х) і "standards mode" (правильний CSS box model).
<!DOCTYPE html>— це команда браузеру: "Вмикай стандартний режим, HTML5". Без DOCTYPE Internet Explorer переключався в quirks mode, де, наприклад, box-sizing поводився інакше, і вся верстка "їхала". HTML5 спростив DOCTYPE до мінімальної форми<!DOCTYPE html>— більше нічого не потрібно.🌐 Що робить
lang="uk"в браузері: (1) Google розуміє мову сторінки і показує її відповідній аудиторії в пошуку; (2) Скрінрідери (NVDA, VoiceOver) перемикають голос і вимову на відповідну мову; (3) Браузерні спелчекери знають якою мовою перевіряти орфографію у<textarea>; (4) Функції перекладу (Chrome Translate) пропонують переклад правильної мови.❌ Типова помилка початківця: Пропускати
<meta name="viewport">. Без нього мобільні браузери рендерять сторінку у "desktop-ширині" (~980px) і зменшують її до розміру екрану — текст стає мікроскопічним.content="width=device-width, initial-scale=1.0"каже браузеру: ширина viewport = ширина пристрою, масштаб = 1:1.
1. Анатомія HTML-документа¶
Цей блок коду — це абсолютний мінімум кожного HTML-документа. Браузер очікує саме цю структуру. <head> містить метадані — інструкції для браузера і пошукових систем, які не відображаються на сторінці. <body> — все що бачить і з чим взаємодіє користувач. Зверни увагу: <link rel="stylesheet"> у <head> завантажується до рендерингу — це критично для того, щоб сторінка не "моргала" ненастилізованим контентом (FOUC — Flash of Unstyled Content).
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Заголовок вкладки браузера</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- Весь видимий вміст тут -->
<h1>Привіт, HTML!</h1>
</body>
</html>
| Елемент | Роль |
|---|---|
<!DOCTYPE html> |
Оголошує браузеру що це HTML5 (не HTML4 чи XHTML) |
<html lang="uk"> |
Корінь документа, lang — для пошукових систем і скрінрідерів |
<head> |
Метадані — не видимі на сторінці (charset, title, CSS, meta) |
<meta charset="UTF-8"> |
Кодування: підтримка кирилиці, emoji, всіх мов |
<meta name="viewport"> |
Адаптивний дизайн: масштаб 1:1 на мобільних |
<title> |
Текст у вкладці браузера і результатах Google |
<body> |
Весь видимий вміст сторінки |
🧠 Ментальна модель: Тег — це конверт з листом. Відкриваючий тег
<a>— передня сторона конверту з адресою (атрибути). Вміст між тегами — лист всередині. Закриваючий тег</a>— заклеєна задня сторона. Void elements (<img>,<br>,<input>) — це листівки: вони самі є повідомленням, їм не потрібен конверт-обгортка.📚 Чому існують атрибути: HTML без атрибутів — це тільки структура. Атрибути додають поведінку і контекст.
hrefу<a>— куди вести посилання.srcу<img>— яке зображення показати.typeу<input>— який тип поля (text, email, number, checkbox). Без атрибутів теги були б занадто обмеженими.🌐 Як браузер бачить це: Браузер читає відкриваючий тег, потім його атрибути (зліва направо), потім вміст, потім закриваючий тег. Атрибути без значення (
required,disabled,checked) — це булеві атрибути: їхня присутність = true, відсутність = false.❌ Типова помилка початківця: Забувати закриваючий тег у не-void елементів (
<div>,<p>,<a>). Браузер виправить, але DOM буде іншим ніж очікуєш — і querySelector поверне несподіваний результат.
2. Анатомія HTML-тегу¶
<a href="https://example.com" target="_blank" class="btn">Текст посилання</a>
│ │ │ │
│ └─ атрибут href │ └─ атрибут class
│ └─ атрибут target
└─ відкриваючий тег
└─ закриваючий тег
Правила¶
- Атрибути — завжди в відкриваючому тегу:
<tag attr="value"> - Значення в лапках:
href="link"(одинарні або подвійні — однаково) - Регістр не важливий (
<DIV>=<div>), але стандарт — мала буква - Деякі теги самозакриваються (void elements):
<img>,<br>,<input>,<meta>,<link>
🧠 Ментальна модель: Заголовки
<h1>–<h6>— це зміст книги.<h1>— назва книги (одна).<h2>— розділи.<h3>— підрозділи. Google читає цю ієрархію і розуміє про що сторінка. Пропускати рівні (<h1>→ одразу<h4>) — як зробити зміст де після назви книги одразу йдуть підпункти без розділів: плутанина.📚 Чому існує різниця між
<strong>/<em>і<b>/<i>:<strong>і<em>— семантичні теги з "значенням".<strong>каже: "це важлива інформація" — скрінрідер може голосніше прочитати.<em>каже: "це акцент" — скрінрідер може змінити інтонацію.<b>і<i>— чисто візуальні. Використовуй семантичні версії за замовчуванням.🌐 Як браузер бачить це:
<p>— блоковий елемент. Браузер автоматично додає margin-top і margin-bottom (зазвичай 1em).<br>— примусовий перенос рядка без нового параграфа.<hr>— тематичний розрив, відображається горизонтальною лінією.❌ Типова помилка початківця: Використовувати
<br><br>для створення відступів між параграфами замість CSSmargin. Це зламає розмітку при зміні font-size і ускладнить підтримку. Відступи — завжди в CSS.
3. Заголовки та текст¶
<h1>Головний заголовок сторінки — тільки ОДИН на сторінці</h1>
<h2>Підрозділ</h2>
<h3>Підпідрозділ</h3>
<h4>Рівень 4</h4>
<h5>Рівень 5</h5>
<h6>Найменший заголовок</h6>
<p>Параграф тексту. Браузер автоматично додає відступ зверху і знизу.</p>
<strong>Важливий текст (жирний + семантично важливий)</strong>
<b>Просто жирний (без семантики)</b>
<em>Акцент (курсив + семантично підкреслений)</em>
<i>Просто курсив (без семантики)</i>
<br> <!-- Перенос рядка (inline) -->
<hr> <!-- Горизонтальна лінія (block) -->
<small>Дрібний текст: копірайт, підпис</small>
<mark>Виділений текст (жовтий маркер)</mark>
<del>Перекреслений текст</del>
<ins>Підкреслений (вставлений) текст</ins>
<sup>верхній індекс</sup> — H<sub>2</sub>O (нижній)
<code>inline код</code>
<pre><code>блок коду
з відступами</code></pre>
🧠 Ментальна модель: Посилання
<a>— це портал між документами.href— адреса порталу. Абсолютні URL (https://...) — портал до іншого сайту. Відносні URL (/about/) — портал всередині того ж сайту. Якірні посилання (#section) — портал до іншого місця на тій самій сторінці.<img>— це "дірка" в документі: браузер завантажує зображення окремим HTTP-запитом і вставляє його в цю дірку.📚 Чому
rel="noopener noreferrer"важливий для безпеки: Без цього атрибута сторінка, відкрита у новій вкладці, черезwindow.openerможе отримати JavaScript-доступ до сторінки, яка її відкрила (атака "reverse tabnapping").noopenerблокує цей доступ.noreferrerдодатково не передає HTTP Referer заголовок.🌐 Як браузер обробляє
<img>: Браузер будує DOM і паралельно відправляє окремий HTTP GET запит для кожногоsrc. Зображення завантажуються асинхронно. Якщоwidthіheightне вказані — браузер не знає розміри заздалегідь і "стрибає" при завантаженні (Cumulative Layout Shift). Вказуй завжди!❌ Типова помилка початківця: Пропускати
altу<img>. Це не просто "текст якщо не завантажилось" — це також те, що Google Images індексує, що скрінрідер оголошує, і що з'являється у браузерах з вимкненими зображеннями.
4. Посилання та зображення¶
<!-- Посилання -->
<a href="https://google.com">Зовнішнє посилання</a>
<a href="/about/">Внутрішнє (відносний шлях)</a>
<a href="#section-id">Якір на тій самій сторінці</a>
<a href="mailto:email@example.com">Email</a>
<a href="tel:+380501234567">Телефон</a>
<a href="https://example.com" target="_blank" rel="noopener noreferrer">
В новій вкладці (rel="noopener" — безпека!)
</a>
<!-- Зображення -->
<img src="photo.jpg" alt="Опис зображення для скрінрідерів і SEO">
<img src="/static/img/logo.png" alt="Логотип" width="200" height="100">
<!-- alt="" — порожній якщо зображення декоративне (не несе змісту) -->
<img src="decorative.jpg" alt="">
Чому
altважливий? - Показується якщо зображення не завантажилось - Читається скрінрідерами для людей з вадами зору - Індексується Google Image Search
🧠 Ментальна модель: Списки — це єдиний правильний HTML для будь-якого переліку. Навігаційне меню — це
<ul>з<li>. Кроки інструкції —<ol>. Словник термінів —<dl>. Не намагайся зробити "список" з окремих<p>або<div>— це зламає семантику, SEO і accessibility.📚 Чому існують три типи списків:
<ul>(unordered) — коли порядок не важливий (пункти меню, теги).<ol>(ordered) — коли порядок критичний (кроки рецепту, інструкції, нумерований рейтинг).<dl>(definition list) — для пар "термін: визначення" (глосарій, metadata-пари ключ-значення).🌐 Як браузер бачить це:
<ul>і<ol>— блокові елементи.<li>— теж блокові. Браузер за замовчуванням додаєpadding-leftі маркери/цифри. CSSlist-style: noneприбирає маркери — це стандартний прийом для навігаційного меню.❌ Типова помилка початківця: Класти в
<ul>щось крім<li>. Прямим дочірнім елементом<ul>/<ol>може бути тільки<li>. Всередині<li>— що завгодно, включно з вкладеними списками.
5. Списки¶
<!-- Невпорядкований список (маркований) -->
<ul>
<li>Пункт 1</li>
<li>Пункт 2</li>
<li>Пункт 3
<ul>
<li>Вкладений пункт</li> <!-- Вкладені списки -->
</ul>
</li>
</ul>
<!-- Впорядкований список (нумерований) -->
<ol>
<li>Перший крок</li>
<li>Другий крок</li>
<li>Третій крок</li>
</ol>
<!-- Список визначень -->
<dl>
<dt>HTML</dt>
<dd>Мова розмітки гіпертексту</dd>
<dt>CSS</dt>
<dd>Каскадні таблиці стилів</dd>
</dl>
🧠 Ментальна модель: Таблиця HTML — це як Excel-таблиця: рядки (
<tr>), заголовки колонок (<th>), дані (<td>).<thead>— заморожена перша рядок Excel.<tbody>— прокручувані дані.<tfoot>— рядок підсумків внизу. Але пам'ятай: Excel — для даних, не для верстки. HTML-таблиці — тільки для табличних даних, не для розміщення блоків на сторінці.📚 Чому існують
<thead>,<tbody>,<tfoot>: Семантика і accessibility: скрінрідери оголошують "заголовок таблиці", "рядок 1 з 10" і т.д. При друку на кілька сторінок —<thead>і<tfoot>повторюються на кожній сторінці автоматично.scope="col"у<th>каже скрінрідеру, до якого стовпця відноситься заголовок.🌐 Як браузер бачить це: Таблиці мають власний layout engine — "table layout algorithm". Браузер обчислює ширину колонок, враховуючи всі клітинки.
colspanіrowspan— об'єднання клітинок горизонтально і вертикально відповідно.❌ Типова помилка початківця: Використовувати
<table>для верстки макету сторінки (дві колонки, сайдбар і контент). Це архаїчна практика 2000-х. Сучасне рішення — CSS Flexbox або Grid. Таблиці для верстки ламають responsive design і accessibility.
6. Таблиці¶
<table>
<caption>Заголовок таблиці (для accessibility)</caption>
<thead>
<tr>
<th scope="col">Ім'я</th>
<th scope="col">Вік</th>
<th scope="col">Місто</th>
</tr>
</thead>
<tbody>
<tr>
<td>Олексій</td>
<td>25</td>
<td>Київ</td>
</tr>
<tr>
<td>Марія</td>
<td>30</td>
<td>Львів</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3">Всього: 2 людини</td>
</tr>
</tfoot>
</table>
| Елемент | Роль |
|---|---|
<table> |
Контейнер таблиці |
<thead> |
Голова таблиці (заголовки колонок) |
<tbody> |
Тіло (основні дані) |
<tfoot> |
Підвал (підсумки) |
<tr> |
Table Row — рядок |
<th> |
Table Header — заголовок комірки |
<td> |
Table Data — дані комірки |
colspan="3" |
Комірка займає 3 колонки |
rowspan="2" |
Комірка займає 2 рядки |
🧠 Ментальна модель: HTML-форма — це конверт з листом. Користувач заповнює поля (пише лист), натискає Submit (заклеює конверт і кидає в скриньку). Браузер — це пошта: він читає адресу (
action), метод доставки (method) і доставляє. Django — це одержувач: відкриває конверт і читає вміст (request.POST).📚 Це найважливіший розділ для Django-розробника: Кожна форма в Django починається з HTML.
request.POST— це не магія: це браузер, який серіалізував поля форми і відправив HTTP POST запит. Розуміння цього механізму дозволяє дебажити форми без Django, тестувати API через curl, і розуміти чому{% csrf_token %}— не опціональний.🌐 Детальний потік: від натискання Submit до
request.POSTу Django:Користувач заповнює форму: <input name="username" value="Олексій"> <input name="email" value="user@example.com"> ↓ Користувач натискає кнопку type="submit" ↓ Браузер читає атрибут method="POST" ↓ Браузер серіалізує всі <input name="x" value="y"> у рядок: username=Олексій&email=user%40example.com&csrfmiddlewaretoken=abc123 (спецсимволи URL-кодуються: @ → %40, пробіл → +) ↓ Браузер відправляє HTTP POST запит: POST /register/ HTTP/1.1 Host: example.com Content-Type: application/x-www-form-urlencoded Content-Length: 58 username=Олексій&email=user%40example.com&csrfmiddlewaretoken=abc123 ↓ Django отримує запит: request.method == 'POST' # True request.POST['username'] == 'Олексій' # True request.POST['email'] == 'user@example.com' # True (Django декодує) request.POST['csrfmiddlewaretoken'] == 'abc123'Різниця між GET і POST — не просто технічна, а семантична:
method="GET": Браузер додає дані в URL як query string: GET /search/?q=python&category=books HTTP/1.1 В Django: request.GET['q'] == 'python' request.GET['category'] == 'books' Коли використовувати GET: ✅ Пошукові форми (URL можна зберегти в закладки) ✅ Фільтри і сортування ✅ Пагінація (?page=2) ❌ НЕ для паролів (URL зберігається в history!) ❌ НЕ для великих даних (URL обмежений ~2000 символів) method="POST": Дані у тілі запиту (невидимі в URL) Коли використовувати POST: ✅ Логін/реєстрація (пароль не в URL) ✅ Створення/оновлення/видалення даних ✅ Завантаження файлів ✅ Будь-які "мутації" стану на сервері❌ Типова помилка початківця: Забути
enctype="multipart/form-data"при завантаженні файлів. Без цього браузер відправляє тільки ім'я файлу, а не його вміст. Django отримає порожнійrequest.FILES. Це помилка яку дуже важко знайти без знання механізму форм.
7. Форми — найважливіший елемент для Django¶
<form method="POST" action="/submit/" enctype="multipart/form-data">
{% csrf_token %}
<!-- Text input -->
<label for="name">Ім'я:</label>
<input type="text" id="name" name="name" placeholder="Введіть ім'я" required>
<!-- Email -->
<input type="email" name="email" required>
<!-- Password -->
<input type="password" name="password" minlength="8">
<!-- Number -->
<input type="number" name="age" min="0" max="120">
<!-- Checkbox -->
<input type="checkbox" id="agree" name="agree" value="yes">
<label for="agree">Погоджуюсь з умовами</label>
<!-- Radio buttons (вибір одного з кількох) -->
<input type="radio" id="m" name="gender" value="male">
<label for="m">Чоловік</label>
<input type="radio" id="f" name="gender" value="female">
<label for="f">Жінка</label>
<!-- Select (випадаючий список) -->
<select name="city">
<option value="">Оберіть місто</option>
<option value="kyiv" selected>Київ</option>
<option value="lviv">Львів</option>
<option value="kharkiv">Харків</option>
</select>
<!-- Textarea (багаторядковий текст) -->
<textarea name="message" rows="5" cols="40" placeholder="Ваше повідомлення..."></textarea>
<!-- File upload -->
<input type="file" name="photo" accept="image/*">
<!-- Hidden field (прихований, не видний користувачу) -->
<input type="hidden" name="user_id" value="42">
<!-- Кнопки -->
<button type="submit">Відправити</button>
<button type="reset">Очистити</button>
<button type="button" onclick="doSomething()">Просто кнопка</button>
<!-- Альтернатива кнопці submit -->
<input type="submit" value="Відправити">
</form>
Атрибути форм — критичні для Django¶
| Атрибут | Значення | Коли використовувати |
|---|---|---|
method="GET" |
Дані в URL: ?name=value |
Пошук, фільтри |
method="POST" |
Дані в тілі запиту | Створення/зміна даних |
action="/url/" |
Куди відправляти | Завжди вказуй, або Django обробить поточний URL |
enctype="multipart/form-data" |
Для файлів | Обов'язково якщо є <input type="file"> |
novalidate |
Вимикає браузерну валідацію | Для тестування або кастомної валідації |
🧠 Ментальна модель: Несемантичний HTML — це місто без вуличних знаків і назв вулиць. Люди (користувачі) ще можуть орієнтуватись за будівлями, але GPS (Google, Bing) і навігатор для незрячих (скрінрідер) — повністю загублені. Семантичний HTML — це те ж місто, але з повноцінними вуличними знаками, назвами районів і покажчиками. Кожен орієнтується миттєво.
📚 Чому семантика важлива для Google: Розглянь два варіанти:
Обидва виглядають однаково у браузері. Але для Google:<div class="nav">— просто блок з класом "nav" (Google не знає що ти мав на увазі).<nav>— навігація. Google розуміє це як "тут посилання для навігації по сайту" і краще будує карту сайту, визначає важливість посилань, розуміє структуру.🌐 Як скрінрідер використовує семантику: Скрінрідер (NVDA, VoiceOver) читає ARIA "ролі".
<header>має роль "banner".<nav>— "navigation".<main>— "main".<footer>— "contentinfo". Сліпий користувач може натиснути клавішу 'H' (headings), 'F' (forms), 'L' (lists), 'N' (navigation) — і скрінрідер миттєво переходить до відповідного семантичного блоку. З<div>це неможливо.❌ Типова помилка початківця: Думати що семантика — це "рекомендація для кращих хлопців". Насправді: несемантична верстка = гірший SEO-рейтинг = менше трафіку = менше грошей. В комерційних проектах різниця в семантиці може означати різницю в позиції в Google.
8. Семантична розмітка HTML5¶
Семантичні теги — теги з змістом: браузер, Google і скрінрідери розуміють що це таке.
❌ Несемантично:
<div id="header">...</div>
<div class="nav">...</div>
<div class="main">...</div>
<div class="sidebar">...</div>
<div id="footer">...</div>
✅ Семантично:
<header>...</header>
<nav>...</nav>
<main>...</main>
<aside>...</aside>
<footer>...</footer>
Повна карта семантичних тегів¶
<header>
<!-- Шапка сторінки або секції: логотип, навігація, заголовок -->
<nav>
<!-- Навігаційні посилання — головне меню, пагінація -->
</nav>
</header>
<main>
<!-- ОДИН на сторінці. Основний унікальний вміст -->
<article>
<!-- Незалежний контент: пост блогу, новина, коментар -->
<header>
<h2>Заголовок статті</h2>
<time datetime="2026-05-21">21 травня 2026</time>
</header>
<p>Текст статті...</p>
<footer>Автор: Іван</footer>
</article>
<section>
<!-- Тематичний розділ з заголовком — "Послуги", "Контакти" -->
<h2>Наші послуги</h2>
<p>...</p>
</section>
</main>
<aside>
<!-- Бічна панель: реклама, теги, пов'язані посилання -->
</aside>
<footer>
<!-- Підвал: копірайт, контакти, карта сайту -->
<address>
<!-- Контактна інформація автора/організації -->
Email: <a href="mailto:info@example.com">info@example.com</a>
</address>
</footer>
<!-- Інші семантичні теги -->
<figure>
<img src="chart.png" alt="Графік продажів">
<figcaption>Графік продажів за 2026 рік</figcaption>
</figure>
<details>
<summary>Натисни щоб розгорнути</summary>
<p>Прихований вміст</p>
</details>
<dialog id="myDialog">
<!-- Нативний модальний діалог (без JS-бібліотек) -->
</dialog>
🧠 Ментальна модель:
<div>— це прозора коробка без маркування.<span>— прозора обгортка без маркування для inline-вмісту. Їх використовують тільки тоді, коли жодна "іменована" коробка (<section>,<article>,<nav>,<aside>) не підходить. Правило: спочатку перевір всі семантичні теги — якщо жоден не підходить, тоді<div>.📚 Чому
<div>і<span>взагалі існують: Не всі UI-патерни мають семантичний еквівалент. "Картка товару в магазині" — це<article>або<div>? "Колонка в grid-макеті" — точно<div>. "Ціна зі знижкою" —<span>з CSS-класом. Ці теги — будівельний матеріал для CSS і JavaScript де немає підходящого семантичного тегу.🌐 Як браузер бачить
<div>і<span>:<div>—display: blockза замовчуванням (займає всю ширину).<span>—display: inline(займає рівно стільки місця, скільки вміст). Браузер не надає їм ніякого ARIA-role — вони "невидимі" для accessibility-дерева без додаткових атрибутів.❌ Типова помилка початківця: "Div-soup" — верстка повністю з
<div>без жодного семантичного тегу. Виглядає однаково в браузері, але Google і скрінрідери отримують "суп" без структури. Проблема діагностується швидко: відкрий F12 → Elements і порахуй<div>— якщо їх більше 80% всіх тегів, є проблема.
9. <div> і <span> — безсемантичні контейнери¶
<!-- div — блоковий контейнер (займає всю ширину рядка) -->
<div class="card">
<div class="card-header">Заголовок</div>
<div class="card-body">Вміст картки</div>
</div>
<!-- span — рядковий контейнер (рівно по розміру вмісту) -->
<p>
Ціна: <span class="price text-danger">1500 грн</span>
(зі знижкою <span class="badge">-20%</span>)
</p>
Правило: <div> і <span> — коли жоден семантичний тег не підходить.
Якщо підходить <section>, <article>, <nav> — використовуй їх.
🧠 Ментальна модель:
id— це паспорт (унікальний на весь документ).class— це форма (однакова у всіх хто її носить). Паспорт використовується для ідентифікації конкретної людини. Форма — для стилізації всіх людей з однаковою роллю.📚 Чому існують і
id, іclass:id— для унікальних елементів: головний контент сторінки, конкретна форма, секція для якірного посилання. CSS-специфічністьidвища ніжclass— це може спричинити проблеми при перевизначенні стилів.class— для повторюваних патернів: картки, кнопки, бейджики. В сучасній практиціidвикористовують переважно для JavaScript, якірних посилань і зв'язку<label for="">/<input id="">.🌐 Як браузер використовує
idіclass: CSS-селектор#idмає специфічність 0-1-0-0..class— 0-0-1-0.tag— 0-0-0-1. Тому#header p { color: red }переможе.content p { color: blue }навіть якщо другий записаний пізніше — через вищу специфічність#header.❌ Типова помилка початківця: Дублювати
idна сторінці.idмає бути УНІКАЛЬНИМ. Якщо є два<div id="card">— CSS і JavaScript поведуться непередбачувано:getElementByIdповерне тільки перший, а CSS може застосуватись до обох (браузери прощають, але це баг).
10. Атрибути id і class¶
<!-- id — унікальний ідентифікатор, ОДИН на сторінці -->
<section id="about">...</section>
<!-- Використання: -->
<!-- 1. Якір посилання: <a href="#about"> -->
<!-- 2. CSS: #about { background: blue; } -->
<!-- 3. JavaScript: document.getElementById('about') -->
<!-- 4. Зв'язок label↔input: <label for="email"> + <input id="email"> -->
<!-- class — можна використовувати багато разів -->
<div class="card">...</div>
<div class="card">...</div>
<div class="card featured">...</div> <!-- кілька класів через пробіл -->
<!-- Використання class у CSS: .card { border: 1px solid; } -->
<!-- Використання class у JS: document.querySelectorAll('.card') -->
🧠 Ментальна модель: Мета-теги в
<head>— це пакувальний листок на вантажному місці. Зміст коробки (<body>) — для людей. Пакувальний листок (<head>) — для системи: що всередині, звідки, куди, як обробляти. Google, Facebook, Twitter читають цей "пакувальний листок" перш ніж показати контент користувачеві.📚 Чому Open Graph існує: Коли ти ділишся посиланням у Facebook, Telegram або LinkedIn — ці платформи не відкривають сторінку "живцем". Вони відправляють свого бота, який читає тільки
<head>і OG-метатеги. Якщоog:imageне вказаний — Telegram покаже посилання без превью. Якщоog:titleвідсутній — візьме<title>. Правильні OG-теги = красивий превью = більше кліків.🌐 Що робить
<title>в браузері: (1) Текст у вкладці браузера; (2) Назва у результатах пошуку Google (перші 50-60 символів); (3) Назва при додаванні до закладок; (4) Назва у task-switcher ОС. Це єдиний елемент з такою широкою "видимістю" поза браузером.❌ Типова помилка початківця: Однаковий
<title>на всіх сторінках сайту. Google вважає це "дублюючим контентом" і знижує рейтинг. У Django використовуй{% block title %}{{ object.name }} | Мій сайт{% endblock %}для унікальних заголовків.
11. Мета-теги для SEO та соцмереж¶
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- SEO -->
<title>Назва сторінки (50–60 символів) | Назва сайту</title>
<meta name="description" content="Опис сторінки для Google (150–160 символів)">
<meta name="keywords" content="html, css, bootstrap, django"> <!-- застарілий, але є -->
<meta name="robots" content="index, follow">
<!-- Open Graph (Facebook, Telegram, LinkedIn) -->
<meta property="og:title" content="Назва">
<meta property="og:description" content="Опис">
<meta property="og:image" content="https://example.com/image.jpg">
<meta property="og:url" content="https://example.com/page">
<meta property="og:type" content="website">
<!-- Twitter Cards -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Назва">
<!-- Favicon -->
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
</head>
Ці помилки — не просто "поганий стиль". Кожна з них має конкретний негативний наслідок: зламаний SEO (менше трафіку), недоступний сайт (порушення законодавства про accessibility), або зламана Django-форма (втрата даних або 403 Forbidden). Розглядай цей список як чеклист перед кожним code review.
🧠 Ментальна модель: Ці помилки — як будівельні порушення. Будинок може стояти (браузер відобразить), але при першій перевірці (SEO аудит, accessibility аудит, Django форма) все впаде.
📚 Чому це важливо для Django-розробника:
<b>замість<strong>— SEO удар. Кілька<h1>— Google не розуміє головну тему сторінки.<br>для відступів — ламається при зміні шрифту. Таблиці для верстки — ламається на мобільних. Немаalt— штраф від Google. Inlinestyle=""— неможливо змінити через CSS. Нема{% csrf_token %}— Django повертає 403 і форма не працює. Неправильнийfor/id— клік по<label>не фокусує<input>, що критично для мобільних і accessibility.🌐 Як браузер бачить типові помилки:
<br><br>— браузер відображає як порожні рядки, але скрінрідер читає їх як "порожній рядок, порожній рядок".<table>для верстки — браузер застосовує table layout algorithm: колонки стискаються до мінімальної ширини вмісту, що ламає responsive design. Inlinestyle=""— специфічність 1-0-0-0, перевизначити можна тільки через!important.❌ Типова помилка початківця: Вважати що якщо сторінка "виглядає правильно" в браузері — все добре. Браузер прощає і виправляє помилки візуально, але Google, скрінрідери і Django — не такі добросердечні.
12. Типові помилки початківців¶
| Помилка | Правило |
|---|---|
<b> замість <strong> для важливого тексту |
<strong> — семантично важливо, <b> — тільки візуально |
Кілька <h1> на сторінці |
<h1> — один, це головний заголовок для SEO |
<br><br><br> для відступів |
Відступи — через CSS margin/padding |
| Таблиці для верстки макету | Таблиці — тільки для табличних даних. Макет — CSS Grid/Flexbox |
Нема alt у <img> |
alt — обов'язковий (навіть alt="" для декоративних) |
<div> замість семантичних тегів |
Спочатку перевір: чи є <header>, <nav>, <main>, <article>? |
Inline-стилі style="..." |
Стилі — в CSS файлі або <style>. Inline тільки для динамічних значень |
Нема {% csrf_token %} у POST-формах |
Django відхилить запит з 403 Forbidden |
for у <label> не збігається з id у <input> |
Кліку по label не активує input |
🧠 Ментальна модель: Block-елементи — це ПАРАГРАФИ. Уяви текстовий редактор Word: кожен параграф займає всю ширину сторінки і починається з нового рядка. Inline-елементи — це СЛОВА в параграфі. Жирне слово, курсивне слово, посилання — вони течуть разом з іншим текстом, не розриваючи рядок.
📚 Чому це фундаментально для CSS: Правило що дивує КОЖНОГО початківця:
widthіheightне працюють на inline-елементах. Якщо ти пишешspan { width: 200px; }— нічого не станеться. Span залишиться шириною свого вмісту. Це не баг — це специфікація: inline-елементи течуть як текст, і text не має фіксованої ширини. Рішення:display: inline-blockабоdisplay: block— тоді всі CSS-властивості починають працювати.🌐 Візуальна схема Block vs Inline:
┌─────────────────────────────────────────┐ │ <div>: Block — займає весь рядок │ └─────────────────────────────────────────┘ ┌─────────────────────────────────────────┐ │ <p>: Block — теж займає весь рядок │ └─────────────────────────────────────────┘ Текст <a>посилання</a> і далі текст рядка │ ← Inline в потоці тексту <span>слово</span> і ще <em>слово</em> │ ← Inline поряд ───────────────────────────────────────── ┌────────────────────────────────────────┐ │ <div>: Block знову починає новий рядок │ └────────────────────────────────────────┘❌ Типова помилка початківця: Спробувати задати
width: 100px; height: 50px; padding: 20pxдля<span>або<a>і дивуватись чому не працює. Вирішення: додатиdisplay: inline-blockабоdisplay: block. Це одна з найпоширеніших точок застрягання при вивченні CSS.
13. Block vs Inline — модель потоку документа¶
Кожен HTML-елемент має тип відображення за замовчуванням. Це фундаментальна концепція — вона пояснює чому елементи розміщуються так, а не інакше.
Block-елементи¶
- Займають всю доступну ширину батьківського контейнера
- Починаються з нового рядка
- Можна задавати
width,height,margin,paddingз усіх 4 сторін
<div> <p> <h1>–<h6> <ul> <ol> <li>
<header> <nav> <main> <section> <article>
<aside> <footer> <form> <table> <blockquote>
<hr> <pre> <figure> <figcaption> <address>
Inline-елементи¶
- Займають рівно стільки місця, скільки їх вміст
- Розміщуються в рядку поряд з іншими inline-елементами
widthіheightне працюють (ширина визначається вмістом)- Вертикальний
margin/paddingобмежений
<span> <a> <strong> <em> <b> <i>
<img> <input> <label> <button> <select>
<code> <abbr> <small> <sup> <sub> <br>
Prediction Exercise¶
<!-- Як браузер відобразить цей код? -->
<h1>Заголовок</h1>
<a href="#">Посилання</a> ← ще один рядок тут
Відповідь:
┌───────────────────────────────────┐
│ Заголовок │ ← <h1> block: займає всю ширину
└───────────────────────────────────┘
Посилання ← ще один рядок тут ← <a> inline: поряд в одному рядку
<h1> — block → форсує перенос рядка до і після себе.
<a> — inline → стоїть поряд з сусіднім текстом без переносу.
Inline-block — гібрид¶
/* display: inline-block — в рядку, але з повним box model */
.badge {
display: inline-block;
padding: 4px 10px; /* ← вертикальний padding ПРАЦЮЄ (на відміну від inline) */
width: 80px; /* ← width ПРАЦЮЄ (на відміну від inline) */
background: #007bff;
}
<!-- Використання: теги, бейджики, кнопки поруч з текстом -->
<p>
Статус: <span class="badge">Активний</span>
Рівень: <span class="badge">Старший</span>
</p>
Таблиця порівняння¶
| Block | Inline | Inline-block | |
|---|---|---|---|
| Новий рядок | Так | Ні | Ні |
width / height |
Так | Ні | Так |
margin (всі 4) |
Так | Тільки горизонтальний | Так |
padding (всі 4) |
Так | Тільки горизонтальний | Так |
| Приклади | <div>, <p>, <h1> |
<span>, <a>, <strong> |
<button>, .badge |
🧠 Ментальна модель: Уяви що ти архітектор і проектуєш будівлю. Accessibility — це пандуси, ліфти, тактильна плитка, шрифт Брайля на кнопках. Ти можеш побудувати будівлю без цього — вона буде "красивою". Але 15-20% людей (це реальна статистика людей з різними видами порушень) не зможуть нею скористатись. І в багатьох країнах тебе ще й оштрафують.
📚 Чому a11y — це не опціонально (юридичний контекст): - EU (Євросоюз): Директива 2016/2102 і EN 301 549 — обов'язкова accessibility для всіх державних сайтів ЄС. Приватні компанії, які продають в ЄС, підпадають під European Accessibility Act (2025+). - США: Americans with Disabilities Act (ADA) — суди вже виносили рішення проти компаній за недоступні сайти (Domino's Pizza case, 2019). - UK: Public Sector Bodies Accessibility Regulations (PSBAR) — державні сайти зобов'язані відповідати WCAG 2.1 AA. - Практика: Навіть якщо твій проект не підпадає під закони прямо зараз — клієнт з ЄС може поставити це як вимогу. Краще навчитись зразу.
🌐 Як скрінрідер навігує сторінкою: Сліпий користувач з NVDA (Windows) або VoiceOver (Mac/iOS) не використовує мишу. Ось типова сесія:
Якщо твоя "навігація" зроблена зTab → перехід між інтерактивними елементами (посилання, кнопки, inputs) H → стрибок до наступного заголовка (h1, h2, h3...) F → стрибок до наступного поля форми L → стрибок до наступного списку N → стрибок до наступного блоку навігації Ins+F7 → список всіх посилань на сторінці<div>замість<nav>, клавіша 'N' не знайде її. Якщо кнопка —<div onclick="...">замість<button>— Tab не доберется до неї.DevTools Accessibility Audit (крок за кроком): 1. Відкрий F12 (DevTools) 2. Знайди вкладку "Lighthouse" (раніше "Audits") 3. Вибери тільки "Accessibility" (зніми решту галочок) 4. Натисни "Analyze page load" 5. Отримаєш оцінку від 0 до 100 + список конкретних проблем з поясненням Також: F12 → Elements → вибери елемент → в правій панелі знайди вкладку "Accessibility" — побачиш ARIA-дерево цього елемента.
❌ Типова помилка початківця: Видаляти
outlineу CSS без заміни:button { outline: none; }. Outline — це візуальний індикатор фокусу для користувачів клавіатури. Без нього вони не бачать де зараз фокус і не можуть навігувати. Правильно: замінити на кастомнийoutlineабоbox-shadow, але не видаляти повністю.
14. Доступність (Accessibility / a11y)¶
Доступний сайт працює для всіх: людей з вадами зору, слуху, моторики. Це не "бонусна функція" — в багатьох країнах вимога за законом.
ARIA-атрибути — смислові підказки¶
<!-- role — каже що це за елемент (якщо тег несемантичний) -->
<div role="navigation">...</div> <!-- = <nav> -->
<div role="main">...</div> <!-- = <main> -->
<div role="button" tabindex="0">Клік</div> <!-- кастомна кнопка -->
<!-- aria-label — доступна назва для скрінрідера -->
<button aria-label="Закрити діалог">✕</button>
<input type="search" aria-label="Пошук по сайту">
<nav aria-label="Головна навігація">...</nav>
<nav aria-label="Пагінація">...</nav>
<!-- aria-hidden — приховати від скрінрідера (декоративні елементи) -->
<span aria-hidden="true">★★★★☆</span>
<span class="sr-only">4 зірки з 5</span> <!-- тільки для скрінрідерів -->
<!-- aria-expanded — стан розгортання (меню, акордеон) -->
<button aria-expanded="false" aria-controls="menu">Меню ▼</button>
<ul id="menu" hidden>...</ul>
<!-- aria-live — динамічний вміст що оновлюється -->
<div aria-live="polite" id="status">Завантаження...</div>
<!-- aria-describedby — додатковий опис елемента -->
<input type="password" id="pwd" aria-describedby="pwd-hint">
<small id="pwd-hint">Мінімум 8 символів, одна цифра</small>
<!-- aria-required — обов'язкове поле -->
<input type="email" aria-required="true" required>
Фокусна навігація (Keyboard Navigation)¶
<!-- tabindex — порядок навігації клавішею Tab -->
<div tabindex="0">Фокусований div (природний порядок)</div>
<div tabindex="-1">Можна фокусувати через JS, але не Tab</div>
<!-- tabindex="1", "2"... — уникати! Ламає природний порядок -->
<!-- Пастка фокусу в модальному вікні (потрібен JS) -->
<dialog id="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
<h2 id="modal-title">Підтвердження</h2>
<p>Ви впевнені?</p>
<button>Так</button>
<button>Скасувати</button>
</dialog>
Контраст і колір¶
<!-- Не покладайся ТІЛЬКИ на колір для передачі інформації -->
<!-- ❌ Погано: тільки зелений/червоний -->
<span style="color: green">✓</span> Успіх
<span style="color: red">✗</span> Помилка
<!-- ✅ Добре: колір + текст + іконка -->
<span class="text-success" role="img" aria-label="Успішно">✓</span> Успішно збережено
<span class="text-danger" role="img" aria-label="Помилка">✗</span> Помилка валідації
Skip-link — для миттєвого переходу до вмісту¶
<!-- Першим елементом в <body> -->
<a href="#main-content" class="skip-link">Перейти до вмісту</a>
<nav>... довга навігація ...</nav>
<main id="main-content" tabindex="-1">
<!-- Основний вміст -->
</main>
/* Схований але доступний для Tab */
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #007bff;
color: white;
padding: 8px;
z-index: 999;
transition: top 0.2s;
}
.skip-link:focus {
top: 0; /* з'являється при натисканні Tab */
}
Checklist доступності¶
✅ Всі <img> мають alt=""
✅ <label> прив'язані до <input> через for/id
✅ Форми мають <fieldset> і <legend> для груп
✅ Кольоровий контраст ≥ 4.5:1 (текст) / 3:1 (великий текст)
✅ Сайт повністю працює тільки з клавіатурою (Tab, Enter, Esc)
✅ Фокус видимий (не видалено outline без заміни)
✅ Динамічний вміст оголошується через aria-live
✅ Семантичні теги замість div/span скрізь де можливо
🧠 Ментальна модель:
data-*атрибути — це Post-it нотатки, приклеєні до HTML-елементів. Вони не змінюють відображення, не вказують браузеру робити щось особливе — вони просто зберігають довільні дані, які може прочитати JavaScript або CSS. В Django-шаблонахdata-*— ідеальний спосіб передати дані з Python/Django у JavaScript без прихованих<input>або окремих API-запитів.📚 Чому
data-*виник: До HTML5 розробники зберігали дані вclass(наприкладclass="product-id-42"), в прихованих<input type="hidden">, або в коментарях. Всі ці підходи — хаки.data-*— стандартизований спосіб зберігати будь-які метадані прямо в розмітці, безпечно і семантично правильно.🌐 Важливий нюанс: camelCase у JavaScript: HTML атрибут
data-note-idстаєdataset.noteIdв JavaScript (дефіс → camelCase).data-in-stock→dataset.inStock.data-created-at→dataset.createdAt. CSS читає через[data-note-id]— оригінальна назва з дефісом.❌ Типова помилка початківця: Зберігати конфіденційні дані в
data-*атрибутах. Вони видимі будь-якому користувачу через F12 → Elements! Для паролів, токенів, і чутливих даних — тільки серверна логіка, ніякихdata-passwordабоdata-api-key.
15. Data-атрибути — власні метадані в HTML¶
<!-- data-* — зберігати довільні дані прямо в HTML -->
<div
class="product-card"
data-id="42"
data-category="electronics"
data-price="1500"
data-in-stock="true"
>
<h3>Смартфон</h3>
<button class="add-to-cart">До кошика</button>
</div>
<!-- Список з даними для JS -->
<ul>
{% for note in notes %}
<li
data-note-id="{{ note.pk }}"
data-created="{{ note.created_at|date:'c' }}"
>
{{ note.title }}
</li>
{% endfor %}
</ul>
// Читання в JavaScript
const card = document.querySelector('.product-card');
const id = card.dataset.id; // "42"
const price = card.dataset.price; // "1500"
const inStock = card.dataset.inStock; // "true" (camelCase!)
// Зміна
card.dataset.price = "1200";
// CSS по data-атрибуту
// .product-card[data-in-stock="false"] { opacity: 0.5; }
/* Стилі по data-атрибутах */
[data-in-stock="false"] {
opacity: 0.5;
pointer-events: none;
}
[data-category="electronics"]::before {
content: "⚡ ";
}
Правило №1 дебагінгу HTML: НІКОЛИ не дебаж HTML через Ctrl+U (View Source). ЗАВЖДИ використовуй F12 → Elements.
Причина критична: Ctrl+U показує тобі те, що Django надіслав — сирий HTML-рядок до того, як браузер його обробив. Але браузер може: (1) автоматично виправити зламану розмітку; (2) JavaScript може змінити DOM після завантаження; (3) браузерні розширення можуть додати/змінити елементи. F12 → Elements завжди показує ПОТОЧНИЙ стан DOM — саме те, що браузер реально відображає на екрані.
🧠 Ментальна модель: DevTools — це рентген браузера. Ctrl+U — це фото будівлі ззовні. F12 → Elements — це архітектурний план зі всіма прихованими трубами, дротами і конструкціями. Ти бачиш не тільки що є, а й чому воно так виглядає (computed styles, box model).
📚 Чому Ctrl+U і F12 показують різне: Django генерує HTML і відправляє його по HTTP. Це фіксований рядок байтів. Браузер отримує цей рядок і: (1) парсить у DOM; (2) виправляє помилки (автоматично закриває теги); (3) виконує JavaScript, який може змінити DOM; (4) браузерні розширення можуть додати власні елементи. Результат — DOM може сильно відрізнятись від source. Якщо ти дебажиш через Ctrl+U — ти дивишся не на те.
🌐 Покрокова інструкція дебагінгу в DevTools:
Крок 1: Відкрий F12 (або правий клік → "Inspect") Крок 2: Вкладка "Elements" — бачиш живий DOM Стрілками розгортай дерево. Кліком на іконку "↖" (Inspect mode) і клік на елемент на сторінці — DevTools виділить цей елемент у дереві. Крок 3: Праворуч — панель "Styles" Бачиш всі CSS-правила, що застосовуються до елемента (від найвищого пріоритету вниз). Перекреслені правила — перевизначені іншими. Можеш клікнути і редагувати значення live. Крок 4: Вкладка "Computed" (праворуч) Фінальне обчислене значення КОЖНОЇ CSS- властивості після каскаду. Бачиш звідки прийшло правило (клік розкриває джерело). Box model: margin → border → padding → content. Крок 5: Редагування HTML live Правий клік на елемент в дереві → "Edit as HTML" → зміни зникнуть при reload, але ідеально для швидкого тестування. Крок 6: Drag & Drop в DOM Можна перетягувати вузли прямо в дереві — браузер одразу перерендерить.❌ Типова помилка початківця: Редагувати HTML-файл, рефрешити браузер, знову дивитись в Ctrl+U для дебагінгу. Правильний workflow: зміни в редакторі → рефреш → F12 → Elements для перевірки DOM і Styles для перевірки CSS.
16. DevTools — дебагінг HTML і DOM¶
Відкрити DevTools¶
| Дія | Windows/Linux | Mac |
|---|---|---|
| Відкрити DevTools | F12 або Ctrl+Shift+I | Cmd+Option+I |
| Inspect element | Ctrl+Shift+C | Cmd+Shift+C |
| Переглянути HTML source | Ctrl+U | Cmd+U |
| Консоль | Ctrl+Shift+J | Cmd+Option+J |
Elements Tab — живий DOM¶
Elements | Console | Sources | Network | ...
▼ <html lang="uk">
▼ <head>
<meta charset="UTF-8">
<title>Мій сайт</title>
▼ <body>
▼ <main id="main-content"> ← клікни щоб розгорнути
▼ <div class="card">
<h2>Заголовок</h2> ← клікни правою → Edit as HTML
<p>Текст</p>
Що можна робити в Elements: - Клік на елемент → праворуч показуються всі CSS-правила (Styles) - Двічі клік на атрибут → редагування на льоту - Правий клік → "Edit as HTML" → зміна HTML в реальному часі (не зберігається) - Drag & drop вузлів DOM → переставити елементи - "Computed" tab → фінальний box model після всіх CSS-правил
Console — швидкий доступ до DOM¶
// Знайти елемент
document.querySelector('.card') // перший .card
document.querySelectorAll('.card') // всі .card (NodeList)
document.getElementById('main-content') // за ID
// Змінити текст
document.querySelector('h1').textContent = 'Новий заголовок';
// Змінити стиль
document.querySelector('.card').style.background = 'red';
// Перевірити data-атрибути
document.querySelector('[data-note-id]').dataset
Source code vs DOM — ключова різниця¶
┌─────────────────────────────────┐
│ Django генерує HTML (server) │
└──────────────┬──────────────────┘
│ HTTP Response
▼
┌─────────────────────────────────┐
│ Ctrl+U — сирий HTML з сервера │
│ (статичний, незмінений) │
└──────────────┬──────────────────┘
│ Browser Parser
▼
┌─────────────────────────────────┐
│ F12 → Elements — живий DOM │
│ (може відрізнятись від source) │
│ (автовиправлення + JS-зміни) │
└─────────────────────────────────┘
Якщо верстка не так виглядає: завжди перевіряй DOM (F12), а не source (Ctrl+U). Браузер міг виправити твій HTML — і ти будеш шукати баг не там.
Цей повний приклад — не просто "код що працює". Це демонстрація КОЖНОЇ концепції з цього файлу в реальному контексті Django-шаблону. Читай його як ансамбль, а не як окремі елементи. Зверни увагу на кожне семантичне рішення — кожне тут присутнє з причиною, а не випадково.
Що розбирати в цьому прикладі (і чому):
<!DOCTYPE html>+lang="uk"— правильний старт, вмикає standards mode і налаштовує мову{% load static %}і{% static '...' %}— Django спосіб підключити CSS/JS (не hardcoded шляхи)<a href="#main-content" class="skip-link">— перший елемент<body>, accessibility для клавіатури<header role="banner">—role="banner"як резервний ARIA для старих скрінрідерів<nav aria-label="Головна навігація">— aria-label розрізняє кілька<nav>на сторінціaria-current="page"— скрінрідер каже "поточна сторінка", не просто "посилання"<main id="main-content" tabindex="-1">—tabindex="-1"дозволяє skip-link перемістити фокус сюдиaria-live="polite"на flash-повідомленнях — оголошується скрінрідером автоматично без перезавантаження<article>всередині<li>— кожна нотатка незалежна і могла б існувати поза списком<time datetime="Y-m-d">— машиночитаний формат дати для SEO і скрінрідерівdata-note-id="{{ note.pk }}"— передача Django-даних у JavaScript без прихованих inputsaria-label="Читати нотатку: {{ note.title }}"— кнопки "Читати →" унікальні для кожної нотатки<footer role="contentinfo">— семантика підвалу сторінки
🧠 Ментальна модель: Цей приклад — як партитура оркестру. Кожен інструмент (тег, атрибут) грає свою роль. Забрати будь-який елемент означає втратити частину звучання: без
aria-labelна кнопках — скрінрідер читає "Читати →, Читати →, Читати →" без розуміння куди; без<time datetime>— Google не розуміє коли опублікована стаття; безdata-note-id— JavaScript не знає ID нотатки без додаткового запиту.📚 Чому цей приклад важливий для Django-розробника: Django-розробник пише серверну логіку, але саме HTML-шаблон — це контракт між сервером і браузером. Поганий шаблон = погане SEO = менше користувачів. Недоступний шаблон = виключені користувачі. Правильний шаблон — це частина якості продукту, не менш важлива ніж правильна Django-view.
🌐 Як браузер обробляє цей шаблон (кроки): 1. Django рендерить шаблон: замінює
{{ note.title }},{% for %},{% url %}— отримує чистий HTML 2. Браузер отримує HTML, починає парсити зверху вниз 3. Знаходить<link rel="stylesheet">у<head>— паралельно завантажує CSS 4. Будує DOM поступово —<header>, потім<main>, потім<ul>з нотатками 5. Після повного завантаження CSS — будує Render Tree і відображає сторінку 6.<script>наприкінці<body>— виконується останнім, DOM вже готовий❌ Типова помилка початківця: Розміщувати
<script>у<head>безdefer. JavaScript у<head>блокує парсинг DOM — браузер зупиняється і чекає завантаження і виконання скрипта. Користувач бачить порожню сторінку. Рішення:<script>наприкінці<body>(як тут) або<script defer src="...">у<head>.
17. Повний приклад — сторінка нотаток із семантикою¶
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Мої нотатки | Hello Django</title>
<meta name="description" content="Список персональних нотаток">
{% load static %}
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
{# Пропуск навігації для скрінрідерів #}
<a href="#main-content" class="skip-link">Перейти до вмісту</a>
<header class="site-header" role="banner">
<nav aria-label="Головна навігація">
<a href="{% url 'notes_app:index' %}">Головна</a>
<a href="{% url 'notes_app:note_list' %}" aria-current="page">Нотатки</a>
</nav>
</header>
<main id="main-content" tabindex="-1">
{# Flash-повідомлення від Django #}
{% if messages %}
<section aria-live="polite" aria-label="Системні повідомлення">
{% for message in messages %}
<div class="alert alert-{{ message.tags }}" role="alert">
{{ message }}
</div>
{% endfor %}
</section>
{% endif %}
<section aria-labelledby="notes-heading">
<h1 id="notes-heading">Мої нотатки</h1>
{% if notes %}
<p>Знайдено нотаток: <strong>{{ notes|length }}</strong></p>
<ul class="notes-list" role="list">
{% for note in notes %}
<li
class="note-card"
data-note-id="{{ note.pk }}"
data-created="{{ note.created_at|date:'c' }}"
>
<article>
<header>
<h2 class="note-title">
<a href="{% url 'notes_app:note_detail' pk=note.pk %}">
{{ note.title }}
</a>
</h2>
<time
class="note-date"
datetime="{{ note.created_at|date:'Y-m-d' }}"
>
{{ note.created_at|date:"d.m.Y H:i" }}
</time>
</header>
{% if note.content %}
<p class="note-content">
{{ note.content|truncatewords:30 }}
</p>
{% endif %}
<footer class="note-actions">
<a
href="{% url 'notes_app:note_detail' pk=note.pk %}"
aria-label="Читати нотатку: {{ note.title }}"
>
Читати →
</a>
</footer>
</article>
</li>
{% endfor %}
</ul>
{% else %}
<p>
Нотаток ще немає.
<a href="{% url 'notes_app:note_create' %}">Створити першу →</a>
</p>
{% endif %}
</section>
</main>
<footer class="site-footer" role="contentinfo">
<p>
<small>© 2026 Hello Django. Всі права захищено.</small>
</p>
</footer>
<script src="{% static 'js/main.js' %}"></script>
</body>
</html>
Що тут правильно:
| Елемент | Чому саме так |
|---|---|
<main id="main-content" tabindex="-1"> |
Skip-link фокусується на main при натисканні |
<nav aria-label="Головна навігація"> |
Кілька <nav> на сторінці — розрізняємо їх |
aria-current="page" |
Скрінрідер оголошує "поточна сторінка" |
aria-live="polite" |
Flash-повідомлення оголошуються автоматично |
<article> всередині <li> |
Кожна нотатка — незалежний матеріал |
<time datetime="..."> |
Машиночитаний формат дати (SEO + accessibility) |
aria-label="Читати нотатку: ..." |
Кнопки "Читати →" — однакові без мітки |
data-note-id="{{ note.pk }}" |
Дані для JS без прихованих inputs |
18. Питання для самоперевірки¶
- Яка різниця між
<strong>і<b>? Коли використовувати кожен? - Чому
<h1>має бути один на сторінці? - Що відбудеться якщо написати
<div class="card">замість<article>? Чи це помилка? - Навіщо
alt=""(порожній alt) для декоративних зображень? - Браузер отримав
<p>Текст<p>Ще текст(без закриваючих тегів). Що буде в DOM? - Яка різниця між
Ctrl+U(source) іF12 → Elements(DOM)? Коли вони відрізняються? - Навіщо
{% csrf_token %}у POST-формі? Що станеться без нього? <img src="logo.png">безalt— яка проблема? Що краще:alt="logo"чиalt=""?- Як зробити щоб клік по
<label>активував<input>? - Чому
<table>не використовують для верстки макету — тільки для таблиць?
Шпаргалка — HTML в Django Templates¶
{# base.html — каркас всіх сторінок #}
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Мій сайт{% endblock %}</title>
{% load static %}
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<nav>
<a href="{% url 'notes_app:index' %}">Головна</a>
<a href="{% url 'notes_app:note_list' %}">Нотатки</a>
</nav>
<main>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% block content %}{% endblock %}
</main>
<footer>© 2026</footer>
<script src="{% static 'js/main.js' %}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>