The term "static web" refers to a style of web development that might seem out of place in today's modern scene of Javascript frameworks. Not only are social media (Facebook, Twitter, etc) and email (GMail, Yahoo, etc) applications delivered as Javascript-based "Single Page Applications", but also document-based websites, such as news websites and blogs. However, it is this proliferation of javascript-based web applications that inspires a reaction to get back to some of the ideals of the web of yesteryear. In this article, we will review the benefits of the static web and look at how Kotlin can bring us some of the benefits of dynamic web programming to the static web world.
Documents, not Apps
The Web was designed to provide documents. HTML is a markup language based on XML which was designed to be the standard format for documents on the web. The addition of Javascript (JS) and AJAX to the standard set of web technologies allowed these documents to be dynamic. Javascript enables programming logic to run within the browser on a web page. AJAX allows JS to make requests to servers to send or receive additional information, within a single web page. This opened up a doorway to deliver cross-platform applications with rich user interfaces. The JS ecosystem developed around this idea of "Web Applications" in order to make it easier to develop these webapps and bring along the perks of a true development environment. Pretty soon, this JS ecosystem became so ubiquitous, that nearly all websites on the internet are web applications.
But this has had some downsides. The amount of data transferred when loading a website has exploded. Sites are built on JS libraries which are built on other libraries which are built on others... Many memes have been written about the `node_modules` directory. All of those files are compiled into the application that is delivered as part of a page load. On top of this, sites have stuffed a myriad of trackers and advertisements into their sites in order to monetize you via surveillance capitalism. In addition to the privacy issues with this, it also makes these sites near unusable on slow connections such as mobile connections or connections in rural areas or developing countries. Dynamic webapps also degrade the idea of the Semantic Web, a concept critical to accessibility on the web, especially for people who rely on screen readers.
Some sites should be webapps, such as eCommerce sites, or web interfaces to applications (think gmail, etc). But most websites are just serving up static content, such as news websites and blogs.
Sites which are delivered as static web pages can enjoy the following benefits:
Speed
Because no javascript needs to be executed, and static content can be cached, loading times can be incredibly fast, even in bandwidth-restricted environments. These sites will also use far less memory in the browser.
Simplicity
Because there is no dynamic code, there are many fewer points of failure. The code is a lot easier to check for errors and maintain.
Portabilty
Any webhost can host a static site. Some sites, such as Neocities (a play on the old Geocities) are specifically designed to do this very cheaply with generous free tiers.
Accessibility
Semantically written static sites work on screen readers out of the box, and can be printed very nicely.
There are a few disadvantages to static sites:
Limited features
Hand-coding HTML and CSS is tedious enough, but hand-coding the XML for an RSS feed would be out of the question.
Code Maintenance
Hand-coded HTML will require a lot of copy and pasting and code duplication
Static site generators such as Jekyll and Hugo help solve these two disadvantages, and are great for non-technical folks. However, they come with their own restrictions. Using Kotlin DSL's, we can get the same benefits as a static site generator, without the restrictions.
I decided to build myself a personal website in order to avoid places like Medium, Twitter, or various slideshow services. In this example, instead of using a template engine such as Jekyll or Hugo, I will be using a "Domain-Specific Language", or DSL. A DSL is a simplified programming language designed for a specific use case, and typically have a declarative style. This is where Kotlin comes in.
Kotlin's functional programming capabilities allows it to be used to create DSLs. The Kotlin team has created HTML and CSS DSLs already. Instead of building a DSL from scratch, I decided to extend these DSLs. I will walk you thought how I did this. Along the way, we will see how DSLs are built, using the HTML DSL as a model example, then how to write our own DSLs by extending the HTML DSL.
First, let's briefly review some Kotlin features used in DSLs.
Lambda as final parameter
In Kotlin, if the last parameter of a function is a lambda, you may "lift" the parameter out of the enclosing parentheses. So, if your function signature looks like:
fun f(lambda: (Int) -> String)
Then, instead of invoking it like this:
val s = f(lambda = {
it.toString()
})
you can invoke it like this:
val s = f {
it.toString()
}
Lambda with receiver
A lambda parameter with receiver looks like this:
fun stringDsl(receiver: StringBuilder.() -> Unit) : String {
StringBuilder().apply(receiver).build()
}
What this means is that the lambda passed in as the parameter will run as if it were the body of an apply() call on an object of the type specified. this allows you to turn a Java-style fluent builder into a Kotlin-style DSL. You can think of it as the extension function version of a lambda.
fun builder() : String {
return StringBuilder().append("content").build()
}
becomes
fun dsl() : String = stringDsl {
append("content")
}
@DslMarker
DSL markers help in DSLs when you are nesting multiple receiver calls. In our example, this would apply when we are within the receiver for an HTML body element, and we don't want to see any methods from the enclosing html element. It keeps in scope only the methods on the innermost receiver, and removes any methods on higher up receivers. They can still be accessed through an explicit call such as this@HTML.meta.
DslMarkers are declared like this:
@DslMarker
annotation class HtmlTagMarker()
The above example is declared within the kotlinx HTML DSL, which we will be extending, so we can re-use it. But if you are creating your own DSL from scratch, you will want to declare your own.
HTML DSL
The Jetbrains team as created a DSL for HTML which serves as a prime example of how to write DSLs in KotlinIt is available on GitHub. Here is an example of using the HTML DSL:
FileWriter("path/to/file").appendHTML().html {
head {
}
body {
h1 { +"Header" }
p { +"paragraph text" }
}
}
Code reuse
One of the first benefits of using a programming language over raw HTML we want to leverage is code reuse. I want every page on my site to contain a navigation element. In order to avoid repeating myself and risk the navigation sections of each page becoming different from each other, I extract the navigation bar of my site to a function. I use an extension function to add a method that I can use within any BODY receiver block
fun BODY.sideNavBar(block: UL.() -> Unit) = div {
classes += "navBar"
style = css {
verticalAlign = VerticalAlign.top
}
ul {
style = css {
listStyleType = ListStyleType.none
color = Color("#9999EE")
}
block(this)
}
}
Using this function, I can build my navigation like this:
fun BODY.sideNav() {
sideNavBar {
li { a(href = "index.html") { +"Home" } }
li { a(href = "travel.html") { +"Travel Guide" } }
}
}
And use it in each page on my site:
fun mainPage() : HTML.() -> Unit = {
head {
}
body {
sideNav()
}
}
Next, instead of strict code reuse, I want to be able to easily replicate a pattern of usage of the DSL. For this example, I will create a DSL for building a rolodex page. I want to be able to display contact information on a page, organized alphabetically. To do this, I will extend the DSL itself in order to eliminate the boilerplate and repetition, but still expose the flexibility needed to do all the things I am looking for. DSLs are usually backed by objects, so to extend the DSL, we need to define a backing object for the thing our extension will describe.
@HtmlTagMarker
class ROLODEX() {
var contactList: List<CONTACT> = listOf()
fun contact(contact: CONTACT.() -> Unit) {
contactList += CONTACT().apply(contact)
}
operator fun invoke(body: BODY) {
// TODO
}
}
@HtmlTagMarker
class CONTACT() {
var picture: String = ""
var name: String = ""
var email: String = ""
var phone: String = ""
operator fun invoke(div: DIV) {
// TODO
}
}
The class contains properties to hold information we will need when rendering this element. It also defines methods for modifying this information. These methods will be the ones available within the receiver block when using the DSL.
Next, the invoke method will define how this element will be rendered. My implementation looks like this:
@HtmlTagMarker
class ROLODEX() {
var contactList: List<CONTACT> = listOf()
fun contact(contact: CONTACT.() -> Unit) {
contactList += CONTACT().apply(contact)
}
operator fun invoke(body: BODY) {
body.apply {
h1 { +"Rolodex" }
div {
this@ROLODEX.contactList
.groupBy { it.name.split(" ").last()0 }
.toList()
.sortedBy { it.first }
.forEach {
h2 {
style = css {
display = Display.block
borderBottom(width = 1.px, style = BorderStyle.solid, color = Color.black)
}
+it.first.toString()
}
it.second.forEach {
it(this)
}
}
}
}
}
}
@HtmlTagMarker
class CONTACT() {
var picture: String = ""
var name: String = ""
var email: String = ""
var phone: String = ""
operator fun invoke(div: DIV) {
div.apply {
div {
div {
style = css {
height = 3.em
display = Display.inlineBlock
}
val imageBase64 = resourceAsBase64(this@CONTACT.picture)
img(src = "data:image/png;base64, $imageBase64") {
style = css {
height = 3.em
maxHeight = 3.em
}
}
}
div {
style = css {
display = Display.inlineBlock
}
span {
style = css {
display = Display.block
fontWeight = FontWeight.bold
fontSize = 1.17.em
}
+this@CONTACT.name
}
field("Email") { a(href = "mailto://" + this@CONTACT.email) { +this@CONTACT.email } }
field("Phone") { +this@CONTACT.phone }
}
}
}
}
}
fun DIV.field(name: String, value: SPAN.() -> Unit) {
div {
span {
style = css {
display = Display.inlineBlock
width = 6.em
}
+name
}
span {
style = css {
display = Display.inlineBlock
}
value(this)
}
}
}
fun BODY.rolodex(rolodex: ROLODEX.() -> Unit) {
ROLODEX().apply(rolodex)(this)
}
Usage
We are ready to use our DSL extensions. An example usage could look like this:
fun rolodex() : HTML.() -> Unit = {
head {
link(href = "styles.css", rel = "stylesheet")
}
body {
rolodex {
contact {
name = "Finn Mertens"
email = "finnthehuman@hero.org"
phone = "+11325554321"
picture = "finn.jpeg"
}
contact {
name = "Bonnibel Bubblegum"
email = "princessbubblegum@candykingdom.gov"
phone = "+11235551234"
picture = "bubblegum.jpeg"
}
}
}
}
You can see this code in action in this demo project on GitHub
Conclusion
Using a DSL lets me design my web page with minimal repetition and boilerplate. If this approach interests you, I encourage to check out the source code for my Website on GitHub, which is built entirely in Kotlin using DSLs.
Additional Resources
This article was first posted on Kotlin Town along with a video demo of this code in action
If you are interested in learning more about Kotlin DSLs, check out the following links:
If you are interested in learning more about the static web, check out the following links:
JavaScript growth and third parties
Discuss this post on Mastodon.