Скелет страницы: doctype, head и первый файл на диске

разметка≈ 11 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Страницу можно написать и без обёртки: заголовок, абзац, сохранить — и браузер всё покажет. Недостающие <html>, <head> и <body> он достроит сам, молча и без единого предупреждения. Но четыре вещи достроить нечем: из каких байтов читать буквы, по каким правилам рисовать, какой шириной считать страницу на телефоне и как она назовётся на вкладке. Это как раз те строки, которых посетитель на странице не видит никогда, — и решают они почти всё. Разберём их тем же парсером, которым браузер читает любую страницу: что достраивается само, что куда попадает и что бывает, если строку пропустить или поставить не туда. А в конце создадим первый настоящий файл на диске.

Разделы темы:·

HTML-файл — обычный текст, и браузер читает его сверху вниз, как читают список покупок: строка за строкой, без заглядования вперёд. Часть браузера, которая этим занимается, называется парсером — от английского parse, «разбирать». Он раскладывает теги на две кучи: служебную, которая на экране не рисуется, и видимую. Правила разбора записаны в стандарте до мелочей и одинаковы во всех современных браузерах, поэтому результат можно не угадывать, а предсказать — и именно этим мы займёмся.

Смотреть будем не глазами, а тем же парсером: функция parse получает текст файла между обратными кавычками, а печатает одну строку из трёх частей через черту — режим отрисовки, теги, стоящие прямо внутри <head>, и теги, стоящие прямо внутри <body>. То, что вложено глубже, в строку не попадает: иначе в ней утонул бы весь магазин.

Внутри обратных кавычек нельзя писать саму обратную кавычку и пару знаков ${ — текст закончится раньше времени, и пример сломается.

Начнём с файла, в котором написано только видимое: заголовок да абзац. Ни <html>, ни <head>, ни <body> в нём нет вовсе. Предскажите все три части строки — и только потом жмите Run: угаданное задним числом не учит ничему.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Парсер открыл <head> и <body> сам: тегов рамки в файле не было, а вывод их называет. Значит, всю рамку — <html>, <head> и <body> — можно не писать. Но две другие части той же строки уже не в пользу файла. <head> пуст: служебных строк в файле не было, и придумать их парсеру неоткуда. А первым словом встал BackCompat — браузер решил, что страница старая и рисовать её надо по старым правилам. Откуда он это взял, объяснит третий раздел.

Достройка рамки — не хитрость и не поблажка: у парсера есть список служебных тегов, и он знает, куда их класть. Проверим на <title> в файле, где <head> снова не написан. Заодно сравните первое слово с прошлым примером: здесь <!doctype html> на месте.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Парсер знает наизусть, какие теги служебные: <title>, <meta>, <link>, <style>, <script>. Пока в начале файла идут только они, он складывает их в <head>. Первое слово текста или первый видимый тег — например, <p> — закрывает <head>: парсер открывает <body>, и с этого места всё подряд идёт туда. Он не читает файл наперёд и не возвращается назад, поэтому порядок строк решает всё.

Отсюда ловушка, в которую попадают даже опытные: служебная строка, поставленная после первого абзаца, остаётся служебной только по названию. Где окажутся <meta> и <title> в этом файле?

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Оба ушли в <body>, а <head> остался пустым. Браузер не жалуется и страницу открывает. Название даже доберётся до вкладки: браузер берёт первый <title> в документе, где бы тот ни стоял. Но это уже не та разметка, которую вы задумали, и полагаться на такие поблажки не стоит — в следующий раз повезёт меньше. Дописать после них </head> тоже не поможет: когда <body> уже открыт, лишний закрывающий тег парсер просто выбрасывает, как опечатку.

Так парсер ведёт себя со всей разметкой: молча достраивает недостающее и молча выбрасывает лишнее, ничего не сообщая. Это удобно — страница открывается даже с дырами в разметке, — и опасно: дыру не видно. Как именно он закрывает забытые теги и что делает с лишними закрывающими, покажет часть II в статье про дерево документа.

Рамку парсер достраивает сам: она одна на все файлы и записана в правилах разбора. А четыре вещи, названные в начале, ему взять неоткуда — их нет ни из чего в самом файле. Три из них ставятся строками в <head>: из каких байтов читать буквы, какой шириной считать страницу на телефоне и как назвать вкладку. Четвёртая — по каким правилам рисовать — идёт самой первой строкой файла, и о ней следующий раздел.

Файл на диске хранит не буквы, а байты — числа от 0 до 255. Кодировка и есть правило, по которому числа превращаются в буквы. В UTF-8 каждая русская буква занимает два байта, и те же два байта, прочитанные по другому правилу, дают совсем другие символы. <meta charset="utf-8"> называет правило, и стандарт требует, чтобы эта строка целиком уместилась в первые 1024 байта файла: браузер ищет её заранее, ещё до разбора остального. Поэтому её ставят первой строкой <head> — раньше <title>, где русские буквы тратят байты вдвое быстрее латинских и могут вытолкнуть объявление за границу.

Без этой строки браузер угадывает, и угадывает по-разному. Файл без неё, открытый двойным щелчком с диска, Chrome и Firefox сегодня распознают как UTF-8 и покажут правильно — поэтому пропуск незаметен, пока страница лежит у вас. Но тот же файл, пришедший с сервера, который кодировку в заголовках не сообщает, браузер может прочитать в старой однобайтовой кодировке. Chrome показывает тогда вместо «Привет, мир» строку «РџСЂРёРІРµС‚, РјРёСЂ», а другой браузер выдаст другую кашу. Страница при этом полностью рабочая: кнопки нажимаются, формы отправляются, и вы можете неделями не знать, что половину посетителей встречает набор знаков.

Строка charset ничего не стоит и снимает вопрос навсегда. Одно уточнение: это обещание, а не превращение. Сам файл должен быть сохранён в UTF-8 — объявление не перекодирует байты, которые уже лежат на диске. VS Code так и сохраняет по умолчанию, а вот Блокнот Windows в старых версиях предлагал выбор, и там промахнуться было легко.

meta charset — первой строкой head, ещё до title

Строка viewport нужна телефону и не нужна компьютеру. Без неё мобильный браузер рассуждает так: страница не объявила ширину, значит, она свёрстана для большого экрана. Он раскладывает её на 980 пикселей, а потом уменьшает снимок до размера экрана. Формально всё честно — страница целиком влезла, — а на деле читать её нельзя: текст мелкий, и приходится раздвигать его пальцами и возить по экрану. width=device-width говорит «ширина страницы равна ширине экрана», а initial-scale=1 — «не уменьшай». В рамке примера этого не увидеть: она не телефонный экран, и строки viewport в её собственном документе нет вовсе.

<title> на странице не виден, но его читают чаще любого заголовка. Это имя вкладки — той самой, по которой вы узнаёте сайт среди тридцати открытых; текст закладки и строки в истории браузера; а поисковики обычно ставят его заголовком синей ссылки в выдаче. Программа чтения с экрана произносит его первым, когда страница открывается, и по нему незрячий человек понимает, туда ли попал. Отсюда простое правило: у каждой страницы сайта название своё — «Корзина — Магазин», «Оформление заказа — Магазин», а не «Магазин» на всех тридцати вкладках сразу.

Первое слово в каждой строке вывода — тот самый режим. Выбирает его первая строка файла, <!DOCTYPE html>, «тип документа». Выглядит она как объявление версии, но работает иначе, и причина тому историческая. Когда браузеры научились рисовать страницы по стандарту CSS, в сети уже лежали миллионы страниц, свёрстанных под ошибки старых браузеров. Почини рендеринг — и они рассыплются у миллионов людей: у кого-то поедет вёрстка магазина, у кого-то перестанет открываться форма заявки. Выход нашли такой: браузер смотрит на начало файла. Современный doctype — рисуем по стандарту. Doctype нет или он старый — страница, видимо, тоже старая, и её рисуют в режиме совместимости (по-английски quirks mode), нарочно повторяя старые ошибки.

Ошибки эти вполне конкретные, а не «где-то чуть иначе». Ширину в стилях, записанную числом без единиц, — width: 100 вместо width: 100px — в режиме совместимости браузер понимает как сто пикселей, а в стандартном отбрасывает как ошибку: там единицы обязательны. Таблица не получает размер шрифта, заданный для всей страницы, — вы увеличили шрифт, а в ячейках он прежний. Сообщений об этом нет ни в консоли, ни в разметке: страница просто выглядит не так, как вы писали, и искать причину придётся глазами.

Режим parse печатает первым словом: CSS1Compat — стандартный, BackCompat — режим совместимости. Ниже три файла: вовсе без doctype, со старым doctype для HTML 4.01 и с коротким современным. Предскажите режим каждого — три строки по порядку, — и проверьте себя запуском.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Длинная строка во втором файле когда-то обещала «переходный» HTML 4.01 — вариант стандарта, где ещё разрешены устаревшие теги. Для браузера это примета старой страницы, и в таком виде, без адреса в конце, она включает тот же режим совместимости, что и отсутствие doctype. Стандартный режим даёт короткая форма <!DOCTYPE html>, и регистр в ней не важен: <!doctype html> работает так же, потому что doctype разбирают до всяких правил о регистре тегов.

Между «стандартным» и «совместимостью» есть ещё один — почти стандартный (almost standards mode). Его включает та же длинная строка HTML 4.01, но записанная полностью, с адресом DTD в конце: именно так её обычно и писали в редакторах нулевых. От старых правил в этом режиме остались мелочи, связанные с высотой строк: например, под картинкой, стоящей в строке без текста, сохраняется щель в несколько пикселей. Функция parse печатает этот режим тоже как CSS1Compat, потому что для расчёта стилей он почти не отличается от стандартного. Знать о нём полезно по одной причине: встретив в чужом коде длинный doctype с адресом, вы поймёте, откуда у страницы странная щель под изображениями.

Doctype не называет версию. Он решает одно: рисовать страницу по стандарту или по старым ошибкам.

Показать разницу на экране прямо здесь не выйдет, и это не недоработка примера. parse только разбирает текст и ничего не рисует, а режим выбирается один раз на весь документ — до того, как в него что-либо добавили. Страница, на которой выполняется каждый пример учебника, сама начинается с <!doctype html>, <html lang="ru"> и <meta charset="utf-8">, и переключить её режим изнутри нельзя. Поэтому дальше мы будем смотреть на строку вывода, а не на картинку.

Соберём теперь файл целиком — так, как его пишут каждый день: шесть служебных строк и два видимых тега. Всё это вы уже видели по отдельности, только в разных примерах.

Прежде чем запускать, предскажите: какие теги окажутся в <head>, а какие в <body>? Запишите ответ в поле под кодом и только потом жмите Run. Прогноз стоит денег ровно тогда, когда он сделан до ответа: угаданное задним числом не учит ничему.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

В <head> три тега, в <body> два. Самих <html>, <head> и <body> в списках нет: это рамка, по которой парсер раскладывает остальное, а не содержимое. Режим — CSS1Compat: <!doctype html> на месте, и именно из-за него вывод отличается от первого примера статьи, где стоял BackCompat.

СтрокаЗачем она
<!doctype html>включает стандартный режим: без неё браузер рисует страницу по старым правилам
<html lang="ru">язык страницы: по нему программа чтения с экрана выбирает произношение, а браузер — правила переноса слов
<meta charset="utf-8">кодировка: как превращать байты файла в буквы
<meta name="viewport" …>ширина страницы на телефоне равна ширине экрана
<title>имя вкладки, закладки и строки в истории браузера
<body>всё, что видно на странице: заголовки, текст, картинки, кнопки

Обратите внимание, кому адресован <head>: не человеку, а браузеру, поисковикам и программам чтения с экрана. Человек не увидит ни charset, ни viewport — он увидит последствия. Поэтому пропуск любой из этих строк не вызывает ни одного сообщения об ошибке: страница откроется, и вы даже не узнаете, что сделали что-то не так, пока не откроете её на телефоне или не пришлёте другому человеку.

Шесть строк — это разница между «документом, который как-то открылся», и страницей, которая ведёт себя предсказуемо на любом устройстве. Рамку парсер дорисует сам, с неё статья и началась; эти шесть строк дописать нечем — их знает только тот, кто сохранил файл.

В <!DOCTYPE html> нет цифр, и это не упущение, а честность: у HTML нет версии. Стандарт один, его ведёт WHATWG — объединение разработчиков браузеров, — и правки в него вносят постоянно, поэтому стандарт называют живым. «HTML5» — имя эпохи примерно 2008–2014 годов, когда в язык пришли <video>, <canvas> и смысловые теги вроде <header> и <nav>. В вакансиях и старых книгах слово осталось и означает просто «современный HTML», без номера.

Версии нет, а вопрос «можно ли уже так писать» никуда не делся: новые теги и свойства приходят в браузеры не одновременно — Safari опаздывает то на год, то на два. Ответ на этот вопрос даёт статус Baseline: его показывают, например, страницы справочника MDN рядом с каждым свойством. Статус считают по основным браузерам — Chrome, Edge, Firefox и Safari на компьютере, а также Chrome, Firefox и Safari на телефоне.

СтатусЧто значитЧто делать
Widely availableвозможность есть во всех основных браузерах, и с тех пор прошло 30 месяцевпишите
Newly availableвозможность есть во всех основных браузерах, но последний из них добавил её меньше 30 месяцев назадзнайте о ней, но помните, что у части читателей браузер старее
Limited availabilityхотя бы в одном основном браузере её нетне рассчитывайте на неё без запасного пути

Порог в 30 месяцев кажется придирчивым, но он практичный: за это время старые версии успевают уйти у тех, кто не обновляется сам, — корпоративные парки, старые телефоны. Когда в прологе встретится свежая возможность, рядом будет её статус. Всё из этой статьи — doctype, <meta>, <title> — давно работает везде и не требует никаких оговорок.

До сих пор скелет разбирала функция в песочнице учебника. Настоящая страница — это файл, и создать его стоит своими руками: это первый шаг пролога вне песочницы, он ждёт вас в первой задаче ниже. Подойдёт любой редактор простого текста — только не Word и не Pages: они сохраняют не текст, а свой формат с невидимыми символами. В бесплатном VS Code скелет не придётся даже набирать: создайте пустой файл index.html, введите ! и примите подсказку Emmet. Появится заготовка, в которой останется заменить lang="en" на lang="ru", а Document — на своё название.

Сохранённый файл открывается двойным щелчком — в браузере, который стоит у вас по умолчанию. Адрес такой страницы не похож на адрес сайта: это путь к файлу на диске со схемой file:/// в начале. Щёлкните в адресную строку, и он будет виден целиком, например file:///Users/имя/prologue/index.html. Все адреса внутри страницы браузер отсчитывает от этого пути, и с косой чертой в начале адреса там есть своя ловушка — о ней статья этой главы про адреса от файла на диске.

Чем наполнить <body>, чтобы у страницы был не только вид, но и смысл, — тема следующей статьи главы, и файл из первой задачи пригодится уже там. Сам скелет понадобится и позже: скрипт встанет в него же, в <head> или перед </body>, — это статья про первую программу и тег script. В конце части I проект «список задач» соберётся из трёх файлов рядом — index.html, style.css и app.js, — и в плане проекта первым из них идёт как раз index.html.

  • HTML-файл — текст, который парсер браузера раскладывает на служебный <head> и видимый <body>. Правила разбора одни для всех браузеров, а теги <html>, <head> и <body> парсер достроит, даже если их не писать.
  • Первое слово текста или первый видимый тег закрывает <head>: всё, что ниже, даже <meta> и <title>, парсер кладёт в <body>. Лишние закрывающие теги он выбрасывает молча.
  • <meta charset="utf-8"> — первой строкой <head>, в первых 1024 байтах файла. Без неё файл с диска Chrome и Firefox прочтут верно, а тот же файл с сервера может превратиться в кашу из чужих букв.
  • viewport не даёт телефону раскладывать страницу на 980 пикселей и уменьшать её. <title> — имя вкладки и закладки, а в поисковой выдаче обычно заголовок ссылки; у каждой страницы своё.
  • Doctype — не версия, а выключатель режима. <!doctype html> в любом регистре даёт стандартный режим (CSS1Compat). Без doctype или со старым браузер рисует в режиме совместимости (BackCompat), нарочно повторяя старые ошибки, и ничего об этом не сообщает.
  • У HTML нет версии: стандарт живой, «HTML5» — имя эпохи. Можно ли писать, решает Baseline: Widely available — пишите, Newly — знайте, Limited — не рассчитывайте.

1/4Файл начинается так: <!doctype html><p>Скидки</p><title>Магазин</title>. Что будет с <title>?

2/4Почему <meta charset="utf-8"> ставят первой строкой <head>?

3/4Какая первая строка файла включает стандартный режим?

4/4У возможности статус Baseline Newly available. Что это значит?

Первый файл на диске

Создайте папку для пролога, а в ней — файл index.html со скелетом из статьи. В <title> напишите своё название, в <body> — заголовок <h1> и абзац по-русски. Сохраните файл и откройте его двойным щелчком.

Проверьте три вещи: название видно на вкладке, русский текст читается без каши, а адрес страницы начинается с file:///. Если вместо страницы вы видите собственный код текстом или файл снова открылся в редакторе — дело не в разметке, а в том, как файл сохранён и как он называется.

Файл целиком:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Две частые причины, по которым страница не открывается как страница. В Windows проводник по умолчанию прячет расширения: файл, переименованный из «Текстового документа», на деле может называться index.html.txt и открываться в Блокноте — включите показ расширений и уберите лишнее. В TextEdit на Mac документ сначала переведите в простой текст («Формат» → «Сделать простым текстом»): в режиме форматированного текста TextEdit сохранит свой собственный HTML, где ваши теги станут обычными буквами, и браузер покажет их как текст.

Скелет с тремя ошибками

Автор этого файла уверен, что всё записанное между <head> и </head> попадёт в служебную часть, а кодировка объявлена как положено. Предскажите строку вывода и запустите пример.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Теперь исправьте текст страницы: стандартный режим, кодировка первой строкой <head>, абзац — под заголовком. Вывод должен стать таким: CSS1Compat | head: meta, title | body: h1, p. Текста примера правка касаться не должна: меняйте только разметку внутри обратных кавычек.

Ошибок три. Нет doctype — отсюда BackCompat в начале строки. Абзац внутри <head> на деле закрыл <head>: парсер открыл <body> прямо перед ним, поэтому <meta> ниже тоже уехала в <body>, а написанные дальше </head> и <body> он пропустил как лишние. Отсюда вывод BackCompat | head: title | body: p, meta, h1. Третья ошибка — кодировка после <title>: даже внутри <head> ей место первой, иначе длинный русский заголовок может вытолкнуть объявление за 1024 байта.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.