A link resource is a redirect node in the doc tree. Visiting its URL 302s to an external target. Inside a parent list or board it renders as a normal row (icon + name + desc) — clicking the row navigates to the target. Implementation: src/content/link.rs.
Surface external resources inside the FDS doc tree without copying their content. Typical uses: cooperation-partner links, upstream specs, social-media or chat-platform URLs that belong in the navigation but aren’t ours to host.
{
"type": "link",
"name": {
"en": "Anthropic",
"ja": "Anthropic",
"zh": "Anthropic"
},
"desc": {
"en": "Maker of Claude.",
"zh": "Claude 的开发商。"
},
"icon": "/static/icons/anthropic.png",
"url": "https://www.anthropic.com/"
}
| Field | Type | Required | Notes |
|---|---|---|---|
type | string — must be "link" | yes | Discriminator. |
name | LangDict | yes | Display label. See Common: Header and LangDict. |
icon | string | optional | URL or path. Cleared on save when empty. |
desc | OptionLangDict | optional | Used as meta description and shown by parent list/board views. |
banner | OptionLangDict | optional | Banner image URL — only relevant on the fallback view (see “Render flow”). |
url | string | yes | Target URL. Single string, not a LangDict. Stored at the top level as url (not target). |
Note: url is a plain string. The implementation in link.rs calls ctx.get("url").string() and stores it on Link::target — there is no per-language URL machinery. If you need different URLs per language, model it as separate link resources (or, more commonly, use a parent list to switch on language).
<resource_folder>/
ctx.json
That’s it. No .md, no body, no extra files.
The dispatcher (content.rs::render, "link" arm) handles link specially:
Link::read.url (target) is non-empty — return a 302 redirect to the URL via redirect_response. The page chrome is never rendered.url is empty — render the fallback body:<div class="alert alert-warning my-3">No URL configured for this link.</div>
This is intentional: an empty url is a misconfiguration, and the warning surfaces it to whoever opened the page (typically an admin). Normal visitors never see the fallback because a properly-configured link always 302s before render() is reached.
When target is non-empty but render() is somehow invoked (e.g. inside a parent list’s row rendering), the fallback path emits <p>Redirecting to <a href="{url}">{url}</a>...</p>.
?view=edit (see Route: edit) exposes:
rename).set_icon — empty string clears).set_desc — empty clears that language; clearing the last language drops the whole desc key from disk).set_target). The URL is a single string, not per-language.Active language comes from the navbar dropdown.
link does not support ?view=download. type_supports_download returns true only for singlemd, mdbook, and file. A ?view=download request on a link falls through to the standard dispatcher and 302s to the target URL like any other link visit. See Route: download.
Link::save() rebuilds ctx.json from scratch (does not preserve unknown keys). The serialised JSON contains:
type: "link"name always; icon/desc/banner only when set)url — the target string (may be empty)Rebuilding from scratch is deliberate: it ensures that clearing the icon or removing all desc languages actually deletes those keys from disk, instead of leaving stale entries from the previous read. Empty-string url is written verbatim — an empty link is the placeholder state for a newly-created resource awaiting configuration.
url dict. The reader calls .string() on the value — a dict will stringify in a way that won’t redirect anywhere useful. Use a single string. Localise the click destination by using a parent list or by creating sibling link resources and selecting one per language at the parent level.redirect_response sends the URL verbatim. www.example.com (without https://) is interpreted as a same-origin path. Always prefix with https://.url in production. Visitors get the yellow “No URL configured” warning page. Either fill in the URL or delete the resource.Link::save() writes only type, the Header, and url. Anything else added by hand survives until the next save, then is silently dropped.