From 178b02da743df580a2a6be033b2cd46187ee658d Mon Sep 17 00:00:00 2001
From: Spencer Brower <6729162+sbrow@users.noreply.github.com>
Date: Tue, 28 Jul 2026 10:37:27 -0400
Subject: [PATCH] feat: Added `page` to template context.
---
AGENTS.md | 6 ++--
TODOS.md | 8 +++++
content.odin | 15 +++++++--
defaults/layouts/page.html | 2 +-
defaults/layouts/section_index.html | 2 +-
mustache/suggest_test.odin | 6 ++--
render.odin | 48 ++++++++---------------------
7 files changed, 42 insertions(+), 45 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index 9716b41..c2a3e14 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -220,7 +220,7 @@ Templates use Mustache with template inheritance (`{{
{{{{page_title}}
{{&content}}
+{{page.title}}
{{&content}}
{{/main}}
{{/base}}
```
@@ -240,7 +240,7 @@ Base_Data :: struct {
}
Page_Data :: struct {
using base: Base_Data, // fields promoted via reflection fallback
- page_title: string,
+ page.title: string,
date: string, // raw ISO 8601; formatted via `| format` in templates
}
Home_Data :: struct {
@@ -249,7 +249,7 @@ Home_Data :: struct {
}
Section_Data :: struct {
using base: Base_Data,
- page_title: string,
+ page.title: string,
posts: [dynamic]Page_Context, // flat list; year grouping done in template via pipe
}
```
diff --git a/TODOS.md b/TODOS.md
index fc990ee..e2a876a 100644
--- a/TODOS.md
+++ b/TODOS.md
@@ -8,6 +8,8 @@
- [ ] consider adding a limit to the context stack in mustache.
- [ ] better diagnostics for syntax errors in treesitter.
- [x] Add heading ids as a default on extension.
+- [ ] show "stack traces" in template error diagnostics
+- [ ] starred must be a param.
## Performance
@@ -32,6 +34,11 @@
- [ ] can markdown extensions run in parallel?
- [ ] enforce MAX_SLUG_LENGTH
+## Remove Privileged content
+
+- [ ] `group_by` currently requires a computed `year` field on the page.
+ - We should replace this with `{{ pages | group_by (date | "2006") }}` or similar
+
## Memory Management
@@ -145,6 +152,7 @@ main :: proc () {
- [ ] `new site` set up new project
- [ ] warn/error when unknown key used in mustache.
- [ ] Import/export packages. Hugo, jekyll, WordPress, etc.
+- [ ] opt-in "strict_keys" mode. in this mode, key lookups may not view parent objects.
## Notes
diff --git a/content.odin b/content.odin
index 1631d57..220c6f9 100644
--- a/content.odin
+++ b/content.odin
@@ -7,6 +7,7 @@ import "core:fmt"
import "core:log"
import "core:os"
import "core:strings"
+import "core:time"
// Fields with underscores should never be set by the user.
Page :: struct {
@@ -18,12 +19,13 @@ Page :: struct {
title: string,
description: string,
date: string,
+ year: string,
lastmod: string,
menu: string,
content: string,
og: Open_Graph,
draft: bool,
- is_starred: bool,
+ starred: bool,
_is_index: bool `private`,
}
@@ -231,9 +233,18 @@ load_page :: proc(
page.title = fm.title
page.description = fm.description
page.date = fm.date
+ if page.date == "" {
+ info, stat_err := os.stat(file_path, context.allocator)
+ if stat_err == nil {
+ page.date, _ = time.time_to_rfc3339(info.modification_time, 0, false, context.allocator)
+ os.file_info_delete(info, context.allocator)
+ log.warnf("no date in frontmatter for %s, using file modification time: %s", file_path, page.date)
+ }
+ }
+ page.year = get_year(page.date)
page.lastmod = fm.lastmod
page.draft = fm.draft
- page.is_starred = fm.isStarred
+ page.starred = fm.isStarred
page.menu = fm.menu
page.layout = fm.layout if fm.layout != "" else infer_layout(section, is_index)
page.og = fm.og
diff --git a/defaults/layouts/page.html b/defaults/layouts/page.html
index 75edc96..32006bf 100644
--- a/defaults/layouts/page.html
+++ b/defaults/layouts/page.html
@@ -2,7 +2,7 @@
{{$main}}
- {{page_title}}
+ {{page.title}}
{{#date_iso}}
{{/date_iso}} {{&content}}
diff --git a/defaults/layouts/section_index.html b/defaults/layouts/section_index.html
index 96b6dd2..d856c11 100644
--- a/defaults/layouts/section_index.html
+++ b/defaults/layouts/section_index.html
@@ -1,7 +1,7 @@
{{
- {{page_title}}
+ {{page.title}}
{{&content}}
{{#posts | group_by year}}
diff --git a/mustache/suggest_test.odin b/mustache/suggest_test.odin
index 3a168de..635ff3a 100644
--- a/mustache/suggest_test.odin
+++ b/mustache/suggest_test.odin
@@ -12,7 +12,7 @@ Inner :: struct {
Outer :: struct {
title: string,
- page_title: string,
+ page.title: string,
inner: Inner,
numbers: [3]int,
}
@@ -150,8 +150,8 @@ test_validate_map_path_silent :: proc(t: ^testing.T) {
@(test)
test_suggest_correction_exact :: proc(t: ^testing.T) {
- available := []string{"title", "page_title", "body"}
- testing.expect_value(t, suggest_correction(available, "page_titel"), "page_title")
+ available := []string{"title", "page.title", "body"}
+ testing.expect_value(t, suggest_correction(available, "page_titel"), "page.title")
}
@(test)
diff --git a/render.odin b/render.odin
index 40e8d84..0acbeb8 100644
--- a/render.odin
+++ b/render.odin
@@ -19,35 +19,14 @@ Template_Context :: struct {
og: Open_Graph,
date_format: string,
timezone: ^datetime.TZ_Region,
-
- // Page Data
- page_title: string,
- date: string,
+ page: Page,
// Home data
- pages: [dynamic]Page_Context,
+ pages: [dynamic]Page,
// Section Data
// TODO: Remove "posts" from the Odin code
- posts: [dynamic]Page_Context,
-}
-
-Page_Context :: struct {
- permalink: string,
- title: string,
- starred: bool,
- date: string,
- year: string,
-}
-
-build_page_context :: proc(page: Page) -> Page_Context {
- return Page_Context {
- permalink = page.permalink,
- title = page.title,
- starred = page.is_starred,
- date = page.date,
- year = get_year(page.date),
- }
+ posts: [dynamic]Page,
}
load_template :: proc(vfs: ^VFS, virtual_path: string) -> mustache.Template {
@@ -268,9 +247,8 @@ render_page_html :: proc(
) -> string {
ctx := ctx
ctx.title = fmt.tprintf("%s | %s", page.title, site.title)
- ctx.page_title = page.title
+ ctx.page = page
ctx.content = page.content
- ctx.date = page.date
ctx.og = og_for_page(site.og, page)
return render_template(content_tpl, ctx, partials)
}
@@ -282,13 +260,12 @@ render_home_html :: proc(
partials: map[string]mustache.Template,
ctx: Template_Context,
) -> string {
- list_pages := make([dynamic]Page_Context)
- defer delete(list_pages)
+ list_pages := make([dynamic]Page, 0, 8, context.temp_allocator)
for page in site.pages {
if page._is_index {
continue
}
- append(&list_pages, build_page_context(page))
+ append(&list_pages, page)
}
ctx := ctx
@@ -309,24 +286,25 @@ render_section :: proc(
partials: map[string]mustache.Template,
ctx: Template_Context,
) -> string {
- posts := make([dynamic]Page_Context)
- defer delete(posts)
+ posts := make([dynamic]Page, 0, len(site.pages) / 2, context.temp_allocator)
for page in site.pages {
if page.section != section || page._is_index {
continue
}
- append(&posts, build_page_context(page))
+ append(&posts, page)
}
ctx := ctx
if has_index {
ctx.content = section_index.content
- ctx.page_title = section_index.title
+ ctx.page = section_index
ctx.title = fmt.tprintf("%s | %s", section_index.title, site.title)
ctx.og = og_for_page(site.og, section_index)
} else {
- ctx.page_title = capitalize(section)
- ctx.title = fmt.tprintf("%s | %s", capitalize(section), site.title)
+ ctx.page = Page {
+ title = capitalize(section),
+ }
+ ctx.title = fmt.tprintf("%s | %s", ctx.page.title, site.title)
ctx.og.title = capitalize(section)
ctx.og.description = ""
ctx.og.url = fmt.tprintf("%s/%s/", site.base_url, section)