8 Tips To Help You Get Started With Publish

February 25, 2020

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 folder
  • generate: generate the website in the current folder
  • run: generate and run a localhost server on default port 8000
  • deploy: 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.Items (i.e. blog posts) and Publish.Pages (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.Items (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 Items. 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 the master 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.