время чтения : 10 minutes

Вы знаете этот момент, когда ваш сайт на локальном хостинге выглядит нелепо, но после развёртывания в сети всё становится идеально? Это случилось с моим плагином Gradle JBake. Задача`serve`отображал квадратные кнопки, тёмный текст на чёрном фоне и странно отсутствующий CSS. однако`publishSite`генерировал тот же сайт, он, безупречен. Виновным не был ни CSS, ни браузер, ни кэш. Это была строка кода Kotlin — и плохая привычка с аргументами командной строки.

Сцена: четыре скриншота, два рендера

Это произошло после сбоя системы. Я сделал четыре скриншота, чтобы сравнить локальный рендер (./gradlew serve`на`localhost:8820) со встроенным рендером (`publishSite`на GitHub Pages). Два скриншота на страницу: верх и низ.

Локальный рендеринг

Верх страницы— Кнопки Project и Template sontмаленькие, прямоугольные, в стандартных цветах (синий и зелёный). Карточка Start Writing имеетсплошной чёрный фон, без рамки.

Rendu local - haut de page

Нижний колонтитул— Подзаголовок «Derniers articles et ressources» являетсяпочти нечитаемый, слишком темный на чёрном фоне. Даты под изображениями блога ("17 October 2013", etc.) являютсяотсутствующие.

Rendu local - bas de page

Онлайн-рендеринг

Верх страницы— те же кнопки являютсябольшие, овцы, яркий неоново-зеленый, текст в верхнем регистре. Карта имеет белый закруглённый блок с тенью.

Rendu en ligne - haut de page

Нижний колонтитул— Подзаголовок естьхорошо читаемый. Даты этовидимые. Карточки статей имеютокруглые углыи названия являютсяжирным.

Rendu en ligne - bas de page

Первый рефлекс: тема. Я использую динамическую систему темы (светлая, тёмная, 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`определяется следующим образом :

SiteManager.kt — La tâche serve (version buggy)
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 интерпретировал это как:

  1. -b→ разобрать флаг bake (логический), потреблённый

  2. /path/to/site→ становится позиционным аргументом 1 (источник)

  3. -s→ анализировать флаг "serve" (булево), сервер запущеннемедленно

  4. /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`в конце :

SiteManager.kt — La tâche serve (version corrigée)
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

Локальная отрисовка теперьпиксель-идентичныйв развернутом рендере.

Почему этот баг был коварным

Вы видите, почему было трудно поймать?

  1. Нет явной ошибки: JBake не падал. Он "работал", просто с неправильной папкой.

  2. Билд работал:`./gradlew bake`правильно генерировал файлы в`build/bake/`.

  3. Развертывание работало(Empty output)`publishSite`Он публиковал хороший контент онлайн.

  4. Только serve был сломан: задача разработки, которую мы постоянно используем для итерации.

  5. Привычка к -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, не браузер, а уж тем более не фреймворк. Часто именно этап непосредственно перед рендерингом обманывает.

Похожие статьи