Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 24 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,33 @@ swift test
# Run the example project
swift run

# Start development server with file watching
swift run watch --watch content --watch Sources --output deploy
# Generate documentation
./generate_docs.sh
```

# With ignore patterns
swift run watch --watch content --output deploy --ignore "*.tmp"
## CLI Commands (`saga`)

# Legacy syntax (still supported)
swift run watch content Sources deploy
```bash
# Create a new Saga project
saga init mysite

# Generate documentation
./generate_docs.sh
# Build a site (runs `swift run` in current directory)
saga build

# Start dev server with file watching and auto-reload
saga dev

# Dev server with custom options
saga dev --watch content --watch Sources --output deploy --port 3000

# Ignore patterns
saga dev --ignore "*.tmp" --ignore "drafts/*"
```

The `saga` CLI is built from `Sources/SagaCLI/`. Install via Homebrew (`brew install loopwerk/tap/saga`) or Mint (`mint install loopwerk/Saga`).

The legacy `watch` command (`Sources/SagaWatch/`) is deprecated in favor of `saga dev`.

## Architecture Overview

Saga is a static site generator written in Swift that follows a **Reader → Processor → Writer** pipeline pattern:
Expand Down Expand Up @@ -58,7 +72,8 @@ Saga is designed for extensibility via external packages:
## Key Directories

- `/Sources/Saga/` - Main library with core architecture
- `/Sources/SagaCLI/` - Command line interface with development server
- `/Sources/SagaCLI/` - `saga` CLI (init, dev, build commands)
- `/Sources/SagaWatch/` - Legacy `watch` command (deprecated)
- `/Tests/SagaTests/` - Unit tests with mock implementations
- `/Example/` - Complete working example demonstrating usage patterns
- `/Sources/Saga/Saga.docc/` - DocC documentation source
Expand Down
36 changes: 36 additions & 0 deletions Example/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Example/Sources/Example/String+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ extension String {
return components.filter { !$0.isEmpty }.count
}

// This is a sloppy implementation but sadly `NSAttributedString(data:options:documentAttributes:)`
// is not available in CoreFoundation, and as such can't run on Linux (blocking CI builds).
/// This is a sloppy implementation but sadly `NSAttributedString(data:options:documentAttributes:)`
/// is not available in CoreFoundation, and as such can't run on Linux (blocking CI builds).
var withoutHtmlTags: String {
return replacingOccurrences(of: "(?m)<pre><span></span><code>[\\s\\S]+?</code></pre>", with: "", options: .regularExpression, range: nil)
.replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression, range: nil)
Expand Down
3 changes: 1 addition & 2 deletions Example/Sources/Example/run.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ struct AppMetadata: Metadata {
struct AlbumMetadata: Metadata {}
struct PhotoMetadata: Metadata {}

// An easy way to only get public articles, since ArticleMetadata.public is optional
/// An easy way to only get public articles, since ArticleMetadata.public is optional
extension Item where M == ArticleMetadata {
var `public`: Bool {
return metadata.public ?? true
Expand Down Expand Up @@ -96,7 +96,6 @@ struct Run {

// 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()
Expand Down
6 changes: 3 additions & 3 deletions Example/Sources/Example/templates.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Foundation
import HTML
import Moon
import PathKit
import Saga
import SagaSwimRenderer
import PathKit

// MARK: - Helpers

Expand Down Expand Up @@ -172,13 +172,13 @@ func renderAlbums(context: ItemsRenderingContext<AlbumMetadata>) -> Node {
baseHtml(title: "Photos") {
div(class: "collections") {
h1 { "Photos" }

div(class: "collections-grid") {
context.items.map { album in
let photos = photosForAlbum(album, allItems: context.allItems)
let previewPhotos = Array(photos.prefix(4))
let folder = album.relativeSource.parent()

return a(class: "collection-card", href: album.url) {
div(class: "card-previews") {
previewPhotos.map { photo in
Expand Down
14 changes: 13 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ let package = Package(
],
products: [
.library(name: "Saga", targets: ["Saga"]),
.executable(name: "watch", targets: ["SagaCLI"]),
.executable(name: "saga", targets: ["SagaCLI"]),
.executable(name: "watch", targets: ["SagaWatch"]),
],
dependencies: [
.package(url: "https://github.com/kylef/PathKit", from: "1.0.1"),
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"),
.package(url: "https://github.com/apple/swift-nio", from: "2.65.0"),
// .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"),
],
targets: [
Expand All @@ -23,6 +25,16 @@ let package = Package(
),
.executableTarget(
name: "SagaCLI",
dependencies: [
"PathKit",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"),
.product(name: "NIOHTTP1", package: "swift-nio"),
]
),
.executableTarget(
name: "SagaWatch",
dependencies: [
"PathKit",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
Expand Down
28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,36 @@ Typed metadata, pluggable readers, multiple writer types, pagination, tags — a
- You've outgrown convention-based SSGs and want full control

**Saga is not for you if:**
- You want a CLI that scaffolds everything with zero code
- You need a large ecosystem of themes and templates
- You're not comfortable with Swift


## Installing the CLI

**Via [Homebrew](https://brew.sh):**
```
$ brew install loopwerk/tap/saga
```

**Via [Mint](https://github.com/yonaskolb/Mint):**
```
$ mint install loopwerk/Saga
```


## Getting started

Getting started takes a few minutes if you're comfortable with Swift and SwiftPM. Follow the [installation guide](https://loopwerk.github.io/Saga/documentation/saga/installation) to set up your first site.
The fastest way to start a new site:

```
$ saga init mysite
$ cd mysite
$ saga dev
```

This scaffolds a complete project with articles, tags, templates, and a stylesheet — ready to build and serve.

For manual setup or more detail, see the [installation guide](https://loopwerk.github.io/Saga/documentation/saga/installation).


## Code over configuration
Expand Down Expand Up @@ -107,7 +129,7 @@ Saga is modular. You compose it with readers and renderers that fit your needs.

## Requirements

Saga requires Swift 5.5+ and runs on macOS 12+ and Linux. The development server requires [browser-sync](https://github.com/BrowserSync/browser-sync) and only works on macOS.
Saga requires Swift 5.5+ and runs on macOS 12+ and Linux.


## Websites using Saga
Expand Down
28 changes: 21 additions & 7 deletions Sources/Saga/MetadataDecoder.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/**
/*
* Publish
* Copyright (c) John Sundell 2019
* MIT license, see LICENSE file for details
Expand All @@ -7,7 +7,10 @@
import Foundation

final class MetadataDecoder: Decoder {
var userInfo: [CodingUserInfoKey: Any] { [:] }
var userInfo: [CodingUserInfoKey: Any] {
[:]
}

let codingPath: [CodingKey]

private let metadata: [String: String]
Expand Down Expand Up @@ -68,7 +71,9 @@ final class MetadataDecoder: Decoder {

private extension MetadataDecoder {
struct KeyedContainer<Key: CodingKey>: KeyedDecodingContainerProtocol {
var allKeys: [Key] { keys.all() }
var allKeys: [Key] {
keys.all()
}

let metadata: [String: String]
let keys: KeyMap<Key>
Expand Down Expand Up @@ -246,8 +251,13 @@ private extension MetadataDecoder {
}

struct UnkeyedContainer: UnkeyedDecodingContainer {
var count: Int? { components.count }
var isAtEnd: Bool { currentIndex == components.endIndex }
var count: Int? {
components.count
}

var isAtEnd: Bool {
currentIndex == components.endIndex
}

let components: [Substring]
let codingPath: [CodingKey]
Expand Down Expand Up @@ -393,7 +403,9 @@ private extension MetadataDecoder {
}

struct UnkeyedDecoder: Decoder {
var userInfo: [CodingUserInfoKey: Any] { [:] }
var userInfo: [CodingUserInfoKey: Any] {
[:]
}

let components: [Substring]
var codingPath: [CodingKey]
Expand Down Expand Up @@ -503,7 +515,9 @@ private extension MetadataDecoder {
}

struct SingleValueDecoder: Decoder {
var userInfo: [CodingUserInfoKey: Any] { [:] }
var userInfo: [CodingUserInfoKey: Any] {
[:]
}

let value: String
let codingPath: [CodingKey]
Expand Down
20 changes: 7 additions & 13 deletions Sources/Saga/Saga.docc/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,31 +209,25 @@ It's also easy to add your own readers and renderers; search for [saga-plugin](h
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 --watch [folder] --output [output-folder]
$ saga dev
```

Use the same relative input- and output folders as you gave to Saga. The `--watch` option can be specified multiple times to watch multiple folders. Example:
By default this watches the `content` and `Sources` folders, outputs to `deploy`, and serves on port 3000. All of these can be customized:

```
$ swift run watch --watch content --watch Sources --output deploy
$ saga dev --watch content --watch Sources --output deploy --port 3000
```

This will rebuild whenever you change your content or your Swift code.

You can also ignore certain files or folders using glob patterns:

```
$ swift run watch --watch content --output deploy --ignore "*.tmp" --ignore "drafts/*"
$ saga dev --ignore "*.tmp" --ignore "drafts/*"
```

For backwards compatibility, the legacy positional argument syntax is also supported:
To just build the site without starting a server:

```
$ swift run watch content Sources deploy
$ saga build
```

This functionality depends on a globally installed [browser-sync](https://github.com/BrowserSync/browser-sync), and only works on macOS, not Linux.

```
$ pnpm install -g browser-sync
```
See <doc:Installation> for how to install the `saga` CLI.
Loading