1use crate::resolve::ext_kind::ExtKind;
46use crate::schema::{FieldType, Widget};
47
48pub struct BuiltinField {
54 pub name: &'static str,
56 pub field_type: FieldType,
58 pub widget: Widget,
60 pub required: bool,
62 pub default_json: Option<&'static str>,
64 pub format: Option<&'static str>,
66 pub enum_values: Option<&'static [&'static str]>,
68 pub items_type: Option<FieldType>,
70 pub one_of_members: Option<&'static [BuiltinField]>,
75 pub description: &'static str,
77 pub label: Option<&'static str>,
81 pub label_key: &'static str,
85 pub score: u8,
91 pub skip_schema: bool,
101 pub group: &'static str,
106 pub file_kinds: Option<&'static [ExtKind]>,
111}
112
113const FIELD_DEFAULTS: BuiltinField = BuiltinField {
116 name: "",
117 field_type: FieldType::String,
118 widget: Widget::TextInput,
119 required: false,
120 default_json: None,
121 format: None,
122 enum_values: None,
123 items_type: None,
124 one_of_members: None,
125 description: "",
126 label: None,
127 label_key: "",
128 score: 0,
129 skip_schema: false,
130 group: "",
131 file_kinds: None,
132};
133
134const CHILDREN_MEMBERS: &[BuiltinField] = &[
138 BuiltinField {
139 name: "",
140 field_type: FieldType::Boolean,
141 widget: Widget::Checkbox,
142 ..FIELD_DEFAULTS
143 },
144 BuiltinField {
145 name: "",
146 field_type: FieldType::String,
147 widget: Widget::WikilinkPicker,
148 ..FIELD_DEFAULTS
149 },
150];
151
152const SERIES_MEMBERS: &[BuiltinField] = &[
155 BuiltinField {
156 name: "",
157 field_type: FieldType::Boolean,
158 widget: Widget::Checkbox,
159 ..FIELD_DEFAULTS
160 },
161 BuiltinField {
162 name: "",
163 field_type: FieldType::Array,
164 widget: Widget::WikilinkListPicker,
165 items_type: Some(FieldType::String),
166 ..FIELD_DEFAULTS
167 },
168];
169
170const SORT_MEMBERS: &[BuiltinField] = &[
177 BuiltinField {
178 name: "",
179 field_type: FieldType::String,
180 widget: Widget::Select,
181 enum_values: Some(&["date", "weight", "title"]),
182 ..FIELD_DEFAULTS
183 },
184 BuiltinField {
185 name: "",
186 field_type: FieldType::Array,
187 widget: Widget::TagInput,
188 items_type: Some(FieldType::String),
189 ..FIELD_DEFAULTS
190 },
191];
192
193const CREDIT_ROW_MEMBERS: &[BuiltinField] = &[
198 BuiltinField {
199 name: "",
200 field_type: FieldType::String,
201 widget: Widget::TextArea,
202 ..FIELD_DEFAULTS
203 },
204 BuiltinField {
205 name: "",
206 field_type: FieldType::Array,
207 widget: Widget::TagInput,
208 items_type: Some(FieldType::String),
209 ..FIELD_DEFAULTS
210 },
211];
212
213pub const BUILTIN_FIELDS: &[BuiltinField] = &[
226 BuiltinField {
229 name: "title",
230 field_type: FieldType::String,
231 widget: Widget::TextInput,
232 required: true,
233 score: 10,
237 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.",
238 label_key: "chip.title.label",
239 group: "This Page",
240 ..FIELD_DEFAULTS
241 },
242 BuiltinField {
243 name: "description",
244 field_type: FieldType::String,
245 widget: Widget::TextArea,
246 score: 20,
248 description: "Page excerpt for SEO meta, og:description, and list previews",
249 label_key: "chip.description.label",
250 group: "This Page",
251 ..FIELD_DEFAULTS
252 },
253 BuiltinField {
254 name: "date",
255 field_type: FieldType::String,
256 widget: Widget::DatePicker,
257 format: Some("date"),
258 score: 30,
260 description: "Publication date (YYYY-MM-DD)",
261 label_key: "chip.date.label",
262 group: "This Page",
263 ..FIELD_DEFAULTS
264 },
265 BuiltinField {
266 name: "author",
267 field_type: FieldType::String,
268 widget: Widget::TextInput,
269 score: 70,
271 description: "Author name (or 'A and B' / 'A, B, and C' for co-authors). Captured by moss import from JSON-LD / OpenGraph.",
272 label_key: "chip.author.label",
273 group: "This Page",
274 ..FIELD_DEFAULTS
275 },
276 BuiltinField {
277 name: "byline",
278 field_type: FieldType::OneOf,
285 widget: Widget::TextArea,
286 one_of_members: Some(CREDIT_ROW_MEMBERS),
287 score: 76,
289 description: "Credit lines shown under the page title — articles and folder-index pages alike — one row per line (or per list entry). Rendered as inline markdown, so a row may carry links. A display string, not structured data — moss makes no machine claim about who did what, and this is independent of `author`.",
290 label_key: "chip.byline.label",
291 group: "This Page",
292 ..FIELD_DEFAULTS
293 },
294 BuiltinField {
295 name: "colophon",
296 field_type: FieldType::OneOf,
298 widget: Widget::TextArea,
299 one_of_members: Some(CREDIT_ROW_MEMBERS),
300 score: 78,
302 description: "Credit lines shown at the foot of the page — where the piece first ran, contributor biographies, production credits. Same shapes and inline-markdown rendering as `byline`; the difference is only where it lands. Everything a reader does not need before the piece belongs here.",
303 label_key: "chip.colophon.label",
304 group: "This Page",
305 ..FIELD_DEFAULTS
306 },
307 BuiltinField {
308 name: "publisher",
309 field_type: FieldType::String,
310 widget: Widget::TextInput,
311 score: 80,
313 description: "Publishing outlet name. Captured by moss import from schema.org publisher (resolved via @id) or OpenGraph site_name.",
314 label_key: "chip.publisher.label",
315 group: "This Page",
316 ..FIELD_DEFAULTS
317 },
318 BuiltinField {
319 name: "cover",
320 field_type: FieldType::String,
321 widget: Widget::FilePicker,
322 score: 54,
324 description: "Cover image path",
325 label_key: "chip.cover.label",
326 group: "This Page",
327 file_kinds: Some(&[ExtKind::Image, ExtKind::Video]),
328 ..FIELD_DEFAULTS
329 },
330 BuiltinField {
331 name: "cover_type",
332 field_type: FieldType::String,
333 widget: Widget::Select,
334 description: "Cover type override: image, video, or iframe (auto-detected if omitted)",
335 skip_schema: true, ..FIELD_DEFAULTS
337 },
338 BuiltinField {
339 name: "tags",
340 field_type: FieldType::Array,
341 widget: Widget::TagInput,
342 items_type: Some(FieldType::String),
343 score: 64,
345 description: "Content tags. Inline #hashtags written in the body are merged into this set. Emitted only as article:tag metadata and JSON-LD keywords - moss generates no tag archive pages and no /tags/ routes, so a link to /tags/<name>/ will 404. To group pages by topic, use folders or also_in.",
346 label_key: "chip.tags.label",
347 group: "This Page",
348 ..FIELD_DEFAULTS
349 },
350 BuiltinField {
351 name: "url",
352 field_type: FieldType::String,
353 widget: Widget::TextInput,
354 score: 55,
356 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.",
357 label_key: "chip.url.label",
358 group: "This Page",
359 ..FIELD_DEFAULTS
360 },
361 BuiltinField {
362 name: "external_url",
363 field_type: FieldType::String,
364 widget: Widget::TextInput,
365 score: 74,
367 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.",
368 label_key: "chip.external_url.label",
369 group: "This Page",
370 ..FIELD_DEFAULTS
371 },
372 BuiltinField {
373 name: "lang",
374 field_type: FieldType::String,
375 widget: Widget::TextInput,
376 score: 56,
378 description: "Language code (e.g. en, zh)",
379 label_key: "chip.lang.label",
380 group: "This Page",
381 ..FIELD_DEFAULTS
382 },
383 BuiltinField {
384 name: "weight",
385 field_type: FieldType::Integer,
386 widget: Widget::NumberInput,
387 score: 75,
389 description: "Sort weight for ordering",
390 label_key: "chip.weight.label",
391 group: "This Page",
392 ..FIELD_DEFAULTS
393 },
394 BuiltinField {
395 name: "draft",
396 field_type: FieldType::Boolean,
397 widget: Widget::Checkbox,
398 score: 92,
400 description: "Hidden from all listings, feeds, and navigation — still published at its direct URL",
401 label_key: "chip.draft.label",
402 group: "This Page",
403 ..FIELD_DEFAULTS
404 },
405 BuiltinField {
406 name: "listed",
407 field_type: FieldType::Boolean,
408 widget: Widget::Checkbox,
409 default_json: Some("false"),
410 score: 93,
412 description: "When off, hidden from listings, feeds, and sitemap — but still indexed and reachable at its URL",
413 label_key: "chip.listed.label",
414 group: "This Page",
415 ..FIELD_DEFAULTS
416 },
417 BuiltinField {
418 name: "slot",
419 field_type: FieldType::String,
420 widget: Widget::TextInput,
421 score: 96,
423 description: "Named slot to inject this page into (e.g. footer-left). Recognized values are validated at build time.",
424 label_key: "chip.slot.label",
425 group: "This Page",
426 ..FIELD_DEFAULTS
427 },
428 BuiltinField {
429 name: "comments",
430 field_type: FieldType::Boolean,
431 widget: Widget::Checkbox,
432 score: 97,
434 description: "Per-page comment opt-in/out",
435 label_key: "chip.comments.label",
436 group: "This Page",
437 ..FIELD_DEFAULTS
438 },
439 BuiltinField {
440 name: "breadcrumb",
441 field_type: FieldType::Boolean,
442 widget: Widget::Checkbox,
443 score: 86,
445 description: "Override site-wide breadcrumb setting for this page",
446 label_key: "chip.breadcrumb.label",
447 group: "This Page",
448 ..FIELD_DEFAULTS
449 },
450 BuiltinField {
451 name: "typesetting",
452 field_type: FieldType::String,
453 widget: Widget::Select,
454 enum_values: Some(&["horizontal", "vertical"]),
455 default_json: Some("\"horizontal\""),
456 score: 76,
458 description: "Typesetting direction: horizontal (default) or vertical (right-to-left columns for CJK content)",
459 label: Some("Typesetting"),
460 label_key: "chip.typesetting.label",
461 group: "This Page",
462 ..FIELD_DEFAULTS
463 },
464 BuiltinField {
465 name: "content_width",
466 field_type: FieldType::String,
467 widget: Widget::Select,
468 enum_values: Some(&["wide", "full"]),
469 score: 77,
471 description: "Page width: default (67ch) for prose, wide (80ch) for grids/tables, full (site max) for dashboards",
472 label: Some("Width"),
473 label_key: "chip.content_width.label",
474 group: "This Page",
475 ..FIELD_DEFAULTS
476 },
477 BuiltinField {
478 name: "layout",
479 field_type: FieldType::String,
480 widget: Widget::Select,
481 enum_values: Some(&["page", "article"]),
482 score: 80,
484 description: "Template layout override (page or article). On a folder-index page, \"article\" suppresses the auto-inserted cover entirely, even when \"cover\" is set — the body owns its own imagery",
485 label: Some("Layout"),
486 label_key: "chip.layout.label",
487 group: "This Page",
488 ..FIELD_DEFAULTS
489 },
490 BuiltinField {
491 name: "translationKey",
492 field_type: FieldType::String,
493 widget: Widget::TextInput,
494 score: 94,
496 description: "Key to link translations of the same content",
497 label: Some("Translation Key"),
498 label_key: "chip.translationKey.label",
499 group: "This Page",
500 ..FIELD_DEFAULTS
501 },
502 BuiltinField {
503 name: "also_in",
504 field_type: FieldType::Array,
505 widget: Widget::TagInput,
506 items_type: Some(FieldType::String),
507 score: 98,
509 description: "Cross-list this page in other folder listings",
510 label: Some("Cross-list In"),
511 label_key: "chip.also_in.label",
512 group: "This Page",
513 ..FIELD_DEFAULTS
514 },
515 BuiltinField {
516 name: "review_of",
517 field_type: FieldType::String,
518 widget: Widget::TextInput,
519 score: 99,
521 description: "URL of item being reviewed (activates review feature)",
522 label: Some("Review Of"),
523 label_key: "chip.review_of.label",
524 group: "This Page",
525 ..FIELD_DEFAULTS
526 },
527 BuiltinField {
528 name: "rating",
529 field_type: FieldType::Integer,
530 widget: Widget::NumberInput,
531 score: 100,
533 description: "Author's rating of the reviewed item (1-5)",
534 label_key: "chip.rating.label",
535 group: "This Page",
536 ..FIELD_DEFAULTS
537 },
538
539 BuiltinField {
541 name: "children",
542 field_type: FieldType::OneOf,
543 widget: Widget::Union,
544 one_of_members: Some(CHILDREN_MEMBERS),
545 default_json: Some("true"),
546 score: 60,
548 description: "Whether to render child pages below content. Accepts true/false or a wikilink like [[News]] to render a specific folder's articles.",
549 label_key: "chip.children.label",
550 group: "Child Pages",
551 ..FIELD_DEFAULTS
552 },
553 BuiltinField {
554 name: "children_source",
555 field_type: FieldType::String,
556 widget: Widget::TextInput,
557 skip_schema: true,
558 description: "Internal: wikilink reference parsed from children field (e.g. [[News]])",
559 ..FIELD_DEFAULTS
560 },
561 BuiltinField {
562 name: "sort",
563 field_type: FieldType::OneOf,
570 widget: Widget::Select,
571 one_of_members: Some(SORT_MEMBERS),
572 enum_values: Some(&["date", "weight", "title"]),
573 score: 70,
575 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.",
576 label_key: "chip.sort.label",
577 group: "Child Pages",
578 ..FIELD_DEFAULTS
579 },
580 BuiltinField {
581 name: "series",
582 field_type: FieldType::OneOf,
583 widget: Widget::Union,
584 one_of_members: Some(SERIES_MEMBERS),
585 score: 92,
587 description: "Declares children as a sequential series. On a folder index: true turns prev/next on for its children, a list of wikilinks declares their order, false turns the sequence off. On a page inside such a folder, `series: false` takes that page out of the reading order entirely — it keeps its place in the folder listing, shows no prev/next of its own, and stops being any sibling's prev or next, so an appendix or an editor's note no longer follows the last chapter. Position (\"2 / 3\") counts only the pages still in the order, so a series still being published reads its own length, not its planned one.",
588 label_key: "chip.series.label",
589 group: "Child Pages",
590 ..FIELD_DEFAULTS
591 },
592 BuiltinField {
593 name: "sidebar",
594 field_type: FieldType::String,
595 widget: Widget::TextInput,
596 score: 98,
598 description: "Deprecated. Use children + children_in: sidebar. Wikilink to folder whose children appear in sidebar (e.g. [[News]]).",
599 label_key: "chip.sidebar.label",
600 group: "Child Pages",
601 ..FIELD_DEFAULTS
602 },
603
604 BuiltinField {
606 name: "children_style",
607 field_type: FieldType::String,
608 widget: Widget::Select,
609 enum_values: Some(&["list", "summary", "grid"]),
610 default_json: Some("\"list\""),
611 score: 70,
613 description: "How child pages are rendered",
614 label: Some("Child Layout"),
615 label_key: "chip.children_style.label",
616 group: "Child Styles",
617 ..FIELD_DEFAULTS
618 },
619 BuiltinField {
620 name: "children_group",
621 field_type: FieldType::String,
622 widget: Widget::Select,
623 enum_values: Some(&["year", "none"]),
624 score: 80,
626 description: "How children are grouped: year (default for list) or none (default for card)",
627 label: Some("Group"),
628 label_key: "chip.children_group.label",
629 group: "Child Styles",
630 ..FIELD_DEFAULTS
631 },
632 BuiltinField {
633 name: "children_depth",
634 field_type: FieldType::String,
635 widget: Widget::Select,
636 enum_values: Some(&["direct", "all"]),
637 default_json: Some("\"direct\""),
638 score: 81,
640 description: "Whether to include only immediate children or all descendants",
641 label: Some("Depth"),
642 label_key: "chip.children_depth.label",
643 group: "Child Styles",
644 ..FIELD_DEFAULTS
645 },
646 BuiltinField {
647 name: "children_in",
648 field_type: FieldType::String,
649 widget: Widget::Select,
650 enum_values: Some(&["body", "sidebar"]),
651 score: 86,
653 description: "Where to render the children feed: body (after page content, default) or sidebar (right rail).",
654 label: Some("Feed Slot"),
655 label_key: "chip.children_in.label",
656 group: "Child Styles",
657 ..FIELD_DEFAULTS
658 },
659 BuiltinField {
660 name: "children_limit",
661 field_type: FieldType::Integer,
662 widget: Widget::NumberInput,
663 score: 82,
665 description: "Cap the feed at N items. If truncated, a 'More \u{2192}' link is added. Absent = no cap.",
666 label: Some("Limit"),
667 label_key: "chip.children_limit.label",
668 group: "Child Styles",
669 ..FIELD_DEFAULTS
670 },
671 BuiltinField {
672 name: "_from_sidebar_alias",
673 field_type: FieldType::Boolean,
674 widget: Widget::Checkbox,
675 skip_schema: true,
676 description: "Internal: marks frontmatter that came from the deprecated sidebar: alias",
677 ..FIELD_DEFAULTS
678 },
679 BuiltinField {
680 name: "cascade",
681 field_type: FieldType::Object,
682 widget: Widget::CodeEditor,
683 score: 96,
685 description: "Frontmatter values to push to all descendant pages",
686 label_key: "chip.cascade.label",
687 group: "Child Styles",
688 ..FIELD_DEFAULTS
689 },
690
691 BuiltinField {
694 name: "logo",
695 field_type: FieldType::String,
696 widget: Widget::FilePicker,
697 score: 70,
699 description: "Site logo image path (rendered before site name in nav)",
700 label_key: "chip.logo.label",
701 group: "Whole Site",
702 file_kinds: Some(&[ExtKind::Image]),
703 ..FIELD_DEFAULTS
704 },
705 BuiltinField {
706 name: "nav",
707 field_type: FieldType::Boolean,
708 widget: Widget::Checkbox,
709 score: 76,
711 description: "Whether to show in site navigation",
712 label_key: "chip.nav.label",
713 group: "Whole Site",
714 ..FIELD_DEFAULTS
715 },
716 BuiltinField {
717 name: "footer",
718 field_type: FieldType::Boolean,
719 widget: Widget::Checkbox,
720 score: 86,
722 description: "Show as a link in the site footer",
723 label_key: "chip.footer.label",
724 group: "Whole Site",
725 ..FIELD_DEFAULTS
726 },
727 BuiltinField {
728 name: "home",
729 field_type: FieldType::Boolean,
730 widget: Widget::Checkbox,
731 description: "Mark this file as its folder's home page (survives folder rename)",
732 skip_schema: true, ..FIELD_DEFAULTS
734 },
735
736 BuiltinField {
738 name: "analytics",
739 field_type: FieldType::Object,
740 widget: Widget::CodeEditor,
741 description: "Analytics configuration (site-level, read from homepage only)",
742 skip_schema: true,
743 ..FIELD_DEFAULTS
744 },
745 BuiltinField {
746 name: "uid",
747 field_type: FieldType::String,
748 widget: Widget::TextInput,
749 description: "Stable note identity: 8 random hex chars minted at first build. NOT derived from the path or the content, and unrecoverable once lost (auto-generated)",
757 skip_schema: true, ..FIELD_DEFAULTS
759 },
760];
761
762pub fn asset_field_names() -> impl Iterator<Item = &'static str> {
773 BUILTIN_FIELDS
774 .iter()
775 .filter(|f| matches!(f.widget, Widget::FilePicker))
776 .map(|f| f.name)
777}
778
779#[cfg(test)]
780#[path = "schema_fields_tests.rs"]
781mod tests;