A board_list is the multi-section grouped list used by the root / of
this site. Visually it looks like several board cards stacked
top-to-bottom, one per section. Unlike board_group,
the sections are virtual labels stored inline, not real sub-folders, and
the items in each section are direct children of this resource’s own
folder. Rendering lives in src/content/board_list.rs.
For each Section { name, desc_raw, items }, the page renders one
render_board_card(...) call (the same helper that board
uses):
title = section.name.get(lang)desc_md = section.desc_raw -> OptionLangDict -> get(lang) (markdown-compiled by the helper)children = section’s items resolved to (slug, Header, folder) against the board_list’s own folderhref_prefix = "" and view_all_href = None — there is no separate section index page to view, so no trailing link.The resource’s own name / desc (from its Header) are stored but not
rendered on the page itself. They exist so that, when the root site /
points to this board_list, the navbar / <title> / SEO tags still know what
to call the site. On /, that name is what shows in the page title.
This is the live root ctx.json for this site (from
programfiles/content/ctx.json):
{
"type": "board_list",
"name": {
"en": "FDS Documents",
"ja": "FDS 文書",
"zh": "FDS 文档"
},
"content": [
{
"name": "FDS",
"items": [
"policies",
"cooperation",
"news",
"doc"
]
},
{
"name": "Events",
"desc": {
"en": "Time-bound community events."
},
"items": [
"minecraft_server"
]
}
]
}
| Field | Type | Required | Notes |
|---|---|---|---|
type | "board_list" | yes | Exact discriminator string. |
name | string or LangDict | yes | The page’s own header. On /, this is what the navbar and <title> show. See Common: Header and LangDict. |
desc | string or LangDict | no | Stored on the header; not rendered on the page itself. |
icon | string | no | Same resolution rules as board; used by a parent that references this board_list (or in the favicon path on /). |
banner | string or LangDict | no | Stored but not rendered by board_list — present for parity with the Header model. |
content | array of section objects | yes | Each section: { name, desc?, items: [slug, ...] }. See below. |
content semantics — section objectsFor board_list (and only board_list / board_group variants), content
is not a flat slug list. Each entry is an object:
{
"name": "FDS",
"desc": { "en": "..." },
"items": ["policies", "cooperation", "news", "doc"]
}
name — string or LangDict, used as the section card title. Stored as a
LangDict (via From<Value>) so all the normal language fallback rules
apply.desc — string or LangDict, optional. Stored raw as desc_raw: Value so
we can preserve the on-disk shape across save/load round-trips; rendered
via OptionLangDict::get(lang) and markdown-compiled by
render_board_card.items — array of slug strings. Each slug must match a sub-folder of the
board_list’s own folder (not a section sub-folder; sections aren’t
folders).BoardList::read(path) reads ctx.json, builds Header, and walks
ctx.content[] building a Vec<Section>. For each section it pulls
name (as LangDict), desc_raw (as raw Value), and string items.BoardList::render skips the banner (this type does not render banners)
and emits one render_board_card(...) per section. For each item slug,
the child’s ctx.json is read; missing/unparseable -> Header::fallback(slug).<self_url>/<item_slug>/. No “View all”
link.The edit route supports a richer surface here, because sections are first-class:
rename(lang, new_name) — rename the board_list’s own header (visible on / in title bar).set_icon(icon) — empty clears.set_desc(lang, desc) — empty clears that lang.add_section(lang, name) — pushes a fresh Section with a LangDict name set in lang, empty desc_raw, no items.rename_section(idx, lang, name) — per-lang section rename.set_section_desc(idx, lang, desc) — promotes desc_raw from None or plain-string to a dict (preserving the existing default-language value), then sets the per-lang entry.remove_section(idx), move_section(from, to) — section list ops; out-of-range -> Err(io::Error{InvalidInput}).add_item_to_section(section_idx, slug), remove_item_from_section(section_idx, item_idx), move_item(section_idx, from, to) — per-section item ops; out-of-range -> Err(io::Error{InvalidInput}).There is no clear-all-langs path on set_section_desc (unlike the Header’s
set_desc); to clear a section desc you currently have to write empty
strings into every language slot or hand-edit ctx.json.
BoardList::save() rebuilds ctx.json from scratch:
let mut ctx = object!({});
ctx.set("type", "board_list");
self.header.write_into(&mut ctx);
let mut content_val = object!([]);
for sec in &self.sections {
let mut sec_val = object!({});
sec_val.set("name", sec.name.value().clone());
if !matches!(sec.desc_raw, Value::None) {
sec_val.set("desc", sec.desc_raw.clone());
}
let mut items_val = object!([]);
for slug in &sec.items {
items_val.push(slug.clone().into());
}
sec_val.set("items", items_val);
content_val.push(sec_val);
}
ctx.set("content", content_val);
The desc_raw field is round-tripped verbatim: if the user provided a
plain-string desc, it’s saved as a plain string; if it was promoted to a
dict by set_section_desc, it’s saved as a dict. The None case is
omitted entirely (no empty "desc" key on disk).
board_group.Err(io::Error{InvalidInput}) on
out-of-range — they do not panic and do not silently no-op.name defaults to a plain string when first set
(add_section writes {lang: name} directly, so the LangDict already
starts as a dict).name is what the navbar and <title> show on /.
Empty it out at your peril.banner on the Header is parsed but not rendered by board_list —
parity-only.