Swift/UnwrapDiscover about Swift, iOS and architecture patterns

Building a blog: Publish

March 16, 2021

In 2020 I decided to build my own tech blog. While appealing (how hard could it be?) it actually took me some time to figure out which stack to put in: Swift or not Swift; how to deploy a website with modern technologies like AWS; how to automate without costing me anything.

In this first article let's focus on the first choice I made: building my blog using Swift thanks to Publish.

Why Swift?

When building my website I first though about using web technologies like React. But then it came to my mind that:

  1. It's run on client-side meaning I would have needed a backend server to deliver the articles content.
  2. It's written in Javascript/Typescript which I barely know. Sure it would have been the occasion to improve my skills on it. But I also knew that doing so I would actually probably never release my project.

Tools like Gatsby or NextJS would have actually me help on doing a SSG (Static Site Generation) with front technologies. But again I preferred to focus on a language I knew well to reach my goal.

Swift web ecosystem is not as vibrant as the one from Javascript. Available frameworks and tools are way more limited. Fortunately however Swift does have (at least) one known static site generator framework: Publish.

Publish

To use Publish you just need to add it to your Package.swift file. Note that in order to make our blog we will create a executable project, not a library.

let package = Package(
  products: [
    .executable(name: "SwiftUnwrap", targets: ["SwiftUnwrap"])
  ,
  dependencies: [
    .package(url: "https://github.com/johnsundell/publish.git", from: "0.7.0")
  ]

Publish does come with pre-defined code (and folders) structure which you have to follow in order to use it. While the README is quite complete it actually took me some time to really grasp how to use the tool. So here are some notes/advice.

Content

First thing to understand is that your website will be an instance of Website. You can put any data inside it that is related to you website but you'll have at least to provide two associated objects: SectionID and ItemMetadata.

SectionID allow you to classify your content. As I'm only publishing articles mine is actually reduced to only one value:

enum SectionID: String, WebsiteSectionID {
  case article
}

One thing important though: SectionID is used when loading your content. Here for instance I had to put all my articles under an "article" folder in order for them to be associated to the ID (and thus accessible from code).

ItemMetadata give you access to your markdown files metadata. Each attribute will match one of your markdown metadata:

struct ItemMetadata: WebsiteItemMetadata {       
  let path: Path
  let tags: [Tag]
  let socialTags: SocialTags
  let description: String
  let isDraft: Bool
}

WebsiteItemMetadata has no required attributes. Yet some have special treatment from Publish.

There is no documentation on them (at least last time I checked) and you'll have to dig into MarkdownContentFactory.makeItem and MarkdownContentFactory.makeContent to find them. At the time of writing these are:

  • 🛣️ path
  • 🏷️ tags
  • 📅 date
  • 🔗 rss
  • 📰 title
  • 🖊️ ️description
  • 🖼 ️image
  • 🎧 audio
  • 📹 video

They will automatically end up either in Item or Content and will be used by Publish to do certain things like path defining your file HTML URL.

One thing to note is that you can still "redefine" them in ItemMetadata. For instance if you take a closer look to the code above you will see that I defined my own tags attribute using a custom enum Tag which allow me to limit labels I can set on an article.

Bear in mind though that doing so you will end up with the attribute in two places:

ItemMetadata.tags and Item.tags each one having with its own type 🐫.

enum Tag: String, Decodable, CaseIterable {
    case spm
    case ui
    case swift
    case testing
    case ci
    case architecture
    case network
    case build
    case rx
}

Also one note of caution about date: if you don't define any date in your markdown then Publish will automatically pick one (file modification date). So be careful if you're using it to define which article to publish or not 🙂

Theming

A Theme is a set of defined methods throughout which you generate your HTML. There are 6 methods you can define but the most important ones are:

  • makeIndexHTML to make your homepage
  • makeSectionHTML to create a page listing content of the given section
  • makeItemHTML generate HTML page for an item (one markdown file in one section). In my blog that's where I generate an article detail page
  • makeTagDetailsHTML to display content associated to a tag.

You can use these information to display whatever you want using Plot DSL.

func makeIndexHTML(for index: Index, context: PublishingContext<SwiftUnwrap>) throws -> HTML {
    HTML.blog(for: index, context) {
        .div(
            .id("home"),
            .class("content"),
            .section(
                .browseAllTags(context: context)
            ),

            .section(
                .class("latest"),
                .h2("Latest articles"),
                .previewList(
                    context.allItems(sortedBy: \.date, order: .descending)
                )
            )
        )
    }
}

Publish command

Ok now how do you generate your website? You'll have to run Website.publish into your main.swift file.

You can use publish with predefined steps but I like being in control so I defined my own pipeline. Important pieces were:

  • To parse code using Splash
  • Add markdown files
  • Generate HTML
  • Generate RSS and sitemap
  • Copy resources
try SwiftUnwrap().publish(
    using: [
        .installPlugin(.splash(withClassPrefix: "")),
        .group([
            .configureMetadataParsing(),
            // Content is actually under Content/en/article, remember SectionID
            .addMarkdownFiles(at: "Content/en"),
        ]),
        .generateHTML(withTheme: .swiftUnwrap),
        .generateMinimizedCSS(),
        .generateRSSFeed(including: [.article]),
        .generateSiteMap(),
        .copyResources(at: "Resources/images", to: "images")
    ]
)

You'll notice generateMinimizedCSS which is a custom step I made to minimize my website CSS. While not available by default in Publish it is very easy to add by yourself:

extension PublishingStep where Site == SwiftUnwrap {
  static func generateMinimizedCSS() -> Self {
        step(named: "minimize css from Resources folder") { context in
            let minimizedCss = try context.createOutputFile(at: "styles.css")
            let resources = try context.folder(at: "Resources")

            try resources.files
                .filter { $0.extension == "css" }
                .forEach { file in
                    try minimizedCss.append(try file.read())
                    try minimizedCss.append("\n")
            }
        }
    }
}

You can now run swift run and generate your website 👏

To test locally use swift run publish-cli run. Your website will then be accessible on localhost:8000.

Now that we know how to generate our website we can deploy it using Github Actions!