Codetown ::: a software developer's community
With John Burns (@wakingrufus)
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.
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. There is no reason these types of sites need to carry all the baggage that comes with a webapp. With todays technologies, static websites can be extremely fast and reliable. There is a new static webhost called Neocities, a name which is an homage to the defunct Geocities, where many people are building static sites.
I decided to build myself a personal website in order to avoid places like Medium, Twitter, or various slideshow services. Anyone who has built a site by hand in HTML knows that this is very tedious. There are some frameworks for generating static sites, such as Jekyll and Hugo, which are great for most people. But I am a programmer, and I'd like the control I get from programming, with the benefits of using a framework as well. 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.
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()
}
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")
}
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.
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" }
}
}
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() {
apply {
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 want to create a page where I can document my travels, and act as a guide to others who might be travelling in the same areas. I want to be able to break areas down into sub areas, and detail specific locations. 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 PLACE(
val name: String
val website: String? = null
val map: String? = null
val description: P.() -> Unit = { }
val level: NestedAreaLevel = NestedAreaLevel(1)) {
private var places: List<PLACE> = ArrayList()
private var subAreas: List<PLACE> = ArrayList()
fun place(
name: String,
website: String? = null,
block: PLACE.() -> Unit) {
places += PLACE(name = name,
website = website,
level = NestedAreaLevel(6)).apply(block)
}
fun subArea(
name: String,
block: PLACE.() -> Unit) {
subAreas += PLACE(name = name, level = level.next()).apply(block)
}
fun description(block: P.() -> Unit) {
description = block
}
operator fun invoke(code: 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. finally, the invoke method will define how this element will be rendered. You might notice the use of a class called NestedAreaLevel. This is a class that I am using to represent the nesting level of the areas. It looks like this:
inline class NestedAreaLevel( val value: Int) {
companion object : {
val default = NestedAreaLevel(2)
} fun next() : NestedAreaLevel {
return when (this.value) {
1 -> NestedAreaLevel(2)
2 -> NestedAreaLevel(3)
3 -> NestedAreaLevel(4)
4 -> NestedAreaLevel(5)
5 -> NestedAreaLevel(6)
6 -> NestedAreaLevel(6)
else -> NestedAreaLevel.default
}
}
}
My implementation looks like this:
class PLACE() {
operator fun invoke(code: DIV) {
code.run {
a { id = this@PLACE.name.toLowerCase() }
headerLevel(this@PLACE.level)(this, null) {
if (this@PLACE.level.value == 6) {
style = css { marginBottom = 1.em }
}
+this@PLACE.name
}
this@PLACE.website?.let {
div {
if (this@PLACE.level.value == 6) {
style = css { fontSize = 0.67.em marginBottom = 1.em }
}
a(href = it) { +"Website" }
this@PLACE.map?.let {
+" " a(href = it) { +"Map" }
}
}
}
p {
if (this@PLACE.level.value == 6) {
style = css { fontSize = 0.67.em }
}
this@PLACE.description(this)
}
div {
style = css { paddingLeft = this@PLACE.level.value.em }
this@PLACE.places.forEach { it(code) }
this@PLACE.subAreas.forEach { it(code) }
}
}
}
}
We are ready to use our DSL extensions. An example usage could look like this:
fun travel() : HTML.() -> Unit = {
head {
link(href = "styles.css", rel = "stylesheet")
}
body {
h1 { +"Travel" }
sideNav()
content {
area("Large Area") {
subArea("example sub area") {
website = "http://www.areawebsite.com"
description { +"subarea description" }
place(name = "Place name", website = "http://www.placewebsite.com") {
map = "link to map"
description { +"place description" }
}
}
}
}
}
}
You can see my actual usage of this DSL on GitHub
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.
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:
Tags:
Whew! Fabulous post!
Codetown is a social network. It's got blogs, forums, groups, personal pages and more! You might think of Codetown as a funky camper van with lots of compartments for your stuff and a great multimedia system, too! Best of all, Codetown has room for all of your friends.
Created by Michael Levin Dec 18, 2008 at 6:56pm. Last updated by Michael Levin May 4, 2018.
Check out the Codetown Jobs group.
As a software engineer or individual contributor, the next step in your career can be to become a principal engineer. The path to becoming a principal engineer at companies can feel unclear, which can inhibit individual engineering careers. But that also provides opportunities for engineers to invent and shape the role of principal engineers.
By Ben Linders, Joy Ebertz, Pablo Fredrikson, Charlotte de Jong SchouwenburgAndy Burgin explains what the customer experience team did in one of their projects, starting from scratch and how they have attained feedback with recommendations from across the business.
By Andy BurginNetflix engineers recently published a deep dive into their Distributed Counter Abstraction, a scalable service designed to track user interactions, feature usage, and business performance metrics with low latency. The system balances performance, accuracy, and cost through configurable counting modes, resilient data aggregation, and a globally distributed architecture.
By Eran StillerAmazon Web Services (AWS) has launched S3 Metadata, enhancing data management for S3 users. This new capability enables near real-time querying and analysis of S3 data via organized metadata updates. By adopting Apache Iceberg, it ensures interoperability and scalability, allowing businesses to efficiently leverage their data for analytics and AI applications.
By Steef-Jan WiggersJake Zimmerman, Technical Lead of Sorbet at Stripe, and Getty Ritter, Ruby Infrastructure Engineer at Stripe, presented Refactoring Stubborn, Legacy Codebases at the 2024 QCon San Francisco conference. Zimmerman and Ritter demonstrated how to fix complaints on maintaining a large codebase with leverage and by ratcheting incremental progress.
By Michael Redlich© 2024 Created by Michael Levin. Powered by