Publish is a static site generator designed specifically for Swift developers. It uses Swift to build the entire website and supports themes, plugins, and a host of customization options.
Introduction
Developer John Sundell
John Sundell, the developer behind Publish, has been dedicated to producing high-quality articles, podcasts, and videos about Swift over the years. Most of his work is published on his independently operated Swift by Sundell. He developed Publish to create and manage his own site.
During the development of Publish, he also open-sourced a number of fundamental libraries, such as Ink (an efficient Markdown parser), Plot (a DSL for creating HTML, XML, RSS), Sweep (an efficient string scanning library), Codextended (enhancements for Codable), etc. These not only together built a powerful Publish but are also outstanding open-source libraries in their respective fields.
Why I Use Publish
A year ago, when I revived my blog, I used Hexo
. Hexo has a solid user base in China, with many excellent tutorials online and many developers contributing their themes and plugins. Despite being quite satisfied with Hexo, as I mainly use Swift and am not very familiar with JavaScript, it was difficult for me to make deeper customizations or modifications to Hexo.
As a developer (even an amateur one), I always wanted to have more comprehensive control over my projects, so Publish, being fully developed in Swift, became my first choice.
As I delved deeper into the process of rebuilding Elbow’s Swift Notebook using Publish, I felt I had made the right choice. Publish allowed me to use the mindset and logic of developing a regular app to create a site and efficiently achieve the various customizations and changes I wanted.
The Reason for Writing This Article
At the time of writing, Publish has already garnered 3.1 K stars on Github. However, there aren’t many introductions to Publish on the internet, especially lacking information and exchanges about template customization and plugin development. The number of results for related plugins and templates on Github is also very limited.
This situation is partly due to Publish being relatively new, having a smaller user base, and the Swift community being smaller. However, I believe the following situation has also exacerbated this: unlike other static site generators, in Publish, developers can implement various functions with small pieces of code. This fragmented code is actually not conducive to sharing and not easy to search; also, as the implementation of templates and website functions in Publish is deeply bound, the utility of sharing templates alone is low.
But it is exactly these characteristics that make Publish appealing.
In light of this, I will introduce Publish in three articles (Getting Started, How to Write Templates, How to Write Plugins) and hope that more domestic Swift developers or enthusiasts can learn about and use this excellent tool.
To help everyone get started quickly, I have placed the code used for this site (including templates, custom plugins, etc.) on Github for easy understanding and mastery of Publish through the code.
Quick Start Guide
How to Install Publish
Like many other static site generators, Publish provides a CLI. You can quickly complete a series of operations such as creating templates, updating content, and remote publishing via the command line. Publish currently runs on Mac and Linux, and given its low dependency on operating systems, it is expected to appear on the Windows platform later.
Installing via brew on Mac
$brew install publish
Installing from Source Code
$git clone https://github.com/JohnSundell/Publish.git
$cd Publish
$make
Creating Your First Project
Let’s create a new Blog project
$mkdir myblog
$cd myblog
$publish new
Publish will create the required project template in the myblog directory. Its basic composition is roughly as follows:
|-- myblog
| |-- Content
| |–– posts
| |–– first-post.md
| |–– index.md
| |–– index.md
| |-- Resources
| |-- Sources
| |–– Myblog
| |–– main.swift
-
Content
Put the markdown files of articles, pages, etc., that you want to publish on the website here.
-
Resources
Some resources needed for the project theme
, such as CSS, images, etc., are currently empty. After your first publication, you will see it contains the default FoundationTheme’s styles.css file.
-
Source
The code describing the site. In
main.swift
, the basic properties of the site and the creation workflow are defined.
Compile and Run
Swift is a compiled language, so your site’s code needs to be compiled and run on your machine after each change to complete content generation, but all this only requires one command.
Let’s let Publish do the above work and start the built-in Web Server for us to browse the newly created project.
$publish run
On the first run, Publish will automatically fetch the required libraries from Github, so please wait a few minutes.
$ publish run
............
Publishing Myblog (6 steps)
[1/6] Copy 'Resources' files
[2/6] Add Markdown files from 'Content' folder
[3/6] Sort items
[4/6] Generate HTML
[5/6] Generate RSS feed
[6/6] Generate site map
✅ Successfully published Myblog
🌍 Starting web server at http://localhost:8000
Press ENTER to stop the server and exit
Now you can access your new site by visiting http://localhost:8000 in your browser.
All content of the website is generated and placed in the Output
directory. You just need to upload its contents to your server, or through simple configuration, like:
.unwrap(.gitHub("fatbobman/fatbobman.github.io", useSSH: true), PublishingStep.deploy)
Then using
$publish deploy
you can publish the content to your github.io (specific configuration explained later).
At this point, you add the following file second-post.md
in Content
- posts
---
date: 2021-01-29 19:58
description: My second article
tags: publish,swift
---
# Hello World
Executing publish run
again, you can see the new article has already appeared on the page.
Quickly Get Started Using My Provided Template
First, make sure the Publish CLI is installed
$git clone https://github.com/fatbobman/PublishThemeForFatbobmanBlog.git
$cd PublishThemeForFatbobmanBlog
$publish run
More About Publish
This section introduces several concepts in Publish, crucial for understanding template customization and feature expansion.
Site
When you create a project with Publish, it automatically generates a Swift Package
. The website’s generation and deployment configuration are completed through this package using native and type-safe Swift code.
Below is the content of main.swift
(the entry file of the package) generated using publish new myblog
.
// Definition of Site
struct Myblog: Website {
enum SectionID: String, WebsiteSectionID {
// Add your required Sections
case posts
}
struct ItemMetadata: WebsiteItemMetadata {
// Add any specific site metadata you want to use here
}
// Some configurations of your website
var url = URL(string: "https://your-website-url.com")!
var name = "Myblog"
var description = "A description of Myblog"
var language: Language { .english }
var imagePath: Path? { nil }
}
// You can access the properties in the Site from templates or plugins
// Entry point for execution. Currently using the default template and Publish's preset generation, export, and publishing process.
// Definition of the workflow, more details in Step
try Myblog().publish(withTheme: .foundation)
Site
not only defines the basic configuration information of the website project but also defines the workflow from generation to publication.
Section
Each Section creates a subdirectory in Output
. In main.swift, Section
is defined using enumeration. You can consider a Section as a container of a group of Items
(articles) or merely pointing to a Page
(a non-Item specific page). To use a Section
to manage a group of articles, you just need to create a subdirectory under Content
with the same name as that Section
, as seen in the example with posts
and project
.
Definition of Section
:
enum SectionID: String, WebsiteSectionID {
case posts // New projects only have this by default, corresponding to the content-posts directory
case links // You can add your own, place the articles belonging to this section in the corresponding directory
case about
}
In Elbow’s Swift Notebook, each Section also corresponds to an option in the upper navigation area. Section
can have various uses, which will be discussed more in the template customization chapter.
Item
Articles saved in Content--corresponding Section
directory. Each Item
corresponds to a Section
without special settings; it belongs to whichever Section
directory it is saved in. If a Section
does not need to act as a file container, you can directly create an md file with the same name as the Section
in Content
. In the example template I provided, about
is of this form.
Page
Articles that do not belong to any Section
. Page
will not appear in the item
list of Section
and usually not in the index (homepage) list either. Create a Page
by adding files in the following structure in a directory under content
that does not belong to any Section
. Note the relationship between the creation path and access path of Page
.
| content -- 404
| | -- index.md
You can view it by visiting http://localhost:8000/404/index/
Page provides us with a way to build free-form pages. For example, you can use it to create articles that don’t need to be displayed in the list, or as demonstrated in the example template, create a 404 page😅.
Content
Specifically refers to the content
attribute in Item
and Page
. As a content set, its range includes text (such as titles and descriptions), tags, converted HTML code, audio, video, and various metadata. Metadata needs to be indicated at the top of the Markdown article.
Section
also has Content
, corresponding to the index.md you create in the Content
subdirectory for that Section
(if necessary).
In the code, you will also encounter another type of Content
, precisely PublishingContext
. It contains all the information of the entire project (Site, Sections, Items, Tags, etc.), and by passing its instance to Step
or Plugin
, various tasks like modifying or configuring the website can be completed.
Metadata
Metadata of the Markdown file, marked at the top of the article (Markdown) file. It is divided into two categories: one is preset by Publish, and the other is custom-defined in ItemMetadata
in Site
.
---
date: 2021-01-29 19:58
description: A description of my first post.
tags: first, article
author:
fat
image: https://cdn.fatbobman.com/first-post.png
---
Preset
- title: Text title. If not set, Publish will directly find the first Top-level Head
#
in the article text as the title. - description: Article description.
- date: Article creation date. If not set, the file’s modificationDate is used directly.
- tags: Article tags, each article can have multiple tags, adding another dimension to the organization of articles.
- image: Image address, e.g., for displaying a theme image of the article in the Item list (needs to be defined in the template).
- audio: Audio data.
- video: Video data. The definition of audio and video is too complex; if really needed, it can be defined by yourself.
Custom
struct ItemMetadata: WebsiteItemMetadata {
// Add any site-specific metadata that you want to use here.
var author:String
}
If the preset metadata
is not enough for your needs, you can define it yourself in ItemMetadata
.
Difference Between Two Types of Metadata
Preset metadata
exists as properties in Publish.
for item in content.allItems(sortedBy: \.date){
print(item.title)
}
Custom metadata
must be accessed as follows:
let author = (item.metadata as! Myblog.ItemMetadata).author
It’s more convenient to use in the template:
.text(item.metadata.author)
In Publish, while preset metadata
is not mandatory for Item
, custom metadata
must be added in the markdown document. index.md
and Page
can be without metadata
(whether custom or not).
Theme
Publish uses Plot as its HTML theme description engine, allowing developers to define pages in a very Swift-like way. If you have used DSL-type development methods, it will feel very familiar. For example, the code below defines the layout presentation of the Section List.
func makeSectionHTML(
for section: Section<Site>,
context: PublishingContext<Site>
) throws -> HTML {
HTML(
.lang(context.site.language), // Web page language
.head(for: section, on: context.site), // Header files, metadata, CSS, etc.
.body(
.header(for: context, selectedSection: section.id), // The upper area of the site, including Logo and navigation bar in the example
.wrapper(
.itemList(for: section.items, on: context.site) // Article list
),
.footer(for: context.site) // Bottom copyright area
)
)
}
The layout details defined in the Theme still need further settings in CSS.
In the code above, header
, wrapper
, etc., are referred to as Node
in Plot. Besides using a large number of Node
preset by Publish, we can use our own written Node
.
More about the Theme will be detailed in Creating a Blog with Publish (Part II).
Step
Publish uses a workflow (Pipeline) approach to clearly define the operation process of each stage, from file reading, markdown parsing, HTML generation, RSS export, etc. In main.swift
generated by publish new
, although only one statement is used,
try Myblog().publish(withTheme: .foundation)
it corresponds to the following series of operation steps behind it:
using: [
.group(plugins.map(PublishingStep.installPlugin)),
.optional(.copyResources()),
.addMarkdownFiles(),
.sortItems(by: \.date, order: .descending),
.group(additionalSteps),
.generateHTML(withTheme: theme, indentation: indentation),
.unwrap(rssFeedConfig) { config in
.generateRSSFeed(
including: rssFeedSections,
config: config
)
},
.generateSiteMap(indentedBy: indentation),
.unwrap(deploymentMethod, PublishingStep.deploy)
]
In most cases, we will explicitly mark each operation step. Each step in Publish is called a Step
. Publish has already preset many Steps
for developers to use. We can also inject our own created Step
into an appropriate position in the workflow to achieve more functions.
Each Step
will be passed a PublishContent
instance, which can be used to change various elements in the website (including files, folders, Items, Pages, etc.). For the difference between PublishContent
and Content
, see above.
Plugins
.installPlugin(.highlightJS()), // Syntax highlighting
The code above in the Publish workflow uses a Step
named installPlugin
to install the highlightJS
plugin.
Developing a Plugin
is very similar to a Step
in Publish, both receiving an instance of PublishContent
to perform relevant tasks.
For example, you can use a Step
for operations with side effects, and a Plugin
for tasks like injecting Modifier
(for custom Markdown parsing).
From a functionality standpoint, both custom code in Step
and Plugin
can accomplish each other’s tasks. Therefore, the choice between using a Step
or a Plugin
when creating a feature extension depends on personal preference.
Detailed explanations on customizing Step
and Plugin
will be provided in “Creating a Blog with Publish (Part III)“.
Who is Publish For?
Publish, compared to mainstream static site generators, has some shortcomings, such as lower community activity, shorter development time, and a smaller user base of the Swift language. Currently, Publish is more suitable for:
- Developers or enthusiasts using the Swift language.
- Those lacking experience in Javascript, or who prefer a Javascript-free style.
- Those seeking efficient and concise web presentation.
- Those who want to fully grasp each aspect of their website and implement features step by step by themselves.
- Early adopters and experimenters.
Next
In “Creating a blog with Publish: Theme” I will explore the development of Themes, and in “Part III,” learn how to extend Publish’s functionality through various means.
If you’re already interested, set up your github.io site on Github and deploy your own blog with Publish’s one-click deployment.