Build your website using Publish
Publish is a static site generator built specifically for Swift developers. It enables entire websites to be built using Swift, and supports themes, plugins and tons of other powerful customization options.
It's an open-source framework developed by John Sundell and powers his website
Publish relies on Plot
and Ink
to make the experience as Swift-y as possible. The former is a domain-specific language (DSL) for writing type-safe HTML, XML and RSS in Swift, whereas the latter is a markdown parser that enabled creating new posts (like this one) by simply adding a markdown file in the correct folder.
Getting Started
The fastest way to work with publish is by using its command-line tool (CLI). To install the CLI clone the repo, navigate inside of it and run make
.
$ git clone https://github.com/JohnSundell/Publish.git
$ cd Publish
$ make
Now, just create an empty folder for your website somewhere on your computer, navigate to that empty folder and run Publish new
.
$ mkdir MyWebsite
$ cd MyWebsite
$ publish new
Finally, run open Package.swift to open up the project in Xcode to start building your new website.
Project Structure
When you open your project for the first time, you will have a structure similar to this:
When you run your project for the first time, a folder named Output will be created. Inside of that folder you can find the files of the static website.
- The project is built using Swift Package Manager and
Package.swift
contains the basic structure to includePublish
in the project. - Content is the folder where the
.md
files go. Each folder name should match aSectionID
in your website (more on that later). - Resources is where the images,
CSS
files and things of that nature would go. main.swift
is the main file where the root code of the website exists. There you can customize the skeleton of your website.
Publish
uses a theme when creating the website. This helps with keeping everything neat and aligned. By default, Publish
ships with a default theme named .foundation
this is the theme John uses for his website.
Deeper look at Main.swift
The default structure of main.swift
is the following.
// This type acts as the configuration for your website.
struct MyWebsite: Website {
enum SectionID: String, WebsiteSectionID {
// Add the sections that you want your website to contain here:
case posts
}
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://your-website-url.com")!
var name = "MyWebsite"
var description = "A description of MyWebsite"
var language: Language { .english }
var imagePath: Path? { nil }
}
// This will generate your website using the built-in Foundation theme:
try MyWebsite().publish(withTheme: .foundation)
-
Each
SectionID
will generate a section in the website. The raw value of theSectionID
case corresponds to the path value of that section in theURL
. -
ItemMetadata
is a list of custom metadata to include and parse in.md
files. -
name
is the title to display when the websiteURL
is shared, or in the tab bar or the web browser. Articles will automatically have the title of the article in addition to the name set here. -
description
is the description displayed when theURL
is shared. -
language
will automatically set thelang =
in thehtml
tag. -
imagePath
is the image associated with the shared link of the website. -
try MyWebsite().publish(withTheme: .foundation)
will generate your website using the built-in Foundation theme. the.publish
function takes in more customization parameters for more flexibility.
Creating a custom theme
Foundation is ok. However, you'd would want your website to have its own feel and look. That's where creating your own theme comes into place.
A theme requires two parameters to be created: an htmlFactory
and a resourcePath
. The latter is self explanatory. It basically requires an array of strings pointing to the relative paths of the resources to include (CSS
files).
The htmlFactory
on the other hand is slightly more complicated. It should be an instance of an object (ideally a Struct
) that conforms to HtmlFactory
and implements the needed methods.
-
func makeIndexHTML(index: Index, context: PublishingContext<TheInkedEngineerWebsite>) throws -> HTML
is required and should compose the code to generate theHTML
code for the home page. -
func makeSectionHTML(section: Section<TheInkedEngineerWebsite>, context: PublishingContext<TheInkedEngineerWebsite>) throws -> HTML
is required and should compose the code to generate theHTML
code for each section in the website. A section is created by adding acase
underSectionID
inmain.swift
. -
func makeItemHTML(item: Item<TheInkedEngineerWebsite>, context: PublishingContext<TheInkedEngineerWebsite>) throws -> HTML
is required and should compose the code to generate theHTML
code for each item (article) in the website. An item is created by adding a.md
file inside a folder named as the raw value of a section. -
func makePageHTML(page: Page, context: PublishingContext<TheInkedEngineerWebsite>) throws -> HTML
is required and should compose the code to generate theHTML
of any free-form page that does not follow the traditional structure. A page is added by creating a.md
file inside the Content folder.
Once the htmlFactory
is created, we generate our theme like so:
static var myTheme: Theme {
Theme(
htmlFactory: CustomHTMLFactory(),
resourcePaths: ["Resources/css/styles.css", "Resources/css/articles.css"]
)
}
To use the theme, got to main.swift
and modify the .publish
method passing your newly created theme instead of .foundation
.
Tips and Tricks
-
Use SplashPublishPlugin to provide correct
Swift
syntax parsing in your.md
code blocks. -
Below is a helper to open links in a new tab:
/// Add an `<a>` HTML element within the current context.
/// Add `target="_blank"` and `rel="noopener` by default.
/// - parameter nodes: The element's attributes and child elements.
static func externalLink(_ nodes: Node<HTML.AnchorContext>...) -> Self {
var nodes = nodes
nodes.append(.target(.blank))
nodes.append(.rel(.noopener))
return .element(named: "a", nodes: nodes)
}
- Use an
enum
to list all yourCSS
classes. It will reduce the possibility of a typo. If you do so, the code snippet below will come in handy:
extension Node where Context: HTMLContext {
/// Adds the raw values of the passed elements as classes.
static func `classes`(_ classes: String...) -> Self {
.class(classes.joined(separator: " "))
}
}
so now, you can add your classes like so: .classes(CSS.underline, CSS.underlineThin, CSS.socialLink)