Building a blog: Publish
March 16, 2021In 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:
- It's run on client-side meaning I would have needed a backend server to deliver the articles content.
- 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 homepagemakeSectionHTML
to create a page listing content of the given sectionmakeItemHTML
generate HTML page for an item (one markdown file in one section). In my blog that's where I generate an article detail pagemakeTagDetailsHTML
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!