<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Thomas Schühly</title>
    <description>The latest articles on DEV Community by Thomas Schühly (@tschuehly).</description>
    <link>https://dev.to/tschuehly</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F591553%2F27ec9c6d-5471-42c1-b3b6-ac206c3c72a1.jpg</url>
      <title>DEV Community: Thomas Schühly</title>
      <link>https://dev.to/tschuehly</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tschuehly"/>
    <language>en</language>
    <item>
      <title>How to publish a Kotlin/Java Spring Boot library with Gradle to Maven Central - Complete Guide</title>
      <dc:creator>Thomas Schühly</dc:creator>
      <pubDate>Wed, 08 Nov 2023 12:03:35 +0000</pubDate>
      <link>https://dev.to/tschuehly/how-to-publish-a-kotlinjava-spring-boot-library-with-gradle-to-maven-central-complete-guide-402a</link>
      <guid>https://dev.to/tschuehly/how-to-publish-a-kotlinjava-spring-boot-library-with-gradle-to-maven-central-complete-guide-402a</guid>
      <description>&lt;p&gt;This is an opinionated &lt;strong&gt;step-by-step guide&lt;/strong&gt; on how to &lt;strong&gt;publish a Kotlin/Java library with Gradle to Maven Central&lt;/strong&gt;&lt;br&gt;
repository.&lt;/p&gt;

&lt;p&gt;It assumes that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the project is built with &lt;strong&gt;Gradle&lt;/strong&gt; (look at Maciej Guide if you want to do it with Maven)&lt;/li&gt;
&lt;li&gt;the project code is hosted on &lt;strong&gt;GitHub&lt;/strong&gt; and &lt;strong&gt;GitHub Actions&lt;/strong&gt; are used to trigger the release&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It uses &lt;a href="https://jreleaser.org/" rel="noopener noreferrer"&gt;JReleaser&lt;/a&gt; - I believe this is the simplest and the most straightforward way of signing&lt;br&gt;
and uploading artifacts.&lt;/p&gt;

&lt;p&gt;This guide is based on the excellent&lt;br&gt;
article &lt;a href="https://maciejwalkowiak.com/blog/guide-java-publish-to-maven-central/" rel="noopener noreferrer"&gt;How to publish a Java library to Maven Central - Complete Guide&lt;/a&gt;&lt;br&gt;
but uses Gradle instead of Maven.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create an account in Sonatype JIRA&lt;/li&gt;
&lt;li&gt;
Create a "New Project" ticket

&lt;ol&gt;
&lt;li&gt;If a custom domain is used as a group id&lt;/li&gt;
&lt;li&gt;If GitHub is used as a group id&lt;/li&gt;
&lt;li&gt;Set ticket to "Open"&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
Create GPG keys

&lt;ol&gt;
&lt;li&gt;Export key to a key server&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
Export public and secret key to GitHub secrets

&lt;ol&gt;
&lt;li&gt;Create GitHub secrets with UI&lt;/li&gt;
&lt;li&gt;Create secrets with GitHub CLI&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
Adjust pom.xml

&lt;ol&gt;
&lt;li&gt;Generate javadocs and sources JARs&lt;/li&gt;
&lt;li&gt;Configure JReleaser Maven Plugin&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Create a GitHub action&lt;/li&gt;
&lt;li&gt;Get familiar with Sonatype Nexus UI&lt;/li&gt;
&lt;li&gt;When is the library actually available to use?&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  1. Create an account in Sonatype JIRA
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://issues.sonatype.org/secure/Signup!default.jspa" rel="noopener noreferrer"&gt;Sign up in Sonatype JIRA&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You do it &lt;strong&gt;only once&lt;/strong&gt; - no matter how many projects you want to release or how many group ids you own.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Create a "New Project" ticket
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://issues.sonatype.org/secure/CreateIssue.jspa?pid=10134&amp;amp;issuetype=21" rel="noopener noreferrer"&gt;Create a "New Project ticket&lt;/a&gt; in&lt;br&gt;
Sonatype JIRA.&lt;/p&gt;

&lt;p&gt;This step is done &lt;strong&gt;once per group id&lt;/strong&gt;. Meaning, for each domain you want to use as a group id - you must create a new&lt;br&gt;
project request.&lt;/p&gt;

&lt;p&gt;Although the official Sonatype guide claims that normally, the process takes less than 2 business days. In my case it&lt;br&gt;
took just a few minutes.&lt;/p&gt;

&lt;p&gt;Once the ticket is created, a Sonatype JIRA bot will post comments with instructions what to do next:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnlkojhvjgc39qq5ig5wh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnlkojhvjgc39qq5ig5wh.png" alt="SonaType Bot Comment" width="763" height="443"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  2.1. If a custom domain is used as a group id
&lt;/h2&gt;

&lt;p&gt;When you want to use a domain like &lt;code&gt;de.tschuehly&lt;/code&gt; as a group id - you must own the domain - and be able to prove it. *&lt;br&gt;
&lt;em&gt;You must add a DNS TXT record with a JIRA ticket id&lt;/em&gt;* to your domain - this is done in the admin panel where your&lt;br&gt;
domain is hosted.&lt;/p&gt;

&lt;p&gt;Once you have added the record, verify that it is added with the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ dig -t txt tschuehly.de
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4err0h7wqybwekpu9s3f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4err0h7wqybwekpu9s3f.png" alt="dig -t txt output" width="800" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2.2. If GitHub is used as a group id
&lt;/h2&gt;

&lt;p&gt;If you don't own the domain it is possible to use your GitHub coordinates as a group id. For example, my GitHub account&lt;br&gt;
name is &lt;code&gt;tschuehly&lt;/code&gt;, so I can use &lt;code&gt;io.github.tschuehly&lt;/code&gt; as a group id.&lt;/p&gt;

&lt;p&gt;To prove that you own that GitHub account, create a temporary repository with a name reflecting the JIRA ticket id.&lt;/p&gt;

&lt;p&gt;This can be done via &lt;a href="https://github.com/new" rel="noopener noreferrer"&gt;github.com/new&lt;/a&gt; or with GitHub CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gh repo create OSSRH-91026 --public
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2.3. Set ticket to "Open"
&lt;/h2&gt;

&lt;p&gt;The comment posted by Sonatype bot says that once you are done with either creating a DNS record or creating a GitHub&lt;br&gt;
repository, "Edit this ticket and set Status to Open.".&lt;/p&gt;

&lt;p&gt;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&lt;br&gt;
top of JIRA ticket, right next to "Agile Board" and "More" (unfortunately I did not make a screenshot on time).&lt;/p&gt;

&lt;p&gt;Once you do it, another comment will be posted by Sonatype bot:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fawn0qsjf5ltcoikcvzg6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fawn0qsjf5ltcoikcvzg6.png" alt="Sonatype Bot success message" width="725" height="277"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This means that our job in the Sonatype JIRA is done. Congratulations 🎉&lt;/p&gt;

&lt;p&gt;(you can now drop the temporary GitHub repository if you've created one)&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Create GPG keys
&lt;/h2&gt;

&lt;p&gt;Artifacts sent to Maven Central must be signed. To sign artifacts you need to generate &lt;strong&gt;GPG keys&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This must be done only once&lt;/strong&gt; - all artifacts you publish to Maven Central can be signed with the same pair of keys.&lt;/p&gt;

&lt;p&gt;Create a key pair with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gpg --gen-key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put your name, email address and passphrase.&lt;/p&gt;

&lt;p&gt;List keys with command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gpg --list-keys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will see output like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example - &lt;code&gt;05342E4134D1F7C1B08F900FC2377C0DD0494024&lt;/code&gt; is the key id. Find your own key id and copy it to&lt;br&gt;
clipboard.&lt;/p&gt;

&lt;p&gt;If you can't find it, you probably used a wrong version of gpg. It didn't work on my Windows machine but worked on my&lt;br&gt;
linux server&lt;/p&gt;
&lt;h2&gt;
  
  
  3.1 Export key to a key server
&lt;/h2&gt;

&lt;p&gt;Next you need to export the public key to a key server with command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gpg --keyserver keyserver.ubuntu.com --send-keys yourKeyId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Export public and secret key to GitHub secrets
&lt;/h2&gt;

&lt;p&gt;JReleaser needs public and secret key to sign artifacts. Since signing will be done by a GitHub action, you need to&lt;br&gt;
export these keys as GitHub secrets.&lt;/p&gt;

&lt;p&gt;Secrets can be set either on the GitHub repository website or with a GitHub CLI.&lt;/p&gt;
&lt;h2&gt;
  
  
  4.1. Create GitHub secrets with UI
&lt;/h2&gt;

&lt;p&gt;Go to repository &lt;code&gt;Settings&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx7vqs69484fwp7qdhn37.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx7vqs69484fwp7qdhn37.png" alt="GitHub secrets ui" width="800" height="633"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Create a repository secret &lt;code&gt;JRELEASER_GPG_PUBLIC_KEY&lt;/code&gt; with a value from running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gpg --export yourKeyId | base64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a key &lt;code&gt;JRELEASER_GPG_SECRET_KEY&lt;/code&gt; with a value from running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gpg --export-secret-keys yourKeyId | base64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a key &lt;code&gt;JRELEASER_GPG_PASSPHRASE&lt;/code&gt; with a value that is a passphrase you used when creating your gpg key.&lt;/p&gt;

&lt;p&gt;Two more secrets unrelated to GPG are needed to release to Maven Central:&lt;/p&gt;

&lt;p&gt;Create a key &lt;code&gt;JRELEASER_NEXUS2_USERNAME&lt;/code&gt; with the username you use to log in to Sonatype JIRA.&lt;/p&gt;

&lt;p&gt;Create a key &lt;code&gt;JRELEASER_NEXUS2_PASSWORD&lt;/code&gt; with the password you use to log in to Sonatype JIRA.&lt;/p&gt;

&lt;h2&gt;
  
  
  4.2. Create secrets with GitHub CLI
&lt;/h2&gt;

&lt;p&gt;If you choose to use the CLI instead, run the following commands (replace things in &amp;lt; brackets &amp;gt; with real values) from the&lt;br&gt;
directory where your project is cloned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gh secret set JRELEASER_GPG_PUBLIC_KEY -b $(gpg --export &amp;lt;key id&amp;gt; | base64)
$ gh secret set JRELEASER_GPG_SECRET_KEY -b $(gpg --export-secret-keys &amp;lt;key id&amp;gt; | base64)
$ gh secret set JRELEASER_GPG_PASSPHRASE -b &amp;lt;passphrase&amp;gt;
$ gh secret set JRELEASER_NEXUS2_USERNAME -b &amp;lt;sonatype-jira-username&amp;gt;
$ gh secret set JRELEASER_NEXUS2_PASSWORD -b &amp;lt;sonatype-jira-password&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Create publishing config
&lt;/h2&gt;

&lt;p&gt;Here is an example config you can adjust to your needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;publishing{
  publications {
    create&amp;lt;MavenPublication&amp;gt;("Maven") {
      from(components["java"])
      groupId = "de.tschuehly"
      artifactId = "spring-view-component-thymeleaf"
      description = "Create server rendered components with thymeleaf"
    }
    withType&amp;lt;MavenPublication&amp;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()
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5.1. Generate javadocs and sources JARs
&lt;/h2&gt;

&lt;p&gt;Artifacts uploaded to Maven Central must have two extra jars: one with sources and one with Javadocs. Both are created&lt;br&gt;
by Gradle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;java {
  withJavadocJar()
  withSourcesJar()
}

tasks.jar{
  enabled = true
  // Remove `plain` postfix from jar file name
  archiveClassifier.set("")
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5.2 Configure JReleaser Maven Plugin
&lt;/h2&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;plugins {
  id("maven-publish")
  id("org.jreleaser") version "1.5.1"
  id("signing")
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add following plugin configuration to the plugins section of the &lt;code&gt;release&lt;/code&gt; profile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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")
        }  
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I recommend to set temporarily &lt;code&gt;closeRepository&lt;/code&gt; and &lt;code&gt;releaseRepository&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt;. At the end once you successfully&lt;br&gt;
release the first version to staging repository in Sonatype Nexus you can switch it to &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  6. Create a GitHub action
&lt;/h2&gt;

&lt;p&gt;The GitHub action will trigger the release each time a tag that starts with &lt;code&gt;v&lt;/code&gt; is created, like &lt;code&gt;v1.0&lt;/code&gt;, &lt;code&gt;v1.1&lt;/code&gt; etc.&lt;/p&gt;

&lt;p&gt;Create a file in your project directory under &lt;code&gt;.github/workflows/release.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adjust the Java version and the distribution to your needs.&lt;/p&gt;

&lt;p&gt;The action will &lt;a href="https://jreleaser.org/guide/latest/examples/maven/staging-artifacts.html" rel="noopener noreferrer"&gt;stage artifact&lt;/a&gt; and then&lt;br&gt;
run &lt;code&gt;jreleaser:deploy&lt;/code&gt; goal to publish artifact to Sonatype Nexus.&lt;/p&gt;
&lt;h2&gt;
  
  
  7. Get familiar with Sonatype Nexus UI
&lt;/h2&gt;

&lt;p&gt;Once you create and push first tag and the GitHub Action finishes with success, you can log in&lt;br&gt;
to &lt;a href="https://s01.oss.sonatype.org/" rel="noopener noreferrer"&gt;Sonatype Nexus&lt;/a&gt; &lt;strong&gt;with your Sonatype JIRA credentials&lt;/strong&gt; to preview your staging&lt;br&gt;
repository.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;Staging Profiles&lt;/code&gt; section you will see all the group ids you own:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fis5e7n3eu4o9d2dtah8h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fis5e7n3eu4o9d2dtah8h.png" alt="Sonatype Staging Profiles" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you set &lt;code&gt;closeRepository&lt;/code&gt; and &lt;code&gt;releaseRepository&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt; in JReleaser configuration, in&lt;br&gt;
the &lt;code&gt;Staging Repositories&lt;/code&gt; section you will see an entry for the version that was released with a GitHub action:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feo5rxtvwo8csj148ijqx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feo5rxtvwo8csj148ijqx.png" alt="Sonatype Staging Repositories" width="585" height="111"&gt;&lt;/a&gt;&lt;br&gt;
(image&lt;br&gt;
from &lt;a href="https://help.sonatype.com/repomanager2/staging-releases/managing-staging-repositories" rel="noopener noreferrer"&gt;https://help.sonatype.com/repomanager2/staging-releases/managing-staging-repositories&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;The first time I did it I needed to wait a long time and there were quite a few timeouts.&lt;/p&gt;

&lt;p&gt;Here you can &lt;code&gt;Close&lt;/code&gt; the repository and &lt;code&gt;Release&lt;/code&gt;. Both actions trigger series of verifications - if your &lt;code&gt;gradle.build.kts&lt;/code&gt;&lt;br&gt;
meets criteria, if packages are properly signed, if your GPG key is uploaded to the key server.&lt;/p&gt;

&lt;p&gt;I recommend triggering these actions manually for the first version you release just to see if everything is fine. Once&lt;br&gt;
the &lt;code&gt;Release&lt;/code&gt; action finishes with success, your library is considered as &lt;strong&gt;published to Maven Central&lt;/strong&gt;.&lt;br&gt;
Congratulations 🎉&lt;/p&gt;

&lt;p&gt;You can now set &lt;code&gt;closeRepository&lt;/code&gt; and &lt;code&gt;releaseRepository&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; in JReleaser configuration.&lt;/p&gt;
&lt;h2&gt;
  
  
  8. When is the library actually available to use?
&lt;/h2&gt;

&lt;p&gt;The library is not immediately available after it is released. Official documentation says that it may take up to 30&lt;br&gt;
minutes before the package is available, some folks claim that it can take few hours. In my case it took just 10&lt;br&gt;
minutes.&lt;/p&gt;

&lt;p&gt;Now your artifact can be referenced in &lt;code&gt;build.gradle.kts&lt;/code&gt; and Gradle will successfully download it. If you try to do it before it&lt;br&gt;
is available, Gradle will mark this library as unavailable and will not try to re-download it until the cache expires.&lt;br&gt;
Use &lt;code&gt;--refresh-dependencies&lt;/code&gt; flag to &lt;code&gt;.\gradlew&lt;/code&gt; command to force Gradle to check for updates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./gradlew build --refresh-dependencies
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't be fooled by the results in &lt;a href="https://search.maven.org/" rel="noopener noreferrer"&gt;search.maven.org&lt;/a&gt;&lt;br&gt;
or &lt;a href="https://mvnrepository.com/" rel="noopener noreferrer"&gt;mvnrepository.com&lt;/a&gt;. Here your artifact or even a new version of the artifact will appear&lt;br&gt;
after around 24 hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I hope this guide was useful, and it helped you to release a library to Maven Central.&lt;/p&gt;

&lt;p&gt;If you find anything unclear drop me a message on &lt;a href="https://twitter.com/tschuehly" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Most of this guide is based on &lt;a href="https://twitter.com/maciejwalkowiak" rel="noopener noreferrer"&gt;Maciej Walkowiak&lt;/a&gt; excellent guide so drop him a thanks!&lt;/p&gt;

&lt;p&gt;I would like to thank &lt;a href="https://twitter.com/aalmiray" rel="noopener noreferrer"&gt;Andres Almiray&lt;/a&gt; for creating JReleaser. This library significantly simplifes the whole process to the point that it's not terribly overcomplicated anymore.&lt;/p&gt;

</description>
      <category>gradle</category>
      <category>java</category>
      <category>kotlin</category>
      <category>springboot</category>
    </item>
    <item>
      <title>Toasts with Thymeleaf, HTMX and Spring Boot</title>
      <dc:creator>Thomas Schühly</dc:creator>
      <pubDate>Mon, 20 Feb 2023 08:36:57 +0000</pubDate>
      <link>https://dev.to/tschuehly/toasts-with-thymeleaf-htmx-and-spring-boot-32fl</link>
      <guid>https://dev.to/tschuehly/toasts-with-thymeleaf-htmx-and-spring-boot-32fl</guid>
      <description>&lt;p&gt;In this example, I will show you a way to return Toast Notifications from your server and render them interactively.&lt;/p&gt;

&lt;h2&gt;
  
  
  The HTML:
&lt;/h2&gt;

&lt;p&gt;We will start with an HTMX Element that creates a request to our spring server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="na"&gt;hx-target=&lt;/span&gt;&lt;span class="s"&gt;"#toast"&lt;/span&gt; &lt;span class="na"&gt;hx-put=&lt;/span&gt;&lt;span class="s"&gt;"/api/someRoute"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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&lt;br&gt;
footer. The footer is shown on every page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;footer&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"toast"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/footer&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But what if you don't want to target the toast element, and instead you want to target another element?&lt;br&gt;
I will show you later how you can change the HTMX target on the server.&lt;/p&gt;

&lt;p&gt;Next is our toast element, to make them generic we use parameterized Thymeleaf fragments.&lt;br&gt;
In this example I use the awesome &lt;a href="https://alpinejs.dev/" rel="noopener noreferrer"&gt;Alpine.js&lt;/a&gt; library, but this is also easily doable with&lt;br&gt;
normal javascript.&lt;/p&gt;

&lt;p&gt;We create a toast.html file in our root directory to use it later as a return view.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;th:fragment=&lt;/span&gt;&lt;span class="s"&gt;"info(message,duration)"&lt;/span&gt;
     &lt;span class="na"&gt;th:attr=&lt;/span&gt;&lt;span class="s"&gt;"x-init='setTimeout(() =&amp;gt; $el.style.display = \'none\',' + ${duration} + ')'"&lt;/span&gt;
     &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"fixed bottom-10 z-40 left-1/2 -translate-x-1/2"&lt;/span&gt;
     &lt;span class="na"&gt;x-data=&lt;/span&gt;&lt;span class="s"&gt;"{}"&lt;/span&gt;
     &lt;span class="na"&gt;x-ref=&lt;/span&gt;&lt;span class="s"&gt;"info"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"alert alert-info shadow-lg my-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;th:text=&lt;/span&gt;&lt;span class="s"&gt;"${message}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"$refs.info.style.display = 'none'"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/svg/x-circle.svg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To break it down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;th:fragment="info(message,duration)&lt;/code&gt; attribute declares that this is a parameterized Thymeleaf fragment we can later
use in our spring backend.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;th:attr="x-init='setTimeout(() =&amp;gt; $el.style.display = \'none\',' + ${duration} + ')'"&lt;/code&gt; creates a timeout function on
initialization that hides the toast after the duration we specified in the fragment parameter&lt;/li&gt;
&lt;li&gt;Next, we position the element with the tailwind classes &lt;code&gt;fixed bottom-10 z-40 left-1/2 -translate-x-1/2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;&amp;lt;span th:text="${message}"&amp;gt;&amp;lt;/span&amp;gt;&lt;/code&gt; element renders our text&lt;/li&gt;
&lt;li&gt;With the &lt;code&gt;&amp;lt;button @click="$refs.info.style.display = 'none'"&amp;gt;&lt;/code&gt; our user can click the toast away before it expires.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also duplicated this element and changed the info to error and changed the styling to return info or error toasts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The backend code:
&lt;/h2&gt;

&lt;p&gt;To specify our toast I used an enum class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Toast&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;INFO&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;ModelAndView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;durationInMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="nc"&gt;ModelAndView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"toast :: info(message = '${message}', duration = '$durationInMs')"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nc"&gt;ERROR&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;ModelAndView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;durationInMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="nc"&gt;ModelAndView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"toast :: error(message = '${message}', duration = '$durationInMs')"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;ModelAndView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;durationInMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;ModelAndView&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can return this enum in our Controller if we want to show either an info message or an error message.&lt;/p&gt;

&lt;p&gt;As you can see we return the parameterized fragment with a message we can extract from our business context and give our&lt;br&gt;
user valuable information.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Controller&lt;/span&gt;
&lt;span class="nc"&gt;ExampleController&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@RequestMapping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/someRoute"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;someRoute&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;ModelAndView&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// do Something&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ModelAndView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"An Error occurred: ${e.message}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Change the HTMX target on the server
&lt;/h2&gt;

&lt;p&gt;But what do you do when you want to swap the element itself and only show a toast if an error occurs?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;
&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;hx-put=&lt;/span&gt;&lt;span class="s"&gt;"/api/someRoute"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can change the behavior of HTMX easily with two Response Headers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RequestMapping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/someRoute"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;someRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;httpServletResponse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;HttpServletResponse&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;ModelAndView&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// do Something&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;httpServletResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"HX-Retarget"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"#errors"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;httpServletResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"HX-Reswap"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"innerHTML"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ModelAndView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"An Error occurred: ${e.message}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We just include the httpServletResponse: HttpServletResponse in the constructor of our function, and then&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With &lt;code&gt;HX-Retarget&lt;/code&gt; we can change the &lt;a href="https://htmx.org/attributes/hx-target/" rel="noopener noreferrer"&gt;hx-target&lt;/a&gt; property&lt;/li&gt;
&lt;li&gt;And with &lt;code&gt;HX-Reswap&lt;/code&gt; we can change the &lt;a href="https://htmx.org/attributes/hx-swap/" rel="noopener noreferrer"&gt;hx-swap&lt;/a&gt; property&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are many more HTMX &lt;a href="https://htmx.org/reference/#response_headers" rel="noopener noreferrer"&gt;Response Headers&lt;/a&gt; you can change to influence the behavior of your application.&lt;/p&gt;

&lt;p&gt;My Indiehacker product is built with htmx: &lt;br&gt;
&lt;a href="https://photoquest.wedding/" rel="noopener noreferrer"&gt;PhotoQuest&lt;/a&gt;&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>education</category>
      <category>learning</category>
    </item>
    <item>
      <title>Fixing the Spring Boot LiveReload Server with Gradle for Thymeleaf and TailwindCSS</title>
      <dc:creator>Thomas Schühly</dc:creator>
      <pubDate>Tue, 05 Jul 2022 12:25:28 +0000</pubDate>
      <link>https://dev.to/tschuehly/fixing-the-spring-boot-livereload-server-with-gradle-for-thymeleaf-and-tailwindcss-2jpf</link>
      <guid>https://dev.to/tschuehly/fixing-the-spring-boot-livereload-server-with-gradle-for-thymeleaf-and-tailwindcss-2jpf</guid>
      <description>&lt;h2&gt;
  
  
  LiveReloading with Thymeleaf
&lt;/h2&gt;

&lt;p&gt;I couldn't get Live Reload with Spring Boot and Thymeleaf to work. The problem seems to be so widespread that even &lt;a href="https://twitter.com/wimdeblauwe" rel="noopener noreferrer"&gt;@wimdeblauwe&lt;/a&gt; the author of Taming Thymeleaf uses gulp/npm to get Live Reloading working.&lt;/p&gt;

&lt;p&gt;I thought why do we need to use Javascript to get our Spring Boot Developer Experience up to par with the big Javascript Frameworks? We have a perfectly fine Build Tool with Gradle!&lt;/p&gt;

&lt;h3&gt;
  
  
  LiveReload.js
&lt;/h3&gt;

&lt;p&gt;First we need to include a script tag which points to the LiveReload Server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"http://localhost:35729/livereload.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Gradle Sync
&lt;/h3&gt;

&lt;p&gt;The plan is the same as with the npm scripts. Sync the templates into the build output so LiveReload triggers properly. This is done with the Gradle Sync Task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// build.gradle.kts&lt;/span&gt;
&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sync"&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
    &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"./src/main/resources/static"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s"&gt;"./src/main/resources/templates"&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;
    &lt;span class="nf"&gt;doLast&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;sync&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"./src/main/resources/static"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;into&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"build/resources/main/static"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nf"&gt;sync&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"./src/main/resources/templates"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;into&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"build/resources/main/templates"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now start a continuous build with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.\gradlew -t sync
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each time one of the inputs.files directory changes the sync task is rerun and LiveReload works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tailwind CSS
&lt;/h2&gt;

&lt;p&gt;To use &lt;a href="https://tailwindcss.com/" rel="noopener noreferrer"&gt;TailwindCSS&lt;/a&gt; we use the node-gradle plugin and create a npx task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// build.gradle.kts&lt;/span&gt;
&lt;span class="nf"&gt;plugins&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
    &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.github.node-gradle.node"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="s"&gt;"3.4.0"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;register&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gradle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;npm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NpxTask&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"tailwind"&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
    &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"-i"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"./src/main/resources/static/styles.css"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
        &lt;span class="s"&gt;"-o"&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s"&gt;"./src/main/resources/static/output.css"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"--watch"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We then need to create a package.json:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"devDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@tailwindcss/forms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^0.5.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3.1.4"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a tailwind.config.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** @type {import('tailwindcss').Config} */&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./src/**/*.{html,js}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a styles.css in resources/static/:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@tailwind&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@tailwind&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@tailwind&lt;/span&gt; &lt;span class="n"&gt;utilities&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and include output.css in our html:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/output.css"&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can start the tailwind npx task in a new terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.\gradlew tailwind
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it works:&lt;/p&gt;


  


&lt;p&gt;You can see an example at: &lt;a href="https://github.com/tschuehly/thymeleaf-livereload-gradle" rel="noopener noreferrer"&gt;github.com/tschuehly/thymeleaf-livereload-gradle&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tailwindcss</category>
      <category>gradle</category>
      <category>springboot</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Implementing Worker Pools in Kotlin to upload Images over WebDav to a Hetzner StorageBox</title>
      <dc:creator>Thomas Schühly</dc:creator>
      <pubDate>Wed, 22 Jun 2022 16:02:56 +0000</pubDate>
      <link>https://dev.to/tschuehly/implementing-worker-pools-in-kotlin-to-upload-images-over-webdav-to-a-hetzner-storagebox-2i64</link>
      <guid>https://dev.to/tschuehly/implementing-worker-pools-in-kotlin-to-upload-images-over-webdav-to-a-hetzner-storagebox-2i64</guid>
      <description>&lt;h1&gt;
  
  
  Worker pools
&lt;/h1&gt;

&lt;p&gt;Recently while doing a project with Go I came across Worker Pools on &lt;a href="https://gobyexample.com/worker-pools" rel="noopener noreferrer"&gt;GoByExample&lt;/a&gt; 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 webapp.&lt;/p&gt;

&lt;h1&gt;
  
  
  Kotlin
&lt;/h1&gt;

&lt;p&gt;Kotlin uses coroutines for concurrency which are fairly similiar to goroutines.&lt;/p&gt;

&lt;p&gt;Coroutines use structured concurrency to delimit the lifetime of each coroutine to a certain scope.&lt;/p&gt;

&lt;p&gt;To be able to create a workergroup we need to create a coroutinescope that is persistent over the livetime of our application. We achieve this behaviour with the SupervisorJob() context.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We then create a buffered channel as queue for our image data and the url where we want to upload it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;val channel = Channel&amp;lt;Pair&amp;lt;String, ByteArray&amp;gt;&amp;gt;(10000)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm using the Spring @PostConstruct annotation to create the worker group and listen to the channel for new data.&lt;br&gt;
Each time a item is in the queue we launch the upload function, if no item is in the queue the function is suspended.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@PostConstruct
    fun createWorkerGroup() {
        coroutineScope.launch {
            for (x in 1..5) {
                launch {
                    println("Create Worker $x")
                    while (true) {
                        uploadImage(channel.receive())
                    }
                }
            }
        }
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally we can send our data to our channel inside a runBlocking coroutine scope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;runBlocking {
  uploadService.channel.send(Pair(url, image.bytes))
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  WebDav
&lt;/h1&gt;

&lt;p&gt;In my webapp users upload images from their mobile phone to my webserver, afterwards I want to upload these pictures to a &lt;a href="https://www.hetzner.com/storage/storage-box" rel="noopener noreferrer"&gt;Hetzner Storage Box&lt;/a&gt; over webdav as a cheap alternative to an S3 object storage.&lt;/p&gt;

&lt;p&gt;I use the &lt;a href="https://github.com/lookfirst/sardine" rel="noopener noreferrer"&gt;sardine&lt;/a&gt; java webdav client library for its simplicity.&lt;/p&gt;

&lt;p&gt;The usage is very straightforward, you configure the client with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;val sardine = SardineFactory.begin("webDavUsername", "webDavPassword")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sardine.put("https://username.your-storagebox.de/foldername/imagename.jpg", ImageByteArray)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is all we need to have a highly parallel File Service&lt;/p&gt;

&lt;p&gt;You can view the Sourcecode at Github: &lt;a href="https://github.com/tschuehly/weddingGame/blob/master/src/main/kotlin/de/tschuehly/weddingGame/service/UploadService.kt#L40=" rel="noopener noreferrer"&gt;UploadService.kt&lt;/a&gt; &lt;a href="https://github.com/tschuehly/weddingGame/blob/512ace07fa1d0a57253b63bbb5cbf5ab3880e151/src/main/kotlin/de/tschuehly/weddingGame/service/ImageService.kt#L19" rel="noopener noreferrer"&gt;ImageService.kt&lt;/a&gt;&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>springboot</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Creating a GO GUI with Alpine.js and Webview</title>
      <dc:creator>Thomas Schühly</dc:creator>
      <pubDate>Mon, 14 Mar 2022 16:55:59 +0000</pubDate>
      <link>https://dev.to/tschuehly/creating-a-go-gui-with-alpinejs-and-webview-3290</link>
      <guid>https://dev.to/tschuehly/creating-a-go-gui-with-alpinejs-and-webview-3290</guid>
      <description>&lt;p&gt;There are a lot of options for building a &lt;a href="https://golangr.com/gui/" rel="noopener noreferrer"&gt;GUI&lt;/a&gt; for Go applications. &lt;br&gt;
Coming from the web development world building the frontend with HTML seems as a no-brainer. &lt;/p&gt;
&lt;h2&gt;
  
  
  Webview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/webview/webview" rel="noopener noreferrer"&gt;Webview&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;To start using webview you need to install webview:&lt;br&gt;
&lt;code&gt;go get github.com/webview/webview&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;On windows you need to have these two &lt;a href="https://github.com/webview/webview/tree/master/dll/x64" rel="noopener noreferrer"&gt;dlls&lt;/a&gt; in the project root folder.&lt;/p&gt;

&lt;p&gt;It supports two-way JavaScript bindings (to call JavaScript from C/C++/Go and to call C/C++/Go from JavaScript).&lt;br&gt;
But writing pure javascript code for the interactivity (imo) is awful. &lt;/p&gt;
&lt;h2&gt;
  
  
  Alpine.js to the rescue
&lt;/h2&gt;

&lt;p&gt;"&lt;a href="https://alpinejs.dev/" rel="noopener noreferrer"&gt;Alpine.js&lt;/a&gt; is a rugged, minimal tool for composing behavior directly in your markup." It fits perfectly for our usecase.&lt;/p&gt;

&lt;p&gt;You can load alpine inline or from a file. The newest version is available at &lt;a href="//unpkg.com/alpinejs"&gt;unpkg.com/alpinejs&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;loadAlpine&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"paste alpine.js source here"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First you must initialize webview.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;webView&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;webview&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;webview&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HintNone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loadAlpine&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To execute go code with alpine we need to call webView.bind("functionName").&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"extractSubDirectories"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sourceFolder&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;folderUrls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;extractSubDirectories&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sourceFolder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tmpl&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Must&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"html"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="c"&gt;// language=GoTemplate&lt;/span&gt;
    &lt;span class="s"&gt;`&amp;lt;div&amp;gt;
      {{range $vendor, $folderDetailsArray := .}}
        &amp;lt;div&amp;gt;
          &amp;lt;h3&amp;gt;Vendor: {{$vendor}}&amp;lt;/h2&amp;gt;
        {{range $folderDetails := $folderDetailsArray}}
          &amp;lt;ul&amp;gt;
            &amp;lt;li&amp;gt;{{ .Path }} filecount:: {{ .FileCount }}&amp;lt;/li&amp;gt;
          &amp;lt;/ul&amp;gt;
        {{end}}
        &amp;lt;/div&amp;gt;
      {{end}}
     &amp;lt;/div&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Buffer&lt;/span&gt;
  &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tmpl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;folderUrls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WritePrint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ERROR: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To create your first page you call webView.Navigate() and supply it with your HTML. Then call webView.Run()&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;`data:text/html`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;`&amp;lt;!doctype html&amp;gt;
&amp;lt;html lang="de" x-data="{ pathInput: '', table : ''}"&amp;gt;
    &amp;lt;body style="padding: 2rem"&amp;gt;
        &amp;lt;h1&amp;gt;JPEG Sorter&amp;lt;/h1&amp;gt;
        &amp;lt;p&amp;gt;Input the folder where the images are stored&amp;lt;/p&amp;gt;
        &amp;lt;input type="text" x-model="pathInput"/&amp;gt;

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

        &amp;lt;div x-html=table&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Alpine.js
&lt;/h2&gt;

&lt;p&gt;As you can see there are quite a lot of non standard html attributes, this is the magic of alpine.js.&lt;br&gt;
You can create local alpine data variables in the scope of the element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"de"&lt;/span&gt; &lt;span class="na"&gt;x-data=&lt;/span&gt;&lt;span class="s"&gt;"{ pathInput: '', table : ''}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can bind input data to the local variables with x-model&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;x-model=&lt;/span&gt;&lt;span class="s"&gt;"pathInput"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the coolest part comes now. With an &lt;a class="mentioned-user" href="https://dev.to/click"&gt;@click&lt;/a&gt; alpine attribute we can call our go functions from the html. The extractSubDirectories() function we binded earlier in this example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"table = await extractSubDirectories(pathInput);"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  analyse folder
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With x-html we can bind the returned html from the go function into our gui.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are the basic steps to get webview and alpine.js working with Go.&lt;/p&gt;

&lt;h2&gt;
  
  
  GUI Example
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8w7azcxrwvfgucs3fdwi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8w7azcxrwvfgucs3fdwi.png" alt="GUI Example" width="633" height="561"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can look at my recent freelance project for a complete example on &lt;a href="https://github.com/tschuehly/pc3000-imagesorter/blob/master/webview.go" rel="noopener noreferrer"&gt;Github&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>go</category>
      <category>webview</category>
      <category>javascript</category>
      <category>programming</category>
    </item>
    <item>
      <title>JWT authentication for Spring Boot simplified using GoTrue and Supabase</title>
      <dc:creator>Thomas Schühly</dc:creator>
      <pubDate>Tue, 01 Mar 2022 22:23:06 +0000</pubDate>
      <link>https://dev.to/tschuehly/simpler-jwt-authentication-for-spring-boot-using-gotrue-and-supabase-1goo</link>
      <guid>https://dev.to/tschuehly/simpler-jwt-authentication-for-spring-boot-using-gotrue-and-supabase-1goo</guid>
      <description>&lt;p&gt;In a quest to have a simpler JWT Authentication flow and not have to deal with security related userdata in my backend I discovered Supabase Auth which is an implementation of Netlify GoTrue.&lt;/p&gt;

&lt;p&gt;For Kotlin there is the awesome supabase &lt;a href="https://github.com/supabase-community/gotrue-kt" rel="noopener noreferrer"&gt;gotrue-kt&lt;/a&gt; library.&lt;/p&gt;

&lt;p&gt;In your User Registration and Login Services you need to create a GoTrueClient&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;val goTrueClient = GoTrueClient.defaultGoTrueClient(
    url = "&amp;lt;base-url&amp;gt;",
    headers = mapOf("Authorization" to "foo", "apiKey" to "bar")
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are using supabase, the base URL will be:&lt;br&gt;
&lt;code&gt;https://&amp;lt;your-project-id&amp;gt;.supabase.co/auth/v1&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Then in your signup method you can just call signUpWithEmail().&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;authDTO&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;goTrueClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signUpWithEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;websiteUserRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WebsiteUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authDTO&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the default client this returns a GoTrueUserResponse which most importantly contains a id which you then can persist in a WebsiteUser Authentication Pojo which holds information related to the user&lt;/p&gt;

&lt;p&gt;With the goTrue Kotlin Library you can also specify a custom return type for example if you turned email confirmation off. &lt;/p&gt;

&lt;p&gt;We define our DTO:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;AuthDTO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;tokenType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;refreshToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;

&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and then create a Client where we pass this dto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;return GoTrueClient.customApacheJacksonGoTrueClient&amp;lt;AuthDTO, GoTrueTokenResponse&amp;gt;(url,headers)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In our Login Method we call signInWithEmail and then return the JWT from the GoTrue Response as Cookie&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
    }
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But we need to actually verify that the JWT is correct when a User request a page and that the user has the required access rights.&lt;/p&gt;

&lt;p&gt;We do this in a JWT Filter that overrides the doFilterInternal method from the OncePerRequestFilter().&lt;/p&gt;

&lt;p&gt;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.&lt;br&gt;
We then set the SecurityContext with the retrieved WebsiteUser&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@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 -&amp;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)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At last we add this filter in our WebSecurityConfiguration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@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)
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>kotlin</category>
      <category>webdev</category>
      <category>springboot</category>
      <category>java</category>
    </item>
    <item>
      <title>Spring Content - The better way to save and serve files and images with Spring Boot</title>
      <dc:creator>Thomas Schühly</dc:creator>
      <pubDate>Thu, 18 Nov 2021 08:01:35 +0000</pubDate>
      <link>https://dev.to/tschuehly/spring-content-the-better-way-to-save-and-serve-files-and-images-with-spring-boot-1lj3</link>
      <guid>https://dev.to/tschuehly/spring-content-the-better-way-to-save-and-serve-files-and-images-with-spring-boot-1lj3</guid>
      <description>&lt;h2&gt;
  
  
  Table Of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Problems with saving images directly&lt;/li&gt;
&lt;li&gt;How to do it using Spring Content&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Save images directly into the DB &lt;a&gt;&lt;/a&gt;
&lt;/h1&gt;

&lt;p&gt;Recently I added a feature to my web app where my customer could upload images and display them inside the app.&lt;/p&gt;

&lt;p&gt;At first I stored them directly into the db as ByteArray inside an JPA Entity with a &lt;a class="mentioned-user" href="https://dev.to/lob"&gt;@lob&lt;/a&gt; annotation&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Entity&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Picture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt;
    &lt;span class="nd"&gt;@Lob&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ByteArray&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then returned them as Byte64 encoded string inside my JSON API Response.&lt;/p&gt;

&lt;h3&gt;
  
  
  Issues
&lt;/h3&gt;

&lt;p&gt;It was working fine in my local dev enviroment, but as soon as I pushed it to prod I encountered an error where my whole json structure needed for my frontend wasn't displayed properly. &lt;br&gt;
The Byte64 encoded image string just stopped in the middle.&lt;/p&gt;

&lt;p&gt;I cloned the prod db locally into a postgres container and then the issue occured also locally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;HHH000100&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Fail-safe&lt;/span&gt; &lt;span class="nf"&gt;cleanup&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;collections&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hibernate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;internal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CollectionLoadContext&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="mf"&gt;2555100f&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;HikariProxyResultSet&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="mi"&gt;1189576917&lt;/span&gt; &lt;span class="n"&gt;wrapping&lt;/span&gt; &lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jdbc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PgResultSet&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="mi"&gt;545635&lt;/span&gt;&lt;span class="n"&gt;a4&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After some 2am debugging I got a JacksonMapping exception and found the error on &lt;a href="https://stackoverflow.com/questions/54013834/could-not-write-json-unable-to-access-lob-stream" rel="noopener noreferrer"&gt;stackoverflow&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I thought the solution was too complex and looked for a simpler way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Learnings
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Lessons: Always test with an environment as similiar as production. I can recommend &lt;a href="https://www.testcontainers.org/" rel="noopener noreferrer"&gt;testcontainers&lt;/a&gt; for easier integration tests.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Lesson: Use a database versioning tool such as liquibase instead of spring.jpa.hibernate.ddl-auto = update. &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With h2 the &lt;a class="mentioned-user" href="https://dev.to/lob"&gt;@lob&lt;/a&gt; was correctly saved as bytearray. With postgres the type was assumed to be oid which caused a issue mapping between &lt;a class="mentioned-user" href="https://dev.to/lob"&gt;@lob&lt;/a&gt; and the bytea column type. &lt;/p&gt;

&lt;p&gt;This was fixed with the hibernate Annotation &lt;/p&gt;

&lt;p&gt;&lt;code&gt;@Type(type = "org.hibernate.type.MaterializedBlobType")&lt;/code&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Lesson: Dont serve images directly as Base64 encoded string. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instead use a seperate controller and serve the imagedata raw. &lt;/p&gt;

&lt;p&gt;Serialize the entity which stores the data and the metadata.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator::class, property = "id")
@JsonIdentityReference(alwaysAsId=true)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This avoids huge json responses and slowdowns to your website.&lt;/p&gt;

&lt;h1&gt;
  
  
  Spring Content to the rescue &lt;a&gt;&lt;/a&gt;
&lt;/h1&gt;

&lt;p&gt;Then I discovered &lt;a href="https://github.com/paulcwarren/spring-content" rel="noopener noreferrer"&gt;spring-content&lt;/a&gt; which looked like a better alternative than saving them directly to a db, as I could switch between db, filesystem, AWS S3 and MongoDB GridFs by just switching the dependency.&lt;/p&gt;

&lt;p&gt;To use Spring Content you just need to add the depencency in your build.gradle.kts&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.github.paulcwarren:content-jpa-spring-boot-starter:1.2.5"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the @ContentId annotation to your file entity. In my case i replaced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;    &lt;span class="nd"&gt;@JsonIgnore&lt;/span&gt;
    &lt;span class="nd"&gt;@Type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"org.hibernate.type.MaterializedBlobType"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ByteArray&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@ContentId&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;imageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a ContentStore&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;interface PictureContentStore : ContentStore&amp;lt;Picture, String&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your method where you save the Files you need to use the contentstore.setContent() method. It is important to first save the entity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;val picture = pictureRepository.save(Picture(pictureFile.originalFilename, pictureFile.contentType,null))
pictureContentStore.setContent(picture, pictureFile.inputStream)
pictureRepository.save(picture)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the handling of pictures to your file entity&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RestController&lt;/span&gt;
&lt;span class="nd"&gt;@RequestMapping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api/picture"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PictureController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;pictureService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PictureService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;pictureContentStore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PictureContentStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/{id}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nd"&gt;@PathVariable&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;InputStreamResource&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;pictureService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;pic&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pictureContentStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;inputStreamResource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;InputStreamResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MediaType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseMediaType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputStreamResource&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nc"&gt;NoSuchElementException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"There is no Image with the corresponding id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and you are done. &lt;/p&gt;

&lt;p&gt;You can look up my source code on &lt;a href="https://github.com/tschuehly/DataRecovery" rel="noopener noreferrer"&gt;Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is my first project as a freelancer. Any suggestions or feedback is very welcome&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>java</category>
      <category>jpa</category>
      <category>springboot</category>
    </item>
    <item>
      <title>Deploying docker-compose the easy way, without registry or scp</title>
      <dc:creator>Thomas Schühly</dc:creator>
      <pubDate>Sun, 28 Mar 2021 14:29:44 +0000</pubDate>
      <link>https://dev.to/tschuehly/deploying-docker-compose-the-easy-way-without-registry-or-scp-2fn</link>
      <guid>https://dev.to/tschuehly/deploying-docker-compose-the-easy-way-without-registry-or-scp-2fn</guid>
      <description>&lt;p&gt;Ever wondered how you can deploy your locally running docker-compose project to a remote server?&lt;br&gt;
There are several options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pushing the images to a container registry like dockerhub, github container registry and pulling them on your server&lt;/li&gt;
&lt;li&gt;Saving the images to a .tar archive and copying it over to your server and loading them there &lt;a href="https://medium.com/@sanketmeghani/docker-transferring-docker-images-without-registry-2ed50726495f" rel="noopener noreferrer"&gt;explained here&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is a easier way using &lt;a href="https://www.docker.com/blog/how-to-deploy-on-remote-docker-hosts-with-docker-compose/" rel="noopener noreferrer"&gt;docker remote host&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;The only thing you have to make sure is to install docker, docker-compose onto your server and have a valid ssh key&lt;/p&gt;

&lt;h3&gt;
  
  
  docker-compose.yml
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version: '3.9'
services:
  backend:
    build: spring-backend
    container_name: spring-backend
    image: spring-backend:0.0.1
    expose:
      - "8088"
  frontend:
    build: angular-frontend
    image: angular-frontend:0.0.1
    container_name: angular-frontend
    ports:
      - 80:80
    depends_on:
      - backend
    command: [nginx-debug, '-g', 'daemon off;']

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  deploy.sh
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash

docker-compose build

for img in $(docker-compose config | awk '{if ($1 == "image:") print $2;}'); do
  images="$images $img"
done

echo $images


docker image save $images | docker -H "ssh://user@serverIp" image load
docker-compose -H "ssh://user@serverIp" up --force-recreate -d
docker-compose -H "ssh://user@serverIp" logs -f
read -p "Press any key to continue... " -n1 -s

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a simple deploy.sh you can build your current state, load them onto your server, run them and attach to the output.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
