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.

var buildReason: BuildReason { get }

Why the current build was triggered.

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 afterWrite(_ hook: @escaping (Saga) async throws -> Void) -> Self

Register a hook that runs after the write phase of each build cycle.

For a search indexing example, see AddingSearch.

Use this for post-build steps like search indexing:

try await Saga(input: "content", output: "deploy")
  .register(...)
  .afterWrite { saga in
    // run pagefind, etc.
  }
  .run()
@discardableResult
@preconcurrency
func beforeRead(_ hook: @escaping (Saga) async throws -> Void) -> Self

Register a hook that runs before the read phase of each build cycle.

For a CSS compilation example, see TailwindCSS.

Use this for pre-build steps like CSS compilation:

try await Saga(input: "content", output: "deploy")
  .beforeRead { _ in
    try await tailwind.run(input: "content/static/input.css", output: "content/static/output.css")
  }
  .register(...)
  .run()
@discardableResult
func i18n(
  locales: [SagaLocale],
  defaultLocale: SagaLocale,
  prefixDefaultLocaleOutputFolder: Bool = false,
  localizedOutputFolders: [String : [SagaLocale : String]] = [:]
) -> Self

Configure internationalization support.

When enabled, Saga expects content to be organized in locale-prefixed folders (e.g. en/articles/, nl/articles/). Each register() call automatically fans out into per-locale processing steps.

For a complete walkthrough, see Internationalization.

try await Saga(input: "content", output: "deploy")
  .i18n(
    locales: ["en", "nl"],
    defaultLocale: "en",
    localizedOutputFolders: ["articles": ["nl": "artikelen"]]
  )
  .register(
    folder: "articles",
    metadata: ArticleMetadata.self,
    readers: [.parsleyMarkdownReader],
    writers: [.itemWriter(swim(renderArticle))]
  )
  .run()

Parameters

locales

The supported locales (e.g. ["en", "nl"]).

defaultLocale

The default locale. Its content is written to the root unless prefixDefaultLocaleOutputFolder is true.

prefixDefaultLocaleOutputFolder

Whether the default locale should also get a subdirectory prefix. Defaults to false.

localizedOutputFolders

A mapping of content folder → [locale → output folder]. Allows output folder names
to differ from content folder names per locale. Locales not in the map use the original folder name.

@discardableResult
func ignore(_ pattern: String) -> Self

Deprecated

@discardableResult
func ignoreChanges(_ pattern: String) -> Self

Add a glob pattern to ignore during file watching in dev mode.

Use this to prevent unnecessary rebuilds when certain files change:

try await Saga(input: "content", output: "deploy")
  .ignoreChanges("output.css")
  .ignoreChanges("*.tmp")
  .register(...)
  .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.

For a minification example, see HTMLMinification.

try await Saga(input: "content", output: "deploy")
  .register(...)
  .postProcess { html, path in
    minifyHTML(html)
  }
  .run()
@discardableResult
func run() async throws -> Self

Execute all the registered steps.

Type Properties

static var isCLI: Bool { get }

This is true when the SAGA_CLI environment variable is set (which saga dev does automatically). Used internally.

static var isDev: Bool { get }

This is true when the SAGA_DEV environment variable is set (which saga dev does automatically). Use it to skip expensive work during development:

.postProcess { html, _ in
  Saga.isDev ? html : minifyHTML(html)
}

Type Methods

@preconcurrency
static func atomFeed<Context>(
  title: String,
  author: String? = nil,
  baseURL: URL,
  summary: ((Item<Context.M>) -> String?)? = nil,
  image: ((Item<Context.M>) -> String?)? = nil,
  dateKeyPath: KeyPath<Item<Context.M>, Date> = \.lastModified
) -> ((Context) -> String) where Context : AtomContext

A renderer which creates an Atom feed for Items.

For JSON Feed and other formats, see CustomFeedFormats.

Parameters

title

The title of the feed, usually your site title. Example: Loopwerk.io.

author

The author of the articles.

baseURL

The base URL of your website, for example https://www.loopwerk.io.

summary

An optional function which takes an Item and returns its summary.

image

An optional function which takes an Item and returns its image URL (absolute or relative to baseURL).

dateKeyPath

A keypath to the date property to use for thefield. Defaults to \.lastModified.

Return Value

A function which takes a rendering context, and returns a string.

static func hashed(_ path: String) -> String

Returns a cache-busted file path by inserting a content hash into the filename.

link(rel: "stylesheet", href: Saga.hashed("/static/output.css"))
// → "/static/output-a1b2c3d4.css"
static func redirectHTML(to url: String) -> String

Returns an HTML page that redirects the browser to the given URL.

Uses both a <meta http-equiv="refresh"> tag and a canonical link for immediate client-side redirection with proper SEO signaling.

  • Parameter url: The destination URL to redirect to.

Return Value

A complete HTML document that redirects to the given URL.

@preconcurrency
static func redirectHTML(to url: String) -> (PageRenderingContext) -> String

A renderer which creates an HTML page that redirects the browser to the given URL.

Uses both a <meta http-equiv="refresh"> tag and a canonical link for immediate client-side redirection with proper SEO signaling.

  • Parameter url: The destination URL to redirect to.

Return Value

A renderer for use with createPage(_:using:).

.createPage("old-path/index.html", using: Saga.redirectHTML(to: "/new-path/"))
@preconcurrency
static func sitemap(
  baseURL: URL,
  filter: ((Path) -> Bool)? = nil
) -> (PageRenderingContext) -> String

A renderer which creates an XML sitemap from all generated pages.

When i18n is configured, the sitemap includes xhtml:link alternate entries for pages that have translations in other locales, following Google’s multilingual sitemap specification.

For a complete walkthrough, see GeneratingSitemaps.

Parameters

baseURL

The base URL of your website, for example https://www.example.com.

filter

An optional filter to exclude certain paths from the sitemap.
The filter receives the relative output path (e.g. "404.html" or "search/index.html").
Return true to include the path, false to exclude it.

Return Value

A renderer for use with createPage(_:using:). Place the sitemap as the last createPage call so it can see all generated pages before it.

.createPage("sitemap.xml", using: sitemap(
  baseURL: URL(string: "https://www.example.com")!,
  filter: { $0 != "404.html" }
))

Relationships

Inherits From

StepBuilder

Conforms To

Swift.Sendable