kage (影, «тень») клонирует сайт в папку, которую можно просматривать офлайн, при этом полностью вырезая все скрипты. Утилита открывает каждую страницу в настоящем безголовом Chrome, ждёт, пока страница стабилизируется, снимает слепок DOM в том виде, в каком его увидел бы человек, а затем удаляет весь JavaScript и сохраняет CSS, изображения и шрифты по локальным путям. То, что оседает на диске, выглядит как живой сайт, но не исполняет никакого кода.
Проблема хорошо знакома. Сохраняешь страницу через «Сохранить как», через полгода открываешь — и видишь пустой экран, бесконечный спиннер или копию, которая всё ещё пытается достучаться до сервера аналитики, которого уже нет. Страница никогда по-настоящему не была твоей. Она была тонким клиентом чужого JavaScript.
kage идёт другим путём. Он управляет настоящим браузером, позволяет странице завершить все свои действия, берёт готовый результат и вырезает из него все скрипты. Никакой слежки, никаких сетевых запросов, никаких сюрпризов. Только .html-файлы, которые можно открывать прямо с диска, передавать другу или упаковать в один файл и забыть о нём на десятилетие.
Полная документация и руководства — на kage.tamnd.com.
Установка
go install github.com/tamnd/kage/cmd/kage@latest
Предпочитаете готовый бинарник? Скачайте архив, пакет .deb/.rpm/.apk или файл с контрольными суммами со страницы releases. Или доверьтесь пакетному менеджеру:
# Homebrew (macOS)
brew install --cask tamnd/tap/kage
# Scoop (Windows)
scoop bucket add tamnd https://github.com/tamnd/scoop-bucket
scoop install kage
# apt (Debian, Ubuntu)
curl -fsSL https://tamnd.github.io/linux-repo/gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/tamnd.gpg
echo "deb [signed-by=/usr/share/keyrings/tamnd.gpg] https://tamnd.github.io/linux-repo/apt stable main" | sudo tee /etc/apt/sources.list.d/tamnd.list
sudo apt update && sudo apt install kage
# dnf (Fedora, RHEL)
sudo dnf config-manager --add-repo https://tamnd.github.io/linux-repo/dnf/tamnd.repo
sudo dnf install kage
Или не устанавливайте Chrome вручную, а используйте образ контейнера, в который уже входит Chromium:
docker run --rm -v "$PWD/out:/out" ghcr.io/tamnd/kage clone paulgraham.com
kage управляет настоящим браузером, поэтому на хосте должен быть Chrome или Chromium. Системную установку утилита обнаруживает сама; указать конкретный путь можно через флаг --chrome или переменную окружения KAGE_CHROME. Контейнеру ничего дополнительного не нужно.
Автодополнение командной строки (shell completion) включено в поставку: kage completion bash|zsh|fish|powershell.
Быстрый старт
Давайте зеркалируем эссе Пола Грэма, чтобы читать их в самолёте, на ноутбуке без Wi-Fi или в 2050 году, когда сайт наконец сменит дизайн:
# 1. Клонировать сайт в $HOME/data/kage/paulgraham.com/
kage clone paulgraham.com
# 2. Открыть зеркало офлайн в браузере
kage serve $HOME/data/kage/paulgraham.com
# open http://127.0.0.1:8800
Вот и весь цикл. Каждое эссе, каждое изображение, каждая таблица стилей — заморожены на диске и доступны без единого сетевого запроса. Следующие два шага необязательны, но удобны: собрать всё в один файл и открыть сайт в собственном окне.
# 3. Упаковать зеркало в один файл для удобного хранения
kage pack paulgraham.com # -> paulgraham.com.zim
kage open paulgraham.com.zim
# 4. Или в один исполняемый файл, который и есть сайт
kage pack paulgraham.com --format binary -o paulgraham
./paulgraham # раздаёт себя сам, ничего устанавливать не нужно
Команды
| Команда | Что делает |
|---|---|
|
рендерит сайт в безголовом Chrome и создаёт доступное для просмотра зеркало без скриптов |
|
открывает клонированную папку через локальный HTTP-сервер для предварительного просмотра |
|
сворачивает зеркало в один ZIM-архив, самодостаточный бинарник или приложение с двойным кликом |
|
отдаёт запакованный ZIM для офлайн-чтения |
Клонирование
# Весь сайт, в $HOME/data/kage/<host>/
kage clone https://paulgraham.com
# Только первые 50 страниц, на глубину двух переходов — для беглого знакомства
kage clone paulgraham.com --max-pages 50 --max-depth 2
# Только один раздел большого сайта
kage clone go.dev --scope-prefix /doc
# Захватить поддомены и прокручивать каждую страницу для ленивой загрузки изображений
kage clone example.com --subdomains --scroll
# Вернуться через месяц и перерендерить страницы, чтобы подхватить новые эссе
kage clone paulgraham.com --refresh
Клонирование — это вежливый обход в ширину (breadth-first crawl). Утилита читает robots.txt, берёт начальные URL из sitemap.xml и держится в пределах исходного хоста, если не указать иное. Операция строго идемпотентна: каждая страница идентифицируется по файлу, в который записывается, поэтому одно и то же эссе, достигнутое по http и https, со слешем в конце и без него, загружается ровно один раз. Нажмите Ctrl-C — прогресс сохраняется; запустите снова — обход продолжится с прерванного места. --refresh перерендерит страницы на месте, --force сотрёт хост и начнёт заново.
Флаги, которые нужны чаще всего:
| Флаг | По умолчанию | Смысл |
|---|---|---|
|
|
Корень вывода; зеркало помещается в |
|
|
Остановиться после N страниц (0 — без ограничений) |
|
|
Глубина обхода по ссылкам (0 — без ограничений) |
|
Обходить только пути с этим префиксом |
|
|
|
Считать поддомены исходного хоста допустимыми |
|
Префиксы путей для пропуска (можно указывать несколько раз) |
|
|
|
Автопрокрутка каждой страницы для активации ленивой загрузки |
|
|
Сколько страниц рендерить одновременно |
|
|
Игнорировать |
|
|
Переопределить значение |
|
|
Удалить существующее зеркало хоста перед началом |
|
Путь к бинарнику Chrome/Chromium |
kage clone --help покажет остальные флаги, включая тайминги рендера, параллелизм и ограничения на размер ресурсов.
Предварительный просмотр
kage serve запускает небольшой статический файловый сервер над клонированной папкой, чтобы ссылки и ресурсы разрешались так же, как на настоящем хосте:
kage serve $HOME/data/kage/paulgraham.com
# open http://127.0.0.1:8800
Упаковка в один файл
Зеркало — это папка: удобно для просмотра, но неудобно для перемещения. Копировать тысячи мелких файлов медленно, а «на, возьми этот каталог» — неловкий способ что-то передать. kage pack сворачивает всё зеркало в один артефакт, и вы сами выбираете форму: открытый ZIM-архив или единственный исполняемый файл, который и есть сам сайт.
Один ZIM-файл
kage pack paulgraham.com # -> paulgraham.com.zim
kage open paulgraham.com.zim
ZIM — открытый формат файлов, созданный именно для этого: целый сайт (или целая Wikipedia) сжимается в один сжатый, индексированный, предназначенный только для чтения файл. kage записывает в него всё зеркало: текст сжимается через zstd, медиафайлы хранятся как есть. Это формат, лежащий в основе Kiwix — проекта офлайн-контента, который используют, чтобы взять с собой Wikipedia, Stack Overflow и Project Gutenberg на корабль, в класс без интернета или в телефон на долгий перелёт. Поскольку формат — задокументированный стандарт, а не изобретение kage, файл paulgraham.com.zim, созданный сегодня, откроется в любом ZIM-ридере через много лет.
Таким образом, вы не привязаны к kage. kage open — самый быстрый способ открыть архив, но тот же файл работает во всей экосистеме Kiwix:
kage open paulgraham.com.zim # открыть через kage
kiwix-serve paulgraham.com.zim # или раздать через Kiwix на http://localhost
Также можно открыть файл двойным кликом в десктопном приложении Kiwix или загрузить в Kiwix для Android или iOS, чтобы читать зеркало на телефоне. Одна оговорка: kage записывает структурно корректный архив со стандартными метаданными, но не строит индекс полнотекстового поиска, который есть в сборках Kiwix, — поэтому навигация и переходы по ссылкам работают везде, а поиск внутри ридера ограничен.
Упаковка детерминирована. Одно и то же зеркало всегда даёт побайтово идентичный файл, а UUID архива выводится из содержимого, а не генерируется случайно, — так что результат безопасно контрольно-суммировать и кешировать. Простое имя хоста разрешается относительно каталога вывода по умолчанию, именно поэтому kage pack paulgraham.com работает сразу после kage clone paulgraham.com.
Самодостаточный бинарник
--format binary приклеивает архив к копии kage и отдаёт вам единственный исполняемый файл, который при запуске раздаёт сайт офлайн. Тому, кому вы его отправите, не нужно ничего устанавливать: ни kage, ни ZIM-ридер, ничего.
kage pack paulgraham.com --format binary -o paulgraham
./paulgraham
Приложенный архив не зависит от платформы; только базовый исполняемый файл привязан к архитектуре. По умолчанию kage добавляет архив к себе, так что вы получаете просмотрщик для той машины, на которой запустили команду. Укажите в --base бинарник kage, собранный под другую ОС (возьмите его со страницы release; для каждой платформы есть свой), чтобы собрать просмотрщик для той платформы прямо со своей машины. kage читает заголовок исполняемого файла из --base, чтобы определить цель, поэтому Windows-просмотрщик автоматически получает расширение .exe:
# Находясь на Mac, собрать Windows-просмотрщик
kage pack paulgraham.com --format binary --base kage-windows-amd64.exe # -> paulgraham.exe
Платой за удобство является размер. Бинарник несёт в себе полный kage, поэтому весит около 13 МиБ плюс сам сайт, как бы мало зеркало ни занимало. Если нужен только контент, ZIM будет значительно компактнее.
Приложение с двойным кликом
Голый бинарник прекрасен из терминала, но двойной клик по нему в файловом менеджере — не лучший опыт: macOS откроет окно Terminal позади сайта, а на Windows рядом мелькнёт консоль. Добавьте --app — и kage обернёт тот же просмотрщик в нормальное десктопное приложение: двойной клик просто откроет сайт, без терминала, со своей иконкой-фавиконом.
На macOS вы получаете настоящий пакет .app:
kage pack paulgraham.com --app # -> paulgraham.app
open paulgraham.app # или двойной клик в Finder
На Linux укажите в --base бинарник kage для Linux — и получите .AppDir в стиле AppImage с запускателем .desktop (Terminal=false, консоль не появится). Если установлен appimagetool, kage сложит всё в один двойным кликом открываемый .AppImage:
kage pack paulgraham.com --app --base kage-linux-amd64 # -> paulgraham.AppDir (+ .AppImage)
Иконку kage извлекает из зеркала (предпочитает крупный apple-touch-icon.png, при его отсутствии берёт favicon.ico); передайте --icon some.png, чтобы задать свою. Сочетание --app с базой на основе webview (см. ниже) даёт эффект «настоящего приложения»: двойной клик открывает нативное окно, а не браузер.
Windows не требует никакого пакета — там отдельный .exe уже сам по себе приложение. Загвоздка — в окне консоли. В релизе есть kage_<version>_windows-gui_<arch>.zip, бинарник которого скомпонован для GUI-подсистемы: просмотрщик, упакованный на его основе, открывается без консоли:
# Собрать Windows-просмотрщик без консоли (с любой ОС)
kage pack paulgraham.com --format binary --base kage-windows-gui-amd64.exe # -> paulgraham.exe
Настоящее окно, а не вкладка браузера
По умолчанию упакованный бинарник открывает системный браузер — сайт появляется как очередная вкладка с адресной строкой среди 47 уже открытых. Соберите kage с тегом webview — и вместо этого сайт откроется в собственном окне, опирающемся на WebView операционной системы (WKWebView на macOS, WebView2 на Windows, WebKitGTK на Linux). Эссе Пола Грэма, офлайн, в чём-то, что выглядит и ощущается как настоящее приложение:
make build-webview # или: CGO_ENABLED=1 go build -tags webview ./cmd/kage
kage pack paulgraham.com --format binary --base bin/kage -o paulgraham
./paulgraham # открывает окно, никакого браузера
Такая сборка требует cgo и линкует платформенный WebView, поэтому она остаётся опциональной. Стандартная сборка — чистый Go (CGO_ENABLED=0), а готовые бинарники в релизах открывают браузер, что упрощает кросс-компиляцию в релизном пайплайне. kage open, собранный с -tags webview, тоже показывает ZIM в нативном окне.
Как это работает
seed URL ─▶ headless Chrome ─▶ final DOM ─▶ strip JS ─▶ localise assets ─▶ disk
(render) (snapshot) (sanitize) (rewrite links)
Пул вкладок Chrome рендерит страницы; отдельный пул загружает ресурсы по обычному HTTP. Каждый URL детерминированно отображается на локальный путь, поэтому ссылки переписываются ещё до того, как ресурс, на который они указывают, успевает загрузиться. Результат на диске выглядит так:
paulgraham.com/
├── index.html # главная страница, скрипты удалены
├── greatwork.html # /greatwork.html, одно из эссе
├── _kage/ # зарезервировано: ресурсы и состояние обхода
│ ├── paulgraham.com/site.css # локализованная таблица стилей (url() переписаны)
│ ├── paulgraham.com/pg.png
│ └── state.json # посещённые URL для возобновления обхода
└── ...
pack использует ту же идею: ссылки в зеркале уже являются относительными путями внутри зеркала, и они однозначно соответствуют записям содержимого в архиве, поэтому клик на обслуживаемой странице попадает в нужную запись без какого-либо переписывания.
Сборка из исходников
git clone https://github.com/tamnd/kage
cd kage
make build # -> bin/kage (чистый Go, открывает браузер)
make build-webview # -> bin/kage с нативным просмотрщиком (требует cgo)
make test # полный набор тестов, включая end-to-end с Chrome
make test-short # пропустить тесты, запускающие браузер
Репозиторий разделён по зонам ответственности:
cmd/kage/ тонкий main: закрепляет главный поток, затем передаёт управление cli.Execute
cli/ дерево команд cobra и привязка флагов
clone/ обход: очередь, воркеры рендеринга, воркеры ресурсов, состояние возобновления
browser/ управление безголовым Chrome и снятие слепков DOM
sanitize/ удаление скриптов, обработчиков событий и javascript: URL из DOM
asset/ загрузка и локализация CSS, изображений и шрифтов
urlx/ детерминированное отображение URL на путь
zim/ чистый Go-ридер и писатель ZIM
pack/ зеркало в ZIM или самодостаточный бинарник, офлайн HTTP-обработчик
viewer/ отдача сайта: системный браузер или нативное окно (тег webview)
docs/ документационный сайт tago
Выпуск релизов
Достаточно запушить тег версии — GitHub Actions запустит GoReleaser, который соберёт архивы, пакеты .deb/.rpm/.apk, мульти-архитектурный образ GHCR с Chromium внутри, контрольные суммы, SBOM и подпись cosign:
git tag v0.1.1
git push --tags
Тег образа не содержит префикса v (ghcr.io/tamnd/kage:0.1.1). Шаги для Homebrew и Scoop самостоятельно отключаются до появления соответствующих токенов, так что первый релиз работает без дополнительных секретов.
Лицензия
MIT. См. LICENSE.