Getting Started with Saga
An overview of how to configure Saga to render your pages and articles.
Overview
Let’s start with the most basic example: rendering all markdown files to HTML.
import Saga
import SagaParsleyMarkdownReader
import SagaSwimRenderer
import HTML
func renderPage(context: ItemRenderingContext<EmptyMetadata>) -> Node {
html(lang: "en-US") {
body {
div(id: "content") {
h1 { context.item.title }
Node.raw(context.item.body)
}
}
}
}
try await Saga(input: "content", output: "deploy")
// All Markdown files within the `input` folder will be parsed to html.
.register(
readers: [.parsleyMarkdownReader],
writers: [.itemWriter(swim(renderPage))]
)
// Run the step we registered above.
// Static files (images, css, etc.) are copied automatically.
.run()
Note This example uses the Swim library via SagaSwimRenderer to create type-safe HTML. If you prefer to work with Mustache-type HTML template files, check out SagaStencilRenderer. The Architecture document has more information on how Saga works.
Custom metadata
Of course Saga can do much more than just render a folder of markdown files as-is. It can also deal with custom metadata contained within markdown files - even multiple types of metadata for different kinds of pages.
Let’s look at an example markdown article, /content/articles/first-article.md:
---
tags: article, news
summary: This is the summary of the first article
date: 2020-01-01
---
# Hello world
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
And an example app for a portfolio, /content/apps/lastfm.md:
---
url: https://itunes.apple.com/us/app/last-fm-scrobbler/id1188681944?ls=1&mt=8
images: lastfm_1.jpg, lastfm_2.jpg
---
# Last.fm Scrobbler
"Get the official Last.fm Scrobbler App to keep track of what you're listening to on Apple Music. Check out your top artist, album and song charts from all-time to last week, and watch videos of your favourite tracks."
As you can see, they both use different metadata: the article has tags, a summary and a date, while the app has a url and images.
Let’s configure Saga to render these files.
struct ArticleMetadata: Metadata {
let tags: [String]
let summary: String?
}
struct AppMetadata: Metadata {
let url: URL?
let images: [String]?
}
try await Saga(input: "content", output: "deploy")
// 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(
readers: [.parsleyMarkdownReader],
writers: [.itemWriter(swim(renderItem))]
)
// Run the steps we registered above.
// Static files (images, css, etc.) are copied automatically.
.run()
While that might look a bit overwhelming, it should be easy to follow what each register step does, each operating on a set of files in a subfolder and processing them in different ways.
Please check out the Example project for a more complete picture of Saga. 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. The example project contains articles with tags and pagination, an app portfolio, static pages, RSS feeds for all articles and per tag, statically typed HTML templates, and more.
You can also check the source of loopwerk.io, which is completely built with Saga.
Writers
In the custom metadata example above, you can see that the articles step uses four different kinds of writers: itemWriter, listWriter, tagWriter, and yearWriter. Each writer takes a renderer function, in this case swim, using a locally defined function with the HTML template. The swim function comes from the SagaSwimRenderer library, whereas renderArticle, renderArticles, renderTag and the rest are locally defined in your project. They are the actual HTML templates, using a strongly typed DSL.
tip If you prefer to work with Mustache-type HTML template files, check out SagaStencilRenderer.
The four different writers are all used for different purposes:
itemWriterwrites a single item to a single file. For examplecontent/articles/my-article.mdwill be written todeploy/articles/my-article.html, orcontent/index.mdtodeploy/index.html.listWriterwrites an array of items to multiple files. For example to create andeploy/articles/index.htmlpage that lists all your articles in a paginated manner.tagWriterwrites an array of items to multiple files, based on a tag. If you tag your articles you can use this to render tag pages likedeploy/articles/iOS/index.html.yearWriteris similar totagWriterbut uses the publication date of the item. You can use this to create year-based archives of your articles, for exampledeploy/articles/2022/index.html.
For more information, please check out Writer.
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.
$ saga dev
By default this watches the content and Sources folders, outputs to deploy, and serves on port 3000. All of these can be customized:
$ saga dev --watch content --watch Sources --output deploy --port 3000
You can also ignore certain files or folders using glob patterns:
$ saga dev --ignore "*.tmp" --ignore "drafts/*"
To just build the site without starting a server:
$ saga build
When running under saga dev, Saga sets the SAGA_DEV environment variable and exposes it as isDev. Use this to skip expensive work during development, such as image generation or HTML minification.
See Installation for how to install the saga CLI.