Migrated to Publish
Today, I'm so glad to share that this website has been migrated to Publish, a static site generator by John Sundell written in Swift! 🎉
I love working in Swift so this was a no-brainer for me and for many others it seems. This website is hosted on GitHub Pages and previously you were almost forced to adopt something like Jekyll, a static site generator written in Ruby. Well, those days are over! I want to take this opportunity to document some learnings and future enhancements.
Getting setup
So how easy was it to migrate this incredibly small website to Publish?
If you have previous Swift knowledge, this will be a breeze to setup. John Sundell really deserves a ton of credit here for designing such a simple yet extensible API.
So lets go through the minimum amount of steps you need to complete in order to generate your fancy new website.
1. Define your fancy new website
This is done by conforming to the Website
protocol.
struct FancyNewSite: Website {
enum SectionID: String, WebsiteSectionID {
// Add the sections that you want your website to contain here:
case posts
case about
}
struct ItemMetadata: WebsiteItemMetadata {
// Add any site-specific metadata that you want to use here.
}
// Update these properties to configure your website:
var url = URL(string: "https://fancynewsite.github.io")!
var name = "A Fancy New Site"
var description = "Let's get fancy, shall we?"
var language: Language { .english }
var imagePath: Path? { nil }
}
2. Create a fancy theme
This is how your website's HTML and CSS will be generated.
extension Theme where Site == FancyNewSite {
static var fancyTheme Self {
Theme(htmlFactory: FancyHTMLFactory(), resourcePaths: ["Resources/fancyTheme/styles.css"])
}
}
FancyHTMLFactory
conforms to the HTMLFactory
protocol. This is the type that will generate your website's HTML. Implement all the required methods and your site will be up and running in no time. Publish includes a default foundation
theme that you can use as a reference.
3. Define your fancy publishing pipeline
There's two ways you can go here. We can get all fancy (pun intended) and use a custom pipeline or go with the default one that is provided out-of-the-box. I went with the default one for now since that has the advantage of being much quicker to setup.
// a default pipeline that generates your fancy site and deploys it to GitHub Pages
try FancyNewSite().publish(withTheme: .fancyTheme,
deployedUsing: .gitHub("fancyNewSite/fancynewsite.github.io", useSSH: true))
4. Run locally then deploy
Publish comes with a command line tool which provides the ability to run your website locally. Just execute the following in your terminal: publish run -p 8000
.
Once you've iterated on your website locally it's time to share your hard work with the outside world. Again this is extremely simple - just run publish deploy
.
That's it! You now have the excuse to get all fancy! 😅
Issues encountered
Alright putting all the fanciness aside, let's talk about some of the issues I encountered along the way.
Local HTTP server blocks ports
Every now and again the HTTP server can develop a glitch and stop working. You'll get a message indicating the port you're trying to use is already taken. Not sure how this ends up happening but it can get a bit annoying as you're forced to choose a different port each time. Since the server used by Publish is written in Python, I tried killing any Python process that was running on my machine but unfortunately that didn't remedy the problem.
However, really the problem was just with my original workflow. Every time I would regenerate my site, I would just restart the server thus increasing the likelihood of running into this problem. A better approach is to keep the HTTP server running and just use another shell to execute publish generate
to regenerate your website without having to constantly restart your server.
Swift Package Manager
Publish is just a Swift package and so is the website project that gets generated after you run publish new
. I have experience with other dependency managers like Carthage, CocoaPods and even NPM but I'm pretty new to Swift Package Manager (SPM).
In order to add Swift syntax highlighting to my posts, I decided to adopt Sundell's Publish plugin for Splash. However, adding SplashPublishPlugin
as a dependency in my website's Package.swift, broke my build. I couldn't generate my website anymore as Xcode would no longer detect that my website had any active targets.
I was stumped. I had no idea how Xcode got into this state. So, I went for a swim in the world of SPM by reading Apple's official docs and even Sundell's article on the subject. An hour went by but I couldn't solve the problem.
My intuition indicated that I somehow needed to reset my build and throw out any existing build artifacts to have a "fresh start". I just didn't know how though. Ultimately, spending a bit of time snooping around Xcode revealed the solution.
File ˃ Swift Packages ˃ Reset Package Caches
This forces Xcode to regenerate your entire package dependency chain. Once I chose this option all my problems were solved and I could generate my website once again. I was back on track.
Section vs. Page
Section
essentially models a directory with a rigid hierarchy. Item
models each page within this directory and a given Section
can contain many items.
Page
, on the other hand, models a standalone HTML page and doesn't hold any pre-conceived notions around directory hierarchy.
Use Section
when you want to group a set of pages together. If something is standalone then use Page
instead.
WebsiteItemMetadata
WebsiteItemMetadata
is a protocol that basically gives you the ability to include custom metadata for each Item
defined by your website. Publish takes advantage of Swift's type system and allows us to work with a strongly typed struct instead of using something like a dictionary, which cannot provide the same compile-time guarantees.
Let's go through an example to solidify our understanding. Say I want to add a timestamp that indicates the last time each post on my website was edited. Posts on my website are embedded within a Section
called posts
and thus each post is an Item
.
All I have to do now is to add the following to my metadata struct:
struct ItemMetadata: WebsiteItemMetadata {
var lastEdited: Date
}
Now I can go through each one of my posts written in markdown and add this information within the --- lines at the top.
---
lastEdited: 2020-10-21 16:49
---
Lastly, I can simply access this new property in Swift via item.lastEdited
. If somehow you don't update each post, Publish will throw an error when the site is regenerated:
Fatal error: Error raised at top level: Publish encountered an error:
[step] Add Markdown files from 'Content' folder
[path] posts/fancy-new-post.md
[info] Missing metadata value for key 'lastEdited'
Consider a website with hundred of posts. It would be easy to make mistakes and miss updating a post here or there. This type of rigid enforcement at generation time prevents such mistakes and doesn't let you move forward with publishing until you fix them. I think that's incredibly useful.
Diffing the generated HTML
The really nice part of Publish is that, if you commit your Output directory to Git, you can track exactly how your website's HTML has changed every time you regenerate it. This is a really nice way to catch regressions. The only issue is that the generated HTML is minified so understanding the diff becomes next to impossible.
If only there was a way to prevent this minification while you're developing? 🤔 Maybe there is but I haven't found it yet. I'll keep digging and report back if I find something!
Deploying to GitHub Pages
GitHub pages expects the content of your website to be situated in the root directory of your repository. However, by default, Publish will situate your generated website in Output/
. Brian Coyner's article on Publish came up with a great solution that I've adopted. The gist of the solution is as follows:
- Create a new branch called
author
or whatever suits your fancy - Push this branch to remote and make this your base branch on GitHub so that all new PRs are opened against this branch by default
author
becomes your main branch where the code for your Publish website package livesmaster
becomes your deployment branch which only holds the generated HTML & CSS for your website- Instead of changing
master
directly, deploy new changes tomaster
by usingpublish deploy
(assuming that your publishing pipeline is already setup to deploy to GitHub)
Future enhancements
Getting the initial version of my website up and running has been so much fun! However, there's at least a couple more things I have in mind that would really streamline my process:
- for every new post, add the ability to automatically cross-publish the post on Medium and submit a tweet
- add the ability to work on drafts without publishing them to the website before they're ready
- a custom 404 page
- support for multiple CSS files that get minified into one CSS file
As I make progress on these enhancements, I hope to share updates. Thank you to all that stuck around till the end. All feedback is welcome, so please don't hesitate to reach out to me on Twitter. Now let's go build some websites! 🚀