Introduction
Thymeleaf has a concept called natural templates. As mentioned in the official docs, this includes JavaScript natural templates and CSS natural templates.
Here’s an example from the documentation:
<script th:inline="javascript">
var username = /*[[${session.user.name}]]*/ "Gertrud Kiwifruit";
</script>
<style th:inline="css">
.main\ elems {
text-align: /*[[${align}]]*/ left;
}
</style>
As you can see, natural templates are implemented using comments. But when you use Vite and treat the template HTML as an entry point (by specifying it in vite.config.ts
via build.rollupOptions.input
)—so you can apply things like Tailwind CSS class name mangling, tree-shaking, minification, and CSS splitting—you’ll run into the problem of Vite breaking Thymeleaf’s natural templates.
To deal with this, here are some practical solutions.
The Solutions
We’ll group the inline blocks we want to skip into three categories:
- Inline
<script>
withouttype="module"
. - Inline
<script>
withtype="module"
. - Inline
<style>
.
Inline <script>
Without type="module"
According to the Vite docs, <script src>
isn’t processed by Vite, so you don’t need to do anything special here:
<script th:inline="javascript">
var username = /*[[${session.user.name}]]*/ "Gertrud Kiwifruit";
</script>
Inline <script>
With type="module"
From the Vite docs, <script type="module" src>
is processed. But there’s a built-in way to skip it: just add the vite-ignore
attribute.
Example:
<script vite-ignore type="module" th:inline="javascript">
var username = /*[[${session.user.name}]]*/ "Gertrud Kiwifruit";
</script>
Inline <style>
Vite doesn’t provide a way to skip inline <style>
blocks. I experimented with writing a plugin that runs as early as possible, but the contents still ended up minified.
Luckily, Thymeleaf has something called prototype-only comment blocks:
<span>hello!</span>
<!--/*/
<div th:text="${...}">
...
</div>
/*/-->
<span>goodbye!</span>
These blocks are treated as comments in static mode, but when Thymeleaf processes the template, the markers <!--/*/
and /*/-->
are stripped and the contents are preserved as real markup.
That means we can wrap our inline <style>
with these markers, and Vite will leave it alone (as long as you’re not running an HTML minifier):
<!--/*/
<style th:inline="css">
.main\ elems {
text-align: /*[[${align}]]*/ left;
}
</style>
/*/-->
If you are using an HTML minifier, you might need to write a custom Vite plugin to prevent it from messing with these special comments. Here’s a plugin example (not guaranteed, but works as a reference):
Vite Plugin to Handle HTML Minify
Place the plugin in ./plugins/vite-plugin-html-ignore-block.ts
, then register it in your vite.config.ts
:
import vitePluginHtmlIgnoreBlock from './plugins/vite-plugin-html-ignore-block';
export default defineConfig({
// ...other config
plugins: [
vitePluginHtmlIgnoreBlock(),
// ...other plugins
],
});
Plugin code:
import type { Plugin } from 'vite';
export default function vitePluginHtmlIgnoreBlock(): Plugin[] {
const rawMap = new Map<string, string>();
let idx = 0;
// Match <!--/*/ ... /*/-->, supports multiline
const blockReg = /<!--\/\*\/([\s\S]*?)\/\*\/-->/g;
// Pre stage: replace with <ignore-N></ignore-N>
const pre: Plugin = {
name: 'vite-plugin-html-ignore-block-pre',
enforce: 'pre',
transformIndexHtml: {
order: 'pre',
handler(html) {
return html.replace(blockReg, (match) => {
const tag = `ignore-${idx}`;
rawMap.set(tag, match);
idx++;
return `<${tag}></${tag}>`;
});
}
}
};
// Post stage: restore original content
const post: Plugin = {
name: 'vite-plugin-html-ignore-block-post',
enforce: 'post',
transformIndexHtml: {
order: 'post',
handler(html) {
let result = html;
for (const [tag, raw] of rawMap.entries()) {
const reg = new RegExp(`<${tag}></${tag}>`, 'g');
result = result.replace(reg, raw);
}
return result;
}
}
};
return [pre, post];
}
Final Notes
That’s it! Hopefully these approaches save you some frustration when combining Vite with Thymeleaf natural templates.
Feel free to drop a comment if you’ve got improvements or run into edge cases. And of course—happy coding!