Let's explore Ktor's built-in way of generating HTML directly in code: the Kotlin HTML DSL.
Getting started
The HTML DSL is in fact a special language that makes it easy to specify HTML directly in your application code without using external templating tools.
To make HTML DSL available in your project you need to include the following dependency:
implementation("io.ktor:ktor-server-html-builder:$ktor_version")implementation "io.ktor:ktor-server-html-builder:$ktor_version"Then import the necessary packages in your code:
// ...
import io.ktor.server.html.*
import kotlinx.html.*
fun Application.module() {
routing {
get("/html") {
}
}
}Great! Now we can start working with the HTML DSL.
Send HTML in response
We can use the call.respondHtml function to generate and return HTML to the user. This function takes a template of HTML code written in the HTML DSL as input. With this language, we can specify any nesting of tags and substitute values in the template using the + symbol. The easiest way to understand this is to see a concrete example:
call.respondHtml(HttpStatusCode.OK) {
head {
title {
+"html page title"
}
}
body {
h1 {
+"Hello, user!"
}
}
}This piece of code returns HTML of the following form:
<html>
<head>
<title>html page title</title>
</head>
<body>
<h1>Hello, user!</h1>
</body>
</html>In the browser:
The HTML DSL allows substituting not only specific string values into a template but also values from variables:
val text = "Hello, user!"
call.respondHtml(HttpStatusCode.OK) {
body {
+text
}
}Output:
<html>
<body>Hello, user!</body>
</html>Embedding a loop
The next important thing you need to know is how to embed a loop in the template. You rarely know exactly how many tags you will have to display. Imagine you have a site where users can leave comments. You don't know in advance how many comments you have to display.
Let's say you have a list containing the current comments you want to output. To do that, you simply wrap the repeating part of the pattern in a for loop:
val comments = listOf("Hello, everyone!", "Great site", "Nice :)")
call.respondHtml(HttpStatusCode.OK) {
body {
ul {
for (comment in comments) {
li {
+comment
}
}
}
}
}Output:
<html>
<body>
<ul>
<li>Hello, everyone!</li>
<li>Great site</li>
<li>Nice :)</li>
</ul>
</body>
</html>
HTML DSL Templates
As you can see, Ktor provides a great opportunity to construct HTML directly in routing. This can be convenient when you need to output a small HTML page quickly, but in large projects, this approach can seriously reduce code readability. Therefore, when working with large HTML pages, we recommend putting them in a separate file. The HTML DSL has its own templating engine for this purpose.
To create a template, we need to define a class that inherits from Template<HTML> and override its HTML.apply() method, in which we put our HTML code:
class MyTemplate : Template<HTML> {
override fun HTML.apply() {
body {
h1 {
+"Hello, user!"
}
}
}
}Next, we can use this template in routing using the call.respondHtmlTemplate function.
get("/html") {
call.respondHtmlTemplate(MyTemplate()) {}
}Now, when we access /html we get the following output:
<html>
<body>
<h1>Hello, user!</h1>
</body>
</html>We got the same result as if we had defined HTML directly in routing as shown in the previous section.
You may have wondered why you need the trailing lambda after calling call.respondHtmlTemplate. This is because templates usually have placeholders for dynamic content which we specify in the lambda argument. If the template is purely static (like the one above), the lambda remains empty.
In the next section, we'll look at how to make our templates dynamic using placeholders.
Using Placeholders in Templates
In the previous example, our template simply displayed the static message "Hello, user!". Usually, templates need to display dynamic data which can be achieved using placeholders. You can think of the template as having slots that can be filled.
To use them, we declare a Placeholder property in our template class. Then inside the apply method, we use insert() to define where that placeholder should appear in the HTML structure.
class MyTemplate() : Template<HTML> {
// Define the placeholder (the slot)
val name = Placeholder<FlowContent>()
override fun HTML.apply() {
body {
h1 {
+"Hello "
// Insert the placeholder
insert(name)
}
}
}
}Now that the template has a placeholder, we must fill it when we use the template in our route. We do this inside the trailing lambda of respondHtmlTemplate:
call.respondHtmlTemplate(MyTemplate()) {
// Fill the placeholder
name {
+"world!"
}
}Result:
Try to pass in a different name and you'll see how the welcome message changes.
Nested Templates
Placeholders aren't limited to simple text or tags; the can also hold entire other templates. This allows you to create nested layouts (like putting a specific page content inside a main site layout).
We call these Template Placeholders. They function similarly to standard placeholders, but they are designed to hold objects that inherit from Template<FlowContent>.
First, let's define a small child template:
class ChildTemplate() : Template<FlowContent> {
override fun FlowContent.apply() {
hr { }
div {
+"Child template"
}
hr { }
}
}Now, let's modify our main MyTemplate to accept this child template. We use TemplatePlaceholder to define the slot, and insert to place it:
class MyTemplate() : Template<HTML> {
val childTemplate = TemplatePlaceholder<ChildTemplate>()
override fun HTML.apply() {
body {
h1 {
+"Hello "
insert(ChildTemplate(), childTemplate)
}
}
}
}Notice that insert here takes two arguments: an instance of the template and the placeholder variable.
Now, in routing, we can fill this template placeholder.
call.respondHtmlTemplate(MyTemplate()) {
childTemplate {
// We can access placeholder properties of ChildTemplate here if it had any
}
}Let's open the browser and see our new result:
<html>
<body>
<h1>Hello
<hr>
<div>Child template</div>
<hr>
</h1>
</body>
</html>Ktor inserted the child template where we defined the insert point.
For more information about the HTML DSL you can refer to the official Ktor documentation.
Conclusion
In this topic, we introduced the HTML DSL.
We learned how to construct HTML directly in routing, and how to use loops to display lists.
We also learned how to create reusable templates using placeholders, allowing us to define slots in our HTML and fill them dynamically with data or even other templates.
Don't worry if you don't remember something from this topic. You can always go back to the theory. Now let's put what we've learned into practice.