This post shares 8 tips and tricks to help you publish and maintain your Publish generated site using GitHub Pages.
Installing Publish
Publish comes with command line tools to build, test locally, and deploy your generated site.
$ git clone https://github.com/JohnSundell/Publish.git
$ cd Publish
$ make
$ which publish
/usr/local/bin/publish
The publish
command line tool provides the following commands.
new
: set up a new website in the current foldergenerate
: generate the website in the current folderrun
: generate and run a localhost server on default port 8000deploy
: generate and deploy the website in the current folder
Tip #1: Prepare for deploying to GitHub Pages
Every GitHub account can host a site directly from a GitHub repo using GitHub Pages. To get started, create a directory that matches this pattern: <github-account-name>.github.io
.
For example, my GitHub account name is briancoyner
, so my GitHub Pages repo name is briancoyner.github.io
. This means that you can access my site at https://briancoyner.github.io.
$ mkdir <github-account-name>.github.io
$ cd <github-account-name>.github.io
Next, initialize the directory with a git repo, and create a new branch named author
.
$ git init
$ (master) git checkout -b author
$ (author)
The author
branch is where your site's Swift source files, markdown files and other resources live. The author
branch name is completely arbitrary. You can call it mickeymouse
if desired.
The master
branch is where your GitHub Pages generated content lives.
Now let's create a new Publish project and test it locally.
$ (author) publish new
# Creates a skeleton project using the default Publish theme.
$ (author) publish generate
# Executes the Publish pipeline to generate the contents of your static site.
# Generated files are placed in the `Output` directory.
$ (author) publish run
# Starts a Python web service that hosts the contents of the `Output` directory
# open http://localhost:8000
Tip #2: Debugging using Xcode
The publish new
command generates a Package.swift
file, along with a bunch of other files. If you're on macOS and have Xcode installed, then you can quickly open the project in Xcode.
$ (author) open Package.swift
Xcode will open and immediately begin resolving all package dependencies.
You can now start debugging though the Publish source to learn more about how it works. For starters, open the main.swift
file, set a breakpoint on the publish
function near the bottom of the file, type Command + R
, and start stepping through the code.
Tip #3: Building a navigation menu
Most blog sites have a navigation menu containing a "Posts" page and various other standalone pages, such as an "About" page. Publish makes this really easy. The trick is to understand how Publish finds your blog posts and standalone pages.
Let's say you want three sections in your navigation menu:
- Articles (an archive listing of all your posts)
- Your Cool App (a standalone page describing one of your cool apps)
- About (a standalone page about you)
Open the main.swift
file and update the MyBlog.SectionID
enum to include three cases (i.e. named sections).
// main.swift
struct MyBlog: Website {
enum SectionID: String, WebsiteSectionID {
// Add the sections that you want your website to contain here:
case articles
case yourcoolapp
case about
}
. . .
}
By default Publish starts looking for your site's content in the Content
directory. Publish uses a Publish.PublishingStep
named addMarkDownFiles
to discover your site's markdown files. To help Publish distinguish between Publish.Item
s (i.e. blog posts) and Publish.Page
s (i.e. standalone pages), the Content
directory structure looks like this:
+ briancoyner.github.io/
+ Content/
+ articles
- awesome_post_1.md
- awesome_post_2.md
- awesome_post_N.md
- about.md
- yourcoolapp.md
After generating your site, the Output
directory looks like this:
+ briancoyner.github.io/
+ Content/
+ Output/
+ about/
- index.html
+ articles/
+ awesome_post_1
- index.html
+ awesome_post_2
- index.html
+ awesome_post_N
- index.html
+ yourcoolapp/
- index.html
- index.html
Each Item
(or post) is placed in directory named after the markdown file. The contents of the item's markdown file are placed in an index.html
page. Each Page
is placed in a directory named after the markdown file. The contents of the page's markdown file are placed into an index.html
page.
Tip #4: Adding metadata to your blog posts
To keep things "simple", associate your blog post markdown files with a single Publish.WebsiteSectionID
case, which for the example site is named articles
. This means that all of your blog post markdown files are placed under the Content/articles
directory.
You can name the markdown files anything you want. A common technique is to append a date to the filename.
yyyy-mm-dd-name-of-the-post.md
Each blog post may contain the following markdown metadata contained between the ---
lines.
---
title: Name of your post
date: 2020-02-07 00:00
description: Short description of your post
---
Your content starts here.
There are a few other properties you can set in the metadata. See Publish.ContentProtocol
for details.
Tip #5: Building a custom blog post list
Let's say you want to build a blog listing that includes the post date, title, and description. Start off by adding a function to Plot.Node
that takes in an ordered collection of Publish.Item
s (i.e. posts). Each Publish.Item
contains the post date, title, and description, which are the values set in the metadata section via the Publish.ContentProtocol
.
Create a file named Node+ArticleList.swift
and paste the following code.
import Foundation
import Publish
import Plot
extension Node where Context == HTML.BodyContext {
static func articleList<T: Website>(for items: [Item<T>], on site: T) -> Node {
return .ul(.class("item-list"),
.forEach(items) { item in
.li(
.div(.class("article-date"),
.text(DateFormatter.localizedString(from: item.date, dateStyle: .medium, timeStyle: .none))
),
.div(.class("article-title"),
.a(
.href(item.path),
.text(item.title)
)
),
.div(.class("article-excerpt"),
.text(item.description)
)
)
}
)
}
}
Next, locate the function named makeSectionHTML(for:context:)
in your HTMLFactory
implementation. The makeSectionHTML(for:context:)
is declared in the Publish.HTMLFactory
factory protocol, and is used to generate your site's blog post listing page.
The HTML structure above is styled using custom CSS. You can use your favorite web inspector tool to dig into any site's CSS for inspiration.
func makeSectionHTML(
for section: Section<Site>,
context: PublishingContext<Site>
) throws -> HTML {
return HTML(
.lang(context.site.language),
.head(for: section, on: context.site),
.body(
.header(for: context, selectedSectionID: section.id),
.wrapper(
.h1(.text(section.title)),
// Here is the call to the function you defined above.
.articleList(
for: context.allItems(sortedBy: \.date, order: .descending),
on: context.site
)
)
),
.footer(for: context.site)
)
}
The PublishingContext
parameter provides access to your blog post Item
s. You'll notice that the items are sorted by date
in descending order. The cool thing is you can build your posts page any way you want. For example, you could group all of your posts by year.
Tip #6: Using syntax highlighters
Publish integrates with a Swift syntax highlighter called Splash. Using it is super-easy.
Add the dependency to your Package.swift
file.
.package(url: "https://github.com/johnsundell/splashpublishplugin", from: "0.1.0")
Here is my project's Package.swift
file
let package = Package(
name: "briancoyner.github.io",
products: [
.executable(name: "briancoyner.github.io", targets: ["briancoyner.github.io"])
],
dependencies: [
.package(url: "https://github.com/johnsundell/publish.git", from: "0.3.0"),
.package(url: "https://github.com/johnsundell/splashpublishplugin", from: "0.1.0")
],
targets: [
.target(
name: "briancoyner.github.io",
dependencies: [
"Publish",
"SplashPublishPlugin"
]
)
]
)
Publish allows developers to inject custom steps into a publish pipeline. Here's how to install the Splash plugin into your custom pipeline.
.installPlugin(.splash(withClassPrefix: "")),
Go ahead and add the plugin as the first step.
try MyBlog().publish(using: [
.installPlugin(.splash(withClassPrefix: "")),
. . .
Finally, add CSS to your stylesheet.
You can also integrate Pygments, a popular Python syntax highlighter. You can read more here.
Tip #7: Deploying to Github Pages
By now you have spent hours preparing your site's content and hacking on CSS. You are now ready to publish your site to GitHub.
In your main.swift
file, add a deploy(using:useSSH:)
step to your publish pipeline.
The deploy
step performs the following:
- commits the generated site content in the
Output
directory to themaster
branch - pushes those changes to GitHub.
.deploy(using: .gitHub("<github-account-name>/<github-account-name>.github.io", useSSH: false))
try MyBlog().publish(using: [
. . .
.deploy(using: .gitHub("briancoyner/briancoyner.github.io", useSSH: false))
])
Now you're ready to push your content changes to the author
branch and push the generated content to the master
branch.
$ (author) git add .
$ (author) git commit -m "added new post about something"
$ (author) git push origin author
$ (author) publish deploy
All changes are now committed to your GitHub repo. In just a few minutes, GitHub Pages will deploy your updates to the world.
You can check on the status of the deployment by visiting the "Environments" area of your GitHub repo.
The URL looks like this: https://github.com/<github-account-name>/<github-account-name>.github.io/deployments
Tip #8: Working with drafts
Let's say you want to work on a new article that is going to take a few days or weeks to complete. During this time, though, you may need to publish a few smaller posts or make changes to a standalone page. This is super easy because our project is backed by git.
Recall that we are using two branches: master
and author
.
- The
master
branch contains the generated content exposed to GitHub Pages. - The
author
branch is where your site's Swift source files, markdown files and other resources live. Remember the name is arbitrary.
You may have already guessed that working with drafts is as simple as creating "feature branches". Let's see how this works.
$ (author) git checkout -b draft/my_great_new_post
$ (draft/my_great_new_post) open Package.swift
Now add a new markdown file for your post and any other resources such as images or videos.
$ (draft/my_great_new_post) git add .
$ (draft/my_great_new_post) git commit -m "initial work"
$ (draft/my_great_new_post) git push -u origin my_great_new_post
As the days unfold you continue working on your draft and commiting the changes to GitHub.
$ (draft/my_great_new_post) git add .
$ (draft/my_great_new_post) git commit -m "finalize draft; ready for publishing"
$ (draft/my_great_new_post) git push origin my_great_new_post
Now you're ready to publish your new post. Merge the draft branch into author
and test locally to make sure all is well.
$ (draft/my_great_new_post) git checkout author
$ (author) git merge --no-ff my_great_new_post
$ (author) publish run
# verify everything looks good
All is well, so deploy.
$ (author) publish deploy
Don't forget to clean up the draft branch.
$ (author) branch -d my_great_new_post
$ (author) branch push origin :my_great_new_post
Summary
I have really enjoyed hacking around with Publish. Many thanks to John Sundell for creating Publish. You have allowed me, and hopefully many others, to move away from Ruby-based site generators (viz. Jekyll).
Besure to checkout the official Publish documentation.