# Настройка публикации статического сайта из VS Code


<!--more-->

## Куда выкладывать статический сайт

Сопоставим основные бесплатные и бюджетные варианты хостинга статики.

| Способ              | Свой домен | SSL                  | Как публикуется           | Ограничения                                                                             |
|---------------------|------------|----------------------|---------------------------|-----------------------------------------------------------------------------------------|
| GitHub Pages        | да         | автоматически        | git push + Actions        | лимиты на объем репозитория и трафик; на бесплатном плане -- лишь публичный контент     |
| Netlify / Vercel    | да         | автоматически        | git push или CLI          | сверх бесплатного лимита минуты сборки и трафик платные                                 |
| Cloudflare Pages    | да         | автоматически        | git push или Wrangler     | конфигурация в веб-панели, собственные правила кеширования                              |
| AWS S3 + CloudFront | да         | через ACM            | `aws s3 sync`             | ручная конфигурация бакета, политик, дистрибуции и сертификата; оплата за каждый гигабайт трафика |
| VPS + Nginx         | да         | Let's Encrypt вручную | rsync/scp                | обновления, сертификаты и безопасность -- ответственность администратора                |
| WebShield           | да         | автоматически        | `webshield sites publish` | лимиты хранилища зависят от тарифного плана                                             |

**Как загрузить сайт на GitHub Pages.** Собранные файлы кладутся в ветку `gh-pages` либо в каталог `docs/` ветки
`main`, после чего Pages включается в параметрах репозитория. Для сборки на серверах GitHub создается workflow в Actions.
Публикация неизменно идет через git.

**Публикация сайта через Netlify** и **деплой проекта на Vercel** устроены одинаково: подключается
репозиторий, указываются команда сборки и папка с результатом, и затем любой push запускает деплой. Готовую
папку можно загрузить и без репозитория с помощью CLI (`netlify deploy`, `vercel`). Бесплатный план ограничен
минутами сборки и объемом трафика.

**Cloudflare Pages** воспроизводит шаги Netlify: подключаем репозиторий, прописываем команду сборки,
получаем домен `*.pages.dev` и подключаем свой. Бесплатный план щедрый, но параметры и правила кеширования
настраиваются исключительно в панели Cloudflare.

**Хостинг статического сайта AWS S3** требует наибольшего объема ручной настройки: бакет, статический веб-хостинг,
политика доступа, CloudFront для HTTPS и CDN, сертификат в ACM. Сверх бесплатного лимита CloudFront любой гигабайт
исходящего трафика тарифицируется отдельно без верхней границы, так что затраты нужно контролировать с помощью
биллинг-алертов.

**VPS с Nginx** обеспечивает полный контроль, однако обновление системы, продление сертификатов и безопасность остаются на вас. Пример
такой конфигурации с ограниченным доступом разобран в статье про
[аутентификацию по клиентским сертификатам на Nginx]({{< relref "client-certificate-authentication-nginx" >}}).

Дальше настроим схему, при которой сборка, публикация и проверка выполняются, не покидая VS Code.

---

## Подготовка

Для примера возьмем сайт на Hugo и хостинг WebShield с его клиентом командной строки. Руководство подходит для любого генератора
статических сайтов (Astro, Next.js, Nuxt, Eleventy, Docusaurus, MkDocs, VitePress) и для сайта из обыкновенных
HTML-файлов -- меняется лишь каталог с результатом сборки.

Что нужно:

* VS Code (или Cursor, VS Codium);
* установленный генератор и проект, который собирается на локальной машине;
* домен с делегированием на сервис WebShield;
* учетная запись и личный API-токен `wsk_…` из панели управления.


### Делегирование домена

У регистратора домена заменяем NS-серверы на `nsbox.webshield.pro` и `nshub.webshield.pro`. Старые NS-записи удаляем,
в противном случае часть запросов станет уходить на прежние серверы и сайт будет открываться через раз. Применение делегирования
может занять до 48 часов.


### Настройка проекта

Структура проекта на Hugo выглядит так:

```
my-site/
├── content/          # статьи в Markdown
├── layouts/          # шаблоны
├── static/           # файлы, попадающие в сборку как есть
├── themes/           # тема
├── hugo.toml         # конфигурация, здесь же baseURL
└── public/           # результат сборки — то, что мы публикуем
```

Каталог `public/` появляется при сборке, так что вносим его в `.gitignore` вместе с временными файлами Hugo.

```gitignore
public/
resources/
.hugo_build.lock
```

Проверяем, что в `hugo.toml` задан верный адрес сайта. Hugo подставляет его в абсолютные ссылки при сборке,
и при неправильном значении на сайте не подгрузятся стили и картинки.

```toml
baseURL = "https://example.com/"
```

Для комфортной работы стоит установить расширения VS Code:

* **Even Better TOML** и **YAML** -- подсветка и проверка front matter и конфигов;
* **Front Matter CMS** -- редактирование метаданных статей через форму;
* **Markdown All in One** -- таблицы, содержание, горячие клавиши;
* **Live Preview** -- просмотр собранной статики без сервера генератора.

Стартуем предпросмотр во встроенном терминале VS Code (**Ctrl+\`**). Флаг `-D` включает показ черновиков.

```shell
hugo server -D
```

Сайт откроется по адресу `http://localhost:1313` и обновляется при каждом сохранении файла.

---

## Установка WebShield CLI

Клиент распространяется одним исполняемым файлом без зависимостей. Исходный код и релизы лежат в репозитории
[webshield-cli на GitHub](https://github.com/webshieldpro/webshield-cli), полный перечень команд -- в
[документации по консольному клиенту](https://docs.webshield.pro/ru/cli/). Готовые бинарники есть для Linux
(x86_64 и aarch64), macOS (Intel и Apple Silicon) и Windows (x86_64). Все команды выполняем во встроенном терминале
VS Code.


### Linux и macOS

Запускаем скрипт установки. Он определит ОС и архитектуру, скачает свежий релиз, сверит контрольную сумму SHA-256,
скопирует бинарный файл в `~/.local/bin` и настроит автодополнение.

```shell
curl -fsSL https://raw.githubusercontent.com/webshieldpro/webshield-cli/main/install.sh | sh
```

Проверяем результат установки.

```shell
webshield --version
```

Если команда не находится, прописываем каталог в `PATH` в файле `~/.bashrc` (Linux) или `~/.zshrc` (macOS) и
перезапускаем VS Code.

```shell
export PATH="$HOME/.local/bin:$PATH"
```

Для zsh установщик в конце выведет строку, которую надо добавить в `fpath`, чтобы заработало автодополнение.


### Windows

Скачиваем архив со [страницы релизов](https://github.com/webshieldpro/webshield-cli/releases), распаковываем и
вносим каталог в пользовательский `PATH`. В PowerShell:

```powershell
$ver = "1.0.2"
$dir = "$env:LOCALAPPDATA\Programs\webshield"
New-Item -ItemType Directory -Force -Path $dir | Out-Null
Invoke-WebRequest -Uri "https://github.com/webshieldpro/webshield-cli/releases/download/v$ver/webshield-$ver-x86_64-pc-windows-gnu.zip" -OutFile "$env:TEMP\webshield.zip"
Expand-Archive -Path "$env:TEMP\webshield.zip" -DestinationPath $dir -Force
[Environment]::SetEnvironmentVariable("Path", "$([Environment]::GetEnvironmentVariable('Path','User'));$dir", "User")
```

Полностью перезапускаем VS Code и проверяем, что установка прошла успешно.

```powershell
webshield --version
```

Активируем автодополнение для PowerShell.

```powershell
webshield completion powershell >> $PROFILE
```

Если вы используете WSL, устанавливайте Linux-версию скриптом и открывайте проект в режиме **WSL: Remote**.


### Сборка из исходников

Если запуск скриптов из интернета запрещен или для вашей платформы нет готового бинарника, компилируем клиент из исходного
кода. Потребуется только Rust.

```shell
git clone https://github.com/webshieldpro/webshield-cli
cd webshield-cli
cargo build --release
mv target/release/webshield ~/.local/bin/
```


### Настройка автодополнения вручную

При ручной установке или нестандартной оболочке генерируем скрипты автодополнения. Поддерживаются bash, zsh, fish,
PowerShell, elvish и nushell.

```shell
webshield completion bash > ~/.local/share/bash-completion/completions/webshield
webshield completion zsh > ~/.zsh/completions/_webshield
webshield completion fish > ~/.config/fish/completions/webshield.fish
```


### Авторизация

Выпускаем токен в личном кабинете в разделе **Настройки → API-токены**. Выдаем необходимые права и, если нужно,
ограничиваем токен конкретным доменом или сайтом.

Сохраняем токен в профиль и проверяем доступ.

```shell
webshield auth login
webshield auth status
```

Профили лежат в `~/.config/webshield/config.toml`. Выбрать профиль можно флагом `--profile` или переменной
`WS_PROFILE`. Токен также можно передать флагом `--token` или переменной `WS_TOKEN`.

---

## Создание сайта

### Добавление домена

Просматриваем список доменов и состояние делегирования. Статус должен быть «делегирован».

```shell
webshield domains list
webshield domains check example.com
```

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

```shell
webshield domains add example.com
```


### Создание сайта

Создаем сайт на главном домене `example.com` и проверяем, что он отобразился в списке.

```shell
webshield sites create example.com --domain example.com
webshield sites list
```

{{< admonition type=info open=true >}}
**Важный момент!** Сайт привязывается к апексу домена или к существующей DNS-записи `A`, `AAAA` или `CNAME`.
Для поддомена предварительно создаем запись, например: `webshield dns add example.com docs CNAME example.com`.
Для того же имени не должно быть включено проксирование.
{{< /admonition >}}

Сайт создается в статусе «Отключен» и недоступен посетителям до первой публикации.

<!-- скриншот: карточка созданного сайта в панели WebShield -->


### Переадресация с www

Создавать второй сайт для `www` не нужно. Добавляем DNS-запись и настраиваем редирект на основной домен,
чтобы у сайта была одна каноническая версия.

```shell
webshield dns add example.com www CNAME example.com
webshield proxy set www.example.com --domain example.com \
    --mode redirect --redirect-target example.com
```


### Настройки хостинга

В карточке сайта задаем параметры до первой публикации. Если сайт уже опубликован, изменения применяются сразу,
без повторной публикации. Подробное описание параметров, ограничений и API -- в документации по
[размещению статического сайта](https://docs.webshield.pro/ru/sites/static-sites/).

* **Генератор сайта** -- пресет для Hugo, Jekyll, Astro, Gatsby, Next.js export, SPA или простого HTML. Заполняет
  остальные параметры соответствующими значениями.
* **Чистые URL** -- правило сопоставления адресов с файлами. «Индекс каталога» (`/about` → `/about/index.html`)
  для Hugo, Jekyll, Astro, Eleventy и Gatsby. «Расширение .html» (`/about` → `/about.html`) для экспорта Next.js.
* **Страница 404** -- файл, отдаваемый со статусом 404, например `404.html`.
* **Режим SPA** -- возвращать `index.html` со статусом 200 на незнакомые пути. Необходим для одностраничных приложений
  с маршрутизацией на клиенте.
* **Публиковать по HTTPS** -- выдача сертификата и редирект с HTTP на HTTPS. Сертификат выпускается при
  публикации и обновляется автоматически.
* **Защита от ботов** -- выключена, проверка браузера или капча. Подробности в статье про
  [защиту сайта от ботов]({{< relref "protection-of-the-website-from-bot" >}}).

---

## Публикация из VS Code

### Проверка и публикация

Собираем сайт и выполняем публикацию в тестовом режиме с флагом `--dry-run`. Файлы на сервер не отправляются,
выводится лишь список добавляемых, измененных и удаляемых файлов.

```shell
hugo --minify
webshield sites publish example.com --dir ./public --dry-run
```

{{< admonition type=warning open=true >}}
**Важный момент!** Публикация синхронизирует каталог целиком: файлы, отсутствующие в `--dir`, будут удалены с сайта.
Перед первой публикацией обязательно просматривайте вывод `--dry-run`, чтобы удостовериться, что публикуется `public/`,
а не корень проекта.
{{< /admonition >}}

Если все корректно, публикуем сайт.

```shell
webshield sites publish example.com --dir ./public
```

Клиент сравнивает хеши локальных файлов с файлами на сервере, отправляет только изменения и выкладывает новую версию
атомарно -- посетители не столкнутся с частично обновленным сайтом. При первой публикации DNS-запись хоста переключается
на инфраструктуру WebShield (прежняя запись восстановится при отключении сайта) и выдается TLS-сертификат.

Проверяем список файлов на сервере.

```shell
webshield sites files example.com
```

Для прочих генераторов меняется только каталог сборки.

```shell
webshield sites publish example.com --dir ./dist    # Astro, Vite, Nuxt
webshield sites publish example.com --dir ./build   # Next.js export, CRA
webshield sites publish example.com --dir ./site    # MkDocs
```


### Задача публикации в VS Code

Создаем в корне проекта файл `.vscode/tasks.json` с задачами сборки и публикации.

```json
{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "build",
      "type": "shell",
      "command": "hugo --minify",
      "problemMatcher": []
    },
    {
      "label": "publish",
      "type": "shell",
      "command": "webshield sites publish example.com --dir ./public",
      "dependsOn": "build",
      "group": { "kind": "build", "isDefault": true },
      "presentation": { "reveal": "always", "panel": "dedicated" },
      "problemMatcher": []
    },
    {
      "label": "publish (dry-run)",
      "type": "shell",
      "command": "webshield sites publish example.com --dir ./public --dry-run",
      "dependsOn": "build",
      "problemMatcher": []
    }
  ]
}
```

Не забудьте поменять все `example.com` на свой домен. Теперь **Ctrl+Shift+B** собирает и выкладывает сайт. Задачу
`publish (dry-run)` запускаем через **Ctrl+Shift+P → Tasks: Run Task**.

Отдельный шаг `build` обязателен: `hugo server` прописывает в `baseURL` адрес `http://localhost:1313/`, и без
повторной сборки на сайт попадут ссылки на localhost.

Файл `.vscode/tasks.json` не хранит секретов, его можно закоммитить в репозиторий. Задачи выполняются в Linux, macOS
и Windows, если `webshield` находится в `PATH`.

Для тестовой среды добавляем задачу с другим адресом и профилем.

```json
{
  "label": "publish (staging)",
  "type": "shell",
  "command": "webshield sites publish staging.example.com --dir ./public --profile staging",
  "dependsOn": "build",
  "problemMatcher": []
}
```

Предварительно создаем для него DNS-запись и сайт.

```shell
webshield dns add example.com staging CNAME example.com
webshield sites create staging.example.com --domain example.com
```

Чтобы запускать публикацию своим сочетанием клавиш, открываем **File → Preferences → Keyboard Shortcuts → Open Keyboard
Shortcuts (JSON)** и добавляем:

```json
{
  "key": "ctrl+alt+p",
  "command": "workbench.action.tasks.runTask",
  "args": "publish"
}
```

<!-- скриншот: терминал VS Code с выводом успешной публикации -->


### Проверка результата

Открываем сайт в браузере и проверяем, что сертификат выпущен на ваш домен. Статистику и расход ресурсов просматриваем
из терминала.

```shell
webshield stats summary example.com --range 7d   # сводка трафика и запросов
webshield stats bans example.com                 # активные баны и проверки
webshield billing usage example.com              # расход относительно лимита тарифа
```

Для скриптов и мониторинга используем вывод в JSON.

```shell
webshield -o json stats summary example.com --range 24h | jq '.requests'
```

Кеш на edge-серверах сбрасывается автоматически при каждой публикации. Если браузер показывает устаревшую версию
страницы, перезагружаем ее с очисткой кеша (**Ctrl+Shift+R**).

---

## Переадресации и формы

### Переадресации в _redirects

Сайт постоянно обновляется. Рано или поздно возникает необходимость в переносе или переименовании страниц.
Когда сайт размещен на собственном сервере или хостинге -- с этим проблем нет, редирект можно настроить прямо в
веб сервере или PHP. На статическом хостинге тоже есть возможность задавать правила переадресации. Для этого пишем их в
файле `_redirects` в корне публикуемого каталога, как описано в [документации](https://docs.webshield.pro/ru/sites/static-sites/#переадресации).
Для Hugo его нужно разместить в каталоге `static/`.

```
# По умолчанию 301 редирект, но можно задать явно
/contacts-old       /contacts
/blog/*             /posts/:splat   301
/black-friday-sale  /               302
/go/telegram        https://t.me/channel
```

Формат совместим с Netlify и Cloudflare Pages, при переносе проблем возникнуть не должно.


### Формы без сервера

Статические сайты идеально подходят для лендингов и сайтов-визиток -- на них обычно нет авторизации и пользовательских
данных. А форму обратной связи для сбора контактов можно давить прямо на статическом сайте, без своего бэкенд сервера.
Подключаем форму, как написано в [документации](https://docs.webshield.pro/ru/sites/forms/).

```html
<form action="/webshieldpro/forms/feedback" method="post">
  <p><label>Имя<br><input name="name" required></label></p>
  <p><label>Электронная почта<br><input name="email" type="email" required></label></p>
  <p><label>Сообщение<br><textarea name="message" rows="5" required></textarea></label></p>
  <p><button type="submit">Отправить</button></p>
</form>
<script src="/webshieldpro/forms.js" defer></script>
```

Здесь имя `feedback` может быть произвольным. По нему потом можно смотреть в личном кабинете, какую форму заполняли.

Для Hugo HTML нужно выносить из Markdown, поэтому форму размещаем в шаблоне или шорткоде, например
`layouts/shortcodes/contact-form.html`, и вставляем на страницу через `{{</* contact-form */>}}`.

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

---

## Публикация из CI

Если сайт редактируют несколько человек, публикацию выносим в CI. В карточке сайта выпускаем **токен публикации**.
Он работает только для этого сайта и показывается один раз -- сохраняем его в секреты репозитория, например
`WS_PUBLISH_TOKEN`.

Прописываем шаг в GitHub Actions.

```yaml
- name: Publish site
  env:
    WS_TOKEN: ${{ secrets.WS_PUBLISH_TOKEN }}
  run: |
    curl -fsSL https://raw.githubusercontent.com/webshieldpro/webshield-cli/main/install.sh | sh
    ~/.local/bin/webshield sites publish example.com --dir ./public
```

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

```shell
webshield sites publish-from-bucket example.com --bucket web --path public/
```

---

## Решение проблем

### `webshield: command not found`

Каталог с исполняемым файлом не внесен в `PATH`. В Linux и macOS добавляем `~/.local/bin` в `~/.bashrc` или
`~/.zshrc`, в Windows проверяем пользовательскую переменную `Path`. После правки полностью перезапускаем VS Code:
встроенный терминал получает окружение, с которым был запущен редактор.

Проверяем, что видит терминал.

```shell
echo $PATH                # Linux, macOS
$env:Path -split ';'      # PowerShell
```

Терминал VS Code в Linux запускается как не-login оболочка и не загружает `~/.profile`, поэтому строку нужно добавлять
в `~/.bashrc`.


### Ошибка 401 при публикации

Токен просрочен, отозван, выпущен для другого сайта или не обладает правами на запись. Проверяем текущий профиль и доступ.

```shell
webshield auth status
```

Если ошибка возникает только в CI, убеждаемся, что:

* секрет пробрасывается в переменные окружения шага, а не только job;
* workflow запущен не из форка -- для таких запусков GitHub не выдает секреты;
* в репозитории отсутствует переменная с тем же именем, что у секрета.

Убедиться, что токен дошел до шага, можно без вывода его значения: `echo "${#WS_TOKEN}"` выведет длину строки.
Значение `0` говорит о том, что переменная пустая.


### Опубликован не тот каталог

На сайте видны `content/`, `themes/` и прочие исходники -- при публикации задан `--dir .` вместо `--dir ./public`.
Публикуем снова с верным путем, новая версия полностью заменит прежнюю.


### Внутренние страницы возвращают 404

В настройках сайта выбран неверный режим «Чистые URL». Проверяем, какой вариант файла есть на сервере.

```shell
curl -I https://example.com/about/index.html
curl -I https://example.com/about.html
```

Если 200 пришел в ответ на первый запрос, выбираем «Индекс каталога», если на второй -- «Расширение .html».


### Не загружаются стили и картинки

В конфигурации генератора `baseURL` не указан или указан неверно. Исправляем значение, пересобираем сайт и снова
публикуем. Повторная публикация без пересборки бесполезна, так как адреса уже зашиты в файлы.


### Домен не открывается или не выпускается сертификат

Делегирование пока не обновилось или у регистратора остались старые NS-серверы. Сертификат не выпустится, пока домен
не направлен на WebShield.

```shell
webshield domains check example.com
dig NS example.com +trace                    # фактическое делегирование
dig example.com @nsbox.webshield.pro         # ответ сервера WebShield
```

Если в выводе `+trace` видны лишние серверы, удаляем их у регистратора и дожидаемся истечения TTL.


### Часть файлов не загрузилась

Проверяем лимиты тарифа на объем хранилища и размер файла. Исполняемые файлы и установщики (`.exe`, `.msi`, `.apk`,
`.bat` и т.п.) можно размещать только на платном тарифе домена, на бесплатном их загрузка отклоняется. Архивы (`.zip`,
`.tar.gz`) разрешены на любом тарифе. Перечень отправляемых файлов можно заранее просмотреть с флагом `--dry-run`.

