<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Thomas Schilling | Spring/HTMX/Claude Code]]></title><description><![CDATA[Youngest Speaker @Spring I/O &amp; Spring ViewComponent creator.

Passionate about building awesome software with Spring + HTMX.
Pushing full-stack development ]]></description><link>https://tschuehly.de</link><generator>RSS for Node</generator><lastBuildDate>Wed, 22 Apr 2026 12:06:50 GMT</lastBuildDate><atom:link href="https://tschuehly.de/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Svelte 5 Islands in Spring Boot with Kotlin: The Preact Alternative]]></title><description><![CDATA[Svelte 5 Islands in Spring Boot with Kotlin: The Preact Alternative
In my previous post I
showed how to use Preact Islands to handle the 5% of interactions where Alpine.js falls apart. Preact works gr]]></description><link>https://tschuehly.de/svelte-islands-spring-boot</link><guid isPermaLink="true">https://tschuehly.de/svelte-islands-spring-boot</guid><category><![CDATA[frontend]]></category><category><![CDATA[htmx]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[Spring]]></category><category><![CDATA[spring-boot]]></category><category><![CDATA[Svelte]]></category><dc:creator><![CDATA[Thomas Schilling]]></dc:creator><pubDate>Wed, 25 Feb 2026 19:17:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/65c1ed52dd6f7b6c0c5bb941/a7b1df97-bb8f-4de3-9dba-557919fb0b60.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Svelte 5 Islands in Spring Boot with Kotlin: The Preact Alternative</h1>
<p>In my <a href="https://tschuehly.de/preact-islands-in-spring-boot-with-htmx-when-alpinejs-isnt-enough-anymore">previous post</a> I
showed how to use <strong>Preact Islands</strong> to handle the 5% of interactions where Alpine.js falls apart. Preact works great,
but if you've ever looked at Svelte and thought "that looks cleaner" — you're right.</p>
<p>Svelte 5 with runes (<code>\(state</code>, <code>\)props</code>, <code>$derived</code>) is a natural fit for islands. No <code>useSignal</code>import,
no re-exporting <code>render</code> and <code>h</code>. The component syntax is just cleaner. And with Bun's plugin system, we compile
<code>.svelte</code> files the same way we compiled <code>.tsx</code> — zero webpack, zero SPA router.</p>
<p>I migrated <a href="https://photoquest.wedding/">PhotoQuest</a> from Preact to Svelte islands, and in this post I'll show you how
to set up the same <strong>Kanban board</strong> example from the Preact article, but with <strong>Svelte 5 + Kotlin</strong>.</p>
<p>If you haven't read the Preact post, don't worry. This article is self-contained. But
the <a href="https://tschuehly.de/preact-islands-in-spring-boot-with-htmx-when-alpinejs-isnt-enough-anymore">Preact version</a> is useful for comparison.</p>
<h1>What Changes from Preact?</h1>
<p>The architecture stays the same. Server renders HTML, passes JSON props, a mount script hydrates the island. What
changes:</p>
<table>
<thead>
<tr>
<th>Preact</th>
<th>Svelte 5</th>
</tr>
</thead>
<tbody><tr>
<td><code>.tsx</code> files</td>
<td><code>.svelte</code> files</td>
</tr>
<tr>
<td><code>useSignal(value)</code></td>
<td><code>let x = $state(value)</code></td>
</tr>
<tr>
<td><code>signal&lt;T&gt;(value)</code> (module-level)</td>
<td>Module-level <code>$state</code> in <code>&lt;script module&gt;</code></td>
</tr>
<tr>
<td>Re-export <code>render, h</code> from preact</td>
<td>Re-export <code>mount</code> from svelte via <code>&lt;script module&gt;</code></td>
</tr>
<tr>
<td><code>render(h(Component, props), el)</code></td>
<td><code>mount(Component, { target: el, props })</code></td>
</tr>
<tr>
<td>Bun builds TSX natively</td>
<td>Bun plugin compiles <code>.svelte</code> via <code>svelte/compiler</code></td>
</tr>
<tr>
<td>Java records for props</td>
<td>Kotlin data classes for props</td>
</tr>
<tr>
<td>Manually maintained <code>.d.ts</code> types</td>
<td>Auto-generated <code>.ts</code> from <code>@Serializable</code> classes</td>
</tr>
</tbody></table>
<h1>Architecture Overview</h1>
<pre><code>src/main/kotlin/de/tschuehly/svelte_islands_demo/web/kanban/
    KanbanComponent.kt                # Server-side ViewComponent + Controller
    KanbanComponent.html              # Thymeleaf template (thin shell)
    KanbanIsland.svelte               # Svelte 5 island (all client logic)
    KanbanIslandProps.ts              # TypeScript interface (auto-generated)
    KanbanService.kt                  # Board data service

src/main/kotlin/de/tschuehly/svelte_islands_demo/common/
    IslandTypeGenerator.kt            # Generates .ts from @Serializable classes

frontend/
    build.ts                         # Bun build script (with Svelte plugin)
    watch.ts                         # Dev file watcher
    bunfig.toml                      # Bun config (no JSX needed)
    tsconfig.json                    # TypeScript config

src/main/resources/static/js/
    mount.js                         # Island mount script (12 lines)
    islands/                         # Build output (gitignored)
        kanban/KanbanIsland.js
</code></pre>
<p>Same colocation principle: the <code>.svelte</code> file lives next to the ViewComponent it belongs to.</p>
<h1>Setting Up Svelte Islands</h1>
<h2>Install Dependencies</h2>
<pre><code class="language-json">// package.json
{
  "dependencies": {
    "svelte": "^5.50.2"
  }
}
</code></pre>
<p>That's it. No <code>@preact/signals</code>, no JSX import source. Just Svelte.</p>
<h2>Configure Bun</h2>
<p>Since we're not using JSX anymore, the config is minimal:</p>
<pre><code class="language-json">// frontend/tsconfig.json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "commonjs"
  }
}
</code></pre>
<p>No <code>jsx</code>, no <code>jsxImportSource</code>, no <code>paths</code> for module resolution. Svelte handles its own compilation.</p>
<h2>The Build Script (with Svelte Plugin)</h2>
<p>This is where the magic happens. Bun doesn't understand <code>.svelte</code> files natively, so we add a <strong>plugin</strong> that compiles
them via <code>svelte/compiler</code>:</p>
<pre><code class="language-typescript">// frontend/build.ts
import {Glob} from "bun";
import {compile} from "svelte/compiler";
import type {BunPlugin} from "bun";

const sourceRoot = "../src/main/kotlin/de/tschuehly/svelte_islands_demo/web/";
const scanRoot = "../src/main/kotlin";

const sveltePlugin: BunPlugin = {
  name: "svelte",
  setup(build) {
    build.onLoad({filter: /\.svelte$/}, async (args) =&gt; {
      const source = await Bun.file(args.path).text();
      const result = compile(source, {
        filename: args.path,
        generate: "client",
        css: "injected",
      });
      return {
        contents: result.js.code,
        loader: "js",
      };
    });
  },
};

const glob = new Glob("**/*.svelte");
const entrypoints: string[] = [];

for await (const file of glob.scan(scanRoot)) {
  entrypoints.push(`\({scanRoot}/\){file}`);
}

console.log(`Found ${entrypoints.length} island(s):`);
entrypoints.forEach((e) =&gt; console.log(`  ${e}`));

await Bun.build({
  entrypoints,
  outdir: "../src/main/resources/static/js/islands",
  root: sourceRoot,
  minify: true,
  naming: "[dir]/[name].js",
  plugins: [sveltePlugin],
});

console.log("Build complete.");
</code></pre>
<p>In detail:</p>
<ul>
<li><code>sveltePlugin</code> -&gt; intercepts every <code>.svelte</code> file import and compiles it to JavaScript using <code>svelte/compiler</code></li>
<li><code>generate: "client"</code> -&gt; generates browser-ready code (not SSR)</li>
<li><code>css: "injected"</code> -&gt; Svelte injects scoped CSS directly into the DOM at runtime. No separate CSS files needed</li>
<li>The rest is identical to the Preact build: scan for entrypoints, output to <code>islands/</code>, mirror the package structure</li>
</ul>
<h2>The Mount Script</h2>
<p>The mount script changes slightly. Instead of Preact's <code>render(h())</code>, we use Svelte's <code>mount()</code>:</p>
<pre><code class="language-javascript">// src/main/resources/static/js/mount.js
document.querySelectorAll("[data-island]").forEach(async (el) =&gt; {
  const name = el.dataset.island;

  try {
    const props = JSON.parse(el.dataset.props || "{}");
    const {default: Component, mount} = await import(`/js/islands/${name}.js`);
    el.innerHTML = "";
    mount(Component, {target: el, props});
  } catch (err) {
    console.error(`Failed to mount island "${name}":`, err);
    el.innerHTML = '&lt;div class="alert alert-error"&gt;Component could not be loaded.&lt;/div&gt;';
  }
});
</code></pre>
<p><strong>Important:</strong> Each Svelte island must re-export <code>mount</code> from svelte in a <code>&lt;script module&gt;</code> block:</p>
<pre><code class="language-svelte">&lt;script module&gt;
  export {mount} from 'svelte';
&lt;/script&gt;
</code></pre>
<p>Why <code>&lt;script module&gt;</code>? This is Svelte 5's way of exporting things at the module level — code that runs once per import,
not once per component instance. The <code>mount</code> function from Svelte is what creates and attaches a component to a DOM
element. Since <code>mount.js</code> is a plain browser module that can't resolve bare <code>'svelte'</code> imports, the island bundle must
include it.</p>
<h2>Include mount.js in Your Layout</h2>
<pre><code class="language-html">&lt;!-- LayoutViewComponent.html --&gt;
&lt;script th:src="@{/js/mount.js}" type="module"&gt;&lt;/script&gt;
</code></pre>
<p>Same as the Preact version.</p>
<h1>Writing the Svelte Island</h1>
<p>Here's the kanban board as a Svelte 5 component. Compare this to the Preact version — no <code>useSignal</code>, no <code>h()</code>, no
explicit re-renders:</p>
<pre><code class="language-svelte">&lt;!-- src/main/kotlin/de/tschuehly/svelte_islands_demo/web/kanban/KanbanIsland.svelte --&gt;
&lt;script module&gt;
  export {mount} from 'svelte';
&lt;/script&gt;

&lt;script lang="ts"&gt;
  import type {KanbanIslandProps, KanbanCard, KanbanColumn} from './KanbanIslandProps.ts';

  let {columns: initialColumns}: KanbanIslandProps = $props();

  let columns: KanbanColumn[] = $state(
      initialColumns.map(col =&gt; ({...col, cards: [...col.cards]}))
  );
  let nextId = $state(
      Math.max(...initialColumns.flatMap(c =&gt; c.cards.map(card =&gt; card.id)), 0) + 1
  );

  // Drag state
  let dragCardId: number | null = $state(null);
  let dragFromColumnId: string | null = $state(null);

  // Edit state — which card is being edited
  let editingCardId: number | null = $state(null);
  let editTitle = $state('');
  let editPriority = $state('medium');
  let editAssignee = $state('');

  // Add state — which column has the add form open
  let addingColumnId: string | null = $state(null);
  let newTitle = $state('');
  let newPriority = $state('medium');
  let newAssignee = $state('');

  function moveCard(cardId: number, fromColId: string, toColId: string) {
    const card = columns.find(c =&gt; c.id === fromColId)!.cards.find(c =&gt; c.id === cardId)!;
    columns = columns.map(col =&gt; {
      if (col.id === fromColId) return {...col, cards: col.cards.filter(c =&gt; c.id !== cardId)};
      if (col.id === toColId) return {...col, cards: [...col.cards, card]};
      return col;
    });
  }

  function deleteCard(colId: string, cardId: number) {
    columns = columns.map(col =&gt;
        col.id === colId ? {...col, cards: col.cards.filter(c =&gt; c.id !== cardId)} : col
    );
  }

  function updateCard(colId: string, cardId: number, data: Partial&lt;KanbanCard&gt;) {
    columns = columns.map(col =&gt;
        col.id === colId
            ? {...col, cards: col.cards.map(c =&gt; c.id === cardId ? {...c, ...data} : c)}
            : col
    );
  }

  function addCard(colId: string, data: Omit&lt;KanbanCard, 'id'&gt;) {
    const id = nextId++;
    columns = columns.map(col =&gt;
        col.id === colId ? {...col, cards: [...col.cards, {id, ...data}]} : col
    );
  }

  function startEdit(card: KanbanCard) {
    editingCardId = card.id;
    editTitle = card.title;
    editPriority = card.priority;
    editAssignee = card.assignee;
  }

  function saveEdit(colId: string) {
    if (editingCardId == null) return;
    updateCard(colId, editingCardId, {
      title: editTitle,
      priority: editPriority,
      assignee: editAssignee
    });
    editingCardId = null;
  }

  function startAdd(colId: string) {
    addingColumnId = colId;
    newTitle = '';
    newPriority = 'medium';
    newAssignee = '';
  }

  function saveAdd() {
    if (addingColumnId == null) return;
    addCard(addingColumnId, {
      title: newTitle,
      priority: newPriority,
      assignee: newAssignee
    });
    addingColumnId = null;
  }
&lt;/script&gt;

&lt;div class="flex gap-4 overflow-x-auto p-4"&gt;
  {#each columns as column (column.id)}
    &lt;div
        class="w-72 rounded-lg p-3 bg-base-200"
        ondragover={(e) =&gt; { e.preventDefault(); }}
        ondrop={() =&gt; {
          if (dragCardId &amp;&amp; dragFromColumnId !== column.id) {
            moveCard(dragCardId, dragFromColumnId, column.id);
          }
        }}
    &gt;
      &lt;div class="flex justify-between items-center mb-3"&gt;
        &lt;h3 class="font-bold"&gt;{column.title}&lt;/h3&gt;
        &lt;span class="badge badge-ghost badge-sm"&gt;{column.cards.length}&lt;/span&gt;
      &lt;/div&gt;

      {#each column.cards as card (card.id)}
        &lt;div
            class="bg-base-100 rounded-lg p-3 mb-2 shadow-sm cursor-grab"
            draggable="true"
            ondragstart={() =&gt; { dragCardId = card.id; dragFromColumnId = column.id; }}
            ondragend={() =&gt; { dragCardId = null; dragFromColumnId = null; }}
        &gt;
          {#if editingCardId === card.id}
            &lt;div class="space-y-2"&gt;
              &lt;input class="input input-sm w-full" bind:value={editTitle} /&gt;
              &lt;select class="select select-sm w-full" bind:value={editPriority}&gt;
                &lt;option value="low"&gt;Low&lt;/option&gt;
                &lt;option value="medium"&gt;Medium&lt;/option&gt;
                &lt;option value="high"&gt;High&lt;/option&gt;
              &lt;/select&gt;
              &lt;input class="input input-sm w-full" bind:value={editAssignee}
                     placeholder="Assignee" /&gt;
              &lt;div class="flex gap-1"&gt;
                &lt;button class="btn btn-xs btn-primary" onclick={() =&gt; saveEdit(column.id)}&gt;
                  Save
                &lt;/button&gt;
                &lt;button class="btn btn-xs" onclick={() =&gt; { editingCardId = null; }}&gt;
                  Cancel
                &lt;/button&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          {:else}
            &lt;div&gt;
              &lt;div class="flex justify-between items-start"&gt;
                &lt;span class="font-medium"&gt;{card.title}&lt;/span&gt;
                &lt;span class="badge badge-sm {
                  card.priority === 'high' ? 'badge-error' :
                  card.priority === 'medium' ? 'badge-warning' : 'badge-info'
                }"&gt;{card.priority}&lt;/span&gt;
              &lt;/div&gt;
              &lt;div class="text-sm text-base-content/60 mt-1"&gt;{card.assignee}&lt;/div&gt;
              &lt;div class="flex gap-1 mt-2"&gt;
                &lt;button class="btn btn-xs" onclick={() =&gt; startEdit(card)}&gt;Edit&lt;/button&gt;
                &lt;button class="btn btn-xs btn-error"
                        onclick={() =&gt; deleteCard(column.id, card.id)}&gt;Delete&lt;/button&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          {/if}
        &lt;/div&gt;
      {/each}

      {#if addingColumnId === column.id}
        &lt;div class="bg-base-100 rounded-lg p-3 mb-2 space-y-2"&gt;
          &lt;input class="input input-sm w-full" bind:value={newTitle}
                 placeholder="Card title" /&gt;
          &lt;select class="select select-sm w-full" bind:value={newPriority}&gt;
            &lt;option value="low"&gt;Low&lt;/option&gt;
            &lt;option value="medium"&gt;Medium&lt;/option&gt;
            &lt;option value="high"&gt;High&lt;/option&gt;
          &lt;/select&gt;
          &lt;input class="input input-sm w-full" bind:value={newAssignee}
                 placeholder="Assignee" /&gt;
          &lt;div class="flex gap-1"&gt;
            &lt;button class="btn btn-xs btn-primary" onclick={saveAdd}&gt;Add&lt;/button&gt;
            &lt;button class="btn btn-xs" onclick={() =&gt; { addingColumnId = null; }}&gt;
              Cancel
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      {:else}
        &lt;button class="btn btn-sm btn-ghost w-full mt-2"
                onclick={() =&gt; startAdd(column.id)}&gt;+ Add Card&lt;/button&gt;
      {/if}
    &lt;/div&gt;
  {/each}
&lt;/div&gt;
</code></pre>
<p>Compare this to the Preact version:</p>
<ul>
<li><strong><code>$state()</code> instead of <code>useSignal()</code></strong> -&gt; same reactivity, but it looks like regular JavaScript. No <code>.value</code> on read,
Svelte's compiler handles that.</li>
<li><strong><code>$props()</code> instead of function parameters</strong> -&gt; destructured directly, type-safe via the imported interface</li>
<li><strong><code>{#each}</code> with keyed blocks</strong> -&gt; Svelte's template syntax replaces <code>.map()</code> with JSX</li>
<li><strong><code>bind:value</code></strong> -&gt; two-way binding built into the language. No <code>onInput</code> handlers needed.</li>
<li><strong><code>&lt;script module&gt;</code></strong> -&gt; exports <code>mount</code> once at module level, no need to re-export <code>render</code> and <code>h</code></li>
<li><strong><code>css: "injected"</code></strong> -&gt; if you add <code>&lt;style&gt;</code> blocks, Svelte scopes them to the component automatically</li>
<li><strong>UI state as <code>$state</code> variables</strong> -&gt; editing and adding state tracked at component level (<code>editingCardId</code>,
<code>addingColumnId</code>), with helper functions like <code>startEdit()</code> and <code>saveEdit()</code> to manage transitions</li>
</ul>
<h1>Wiring the Server Side (Kotlin)</h1>
<h2>The ViewComponent</h2>
<p>Same pattern as the Java version, but with Kotlin data classes and Spring ViewComponent:</p>
<pre><code class="language-kotlin">// KanbanComponent.kt
@ViewComponent
@Controller
class KanbanComponent(
    private val objectMapper: ObjectMapper,
    private val kanbanService: KanbanService,
    private val layoutViewComponent: LayoutViewComponent
) {

    @Serializable
    data class KanbanCard(val id: Int, val title: String, val priority: String, val assignee: String)

    @Serializable
    data class KanbanColumn(val id: String, val title: String, val cards: List&lt;KanbanCard&gt;)

    @Serializable
    data class KanbanIslandProps(val columns: List&lt;KanbanColumn&gt;)

    data class KanbanView(val islandPropsJson: String) : ViewContext

    @GetMapping("/")
    fun index(): ViewContext = layoutViewComponent.render(render())

    fun render(): KanbanView {
        val columns = kanbanService.getColumns()
        val islandProps = KanbanIslandProps(columns)
        return KanbanView(objectMapper.writeValueAsString(islandProps))
    }
}
</code></pre>
<p>In detail:</p>
<ul>
<li>Kotlin data classes serialize to JSON with Jackson out of the box — same as Java records but with less boilerplate</li>
<li><code>@Serializable</code> -&gt; marks the class for <code>kotlinx.serialization</code>. This is what the <code>IslandTypeGenerator</code> uses to produce TypeScript interfaces automatically</li>
<li><code>KanbanCard</code> and <code>KanbanColumn</code> are the <strong>single source of truth</strong> — the TypeScript types are generated from them, not manually maintained</li>
<li>The <code>objectMapper.writeValueAsString()</code> call turns the props into JSON that gets injected into <code>data-props</code></li>
</ul>
<h2>The KanbanService</h2>
<pre><code class="language-kotlin">// KanbanService.kt
@Service
class KanbanService {

    fun getColumns(): List&lt;KanbanComponent.KanbanColumn&gt; = listOf(
        KanbanComponent.KanbanColumn(
            "todo", "To Do", listOf(
                KanbanComponent.KanbanCard(1, "Design landing page", "high", "Alice"),
                KanbanComponent.KanbanCard(2, "Write API docs", "medium", "Bob"),
            )
        ),
        KanbanComponent.KanbanColumn(
            "progress", "In Progress", listOf(
                KanbanComponent.KanbanCard(3, "Auth flow", "high", "Charlie"),
            )
        ),
        KanbanComponent.KanbanColumn("done", "Done", listOf()),
    )
}
</code></pre>
<h2>The Thymeleaf Template</h2>
<p>Identical to the Preact version. The template doesn't care what framework the island uses:</p>
<pre><code class="language-html">&lt;!-- KanbanComponent.html --&gt;
&lt;!--/*@thymesVar id="kanbanView" type="de.tschuehly.svelte_islands_demo.web.kanban.KanbanComponent.KanbanView"*/--&gt;

&lt;div class="px-6 py-4"&gt;
  &lt;!-- Svelte Island mounts here --&gt;
  &lt;div data-island="kanban/KanbanIsland"
       th:data-props="${kanbanView.islandPropsJson}"&gt;
    &lt;span class="loading loading-spinner loading-lg"&gt;&lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>
<h1>Type-Safe Island Props (Auto-Generated)</h1>
<p>In the Preact version, we manually maintained a <code>.d.ts</code> file that mirrored the Java records. That works, but it's one
more thing that can drift out of sync. With Kotlin, we can do better.</p>
<p>The <code>IslandTypeGenerator</code> scans for any class ending in <code>IslandProps</code> that's annotated with <code>@Serializable</code>, and
generates the TypeScript interface automatically using <a href="https://github.com/nicholasgasior/kxs-ts-gen">kxs-ts-gen</a>:</p>
<pre><code class="language-kotlin">// IslandTypeGenerator.kt
package de.tschuehly.svelte_islands_demo.common

import dev.adamko.kxstsgen.KxsTsGenerator
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.serializer
import java.io.File

@OptIn(InternalSerializationApi::class)
fun main() {
    val tsGenerator = KxsTsGenerator()
    val classesDir = File("build/classes/kotlin/main")
    val basePackage = "de/tschuehly/svelte_islands_demo"

    val islandPropsFiles = classesDir.resolve(basePackage)
        .walk()
        .filter { it.isFile &amp;&amp; it.name.endsWith("IslandProps.class") }
        .toList()

    var count = 0
    for (classFile in islandPropsFiles) {
        val className = classFile.relativeTo(classesDir).path
            .removeSuffix(".class")
            .replace(File.separatorChar, '.')
        val clazz = Class.forName(className)
        val serializer = clazz.kotlin.serializer()
        val tsContent = tsGenerator.generate(serializer)

        val packagePath = clazz.`package`.name.replace('.', '/')
        val outputFile = File("src/main/kotlin/\(packagePath/\){clazz.simpleName}.ts")
        outputFile.writeText(buildString {
            appendLine("// Auto-generated by IslandTypeGenerator — do not edit")
            appendLine()
            appendLine(tsContent)
        })
        println("Generated ${outputFile.path}")
        count++
    }
    println("Generated $count island type definition(s)")
}
</code></pre>
<p>In detail:</p>
<ul>
<li><code>KxsTsGenerator</code> -&gt; converts <code>kotlinx.serialization</code> descriptors to TypeScript interfaces</li>
<li>The walker scans compiled classes for anything ending in <code>IslandProps.class</code> — this also matches inner classes like
<code>KanbanComponent$KanbanIslandProps.class</code>, which is exactly what we want since the props are nested inside the
ViewComponent</li>
<li><code>clazz.kotlin.serializer()</code> -&gt; gets the serializer that <code>@Serializable</code> generated at compile time</li>
<li>The output <code>.ts</code> file is written <strong>next to the Kotlin source</strong> — same colocation principle as the <code>.svelte</code> files</li>
<li>The <code>// Auto-generated</code> header makes it clear this file shouldn't be hand-edited</li>
</ul>
<p>Running <code>./gradlew generateIslandTypes</code> produces this file:</p>
<pre><code class="language-typescript">// KanbanIslandProps.ts
// Auto-generated by IslandTypeGenerator — do not edit

export interface KanbanCard {
  id: number;
  title: string;
  priority: string;
  assignee: string;
}

export interface KanbanColumn {
  id: string;
  title: string;
  cards: KanbanCard[];
}

export interface KanbanIslandProps {
  columns: KanbanColumn[];
}
</code></pre>
<p>No manual syncing. Add a field to the Kotlin data class, run the build, and the TypeScript interface updates
automatically. The Svelte component imports it the same way:</p>
<pre><code class="language-svelte">&lt;script lang="ts"&gt;
  import type {KanbanIslandProps, KanbanCard, KanbanColumn} from './KanbanIslandProps.ts';
&lt;/script&gt;
</code></pre>
<p>You need two dependencies in your <code>build.gradle.kts</code> for this to work:</p>
<pre><code class="language-kotlin">// build.gradle.kts (dependencies block)
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3")
implementation("dev.adamko.kxstsgen:kxs-ts-gen-core:0.2.1")
</code></pre>
<p>And the <code>kotlinx.serialization</code> Gradle plugin:</p>
<pre><code class="language-kotlin">// build.gradle.kts (plugins block)
plugins {
    kotlin("plugin.serialization") version "2.1.0"
}
</code></pre>
<h1>Gradle Integration</h1>
<p>The Gradle tasks are nearly identical to the Preact version — swap <code>.tsx</code> for <code>.svelte</code> and add the type generation
step:</p>
<pre><code class="language-kotlin">// build.gradle.kts
val bunBinary: String = System.getenv("BUN_INSTALL")?.let { "$it/bin/bun" }
    ?: "${System.getProperty("user.home")}/.bun/bin/bun"

tasks.register&lt;Exec&gt;("bunInstall") {
    commandLine(bunBinary, "install")
}

tasks.register&lt;JavaExec&gt;("generateIslandTypes") {
    dependsOn("compileKotlin")
    mainClass.set("de.tschuehly.svelte_islands_demo.common.IslandTypeGeneratorKt")
    classpath = files(
        tasks.named("compileKotlin").map { it.outputs.files },
        sourceSets["main"].compileClasspath
    )
}

tasks.register&lt;Exec&gt;("bunBuild") {
    dependsOn("bunInstall", "generateIslandTypes")
    workingDir = file("frontend")
    commandLine(bunBinary, "run", "build.ts")
    inputs.files(fileTree("src/main/kotlin").matching { include("**/*.svelte") })
    inputs.file("frontend/build.ts")
    outputs.dir("src/main/resources/static/js/islands")
}

tasks.register&lt;Exec&gt;("bunWatch") {
    dependsOn("bunInstall")
    workingDir = file("frontend")
    commandLine(bunBinary, "run", "watch.ts")
}

tasks.named&lt;ProcessResources&gt;("processResources") {
    dependsOn("bunBuild")
}
</code></pre>
<p>In detail:</p>
<ul>
<li><code>generateIslandTypes</code> -&gt; compiles Kotlin first, then runs the <code>IslandTypeGenerator</code> against the compiled classes.
This generates the <code>.ts</code> files before Bun tries to build the islands</li>
<li><code>bunBuild</code> depends on both <code>bunInstall</code> and <code>generateIslandTypes</code> -&gt; so the TypeScript interfaces are always
up-to-date when Bun compiles the <code>.svelte</code> files</li>
<li>The <code>inputs.files</code> pattern uses <code>**/*.svelte</code> so Gradle knows when to re-run the build</li>
</ul>
<h1>Live Reload in Development</h1>
<h2>The Watch Script</h2>
<p>Same approach as Preact, watching for <code>.svelte</code> changes:</p>
<pre><code class="language-typescript">// frontend/watch.ts
import {watch} from "fs";
import {resolve} from "path";

const scanRoot = resolve(import.meta.dir, "../src/main/kotlin");

async function build() {
  const proc = Bun.spawn(["bun", "run", "build.ts"], {
    cwd: import.meta.dir,
    stdout: "inherit",
    stderr: "inherit",
  });
  await proc.exited;
}

await build();

console.log(`Watching ${scanRoot} for .svelte changes...`);

let debounce: Timer | null = null;
watch(scanRoot, {recursive: true}, (event, filename) =&gt; {
  if (!filename?.endsWith(".svelte")) return;
  if (debounce) clearTimeout(debounce);
  debounce = setTimeout(async () =&gt; {
    console.log(`\n${filename} changed, rebuilding...`);
    await build();
  }, 200);
});
</code></pre>
<h2>Spring Boot DevTools Integration (Kotlin)</h2>
<p>The LiveReload config in Kotlin, excluding <code>.svelte</code> from triggering app restarts:</p>
<pre><code class="language-kotlin">// IslandLiveReloadConfig.kt
@Configuration
@Profile("dev")
@ConditionalOnClass(FileSystemWatcher::class)
@EnableConfigurationProperties(DevToolsProperties::class)
class IslandLiveReloadConfig {

    private val logger = LoggerFactory.getLogger(IslandLiveReloadConfig::class.java)

    @Bean
    @RestartScope
    fun liveReloadServer(properties: DevToolsProperties): LiveReloadServer {
        return object : LiveReloadServer(
            properties.livereload.port,
            Restarter.getInstance().threadFactory
        ) {
            @Volatile
            private var lastReloadTime = 0L

            override fun triggerReload() {
                val now = System.currentTimeMillis()
                if (now - lastReloadTime &lt; 3000) {
                    logger.debug("Debounced duplicate livereload")
                    return
                }
                lastReloadTime = now
                super.triggerReload()
            }
        }
    }

    @Bean
    fun islandFileSystemWatcher(liveReloadServer: LiveReloadServer): FileSystemWatcher {
        val watcher = FileSystemWatcher()
        val islandsDir = File("src/main/resources/static/js/islands")
        watcher.addSourceDirectory(islandsDir)
        watcher.addListener { changeSet -&gt;
            if (changeSet.any { cf -&gt; cf.files.any { it.relativeName.endsWith(".js") } }) {
                liveReloadServer.triggerReload()
            }
        }
        watcher.start()
        return watcher
    }

    @Bean
    fun classPathRestartStrategy(properties: DevToolsProperties): ClassPathRestartStrategy {
        val delegate = PatternClassPathRestartStrategy(properties.restart.allExclude)
        return ClassPathRestartStrategy { changedFile -&gt;
            if (changedFile.relativeName.endsWith(".svelte")) false
            else delegate.isRestartRequired(changedFile)
        }
    }
}
</code></pre>
<h1>Why Svelte over Preact?</h1>
<p>Both work great for islands. Choose based on preference:</p>
<table>
<thead>
<tr>
<th>Aspect</th>
<th>Preact</th>
<th>Svelte 5</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Bundle size</strong></td>
<td>~3KB gzipped</td>
<td>~2KB gzipped (no runtime)</td>
</tr>
<tr>
<td><strong>Reactivity</strong></td>
<td><code>useSignal()</code> + <code>.value</code></td>
<td><code>$state()</code> — reads like plain JS</td>
</tr>
<tr>
<td><strong>Component syntax</strong></td>
<td>JSX (familiar to React devs)</td>
<td><code>.svelte</code> template (HTML-first)</td>
</tr>
<tr>
<td><strong>CSS scoping</strong></td>
<td>None built-in</td>
<td>Automatic with <code>&lt;style&gt;</code> blocks</td>
</tr>
<tr>
<td><strong>Build step</strong></td>
<td>Native Bun support</td>
<td>15-line Bun plugin</td>
</tr>
<tr>
<td><strong>Two-way binding</strong></td>
<td>Manual <code>onInput</code> handlers</td>
<td><code>bind:value</code></td>
</tr>
<tr>
<td><strong>Learning curve</strong></td>
<td>Zero if you know React</td>
<td>Small, but the syntax is arguably simpler</td>
</tr>
<tr>
<td><strong>Props types</strong></td>
<td>Manually maintained <code>.d.ts</code></td>
<td>Auto-generated from <code>@Serializable</code></td>
</tr>
</tbody></table>
<p>I personally switched PhotoQuest to Svelte because <code>$state</code> reads cleaner than <code>useSignal()</code> and the built-in CSS
scoping is a nice bonus for islands that need custom styling.</p>
<h1>When to Use What</h1>
<table>
<thead>
<tr>
<th>Use Case</th>
<th>Tool</th>
</tr>
</thead>
<tbody><tr>
<td>Toggle visibility, dropdowns, simple state</td>
<td>Alpine.js</td>
</tr>
<tr>
<td>Server-driven updates, form submissions</td>
<td>HTMX</td>
</tr>
<tr>
<td>Drag-and-drop, per-item state, deep component nesting</td>
<td>Svelte Island</td>
</tr>
</tbody></table>
<p>Same as before. The three coexist perfectly. HTMX handles 90% of interactions. Alpine.js handles UI toggles. Svelte
Islands handle the 5% where you need real component state and composability.</p>
<h1>Summary</h1>
<table>
<thead>
<tr>
<th>Preact Islands</th>
<th>Svelte 5 Islands</th>
</tr>
</thead>
<tbody><tr>
<td>Re-export <code>render, h</code> from preact</td>
<td>Re-export <code>mount</code> from svelte in <code>&lt;script module&gt;</code></td>
</tr>
<tr>
<td>Bun compiles TSX natively</td>
<td>15-line Bun plugin compiles <code>.svelte</code></td>
</tr>
<tr>
<td><code>useSignal()</code> + <code>.value</code> everywhere</td>
<td><code>$state()</code> — plain variable assignment</td>
</tr>
<tr>
<td>JSX with explicit event handlers</td>
<td>Template syntax with <code>bind:value</code></td>
</tr>
<tr>
<td>No built-in CSS scoping</td>
<td>Scoped <code>&lt;style&gt;</code> blocks out of the box</td>
</tr>
<tr>
<td>Java records for props</td>
<td>Kotlin data classes for props</td>
</tr>
<tr>
<td>Manually maintained <code>.d.ts</code></td>
<td>Auto-generated <code>.ts</code> via <code>IslandTypeGenerator</code></td>
</tr>
</tbody></table>
<p>The architecture is identical: server renders HTML, passes typed JSON props, a 12-line mount script hydrates the island.
The only difference is which frontend framework does the client-side rendering — and with the <code>IslandTypeGenerator</code>,
the TypeScript interfaces stay in sync with your Kotlin data classes automatically. Pick whichever syntax you prefer.</p>
<p>If you want to learn more about HTMX + Spring Boot check out
my <a href="https://www.tschuehly.de/posts/spring-boot-htmx/">HTMX + Spring Boot series</a>. My side
business <a href="https://photoquest.wedding/">PhotoQuest</a> is built with Svelte islands + Kotlin + Spring Boot — exactly this
stack.</p>
<p>If you have questions, ping me on <a href="https://twitter.com/tschuehly">twitter.com/tschuehly</a>!</p>
]]></content:encoded></item><item><title><![CDATA[Fixing Gradle on Claude Code Web with a PreToolUse Hook]]></title><description><![CDATA[Claude Code on the Web runs inside a sandboxed environment that routes all traffic through an HTTP proxy.
Tools like curl pick up the https_proxy environment variable automatically - but Java doesn't. 
The Gradle wrapper uses java.net.HttpURLConnecti...]]></description><link>https://tschuehly.de/claude-code-hooks-gradle-proxy-fix</link><guid isPermaLink="true">https://tschuehly.de/claude-code-hooks-gradle-proxy-fix</guid><category><![CDATA[claude]]></category><category><![CDATA[devtools]]></category><category><![CDATA[gradle]]></category><category><![CDATA[spring-boot]]></category><dc:creator><![CDATA[Thomas Schilling]]></dc:creator><pubDate>Mon, 23 Feb 2026 11:35:13 GMT</pubDate><enclosure url="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/65c1ed52dd6f7b6c0c5bb941/3795365f-e762-4b7f-bcb4-965157b2949e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Claude Code on the Web runs inside a sandboxed environment that routes all traffic through an HTTP proxy.
Tools like <code>curl</code> pick up the <code>https_proxy</code> environment variable automatically - but <strong>Java doesn't</strong>. 
The Gradle wrapper uses <code>java.net.HttpURLConnection</code> under the hood, which completely ignores <code>https_proxy</code>. 
The result? <code>java.net.UnknownHostException: services.gradle.org</code> every single time.</p>
<p>This is a <a target="_blank" href="https://github.com/anthropics/claude-code/issues/16222">known issue</a> that affects every Java/Kotlin project on Claude Code Web. I wrote a Claude Code Hook that fixes it.</p>
<h1 id="heading-the-problem">The Problem</h1>
<p>When you run <code>./gradlew</code> in a Claude Code Web session, Java tries to resolve <code>services.gradle.org</code> directly - bypassing the sandbox proxy entirely. You get this:</p>
<pre><code>Exception <span class="hljs-keyword">in</span> thread <span class="hljs-string">"main"</span> java.net.UnknownHostException: services.gradle.org
    at java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:<span class="hljs-number">567</span>)
    ...
    at org.gradle.wrapper.GradleWrapperMain.main(SourceFile:<span class="hljs-number">67</span>)
</code></pre><p>The frustrating part: <code>curl</code> downloads the exact same URL just fine because it honors <code>https_proxy</code>. The sandbox network works, Java just doesn't know about it.</p>
<h1 id="heading-the-fix-a-pretooluse-hook">The Fix: A PreToolUse Hook</h1>
<p>Claude Code <a target="_blank" href="https://docs.anthropic.com/en/docs/claude-code/hooks">Hooks</a> let you run shell scripts before or after specific tool invocations. We can use a <code>PreToolUse</code> hook on the <code>Bash</code> tool to configure the proxy before any command runs.</p>
<p>Let's first look at the full hook script:</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>
<span class="hljs-built_in">set</span> -euo pipefail

<span class="hljs-comment"># Only run in remote (Claude Code on the web) environments</span>
<span class="hljs-keyword">if</span> [ <span class="hljs-string">"<span class="hljs-variable">${CLAUDE_CODE_REMOTE:-}</span>"</span> != <span class="hljs-string">"true"</span> ]; <span class="hljs-keyword">then</span>
  <span class="hljs-built_in">exit</span> 0
<span class="hljs-keyword">fi</span>

<span class="hljs-comment"># Configure Gradle proxy so Java can resolve external hosts</span>
<span class="hljs-keyword">if</span> [ -n <span class="hljs-string">"<span class="hljs-variable">${https_proxy:-}</span>"</span> ]; <span class="hljs-keyword">then</span>
  PROXY_HOST=$(<span class="hljs-built_in">echo</span> <span class="hljs-string">"<span class="hljs-variable">$https_proxy</span>"</span> | sed <span class="hljs-string">'s|.*@||'</span> | sed <span class="hljs-string">'s|:.*||'</span>)
  PROXY_PORT=$(<span class="hljs-built_in">echo</span> <span class="hljs-string">"<span class="hljs-variable">$https_proxy</span>"</span> | sed <span class="hljs-string">'s|.*:||'</span>)
  PROXY_USER=$(<span class="hljs-built_in">echo</span> <span class="hljs-string">"<span class="hljs-variable">$https_proxy</span>"</span> | sed <span class="hljs-string">'s|http://||'</span> | sed <span class="hljs-string">'s|@.*||'</span> | sed <span class="hljs-string">'s|:.*||'</span>)
  PROXY_PASS=$(<span class="hljs-built_in">echo</span> <span class="hljs-string">"<span class="hljs-variable">$https_proxy</span>"</span> | sed <span class="hljs-string">'s|http://||'</span> | sed <span class="hljs-string">'s|@.*||'</span> | sed <span class="hljs-string">'s|^[^:]*:||'</span>)

  mkdir -p ~/.gradle
  cat &gt; ~/.gradle/gradle.properties &lt;&lt; PROPEOF
systemProp.http.proxyHost=<span class="hljs-variable">$PROXY_HOST</span>
systemProp.http.proxyPort=<span class="hljs-variable">$PROXY_PORT</span>
systemProp.http.proxyUser=<span class="hljs-variable">$PROXY_USER</span>
systemProp.http.proxyPassword=<span class="hljs-variable">$PROXY_PASS</span>
systemProp.https.proxyHost=<span class="hljs-variable">$PROXY_HOST</span>
systemProp.https.proxyPort=<span class="hljs-variable">$PROXY_PORT</span>
systemProp.https.proxyUser=<span class="hljs-variable">$PROXY_USER</span>
systemProp.https.proxyPassword=<span class="hljs-variable">$PROXY_PASS</span>
systemProp.jdk.http.auth.tunneling.disabledSchemes=
systemProp.jdk.http.auth.proxying.disabledSchemes=
PROPEOF
<span class="hljs-keyword">fi</span>

<span class="hljs-comment"># Download Gradle distribution if not already cached</span>
<span class="hljs-built_in">cd</span> <span class="hljs-string">"<span class="hljs-variable">$CLAUDE_PROJECT_DIR</span>"</span>
./gradlew --version &gt; /dev/null 2&gt;&amp;1

<span class="hljs-comment"># Warm Gradle dependency cache</span>
./gradlew compileKotlin compileTestKotlin --no-daemon 2&gt;/dev/null || <span class="hljs-literal">true</span>
</code></pre>
<p>In detail:</p>
<ul>
<li><code>CLAUDE_CODE_REMOTE</code> -&gt; environment variable that's only set in Claude Code Web sessions, so the hook is a no-op locally</li>
<li>We parse the <code>https_proxy</code> URL (format: <code>http://user:pass@host:port</code>) into its components using <code>sed</code></li>
<li><code>~/.gradle/gradle.properties</code> -&gt; where Gradle reads JVM proxy system properties from</li>
<li><code>systemProp.http.proxyHost</code> / <code>systemProp.https.proxyHost</code> -&gt; tells Java's HTTP client to route through the sandbox proxy</li>
<li><code>jdk.http.auth.tunneling.disabledSchemes=</code> / <code>jdk.http.auth.proxying.disabledSchemes=</code> -&gt; by default Java disables Basic auth for HTTPS proxies, we clear that restriction so proxy authentication works</li>
<li><code>./gradlew --version</code> -&gt; triggers the Gradle distribution download through the now-configured proxy</li>
<li><code>./gradlew compileKotlin compileTestKotlin</code> -&gt; warms the dependency cache so Claude doesn't wait for downloads mid-task</li>
</ul>
<h1 id="heading-setting-up-the-hook">Setting Up the Hook</h1>
<p>Add this to your project's <code>.claude/settings.json</code>:</p>
<pre><code class="lang-json"><span class="hljs-comment">// .claude/settings.json</span>
{
  <span class="hljs-attr">"hooks"</span>: {
    <span class="hljs-attr">"PreToolUse"</span>: [
      {
        <span class="hljs-attr">"matcher"</span>: <span class="hljs-string">"Bash"</span>,
        <span class="hljs-attr">"hooks"</span>: [
          {
            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"command"</span>,
            <span class="hljs-attr">"command"</span>: <span class="hljs-string">"/bin/bash .claude/hooks/pre-tool-use.sh"</span>
          }
        ]
      }
    ]
  }
}
</code></pre>
<p>Then place the script at <code>.claude/hooks/pre-tool-use.sh</code> in your project.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">The hook runs before every Bash tool invocation. Since it checks <code>CLAUDE_CODE_REMOTE</code> first and exits immediately in local environments, there's zero overhead when running Claude Code on your own machine.</div>
</div>

<h1 id="heading-why-a-hook-instead-of-a-claudemd-instruction">Why a Hook Instead of a CLAUDE.md Instruction?</h1>
<p>You could write "always configure the Gradle proxy before running builds" in your <code>CLAUDE.md</code>, but:</p>
<ol>
<li>Claude might forget or skip it</li>
<li>It wastes tokens re-explaining the setup every session</li>
<li>A hook is <strong>deterministic</strong> it runs every time, no LLM reasoning required</li>
</ol>
<p>Hooks are the right tool when you need guaranteed, repeatable setup. Save <code>CLAUDE.md</code> for guidance that requires judgment.</p>
<h1 id="heading-the-result">The Result</h1>
<p>With this hook in place, Claude Code Web sessions with Spring Boot / Kotlin projects just work. No more <code>UnknownHostException</code>, no more manually downloading Gradle distributions with <code>curl</code>, no more wasted context window explaining proxy configuration to the AI.</p>
<p>If you want to learn more about Claude Code check out my other post on <a target="_blank" href="https://tschuehly.de/posts/claude-code-planning-security-migration">how I used Claude Code to plan a complex security migration</a>. My side business <a target="_blank" href="https://photoquest.wedding/">PhotoQuest</a> is also built with Spring Boot + htmx and was the project where I first ran into this issue.</p>
<p>Questions? Ping me on <a target="_blank" href="https://twitter.com/tschuehly">twitter.com/tschuehly</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Preact Islands in Spring Boot with htmx: When Alpine.js Isn't Enough Anymore]]></title><description><![CDATA[I build my webserver-rendered web applications with Spring Boot, Thymeleaf, and HTMX. I sprinkle some Alpine.js on top for dropdowns and toggles. And for 95% of interactions, this stack is perfect.
But then you hit that one feature – a kanban board w...]]></description><link>https://tschuehly.de/preact-islands-in-spring-boot-with-htmx-when-alpinejs-isnt-enough-anymore</link><guid isPermaLink="true">https://tschuehly.de/preact-islands-in-spring-boot-with-htmx-when-alpinejs-isnt-enough-anymore</guid><category><![CDATA[htmx]]></category><category><![CDATA[Preact]]></category><category><![CDATA[Springboot]]></category><category><![CDATA[Spring]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[alpinejs]]></category><dc:creator><![CDATA[Thomas Schilling]]></dc:creator><pubDate>Wed, 18 Feb 2026 07:24:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1771399690659/3364ff95-2e1e-4967-a188-3bc90b5ff3b8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I build my webserver-rendered web applications with Spring Boot, Thymeleaf, and HTMX. I sprinkle some Alpine.js on top for dropdowns and toggles. And for 95% of interactions, this stack is <strong>perfect</strong>.</p>
<p>But then you hit that one feature – a kanban board with draggable cards, inline editing, cross-column state, and per-card interactions. And you're suddenly writing an entire application inside HTML attributes.</p>
<p><strong>Preact Islands</strong> fix this without throwing away your server-rendered architecture. Drop a single <code>.tsx</code> file next to your ViewComponent, and it becomes a self-contained interactive widget. No webpack, no SPA router. <strong>3KB</strong> of Preact and your component logic.</p>
<p>I personally use this pattern in <a target="_blank" href="https://photoquest.wedding/">PhotoQuest</a> for the few components where HTMX + Alpine.js weren't cutting it. Let me show you how to set it up.</p>
<h1 id="heading-the-problem-javascript-in-html-attributes">The Problem: JavaScript in HTML Attributes</h1>
<p>Here's a kanban board in Alpine.js. Users drag cards between columns, edit cards inline, and add new ones:</p>
<pre><code class="lang-html"><span class="hljs-comment">&lt;!-- KanbanComponent.html --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">x-data</span>=<span class="hljs-string">"{
    columns: [
        { id: 'todo', title: 'To Do', cards: [
            { id: 1, title: 'Design landing page', priority: 'high', assignee: 'Alice' },
            { id: 2, title: 'Write API docs', priority: 'medium', assignee: 'Bob' },
        ]},
        { id: 'progress', title: 'In Progress', cards: [
            { id: 3, title: 'Auth flow', priority: 'high', assignee: 'Charlie' },
        ]},
        { id: 'done', title: 'Done', cards: [] },
    ],
    dragCard: null,
    dragFromCol: null,
    editingCard: null,
    editForm: { title: '', priority: 'medium', assignee: '' },
    addingToCol: null,
    newCard: { title: '', priority: 'medium', assignee: '' },
    nextId: 4,
    startDrag(card, colId) { this.dragCard = card; this.dragFromCol = colId },
    onDrop(targetColId) {
        if (!this.dragCard || this.dragFromCol === targetColId) return;
        const from = this.columns.find(c =&gt; c.id === this.dragFromCol);
        const to = this.columns.find(c =&gt; c.id === targetColId);
        from.cards = from.cards.filter(c =&gt; c.id !== this.dragCard.id);
        to.cards.push(this.dragCard);
        this.dragCard = null; this.dragFromCol = null;
    },
    startEdit(card) {
        this.editingCard = card.id;
        this.editForm = { title: card.title, priority: card.priority, assignee: card.assignee };
    },
    saveEdit(colId) {
        const col = this.columns.find(c =&gt; c.id === colId);
        Object.assign(col.cards.find(c =&gt; c.id === this.editingCard), this.editForm);
        this.editingCard = null;
    },
    startAdd(colId) { this.addingToCol = colId; this.newCard = { title: '', priority: 'medium', assignee: '' } },
    confirmAdd() {
        this.columns.find(c =&gt; c.id === this.addingToCol).cards.push({ id: this.nextId++, ...this.newCard });
        this.addingToCol = null;
    },
    deleteCard(colId, cardId) {
        const col = this.columns.find(c =&gt; c.id === colId);
        col.cards = col.cards.filter(c =&gt; c.id !== cardId);
    },
    priorityColor(p) { return { high: 'badge-error', medium: 'badge-warning', low: 'badge-info' }[p] || '' }
}"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex gap-4"</span>&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">template</span> <span class="hljs-attr">x-for</span>=<span class="hljs-string">"col in columns"</span> <span class="hljs-attr">:key</span>=<span class="hljs-string">"col.id"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"w-72 bg-base-200 rounded-lg p-3"</span> @<span class="hljs-attr">dragover.prevent</span> @<span class="hljs-attr">drop</span>=<span class="hljs-string">"onDrop(col.id)"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h3</span> <span class="hljs-attr">x-text</span>=<span class="hljs-string">"col.title"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"font-bold mb-3"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">template</span> <span class="hljs-attr">x-for</span>=<span class="hljs-string">"card in col.cards"</span> <span class="hljs-attr">:key</span>=<span class="hljs-string">"card.id"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"bg-base-100 rounded p-3 mb-2 cursor-grab"</span>
             <span class="hljs-attr">draggable</span>=<span class="hljs-string">"true"</span> @<span class="hljs-attr">dragstart</span>=<span class="hljs-string">"startDrag(card, col.id)"</span>&gt;</span>

          <span class="hljs-tag">&lt;<span class="hljs-name">template</span> <span class="hljs-attr">x-if</span>=<span class="hljs-string">"editingCard !== card.id"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex justify-between"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">x-text</span>=<span class="hljs-string">"card.title"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"font-medium"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">:class</span>=<span class="hljs-string">"'badge badge-sm ' + priorityColor(card.priority)"</span>
                      <span class="hljs-attr">x-text</span>=<span class="hljs-string">"card.priority"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
              <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-sm opacity-60 mt-1"</span> <span class="hljs-attr">x-text</span>=<span class="hljs-string">"card.assignee"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex gap-1 mt-2"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"startEdit(card)"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn btn-xs"</span>&gt;</span>Edit<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"deleteCard(col.id, card.id)"</span>
                        <span class="hljs-attr">class</span>=<span class="hljs-string">"btn btn-xs btn-error"</span>&gt;</span>Delete
                <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
              <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>

          <span class="hljs-tag">&lt;<span class="hljs-name">template</span> <span class="hljs-attr">x-if</span>=<span class="hljs-string">"editingCard === card.id"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"space-y-2"</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">x-model</span>=<span class="hljs-string">"editForm.title"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"input input-sm w-full"</span>/&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">select</span> <span class="hljs-attr">x-model</span>=<span class="hljs-string">"editForm.priority"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"select select-sm w-full"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"low"</span>&gt;</span>Low<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"medium"</span>&gt;</span>Medium<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"high"</span>&gt;</span>High<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
              <span class="hljs-tag">&lt;/<span class="hljs-name">select</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">x-model</span>=<span class="hljs-string">"editForm.assignee"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"input input-sm w-full"</span>
                     <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"Assignee"</span>/&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex gap-1"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"saveEdit(col.id)"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn btn-xs btn-primary"</span>&gt;</span>Save<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"editingCard = null"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn btn-xs"</span>&gt;</span>Cancel<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
              <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">template</span> <span class="hljs-attr">x-if</span>=<span class="hljs-string">"addingToCol === col.id"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"bg-base-100 rounded p-3 mb-2 space-y-2"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">x-model</span>=<span class="hljs-string">"newCard.title"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"input input-sm w-full"</span> <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"Card title"</span>/&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">select</span> <span class="hljs-attr">x-model</span>=<span class="hljs-string">"newCard.priority"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"select select-sm w-full"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"low"</span>&gt;</span>Low<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"medium"</span>&gt;</span>Medium<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"high"</span>&gt;</span>High<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">select</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">x-model</span>=<span class="hljs-string">"newCard.assignee"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"input input-sm w-full"</span> <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"Assignee"</span>/&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex gap-1"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"confirmAdd()"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn btn-xs btn-primary"</span>&gt;</span>Add<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"addingToCol = null"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn btn-xs"</span>&gt;</span>Cancel<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"startAdd(col.id)"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn btn-sm btn-ghost w-full mt-2"</span>&gt;</span>+ Add Card<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>This technically works. But look at what we're dealing with:</p>
<ul>
<li><p><strong>One shared</strong> <code>editForm</code> and <code>editingCard</code> -&gt; only one card can be edited at a time, and the state lives far from the card it belongs to</p>
</li>
<li><p><strong>Six methods</strong> that all find columns by ID with <code>.find()</code> -&gt; duplicated lookup logic everywhere</p>
</li>
<li><p><strong>Drag state as two top-level variables</strong> -&gt; <code>dragCard</code> and <code>dragFromCol</code> are global to the entire board</p>
</li>
<li><p><strong>Nested</strong> <code>x-for</code> + <code>x-if</code> -&gt; the template is 80+ lines of interleaved logic and markup with no way to extract reusable pieces</p>
</li>
<li><p><strong>No syntax highlighting</strong> inside those HTML attributes, no IntelliJ autocomplete, no type safety</p>
</li>
</ul>
<h1 id="heading-the-solution-preact-islands">The Solution: Preact Islands</h1>
<ol>
<li><p>Place a <code>.tsx</code> file next to your ViewComponent</p>
</li>
<li><p>Bun compiles it to a JS bundle in <code>static/js/islands/</code></p>
</li>
<li><p>A 12-line <code>mount.js</code> script finds <code>[data-island]</code> elements and mounts the Preact component</p>
</li>
<li><p>The server passes typed props as JSON via <code>data-props</code></p>
</li>
</ol>
<h2 id="heading-architecture-overview">Architecture Overview</h2>
<pre><code class="lang-plaintext">src/main/java/de/tschuehly/preact_islands_demo/web/kanban/
    KanbanComponent.java              # Server-side ViewComponent + Controller
    KanbanComponent.html              # Thymeleaf template (thin shell)
    KanbanIsland.tsx                   # Preact island (all client logic)
    KanbanIslandProps.d.ts             # TypeScript interface (manually maintained)
    KanbanService.java                 # Board data service

frontend/
    build.ts                         # Bun build script
    watch.ts                         # Dev file watcher
    bunfig.toml                      # JSX config
    tsconfig.json                    # TypeScript config

src/main/resources/static/js/
    mount.js                         # Island mount script (12 lines)
    islands/                         # Build output (gitignored)
        kanban/KanbanIsland.js
</code></pre>
<p>The <code>.tsx</code> lives next to the ViewComponent it belongs to, in the same package directory. Not in a separate <code>frontend/src/components/</code> directory three folders away.</p>
<h1 id="heading-setting-up-preact-islands">Setting Up Preact Islands</h1>
<h2 id="heading-install-dependencies">Install Dependencies</h2>
<p>Add preact to your root <code>package.json</code> so IntelliJ resolves the imports properly:</p>
<pre><code class="lang-json"><span class="hljs-comment">// package.json</span>
{
  <span class="hljs-attr">"dependencies"</span>: {
    <span class="hljs-attr">"preact"</span>: <span class="hljs-string">"^10.28.3"</span>,
    <span class="hljs-attr">"@preact/signals"</span>: <span class="hljs-string">"^2.7.0"</span>
  }
}
</code></pre>
<h2 id="heading-configure-bun-for-preact-jsx">Configure Bun for Preact JSX</h2>
<pre><code class="lang-toml"><span class="hljs-comment"># frontend/bunfig.toml</span>
<span class="hljs-section">[build]</span>
<span class="hljs-attr">jsx</span> = <span class="hljs-string">"react-jsx"</span>
<span class="hljs-attr">jsxImportSource</span> = <span class="hljs-string">"preact"</span>
</code></pre>
<pre><code class="lang-json"><span class="hljs-comment">// frontend/tsconfig.json</span>
{
  <span class="hljs-attr">"compilerOptions"</span>: {
    <span class="hljs-attr">"target"</span>: <span class="hljs-string">"es2022"</span>,
    <span class="hljs-attr">"module"</span>: <span class="hljs-string">"commonjs"</span>,
    <span class="hljs-attr">"jsx"</span>: <span class="hljs-string">"react-jsx"</span>,
    <span class="hljs-attr">"jsxImportSource"</span>: <span class="hljs-string">"preact"</span>,
    <span class="hljs-attr">"baseUrl"</span>: <span class="hljs-string">".."</span>,
    <span class="hljs-attr">"paths"</span>: {
      <span class="hljs-attr">"preact"</span>: [
        <span class="hljs-string">"./node_modules/preact"</span>
      ],
      <span class="hljs-attr">"preact/*"</span>: [
        <span class="hljs-string">"./node_modules/preact/*"</span>
      ]
    }
  }
}
</code></pre>
<h2 id="heading-the-build-script">The Build Script</h2>
<p><code>frontend/build.ts</code> scans for all <code>.tsx</code> files in the Java source tree and compiles them:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// frontend/build.ts</span>
<span class="hljs-keyword">import</span> {Glob} <span class="hljs-keyword">from</span> <span class="hljs-string">"bun"</span>;

<span class="hljs-keyword">const</span> sourceRoot = <span class="hljs-string">"../src/main/java/de/tschuehly/preact_islands_demo/web/"</span>;
<span class="hljs-keyword">const</span> scanRoot = <span class="hljs-string">"../src/main/java"</span>;
<span class="hljs-keyword">const</span> glob = <span class="hljs-keyword">new</span> Glob(<span class="hljs-string">"**/*.tsx"</span>);
<span class="hljs-keyword">const</span> entrypoints: <span class="hljs-built_in">string</span>[] = [];

<span class="hljs-keyword">for</span> <span class="hljs-keyword">await</span> (<span class="hljs-keyword">const</span> file <span class="hljs-keyword">of</span> glob.scan(scanRoot)) {
  entrypoints.push(<span class="hljs-string">`<span class="hljs-subst">${scanRoot}</span>/<span class="hljs-subst">${file}</span>`</span>);
}

<span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Found <span class="hljs-subst">${entrypoints.length}</span> island(s):`</span>);
entrypoints.forEach(<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`  <span class="hljs-subst">${e}</span>`</span>));

<span class="hljs-keyword">await</span> Bun.build({
  entrypoints,
  outdir: <span class="hljs-string">"../src/main/resources/static/js/islands"</span>,
  root: sourceRoot,
  minify: <span class="hljs-literal">true</span>,
  naming: <span class="hljs-string">"[dir]/[name].js"</span>,
});

<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Build complete."</span>);
</code></pre>
<p>In detail:</p>
<ul>
<li><p><code>root</code> is set to <code>sourceRoot</code> -&gt; the output directory structure mirrors the package structure</p>
</li>
<li><p><code>naming: "[dir]/[name].js"</code> -&gt; a TSX file at <code>web/kanban/KanbanIsland.tsx</code> produces <code>islands/kanban/KanbanIsland.js</code></p>
</li>
<li><p><code>minify: true</code> -&gt; Bun tree-shakes and minifies everything in one pass</p>
</li>
</ul>
<h2 id="heading-the-mount-script">The Mount Script</h2>
<p>This is the entire client-side runtime. 12 lines.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// src/main/resources/static/js/mount.js</span>
<span class="hljs-built_in">document</span>.querySelectorAll(<span class="hljs-string">"[data-island]"</span>).forEach(<span class="hljs-keyword">async</span> (el) =&gt; {
  <span class="hljs-keyword">const</span> name = el.dataset.island;
  <span class="hljs-keyword">const</span> props = <span class="hljs-built_in">JSON</span>.parse(el.dataset.props || <span class="hljs-string">"{}"</span>);

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> {<span class="hljs-attr">default</span>: Component, render, h} = <span class="hljs-keyword">await</span> <span class="hljs-keyword">import</span>(<span class="hljs-string">`/js/islands/<span class="hljs-subst">${name}</span>.js`</span>);
    el.innerHTML = <span class="hljs-string">""</span>;
    render(h(Component, props), el);
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">`Failed to mount island "<span class="hljs-subst">${name}</span>":`</span>, err);
    el.innerHTML = <span class="hljs-string">'&lt;div class="alert alert-error"&gt;Component could not be loaded.&lt;/div&gt;'</span>;
  }
});
</code></pre>
<p>It finds every <code>[data-island]</code> element, dynamically imports the JS bundle, and mounts it with the server-provided props. That's it.</p>
<p><strong>Important:</strong> Each island TSX must re-export <code>render</code> and <code>h</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> {render, h};
</code></pre>
<p>Why? <code>mount.js</code> is a plain browser JS module. It's not processed by Bun. It can't <code>import { render, h } from "preact"</code> because bare module specifiers don't resolve in the browser without an import map. Bun inlines Preact <em>into</em> each island bundle, so re-exporting from the island is how <code>mount.js</code> gets access.</p>
<h2 id="heading-include-mountjs-in-your-layout">Include mount.js in Your Layout</h2>
<p>Add a single <code>&lt;script&gt;</code> tag to your layout template:</p>
<pre><code class="lang-html"><span class="hljs-comment">&lt;!-- LayoutViewComponent.html --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">th:src</span>=<span class="hljs-string">"@{/js/mount.js}"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"module"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p><code>type="module"</code> ensures it runs after the DOM is ready and supports dynamic <code>import()</code>.</p>
<h1 id="heading-writing-the-preact-island">Writing the Preact Island</h1>
<p>Here's the same kanban board as composable Preact components:</p>
<pre><code class="lang-tsx">// src/main/java/de/tschuehly/preact_islands_demo/web/kanban/KanbanIsland.tsx
import {render, h} from "preact";
import {useSignal, signal} from "@preact/signals";

export {render, h};

import type {KanbanIslandProps, KanbanCard, KanbanColumn} from "./KanbanIslandProps";

// --- Shared drag state (module-level signal) ---

const dragState = signal&lt;{ cardId: number; fromColumnId: string } | null&gt;(null);

// --- Sub-components ---

function PriorityBadge({priority}: { priority: string }) {
  const colors: Record&lt;string, string&gt; = {
    high: "badge-error", medium: "badge-warning", low: "badge-info",
  };
  return &lt;span class={`badge badge-sm ${colors[priority] || ""}`}&gt;{priority}&lt;/span&gt;;
}

function Card({card, columnId, onDelete, onUpdate}: {
  card: KanbanCard;
  columnId: string;
  onDelete: (cardId: number) =&gt; void;
  onUpdate: (cardId: number, data: Partial&lt;KanbanCard&gt;) =&gt; void;
}) {
  const editing = useSignal(false);
  const title = useSignal(card.title);
  const priority = useSignal(card.priority);
  const assignee = useSignal(card.assignee);

  function save() {
    onUpdate(card.id, {title: title.value, priority: priority.value, assignee: assignee.value});
    editing.value = false;
  }

  return (
      &lt;div class="bg-base-100 rounded-lg p-3 mb-2 shadow-sm cursor-grab"
           draggable
           onDragStart={() =&gt; {
             dragState.value = {cardId: card.id, fromColumnId: columnId}
           }}
           onDragEnd={() =&gt; {
             dragState.value = null
           }}&gt;
        {editing.value ? (
            &lt;div class="space-y-2"&gt;
              &lt;input class="input input-sm w-full" value={title}
                     onInput={e =&gt; title.value = e.currentTarget.value}/&gt;
              &lt;select class="select select-sm w-full" value={priority}
                      onChange={e =&gt; priority.value = e.currentTarget.value}&gt;
                &lt;option value="low"&gt;Low&lt;/option&gt;
                &lt;option value="medium"&gt;Medium&lt;/option&gt;
                &lt;option value="high"&gt;High&lt;/option&gt;
              &lt;/select&gt;
              &lt;input class="input input-sm w-full" value={assignee}
                     onInput={e =&gt; assignee.value = e.currentTarget.value} placeholder="Assignee"/&gt;
              &lt;div class="flex gap-1"&gt;
                &lt;button class="btn btn-xs btn-primary" onClick={save}&gt;Save&lt;/button&gt;
                &lt;button class="btn btn-xs" onClick={() =&gt; editing.value = false}&gt;Cancel&lt;/button&gt;
              &lt;/div&gt;
            &lt;/div&gt;
        ) : (
            &lt;div&gt;
              &lt;div class="flex justify-between items-start"&gt;
                &lt;span class="font-medium"&gt;{card.title}&lt;/span&gt;
                &lt;PriorityBadge priority={card.priority}/&gt;
              &lt;/div&gt;
              &lt;div class="text-sm text-base-content/60 mt-1"&gt;{card.assignee}&lt;/div&gt;
              &lt;div class="flex gap-1 mt-2"&gt;
                &lt;button class="btn btn-xs" onClick={() =&gt; editing.value = true}&gt;Edit&lt;/button&gt;
                &lt;button class="btn btn-xs btn-error" onClick={() =&gt; onDelete(card.id)}&gt;Delete&lt;/button&gt;
              &lt;/div&gt;
            &lt;/div&gt;
        )}
      &lt;/div&gt;
  );
}

function AddCardForm({onAdd, onCancel}: {
  onAdd: (data: Omit&lt;KanbanCard, "id"&gt;) =&gt; void;
  onCancel: () =&gt; void;
}) {
  const title = useSignal("");
  const priority = useSignal("medium");
  const assignee = useSignal("");

  return (
      &lt;div class="bg-base-100 rounded-lg p-3 mb-2 space-y-2"&gt;
        &lt;input class="input input-sm w-full" value={title}
               onInput={e =&gt; title.value = e.currentTarget.value} placeholder="Card title"/&gt;
        &lt;select class="select select-sm w-full" value={priority}
                onChange={e =&gt; priority.value = e.currentTarget.value}&gt;
          &lt;option value="low"&gt;Low&lt;/option&gt;
          &lt;option value="medium"&gt;Medium&lt;/option&gt;
          &lt;option value="high"&gt;High&lt;/option&gt;
        &lt;/select&gt;
        &lt;input class="input input-sm w-full" value={assignee}
               onInput={e =&gt; assignee.value = e.currentTarget.value} placeholder="Assignee"/&gt;
        &lt;div class="flex gap-1"&gt;
          &lt;button class="btn btn-xs btn-primary" onClick={() =&gt; {
            onAdd({title: title.value, priority: priority.value, assignee: assignee.value});
          }}&gt;Add
          &lt;/button&gt;
          &lt;button class="btn btn-xs" onClick={onCancel}&gt;Cancel&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  );
}

function Column({column, onMove, onDelete, onUpdate, onAdd}: {
  column: KanbanColumn;
  onMove: (cardId: number, fromColId: string, toColId: string) =&gt; void;
  onDelete: (colId: string, cardId: number) =&gt; void;
  onUpdate: (colId: string, cardId: number, data: Partial&lt;KanbanCard&gt;) =&gt; void;
  onAdd: (colId: string, data: Omit&lt;KanbanCard, "id"&gt;) =&gt; void;
}) {
  const adding = useSignal(false);
  const dragOver = useSignal(false);

  return (
      &lt;div class={`w-72 rounded-lg p-3 ${dragOver.value ? "bg-primary/10" : "bg-base-200"}`}
           onDragOver={(e) =&gt; {
             e.preventDefault();
             dragOver.value = true
           }}
           onDragLeave={() =&gt; dragOver.value = false}
           onDrop={() =&gt; {
             dragOver.value = false;
             if (dragState.value &amp;&amp; dragState.value.fromColumnId !== column.id) {
               onMove(dragState.value.cardId, dragState.value.fromColumnId, column.id);
             }
           }}&gt;
        &lt;div class="flex justify-between items-center mb-3"&gt;
          &lt;h3 class="font-bold"&gt;{column.title}&lt;/h3&gt;
          &lt;span class="badge badge-ghost badge-sm"&gt;{column.cards.length}&lt;/span&gt;
        &lt;/div&gt;

        {column.cards.map(card =&gt; (
            &lt;Card key={card.id} card={card} columnId={column.id}
                  onDelete={(cardId) =&gt; onDelete(column.id, cardId)}
                  onUpdate={(cardId, data) =&gt; onUpdate(column.id, cardId, data)}/&gt;
        ))}

        {adding.value ? (
            &lt;AddCardForm
                onAdd={(data) =&gt; {
                  onAdd(column.id, data);
                  adding.value = false
                }}
                onCancel={() =&gt; adding.value = false}/&gt;
        ) : (
            &lt;button class="btn btn-sm btn-ghost w-full mt-2"
                    onClick={() =&gt; adding.value = true}&gt;+ Add Card&lt;/button&gt;
        )}
      &lt;/div&gt;
  );
}

// --- Main island component ---

export default function KanbanIsland({columns: initialColumns}: KanbanIslandProps) {
  const columns = useSignal(
      initialColumns.map(col =&gt; ({...col, cards: [...col.cards]}))
  );
  const nextId = useSignal(
      Math.max(...initialColumns.flatMap(c =&gt; c.cards.map(card =&gt; card.id)), 0) + 1
  );

  function moveCard(cardId: number, fromColId: string, toColId: string) {
    const prev = columns.value;
    const card = prev.find(c =&gt; c.id === fromColId)!.cards.find(c =&gt; c.id === cardId)!;
    columns.value = prev.map(col =&gt; {
      if (col.id === fromColId) return {...col, cards: col.cards.filter(c =&gt; c.id !== cardId)};
      if (col.id === toColId) return {...col, cards: [...col.cards, card]};
      return col;
    });
  }

  function deleteCard(colId: string, cardId: number) {
    columns.value = columns.value.map(col =&gt;
        col.id === colId ? {...col, cards: col.cards.filter(c =&gt; c.id !== cardId)} : col
    );
  }

  function updateCard(colId: string, cardId: number, data: Partial&lt;KanbanCard&gt;) {
    columns.value = columns.value.map(col =&gt;
        col.id === colId
            ? {...col, cards: col.cards.map(c =&gt; c.id === cardId ? {...c, ...data} : c)}
            : col
    );
  }

  function addCard(colId: string, data: Omit&lt;KanbanCard, "id"&gt;) {
    const id = nextId.value++;
    columns.value = columns.value.map(col =&gt;
        col.id === colId ? {...col, cards: [...col.cards, {id, ...data}]} : col
    );
  }

  return (
      &lt;div class="flex gap-4 overflow-x-auto p-4"&gt;
        {columns.value.map(col =&gt; (
            &lt;Column key={col.id} column={col}
                    onMove={moveCard} onDelete={deleteCard}
                    onUpdate={updateCard} onAdd={addCard}/&gt;
        ))}
      &lt;/div&gt;
  );
}
</code></pre>
<p>We went from one massive <code>x-data</code> block to <strong>five focused components</strong>:</p>
<ul>
<li><p><code>PriorityBadge</code> -&gt; tiny display component, reused in every card.</p>
</li>
<li><p><code>Card</code> -&gt; owns its own edit state. Each card can be edited independently. No shared <code>editingCard</code> variable.</p>
</li>
<li><p><code>AddCardForm</code> -&gt; owns its own form state. Each column gets its own form instance. No shared <code>newCard</code> + <code>addingToCol</code>.</p>
</li>
<li><p><code>Column</code> -&gt; handles drag-over highlighting and composes cards + add form. The <code>dragOver</code> signal is local to each column.</p>
</li>
<li><p><code>KanbanIsland</code> -&gt; board-level state and CRUD operations passed down as callbacks.</p>
</li>
</ul>
<h1 id="heading-wiring-the-server-side">Wiring the Server Side</h1>
<h2 id="heading-the-viewcomponent">The ViewComponent</h2>
<p>The ViewComponent prepares the props as JSON and passes them to the Thymeleaf template. We define the props as Java records these are the <strong>single source of truth</strong> that we mirror in the TypeScript interface.</p>
<pre><code class="lang-java"><span class="hljs-comment">// KanbanComponent.java</span>
<span class="hljs-meta">@ViewComponent</span>
<span class="hljs-meta">@Controller</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">KanbanComponent</span> </span>{

  <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> ObjectMapper objectMapper;
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> KanbanService kanbanService;
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> LayoutViewComponent layoutViewComponent;

  <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">KanbanComponent</span><span class="hljs-params">(ObjectMapper objectMapper, KanbanService kanbanService,
      LayoutViewComponent layoutViewComponent)</span> </span>{
    <span class="hljs-keyword">this</span>.objectMapper = objectMapper;
    <span class="hljs-keyword">this</span>.kanbanService = kanbanService;
    <span class="hljs-keyword">this</span>.layoutViewComponent = layoutViewComponent;
  }

  <span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">KanbanCard</span><span class="hljs-params">(<span class="hljs-keyword">int</span> id, String title, String priority, String assignee)</span> </span>{}

  <span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">KanbanColumn</span><span class="hljs-params">(String id, String title, List&lt;KanbanCard&gt; cards)</span> </span>{}

  <span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">KanbanIslandProps</span><span class="hljs-params">(List&lt;KanbanColumn&gt; columns)</span> </span>{}

  <span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">KanbanView</span><span class="hljs-params">(String islandPropsJson)</span> implements IViewContext </span>{}

  <span class="hljs-meta">@GetMapping("/")</span>
  <span class="hljs-function"><span class="hljs-keyword">public</span> IViewContext <span class="hljs-title">index</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-keyword">return</span> layoutViewComponent.render(render());
  }

  <span class="hljs-function"><span class="hljs-keyword">public</span> KanbanView <span class="hljs-title">render</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-keyword">var</span> columns = kanbanService.getColumns();
    <span class="hljs-keyword">var</span> islandProps = <span class="hljs-keyword">new</span> KanbanIslandProps(columns);
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> KanbanView(objectMapper.writeValueAsString(islandProps));
  }
}
</code></pre>
<p>In detail:</p>
<ul>
<li><p><code>@ViewComponent @Controller</code> -&gt; the component is both a Spring MVC controller and a ViewComponent. The <code>@GetMapping</code> handles the page route, <code>render()</code> builds the view context.</p>
</li>
<li><p>Jackson automatically serializes Java records → no extra annotations needed</p>
</li>
<li><p><code>KanbanCard</code> and <code>KanbanColumn</code> are public nested records reused by both the service and the TypeScript interface</p>
</li>
<li><p>The board data comes from a service → the server controls the initial state. The island handles all client-side interactions (drag, edit, add). You could add HTMX calls to persist changes back to the server.</p>
</li>
</ul>
<h2 id="heading-the-kanbanservice">The KanbanService</h2>
<pre><code class="lang-java"><span class="hljs-comment">// KanbanService.java</span>
<span class="hljs-meta">@Service</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">KanbanService</span> </span>{

  <span class="hljs-function"><span class="hljs-keyword">public</span> List&lt;KanbanColumn&gt; <span class="hljs-title">getColumns</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-keyword">return</span> List.of(
        <span class="hljs-keyword">new</span> KanbanColumn(<span class="hljs-string">"todo"</span>, <span class="hljs-string">"To Do"</span>, List.of(
            <span class="hljs-keyword">new</span> KanbanCard(<span class="hljs-number">1</span>, <span class="hljs-string">"Design landing page"</span>, <span class="hljs-string">"high"</span>, <span class="hljs-string">"Alice"</span>),
            <span class="hljs-keyword">new</span> KanbanCard(<span class="hljs-number">2</span>, <span class="hljs-string">"Write API docs"</span>, <span class="hljs-string">"medium"</span>, <span class="hljs-string">"Bob"</span>)
        )),
        <span class="hljs-keyword">new</span> KanbanColumn(<span class="hljs-string">"progress"</span>, <span class="hljs-string">"In Progress"</span>, List.of(
            <span class="hljs-keyword">new</span> KanbanCard(<span class="hljs-number">3</span>, <span class="hljs-string">"Auth flow"</span>, <span class="hljs-string">"high"</span>, <span class="hljs-string">"Charlie"</span>)
        )),
        <span class="hljs-keyword">new</span> KanbanColumn(<span class="hljs-string">"done"</span>, <span class="hljs-string">"Done"</span>, List.of())
    );
  }
}
</code></pre>
<p>In a real app this would query a database. For our demo, hardcoded data is fine.</p>
<h2 id="heading-the-thymeleaf-template-just-a-shell">The Thymeleaf Template: Just a Shell</h2>
<pre><code class="lang-html"><span class="hljs-comment">&lt;!-- KanbanComponent.html --&gt;</span>
<span class="hljs-comment">&lt;!--/*@thymesVar id="kanbanView" type="de.tschuehly.preact_islands_demo.web.kanban.KanbanComponent.KanbanView"*/--&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"px-6 py-4"</span>&gt;</span>
  <span class="hljs-comment">&lt;!-- Preact Island mounts here --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">data-island</span>=<span class="hljs-string">"kanban/KanbanIsland"</span>
       <span class="hljs-attr">th:data-props</span>=<span class="hljs-string">"${kanbanView.islandPropsJson()}"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"loading loading-spinner loading-lg"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>In detail:</p>
<ul>
<li><p><code>data-island="kanban/KanbanIsland"</code> -&gt; maps to the output path <code>/js/islands/kanban/KanbanIsland.js</code></p>
</li>
<li><p><code>th:data-props="${kanbanView.islandPropsJson()}"</code> -&gt; injects the server-side props as JSON</p>
</li>
<li><p>The spinner inside the <code>div</code> is shown until Preact mounts and replaces the content, giving you a loading state for free</p>
</li>
</ul>
<h1 id="heading-type-safe-island-props">Type-Safe Island Props</h1>
<p>The TypeScript interface mirrors the Java records and lives right next to the island:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// KanbanIslandProps.d.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> KanbanCard {
  id: <span class="hljs-built_in">number</span>;
  title: <span class="hljs-built_in">string</span>;
  priority: <span class="hljs-built_in">string</span>;
  assignee: <span class="hljs-built_in">string</span>;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> KanbanColumn {
  id: <span class="hljs-built_in">string</span>;
  title: <span class="hljs-built_in">string</span>;
  cards: KanbanCard[];
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> KanbanIslandProps {
  columns: KanbanColumn[];
}
</code></pre>
<p>The TSX imports it with a simple relative path:</p>
<pre><code class="lang-tsx">import type {KanbanIslandProps, KanbanCard, KanbanColumn} from "./KanbanIslandProps";
</code></pre>
<p>When you add or change a field on a Java record, update the <code>.d.ts</code> to match. With a handful of islands, this takes seconds. The Bun compiler catches any mismatch between the <code>.d.ts</code> and your TSX on the next build. You get a type error instead of a runtime surprise.</p>
<p>If you add a field to <code>KanbanIslandProps</code> or <code>KanbanCard</code>, the TSX compiler catches any missing props. <strong>Type safety from Java to TypeScript.</strong></p>
<h1 id="heading-gradle-integration">Gradle Integration</h1>
<p>Let's wire everything into the build. Add these tasks to <code>build.gradle.kts</code>:</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// build.gradle.kts</span>
<span class="hljs-keyword">val</span> bunBinary: String = System.getenv(<span class="hljs-string">"BUN_INSTALL"</span>)?.let { <span class="hljs-string">"<span class="hljs-variable">$it</span>/bin/bun"</span> }
    ?: <span class="hljs-string">"<span class="hljs-subst">${System.getProperty(<span class="hljs-string">"user.home"</span>)}</span>/.bun/bin/bun"</span>

tasks.register&lt;Exec&gt;(<span class="hljs-string">"bunInstall"</span>) {
    commandLine(bunBinary, <span class="hljs-string">"install"</span>)
}

tasks.register&lt;Exec&gt;(<span class="hljs-string">"bunBuild"</span>) {
    dependsOn(<span class="hljs-string">"bunInstall"</span>)
    workingDir = file(<span class="hljs-string">"frontend"</span>)
    commandLine(bunBinary, <span class="hljs-string">"run"</span>, <span class="hljs-string">"build.ts"</span>)
}

tasks.register&lt;Exec&gt;(<span class="hljs-string">"bunWatch"</span>) {
    dependsOn(<span class="hljs-string">"bunInstall"</span>)
    workingDir = file(<span class="hljs-string">"frontend"</span>)
    commandLine(bunBinary, <span class="hljs-string">"run"</span>, <span class="hljs-string">"watch.ts"</span>)
}

tasks.named&lt;ProcessResources&gt;(<span class="hljs-string">"processResources"</span>) {
    dependsOn(<span class="hljs-string">"bunBuild"</span>)
}
</code></pre>
<p>Build chain: <code>bunInstall</code> -&gt; <code>bunBuild</code> -&gt; <code>processResources</code>.</p>
<p>In detail:</p>
<ul>
<li><p><code>bunInstall</code> runs first -&gt; ensures <code>node_modules/</code> is up to date</p>
</li>
<li><p><code>bunBuild</code> compiles the TSX with the <code>.d.ts</code> files in place -&gt; Bun's type checker catches mismatches between Java and TypeScript</p>
</li>
<li><p><code>bunWatch</code> is for development -&gt; runs alongside Spring Boot</p>
</li>
<li><p><code>processResources</code> depends on <code>bunBuild</code> -&gt; the compiled JS is in place before the app starts</p>
</li>
</ul>
<h2 id="heading-gitignore">.gitignore</h2>
<p>Remember to gitignore the build artifacts:</p>
<pre><code class="lang-plaintext">/frontend/node_modules/
/src/main/resources/static/js/islands/
</code></pre>
<p>The compiled JS in <code>islands/</code> is a build artifact. It gets regenerated on every build.</p>
<h1 id="heading-live-reload-in-development">Live Reload in Development</h1>
<p>For a great DX, we want TSX changes to appear in the browser instantly without restarting Spring Boot.</p>
<h2 id="heading-the-watch-script">The Watch Script</h2>
<p><code>frontend/watch.ts</code> watches for <code>.tsx</code> changes and triggers a rebuild:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// frontend/watch.ts</span>
<span class="hljs-keyword">import</span> {watch} <span class="hljs-keyword">from</span> <span class="hljs-string">"fs"</span>;
<span class="hljs-keyword">import</span> {resolve} <span class="hljs-keyword">from</span> <span class="hljs-string">"path"</span>;

<span class="hljs-keyword">const</span> scanRoot = resolve(<span class="hljs-keyword">import</span>.meta.dir, <span class="hljs-string">"../src/main/java"</span>);

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">build</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> proc = Bun.spawn([<span class="hljs-string">"bun"</span>, <span class="hljs-string">"run"</span>, <span class="hljs-string">"build.ts"</span>], {
    cwd: <span class="hljs-keyword">import</span>.meta.dir,
    stdout: <span class="hljs-string">"inherit"</span>,
    stderr: <span class="hljs-string">"inherit"</span>,
  });
  <span class="hljs-keyword">await</span> proc.exited;
}

<span class="hljs-keyword">await</span> build();

<span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Watching <span class="hljs-subst">${scanRoot}</span> for .tsx changes...`</span>);

<span class="hljs-keyword">let</span> debounce: Timer | <span class="hljs-literal">null</span> = <span class="hljs-literal">null</span>;
watch(scanRoot, {recursive: <span class="hljs-literal">true</span>}, <span class="hljs-function">(<span class="hljs-params">event, filename</span>) =&gt;</span> {
  <span class="hljs-keyword">if</span> (!filename?.endsWith(<span class="hljs-string">".tsx"</span>)) <span class="hljs-keyword">return</span>;
  <span class="hljs-keyword">if</span> (debounce) <span class="hljs-built_in">clearTimeout</span>(debounce);
  debounce = <span class="hljs-built_in">setTimeout</span>(<span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`\n<span class="hljs-subst">${filename}</span> changed, rebuilding...`</span>);
    <span class="hljs-keyword">await</span> build();
  }, <span class="hljs-number">200</span>);
});
</code></pre>
<p>Run it alongside your Spring Boot app:</p>
<pre><code class="lang-bash">./gradlew bunWatch
</code></pre>
<h2 id="heading-spring-boot-devtools-integration">Spring Boot DevTools Integration</h2>
<p>The watch script rebuilds the JS, but the browser still needs to reload. Spring DevTools has a LiveReload server, but it needs to know about island changes. Also, <code>.tsx</code> files in the classpath must <strong>not</strong> trigger a full app restart, that would defeat the purpose.</p>
<pre><code class="lang-java"><span class="hljs-comment">// IslandLiveReloadConfig.java</span>
<span class="hljs-meta">@Configuration</span>
<span class="hljs-meta">@Profile("dev")</span>
<span class="hljs-meta">@ConditionalOnClass(FileSystemWatcher.class)</span>
<span class="hljs-meta">@EnableConfigurationProperties(DevToolsProperties.class)</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">IslandLiveReloadConfig</span> </span>{

  <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> Logger logger = LoggerFactory.getLogger(IslandLiveReloadConfig.class);

  /**
   * Debounced LiveReloadServer -- prevents <span class="hljs-keyword">double</span>-reload when both
   * our island watcher and devtools classpath watcher fire.
   */
  <span class="hljs-meta">@Bean</span>
  <span class="hljs-meta">@RestartScope</span>
  <span class="hljs-function">LiveReloadServer <span class="hljs-title">liveReloadServer</span><span class="hljs-params">(DevToolsProperties properties)</span> </span>{
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> LiveReloadServer(
        properties.getLivereload().getPort(),
        Restarter.getInstance().getThreadFactory()
    ) {
      <span class="hljs-keyword">private</span> <span class="hljs-keyword">volatile</span> <span class="hljs-keyword">long</span> lastReloadTime = <span class="hljs-number">0L</span>;

      <span class="hljs-meta">@Override</span>
      <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">triggerReload</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">long</span> now = System.currentTimeMillis();
        <span class="hljs-keyword">if</span> (now - lastReloadTime &lt; <span class="hljs-number">3000</span>) {
          logger.debug(<span class="hljs-string">"Debounced duplicate livereload"</span>);
          <span class="hljs-keyword">return</span>;
        }
        lastReloadTime = now;
        <span class="hljs-keyword">super</span>.triggerReload();
      }
    };
  }

  <span class="hljs-comment">/**
   * Watches bun build output for changes and triggers LiveReload.
   */</span>
  <span class="hljs-meta">@Bean</span>
  <span class="hljs-function">FileSystemWatcher <span class="hljs-title">islandFileSystemWatcher</span><span class="hljs-params">(LiveReloadServer liveReloadServer)</span> </span>{
    <span class="hljs-keyword">var</span> watcher = <span class="hljs-keyword">new</span> FileSystemWatcher();
    <span class="hljs-keyword">var</span> islandsDir = <span class="hljs-keyword">new</span> File(<span class="hljs-string">"src/main/resources/static/js/islands"</span>);
    watcher.addSourceDirectory(islandsDir);
    watcher.addListener(changeSet -&gt; {
      <span class="hljs-keyword">boolean</span> hasJs = changeSet.stream()
          .flatMap(cf -&gt; cf.getFiles().stream())
          .anyMatch(f -&gt; f.getRelativeName().endsWith(<span class="hljs-string">".js"</span>));
      <span class="hljs-keyword">if</span> (hasJs) {
        liveReloadServer.triggerReload();
      }
    });
    watcher.start();
    <span class="hljs-keyword">return</span> watcher;
  }

  <span class="hljs-comment">/**
   * Exclude .tsx from triggering a full app restart.
   */</span>
  <span class="hljs-meta">@Bean</span>
  <span class="hljs-function">ClassPathRestartStrategy <span class="hljs-title">classPathRestartStrategy</span><span class="hljs-params">(DevToolsProperties properties)</span> </span>{
    <span class="hljs-keyword">var</span> delegate = <span class="hljs-keyword">new</span> PatternClassPathRestartStrategy(properties.getRestart().getAllExclude());
    <span class="hljs-keyword">return</span> changedFile -&gt; {
      <span class="hljs-keyword">if</span> (changedFile.getRelativeName().endsWith(<span class="hljs-string">".tsx"</span>))
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
      <span class="hljs-keyword">return</span> delegate.isRestartRequired(changedFile);
    };
  }
}
</code></pre>
<p>In detail:</p>
<ul>
<li><p><code>@RestartScope</code> on the <code>LiveReloadServer</code> bean -&gt; survives DevTools restarts, so we don't lose the debounce state</p>
</li>
<li><p><code>islandFileSystemWatcher</code> -&gt; watches <code>static/js/islands/</code> for new JS files and triggers LiveReload</p>
</li>
<li><p><code>classPathRestartStrategy</code> -&gt; prevents <code>.tsx</code> files from triggering a full Spring Boot restart</p>
</li>
</ul>
<p>This gives us a tight dev loop:</p>
<ol>
<li><p>Edit a <code>.tsx</code> file in IntelliJ</p>
</li>
<li><p><code>bunWatch</code> detects the change, rebuilds in ~50ms</p>
</li>
<li><p>The <code>FileSystemWatcher</code> sees new JS in <code>static/js/islands/</code></p>
</li>
<li><p>LiveReload triggers a browser refresh</p>
</li>
<li><p><strong>No Spring Boot restart required</strong></p>
</li>
</ol>
<h1 id="heading-how-the-pieces-fit-together">How the Pieces Fit Together</h1>
<pre><code class="lang-plaintext">  ┌──────────────────────┐
  │  Java records         │
  │  KanbanCard           │──── manually ────▶  KanbanIslandProps.d.ts
  │  KanbanColumn         │     mirrored        (TypeScript)
  │  KanbanIslandProps    │                        │
  └──────────┬────────────┘                        │
             │ ObjectMapper                        │ import type
  ┌──────────▼────────────┐                        │
  │   ViewComponent       │                        │
  │   objectMapper        │                        │
  │     .writeValueAsStr  │                        │
  └──────────┬────────────┘                        │
             │ data-props="{...}"                   │
  ┌──────────▼────────────┐                        │
  │   Thymeleaf Template  │                        │
  │   &lt;div data-island=   │                        │
  │        data-props= /&gt; │                        │
  └──────────┬────────────┘                        │
             │ HTML sent to browser                │
  ┌──────────▼────────────┐                        │
  │   mount.js            │                        │
  │   import → render     │                        │
  └──────────┬────────────┘                        │
             │                                     │
  ┌──────────▼────────────┐                        │
  │   Preact Island       │◀───────────────────────┘
  │   (.tsx)              │
  │   Composable pieces:  │
  │   Card                │
  │   Column              │
  │   AddCardForm         │
  └───────────────────────┘
</code></pre>
<h1 id="heading-why-preact-over-react">Why Preact over React?</h1>
<ul>
<li><p><strong>3KB</strong> gzipped vs React's 40KB+</p>
</li>
<li><p>Same JSX/hooks API – your React knowledge transfers 1:1</p>
</li>
<li><p><code>@preact/signals</code> with <code>useComputed</code> is lighter and more ergonomic than <code>useState</code> + <code>useMemo</code> chains</p>
</li>
<li><p>Bun builds a Preact island in ~50ms</p>
</li>
</ul>
<h1 id="heading-when-to-use-what">When to Use What</h1>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Use Case</td><td>Tool</td></tr>
</thead>
<tbody>
<tr>
<td>Toggle visibility, dropdowns, simple state</td><td>Alpine.js</td></tr>
<tr>
<td>Server-driven updates, form submissions</td><td>HTMX</td></tr>
<tr>
<td>Drag-and-drop, per-item state, deep component nesting</td><td>Preact Island</td></tr>
</tbody>
</table>
</div><p>The three coexist perfectly. HTMX handles 90% of interactions. Alpine.js handles UI toggles. Preact Islands handle the 5% of components where you need per-item component state, drag-and-drop, or deep composability with type safety.</p>
<h1 id="heading-summary">Summary</h1>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Before (Alpine.js)</td><td>After (Preact Island)</td></tr>
</thead>
<tbody>
<tr>
<td>All logic in one flat <code>x-data</code> block</td><td>Five focused components (<code>Card</code>, <code>Column</code>, <code>AddCardForm</code>, etc.)</td></tr>
<tr>
<td>One shared <code>editingCard</code> + <code>editForm</code></td><td>Each <code>Card</code> owns its own edit state via <code>useSignal</code></td></tr>
<tr>
<td>One shared <code>newCard</code> + <code>addingToCol</code></td><td>Each <code>AddCardForm</code> is an independent component instance</td></tr>
<tr>
<td>Drag state as global variables</td><td>Module-level <code>signal</code> shared across components</td></tr>
<tr>
<td>Nested <code>x-for</code> + <code>x-if</code> (80+ lines)</td><td>Clean component tree with props and callbacks</td></tr>
<tr>
<td>No type safety</td><td>Full TypeScript with Java-mirrored interfaces</td></tr>
<tr>
<td>Can't extract or test pieces</td><td>Components testable in isolation</td></tr>
</tbody>
</table>
</div><p>The server controls the initial board state and passes it as typed JSON props, with the Java record as the single source of truth for the TypeScript interface. The rest of the page remains server-rendered with HTMX. Preact handles the interactive island. The same approach works with Svelte aswell.</p>
<p>If you want to learn more about HTMX + Spring Boot check out my <a target="_blank" href="https://www.tschuehly.de/posts/spring-boot-htmx/">HTMX + Spring Boot series</a>. My side business <a target="_blank" href="https://photoquest.wedding/">PhotoQuest</a> is also built with this exact stack.</p>
<p>If you have questions, ping me on <a target="_blank" href="https://twitter.com/tschuehly">twitter.com/tschuehly</a>!</p>
]]></content:encoded></item><item><title><![CDATA[How I Used Claude Code to Plan a Complex Security Migration using AskUserQuestionTool]]></title><description><![CDATA[I recently migrated my SaaS application PhotoQuest from Supabase Auth to Spring Security
7.
The result? ~700 lines of custom auth code eliminated.
But the real story isn't the code. It's how two conversations with Claude Code shaped the entire approa...]]></description><link>https://tschuehly.de/claude-code-plan-security-migration</link><guid isPermaLink="true">https://tschuehly.de/claude-code-plan-security-migration</guid><category><![CDATA[claude-code]]></category><category><![CDATA[spring security]]></category><dc:creator><![CDATA[Thomas Schilling]]></dc:creator><pubDate>Sun, 11 Jan 2026 16:17:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768148172407/805888e7-8908-4099-b928-421a429fc245.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I recently migrated my SaaS application <a target="_blank" href="https://photoquest.wedding/">PhotoQuest</a> from Supabase Auth to Spring Security
7.</p>
<p>The result? <strong>~700 lines of custom auth code eliminated.</strong></p>
<p>But the real story isn't the code. It's how two conversations with Claude Code shaped the entire approach.</p>
<p>Planning complex migrations is one of those things developers usually do in their heads. We sketch something on a
whiteboard, maybe write a few notes, then dive into code, and figure out the edge cases as we go. Claude Code can
interview you about your architecture decisions and produce a comprehensive spec before you write a single line of code.</p>
<h1 id="heading-the-first-conversation-wrong-direction-right-process">The First Conversation: Wrong Direction, Right Process</h1>
<p>I started with a different goal. Every time I needed to get the current wedding/event, I was doing a 3-table join:</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// EventService.kt</span>
<span class="hljs-keyword">val</span> event = sql.select()
    .from(EVENT)
    .join(EVENT_MEMBER).on(EVENT_MEMBER.EVENT_ID.eq(EVENT.ID))
    .join(WEBSITE_USER).on(WEBSITE_USER.SUPABASE_ID.eq(EVENT_MEMBER.SUPABASE_USER_ID))
    .<span class="hljs-keyword">where</span>(WEBSITE_USER.SUPABASE_ID.eq(currentUserId))
    .fetchOne()
</code></pre>
<p>My idea: put <code>event_id</code> directly in the Supabase JWT. The query becomes a simple lookup:</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// EventService.kt (refactored)</span>
<span class="hljs-keyword">val</span> event = sql.selectFrom(EVENT)
    .<span class="hljs-keyword">where</span>(EVENT.ID.eq(eventIdFromJwt))
    .fetchOne()
</code></pre>
<p>Much cleaner, right?</p>
<p>Let's first ask Claude to interview me about this refactor:</p>
<pre><code>Currently I implemented multiple users <span class="hljs-keyword">for</span> one event via the event_member table.
I was thinking <span class="hljs-keyword">of</span> refactoring <span class="hljs-built_in">this</span> and putting the event_id <span class="hljs-keyword">in</span> the JWT <span class="hljs-keyword">of</span> the SupabaseUser.

Interview me <span class="hljs-keyword">in</span> detail using the AskUserQuestionTool about literally anything:
technical implementation, UI &amp; UX, concerns, tradeoffs, etc.
</code></pre><p>The key here is asking Claude to use the <code>AskUserQuestionTool</code>. This triggers an interactive interview mode
where Claude asks you questions one by one, rather than making assumptions.</p>
<p>Claude produced a comprehensive spec and wrote it to <code>docs/TODO/event-member-refactor-spec.md</code>.</p>
<p><strong>Summary of key decisions from interview:</strong></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Area</td><td>Decision</td></tr>
</thead>
<tbody>
<tr>
<td>JWT claims</td><td><code>active_event_id</code> + <code>active_event_role</code> (CREATOR/MANAGER only)</td></tr>
<tr>
<td>Multi-event</td><td>Single active event, switcher in settings page</td></tr>
<tr>
<td>event_member</td><td>Remove entirely after migration</td></tr>
<tr>
<td>Owner tracking</td><td>New <code>event.owner_user_id</code> column replaces CREATOR role</td></tr>
<tr>
<td>Relation</td><td>Move to <code>website_user</code>, nullable, single-relation accepted</td></tr>
<tr>
<td>onboarding_step</td><td>Remove</td></tr>
<tr>
<td>JWT update</td><td>Direct SQL to <code>auth.users.raw_app_meta_data</code></td></tr>
<tr>
<td>Event creation</td><td>PENDING → ACTIVE status flow, email admin on JWT failure</td></tr>
<tr>
<td>Invitation accept</td><td>Force JWT refresh via redirect to <code>/api/user/jwt</code></td></tr>
<tr>
<td>Starter library</td><td>Breaking change, expose <code>appMetadata</code> as raw Map</td></tr>
<tr>
<td>Migration</td><td>6 phases: prep → starter → dual-write → dual-read → data → cleanup</td></tr>
</tbody>
</table>
</div><p>Then I asked one question that changed everything:</p>
<blockquote>
<p><strong>Me:</strong> What if we moved away from Supabase Auth and just used Spring Security?</p>
</blockquote>
<p>What followed was an interview that systematically explored every dimension of this alternative. Claude didn't just
say yes or no. It asked questions that forced me to think through the implications.</p>
<h2 id="heading-the-systematic-interview">The Systematic Interview</h2>
<p>Claude started with the fundamentals:</p>
<blockquote>
<p><strong>Claude:</strong> What's driving the consideration to move away from Supabase Auth? Is it the complexity of JWT claim
management we just discussed, or other factors?</p>
</blockquote>
<p>My answer: "Want more control over auth flow."</p>
<blockquote>
<p><strong>Claude:</strong> Supabase Auth currently handles: anonymous sign-in, email magic links, OAuth (Google/Apple/Azure), email
confirmation flow. Which of these do you actually need long-term?</p>
</blockquote>
<p>My answer: "All of them I need feature parity."</p>
<p>This established the scope. Then Claude drilled into the technical tradeoffs:</p>
<h2 id="heading-session-vs-jwt">Session vs JWT</h2>
<blockquote>
<p><strong>Claude:</strong> For anonymous sign-in with Spring Security: this typically requires session-based auth (cookie), not JWT.
Are you okay with session-based auth, or do you want stateless JWT throughout?</p>
</blockquote>
<p>My answer: "Haven't thought about it, need guidance."</p>
<blockquote>
<p><strong>Claude:</strong> Session vs JWT tradeoff: Sessions are simpler (no refresh tokens, instant revocation, easy to store
event_id), but require sticky sessions or shared session store for horizontal scaling. What's your current/planned
deployment?</p>
</blockquote>
<p>My answer: "Multiple instances, no shared store yet."</p>
<blockquote>
<p><strong>Claude:</strong> For multiple instances without a shared store: you'll need either Redis for sessions, or go stateless JWT.
Redis adds infrastructure but simplifies auth. JWT adds token refresh complexity. Preference?</p>
</blockquote>
<p>My answer: "Go JWT, accept refresh complexity."</p>
<p>This chain of questions narrowed down the architecture. Each answer constrained the next decision.</p>
<p>In detail:</p>
<ul>
<li><code>Session-based auth</code> → simpler token management, but requires shared session store for scaling</li>
<li><code>JWT</code> → stateless and scales easily, but adds refresh token complexity</li>
<li>I chose JWT because I didn't want to add Redis just for sessions</li>
</ul>
<h2 id="heading-anonymous-user-flow">Anonymous User Flow</h2>
<p>The anonymous→verified user flow was critical. Claude mapped it out:</p>
<blockquote>
<p><strong>Claude:</strong> Anonymous users are currently Supabase anonymous auth. With Spring Security, options: (A) guest session
with upgrade path, (B) lazy account creation on first action, (C) require sign-up upfront. Which fits your UX?</p>
</blockquote>
<p>My answer: "Keep current flow: anon -&gt; email confirmation."</p>
<blockquote>
<p><strong>Claude:</strong> For anonymous-&gt;confirmed user flow with JWT: the anon user gets a JWT, then confirms email, and the JWT
needs to be replaced with one containing their verified identity. How should this transition work?</p>
</blockquote>
<p>My answer: "The Anon JWT needs to be long lived as I want the user to convert at a later date. What do you think?"</p>
<blockquote>
<p><strong>Claude:</strong> Long-lived anon JWT that converts: the cleanest approach is to keep the same user ID (UUID) throughout,
just upgrade the 'verified' flag and add email claim on confirmation. The JWT gets replaced but user identity
persists.
Sound right?</p>
</blockquote>
<p>My answer: "Yes, same user ID, upgrade claims."</p>
<h2 id="heading-infrastructure-decisions">Infrastructure Decisions</h2>
<p>Claude systematically uncovered my infrastructure constraints:</p>
<blockquote>
<p><strong>Claude:</strong> Email magic links without Supabase means you need: email sending, token generation/validation, rate
limiting. Do you have email infrastructure already (SES, Resend, etc.)?</p>
</blockquote>
<p>My answer: "Yes, already using SES/similar."</p>
<blockquote>
<p><strong>Claude:</strong> Rate limiting and brute-force protection: Supabase handles this for you. With Spring Security, you'll need
to implement it. Do you have rate limiting infrastructure (Redis, bucket4j, etc.)?</p>
</blockquote>
<p>My answer: "No rate limiting infra yet."</p>
<blockquote>
<p><strong>Claude:</strong> For rate limiting without Redis: bucket4j can use JCache with local cache (Caffeine) for single-instance,
, or PostgreSQL for distributed. Given your multi-instance setup, prefer DB-backed rate limiting or add Redis?</p>
</blockquote>
<p>My answer: "DB-backed rate limiting (simpler infra)."</p>
<p>Each question built on the previous answer. The "no Redis" constraint shaped everything that followed.</p>
<h2 id="heading-the-tradeoff-explanations">The Tradeoff Explanations</h2>
<p>When I needed guidance, Claude explained the options in detail:</p>
<h2 id="heading-magic-link-token-storage">Magic Link Token Storage</h2>
<blockquote>
<p><strong>Claude:</strong> Let me explain the magic link token tradeoffs:</p>
<p><strong>Option A: Dedicated table</strong> - Easy revocation, can track usage, simple expiry queries. Cons: DB write per request.</p>
<p><strong>Option B: Stateless signed token</strong> - No DB storage, scales infinitely. Cons: Cannot revoke before expiry.</p>
<p><strong>Option C: Hybrid</strong> - Fast validation (check signature first), DB only for revocation.</p>
<p><strong>Recommendation:</strong> Given you want anon → confirmed conversion and might need to invalidate tokens, I'd lean toward
Option A - the DB overhead is minimal for magic links (low volume).</p>
</blockquote>
<h2 id="heading-csrf-protection">CSRF Protection</h2>
<blockquote>
<p><strong>Claude:</strong> <code>Sec-Fetch-Site</code> is a modern approach to CSRF protection. The header is browser-set (cannot be forged):</p>
<ul>
<li><code>same-origin</code>: Request from your domain</li>
<li><code>cross-site</code>: Request from different domain</li>
</ul>
<p>For CSRF protection, you'd reject state-changing requests where <code>Sec-Fetch-Site: cross-site</code>.</p>
<p><strong>Pros:</strong> Simpler than CSRF tokens, no JS configuration needed for HTMX.
<strong>Cons:</strong> Older browsers don't send it (Safari &lt;16.4).</p>
<p><strong>Recommendation:</strong> This works well for PhotoQuest since you target modern browsers (wedding guests with smartphones).</p>
</blockquote>
<h2 id="heading-the-pivot-moment">The Pivot Moment</h2>
<p>After 14 tool calls and 6 messages, Claude had mapped out the entire architecture. I asked the final question:</p>
<blockquote>
<p><strong>Claude:</strong> Should this Spring Security migration be a separate spec, or should I update the existing
event-member-refactor-spec to replace the Supabase JWT approach entirely?</p>
</blockquote>
<p>My answer: "Combine into one comprehensive auth refactor spec."</p>
<blockquote>
<p><strong>Claude:</strong> Should I keep the phased approach where Supabase JWT refactor is Phase 1 (quick win), and Spring Security
migration is Phase 2 (full control)? Or skip Supabase refactor entirely and go straight to Spring Security?</p>
</blockquote>
<p>My answer: "Skip Supabase refactor, go straight to Spring."</p>
<p>Claude rewrote the entire spec. The original Supabase JWT approach was replaced with a comprehensive Spring Security
migration. But I realized we'd only scratched the surface.</p>
<h1 id="heading-the-second-conversation-deep-dive-interview">The Second Conversation: Deep-Dive Interview</h1>
<p>The next day, I started fresh with the Spring Security spec. This time I wanted a thorough interview:</p>
<pre><code>Read <span class="hljs-built_in">this</span> @docs/TODO/supabase-auth-refactor-spec.md and interview me <span class="hljs-keyword">in</span> detail
about literally anything: technical implementation, UI &amp; UX, concerns, tradeoffs,
etc. but make sure the questions are not obvious - be very <span class="hljs-keyword">in</span>-depth and <span class="hljs-keyword">continue</span>
interviewing me continually until it<span class="hljs-string">'s complete

Lookup Spring Security best practices: query-docs
/websites/spring_io_spring-security_reference_7_0 {query}</span>
</code></pre><p>💡
I'm using the <code>@</code> symbol to reference a file directly in the prompt. Claude Code will read the file and include it in context. The <code>query-docs</code> part tells Claude to look up Spring Security documentation as needed.</p>
<p>What followed: <strong>15+ rounds of probing questions</strong>. Claude also looked up Spring Security 7 documentation in real-time.
This meant the interview was grounded in current best practices, not outdated patterns from Stack Overflow circa 2019.</p>
<p>Here are some questions that forced me to think:</p>
<h2 id="heading-session-amp-cookie-edge-cases">Session &amp; Cookie Edge Cases</h2>
<blockquote>
<p><strong>Claude:</strong> For Guest users with device cookies: What's the recovery strategy when a guest clears cookies mid-upload
or switches devices?</p>
</blockquote>
<p>My answer: Accept data loss. Guests must stay on one device. Simple beats complex.</p>
<blockquote>
<p><strong>Claude:</strong> Session timeout is 30 minutes. For wedding guests who might take photos over 3–4 hours, what's the
acceptable re-auth UX?</p>
</blockquote>
<p>My answer: Use persistent remember-me tokens. This led to adding <code>persistent_logins</code> table.</p>
<h2 id="heading-oauth-provider-quirks">OAuth Provider Quirks</h2>
<blockquote>
<p><strong>Claude:</strong> Apple OAuth may not return email on subsequent logins. How do we handle account linking when email is
missing?</p>
</blockquote>
<p>My answer: Require email on first Apple login only. Store in <code>oauth_account.provider_email</code>.</p>
<h2 id="heading-security-vs-ux-tradeoffs">Security vs. UX Tradeoffs</h2>
<blockquote>
<p><strong>Claude:</strong> If oneTimeTokenLogin validation fails (expired, invalid, already used), should we show specific error
message or generic 'Invalid link'?</p>
</blockquote>
<p>Security says generic. UX says specific. I chose specific: "Link expired", "Link already used". The attack surface is
minimal.</p>
<h2 id="heading-catching-claudes-confusion">Catching Claude's Confusion</h2>
<p>The best moment: I caught Claude mixing concepts.</p>
<blockquote>
<p><strong>Claude:</strong> Anonymous user creation: When should anon users be created? On first visit, or only when they perform
action?</p>
</blockquote>
<p>My answer: "You are mixing guest and anon users. Anon users are brides trying the demo. Guests don't have an account."</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">This is why the interview format works. Claude's confusion surfaced a domain concept that was only in my head. By catching it early, we avoided building the wrong abstractions.</div>
</div>

<p>This clarification became a table in the final spec:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>User Type</td><td>Purpose</td><td>Auth Mechanism</td><td>Has Session?</td></tr>
</thead>
<tbody>
<tr>
<td><strong>Guest</strong></td><td>Wedding guest uploading</td><td>Device cookie</td><td>No</td></tr>
<tr>
<td><strong>Anonymous</strong></td><td>Bride trying demo</td><td>Session (is_anonymous=true)</td><td>Yes</td></tr>
<tr>
<td><strong>Verified</strong></td><td>Paid customer</td><td>Session (email verified)</td><td>Yes</td></tr>
</tbody>
</table>
</div><h2 id="heading-asking-claude-for-recommendations">Asking Claude for Recommendations</h2>
<blockquote>
<p><strong>Claude:</strong> Integration tests should cover magic link + OAuth flows. For OAuth, should we mock provider responses or
use Spring Security's OAuth2 test support?</p>
</blockquote>
<p>My answer: "What would you recommend?"</p>
<p>Claude suggested Spring Security's built-in test support plus GreenMail for E2E magic link tests. Both made it into the
spec.</p>
<h1 id="heading-the-output">The Output</h1>
<p>Two conversations. <strong>900-line specification</strong>. And we are done!</p>
<p>Here's what the final spec covered:</p>
<ul>
<li>Database migrations (6 tables: <code>website_user</code>, <code>oauth_account</code>, <code>magic_link_token</code>, <code>device_cookie</code>,
<code>persistent_logins</code>, <code>rate_limit_bucket</code>)</li>
<li>Complete <code>SecurityConfig.kt</code> with remember-me, CSRF, session fixation protection</li>
<li>Unified <code>PhotoQuestUser</code> implementing both <code>UserDetails</code> and <code>OAuth2User</code></li>
<li>Account deletion service with ownership transfer checks</li>
<li>Anonymous user cleanup job (7-day grace period)</li>
<li>Data migration strategy with email normalization</li>
</ul>
<h1 id="heading-why-this-works">Why This Works</h1>
<p><strong>Architecture exploration.</strong> The first conversation explored Supabase JWT claims in depth. This made the pivot
question ("what about Spring Security?") more informed. Sometimes you need to plan the wrong thing to discover the right
thing.</p>
<p><strong>Rubber duck that asks questions.</strong> Claude forced me to articulate decisions I would have made implicitly – and
probably inconsistently. Unlike a real rubber duck, this one pushes back.</p>
<p><strong>Documentation as a side effect.</strong> The interview process produced a spec that's actually useful for implementation. No
more "I'll document it later" (We all know how that ends).</p>
<p><strong>Edge case discovery.</strong> Questions like "what if Apple OAuth doesn't return email?" aren't obvious when you're thinking
about the happy path. Claude surfaces these before they become production bugs.</p>
<h1 id="heading-try-it-yourself">Try It Yourself</h1>
<p>For your next complex feature or migration:</p>
<ol>
<li>Write a rough spec or design doc</li>
<li>Ask Claude to interview you using <code>AskUserQuestionTool</code></li>
<li>Be open to pivots – if Claude's questions reveal a better approach, explore it</li>
<li>Answer honestly – including "I don't know" or "what would you recommend?"</li>
<li>Let Claude write the updated spec</li>
</ol>
<p>The interview format forces you to make decisions explicitly – rather than discovering them during implementation.</p>
<p>And sometimes the right question changes the entire direction.</p>
<hr />
<p>If you want to learn more about building with Spring Security, HTMX, and server-side rendering, check out
my <a target="_blank" href="https://tschuehly.de/posts/htmx-tailwindcss-spring-boot/">HTMX + Spring Boot series</a>. My side
business <a target="_blank" href="https://photoquest.wedding/">PhotoQuest</a> is also built entirely with this stack – no JavaScript framework
headaches, just clean server-rendered HTML with htmx for interactivity.</p>
<p>If you have questions about using Claude Code for architecture planning, ping me
on <a target="_blank" href="https://twitter.com/tschuehly">twitter.com/tschuehly</a>!</p>
]]></content:encoded></item><item><title><![CDATA[Deploy your Spring Boot Application in minutes instead of hours with Dokploy on a Hetzner VPS]]></title><description><![CDATA[I’ve recently started using Dokploy for getting my projects to production quickly. It is a great alternative to Coolify.
Some of the features that are really nice to have:

Continuous deployment using GitHub

System Monitoring

Paketo Buildpack suppo...]]></description><link>https://tschuehly.de/deploy-your-spring-boot-application-in-minutes-instead-of-hours-with-dokploy-on-a-hetzner-vps</link><guid isPermaLink="true">https://tschuehly.de/deploy-your-spring-boot-application-in-minutes-instead-of-hours-with-dokploy-on-a-hetzner-vps</guid><category><![CDATA[dokploy]]></category><category><![CDATA[Hetzner]]></category><category><![CDATA[deployment]]></category><category><![CDATA[Springboot]]></category><category><![CDATA[Spring]]></category><category><![CDATA[Docker]]></category><category><![CDATA[System administration]]></category><dc:creator><![CDATA[Thomas Schilling]]></dc:creator><pubDate>Sat, 26 Oct 2024 08:01:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1729929619557/50af314e-da6a-4692-88f4-866ffd797ef9.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I’ve recently started using <a target="_blank" href="https://dokploy.com/">Dokploy</a> for getting my projects to production quickly. It is a great alternative to Coolify.</p>
<p>Some of the features that are really nice to have:</p>
<ul>
<li><p>Continuous deployment using GitHub</p>
</li>
<li><p>System Monitoring</p>
</li>
<li><p>Paketo Buildpack support</p>
</li>
<li><p>One Click Database deployment</p>
</li>
<li><p>Automatic SSL certificates with LetsEncrypt</p>
</li>
</ul>
<p>I personally use Hetzner for my VPS needs: <a target="_blank" href="https://hetzner.cloud/?ref=DgEuiDbXP7tf">Get 20€ credit in Hetzner-Cloud for free with this link!</a></p>
<h1 id="heading-creating-a-secure-vps-on-hetzner">Creating a Secure VPS on Hetzner</h1>
<p>First we will need to create an ssh keypair: <code>ssh-keygen -t ed25519</code></p>
<p>Then go to <a target="_blank" href="https://console.hetzner.cloud/projects">https://console.hetzner.cloud/projects</a> and create a new VPS.</p>
<p>During the setup add this cloud-init script in the Cloud Config section. We need to replace <code>YOUR_SSH_PUBLIC_KEY</code> with the public key we just generated.</p>
<pre><code class="lang-yaml"><span class="hljs-comment">#cloud-config</span>
<span class="hljs-attr">users:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">tschuehly</span>
    <span class="hljs-attr">groups:</span> <span class="hljs-string">users,</span> <span class="hljs-string">admin,</span> <span class="hljs-string">docker</span>
    <span class="hljs-attr">sudo:</span> <span class="hljs-string">ALL=(ALL)</span> <span class="hljs-string">NOPASSWD:ALL</span>
    <span class="hljs-attr">shell:</span> <span class="hljs-string">/bin/bash</span>
    <span class="hljs-attr">ssh_authorized_keys:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">YOUR_SSH_PUBLIC_KEY</span>
<span class="hljs-attr">packages:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">fail2ban</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">ufw</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">unattended-upgrades</span>
<span class="hljs-attr">package_update:</span> <span class="hljs-literal">true</span>
<span class="hljs-attr">package_upgrade:</span> <span class="hljs-literal">true</span>
<span class="hljs-attr">runcmd:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">printf</span> <span class="hljs-string">"[sshd]\nenabled = true\nbanaction = iptables-multiport"</span> <span class="hljs-string">&gt;</span> <span class="hljs-string">/etc/fail2ban/jail.local</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">systemctl</span> <span class="hljs-string">enable</span> <span class="hljs-string">fail2ban</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">ufw</span> <span class="hljs-string">allow</span> <span class="hljs-string">OpenSSH</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">ufw</span> <span class="hljs-string">enable</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">sed</span> <span class="hljs-string">-i</span> <span class="hljs-string">-e</span> <span class="hljs-string">'/^\(#\|\)PermitRootLogin/s/^.*$/PermitRootLogin no/'</span> <span class="hljs-string">/etc/ssh/sshd_config</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">sed</span> <span class="hljs-string">-i</span> <span class="hljs-string">-e</span> <span class="hljs-string">'/^\(#\|\)PasswordAuthentication/s/^.*$/PasswordAuthentication no/'</span> <span class="hljs-string">/etc/ssh/sshd_config</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">sed</span> <span class="hljs-string">-i</span> <span class="hljs-string">-e</span> <span class="hljs-string">'/^\(#\|\)KbdInteractiveAuthentication/s/^.*$/KbdInteractiveAuthentication no/'</span> <span class="hljs-string">/etc/ssh/sshd_config</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">sed</span> <span class="hljs-string">-i</span> <span class="hljs-string">-e</span> <span class="hljs-string">'/^\(#\|\)ChallengeResponseAuthentication/s/^.*$/ChallengeResponseAuthentication no/'</span> <span class="hljs-string">/etc/ssh/sshd_config</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">sed</span> <span class="hljs-string">-i</span> <span class="hljs-string">-e</span> <span class="hljs-string">'/^\(#\|\)MaxAuthTries/s/^.*$/MaxAuthTries 2/'</span> <span class="hljs-string">/etc/ssh/sshd_config</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">sed</span> <span class="hljs-string">-i</span> <span class="hljs-string">-e</span> <span class="hljs-string">'/^\(#\|\)AllowTcpForwarding/s/^.*$/AllowTcpForwarding no/'</span> <span class="hljs-string">/etc/ssh/sshd_config</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">sed</span> <span class="hljs-string">-i</span> <span class="hljs-string">-e</span> <span class="hljs-string">'/^\(#\|\)X11Forwarding/s/^.*$/X11Forwarding no/'</span> <span class="hljs-string">/etc/ssh/sshd_config</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">sed</span> <span class="hljs-string">-i</span> <span class="hljs-string">-e</span> <span class="hljs-string">'/^\(#\|\)AllowAgentForwarding/s/^.*$/AllowAgentForwarding no/'</span> <span class="hljs-string">/etc/ssh/sshd_config</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">sed</span> <span class="hljs-string">-i</span> <span class="hljs-string">-e</span> <span class="hljs-string">'/^\(#\|\)AuthorizedKeysFile/s/^.*$/AuthorizedKeysFile .ssh\/authorized_keys/'</span> <span class="hljs-string">/etc/ssh/sshd_config</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">sed</span> <span class="hljs-string">-i</span> <span class="hljs-string">'$a AllowUsers tschuehly'</span> <span class="hljs-string">/etc/ssh/sshd_config</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">reboot</span>
</code></pre>
<h1 id="heading-installing-dokploy">Installing Dokploy</h1>
<p>To install Dokploy, we need to ssh into the VPS, I personally use <a target="_blank" href="https://termius.com/">Termius</a> as SSH client, but we can also use ssh directly:</p>
<pre><code class="lang-bash">ssh tschuehly@94.130.99.182
</code></pre>
<p>Then run this command to install Dokploy: `</p>
<pre><code class="lang-bash">curl -sSL https://dokploy.com/install.sh | sudo sh
</code></pre>
<p>After a few minutes we should see a success message:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729883158437/e642f2fd-e77e-4f6b-8c97-f38b0aaab6df.png" alt class="image--center mx-auto" /></p>
<p>We can now navigate to the dokploy instance and setup the admin account</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729883376995/81ee69eb-b080-44a0-9cd7-302db22764d6.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-connect-your-github-account">Connect your GitHub Account</h1>
<p>Now we need to connect our GitHub Account. Click on <code>Settings</code>, then on <code>Git</code>, then on <code>GitHub</code> and then <code>Create GitHub App</code> . Follow the Wizard and then click on <code>Install App</code> . Either give Dokploy access to all your repositories or only the one you want to deploy.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729884162733/169690be-7c18-4867-acad-c23142e8313c.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-deploy-your-spring-application">Deploy your Spring Application!</h1>
<p>I will deploy my <a target="_blank" href="https://github.com/tschuehly/htmx-spring-workshop">htmx-spring-workshop</a> application. Let’s first create a new project!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729883506056/6d2f3f72-e460-43b8-8669-0596f7499ff3.png" alt class="image--center mx-auto" /></p>
<p>Now we need to create a new application. Give it a name and then click on create!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729883593820/bd8aaa23-ad4f-45ce-89d1-4f12ad1d7a86.png" alt class="image--center mx-auto" /></p>
<p>We can now select the GitHub App, the repository, the branch and also the path of the Spring Application if your repository is a monorepo.</p>
<p>We will select Paketo Buildpacks, because <a target="_blank" href="https://www.youtube.com/watch?v=cfwjdUS7nAU">friends don't let friends use Dockerfiles</a>!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729884254109/90ede50c-b9fd-4b5b-8a18-64826a263dbb.png" alt class="image--center mx-auto" /></p>
<p>Before deploying we need to set the Java Version for the build process in the Environment Tab:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729884729761/c01bb1cf-1258-40ec-bdbe-d2132acc0471.png" alt class="image--center mx-auto" /></p>
<p>To configure a domain we can create a new DNS A record, it is possible to use a wildcard to only have to do it once.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729885478929/6b6aca97-5085-4b36-876e-28d58cf2ca5c.png" alt class="image--center mx-auto" /></p>
<p>Then we will configure the domain, and enable HTTPS with LetsEncrypt.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729885698560/baa9583c-a92e-4dd9-9855-d9e2ce08e02b.png" alt class="image--center mx-auto" /></p>
<p>Finally we can click on deploy:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729885002068/d94ccd4f-7231-400e-b109-3e11e686d89b.png" alt class="image--center mx-auto" /></p>
<p>We can now see the progress of the deployment:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729885071609/4c382cd7-6c83-45ff-a252-670cbacd1d23.png" alt class="image--center mx-auto" /></p>
<p>After a couple of minutes we can see that our application successfully started:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729885659914/2f92d896-ec7a-430e-9ffe-bf8671e25852.png" alt class="image--center mx-auto" /></p>
<p>And we are finished! Our application is live! <a target="_blank" href="https://htmx.tschuehly.de/">https://htmx.tschuehly.de/</a></p>
<p>If we now commit to the selected branch, the new application will automatically be rebuilt and redeployed.</p>
<h1 id="heading-deploy-a-database">Deploy a Database!</h1>
<p>We can also easily deploy a Database like PostgreSQL.</p>
<p>Click on <code>Create Service</code> and then <code>Database</code>. Choose your favourite database and fill in the details.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729885911831/e2266df9-fb54-40a5-b05b-bc5d9d1a3e4e.png" alt class="image--center mx-auto" /></p>
<p>You can then click on <code>Deploy</code>. All the details to connect to your database are listed:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729885974553/36850274-7a91-4d85-b218-59390b4be454.png" alt class="image--center mx-auto" /></p>
<p>We can then connect to the database with our Spring App by setting the environment variables:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729886765672/f2cc9695-7388-4fc5-ae21-3cab02dd58d5.png" alt class="image--center mx-auto" /></p>
<p>And we are done!<br />We now have a SSL secured Spring Boot Application with a PostgreSQL database in production.</p>
<p>I deploy my own SaaS application this way: <a target="_blank" href="https://photoquest.wedding/">PhotoQuest</a></p>
]]></content:encoded></item><item><title><![CDATA[JTE is now available on start.spring.io!]]></title><description><![CDATA[My favourite template engine JTE (the Java Template Engine) is now available on start.spring.io.
It compiles the templates into native java classes offering compile time safety. With hot reloading and the IntelliJ plugin it makes me really productive...]]></description><link>https://tschuehly.de/jte-is-now-available-on-startspringio</link><guid isPermaLink="true">https://tschuehly.de/jte-is-now-available-on-startspringio</guid><category><![CDATA[Springboot]]></category><category><![CDATA[Spring]]></category><category><![CDATA[Spring framework]]></category><dc:creator><![CDATA[Thomas Schilling]]></dc:creator><pubDate>Mon, 30 Sep 2024 14:43:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1727706357714/90e6e109-a84a-46c6-8d93-1289ff689a01.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>My favourite template engine <a target="_blank" href="https://jte.gg/">JTE</a> (the Java Template Engine) is now available on <a target="_blank" href="https://start.spring.io/">start.spring.io</a>.</p>
<p>It compiles the templates into native java classes offering compile time safety. With hot reloading and the IntelliJ plugin it makes me really productive.</p>
<p>My tutorial <a target="_blank" href="https://tschuehly.de/interactive-web-applications-with-htmx-and-spring-boot">Interactive web applications with htmx and Spring Boot</a> gives a short introduction to JTE. My workshop <a target="_blank" href="https://tschuehly.gitbook.io/server-side-spring-htmx-workshop">Building server-side web applications with htmx</a> also uses JTE.</p>
<p>After running my business for one year in production with Thymeleaf I recently decided to migrate to JTE as the developer experience is much better especially when refactoring model properties.</p>
<p>JTE is also one of the supported template languages of <a target="_blank" href="https://github.com/tschuehly/spring-view-component">Spring ViewComponent</a></p>
<p>If you want to learn more about HTMX + Spring Boot check out my series <a target="_blank" href="https://tschuehly.de/series/full-stack">Web development without the JavaScript headache with Spring + HTMX</a>.</p>
<p>My side business <a target="_blank" href="https://photoquest.wedding/">PhotoQuest</a> is also built with HTMX + JTE</p>
]]></content:encoded></item><item><title><![CDATA[The best way to build Spring Boot applications with htmx]]></title><description><![CDATA[Why should you even care?
Spring Boot developers usually build JSON APIs that get client-side rendered on the browser.
Marcus Hellberg tells us why we should aim to build a full-stack team:

But with the rise in complexity in modern JavaScript framew...]]></description><link>https://tschuehly.de/the-best-way-to-build-spring-boot-applications-with-htmx</link><guid isPermaLink="true">https://tschuehly.de/the-best-way-to-build-spring-boot-applications-with-htmx</guid><category><![CDATA[htmx]]></category><category><![CDATA[Springboot]]></category><category><![CDATA[Spring framework]]></category><category><![CDATA[Server side rendering]]></category><category><![CDATA[hypermedia]]></category><dc:creator><![CDATA[Thomas Schilling]]></dc:creator><pubDate>Mon, 01 Jul 2024 16:22:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1717305647325/feac06d5-44dc-4cee-976a-f039dca3ff58.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-why-should-you-even-care">Why should you even care?</h2>
<p>Spring Boot developers usually build JSON APIs that get client-side rendered on the browser.</p>
<p><a target="_blank" href="https://www.youtube.com/watch?v=LeYF1NE3jQ4">Marcus Hellberg</a> tells us why we should aim to build a full-stack team:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1719769891389/afa9fab8-76d3-4592-ada5-d1716553056c.png" alt class="image--center mx-auto" /></p>
<p>But with the rise in complexity in modern JavaScript frameworks and the constant change it's hard to keep up!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1719770345129/65daae9d-c4ba-4a09-b6dc-a651f79023e5.png" alt class="image--center mx-auto" /></p>
<p>The browser can only display HTML! Why send JSON to the client that needs to be rendered to HTML? We can just remove this unnecessary complexity!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1719770634620/c85d28c6-c159-4dc1-8fca-157197c4b5b0.png" alt class="image--center mx-auto" /></p>
<p>We can render data directly into a HTML template on the server with an engine like <a target="_blank" href="https://jte.gg/">JTE</a>. We then return HTML to the browser directly.</p>
<p>But we lost the interactivity of the JavaScript framework. We can return it by leveraging Hypermedia.</p>
<h2 id="heading-hypermedia-driven-applications">Hypermedia Driven Applications</h2>
<p>The core of a hypermedia driven application is hypertext. HTML defines how the application looks and how the user can interact with the application based on the current state of the application.</p>
<p>The underlying principle is "Hypermedia as Engine of Application State".</p>
<p>A simple example is an embedded form that returns a success message on successful submission:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1719771493712/42efd676-fabf-4063-86b9-4d76514f5542.gif" alt class="image--center mx-auto" /></p>
<p>We want a great user experience with our server-side rendered web application.</p>
<h3 id="heading-htmx">htmx</h3>
<p>The <strong>htmx</strong> library enables modern UX that is comparable to a single page application.<br />htmx completes HTML as a hypertext and brings it to the 21st century.</p>
<blockquote>
<p>The harsh reality is that HTML never got to the point that it could, by itself, offer UX approaching the thick client. Pure server-side rendered HTML offered only a simple &amp; clunky Click → Request → Page Render model. For reasons I can’t completely understand, HTML never moved beyond this extremely basic UX model.</p>
<p><a target="_blank" href="https://intercoolerjs.org/2020/01/14/taking-html-seriously">https://intercoolerjs.org/2020/01/14/taking-html-seriously</a></p>
</blockquote>
<p><strong>htmx</strong> enables interactivity by swapping parts of the DOM without reloading the page. It embeds hx attributes (application control information) in the HTML.</p>
<p>If you want to get an introduction to HTMX + Spring Boot:<br /><a target="_blank" href="https://tschuehly.de/interactive-web-applications-with-htmx-and-spring-boot">Interactive web applications with htmx and Spring Boot</a></p>
<h2 id="heading-ui-components">UI Components</h2>
<p>Spring Boot is an awesome framework for building applications.<br />However, the traditional Spring MVC style lacks cohesion between the View (Templates) and the Model (Data).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1717304025797/ef8fb507-154b-4de4-893e-d5c30a89d58f.png" alt /></p>
<p>The JavaScript frontend frameworks like React, Angular and Vue share one trait.<br />They build user interfaces with components. Components enable reusability and define a clear API.</p>
<p>In the Ruby and PHP communities, server-rendered UI components established themselves with <a target="_blank" href="https://laravel-livewire.com/">Laravel Livewire</a> and <a target="_blank" href="https://viewcomponent.org/">Rails ViewComponent</a></p>
<p>The main benefit is single responsibility:</p>
<blockquote>
<p>ViewComponents consolidate the logic needed for a template into a single class, resulting in a cohesive object that is easy to understand</p>
<p><a target="_blank" href="https://viewcomponent.org/#single-responsibility">https://viewcomponent.org/#single-responsibility</a></p>
</blockquote>
<p>With the open-source <a target="_blank" href="https://github.com/tschuehly/spring-view-component">Spring ViewComponent</a> library Spring Developers can enjoy the benefits of UI components.</p>
<h2 id="heading-introducing-spring-viewcomponent">Introducing Spring ViewComponent</h2>
<p>A ViewComponent is a Spring managed bean. We can leverage backend patterns for UI composition.</p>
<p>We create a ViewComponent by annotating a class with the <code>@ViewComponent</code> annotation.</p>
<p>A <strong>ViewComponent</strong> class defines a rendering context for a corresponding template, this context is called <strong>ViewContext</strong>. We define a public nested record that implements the <code>ViewContext</code> interface.</p>
<p><strong>SimpleViewComponent.java</strong></p>
<pre><code class="lang-java"><span class="hljs-meta">@ViewComponent</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SimpleViewComponent</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">SimpleViewContext</span><span class="hljs-params">(String helloWorld)</span> implements ViewContext </span>{
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> SimpleView <span class="hljs-title">render</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> SimpleView(<span class="hljs-string">"Hello World"</span>);
    }
}
</code></pre>
<p>A ViewComponent needs to have a template with the same name defined in the same package.<br />In the template, we can access the fields of the ViewContext record.</p>
<p><strong>SimpleViewComponent.jte</strong></p>
<pre><code class="lang-xml">@param SimpleViewContext simpleViewContext
<span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>
${simpleViewContext.helloWorld()}
<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Spring ViewComponent wraps the underlying MVC model using Spring AOP. <a target="_blank" href="https://github.com/tschuehly/spring-view-component?tab=readme-ov-file#technical-implementation">Technical Implementation</a></div>
</div>

<p>To render the ViewComponent we inject it in the Controller and then call the render method in the endpoint mapping and return the ViewContext result:</p>
<pre><code class="lang-java"><span class="hljs-meta">@Controller</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SimpleController</span> </span>{

  <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> SimpleViewComponent simpleViewComponent;

  <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">TestController</span><span class="hljs-params">(SimpleViewComponent simpleViewComponent)</span> </span>{
    <span class="hljs-keyword">this</span>.simpleViewComponent = simpleViewComponent;
  }

  <span class="hljs-meta">@GetMapping("/")</span>
  <span class="hljs-function">ViewContext <span class="hljs-title">simple</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-keyword">return</span> simpleViewComponent.render();
  }
}
</code></pre>
<h2 id="heading-perfect-fit-for-hateoas">Perfect fit for HATEOAS</h2>
<p>Spring ViewComponent is a perfect fit for building hypermedia-driven web applications.</p>
<p>When the user interacts with the application the state of the application on the server changes.<br />We now need to send HTML back to swap it into the DOM to reflect the changes in the UI.</p>
<p>With server-side rendered ViewComponents we can call the render method of a ViewComponent again and swap out that component.</p>
<h3 id="heading-example-application">Example Application</h3>
<p>To explain the benefits I will show you a simple user management application, where we can edit and create users.</p>
<p>If you want to build along, a more detailed guide can be found in my workshop:<br /><a target="_blank" href="https://tschuehly.gitbook.io/server-side-spring-htmx-workshop/lab-2-introducing-spring-viewcomponent"><strong>Building server-side web applications with htmx</strong></a></p>
<p>You can play around with the application at <a target="_blank" href="https://htmx.tschuehly.de/">htmx.tschuehly.de</a>. Open the DevTools and look at what requests are happening when you interact with the application!</p>
<p><img src="https://tschuehly.gitbook.io/~gitbook/image?url=https%3A%2F%2F1140842274-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252FveUvHIoWj41mt9oe9a7o%252Fuploads%252FMKo0YVVUV6LiVOT91IVU%252Fimage.png%3Falt%3Dmedia%26token%3Dcd36d8c0-c593-4cbd-ab28-226973d05d16&amp;width=768&amp;dpr=4&amp;quality=100&amp;sign=1de37dab9ed6a4d45a6af04a4c5ed7811a411114b0054607e76ef2f8a1609a54" alt /></p>
<p>When we want to create/edit a user a modal pops up. The modal is closed when we save the user.</p>
<p>A new table row is automatically appended to the top of the table when we save a new user. When we edit and save an existing user the table row is updated.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710543689860/5dc769cf-544a-411e-a584-9c9b963eec1c.png" alt class="image--center mx-auto" /></p>
<p>This is where Spring ViewComponent comes in. Each part of the UI is a ViewComponent.</p>
<p>We have a <code>UserTableComponent</code>, a <code>UserRowComponent</code>, an <code>EditUserFormComponent</code> and a <code>CreateUserFormComponent</code>.</p>
<p>When we click on the <code>Create new User</code> button htmx will create an HTTP GET request to the <code>/create-user/modal</code> endpoint.</p>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- UserTableComponent.jte --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">hx-get</span>=<span class="hljs-string">"${GET_CREATE_USER_MODAL}"</span> 
        <span class="hljs-attr">hx-target</span>=<span class="hljs-string">"#${MODAL_CONTAINER_ID}"</span>&gt;</span>
  Create new User
<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
</code></pre>
<p>This will return the HTML of the <code>CreateUserFormComponent</code> that will be rendered in the element with the id: <code>MODAL_CONTAINER_ID</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1717527361647/f6be9fa1-c776-4692-92be-0462a17fde70.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-java"><span class="hljs-comment">// UserController.java</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> String GET_CREATE_USER_MODAL = <span class="hljs-string">"/create-user/modal"</span>;
<span class="hljs-meta">@GetMapping(GET_CREATE_USER_MODAL)</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> ViewContext <span class="hljs-title">getCreateUserModal</span><span class="hljs-params">()</span> </span>{
  <span class="hljs-keyword">return</span> createUserComponent.render();
}
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">I recommend to use constants for the URL endpoints as you can navigate between the controller and the usages with ctrl + b in IntelliJ</div>
</div>

<p>When we click on <code>Save User</code> an HTTP POST request is created to the <code>/create-user</code> endpoint.</p>
<pre><code class="lang-java"><span class="hljs-comment">// UserController.java</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> String POST_CREATE_USER = <span class="hljs-string">"/create-user"</span>;
<span class="hljs-meta">@PostMapping(POST_CREATE_USER)</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> ViewContext <span class="hljs-title">createUser</span><span class="hljs-params">(String username, String password)</span> </span>{
  EasyUser user = userService.createUser(username, password);
  <span class="hljs-keyword">return</span> userRowComponent.renderNewRow(user);
}
</code></pre>
<p>This will first create a new user. Then it will call the <code>UserRowComponent.renderUserRow</code> with the updated user.</p>
<pre><code class="lang-java"><span class="hljs-comment">// UserRowComponent.java</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> ViewContext <span class="hljs-title">renderNewRow</span><span class="hljs-params">(EasyUser user)</span> </span>{
  HtmxUtil.retargetId(UserTableComponent.USER_TABLE_BODY_ID);
  HtmxUtil.reswap(HxSwapType.AFTER_BEGIN);
  HtmxUtil.trigger(UserManagementComponent.CLOSE_MODAL_EVENT);
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> UserRowContext(user);
}
</code></pre>
<p>Normally with htmx you define the target, swap and trigger behaviour with hx-attributes. I prefer defining them with <code>HX-</code> reponse headers in the component method. This decouples the source template from the target component.</p>
<p>Here we tell htmx to target the <code>UserTableComponent</code> body, insert the response before the first child and then trigger the <code>CLOSE_MODAL_EVENT</code></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">As you can see we use public constants as API to access the internal behaviour of the respective ViewComponent</div>
</div>

<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- UserRowComponent.jte --&gt;</span>
!{var uuid = userRowContext.easyUser().uuid;} 
<span class="hljs-tag">&lt;<span class="hljs-name">tr</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"${UserRowContext.htmlUserId(uuid)}"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>
        ${uuid.toString()}
    <span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>
        ${userRowContext.easyUser().username}
    <span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>
        ${userRowContext.easyUser().password}
    <span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">hx-get</span>=<span class="hljs-string">"${URI(UserController.GET_EDIT_USER_MODAL,uuid)}"</span>
                <span class="hljs-attr">hx-target</span>=<span class="hljs-string">"#${MODAL_CONTAINER_ID}"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"/edit.svg"</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">tr</span>&gt;</span>
</code></pre>
<p>The <code>UserRowComponent.jte</code> has an edit button that calls the <code>GET_EDIT_USER_MODAL</code> endpoint with the uuid of the user as parameter.</p>
<p>After editing a user we need to swap the row of the user we just changed. We can create a rerender method and call it from the corresponding endpoint.</p>
<pre><code class="lang-java"><span class="hljs-comment">// UserRowComponent.java</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> ViewContext <span class="hljs-title">rerender</span><span class="hljs-params">(EasyUser easyUser)</span> </span>{
  String target = HtmxUtil.target(UserRowContext.htmlUserId(easyUser.uuid)); 
  HtmxUtil.retarget(target); 
  HtmxUtil.reswap(HxSwapType.OUTER_HTML);
  HtmxUtil.trigger(UserManagementComponent.CLOSE_MODAL_EVENT);
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> UserRowContext(easyUser); 
}
</code></pre>
<h2 id="heading-advanced-patterns">Advanced Patterns</h2>
<p>With Spring ViewComponent we can leverage Dependency Injection to compose the UI while adhering to the <strong>open–closed principle.</strong></p>
<p>Another interesting topic is declarative UI with the Strategy Pattern. I have an example repository here: <a target="_blank" href="https://github.com/tschuehly/svc-ui">github.com/tschuehly/svc-ui</a></p>
<p>Lastly we can create higher level abstractions on top of the simple concept of swapping Elements in the DOM that are specific to our domain and use cases.</p>
<p>I will create more blog posts in the future showing the patterns we use at <a target="_blank" href="https://alanda.io/">alanda.io</a> to create an enterprise application with Spring ViewComponent and htmx.</p>
<p>If you want to learn more about HTMX + Spring Boot check out my series <a target="_blank" href="https://tschuehly.de/series/full-stack">Web development without the JavaScript headache with Spring + HTMX</a>.</p>
<p>My side business <a target="_blank" href="https://photoquest.wedding/">PhotoQuest</a> is also built with HTMX + JTE</p>
]]></content:encoded></item><item><title><![CDATA[How to use Gradle test fixtures in Maven via dependency]]></title><description><![CDATA[You can share test code between modules/projects easily using the Gradle java-test-fixtures plugin.
plugins {
    id("java-test-fixtures")
}

This will allow you to create a new source set in src/testFixtures where you can define the test code you wa...]]></description><link>https://tschuehly.de/how-to-use-gradle-test-fixtures-in-maven-via-dependency</link><guid isPermaLink="true">https://tschuehly.de/how-to-use-gradle-test-fixtures-in-maven-via-dependency</guid><category><![CDATA[gradle]]></category><category><![CDATA[maven]]></category><dc:creator><![CDATA[Thomas Schilling]]></dc:creator><pubDate>Sat, 06 Apr 2024 15:53:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1712418800308/5307cbfe-3b4c-4220-9601-7d711f4a1cb9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You can share test code between modules/projects easily using the Gradle <code>java-test-fixtures</code> plugin.</p>
<pre><code class="lang-xml">plugins {
    id("java-test-fixtures")
}
</code></pre>
<p>This will allow you to create a new source set in <code>src/testFixtures</code> where you can define the test code you want to share with other modules.</p>
<p>In Gradle you can use these textFixtures easily with:</p>
<pre><code class="lang-xml">testImplementation(testFixtures("de.tschuehly:spring-view-component-core:0.7.3-SNAPSHOT"))
</code></pre>
<p>If you want to use these Gradle test fixtures in a maven project you need to add the dependency with the test-fixtures classifier:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">dependency</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>de.tschuehly<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>spring-view-component-core<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">version</span>&gt;</span>${view.component.version}<span class="hljs-tag">&lt;/<span class="hljs-name">version</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">classifier</span>&gt;</span>test-fixtures<span class="hljs-tag">&lt;/<span class="hljs-name">classifier</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">dependency</span>&gt;</span>
</code></pre>
<p>If you want to learn more about HTMX + Spring Boot check out my series <a target="_blank" href="https://tschuehly.de/series/full-stack">Web development without the JavaScript headache with Spring + HTMX</a>.</p>
<p>My side business <a target="_blank" href="https://photoquest.wedding/">PhotoQuest</a> is also built with HTMX + JTE</p>
]]></content:encoded></item><item><title><![CDATA[Interactive web applications with htmx and Spring Boot]]></title><description><![CDATA[Nowadays Spring is typically only used for JSON API backend development.We build the web frontend with a JavaScript framework, but why?
Probably everyone has experienced that JavaScript headache when it behaves like a well-tested modern language:

ht...]]></description><link>https://tschuehly.de/interactive-web-applications-with-htmx-and-spring-boot</link><guid isPermaLink="true">https://tschuehly.de/interactive-web-applications-with-htmx-and-spring-boot</guid><category><![CDATA[htmx]]></category><category><![CDATA[Springboot]]></category><category><![CDATA[Spring framework]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Thomas Schilling]]></dc:creator><pubDate>Sat, 23 Mar 2024 12:59:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1711198590557/df7ee637-4d70-465e-9e9a-b67c86627b19.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Nowadays Spring is typically only used for JSON API backend development.<br />We build the web frontend with a JavaScript framework, but <strong>why</strong>?</p>
<p>Probably everyone has experienced that JavaScript headache when it behaves like a well-tested modern language:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1711197552326/9d6575f5-9af7-412e-9637-d74be0b970c0.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-htmx-to-the-rescue">htmx to the rescue</h3>
<p>htmx enables us to create interactive web applications with server-side rendered templates without JavaScript.<br />It enables interactivity by swapping parts of the DOM when new information is requested from the server.</p>
<p>We leverage the REST constraint "Hypermedia as the Engine of Application State".</p>
<p>With HATEOAS we don't send JSON to the Browser and render the page on the client.<br />Instead, the Server provides the presentation of information (HTML) and the application control information embedded within (htmx attributes).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710705281051/79314554-7947-46d3-8f0c-e0e649a6473a.gif" alt class="image--center mx-auto" /></p>
<p>This decouples the client and server at the network level.<br />It enables the server to evolve independently from the client, as the server provides the data and how to interact with the server based on that data.</p>
<h1 id="heading-spring-usermanagement-application">Spring UserManagement Application</h1>
<p>We have a table of users and we want to be able to edit users only with the power of HATEOAS and htmx.</p>
<p>We are using <a target="_blank" href="https://jte.gg">JTE</a> as the server-side template language. CSS is omitted for the sake of readability.</p>
<h3 id="heading-display-a-list-of-users">Display a List of Users</h3>
<p>We add an easyUserList attribute to our MVC model.<br />(I explain the MVC pattern here <a target="_blank" href="https://youtu.be/DPAtQU-erM4?si=iiuB70a2KbJvp5VI&amp;t=628">Spring MVC explained: Spring I/O 2023</a>.)<br />We also define a constant for the UserTable ID and an ID for a modal container.</p>
<pre><code class="lang-java"><span class="hljs-comment">// UserController.java</span>
<span class="hljs-meta">@Controller</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserController</span> </span>{
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> String MODAL_CONTAINER_ID = <span class="hljs-string">"modalContainer"</span>;
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> String USER_TABLE_BODY_ID = <span class="hljs-string">"userTableBody"</span>;
  <span class="hljs-meta">@Autowired</span>
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> UserService userService;

  <span class="hljs-meta">@GetMapping("/")</span>
  <span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">userManagement</span><span class="hljs-params">(Model model)</span> </span>{
    model.addAttribute(<span class="hljs-string">"easyUserList"</span>, userService.findAll());
    <span class="hljs-keyword">return</span> <span class="hljs-string">"UserManagement"</span>;
  }
}
</code></pre>
<p>We add this easyUserList as a parameter to the template.<br />In the table body, we loop over the easyUserList and call the userRow JTE template.</p>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- UserManagement.jte --&gt;</span>
@import static de.tschuehly.easy.spring.auth.controller.UserController.*
@param List<span class="hljs-tag">&lt;<span class="hljs-name">EasyUser</span>&gt;</span> easyUserList
<span class="hljs-tag">&lt;<span class="hljs-name">table</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- thead omitted --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">tbody</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"${USER_TABLE_BODY_ID}"</span>&gt;</span>
    @for(var user: easyUserList)
        @template.userRow(easyUser = user)
    @endfor
    <span class="hljs-tag">&lt;/<span class="hljs-name">tbody</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">table</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"${MODAL_CONTAINER_ID}"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<h3 id="heading-display-one-user-as-table-row">Display one User as Table Row</h3>
<p>The UserRow template defines an EasyUser parameter and a local variable with the exclamation mark JTE expression: <code>!{var name = value}</code> .</p>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- UserRow.jte --&gt;</span>
@import de.tschuehly.easy.spring.auth.domain.EasyUser
@import static de.tschuehly.easy.spring.auth.controller.UserController.*
@param EasyUser easyUser
!{var uuid = easyUser.uuid.toString();}
<span class="hljs-tag">&lt;<span class="hljs-name">tr</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"user-${uuid}"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>
        ${uuid}
    <span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>
        ${easyUser.username}
    <span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>
        ${easyUser.password}
    <span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">hx-get</span>=<span class="hljs-string">"${URI(EDIT_USER_MODAL,uuid)}"</span>
                <span class="hljs-attr">hx-target</span>=<span class="hljs-string">"#${MODAL_CONTAINER_ID}"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"/edit.svg"</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">tr</span>&gt;</span>
</code></pre>
<p>As you can see we are using static constants heavily, to make it easy to understand what controller mappings htmx sends requests to.</p>
<p><code>hx-get="${URI(EDIT_USER_MODAL,uuid)}</code> creates an HTTP get request to /user/edit/{uuid} when the button element is clicked. The uuid variable is interpolated with a static URI method we will define next.</p>
<p><code>hx-target="#${MODAL_CONTAINER_ID}"</code> tells HTMX to swap the response body with the element that has the id value "modalContainer".</p>
<p>If we now go to the localhost:8080 we can see the table rendered:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1711129546288/b7a72623-d81b-41cb-b4b1-d858bedc6b9d.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-edit-user-attributes">Edit User attributes</h3>
<pre><code class="lang-java"><span class="hljs-comment">// UserController.java</span>
<span class="hljs-meta">@Controller</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserController</span> </span>{
  <span class="hljs-meta">@Autowired</span>
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> UserService userService;

  <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> String MODAL_CONTAINER_ID = <span class="hljs-string">"modalContainer"</span>;
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> String EDIT_USER_MODAL = <span class="hljs-string">"/user/edit/{uuid}"</span>;

  <span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">UserForm</span><span class="hljs-params">(String uuid, String username, String password)</span> </span>{}

  <span class="hljs-meta">@GetMapping(EDIT_USER_MODAL)</span>
  <span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">editUserModal</span><span class="hljs-params">(Model model, <span class="hljs-meta">@PathVariable</span> UUID uuid)</span> </span>{
    <span class="hljs-keyword">var</span> user = userService.findById(uuid);
    model.addAttribute(<span class="hljs-string">"userForm"</span>, <span class="hljs-keyword">new</span> UserForm(
        user.uuid.toString(), user.username, user.password
    ));
    <span class="hljs-keyword">return</span> <span class="hljs-string">"EditUserForm"</span>;
  }

  <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> String <span class="hljs-title">URI</span><span class="hljs-params">(String uriTemplate, Object... variables)</span> </span>{
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> UriTemplate(uriTemplate)
        .expand(variables).toString();
  }

}
</code></pre>
<p>The EDIT_USER_MODAL endpoint adds a UserForm record to the model with the values retrieved from the UserService.</p>
<p>The <code>EditUserForm.jte</code> template displays a form with an hx-post attribute to "/create-user" that we reference by the static constant "CREATE_USER".</p>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- EditUserForm.jte --&gt;</span>
@import static de.tschuehly.easy.spring.auth.controller.UserController.SAVE_USER
@param de.tschuehly.easy.spring.auth.controller.UserController.UserForm userForm
<span class="hljs-tag">&lt;<span class="hljs-name">form</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">label</span>&gt;</span>
        UUID
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> <span class="hljs-attr">readonly</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"uuid"</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"${userForm.uuid()}"</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">label</span>&gt;</span>
        Username
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"username"</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"${userForm.username()}"</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">label</span>&gt;</span>
        Password
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"password"</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"${userForm.password()}"</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span> <span class="hljs-attr">hx-post</span>=<span class="hljs-string">"${SAVE_USER}"</span>&gt;</span>
        Save User
    <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>
</code></pre>
<p>When clicking on the edit button the EditUserForm is rendered in the modalContainer.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1711130330369/b9a1e13f-db1f-4e05-864a-7651811b42f7.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-save-changed-user-attributes">Save changed User attributes</h3>
<p>In the controller method, we save the user and add the saved user to the model.</p>
<p>We add <code>HX-Retarget = #user-${user.uuid}</code>to target the table row &lt;tr&gt; element that contains the user we just edited.<br />With <code>HX-Reswap = outerHTML</code> we tell htmx to swap the whole table row.</p>
<pre><code class="lang-java"><span class="hljs-meta">@Controller</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserController</span> </span>{
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> String SAVE_USER = <span class="hljs-string">"/save-user"</span>;
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> String CLOSE_MODAL_EVENT = <span class="hljs-string">"close-modal"</span>;

  <span class="hljs-meta">@PostMapping(SAVE_USER)</span>
  <span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">saveUser</span><span class="hljs-params">(UUID uuid, String username, String password, Model model,
      HttpServletResponse response)</span> </span>{
    EasyUser user = userService.saveUser(
        uuid,
        username,
        password
    );
    model.addAttribute(<span class="hljs-string">"easyUser"</span>, user);
    response.addHeader(<span class="hljs-string">"HX-Retarget"</span>, <span class="hljs-string">"#user-"</span> + user.uuid);
    response.addHeader(<span class="hljs-string">"HX-Reswap"</span>, <span class="hljs-string">"outerHTML"</span>);
    response.addHeader(<span class="hljs-string">"HX-Trigger"</span>, CLOSE_MODAL_EVENT);
    <span class="hljs-keyword">return</span> <span class="hljs-string">"UserRow"</span>;
  }
}
</code></pre>
<p>With <code>HX-Trigger = close-modal</code> we tell htmx to trigger a JavaScript event <code>close-modal</code> in the browser when the HTTP response is received.</p>
<p>We add an <code>hx-on:</code> attribute to clear the innerHTML of the modalContainer to remove the HTML from the DOM when the event is triggered.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"${MODAL_CONTAINER_ID}"</span> 
     <span class="hljs-attr">hx-on:</span>$<span class="hljs-attr">unsafe</span>{<span class="hljs-attr">CLOSE_MODAL_EVENT</span>}=<span class="hljs-string">"this.innerHTML = null"</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>If we click the <code>Save User</code> button and go to Chrome DevTools we can see HATEOAS in action.<br />The new application state after saving the user is transferred via HTML to the browser.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1711131315282/e893c6dc-9d9d-4b6d-9225-afe601d2c9c3.png" alt class="image--center mx-auto" /></p>
<p>We have created a fully interactive web application without leaving our lovely JVM!</p>
<p>You can find the source code of this tutorial at <a target="_blank" href="https://github.com/tschuehly/easy-spring-auth/tree/lab-1">github.com/tschuehly/easy-spring-auth</a>.<br />If you have questions ping me on <a target="_blank" href="https://twitter.com/tschuehly">twitter.com/tschuehly</a>.</p>
<p>If you want to learn more about server-side rendering with Spring Boot you can find more information here: <a target="_blank" href="https://github.com/tschuehly/fullstack-with-spring">github.com/tschuehly/fullstack-with-spring</a></p>
<p>If you want to learn more about HTMX + Spring Boot check out my series <a target="_blank" href="https://tschuehly.de/series/full-stack">Web development without the JavaScript headache with Spring + HTMX</a>.</p>
<p>My side business <a target="_blank" href="https://photoquest.wedding/">PhotoQuest</a> is also built with HTMX + JTE</p>
]]></content:encoded></item><item><title><![CDATA[An introduction to frontend development with Spring Boot]]></title><description><![CDATA[I've been building my web frontends with Spring Boot for a few years. But I recently heard that some Spring Boot developers still create their web frontend with something else than Spring ?!
But creating a web frontend without leaving your favourite ...]]></description><link>https://tschuehly.de/an-introduction-to-frontend-development-with-spring-boot</link><guid isPermaLink="true">https://tschuehly.de/an-introduction-to-frontend-development-with-spring-boot</guid><category><![CDATA[Spring]]></category><category><![CDATA[Springboot]]></category><category><![CDATA[jte]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Thomas Schilling]]></dc:creator><pubDate>Mon, 11 Mar 2024 21:51:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1710693557606/9b80150d-ea1b-4f64-b27d-44811b67207d.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I've been building my web frontends with Spring Boot for a few years. But I recently heard that some Spring Boot developers still create their web frontend with something else than Spring ?!</p>
<p>But creating a web frontend without leaving your favourite Java Framework is easy!</p>
<p>We go to everyone's 2nd favorite place online: <a target="_blank" href="https://start.spring.io/#!type=gradle-project-kotlin&amp;language=java&amp;platformVersion=3.2.3&amp;packaging=jar&amp;jvmVersion=21&amp;groupId=com.bootiful&amp;artifactId=frontend&amp;name=frontend&amp;description=Bootiful%20frontends&amp;packageName=com.bootiful.frontend&amp;dependencies=web">start.spring.io</a> and create a new project.<br />We will not choose a template language as you can sadly not find the best template language there!</p>
<p>Instead, we go to <a target="_blank" href="https://jte.gg/spring-boot-starter-3/">jte.gg</a> and add the latest spring-boot-starter as a dependency to our project.</p>
<pre><code class="lang-kotlin">implementation(<span class="hljs-string">"gg.jte:jte-spring-boot-starter-3:3.1.9"</span>)
implementation(<span class="hljs-string">"gg.jte:jte:3.1.9"</span>)
</code></pre>
<p>We start with a WebController and define our Data model.<br />We have a <code>Link</code> record which has a displayValue and a hrefValue field.<br />We also have a <code>SpringResources</code> record which has a topics HashMap field.</p>
<pre><code class="lang-java"><span class="hljs-meta">@Controller</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">WebController</span> </span>{
  <span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">Link</span><span class="hljs-params">(String displayValue, String hrefValue)</span> </span>{}

  <span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">SpringResources</span><span class="hljs-params">(HashMap&lt;String, List&lt;Link&gt;&gt; topics)</span> </span>{}
}
</code></pre>
<p>Then we create a JTE template in a new file located in <code>src/main/jte/spring.jte</code><br />As JTE is compiled to Java we start by importing the <code>SpringResources</code> record.<br />JTE expects a parameter of type <code>SpringResources</code> in the model</p>
<pre><code class="lang-xml">@import com.bootiful.frontend.web.WebController.SpringResources
@param SpringResources springResources
</code></pre>
<p>We use the <code>@for</code> syntax to loop through the hashMap entrySet and show a heading for each key.</p>
<pre><code class="lang-xml">@import com.bootiful.frontend.web.WebController.SpringResources
@param SpringResources springResources

@for(var resource: springResources.topics().entrySet())
    <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>
        ${resource.getKey()}
    <span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
@endfor
</code></pre>
<p>Then we create an <code>&lt;ul&gt;</code> tag for each topic. Then for each link object in the list, we create a <code>&lt;li&gt;</code> tag and a <code>&lt;a&gt;</code> tag with the href set to the hrefValue, and render the displayValue inside the tag.</p>
<pre><code class="lang-xml">@import com.bootiful.frontend.web.WebController.SpringResources
@param SpringResources springResources

@for(var resource: springResources.topics().entrySet())
    <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>
        ${resource.getKey()}
    <span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">ul</span>&gt;</span>
        @for(var link: resource.getValue())
            <span class="hljs-tag">&lt;<span class="hljs-name">li</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"${link.hrefValue()}"</span>&gt;</span>${link.displayValue()}<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
        @endfor
    <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>
@endfor
</code></pre>
<p>Now we add a Controller Endpoint where we add a new SpringResource object to the Model as <code>springResources</code> attribute.<br />Then we return a <code>spring</code> string which is a reference to the <code>spring.jte</code> template.</p>
<pre><code class="lang-java"><span class="hljs-meta">@Controller</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">WebController</span> </span>{
  ...
  <span class="hljs-meta">@GetMapping("/")</span>
  <span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">index</span><span class="hljs-params">(Model model)</span> </span>{
    model.addAttribute(<span class="hljs-string">"springResources"</span>, <span class="hljs-keyword">new</span> SpringResources(<span class="hljs-keyword">new</span> HashMap&lt;&gt;(
        Map.of(<span class="hljs-string">"Spring"</span>,List.of(
            <span class="hljs-keyword">new</span> Link(<span class="hljs-string">"Spring"</span>,<span class="hljs-string">"https://spring.io"</span>),
            <span class="hljs-keyword">new</span> Link(<span class="hljs-string">"Spring Academy"</span>,<span class="hljs-string">"https://spring.academy"</span>)
        ))
    )));
    <span class="hljs-keyword">return</span> <span class="hljs-string">"spring"</span>;
  }
}
</code></pre>
<p>If we now navigate to <a target="_blank" href="http://localhost:8080/">http://localhost:8080/</a> you can see that our template is rendered correctly:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710193331081/264d2fcb-9ea0-4534-be4e-870af62b74ac.png" alt class="image--center mx-auto" /></p>
<p>You can find the source code for this tutorial here: <a target="_blank" href="https://github.com/tschuehly/bootiful-frontend">github.com/tschuehly/bootiful-frontend</a></p>
<p>If you want to learn more about HTMX + Spring Boot check out my series <a target="_blank" href="https://tschuehly.de/series/full-stack">Web development without the JavaScript headache with Spring + HTMX</a>.</p>
<p>My side business <a target="_blank" href="https://photoquest.wedding/">PhotoQuest</a> is also built with HTMX + JTE</p>
]]></content:encoded></item><item><title><![CDATA[Introducing the htmx-supabase-spring-boot-starter v0.3]]></title><description><![CDATA[htmx is an awesome library to build interactive web applications directly in HTML, without the javascript headache.
In my htmx project, I wanted to use a hosted authentication provider. I did not want to deal with user credentials myself.
I researche...]]></description><link>https://tschuehly.de/introducing-the-htmx-supabase-spring-boot-starter-v03</link><guid isPermaLink="true">https://tschuehly.de/introducing-the-htmx-supabase-spring-boot-starter-v03</guid><category><![CDATA[Springboot]]></category><category><![CDATA[htmx]]></category><dc:creator><![CDATA[Thomas Schilling]]></dc:creator><pubDate>Wed, 20 Dec 2023 11:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1709398375148/abdaf43b-7515-42b1-bea5-f6f583616f3d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://htmx.org/">htmx</a> is an awesome library to build interactive web applications directly in HTML, without the javascript headache.</p>
<p>In my htmx project, I wanted to use a hosted authentication provider. I did not want to deal with user credentials myself.</p>
<p>I researched the market and found Supabase which had an interesting offering for free!</p>
<ul>
<li><p>50.000 monthly active users</p>
</li>
<li><p>Hosted Postgres server with 500 MB storage</p>
</li>
</ul>
<p>This was definitely sufficient for my side projects!</p>
<p>However the Supabase Authentication was built on GoTrue and was not ready to be integrated into my Spring Boot application.</p>
<p>That is why I decided to create the htmx-supabase-spring-boot-starter!</p>
<h2 id="heading-key-features">Key features:</h2>
<ul>
<li><p>Supabase Authentication integration</p>
</li>
<li><p>Spring Security configuration with application.yaml/properties</p>
</li>
<li><p>Role-Based Access Control</p>
</li>
<li><p>Basic Authentication</p>
</li>
</ul>
<h2 id="heading-integrating-htmx-supabase-spring-boot-starter">Integrating htmx-supabase-spring-boot-starter</h2>
<p>The setup is straightforward -&gt;</p>
<p>Add the Dependency</p>
<pre><code class="lang-kotlin">implementation(<span class="hljs-string">"de.tschuehly:htmx-supabase-spring-boot-starter:0.3.0"</span>)
</code></pre>
<p>Configure your application.yaml:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">supabase:</span>
  <span class="hljs-attr">projectId:</span> <span class="hljs-string">yourProjectId</span>
  <span class="hljs-attr">anonKey:</span> <span class="hljs-string">${SUPABASE_ANON_KEY}</span>
  <span class="hljs-attr">databasePassword:</span> <span class="hljs-string">${SUPABASE_DATABASE_PW}</span>
  <span class="hljs-attr">jwtSecret:</span> <span class="hljs-string">${SUPABASE_JWT_SECRET}</span>
  <span class="hljs-attr">successfulLoginRedirectPage:</span> <span class="hljs-string">"/account"</span>
  <span class="hljs-attr">passwordRecoveryPage:</span> <span class="hljs-string">/updatePassword</span>
  <span class="hljs-attr">unauthenticatedPage:</span> <span class="hljs-string">/unauthenticated</span>
  <span class="hljs-attr">unauthorizedPage:</span> <span class="hljs-string">/unauthorizedPage</span>
  <span class="hljs-attr">sslOnly:</span> <span class="hljs-literal">false</span>
</code></pre>
<p>Add the necessary public paths:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">supabase:</span>
  <span class="hljs-attr">public:</span>
    <span class="hljs-attr">get:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"/unauthenticated"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"/unauthorized"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"/api/user/logout"</span>
    <span class="hljs-attr">post:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"/api/user/signup"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"/api/user/sendEmailOtp"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"/api/user/login"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"/api/user/jwt"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"/api/user/sendPasswordResetEmail"</span>
</code></pre>
<p>Add the javascript snippets that authorize a user after login:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">window</span>.location.hash.startsWith(<span class="hljs-string">"#access_token"</span>)) {
        htmx.ajax(<span class="hljs-string">'POST'</span>, <span class="hljs-string">'/api/user/jwt'</span>, {<span class="hljs-attr">target</span>: <span class="hljs-string">'#body'</span>, <span class="hljs-attr">swap</span>: <span class="hljs-string">'outerHTML'</span>})
            .then(<span class="hljs-built_in">window</span>.location.hash = <span class="hljs-string">""</span>)
    }
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>Now add a signup form:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">form</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">label</span>&gt;</span>Email:
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"email"</span>/&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">label</span>&gt;</span>Password:
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"password"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"password"</span>/&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">hx-post</span>=<span class="hljs-string">"/api/user/signup"</span>&gt;</span>Submit<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>
</code></pre>
<p>And a login form:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">form</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">label</span>&gt;</span>Email:
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"email"</span>/&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">label</span>&gt;</span>Password:
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"password"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"password"</span>/&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">hx-post</span>=<span class="hljs-string">"/api/user/login"</span>&gt;</span>Submit<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>
</code></pre>
<p>That's it! You now have a secured Spring Boot + htmx application, where a user can register and then log in.</p>
<p>If you want to dive deeper into the configuration, you can check out the README in the GitHub repo:</p>
<p><a target="_blank" href="https://github.com/tschuehly/htmx-supabase-spring-boot-starter">github.com/tschuehly/htmx-supabase-spring-boot-starter</a></p>
<p>If you want to learn more about HTMX + Spring Boot check out my series <a target="_blank" href="https://tschuehly.de/series/full-stack">Web development without the JavaScript headache with Spring + HTMX</a>.</p>
<p>My side business <a target="_blank" href="https://photoquest.wedding/">PhotoQuest</a> is also built with HTMX + JTE</p>
]]></content:encoded></item><item><title><![CDATA[Spring ViewComponent v0.7.0 release]]></title><description><![CDATA[I'm excited to announce the release of Spring ViewComponent v0.7.0 today.
You can grab the release from Maven Central by following the instructions at: https://github.com/tschuehly/spring-view-component#installation
The two biggest changes to this re...]]></description><link>https://tschuehly.de/spring-viewcomponent-v070-release</link><guid isPermaLink="true">https://tschuehly.de/spring-viewcomponent-v070-release</guid><category><![CDATA[jte]]></category><category><![CDATA[Springboot]]></category><category><![CDATA[Thymeleaf]]></category><category><![CDATA[htmx]]></category><category><![CDATA[Java]]></category><category><![CDATA[Kotlin]]></category><dc:creator><![CDATA[Thomas Schilling]]></dc:creator><pubDate>Wed, 01 Nov 2023 11:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1709398548984/a9768bc8-a7a2-4b37-867d-fff16dd3c624.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'm excited to announce the release of Spring ViewComponent v0.7.0 today.</p>
<p>You can grab the release from Maven Central by following the instructions at: <a target="_blank" href="https://github.com/tschuehly/spring-view-component#installation">https://github.com/tschuehly/spring-view-component#installation</a></p>
<p>The two biggest changes to this release are the annotation processor for processing ViewComponents at build time and an improved way to create a ViewContext.</p>
<h1 id="heading-annotation-processor">Annotation Processor</h1>
<p>Previously the templates were processed at request time by parsing them with <a target="_blank" href="https://jsoup.org/">jsoup</a> and then transforming the <code>view:action</code> attributes after rendering the templates was finished.</p>
<p>This was complicated as we needed to deal with HTML parsing and figuring out what ViewComponent the <code>view:action</code> attribute was in and severely impacted performance, as this was done on each request and not just once.</p>
<p>Now a Java annotation processor will process the templates at build time to translate the <code>view:action</code> attributes to <code>hx-</code> attributes.</p>
<p>But hot reloading the templates also still works as I integrated a Spring Devtools FileSystem Watcher to parse the templates at development time.</p>
<p><img src="https://www.tschuehly.de/images/0.7.0-annotation-processor.jpg" alt /></p>
<h1 id="heading-creating-a-viewcontext">Creating a ViewContext</h1>
<p><img src="https://www.tschuehly.de/images/records_everywhere.jpg" alt /></p>
<p>Previously we created a ViewContext by calling the ViewContext constructor and passing ViewProperties.</p>
<p>This will now be replaced by creating a dedicated record that needs to implement the ViewContext interface. This will enable type checking of templates in the future.</p>
<p><img src="https://www.tschuehly.de/images/0.7.0-records-example.jpg" alt /></p>
<p>This simplifies the @param annotation inside JTE templates and the @thymesVar comment inside Thymeleaf templates immensely!</p>
<p><img src="https://www.tschuehly.de/images/0.7.0-records-param.jpg" alt /></p>
<p>If you have any questions just open an issue in the <a target="_blank" href="https://github.com/tschuehly/spring-view-component">GitHub Repository</a>!</p>
<p>If you want to learn more about HTMX + Spring Boot check out my series <a target="_blank" href="https://tschuehly.de/series/full-stack">Web development without the JavaScript headache with Spring + HTMX</a>.</p>
<p>My side business <a target="_blank" href="https://photoquest.wedding/">PhotoQuest</a> is also built with HTMX + JTE</p>
]]></content:encoded></item><item><title><![CDATA[Creating a JPA-style CrudRepository using Microstream]]></title><description><![CDATA[Example GitHub Repository
What is Microstream?
MicroStream is a Java native persistence layer that can store a Java Object Graph in a binary storage format, persisting it across application restarts.
With MicroStream your data lives in your RAM givin...]]></description><link>https://tschuehly.de/creating-a-jpa-style-crudrepository-using-microstream</link><guid isPermaLink="true">https://tschuehly.de/creating-a-jpa-style-crudrepository-using-microstream</guid><category><![CDATA[microstream]]></category><category><![CDATA[eclipsestore]]></category><category><![CDATA[jpa]]></category><category><![CDATA[Kotlin]]></category><dc:creator><![CDATA[Thomas Schilling]]></dc:creator><pubDate>Tue, 05 Sep 2023 10:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1709398800103/febdc24c-b003-4e9c-93a7-1e2692012a65.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://github.com/tschuehly/microstream-crudrepository-example">Example GitHub Repository</a></p>
<h2 id="heading-what-is-microstream">What is Microstream?</h2>
<p><a target="_blank" href="https://microstream.one">MicroStream</a> is a Java native persistence layer that can store a Java Object Graph in a binary storage format, persisting it across application restarts.</p>
<p>With MicroStream your data lives in your RAM giving you <strong>blazingly</strong> fast access times.</p>
<p><img src="https://www.tschuehly.de/posts/microstream-perfomance.png" alt="microstream-perfomance.png" /></p>
<h2 id="heading-why-do-we-want-microstream">Why do we want MicroStream</h2>
<p>I wanted to try out using MicroStream after using JPA for most of my developer career as I was looking to simplify my data storage, after hearing about it at JCON 2023.</p>
<p>I'm very interested in making my application as simple as possible to ease development and get to production faster.</p>
<p>They market MicroStream heavily for microservices, but I think the best use case is a fat modulith running on a VPS at Hetzner or similiar.</p>
<p>You can get a 32 Core,64 Thread dedicated Server with 1TB of RAM for 530€ a month at Hetzner, or a 128 Core, 256 Thread dedicated Server with 2TB of RAM for 2400€ a month at hostcircle.nl. Tell me the workload that doesn't fit on that machine.</p>
<h2 id="heading-migrating-to-jpa">Migrating to JPA</h2>
<p>MicroStream has support for Spring Boot. As we want to store a Java Object Graph we need to define a Root Node for the Graph.</p>
<p><img src="https://www.tschuehly.de/posts/microstream_object_graph.png" alt="microstream_object_graph.png" /></p>
<p>We can do that with the <code>@Storage</code> annotation:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Storage</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Root</span> </span>{
    <span class="hljs-meta">@Autowired</span>
    <span class="hljs-meta">@Transient</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> storageManager: StorageManager
    <span class="hljs-comment">//...</span>
}
</code></pre>
<p>We autowire the StorageManager that is responsible for interacting with MicroStream.</p>
<p>We want to have a primary key for every Object in the DataStore to emulate the Database behavior we are used to.</p>
<p>Instead of a Database Table PERSON we create a mutableMap with a Long Key and a Person Value.</p>
<p>We also create an AtomicLong as an Index and create a function that increases the index and stores it.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Storage</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Root</span> </span>{
    <span class="hljs-comment">//...</span>
    <span class="hljs-keyword">val</span> personMap = mutableMapOf&lt;<span class="hljs-built_in">Long</span>, Person&gt;()

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> index: AtomicLong = AtomicLong(<span class="hljs-number">0</span>)

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">newIndex</span><span class="hljs-params">()</span></span>: <span class="hljs-built_in">Long</span> {
        <span class="hljs-keyword">val</span> newIndex = index.incrementAndGet()
        storageManager.store(index)
        <span class="hljs-keyword">return</span> newIndex
    }
}
</code></pre>
<p>We create a generic store function to store our map in our Root Object</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Root</span></span>{
    <span class="hljs-comment">//...</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-type">&lt;T&gt;</span> <span class="hljs-title">store</span><span class="hljs-params">(map: <span class="hljs-type">MutableMap</span>&lt;<span class="hljs-type">Long</span>, T&gt;)</span></span> {
        storageManager.store(map)
    }
}
</code></pre>
<p>To emulate the CrudRepository we know from JPA, I created a generic CrudRepository which we can implement in our Domain Repositories. It expects a TypeArgument which implements the Entity interface which has an id parameter.</p>
<p>It exposes methods like getAll, getByIdOrNull, save, and saveAll.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">open</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CrudRepository</span>&lt;<span class="hljs-type">T : Entity</span>&gt;</span>(
    <span class="hljs-keyword">open</span> <span class="hljs-keyword">val</span> root: Root,
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> mutableMap: MutableMap&lt;<span class="hljs-built_in">Long</span>, T&gt;,
) {
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> lock: ReentrantReadWriteLock = ReentrantReadWriteLock()
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getAll</span><span class="hljs-params">()</span></span>: List&lt;T&gt; {
        <span class="hljs-keyword">return</span> readAction {
            mutableMap.values.toList()
        }
    }

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getByIdOrNull</span><span class="hljs-params">(id: <span class="hljs-type">Long</span>)</span></span>: T? {
        <span class="hljs-keyword">return</span> readAction{
            mutableMap[id]
        }
    }

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">save</span><span class="hljs-params">(obj: <span class="hljs-type">T</span>)</span></span>: T {
        <span class="hljs-keyword">return</span> writeAction{
            (obj.id ?: root.newIndex()).let { id -&gt;
                obj.id = id
                mutableMap[id] = obj
                store()
            }
            obj
        }

    }

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">saveAll</span><span class="hljs-params">(objList: <span class="hljs-type">List</span>&lt;<span class="hljs-type">T</span>&gt;)</span></span>: List&lt;T&gt; {
        <span class="hljs-keyword">return</span> writeAction{
            <span class="hljs-keyword">val</span> objMap = objList.map { obj -&gt;
                (obj.id ?: root.newIndex()).let { id -&gt;
                    obj.id = id
                    <span class="hljs-keyword">return</span><span class="hljs-symbol">@map</span> Pair(id,obj)
                }
            }.toMap()
            mutableMap.putAll(objMap)
            store()
            objMap.values.toList()
        }
    }
    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">store</span><span class="hljs-params">()</span></span>{
        root.store(mutableMap)
    }

    <span class="hljs-keyword">open</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-type">&lt;T&gt;</span> <span class="hljs-title">readAction</span><span class="hljs-params">(supplier: <span class="hljs-type">Supplier</span>&lt;<span class="hljs-type">T</span>&gt;)</span></span>: T {
        lock.readLock().lock()
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">try</span> {
            supplier.<span class="hljs-keyword">get</span>()
        } <span class="hljs-keyword">finally</span> {
            lock.readLock().unlock()
        }
    }

    <span class="hljs-keyword">open</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-type">&lt;T&gt;</span> <span class="hljs-title">writeAction</span><span class="hljs-params">(supplier: <span class="hljs-type">Supplier</span>&lt;<span class="hljs-type">T</span>&gt;)</span></span>: T {
        lock.writeLock().lock()
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">try</span> {
            supplier.<span class="hljs-keyword">get</span>()
        } <span class="hljs-keyword">finally</span> {
            lock.writeLock().unlock()
        }
    }
}
</code></pre>
<p>We can now implement a JPA style PersonRepository extending our CrudRepository.</p>
<p>In this example we create a findByName function that filters through all map values by name.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Repository</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PersonRepository</span></span>(
    <span class="hljs-keyword">override</span> <span class="hljs-keyword">val</span> root: Root
) : CrudRepository&lt;Person&gt;(root, root.personMap){
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">findByName</span><span class="hljs-params">(name: <span class="hljs-type">String</span>)</span></span>: List&lt;Person&gt; {
        <span class="hljs-keyword">return</span> readAction {
            root.personMap.values.filter {
                it.name == name
            }
        }
    }
}
</code></pre>
<h2 id="heading-example">Example</h2>
<p>You can find the example in the <a target="_blank" href="https://github.com/tschuehly/microstream-crudrepository-example">GitHub Repository</a></p>
<p>If you want to learn more about HTMX + Spring Boot check out my series <a target="_blank" href="https://tschuehly.de/series/full-stack">Web development without the JavaScript headache with Spring + HTMX</a>.</p>
<p>My side business <a target="_blank" href="https://photoquest.wedding/">PhotoQuest</a> is also built with HTMX + JTE</p>
]]></content:encoded></item><item><title><![CDATA[Secure your Spring Boot Actuator Endpoints and configure Prometheus with Basic Authentication]]></title><description><![CDATA[When setting up Prometheus for the first time it might not work. If you enable debug logs you will see that Prometheus cannot scrape the actuator logs.
To fix this we need to configure your Spring App and Prometheus with Basic Authentication and conf...]]></description><link>https://tschuehly.de/secure-your-spring-boot-actuator-endpoints-and-configure-prometheus-with-basic-authentication</link><guid isPermaLink="true">https://tschuehly.de/secure-your-spring-boot-actuator-endpoints-and-configure-prometheus-with-basic-authentication</guid><category><![CDATA[#prometheus]]></category><category><![CDATA[Springboot]]></category><category><![CDATA[Spring]]></category><category><![CDATA[Spring framework]]></category><category><![CDATA[observability]]></category><category><![CDATA[Actuator]]></category><dc:creator><![CDATA[Thomas Schilling]]></dc:creator><pubDate>Sun, 18 Jun 2023 10:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1709459984187/81789309-f7e6-46b6-8b10-bbcefd92625d.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When setting up Prometheus for the first time it might not work. If you enable debug logs you will see that Prometheus cannot scrape the actuator logs.</p>
<p>To fix this we need to configure your Spring App and Prometheus with Basic Authentication and configure Prometheus to access the Actuator Endpoints.</p>
<p>To set up your Prometheus + Grafana Setup you can follow the excellent guide on <a target="_blank" href="https://refactorfirst.com/spring-boot-prometheus-grafana">Refactor First: Monitoring Spring Boot Application with Prometheus and Grafana</a> by <a target="_blank" href="https://twitter.com/amrutprabhu42">Amrut Prabhu</a></p>
<h2 id="heading-custom-securityconfig">Custom SecurityConfig</h2>
<p>After you got everything working without Authentication you need to configure your SecurityConfiguration like this:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Configuration</span>
<span class="hljs-meta">@EnableWebSecurity</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SecurityConfig</span></span>{
    <span class="hljs-keyword">val</span> logger: Logger = LoggerFactory.getLogger(SupabaseSecurityConfig::<span class="hljs-keyword">class</span>.java)

    @Bean
    <span class="hljs-keyword">fun</span> filterChain(
        http: HttpSecurity,
        authManager: AuthenticationManager
    ): SecurityFilterChain {
        http.invoke {
            authorizeHttpRequests {
                authorize(EndpointRequest.toAnyEndpoint(), hasRole(<span class="hljs-string">"ACTUATOR"</span>))
                authorize(anyRequest, authenticated)
            }
            authenticationManager = authManager
            httpBasic {}
        }
        <span class="hljs-keyword">return</span> http.build()
    }

    <span class="hljs-meta">@Bean</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">authManager</span><span class="hljs-params">(
        http: <span class="hljs-type">HttpSecurity</span>
    )</span></span>: AuthenticationManager {
        <span class="hljs-keyword">val</span> authenticationManagerBuilder = http.getSharedObject(
            AuthenticationManagerBuilder::<span class="hljs-keyword">class</span>.java
        )
        authenticationManagerBuilder.inMemoryAuthentication()
            .withUser(<span class="hljs-string">"prometheus"</span>)
            .password(<span class="hljs-string">"{bcrypt}\$2a\$\$LVUNCy8Lht68w7KA0nobWuwyzbW8AdF3bRC25glv7M12ACAZ4PT8u"</span>)
            .roles(<span class="hljs-string">"ACTUATOR"</span>)
        <span class="hljs-keyword">return</span> authenticationManagerBuilder.build()
    }

}
</code></pre>
<p>Using a custom authenticationManager gives us the ability to add other AuthenticationProviders using:</p>
<p><code>authenticationManagerBuilder.authenticationProvider(customAuthenticationProvider)</code></p>
<h2 id="heading-supabase-security-spring-boot-starter">Supabase Security Spring Boot Starter</h2>
<p>If you are using the <a target="_blank" href="https://github.com/tschuehly/supabase-security-spring-boot-starter">Supabase Security Spring Boot Starter</a> it is even easier!</p>
<pre><code class="lang-yaml"><span class="hljs-attr">supabase:</span>
  <span class="hljs-attr">basicAuth:</span>
    <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>
    <span class="hljs-attr">username:</span> <span class="hljs-string">prometheus</span>
    <span class="hljs-attr">password:</span> <span class="hljs-string">"{bcrypt}$2a$10$AqgP120RLJ48mvTv.diNHeVlQA/WdsrgEr0aLe5P1ffYPy1FQAecy"</span>
    <span class="hljs-attr">roles:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"ACTUATOR"</span>
  <span class="hljs-attr">roles:</span>
    <span class="hljs-attr">admin:</span>
      <span class="hljs-attr">get:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">"/actuator/**"</span>
</code></pre>
<p>You can encrypt the password using the <a target="_blank" href="https://docs.spring.io/spring-boot/docs/current/reference/html/cli.html">Spring Boot CLI</a></p>
<h2 id="heading-prometheus">Prometheus</h2>
<p>Then you can configure your prometheus.yaml with the basic auth credentials:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">scrape_configs:</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">job_name:</span> <span class="hljs-string">'Spring Boot Application input'</span>
  <span class="hljs-attr">metrics_path:</span> <span class="hljs-string">'/actuator/prometheus'</span>
  <span class="hljs-attr">scrape_interval:</span> <span class="hljs-string">2s</span>
  <span class="hljs-attr">static_configs:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">targets:</span> [<span class="hljs-string">'localhost:8080'</span>]
      <span class="hljs-attr">labels:</span>
      <span class="hljs-attr">application:</span> <span class="hljs-string">'My Spring Boot Application'</span>
      <span class="hljs-attr">basic_auth:</span>
      <span class="hljs-attr">username:</span> <span class="hljs-string">"prometheus"</span>
      <span class="hljs-attr">password:</span> <span class="hljs-string">"plain-text-password"</span>
</code></pre>
<p>If you want to learn more about HTMX + Spring Boot check out my series <a target="_blank" href="https://tschuehly.de/series/full-stack">Web development without the JavaScript headache with Spring + HTMX</a>.</p>
<p>My side business <a target="_blank" href="https://photoquest.wedding/">PhotoQuest</a> is also built with HTMX + JTE</p>
]]></content:encoded></item><item><title><![CDATA[How to publish a Kotlin/Java Spring Boot library with Gradle to Maven Central - Complete Guide]]></title><description><![CDATA[This is an opinionated step-by-step guide on how to publish a Kotlin/Java library with Gradle to the Maven Central repository.
It assumes that:

the project is built with Gradle (look at Maciej Guide if you want to do it with Maven)

the project code...]]></description><link>https://tschuehly.de/how-to-publish-a-kotlinjava-spring-boot-library-with-gradle-to-maven-central-complete-guide</link><guid isPermaLink="true">https://tschuehly.de/how-to-publish-a-kotlinjava-spring-boot-library-with-gradle-to-maven-central-complete-guide</guid><category><![CDATA[maven]]></category><category><![CDATA[gradle]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[Java]]></category><dc:creator><![CDATA[Thomas Schilling]]></dc:creator><pubDate>Mon, 01 May 2023 10:00:00 GMT</pubDate><content:encoded><![CDATA[<p>This is an opinionated <strong>step-by-step guide</strong> on how to <strong>publish a Kotlin/Java library with Gradle to the Maven Central</strong> repository.</p>
<p>It assumes that:</p>
<ul>
<li><p>the project is built with <strong>Gradle</strong> (look at Maciej Guide if you want to do it with Maven)</p>
</li>
<li><p>the project code is hosted on <strong>GitHub</strong> and <strong>GitHub Actions</strong> are used to trigger the release</p>
</li>
</ul>
<p>It uses <a target="_blank" href="https://jreleaser.org/">JReleaser</a> - I believe this is the most straightforward way of signing and uploading artifacts.</p>
<p>This guide is based on the excellent article <a target="_blank" href="https://maciejwalkowiak.com/blog/guide-java-publish-to-maven-central/">How to publish a Java library to Maven Central - Complete Guide</a> but uses Gradle instead of Maven.</p>
<ol>
<li><p><a class="post-section-overview" href="#1-create-an-account-in-sonatype-jira">Create an account in Sonatype JIRA</a></p>
</li>
<li><p><a class="post-section-overview" href="#2-create-a-new-project-ticket">Create a "New Project" ticket</a></p>
<ol>
<li><p><a class="post-section-overview" href="#21-if-a-custom-domain-is-used-as-a-group-id">If a custom domain is used as a group id</a></p>
</li>
<li><p><a class="post-section-overview" href="#22-if-github-is-used-as-a-group-id">If GitHub is used as a group id</a></p>
</li>
<li><p><a class="post-section-overview" href="#23-set-ticket-to-open">Set ticket to "Open"</a></p>
</li>
</ol>
</li>
<li><p><a class="post-section-overview" href="#3-create-gpg-keys">Create GPG keys</a></p>
<ol>
<li><a class="post-section-overview" href="#31-export-key-to-a-key-server">Export key to a key server</a></li>
</ol>
</li>
<li><p><a class="post-section-overview" href="#4-export-public-and-secret-key-to-github-secrets">Export public and secret key to GitHub secrets</a></p>
<ol>
<li><p><a class="post-section-overview" href="#41-create-github-secrets-with-ui">Create GitHub secrets with UI</a></p>
</li>
<li><p><a class="post-section-overview" href="#42-create-secrets-with-github-cli">Create secrets with GitHub CLI</a></p>
</li>
</ol>
</li>
<li><p><a class="post-section-overview" href="#5-adjust-pomxml">Adjust pom.xml</a></p>
<ol>
<li><p><a class="post-section-overview" href="#51-generate-javadocs-and-sources-jars">Generate javadocs and sources JARs</a></p>
</li>
<li><p><a class="post-section-overview" href="#52-configure-jreleaser-maven-plugin">Configure JReleaser Maven Plugin</a></p>
</li>
</ol>
</li>
<li><p><a class="post-section-overview" href="#6-create-a-github-action">Create a GitHub action</a></p>
</li>
<li><p><a class="post-section-overview" href="#7-get-familiar-with-sonatype-nexus-ui">Get familiar with Sonatype Nexus UI</a></p>
</li>
<li><p><a class="post-section-overview" href="#8-when-is-the-library-actually-available-to-use">When is the library actually available to use?</a></p>
</li>
</ol>
<h2 id="heading-1-create-an-account-in-sonatype-jira">1. Create an account in Sonatype JIRA</h2>
<p><a target="_blank" href="https://issues.sonatype.org/secure/Signup!default.jspa">Sign up in Sonatype JIRA</a>.</p>
<p>You do it <strong>only once</strong> - no matter how many projects you want to release or how many group ids you own.</p>
<h2 id="heading-2-create-a-new-project-ticket">2. Create a "New Project" ticket</h2>
<p><a target="_blank" href="https://issues.sonatype.org/secure/CreateIssue.jspa?pid=10134&amp;issuetype=21">Create a "New Project ticket</a> in Sonatype JIRA.</p>
<p>This step is done <strong>once per group id</strong>. Meaning, that for each domain you want to use as a group id - you must create a new project request.</p>
<p>Although the official Sonatype guide claims that normally, the process takes less than 2 business days. In my case, it took just a few minutes.</p>
<p>Once the ticket is created, a Sonatype JIRA bot will post comments with instructions on what to do next:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709319679709/e0057d1e-ef54-4e6c-93f6-b1b7f5a667c6.png" alt="SonaType Bot Comment" /></p>
<h2 id="heading-21-if-a-custom-domain-is-used-as-a-group-id">2.1. If a custom domain is used as a group id</h2>
<p>When you want to use a domain like <code>de.tschuehly</code> as a group id - you must own the domain - and be able to prove it. * <em>You must add a DNS TXT record with a JIRA ticket id</em>* to your domain - this is done in the admin panel where your domain is hosted.</p>
<p>Once you have added the record, verify that it is added with the following command:</p>
<pre><code class="lang-plaintext">$ dig -t txt tschuehly.de
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709319681006/2b93e8dc-2a87-4cc5-8d20-2ed53689b849.png" alt="dig -t txt output" /></p>
<h2 id="heading-22-if-github-is-used-as-a-group-id">2.2. If GitHub is used as a group id</h2>
<p>If you don't own the domain it is possible to use your GitHub coordinates as a group id. For example, my GitHub account name is <code>tschuehly</code>, so I can use <code>io.github.tschuehly</code> as a group id.</p>
<p>To prove that you own that GitHub account, create a temporary repository with a name reflecting the JIRA ticket id.</p>
<p>This can be done via <a target="_blank" href="https://github.com/new">github.com/new</a> or with GitHub CLI:</p>
<pre><code class="lang-plaintext">$ gh repo create OSSRH-91026 --public
</code></pre>
<h2 id="heading-23-set-ticket-to-open">2.3. Set ticket to "Open"</h2>
<p>The comment posted by Sonatype bot says that once you are done with either creating a DNS record or creating a GitHub repository, "Edit this ticket and set Status to Open.".</p>
<p>I did not find any way to change status to "Open" in the edit form, but instead I had to click one of the buttons at the top of JIRA ticket, right next to "Agile Board" and "More" (unfortunately I did not make a screenshot on time).</p>
<p>Once you do it, another comment will be posted by Sonatype bot:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709319682358/d74b553b-e203-43ce-9c1e-182de6c39c2f.png" alt="Sonatype Bot success message" /></p>
<p>This means that our job in the Sonatype JIRA is done. Congratulations 🎉</p>
<p>(you can now drop the temporary GitHub repository if you've created one)</p>
<h2 id="heading-3-create-gpg-keys">3. Create GPG keys</h2>
<p>Artifacts sent to Maven Central must be signed. To sign artifacts you need to generate <strong>GPG keys</strong>.</p>
<p><strong>This must be done only once</strong> - all artifacts you publish to Maven Central can be signed with the same pair of keys.</p>
<p>Create a key pair with:</p>
<pre><code class="lang-plaintext">$ gpg --gen-key
</code></pre>
<p>Put your name, email address and passphrase.</p>
<p>List keys with the command:</p>
<pre><code class="lang-plaintext">$ gpg --list-keys
</code></pre>
<p>You will see output like this:</p>
<pre><code class="lang-plaintext">pub   ed25519 2022-11-05 [SC] [expires: 2024-11-04]
      05342E4134D1F7C1B08F900FC2377C0DD0494024
uid           [ultimate] john@doe.com
sub   cv25519 2022-11-05 [E] [expires: 2024-11-04]
</code></pre>
<p>In this example - <code>05342E4134D1F7C1B08F900FC2377C0DD0494024</code> is the key id. Find your key id and copy it to the clipboard.</p>
<p>If you can't find it, you probably used the wrong version of gpg. It didn't work on my Windows machine but worked on my Linux server</p>
<h2 id="heading-31-export-key-to-a-key-server">3.1 Export key to a key server</h2>
<p>Next, you need to export the public key to a key server with the command:</p>
<pre><code class="lang-plaintext">$ gpg --keyserver keyserver.ubuntu.com --send-keys yourKeyId
</code></pre>
<h2 id="heading-4-export-public-and-secret-key-to-github-secrets">4. Export public and secret key to GitHub secrets</h2>
<p>JReleaser needs public and secret key to sign artifacts. Since signing will be done by a GitHub action, you need to export these keys as GitHub secrets.</p>
<p>Secrets can be set either on the GitHub repository website or with a GitHub CLI.</p>
<h2 id="heading-41-create-github-secrets-with-ui">4.1. Create GitHub secrets with UI</h2>
<p>Go to repository <code>Settings</code>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709319683550/b57eac6d-325f-4d33-9d10-1a56a972a821.png" alt="GitHub secrets ui" /></p>
<p>Create a repository secret <code>JRELEASER_GPG_PUBLIC_KEY</code> with a value from running:</p>
<pre><code class="lang-plaintext">$ gpg --export yourKeyId | base64
</code></pre>
<p>Create a key <code>JRELEASER_GPG_SECRET_KEY</code> with a value from running:</p>
<pre><code class="lang-plaintext">$ gpg --export-secret-keys yourKeyId | base64
</code></pre>
<p>Create a key <code>JRELEASER_GPG_PASSPHRASE</code> with a value that is a passphrase you used when creating your gpg key.</p>
<p>Two more secrets unrelated to GPG are needed to release to Maven Central:</p>
<p>Create a key <code>JRELEASER_NEXUS2_USERNAME</code> with the username you use to log in to Sonatype JIRA.</p>
<p>Create a key <code>JRELEASER_NEXUS2_PASSWORD</code> with the password you use to log in to Sonatype JIRA.</p>
<h2 id="heading-42-create-secrets-with-github-cli">4.2. Create secrets with GitHub CLI</h2>
<p>If you choose to use the CLI instead, run the following commands (replace things in &lt; brackets &gt; with real values) from the directory where your project is cloned:</p>
<pre><code class="lang-plaintext">$ gh secret set JRELEASER_GPG_PUBLIC_KEY -b $(gpg --export &lt;key id&gt; | base64)
$ gh secret set JRELEASER_GPG_SECRET_KEY -b $(gpg --export-secret-keys &lt;key id&gt; | base64)
$ gh secret set JRELEASER_GPG_PASSPHRASE -b &lt;passphrase&gt;
$ gh secret set JRELEASER_NEXUS2_USERNAME -b &lt;sonatype-jira-username&gt;
$ gh secret set JRELEASER_NEXUS2_PASSWORD -b &lt;sonatype-jira-password&gt;
</code></pre>
<h2 id="heading-5-create-a-publishing-config">5. Create a publishing config</h2>
<p>Here is an example config you can adjust to your needs:</p>
<pre><code class="lang-plaintext">publishing{
  publications {
    create&lt;MavenPublication&gt;("Maven") {
      from(components["java"])
      groupId = "de.tschuehly"
      artifactId = "spring-view-component-thymeleaf"
      description = "Create server rendered components with thymeleaf"
    }
    withType&lt;MavenPublication&gt; {
      pom {
        packaging = "jar"
        name.set("spring-view-component-thymeleaf")
        description.set("Spring ViewComponent Thymeleaf")
        url.set("https://github.com/tschuehly/spring-view-component/")
        inceptionYear.set("2023")
        licenses {
          license {
            name.set("MIT license")
            url.set("https://opensource.org/licenses/MIT")
          }
        }
        developers {
          developer {
            id.set("tschuehly")
            name.set("Thomas Schuehly")
            email.set("thomas.schuehly@outlook.com")
          }
        }
        scm {
          connection.set("scm:git:git@github.com:tschuehly/spring-view-component.git")
          developerConnection.set("scm:git:ssh:git@github.com:tschuehly/spring-view-component.git")
          url.set("https://github.com/tschuehly/spring-view-component")
        }
      }
    }
  }
  repositories {
    maven {
        url = layout.buildDirectory.dir("staging-deploy").get().asFile.toURI()
    }
  }
}
</code></pre>
<h2 id="heading-51-generate-javadocs-and-sources-jars">5.1. Generate javadocs and sources JARs</h2>
<p>Artifacts uploaded to Maven Central must have two extra jars: one with sources and one with Javadocs. Both are created by Gradle.</p>
<pre><code class="lang-plaintext">java {
  withJavadocJar()
  withSourcesJar()
}

tasks.jar{
  enabled = true
  // Remove `plain` postfix from jar file name
  archiveClassifier.set("")
}
</code></pre>
<h2 id="heading-52-configure-jreleaser-maven-plugin">5.2 Configure JReleaser Maven Plugin</h2>
<p>JReleaser can be invoked either as a standalone CLI application or a Gradle Plugin. To use the Gradle Plugin you need to add these plugins:</p>
<pre><code class="lang-plaintext">plugins {
  id("maven-publish")
  id("org.jreleaser") version "1.5.1"
  id("signing")
}
</code></pre>
<p>Add the following plugin configuration to the plugins section of the <code>release</code> profile:</p>
<pre><code class="lang-plaintext">jreleaser {
  project {
    copyright.set("Thomas Schuehly")
  }
  gitRootSearch.set(true)
  signing {
    active.set(Active.ALWAYS)
      armored.set(true)
  }
  deploy {
      maven {
        nexus2 {
          create("maven-central") {
            active.set(Active.ALWAYS)
            url.set("https://s01.oss.sonatype.org/service/local")
            closeRepository.set(true)
            releaseRepository.set(true)
            stagingRepositories.add("build/staging-deploy")
          }  
        }
    }
  }
}
</code></pre>
<p>I recommend to set temporarily <code>closeRepository</code> and <code>releaseRepository</code> to <code>false</code>. At the end, once you successfully release the first version to the staging repository in Sonatype Nexus you can switch it to <code>true</code>.</p>
<h2 id="heading-6-create-a-github-action">6. Create a GitHub action</h2>
<p>The GitHub action will trigger the release each time a tag that starts with <code>v</code> is created, like <code>v1.0</code>, <code>v1.1</code> etc.</p>
<p>Create a file in your project directory under <code>.github/workflows/release.yml</code>:</p>
<pre><code class="lang-plaintext">name: Publish package to the Maven Central Repository
on:
  push:
    tags:
      - v*
  pull_request:
    branches: [ main ]
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up Java
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'adopt'
      - name: Run chmod to make gradlew executable
        run: chmod +x ./gradlew
      - name: Publish package to local staging directory
        run: ./gradlew :publish
      - name: Publish package to maven central
        env:
          JRELEASER_NEXUS2_USERNAME: ${{ secrets.JRELEASER_NEXUS2_USERNAME }}
          JRELEASER_NEXUS2_PASSWORD: ${{ secrets.JRELEASER_NEXUS2_PASSWORD }}
          JRELEASER_GPG_PASSPHRASE: ${{ secrets.JRELEASER_GPG_PASSPHRASE }}
          JRELEASER_GPG_SECRET_KEY: ${{ secrets.JRELEASER_GPG_SECRET_KEY }}
          JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.JRELEASER_GPG_PUBLIC_KEY }}
          JRELEASER_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: ./gradlew :jreleaserDeploy -DaltDeploymentRepository=local::file:./build/staging-deploy
</code></pre>
<p>Adjust the Java version and the distribution to your needs.</p>
<p>The action will <a target="_blank" href="https://jreleaser.org/guide/latest/examples/maven/staging-artifacts.html">stage artifact</a> and then run <code>jreleaser:deploy</code> goal to publish artifact to Sonatype Nexus.</p>
<h2 id="heading-7-get-familiar-with-sonatype-nexus-ui">7. Get familiar with Sonatype Nexus UI</h2>
<p>Once you create and push the first tag and the GitHub Action finishes with success, you can log in to <a target="_blank" href="https://s01.oss.sonatype.org/">Sonatype Nexus</a><strong>with your Sonatype JIRA credentials</strong> to preview your staging repository.</p>
<p>In the <code>Staging Profiles</code> section you will see all the group ids you own:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709319684987/a3e6ceff-9e96-438c-9078-0a38f1a4913a.png" alt="Sonatype Staging Profiles" /></p>
<p>If you set <code>closeRepository</code> and <code>releaseRepository</code> to <code>false</code> in JReleaser configuration, in the <code>Staging Repositories</code> section you will see an entry for the version that was released with a GitHub action:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709319686005/5c937316-a8a0-4250-ae33-4cc96f01195a.png" alt="Sonatype Staging Repositories" /></p>
<p>(image from <a target="_blank" href="https://help.sonatype.com/repomanager2/staging-releases/managing-staging-repositories">https://help.sonatype.com/repomanager2/staging-releases/managing-staging-repositories</a>)</p>
<p>The first time I did it I needed to wait a long time and there were quite a few timeouts.</p>
<p>Here you can <code>Close</code> the repository and <code>Release</code>. Both actions trigger series of verifications - if your <code>gradle.build.kts</code> meets criteria, if packages are properly signed, if your GPG key is uploaded to the key server.</p>
<p>I recommend triggering these actions manually for the first version you release just to see if everything is fine. Once the <code>Release</code> action finishes with success, your library is considered as <strong>published to Maven Central</strong>. Congratulations 🎉</p>
<p>You can now set <code>closeRepository</code> and <code>releaseRepository</code> to <code>true</code> in JReleaser configuration.</p>
<h2 id="heading-8-when-is-the-library-actually-available-to-use">8. When is the library actually available to use?</h2>
<p>The library is not immediately available after it is released. Official documentation says that it may take up to 30 minutes before the package is available, some folks claim that it can take few hours. In my case it took just 10 minutes.</p>
<p>Now your artifact can be referenced in <code>build.gradle.kts</code> and Gradle will successfully download it. If you try to do it before it is available, Gradle will mark this library as unavailable and will not try to re-download it until the cache expires. Use <code>--refresh-dependencies</code> flag to <code>.\gradlew</code> command to force Gradle to check for updates:</p>
<pre><code class="lang-plaintext">$ ./gradlew build --refresh-dependencies
</code></pre>
<p>Don't be fooled by the results in <a target="_blank" href="https://search.maven.org/">search.maven.org</a> or <a target="_blank" href="https://mvnrepository.com/">mvnrepository.com</a>. Here your artifact or even a new version of the artifact will appear after around 24 hours.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>I hope this guide was useful, and it helped you to release a library to Maven Central.</p>
<p>If you find anything unclear drop me a message on <a target="_blank" href="https://twitter.com/tschuehly">Twitter</a>.</p>
<p>Most of this guide is based on <a target="_blank" href="https://twitter.com/maciejwalkowiak">Maciej Walkowiak</a> excellent guide so drop him a thanks!</p>
<p>I would like to thank <a target="_blank" href="https://twitter.com/aalmiray">Andres Almiray</a> for creating JReleaser. This library significantly simplifies the whole process to the point that it's not overcomplicated anymore.</p>
<p>If you want to learn more about HTMX + Spring Boot check out my series <a target="_blank" href="https://tschuehly.de/series/full-stack">Web development without the JavaScript headache with Spring + HTMX</a>.</p>
<p>My side business <a target="_blank" href="https://photoquest.wedding/">PhotoQuest</a> is also built with HTMX + JTE</p>
]]></content:encoded></item><item><title><![CDATA[Toasts with HTMX, Thymeleaf, and Spring Boot]]></title><description><![CDATA[In this example, I will show you a way to return Toast Notifications from your server and render them interactively.
The HTML:
We will start with an HTMX Element that creates a request to our spring server:
<form class="" hx-target="#toast" hx-put="/...]]></description><link>https://tschuehly.de/dynamic-server-rendered-toast-notification-with-htmx-thymeleaf-and-spring-boot</link><guid isPermaLink="true">https://tschuehly.de/dynamic-server-rendered-toast-notification-with-htmx-thymeleaf-and-spring-boot</guid><category><![CDATA[Springboot]]></category><category><![CDATA[Spring]]></category><category><![CDATA[htmx]]></category><category><![CDATA[alpinejs]]></category><dc:creator><![CDATA[Thomas Schilling]]></dc:creator><pubDate>Mon, 20 Feb 2023 08:36:57 GMT</pubDate><content:encoded><![CDATA[<p>In this example, I will show you a way to return Toast Notifications from your server and render them interactively.</p>
<h2 id="heading-the-html">The HTML:</h2>
<p>We will start with an HTMX Element that creates a request to our spring server:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">form</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span> <span class="hljs-attr">hx-target</span>=<span class="hljs-string">"#toast"</span> <span class="hljs-attr">hx-put</span>=<span class="hljs-string">"/api/someRoute"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>
</code></pre>
<p>As you can see we target an element with the id toast. To make this work I created a div with the id that is inside my footer. The footer is shown on every page.</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">footer</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"toast"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">footer</span>&gt;</span>
</code></pre>
<p>But what if you don't want to target the toast element, and instead you want to target another element? I will show you later how you can <a class="post-section-overview" href="#change-the-htmx-target-on-the-server">change the HTMX target on the server</a>.</p>
<p>Next is our toast element, to make them generic we use parameterized Thymeleaf fragments. In this example, I use the awesome <a target="_blank" href="https://alpinejs.dev/">Alpine.js</a> library, but this is also easily doable with normal javascript.</p>
<p>We create a toast.html file in our root directory to use it later as a return view.</p>
<pre><code class="lang-html">
<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">th:fragment</span>=<span class="hljs-string">"info(message,duration)"</span>
     <span class="hljs-attr">th:attr</span>=<span class="hljs-string">"x-init='setTimeout(() =&gt; $el.style.display = \'none\',' + ${duration} + ')'"</span>
     <span class="hljs-attr">class</span>=<span class="hljs-string">"fixed bottom-10 z-40 left-1/2 -translate-x-1/2"</span>
     <span class="hljs-attr">x-data</span>=<span class="hljs-string">"{}"</span>
     <span class="hljs-attr">x-ref</span>=<span class="hljs-string">"info"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"alert alert-info shadow-lg my-6"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">th:text</span>=<span class="hljs-string">"${message}"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"$refs.info.style.display = 'none'"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"/svg/x-circle.svg"</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>To break it down:</p>
<ul>
<li><p><code>th:fragment="info(message,duration)</code> attribute declares that this is a parameterized Thymeleaf fragment we can later use in our spring backend.</p>
</li>
<li><p><code>th:attr="x-init='setTimeout(() =&gt; $el.style.display = \'none\',' + ${duration} + ')'"</code> creates a timeout function on initialization that hides the toast after the duration we specified in the fragment parameter</p>
</li>
<li><p>Next, we position the element with the tailwind classes <code>fixed bottom-10 z-40 left-1/2 -translate-x-1/2</code></p>
</li>
<li><p>The <code>&lt;span th:text="${message}"&gt;&lt;/span&gt;</code> element renders our text</p>
</li>
<li><p>With the <code>&lt;button @click="$refs.info.style.display = 'none'"&gt;</code> our user can click the toast away before it expires.</p>
</li>
</ul>
<p>I also duplicated this element and changed the info to error and changed the styling to return info or error toasts.</p>
<h2 id="heading-the-backend-code">The backend code:</h2>
<p>To specify our toast I used an enum class:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">enum</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Toast</span> </span>{
    INFO {
        <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">ModelAndView</span><span class="hljs-params">(message: <span class="hljs-type">String</span>, durationInMs: <span class="hljs-type">Int</span>)</span></span> =
            ModelAndView(<span class="hljs-string">"toast :: info(message = '<span class="hljs-subst">${message}</span>', duration = '<span class="hljs-variable">$durationInMs</span>')"</span>)
    },
    ERROR {
        <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">ModelAndView</span><span class="hljs-params">(message: <span class="hljs-type">String</span>, durationInMs: <span class="hljs-type">Int</span>)</span></span> =
            ModelAndView(<span class="hljs-string">"toast :: error(message = '<span class="hljs-subst">${message}</span>', duration = '<span class="hljs-variable">$durationInMs</span>')"</span>)
    };

    <span class="hljs-keyword">abstract</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">ModelAndView</span><span class="hljs-params">(message: <span class="hljs-type">String</span>, durationInMs: <span class="hljs-type">Int</span>)</span></span>: ModelAndView
}
</code></pre>
<p>We can return this enum in our Controller if we want to show either an info message or an error message.</p>
<p>As you can see we return the parameterized fragment with a message we can extract from our business context and give our user valuable information.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Controller</span>
ExampleController() {
    <span class="hljs-meta">@RequestMapping(<span class="hljs-meta-string">"/api/someRoute"</span>)</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">someRoute</span><span class="hljs-params">()</span></span>: ModelAndView {
        <span class="hljs-keyword">try</span> {
            <span class="hljs-comment">// do Something</span>
        } <span class="hljs-keyword">catch</span> (e: Exception) {
            <span class="hljs-keyword">return</span> Toast.ERROR.ModelAndView(<span class="hljs-string">"An Error occurred: <span class="hljs-subst">${e.message}</span>"</span>, <span class="hljs-number">5000</span>)
        }

    }
}
</code></pre>
<h2 id="heading-change-the-htmx-target-on-the-server">Change the HTMX target on the server</h2>
<p>But what do you do when you want to swap the element itself and only show a toast if an error occurs?</p>
<pre><code class="lang-html">
<span class="hljs-tag">&lt;<span class="hljs-name">form</span> <span class="hljs-attr">hx-put</span>=<span class="hljs-string">"/api/someRoute"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>
</code></pre>
<p>We can change the behavior of HTMX easily with two Response Headers:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@RequestMapping(<span class="hljs-meta-string">"/api/someRoute"</span>)</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">someRoute</span><span class="hljs-params">(
    httpServletResponse: <span class="hljs-type">HttpServletResponse</span>
)</span></span>: ModelAndView {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-comment">// do Something</span>
    } <span class="hljs-keyword">catch</span> (e: Exception) {
        httpServletResponse.addHeader(<span class="hljs-string">"HX-Retarget"</span>, <span class="hljs-string">"#errors"</span>);
        httpServletResponse.addHeader(<span class="hljs-string">"HX-Reswap"</span>, <span class="hljs-string">"innerHTML"</span>);
        <span class="hljs-keyword">return</span> Toast.ERROR.ModelAndView(<span class="hljs-string">"An Error occurred: <span class="hljs-subst">${e.message}</span>"</span>, <span class="hljs-number">5000</span>)
    }

}
</code></pre>
<p>We just include the httpServletResponse: HttpServletResponse in the constructor of our function, and then</p>
<ul>
<li><p>With <code>HX-Retarget</code> we can change the <a target="_blank" href="https://htmx.org/attributes/hx-target/">hx-target</a> property</p>
</li>
<li><p>And with <code>HX-Reswap</code> we can change the <a target="_blank" href="https://htmx.org/attributes/hx-swap/">hx-swap</a> property</p>
</li>
</ul>
<p>There are many more HTMX <a target="_blank" href="https://htmx.org/reference/#response_headers">Response Headers</a> you can change to influence the behavior of your application.</p>
<p>If you want to learn more about HTMX + Spring Boot check out my series <a target="_blank" href="https://tschuehly.de/series/full-stack">Web development without the JavaScript headache with Spring + HTMX</a>.</p>
<p>My side business <a target="_blank" href="https://photoquest.wedding/">PhotoQuest</a> is also built with HTMX + JTE</p>
]]></content:encoded></item><item><title><![CDATA[Local development with Testcontainers, Kotlin and Spring Boot]]></title><description><![CDATA[It is always the best to keep your development environment as close as you can to your production environment. Don't use a h2 database as it can behave differently to a production database: Issues mapping @Lob
Recently I found the article Local devel...]]></description><link>https://tschuehly.de/local-development-with-testcontainers-kotlin-and-spring-boot</link><guid isPermaLink="true">https://tschuehly.de/local-development-with-testcontainers-kotlin-and-spring-boot</guid><category><![CDATA[Springboot]]></category><category><![CDATA[Testcontainers]]></category><category><![CDATA[Kotlin]]></category><dc:creator><![CDATA[Thomas Schilling]]></dc:creator><pubDate>Fri, 08 Jul 2022 10:00:00 GMT</pubDate><content:encoded><![CDATA[<p>It is always the best to keep your development environment as close as you can to your production environment. Don't use a h2 database as it can behave differently to a production database: <a target="_blank" href="/posts/spring-content/#issues">Issues mapping @Lob</a></p>
<p>Recently I found the article <a target="_blank" href="https://bsideup.github.io/posts/local_development_with_testcontainers/">Local development with Testcontainers</a> by Sergei Egorov <a target="_blank" href="https://twitter.com/bsideup">@bsideup</a>.</p>
<p>I couldn't get it working with Kotlin, but after some help from the awesome twitter developer community I got it working.</p>
<p>Refactor your Application.kt, so you expose createSpringApplication as a static method:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@SpringBootApplication</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ExampleApplication</span></span>{
    <span class="hljs-keyword">companion</span> <span class="hljs-keyword">object</span> {
        <span class="hljs-meta">@JvmStatic</span>
        <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">createSpringApplication</span><span class="hljs-params">()</span></span>: SpringApplication {
            <span class="hljs-keyword">return</span> SpringApplication(ExampleApplication::<span class="hljs-keyword">class</span>.java)
        }
    }

}

<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">main</span><span class="hljs-params">(args: <span class="hljs-type">Array</span>&lt;<span class="hljs-type">String</span>&gt;)</span></span> {
    runApplication&lt;ExampleApplication&gt;(*args)
}
</code></pre>
<p>Create a DevelopmentInitializer.kt in src/test/kotlin/com.example/DevelopmentInitializer.kt</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@RunWith(SpringRunner::class)</span>
<span class="hljs-meta">@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)</span>
<span class="hljs-meta">@ContextConfiguration(initializers = [DevelopmentInitializer.Initializer::class])</span>
<span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DevelopmentInitializer</span> </span>{
    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Initializer</span> : <span class="hljs-type">ApplicationContextInitializer</span>&lt;<span class="hljs-type">ConfigurableApplicationContext</span>&gt; </span>{
        <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">initialize</span><span class="hljs-params">(context: <span class="hljs-type">ConfigurableApplicationContext</span>)</span></span> {
            <span class="hljs-keyword">val</span> env = context.environment
            env.propertySources.addFirst(
                MapPropertySource(
                    <span class="hljs-string">"testcontainers"</span>, properties
                )
            )
        }

        <span class="hljs-keyword">companion</span> <span class="hljs-keyword">object</span> {
            <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> postgresContainer = PostgreSQLContainer(<span class="hljs-string">"postgres:14.4"</span>)
                .withUsername(<span class="hljs-string">"postgres"</span>)
                .withPassword(<span class="hljs-string">"password"</span>)
                .withDatabaseName(<span class="hljs-string">"postgres"</span>)
                .withExposedPorts(<span class="hljs-number">5432</span>)
                .withReuse(<span class="hljs-literal">true</span>)
            <span class="hljs-keyword">val</span> properties: Map&lt;String, String&gt;
                <span class="hljs-keyword">get</span>() {
                    Startables.deepStart(postgresContainer).join()
                    <span class="hljs-keyword">return</span> mapOf(
                        <span class="hljs-string">"spring.datasource.url"</span> to postgresContainer.jdbcUrl,
                        <span class="hljs-string">"spring.datasource.password"</span> to postgresContainer.password,
                        <span class="hljs-string">"spring.datasource.username"</span> to postgresContainer.username,
                    )
                }
        }
    }
}
</code></pre>
<p>To create reusable containers you need to set <code>testcontainers.reuse.enable=true</code> in <code>$HOME/.testcontainers.properties</code> file</p>
<p>Now create a DevelopmentApplication.kt in src/test/kotlin/com.example/DevelopmentApplication.kt</p>
<pre><code class="lang-kotlin"><span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">main</span><span class="hljs-params">(args: <span class="hljs-type">Array</span>&lt;<span class="hljs-type">String</span>&gt;)</span></span> {
    <span class="hljs-keyword">val</span> app = ExampleApplication.createSpringApplication()
    app.addInitializers(DevelopmentInitializer.Initializer())
    app.run(*args)
}
</code></pre>
<p>You can start DevelopmentApplication.main() to get your Spring Boot Application with an autoconfigured postgres container.</p>
<p>I created a working example at my GitHub: <a target="_blank" href="https://github.com/tschuehly/testcontainers-localdev-kotlin">github.com/tschuehly/testcontainers-localdev-kotlin</a></p>
<h2 id="heading-fixed-ports">Fixed Ports</h2>
<p>You can also use a FixedHostPortContainer, then you can connect for example your IntelliJ Database Explorer and don't have to reconfigure it if you restart your Docker Environment</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> postgresContainer = FixedHostPortGenericContainer(<span class="hljs-string">"postgres:14.4"</span>)
    .withEnv(<span class="hljs-string">"POSTGRES_USER"</span>,<span class="hljs-string">"postgres"</span>)
    .withEnv(<span class="hljs-string">"POSTGRES_PASSWORD"</span>,<span class="hljs-string">"password"</span>)
    .withEnv(<span class="hljs-string">"POSTGRES_DB"</span>,<span class="hljs-string">"postgres"</span>)
    .withFixedExposedPort(<span class="hljs-number">5432</span>,<span class="hljs-number">5432</span>)
    .withReuse(<span class="hljs-literal">true</span>)

<span class="hljs-keyword">val</span> properties: Map&lt;String, String&gt;
    <span class="hljs-keyword">get</span>(){
        Startables.deepStart(postgresContainer, minioContainer).join()
        <span class="hljs-keyword">return</span> mapOf(
            <span class="hljs-string">"spring.datasource.url"</span> to 
                    <span class="hljs-string">"jdbc:postgresql://"</span> + postgresContainer.host + <span class="hljs-string">":5432/postgres"</span>,
            <span class="hljs-string">"spring.datasource.password"</span> to <span class="hljs-string">"password"</span>,
            <span class="hljs-string">"spring.datasource.username"</span> to <span class="hljs-string">"postgres"</span>,
    )
}
</code></pre>
<p>If you want to learn more about HTMX + Spring Boot check out my series <a target="_blank" href="https://tschuehly.de/series/full-stack">Web development without the JavaScript headache with Spring + HTMX</a>.</p>
<p>My side business <a target="_blank" href="https://photoquest.wedding/">PhotoQuest</a> is also built with HTMX + JTE</p>
]]></content:encoded></item><item><title><![CDATA[Parallel processing with Worker Pools in Kotlin]]></title><description><![CDATA[Worker pools
Recently while doing a project with Go I came across Worker Pools on GoByExample to do parallel processing. I didn't find many resources for implementing Worker Pools in Kotlin, but it seemed a good idea for my current Spring Boot + Kotl...]]></description><link>https://tschuehly.de/parallel-processing-with-worker-pools-in-kotlin</link><guid isPermaLink="true">https://tschuehly.de/parallel-processing-with-worker-pools-in-kotlin</guid><category><![CDATA[Kotlin]]></category><category><![CDATA[Springboot]]></category><category><![CDATA[webdav]]></category><category><![CDATA[Hetzner]]></category><dc:creator><![CDATA[Thomas Schilling]]></dc:creator><pubDate>Wed, 22 Jun 2022 10:00:00 GMT</pubDate><content:encoded><![CDATA[<h1 id="heading-worker-pools">Worker pools</h1>
<p>Recently while doing a project with Go I came across Worker Pools on <a target="_blank" href="https://gobyexample.com/worker-pools">GoByExample</a> to do parallel processing. I didn't find many resources for implementing Worker Pools in Kotlin, but it seemed a good idea for my current Spring Boot + Kotlin application.</p>
<h1 id="heading-kotlin">Kotlin</h1>
<p>Kotlin uses coroutines for concurrency which are fairly similar to goroutines.</p>
<p>Coroutines use structured concurrency to delimit the lifetime of each coroutine to a certain scope.</p>
<p>To be able to create a worker group we need to create a coroutine scope that is persistent over the lifetime of our application. We achieve this behavior with the SupervisorJob() context.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
</code></pre>
<p>We then create a buffered channel as a queue for our image data and the URL where we want to upload it.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> channel = Channel&lt;Pair&lt;String, ByteArray&gt;&gt;(<span class="hljs-number">10000</span>)
</code></pre>
<p>I'm using the Spring @PostConstruct annotation to create the worker group and listen to the channel for new data. Each time an item is in the queue we launch the upload function, if no item is in the queue the function is suspended.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@PostConstruct</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">createWorkerGroup</span><span class="hljs-params">()</span></span> {
        coroutineScope.launch {
            <span class="hljs-keyword">for</span> (x <span class="hljs-keyword">in</span> <span class="hljs-number">1</span>..<span class="hljs-number">5</span>) {
                launch {
                    println(<span class="hljs-string">"Create Worker <span class="hljs-variable">$x</span>"</span>)
                    <span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {
                        uploadImage(channel.receive())
                    }
                }
            }
        }
    }
</code></pre>
<p>Finally, we can send our data to our channel inside a runBlocking coroutine scope:</p>
<pre><code class="lang-kotlin">runBlocking {
  uploadService.channel.send(Pair(url, image.bytes))
}
</code></pre>
<h1 id="heading-webdav">WebDav</h1>
<p>In my web application users upload images from their mobile phone to my webserver, afterwards I want to upload these pictures to a <a target="_blank" href="https://www.hetzner.com/storage/storage-box">Hetzner Storage Box</a> over webdav as a cheap alternative to an S3 object storage.</p>
<p>I use the <a target="_blank" href="https://github.com/lookfirst/sardine">sardine</a> java webdav client library for its simplicity.</p>
<p>The usage is very straightforward, you configure the client with:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> sardine = SardineFactory.begin(<span class="hljs-string">"webDavUsername"</span>, <span class="hljs-string">"webDavPassword"</span>)
</code></pre>
<p>The uploadImage Function is called every time a new image is sent over the channel we created earlier. In this function, we call sarding.put() to save the image file.</p>
<pre><code class="lang-kotlin">sardine.put(<span class="hljs-string">"https://username.your-storagebox.de/foldername/imagename.jpg"</span>, ImageByteArray)
</code></pre>
<p>That is all we need to have a highly parallel File upload.</p>
<p>If you want to learn more about HTMX + Spring Boot check out my series <a target="_blank" href="https://tschuehly.de/series/full-stack">Web development without the JavaScript headache with Spring + HTMX</a>.</p>
<p>My side business <a target="_blank" href="https://photoquest.wedding/">PhotoQuest</a> is also built with HTMX + JTE</p>
]]></content:encoded></item><item><title><![CDATA[Creating a GO GUI with Alpine.js and Webview]]></title><description><![CDATA[There are a lot of options for building a GUI for Go applications. Coming from the web development world building the front end with HTML seems like a no-brainer.
Webview
Webview is a tiny cross-platform library for C/C++/Golang to build modern cross...]]></description><link>https://tschuehly.de/creating-a-go-gui-with-alpinejs-and-webview</link><guid isPermaLink="true">https://tschuehly.de/creating-a-go-gui-with-alpinejs-and-webview</guid><category><![CDATA[Go Language]]></category><category><![CDATA[alpinejs]]></category><category><![CDATA[User Interface]]></category><dc:creator><![CDATA[Thomas Schilling]]></dc:creator><pubDate>Mon, 14 Mar 2022 16:55:59 GMT</pubDate><content:encoded><![CDATA[<p>There are a lot of options for building a <a target="_blank" href="https://golangr.com/gui/">GUI</a> for Go applications. Coming from the web development world building the front end with HTML seems like a no-brainer.</p>
<h2 id="heading-webview">Webview</h2>
<p><a target="_blank" href="https://github.com/webview/webview">Webview</a> is a tiny cross-platform library for C/C++/Golang to build modern cross-platform GUIs. The goal of the project is to create a common HTML5 UI abstraction layer for the most widely used platforms.</p>
<p>To start using Webview you need to install Webview: <code>go get github.com/webview/webview</code></p>
<p>On Windows, you need to have these two <a target="_blank" href="https://github.com/webview/webview/tree/master/dll/x64">dlls</a> in the project root folder.</p>
<p>It supports two-way JavaScript bindings (to call JavaScript from C/C++/Go and to call C/C++/Go from JavaScript). But writing pure JavaScript code for the interactivity is awful.</p>
<h2 id="heading-alpinejs-to-the-rescue">Alpine.js to the rescue</h2>
<p>"<a target="_blank" href="https://alpinejs.dev/">Alpine.js</a> is a rugged, minimal tool for composing behavior directly in your markup." It fits perfectly for our use case.</p>
<p>You can load Alpine inline or from a file. The newest version is available at <a target="_blank" href="unpkg.com/alpinejs">unpkg.com/alpinejs</a></p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">loadAlpine</span><span class="hljs-params">()</span> <span class="hljs-title">string</span></span> {
    <span class="hljs-keyword">return</span> <span class="hljs-string">"paste alpine.js source here"</span>
}
</code></pre>
<p>First, you must initialize Webview.</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    webView := webview.New(<span class="hljs-literal">true</span>)
    <span class="hljs-keyword">defer</span> webView.Destroy()

    webView.SetSize(<span class="hljs-number">600</span>, <span class="hljs-number">600</span>, webview.HintNone)
    webView.Init(loadAlpine())
</code></pre>
<p>To execute go code with Alpine we need to call webView.bind("functionName").</p>
<pre><code class="lang-go">webView.Bind(<span class="hljs-string">"extractSubDirectories"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(sourceFolder <span class="hljs-keyword">string</span>)</span> <span class="hljs-title">string</span></span> {
  folderUrls = extractSubDirectories(sourceFolder)
    tmpl := template.Must(template.New(<span class="hljs-string">"html"</span>).Parse(
      <span class="hljs-comment">// language=GoTemplate</span>
    <span class="hljs-string">`&lt;div&gt;
      {{range $vendor, $folderDetailsArray := .}}
        &lt;div&gt;
          &lt;h3&gt;Vendor: {{$vendor}}&lt;/h2&gt;
        {{range $folderDetails := $folderDetailsArray}}
          &lt;ul&gt;
            &lt;li&gt;{{ .Path }} filecount:: {{ .FileCount }}&lt;/li&gt;
          &lt;/ul&gt;
        {{end}}
        &lt;/div&gt;
      {{end}}
     &lt;/div&gt;`</span>))
  <span class="hljs-keyword">var</span> html bytes.Buffer
  err := tmpl.Execute(&amp;html, folderUrls)
  <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
    logger.WritePrint(<span class="hljs-string">"ERROR: "</span> + err.Error())
  }
  <span class="hljs-keyword">return</span> html.String()
})
</code></pre>
<p>To create your first page you call webView.Navigate() and supply it with your HTML. Then call webView.Run()</p>
<pre><code class="lang-go">webView.Navigate(<span class="hljs-string">`data:text/html`</span> + <span class="hljs-string">`&lt;!doctype html&gt;
&lt;html lang="de" x-data="{ pathInput: '', table : ''}"&gt;
    &lt;body style="padding: 2rem"&gt;
        &lt;h1&gt;JPEG Sorter&lt;/h1&gt;
        &lt;p&gt;Input the folder where the images are stored&lt;/p&gt;
        &lt;input type="text" x-model="pathInput"/&gt;

        &lt;button @click="table = ''; table = await extractSubDirectories(pathInput);"&gt;analyse folder&lt;/button&gt;

        &lt;div x-html=table&gt;&lt;/div&gt;
    &lt;/body&gt;
&lt;/html&gt;`</span>)
webView.Run()
</code></pre>
<h2 id="heading-alpinejs">Alpine.js</h2>
<p>As you can see there are quite a lot of non-standard html attributes.</p>
<p>This is the magic of alpine.js. You can create local alpine data variables in the scope of the element:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"de"</span> <span class="hljs-attr">x-data</span>=<span class="hljs-string">"{ pathInput: '', table : ''}"</span>&gt;</span>
</code></pre>
<p>You can bind input data to the local variables with x-model</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> <span class="hljs-attr">x-model</span>=<span class="hljs-string">"pathInput"</span>/&gt;</span>
</code></pre>
<p>But the coolest part comes now. With an @click alpine attribute, we can call our go functions from the HTML. The extractSubDirectories() function we bound earlier in this example.</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"table = await extractSubDirectories(pathInput);"</span>&gt;</span>
  analyse folder
<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
</code></pre>
<p>With x-html we can bind the returned HTML from the go function into our GUI.</p>
<pre><code class="lang-go">&lt;div x-html=table&gt;&lt;/div&gt;
</code></pre>
<p>These are the basic steps to get Webview and alpine.js working with Go.</p>
<h2 id="heading-gui-example">GUI Example</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709319690093/46c2ee3a-090e-4f92-984b-2737f27ba1f8.png" alt="GUI Example" /></p>
<p>If you want to learn more about HTMX + Spring Boot check out my series <a target="_blank" href="https://tschuehly.de/series/full-stack">Web development without the JavaScript headache with Spring + HTMX</a>.</p>
<p>My side business <a target="_blank" href="https://photoquest.wedding/">PhotoQuest</a> is also built with HTMX + JTE</p>
]]></content:encoded></item><item><title><![CDATA[JWT authentication for Spring Boot simplified using GoTrue and Supabase]]></title><description><![CDATA[In a quest to have a simpler JWT Authentication flow and not have to deal with security-related related userdata in my backend I discovered Supabase Auth which is an implementation of Netlify GoTrue.
For Kotlin there is the awesome supabase gotrue-kt...]]></description><link>https://tschuehly.de/jwt-authentication-for-spring-boot-simplified-using-gotrue-and-supabase</link><guid isPermaLink="true">https://tschuehly.de/jwt-authentication-for-spring-boot-simplified-using-gotrue-and-supabase</guid><category><![CDATA[Springboot]]></category><category><![CDATA[supabase]]></category><category><![CDATA[authentication]]></category><category><![CDATA[JWT]]></category><dc:creator><![CDATA[Thomas Schilling]]></dc:creator><pubDate>Tue, 01 Mar 2022 22:23:06 GMT</pubDate><content:encoded><![CDATA[<p>In a quest to have a simpler JWT Authentication flow and not have to deal with security-related related userdata in my backend I discovered Supabase Auth which is an implementation of Netlify GoTrue.</p>
<p>For Kotlin there is the awesome supabase <a target="_blank" href="https://github.com/supabase-community/gotrue-kt">gotrue-kt</a> library.</p>
<p>In your User Registration and Login Services you need to create a GoTrueClient</p>
<pre><code class="lang-plaintext">val goTrueClient = GoTrueClient.defaultGoTrueClient(
    url = "&lt;base-url&gt;",
    headers = mapOf("Authorization" to "foo", "apiKey" to "bar")
)
</code></pre>
<p>If you are using supabase, the base URL will be: <code>https://&lt;your-project-id&gt;.supabase.co/auth/v1</code></p>
<p>Then in your signup method you can just call signUpWithEmail().</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> authDTO = goTrueClient()
    .signUpWithEmail(credentials[<span class="hljs-string">"email"</span>]!!, credentials[<span class="hljs-string">"password"</span>]!!)
websiteUserRepository.save(WebsiteUser(authDTO))
</code></pre>
<p>With the default client, this returns a GoTrueUserResponse which most importantly contains a id that you then can persist in a WebsiteUser Authentication Pojo which holds information related to the user</p>
<p>With the goTrue Kotlin Library, you can also specify a custom return type for example if you turned email confirmation off.</p>
<p>We define our DTO:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthDTO</span></span>(
    <span class="hljs-keyword">val</span> accessToken: String,
    <span class="hljs-keyword">val</span> tokenType: String,
    <span class="hljs-keyword">val</span> refreshToken: String,
    <span class="hljs-keyword">val</span> user: User
)

<span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span></span>(
    <span class="hljs-keyword">val</span> id: UUID,
    <span class="hljs-keyword">val</span> email: String,
    <span class="hljs-keyword">val</span> phone: String

)
</code></pre>
<p>and then create a Client where we pass this DTO:</p>
<pre><code class="lang-plaintext">return GoTrueClient.customApacheJacksonGoTrueClient&lt;AuthDTO, GoTrueTokenResponse&gt;(url,headers)
</code></pre>
<p>In the Login method, we call signInWithEmail and then return the JWT from the GoTrue Response as Cookie</p>
<pre><code class="lang-plaintext">val repsonse = goTrueClient().signInWithEmail(
  credentials["email"],
  credentials["password"]
)
response.addCookie(
    Cookie("JWT", resp.accessToken).also {
        it.secure = true
        it.isHttpOnly = true
        it.path = "/"
        it.maxAge = 6000
    }
)
</code></pre>
<p>But we need to verify that the JWT is correct when a User requests a page and that the user has the required access rights.</p>
<p>We do this in a JWT Filter that overrides the doFilterInternal method from the OncePerRequestFilter().</p>
<p>When our current SecurityContext Authentication is empty we need to extract the JWT from the Cookie and get the UserID from GoTrue to find the WebsiteUser we persisted earlier. We then set the SecurityContext with the retrieved WebsiteUser</p>
<pre><code class="lang-plaintext">@Component
class JwtFilter(
    val websiteUserRepository: WebsiteUserRepository
) : OncePerRequestFilter() {
    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        if (SecurityContextHolder.getContext().authentication == null) {
            val auth = SecurityContextHolder.getContext()
            request.cookies?.find { it.name == "JWT" }?.let { cookie -&gt;
                try {
                    goTrueClient.getUser(cookie.value).let {
                       SecurityContextHolder.getContext().authentication = websiteUserRepository.findByIdWithJPQL(it.id)
                    }
                } catch (e: GoTrueHttpException) {
                    if (e.data?.contains("Invalid token") == true) {
                        val oldCookie = request.cookies.find { it.name == "JWT" }
                        oldCookie?.maxAge = 0

                        response.addCookie(oldCookie)
                        response.sendRedirect("/")
                    }
                }
            }
        }

        filterChain.doFilter(request, response)
    }
}
</code></pre>
<p>At last, we add this filter in our WebSecurityConfiguration:</p>
<pre><code class="lang-plaintext">@Configuration
@EnableWebSecurity(debug = false)
class SpringSecurityConfig(
    val jwtFilter: JwtFilter
) : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        http.formLogin()
            .loginPage("/login")
            .and()
            .logout()
            .deleteCookies("JWT","authenticated")
            .logoutUrl("/logout")
            .logoutSuccessUrl("/")
            // Our private endpoints
            .antMatchers("/konto").hasRole("USER")
            .antMatchers("/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated()
            .and()
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter::class.java)
    }
</code></pre>
<p>If you want to learn more about HTMX + Spring Boot check out my series <a target="_blank" href="https://tschuehly.de/series/full-stack">Web development without the JavaScript headache with Spring + HTMX</a>.</p>
<p>My side business <a target="_blank" href="https://photoquest.wedding/">PhotoQuest</a> is also built with HTMX + JTE</p>
]]></content:encoded></item></channel></rss>