У меня был статический сайт-портфолио: несколько заранее сверстанных страниц, которые бесплатный хостинг репозиториев просто отдает как есть. Никакого сервера со своей логикой, никакой базы - набор файлов. На главной жил переключатель языка. Потом понадобился раздел блога, тоже на двух языках.
Звучит как задача на полвечера: отрендерить страницы, добавить кнопку. На первой же настоящей статье выяснилось, что часть читателей видит пустую страницу.
К концу этого текста вы увидите устройство простого двуязычного блога на статике и три решения, которые определяют, будет он работать или тихо ломаться. Тихо - здесь главное слово. Ни одна из этих поломок не роняет сайт и не пишет ошибку в лог. Страница открывается, отдается успешно, выглядит целой - и не содержит текста.
Задача: две аудитории, один сайт
Условие простое: есть русскоязычные читатели и есть англоязычные, а сайт один. Плодить два параллельных сайта не хочется - это два раза по всему: верстка, навигация, публикация, вычитка. Значит, одна статья должна существовать в двух языковых версиях и жить по одному адресу.
Статика накладывает ограничение, о котором стоит сказать вслух, если вы раньше работали только с приложениями на сервере. Здесь некому принять решение в момент запроса. Нет кода, который посмотрит на читателя и отдаст ему нужный вариант. Хостинг выдает файл, который вы туда положили, - и все. Любая логика выбора языка живет уже внутри страницы, у читателя в браузере.
Отсюда три развилки, на которых обычно и ломается: где физически лежит второй язык, кто и когда его делает, и кто знает, что уже опубликовано. Разберу их по очереди.
flowchart TB U["Читатель открывает<br/>один адрес статьи"] --> P["Одна HTML-страница"] P --> B1["Блок ru"] P --> B2["Блок en"] SW["Переключатель языка"] -->|"показать / спрятать"| B1 SW -->|"показать / спрятать"| B2 P -.->|"ничего не догружается"| NET["Сеть не нужна"]
Решение первое: обе версии в исходнике страницы
Обе языковые версии статьи лежат в исходном коде страницы как два блока. Переключатель ничего не загружает - он только показывает нужный блок и прячет второй. Да, это значит, что каждый читатель получает вдвое больше текста, чем читает.
Альтернатива - держать вторую версию отдельным файлом и подтягивать ее запросом, когда читатель нажмет кнопку. Так делать нельзя, и причина не в скорости.
Текст, которого нет в исходном коде страницы, не увидит никто, кроме человека с включенным JavaScript. Поисковый робот, который не дожидается подгрузки, проиндексирует пустую оболочку. Мессенджер, который строит превью ссылки по разметке страницы, покажет заголовок без описания - а именно так статью и открывают в первые дни после публикации. Вы потеряете ровно тех читателей, ради которых писали.
Поэтому правило простое: оба текста лежат в файле, который отдает хостинг. Переключение языка - это работа с видимостью того, что уже пришло, а не поход за данными.
Ловушка, которую я поймала на первой же статье
У переключателя есть вопрос, на который он должен ответить перед показом: а есть ли вообще версия на этом языке? Если ее нет, надо честно показать ту, что есть, а не пустоту.
Моя проверка искала языковой блок по всей странице. А в шапке сайта лежат английские подписи пунктов меню - они там всегда, независимо от языка статьи. Проверка находила их, делала вывод «английская версия есть» и прятала русский текст. У читателя, у которого в браузере английская локаль - то есть язык, выставленный в настройках системы или браузера, - статья открывалась пустой. Не с ошибкой. Просто пустой.
Лечится тем, что проверку надо ограничить областью самой статьи: искать не «где-нибудь на странице», а внутри контейнера с текстом. Урок шире частного бага - любая проверка вида «а есть ли у нас X» должна знать, где именно она ищет. Страница целиком - это почти всегда слишком широкая область, потому что в шапке, подвале и навигации живет своя жизнь.
Оформление: блог как часть сайта
Дизайн-токены - переменные с цветами, шрифтами и отступами - я взяла с главной страницы. Не подбирала похожие на глаз, а именно взяла те же значения. Это дешевая операция, которая решает вопрос «почему блог выглядит как приклеенный сбоку раздел» до того, как он возникнет.
Генератор надо ломать настоящим текстом
Отдельный урок, который стоил мне неприятного пятиминутного открытия. Генератор страниц - скрипт, превращающий текст статьи в готовый HTML, - я проверяла на тестовом материале, который сама же и написала. Он проходил.
Первая настоящая статья пришла с подзаголовками третьего уровня и нумерованными списками. Ни того, ни другого генератор не понимал: он бы вывалил их на сайт сырыми, прямо с исходными символами разметки. Тестовый текст был слишком вежливым - я неосознанно писала его под то, что уже умела обрабатывать.
Проверяйте генератор на самой сложной реальной статье, которая у вас есть. Не на той, которую написали для теста.
flowchart LR
DB["База: статья и статус"] --> T["Перевод - отдельный шаг,<br/>не автоматика публикации"]
T --> S{"Статус = Scheduled?"}
S -->|"нет"| W["Ничего не происходит"]
S -->|"да"| PUB["Публикация"]
PUB --> REPO["Репозиторий:<br/>страница в списке = вышла"]
REPO -->|"источник правды<br/>о факте"| DBРешение второе: перевод - отдельный шаг, а не автоматика
Соблазн очевидный: раз обе версии нужны в одном файле, пусть публикация сама переведет русский текст и положит английский рядом. Одна команда - и готово.
Я так не сделала. Английская версия не переводится автоматически в момент публикации. Это отдельный шаг по явной команде, результат кладется в отдельное поле и проходит вычитку человеком - вместе с русской версией, а не после нее.
Причина не в качестве перевода. Причина в том, что у автоматики внутри публикации нет момента, когда текст можно прочитать. Он рождается и уезжает на сайт в одном действии. Вы узнаете, что там написано, когда это увидит читатель. Разнеся шаги, вы получаете обычную редакторскую паузу: перевод есть, он лежит в поле, его видно, его можно править.
Побочный эффект приятный: английская версия перестает быть обязательной. Статья может выйти сначала на одном языке, а перевод приехать позже - и первое решение это уже поддерживает, потому что переключатель умеет честно отвечать «версии нет».
Решение третье: источник правды о том, что уже опубликовано
Последний вопрос самый скучный и самый важный: откуда система знает, какие статьи уже на сайте?
Очевидный ответ - из базы черновиков: там же есть флажок «опубликовано». Этот ответ неверный, и вот почему.
Публикация устроена как выкладка статики: скрипт рендерит страницы, коммитит их и отправляет в репозиторий, а хостинг раскатывает сам за пару минут. В этой цепочке несколько мест, где все может встать: рендер, коммит, отправка, раскатка. Флажок в базе ставится по намерению опубликовать, а не по факту, что файл лежит на хостинге. Стоит одному звену промолчать - и база уверена, что статья вышла, а по ссылке ничего нет.
Поэтому список опубликованного ведет репозиторий. Файл со списком статей лежит рядом со страницами и обновляется при каждой публикации - тем же коммитом, что и сами страницы. Если статья попала в список, значит, ее страница действительно доехала: они приезжают вместе или не приезжают вовсе.
Формулировка, которую я держу в голове: база знает про намерения, репозиторий - про факт. И это касается не одного блога. Как только у вас появляется отдельное хранилище состояния и отдельное место, где состояние реально живет, вопрос «кто из них прав» надо решать сознательно - иначе он решится сам, и не в вашу пользу.
Применить у себя
Что проверить на практике
Если вы собираете что-то похожее, вот конкретные проверки. Они занимают минут двадцать и ловят все три поломки, которые я описала.
- Откройте опубликованную статью и посмотрите исходный код страницы (в браузере это
Ctrl+Uили пункт «Просмотр кода страницы»). Найдите поиском по странице фрагмент русского текста и фрагмент английского. Оба должны быть там - до всякого нажатия на переключатель. - Переключите язык браузера или системы на английский и откройте статью заново, желательно в приватном окне, чтобы не мешали сохраненные настройки. Текст должен быть. Повторите для русского.
- Проверьте статью, у которой английской версии еще нет. Она должна открыться на русском, а не пустой.
- Найдите в коде проверку «есть ли версия на этом языке» и посмотрите, в какой области она ищет. Если поиск идет по всей странице - ограничьте его контейнером статьи и перепроверьте предыдущие два пункта.
- Киньте ссылку на статью себе в мессенджер и посмотрите превью. Заголовок и описание должны подтянуться.
- Прогоните генератор страниц на самой сложной реальной статье: с подзаголовками разных уровней, нумерованными и обычными списками, ссылками, выделениями. Смотрите на результат в браузере, а не на то, что скрипт отработал без ошибки.
- Сверьте список опубликованного в базе со списком в репозитории. Расхождение - это ваш ответ на вопрос, кто из них источник правды.
Как понять, что получилось
Критерий простой и проверяемый: статья открывается непустой при любом языке браузера, и оба текста видны в исходном коде страницы. Не «переключатель работает» - переключатель работал и в сломанной версии, он исправно прятал единственный имеющийся текст.
Второй критерий - про публикацию: через пару минут после отправки в репозиторий страница доступна по прямой ссылке, и она есть в файле-списке. Если она в списке, но не открывается, или открывается, но не в списке - источник правды у вас все еще не один.
И третий, неформальный: после публикации вам не нужно ничего править руками. Ни разметку, которую генератор не понял, ни перевод, который приехал сам. Если после каждой статьи есть пятиминутка ручной доводки - это не мелочь, это счет, который вы будете оплачивать при каждой публикации.