feat: Initial commit.
All checks were successful
Create and publish a Docker image / build-and-push-image (push) Successful in 2m50s

This commit is contained in:
2025-12-12 13:13:14 -05:00
commit 0c6b84a872
24 changed files with 1187 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
import Foundation
import Saga
/// Represents constants about the site.
enum SiteMetadata {
#if DEBUG
static let url = URL(string: "http://localhost:8080")!
#else
static let url = URL(string: "https://mhoush.com")!
#endif
static let name = "mhoush"
static let author = "Michael Housh"
/// Summary used for metadata / twitter card for home page,
/// also displayed at bottom of articles.
static let summary = """
Test saga tables.
"""
/// The default twitter image when linking to home page.
static let twitterImage = "/static/images/home-twitter-image.png"
}
/// Represents valid metadata for the files that are not an `article`.
struct PageMetadata: Metadata {
/// The section of the website for the file.
let section: String?
}

View File

@@ -0,0 +1,9 @@
/// Represents different sections of the website.
///
/// This is used to render base layouts appropriately for the given section.
enum Section: String {
/// The home page of the site.
case home
/// The articles / blog posts of the site.
case table
}

26
Sources/Site/run.swift Normal file
View File

@@ -0,0 +1,26 @@
import Foundation
import HTML
import PathKit
@preconcurrency import Saga
import SagaParsleyMarkdownReader
import SagaSwimRenderer
@main
struct Run {
static func main() async throws {
try await Saga(input: "content", output: "deploy")
// All the remaining markdown files will be parsed to html,
// using the default EmptyMetadata as the Item's metadata type.
.register(
metadata: PageMetadata.self,
readers: [.parsleyMarkdownReader],
itemWriteMode: .keepAsFile, // need to keep 404.md as 404.html, not 404/index.html
writers: [.itemWriter(swim(renderPage))]
)
// 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()
}
}

View File

@@ -0,0 +1,53 @@
import Foundation
import HTML
/// The base page layout used to render the different sections of the website.
///
/// - Parameters:
/// - conocicalURL: The url for the page.
/// - section: The section of the page.
/// - title: The page title.
/// - rssLink: A prefix for generating an rss feed for the page (generally only used for articles).
/// - extraHeader: Any extra items to be placed in the `head` of the html.
func baseLayout(
canocicalURL: String,
section: Section,
title pageTitle: String,
rssLink: String = "",
extraHeader: NodeConvertible = Node.fragment([]),
@NodeBuilder children: () -> NodeConvertible
) -> Node {
return [
.documentType("html"),
html(lang: "en-US") {
generateHeader(pageTitle, extraHeader)
body(class: "bg-page text-white pb-5 font-avenir \(section.rawValue)") {
div(class: "content") {
children()
}
footer()
}
},
]
}
private func footer() -> Node {
div(class: "site-footer text-gray gray-links border-t border-light text-center pt-6 mt-8 text-sm")
{
p {
"Copyright © Michael Housh 2023-\(Date().description.prefix(4))."
}
p {
"Built in Swift using"
a(href: "https://github.com/loopwerk/Saga", rel: "nofollow", target: "_blank") { "Saga" }
}
}
}
private func generateHeader(_ pageTitle: String, _ extraHeader: NodeConvertible) -> Node {
head {
meta(charset: "utf-8")
title { SiteMetadata.name + ": \(pageTitle)" }
link(href: "/static/style.css", rel: "stylesheet")
}
}

View File

@@ -0,0 +1,36 @@
import HTML
import Saga
func renderPage(context: ItemRenderingContext<PageMetadata>) -> Node {
let section = Section(rawValue: context.item.metadata.section ?? "")
assert(section != nil)
return baseLayout(
canocicalURL: context.item.url,
section: section!,
title: context.item.title
) {
switch section {
case .home:
renderHome(body: context.item.body)
default:
renderNonHome(body: context.item.body)
}
}
}
func renderHome(body: String) -> Node {
div {
Node.raw(body)
}
}
func renderNonHome(body: String) -> Node {
div {
article {
div {
Node.raw(body)
}
}
}
}