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<String>` to pages, as well as an `author: Option<String>`
to Config that will act as a default to use in RSS and Atom templates if
no page-level authors are specified.
This commit is contained in:
Seth Morabito 2023-02-11 06:09:12 -08:00 committed by Vincent Prouillet
parent b2f8a94b8d
commit f4a1e99b98
10 changed files with 165 additions and 42 deletions

View File

@ -58,6 +58,8 @@ pub struct Config {
/// If set, files from static/ will be hardlinked instead of copied to the output dir. /// If set, files from static/ will be hardlinked instead of copied to the output dir.
pub hard_link_static: bool, pub hard_link_static: bool,
pub taxonomies: Vec<taxonomies::TaxonomyConfig>, pub taxonomies: Vec<taxonomies::TaxonomyConfig>,
/// The default author for pages.
pub author: Option<String>,
/// Whether to compile the `sass` directory and output the css files into the static folder /// Whether to compile the `sass` directory and output the css files into the static folder
pub compile_sass: bool, pub compile_sass: bool,
@ -103,6 +105,7 @@ pub struct SerializedConfig<'a> {
generate_feed: bool, generate_feed: bool,
feed_filename: &'a str, feed_filename: &'a str,
taxonomies: &'a [taxonomies::TaxonomyConfig], taxonomies: &'a [taxonomies::TaxonomyConfig],
author: &'a Option<String>,
build_search_index: bool, build_search_index: bool,
extra: &'a HashMap<String, Toml>, extra: &'a HashMap<String, Toml>,
markdown: &'a markup::Markdown, markdown: &'a markup::Markdown,
@ -324,6 +327,7 @@ impl Config {
generate_feed: options.generate_feed, generate_feed: options.generate_feed,
feed_filename: &options.feed_filename, feed_filename: &options.feed_filename,
taxonomies: &options.taxonomies, taxonomies: &options.taxonomies,
author: &self.author,
build_search_index: options.build_search_index, build_search_index: options.build_search_index,
extra: &self.extra, extra: &self.extra,
markdown: &self.markdown, markdown: &self.markdown,
@ -373,6 +377,7 @@ impl Default for Config {
feed_filename: "atom.xml".to_string(), feed_filename: "atom.xml".to_string(),
hard_link_static: false, hard_link_static: false,
taxonomies: Vec::new(), taxonomies: Vec::new(),
author: None,
compile_sass: false, compile_sass: false,
minify_html: false, minify_html: false,
mode: Mode::Build, mode: Mode::Build,
@ -858,4 +863,15 @@ highlight_theme = "css"
let serialised = config.serialize(&config.default_language); let serialised = config.serialize(&config.default_language);
assert_eq!(serialised.markdown.highlight_theme, config.markdown.highlight_theme); 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()))
}
} }

View File

@ -49,6 +49,8 @@ pub struct PageFrontMatter {
pub taxonomies: HashMap<String, Vec<String>>, pub taxonomies: HashMap<String, Vec<String>>,
/// Integer to use to order content. Highest is at the bottom, lowest first /// Integer to use to order content. Highest is at the bottom, lowest first
pub weight: Option<usize>, pub weight: Option<usize>,
/// The authors of the page.
pub authors: Vec<String>,
/// All aliases for that page. Zola will create HTML templates that will /// All aliases for that page. Zola will create HTML templates that will
/// redirect to this /// redirect to this
#[serde(skip_serializing)] #[serde(skip_serializing)]
@ -153,6 +155,7 @@ impl Default for PageFrontMatter {
path: None, path: None,
taxonomies: HashMap::new(), taxonomies: HashMap::new(),
weight: None, weight: None,
authors: Vec::new(),
aliases: Vec::new(), aliases: Vec::new(),
template: None, template: None,
extra: Map::new(), extra: Map::new(),
@ -502,4 +505,27 @@ taxonomies:
println!("{:?}", res); println!("{:?}", res);
assert!(res.is_err()); 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
);
}
} }

View File

@ -345,6 +345,32 @@ Hello world"#;
assert_eq!(page.content, "<p>Hello world</p>\n".to_string()); assert_eq!(page.content, "<p>Hello world</p>\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] #[test]
fn test_can_make_url_from_sections_and_slug() { fn test_can_make_url_from_sections_and_slug() {
let content = r#" let content = r#"

View File

@ -55,6 +55,7 @@ pub struct SerializingPage<'a> {
month: Option<u8>, month: Option<u8>,
day: Option<u8>, day: Option<u8>,
taxonomies: &'a HashMap<String, Vec<String>>, taxonomies: &'a HashMap<String, Vec<String>>,
authors: &'a [String],
extra: &'a Map<String, Value>, extra: &'a Map<String, Value>,
path: &'a str, path: &'a str,
components: &'a [String], components: &'a [String],
@ -119,6 +120,7 @@ impl<'a> SerializingPage<'a> {
month, month,
day, day,
taxonomies: &page.meta.taxonomies, taxonomies: &page.meta.taxonomies,
authors: &page.meta.authors,
path: &page.path, path: &page.path,
components: &page.components, components: &page.components,
summary: &page.summary, summary: &page.summary,

View File

@ -836,6 +836,31 @@ fn panics_on_invalid_external_domain() {
site.load().expect("link check test_site"); 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 // Follows test_site/themes/sample/templates/current_path.html
fn current_path(path: &str) -> String { fn current_path(path: &str) -> String {
format!("[current_path]({})", path) format!("[current_path]({})", path)

View File

@ -1,32 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="{{ lang }}"> <feed xmlns="http://www.w3.org/2005/Atom" xml:lang="{{ lang }}">
<title>{{ config.title }} <title>{{ config.title }}
{%- if term %} - {{ term.name }} {%- if term %} - {{ term.name }}
{%- elif section.title %} - {{ section.title }} {%- elif section.title %} - {{ section.title }}
{%- endif -%} {%- endif -%}
</title> </title>
{%- if config.description %} {%- if config.description %}
<subtitle>{{ config.description }}</subtitle> <subtitle>{{ config.description }}</subtitle>
{%- endif %} {%- endif %}
<link href="{{ feed_url | safe }}" rel="self" type="application/atom+xml"/> <link href="{{ feed_url | safe }}" rel="self" type="application/atom+xml"/>
<link href=" <link href="
{%- if section -%} {%- if section -%}
{{ section.permalink | escape_xml | safe }} {{ section.permalink | escape_xml | safe }}
{%- else -%} {%- else -%}
{{ config.base_url | escape_xml | safe }} {{ config.base_url | escape_xml | safe }}
{%- endif -%} {%- endif -%}
"/> "/>
<generator uri="https://www.getzola.org/">Zola</generator> <generator uri="https://www.getzola.org/">Zola</generator>
<updated>{{ last_updated | date(format="%+") }}</updated> <updated>{{ last_updated | date(format="%+") }}</updated>
<id>{{ feed_url | safe }}</id> <id>{{ feed_url | safe }}</id>
{%- for page in pages %} {%- for page in pages %}
<entry xml:lang="{{ page.lang }}"> <entry xml:lang="{{ page.lang }}">
<title>{{ page.title }}</title> <title>{{ page.title }}</title>
<published>{{ page.date | date(format="%+") }}</published> <published>{{ page.date | date(format="%+") }}</published>
<updated>{{ page.updated | default(value=page.date) | date(format="%+") }}</updated> <updated>{{ page.updated | default(value=page.date) | date(format="%+") }}</updated>
<link rel="alternate" href="{{ page.permalink | safe }}" type="text/html"/> <author>
<id>{{ page.permalink | safe }}</id> <name>
<content type="html">{{ page.content }}</content> {%- if page.authors -%}
</entry> {{ page.authors[0] }}
{%- endfor %} {%- elif config.author -%}
{{ config.author }}
{%- else -%}
Unknown
{%- endif -%}
</name>
</author>
<link rel="alternate" href="{{ page.permalink | safe }}" type="text/html"/>
<id>{{ page.permalink | safe }}</id>
<content type="html">{{ page.content }}</content>
</entry>
{%- endfor %}
</feed> </feed>

View File

@ -6,25 +6,35 @@
{%- elif section.title %} - {{ section.title }} {%- elif section.title %} - {{ section.title }}
{%- endif -%} {%- endif -%}
</title> </title>
<link>{%- if section -%} <link>
{{ section.permalink | escape_xml | safe }} {%- if section -%}
{%- else -%} {{ section.permalink | escape_xml | safe }}
{{ config.base_url | escape_xml | safe }} {%- else -%}
{%- endif -%} {{ config.base_url | escape_xml | safe }}
</link> {%- endif -%}
<description>{{ config.description }}</description> </link>
<generator>Zola</generator> <description>{{ config.description }}</description>
<language>{{ lang }}</language> <generator>Zola</generator>
<atom:link href="{{ feed_url | safe }}" rel="self" type="application/rss+xml"/> <language>{{ lang }}</language>
<lastBuildDate>{{ last_updated | date(format="%a, %d %b %Y %H:%M:%S %z") }}</lastBuildDate> <atom:link href="{{ feed_url | safe }}" rel="self" type="application/rss+xml"/>
{%- for page in pages %} <lastBuildDate>{{ last_updated | date(format="%a, %d %b %Y %H:%M:%S %z") }}</lastBuildDate>
<item> {%- for page in pages %}
<title>{{ page.title }}</title> <item>
<pubDate>{{ page.date | date(format="%a, %d %b %Y %H:%M:%S %z") }}</pubDate> <title>{{ page.title }}</title>
<link>{{ page.permalink | escape_xml | safe }}</link> <pubDate>{{ page.date | date(format="%a, %d %b %Y %H:%M:%S %z") }}</pubDate>
<guid>{{ page.permalink | escape_xml | safe }}</guid> <author>
<description>{% if page.summary %}{{ page.summary }}{% else %}{{ page.content }}{% endif %}</description> {%- if page.authors -%}
</item> {{ page.authors[0] }}
{%- endfor %} {%- elif config.author -%}
{{ config.author }}
{%- else -%}
Unknown
{%- endif -%}
</author>
<link>{{ page.permalink | escape_xml | safe }}</link>
<guid>{{ page.permalink | escape_xml | safe }}</guid>
<description>{% if page.summary %}{{ page.summary }}{% else %}{{ page.content }}{% endif %}</description>
</item>
{%- endfor %}
</channel> </channel>
</rss> </rss>

View File

@ -126,6 +126,10 @@ path = ""
# current one. This takes an array of paths, not URLs. # current one. This takes an array of paths, not URLs.
aliases = [] 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 # 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 # `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. # hasn't set `in_search_index` to "false" in its front matter.

View File

@ -11,6 +11,8 @@ taxonomies = [
ignored_content = ["*/ignored.md"] ignored_content = ["*/ignored.md"]
author = "config@example.com (Config Author)"
[markdown] [markdown]
highlight_code = true highlight_code = true
highlight_theme = "custom_gruvbox" highlight_theme = "custom_gruvbox"

View File

@ -2,4 +2,5 @@
title = "A transparent page" title = "A transparent page"
description = "" description = ""
date = 2018-10-10 date = 2018-10-10
authors = ["page@example.com (Page Author)"]
+++ +++