A board is the workhorse container: a single card with a heading, an
optional markdown blurb, and one row per child slug. Each row shows the
child’s icon, name, and one-line description. Rendering lives in
src/content/board.rs and is built on top of the shared
render_board_card helper, which is reused by board_group
and board_list.
A standalone Board renders one card (render_board_card with
href_prefix="" and view_all_href=None) wrapped in
<div class="mb-4 p-3 bg-body rounded shadow-sm">. The heading uses
<h5 class="border-bottom pb-2 mb-0">. Each child row is an <a href="slug/">
containing a 64x64 icon (see render_icon) plus <strong>name</strong> and
<small>desc</small>. The bottom border is dropped on the last row so the
card ends cleanly.
If desc is empty the second <div class="border-bottom pt-2 mb-0"> is not
emitted at all (no blank gap under the title).
{
"type": "board",
"name": {
"en": "Doc Server",
"ja": "文書サーバー",
"zh": "文档服务器"
},
"desc": {
"en": "Guides for using, administering, and developing this documentation server.",
"ja": "本文書サーバーの利用・運営・開発に関する案内。",
"zh": "本文档服务器的使用、管理与开发指南。"
},
"icon": "/images/svg/board/",
"content": ["user", "admin", "dev"]
}
| Field | Type | Required | Notes |
|---|---|---|---|
type | "board" | yes | Exact discriminator string. |
name | string or LangDict | yes | See Common: Header and LangDict. |
desc | string or LangDict | no | Markdown-compiled at render time. Empty / missing suppresses the description block entirely. |
icon | string | no | Absolute URL or /-prefixed path -> <img>; relative path -> read as inline SVG from the child’s folder; missing -> falls back to crate::content::default_icon() (from programfiles/op/config.json). |
content | array of strings | yes | Sibling subdirectory slugs. Each must contain its own ctx.json. |
content semanticsFor board (and list), content is a flat array of slug
strings. Each slug must match a sibling subdirectory of the board’s own
folder, and that subdirectory must contain a ctx.json. The parser is
tolerant: parse_slug_list in src/content/list.rs also accepts the legacy
{file: "slug"} shape, but new entries should always be plain strings.
Board::read(path) reads ctx.json via fop::read_json_file, builds a
Header via Header::from_value, and a slug Vec<String> via
parse_slug_list.Board::render resolves each slug into a (slug, Header, folder) triple:
ctx.json if present, otherwise calls
Header::fallback(slug) so a missing or unparseable child still renders
with the slug as its display name (instead of disappearing or 500ing).render_board_card(&title, &desc_md, &children, "", None, &lang).The edit route supports, against a Board:
rename(lang, new_name) — per-language rename, promotes a plain-string
name into a LangDict on first per-lang edit.set_icon(icon) — empty string clears.set_desc(lang, desc) — empty string clears that lang from desc; if it
was the last entry, desc is dropped from disk.add_card(slug), remove_card(idx), move_card(from, to) — manipulate
the slug list. Out-of-range indices return io::Error with
InvalidInput; the route surfaces that as a 4xx.There is no per-row icon/name/desc override on the parent: each row’s display
comes from the child’s own ctx.json (via Header::from_value). To change
how a child appears in its parent’s list, edit the child’s ctx.json.
Board::save() rebuilds ctx.json from scratch:
let mut ctx = object!({});
ctx.set("type", "board");
self.header.write_into(&mut ctx); // drops empty icon / desc
ctx.set("content", <slugs as JSON array>);
fop::write_json_file(ctx_path, &ctx);
The rebuild-from-scratch approach matters because Header::write_into only
writes optional fields when they are present. If we mutated the on-disk JSON
in place, a cleared icon or desc would silently linger.
remove_card / move_card return Err(io::Error{InvalidInput}) for
out-of-range indices — they do not panic and do not silently no-op.content is a string list, not an object list. A [{"file":"slug"}]
shape still parses (legacy compatibility in parse_slug_list), but anything
more exotic — for instance the section-object shape used by
board_list — will simply skip entries.header.icon -> default_icon() ->
nothing) means a missing per-row icon does not blow up: it draws the
site-wide default, or no icon at all.href_prefix="" and
view_all_href=None. The href_prefix / view_all_href parameters on
render_board_card are for board_group reuse.