Type: board

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.

Rendering style

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).

ctx.json shape

{
    "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 reference

FieldTypeRequiredNotes
type"board"yesExact discriminator string.
namestring or LangDictyesSee Common: Header and LangDict.
descstring or LangDictnoMarkdown-compiled at render time. Empty / missing suppresses the description block entirely.
iconstringnoAbsolute 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).
contentarray of stringsyesSibling subdirectory slugs. Each must contain its own ctx.json.

content semantics

For 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.

Render flow

  1. 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.
  2. Board::render resolves each slug into a (slug, Header, folder) triple:
    • reads the child’s 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).
  3. It then delegates to render_board_card(&title, &desc_md, &children, "", None, &lang).

Editor capabilities

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.

Save

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.

Gotchas

  • 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.
  • The icon resolution chain (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.
  • The standalone-Board render passes href_prefix="" and view_all_href=None. The href_prefix / view_all_href parameters on render_board_card are for board_group reuse.