Swiftpack.co -  loopwerk/Saga as Swift Package
Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.
loopwerk/Saga
A static site generator written in Swift
.package(url: "https://github.com/loopwerk/Saga.git", from: "0.21.1")

tag-changelog

A static site generator, written in Swift, allowing you to supply your own metadata types for your items. Saga uses a system of extensible readers, writers and renderers supporting things like Atom feeds, paginating and strongly typed HTML templates.

Saga requires at least Swift 5.2, and runs on both Mac and Linux.

Usage

Saga is quite flexible: for example you can have one set of metadata for the articles on your blog, and another set of metadata for the apps in your portfolio. At the same time it's quite easy to configure:

import Saga
import SagaParsleyMarkdownReader
import SagaSwimRenderer

struct ArticleMetadata: Metadata {
  let tags: [String]
  let summary: String?
}

struct AppMetadata: Metadata {
  let url: URL?
  let images: [String]?
}

// SiteMetadata is given to every RenderingContext.
// You can put whatever you want in here.
struct SiteMetadata: Metadata {
  let url: URL
  let name: String
}

let siteMetadata = SiteMetadata(
  url: URL(string: "http://www.example.com")!,
  name: "Example website"
)

try Saga(input: "content", output: "deploy", siteMetadata: siteMetadata)
  // All markdown files within the "articles" subfolder will be parsed to html,
  // using ArticleMetadata as the Item's metadata type.
  .register(
    folder: "articles",
    metadata: ArticleMetadata.self,
    readers: [.parsleyMarkdownReader()],
    writers: [
      .itemWriter(swim(renderArticle)),
      .listWriter(swim(renderArticles), paginate: 20),
      .tagWriter(swim(renderTag), tags: \.metadata.tags),
      .yearWriter(swim(renderYear)),
      
      // Atom feed for all articles, and a feed per tag
      .listWriter(swim(renderFeed), output: "feed.xml"),
      .tagWriter(swim(renderTagFeed), output: "tag/[key]/feed.xml", tags: \.metadata.tags),
    ]
  )
  // All markdown files within the "apps" subfolder will be parsed to html,
  // using AppMetadata as the Item's metadata type.
  .register(
    folder: "apps",
    metadata: AppMetadata.self,
    readers: [.parsleyMarkdownReader()],
    writers: [.listWriter(swim(renderApps))]
  )
  // All the remaining markdown files will be parsed to html,
  // using the default EmptyMetadata as the Item's metadata type.
  .register(
    metadata: EmptyMetadata.self,
    readers: [.parsleyMarkdownReader()],
    writers: [.itemWriter(swim(renderItem))]
  )
  // Run the steps we registered above
  .run()
  // All the remaining files that were not parsed to markdown, so for example images,
  // raw html files and css, are copied as-is to the output folder.
  .staticFiles()

For more examples please check out the Example folder. Simply open Package.swift, wait for the dependencies to be downloaded, and run the project from within Xcode. Or run from the command line: swift run.

You can also check the source of loopwerk.io, which is completely built with Saga.

Extending Saga

It's very easy to add your own step to Saga where you can access the items and run your own code:

extension Saga {
  @discardableResult
  func createArticleImages() -> Self {
    let articles = fileStorage.compactMap { $0.item as? Item<ArticleMetadata> }

    for article in articles {
      let destination = (self.outputPath + article.relativeDestination.parent()).string + ".png"
      _ = try? shellOut(to: "python image.py", arguments: ["\"\(article.title)\"", destination], at: (self.rootPath + "ImageGenerator").string)
    }

    return self
  }
}

try Saga(input: "content", output: "deploy")
 // ...register and run steps...
 .createArticleImages()

But probably more common and useful is to use the itemProcessor parameter of the readers:

func itemProcessor(item: Item<EmptyMetadata>) {
  // Do whatever you want with the Item
  item.title.append("!")
}

try Saga(input: "content", output: "deploy")
  .register(
    metadata: EmptyMetadata.self,
    readers: [.parsleyMarkdownReader(itemProcessor: itemProcessor)],
    writers: [.itemWriter(swim(renderItem))]
  )

It's also easy to add your own readers, writers, and renderers; search for saga-plugin on Github. For example, SagaInkMarkdownReader adds an .inkMarkdownReader that uses Ink and Splash.

Getting started

Create a new folder and inside of it run swift package init --type executable, and then open Package.swift. Edit Package.swift to add the Saga dependency, plus a reader and optionally a renderer (see Architecture below), so that it looks something like this:

// swift-tools-version:5.2

import PackageDescription

let package = Package(
  name: "MyWebsite",
  platforms: [
    .macOS(.v10_15)
  ],
  dependencies: [
    .package(url: "https://github.com/loopwerk/Saga", from: "0.19.0"),
    .package(url: "https://github.com/loopwerk/SagaParsleyMarkdownReader", from: "0.4.0"),
    .package(url: "https://github.com/loopwerk/SagaSwimRenderer", from: "0.4.0"),
  ],
  targets: [
    .target(
      name: "MyWebsite",
      dependencies: [
        "Saga", 
        "SagaParsleyMarkdownReader", 
        "SagaSwimRenderer"
      ]
    ),
    .testTarget(
      name: "MyWebsiteTests",
      dependencies: ["MyWebsite"]),
  ]
)

Now, inside of Sources/MyWebsite/main.swift you can import Saga and use it.

Development server

From your website folder you can run the following command to start a development server, which rebuilds your website on changes, and reloads the browser as well.

swift run watch [input-folder] [output-folder]

Use the same relative input- and output folders as you gave to Saga.

This functionality does depend on a globally installed browser-sync, and only works on macOS, not Linux.

npm install -g browser-sync

Architecture

Saga does its work in multiple stages.

  1. First, it finds all the files within the input folder
  2. Then, for every registered step, it passes those files to matching readers (matching based on the extensions the reader declares it supports). Readers are responsible for turning for example Markdown or RestructuredText files, into Item instances. Such readers are not bundled with Saga itself, instead you'll have to install one such as SagaParsleyMarkdownReader, SagaPythonMarkdownReader, or SagaInkMarkdownReader.
  3. Finally Saga runs all the registered steps again, now executing the writers. These writers expect to be given a function that can turn a RenderingContext (which hold the Item among other things) into a String, which it'll then write to disk, to the output folder. To turn an Item into a HTML String, you'll want to use a template language or a HTML DSL, such as SagaSwimRenderer or SagaStencilRenderer.

Readers are expected to support the parsing of metadata contained within a document, such as this example for Markdown files:

---
tags: article, news
summary: This is the summary
---
# Hello world
Hello there.

The three officially supported Markdown readers all do support the parsing of metadata.

The official recommendation is to use SagaParsleyMarkdownReader for reading Markdown files and SagaSwimRenderer to render them using Swim, which offers a great HTML DSL using Swift's function builders.

Thanks

Inspiration for the API of Saga is very much owed to my favorite (but sadly long unmaintained) static site generator: liquidluck. Its system of multiple readers and writers is really good and I wanted something similar.

Thanks also goes to Publish, another static site generator written in Swift, for inspiring me towards custom strongly typed metadata. A huge thanks also for its metadata decoder, which was copied over shamelessly.

You can read this series of articles discussing the inspiration behind the API.

Websites using Saga

GitHub

link
Stars: 19
Last commit: 1 week ago

Ad: Job Offers

iOS Software Engineer @ Perry Street Software
Perry Street Software is Jack’d and SCRUFF. We are two of the world’s largest gay, bi, trans and queer social dating apps on iOS and Android. Our brands reach more than 20 million members worldwide so members can connect, meet and express themselves on a platform that prioritizes privacy and security. We invest heavily into SwiftUI and using Swift Packages to modularize the codebase.

Release Notes

Release 0.21.1
2 weeks ago

Bugfixes

  • the AnyItem protocol is now correctly using AnyObject instead of class

Performance Improvements

  • reuse the year date formatter instead of creating one for each article

Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API