4.3 KiB
Spine — Mustache Template Rendering
Replaces inline HTML generation with Mustache template rendering via
spine-render. The mockup-A mustache template becomes the canonical
index page template. A view-model builder function transforms spine-books
plists into the template's expected context shape.
Architecture
spine.org ──→ [spine-books] ──→ book plists
│
[spine-index-model] ──→ view model (ht)
│
templates/index.mustache ───────────┼──→ [spine-render] ──→ HTML
Template
spine-mockup-a-agenda.mustache moves to templates/index.mustache.
The template already contains:
- Pico CSS v2.1.1 CDN link
- Tabler Icons CDN link
- All custom CSS in
<style>block (agenda layout, pills, detail, dark mode) - Mustache sections:
{{#groups}},{{#books}},{{#detail}},{{#notes}},{{#minibuffer}} minibuffersection kept in the template but not populated (empty context → section omitted)
View model
A new function spine-index-model takes the book plists and an optional
selected-id, returns an ht hash table shaped for the template:
(spine-index-model books &optional selected-id) → ht
{
"app_title": "spine",
"total_books": 5,
"reading_count": 1,
"groups": [
{
"label": "on deck",
"count": 3,
"books": [
{
"format_icon": "ti-book",
"status_class": "want",
"status_label": "want",
"title": "The Left Hand of Darkness",
"author": "Ursula K. Le Guin",
"tags_inline": null,
"rec_via": "Alex",
"rating_display": null,
"selected": false,
"detail": null
}
]
}
]
}
Field mappings
| Plist key | Template field | Transform |
|---|---|---|
:status |
groups[].label |
WANT→"on deck", READING→"reading", READ→"read", ABANDONED→"abandoned" |
:status |
status_class / status_label |
lowercased |
:format |
format_icon |
hardcover→ti-book, ebook→ti-device-tablet, audiobook→ti-headphones, nil→"" |
:tags |
tags_inline |
("scifi" "literary") → ":scifi:literary:" (string), nil→nil |
:rec_by |
rec_via |
direct string, nil→nil |
:rating |
rating_display |
"5"→"★★★★★", nil→nil |
:id = selected-id |
selected |
boolean |
:notes |
detail.notes |
list of {date, text} |
:added + :format |
detail.meta |
composite string |
Detail meta format
"started [2026-02-20] · hardcover" (WANT/READING)
"finished [2026-01-10] · ebook" (READ, use most recent LOGBOOK date)
"" (no dates)
Group ordering
on deck (WANT) → reading (READING) → read (READ) → abandoned (ABANDONED).
Zero-book groups omitted.
Handler
(defservlet index text/html (path query request)
(let ((books (spine-books)))
(if books
(insert (spine-render "index.mustache"
(spine-index-model books (cadr (assoc "id" query)))))
(insert (spine-render-empty-state)))))
spine-render-empty-state keeps its current Pico-styled inline HTML
(no Mustache needed for a single static message).
Templates directory
spine-mockup-a-agenda.mustache → templates/index.mustache
spine-mockup-b-journal.mustache stays in root as a reference/mockup
for the future detail view.
Files changed
| File | Change |
|---|---|
templates/index.mustache |
New — moved from root mockup |
spine.el |
Add spine-index-model, update handler, remove old spine-render-index |
spine-mockup-a-agenda.mustache |
Deleted (moved) |
Out of scope
- Populating the
minibuffersection (capture UI) - Mockup B / journal detail view
- Cover image display
Acceptance
- Index page renders via Mustache, not inline HTML
- Pico CSS + Tabler Icons CDN links present
- Groups: on deck, reading, read — correct order, zero-book groups omitted
- Format shown as Tabler icon glyph
- Status pills have distinct colors per state
?id=...expands matching book detail- Recommendation appears as "via {name}"
- ERT tests still pass (model layer unchanged)