30 Dec 201911 min read

Generating static website

Generating static website

Back in the 2000s when I was still a kid and had no idea about professional programming, website development was something I was passionate about. There was no JavaScript nor even CSS. My first website was created using Microsoft FrontPage and it was mostly based on tables. It was so much fun for me when I was 10 years old. After that I have learned HTML and why would I need anything else? Then the times of Flash websites and other different frameworks come, including JavaScript. I did some web development with the help of PHP but kinda lost interest in that for “some” reason. There was a WordPress anyway so used that for whatever I needed. I always enjoyed HTML and CSS, just too bad it was not efficient to create a website with pure HTML+CSS. Then, out of the blue...

Plot, the static website generator in Swift was announced by John Sundell. I got excited about that and was looking forward to releasing it. I had a year-long break from blogging and I promised myself I will start again once I rewrite my blog using Plot. So here we come...

Server

Before we start developing our website with Swift, first of all, we need a http server, thankfully if we are on a macOS it comes with already installed python. All we need it the command below

python -m SimpleHTTPServer 8080

It will start a server on the localhost in the current directory - we will run this command in the directory where all generated files will be saved to.

It would be possible to test everything on a remote server but that would make our lives harder, having to upload all changes to the remote one is quite a time consuming while doing that manually and we rather want to test our changes immediately.


I wonder if it would be possible to wrap the generator with iOS apps (written in SwiftUI) to test it on mobile as well. It’s something I must give it a try someday.


Preparation

Besides the working server, we need to plan the file structure of our project. For my blog, I didn’t want to worry about any missing files when switching between computers while working on my blog so all the resources for the blog are added to the Xcode project and used to generate HTML or copied when needed. It includes article files (markdown), static images (ie. a logo) as well as all other needed files such as styles file and .htaccess.

So below you can see how it looks like

├── ordinary-coding
│   ├── ordinary-coding
│   │   ├── generator
│   │   │   ├── ArticlesGenerator.swift
│   │   │   ├── BlogGenerator.swift
│   │   │   ├── GlobalFilesGenerator.swift
│   │   │   └── managers
│   │   │       ├── FilesManager.swift
│   │   │       └── ImagesManager.swift
│   │   ├── main.swift
│   │   ├── models
│   │   │   ├── Article.swift
│   │   │   └── Project.swift
│   │   └── website
│   │       ├── articles
│   │       │   ├── 1
│   │       │   │   ├── 1.gif
│   │       │   │   └── article.md
│   │       │   ├── 2
│   │       │   │   ├── 1.png
│   │       │   │   ├── 2.png
│   │       │   │   ├── 3.png
│   │       │   │   └── article.md
│   │       │   ├── 3
│   │       │   │   ├── 1.png
│   │       │   │   ├── 2.png
│   │       │   │   ├── 3.png
│   │       │   │   ├── 4.png
│   │       │   │   ├── 5.png
│   │       │   │   ├── 6.png
│   │       │   │   ├── 7.png
│   │       │   │   ├── 8.png
│   │       │   │   ├── 9.png
│   │       │   │   ├── 10.gif
│   │       │   │   └── article.md
│   │       │   ├── 4
│   │       │   │   └── article.md
│   │       │   ├── 5
│   │       │   │   ├── 1.png
│   │       │   │   ├── 2.png
│   │       │   │   ├── 3.gif
│   │       │   │   └── article.md
│   │       │   ├── 6
│   │       │   │   └── article.md
│   │       │   ├── 7
│   │       │   │   ├── 1.png
│   │       │   │   ├── 2.png
│   │       │   │   ├── 3.png
│   │       │   │   └── article.md
│   │       │   ├── 8
│   │       │   │   ├── 1.png
│   │       │   │   └── article.md
│   │       │   └── 9
│   │       │       └── article.md
│   │       ├── fonts
│   │       │   ├── SourceSansPro-Bold.ttf
│   │       │   ├── SourceSansPro-LightItalic.ttf
│   │       │   └── SourceSansPro-Regular.ttf
│   │       ├── images
│   │       │   ├── about-me.png
│   │       │   ├── favicon.png
│   │       │   ├── logo.png
│   │       │   └── social.png
│   │       ├── projects
│   │       │   ├── expenses-pal.png
│   │       │   └── watermaniac.png
│   │       └── styles.css
│   │       └── .htaccess
│   └── ordinary-coding.xcodeproj
└── website

The ordinary-coding/ordinary-coding is where all the generator and resource files are. The ordinary-coding/website is where all resource files are copied to and HTML files are generated. It’s pretty much website ready to upload to the production server. In this directory, I also run my localhost HTTP server.

Aa you can see all my articles are grouped into separate directories that contain markdown file and all the images needed for a given article. Also, in directories fonts, images and projects are static files that are copied (including styles.css and .htaccess. I will explain some of the .swift files later on.

Project

As a template for my project, I have chosen the Command Line Tool under the macOS category in Xcode. For adding dependencies the easiest and fastest way is to just use built-in into Xcode package manager (Swift Package Manager). Great „Managing dependencies using the Swift Package Manager„ tutorial about that can be found on John’s blog.

Dependencies I added are Plot, Ink, and Splash.

Plot


A DSL for writing type-safe HTML, XML and RSS in Swift


The core of the static generator. It’s where the most „magic” comes from.

Ink


A fast and flexible Markdown parser written in Swift


It is somehow optional but quite convenient if you want to keep your articles in markdown (a lightweight markup language). You could just stick to Plot and generate HTML for articles using it but I think it’s way faster to write them using Markdown - ie. right now I am writing this article in Markdown. I am kinda busy so writing articles the other way wouldn’t be efficient for me. I sync markdown files on all my devices so whenever I have a little bit of time I write on either Mac Book, PC with Windows or my phone. And Apple’s Notes are all you need pretty much (If anyone knows any good application for Markdown with syncing let me know, please).

Splash


A fast, lightweight and flexible Swift syntax highlighter for blogs, tools and fun!


if you want to include Swift code in your articles it is a must-have, also works quite good with other languages, and if not, can be customized to make it work. On my blog I post both Swift and Dart code, and as you might have seen it works fine for Dart just fine out of the box. I will try to improve it in the future most likely though.

Generating

First of all, read Plot’s README carefully. It contains so many useful tips that you will regret not knowing if you do not read that. The API is quite big so reading all the code line by line, or looking for a specific function might be challenging.

FilesManager.swift is a class that handles all file-related operations.

struct File {
    let content: String
    let directory: String?
    let fileName: String
}

final class FilesManager {
    private let basePath = "root/path/for/project"

    private lazy var baseUrl: URL = {
        guard let url = try? FileManager.default.url(for: .userDirectory, in: .allDomainsMask, appropriateFor: nil, create: true).appendingPathComponent(basePath)else {
            fatalError("Incorrect fromUrl in FilesManager")
        }

        return url
    }()

    var fromUrl: URL {
        self.baseUrl.appendingPathComponent("path/to/resources")
    }

    private var toUrl: URL {
        self.baseUrl.appendingPathComponent("destination/path/for/website")
    }


    /// Removes all website related files so nothing is left when generating new version of the website
    func clearAll() {
        do {
            let files = try FileManager.default.contentsOfDirectory(at: toUrl, includingPropertiesForKeys: nil)
            try files.forEach {
                try FileManager.default.removeItem(at: $0)
                print("🟧 Removed " + $0.path)
            }
        } catch {
            print("🟥 Error: " + error.localizedDescription)
        }
    }

    /// Copies given files to destination URL and optionally creating new directory where the files will be copied to
    func copyFiles(_ urls: [URL], to path: String? = nil) {
        guard let dUrl = toUrl.destinationUrl(with: path) else {
            return
        }

        urls.forEach {
            let newUrl = dUrl.appendingPathComponent($0.lastPathComponent)

            do {
                try FileManager.default.copyItem(at: $0, to: newUrl)
                print("🟦 Saved: " + newUrl.path)
            } catch {
                print("🟥 Error: " + error.localizedDescription)
            }
        }
    }

    /// Saves given files to destination URL and optionally creating new directory where the files will be saved to
    func saveFiles(_ files: [File], to path: String? = nil) {
        guard let dUrl = toUrl.destinationUrl(with: path) else {
            return
        }

        files.forEach {
            let fileUrl: URL
            if let directory = $0.directory {
                guard let url = dUrl.destinationUrl(with: directory) else {
                    fatalError("🟥 Cannot create specified directory")
                }

                fileUrl = url.appendingPathComponent($0.fileName)
            } else {
                fileUrl = dUrl.appendingPathComponent($0.fileName)
            }

            do {
                try $0.content.write(to: fileUrl, atomically: false, encoding: .utf8)
                print("🟦 Saved: " + fileUrl.path)
            } catch {
                print("🟥 Error: " + error.localizedDescription)
            }
        }
    }
}

extension URL {
    func destinationUrl(with path: String?) -> URL? {
        let destinationUrl: URL

        if let path = path {
            destinationUrl = appendingPathComponent(path)
        } else {
            destinationUrl = self
        }

        do {
            try FileManager.default.createDirectory(atPath: destinationUrl.path, withIntermediateDirectories: true, attributes: nil)
        } catch {
            print("🟥 Error: " + error.localizedDescription)
            return nil
        }

        return destinationUrl
    }
}

extension FilesManager {
    func copyCommon() {
        copyFonts()
        copyStyles()
        copyHtaccess()
    }

    private func copyFonts() {
        let fUrl = fromUrl.appendingPathComponent("fonts")

        guard let imageURLs = try? FileManager.default.contentsOfDirectory(at: fUrl, includingPropertiesForKeys: nil) else {
            return
        }

        copyFiles(imageURLs, to: "fonts")
    }

    private func copyStyles() {
        let styleCssUrl = fromUrl.appendingPathComponent("styles.css")

        copyFiles([
            styleCssUrl
        ])
    }

    private func copyHtaccess() {
        let htaccessUrl = fromUrl.appendingPathComponent(".htaccess")

        copyFiles([
            htaccessUrl
        ])
    }
}

Please, keep in mind that this little project of mine is still in development are some of the code I show is most likely not the final version.


What would our blog be without articles? I load them by listing all directories inside articles folder and then by iterating them and loading article.md file in each of them. Besides that, I had to copy images and I do that by preparing a list of resources to copy (simply adding a URL of the file that is not a markdown file to an array).

Also since my articles contain some block of codes I had to add a modifier to the markdown parser and highlight the code by using Splash framework. Ink (the parser) returns HTML with the pre and code HTML tags and code highlighter escapes the HTML output so I unescape it back. I believe the rest of the code does not need much explanation.

struct Article {
    let title: String
    let tags: [String]
    let description: String
    let date: String
    let markdown: Markdown
    let assets: [URL]
}

func prepareArticles() -> [Article] {
        guard let fileURLs = try? FileManager.default.contentsOfDirectory(at: filesManager.fromUrl.appendingPathComponent("articles"), includingPropertiesForKeys: nil) else {
            return []
        }

        var parser = MarkdownParser()
        let codeModifier = Modifier(target: .codeBlocks) { html, markdown in
            return self.highlighter.highlight(html).unescaped
        }

        let inlineCodeModifier = Modifier(target: .inlineCode) { html, markdown in
            return self.highlighter.highlight(html).unescaped
        }

        let quoteModifier = Modifier(target: .blockquotes) { html, markdown in
            return "<.hr>\(html)<.hr>" // Note: Dot before hr tag to avoid rendering it (for article purpose, remove the dot when copied to Xcode)
        }

        parser.addModifier(codeModifier)
        parser.addModifier(inlineCodeModifier)
        parser.addModifier(quoteModifier)

        var articles = [Article]()

        fileURLs.forEach { url in
            if let markdown: String = try? String(contentsOfFile: url.appendingPathComponent("article.md").path, encoding: .utf8) {
                let result = parser.parse(markdown)

                guard
                    let title = result.title,
                    let topics = result.metadata["topics"],
                    let excerpt = result.metadata["excerpt"],
                    let date = result.metadata["date"]
                else {
                    fatalError("Article's metadata is missing")
                }

                var assets = [URL]()
                if let assetURLs = try? FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: [.nameKey]) {
                    assetURLs.forEach {
                        if let resources = try? $0.resourceValues(forKeys: [.nameKey]), let name = resources.name {
                            if !name.contains(".md") { assets.append($0) }
                        } else {
                            fatalError("Incorrect file - cannot determine its name")
                        }
                    }
                }

                let article = Article(title: title, tags: topics.components(separatedBy: ","), description: excerpt, date: date, markdown: result, assets: assets)

                articles.append(article)
            }
        }

        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "dd.MM.yyyy"
        dateFormatter.locale = Locale(identifier: "en_US_POSIX")

        return articles.sorted { a1, a2 -> Bool in
            guard
                let date1 = dateFormatter.date(from: a1.date),
                let date2 = dateFormatter.date(from: a2.date)
            else {
                fatalError("Date is in incorrect format.")
            }

            return date1 > date2
        }
}

extension String {
    var unescaped: String {
        return replacingOccurrences(of: "&lt;", with: "<")
            .replacingOccurrences(of: "&gt;", with: ">")
    }
}

Now, the last thing is to generate the actual website. In the BlogGenerator class, I have a method

private func html(title: String, url: String, description: String, content: Node<HTML.BodyContext>) -> String {
    let html = HTML(
        .lang("en"),
        .blogHead(title: title, url: url, description: description),
        .body(
            .header(
                .div(.class("wrapper"),
                     .menu()
                )
            ),
            content,
            .footerContent()
        )
     )

    return html.render()
}

where I pass title, description and content arguments so everything else can be shared between all the pages on the website. When adding an article’s HTML to the page just remember to use .raw(articleContent). A good practice is also to use an extension to define your own type-safe elements and attributes so it’s easier to reuse them wherever needed.

Conclusion

It took me about 2 weeks (around 1-3 hours each day) to create the website from scratch and the most time-consuming task was to rewrite 9 articles to markdown language and to prepare the CSS file. Besides that, you still need to have at least a basic knowledge of how to write HTML and how to style it with CSS (it might change once the Publish is released by John since it will include some themes I believe).

It’s also great that while using this DSL there is still room for your own skills to shine or learn new things and there are endless possibilities how this framework can be used. It will be fun to see what people will come with.


Plot is a missing link between Swift and HTML.


And I really hope the static website generators will become more and more popular. I had a lot of fun playing with Plot and recommend it to everyone who wants to build such a website.

Final words

I hope you find this article somehow useful while building your website using Plot. If you have any questions feel free to contact me, preferably on Twitter or by e-mail.

Thank you for reading.