A modern Swift 6 framework for building fast, type-safe static websites.
SwiftSite replaces stagnated tooling (Publish, Plot, Ink) with idiomatic Swift 6: a result-builder pipeline, an any Component HTML DSL, AST-based Markdown parsing, and first-class retina and dark mode image support. Your site's content schema, sections, and theme are all Swift types — no configuration files, no runtime reflection.
- macOS 15+
- Swift 6
Add SwiftSite to your Package.swift:
dependencies: [
.package(url: "https://github.com/your-org/swift-site.git", from: "1.0.0"),
],
targets: [
.executableTarget(
name: "MySite",
dependencies: [
.product(name: "SwiftSiteCore", package: "swift-site"),
.product(name: "SwiftSiteHTML", package: "swift-site"),
.product(name: "SwiftSiteMarkdown", package: "swift-site"),
]
),
]1. Define your site
// MySite.swift
import SwiftSiteCore
struct MySite: Site {
var url = URL(string: "https://example.com")!
var name = "My Site"
}
enum MySiteSection: String, SiteSection {
case blog
case about
}
struct MyMetadata: ItemMetadataProtocol {
var featured: Bool?
var author: String?
}2. Build a theme
// MyTheme.swift
import SwiftSiteCore
import SwiftSiteHTML
struct MyTheme: Theme {
typealias S = MySite
func makeIndexPage(index: SiteIndex<MySite>, context: GenerationContext<MySite>) -> HTMLDocument {
HTMLDocument {
TitleTag(context.site.name)
} body: {
H1("Welcome to \(context.site.name)")
UL {
for item in context.allItems {
LI { Link(item.title, url: item.path.absoluteString) }
}
}
}
}
// ... makeItemPage, makeSectionPage, makeStandalonePage
}3. Wire up the pipeline
// Main.swift
import SwiftSiteCore
@main
struct MySiteApp: SiteApplication {
@OptionGroup var options: SiteOptions
var site: MySite { MySite() }
var pipeline: Pipeline<MySite> {
Pipeline {
CopyResources()
ParseMarkdownFiles()
SortItems(by: \.date, order: .descending)
GenerateHTML(theme: MyTheme())
GenerateSiteMap()
}
}
}4. Write content
Content/
├── index.md
└── blog/
├── index.md
└── hello-world.md
---
title: Hello, World!
date: 2024-06-01
featured: true
---
Your content here.5. Generate
swift run MySiteApp --content Content --output Output| Module | Purpose |
|---|---|
SwiftSiteCore |
Content model, pipeline engine, built-in steps, SiteApplication |
SwiftSiteHTML |
HTML DSL — Component protocol, result builder, all standard elements |
SwiftSiteMarkdown |
Markdown + YAML parsing, plugin system |
Bootstrap |
Bootstrap 5 utility DSL for themes |
The pipeline engine. Define a Site, declare a Pipeline of PipelineStep values, and run it via the SiteApplication protocol.
Built-in steps:
| Step | Description |
|---|---|
GenerateRetinaImages() |
Downscales @3x images to @2x and @1x using sips |
CopyResources() |
Copies Resources/ into the output directory |
ParseMarkdownFiles(plugins:) |
Parses all .md files, decodes frontmatter, renders Markdown |
SortItems(by:order:) |
Sorts all items by a key path |
GenerateHTML(theme:) |
Renders every page via your Theme |
GenerateSiteMap() |
Writes sitemap.xml |
A composable HTML DSL built on any Component existentials and @HTMLBuilder. Every HTML element is a named Swift type; attributes are applied with fluent modifiers:
Article {
H2 { Link(item.title, url: item.path.absoluteString) }
Paragraph(item.description).class("text-muted")
RawHTML(item.body.html)
}
.class("card", "mb-4")
.id("post-\(item.path.lastComponent)")Retina and dark mode images:
// Retina only — serves @1x, @2x, @3x via srcset
RetinaPicture(url: "/images/hero@3x.png", alt: "Hero image")
// Retina + dark mode
RetinaPicture(url: "/images/hero@3x.png", alt: "Hero image", includeDark: true)
// With inner <img> modifier
RetinaPicture(url: "/images/hero@3x.png") { img in
img.data("aos", value: "fade-left")
}Provide hero@3x.png and (optionally) hero-dark@3x.png in Resources/ — GenerateRetinaImages produces the @2x and @1x variants automatically.
Parses Markdown using Apple's swift-markdown (AST-based, CommonMark + tables + strikethrough). YAML frontmatter is decoded with Yams.
Markdown plugins intercept specific node types during rendering:
ParseMarkdownFiles(plugins: [
RetinaImagePlugin(includeDark: true), // @3x → <picture> with srcset
HeadingAnchorPlugin(), // adds id= slugs to headings
])Implement MarkdownPlugin to customise image, heading, or code block rendering.
Pass values down the component tree without threading parameters manually:
extension EnvironmentKeys {
var currentPath: EnvironmentKey<String> { .init(defaultValue: "") }
}
// Set at the page level
Nav { /* links */ }.environmentValue(item.path.absoluteString, for: \.currentPath)
// Read anywhere in the tree
struct NavLink: BodyComponent {
@EnvironmentValue(\.currentPath) var currentPath
let title: String; let url: String
var body: any Component {
Link(title, url: url).class("active", when: url == currentPath)
}
}struct RemoveDrafts<S: Site>: PipelineStep {
var name: String { "Remove drafts" }
func run(context: inout GenerationContext<S>) async throws {
for id in S.SectionID.allCases {
context.sections[id]?.items.removeAll {
($0.metadata as? DraftMetadata)?.draft == true
}
}
}
}Full DocC documentation is available in Xcode — open the package and choose Product → Build Documentation.
Conceptual articles:
- Getting Started
- Defining Your Site
- Writing Content
- Building a Theme
- Pipeline Steps
- Writing Custom Steps
SwiftSite is available under the MIT license. See LICENSE for details.