How to write HTML in Swift?
This tutorial is all about rendering HTML docs using a brand new DSL library called SwiftHtml and the Vapor web framework.
Introducing SwiftHtml
This time we’re going to start everything from scratch. In the first section of this article I’m going to show you how to setup the SwiftHtml as a package dependency and how to generate HTML output based on a template file. Let’s start by creating a brand new executable Swift package.
mkdir Example
cd "$_"
swift package init --type=executable
open Package.swift
You can also start with a macOS Command Line Tool from Xcode if you wish, but nowadays I prefer Swift Packages. Anyway, we should add SwiftHtml as a dependency to our package right away.
// swift-tools-version:5.5
import PackageDescription
let package = Package(
name: "Example",
platforms: [
.macOS(.v12)
],
dependencies: [
.package(url: "https://github.com/binarybirds/swift-html", from: "1.2.0"),
],
targets: [
.executableTarget(name: "Example", dependencies: [
.product(name: "SwiftHtml", package: "swift-html"),
]),
.testTarget(name: "ExampleTests", dependencies: ["Example"]),
]
)
All right, now we’re ready to write some Swift DSL code. We’re going to start with a really basic example to get to know with SwiftHtml. In the main.swift file we should create a new HTML document, then we can use SwiftHtml’s built-in renderer to print the html source. 🖨
import SwiftHtml
let doc = Document(.html) {
Html {
Head {
Title("Hello, World!")
Meta().charset("utf-8")
Meta().name(.viewport).content("width=device-width, initial-scale=1")
}
Body {
Main {
Div {
H1("Hello, World!")
P("This page was generated by the SwiftHtml library.")
}
}
.class("container")
}
}
}
let html = DocumentRenderer(minify: false, indent: 2).render(doc)
print(html)
As you can see the code is pretty straightforward, especially if you know a bit about HTML. The SwiftHtml library tries to follow the naming conventions as closely as possible, so if you’ve written HTML before this syntax should be very familiar, except that you don’t have to write opening and closing tags, but we can utilize the Swift compiler to do the boring repetative tasks instead of us.
Since we’re using a domain specific language in Swift, the compiler can type-check everything at build-time, this way it’s 100% sure that our HTML code won’t have syntax issues. Of course you can still make semantic mistakes, but that’s also possible if you’re not using a DSL. 😅
The main advantage here is that you won’t be able to mistype or misspell tags, and you don’t even have to think about closing tags, but you can use result builders to construct the HTML node tree. SwiftHtml uses tags and it’ll build a tree from them, this way it is possible to efficiently render the entire structure with proper indentation or minification if it is needed.
The DocumentRenderer object can render a document, it is also possible to create all sorts of SGML-based document types, because the SwiftHtml package comes with an abstraction layer. If you take a look at the package structure you should see that inside the Sources directory there are several other directories, the core of the package is the SwiftSgml component, which allows developers to create other domain specific languages on top of the base components. 🤔
For example, if you take a look at the SwiftRss package you will see that it’s a simple extension over the SwiftSgml library. You can subclass the Tag object to create a new (domain specific) tag with an underlying Node object to represent a custom item for your document.
The SwiftSgml library is very lightweight. The Node struct is a representation of a given SGML node with a custom type, name and attributes. The Tag class is all about building a hierarchy in between the nodes. The Document struct is a special object which is responsible for rendering the doctype declaration before the root tag if needed, also of course the document contains the root tag, which is the beginning of everything. 😅
SwiftSgml also contains the DocumentRenderer and a simple TagBuilder enum, which is a result builder and it allows us to define our structure in a SwiftUI-like style.
So the SwiftHtml package is just a set of HTML rules on top of the SwiftSgml library and it follows the W3C HTML reference guides. You can use the output string to save a HTML file, this way you can generate static websites by using the SwiftHtml library.
import Foundation
import SwiftHtml
let doc = Document(.html) {
Html {
Head {
Title("Hello, World!")
Meta().charset("utf-8")
Meta().name(.viewport).content("width=device-width, initial-scale=1")
}
Body {
Main {
Div {
H1("Hello, World!")
P("This page was generated by the SwiftHtml library.")
}
}
.class("container")
}
}
}
do {
let dir = FileManager.default.homeDirectoryForCurrentUser
let file = dir.appendingPathComponent("index.html")
let html = DocumentRenderer(minify: false, indent: 2).render(doc)
try html.write(to: file, atomically: true, encoding: .utf8)
}
catch {
fatalError(error.localizedDescription)
}
This is just one way to use SwiftHtml, in my opinion static site generators are fine, but the real fun begins when you can render websites based on some kind of dynamic data. 🙃
Using SwiftHtml with Vapor
Vapor has an official template engine called Leaf plus the community also created a type-safe HTML DSL library called HTMLKit, so why create something very similar?
Well, I tried all the available Swift HTML DSL libraries that I was able to find on GitHub, but I was not entirely satisfied with the currently available solutions. Many of them was outdated, incomplete or I simply didn’t like the flavor of the DSL. I wanted to have a library which is freakin’ lightweight and follows the standards, that’s the reason why I’ve built SwiftHtml. 🤐
How can we integrate SwiftHtml with Vapor? Well, it’s pretty simple, let’s add Vapor as a dependency to our project first.
// swift-tools-version:5.5
import PackageDescription
let package = Package(
name: "Example",
platforms: [
.macOS(.v12)
],
dependencies: [
.package(url: "https://github.com/binarybirds/swift-html", from: "1.2.0"),
.package(url: "https://github.com/vapor/vapor", from: "4.54.0"),
],
targets: [
.executableTarget(name: "Example", dependencies: [
.product(name: "SwiftHtml", package: "swift-html"),
.product(name: "Vapor", package: "vapor"),
]),
.testTarget(name: "ExampleTests", dependencies: ["Example"]),
]
)
We’re going to need a new protocol, which we can use construct a Tag, this is going to represent a template file, so let’s call it TemplateRepresentable.
import Vapor
import SwiftSgml
public protocol TemplateRepresentable {
@TagBuilder
func render(_ req: Request) -> Tag
}
Next, we need something that can render a template file and return with a Response object, that we can use inside a request handler when we setup the route handlers in Vapor. Since we’re going to return a HTML string, it is necessary to set the proper response headers too.
import Vapor
import SwiftHtml
public struct TemplateRenderer {
var req: Request
init(_ req: Request) {
self.req = req
}
public func renderHtml(_ template: TemplateRepresentable, minify: Bool = false, indent: Int = 4) -> Response {
let doc = Document(.html) { template.render(req) }
let body = DocumentRenderer(minify: minify, indent: indent).render(doc)
return Response(status: .ok, headers: ["content-type": "text/html"], body: .init(string: body))
}
}
Finally we can extend the built-in Request object to return a new template renderer if we need it.
import Vapor
public extension Request {
var templates: TemplateRenderer { .init(self) }
}
Now we just have to create a HTML template file. I’m usually creating a context object right next to the template this way I’m going to be able to pass around contextual variables for each template file. I’m quite happy with this approach so far. ☺️
import Vapor
import SwiftHtml
struct IndexContext {
let title: String
let message: String
}
struct IndexTemplate: TemplateRepresentable {
let context: IndexContext
init(_ context: IndexContext) {
self.context = context
}
func render(_ req: Request) -> Tag {
Html {
Head {
Title(context.title)
Meta().charset("utf-8")
Meta().name(.viewport).content("width=device-width, initial-scale=1")
}
Body {
Main {
Div {
H1(context.title)
P(context.message)
}
}
.class("container")
}
}
}
}
Finally we just have to write some boilerplate code to start up our Vapor web server, we can use the app instance and set a get request handler and render our template using the newly created template renderer extension on the Request object.
import Vapor
import SwiftHtml
var env = try Environment.detect()
try LoggingSystem.bootstrap(from: &env)
let app = Application(env)
defer { app.shutdown() }
app.get { req -> Response in
let template = IndexTemplate(.init(title: "Hello, World!",
message: "This page was generated by the SwiftHtml library."))
return req.templates.renderHtml(template)
}
try app.run()
More or less that’s it, you should be able to run the server and hopefully you should see the rendered HTML document if you open the http://localhost:8080/
address using your browser.
It is also possible to use one template inside another, since you can call the render method on a template and that template will return a Tag. The beauty of this approach is that you can compose smaller templates together, this way you can come up with a nice project structure with reusable HTML templates written entirely in Swift. I’m more than happy with this simple solution and seems like, for me, there is no turning back to Leaf or Tau… 🤓
Related posts
10 short advices that will make you a better Vapor developer right away
As a beginner server side Swift developer you'll face many obstackles. I'll show you how to avoid the most common ones.
A generic CRUD solution for Vapor 4
Learn how to build a controller component that can serve models as JSON objects through a RESTful API written in Swift.
A simple HTTP/2 server using Vapor 4
Get started with server-side Swift using the Vapor 4 framework. Learn how to build a really simple HTTP/2 backend server.
AJAX calls using Vapor 4
Learn how to implement Asynchronous JavaScript and XML (AJAX) calls using Leaf templates and Vapor 4 as a server.