feat: Added page to template context.

This commit is contained in:
Spencer Brower
2026-07-28 10:37:27 -04:00
parent e742832a4b
commit 178b02da74
7 changed files with 42 additions and 45 deletions
+3 -3
View File
@@ -220,7 +220,7 @@ Templates use Mustache with template inheritance (`{{<base}}` / `{{$block}}`):
<!-- page.html (content layout) --> <!-- page.html (content layout) -->
{{<base}} {{<base}}
{{$main}} {{$main}}
<main><article><h1>{{page_title}}</h1>{{&content}}</article></main> <main><article><h1>{{page.title}}</h1>{{&content}}</article></main>
{{/main}} {{/main}}
{{/base}} {{/base}}
``` ```
@@ -240,7 +240,7 @@ Base_Data :: struct {
} }
Page_Data :: struct { Page_Data :: struct {
using base: Base_Data, // fields promoted via reflection fallback 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 date: string, // raw ISO 8601; formatted via `| format` in templates
} }
Home_Data :: struct { Home_Data :: struct {
@@ -249,7 +249,7 @@ Home_Data :: struct {
} }
Section_Data :: struct { Section_Data :: struct {
using base: Base_Data, using base: Base_Data,
page_title: string, page.title: string,
posts: [dynamic]Page_Context, // flat list; year grouping done in template via pipe posts: [dynamic]Page_Context, // flat list; year grouping done in template via pipe
} }
``` ```
+8
View File
@@ -8,6 +8,8 @@
- [ ] consider adding a limit to the context stack in mustache. - [ ] consider adding a limit to the context stack in mustache.
- [ ] better diagnostics for syntax errors in treesitter. - [ ] better diagnostics for syntax errors in treesitter.
- [x] Add heading ids as a default on extension. - [x] Add heading ids as a default on extension.
- [ ] show "stack traces" in template error diagnostics
- [ ] starred must be a param.
## Performance ## Performance
@@ -32,6 +34,11 @@
- [ ] can markdown extensions run in parallel? - [ ] can markdown extensions run in parallel?
- [ ] enforce MAX_SLUG_LENGTH - [ ] 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 ## Memory Management
@@ -145,6 +152,7 @@ main :: proc () {
- [ ] `new site` set up new project - [ ] `new site` set up new project
- [ ] warn/error when unknown key used in mustache. - [ ] warn/error when unknown key used in mustache.
- [ ] Import/export packages. Hugo, jekyll, WordPress, etc. - [ ] Import/export packages. Hugo, jekyll, WordPress, etc.
- [ ] opt-in "strict_keys" mode. in this mode, key lookups may not view parent objects.
## Notes ## Notes
+13 -2
View File
@@ -7,6 +7,7 @@ import "core:fmt"
import "core:log" import "core:log"
import "core:os" import "core:os"
import "core:strings" import "core:strings"
import "core:time"
// Fields with underscores should never be set by the user. // Fields with underscores should never be set by the user.
Page :: struct { Page :: struct {
@@ -18,12 +19,13 @@ Page :: struct {
title: string, title: string,
description: string, description: string,
date: string, date: string,
year: string,
lastmod: string, lastmod: string,
menu: string, menu: string,
content: string, content: string,
og: Open_Graph, og: Open_Graph,
draft: bool, draft: bool,
is_starred: bool, starred: bool,
_is_index: bool `private`, _is_index: bool `private`,
} }
@@ -231,9 +233,18 @@ load_page :: proc(
page.title = fm.title page.title = fm.title
page.description = fm.description page.description = fm.description
page.date = fm.date 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.lastmod = fm.lastmod
page.draft = fm.draft page.draft = fm.draft
page.is_starred = fm.isStarred page.starred = fm.isStarred
page.menu = fm.menu page.menu = fm.menu
page.layout = fm.layout if fm.layout != "" else infer_layout(section, is_index) page.layout = fm.layout if fm.layout != "" else infer_layout(section, is_index)
page.og = fm.og page.og = fm.og
+1 -1
View File
@@ -2,7 +2,7 @@
{{$main}} {{$main}}
<main> <main>
<article> <article>
<h1>{{page_title}}</h1> <h1>{{page.title}}</h1>
{{#date_iso}} <time class="subtitle" datetime="{{date_iso}}">{{date_display}}</time> {{#date_iso}} <time class="subtitle" datetime="{{date_iso}}">{{date_display}}</time>
{{/date_iso}} {{&content}} {{/date_iso}} {{&content}}
</article> </article>
+1 -1
View File
@@ -1,7 +1,7 @@
{{<base}} {{<base}}
{{$main}} {{$main}}
<main> <main>
<h1>{{page_title}}</h1> <h1>{{page.title}}</h1>
{{&content}} {{&content}}
{{#posts | group_by year}} {{#posts | group_by year}}
<section> <section>
+3 -3
View File
@@ -12,7 +12,7 @@ Inner :: struct {
Outer :: struct { Outer :: struct {
title: string, title: string,
page_title: string, page.title: string,
inner: Inner, inner: Inner,
numbers: [3]int, numbers: [3]int,
} }
@@ -150,8 +150,8 @@ test_validate_map_path_silent :: proc(t: ^testing.T) {
@(test) @(test)
test_suggest_correction_exact :: proc(t: ^testing.T) { test_suggest_correction_exact :: proc(t: ^testing.T) {
available := []string{"title", "page_title", "body"} available := []string{"title", "page.title", "body"}
testing.expect_value(t, suggest_correction(available, "page_titel"), "page_title") testing.expect_value(t, suggest_correction(available, "page_titel"), "page.title")
} }
@(test) @(test)
+13 -35
View File
@@ -19,35 +19,14 @@ Template_Context :: struct {
og: Open_Graph, og: Open_Graph,
date_format: string, date_format: string,
timezone: ^datetime.TZ_Region, timezone: ^datetime.TZ_Region,
page: Page,
// Page Data
page_title: string,
date: string,
// Home data // Home data
pages: [dynamic]Page_Context, pages: [dynamic]Page,
// Section Data // Section Data
// TODO: Remove "posts" from the Odin code // TODO: Remove "posts" from the Odin code
posts: [dynamic]Page_Context, posts: [dynamic]Page,
}
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),
}
} }
load_template :: proc(vfs: ^VFS, virtual_path: string) -> mustache.Template { load_template :: proc(vfs: ^VFS, virtual_path: string) -> mustache.Template {
@@ -268,9 +247,8 @@ render_page_html :: proc(
) -> string { ) -> string {
ctx := ctx ctx := ctx
ctx.title = fmt.tprintf("%s | %s", page.title, site.title) ctx.title = fmt.tprintf("%s | %s", page.title, site.title)
ctx.page_title = page.title ctx.page = page
ctx.content = page.content ctx.content = page.content
ctx.date = page.date
ctx.og = og_for_page(site.og, page) ctx.og = og_for_page(site.og, page)
return render_template(content_tpl, ctx, partials) return render_template(content_tpl, ctx, partials)
} }
@@ -282,13 +260,12 @@ render_home_html :: proc(
partials: map[string]mustache.Template, partials: map[string]mustache.Template,
ctx: Template_Context, ctx: Template_Context,
) -> string { ) -> string {
list_pages := make([dynamic]Page_Context) list_pages := make([dynamic]Page, 0, 8, context.temp_allocator)
defer delete(list_pages)
for page in site.pages { for page in site.pages {
if page._is_index { if page._is_index {
continue continue
} }
append(&list_pages, build_page_context(page)) append(&list_pages, page)
} }
ctx := ctx ctx := ctx
@@ -309,24 +286,25 @@ render_section :: proc(
partials: map[string]mustache.Template, partials: map[string]mustache.Template,
ctx: Template_Context, ctx: Template_Context,
) -> string { ) -> string {
posts := make([dynamic]Page_Context) posts := make([dynamic]Page, 0, len(site.pages) / 2, context.temp_allocator)
defer delete(posts)
for page in site.pages { for page in site.pages {
if page.section != section || page._is_index { if page.section != section || page._is_index {
continue continue
} }
append(&posts, build_page_context(page)) append(&posts, page)
} }
ctx := ctx ctx := ctx
if has_index { if has_index {
ctx.content = section_index.content 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.title = fmt.tprintf("%s | %s", section_index.title, site.title)
ctx.og = og_for_page(site.og, section_index) ctx.og = og_for_page(site.og, section_index)
} else { } else {
ctx.page_title = capitalize(section) ctx.page = Page {
ctx.title = fmt.tprintf("%s | %s", capitalize(section), site.title) title = capitalize(section),
}
ctx.title = fmt.tprintf("%s | %s", ctx.page.title, site.title)
ctx.og.title = capitalize(section) ctx.og.title = capitalize(section)
ctx.og.description = "" ctx.og.description = ""
ctx.og.url = fmt.tprintf("%s/%s/", site.base_url, section) ctx.og.url = fmt.tprintf("%s/%s/", site.base_url, section)