• Stars
    star
    4,840
  • Rank 8,693 (Top 0.2 %)
  • Language
    Swift
  • License
    MIT License
  • Created almost 5 years ago
  • Updated 6 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A static site generator for Swift developers

Publish

Swift Package Manager Mac + Linux Twitter: @johnsundell

Welcome to Publish, 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.

Publish is used to build all of swiftbysundell.com.

Websites as Swift packages

When using Publish, each website is defined as a Swift package, which acts as the configuration as to how the website should be generated and deployed — all using native, type-safe Swift code. For example, here’s what the configuration for a food recipe website might look like:

struct DeliciousRecipes: Website {
    enum SectionID: String, WebsiteSectionID {
        case recipes
        case links
        case about
    }

    struct ItemMetadata: WebsiteItemMetadata {
        var ingredients: [String]
        var preparationTime: TimeInterval
    }

    var url = URL(string: "https://cooking-with-john.com")!
    var name = "Delicious Recipes"
    var description = "Many very delicious recipes."
    var language: Language { .english }
    var imagePath: Path? { "images/logo.png" }
}

Each website built using Publish can freely decide what kind of sections and metadata that it wants to support. Above, we’ve added three sections — Recipes, Links, and About — which can then contain any number of items. We’ve also added support for our own, site-specific item metadata through the ItemMetadata type, which we’ll be able to use in a fully type-safe manner all throughout our publishing process.

Start out simple, and customize when needed

While Publish offers a really powerful API that enables almost every aspect of the website generation process to be customized and tweaked, it also ships with a suite of convenience APIs that aims to make it as quick and easy as possible to get started.

To start generating the Delicious Recipes website we defined above, all we need is a single line of code, that tells Publish which theme to use to generate our website’s HTML:

try DeliciousRecipes().publish(withTheme: .foundation)

Not only does the above call render our website’s HTML, it also generates an RSS feed, a site map, and more.

Above we’re using Publish’s built-in Foundation theme, which is a very basic theme mostly provided as a starting point, and as an example of how Publish themes may be built. We can of course at any time replace that theme with our own, custom one, which can include any sort of HTML and resources that we’d like.

By default, Publish will generate a website’s content based on Markdown files placed within that project’s Content folder, but any number of content items and custom pages can also be added programmatically.

Publish supports three types of content:

Sections, which are created based on the members of each website’s SectionID enum. Each section both has its own HTML page, and can also act as a container for a list of Items, which represent the nested HTML pages within that section. Finally, Pages provide a way to build custom free-form pages that can be placed into any kind of folder hierarchy.

Each Section, Item, and Page can define its own set of Content — which can range from text (like titles and descriptions), to HTML, audio, video and various kinds of metadata.

Here’s how we could extend our basic publish() call from before to inject our own custom publishing pipeline — which enables us to define new items, modify sections, and much more:

try DeliciousRecipes().publish(
    withTheme: .foundation,
    additionalSteps: [
        // Add an item programmatically
        .addItem(Item(
            path: "my-favorite-recipe",
            sectionID: .recipes,
            metadata: DeliciousRecipes.ItemMetadata(
                ingredients: ["Chocolate", "Coffee", "Flour"],
                preparationTime: 10 * 60
            ),
            tags: ["favorite", "featured"],
            content: Content(
                title: "Check out my favorite recipe!"
            )
        )),
        // Add default titles to all sections
        .step(named: "Default section titles") { context in
            context.mutateAllSections { section in
                guard section.title.isEmpty else { return }
                
                switch section.id {
                case .recipes:
                    section.title = "My recipes"
                case .links:
                    section.title = "External links"
                case .about:
                    section.title = "About this site"
                }
            }
        }
    ]
)

Of course, defining all of a program’s code in one single place is rarely a good idea, so it’s recommended to split up a website’s various generation operations into clearly separated steps — which can be defined by extending the PublishingStep type with static properties or methods, like this:

extension PublishingStep where Site == DeliciousRecipes {
    static func addDefaultSectionTitles() -> Self {
        .step(named: "Default section titles") { context in
            context.mutateAllSections { section in
                guard section.title.isEmpty else { return }

                switch section.id {
                case .recipes:
                    section.title = "My recipes"
                case .links:
                    section.title = "External links"
                case .about:
                    section.title = "About this site"
                }
            }
        }
    }
}

Each publishing step is passed an instance of PublishingContext, which it can use to mutate the current context in which the website is being published — including its files, folders, and content.

Using the above pattern, we can implement any number of custom publishing steps that’ll fit right in alongside all of the default steps that Publish ships with. This enables us to construct really powerful pipelines in which each step performs a single part of the generation process:

try DeliciousRecipes().publish(using: [
    .addMarkdownFiles(),
    .copyResources(),
    .addFavoriteItems(),
    .addDefaultSectionTitles(),
    .generateHTML(withTheme: .delicious),
    .generateRSSFeed(including: [.recipes]),
    .generateSiteMap()
])

Above we’re constructing a completely custom publishing pipeline by calling the publish(using:) API.

To learn more about Publish’s built-in publishing steps, check out this file.

Building an HTML theme

Publish uses Plot as its HTML theming engine, which enables entire HTML pages to be defined using Swift. When using Publish, it’s recommended that you build your own website-specific theme — that can make full use of your own custom metadata, and be completely tailored to fit your website’s design.

Themes are defined using the Theme type, which uses an HTMLFactory implementation to create all of a website’s HTML pages. Here’s an excerpt of what the implementation for the custom .delicious theme used above may look like:

extension Theme where Site == DeliciousRecipes {
    static var delicious: Self {
        Theme(htmlFactory: DeliciousHTMLFactory())
    }

    private struct DeliciousHTMLFactory: HTMLFactory {
        ...
        func makeItemHTML(
            for item: Item<DeliciousRecipes>,
            context: PublishingContext<DeliciousRecipes>
        ) throws -> HTML {
            HTML(
                .head(for: item, on: context.site),
                .body(
                    .ul(
                        .class("ingredients"),
                        .forEach(item.metadata.ingredients) {
                            .li(.text($0))
                        }
                    ),
                    .p(
                        "This will take around ",
                        "\(Int(item.metadata.preparationTime / 60)) ",
                        "minutes to prepare"
                    ),
                    .contentBody(item.body)
                )
            )
        }
        ...
    }
}

Above we’re able to access both built-in item properties, and the custom metadata properties that we defined earlier as part of our website’s ItemMetadata struct, all in a way that retains full type safety.

More thorough documentation on how to build Publish themes, and some of the recommended best practices for doing so, will be added shortly.

Building plugins

Publish also supports plugins, which can be used to share setup code between various projects, or to extend Publish’s built-in functionality in various ways. Just like publishing steps, plugins perform their work by modifying the current PublishingContext — for example by adding files or folders, by mutating the website’s existing content, or by adding Markdown parsing modifiers.

Here’s an example of a plugin that ensures that all of a website’s items have tags:

extension Plugin {
    static var ensureAllItemsAreTagged: Self {
        Plugin(name: "Ensure that all items are tagged") { context in
            let allItems = context.sections.flatMap { $0.items }

            for item in allItems {
                guard !item.tags.isEmpty else {
                    throw PublishingError(
                        path: item.path,
                        infoMessage: "Item has no tags"
                    )
                }
            }
        }
    }
}

Plugins are then installed by adding the installPlugin step to any publishing pipeline:

try DeliciousRecipes().publish(using: [
    ...
    .installPlugin(.ensureAllItemsAreTagged)
])

If your plugin is hosted on GitHub you can use the publish-plugin topic so it can be found with the rest of community plugins.

For a real-world example of a Publish plugin, check out the official Splash plugin, which makes it really easy to integrate the Splash syntax highlighter with Publish.

System requirements

To be able to successfully use Publish, make sure that your system has Swift version 5.4 (or later) installed. If you’re using a Mac, also make sure that xcode-select is pointed at an Xcode installation that includes the required version of Swift, and that you’re running macOS Big Sur (11.0) or later.

Please note that Publish does not officially support any form of beta software, including beta versions of Xcode and macOS, or unreleased versions of Swift.

Installation

Publish is distributed using the Swift Package Manager. To install it into a project, add it as a dependency within your Package.swift manifest:

let package = Package(
    ...
    dependencies: [
        .package(url: "https://github.com/johnsundell/publish.git", from: "0.1.0")
    ],
    ...
)

Then import Publish wherever you’d like to use it:

import Publish

For more information on how to use the Swift Package Manager, check out this article, or its official documentation.

Publish also ships with a command line tool that makes it easy to set up new website projects, and to generate and deploy existing ones. To install that command line tool, simply run make within a local copy of the Publish repo:

$ git clone https://github.com/JohnSundell/Publish.git
$ cd Publish
$ make

Then run publish help for instructions on how to use it.

The Publish command line tool is also available via Homebrew and can be installed using the following command if you have Homebrew installed:

brew install publish

However, please note that Homebrew support is not officially maintained by John Sundell, and you might therefore be installing an older version of the Publish command line tool when using Homebrew. Using make, as described above, is the preferred way to install the Publish command line tool.

Running and deploying

Since all Publish websites are implemented as Swift packages, they can be generated simply by opening up a website’s package in Xcode (by opening its Package.swift file), and then running it using the Product > Run command (or ⌘+R).

Publish can also facilitate the deployment of websites to external servers through its DeploymentMethod API, and ships with built-in implementations for Git and GitHub-based deployments. To define a deployment method for a website, add the deploy step to your publishing pipeline:

try DeliciousRecipes().publish(using: [
    ...
    .deploy(using: .gitHub("johnsundell/delicious-recipes"))
])

Even when added to a pipeline, deployment steps are disabled by default, and are only executed when the --deploy command line flag was passed (which can be added through Xcode’s Product > Scheme > Edit Scheme... menu), or by running the command line tool using publish deploy.

Publish can also start a localhost web server for local testing and development, by using the publish run command. To regenerate site content with the server running, use Product > Run on your site's package in Xcode.

Quick start

To quickly get started with Publish, install the command line tool by first cloning this repository, and then run make within the cloned folder:

$ git clone https://github.com/JohnSundell/Publish.git
$ cd Publish
$ make

Note: If you encounter an error while running make, ensure that you have your Command Line Tools location set from Xcode's preferences. It's in Preferences > Locations > Locations > Command Line Tools. The dropdown will be blank if it hasn't been set yet.

Then, create a new folder for your new website project and simply run publish new within it to get started:

$ mkdir MyWebsite
$ cd MyWebsite
$ publish new

Finally, run open Package.swift to open up the project in Xcode to start building your new website.

Additional documentation

You can find a growing collection of additional documentation about Publish’s various features and capabilities within the Documentation folder.

Design and goals

Publish was first and foremost designed to be a powerful and heavily customizable tool for building static websites in Swift — starting with Swift by Sundell, a website which has over 300 individual pages and a pipeline consisting of over 25 publishing steps.

While the goal is definitely also to make Publish as accessible and easy to use as possible, it will most likely keep being a quite low-level tool that favors code-level control over file system configuration files, and customizability over strongly held conventions.

The main trade-off of that design is that Publish will likely have a steeper learning curve than most other static site generators, but hopefully it’ll also offer a much greater degree of power, flexibility and type safety as a result. Over time, and with the community’s help, we should be able to make that learning curve much less steep though — through much more thorough documentation and examples, and through shared tools and convenience APIs.

Publish was also designed with code reuse in mind, and hopefully a much larger selection of themes, tools, plugins and other extensions will be developed by the community over time.

Contributions and support

Publish is developed completely in the open, and your contributions are more than welcome.

Before you start using Publish in any of your projects, it’s highly recommended that you spend a few minutes familiarizing yourself with its documentation and internal implementation, so that you’ll be ready to tackle any issues or edge cases that you might encounter.

Since this is a very young project, it’s likely to have many limitations and missing features, which is something that can really only be discovered and addressed as more people start using it. While Publish is used in production to build all of Swift by Sundell, it’s recommended that you first try it out for your specific use case, to make sure it supports the features that you need.

This project does not come with GitHub Issues-based support, and users are instead encouraged to become active participants in its continued development — by fixing any bugs that they encounter, or by improving the documentation wherever it’s found to be lacking.

If you wish to make a change, open a Pull Request — even if it just contains a draft of the changes you’re planning, or a test that reproduces an issue — and we can discuss it further from there.

Hope you’ll enjoy using Publish!

More Repositories

1

SwiftTips

A collection of Swift tips & tricks that I've shared on Twitter
3,971
star
2

Files

A nicer way to handle files & folders in Swift
Swift
2,456
star
3

Ink

A fast and flexible Markdown parser written in Swift.
Swift
2,377
star
4

Plot

A DSL for writing type-safe HTML, XML and RSS in Swift.
Swift
1,979
star
5

Unbox

[Deprecated] The easy to use Swift JSON decoder
Swift
1,956
star
6

Marathon

[DEPRECATED] Marathon makes it easy to write, run and manage your Swift scripts 🏃
Swift
1,863
star
7

ImagineEngine

A project to create a blazingly fast Swift game engine that is a joy to use 🚀
Swift
1,825
star
8

SwiftPlate

Easily generate cross platform Swift framework projects from the command line
Swift
1,766
star
9

Splash

A fast, lightweight and flexible Swift syntax highlighter for blogs, tools and fun!
Swift
1,758
star
10

TestDrive

Quickly try out any Swift pod or framework in a playground
Swift
1,597
star
11

Codextended

Extensions giving Swift's Codable API type inference super powers 🦸‍♂️🦹‍♀️
Swift
1,495
star
12

ShellOut

Easily run shell commands from a Swift script or command line tool
Swift
836
star
13

Wrap

[DEPRECATED] The easy to use Swift JSON encoder
Swift
732
star
14

CollectionConcurrencyKit

Async and concurrent versions of Swift’s forEach, map, flatMap, and compactMap APIs.
Swift
730
star
15

Sweep

Fast and powerful Swift string scanning made simple
Swift
531
star
16

Playground

Instantly create Swift playgrounds from the command line
Swift
439
star
17

Require

Require optional values to be non-nil, or crash gracefully
Swift
414
star
18

XcodeTheme

My Xcode theme - Sundell's Colors
Swift
408
star
19

AsyncCompatibilityKit

iOS 13-compatible backports of commonly used async/await-based system APIs that are only available from iOS 15 by default.
Swift
378
star
20

Shapeshift

Quickly convert a folder containing Swift files into an iPad-compatible Playground
Swift
338
star
21

Identity

🆔 Type-safe identifiers in Swift
Swift
298
star
22

SwiftBySundell

Code samples from the Swift by Sundell website & podcast
Swift
289
star
23

SwiftScripting

A list of Swift scripting tools, frameworks & examples
235
star
24

SuperSpriteKit

Extensions to Apple's SpriteKit game engine
Objective-C
224
star
25

Flow

Operation Oriented Programming in Swift
Swift
217
star
26

Xgen

A Swift package for generating Xcode workspaces & playgrounds
Swift
189
star
27

IndieSupportWeeks

A two-week effort to help support indie developers shipping apps on Apple's platforms who have been financially impacted by the COVID-19 pandemic.
182
star
28

CGOperators

Easily manipulate CGPoints, CGSizes and CGVectors using math operators
Swift
148
star
29

Animate

Declarative UIView animations without nested closures
Swift
129
star
30

SplashPublishPlugin

A Splash plugin for the Publish static site generator
Swift
92
star
31

Assert

A collection of convenient assertions for Swift testing
Swift
69
star
32

UITestingExample

Example code from my blog post about UI testing
Swift
67
star
33

Marathon-Examples

A collection of example Swift scripts that can easily be run using Marathon
Swift
55
star
34

Releases

A Swift package for resolving released versions from a Git repository
Swift
51
star
35

BlockSnippets

Xcode snippets that are very handy when working with blocks in various contexts
51
star
36

PlotPlayground

A Swift playground that comes pre-loaded with Plot, that can be used to explore the new component API.
Swift
49
star
37

SwiftKit

A collection of Swift utilities that I share across my Swift-based projects
Swift
38
star
38

UnitTestingWorkshop

Project used during my workshop "Getting started with unit testing in Swift"
Swift
36
star
39

JSUpdateLookup

A lightweight, easy to use Objective-C class to check if your iOS app has an update available
Objective-C
28
star
40

SwiftAveiro

Skeleton project for my Swift Aveiro workshop "Everyone is an API designer"
Swift
15
star
41

CloudKitChat

A demo chat application powered by CloudKit
Objective-C
14
star
42

swiftbysundell-beta-feedback

Submit your feedback on the Swift by Sundell 2.0 beta
9
star
43

JSGeometry

A set of utility functions that enables easy one-line manipulation of CoreGraphics geometry structs like CGPoint, CGSize & CGRect.
Objective-C
6
star
44

JSAutoCopy

An Objective-C category that enables automatic copying of any object
Objective-C
4
star
45

UnboxDemoPlayground

A Swift Playground that comes setup with Unbox & Wrap, used in my CocoaHeads Stockholm presentation
Swift
3
star
46

JSAutoEncodedObject

Automatically encode or decode any Objective-C object
Objective-C
3
star
47

JSLocalization

An Objective-C class that enables dynamic localization of an iOS app.
Objective-C
3
star
48

MarathonTestScriptWithDependencies

A test script with dependencies - used for Marathon's tests
Swift
2
star
49

JSObservableObject

Easily add protocol-based observation to any Objective-C class
Objective-C
2
star
50

MarathonTestScript

A Swift script that's used in Marathon's tests
Swift
1
star
51

MarathonTestPackage

A Swift package that's used in Marathon's tests
Swift
1
star