Type: singlemd

A singlemd resource is a single markdown page. One ctx.json plus one .md file per language. This is the workhorse type — every news post, every plain documentation page is a singlemd. Implementation: src/content/singlemd.rs.

Purpose

Render one markdown body inside the standard page chrome (breadcrumb h1, navbar, footer). The body is compiled to HTML via pulldown_cmark with tables, smart-punctuation, and heading-attributes enabled (see fop::compile_markdown in src/content/fop.rs).

ctx.json shape

LangDict form — each language has its own markdown file:

{
    "type": "singlemd",
    "name": {
        "en": "Reading the Docs",
        "ja": "閲覧ガイド",
        "zh": "阅读指南"
    },
    "desc": {
        "en": "How to browse FDS Documents.",
        "ja": "FDS 文書の閲覧方法。",
        "zh": "如何浏览 FDS 文档。"
    },
    "file": {
        "en": "user_en",
        "ja": "user_ja",
        "zh": "user_zh"
    }
}

String form — every language reads the same .md (useful when content is not localised):

{
    "type": "singlemd",
    "name": { "en": "Release notes 2025.02.01" },
    "file": "release_2025_02_01"
}

Field reference

FieldTypeRequiredNotes
typestring — must be "singlemd"yesDiscriminator parsed in src/content.rs::render.
nameLangDict (string or {en,ja,zh})yesPage title. See Common: Header and LangDict.
iconstringoptionalURL or path. Cleared on save when empty.
descOptionLangDictoptionalUsed as meta description and OG description.
bannerOptionLangDictoptionalURL of an image rendered above the body via with_banner.
filestring OR LangDictyesFilename stem (no .md) of the markdown body.

file semantics:

  • String form — all languages read <file>.md.
  • LangDict form — language X reads <file[X]>.md. Missing language entries fall back to the default-language entry (LangDict::get behaviour).
  • The slug must be safe to interpolate into a path; in practice always [A-Za-z0-9_-]+.

File-on-disk layout

<resource_folder>/
    ctx.json
    <file>.md             ← string form
    <file_en>.md          ← LangDict form
    <file_ja>.md
    <file_zh>.md

Markdown is read by fop::read_markdown_file, which joins programfiles/content/ + resource folder + <slug>.md and compiles. Missing files render as an empty string (silently).

Render flow

  1. content.rs::render reads ctx.json, sees "singlemd", calls SingleMd::read(path).
  2. SingleMd::render resolves the active language via crate::lang_for(req).
  3. md_content(lang) reads <file_for_lang>.md and compiles it.
  4. The HTML is wrapped in <div class="container-func"><div class="markdown-content">…</div></div> and spliced into general.html by the page-level wrapper (banner, breadcrumb, SEO meta).

Editor capabilities

?view=edit (see Route: edit) lets you:

  • Rename the page for the active language (SingleMd::rename).
  • Edit the markdown body for the active language (read_body / write_body). The body is written verbatim to <slug>.md via fop::write_text_file.
  • Set/clear icon, desc via the Header editor (see Common: Header and LangDict).

The active language is the navbar dropdown’s current selection. To edit the Japanese body, switch to JA first, then enter edit mode.

Markdown rule reminder

Use ## and ### only — never #. The page’s <h1> is supplied by the breadcrumb’s last segment (see breadcrumb_split in src/content.rs). A stray # in the body produces a second h1 and tanks SEO.

Download behavior

singlemd supports ?view=download and returns the raw .md file for the active language with Content-Type: text/markdown; charset=UTF-8. See Route: download.

Save semantics

SingleMd::save():

  1. Reads the existing ctx.json (preserves unknown keys).
  2. Forces type to "singlemd".
  3. Writes the Header (name, plus icon/desc/banner only when set).
  4. Writes the file field from the in-memory LangDict.
  5. JSON-encodes via fop::write_json_file.

Notably save() does not touch the .md files — those are written by write_body.

Gotchas / common edit mistakes

  • Forgetting to create the .md file after renaming the file slug. read_markdown_file returns an empty body silently — the page will render blank instead of erroring. Always rename or create the file on disk alongside the JSON edit.
  • Using # in the body. See “Markdown rule reminder” above.
  • Mixing string and dict form mid-edit. If you switch file from string to dict, ensure every language entry actually points to a real file. A missing language with no default-lang fallback renders blank.
  • save() preserves unknown ctx keys — fine for forward compatibility, but means a typo’d key (fiel instead of file) lingers in the file forever. Inspect the saved JSON if something looks wrong.
  • Path prefix. All fop helpers prepend programfiles/content/. Never call std::fs::write on a resource-relative path directly.