Migrating from Jekyll to Publish: A site generator for Swift developers

Publish is a static site generator built for Swift developers. We'll be migrating a static blog generation from Jekyll to Publish. The previous version of a blog was generated using Jekyll and a provided theme without putting too much effort in understanding the whole creation process. This time I decided to dig deeper into understanding Publish and remembering long forgotten HTML and CSS skills.

We'll be trying to create a similar style blog that was created using Jekyll. It should remain adaptable for bigger or smaller screen sizes:

Previous desktop version

Previous desktop version of a blog

Previous mobile version

Previous mobile version of a blog

Getting Started

After running the steps described in a Publish repository we end up with a sample project that showcases the main parts and features of Publish.

In the folder structure we can see posts folder that contains an example post. I copy-pasted my blog posts .md files from old Jekyll blog to see if it works. We only need to change couple fields to make it work. The only difference is a metadata format on top of .md file. By removing quotes from strings, brackets from arrays and renaming categories to tags we can get Jekyll articles immediately generated into a new blog.

Previous desktop version of a blog

Website

If we open up main.swift file we see:

try Blog().publish(withTheme: .foundation)

In Publish a website is a simple Swift struct that should contain mandatory fields required by a Website protocol as well as any additional fields that might be needed.

As this blog contains personal information as well as links to social media accounts, I included them in a Blog structure.

struct Blog: Website {
    enum SectionID: String, WebsiteSectionID {
        case posts
        case about
    }

    struct ItemMetadata: WebsiteItemMetadata {
    	// Additional field in post .md metadata
        var excerpt: String
    }

    var url = URL(string: "https://www.staskus.io")!
    var title = "staskus.io"
    var name = "Povilas Staškus"
    var description = "iOS Developer"
    var language: Language { .english }
    var imagePath: Path? { nil }
    var socialMediaLinks: [SocialMediaLink] { [.location, .email, .linkedIn, .github, .twitter] }
}

Theme

Basic foundation theme is used after generating a website for the first time. However, it's only there to give an example of how custom site theme should be created.

A Theme is built by conforming to HTMLFactory protocol.

Not all the pages are needed to be created for your site to be fully functioning. For a blog we must have: 1) an index page, that shows header, sidebar and list of posts. 2) item page, that will show full post.

class BlogHTMLFactory: HTMLFactory {

    func makeIndexHTML(for index: Index,
                       context: PublishingContext<Blog>) throws -> HTML {
    	// Returning HTML of an Index Page
    }

    func makeSectionHTML(for section: Section<Site>,
                         context: PublishingContext<Blog>) throws -> HTML {
    	// Returning HTML of a Section Index Page that displays section items
    }

    func makeItemHTML(for item: Item<Site>,
                      context: PublishingContext<Blog>) throws -> HTML {
    	// Returning HTML of a Post Page
    }

    func makePageHTML(for page: Page,
                      context: PublishingContext<Blog>) throws -> HTML {
    	// Returning HTML of a Section Page 
    }

    func makeTagListHTML(for page: TagListPage,
                         context: PublishingContext<Blog>) throws -> HTML? {
    	// Returning HTML of a Page with list of available tags
    }

    func makeTagDetailsHTML(for page: TagDetailsPage,
                            context: PublishingContext<Blog>) throws -> HTML? {
    	// Returning HTML of a Page for a specific tag
    }
}

Although we can write HTML code using Swift, the styling of the site is done using CSS. For the creation of this blog, I decided to use Pure.css. The desired blog theme is quite minimalist and the features that Pure.css provides seem more than enough to achieve what we want. Even after many years without any CSS and HTML experience it was possible to make pretty decent layout. For the rest of this article, we won't be focusing on that. The code can be found on GitHub for those who are interested.

Index Page

makeIndexHTML builds HTML of an index page. The structure is the same as writing plain HTML tags. We set language, head and then structure layout inside body.

Body uses .grid node that creates Pure.css grid in which we can structure our layout. We have .header, .sidebar, .posts and .footer that are put one after the other.

class BlogHTMLFactory: HTMLFactory {

    func makeIndexHTML(for index: Index,
                       context: PublishingContext<Blog>) throws -> HTML {
    	HTML(
            .lang(context.site.language),
            .head(for: context.site),
            .body(
                .grid(
                    .header(for: context.site),
                    .sidebar(for: context.site),
                    .posts(
                        for: context.allItems(
                            sortedBy: \.date,
                            order: .descending
                        ),
                        on: context.site,
                        title: "Recent posts"
                    ),
                    .footer(for: context.site)
                )
            )
        )
    }
}

Any node can be written as a static func extension of a Node. Context == HTML.BodyContext indicate that this node can be only used inside body. It's a nice and clean way to declare new nodes, as they can be then used using dot syntax.

extension Node where Context == HTML.BodyContext {
    static func grid(_ nodes: Node...) -> Node {
        .div(
            .id("layout"),
            .class("pure-g"),
            .group(nodes)
        )
    }
}

Post

A post can be declared the same way as any other node. On the top, we show the title of the post. Below, we show date. We can use any powerful features that Swift infrastructure provides us. In this instance, we use DateFormatter to format a date. In a similar way any more complex or sophisticated logic could be used inside this code.

extension Node where Context == HTML.BodyContext {
    static func post(for item: Item<Blog>, on site: Blog) -> Node {
        return .pageContent(
            .h2(
                .class("post-title"),
                .a(
                    .href(item.path),
                    .text(item.title)
                )
            ),
            .p(
                .class("post-meta"),
                .text(DateFormatter.blog.string(from: item.date))
            ),
            .tagList(for: item, on: site),
            .div(
                .class("post-description"),
                .div(
                    .contentBody(item.body)
                )
            )
        )
    }
}

Finishing the theme

We can make our theme accessible by creating Theme object and passing our BlogHTMLFactory. Declaring it as a static var inside an extension allows reaching it more conveniently.

extension Theme where Site == Blog {
    static var blog: Self {
        Theme(htmlFactory: BlogHTMLFactory())
    }
}

Generating the site

We've seen how by creating nodes we can step by step create pages for our blog. However, I would argue that the biggest power of Publish comes from plugins and additional building steps that we can create or use.

Splash is a Swift syntax highlighter for blogs that has a plugin built for Publish. During the blog generation process, this plugin uses markdown parser to identify code inside .md files and apply syntax highlighting.

We can use or build any steps that might do additional checks, append necessary information or simply tweak a site in any way needed.

import SplashPublishPlugin

try Blog().publish(
    withTheme: .blog,
    additionalSteps: [.deploy(using: .gitHub("nitesuit/nitesuit.github.io"))],
    plugins: [.splash(withClassPrefix: "")]
)

Result

Publish worked seamlessly during development process. It provided all the convenience of Swift strong type system and helped stay focused while trying to figure unfamiliar web development workflows. Although Publish still misses many features that established static site generators have, it serves perfectly for its intended audience - Swift developers. Now that all the parts of the site are well understood, Publish together with Swift gives a lot of power for future improvements.

The code of the blog can be found on GitHub.

New desktop version

New desktop version of a blog

New mobile version

New mobile version of a blog