Yaml frontmatter (#990)

* Accept dates presented as strings

Still, if we find a string we involve the TOML parser to make sure the
content of said string would be a correct TOML date. In a pure TOML
world this isn't exactly relevant, but this will come in handy when
using a YAML deserializer.

Signed-off-by: Kevin Ottens <ervin@ipsquad.net>

* Add serde_yaml and test-case dependencies

This will be necessary for the YAML frontmatter handling and
corresponding unit tests.

Signed-off-by: Kevin Ottens <ervin@ipsquad.net>

* Add YAML front matter handling

Signed-off-by: Kevin Ottens <ervin@ipsquad.net>

* Switch RawFrontMatter enum to wrap &str instead of String

Signed-off-by: Kevin Ottens <ervin@ipsquad.net>

* Update the documentation to mention YAML frontmatter

This is just a light update on purpose. There would be no point in
pushing YAML too much, this is mainly here to help people with a backlog
of posts to transition.

Signed-off-by: Kevin Ottens <ervin@ipsquad.net>
This commit is contained in:
Kevin Ottens 2020-12-08 19:18:14 +01:00 committed by Vincent Prouillet
parent f20c52b872
commit 4754cb5792
8 changed files with 403 additions and 155 deletions

26
Cargo.lock generated
View file

@ -641,7 +641,9 @@ dependencies = [
"regex",
"serde",
"serde_derive",
"serde_yaml",
"tera",
"test-case",
"toml",
"utils",
]
@ -2341,6 +2343,18 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7baae0a99f1a324984bcdc5f0718384c1f69775f1c7eec8b859b71b443e3fd7"
dependencies = [
"dtoa",
"linked-hash-map",
"serde",
"yaml-rust",
]
[[package]]
name = "sha-1"
version = "0.8.2"
@ -2625,6 +2639,18 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "test-case"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "199464148b42bcf3da8b2a56f6ee87ca68f47402496d1268849291ec9fb463c8"
dependencies = [
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "textwrap"
version = "0.11.0"

View file

@ -9,9 +9,14 @@ tera = "1"
chrono = "0.4"
serde = "1"
serde_derive = "1"
serde_yaml = "0.8"
toml = "0.5"
regex = "1"
lazy_static = "1"
errors = { path = "../errors" }
utils = { path = "../utils" }
[dev-dependencies]
test-case = "1.0"

View file

@ -3,7 +3,9 @@ use serde_derive::{Deserialize, Serialize};
use errors::{bail, Error, Result};
use regex::Regex;
use serde_yaml;
use std::path::Path;
use toml;
mod page;
mod section;
@ -12,8 +14,31 @@ pub use page::PageFrontMatter;
pub use section::SectionFrontMatter;
lazy_static! {
static ref PAGE_RE: Regex =
static ref TOML_RE: Regex =
Regex::new(r"^[[:space:]]*\+\+\+(\r?\n(?s).*?(?-s))\+\+\+\r?\n?((?s).*(?-s))$").unwrap();
static ref YAML_RE: Regex =
Regex::new(r"^[[:space:]]*---(\r?\n(?s).*?(?-s))---\r?\n?((?s).*(?-s))$").unwrap();
}
pub enum RawFrontMatter<'a> {
Toml(&'a str),
Yaml(&'a str),
}
impl RawFrontMatter<'_> {
fn deserialize<T>(&self) -> Result<T>
where
T: serde::de::DeserializeOwned,
{
let f: T = match self {
RawFrontMatter::Toml(s) => toml::from_str(s)?,
RawFrontMatter::Yaml(s) => match serde_yaml::from_str(s) {
Ok(d) => d,
Err(e) => bail!(format!("YAML deserialize error: {:?}", e)),
},
};
Ok(f)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
@ -37,20 +62,30 @@ pub enum InsertAnchor {
/// Split a file between the front matter and its content
/// Will return an error if the front matter wasn't found
fn split_content<'c>(file_path: &Path, content: &'c str) -> Result<(&'c str, &'c str)> {
if !PAGE_RE.is_match(content) {
fn split_content<'c>(file_path: &Path, content: &'c str) -> Result<(RawFrontMatter<'c>, &'c str)> {
let (re, is_toml) = if TOML_RE.is_match(content) {
(&TOML_RE as &Regex, true)
} else if YAML_RE.is_match(content) {
(&YAML_RE as &Regex, false)
} else {
bail!(
"Couldn't find front matter in `{}`. Did you forget to add `+++`?",
"Couldn't find front matter in `{}`. Did you forget to add `+++` or `---`?",
file_path.to_string_lossy()
);
}
};
// 2. extract the front matter and the content
let caps = PAGE_RE.captures(content).unwrap();
let caps = re.captures(content).unwrap();
// caps[0] is the full match
// caps[1] => front matter
// caps[2] => content
Ok((caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str()))
let front_matter = caps.get(1).unwrap().as_str();
let content = caps.get(2).unwrap().as_str();
if is_toml {
Ok((RawFrontMatter::Toml(front_matter), content))
} else {
Ok((RawFrontMatter::Yaml(front_matter), content))
}
}
/// Split a file between the front matter and its content.
@ -88,71 +123,125 @@ pub fn split_page_content<'c>(
#[cfg(test)]
mod tests {
use std::path::Path;
use test_case::test_case;
use super::{split_page_content, split_section_content};
#[test]
fn can_split_page_content_valid() {
let content = r#"
#[test_case(r#"
+++
title = "Title"
description = "hey there"
date = 2002-10-12
+++
Hello
"#;
"#; "toml")]
#[test_case(r#"
---
title: Title
description: hey there
date: 2002-10-12
---
Hello
"#; "yaml")]
fn can_split_page_content_valid(content: &str) {
let (front_matter, content) = split_page_content(Path::new(""), content).unwrap();
assert_eq!(content, "Hello\n");
assert_eq!(front_matter.title.unwrap(), "Title");
}
#[test]
fn can_split_section_content_valid() {
let content = r#"
#[test_case(r#"
+++
paginate_by = 10
+++
Hello
"#;
"#; "toml")]
#[test_case(r#"
---
paginate_by: 10
---
Hello
"#; "yaml")]
fn can_split_section_content_valid(content: &str) {
let (front_matter, content) = split_section_content(Path::new(""), content).unwrap();
assert_eq!(content, "Hello\n");
assert!(front_matter.is_paginated());
}
#[test]
fn can_split_content_with_only_frontmatter_valid() {
let content = r#"
#[test_case(r#"
+++
title = "Title"
description = "hey there"
date = 2002-10-12
+++"#;
+++"#; "toml")]
#[test_case(r#"
---
title: Title
description: hey there
date: 2002-10-12
---"#; "yaml")]
fn can_split_content_with_only_frontmatter_valid(content: &str) {
let (front_matter, content) = split_page_content(Path::new(""), content).unwrap();
assert_eq!(content, "");
assert_eq!(front_matter.title.unwrap(), "Title");
}
#[test]
fn can_split_content_lazily() {
let content = r#"
#[test_case(r#"
+++
title = "Title"
description = "hey there"
date = 2002-10-02T15:00:00Z
+++
+++"#;
let (front_matter, content) = split_page_content(Path::new(""), content).unwrap();
assert_eq!(content, "+++");
assert_eq!(front_matter.title.unwrap(), "Title");
}
#[test]
fn errors_if_cannot_locate_frontmatter() {
let content = r#"
+++"#, "+++"; "toml with pluses in content")]
#[test_case(r#"
+++
title = "Title"
description = "hey there"
date = 2002-10-12"#;
date = 2002-10-02T15:00:00Z
+++
---"#, "---"; "toml with minuses in content")]
#[test_case(r#"
---
title: Title
description: hey there
date: 2002-10-02T15:00:00Z
---
+++"#, "+++"; "yaml with pluses in content")]
#[test_case(r#"
---
title: Title
description: hey there
date: 2002-10-02T15:00:00Z
---
---"#, "---"; "yaml with minuses in content")]
fn can_split_content_lazily(content: &str, expected: &str) {
let (front_matter, content) = split_page_content(Path::new(""), content).unwrap();
assert_eq!(content, expected);
assert_eq!(front_matter.title.unwrap(), "Title");
}
#[test_case(r#"
+++
title = "Title"
description = "hey there"
date = 2002-10-12"#; "toml")]
#[test_case(r#"
+++
title = "Title"
description = "hey there"
date = 2002-10-12
---"#; "toml unmatched")]
#[test_case(r#"
---
title: Title
description: hey there
date: 2002-10-12"#; "yaml")]
#[test_case(r#"
---
title: Title
description: hey there
date: 2002-10-12
+++"#; "yaml unmatched")]
fn errors_if_cannot_locate_frontmatter(content: &str) {
let res = split_page_content(Path::new(""), content);
assert!(res.is_err());
}

View file

@ -7,6 +7,8 @@ use tera::{Map, Value};
use errors::{bail, Result};
use utils::de::{fix_toml_dates, from_toml_datetime};
use crate::RawFrontMatter;
/// The front matter of every page
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(default)]
@ -69,11 +71,8 @@ fn parse_datetime(d: &str) -> Option<NaiveDateTime> {
}
impl PageFrontMatter {
pub fn parse(toml: &str) -> Result<PageFrontMatter> {
let mut f: PageFrontMatter = match toml::from_str(toml) {
Ok(d) => d,
Err(e) => bail!(e),
};
pub fn parse(raw: &RawFrontMatter) -> Result<PageFrontMatter> {
let mut f: PageFrontMatter = raw.deserialize()?;
if let Some(ref slug) = f.slug {
if slug == "" {
@ -140,21 +139,27 @@ impl Default for PageFrontMatter {
#[cfg(test)]
mod tests {
use super::PageFrontMatter;
use super::RawFrontMatter;
use tera::to_value;
use test_case::test_case;
#[test]
fn can_have_empty_front_matter() {
let content = r#" "#;
#[test_case(&RawFrontMatter::Toml(r#" "#); "toml")]
#[test_case(&RawFrontMatter::Toml(r#" "#); "yaml")]
fn can_have_empty_front_matter(content: &RawFrontMatter) {
let res = PageFrontMatter::parse(content);
println!("{:?}", res);
assert!(res.is_ok());
}
#[test]
fn can_parse_valid_front_matter() {
let content = r#"
title = "Hello"
description = "hey there""#;
#[test_case(&RawFrontMatter::Toml(r#"
title = "Hello"
description = "hey there"
"#); "toml")]
#[test_case(&RawFrontMatter::Yaml(r#"
title: Hello
description: hey there
"#); "yaml")]
fn can_parse_valid_front_matter(content: &RawFrontMatter) {
let res = PageFrontMatter::parse(content);
assert!(res.is_ok());
let res = res.unwrap();
@ -162,183 +167,281 @@ mod tests {
assert_eq!(res.description.unwrap(), "hey there".to_string())
}
#[test]
fn errors_with_invalid_front_matter() {
let content = r#"title = 1\n"#;
#[test_case(&RawFrontMatter::Toml(r#"title = |\n"#); "toml")]
#[test_case(&RawFrontMatter::Yaml(r#"title: |\n"#); "yaml")]
fn errors_with_invalid_front_matter(content: &RawFrontMatter) {
let res = PageFrontMatter::parse(content);
assert!(res.is_err());
}
#[test]
fn errors_on_present_but_empty_slug() {
let content = r#"
title = "Hello"
description = "hey there"
slug = """#;
#[test_case(&RawFrontMatter::Toml(r#"
title = "Hello"
description = "hey there"
slug = ""
"#); "toml")]
#[test_case(&RawFrontMatter::Yaml(r#"
title: Hello
description: hey there
slug: ""
"#); "yaml")]
fn errors_on_present_but_empty_slug(content: &RawFrontMatter) {
let res = PageFrontMatter::parse(content);
assert!(res.is_err());
}
#[test]
fn errors_on_present_but_empty_path() {
let content = r#"
title = "Hello"
description = "hey there"
path = """#;
#[test_case(&RawFrontMatter::Toml(r#"
title = "Hello"
description = "hey there"
path = ""
"#); "toml")]
#[test_case(&RawFrontMatter::Yaml(r#"
title: Hello
description: hey there
path: ""
"#); "yaml")]
fn errors_on_present_but_empty_path(content: &RawFrontMatter) {
let res = PageFrontMatter::parse(content);
assert!(res.is_err());
}
#[test]
fn can_parse_date_yyyy_mm_dd() {
let content = r#"
title = "Hello"
description = "hey there"
date = 2016-10-10
"#;
#[test_case(&RawFrontMatter::Toml(r#"
title = "Hello"
description = "hey there"
date = 2016-10-10
"#); "toml")]
#[test_case(&RawFrontMatter::Yaml(r#"
title: Hello
description: hey there
date: 2016-10-10
"#); "yaml")]
fn can_parse_date_yyyy_mm_dd(content: &RawFrontMatter) {
let res = PageFrontMatter::parse(content).unwrap();
assert!(res.datetime.is_some());
}
#[test]
fn can_parse_date_rfc3339() {
let content = r#"
title = "Hello"
description = "hey there"
date = 2002-10-02T15:00:00Z
"#;
#[test_case(&RawFrontMatter::Toml(r#"
title = "Hello"
description = "hey there"
date = 2002-10-02T15:00:00Z
"#); "toml")]
#[test_case(&RawFrontMatter::Yaml(r#"
title: Hello
description: hey there
date: 2002-10-02T15:00:00Z
"#); "yaml")]
fn can_parse_date_rfc3339(content: &RawFrontMatter) {
let res = PageFrontMatter::parse(content).unwrap();
assert!(res.datetime.is_some());
}
#[test]
fn can_parse_date_rfc3339_without_timezone() {
let content = r#"
title = "Hello"
description = "hey there"
date = 2002-10-02T15:00:00
"#;
#[test_case(&RawFrontMatter::Toml(r#"
title = "Hello"
description = "hey there"
date = 2002-10-02T15:00:00
"#); "toml")]
#[test_case(&RawFrontMatter::Yaml(r#"
title: Hello
description: hey there
date: 2002-10-02T15:00:00
"#); "yaml")]
fn can_parse_date_rfc3339_without_timezone(content: &RawFrontMatter) {
let res = PageFrontMatter::parse(content).unwrap();
assert!(res.datetime.is_some());
}
#[test]
fn can_parse_date_rfc3339_with_space() {
let content = r#"
title = "Hello"
description = "hey there"
date = 2002-10-02 15:00:00+02:00
"#;
#[test_case(&RawFrontMatter::Toml(r#"
title = "Hello"
description = "hey there"
date = 2002-10-02 15:00:00+02:00
"#); "toml")]
#[test_case(&RawFrontMatter::Yaml(r#"
title: Hello
description: hey there
date: 2002-10-02 15:00:00+02:00
"#); "yaml")]
fn can_parse_date_rfc3339_with_space(content: &RawFrontMatter) {
let res = PageFrontMatter::parse(content).unwrap();
assert!(res.datetime.is_some());
}
#[test]
fn can_parse_date_rfc3339_with_space_without_timezone() {
let content = r#"
title = "Hello"
description = "hey there"
date = 2002-10-02 15:00:00
"#;
#[test_case(&RawFrontMatter::Toml(r#"
title = "Hello"
description = "hey there"
date = 2002-10-02 15:00:00
"#); "toml")]
#[test_case(&RawFrontMatter::Yaml(r#"
title: Hello
description: hey there
date: 2002-10-02 15:00:00
"#); "yaml")]
fn can_parse_date_rfc3339_with_space_without_timezone(content: &RawFrontMatter) {
let res = PageFrontMatter::parse(content).unwrap();
assert!(res.datetime.is_some());
}
#[test]
fn can_parse_date_rfc3339_with_microseconds() {
let content = r#"
title = "Hello"
description = "hey there"
date = 2002-10-02T15:00:00.123456Z
"#;
#[test_case(&RawFrontMatter::Toml(r#"
title = "Hello"
description = "hey there"
date = 2002-10-02T15:00:00.123456Z
"#); "toml")]
#[test_case(&RawFrontMatter::Yaml(r#"
title: Hello
description: hey there
date: 2002-10-02T15:00:00.123456Z
"#); "yaml")]
fn can_parse_date_rfc3339_with_microseconds(content: &RawFrontMatter) {
let res = PageFrontMatter::parse(content).unwrap();
assert!(res.datetime.is_some());
}
#[test]
fn cannot_parse_random_date_format() {
let content = r#"
title = "Hello"
description = "hey there"
date = 2002/10/12"#;
#[test_case(&RawFrontMatter::Toml(r#"
title = "Hello"
description = "hey there"
date = 2002/10/12
"#); "toml")]
#[test_case(&RawFrontMatter::Yaml(r#"
title: Hello
description: hey there
date: 2002/10/12
"#); "yaml")]
fn cannot_parse_random_date_format(content: &RawFrontMatter) {
let res = PageFrontMatter::parse(content);
assert!(res.is_err());
}
#[test]
fn cannot_parse_invalid_date_format() {
let content = r#"
title = "Hello"
description = "hey there"
date = 2002-14-01"#;
#[test_case(&RawFrontMatter::Toml(r#"
title = "Hello"
description = "hey there"
date = 2002-14-01
"#); "toml")]
#[test_case(&RawFrontMatter::Yaml(r#"
title: Hello
description: hey there
date: 2002-14-01
"#); "yaml")]
fn cannot_parse_invalid_date_format(content: &RawFrontMatter) {
let res = PageFrontMatter::parse(content);
assert!(res.is_err());
}
#[test]
fn cannot_parse_date_as_string() {
let content = r#"
title = "Hello"
description = "hey there"
date = "2002-14-01""#;
#[test_case(&RawFrontMatter::Toml(r#"
title = "Hello"
description = "hey there"
date = "2016-10-10"
"#); "toml")]
#[test_case(&RawFrontMatter::Yaml(r#"
title: Hello
description: hey there
date: "2016-10-10"
"#); "yaml")]
fn can_parse_valid_date_as_string(content: &RawFrontMatter) {
let res = PageFrontMatter::parse(content).unwrap();
assert!(res.date.is_some());
}
#[test_case(&RawFrontMatter::Toml(r#"
title = "Hello"
description = "hey there"
date = "2002-14-01"
"#); "toml")]
#[test_case(&RawFrontMatter::Yaml(r#"
title: Hello
description: hey there
date: "2002-14-01"
"#); "yaml")]
fn cannot_parse_invalid_date_as_string(content: &RawFrontMatter) {
let res = PageFrontMatter::parse(content);
assert!(res.is_err());
}
#[test]
fn can_parse_dates_in_extra() {
let content = r#"
title = "Hello"
description = "hey there"
#[test_case(&RawFrontMatter::Toml(r#"
title = "Hello"
description = "hey there"
[extra]
some-date = 2002-14-01"#;
[extra]
some-date = 2002-14-01
"#); "toml")]
#[test_case(&RawFrontMatter::Yaml(r#"
title: Hello
description: hey there
extra:
some-date: 2002-14-01
"#); "yaml")]
fn can_parse_dates_in_extra(content: &RawFrontMatter) {
let res = PageFrontMatter::parse(content);
println!("{:?}", res);
assert!(res.is_ok());
assert_eq!(res.unwrap().extra["some-date"], to_value("2002-14-01").unwrap());
}
#[test]
fn can_parse_nested_dates_in_extra() {
let content = r#"
title = "Hello"
description = "hey there"
#[test_case(&RawFrontMatter::Toml(r#"
title = "Hello"
description = "hey there"
[extra.something]
some-date = 2002-14-01"#;
[extra.something]
some-date = 2002-14-01
"#); "toml")]
#[test_case(&RawFrontMatter::Yaml(r#"
title: Hello
description: hey there
extra:
something:
some-date: 2002-14-01
"#); "yaml")]
fn can_parse_nested_dates_in_extra(content: &RawFrontMatter) {
let res = PageFrontMatter::parse(content);
println!("{:?}", res);
assert!(res.is_ok());
assert_eq!(res.unwrap().extra["something"]["some-date"], to_value("2002-14-01").unwrap());
}
#[test]
fn can_parse_fully_nested_dates_in_extra() {
let content = r#"
title = "Hello"
description = "hey there"
#[test_case(&RawFrontMatter::Toml(r#"
title = "Hello"
description = "hey there"
[extra]
date_example = 2020-05-04
[[extra.questions]]
date = 2020-05-03
name = "Who is the prime minister of Uganda?""#;
[extra]
date_example = 2020-05-04
[[extra.questions]]
date = 2020-05-03
name = "Who is the prime minister of Uganda?"
"#); "toml")]
#[test_case(&RawFrontMatter::Yaml(r#"
title: Hello
description: hey there
extra:
date_example: 2020-05-04
questions:
- date: 2020-05-03
name: "Who is the prime minister of Uganda?"
"#); "yaml")]
fn can_parse_fully_nested_dates_in_extra(content: &RawFrontMatter) {
let res = PageFrontMatter::parse(content);
println!("{:?}", res);
assert!(res.is_ok());
assert_eq!(res.unwrap().extra["questions"][0]["date"], to_value("2020-05-03").unwrap());
}
#[test]
fn can_parse_taxonomies() {
let content = r#"
#[test_case(&RawFrontMatter::Toml(r#"
title = "Hello World"
[taxonomies]
tags = ["Rust", "JavaScript"]
categories = ["Dev"]
"#;
"#); "toml")]
#[test_case(&RawFrontMatter::Yaml(r#"
title: Hello World
taxonomies:
tags:
- Rust
- JavaScript
categories:
- Dev
"#); "yaml")]
fn can_parse_taxonomies(content: &RawFrontMatter) {
let res = PageFrontMatter::parse(content);
println!("{:?}", res);
assert!(res.is_ok());

View file

@ -2,9 +2,11 @@ use serde_derive::{Deserialize, Serialize};
use tera::{Map, Value};
use super::{InsertAnchor, SortBy};
use errors::{bail, Result};
use errors::Result;
use utils::de::fix_toml_dates;
use crate::RawFrontMatter;
static DEFAULT_PAGINATE_PATH: &str = "page";
/// The front matter of every section
@ -73,11 +75,8 @@ pub struct SectionFrontMatter {
}
impl SectionFrontMatter {
pub fn parse(toml: &str) -> Result<SectionFrontMatter> {
let mut f: SectionFrontMatter = match toml::from_str(toml) {
Ok(d) => d,
Err(e) => bail!(e),
};
pub fn parse(raw: &RawFrontMatter) -> Result<SectionFrontMatter> {
let mut f: SectionFrontMatter = raw.deserialize()?;
f.extra = match fix_toml_dates(f.extra) {
Value::Object(o) => o,

View file

@ -1,12 +1,32 @@
use serde::{Deserialize, Deserializer};
use serde_derive::Deserialize;
use tera::{Map, Value};
/// Used as an attribute when we want to convert from TOML to a string date
/// If a TOML datetime isn't present, it will accept a string and push it through
/// TOML's date time parser to ensure only valid dates are accepted.
/// Inspired by this proposal: https://github.com/alexcrichton/toml-rs/issues/269
pub fn from_toml_datetime<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
toml::value::Datetime::deserialize(deserializer).map(|s| Some(s.to_string()))
use serde::de::Error;
use std::str::FromStr;
#[derive(Deserialize)]
#[serde(untagged)]
enum MaybeDatetime {
Datetime(toml::value::Datetime),
String(String),
}
match MaybeDatetime::deserialize(deserializer)? {
MaybeDatetime::Datetime(d) => Ok(Some(d.to_string())),
MaybeDatetime::String(s) => match toml::value::Datetime::from_str(&s) {
Ok(d) => Ok(Some(d.to_string())),
Err(e) => Err(D::Error::custom(e)),
},
}
}
/// Returns key/value for a converted date from TOML.

View file

@ -79,6 +79,9 @@ by triple pluses (`+++`).
Although none of the front matter variables are mandatory, the opening and closing `+++` are required.
Note that even though the use of TOML is encouraged, YAML front matter is also supported to ease porting
legacy content. In this case the embedded metadata must be enclosed by triple minuses (`---`).
Here is an example page with all the available variables. The values provided below are the
default values.

View file

@ -33,6 +33,9 @@ to your templates through the `section.content` variable.
Although none of the front matter variables are mandatory, the opening and closing `+++` are required.
Note that even though the use of TOML is encouraged, YAML front matter is also supported to ease porting
legacy content. In this case the embedded metadata must be enclosed by triple minuses (`---`).
Here is an example `_index.md` with all the available variables. The values provided below are the
default values.