Extending the server

Two big things contributors add: a new content type, and a new language. Both are mechanical once you know the touch-points; this chapter enumerates them.

Adding a new content type

A content type is a Rust struct that implements crate::content::File, plus the dispatch arms that route URLs and editor verbs to it. The default templates in src/content/board.rs and src/content/singlemd.rs are the canonical references — start by copying the closer of the two to your use case.

  1. Create src/content/<type>.rs with a struct (e.g. MyType { header: Header, ..., path: String }) that implements crate::content::File. The trait gives you read<T>(path: T) -> Self, header(&self) -> &Header, path(&self) -> String, render(&self, req: &mut HttpReqCtx) -> String, plus the default conveniences (name, icon, desc, path_value) backed by Header. The read impl is expected to call fop::split_path + fop::read_json_file on ctx.json and pass the resulting Value to Header::from_value; see src/content/any.rs for the minimal shape.

  2. Wire the module at the bottom of src/content.rs. Add pub mod <type>; and pub use <type>::MyType; next to the existing pub mod mdbook; / pub use mdbook::MdBook; lines. Other modules import the type through crate::content::MyType.

  3. Add a render arm in src/content.rs’s render(req, can_edit, can_manage_perms) function. The match on content_type.as_str() is where every type plugs in. Copy the "board" arm verbatim, rename the type, and keep every step: with_banner wraps the body, html_escape runs on name / description / canonical / og_image / breadcrumb fields, prefix_breadcrumb injects the /lang_code prefix when present, breadcrumb_split peels off the last item for the <h1>, breadcrumb_jsonld emits the schema.org block, pageprop_for re-localizes navbar/footer for the resolved language, and the call ends in akari_render!("general.html", ...). The shape is identical across types on purpose — the escaping discipline lives at this boundary and nowhere else.

  4. Mark the type as editable in create_sub_resource around line 584 of src/content.rs. The matcher

    "mdbook" | "singlemd" | "list" | "board" | "board_group" | "board_list" | "file" | "link"
    

    is the allowlist for what an editor “New sub-resource” form can create. Add your discriminator here or create_sub_resource will reject it with unsupported type: ....

  5. Add a default-template arm around line 607 of src/content.rs. After the name/type keys are set, the match type_str block writes the type-specific seed fields into the freshly created ctx.json. Folder-of-children types (mdbook | list | board | board_group | board_list) start with content: []; singlemd seeds file: {<lang>: <slug>} and writes an empty <slug>.md body; file seeds file: "" and download_only: false; link seeds url: "". Pick the closest shape and add an arm.

  6. Add an editor branch in src/routes/edit.rs if the type accepts edits. Two dispatch tables route everything: render_editor (GET form) at around line 187 and handle (POST/multipart) at around line 39. Add "<type>" => render_<type>(req, resource_path, &lang) to the first and "<type>" => handle_<type>(req, &resource_path, &lang, &action, &form_vals, &user) to the second, then implement both functions following the existing render_board / handle_board pair. The pair owns its own action vocabulary (save, add_child, rename, …) — keep names consistent with neighbouring types so the JS sends the right verb.

  7. Add a download branch in src/routes/download.rs if the type should support ?view=download. The current arms are singlemd (serves the .md directly), mdbook (zips every chapter into a single archive), and file (raw bytes with the original filename); the catch-all returns 400. Also update type_supports_download in src/content.rs so the general.html template renders the download button — its return value drives can_download.

  8. Document the new type with a chapter at programfiles/content/doc/dev/type_<name>.md and append {"name": "Type: <name>", "file": "type_<name>"} to the content array in programfiles/content/doc/dev/ctx.json.

Adding a new language

  1. Append the code to programfiles/op/support_lang.json. The first entry is the default language (currently "en") and is served at the bare URL with no prefix. Every other entry gets a prefix endpoint: adding "ko" makes /ko/<**path> a valid URL form.

  2. Translate navbar and footer. Add a <code> key to programfiles/op/navbar.json and programfiles/op/footer.json with translations for every existing label. Missing keys fall back to the default-language value via LangDict::get, so the page won’t break — but the UX is jarring (English Policy next to a Korean breadcrumb), so do this work properly.

  3. Rebuild. build.rs declares cargo:rerun-if-changed=programfiles/op/support_lang.json, so editing the file triggers a recompile on the next cargo build. The codegen at $OUT_DIR/lang_endpoints.rs emits endpoint! { APP.url("/<code>/<**path>"), pub view_<ident> <HTTP> { ... } } for every non-default code. There is no runtime hot-path that picks up new languages — a restart is mandatory.

  4. Backfill content name / desc dicts as needed. A node with name: "Foo" (string form) returns "Foo" for every language because LangDict treats a bare string as all-langs-same. To localize, convert to a dict: name: {"en": "Foo", "ko": "푸"}. Same for desc, banner, and file on singlemd / file nodes.

  5. Verify by hand on a deployed build:

    • /<code>/ renders the root with navbar, footer, and <html lang> in the new language.
    • hreflang alternates in <head> include the new code on every page (see hreflang_alternates in src/content.rs).
    • sitemap.xml includes per-<code> <xhtml:link> rows.
    • The language dropdown in the navbar offers the new code.
    • /op/switch_lang/<code> redirects correctly and sets the cookie.

Cross-link: Language system for the runtime side, SEO and sitemap for the per-page emission rules.