В прошлой статье я подробно разбирал линтинг кода: как подключить ESLint, как автоматизировать проверки через хуки, как сделать так, чтобы в репозиторий физически не попадал «грязный» код.

Вот ссылка на предыдущую статью!

Но со временем я заметил странный перекос.

Мы очень строго относимся к исходникам, к JavaScript или TypeScript, к тестам, к типам. Там всё проверяется, форматируется, валидируется. А вот всё, что находится вокруг — HTML-шаблоны, Markdown-документация, YAML-конфиги — часто живёт по принципу «и так сойдёт».

Ни ESLint, ни тесты, ни типизация не скажут вам, что в YAML лишний пробел или в HTML забыли alt у картинки. Для них это всего лишь текст.

А для продакшена — потенциальный баг.

Когда я прихожу в новый проект и провожу аудит, почти всегда вижу одну и ту же картину. Код аккуратный, линтеры строгие, CI настроен. Но стоит открыть разметку или конфиги — и начинается творческий беспорядок. Кто-то форматирует по одному, кто-то по другому, кто-то копирует куски из StackOverflow, не особо понимая синтаксис.

Получается парадокс: мы защищаем самую очевидную часть системы и игнорируем инфраструктуру, документацию и шаблоны. Хотя по факту это такие же контракты проекта, просто записанные не на языке программирования, а на языках разметки.

Со временем я перестал разделять «код» и «не код». Если файл участвует в работе продукта — он должен быть проверяемым. Автоматически. Без надежды на внимательность разработчика.

Дальше я покажу, как именно это выглядит на практике и какие инструменты я использую каждый день.

Перед началом: как я применяю лин��еры на практике

Прежде чем углубляться в конкретные инструменты для HTML, Markdown и YAML, хочу поделиться своим подходом к применению линтеров в реальной работе.

В своей практике я использую два основных формата проверки файлов.

Первый — локальный, на уровне коммитов. Для этого у меня есть проверенные shell-скрипты, размещённые в репозитории. Они позволяют запускать линтеры только на тех файлах, которые были изменены и попали в git индекс. Это экономит время и ресурсы разработчика: линтер не тратит лишнее время на проверку всего проекта, а концентрируется только на новых или изменённых файлах.

Пример запуска локальной проверки через pre-commit выглядит так:

#!/bin/bash
set -e

ALL_FILE_ARRAY=()
while IFS= read -r line; do
    ALL_FILE_ARRAY+=("$line")
done < <(git diff --cached --name-only --diff-filter=ACM || true)

echo "Markdown code style checker..."
bash scripts/check_scripts/check_markdownlint.sh "${ALL_FILE_ARRAY[@]}"
echo "----------"

Все представленные в этой записи скрипты, я разместил в своём репозитории:

https://github.com/prog-time/git-hooks

Эти скрипты интегрированы через git-хуки, обычно pre-commit или pre-push. Хук получает список изменённых файлов, передаёт их в скрипт, и линтер проверяет именно эти файлы. Если находятся ошибки, коммит или пуш блокируется, и разработчик сразу получает обратную связь.

Второй формат проверки — полная проверка проекта в CI. Здесь линтеры анализируют все файлы, независимо от того, что изменилось. Это полезно для выявления старых ошибок, которые могли «просочиться» раньше, или для полной регрессионной проверки после обновления конфигурации линтеров. В моём репозитории такие скрипты имеют суффикс _all, чтобы отличать их от версий, работающих с отдельными файлами.

Привет работы линтеров в CI
Привет работы линтеров в CI

Использование комбинации локальных проверок через хуки и полной проверки в CI — это то, что я называю двухуровневой стратегией линтинга. Локально линтер экономит время разработчика и ловит ошибки сразу, а CI гарантирует, что весь проект остаётся консистентным, даже если что-то пропустили на этапе коммита.

HTML-линтинг: htmlhint как первый рубеж валидации шаблонов

CSS опирается на структуру DOM. JavaScript опирается на селекторы и атрибуты. Поисковики — на валидность и иерархию.

И при этом именно HTML чаще всего пишут в режиме «да браузер всё равно проглотит».

Браузеры действительно очень терпеливые. Они автоматически закрывают теги, чинят вложенность, додумывают структуру. Но они не обязаны работать так, как вы ожидаете.

Когда вы полагаетесь только на рендеринг, вы перекладываете ответственность на поведение движка. Но движков несколько, и у каждого свои особенности. То, что Chrome аккуратно «починил», в Firefox или Safari может дать совершенно другую структуру DOM.

Плюс есть вещи, которые браузер вообще не обязан проверять. Например, наличие alt у изображений или корректную семантику заголовков. Для него это валидно. Для пользователя — нет.

С точки зрения доступности и поддержки продукта такие «мелочи» стоят очень дорого. Особенно когда проект растёт и шаблонов становится сотни.

В своих проектах я ставлю HTML-линтер буквально одним из первых инструментов. Сейчас это HTMLHint.

Он делает ровно то, что должен делать хороший линтер: спокойно и без лишнего шума находит структурные проблемы, которые человек легко пропускает глазами.

Речь не только про банальные незакрытые теги. Линтер проверяет корректность атрибутов, следит за семантикой, предупреждает о странной вложенности элементов, помогает не забывать обязательные вещи вроде alt или уникальных id. По сути, это автоматический «придирчивый ревьюер», который никогда не устает.

Особенно хорошо он ловит кроссбраузерные грабли — некорректный nesting, дублирование атрибутов, потенциально опасные конструкции, которые потом превращаются в трудноуловимые визуальные баги.

Я не сторонник агрессивных правил ради правил. Конфиг должен решать реальные проблемы, а не мешать работе. Поэтому обычно начинаю с довольно приземлённого набора.

Вот пример конфига .htmlhintrc:

{
  "tagname-lowercase": true,
  "attr-lowercase": true,
  "attr-value-double-quotes": true,
  "attr-no-duplication": true,
  "doctype-first": false,
  "empty-tag-not-self-closed": true,
  "id-unique": true,
  "src-not-empty": true,
  "title-require": false,
  "alt-require": true,
  "id-class-value": false,
  "style-disabled": false,
  "inline-style-disabled": false,
  "head-script-disabled": false,
  "img-alt-require": true,
  "attr-unsafe-chars": true
}

Плюсы конфигурации:

  1. tagname-lowercase: true
    Требует писать имена тегов в нижнем регистре (div, img, section).

  2. attr-lowercase: true
    Атрибуты только в нижнем регистре (class, href, src).

  3. attr-value-double-quotes: true
    Значения атрибутов только в двойных кавычках.

  4. attr-no-duplication: true
    Запрещает дублирование атрибутов (class=”a” class=”b”).

  5. doctype-first: false
    Не требует <!DOCTYPE html> в начале файла. Удобно для partials, layout’ов и шаблонов, где doctype генерируется отдельно.

  6. empty-tag-not-self-closed: true
    Требует корректный синтаксис для пустых (void) тегов (img, br, hr)..

  7. id-unique: true
    Каждый id должен быть уникальным.

  8. src-not-empty: true
    Запрещает пустой src у img/script/iframe.

  9. title-require: false
    Не требует title в каждом файле. Подходит для шаблонных систем.

  10. alt-require: true
    Требует alt у изображений.

  11. id-class-value: false
    Не навязывает стиль имен (kebab-case, BEM и т.д.).

  12. style-disabled: false
    Разрешает тег style. Практично для контентных страниц.

  13. inline-style-disabled: false
    Разрешает inline style. Для больших приложений нежелательно, но для блога нормально.

  14. head-script-disabled: false
    Разрешает script в head.

  15. img-alt-require: true
    Дополнительно проверяет наличие alt именно у img.

  16. attr-unsafe-chars: true
    Запрещает небезопасные символы в атрибутах. Защищает от кривой разметки и потенциальных XSS/парсинг-ошибок.

Вот пример скрипта для проверки HTML файлов всего проекта:

#!/bin/bash
# ----------------------------------------
# HTML Code Checker (Full Project)
#
# This script checks all HTML files in the
# project for issues using HTMLHint.
# Used in pre-push-check.sh
#
# Usage:
#   ./check_htmlhint_all.sh
# ----------------------------------------

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"

HTML_FILES=$(find "$PROJECT_DIR" -name "*.html" -not -path "*/_site/*" -not -path "*/node_modules/*" -not -path "*/.git/*")

if [ -z "$HTML_FILES" ]; then
    echo "No HTML files found"
    exit 0
fi

exec "$SCRIPT_DIR/check_htmlhint.sh" $HTML_FILES

На практике этого достаточно, чтобы заметно снизить количество мелких багов в шаблонах.

Markdownlint: линтер для документации и публикаций

Когда речь заходит о документации, многие думают: «Markdown — это просто текст, ошибок здесь быть не может». На практике это заблуждение.

Markdown — это не просто README. Это спецификации, инструкции для коллег, шаблоны PR, чеклисты и даже часть автоматических changelog‑ов. И если в этих файлах допустить опечатку или сбой форматирования, последствия будут заметны: списки съезжают, заголовки теряют иерархию, ссылки становятся битые, изображения не отображаются, и автоматические сборки changelog‑ов ломаются.

Markdownlint помогает автоматизировать проверку, ловя сразу несколько классов проблем. Он следит за последовательностью заголовков, соблюдением отступов и пробелов вокруг элементов, корректным оформлением списков и длиной строк. Линтер проверяет ссылки и изображения, что критично для документации, которая активно используется в CI/CD пайплайнах или генерирует автоматические отчёты.

Для меня Markdownlint стал неотъемлемой частью работы с текстом, особенно когда речь идёт о блоге. Я использую Jekyll в качестве движка, и весь контент хранится в Markdown‑файлах.

В своём блоге я использую следующую конфигурацию .markdownlint.yml:

# Markdownlint configuration for blog project

# Disable line length — blog posts have long prose lines
MD013: false

# Disable blanks around lists — Jekyll content often has lists right after text
MD032: false

# Disable unordered list style — allow both - and *
MD004: false

# Disable list marker space — allow flexible spacing
MD030: false

# Disable trailing spaces — not critical for blog
MD009: false

# Disable fenced code language — not all code blocks need a language
MD040: false

# Disable emphasis as heading — used stylistically in posts
MD036: false

# Disable bare URLs — acceptable in blog content
MD034: false

# Disable multiple blank lines — used for visual separation
MD012: false

# Disable blanks around headings — Jekyll layout constraints
MD022: false

# Disable ordered list prefix style — allow 1. 2. 3.
MD029: false

# Disable blanks around fences — layout constraints
MD031: false

# Disable heading increment — blog posts may skip heading levels
MD001: false

# Disable table column style — flexible table formatting
MD060: false

# Require single trailing newline
MD047: true

Плюсы конфигурации:

  1. Гибкость в длине строк (MD013: false)
    Отлично для блогов с длинными абзацами. Не придётся ломать строки искусственно.

  2. Более свободная работа со списками (MD004, MD030, MD029, MD032)
    Позволяет использовать разные маркеры (- или *) и разные стили нумерации.

  3. Стиль и визуальное оформление (MD036, MD034, MD012, MD022, MD031)
    Разрешены пропуски между блоками, пропуски вокруг заголовков и визуальные разделители. Это удобно для Jekyll-шаблонов и при использовании Markdown как исходника для HTML с кастомным CSS.

  4. Техническая гибкость (MD009, MD040, MD047)
    Игнорируются trailing spaces и отсутствие указания языка в fenced code blocks. Требуется только один trailing newline (MD047), что важно для корректной компиляции Markdown в HTML.

Вот пример скрипта для проверки Markdown файлов всего проекта:

#!/bin/bash
# ----------------------------------------
# Markdown Code Style Checker (Full Project)
#
# This script checks all Markdown files in the
# project for style issues using markdownlint.
# Used in pre-push-check.sh
#
# Usage:
#   ./check_markdownlint_all.sh
# ----------------------------------------

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"

MD_FILES=$(find "$PROJECT_DIR" -name "*.md" -not -path "*/_site/*" -not -path "*/node_modules/*" -not -path "*/.git/*")

if [ -z "$MD_FILES" ]; then
    echo "No Markdown files found"
    exit 0
fi

exec "$SCRIPT_DIR/check_markdownlint.sh" $MD_FILES

Markdownlint стал для меня таким же естественным инструментом, как ESLint для кода: не для красоты, а для надёжности и предсказуемости проекта.

YAML: где мелкая ошибка стоит дорого

Если HTML и Markdown иногда «прощают» мелкие ошибки, то YAML почти никогда.

Это один из самых популярных форматов в DevOps:

  • CI/CD пайплайны,

  • Docker Compose,

  • GitHub Actions,

  • Kubernetes-манифесты

Почти вся современная инфраструктура описывается именно в YAML.

Проблема в том, что YAML очень чувствителен к отступам, спецсимволам и контексту. Один лишний или недостающий пробел способен превратить рабочий пайплайн в silent‑fail: процесс просто не запускается, а причина в логах бывает неочевидной.

На практике я решаю эту проблему с помощью yamllint. Он не только ловит синтаксические ошибки, но и проверяет структуру файлов, соблюдение отступов, корректность типов и повторяющихся ключей. По сути, это автоматический сторож, который гарантирует, что конфиги остаются читаемыми и работоспособными.

Особенно полезно интегрировать yamllint прямо в CI/CD пайплайн. На этапе сборки или тестирования он проверяет все YAML‑файлы, и если есть нарушения — сборка падает ещё до деплоя.

Вот пример конфигурации .yamllint.yml:

---
extends: default

rules:
  line-length:
    max: 200
  truthy:
    check-keys: false
  comments:
    min-spaces-from-content: 1
  document-start: disable
  braces:
    max-spaces-inside: 1
  indentation:
    spaces: 2
    indent-sequences: true

Плюсы конфигурации:

  1. line-length: max 200
    Значение 200 — очень гибкое. Позволяет писать длинные строки, например, длинные ссылки, сложные тексты или JSON в YAML. Для блог-постов или конфигураций Jekyll/Hugo удобно.

  2. truthy: check-keys: false
    Отключает проверку ключей на “truthy” (yes, no, on, off). Удобно, если ключи используются в строковом виде (“yes”, “no”), а не как булевы значения.

  3. comments: min-spaces-from-content: 1
    Требует хотя бы 1 пробел после #. Хорошая практика, делает комментарии читаемыми.

  4. document-start: disable
    Не обязательно ставить — в начале файла. Часто нужно для небольших YAML-конфигов или фронтматтеров Jekyll.

  5. braces: max-spaces-inside: 1
    Контролирует пробелы внутри {} и []. Например, { key: value } с 1 пробелом — аккуратно, но не строгая фиксация. Полезно, если в проекте есть inline объекты или массивы.

  6. indentation: spaces: 2
    Классические 2 пробела, что стандартно для большинства проектов.

  7. indent-sequences: true значит, что элементы списков должны быть на том же уровне, что и их родительский блок, а не на один уровень меньше.

Использование yamllint превращает YAML из потенциального источника скрытых багов в предсказуемый и надёжный инструмент управления инфраструктурой. И чем раньше его подключить, тем меньше «маленьких» ошибок потом вырастают в большие проблемы.

Итог

Благодаря линтерам ревьюеры могут концентрироваться на том, что действительно важно: архитектуре, дизайну интерфейса, логике и решениях, которые влияют на продукт. Они больше не тратят время на исправление лишних пробелов, неправильных отступов или мелких синтаксических ошибок в YAML, Markdown или HTML.

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

В итоге линтеры превращают хаос в предсказуемость, случайности — в стабильность, а рутинные ошибки — в уверенность. И это именно то, что делает проекты масштабируемыми и команды эффективными.