Class

Saga

class Saga

The main Saga class, used to configure and build your website.

try await Saga(input: "content", output: "deploy")
  // All files in the input folder will be parsed to html, and written to the output folder.
  .register(
    metadata: EmptyMetadata.self,
    readers: [.parsleyMarkdownReader],
    writers: [.itemWriter(swim(renderPage))]
  )

  // Run the steps we registered above.
  // Static files (images, css, etc.) are copied automatically.
  .run()

Mentioned In

Initializers

init(
  input: Path,
  output: Path = "deploy",
  fileIO: FileIO = .diskAccess,
  originFilePath: StaticString = #filePath
) throws

Instance Properties

var allItems: [any AnyItem] { get }

All Items across all registered processing steps.

let inputPath: Path

The path that contains your text files, relative to the rootPath. For example “content”.

let outputPath: Path

The path that Saga will write the rendered website to, relative to the rootPath. For example “deploy”.

let rootPath: Path

The root working path. This is automatically set to the same folder that holds Package.swift.

Instance Methods

@discardableResult
@preconcurrency
func createPage(
  _ output: Path,
  using renderer: @escaping (PageRenderingContext) async throws -> String
) -> Self

Create a template-driven page without needing an Item or markdown file.

Use this for pages that are purely driven by a template, such as a homepage showing the latest articles, a search page, or a 404 page. The renderer receives a PageRenderingContext with access to all items across all processing steps.

try await Saga(input: "content", output: "deploy")
  .register(
    folder: "articles",
    metadata: ArticleMetadata.self,
    readers: [.parsleyMarkdownReader],
    writers: [.listWriter(swim(renderArticles))]
  )
  .createPage("index.html", using: swim(renderHome))
  .run()
@discardableResult
@preconcurrency
func postProcess(_ transform: @escaping (String, Path) throws -> String) -> Self

Apply a transform to every file written by Saga.

The transform receives the rendered content and relative output path. Multiple calls stack: each wraps the previous write.

try await Saga(input: "content", output: "deploy")
  .register(...)
  .postProcess { html, path in
    minifyHTML(html)
  }
  .run()
@discardableResult
@preconcurrency
func register<M>(
  folder: Path? = nil,
  metadata: M.Type = EmptyMetadata.self,
  readers: [Reader],
  itemProcessor: ((Item<M>) async -> Void)? = nil,
  filter: @escaping (Item<M>) -> Bool = { _ in true },
  claimExcludedItems: Bool = true,
  itemWriteMode: ItemWriteMode = .moveToSubfolder,
  sorting: @escaping (Item<M>, Item<M>) -> Bool = { $0.date > $1.date },
  writers: [Writer<M>]
) throws -> Self where M : Metadata

Register a new processing step.

Parameters

folder

The folder (relative to input) to operate on. If nil, it operates on the input folder itself.
Append /** (e.g. "photos/**") to create a separate processing step for each subfolder.
Each subfolder gets its own scoped items array, previous/next navigation, and writers.

metadata

The metadata type used for the processing step. You can use EmptyMetadata if you don't need any custom metadata (which is the default value).

readers

The readers that will be used by this step.

itemProcessor

A function to modify the generated Item as you see fit.

filter

A filter to only include certain items from the input folder.

claimExcludedItems

When an item is excluded by the filter, should this step claim it? If true (the default), excluded items won't be available to subsequent processing steps.

itemWriteMode

The ItemWriteMode used by this step.

sorting

A comparison function used to sort items. Defaults to date descending (newest first).

writers

The writers that will be used by this step.

Return Value

The Saga instance itself, so you can chain further calls onto it.

@discardableResult
@preconcurrency
func register<M>(
  metadata: M.Type = EmptyMetadata.self,
  fetch: @escaping () async throws -> [Item<M>],
  itemProcessor: ((Item<M>) async -> Void)? = nil,
  sorting: @escaping (Item<M>, Item<M>) -> Bool = { $0.date > $1.date },
  writers: [Writer<M>]
) -> Self where M : Metadata

Register a processing step that fetches items programmatically instead of reading from files.

Parameters

metadata

The metadata type used for the processing step. You can use EmptyMetadata if you don't need any custom metadata (which is the default value).

fetch

An async function that returns an array of items.

itemProcessor

A function to modify each fetched Item as you see fit.

sorting

A comparison function used to sort items. Defaults to date descending (newest first).

writers

The writers that will be used by this step.

Return Value

The Saga instance itself, so you can chain further calls onto it.

@discardableResult
@preconcurrency
func register(
  read: @escaping (Saga) async throws -> [any AnyItem],
  write: @escaping (Saga) async throws -> Void
) -> Self

Register a custom processing step with user-provided read and write closures.

Use this for custom logic that doesn’t fit the standard reader/writer pipeline. The read closure runs during the read phase (before items are sorted) and returns items, and the write closure runs during the write phase (after all readers have finished).

@discardableResult
@preconcurrency
func register(write: @escaping (Saga) async throws -> Void) -> Self

Register a custom write-only processing step.

Use this for custom logic that doesn’t fit the standard reader/writer pipeline. The closure runs during the write phase, after all readers have finished and items are sorted.

try await Saga(input: "content", output: "deploy")
  .register(...)
  .register { saga in
    // custom write logic with access to saga.allItems
  }
  .run()
@discardableResult
func run() async throws -> Self

Execute all the registered steps.

Relationships

Conforms To

Swift.Sendable