A small Kotlin/Ktor sample that renders HTML on the server and uses htmx for component-style page updates without a separate JavaScript application framework.
This repository is best treated as a compact example or starting point, not as a production-ready web application template.
- Server-rendered HTML with Ktor and
kotlinx.html - Showcase content that explains the Ktor route and htmx fragment-swap model
- htmx-powered navigation for dynamic component swaps
- Active navigation state for direct loads, htmx swaps, and browser history
- Direct component URLs that still render the full application shell on refresh
- Server-owned incident search, filtering, sorting, and pagination
- Refreshable incident detail routes with focused htmx swaps
- Focused operational activity polling through a server-rendered fragment
- Tailwind utility classes compiled into a static CSS asset
- Netty-based Ktor server
- Sitemap and robots.txt endpoints for the configured public URL
- Dockerfile-based container packaging for deployment demos
- Route and htmx contract tests with Ktor
testApplication - JaCoCo coverage reporting and verification
- Kotlin
2.3.21 - Ktor
3.5.0 - Gradle wrapper
9.5.1 - Logback
1.5.33 - JaCoCo
0.8.14 - htmx
2.0.10 - Tailwind CSS
4.3.0 - Tailwind CLI
4.3.0
- JDK 17 or 21
- Node.js and npm for rebuilding Tailwind CSS
- No separate Gradle installation is required; use the checked-in Gradle wrapper.
- Docker is optional and only required for container build checks.
The project was last verified locally with OpenJDK 21.0.10.
git clone https://github.com/rf43/htmx-ktor.git
cd htmx-ktor
npm ci
npm run build:css
./gradlew runOpen http://localhost:8080.
Development mode is enabled by default through gradle.properties.
To run without Ktor development mode:
./gradlew run -Pdevelopment=falseRun tests:
./gradlew testThe test suite verifies the root application shell, static asset routing, htmx navigation attributes, component route registration, server-owned incident queries, focused activity polling, and route-specific showcase content.
Rebuild the compiled Tailwind CSS asset:
npm run build:cssGradle packages the current src/main/resources/static/app.css; it does not regenerate Tailwind CSS. Run npm run build:css after changing Tailwind classes or src/main/resources/styles/tailwind.css.
Watch Tailwind source inputs during local UI work:
npm run watch:cssBuild the project:
./gradlew buildGitHub Actions runs ./gradlew test followed by ./gradlew build for pull
requests and pushes to main, using Temurin JDK 21. The build includes the
Gradle check lifecycle and JaCoCo coverage verification.
Run ./gradlew build before opening a pull request to reproduce the full
project check locally.
Build the container image:
docker build -t htmx-ktor .The Docker build regenerates app.css before packaging the Ktor distribution.
The Docker build runs Kotlin compilation in-process to avoid short-lived Kotlin daemon files during hosted image builds.
Run the container locally:
docker run --rm -p 8080:8080 htmx-ktorThe server reads PORT from the environment and defaults to 8080, which keeps local runs simple while allowing managed platforms to provide the runtime port.
The application supports these environment variables:
PORT- HTTP port used by the Netty server. Defaults to8080.PUBLIC_SITE_URL- absolute public base URL used by/sitemap.xmland/robots.txt. Defaults tohttps://example.com/.
Set PUBLIC_SITE_URL in the deployment environment so generated search endpoints point at the deployed site. Forks should set this to their own public URL rather than changing source code.
Generate the JaCoCo report:
./gradlew jacocoTestReportVerify coverage:
./gradlew jacocoTestCoverageVerificationThe HTML coverage report is written to build/reports/jacoco/test/html/index.html.
htmx-ktor/
|-- .github/workflows/
| `-- ci.yml
|-- src/
| |-- main/
| | |-- kotlin/io/ivycreek/
| | | |-- about/
| | | |-- incidents/
| | | |-- contact/
| | | |-- content/
| | | |-- dashboard/
| | | |-- navbar/
| | | |-- plugins/
| | | |-- projects/
| | | |-- team/
| | | `-- Application.kt
| | `-- resources/
| | |-- logback.xml
| | |-- styles/
| | `-- static/
| `-- test/kotlin/io/ivycreek/
|-- build.gradle.kts
|-- Dockerfile
|-- package.json
|-- package-lock.json
|-- .dockerignore
|-- gradle.properties
|-- settings.gradle.kts
`-- gradle/wrapper/
Each page package generally has two files:
Feature.ktrenders the HTML fragment.FeatureRouter.ktregisters the/components/...route.
The root page and static resource routing live in src/main/kotlin/io/ivycreek/plugins/Routing.kt.
Component routes return fragments for htmx requests and the full application shell for normal browser requests, so pushed URLs remain refreshable and bookmarkable.
The incident workspace uses ordinary GET query parameters for search, filters, sorting, and paging. htmx submits the same URLs and swaps only the queue or detail region. A compact operational activity snapshot polls its own Ktor fragment while keeping the initial server-rendered content useful when JavaScript is unavailable.
Direct incident URLs normalize filter and page context so the selected incident remains visible in the queue after a refresh.
- Tailwind is built with the CLI from
src/main/resources/styles/tailwind.cssintosrc/main/resources/static/app.css, which Ktor serves at/app.css. - The project currently loads htmx from a pinned 2.x CDN URL with subresource integrity.
This project is licensed under the MIT License. See LICENSE for details.