Splits the landing page into / and /ru/ so each language can rank on its own, with hreflang links between them; boot.js/main.js now do a real navigation between the two instead of toggling text in place. Adds robots.txt and sitemap.xml, JSON-LD SoftwareApplication markup, Twitter Card tags, and a proper 1200x630 og-image.png in place of the SVG favicon for social previews. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
429 lines
35 KiB
Markdown
429 lines
35 KiB
Markdown
# cursor-lang
|
|
|
|
[English version](README.md)
|
|
|
|
Приложение WPF показывает раскладку клавиатуры у курсора.
|
|
|
|
## Клонирование
|
|
|
|
Двоичные ресурсы — иконка exe и логотипы MSIX — хранятся в Git LFS, поэтому
|
|
перед клонированием нужно установить [git-lfs](https://git-lfs.com):
|
|
|
|
```powershell
|
|
git lfs install
|
|
git clone git@git.alrakis.kz:alrakis/cursor-lang.git
|
|
```
|
|
|
|
Клонирование без него проходит успешно, но вместо изображений остаются
|
|
текстовые файлы-указатели, и сборка затем падает на нечитаемой иконке.
|
|
Существующий клон чинится командой `git lfs install`, а затем `git lfs pull`.
|
|
|
|
## Запуск во время работы над приложением
|
|
|
|
```powershell
|
|
dotnet run --project CursorLang.Agent
|
|
```
|
|
|
|
Это целое приложение, а не одна фоновая половина: сборка агента кладёт окно настроек
|
|
рядом с ним, потому что именно там агент его и ищет — меню трея запускает окно по
|
|
пути, а в установленном приложении они лежат в одной папке. Собери агента отдельно —
|
|
и пункт «Настройки» молча ничего бы не делал.
|
|
|
|
Rider и Visual Studio берут профили запуска из
|
|
`CursorLang.Agent/Properties/launchSettings.json` и
|
|
`CursorLang.Settings/Properties/launchSettings.json`, так что в списке конфигураций
|
|
появляются три:
|
|
|
|
| Профиль | Что делает |
|
|
|---|---|
|
|
| `Agent` | Запуск как у пользователя: иконка в трее и сразу окно настроек |
|
|
| `Agent (started by Windows)` | Добавляет `--startup`: только трей, без окна — случай входа в систему |
|
|
| `Settings` | Окно настроек само по себе, без агента за спиной |
|
|
|
|
Обе половины читают `%APPDATA%\CursorLang\settings.json` — тот же файл, что и у
|
|
установленного приложения: путь вычисляется в одном месте, и обе половины зовут
|
|
именно его. Значит, отладка правит настоящие настройки; так и задумано — смотреть,
|
|
как агент подхватывает правку из окна, это и есть большая часть того, на что тут
|
|
стоит смотреть.
|
|
|
|
Чтобы шагать по обеим сразу, включи в отладчике присоединение к дочерним процессам:
|
|
окно настроек агент запускает отдельным процессом, и без этого отладчик останется
|
|
на агенте.
|
|
|
|
## Права администратора
|
|
|
|
Приложение работает от имени обычного пользователя. От повышения прав
|
|
отказались, чтобы приложение можно было опубликовать в Microsoft Store: пакеты
|
|
MSIX всегда выполняются в контексте вошедшего пользователя, а политика Store
|
|
отклоняет приложения, которым права администратора нужны для любой части
|
|
функциональности.
|
|
|
|
Цена этого — необязательная горячая клавиша Caps Lock. Пока фокусом владеет
|
|
окно с более высоким уровнем целостности — Диспетчер задач, Редактор реестра,
|
|
запросы UAC, — Windows не доставляет нажатия низкоуровневому хуку и не принимает
|
|
запрос на смену раскладки, так что горячая клавиша там ничего не делает. Чтение
|
|
раскладки чужого окна не ограничено, поэтому сама подсказка продолжает работать
|
|
везде.
|
|
|
|
## Два процесса
|
|
|
|
Приложение — это два исполняемых файла в одной папке.
|
|
|
|
`CursorLang.exe` — агент: перехват клавиатуры, опрос раскладки, подсказка и
|
|
иконка в трее. Именно его Windows запускает при входе в систему и именно он
|
|
целый день сидит в трее, поэтому он собран без WPF: фоновый процесс,
|
|
таскающий за собой движок отрисовки, стоит около ста мегабайт ради подсказки,
|
|
которую средствами Win32 рисуют за восемь. Так он держится ниже 9 МБ.
|
|
|
|
`CursorLang.Settings.exe` — окно настроек и ничего больше. Агент запускает его
|
|
из меню трея; закрытие окна завершает процесс, и занятая WPF память целиком
|
|
возвращается системе. Плата — холодный старт около полусекунды при следующем
|
|
открытии окна.
|
|
|
|
`CursorLang.Core.dll` — общее для обоих: модели, файл настроек, слежение за
|
|
раскладкой. Без UI, и так должно остаться: всё, что попадёт туда, попадёт и
|
|
в фоновый процесс.
|
|
|
|
Всё, что окно сообщает агенту, идёт только через `settings.json`. Окно пишет его
|
|
целиком, во временный файл, который одним движением встаёт на место, и посылает
|
|
агенту зарегистрированное оконное сообщение, по которому тот перечитывает файл.
|
|
Сообщение не несёт данных: пересылка самих изменений лишила бы файл роли
|
|
единственного источника правды. Работающий агент необязателен — без него окно
|
|
работает так же. За файлом никто не следит: писатель у него один, и он сам
|
|
сообщает о записи.
|
|
|
|
В обратную сторону агент говорит одно слово, и то последнее: пункт «Выход» в меню
|
|
трея просит открытое окно настроек закрыться, прежде чем агент уйдёт, — чтобы на
|
|
экране не осталось окна, которому больше ничего не принадлежит. Просьба идёт
|
|
именованным событием сессии, а не оконным сообщением: агент запускает этот процесс
|
|
и намеренно не держит на него ссылки. Чаще всего слушать её некому, и это
|
|
нормальный ответ.
|
|
|
|
## Трей
|
|
|
|
Окно настроек — гость на экране, а не само приложение: оно показывается после
|
|
установки и всякий раз, когда его просят иконка или её меню. Обе кнопки в
|
|
заголовке окна означают ровно то, что написано: окно закрывается, а его процесс
|
|
завершается. Выход из самого приложения — пункт «Выход» в меню трея, и открытое
|
|
окно настроек он закрывает вместе с агентом.
|
|
|
|
Меню иконки системное, его рисует Windows. Надписи по-прежнему следуют языку,
|
|
выбранному в настройках, а тема до меню больше не дотягивается: меню на WPF
|
|
означало бы весь движок отрисовки в фоновом процессе — ровно то, чего этот
|
|
процесс и создан избегать.
|
|
|
|
Запущенное самой Windows, приложение не показывает окна вовсе и сразу уходит в
|
|
трей — пользователь просил, чтобы оно было на месте при входе в систему, а не
|
|
чтобы окно встречало его каждое утро. Такой запуск две сборки распознают
|
|
по-разному: запись в реестре у распакованной сборки несёт аргумент `--startup`, а
|
|
пакет своей командной строкой не распоряжается, и вместо неё Windows
|
|
спрашивают об активации.
|
|
|
|
Если Windows откажет иконке — в сеансе без рабочего стола области уведомлений
|
|
нет, — агент открывает окно настроек при любом запуске. Иначе у пользователя
|
|
осталось бы приложение, которое он не может ни увидеть, ни закрыть.
|
|
|
|
## Автозапуск
|
|
|
|
Автозапуск включается из настроек приложения тем способом, который доступен
|
|
сборке. Пакет объявляет его в манифесте как `windows.startupTask`. Сборка,
|
|
распакованная в папку, прописывается так, как это всегда делали программы для
|
|
рабочего стола, — в `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, и прав
|
|
администратора для этого не нужно. Записанная там команда заканчивается на
|
|
`--startup`: так приложение отличает запуск, устроенный самой Windows, от
|
|
запуска пользователем.
|
|
|
|
И в том, и в другом случае Windows показывает приложение в разделе Параметры —
|
|
Приложения — Автозагрузка; если пользователь выключит его там, приложение уже не
|
|
сможет включить его обратно и скажет об этом вместо молчаливого отказа. Запись в
|
|
реестре при этом остаётся на месте: решение пользователя Windows хранит отдельно
|
|
от неё, в `StartupApproved`, и приложение с ним считается.
|
|
|
|
## Настройки
|
|
|
|
Расположение `settings.json` зависит от способа установки приложения. Отдельная
|
|
установка хранит его в `%APPDATA%\CursorLang`. Пакетная сборка хранит его в
|
|
собственной папке данных пакета, которую Windows удаляет вместе с приложением —
|
|
предполагается, что приложения из Store не оставляют после себя ничего.
|
|
|
|
При первом запуске пакетная сборка подхватывает настройки, оставленные отдельной
|
|
установкой, и копирует их себе. Исходный файл остаётся на месте: обе сборки могут
|
|
быть установлены рядом, и приложение не вправе удалять настройки, которые ему не
|
|
принадлежат.
|
|
|
|
Всё, что относится к всплывающей подсказке, принадлежит режиму размещения, а не
|
|
приложению, и файл хранит по разделу на каждый режим — `AtCursor`, `AtCaret` и
|
|
`FixedPoint`. В каждом лежат сторона, отступ, размер шрифта, прозрачность и оба
|
|
цвета. Подсказка рядом с курсором ввода находится внутри читаемого текста, и её
|
|
хотят видеть небольшой и незаметной; ту, что в углу монитора, ищут намеренно, и её
|
|
хотят видеть крупной. Общее для всех режимов оформление приходилось настраивать
|
|
заново после каждого переключения, поэтому окно настроек показывает оформление
|
|
выбранного выше режима и записывает только в него.
|
|
|
|
Сторона означает в каждом режиме своё, и у каждого для неё собственный тип. Рядом с
|
|
курсором мыши это любой из шести углов и сторон. Рядом с курсором ввода — только
|
|
слева или справа: сверху и снизу от каретки находится следующая строка текста, и
|
|
подсказка закрывала бы читаемое. Для фиксированной точки это место на мониторе, а
|
|
отступ — расстояние от его края: в середине края нет, поэтому выбор середины
|
|
сбрасывает отступ в нуль, а окно настроек показывает его неактивным. Отступ остаётся
|
|
на месте, а не исчезает из раздела — появляющаяся и исчезающая строка сдвигала бы
|
|
всё, что ниже, при каждой смене места.
|
|
|
|
Положение каретки сообщает не всякое приложение, и тогда рядом с курсором ввода —
|
|
нигде. Подсказка уходит на фиксированную точку: место, отступ и оформление берутся
|
|
оттуда, из режима каретки не берётся ничего. Курсор мыши был бы запасным вариантом
|
|
поближе — и неверным: он там, где его оставили, сбоку или на другом мониторе, тогда
|
|
как смотрят на каретку. Фиксированная точка всегда на одном и том же месте, и потому
|
|
известно, куда смотреть, — а вместе с местом должно приходить и оформление: подсказка,
|
|
настроенная мелкой и незаметной для середины текста, в углу монитора останется
|
|
незамеченной.
|
|
|
|
Какие-то приложения, решает не то, на чём они написаны, а то, что окно о себе
|
|
сообщает. Обычный элемент Win32 заводит системную каретку, и её читают сразу;
|
|
Chrome и Electron рисуют свою и сообщают о ней через интерфейсы доступности.
|
|
Окну, которое рисует текст на голом холсте, сообщать нечего — и оба случая
|
|
уживаются в одном приложении: редакторы JetBrains отвечают, потому что каретку
|
|
для них держит их среда выполнения, а панель инструментов, нарисованная Compose,
|
|
не содержит текстового элемента, который нашли бы UI Automation или Java Access
|
|
Bridge. Ради такого окна фиксированная точка и нужна.
|
|
|
|
К какому режиму относится оформление, объясняет всплывающая подсказка рядом с самим
|
|
режимом, а не строка текста в разделе: ползунки ниже показывают другие числа после
|
|
переключения режима, и это вопрос, который задают один раз.
|
|
|
|
## Обновления
|
|
|
|
Приложение обновляет Store, а само приложение об этом не заботится: раздела
|
|
обновлений в окне нет, запросов в сеть нет и кода для них тоже нет.
|
|
|
|
Дело не во вкусе, а в цене подписи. MSIX Windows установит только тогда, когда
|
|
доверяет подписи на нём, а публично доверенный сертификат для подписи кода
|
|
оказался недосягаем: удостоверяющие центры, которые их продают, здесь его не
|
|
выдают, а те, что держали бы ключ в облачном HSM, — тем более, а сертификат на
|
|
USB-токене сюда не привезти. Без подписи пакет установится только на машине в
|
|
режиме разработчика, значит выкладывать в выпуск нечего и искать обновления
|
|
негде. Store подписывает пакет своим сертификатом и обновляет приложение сам —
|
|
на этом вопрос и закрыт.
|
|
|
|
Версия работающего приложения стоит в заголовке окна настроек:
|
|
`CursorLang 1.2.3 — Настройки`. Больше её в интерфейсе нигде нет, и стоит она
|
|
там для того, чтобы её можно было назвать в сообщении об ошибке.
|
|
|
|
## Тесты
|
|
|
|
```powershell
|
|
dotnet test
|
|
```
|
|
|
|
На каждый проект свой набор — `CursorLang.Core.Tests`, `CursorLang.Agent.Tests`
|
|
и `CursorLang.Settings.Tests`, — все на xUnit, а подделки и вспомогательный код
|
|
общие и вынесены в отдельную библиотеку `CursorLang.Tests.Shared`. Сама библиотека
|
|
про xUnit не знает: единственному месту, которому нужно было утверждение, хватило
|
|
исключения. У набора для Core нет ссылки на WPF, и это само по себе проверка: то,
|
|
что затянуло бы WPF в фоновый процесс, там попросту не скомпилируется.
|
|
|
|
Половина приложения — окна, таймеры, перехват клавиатуры — живёт только на
|
|
потоке STA с очередью сообщений, поэтому тесты держат один такой поток на весь
|
|
прогон и выполняют на нём всё, что этого требует. Для Core и агента на этом
|
|
потоке крутится обычный цикл Win32 (`CursorLang.Tests.Shared/Pump.cs`), для окна
|
|
настроек —
|
|
диспетчер WPF.
|
|
|
|
Части проверок нужно настоящее окно переднего плана: положение каретки и
|
|
просьбу сменить раскладку видно только там. Право вывести окно вперёд Windows
|
|
даёт не всегда, и такие проверки сообщают о себе как о пропущенных, а не как
|
|
о провалившихся — без окна переднего плана проверять нечего. Сквозные проверки
|
|
запускают собранное приложение отдельным процессом и пропускают себя, если
|
|
приложение уже работает: вмешиваться в чужой запущенный экземпляр они не вправе.
|
|
|
|
Покрытие снимается так:
|
|
|
|
```powershell
|
|
dotnet test --collect:"XPlat Code Coverage" --settings coverage.runsettings
|
|
```
|
|
|
|
Непокрытым остаётся то, до чего тестовому процессу не дотянуться: пути, которым
|
|
нужен установленный пакет MSIX — `StartupTask` и папка данных пакета, — и
|
|
композиционные корни `App.xaml.cs` и `Agent.cs`, которые вместо этого
|
|
проверяются сквозным запуском приложения. Одна из таких проверок читает список
|
|
модулей работающего агента и падает, если среди них окажется хоть что-то от
|
|
движка отрисовки WPF.
|
|
|
|
## Непрерывная сборка
|
|
|
|
Пайплайны лежат в `.gitea/workflows` и работают на Gitea Actions. Запрос
|
|
на слияние в `master` собирается и проверяется тестами; по тегу вида `v1.2.3`
|
|
проект собирается, проходит тесты и пакуется в MSIX, который остаётся
|
|
в артефактах прогона. Номер версии берётся только из тега — тег любого другого
|
|
вида останавливает прогон в самом начале. Версия пакета получается `1.2.3.0`:
|
|
Store принимает четыре числа и последнее оставляет себе, так что тег на него не
|
|
влияет.
|
|
|
|
Обоим пайплайнам нужен runner под Windows с меткой `windows-x64`, на нём —
|
|
.NET 10 SDK и git-lfs. Выгрузка исходников тянет файлы LFS: без них иконка
|
|
остаётся текстовой заглушкой и сборка на ней падает. Работать runner лучше
|
|
в интерактивном сеансе рабочего стола: тесты поднимают настоящие окна, и те
|
|
проверки, которым нужен свой рабочий стол — сквозные, а также те, что просят
|
|
окно на переднем плане или каретку, — на runner’е, живущем службой в нулевом
|
|
сеансе, пропускают себя: показать окно там негде.
|
|
|
|
Пакет идёт в Store и больше никуда: он не подписан, подпись на него ставит сам
|
|
Partner Center. Поэтому прогон оставляет его в артефактах под именем
|
|
`msix-1.2.3.0`, откуда его забирают и загружают руками; к релизу не прикладывается
|
|
ничего — релиз по тегу Gitea заводит сама, и в нём один только тег. Неподписанный
|
|
пакет, висящий в релизе, выглядел бы как то, что можно установить, и не
|
|
устанавливался бы нигде — см. раздел об обновлениях.
|
|
|
|
Identity берётся из переменных репозитория, а если те не заданы — из значений по
|
|
умолчанию в скрипте: `MSIX_IDENTITY_NAME`, `MSIX_PUBLISHER`
|
|
и `MSIX_PUBLISHER_DISPLAY_NAME`. Вместе identity и publisher задают family name
|
|
пакета, поэтому от версии к версии оба должны оставаться прежними — иначе Store
|
|
примет следующую за другое приложение.
|
|
|
|
## Сборка пакета MSIX
|
|
|
|
Ни Visual Studio, ни Windows SDK не требуются — `makeappx` поставляется из
|
|
пакета NuGet. Порядок публикации целиком — от регистрации разработчика
|
|
до отправки на проверку — описан в
|
|
[Packaging/PUBLISHING.RU.md](Packaging/PUBLISHING.RU.md).
|
|
|
|
```powershell
|
|
# Разово: отрисовать логотипы и иконку exe (уже в репозитории, повторить после правок)
|
|
powershell -File Packaging\New-Assets.ps1
|
|
|
|
# Проверка на своей машине
|
|
powershell -File Packaging\build-msix.ps1
|
|
|
|
# Для Partner Center — identity та, что зарезервирована там
|
|
powershell -File Packaging\build-msix.ps1 -Version 1.0.1.0 `
|
|
-IdentityName 12345AleksandrNeichev.CursorLang -Publisher "CN=ABCD1234-..."
|
|
```
|
|
|
|
Результат — `artifacts\packages\CursorLang-<версия>-x64.msix`. Он загружается в
|
|
Partner Center как есть.
|
|
|
|
Собирается только x64. Сборка под arm64 удвоила бы вес каждого релиза ради
|
|
машин, которые и так выполняют x64 через эмуляцию.
|
|
|
|
Здесь ничего не подписывается: подпись на пакет ставит сам Store, а для установки
|
|
на эту машину вместо пакета регистрируется layout — см. `-Install` ниже.
|
|
|
|
Приложение поставляется с собственной копией .NET: Windows не включает .NET 10, а
|
|
MSIX не может установить среду выполнения как зависимость пакета.
|
|
|
|
Чтобы посмотреть, как пакет работает на этой машине, соберите его с `-Install`:
|
|
|
|
```powershell
|
|
pwsh -File Packaging\build-msix.ps1 -Install
|
|
```
|
|
|
|
Приложение появится в меню «Пуск» как любое установленное. Регистрируется не сам
|
|
файл пакета, а layout, из которого пакет собирается, поэтому подпись не нужна —
|
|
достаточно режима разработчика. Обратная сторона в том, что приложение работает
|
|
прямо из `artifacts\layout`, и пересборка вытащила бы файлы у него из-под ног:
|
|
такую регистрацию скрипт снимает перед очисткой папки независимо от того,
|
|
просили ли `-Install`. Копию, установленную из Store, он не трогает, хотя
|
|
называется она так же.
|
|
|
|
Удалить вручную:
|
|
|
|
```powershell
|
|
Remove-AppxPackage (Get-AppxPackage -Name AleksandrNeichev.CursorLang).PackageFullName
|
|
```
|
|
|
|
## Сборка установщика
|
|
|
|
То же приложение собирается и обычным MSI — чтобы раздавать помимо Store: пока
|
|
Store не вынес решение или если так его и не вынесет. Ставить ничего, кроме .NET
|
|
SDK, не нужно: WiX приезжает пакетом NuGet, как и `makeappx`.
|
|
|
|
```powershell
|
|
# Собрать и запустить — посмотреть глазами пользователя
|
|
pwsh -File Packaging\build-installer.ps1 -Install
|
|
|
|
# Всё, что нужно релизу
|
|
pwsh -File Packaging\build-installer.ps1 -Version 1.0.1
|
|
```
|
|
|
|
Результат — `artifacts\installers\CursorLang-<версия>-x64.msi`. Всё лежит внутри
|
|
`.msi` — отдельного архива рядом с ним нет.
|
|
|
|
Устанавливается только для текущего пользователя, в
|
|
`%LOCALAPPDATA%\Programs\CursorLang`, поэтому не просит ни прав администратора,
|
|
ни подтверждения. Удаление идёт через «Параметры» — «Приложения», как у любой
|
|
программы, и уносит с собой запись автозапуска: иначе Windows продолжала бы
|
|
показывать в автозагрузке приложение, которого уже нет.
|
|
|
|
Установщик никто не подписывает, поэтому Windows предупреждает о неизвестном
|
|
издателе и пользователю приходится настоять. Покупка сертификата это сразу не
|
|
снимет: SmartScreen смотрит на репутацию, а у нового сертификата её нет, пока
|
|
приложение не наберёт установок.
|
|
|
|
Про проект WiX стоит знать две вещи, прежде чем его править.
|
|
|
|
Он закреплён на WiX 5, а не на нынешней 7: начиная с шестой версии инструмент
|
|
требует принимать лицензию Open Source Maintenance Fee — бесплатную при доходе
|
|
меньше $10 000 в год, но принимать её должен человек, а не сборочный скрипт.
|
|
|
|
И он собирается без проверки MSI. Проверки ICE выполняются службой установщика
|
|
Windows, до которой сборочному агенту не дотянуться: каждая возвращается
|
|
ошибкой `WIX0217`, и сборка умирает на без малого сотне таких. На обычной
|
|
машине служба отвечает, и проверка включается одним ключом:
|
|
|
|
```powershell
|
|
dotnet build Packaging\Installer\CursorLang.wixproj -p:SuppressValidation=false
|
|
```
|
|
|
|
Три правила остаются подавленными и тогда. MSI исходит из установки на всю
|
|
машину, а установка в профиль пользователя нарушает правила, которые описывают
|
|
ровно то, что здесь и задумано.
|
|
|
|
## Сайт
|
|
|
|
`site/index.html` — страница, на которую ссылается заявка в Store: что делает
|
|
приложение и политика конфиденциальности, говорящая, что оно ничего не собирает.
|
|
Partner Center не требует политику от приложения, которое не собирает данных, но
|
|
страница, сказавшая это вслух, снимает вопрос у проверяющего.
|
|
|
|
Ничего больше ниоткуда не подгружается: ни фреймворка, ни сборки, ни шрифта из
|
|
сети.
|
|
|
|
| Файл | Что внутри |
|
|
|---|---|
|
|
| `index.html` | Английская страница — разметка, оба языка сразу |
|
|
| `ru/index.html` | Та же разметка, впереди по умолчанию — русский |
|
|
| `styles.css` | Все правила, включая палитру |
|
|
| `boot.js` | Язык и тема прошлого визита — до первого кадра |
|
|
| `main.js` | Остальное: переключатели, меню, демонстрация, предпросмотр |
|
|
| `favicon.svg` | Та же тёмная плашка с «Aя», что и у иконки приложения |
|
|
| `robots.txt` | Указывает краулерам на sitemap, ничего не запрещает |
|
|
| `sitemap.xml` | Обе страницы, связанные между собой через `hreflang` |
|
|
|
|
`boot.js` вынесен отдельно и мал по одной причине: он должен отработать до
|
|
разбора `body`. Он читает выбранное в прошлый раз и ставит это на элемент
|
|
`<html>` — без него первый кадр был бы английским и в системной теме, а потом
|
|
на глазах исправлялся бы. `main.js` работает по разметке, которая к тому времени
|
|
должна существовать, поэтому он отложенный.
|
|
|
|
Оба языка живут в разметке каждой из страниц одновременно, а переключатель в
|
|
углу выбирает один. Каждый переведённый кусок — это `<span lang="en">` рядом с
|
|
`<span lang="ru">`, и тот, который не совпадает с языком на элементе `<html>`,
|
|
скрывается правилом в две строки. С выключенными скриптами любая из страниц
|
|
всё равно читается — на том языке, что у неё по умолчанию.
|
|
|
|
Английский и русский — это две страницы, а не одна с переключателем на клиенте,
|
|
чтобы каждая могла ранжироваться в поиске сама по себе: `/` и `/ru/`, со
|
|
ссылками `hreflang` друг на друга и в разметке, и в `sitemap.xml`, и у каждой —
|
|
свой заголовок, описание и canonical. Клик по EN или RU — это настоящий переход
|
|
между ними, а не перерисовка на месте, а `boot.js` отправляет посетителя,
|
|
зашедшего впервые, на страницу под язык его браузера — но никогда не уводит с
|
|
той из двух, на которую уже привела ссылка: иначе раздельное ранжирование
|
|
пропадало бы для всех, кто пришёл именно так, включая краулеров.
|
|
|
|
Выкладка сводится к копированию файлов как есть — на общие файлы (`/styles.css`
|
|
и другие) ссылаются по абсолютному пути, так что `ru/index.html` обращается к
|
|
той же копии, не заводя свою.
|
|
|
|
Поддержка — `support@alrakis.kz`: адрес стоит в разделе о данных, под кнопками
|
|
загрузки и в подвале.
|