RE:NODE

Веб-хостинг12 мин чтения

Хостинг статического сайта: сборка, загрузка и кэширование

Как правильно хостить статический сайт или Jamstack: какую папку загружать, чистые URL, правила кэширования, редиректы и то, чего статический сайт всё ещё не умеет.

0 прочтений

Статический сайт - это каталог файлов, которые веб-сервер отдаёт без изменений. Нет приложения, которое надо держать запущенным, нет базы данных, которую нужно бэкапить, и нет ничего, что придётся обновлять во вторник. Хорошо захостить такой сайт - значит принять четыре решения: какой каталог загружать, как URL отображаются на файлы, что говорят заголовки кэша и что происходит с URL, которые вы выводите из обращения. Сделайте их правильно, и тариф на 1 ГБ будет обслуживать сайт, для которого пятнадцать лет назад понадобился бы небольшой парк машин.

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

Что такое статический хостинг и чем за него приходится платить#

Сервер почти ничего не делает: приходит запрос на /about/, он находит на диске about/index.html и отправляет его. Никакой процесс PHP не запускается, никакой запрос не выполняется. Поэтому статические сайты быстры по умолчанию, а время до первого байта ограничено только сетью и диском.

Вы отказываетесь от всего, что должно происходить на сервере во время запроса:

  • Формы. Форме нужно куда-то отправлять данные. У статического хоста такого места нет.
  • Всё, что зависит от пользователя. Входы, корзины, личный кабинет. Это можно сделать на стороне клиента через API, но API должно где-то работать.
  • Поиск на стороне сервера. Страницы - это файлы, поэтому поиск происходит либо в браузере посетителя, либо у третьей стороны.
  • Редактирование контента нетехническими людьми. Без CMS любая правка - это пересборка, если только вы не добавите headless CMS и хук сборки.

Сохраняете вы возможность перенести сайт куда угодно за десять минут и сайт, который нельзя взломать через плагин, который никто не обновил. Для маркетингового сайта, документации, портфолио или блога этот обмен почти всегда верный. Для магазина - нет: смотрите требования к хостингу для WooCommerce или запустите CMS как следует.

Полезная золотая середина - статический сайт на тарифе, где работает ещё и PHP. Весь сайт состоит из файлов, а один небольшой скрипт PHP обрабатывает форму обратной связи. Именно такой формы должно быть большинство небольших сайтов компаний, и фреймворк ей вообще не нужен.

Что загружать: результат сборки по генераторам#

Самая частая ошибка - загружать проект вместо сборки. В вашем репозитории лежат исходники, конфигурация, node_modules и история. Веб-сервер не должен видеть ничего из этого. Каждый генератор записывает готовый каталог; этот каталог, и только его содержимое, попадает на сервер.

ГенераторКоманда сборкиКаталог результата
Hugohugopublic/
Jekylljekyll build_site/
Eleventynpx @11ty/eleventy_site/
Astronpm run builddist/
Vite (чистый)npm run builddist/
SvelteKit (статический адаптер)npm run buildbuild/
Next.js (output: 'export')npm run buildout/
Nuxt (nuxt generate)npm run generate.output/public/
Gatsbygatsby buildpublic/
Docusaurusnpm run buildbuild/
MkDocsmkdocs buildsite/

Проверяйте результат до загрузки. Стоит каждый раз убеждаться в двух вещах: что index.html лежит на верхнем уровне, а не на каталог ниже, и что URL ассетов заданы от корня. Сайт, собранный с базовым путём /my-project/ и развёрнутый в корне домена, даёт страницу без стилей и консоль браузера, полную ошибок 404. У каждого генератора есть для этого настройка - baseURL в Hugo, base в Astro и Vite, baseurl в Jekyll, - и она должна совпадать с тем, где сайт реально живёт.

Собирайте на своей машине или в CI, а не на веб-сервере. Тариф на 1 ГБ может выполнить сборку, но большой проект на JavaScript упрётся в лимит памяти, а хост, останавливающий контейнер на лимите, остановит его посреди сборки. Нет причин ставить Node на сервер, вся работа которого - отдавать файлы.

Как доставить файлы на сервер#

Три способа, в порядке того, насколько они вам понравятся.

rsync, если у вас есть SSH. Он копирует только изменившееся и умеет удалять то, что вы убрали, а это важно, потому что устаревшие файлы - именно то, из-за чего удалённая страница остаётся в сети на год.

bash
$ rsync -avz --delete --exclude '.DS_Store' dist/ user@example.com:/var/www/site/

SFTP, который есть у любого хоста. Берите клиент, который умеет зеркалировать локальный каталог, и помните, что большинство не удаляет удалённые файлы, которых у вас локально больше нет, пока вы не попросите. Загружайте во временный каталог и меняйте местами, если хотите, чтобы изменение было атомарным.

Архив, самый быстрый вариант в панельном хостинге. Упакуйте сборку в zip, загрузите один файл, распакуйте на месте. На RE:NODE файловый менеджер распаковывает архивы там, где они лежат, и содержит редактор в браузере для файла, который неизбежно понадобится поправить потом, а данные SFTP выдаются на каждый сервер: подробности подключения - в статье SFTP и файловый менеджер.

Чем бы вы ни пользовались, исключайте node_modules, .git, src и файлы окружения. Загрузка .git публикует всю вашу историю, включая всё, что вы закоммитили и потом удалили, а автоматические сканеры ищут /.git/config на каждом найденном сайте.

URL: чистые пути, страницы 404 и запасной вариант для SPA#

Большинство генераторов записывают about/index.html, а не about.html, потому что сервер, получив /about/, ищет index.html внутри этого каталога. Так по умолчанию ведёт себя nginx с index index.html; и практически любой другой веб-сервер, поэтому хорошо собранному статическому сайту не нужна никакая конфигурация, чтобы получить чистые URL.

Там, где вы управляете блоком сервера, три строки покрывают почти всё:

nginx
root /var/www/site;index index.html;location / {    try_files $uri $uri.html $uri/ =404;}error_page 404 /404.html;

try_files проверяет каждого кандидата по очереди: точный файл, тот же путь с добавленным .html - благодаря чему /about работает для генератора, записавшего about.html, - затем каталог, затем 404. Строка error_page - то, что заставляет появляться вашу оформленную страницу 404 вместо серой страницы сервера. Ваш генератор почти наверняка уже собирает 404.html.

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

nginx
location / {    try_files $uri $uri/ /index.html;}

Такой запасной вариант удобен и скрывает ошибки, потому что опечатка в пути к ассету возвращает ваш HTML с кодом 200 вместо 404, а браузер пытается разобрать его как JavaScript. Если вы видите в консоли «Unexpected token '<'», случилось именно это.

Если вы не можете править конфигурацию сервера, соберите сайт так, чтобы ему не нужны были правила. Вывод «каталог на страницу» с настоящими файлами index.html работает везде, не требует try_files и переживает переезд на любой хост. Учтите, что файлы .htaccess ничего не делают в nginx, на котором работает большая часть современного хостинга: инструкции для Apache, которые вы найдёте, молча не окажут никакого эффекта.

Кэширование и сжатие#

Здесь статический сайт выигрывает или проигрывает, и правил два.

Ассеты с отпечатком кэшируются навсегда. Современные инструменты сборки записывают файлы с хешем содержимого в имени - app.9f2c1b.css. Изменился файл - изменилось имя. Поскольку имя уникально для содержимого, браузеру никогда не нужно возвращаться и проверять:

nginx
location ~* \.(css|js|woff2|avif|webp|png|jpg|svg)$ {    expires 1y;    add_header Cache-Control "public, max-age=31536000, immutable";}

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

nginx
location ~* \.html$ {    add_header Cache-Control "public, no-cache";}

no-cache не значит «не хранить». Это значит «хранить, но спрашивать, прежде чем использовать повторно», что в большинстве случаев даёт дешёвый ответ 304 и мгновенное обновление, когда вы публикуете. В статье заголовки HTTP-кэширования простыми словами разобрана каждая директива и то, что с ней делают прокси.

Сжатие - вторая половина. Текст сжимается примерно на 70-80 процентов, так что HTML в 200 КБ превращается в примерно 50 КБ в сети. nginx сжимает на лету с gzip on; и правильным gzip_types, а с gzip_static on; может отдавать предварительно сжатые файлы, если ваша сборка записывает .gz рядом с каждым ассетом, и это ничего не стоит серверу в момент запроса. Не сжимайте изображения и видео: они уже сжаты, а повторное сжатие впустую тратит CPU.

Для изображений формат файла значит больше любой настройки сервера. Главное изображение, экспортированное как PNG в 2 МБ и показываемое шириной 800 пикселей, - самая частая причина медленного «быстрого» сайта. Экспортируйте в том размере, в каком показываете, используйте WebP или AVIF и всегда задавайте атрибуты width и height, чтобы браузер заранее резервировал место и макет не прыгал.

Редиректы и как изменить URL, не потеряв трафик#

Каждый когда-либо опубликованный URL - это обещание. Когда вы перестраиваете сайт, старые пути должны продолжать работать, иначе ссылки на них перестанут чего-либо стоить.

nginx
location = /old-page.html { return 301 /new-page/; }location ^~ /blog/2019/ { return 301 /archive/; }

Используйте 301 для постоянного переезда, а почти всегда вы имеете в виду именно его. Браузеры кэшируют 301 агрессивно, поэтому проверяйте через curl -I, прежде чем выкатывать его, и никогда не используйте 301 для того, что можете вернуть обратно. 302 - временный и безопасный выбор, когда вы не уверены.

Там, где нет доступа к конфигурации сервера, запасной вариант - страница с <meta http-equiv="refresh"> по старому пути. Для людей она работает, большую часть ценности ссылок со временем передаёт и хуже 301 по всем статьям. Используйте её только тогда, когда альтернатива - мёртвый URL.

Другой редирект, нужный каждому сайту, - между формой apex и формой с www: выберите один канонический хост и отправляйте на него другой через 301, чтобы две версии не конкурировали. В статье www или apex: домен и редиректы разобрана сторона DNS, в том числе почему CNAME на apex - не то, что ваш регистратор обязательно умеет.

Формы, поиск и комментарии без бэкенда#

Три функции, которых людям не хватает, и честные варианты для каждой.

Формы. Либо отправляйте данные во внешний сервис форм, либо добавьте небольшой скрипт PHP на тарифе, где работает PHP. Скрипт - около двадцати строк: проверить поля, отправить сообщение, перенаправить на страницу благодарности. Одно предупреждение, на котором попадаются все: функции PHP mail() нужен локальный агент передачи почты, а в контейнере хостинга его обычно нет. Отправляйте через API провайдера SMTP, с домена, для которого вы настроили записи: в статье почему письма с вашего домена попадают в спам объясняются SPF, DKIM и DMARC, которые понадобятся вам независимо от того, кто отправляет почту. RE:NODE почту не хостит, так что почта для вашего домена - всегда чей-то чужой сервис.

Поиск. Для сайта до нескольких тысяч страниц правильный ответ - индекс на стороне клиента: Pagefind строит его при сборке и подгружает фрагменты по требованию, Lunr и Fuse индексируют прямо в браузере. Дальше - внешний сервис поиска. Ничто на веб-сервере не меняется ни в том, ни в другом случае.

Комментарии. Внешний виджет, ссылка на обсуждение где-то ещё или ничего. «Ничего» - защитимый выбор и единственный, у которого нет последствий для приватности.

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

Домен, сертификат и чек-лист запуска#

Направить домен на статические файлы - то же самое, что направить его куда угодно: запись A для apex и запись для www, затем сертификат для обоих имён. На хосте с обратным прокси впереди вы направляете запись на выданный им адрес, и сертификат выпускается, как только имя разрешается туда; на RE:NODE продление происходит автоматически в окне в 21 день, без ежегодной задачи. В статье привязка домена к вашему серверу описан порядок действий, а HTTPS и Let's Encrypt простыми словами объясняет, почему выпуск не удаётся, когда DNS ещё не готов.

Прежде чем что-либо анонсировать:

  1. Загрузите сайт по https:// и убедитесь, что браузер не показывает предупреждений о смешанном содержимом.
  2. Проверьте, что /404 возвращает вашу страницу 404 со статусом 404, а не 200.
  3. Проверьте, что robots.txt и sitemap.xml существуют и перечисляют канонический хост.
  4. Пройдите по своим редиректам через curl -I и убедитесь, что каждый - один переход.
  5. Загрузите сайт на телефоне через мобильный интернет, а не через офисный wifi.

Диагностика проблем#

Страница загружается без стилей. Неверный базовый путь или CSS отдаёт 404. Откройте вкладку сети: если /assets/app.css возвращает HTML, ваш запасной вариант для SPA перехватывает запрос, а настоящий файл лежит в другом месте.

Вместо сайта список каталога. Там, где смотрит сервер, нет index.html, а индексы каталогов включены. Проверьте, что вы загрузили содержимое каталога сборки, а не сам каталог, - именно так всё и оказывается на уровень глубже, чем нужно.

Изменения не появляются. Что-то между вами и файлом кэширует. Перезагрузите страницу в обход кэша, проверьте заголовок Cache-Control, который сервер отправляет для HTML, и, если вы за CDN, очистите его. Это цена длинного max-age для HTML и причина, по которой его не стоит задавать.

403 Forbidden на каждой странице. Права доступа к файлам. Файлы должны быть читаемы пользователем веб-сервера, а у каталогов должен быть бит выполнения, чтобы их можно было пройти: 644 для файлов, 755 для каталогов.

Сайт работает на `www`, но не на apex, или наоборот. Только одно имя есть в сертификате или только у одного есть запись DNS. Нужно, чтобы у обоих было и то и другое.

У вас всё быстро, а у посетителей медленно. Вы рядом с сервером, а они нет. Оставшаяся задержка статического сайта - это расстояние, которое больший тариф не меняет: смотрите TTFB, Core Web Vitals и что меняет хостинг и поставьте CDN впереди, если ваша аудитория разбросана по континентам.

FAQ#

Сколько хостинга на самом деле нужно статическому сайту?

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

Нужен ли Node.js на сервере?

Нет. Node собирает сайт, но не отдаёт его. Собирайте локально или в CI и загружайте результат. Если вам хочется, чтобы Node работал на сервере, то у вас приложение, а не статический сайт, и ему место на тарифе для приложений.

Можно ли разместить несколько статических сайтов на одном тарифе?

В пределах хранилища и лимитов прокси тарифа - да. Каждому сайту нужен свой каталог и своё имя хоста, направленное на сервер. Тарифы выше несут больше хранилища и больше ёмкости прокси.

Обязателен ли CDN?

Только если ваши посетители далеко от сервера или вы отдаёте большие медиафайлы. CDN кэширует копии рядом с посетителем, что убирает расстояние из уравнения для статических ассетов. Он ничего не даёт для медленного источника, отдающего некэшированный HTML, и добавляет ещё один слой для отладки: в статье Cloudflare для сайтов и игровых серверов описано, что прокси делает, а что нет.

Как настроить автоматический деплой при push?

Собирайте в CI и пусть задача загружает результат через SFTP или rsync. Делайте деплой одним шагом, заменяющим содержимое каталога, чтобы наполовину завершённая загрузка никогда не превращалась в наполовину сломанный сайт.

Что делает backup статического сайта?

Ваш репозиторий - это backup исходников, но не сборки и не всего, что было загружено после. Делайте backup и обслуживаемого каталога по расписанию и один раз восстановите его, чтобы убедиться, что он работает: почему эта вторая половина важна, объясняет статья backup, который действительно восстанавливается.


Комментарии

Полностью анонимно: без аккаунта, без почты, без cookie. Мы храним имя, которое вы ввели, текст и время - больше ничего. Количество ссылок ограничено, разметка не отображается.

0/2000