reading time: 10 minutes

You know that moment when your local site looks like nothing, but once deployed online everything is perfect? That’s what happened to me with my JBake Gradle plugin. The task`serve`displayed square buttons, dark text on a black background, and weirdly missing CSS. Yet`publishSite`generated the same site, and it was impeccable. The culprit was neither the CSS, nor the browser, nor the cache. It was a line of Kotlin code — and a bad habit with command-line arguments.

toc

[]

The scene: four screenshots, two renderings

It was after a system crash. I had taken four screenshots to compare the local rendering (./gradlew serve`on`localhost:8820) with the online rendering (`publishSite`on GitHub Pages). Two screenshots per page: the top and the bottom.

Local Rendering

Top of page— The Project and Template buttons aresmall, rectangular, with standard colors (blue and green). The Start Writing card has asolid black background, without a border.

Rendu local - haut de page

Bottom of page— The subtitle "Latest articles and resources" isalmost unreadable, too dark on a black background. The dates under the blog images ("17 October 2013", etc.) aremissing.

Rendu local - bas de page

Online Rendering

Top of page— The same buttons arelarge, oval, flashy neon green, uppercase text. The card has arounded white border with a drop shadow.

Rendu en ligne - haut de page

Bottom of page— The subtitle isclearly legible. The dates arevisible. The article cards haverounded cornersand the titles arebold.

Rendu en ligne - bas de page

First reflex: the theme. I use a dynamic theme system (light, dark, high-contrast) based on`localStorage`. Maybe the local store triggered a high-contrast theme? No. I had already writtenhttps://cheroliv.com/blog/2025/0098_preloading_css_variable_post.html[an entire article on theme preloading], I know how it works. And besides, even in high-contrast mode, the CSS structure (oval buttons, shadows) should have been there. It wasn’t.

Second reflex: browser cache. No, I had tested with clean profiles. The diff persisted.

Third reflex:`serve`was not serving the same files as`publishSite`.

The diagnostic: serve was not pointing to the right folder

My Gradle plugin`bakery`has two main tasks:

  • bake: generates the static site in`build/bake/`

  • serve: launches a local web server

  • publishSite: pushes`build/bake/`to GitHub Pages

The divergence could only come from`serve`. Si publishSite`published the correct content, then`build/bake/`was correct. If`serve`displayed something else, it meant it wasn’t serving`build/bake/.

Let’s look at the code. In`SiteManager.kt`, the task`serve`is defined as follows:

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`is the entry point of the JBake 2.7.0 CLI. The idea: pass-b`for the source folder and`-s`for the destination folder, then launch the server mode. Except that…​

The root cause: JBake CLI doesn’t work that way

JBake 2.7.0 expectspositional argumentsfor source and destination, then optional flags. The signature is:

jbake <source> <destination> [options]

Or in my code, I wrote:

args = listOf("-b", "/path/to/site", "-s", "/path/to/build/bake")

JBake interpreted this as:

  1. -b→ parses the "bake" flag (boolean), consumed

  2. /path/to/site→ becomes positional argument 1 (source)

  3. -s→ parses the "serve" flag (boolean), server launchedimmediately

  4. /path/to/build/bake→ becomes an orphan argument, ignored or misinterpreted

Result: JBake took`/path/to/site`as the source,ignored the provided destination(build/bake), and used its default directory (often`./output`or a temporary copy). The`css/styles.css`file from the build was therefore overwritten or ignored, and an old default CSS (without custom variables, without rounded corners, without shadows) was served.

In comparison,bake et publishSite`use theofficial JBake Gradle plugin(`jbake-gradle-plugin) which writes properly into`build/bake/`via the Gradle API. They do not go through the CLI.

The solution: positional arguments, not flags

The correction is trivial — once you know it. Simply pass the source and destination as positional arguments, then`-s`last:

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"
)

That’s it. Three lines changed, bug resolved.

After local publication of the plugin (publishToMavenLocal), un `./gradlew serve`launches JBake with the correct syntax:

jbake /home/user/project/site /home/user/project/build/bake -s

And this time, the Jetty server integrated into JBake correctly serves the content of`build/bake/`— identical to what is published online.

serve vs publish diagram

Quick verification with curl

To ensure the server is serving the correct content, a simple`curl`confirms that`index.html`contains`data-bs-theme="light"`and that`build/bake/css/styles.css`weighs exactly 31,402 bytes — identical to the file generated by`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

The local rendering is nowpixel-identicalto the deployed rendering.

Why this bug was vicious

See why it was hard to catch?

  1. No explicit error: JBake didn’t crash. It "worked", just with the wrong folder.

  2. The build worked:`./gradlew bake`correctly generated files in`build/bake/`.

  3. The deployment worked:`publishSite`pushed the correct content online.

  4. Only serve was broken: the development task, the one used constantly for iterating.

  5. The -key value habit: as a developer, we are conditioned by years of Unix CLI (-o output, -i input). JBake CLI is an exception — source and destination are positional.

Conclusion

If you maintain a Gradle plugin that wraps JBake (or any CLI tool),read the argument docseven if you think you know them. An implicit assumption (-s= "set destination") can cost you hours of visual debugging.

Here, the fix was literally to change:

args = listOf("-b", src, "-s", dest)   // ❌ BUG

en :

args = listOf(src, dest, "-s")         // ✅ FIX

Three tokens moved, and my local site is as beautiful as the production site again.

Lesson: when the rendering differs between local and prod for no obvious reason, first suspect the pipeline — not the CSS, not the browser, and even less the framework. It’s often the step just before rendering that cheats.

Related articles