From c7b42ce7aaf5f17e7a29a13e917845af26707e66 Mon Sep 17 00:00:00 2001 From: Spencer Brower <6729162+sbrow@users.noreply.github.com> Date: Thu, 16 Jul 2026 17:48:41 -0400 Subject: [PATCH] refactor: Simplified content.odin. --- TODOS.md | 6 ++++ content.odin | 85 ++++++++++++++++++++++------------------------------ feed.odin | 26 ++++++++-------- main.odin | 5 ++-- render.odin | 83 +++++++++++++++++++++++++------------------------- site.odin | 1 + 6 files changed, 99 insertions(+), 107 deletions(-) diff --git a/TODOS.md b/TODOS.md index 2c1c584..99966eb 100644 --- a/TODOS.md +++ b/TODOS.md @@ -3,12 +3,17 @@ - [ ] Content-hash fingerprinting for CSS and JS cache busting - [ ] Performance - [ ] See if we can disable bounds checks in `write_indented` and elsewhere. + - [ ] Instead of loading the site fresh each time in watch mode, create a + `reload_site` proc, that just updates changed resources. - [ ] mustache data keys for opengraph, etc. +- [ ] Infer page types / collections rather than having dedicated procs for + everything - [ ] Block attributes on code fences (`{ #ex-1 }`) — hello-world.md - [ ] include-code shortcode (`{{< include-code ... >}}`) — i-ported-fd-to-odin - [ ] ensure sidenote numbers render in display order and not in declaration order. - [ ] OpenGraph meta tags — verify all fields match production site - [ ] Add Opt-in deflist support. +- [ ] We need to be able to do `Year_Section` in a non-magical, unpriviledged way. - [ ] set opengraph tags / description automatically if unset. (Like hugo does) - [ ] Table of contents support. - [ ] Nav items should be active when the current page is selected. @@ -29,6 +34,7 @@ - [ ] filesystem poll loop - [ ] event based - [ ] Free cmark HTML output (`body_html`) — cmark allocates via C malloc, not the arena, so it leaks per iteration in watch mode +- [ ] Split `load_page` into frontmatter-parse + body-process phases so draft pages can skip the markdown pipeline entirely - [ ] commands - [ ] `build` alias of default - [ ] `init` set up new project diff --git a/content.odin b/content.odin index 91dbb6f..0fa1ef4 100644 --- a/content.odin +++ b/content.odin @@ -8,7 +8,7 @@ import "core:os" import "core:strings" Page_Type :: enum { - Standalone, + Page, Post, Home, } @@ -23,53 +23,30 @@ Page :: struct { draft: bool, is_starred: bool, menu: string, - body: string, body_html: string, bundle_dir: string, } -// walk_content reads the content directory and returns all non-draft pages -// (or all pages if include_drafts is true). -// -// TODO: What is the lifetime of pages? -walk_content :: proc(site: ^Site) -> []Page { - content_path := site.content_dir - include_drafts := .Drafts in site.features - ext := site.markdown_extensions +// site_load_content reads the content directory and populates site.pages. +// Drafts are excluded unless .Drafts is enabled. +site_load_content :: proc(site: ^Site) { + site.pages = make([dynamic]Page, 0, 8, site_allocator(site)) - allocator := site_allocator(site) - - pages := make([dynamic]Page, allocator) - - collect_home(&pages, content_path, ext) - collect_standalone(&pages, content_path, ext) - - posts_path := fmt.tprintf("%s/posts", content_path) - if os.exists(posts_path) { - collect_posts(&pages, posts_path, ext) - } - - if include_drafts { - return pages[:] - } else { - filtered := make([dynamic]Page, allocator) - for &page in pages { - if !page.draft { - append(&filtered, page) - } - } - delete(pages) - return filtered[:] - } + load_homepage(site) + load_pages(site) + load_posts(site) } -collect_home :: proc(pages: ^[dynamic]Page, content_path: string, ext: bit_set[Markdown_Extension]) { +load_homepage :: proc(site: ^Site) { + content_path := site.content_dir + ext := site.markdown_extensions + html_path := fmt.tprintf("%s/index.html", content_path) if os.exists(html_path) { page, ok := load_page(html_path, .Home, "", ext) - if ok { + if ok && (!page.draft || .Drafts in site.features) { page.permalink = "/" - append(pages, page) + append(&site.pages, page) } return } @@ -77,14 +54,17 @@ collect_home :: proc(pages: ^[dynamic]Page, content_path: string, ext: bit_set[M md_path := fmt.tprintf("%s/index.md", content_path) if os.exists(md_path) { page, ok := load_page(md_path, .Home, "", ext) - if ok { + if ok && (!page.draft || .Drafts in site.features) { page.permalink = "/" - append(pages, page) + append(&site.pages, page) } } } -collect_standalone :: proc(pages: ^[dynamic]Page, content_path: string, ext: bit_set[Markdown_Extension]) { +load_pages :: proc(site: ^Site) { + content_path := site.content_dir + ext := site.markdown_extensions + entries, err := os.read_all_directory_by_path(content_path, context.allocator) if err != nil { log.warnf("thor: cannot read %s: %v", content_path, err) @@ -104,14 +84,20 @@ collect_standalone :: proc(pages: ^[dynamic]Page, content_path: string, ext: bit } slug := strip_extension(entry.name) - page, ok := load_page(entry.fullpath, .Standalone, slug, ext) - if ok { - append(pages, page) + page, ok := load_page(entry.fullpath, .Page, slug, ext) + if ok && (!page.draft || .Drafts in site.features) { + append(&site.pages, page) } } } -collect_posts :: proc(pages: ^[dynamic]Page, posts_path: string, ext: bit_set[Markdown_Extension]) { +load_posts :: proc(site: ^Site) { + posts_path := fmt.tprintf("%s/posts", site.content_dir) + if !os.exists(posts_path) { + return + } + + ext := site.markdown_extensions entries, err := os.read_all_directory_by_path(posts_path, context.allocator) if err != nil { log.warnf("thor: cannot read %s: %v", posts_path, err) @@ -127,8 +113,8 @@ collect_posts :: proc(pages: ^[dynamic]Page, posts_path: string, ext: bit_set[Ma } slug := strip_extension(entry.name) page, ok := load_page(entry.fullpath, .Post, slug, ext) - if ok { - append(pages, page) + if ok && (!page.draft || .Drafts in site.features) { + append(&site.pages, page) } case .Directory: index_path := fmt.tprintf("%s/index.html", entry.fullpath) @@ -139,9 +125,9 @@ collect_posts :: proc(pages: ^[dynamic]Page, posts_path: string, ext: bit_set[Ma continue } page, ok := load_page(index_path, .Post, entry.name, ext) - if ok { + if ok && (!page.draft || .Drafts in site.features) { page.bundle_dir = entry.fullpath - append(pages, page) + append(&site.pages, page) } case .Undetermined, .Symlink, .Named_Pipe, .Socket, .Block_Device, .Character_Device: } @@ -177,7 +163,6 @@ load_page :: proc( page.draft = fm.draft page.is_starred = fm.isStarred page.menu = fm.menu - page.body = strings.clone(body) if strings.has_suffix(file_path, ".html") { page.body_html = strings.clone(body) @@ -212,7 +197,7 @@ load_page :: proc( page.permalink = "/" case .Post: page.permalink = fmt.aprintf("/posts/%s/", slug) - case .Standalone: + case .Page: page.permalink = fmt.aprintf("/%s/", slug) } diff --git a/feed.odin b/feed.odin index b76cc3d..64fb01a 100644 --- a/feed.odin +++ b/feed.odin @@ -4,7 +4,7 @@ import "core:fmt" import "core:strings" import "core:time" -generate_rss :: proc(pages: []Page, config: Site) -> string { +generate_rss :: proc(site: ^Site) -> string { sb := strings.builder_make() strings.write_string(&sb, fmt.aprintf( @@ -16,13 +16,13 @@ generate_rss :: proc(pages: []Page, config: Site) -> string { %s en-us `, - xml_escape(config.title), - config.base_url, - xml_escape(config.description), - config.base_url, + xml_escape(site.title), + site.base_url, + xml_escape(site.description), + site.base_url, )) - for page in pages { + for page in site.pages { if page.type == .Home { continue } @@ -42,10 +42,10 @@ generate_rss :: proc(pages: []Page, config: Site) -> string { `, xml_escape(page.title), - config.base_url, + site.base_url, page.permalink, pub_date, - config.base_url, + site.base_url, page.permalink, xml_escape(page.body_html), )) @@ -55,26 +55,26 @@ generate_rss :: proc(pages: []Page, config: Site) -> string { return strings.to_string(sb) } -generate_sitemap :: proc(pages: []Page, base_url: string) -> string { +generate_sitemap :: proc(site: ^Site) -> string { sb := strings.builder_make() strings.write_string(&sb, ` `) - for page in pages { + for page in site.pages { lastmod := "" if page.date != "" { lastmod = fmt.aprintf("%s", page.date) } strings.write_string(&sb, fmt.aprintf( - "%s%s%s\n", base_url, page.permalink, lastmod, + "%s%s%s\n", site.base_url, page.permalink, lastmod, )) } // Posts list page posts_lastmod := "" - for page in pages { + for page in site.pages { if page.type == .Post && page.date > posts_lastmod { posts_lastmod = page.date } @@ -84,7 +84,7 @@ generate_sitemap :: proc(pages: []Page, base_url: string) -> string { posts_lm = fmt.aprintf("%s", posts_lastmod) } strings.write_string(&sb, fmt.aprintf( - "%s/posts/%s\n", base_url, posts_lm, + "%s/posts/%s\n", site.base_url, posts_lm, )) strings.write_string(&sb, "") diff --git a/main.odin b/main.odin index 1497be5..b47a3c0 100644 --- a/main.odin +++ b/main.odin @@ -45,8 +45,8 @@ main :: proc() { // TODO: Make it so this isn't necessary context.allocator = site_allocator(&site) - pages := walk_content(&site) - render_site(pages, site) + site_load_content(&site) + render_site(&site) if !(.Watch in site.features) { break @@ -72,3 +72,4 @@ when SPALL { spall._buffer_end(&spall_ctx, &spall_buffer) } } + diff --git a/render.odin b/render.odin index 829e4bc..50cad52 100644 --- a/render.odin +++ b/render.odin @@ -126,13 +126,14 @@ render_template :: proc( return result } -render_site :: proc(pages: []Page, config: Site) { +render_site :: proc(site: ^Site) { + pages := site.pages[:] sort_pages_by_date(pages) // Load shared resources once - partials := load_partials(config.layouts_dir) - partials["base"] = load_template(config.layouts_dir, "base.html") - post_tpl := load_template(config.layouts_dir, "post.html") + partials := load_partials(site.layouts_dir) + partials["base"] = load_template(site.layouts_dir, "base.html") + post_tpl := load_template(site.layouts_dir, "post.html") now, ok := time.time_to_datetime(time.now()) assert(ok) @@ -140,11 +141,11 @@ render_site :: proc(pages: []Page, config: Site) { // Build base data once base := Base_Data { now = now, - author = config.author, - params = config.params, - og_site_name = config.title, - og_description = config.description, - og_image = fmt.tprintf("%s/avatar.jpg", config.base_url), + author = site.author, + params = site.params, + og_site_name = site.title, + og_description = site.description, + og_image = fmt.tprintf("%s/avatar.jpg", site.base_url), } // Find home page @@ -163,56 +164,56 @@ render_site :: proc(pages: []Page, config: Site) { if page.type == .Home { continue } - html := render_page_html(page, config, post_tpl, partials, base) - if .Minify in config.features { + html := render_page_html(page, site, post_tpl, partials, base) + if .Minify in site.features { html = minify_html(html) } - write_page(config.output_dir, page.permalink, html) + write_page(site.output_dir, page.permalink, html) } // Render home page if has_home { - home_tpl := load_template(config.layouts_dir, "home.html") - home_html := render_home_html(home, pages, config, home_tpl, partials, base) - if .Minify in config.features { + home_tpl := load_template(site.layouts_dir, "home.html") + home_html := render_home_html(home, site, home_tpl, partials, base) + if .Minify in site.features { home_html = minify_html(home_html) } - write_file(fmt.tprintf("%s/index.html", config.output_dir), home_html) + write_file(fmt.tprintf("%s/index.html", site.output_dir), home_html) } // Render posts list page - posts_tpl := load_template(config.layouts_dir, "posts_list.html") - posts_html := render_posts_html(pages, config, posts_tpl, partials, base) - if .Minify in config.features { + posts_tpl := load_template(site.layouts_dir, "posts_list.html") + posts_html := render_posts_html(site, posts_tpl, partials, base) + if .Minify in site.features { posts_html = minify_html(posts_html) } - write_page(config.output_dir, "/posts/", posts_html) + write_page(site.output_dir, "/posts/", posts_html) // Generate RSS feed - rss := generate_rss(pages, config) - write_file(fmt.tprintf("%s/index.xml", config.output_dir), rss) + rss := generate_rss(site) + write_file(fmt.tprintf("%s/index.xml", site.output_dir), rss) // Generate sitemap - sitemap := generate_sitemap(pages, config.base_url) - write_file(fmt.tprintf("%s/sitemap.xml", config.output_dir), sitemap) + sitemap := generate_sitemap(site) + write_file(fmt.tprintf("%s/sitemap.xml", site.output_dir), sitemap) // Copy and optionally minify assets directory - copy_assets_dir(config.assets_dir, config.output_dir, config.features) + copy_assets_dir(site.assets_dir, site.output_dir, site.features) // Generate robots.txt - robots := fmt.aprintf("User-agent: *\nAllow: /\nSitemap: %s/sitemap.xml\n", config.base_url) - write_file(fmt.tprintf("%s/robots.txt", config.output_dir), robots) + robots := fmt.aprintf("User-agent: *\nAllow: /\nSitemap: %s/sitemap.xml\n", site.base_url) + write_file(fmt.tprintf("%s/robots.txt", site.output_dir), robots) total := len(pages) + 1 if !has_home { total += 1 } - fmt.printfln("Rendered %d pages to %s", total, config.output_dir) + fmt.printfln("Rendered %d pages to %s", total, site.output_dir) } render_page_html :: proc( page: Page, - config: Site, + site: ^Site, content_tpl: mustache.Template, partials: map[string]mustache.Template, base: Base_Data, @@ -221,13 +222,13 @@ render_page_html :: proc( data := Page_Data { base = base, } - data.title = fmt.tprintf("%s | %s", page.title, config.title) + data.title = fmt.tprintf("%s | %s", page.title, site.title) data.page_title = page.title data.body = page.body_html data.date_iso = page.date data.date_display = format_date(page.date) data.is_post = is_article - data.og_url = fmt.tprintf("%s%s", config.base_url, page.permalink) + data.og_url = fmt.tprintf("%s%s", site.base_url, page.permalink) data.og_title = strip_html_tags(page.title) data.og_type = og_type(is_article) data.is_article = is_article @@ -238,15 +239,14 @@ render_page_html :: proc( render_home_html :: proc( home: Page, - pages: []Page, - config: Site, + site: ^Site, content_tpl: mustache.Template, partials: map[string]mustache.Template, base: Base_Data, ) -> string { list_pages := make([dynamic]Page_Context) defer delete(list_pages) - for page in pages { + for page in site.pages { if page.type == .Home { continue } @@ -256,19 +256,18 @@ render_home_html :: proc( data := Home_Data { base = base, } - data.title = config.title + data.title = site.title data.body = home.body_html data.list_pages = list_pages - data.og_url = fmt.tprintf("%s/", config.base_url) - data.og_title = config.title + data.og_url = fmt.tprintf("%s/", site.base_url) + data.og_title = site.title data.og_type = "website" data.is_article = false return render_template(content_tpl, data, partials) } render_posts_html :: proc( - pages: []Page, - config: Site, + site: ^Site, content_tpl: mustache.Template, partials: map[string]mustache.Template, base: Base_Data, @@ -276,7 +275,7 @@ render_posts_html :: proc( year_sections := make([dynamic]Year_Section) defer delete(year_sections) current_year := "" - for page in pages { + for page in site.pages { if page.type != .Post { continue } @@ -291,9 +290,9 @@ render_posts_html :: proc( data := Posts_Data { base = base, } - data.title = fmt.tprintf("Posts | %s", config.title) + data.title = fmt.tprintf("Posts | %s", site.title) data.year_sections = year_sections - data.og_url = fmt.tprintf("%s/posts/", config.base_url) + data.og_url = fmt.tprintf("%s/posts/", site.base_url) data.og_title = "Posts" data.og_type = "website" data.is_article = false diff --git a/site.odin b/site.odin index d999081..f8f9ea6 100644 --- a/site.odin +++ b/site.odin @@ -12,6 +12,7 @@ import "core:strings" // Site is the primary workhorse. Site :: struct { arena: mem.Dynamic_Arena, + pages: [dynamic]Page, title: string, description: string, author: string,