Class
Saga
class SagaThe 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
) throwsInstance Properties
var allItems: [any AnyItem] { get }All Items across all registered processing steps.
let inputPath: PathThe path that contains your text files, relative to the rootPath. For example “content”.
let outputPath: PathThe path that Saga will write the rendered website to, relative to the rootPath. For example “deploy”.
let rootPath: PathThe 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
) -> SelfCreate 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) -> SelfApply 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 : MetadataRegister a new processing step.
Parameters
folderThe folder (relative to
input) to operate on. Ifnil, it operates on theinputfolder itself.
Append/**(e.g."photos/**") to create a separate processing step for each subfolder.
Each subfolder gets its own scopeditemsarray,previous/nextnavigation, and writers.metadataThe metadata type used for the processing step. You can use
EmptyMetadataif you don't need any custom metadata (which is the default value).readersThe readers that will be used by this step.
itemProcessorA function to modify the generated
Itemas you see fit.filterA filter to only include certain items from the input folder.
claimExcludedItemsWhen 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.itemWriteModeThe
ItemWriteModeused by this step.sortingA comparison function used to sort items. Defaults to date descending (newest first).
writersThe 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 : MetadataRegister a processing step that fetches items programmatically instead of reading from files.
Parameters
metadataThe metadata type used for the processing step. You can use
EmptyMetadataif you don't need any custom metadata (which is the default value).fetchAn async function that returns an array of items.
itemProcessorA function to modify each fetched
Itemas you see fit.sortingA comparison function used to sort items. Defaults to date descending (newest first).
writersThe 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
) -> SelfRegister 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) -> SelfRegister 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 -> SelfExecute all the registered steps.
Relationships
Conforms To
Swift.Sendable