Skip to content

Conversation

@kocheick
Copy link

@kocheick kocheick commented Aug 22, 2025

Overview

This PR adds support for injecting custom <body> elements (such as scripts, meta tags, and other HTML content) directly through the Kobweb application block configuration, enabling developers to add analytics, Bootstrap, and other scripts without modifying HTML templates.
Closes #299

What's Changed

  • Added AppBlock.body configuration block for defining custom <body> content in build.gradle.kts
  • Enhanced KobwebGenerateSiteIndexTask to process and inject custom body elements during site generation
  • Added serializeBodyContents utility for creating and serializing custom <body> content blocks
  • Updated IndexTemplate to support custom <body> element injection
  • Added comprehensive logging during index generation for better visibility of injected elements
  • Extended HtmlUtil with body content serialization capabilities

API Usage

Developers can now add custom body content in their build.gradle.kts:

kobweb {
    app {
        index {
            description.set("My awesome Kobweb app")
            
            // Add custom body elements using body.add {}
            body.add {
                script {
                    src = "https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"
                    async = true
                }
            }
            
            body.add {
                script {
                    src = "https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
                    attributes["integrity"] = "sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz"
                    attributes["crossorigin"] = "anonymous"
                }
            }
            
            body.add {
                script {
                    unsafe {
                        raw("""
                            // Custom analytics or other JavaScript
                            console.log('Custom body script loaded');
                        """.trimIndent())
                    }
                }
            }
        }
    }
}

Use Cases

  • Adding analytics scripts (Google Analytics, Meta Pixel, etc.)
  • Including Bootstrap or other CSS/JS frameworks that need body injection
  • Injecting custom scripts that need to be placed in the <body> tag
  • Adding meta tags or other HTML content that belongs in the document body
  • Supporting third-party integrations that require specific script placement

Implementation Details

  • File Changes: 5 files modified across the application plugin
  • Backward Compatibility: Fully backward compatible - no breaking changes
  • Build Integration: Seamlessly integrates with existing Kobweb build process
  • Template System: Leverages existing index template generation infrastructure
  • API Design: Uses body.add {} blocks for adding multiple body elements

Testing

  • ✅ Tested with Bootstrap integration
  • ✅ Tested with analytics script injection
  • ✅ Verified proper HTML serialization and template integration
  • ✅ Confirmed logging output during site generation
  • ✅ Validated backward compatibility with existing projects

Breaking Changes

None - this is a purely additive feature that maintains full backward compatibility.

body.add {
script {
src = "https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
attributes["integrity"] = "sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where did you get this sha from? Please document :)

Copy link
Author

@kocheick kocheick Aug 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's really fake/random data just to see what it looks like in the playground site

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Advice: When someone asks you a question on a code review, don't just think about is as a one-off question to answer. Think about it as a question that EVERY person who sees this code in the future might ask.

Therefore, when I get a question in a code review, I always ask myself if I should also add a comment, or change a variable or function name for clarity.

So in this case, that would look like:

body.add {
   script {
      src = "https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
      // Made up value, for testing:
      attributes["integrity"] = "sha-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
   }
}

Notice the added comment AND obviously fake sha value.

Of course, you don't always have to document a question; sometimes as a reviewer I might ask a dumb question because I was tired and not thinking straight, and you already feel like the code is obvious. However, I'd guess 80%+ I assume the question means something I did could be made clearer.

…AFTER_SCRIPT, END) during index generation
Copy link
Member

@bitspittle bitspittle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey I'm confused, what's going on here? I explicitly asked you NOT to add support for targeting different body locations but then you went ahead and added it. Please let me know how this miscommunication happened so I can be more careful next time. I don't want to waste your time.

Remember, adding more code just because we can is NOT an advantage. It's more code we have to maintain, more code we might potentially have to deprecate later if we made a mistake, and more area that bugs can slip in.

@kocheick
Copy link
Author

kocheick commented Aug 25, 2025

You're right, I'll be happy to switch back. I intended to do a fix which consists of exposing body {...} instead of body.add {...}, I ended up getting carried away and added those bodyTargets. I do believe the initial PR suffice enough until we get more requests/feedbacks from users

@bitspittle
Copy link
Member

Yes, please switch back. I don't want to add extra code that we don't even know if anyone needs.

All I wanted was a discussion to ensure that we weren't painting ourselves into a corner.

…obweb-block' into Allow-adding-body-scripts-in-a-kobweb-block

# Conflicts:
#	playground/site/build.gradle.kts
#	tools/gradle-plugins/application/src/main/kotlin/com/varabyte/kobweb/gradle/application/extensions/AppBlock.kt
… collections into a single `bodyElements` list and updating related processing logic
@kocheick
Copy link
Author

kocheick commented Sep 4, 2025

All should be good, feel free to review the overall changes and let me know if there are any updates needed

@bitspittle bitspittle force-pushed the dev branch 2 times, most recently from 5c34e2e to e726ac5 Compare September 11, 2025 18:07
Copy link
Member

@bitspittle bitspittle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I just noticed this PR misses communicating body blocks from library modules to the app module.

In other words, in playground, go into sitelib/build.gradle.kts, and type:

kobweb {
    library { 
        index {
            body.add { ... }
        }
    }
}

Next, check out LibraryMetadata.kt, which has this code:

@Serializable
class LibraryMetadata(val index: Index) {
    @Serializable
+   class Index(val headElements: String?)
}

Follow through what headElements is doing and add support for bodyElements as well.

buildTarget
src = basePath.prependTo(confInputs.script.substringAfterLast("/").prefixIfNot("/")),
scriptAttributes = indexBlock.scriptAttributes.get(),
bodyElements = bodyElements,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You had to add named parameters to everything because you put bodyElements out of order. Instead, just do this:

createIndexFile(
  confInputs.title,
  indexBlock.lang.get(),
  headElements.applyUrlInterceptors(basePath).also { result ->
    logger.lifecycle("Final <head> elements:")
    logger.lifecycle("```")
    result.elements.forEach { logger.lifecycle("  ${it.replace("\n", "")}") },
  bodyElements.also { result -> ... }
)

return this.finalize()
}


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Remove added dead space

*/
fun serializeBodyContents(block: BODY.() -> Unit): String =
createHTML(prettyPrint = false, xhtmlCompatible = true).bodyFragment(block)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super nit: Only one line of space between methods.

body.add {
script {
src = "https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
attributes["integrity"] = "sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Advice: When someone asks you a question on a code review, don't just think about is as a one-off question to answer. Think about it as a question that EVERY person who sees this code in the future might ask.

Therefore, when I get a question in a code review, I always ask myself if I should also add a comment, or change a variable or function name for clarity.

So in this case, that would look like:

body.add {
   script {
      src = "https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
      // Made up value, for testing:
      attributes["integrity"] = "sha-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
   }
}

Notice the added comment AND obviously fake sha value.

Of course, you don't always have to document a question; sometimes as a reviewer I might ask a dumb question because I was tired and not thinking straight, and you already feel like the code is obvious. However, I'd guess 80%+ I assume the question means something I did could be made clearer.

attributes["crossorigin"] = "anonymous"
}
}
body.add {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this second body test is unnecessary and we should just remove it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, I left some comments about the body script, but I'm thinking about deleting it. Because at this point, the code is fake but would require every Kobweb developer to develop bootstrap for no reason.

I think this feature is simple enough to just test it locally and then not check it in. At some point, I'll need to write actual unit tests for Kobweb sites, and that will be the right place to add fake body data like you are doing here.

/**
* A list of element builders to add to the `<body>` of the generated `index.html` file.
*
* Elements are added after the main application script, making them suitable for:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would just simplify this to:

Elements are added after the main application script. This positions them
at the end of the body block (where instructions usually tell you to add
them) and also ensures they run after the main script is already loaded.

You should normally use [ListProperty.add] ...

)

// Optional: Log body elements like we do for head ~ feel free to remove this if not needed
if (bodyElements.isNotEmpty()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic should be done in an also block like we do with headElements above.

lang.convention("en")

// Initialize body as empty list (no defaults like head has)
body.set(emptyList())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is necessary?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants