Почему `serve` отображал некрасивый сайт, тогда как `publishSite` был идеальным
Опубликовано 22 April 2026
Вы знаете этот момент, когда ваш сайт на локальном хостинге выглядит нелепо, но после развёртывания в сети всё становится идеально? Это случилось с моим плагином Gradle JBake. Задача`serve`отображал квадратные кнопки, тёмный текст на чёрном фоне и странно отсутствующий CSS. однако`publishSite`генерировал тот же сайт, он, безупречен. Виновным не был ни CSS, ни браузер, ни кэш. Это была строка кода Kotlin — и плохая привычка с аргументами командной строки.
Сцена: четыре скриншота, два рендера
Это произошло после сбоя системы. Я сделал четыре скриншота, чтобы сравнить локальный рендер (./gradlew serve`на`localhost:8820) со встроенным рендером (`publishSite`на GitHub Pages). Два скриншота на страницу: верх и низ.
Локальный рендеринг
Верх страницы— Кнопки Project и Template sontмаленькие, прямоугольные, в стандартных цветах (синий и зелёный). Карточка Start Writing имеетсплошной чёрный фон, без рамки.
Нижний колонтитул— Подзаголовок «Derniers articles et ressources» являетсяпочти нечитаемый, слишком темный на чёрном фоне. Даты под изображениями блога ("17 October 2013", etc.) являютсяотсутствующие.
Онлайн-рендеринг
Верх страницы— те же кнопки являютсябольшие, овцы, яркий неоново-зеленый, текст в верхнем регистре. Карта имеет белый закруглённый блок с тенью.
Нижний колонтитул— Подзаголовок естьхорошо читаемый. Даты этовидимые. Карточки статей имеютокруглые углыи названия являютсяжирным.
Первый рефлекс: тема. Я использую динамическую систему темы (светлая, тёмная, high-contrast) основанную на`localStorage`. Возможно, что локальный магазин запустил тему высокой контрастности ? Нет. Я уже написалhttps://cheroliv.com/blog/2025/0098_preloading_css_variable_post.html[полная статья о предварительной загрузке тем], Я знаю, как это работает. А потом, даже в режиме высокой контрастности, структура CSS (овальные кнопки, тени) должна была быть там. Её там не было.
Второй рефлекс: кэш браузера. Нет, я уже проводил тест с чистыми профилями. Разница сохранялась.
Третий рефлекс :`serve`не обслуживал те же файлы, что`publishSite`.
Диагностика: serve не указывала на правильную папку
Мой плагин Gradle`bakery`имеет два основных задачи:
-
bake: генерирует статический сайт в`build/bake/` -
`serve`Запусти локальный веб-сервер
-
publishSite: росток`build/bake/`на GitHub Pages
Разногласие могло возникать только из`serve`. Si publishSite`опубликовывал правильное содержимое, это что`build/bake/`был хорош. Если`serve`он показывал что-то другое, значит, он не работал`build/bake/.
Давайте посмотрим на код. В`SiteManager.kt`, задача`serve`определяется следующим образом :
tasks.register("serve", JavaExec::class.java) { task ->
task.apply {
mainClass.set("org.jbake.launcher.Main")
classpath = jbakeRuntime
environment("GEM_PATH", jbakeRuntime.asPath)
jvmArgs(/* ... */)
args = listOf(
"-b", file(site.bake.srcPath).absolutePath,
"-s", layout.buildDirectory.get()
.asFile.resolve(site.bake.destDirPath)
.absolutePath
)
}
}
org.jbake.launcher.Main`является точкой входа CLI JBake 2.7.0. Идея: передать-b`для исходной папки и`-s`pour целевой папки, затем запустить серверный режим. Но…
Основная причина: JBake CLI не работает так
JBake 2.7.0 ждет некоторыхпозиционные аргументыдля источника и назначения, затем необязательные флаги. Сигнатура выглядит так:
jbake <source> <destination> [options]
Однако в моём коде я писал :
args = listOf("-b", "/path/to/site", "-s", "/path/to/build/bake")
JBake интерпретировал это как:
-
-b→ разобрать флагbake(логический), потреблённый -
/path/to/site→ становится позиционным аргументом 1 (источник) -
-s→ анализировать флаг "serve" (булево), сервер запущеннемедленно -
/path/to/build/bake→ становится сиротским аргументом, игнорируемым или неправильно истолкованным
Результат: JBake брал`/path/to/site`как источник,не учитывал пункт назначенияпредоставлена (build/bake), и использовал его каталог по умолчанию (часто`./output`или временная копия). Файл`css/styles.css`сборка была поэтому перезаписана или проигнорирована, и обслуживался старый CSS по умолчанию (без пользовательских переменных, без закруглений, без теней).
В сравнении,bake et publishSite`они используютофициальный плагин Gradle JBake(`jbake-gradle-plugin) кто пишет аккуратно в`build/bake/`через API Gradle. Они не проходят через CLI.
Решение: позиционные аргументы, а не флаги
Поправка тривиальна — стоит только знать. Достаточно передать источник и назначение как позиционные аргументы, затем`-s`в конце :
args = listOf(
file(site.bake.srcPath).absolutePath,
layout.buildDirectory.get()
.asFile.resolve(site.bake.destDirPath)
.absolutePath,
"-s"
)
Всё. Изменено три строки, баг исправлен.
После локальной публикации плагина (publishToMavenLocal), un `./gradlew serve`Запустите JBake с правильным синтаксисом:
jbake /home/user/project/site /home/user/project/build/bake -s
И в этот раз сервер Jetty, встроенный в JBake, хорошо обслуживает содержимое`build/bake/`— идентично тому, что опубликовано в сети.
@startuml
!define RECTANGLE class
package "AVANT (bug)" {
RECTANGLE JBakeCLI1 {
+ args = ["-b", "site", "-s", "build/bake"]
}
RECTANGLE Source1 {
+ site/
}
RECTANGLE Dest1 {
+ ??? (dossier temporaire)
}
RECTANGLE Jetty1 {
+ sert mauvais fichiers
}
JBakeCLI1 --> Source1 : кровать
JBakeCLI1 -[#red]x Dest1 : destination ignorée
Jetty1 --> Dest1 : служит
}
package "APRÈS (fix)" {
RECTANGLE JBakeCLI2 {
+ args = ["site", "build/bake", "-s"]
}
RECTANGLE Source2 {
+ site/
}
RECTANGLE Dest2 {
+ build/bake/
}
RECTANGLE Jetty2 {
+ sert les bons fichiers
}
JBakeCLI2 --> Source2 : кровать
JBakeCLI2 --> Dest2 : написано в
Jetty2 --> Dest2 : служит ОК
}
@enduml
Быстрая проверка с помощью curl
Чтобы убедиться, что сервер правильно обслуживает нужный контент, простой`curl`подтверждает, что`index.html`содержит`data-bs-theme="light"`и что`build/bake/css/styles.css`весит ровно 31 402 байта — идентичен файлу, сгенерированному`bake`.
$ ./gradlew serve
# Dans un autre terminal :
$ curl -s http://localhost:8820/ | grep data-bs-theme
<html ... data-bs-theme="light" ...>
$ curl -I http://localhost:8820/css/styles.css
HTTP/1.1 200 OK
Content-Length: 31402
Content-Type: text/css
Локальная отрисовка теперьпиксель-идентичныйв развернутом рендере.
Почему этот баг был коварным
Вы видите, почему было трудно поймать?
-
Нет явной ошибки: JBake не падал. Он "работал", просто с неправильной папкой.
-
Билд работал:`./gradlew bake`правильно генерировал файлы в`build/bake/`.
-
Развертывание работало(Empty output)`publishSite`Он публиковал хороший контент онлайн.
-
Только
serveбыл сломан: задача разработки, которую мы постоянно используем для итерации. -
Привычка к
-key valueв качестве разработчика, мы привыкли к годам использования CLI Unix-o output`,-i input). JBake CLI — это исключение — источник и назначение являются позиционными.
Вывод
Если вы поддерживаете плагин Gradle, который оборачивает JBake (или любой инструмент CLI)читайте документацию аргументовдаже если вы думаете, что знаете их. Неявное предположение (-s = "set destination") может стоить вам часов визуальной отладки.
Здесь fix буквально означал изменить :
args = listOf("-b", src, "-s", dest) // ❌ BUG
en :
args = listOf(src, dest, "-s") // ✅ FIX
Три перемещённых токена, и мой локальный сайт снова становится таким же красивым, как сайт в продакшене.
Урок: когда рендеринг отличается между локальной и production-средой без очевидной причины, сначала подозревайте pipeline — не CSS, не браузер, а уж тем более не фреймворк. Часто именно этап непосредственно перед рендерингом обманывает.