diff --git a/TODOS.md b/TODOS.md index 5ac13b2..91fee30 100644 --- a/TODOS.md +++ b/TODOS.md @@ -5,8 +5,6 @@ should be Hugo / go. - [ ] Deal with the favicon - [ ] Icon support. -- [ ] RSS -- [ ] sitemap - [ ] robots.txt? - [ ] Evaluate Tufte CSS — borrow sidenote CSS or replace TailwindCSS entirely - Option B: Steal Tufte's sidenote/margin-note CSS (adapt for dark theme), keep TailwindCSS diff --git a/feed.odin b/feed.odin new file mode 100644 index 0000000..f1db857 --- /dev/null +++ b/feed.odin @@ -0,0 +1,149 @@ +package main + +import "core:fmt" +import "core:strings" + +WEEKDAYS: [7]string = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"} + +generate_rss :: proc( + pages: []Page, + site_title: string, + site_desc: string, + base_url: string, +) -> string { + parts: [dynamic]string + defer delete(parts) + + append(&parts, fmt.aprintf( + ` + + +%s +%s/ +%s +en-us +`, + xml_escape(site_title), + base_url, + xml_escape(site_desc), + base_url, + )) + + for page in pages { + if page.type == .Home { + continue + } + + pub_date := "Mon, 01 Jan 0001 00:00:00 +0000" + if page.date != "" { + pub_date = format_rfc822(page.date) + } + + append(&parts, fmt.aprintf( + ` +%s +%s%s +%s +%s%s +%s + +`, + xml_escape(page.title), + base_url, + page.permalink, + pub_date, + base_url, + page.permalink, + xml_escape(page.body_html), + )) + } + + append(&parts, "\n") + + return strings.join(parts[:], "") +} + +generate_sitemap :: proc(pages: []Page, base_url: string) -> string { + parts: [dynamic]string + defer delete(parts) + + append( + &parts, + ` + +`, + ) + + for page in pages { + lastmod := "" + if page.date != "" { + lastmod = fmt.aprintf("%s", page.date) + } + append(&parts, fmt.aprintf( + "%s%s%s\n", + base_url, + page.permalink, + lastmod, + )) + } + + // Posts list page + posts_lastmod := "" + for page in pages { + if page.type == .Post && page.date > posts_lastmod { + posts_lastmod = page.date + } + } + posts_lm := "" + if posts_lastmod != "" { + posts_lm = fmt.aprintf("%s", posts_lastmod) + } + append(&parts, fmt.aprintf("%s/posts/%s\n", base_url, posts_lm)) + + append(&parts, "") + + return strings.join(parts[:], "") +} + +format_rfc822 :: proc(iso: string) -> string { + if len(iso) < 19 { + return iso + } + + year := (int(iso[0]) - 0x30) * 1000 + (int(iso[1]) - 0x30) * 100 + + (int(iso[2]) - 0x30) * 10 + (int(iso[3]) - 0x30) + month := (int(iso[5]) - 0x30) * 10 + (int(iso[6]) - 0x30) + day := (int(iso[8]) - 0x30) * 10 + (int(iso[9]) - 0x30) + time := iso[11:19] + + // Timezone: -04:00 → -0400 + tz := "+0000" + if len(iso) >= 25 && (iso[19] == '+' || iso[19] == '-') { + tz = fmt.tprintf("%c%s%s", iso[19], iso[20:22], iso[23:25]) + } + + // Sakamoto's method for day of week (0=Sunday) + t := [12]int{0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4} + y := year + if month < 3 { + y -= 1 + } + dow := (y + y / 4 - y / 100 + y / 400 + t[month - 1] + day) % 7 + + return fmt.aprintf( + "%s, %d %s %d %s %s", + WEEKDAYS[dow], + day, + MONTHS[month - 1], + year, + time, + tz, + ) +} + +xml_escape :: proc(s: string) -> string { + r, _ := strings.replace_all(s, "&", "&") + r, _ = strings.replace_all(r, "<", "<") + r, _ = strings.replace_all(r, ">", ">") + return r +} diff --git a/main.odin b/main.odin index 1508938..23a4b56 100644 --- a/main.odin +++ b/main.odin @@ -20,7 +20,7 @@ main :: proc() { pages := walk_content(opt.content_dir, opt.drafts) - render_site(pages, opt.content_dir, opt.output_dir) + render_site(pages, opt.content_dir, opt.output_dir, opt.base_url) } load_default_options :: proc(opt: ^Options) { @@ -31,6 +31,10 @@ load_default_options :: proc(opt: ^Options) { if opt.output_dir == "" { opt.output_dir = "./public" } + + if opt.base_url == "" { + opt.base_url = "http://localhost:8080" + } } print_summary :: proc(pages: []Page) { diff --git a/render.odin b/render.odin index ceb1100..2551c9e 100644 --- a/render.odin +++ b/render.odin @@ -9,7 +9,7 @@ MONTHS: [12]string = { "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", } -render_site :: proc(pages: []Page, content_path: string, output_dir: string) { +render_site :: proc(pages: []Page, content_path: string, output_dir: string, base_url: string) { sort_pages_by_date(pages) // Find home page and extract site title @@ -48,6 +48,16 @@ render_site :: proc(pages: []Page, content_path: string, output_dir: string) { posts_html := render_posts_html(pages, site_title) write_page(output_dir, "/posts/", posts_html) + // Generate RSS feed + if has_home { + rss := generate_rss(pages, site_title, home.description, base_url) + write_file(fmt.tprintf("%s/index.xml", output_dir), rss) + } + + // Generate sitemap + sitemap := generate_sitemap(pages, base_url) + write_file(fmt.tprintf("%s/sitemap.xml", output_dir), sitemap) + total := len(pages) + 1 if !has_home { total += 1