refactor: use ssg package and sync content from elmstatic
Replace ~200 lines of inline SSG logic (types, accessors, parsing, sorting, tags, file I/O) with imports from the new ssg package. Sync updated Lyceum article, images, snippet, and CSS fixes (h3/h4 font-bold, ol list-decimal, blockquote/li spacing) from blu-elmstatic. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
199
main.lux
199
main.lux
@@ -1,16 +1,10 @@
|
||||
import ssg
|
||||
import markdown
|
||||
import frontmatter
|
||||
import path
|
||||
|
||||
type SiteConfig =
|
||||
| SiteConfig(String, String, String, String, String, String, String)
|
||||
|
||||
type Page =
|
||||
| Page(String, String, String, String, String)
|
||||
|
||||
type TagEntry =
|
||||
| TagEntry(String, String, String, String, String)
|
||||
|
||||
fn loadConfig(path: String): SiteConfig with {File} = {
|
||||
let raw = File.read(path)
|
||||
let json = match Json.parse(raw) {
|
||||
@@ -96,88 +90,6 @@ fn cfgStaticDir(c: SiteConfig): String =
|
||||
SiteConfig(_, _, _, _, _, _, sd) => sd,
|
||||
}
|
||||
|
||||
fn pgDate(p: Page): String =
|
||||
match p {
|
||||
Page(d, _, _, _, _) => d,
|
||||
}
|
||||
|
||||
fn pgTitle(p: Page): String =
|
||||
match p {
|
||||
Page(_, t, _, _, _) => t,
|
||||
}
|
||||
|
||||
fn pgSlug(p: Page): String =
|
||||
match p {
|
||||
Page(_, _, s, _, _) => s,
|
||||
}
|
||||
|
||||
fn pgTags(p: Page): String =
|
||||
match p {
|
||||
Page(_, _, _, t, _) => t,
|
||||
}
|
||||
|
||||
fn pgContent(p: Page): String =
|
||||
match p {
|
||||
Page(_, _, _, _, c) => c,
|
||||
}
|
||||
|
||||
fn teTag(e: TagEntry): String =
|
||||
match e {
|
||||
TagEntry(t, _, _, _, _) => t,
|
||||
}
|
||||
|
||||
fn teTitle(e: TagEntry): String =
|
||||
match e {
|
||||
TagEntry(_, t, _, _, _) => t,
|
||||
}
|
||||
|
||||
fn teDate(e: TagEntry): String =
|
||||
match e {
|
||||
TagEntry(_, _, d, _, _) => d,
|
||||
}
|
||||
|
||||
fn teSlug(e: TagEntry): String =
|
||||
match e {
|
||||
TagEntry(_, _, _, s, _) => s,
|
||||
}
|
||||
|
||||
fn teSection(e: TagEntry): String =
|
||||
match e {
|
||||
TagEntry(_, _, _, _, s) => s,
|
||||
}
|
||||
|
||||
fn slugFromFilename(filename: String): String = path.stripExtension(filename)
|
||||
|
||||
fn formatDate(isoDate: String): String = {
|
||||
if String.length(isoDate) < 10 then isoDate else {
|
||||
let year = String.substring(isoDate, 0, 4)
|
||||
let month = String.substring(isoDate, 5, 7)
|
||||
let day = String.substring(isoDate, 8, 10)
|
||||
let monthName = if month == "01" then "Jan" else if month == "02" then "Feb" else if month == "03" then "Mar" else if month == "04" then "Apr" else if month == "05" then "May" else if month == "06" then "Jun" else if month == "07" then "Jul" else if month == "08" then "Aug" else if month == "09" then "Sep" else if month == "10" then "Oct" else if month == "11" then "Nov" else "Dec"
|
||||
year + " " + monthName + " " + day
|
||||
}
|
||||
}
|
||||
|
||||
fn basename(p: String): String = path.basename(p)
|
||||
|
||||
fn dirname(p: String): String = path.dirname(p)
|
||||
|
||||
fn sortInsert(sorted: List<Page>, item: Page): List<Page> = insertByDate(sorted, item)
|
||||
|
||||
fn sortByDateDesc(items: List<Page>): List<Page> = List.fold(items, [], sortInsert)
|
||||
|
||||
fn insertByDate(sorted: List<Page>, item: Page): List<Page> = {
|
||||
match List.head(sorted) {
|
||||
None => [item],
|
||||
Some(first) => if pgDate(item) >= pgDate(first) then List.concat([item], sorted) else match List.tail(sorted) {
|
||||
Some(rest) => List.concat([first], insertByDate(rest, item)),
|
||||
None => [first, item],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn convertMd(text: String): String = markdown.toHtml(text)
|
||||
|
||||
fn htmlHead(title: String, description: String): String = "<!doctype html><html lang=\"en\"><head>" + "<title>" + title + "</title>" + "<meta charset=\"utf-8\">" + "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">" + "<meta name=\"description\" content=\"" + description + "\">" + "<meta property=\"og:title\" content=\"" + title + "\">" + "<meta property=\"og:description\" content=\"" + description + "\">" + "<meta property=\"og:type\" content=\"website\">" + "<meta property=\"og:url\" content=\"https://blu.cx\">" + "<meta property=\"og:image\" content=\"https://blu.cx/images/social-card.png\">" + "<meta property=\"og:site_name\" content=\"Brandon Lucas\">" + "<meta name=\"twitter:card\" content=\"summary_large_image\">" + "<meta name=\"twitter:title\" content=\"" + title + "\">" + "<meta name=\"twitter:description\" content=\"" + description + "\">" + "<link rel=\"canonical\" href=\"https://blu.cx\">" + "<link rel=\"preload\" href=\"/fonts/EBGaramond-Regular.woff2\" as=\"font\" type=\"font/woff2\" crossorigin=\"\">" + "<link rel=\"preload\" href=\"/fonts/UnifrakturMaguntia-Regular.woff2\" as=\"font\" type=\"font/woff2\" crossorigin=\"\">" + "<link href=\"/styles.css\" rel=\"stylesheet\" type=\"text/css\">" + "<link href=\"/highlight/tokyo-night-dark.min.css\" rel=\"stylesheet\" type=\"text/css\">" + "<script src=\"/highlight/highlight.min.js\" defer=\"\"></script>" + "<script>document.addEventListener('DOMContentLoaded', function() \{ hljs.highlightAll(); \});</script>" + "</head>"
|
||||
|
||||
fn htmlNav(): String = "<a href=\"/\"><img src=\"/images/favicon.webp\" alt=\"Narsil Logo\" width=\"59\" height=\"80\"></a>"
|
||||
@@ -202,68 +114,26 @@ fn htmlSnippetCard(content: String): String = "<div class=\"flex flex-col gap-4
|
||||
|
||||
fn htmlTagPage(tagName: String, postsHtml: String): String = "<div class=\"page w-[80%] flex flex-col gap-8\">" + "<h1 class=\"text-4xl text-center font-bold w-full\">Tag: " + tagName + "</h1>" + "<div class=\"flex flex-col gap-4\">" + postsHtml + "</div></div>"
|
||||
|
||||
fn parseFile(path: String): Page with {File} = {
|
||||
let raw = File.read(path)
|
||||
let doc = frontmatter.parse(raw)
|
||||
let title = frontmatter.title(doc)
|
||||
let date = frontmatter.date(doc)
|
||||
let tags = frontmatter.getOrDefault(doc, "tags", "")
|
||||
let body = frontmatter.body(doc)
|
||||
let htmlContent = convertMd(body)
|
||||
let filename = basename(path)
|
||||
let slug = slugFromFilename(filename)
|
||||
Page(date, title, slug, tags, htmlContent)
|
||||
}
|
||||
|
||||
fn mapParseFiles(dir: String, files: List<String>): List<Page> with {File} =
|
||||
match List.head(files) {
|
||||
None => [],
|
||||
Some(filename) => {
|
||||
let page = parseFile(dir + "/" + filename)
|
||||
match List.tail(files) {
|
||||
Some(rest) => List.concat([page], mapParseFiles(dir, rest)),
|
||||
None => [page],
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
fn readSection(contentDir: String, section: String): List<Page> with {File} = {
|
||||
let dir = contentDir + "/" + section
|
||||
if File.exists(dir) then {
|
||||
let entries = File.readDir(dir)
|
||||
let mdFiles = List.filter(entries, fn(e: String): Bool => String.endsWith(e, ".md"))
|
||||
mapParseFiles(dir, mdFiles)
|
||||
} else []
|
||||
}
|
||||
|
||||
fn ensureDir(path: String): Unit with {File} = {
|
||||
if File.exists(path) then () else {
|
||||
let parent = dirname(path)
|
||||
if parent != "." then if parent != path then ensureDir(parent) else () else ()
|
||||
File.mkdir(path)
|
||||
}
|
||||
}
|
||||
|
||||
fn writePostPage(outputDir: String, section: String, page: Page, siteTitle: String, siteDesc: String): Unit with {File} = {
|
||||
let slug = pgSlug(page)
|
||||
let title = pgTitle(page)
|
||||
let date = pgDate(page)
|
||||
let tagsRaw = pgTags(page)
|
||||
let content = pgContent(page)
|
||||
fn writePostPage(outputDir: String, section: String, page: Post, siteTitle: String, siteDesc: String): Unit with {File} = {
|
||||
let slug = ssg.postSlug(page)
|
||||
let title = ssg.postTitle(page)
|
||||
let date = ssg.postDate(page)
|
||||
let tagsRaw = ssg.postTags(page)
|
||||
let content = ssg.postContent(page)
|
||||
let tags = if tagsRaw == "" then [] else String.split(tagsRaw, " ")
|
||||
let tagsHtml = renderTagLinks(tags)
|
||||
let formattedDate = formatDate(date)
|
||||
let formattedDate = ssg.formatDate(date)
|
||||
let body = htmlPostPage(title, formattedDate, tagsHtml, content)
|
||||
let pageTitle = title + " | " + siteTitle
|
||||
let html = htmlDocument(pageTitle, siteDesc, body)
|
||||
let dir = outputDir + "/posts/" + section + "/" + slug
|
||||
ensureDir(dir)
|
||||
ssg.ensureDir(dir)
|
||||
File.write(dir + "/index.html", html)
|
||||
}
|
||||
|
||||
fn writeSectionIndex(outputDir: String, section: String, pages: List<Page>, siteTitle: String, siteDesc: String): Unit with {File} = {
|
||||
let sorted = sortByDateDesc(pages)
|
||||
let postEntries = List.map(sorted, fn(page: Page): String => htmlPostEntry(pgTitle(page), formatDate(pgDate(page)), "/posts/" + section + "/" + pgSlug(page)))
|
||||
fn writeSectionIndex(outputDir: String, section: String, pages: List<Post>, siteTitle: String, siteDesc: String): Unit with {File} = {
|
||||
let sorted = ssg.sortByDateDesc(pages)
|
||||
let postEntries = List.map(sorted, fn(page: Post): String => htmlPostEntry(ssg.postTitle(page), ssg.formatDate(ssg.postDate(page)), "/posts/" + section + "/" + ssg.postSlug(page)))
|
||||
let postsHtml = String.join(postEntries, "
|
||||
")
|
||||
let sectionName = if section == "articles" then "Articles" else if section == "blog" then "Blog" else if section == "journal" then "Journal" else section
|
||||
@@ -271,36 +141,21 @@ fn writeSectionIndex(outputDir: String, section: String, pages: List<Page>, site
|
||||
let pageTitle = sectionName + " | " + siteTitle
|
||||
let html = htmlDocument(pageTitle, siteDesc, body)
|
||||
let dir = outputDir + "/posts/" + section
|
||||
ensureDir(dir)
|
||||
ssg.ensureDir(dir)
|
||||
File.write(dir + "/index.html", html)
|
||||
}
|
||||
|
||||
fn collectTagsForPage(section: String, page: Page): List<TagEntry> = {
|
||||
let tagsRaw = pgTags(page)
|
||||
let tags = if tagsRaw == "" then [] else String.split(tagsRaw, " ")
|
||||
List.map(tags, fn(tag: String): TagEntry => TagEntry(tag, pgTitle(page), pgDate(page), pgSlug(page), section))
|
||||
}
|
||||
|
||||
fn collectTags(section: String, pages: List<Page>): List<TagEntry> = {
|
||||
let nested = List.map(pages, fn(page: Page): List<TagEntry> => collectTagsForPage(section, page))
|
||||
List.fold(nested, [], fn(acc: List<TagEntry>, entries: List<TagEntry>): List<TagEntry> => List.concat(acc, entries))
|
||||
}
|
||||
|
||||
fn addIfUnique(acc: List<String>, e: TagEntry): List<String> = if List.any(acc, fn(t: String): Bool => t == teTag(e)) then acc else List.concat(acc, [teTag(e)])
|
||||
|
||||
fn getUniqueTags(entries: List<TagEntry>): List<String> = List.fold(entries, [], addIfUnique)
|
||||
|
||||
fn tagEntryToHtml(e: TagEntry): String = htmlPostEntry(teTitle(e), formatDate(teDate(e)), "/posts/" + teSection(e) + "/" + teSlug(e))
|
||||
fn tagEntryToHtml(e: TagEntry): String = htmlPostEntry(ssg.tagTitle(e), ssg.formatDate(ssg.tagDate(e)), "/posts/" + ssg.tagSection(e) + "/" + ssg.tagSlug(e))
|
||||
|
||||
fn writeOneTagPage(outputDir: String, tag: String, allTagEntries: List<TagEntry>, siteTitle: String, siteDesc: String): Unit with {File} = {
|
||||
let entries = List.filter(allTagEntries, fn(e: TagEntry): Bool => teTag(e) == tag)
|
||||
let entries = ssg.entriesForTag(allTagEntries, tag)
|
||||
let postsHtml = String.join(List.map(entries, tagEntryToHtml), "
|
||||
")
|
||||
let body = htmlTagPage(tag, postsHtml)
|
||||
let pageTitle = "Tag: " + tag + " | " + siteTitle
|
||||
let html = htmlDocument(pageTitle, siteDesc, body)
|
||||
let dir = outputDir + "/tags/" + tag
|
||||
ensureDir(dir)
|
||||
ssg.ensureDir(dir)
|
||||
File.write(dir + "/index.html", html)
|
||||
}
|
||||
|
||||
@@ -316,15 +171,15 @@ fn writeTagPagesLoop(outputDir: String, tags: List<String>, allTagEntries: List<
|
||||
},
|
||||
}
|
||||
|
||||
fn writeTagPages(outputDir: String, allTagEntries: List<TagEntry>, siteTitle: String, siteDesc: String): Unit with {File, Console} = {
|
||||
let uniqueTags = getUniqueTags(allTagEntries)
|
||||
fn writeTagPages(outputDir: String, allTagEntries: List<TagEntry>, siteTitle: String, siteDesc: String): Unit with {File} = {
|
||||
let uniqueTags = ssg.uniqueTags(allTagEntries)
|
||||
writeTagPagesLoop(outputDir, uniqueTags, allTagEntries, siteTitle, siteDesc)
|
||||
}
|
||||
|
||||
fn renderSnippetFile(snippetDir: String, filename: String): String with {File} = {
|
||||
let raw = File.read(snippetDir + "/" + filename)
|
||||
let doc = frontmatter.parse(raw)
|
||||
htmlSnippetCard(convertMd(frontmatter.body(doc)))
|
||||
htmlSnippetCard(markdown.toHtml(frontmatter.body(doc)))
|
||||
}
|
||||
|
||||
fn renderSnippets(snippetDir: String, files: List<String>): List<String> with {File} =
|
||||
@@ -364,7 +219,7 @@ fn writeHomePage(outputDir: String, contentDir: String, siteTitle: String, siteD
|
||||
File.write(outputDir + "/index.html", html)
|
||||
}
|
||||
|
||||
fn writeAllPostPages(outputDir: String, section: String, pages: List<Page>, siteTitle: String, siteDesc: String): Unit with {File} =
|
||||
fn writeAllPostPages(outputDir: String, section: String, pages: List<Post>, siteTitle: String, siteDesc: String): Unit with {File} =
|
||||
match List.head(pages) {
|
||||
None => (),
|
||||
Some(page) => {
|
||||
@@ -389,11 +244,11 @@ fn main(): Unit with {File, Console, Process} = {
|
||||
Console.print("Content: " + contentDir)
|
||||
Console.print("Output: " + outputDir)
|
||||
Console.print("")
|
||||
ensureDir(outputDir)
|
||||
ssg.ensureDir(outputDir)
|
||||
Console.print("Reading content...")
|
||||
let articles = readSection(contentDir, "articles")
|
||||
let blogPosts = readSection(contentDir, "blog")
|
||||
let journalPosts = readSection(contentDir, "journal")
|
||||
let articles = ssg.readSection(contentDir, "articles")
|
||||
let blogPosts = ssg.readSection(contentDir, "blog")
|
||||
let journalPosts = ssg.readSection(contentDir, "journal")
|
||||
Console.print(" Articles: " + toString(List.length(articles)))
|
||||
Console.print(" Blog posts: " + toString(List.length(blogPosts)))
|
||||
Console.print(" Journal entries: " + toString(List.length(journalPosts)))
|
||||
@@ -407,9 +262,9 @@ fn main(): Unit with {File, Console, Process} = {
|
||||
writeSectionIndex(outputDir, "blog", blogPosts, siteTitle, siteDesc)
|
||||
writeSectionIndex(outputDir, "journal", journalPosts, siteTitle, siteDesc)
|
||||
Console.print("Writing tag pages...")
|
||||
let articleTags = collectTags("articles", articles)
|
||||
let blogTags = collectTags("blog", blogPosts)
|
||||
let journalTags = collectTags("journal", journalPosts)
|
||||
let articleTags = ssg.collectTags("articles", articles)
|
||||
let blogTags = ssg.collectTags("blog", blogPosts)
|
||||
let journalTags = ssg.collectTags("journal", journalPosts)
|
||||
let allTags = List.concat(List.concat(articleTags, blogTags), journalTags)
|
||||
writeTagPages(outputDir, allTags, siteTitle, siteDesc)
|
||||
Console.print("Writing home page...")
|
||||
|
||||
Reference in New Issue
Block a user