Creating a Blog with Publish: Theme

Published on

Get weekly handpicked updates on Swift and SwiftUI!

Having a powerful theme system is one of the key factors in the success of a static website generator. Publish uses Plot as the development tool for themes, allowing developers to enjoy the benefits of Swift’s type safety while efficiently writing themes. This article will start with an introduction to Plot, and ultimately teach readers how to create a Publish theme.

Plot

Introduction

To develop a Theme for Publish, we cannot ignore Plot.

In the Swift community, there are many excellent projects dedicated to generating HTML using Swift, such as Leaf from Vapor and swift-html from Point-Free, Plot is also one of them. Plot was originally written by John Sundell and is part of the Publish suite. Its main focus is on generating static website HTML using Swift and creating other format documents required for building a website, including RSS, podcast, and Sitemap. It is tightly integrated with Publish but also exists as an independent project.

Plot uses a technique called Phantom Types, which uses types as “tags” for the compiler, allowing type safety to be enforced through generic constraints. Plot uses a very lightweight API design that minimizes external parameter labels to reduce the amount of syntax required to render documents, resulting in a code representation with a “DSL-like” appearance.

Usage

Basics

  • Node

It is the core component that represents all elements and attributes in any Plot document. It can represent elements and attributes, as well as text content and node groups. Each node is bound to a Context type that determines which DSL API it can access (for example, HTML.BodyContext is used to place nodes in the <body> section of an HTML page).

  • Element

Represents an element that can be opened and closed with two separate tags (e.g. <body></body>) or self-closed (e.g. <img/>). When using Plot, you typically don’t need to interact with this type, as instances of it are created in the basic Node.

  • Attribute

Represents an attribute attached to an element, such as the href of an <a> element or the src of an <img> element. You can construct an Attribute value using its initializer or use the .attribute() command with the DSL.

  • Document and DocumentFormat

A document in a given format, such as HTML, RSS, and PodcastFeed. These are the highest-level types that you can use Plot’s DSL to start a document-building session.

DSL-like syntax

Swift
import Plot

let html = HTML(
    .head(
        .title("My website"),
        .stylesheet("styles.css")
    ),
    .body(
        .div(
            .h1("My website"),
            .p("Writing HTML in Swift is pretty great!")
        )
    )
)

The Swift code above will generate the HTML code below. The code format is very similar to DSL and has very little code pollution.

HTML
<!DOCTYPE html>
<html>
    <head>
        <title>My website</title>
        <meta name="twitter:title" content="My website"/>
        <meta name="og:title" content="My website"/>
        <link rel="stylesheet" href="styles.css" type="text/css"/>
    </head>
    <body>
        <div>
            <h1>My website</h1>
            <p>Writing HTML in Swift is pretty great!</p>
        </div>
    </body>
</html>

Sometimes, it feels like Plot just maps each function directly to an equivalent HTML element - at least that’s what it looks like from the code above, but actually Plot automatically inserts a lot of very valuable metadata, and we will see more of Plot’s features later.

Attributes

The application of attributes can also be exactly the same as adding child elements, just add another entry in the comma-separated list of an element. For example, here’s how to define an anchor element with both a CSS class and a URL. Attributes, elements, and inline text are all defined in the same way, which not only makes Plot’s API easier to learn, but also makes the input experience very smooth - because you can simply type . to continuously define new attributes and elements in any context.

Swift
let html = HTML(
    .body(
        .a(.class("link"), .href("https://github.com"), "GitHub")
    )
)

Type Safety

Plot extensively uses Swift’s advanced generic capabilities, which not only makes it possible to write HTML and XML using native code, but also achieves complete type safety in the process. All elements and attributes in Plot are implemented as context-bound nodes, which enforces valid HTML semantics and enables Xcode and other IDEs to provide rich autocomplete information when writing code using Plot’s DSL.

Swift
let html = HTML(.body(
    .p(.href("https://github.com"))
))

For example, <herf> cannot be directly placed in <p>, and auto-completion will not suggest it when typing .p (because the context does not match), and the code will also result in an error during compilation.

This high level of type safety not only brings a very pleasant development experience, but also greatly increases the chances of creating HTML and XML documents that are semantically correct using Plot - especially compared to writing documents and markup with raw strings.

For someone like me who is extremely lacking in HTML knowledge, I cannot even write the following erroneous code (which cannot pass) in Plot.

Swift
let html = HTML(.body)
    .ul(.p("Not allowed"))
))

Custom Components

Similarly, the Node architecture bound to the context not only gives Plot a high degree of type safety, but also allows for the definition of more and higher-level components, which can then be flexibly mixed with the elements defined by Plot itself.

For instance, let’s say we wish to integrate an advertisement component into a website, which is restricted to the <body> context of the HTML document.

Swift
extension Node where Context: HTML.BodyContext { // Strict context binding
    static func advertising(_ slogan: String,href:String) -> Self {
        .div(
            .class("advertising"),
            .a(
                .href(href),
                .text(slogan)
            )
        )
    }
}

Now, we can use the advertising just like any built-in element with the same syntax.

Swift
let html = HTML(
    .body(
        .div(
            .class("wrapper"),
            .article(
               ....
            ),
            .advertising("肘子的Swift记事本", herf: "https://fatbobman.com")
        )
    ))

Control Flow

Although Plot focuses on static site generation, it comes with several control flow mechanisms that allow you to use its DSL for inline logic. Currently supported control commands include .if( ), .if(_,else:), unwrap(), and forEach().

Swift
var books:[Book] = getbooks()
let show:Bool = true
let html = HTML(.body(
    .h2("Books"),
    .if(show,
    .ul(.forEach(books) { book in
        .li(.class("book-title"), .text(book.title))
    })
    ,else:
        .text("Please add a book to the library")
    )
))

Using the above control flow mechanisms, especially in combination with custom components, allows you to build truly flexible themes in a type-safe way, creating the desired documents and HTML pages.

Custom Elements and Attributes

Although Plot aims to cover as many standards related to the document formats it supports, you may still encounter some form of element or attribute that Plot does not yet have. We can easily customize elements and attributes in Plot, which is particularly useful when generating XML.

Swift
extension Node where Context == XML.ProductContext {
    static func name(_ name: String) -> Self {
        .element(named: "name", text: name)
    }

    static func isAvailable(_ bool: Bool) -> Self {
        .attribute(named: "available", value: String(bool))
    }
}

Document rendering

Swift
let header = Node.header(
    .h1("Title"),
    .span("Description")
)

let string = header.render()

It is also possible to control the indentation of the output.

Swift
html.render(indentedBy: .tabs(4))

Other Support

Plot also supports generating RSS feeds, podcasting, site maps, etc. The corresponding parts in Publish are also implemented by Plot.

Publish Theme

Before reading the following content, it is best to have read Creating a Blog with Publish - Getting Started.

The sample template mentioned in the article can be downloaded from GIthub.

Custom Theme

In Publish, themes need to follow the HTMLFactory protocol. The following code can define a new theme:

Swift
import Foundation
import Plot
import Publish

extension Theme {
    public static var myTheme: Self {
        Theme(
            htmlFactory: MyThemeHTMLFactory<MyWebsite>(),
            resourcePaths: ["Resources/MyTheme/styles.css"]
        )
    }
}

private struct MyThemeHTMLFactory<Site: Website>: HTMLFactory {
        // ... Specific pages, six methods need to be implemented
}

private extension Node where Context == HTML.BodyContext {
        // Definition of Nodes, such as header, footer, etc.
}

Use the following code in the pipeline to specify a theme:

Swift
.generateHTML(withTheme:.myTheme ), //Use custom theme

The HTMLFactory protocol requires us to implement all six methods, corresponding to six types of pages:

  • makeIndexHTML(for index: Index,context: PublishingContext<Site>)

Homepage of the website usually displays recent articles, popular recommendations, etc. The default theme explicitly displays all Item lists.

  • makeSectionHTML(for section: Section<Site>,context: PublishingContext<Site>)

Displays the page when Section is used as an Item container. Usually, it displays the list of Item that belong to the Section.

  • makeItemHTML(for item: Item<Site>, context: PublishingContext<Site>)

Displays the page for a single article (Item).

  • makePageHTML(for page: Page,context: PublishingContext<Site>)

Displays the page for other articles (Page). When the section is not used as a container, its index.md is also rendered as a Page.

  • makeTagListHTML(for page: TagListPage,context: PublishingContext<Site>)

Displays the page for the Tag list. Usually, it displays all the Tags that appeared in the site’s articles.

  • makeTagDetailsHTML(for page: TagDetailsPage,context: PublishingContext<Site>)

Displays the list of Items that have the particular Tag.

We can write each method in MyThemeHTMLFactory according to the Plot representation described above. For example:

Swift
func makePageHTML(for page: Page,
                 context: PublishingContext<Site>) throws -> HTML {
    HTML(
        .lang(context.site.language),
        .head(for: page, on: context.site),
        .body(
            .header(for: context, selectedSection: nil),
            .wrapper(.contentBody(page.body)),
            .footer(for: context.site)
            )
        )
    }

header, wrapper, and footer are all custom Nodes.

Generation Mechanism

Publish uses a workflow mechanism to operate data in the Pipeline. Refer to the sample code to see how the data is processed in the Pipeline.

Swift
try FatbobmanBlog().publish(
    using: [
        .installPlugin(.highlightJS()), // Add syntax highlighting plugin, which is called during markdown parsing
        .copyResources(), // Copy required website resources, files under the Resource directory
        .addMarkdownFiles(),
        /* Read markdown files one by one in Content directory, and parse:
        1: Parse metadata and save it to the corresponding Item
        2: Parse and convert markdown paragraphs in the article to HTML data one by one
        3: When encountering code blocks that highlightJS needs to process, call the plugin
        4: Save all processed content to PublishingContext
        */
        .setSctionTitle(), // Modify the displayed title of the section
        .installPlugin(.setDateFormatter()), // Set time display format for HTML output
        .installPlugin(.countTag()), // Inject to add tagCount property to tag, and calculate how many articles are under each tag
        .installPlugin(.colorfulTags(defaultClass: "tag", variantPrefix: "variant", numberOfVariants: 8)), // Inject to add colorfiedClass property to each tag, and return the corresponding color definition in the css file
        .sortItems(by: \.date, order: .descending), // Sort all articles in descending order
        .generateHTML(withTheme: .fatTheme), // Specify the custom theme and generate HTML files in the Output directory
        /*
        Use the theme template, and call the page generation method one by one.
        According to the different parameters required by each method, pass the corresponding PublishingContext, Item, Section, etc.
        Theme methods render HTML using Plot based on data.
        For example, to display the content of a page article in makePageHTML, it is obtained through page.body
        */
        .generateRSSFeed(
            including: [.posts,.project],
            itemPredicate: nil
        ), // Generate RSS using Plot
        .generateSiteMap(), // Generate Sitemap using Plot
    ]
)

From the above code, we can see that the generation of HTML using the theme template and saving it is at the end of the entire Pipeline. In general, when the theme method is called with the given data, the data is already prepared. However, since Publish’s theme is not a descriptive file but standard program code, we can still process the data again before the final render.

Although Publish currently offers only a few types of pages, we can still achieve completely different rendering results for different content using just these types. For example:

Swift
func makeSectionHTML(for section: Section<Site>,
                         context: PublishingContext<Site>) throws -> HTML {
    // If section is "posts", display a completely different page
    if section.id as! Myblog.SectionID == .posts {
            return HTML(
                postSectionList(for section: Section<Site>,
                context: PublishingContext<Site>)
            )
    }
    else {
           return HTML(
                otherSctionList(for section: Section<Site>,
                context: PublishingContext<Site>)
            )
       }
   }

We can also use Plot’s control commands to achieve the same effect. The following code is equivalent to the above:

Swift
func makeSectionHTML(for section: Section<Site>,
                         context: PublishingContext<Site>) throws -> HTML {
      HTML(
        .if(section.id as! Myblog.SectionID  == .posts,
              postSectionList(for section: Section<Site>,
                context: PublishingContext<Site>)
            ,
            else:
              otherSctionList(for section: Section<Site>,
                context: PublishingContext<Site>)
           )
        )
    }

Basically, in Publish, you can handle web pages using the mindset of writing regular programs. Themes are not just about describing files.

Collaboration with CSS

The theme code defines the basic layout and logic of the corresponding page, while more specific layout, size, color, effect, etc. are set in the CSS file. The CSS file is specified when defining the theme (there can be multiple).

If you are an experienced CSS user, there should be no difficulty. However, the author is almost completely unfamiliar with CSS, and spent the longest time and energy on it during the process of rebuilding a blog using Publish.

Please recommend a tool or VS Code plugin that can organize CSS for me. Since I have no experience in CSS, my code is messy. Is it possible to automatically adjust tags or classes of the same level or similar together for easy searching?

Practice

Next, we will experience the development process by modifying two theme methods.

Preparation

At the beginning, it is not very realistic to completely rebuild all the theme code, so I recommend starting with the default theme foundation that comes with Publish.

Complete the installation work in Creating a Blog with Publish - Getting Started.

Modify main.swift:

Swift
enum SectionID: String, WebsiteSectionID {
        // Add the sections that you want your website to contain here:
        case posts
        case about //add one for demonstrating the navigation bar above
    }
Bash
$publish run

Access http://localhost:8000, the page should look like this:

https://cdn.fatbobman.com/publis-2-defaultIndex.png

Create a MyTheme directory in the Resource folder. Copy the two files styles.css and Theme+Foundation.swift from the Publish library to the MyTheme directory in XCode. Alternatively, create new files in the MyTheme directory and paste the code.

Publish--Resources--FoundatioinTheme-- styles.css
Bash
Publish--Sources--Publish--API-- Theme+Foundation.swift

Renamed Theme+Foundation.swift to MyTheme.swift, and edited its contents as follows:

From:

Swift
private struct FoundationHTMLFactory<Site: Website>: HTMLFactory {

To:

Swift
private struct MyThemeHTMLFactory<Site: Website>: HTMLFactory {

From:

Swift
 static var foundation: Self {
        Theme(
            htmlFactory: FoundationHTMLFactory(),
            resourcePaths: ["Resources/FoundationTheme/styles.css"]
        )
 }

To:

Swift
static var myTheme: Self {
        Theme(
            htmlFactory: MyThemeHTMLFactory(),
            resourcePaths: ["Resources/MyTheme/styles.css"]
        )
}

In main.swift, from:

Swift
try Myblog().publish(withTheme: .foundation)

To:

Swift
try Myblog().publish(withTheme: .myTheme)

Just create a few .md files under the posts directory in Content. For example:

Markdown
---
date: 2021-01-30 19:58
description: 第二篇
tags: second, article
title: My second post
---

hello world
...

Preparations are complete. The page looks something like this. The makeIndexHTML method creates the current display page.

https://cdn.fatbobman.com/publish-2-defaultindex2.png

Example 1: Changing the display of Item Row in makeIndexHTML

The current code for makeIndexHTML is as follows:

Swift
func makeIndexHTML(for index: Index,
                       context: PublishingContext<Site>) throws -> HTML {
        HTML(
            .lang(context.site.language),  //<html lang="en"> language can be modified in main.swift
            .head(for: index, on: context.site), //<head> content, including title and meta
            .body(
                .header(for: context, selectedSection: nil), // Site.name and nav navigation SectionID at the top
                .wrapper(
                    .h1(.text(index.title)), // Welcome to MyBlog! corresponds to the title in Content--index.md
                    .p(
                        .class("description"),  // corresponds to .description in styels.css
                        .text(context.site.description) // corresponds to Site.description in main.swift
                    ),
                    .h2("Latest content"),
                    .itemList(  // custom Node that displays the Item list, currently used in makeIndex, makeSection, and makeTagList
                        for: context.allItems(
                            sortedBy: \.date, // sorted in descending order by metadata date of creation
                            order: .descending
                        ),
                        on: context.site
                    )
                ),
                .footer(for: context.site) // custom Node that displays the copyright information at the bottom
            )
        )
    }

Make the following modifications to makeIndexHTML:

Swift
.itemList(

To:

Swift
.indexItemList(

Afterward, add .h2("Latest content") to the code, which becomes:

Swift
       .h2("Latesht content"),
       .unwrap(context.sections.first{ $0.id as! Myblog.SectionID == .posts}){ posts in
              .a(
                  .href(posts.path),
                  .text("显示全部文章")
                 )
              },

Adding in extension Node where Context == HTML.BodyContext:

Swift
    static func indexItemList<T: Website>(for items: [Item<T>], on site: T) -> Node {
        let limit:Int = 2 //Set the maximum number of Item entries to display on the index page
        let items = items[0...min((limit - 1),items.count)]
        return .ul(
            .class("item-list"),
            .forEach(items) { item in
                .li(.article(
                    .h1(.a(
                        .href(item.path),
                        .text(item.title)
                    )),
                    .tagList(for: item, on: site),
                    .p(.text(item.description)),
                    .p(item.content.body.node) //Add the full content of the item to be displayed
                ))
            }
        )
    }

Now the Index page looks like this:

https://cdn.fatbobman.com/publish-2-index-finish.png

Example 2: Adding navigation for recently articles to makeItemHTML

In this example, we will add article navigation functionality to makeItemHTML, similar to the following effect:

https://cdn.fatbobman.com/publish-2-item-navigatore-demo.png

Click to enter any item (article)

Swift
     func makeItemHTML (for item: Item <Site>,
                      context: PublishingContext <Site>)throws-> HTML {
         HTML(
             .lang(context.site.language),
             .head(for: item,on: context.site),
             .body(
                 .class(“item-page”),
                 .header(for: context,selectedSection: item.sectionID),
                 .wrapper(
                     .article( //<article> tag
                         .div(
                             .class(“content”), //css .content
                             .contentBody(item.body) //.raw(body.html) display item.body.html article text
                         ),
                         .span(“Tagged with:”),
                         .tagList(for: item,on: context.site) //tag list below, forEach(item.tags)

                 ),
                 .footer(for: context.site)


     }

Add the following content before the HTML ( in the code:

Swift
         var previous:Item <Site>= nil //previous item
         var next:Item <Site>= nil //next item

         let items = context.allItems (sortedBy: \.date,order:.descending) //get all items
         /*
         We are currently getting all items, and the scope can be limited when getting, for example:
         let items = context.allItems(sortedBy: \.date,order: .descending)
                           .filter{$0.tags.contains(Tag("article"))}
         */
         //index of current item
         guard let index = items.firstIndex (where: {$0 == item})else {
             return HTML()
         }

         if index> 0 {
             previous = items [index-1]
         }

         if index <(items.count-1) {
             next = items [index + 1]
         }

         return HTML (
           ....

Add before .footer

Swift
.itemNavigator(previousItem:previous,nextItem:next),
.footer(for: context.site)

Add a custom node itemNavigator in extension Node where Context == HTML.BodyContext.

Swift
   static func itemNavigator<Site: Website>(previousItem: Item<Site>?, nextItem: Item<Site>?) -> Node{
        return
            .div(
                .class("item-navigator"),
                .table(
                    .tr(
                        .td(
                            .unwrap(previousItem){ item in
                                .a(
                                    .href(item.path),
                                    .text(item.title)
                                )
                            }
                        ),
                        .td(
                            .unwrap(nextItem){ item in
                                .a(
                                    .href(item.path),
                                    .text(item.title)
                                )
                            }
                        )
                    )
                )
            )
    }

Add in styles.css

CSS
.item-navigator table{
    width:100%;
}

.item-navigator td{
    width:50%;
}

The above code is for demonstration purposes only. The result is as follows:

https://cdn.fatbobman.com/publish-2-makeitem-with-navigator.png

Summary

If you have experience with SwiftUI development, you will find that the usage is very similar. In the Publish theme, you have ample means to organize, process data, and layout views (treating Node as View).

The FoundationHTMLFactory of Publish currently only defines six page types. There are two ways to add new types:

  1. Fork Publish and directly extend its code.

This method is the most thorough, but it is more difficult to maintain.

  1. After executing .generateHTML in the pipeline, execute a custom generate step.

There is no need to modify the core code. There may be redundant actions, and we need to do some processing in the built-in methods of FoundationHTMLFactory to connect with our newly defined pages. For example, currently index and section list do not support pagination (only outputting one HTML file), we can regenerate a set of paginated index after the built-in makeIndex, and overwrite the original.

In this article, we introduced how to use Plot and customize your own theme in Publish. In the next article, we will explore how to add various functions without modifying the core code of Publish (not just plugins).

Weekly Swift & SwiftUI insights, delivered every Monday night. Join developers worldwide.
Easy unsubscribe, zero spam guaranteed