From edf20c2e01bbd74f95dfffa502adbbac17dc7d0f Mon Sep 17 00:00:00 2001 From: Seth Morabito Date: Sat, 11 Feb 2023 06:09:12 -0800 Subject: [PATCH] Page and config authors (#2024) (#2092) The W3C feed validator fails to validate RSS 2.0 and Atom 1.0 feed elements that do not contain a valid author. This change adds an `authors: Vec` to pages, as well as an `author: Option` to Config that will act as a default to use in RSS and Atom templates if no page-level authors are specified. --- components/config/src/config/mod.rs | 16 ++++++ components/content/src/front_matter/page.rs | 26 +++++++++ components/content/src/page.rs | 26 +++++++++ components/content/src/ser.rs | 2 + components/site/tests/site.rs | 25 +++++++++ components/templates/src/builtins/atom.xml | 55 +++++++++++-------- components/templates/src/builtins/rss.xml | 50 ++++++++++------- docs/content/documentation/content/page.md | 4 ++ test_site/config.toml | 2 + .../content/posts/2018/transparent-page.md | 1 + 10 files changed, 165 insertions(+), 42 deletions(-) diff --git a/components/config/src/config/mod.rs b/components/config/src/config/mod.rs index c8a8f6359a..e0674276dd 100644 --- a/components/config/src/config/mod.rs +++ b/components/config/src/config/mod.rs @@ -58,6 +58,8 @@ pub struct Config { /// If set, files from static/ will be hardlinked instead of copied to the output dir. pub hard_link_static: bool, pub taxonomies: Vec, + /// The default author for pages. + pub author: Option, /// Whether to compile the `sass` directory and output the css files into the static folder pub compile_sass: bool, @@ -103,6 +105,7 @@ pub struct SerializedConfig<'a> { generate_feed: bool, feed_filename: &'a str, taxonomies: &'a [taxonomies::TaxonomyConfig], + author: &'a Option, build_search_index: bool, extra: &'a HashMap, markdown: &'a markup::Markdown, @@ -324,6 +327,7 @@ impl Config { generate_feed: options.generate_feed, feed_filename: &options.feed_filename, taxonomies: &options.taxonomies, + author: &self.author, build_search_index: options.build_search_index, extra: &self.extra, markdown: &self.markdown, @@ -373,6 +377,7 @@ impl Default for Config { feed_filename: "atom.xml".to_string(), hard_link_static: false, taxonomies: Vec::new(), + author: None, compile_sass: false, minify_html: false, mode: Mode::Build, @@ -858,4 +863,15 @@ highlight_theme = "css" let serialised = config.serialize(&config.default_language); assert_eq!(serialised.markdown.highlight_theme, config.markdown.highlight_theme); } + + #[test] + fn sets_default_author_if_present() { + let config = r#" +title = "My Site" +base_url = "example.com" +author = "person@example.com (Some Person)" +"#; + let config = Config::parse(config).unwrap(); + assert_eq!(config.author, Some("person@example.com (Some Person)".to_owned())) + } } diff --git a/components/content/src/front_matter/page.rs b/components/content/src/front_matter/page.rs index 60d8f7560c..4d2ed5ebfb 100644 --- a/components/content/src/front_matter/page.rs +++ b/components/content/src/front_matter/page.rs @@ -49,6 +49,8 @@ pub struct PageFrontMatter { pub taxonomies: HashMap>, /// Integer to use to order content. Highest is at the bottom, lowest first pub weight: Option, + /// The authors of the page. + pub authors: Vec, /// All aliases for that page. Zola will create HTML templates that will /// redirect to this #[serde(skip_serializing)] @@ -153,6 +155,7 @@ impl Default for PageFrontMatter { path: None, taxonomies: HashMap::new(), weight: None, + authors: Vec::new(), aliases: Vec::new(), template: None, extra: Map::new(), @@ -502,4 +505,27 @@ taxonomies: println!("{:?}", res); assert!(res.is_err()); } + + #[test_case(&RawFrontMatter::Toml(r#" +authors = ["person1@example.com (Person One)", "person2@example.com (Person Two)"] +"#); "toml")] + #[test_case(&RawFrontMatter::Yaml(r#" +title: Hello World +authors: + - person1@example.com (Person One) + - person2@example.com (Person Two) +"#); "yaml")] + fn can_parse_authors(content: &RawFrontMatter) { + let res = PageFrontMatter::parse(content); + assert!(res.is_ok()); + let res2 = res.unwrap(); + assert_eq!(res2.authors.len(), 2); + assert_eq!( + vec!( + "person1@example.com (Person One)".to_owned(), + "person2@example.com (Person Two)".to_owned() + ), + res2.authors + ); + } } diff --git a/components/content/src/page.rs b/components/content/src/page.rs index f54d42c5d1..461b8a736f 100644 --- a/components/content/src/page.rs +++ b/components/content/src/page.rs @@ -345,6 +345,32 @@ Hello world"#; assert_eq!(page.content, "

Hello world

\n".to_string()); } + #[test] + fn can_parse_author() { + let config = Config::default_for_test(); + let content = r#" ++++ +title = "Hello" +description = "hey there" +authors = ["person@example.com (A. Person)"] ++++ +Hello world"#; + let res = Page::parse(Path::new("post.md"), content, &config, &PathBuf::new()); + assert!(res.is_ok()); + let mut page = res.unwrap(); + page.render_markdown( + &HashMap::default(), + &Tera::default(), + &config, + InsertAnchor::None, + &HashMap::new(), + ) + .unwrap(); + + assert_eq!(1, page.meta.authors.len()); + assert_eq!("person@example.com (A. Person)", page.meta.authors.get(0).unwrap()); + } + #[test] fn test_can_make_url_from_sections_and_slug() { let content = r#" diff --git a/components/content/src/ser.rs b/components/content/src/ser.rs index a767bcfa08..ebc193ef8e 100644 --- a/components/content/src/ser.rs +++ b/components/content/src/ser.rs @@ -55,6 +55,7 @@ pub struct SerializingPage<'a> { month: Option, day: Option, taxonomies: &'a HashMap>, + authors: &'a [String], extra: &'a Map, path: &'a str, components: &'a [String], @@ -119,6 +120,7 @@ impl<'a> SerializingPage<'a> { month, day, taxonomies: &page.meta.taxonomies, + authors: &page.meta.authors, path: &page.path, components: &page.components, summary: &page.summary, diff --git a/components/site/tests/site.rs b/components/site/tests/site.rs index 323a44f19a..60e5adaa1e 100644 --- a/components/site/tests/site.rs +++ b/components/site/tests/site.rs @@ -836,6 +836,31 @@ fn panics_on_invalid_external_domain() { site.load().expect("link check test_site"); } +#[test] +fn can_find_site_and_page_authors() { + let mut path = env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf(); + path.push("test_site"); + let config_file = path.join("config.toml"); + let mut site = Site::new(&path, config_file).unwrap(); + site.load().unwrap(); + let library = site.library.read().unwrap(); + + // The config has a global default author set. + let author = site.config.author; + assert_eq!(Some("config@example.com (Config Author)".to_string()), author); + + let posts_path = path.join("content").join("posts"); + let posts_section = library.sections.get(&posts_path.join("_index.md")).unwrap(); + + let p1 = &library.pages[&posts_section.pages[0]]; + let p2 = &library.pages[&posts_section.pages[1]]; + + // Only the first page has had an author added. + assert_eq!(1, p1.meta.authors.len()); + assert_eq!("page@example.com (Page Author)", p1.meta.authors.get(0).unwrap()); + assert_eq!(0, p2.meta.authors.len()); +} + // Follows test_site/themes/sample/templates/current_path.html fn current_path(path: &str) -> String { format!("[current_path]({})", path) diff --git a/components/templates/src/builtins/atom.xml b/components/templates/src/builtins/atom.xml index e515636d06..acdd6b2e39 100644 --- a/components/templates/src/builtins/atom.xml +++ b/components/templates/src/builtins/atom.xml @@ -1,32 +1,43 @@ - {{ config.title }} - {%- if term %} - {{ term.name }} + <title>{{ config.title }} + {%- if term %} - {{ term.name }} {%- elif section.title %} - {{ section.title }} - {%- endif -%} - - {%- if config.description %} - {{ config.description }} - {%- endif %} - - + - Zola - {{ last_updated | date(format="%+") }} - {{ feed_url | safe }} - {%- for page in pages %} - - {{ page.title }} - {{ page.date | date(format="%+") }} - {{ page.updated | default(value=page.date) | date(format="%+") }} - - {{ page.permalink | safe }} - {{ page.content }} - - {%- endfor %} + Zola + {{ last_updated | date(format="%+") }} + {{ feed_url | safe }} + {%- for page in pages %} + + {{ page.title }} + {{ page.date | date(format="%+") }} + {{ page.updated | default(value=page.date) | date(format="%+") }} + + + {%- if page.authors -%} + {{ page.authors[0] }} + {%- elif config.author -%} + {{ config.author }} + {%- else -%} + Unknown + {%- endif -%} + + + + {{ page.permalink | safe }} + {{ page.content }} + + {%- endfor %} diff --git a/components/templates/src/builtins/rss.xml b/components/templates/src/builtins/rss.xml index d68302b543..1c09e4cf4d 100644 --- a/components/templates/src/builtins/rss.xml +++ b/components/templates/src/builtins/rss.xml @@ -6,25 +6,35 @@ {%- elif section.title %} - {{ section.title }} {%- endif -%} - {%- if section -%} - {{ section.permalink | escape_xml | safe }} - {%- else -%} - {{ config.base_url | escape_xml | safe }} - {%- endif -%} - - {{ config.description }} - Zola - {{ lang }} - - {{ last_updated | date(format="%a, %d %b %Y %H:%M:%S %z") }} - {%- for page in pages %} - - {{ page.title }} - {{ page.date | date(format="%a, %d %b %Y %H:%M:%S %z") }} - {{ page.permalink | escape_xml | safe }} - {{ page.permalink | escape_xml | safe }} - {% if page.summary %}{{ page.summary }}{% else %}{{ page.content }}{% endif %} - - {%- endfor %} + + {%- if section -%} + {{ section.permalink | escape_xml | safe }} + {%- else -%} + {{ config.base_url | escape_xml | safe }} + {%- endif -%} + + {{ config.description }} + Zola + {{ lang }} + + {{ last_updated | date(format="%a, %d %b %Y %H:%M:%S %z") }} + {%- for page in pages %} + + {{ page.title }} + {{ page.date | date(format="%a, %d %b %Y %H:%M:%S %z") }} + + {%- if page.authors -%} + {{ page.authors[0] }} + {%- elif config.author -%} + {{ config.author }} + {%- else -%} + Unknown + {%- endif -%} + + {{ page.permalink | escape_xml | safe }} + {{ page.permalink | escape_xml | safe }} + {% if page.summary %}{{ page.summary }}{% else %}{{ page.content }}{% endif %} + + {%- endfor %} diff --git a/docs/content/documentation/content/page.md b/docs/content/documentation/content/page.md index 18d0a8cb96..c5e9a962d0 100644 --- a/docs/content/documentation/content/page.md +++ b/docs/content/documentation/content/page.md @@ -126,6 +126,10 @@ path = "" # current one. This takes an array of paths, not URLs. aliases = [] +# A list of page authors. If a site feed is enabled, the first author (if any) +# will be used as the page's author in the default feed template. +authors = [] + # When set to "true", the page will be in the search index. This is only used if # `build_search_index` is set to "true" in the Zola configuration and the parent section # hasn't set `in_search_index` to "false" in its front matter. diff --git a/test_site/config.toml b/test_site/config.toml index 21b9e74949..e3fcbfd64d 100644 --- a/test_site/config.toml +++ b/test_site/config.toml @@ -11,6 +11,8 @@ taxonomies = [ ignored_content = ["*/ignored.md"] +author = "config@example.com (Config Author)" + [markdown] highlight_code = true highlight_theme = "custom_gruvbox" diff --git a/test_site/content/posts/2018/transparent-page.md b/test_site/content/posts/2018/transparent-page.md index 21f207facb..767c997958 100644 --- a/test_site/content/posts/2018/transparent-page.md +++ b/test_site/content/posts/2018/transparent-page.md @@ -2,4 +2,5 @@ title = "A transparent page" description = "" date = 2018-10-10 +authors = ["page@example.com (Page Author)"] +++