use crate::schema::{FieldType, Widget};
pub struct BuiltinField {
pub name: &'static str,
pub field_type: FieldType,
pub widget: Widget,
pub required: bool,
pub default_json: Option<&'static str>,
pub format: Option<&'static str>,
pub enum_values: Option<&'static [&'static str]>,
pub items_type: Option<FieldType>,
pub one_of_members: Option<&'static [BuiltinField]>,
pub description: &'static str,
pub label: Option<&'static str>,
pub label_key: &'static str,
pub score: u8,
pub skip_schema: bool,
pub group: &'static str,
}
const FIELD_DEFAULTS: BuiltinField = BuiltinField {
name: "",
field_type: FieldType::String,
widget: Widget::TextInput,
required: false,
default_json: None,
format: None,
enum_values: None,
items_type: None,
one_of_members: None,
description: "",
label: None,
label_key: "",
score: 0,
skip_schema: false,
group: "",
};
const CHILDREN_MEMBERS: &[BuiltinField] = &[
BuiltinField {
name: "",
field_type: FieldType::Boolean,
widget: Widget::Checkbox,
..FIELD_DEFAULTS
},
BuiltinField {
name: "",
field_type: FieldType::String,
widget: Widget::WikilinkPicker,
..FIELD_DEFAULTS
},
];
const SERIES_MEMBERS: &[BuiltinField] = &[
BuiltinField {
name: "",
field_type: FieldType::Boolean,
widget: Widget::Checkbox,
..FIELD_DEFAULTS
},
BuiltinField {
name: "",
field_type: FieldType::Array,
widget: Widget::WikilinkListPicker,
items_type: Some(FieldType::String),
..FIELD_DEFAULTS
},
];
pub const BUILTIN_FIELDS: &[BuiltinField] = &[
BuiltinField {
name: "title",
field_type: FieldType::String,
widget: Widget::TextInput,
required: true,
score: 10,
description: "Title of the page. Drives the visible heading, <title>, og:title, RSS, nav, breadcrumb, and link cards. Filename is used when this field is missing — by convention, name files after the title in the page's own language and let it fall back. Set to an empty string to suppress the auto-injected page heading.",
label_key: "chip.title.label",
group: "This Page",
..FIELD_DEFAULTS
},
BuiltinField {
name: "description",
field_type: FieldType::String,
widget: Widget::TextArea,
score: 20,
description: "Page excerpt for SEO meta, og:description, and list previews",
label_key: "chip.description.label",
group: "This Page",
..FIELD_DEFAULTS
},
BuiltinField {
name: "date",
field_type: FieldType::String,
widget: Widget::DatePicker,
format: Some("date"),
score: 30,
description: "Publication date (YYYY-MM-DD)",
label_key: "chip.date.label",
group: "This Page",
..FIELD_DEFAULTS
},
BuiltinField {
name: "author",
field_type: FieldType::String,
widget: Widget::TextInput,
score: 70,
description: "Author name (or 'A and B' / 'A, B, and C' for co-authors). Captured by moss import from JSON-LD / OpenGraph.",
label_key: "chip.author.label",
group: "This Page",
..FIELD_DEFAULTS
},
BuiltinField {
name: "publisher",
field_type: FieldType::String,
widget: Widget::TextInput,
score: 80,
description: "Publishing outlet name. Captured by moss import from schema.org publisher (resolved via @id) or OpenGraph site_name.",
label_key: "chip.publisher.label",
group: "This Page",
..FIELD_DEFAULTS
},
BuiltinField {
name: "cover",
field_type: FieldType::String,
widget: Widget::FilePicker,
score: 54,
description: "Cover image path",
label_key: "chip.cover.label",
group: "This Page",
..FIELD_DEFAULTS
},
BuiltinField {
name: "cover_type",
field_type: FieldType::String,
widget: Widget::Select,
description: "Cover type override: image, video, or iframe (auto-detected if omitted)",
skip_schema: true, ..FIELD_DEFAULTS
},
BuiltinField {
name: "tags",
field_type: FieldType::Array,
widget: Widget::TagInput,
items_type: Some(FieldType::String),
score: 64,
description: "Content tags for organization",
label_key: "chip.tags.label",
group: "This Page",
..FIELD_DEFAULTS
},
BuiltinField {
name: "url",
field_type: FieldType::String,
widget: Widget::TextInput,
score: 55,
description: "Custom URL slug (e.g. `links` → /links/). Pin a stable ASCII slug when the filename isn't one — moss's convention is to name files after the page title in their own language, then pin `url:` here (`隐私.md` + `url: privacy` → /privacy). Keeps `[[wikilinks]]` working across a rename.",
label_key: "chip.url.label",
group: "This Page",
..FIELD_DEFAULTS
},
BuiltinField {
name: "external_url",
field_type: FieldType::String,
widget: Widget::TextInput,
score: 74,
description: "Linkblog target: when set, internal references to this page (cards, link rewrites, canonical, sitemap) point here instead of the local URL. The page is still built locally — direct visits to its slug still work — but the canonical home is elsewhere on the web. Pattern from JSON Feed 1.1.",
label_key: "chip.external_url.label",
group: "This Page",
..FIELD_DEFAULTS
},
BuiltinField {
name: "lang",
field_type: FieldType::String,
widget: Widget::TextInput,
score: 56,
description: "Language code (e.g. en, zh)",
label_key: "chip.lang.label",
group: "This Page",
..FIELD_DEFAULTS
},
BuiltinField {
name: "weight",
field_type: FieldType::Integer,
widget: Widget::NumberInput,
score: 75,
description: "Sort weight for ordering",
label_key: "chip.weight.label",
group: "This Page",
..FIELD_DEFAULTS
},
BuiltinField {
name: "draft",
field_type: FieldType::Boolean,
widget: Widget::Checkbox,
score: 92,
description: "Hidden from all listings, feeds, and navigation — still published at its direct URL",
label_key: "chip.draft.label",
group: "This Page",
..FIELD_DEFAULTS
},
BuiltinField {
name: "listed",
field_type: FieldType::Boolean,
widget: Widget::Checkbox,
default_json: Some("false"),
score: 93,
description: "When off, hidden from listings, feeds, and sitemap — but still indexed and reachable at its URL",
label_key: "chip.listed.label",
group: "This Page",
..FIELD_DEFAULTS
},
BuiltinField {
name: "slot",
field_type: FieldType::String,
widget: Widget::TextInput,
score: 96,
description: "Named slot to inject this page into (e.g. footer-left). Recognized values are validated at build time.",
label_key: "chip.slot.label",
group: "This Page",
..FIELD_DEFAULTS
},
BuiltinField {
name: "comments",
field_type: FieldType::Boolean,
widget: Widget::Checkbox,
score: 97,
description: "Per-page comment opt-in/out",
label_key: "chip.comments.label",
group: "This Page",
..FIELD_DEFAULTS
},
BuiltinField {
name: "breadcrumb",
field_type: FieldType::Boolean,
widget: Widget::Checkbox,
score: 86,
description: "Override site-wide breadcrumb setting for this page",
label_key: "chip.breadcrumb.label",
group: "This Page",
..FIELD_DEFAULTS
},
BuiltinField {
name: "typesetting",
field_type: FieldType::String,
widget: Widget::Select,
enum_values: Some(&["horizontal", "vertical"]),
default_json: Some("\"horizontal\""),
score: 76,
description: "Typesetting direction: horizontal (default) or vertical (right-to-left columns for CJK content)",
label: Some("Typesetting"),
label_key: "chip.typesetting.label",
group: "This Page",
..FIELD_DEFAULTS
},
BuiltinField {
name: "content_width",
field_type: FieldType::String,
widget: Widget::Select,
enum_values: Some(&["wide", "full"]),
score: 77,
description: "Page width: default (67ch) for prose, wide (80ch) for grids/tables, full (site max) for dashboards",
label: Some("Width"),
label_key: "chip.content_width.label",
group: "This Page",
..FIELD_DEFAULTS
},
BuiltinField {
name: "translationKey",
field_type: FieldType::String,
widget: Widget::TextInput,
score: 94,
description: "Key to link translations of the same content",
label: Some("Translation Key"),
label_key: "chip.translationKey.label",
group: "This Page",
..FIELD_DEFAULTS
},
BuiltinField {
name: "also_in",
field_type: FieldType::Array,
widget: Widget::TagInput,
items_type: Some(FieldType::String),
score: 98,
description: "Cross-list this page in other folder listings",
label: Some("Cross-list In"),
label_key: "chip.also_in.label",
group: "This Page",
..FIELD_DEFAULTS
},
BuiltinField {
name: "review_of",
field_type: FieldType::String,
widget: Widget::TextInput,
score: 99,
description: "URL of item being reviewed (activates review feature)",
label: Some("Review Of"),
label_key: "chip.review_of.label",
group: "This Page",
..FIELD_DEFAULTS
},
BuiltinField {
name: "rating",
field_type: FieldType::Integer,
widget: Widget::NumberInput,
score: 100,
description: "Author's rating of the reviewed item (1-5)",
label_key: "chip.rating.label",
group: "This Page",
..FIELD_DEFAULTS
},
BuiltinField {
name: "children",
field_type: FieldType::OneOf,
widget: Widget::Union,
one_of_members: Some(CHILDREN_MEMBERS),
default_json: Some("true"),
score: 60,
description: "Whether to render child pages below content. Accepts true/false or a wikilink like [[News]] to render a specific folder's articles.",
label_key: "chip.children.label",
group: "Child Pages",
..FIELD_DEFAULTS
},
BuiltinField {
name: "children_source",
field_type: FieldType::String,
widget: Widget::TextInput,
skip_schema: true,
description: "Internal: wikilink reference parsed from children field (e.g. [[News]])",
..FIELD_DEFAULTS
},
BuiltinField {
name: "sort",
field_type: FieldType::String,
widget: Widget::Select,
enum_values: Some(&["date", "weight", "title"]),
score: 70,
description: "How to sort children in this folder's listing. Use date for chronological streams, weight for authored order, title for alphabetical. A list of child stems (e.g. [intro, setup]) declares explicit order.",
label_key: "chip.sort.label",
group: "Child Pages",
..FIELD_DEFAULTS
},
BuiltinField {
name: "series",
field_type: FieldType::OneOf,
widget: Widget::Union,
one_of_members: Some(SERIES_MEMBERS),
score: 92,
description: "Declares children as sequential series. Use true for weight-based ordering, or a list of wikilinks for explicit order.",
label_key: "chip.series.label",
group: "Child Pages",
..FIELD_DEFAULTS
},
BuiltinField {
name: "sidebar",
field_type: FieldType::String,
widget: Widget::TextInput,
score: 98,
description: "Deprecated. Use children + children_in: sidebar. Wikilink to folder whose children appear in sidebar (e.g. [[News]]).",
label_key: "chip.sidebar.label",
group: "Child Pages",
..FIELD_DEFAULTS
},
BuiltinField {
name: "children_style",
field_type: FieldType::String,
widget: Widget::Select,
enum_values: Some(&["list", "summary", "grid"]),
default_json: Some("\"list\""),
score: 70,
description: "How child pages are rendered",
label: Some("Child Layout"),
label_key: "chip.children_style.label",
group: "Child Styles",
..FIELD_DEFAULTS
},
BuiltinField {
name: "children_group",
field_type: FieldType::String,
widget: Widget::Select,
enum_values: Some(&["year", "none"]),
score: 80,
description: "How children are grouped: year (default for list) or none (default for card)",
label: Some("Group"),
label_key: "chip.children_group.label",
group: "Child Styles",
..FIELD_DEFAULTS
},
BuiltinField {
name: "children_depth",
field_type: FieldType::String,
widget: Widget::Select,
enum_values: Some(&["direct", "all"]),
default_json: Some("\"direct\""),
score: 81,
description: "Whether to include only immediate children or all descendants",
label: Some("Depth"),
label_key: "chip.children_depth.label",
group: "Child Styles",
..FIELD_DEFAULTS
},
BuiltinField {
name: "children_in",
field_type: FieldType::String,
widget: Widget::Select,
enum_values: Some(&["body", "sidebar"]),
score: 86,
description: "Where to render the children feed: body (after page content, default) or sidebar (right rail).",
label: Some("Feed Slot"),
label_key: "chip.children_in.label",
group: "Child Styles",
..FIELD_DEFAULTS
},
BuiltinField {
name: "children_limit",
field_type: FieldType::Integer,
widget: Widget::NumberInput,
score: 82,
description: "Cap the feed at N items. If truncated, a 'More \u{2192}' link is added. Absent = no cap.",
label: Some("Limit"),
label_key: "chip.children_limit.label",
group: "Child Styles",
..FIELD_DEFAULTS
},
BuiltinField {
name: "_from_sidebar_alias",
field_type: FieldType::Boolean,
widget: Widget::Checkbox,
skip_schema: true,
description: "Internal: marks frontmatter that came from the deprecated sidebar: alias",
..FIELD_DEFAULTS
},
BuiltinField {
name: "cascade",
field_type: FieldType::Object,
widget: Widget::CodeEditor,
score: 96,
description: "Frontmatter values to push to all descendant pages",
label_key: "chip.cascade.label",
group: "Child Styles",
..FIELD_DEFAULTS
},
BuiltinField {
name: "logo",
field_type: FieldType::String,
widget: Widget::FilePicker,
score: 70,
description: "Site logo image path (rendered before site name in nav)",
label_key: "chip.logo.label",
group: "Whole Site",
..FIELD_DEFAULTS
},
BuiltinField {
name: "nav",
field_type: FieldType::Boolean,
widget: Widget::Checkbox,
score: 76,
description: "Whether to show in site navigation",
label_key: "chip.nav.label",
group: "Whole Site",
..FIELD_DEFAULTS
},
BuiltinField {
name: "footer",
field_type: FieldType::Boolean,
widget: Widget::Checkbox,
score: 86,
description: "Show as a link in the site footer",
label_key: "chip.footer.label",
group: "Whole Site",
..FIELD_DEFAULTS
},
BuiltinField {
name: "home",
field_type: FieldType::Boolean,
widget: Widget::Checkbox,
description: "Mark this file as its folder's home page (survives folder rename)",
skip_schema: true, ..FIELD_DEFAULTS
},
BuiltinField {
name: "analytics",
field_type: FieldType::Object,
widget: Widget::CodeEditor,
description: "Analytics configuration (site-level, read from homepage only)",
skip_schema: true,
..FIELD_DEFAULTS
},
BuiltinField {
name: "uid",
field_type: FieldType::String,
widget: Widget::TextInput,
description: "Content-addressable unique identifier (auto-generated)",
skip_schema: true, ..FIELD_DEFAULTS
},
BuiltinField {
name: "layout",
field_type: FieldType::String,
widget: Widget::Select,
enum_values: Some(&["page", "article"]),
description: "Template layout override (page or article)",
skip_schema: true, ..FIELD_DEFAULTS
},
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_duplicate_field_names() {
let mut seen = std::collections::HashSet::new();
for field in BUILTIN_FIELDS {
assert!(
seen.insert(field.name),
"duplicate field name '{}' in BUILTIN_FIELDS",
field.name
);
}
}
#[test]
fn test_array_fields_have_items_type() {
for field in BUILTIN_FIELDS {
if field.field_type == FieldType::Array {
assert!(
field.items_type.is_some(),
"array field '{}' must have items_type set",
field.name
);
}
}
}
#[test]
fn test_labels_propagate_to_schema() {
let schema = crate::schema::builtin_schema();
let depth = schema.frontmatter.fields.get("children_depth").expect("children_depth");
assert_eq!(depth.label.as_deref(), Some("Depth"));
}
#[test]
fn test_no_label_means_none() {
let schema = crate::schema::builtin_schema();
let title = schema.frontmatter.fields.get("title").expect("title");
assert!(title.label.is_none());
}
#[test]
fn test_select_fields_have_enum_values() {
for field in BUILTIN_FIELDS {
if field.widget == Widget::Select && !field.skip_schema {
assert!(
field.enum_values.is_some(),
"select widget field '{}' should have enum_values",
field.name
);
}
}
}
#[test]
fn test_all_non_skip_fields_have_a_group() {
for field in BUILTIN_FIELDS {
if !field.skip_schema {
assert!(
!field.group.is_empty(),
"field '{}' has skip_schema=false but no group",
field.name
);
}
}
}
#[test]
fn test_groups_are_valid_scope_groups() {
const VALID: &[&str] = &["This Page", "Child Pages", "Child Styles", "Whole Site"];
for field in BUILTIN_FIELDS {
if !field.skip_schema {
assert!(
VALID.contains(&field.group),
"field '{}' has unexpected group '{}'; expected one of {:?}",
field.name,
field.group,
VALID
);
}
}
}
#[test]
fn test_score_in_valid_range() {
for field in BUILTIN_FIELDS {
if !field.skip_schema {
assert!(
field.score > 0,
"non-skip field '{}' has score=0; set a score >= 1",
field.name
);
}
}
}
}