feat: Initial working site, with single network article.

This commit is contained in:
2025-04-02 14:24:40 -04:00
parent e66f29f910
commit 74147f8b67
16 changed files with 629 additions and 3015 deletions

View File

@@ -5,58 +5,72 @@ import PathKit
import SagaParsleyMarkdownReader
import SagaSwimRenderer
func permalink(item: Item<ArticleMetadata>) {
// Insert the publication year into the permalink.
// If the `relativeDestination` was "articles/looking-for-django-cms/index.html", then it becomes "articles/2009/looking-for-django-cms/index.html"
var components = item.relativeDestination.components
components.insert("\(Calendar.current.component(.year, from: item.date))", at: 1)
item.relativeDestination = Path(components: components)
}
func removingBreaks<M>(item: Item<M>) {
// remove explicit <br /> from items that show up likely due to how prettier formats
// markdown files inside of neovim.
item.body = item.body.removeBreaks
}
@main
struct Run {
static func main() async throws {
// 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.
// // Furthermore we are only interested in public articles.
// .register(
// folder: "articles",
// metadata: ArticleMetadata.self,
// readers: [.parsleyMarkdownReader],
// itemProcessor: sequence(removingBreaks, publicationDateInFilename, permalink),
// filter: \.public,
// writers: [
// .itemWriter(swim(renderArticle)),
// .listWriter(swim(renderArticles)),
// .tagWriter(swim(renderTag), tags: \.metadata.tags),
// .yearWriter(swim(renderYear)),
// // Atom feed for all articles, and a feed per tag
// .listWriter(
// atomFeed(
// title: SiteMetadata.name,
// author: SiteMetadata.author,
// baseURL: SiteMetadata.url,
// summary: \.metadata.summary
// ),
// output: "feed.xml"
// ),
// .tagWriter(
// atomFeed(
// title: SiteMetadata.name,
// author: SiteMetadata.author, baseURL: SiteMetadata.url, summary: \.metadata.summary
// ),
// output: "tag/[key]/feed.xml",
// tags: \.metadata.tags
// )
// ]
// )
// // 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],
// itemProcessor: removingBreaks,
// 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()
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.
// Furthermore we are only interested in public articles.
.register(
folder: "articles",
metadata: ArticleMetadata.self,
readers: [.parsleyMarkdownReader],
itemProcessor: sequence(removingBreaks, publicationDateInFilename, permalink),
filter: \.public,
writers: [
.itemWriter(swim(renderArticle)),
.listWriter(swim(renderArticles)),
.tagWriter(swim(renderTag), tags: \.metadata.tags),
.yearWriter(swim(renderYear)),
// Atom feed for all articles, and a feed per tag
.listWriter(
atomFeed(
title: SiteMetadata.name,
author: SiteMetadata.author,
baseURL: SiteMetadata.url,
summary: \.metadata.summary
),
output: "feed.xml"
),
.tagWriter(
atomFeed(
title: SiteMetadata.name,
author: SiteMetadata.author, baseURL: SiteMetadata.url, summary: \.metadata.summary
),
output: "tag/[key]/feed.xml",
tags: \.metadata.tags
)
]
)
// 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],
itemProcessor: removingBreaks,
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

@@ -24,7 +24,7 @@ func baseLayout(
body(class: "bg-page text-white pb-5 font-avenir \(section.rawValue)") {
siteHeader(section)
div(class: "container pt-12 lg:pt-28") {
div(class: "container") {
children()
}
@@ -40,7 +40,7 @@ private func siteHeader(_ section: Section) -> Node {
div(class: "header__logo") {
a(href: "/") {
div(class: "logo") {
"mhoush.com"
"docs.housh.dev"
}
}
}
@@ -59,44 +59,27 @@ private func siteHeader(_ section: Section) -> Node {
}
private func footer(_ rssLink: String) -> Node {
div(class: "site-footer text-gray gray-links border-t border-light text-center pt-6 mt-8 text-sm") {
p {
div(class: "site-footer text-slate-200 border-t border-light text-center pt-6 mt-8 text-sm") {
div {
"Copyright © Michael Housh 2023-\(Date().description.prefix(4))."
}
p {
p(class: "mb-2") {
"Built in Swift using"
a(href: "https://github.com/loopwerk/Saga", rel: "nofollow", target: "_blank") { "Saga" }
a(
class: "text-orange-400 [&:hover]:border-b border-green-400",
href: "https://github.com/loopwerk/Saga",
rel: "nofollow",
target: "_blank"
) { "Saga" }
"("
%a(href: "https://github.com/m-housh/mhoush.com", rel: "nofollow", target: "_blank") { "source" }
%a(
class: "[&:hover]:border-b border-green-400",
href: "https://github.com/m-housh/mhoush.com",
rel: "nofollow",
target: "_blank"
) { "source" }
%")."
}
p {
a(
href: "\(SiteMetadata.url.absoluteString)/articles/\(rssLink)feed.xml",
rel: "nofollow",
target: "_blank"
) { "RSS" }
" | "
a(href: "https://github.com/m-housh", rel: "nofollow", target: "_blank") { "Github" }
" | "
a(
href: "https://www.youtube.com/channel/UCb58SeURd5bObfTiL0KoliA",
rel: "nofollow",
target: "_blank"
) { "Youtube" }
" | "
a(href: "https://www.facebook.com/michael.housh", rel: "nofollow", target: "_blank") { "Facebook" }
" | "
a(href: "mailto:michael@mhoush.com", rel: "nofollow") { "Email" }
}
p {
span {
"All articles are licensed under Creative-Commons (CC BY-NC) 4.0"
}
a(href: "https://creativecommons.org/licenses/by-nc/4.0/") {
img(class: "justify-center", src: "/static/images/by-nc.png", width: "100")
}
}
script(src: "https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js")
script(src: "https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/keep-markup/prism-keep-markup.min.js")
script(src: "https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js")

View File

@@ -14,7 +14,7 @@ func tagPrefix(index: Int, totalTags: Int) -> Node {
}
func renderArticleInfo(_ article: Item<ArticleMetadata>) -> Node {
div(class: "text-gray gray-links text-sm") {
div(class: "text-slate-400 gray-links text-sm mb-8") {
span(class: "border-r border-gray pr-2 mr-2") {
article.date.formatted("MMMM dd, yyyy")
}
@@ -27,7 +27,7 @@ func renderArticleInfo(_ article: Item<ArticleMetadata>) -> Node {
Node.raw("""
<i class="fa fa-home"></i>
"""),
%a(class: "text-orange [&:hover]:border-b border-green", href: "/articles/tag/\(tag.slugified)/") {
%a(class: "text-orange-400 [&:hover]:border-b border-green-400", href: "/articles/tag/\(tag.slugified)/") {
tag
}
])
@@ -100,12 +100,12 @@ func renderArticle(context: ItemRenderingContext<ArticleMetadata>) -> Node {
title: context.item.title,
extraHeader: generateHeader(.article(context.item))
) {
article(class: "prose") {
article(class: "prose pt-8") {
h1 { context.item.title }
div(class: "-mt-6") {
div {
renderArticleInfo(context.item)
}
img(alt: "banner", src: context.item.imagePath)
// img(alt: "banner", src: context.item.imagePath)
Node.raw(context.item.body)
}
@@ -135,23 +135,6 @@ func renderArticle(context: ItemRenderingContext<ArticleMetadata>) -> Node {
}
}
}
// Giscus comment section.
commentSection
div(class: "border-t border-light mt-8 pt-8") {
h2(class: "text-4xl font-extrabold mb-8") { "Author" }
div(class: "flex flex-col lg:flex-row gap-8") {
div(class: "flex-[0_0_120px]") {
img(class: "w-[120px] h-[120px] rounded-full", src: "/static/images/avatar.png")
}
div(class: "prose") {
h3(class: "!m-0") { SiteMetadata.author }
p(class: "text-gray") { SiteMetadata.summary }
}
}
}
}
}
@@ -171,26 +154,3 @@ func renderArticleForGrid(article: Item<ArticleMetadata>) -> Node {
}
}
}
private var commentSection: Node {
div(class: "border-t border-light pt-8") {
Node.raw("""
<script src="https://giscus.app/client.js"
data-repo="m-housh/mhoush.com"
data-repo-id="R_kgDOJagAXA"
data-category="Article Discussions"
data-category-id="DIC_kwDOJagAXM4CnLfv"
data-mapping="pathname"
data-strict="0"
data-reactions-enabled="1"
data-emit-metadata="0"
data-input-position="bottom"
data-theme="preferred_color_scheme"
data-lang="en"
data-loading="lazy"
crossorigin="anonymous"
async>
</script>
""")
}
}

View File

@@ -27,7 +27,7 @@ func renderPage(context: ItemRenderingContext<PageMetadata>) -> Node {
func renderHome(body: String) -> Node {
div {
img(alt: "Avatar", class: "my-24 w-[315px] h-200px mx-auto", src: "/static/images/avatar.png")
// img(alt: "Avatar", class: "my-24 w-[315px] h-200px mx-auto", src: "/static/images/avatar.png")
div(class: "my-24 uppercase font-avenir text-[40px] leading-[1.25] font-thin text-center [&>h1>strong]:font-bold") {
Node.raw(body)