Что такое статический контент сайта и что к нему относится
Статический контент - это файлы, которые сервер отдаёт клиенту без генерации на каждый запрос. Он читает их с диска или из кэша, добавляет HTTP-заголовки и отправляет браузеру. Запрос к /static/css/main.css возвращает одинаковые байты до тех пор, пока файл не изменился. Отсюда два практических следствия: статику просто кэшировать, а её отдача почти не расходует CPU сервера.
В состав статики на типовом сайте входят восемь групп файлов:
- готовые HTML-страницы;
- CSS-файлы, исходные и минифицированные;
- JavaScript-файлы и собранные бандлы;
- изображения: PNG, JPEG, WebP, AVIF, SVG, ICO;
- медиа: MP4, WebM, MP3, OGG, WAV;
- документы: PDF, DOCX, XLSX, CSV;
- шрифты: WOFF, WOFF2, TTF, OTF, EOT;
- служебные файлы: robots.txt, sitemap.xml, favicon.ico, manifest.json, .htaccess, web.config, humans.txt, security.txt.
Каждая группа имеет свои расширения и свои правила хранения. Ниже - критерии, по которым файл относят к статике, а не к шаблонам или пользовательским данным.
HTML, CSS и JavaScript: базовый набор статики
Готовый HTML-файл - статика. Файл index.html лежит на диске и отправляется как есть. Шаблон index.twig, index.blade.php или index.jinja2 к статике не относится: его обрабатывает шаблонизатор при каждом запросе, подставляя переменные и выполняя условия.
Тот же принцип для стилей и скриптов. Файл static/css/style.css, который уходит браузеру, - статика. Исходник src/scss/style.scss - нет: его компилируют в CSS на этапе сборки. Минифицированные версии вроде style.min.css тоже относятся к статике, если сборщик подготовил их заранее и сервер отдаёт готовый файл.
Когда страницы собирают заранее (подход SSG), HTML попадает в ту же категорию: сервер отдаёт подготовленный файл, а не собирает страницу из шаблона. Как выбирать между предварительной сборкой, серверным и клиентским рендерингом, разобрано в руководстве по стратегиям рендеринга.
Изображения, медиа, документы и шрифты
Эти файлы хранятся на диске целиком и отдаются как есть, без преобразования на лету:
- изображения: PNG, JPEG, WebP, AVIF, ICO, SVG;
- видео: MP4, WebM;
- аудио: MP3, OGG, WAV;
- документы: PDF, DOCX, XLSX, CSV;
- шрифты: WOFF, WOFF2, TTF, OTF, EOT.
Современные форматы сжатия уменьшают вес изображения: по данным сравнений, WebP и AVIF весят на 25–50% меньше JPG при том же качестве, а AVIF может сжимать картинку на 20–40% эффективнее WebP при равном качестве. Поэтому для одной картинки часто держат несколько форматов и подключают их через тег picture: браузер сам выбирает первый поддерживаемый формат, обычно сначала предлагают AVIF, затем WebP, а JPG оставляют как универсальный запасной вариант. Автоматический выбор формата по заголовку Accept в проверенных материалах не подтверждён, поэтому на него лучше не опираться как на основной механизм.
SVG занимает пограничное положение: как отдельный файл icon.svg он статика, а инлайн-вставка того же SVG в разметку становится частью HTML-документа и кэшируется вместе с ним.
Служебные файлы: robots.txt, sitemap.xml, favicon и другие
Служебные файлы обычно лежат в корне сайта и тоже отдаются статикой: robots.txt, sitemap.xml, favicon.ico, manifest.json, humans.txt, security.txt. Конфигурации .htaccess и web.config попадают в эту же группу, хотя их читает веб-сервер, а не браузер.
sitemap.xml бывает обоих типов. На статическом сайте это подготовленный файл, а в CMS его часто генерирует модуль по расписанию. Если карта сайта собирается на каждый запрос, к статике она не относится.
Как отделить статику от шаблонов и пользовательских загрузок
Три категории файлов живут на одном сервере, но требуют разного обращения. Путаница между ними приводит к потере данных при деплое и к неверному кэшированию.
| Категория | Типичные пути | Кто меняет содержимое | Кэширование |
|---|---|---|---|
| Статика | public/static/, public/assets/ | Разработчик, через релиз | Долгое, с версией в имени файла |
| Шаблоны | templates/, views/, themes/ | Разработчик, через релиз | Не кэшируется, обрабатывается на сервере |
| Пользовательские загрузки | uploads/, media/, /mnt/data/uploads/ | Пользователи, API, админка | Осторожное, отдельный бэкап |
Шаблоны: почему они не статика
Шаблон содержит переменные, циклы и условия, а обрабатывает его шаблонизатор (Twig, Blade, Jinja2, Handlebars) на стороне сервера. Результат обработки - HTML, который уходит в ответ. Сам файл шаблона браузер не получает никогда.
Практический критерий простой: если файл лежит в templates/ или views/ и содержит управляющие конструкции, он не статика. Некоторые CMS кэшируют результат рендеринга в HTML-файл, и такая копия ведёт себя как статика: отдаётся напрямую, пока не истечёт срок жизни кэша.
Пользовательские загрузки: почему их нельзя смешивать со статикой
Загрузки приходят через формы, API и админку: аватары, вложения, изображения товаров. Их содержимое меняется без участия разработчика, и перезаписывать их при релизе нельзя.
Каталог static/uploads/ опасен: при деплое, который заменяет весь static/, файлы пользователей исчезнут. Безопаснее хранить их отдельно, например в var/uploads/, /mnt/data/uploads/ или в объектном хранилище. Разделение даёт три эффекта: деплой не затрагивает данные, бэкап загрузок идёт по своему расписанию, а права на запись выдаются только каталогу загрузок.
Типовая структура каталогов статики сайта
Рабочая схема отвечает на один вопрос: что видит веб-сервер, а что остаётся в репозитории. Корень сайта (обычно public/) содержит только то, что должно быть доступно по HTTP. Исходники, конфигурации сборки и пользовательские загрузки лежат вне него.
Базовый вариант: public/static с подкаталогами
Минимальная структура, которую можно применить сразу:
public/
index.html
robots.txt
sitemap.xml
favicon.ico
static/
css/
js/
img/
fonts/
media/
docs/
vendor/
Каталог vendor/ отведён под сторонние библиотеки: bootstrap, jquery, шрифтовые иконочные наборы. Свои файлы держат рядом, но отдельно, иначе обновление библиотеки затирает собственную вёрстку. Каталог docs/ хранит PDF и другие документы, media/ - видео и аудио. Такая схема подходит статическим сайтам и небольшим проектам без сборщика.
Вариант для сборки: src/ и dist/
Когда есть сборщик, исходники и результат разводят по разным каталогам:
src/ scss/style.scss js/app.js img/hero.png dist/ css/style.min.css js/app.min.js img/hero.webp
Веб-сервер отдаёт только dist/, а src/ хранится в git и на продакшен не копируется. Каталог dist/ пересоздают при каждом деплое, поэтому руками его не правят: правки, внесённые прямо в него, потеряются при следующей сборке. Каталог dist/ удобно держать в .gitignore, чтобы не смешивать артефакты с исходниками.
Вариант для CMS: отделение статики от шаблонов и загрузок
В WordPress стили и скрипты темы обычно лежат в wp-content/themes/имя_темы/assets/, файлы шаблонов - в wp-content/themes/имя_темы/templates/, загрузки - в wp-content/uploads/. Плагины приносят собственную статику в своих каталогах.
В Drupal статика темы и модулей лежит внутри их каталогов, а загрузки - в sites/default/files/. Битрикс хранит статику шаблона в local/templates/ или bitrix/templates/, а загрузки - в upload/.
Частая ошибка в CMS: изображения из редактора складывают в assets/ темы, рядом с файлами вёрстки. Тогда очистка assets/ перед релизом уносит пользовательский контент, который восстановить из репозитория не получится.
Правила именования и версионирования статических файлов
Имена файлов читаются в логах, в конфигурациях nginx, в скриптах деплоя и в коде. Предсказуемые имена экономят время при разборе инцидентов.
Правила именования: регистр, разделители, запрещённые символы
Рабочее правило для команды: латиница, цифры, дефис и подчёркивание, все буквы в нижнем регистре. Пробелы заменяют дефисом. Кириллица в именах создаёт проблемы при переносе между файловыми системами и при кодировании в URL.
Хорошие имена: main-style.css, hero-image.webp, icon-search.svg, guide-2026-09-22.pdf. Плохие: file1.css, new.css, final.css, final-final2.css, Стили.css, MainStyle.CSS. Отдельно стоит отказаться от дат вида 12.09.2026 в имени: точки и порядок частей ломают сортировку. Формат ГГГГ-ММ-ДД сортируется как обычная строка.
Cache busting: зачем добавлять хеш в имя файла
Браузер кэширует файл по URL. Если URL не меняется, браузер отдаёт сохранённую копию, и пользователь видит старую вёрстку после обновления сайта. Решение - менять URL вместе с содержимым.
Способов два. Первый: параметр версии, /css/style.css?v=2. Второй: хеш содержимого в имени, style.a1b2c3.css. Хеш надёжнее: он меняется только при реальном изменении файла, а не при каждом релизе, поэтому кэш не сбрасывается зря. Webpack добавляет такой хеш автоматически через подстановку [contenthash]: она формирует уникальный хеш на основе содержимого актива, и при изменении содержимого хеш тоже меняется (документация webpack по кэшированию). Для Vite аналогичное поведение в проверенных материалах не подтверждено, поэтому хеширование имён стоит проверять в конфигурации проекта.
Для файлов с хешем в имени ставят длинный срок жизни и флаг immutable. Расширение immutable в Cache-Control позволяет серверам помечать ресурсы, которые не будут обновляться в течение их срока свежести, чтобы клиент не выполнял условные запросы для проверки изменений (RFC 8246). RFC 8246 описывает паттерн версионированных URL: ссылки на подресурс меняются одновременно с его содержимым, что позволяет использовать очень большой срок свежести без угадывания момента обновления. Клиенты не должны выдавать условный запрос в течение срока свежести ответа с immutable, если пользователь явно не переопределил это, например принудительной перезагрузкой. Конкретные числовые значения max-age в проверенных материалах не зафиксированы, поэтому срок выбирают по политике проекта.
Сборка и деплой статики: от исходников до продакшена
Путь статики на продакшен выглядит так: исходники в репозитории, сборка в CI, готовые файлы на сервере, отдача через nginx или CDN. Каждый шаг автоматизируется, и это стоит сделать: ручная сборка быстро расходится с репозиторием.
Инструменты сборки: webpack, vite, gulp и npm scripts
Выбор зависит от того, что именно нужно собирать.
- webpack - гибкий сборщик для сложных проектов с разными типами ресурсов; плата за гибкость - объём конфигурации.
- vite - быстрый вариант для новых проектов: мгновенный старт дев-сервера, хеширование имён из коробки.
- gulp - потоковый сборщик задач: минификация, сжатие изображений, копирование файлов; для сборки JS-модулей его берут реже.
- npm scripts - минимальный вариант без лишних зависимостей, когда хватает пары команд.
Практический ориентир: новый проект на современном стеке - vite, унаследованный проект с большим набором лоадеров - webpack, статический сайт без модулей - npm scripts или gulp.
Отдача статики через nginx и CDN
nginx отдаёт файлы напрямую с диска, а динамические запросы проксирует на бэкенд. Базовый блок для статики: location /static/ с директивой alias или root на каталог dist, expires 30d и add_header Cache-Control "public, immutable". Сжатие включают через gzip и brotli. Динамическое сжатие экономит трафик и ускоряет отдачу HTML, CSS, JS и API-ответов, и большинство текстовых форматов выигрывает от сжатия: HTML, JSON, CSS, JS, XML, SVG, TTF/OTF (обзор сжатия в nginx и Apache). Brotli обеспечивает на 15–25% лучшее сжатие по сравнению с GZIP при сопоставимой скорости, и современные браузеры поддерживают его через заголовок (оптимизация nginx). При этом нельзя сжимать всё подряд: многие форматы уже сжаты (WOFF2, JPEG/PNG/WebP/AVIF, ZIP, PDF, видео и аудио), а некоторые сценарии ломаются от компрессии. Для статики рекомендуется предварительное сжатие через gzip_static и brotli_static, чтобы не тратить CPU на сжатие при каждом запросе.
Готовые наборы директив для разных типов файлов собраны в статье про кэширование статических файлов в nginx.
CDN добавляет смысл, когда аудитория распределена географически: файл отдаёт ближайшая точка присутствия, а не единственный сервер. При переносе сайта не забудьте сбросить кэш CDN, иначе пользователи ещё какое-то время будут получать старые версии файлов.
Чек-лист аудита статических ресурсов на существующем проекте
Проверку удобно вести по порядку, от структуры к заголовкам. Десять пунктов ниже закрывают основные проблемы.
- Убедитесь, что статика отделена от шаблонов и загрузок: разные каталоги и разные политики бэкапа.
- Найдите дубликаты: одинаковые CSS и JS в нескольких каталогах раздувают вес и расходятся по версиям.
- Проверьте битые ссылки на статику: 404 по CSS или JS ломает вёрстку и ловится в логах.
- Найдите неиспользуемые файлы: браузер их не запрашивает, но они попадают в бэкап и в репозиторий.
- Посмотрите общий размер статики и вес отдельных файлов.
- Проверьте сжатие изображений: PNG рядом с WebP и AVIF без необходимости - частая находка.
- Проверьте права: на запись статика доступна быть не должна.
- Проверьте заголовки кэширования и сжатия для каждой группы файлов.
- Проверьте версионирование: имена CSS и JS должны меняться при правках.
- Проверьте, что деплой не затрагивает каталог загрузок и что статика попадает в бэкап.
Поиск дубликатов и неиспользуемых файлов
Дубликаты ищут сравнением хешей: md5sum или sha256sum по всем файлам с последующей сортировкой даёт группы совпадений. Второй признак - одинаковые имена в разных каталогах, например jquery.min.js в static/js/ и static/vendor/.
Неиспользуемые файлы вычисляют через логи: список запрошенных URL из access.log сверяют со списком файлов на диске, разницу дают find и grep. Файл, который не запрашивали ни разу за месяц, стоит проверить и, если он не нужен, удалить из репозитория вместе со сборкой.
Проверка прав доступа и владельцев файлов
Статике нужны права на чтение, а не на запись. Для директорий веб-корня рекомендуют права 755 (rwxr-xr-x), а для статических файлов (HTML, CSS, JS, изображения) - 644 (rw-r--r--) (чек-лист безопасности веб-сервера). В некоторых руководствах для статических файлов встречаются и более строгие варианты вроде 640 или 600, поэтому итоговый набор стоит согласовать с политикой проекта. Владелец - пользователь, от которого работает nginx, или root. Правильные права доступа предотвращают повышение привилегий после первичного проникновения, поэтому здесь действует принцип минимальных привилегий. Если статика доступна на запись процессу веб-сервера, подмена файла становится возможной при первой уязвимости в приложении. Права с лишним битом записи для группы и остальных ищут командой вида find /var/www/site/static -type f -perm /022 -ls.
Диагностику ошибок 403 и 404 из-за прав, симлинков и SELinux разбирает статья про ошибки веб-сервера 403, 404 и 500. Запрет на запись в каталоги со статикой дополняет базовую защиту веб-сервера, о которой речь идёт в материале про HTTPS-заголовки, WAF и защиту от атак.
Проверка кэширования и сжатия
Заголовки проверяют запросом curl -I https://site/static/css/style.css. В ответе смотрят три вещи: Cache-Control с большим max-age, заголовок Expires и Content-Encoding со значением gzip или br. Отсутствие Content-Encoding означает, что сжатие выключено или MIME-тип файла не попал в список сжимаемых.
Если max-age слишком мал, браузер перезапрашивает файлы при каждом переходе. Для CSS, JS, шрифтов и изображений с хешем в имени разумный срок - от месяца до года.
Типичные ошибки при переносе статики и как их избежать
Перенос сайта обнажает то, что годами работало случайно. Семь ошибок ниже встречаются чаще всего.
- Забыли служебные файлы: robots.txt, sitemap.xml, favicon.ico остались на старом сервере. Проверяйте корень целиком, а не только каталог со вёрсткой.
- Не учли регистр имён: ссылка на Style.css при файле style.css на Linux даёт 404.
- Перенесли абсолютные пути, привязанные к домену или подкаталогу, и получили битые ссылки на CSS, JS и изображения.
- Не перенесли симлинки: копирование по HTTP или распаковка архива без ключа для ссылок превращает их в пустые файлы или обрывает.
- Не проверили права и владельца после распаковки архива, и сайт отдаёт 403 на статику.
- Забыли сбросить кэш CDN и браузерный кэш: правки не видны пользователям.
- Смешали статику с загрузками и потеряли файлы пользователей при первом же деплое.
Регистр имён файлов: почему это ломает сайт на Linux
Windows и macOS по умолчанию не различают регистр в именах файлов, Linux различает. Проект, собранный на macOS, может ссылаться на Hero.webp при файле hero.webp. На Linux такой запрос вернёт 404, и вёрстка потеряет изображение. Лечится единым правилом: все имена в нижнем регистре плюс проверка ссылок перед релизом.
Абсолютные и относительные пути: что выбрать
Абсолютный путь /static/css/style.css привязан к корню домена. Он предсказуем и не зависит от глубины страницы. Относительный путь ../css/style.css зависит от текущего URL, и на страницах разной вложенности он указывает в разные места.
Для статики практичнее абсолютные пути от корня. При переезде в подкаталог их придётся менять, поэтому базовый URL лучше вынести в одну настройку шаблона или в переменную окружения и подставлять в разметку.
Возьмите чек-лист из раздела про аудит и пройдите его на одном проекте: сначала разделите каталоги статики, шаблонов и загрузок, затем включите версионирование имён и заголовки кэширования. Эти три шага убирают большинство проблем с деплоем, бэкапом и отдачей файлов.