Перейти до змісту

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> для створення відступів між параграфами замість CSS margin. Це зламає розмітку при зміні 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 і маркери/цифри. CSS list-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: Розглянь два варіанти:

❌ <div class="nav">               ✅ <nav>
     <a href="/">Головна</a>            <a href="/">Головна</a>
   </div>                           </nav>
Обидва виглядають однаково у браузері. Але для 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. Inline style="" — неможливо змінити через CSS. Нема {% csrf_token %} — Django повертає 403 і форма не працює. Неправильний for/id — клік по <label> не фокусує <input>, що критично для мобільних і accessibility.

🌐 Як браузер бачить типові помилки: <br><br> — браузер відображає як порожні рядки, але скрінрідер читає їх як "порожній рядок, порожній рядок". <table> для верстки — браузер застосовує table layout algorithm: колонки стискаються до мінімальної ширини вмісту, що ламає responsive design. Inline style="" — специфічність 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> Помилка валідації
<!-- Першим елементом в <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 без прихованих inputs
  • aria-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. Питання для самоперевірки

  1. Яка різниця між <strong> і <b>? Коли використовувати кожен?
  2. Чому <h1> має бути один на сторінці?
  3. Що відбудеться якщо написати <div class="card"> замість <article>? Чи це помилка?
  4. Навіщо alt="" (порожній alt) для декоративних зображень?
  5. Браузер отримав <p>Текст<p>Ще текст (без закриваючих тегів). Що буде в DOM?
  6. Яка різниця між Ctrl+U (source) і F12 → Elements (DOM)? Коли вони відрізняються?
  7. Навіщо {% csrf_token %} у POST-формі? Що станеться без нього?
  8. <img src="logo.png"> без alt — яка проблема? Що краще: alt="logo" чи alt=""?
  9. Як зробити щоб клік по <label> активував <input>?
  10. Чому <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>