Added support for multiple feeds (i.e. generating both Atom and RSS) (#2477)

* Added support for multiple feeds

* Implemented backwards-compatibility for feed config

* Added a test for feed config backwards-compat, fixed bugs

- Fixed language config merge bug found by a test
- Adjusted two existing tests to fully check stuff related to multiple feeds
- Added a new test for backwards-compatibility of the changes
- Fixed bugs found by the newly added test

* Renamed MightBeSingle to SingleOrVec

* Made the multiple feeds config changes "loudly" backwards-incompatible

* added #[serde(deny_unknown_fields)] to front-matter, fixed problems this found in tests
This commit is contained in:
LunarEclipse363 2024-06-19 14:57:18 +02:00 committed by Vincent Prouillet
parent 7f59792d50
commit c054ed1b10
23 changed files with 163 additions and 128 deletions

View file

@ -11,6 +11,10 @@
- Add `render = false` capability to pages - Add `render = false` capability to pages
- Handle string dates in YAML front-matter - Handle string dates in YAML front-matter
- Add support for fuse.js search format - Add support for fuse.js search format
- Added support for generating multiple kinds of feeds at once
- Changed config options named `generate_feed` to `generate_feeds` (both in config.toml and in section front-matter)
- Changed config option `feed_filename: String` to `feed_filenames: Vec<String>`
- The config file no longer allows arbitrary fields outside the `[extra]` section
## 0.18.0 (2023-12-18) ## 0.18.0 (2023-12-18)

View file

@ -8,17 +8,17 @@ use crate::config::search;
use crate::config::taxonomies; use crate::config::taxonomies;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)] #[serde(default, deny_unknown_fields)]
pub struct LanguageOptions { pub struct LanguageOptions {
/// Title of the site. Defaults to None /// Title of the site. Defaults to None
pub title: Option<String>, pub title: Option<String>,
/// Description of the site. Defaults to None /// Description of the site. Defaults to None
pub description: Option<String>, pub description: Option<String>,
/// Whether to generate a feed for that language, defaults to `false` /// Whether to generate feeds for that language, defaults to `false`
pub generate_feed: bool, pub generate_feeds: bool,
/// The filename to use for feeds. Used to find the template, too. /// The filenames to use for feeds. Used to find the templates, too.
/// Defaults to "atom.xml", with "rss.xml" also having a template provided out of the box. /// Defaults to ["atom.xml"], with "rss.xml" also having a template provided out of the box.
pub feed_filename: String, pub feed_filenames: Vec<String>,
pub taxonomies: Vec<taxonomies::TaxonomyConfig>, pub taxonomies: Vec<taxonomies::TaxonomyConfig>,
/// Whether to generate search index for that language, defaults to `false` /// Whether to generate search index for that language, defaults to `false`
pub build_search_index: bool, pub build_search_index: bool,
@ -66,9 +66,10 @@ impl LanguageOptions {
merge_field!(self.title, other.title, "title"); merge_field!(self.title, other.title, "title");
merge_field!(self.description, other.description, "description"); merge_field!(self.description, other.description, "description");
merge_field!( merge_field!(
self.feed_filename == "atom.xml", self.feed_filenames.is_empty()
self.feed_filename, || self.feed_filenames == LanguageOptions::default().feed_filenames,
other.feed_filename, self.feed_filenames,
other.feed_filenames,
"feed_filename" "feed_filename"
); );
merge_field!(self.taxonomies.is_empty(), self.taxonomies, other.taxonomies, "taxonomies"); merge_field!(self.taxonomies.is_empty(), self.taxonomies, other.taxonomies, "taxonomies");
@ -79,7 +80,7 @@ impl LanguageOptions {
"translations" "translations"
); );
self.generate_feed = self.generate_feed || other.generate_feed; self.generate_feeds = self.generate_feeds || other.generate_feeds;
self.build_search_index = self.build_search_index || other.build_search_index; self.build_search_index = self.build_search_index || other.build_search_index;
if self.search == search::Search::default() { if self.search == search::Search::default() {
@ -101,8 +102,8 @@ impl Default for LanguageOptions {
LanguageOptions { LanguageOptions {
title: None, title: None,
description: None, description: None,
generate_feed: false, generate_feeds: false,
feed_filename: "atom.xml".to_string(), feed_filenames: vec!["atom.xml".to_string()],
taxonomies: vec![], taxonomies: vec![],
build_search_index: false, build_search_index: false,
search: search::Search::default(), search: search::Search::default(),
@ -129,8 +130,8 @@ mod tests {
let mut base_default_language_options = LanguageOptions { let mut base_default_language_options = LanguageOptions {
title: Some("Site's title".to_string()), title: Some("Site's title".to_string()),
description: None, description: None,
generate_feed: true, generate_feeds: true,
feed_filename: "atom.xml".to_string(), feed_filenames: vec!["atom.xml".to_string()],
taxonomies: vec![], taxonomies: vec![],
build_search_index: true, build_search_index: true,
search: search::Search::default(), search: search::Search::default(),
@ -140,8 +141,8 @@ mod tests {
let section_default_language_options = LanguageOptions { let section_default_language_options = LanguageOptions {
title: None, title: None,
description: Some("Site's description".to_string()), description: Some("Site's description".to_string()),
generate_feed: false, generate_feeds: false,
feed_filename: "rss.xml".to_string(), feed_filenames: vec!["rss.xml".to_string()],
taxonomies: vec![], taxonomies: vec![],
build_search_index: true, build_search_index: true,
search: search::Search::default(), search: search::Search::default(),
@ -156,8 +157,8 @@ mod tests {
let mut base_default_language_options = LanguageOptions { let mut base_default_language_options = LanguageOptions {
title: Some("Site's title".to_string()), title: Some("Site's title".to_string()),
description: Some("Duplicate site description".to_string()), description: Some("Duplicate site description".to_string()),
generate_feed: true, generate_feeds: true,
feed_filename: "".to_string(), feed_filenames: vec![],
taxonomies: vec![], taxonomies: vec![],
build_search_index: true, build_search_index: true,
search: search::Search::default(), search: search::Search::default(),
@ -167,8 +168,8 @@ mod tests {
let section_default_language_options = LanguageOptions { let section_default_language_options = LanguageOptions {
title: None, title: None,
description: Some("Site's description".to_string()), description: Some("Site's description".to_string()),
generate_feed: false, generate_feeds: false,
feed_filename: "Some feed_filename".to_string(), feed_filenames: vec!["Some feed_filename".to_string()],
taxonomies: vec![], taxonomies: vec![],
build_search_index: true, build_search_index: true,
search: search::Search::default(), search: search::Search::default(),

View file

@ -30,7 +30,7 @@ pub enum Mode {
} }
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
#[serde(default)] #[serde(default, deny_unknown_fields)]
pub struct Config { pub struct Config {
/// Base URL of the site, the only required config argument /// Base URL of the site, the only required config argument
pub base_url: String, pub base_url: String,
@ -49,13 +49,13 @@ pub struct Config {
/// The translations strings for the default language /// The translations strings for the default language
translations: HashMap<String, String>, translations: HashMap<String, String>,
/// Whether to generate a feed. Defaults to false. /// Whether to generate feeds. Defaults to false.
pub generate_feed: bool, pub generate_feeds: bool,
/// The number of articles to include in the feed. Defaults to including all items. /// The number of articles to include in the feed. Defaults to including all items.
pub feed_limit: Option<usize>, pub feed_limit: Option<usize>,
/// The filename to use for feeds. Used to find the template, too. /// The filenames to use for feeds. Used to find the templates, too.
/// Defaults to "atom.xml", with "rss.xml" also having a template provided out of the box. /// Defaults to ["atom.xml"], with "rss.xml" also having a template provided out of the box.
pub feed_filename: String, pub feed_filenames: Vec<String>,
/// 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>,
@ -109,7 +109,7 @@ pub struct SerializedConfig<'a> {
languages: HashMap<&'a String, &'a languages::LanguageOptions>, languages: HashMap<&'a String, &'a languages::LanguageOptions>,
default_language: &'a str, default_language: &'a str,
generate_feed: bool, generate_feed: bool,
feed_filename: &'a str, feed_filenames: &'a [String],
taxonomies: &'a [taxonomies::TaxonomyConfig], taxonomies: &'a [taxonomies::TaxonomyConfig],
author: &'a Option<String>, author: &'a Option<String>,
build_search_index: bool, build_search_index: bool,
@ -183,8 +183,10 @@ impl Config {
/// Makes a url, taking into account that the base url might have a trailing slash /// Makes a url, taking into account that the base url might have a trailing slash
pub fn make_permalink(&self, path: &str) -> String { pub fn make_permalink(&self, path: &str) -> String {
let trailing_bit = let trailing_bit = if path.ends_with('/')
if path.ends_with('/') || path.ends_with(&self.feed_filename) || path.is_empty() { || self.feed_filenames.iter().any(|feed_filename| path.ends_with(feed_filename))
|| path.is_empty()
{
"" ""
} else { } else {
"/" "/"
@ -212,8 +214,8 @@ impl Config {
let mut base_language_options = languages::LanguageOptions { let mut base_language_options = languages::LanguageOptions {
title: self.title.clone(), title: self.title.clone(),
description: self.description.clone(), description: self.description.clone(),
generate_feed: self.generate_feed, generate_feeds: self.generate_feeds,
feed_filename: self.feed_filename.clone(), feed_filenames: self.feed_filenames.clone(),
build_search_index: self.build_search_index, build_search_index: self.build_search_index,
taxonomies: self.taxonomies.clone(), taxonomies: self.taxonomies.clone(),
search: self.search.clone(), search: self.search.clone(),
@ -320,8 +322,8 @@ impl Config {
description: &options.description, description: &options.description,
languages: self.languages.iter().filter(|(k, _)| k.as_str() != lang).collect(), languages: self.languages.iter().filter(|(k, _)| k.as_str() != lang).collect(),
default_language: &self.default_language, default_language: &self.default_language,
generate_feed: options.generate_feed, generate_feed: options.generate_feeds,
feed_filename: &options.feed_filename, feed_filenames: &options.feed_filenames,
taxonomies: &options.taxonomies, taxonomies: &options.taxonomies,
author: &self.author, author: &self.author,
build_search_index: options.build_search_index, build_search_index: options.build_search_index,
@ -369,9 +371,9 @@ impl Default for Config {
theme: None, theme: None,
default_language: "en".to_string(), default_language: "en".to_string(),
languages: HashMap::new(), languages: HashMap::new(),
generate_feed: false, generate_feeds: false,
feed_limit: None, feed_limit: None,
feed_filename: "atom.xml".to_string(), feed_filenames: vec!["atom.xml".to_string()],
hard_link_static: false, hard_link_static: false,
taxonomies: Vec::new(), taxonomies: Vec::new(),
author: None, author: None,
@ -428,8 +430,8 @@ mod tests {
languages::LanguageOptions { languages::LanguageOptions {
title: None, title: None,
description: description_lang_section.clone(), description: description_lang_section.clone(),
generate_feed: true, generate_feeds: true,
feed_filename: config.feed_filename.clone(), feed_filenames: config.feed_filenames.clone(),
taxonomies: config.taxonomies.clone(), taxonomies: config.taxonomies.clone(),
build_search_index: false, build_search_index: false,
search: search::Search::default(), search: search::Search::default(),
@ -456,8 +458,8 @@ mod tests {
languages::LanguageOptions { languages::LanguageOptions {
title: title_lang_section.clone(), title: title_lang_section.clone(),
description: None, description: None,
generate_feed: true, generate_feeds: true,
feed_filename: config.feed_filename.clone(), feed_filenames: config.feed_filenames.clone(),
taxonomies: config.taxonomies.clone(), taxonomies: config.taxonomies.clone(),
build_search_index: false, build_search_index: false,
search: search::Search::default(), search: search::Search::default(),
@ -976,4 +978,16 @@ author = "person@example.com (Some Person)"
let config = Config::parse(config).unwrap(); let config = Config::parse(config).unwrap();
assert_eq!(config.author, Some("person@example.com (Some Person)".to_owned())) assert_eq!(config.author, Some("person@example.com (Some Person)".to_owned()))
} }
#[test]
#[should_panic]
fn test_backwards_incompatibility_for_feeds() {
let config = r#"
base_url = "example.com"
generate_feed = true
feed_filename = "test.xml"
"#;
Config::parse(config).unwrap();
}
} }

View file

@ -12,7 +12,7 @@ static DEFAULT_PAGINATE_PATH: &str = "page";
/// The front matter of every section /// The front matter of every section
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)] #[serde(default, deny_unknown_fields)]
pub struct SectionFrontMatter { pub struct SectionFrontMatter {
/// <title> of the page /// <title> of the page
pub title: Option<String>, pub title: Option<String>,
@ -69,7 +69,7 @@ pub struct SectionFrontMatter {
pub aliases: Vec<String>, pub aliases: Vec<String>,
/// Whether to generate a feed for the current section /// Whether to generate a feed for the current section
#[serde(skip_serializing)] #[serde(skip_serializing)]
pub generate_feed: bool, pub generate_feeds: bool,
/// Any extra parameter present in the front matter /// Any extra parameter present in the front matter
pub extra: Map<String, Value>, pub extra: Map<String, Value>,
} }
@ -113,7 +113,7 @@ impl Default for SectionFrontMatter {
transparent: false, transparent: false,
page_template: None, page_template: None,
aliases: Vec::new(), aliases: Vec::new(),
generate_feed: false, generate_feeds: false,
extra: Map::new(), extra: Map::new(),
draft: false, draft: false,
} }

View file

@ -283,7 +283,7 @@ mod tests {
create_dir_all(path.join(&article_path).join("foo/baz/quux")) create_dir_all(path.join(&article_path).join("foo/baz/quux"))
.expect("create nested temp dir"); .expect("create nested temp dir");
let mut f = File::create(article_path.join("_index.md")).unwrap(); let mut f = File::create(article_path.join("_index.md")).unwrap();
f.write_all(b"+++\nslug=\"hey\"\n+++\n").unwrap(); f.write_all(b"+++\n+++\n").unwrap();
File::create(article_path.join("example.js")).unwrap(); File::create(article_path.join("example.js")).unwrap();
File::create(article_path.join("graph.jpg")).unwrap(); File::create(article_path.join("graph.jpg")).unwrap();
File::create(article_path.join("fail.png")).unwrap(); File::create(article_path.join("fail.png")).unwrap();

View file

@ -160,7 +160,7 @@ pub struct SerializingSection<'a> {
subsections: Vec<&'a str>, subsections: Vec<&'a str>,
translations: Vec<TranslatedContent<'a>>, translations: Vec<TranslatedContent<'a>>,
backlinks: Vec<BackLink<'a>>, backlinks: Vec<BackLink<'a>>,
generate_feed: bool, generate_feeds: bool,
transparent: bool, transparent: bool,
} }
@ -220,7 +220,7 @@ impl<'a> SerializingSection<'a> {
reading_time: section.reading_time, reading_time: section.reading_time,
assets: &section.serialized_assets, assets: &section.serialized_assets,
lang: &section.lang, lang: &section.lang,
generate_feed: section.meta.generate_feed, generate_feeds: section.meta.generate_feeds,
transparent: section.meta.transparent, transparent: section.meta.transparent,
pages, pages,
subsections, subsections,

View file

@ -42,7 +42,7 @@ fn bench_render_feed(b: &mut test::Bencher) {
let public = &tmp_dir.path().join("public"); let public = &tmp_dir.path().join("public");
site.set_output_path(&public); site.set_output_path(&public);
b.iter(|| { b.iter(|| {
site.render_feed( site.render_feeds(
site.library.read().unwrap().pages.values().collect(), site.library.read().unwrap().pages.values().collect(),
None, None,
&site.config.default_language, &site.config.default_language,

View file

@ -27,13 +27,13 @@ impl<'a> SerializedFeedTaxonomyItem<'a> {
} }
} }
pub fn render_feed( pub fn render_feeds(
site: &Site, site: &Site,
all_pages: Vec<&Page>, all_pages: Vec<&Page>,
lang: &str, lang: &str,
base_path: Option<&PathBuf>, base_path: Option<&PathBuf>,
additional_context_fn: impl Fn(Context) -> Context, additional_context_fn: impl Fn(Context) -> Context,
) -> Result<Option<String>> { ) -> Result<Option<Vec<String>>> {
let mut pages = all_pages.into_iter().filter(|p| p.meta.date.is_some()).collect::<Vec<_>>(); let mut pages = all_pages.into_iter().filter(|p| p.meta.date.is_some()).collect::<Vec<_>>();
// Don't generate a feed if none of the pages has a date // Don't generate a feed if none of the pages has a date
@ -73,9 +73,13 @@ pub fn render_feed(
context.insert("config", &site.config.serialize(lang)); context.insert("config", &site.config.serialize(lang));
context.insert("lang", lang); context.insert("lang", lang);
let feed_filename = &site.config.feed_filename; let mut feeds = Vec::new();
for feed_filename in &site.config.feed_filenames {
let mut context = context.clone();
let feed_url = if let Some(base) = base_path { let feed_url = if let Some(base) = base_path {
site.config.make_permalink(&base.join(feed_filename).to_string_lossy().replace('\\', "/")) site.config
.make_permalink(&base.join(feed_filename).to_string_lossy().replace('\\', "/"))
} else { } else {
site.config.make_permalink(feed_filename) site.config.make_permalink(feed_filename)
}; };
@ -84,7 +88,8 @@ pub fn render_feed(
context = additional_context_fn(context); context = additional_context_fn(context);
let feed = render_template(feed_filename, &site.tera, context, &site.config.theme)?; feeds.push(render_template(feed_filename, &site.tera, context, &site.config.theme)?);
}
Ok(Some(feed))
Ok(Some(feeds))
} }

View file

@ -1,4 +1,4 @@
pub mod feed; pub mod feeds;
pub mod link_checking; pub mod link_checking;
mod minify; mod minify;
pub mod sass; pub mod sass;
@ -746,23 +746,23 @@ impl Site {
start = log_time(start, "Rendered sitemap"); start = log_time(start, "Rendered sitemap");
let library = self.library.read().unwrap(); let library = self.library.read().unwrap();
if self.config.generate_feed { if self.config.generate_feeds {
let is_multilingual = self.config.is_multilingual(); let is_multilingual = self.config.is_multilingual();
let pages: Vec<_> = if is_multilingual { let pages: Vec<_> = if is_multilingual {
library.pages.values().filter(|p| p.lang == self.config.default_language).collect() library.pages.values().filter(|p| p.lang == self.config.default_language).collect()
} else { } else {
library.pages.values().collect() library.pages.values().collect()
}; };
self.render_feed(pages, None, &self.config.default_language, |c| c)?; self.render_feeds(pages, None, &self.config.default_language, |c| c)?;
start = log_time(start, "Generated feed in default language"); start = log_time(start, "Generated feed in default language");
} }
for (code, language) in &self.config.other_languages() { for (code, language) in &self.config.other_languages() {
if !language.generate_feed { if !language.generate_feeds {
continue; continue;
} }
let pages: Vec<_> = library.pages.values().filter(|p| &p.lang == code).collect(); let pages: Vec<_> = library.pages.values().filter(|p| &p.lang == code).collect();
self.render_feed(pages, Some(&PathBuf::from(code)), code, |c| c)?; self.render_feeds(pages, Some(&PathBuf::from(code)), code, |c| c)?;
start = log_time(start, "Generated feed in other language"); start = log_time(start, "Generated feed in other language");
} }
self.render_themes_css()?; self.render_themes_css()?;
@ -965,14 +965,16 @@ impl Site {
} else { } else {
PathBuf::from(format!("{}/{}/{}", taxonomy.lang, taxonomy.slug, item.slug)) PathBuf::from(format!("{}/{}/{}", taxonomy.lang, taxonomy.slug, item.slug))
}; };
self.render_feed( self.render_feeds(
item.pages.iter().map(|p| library.pages.get(p).unwrap()).collect(), item.pages.iter().map(|p| library.pages.get(p).unwrap()).collect(),
Some(&tax_path), Some(&tax_path),
&taxonomy.lang, &taxonomy.lang,
|mut context: Context| { |mut context: Context| {
context.insert("taxonomy", &taxonomy.kind); context.insert("taxonomy", &taxonomy.kind);
context context.insert(
.insert("term", &feed::SerializedFeedTaxonomyItem::from_item(item)); "term",
&feeds::SerializedFeedTaxonomyItem::from_item(item),
);
context context
}, },
) )
@ -1028,23 +1030,23 @@ impl Site {
Ok(()) Ok(())
} }
/// Renders a feed for the given path and at the given path /// Renders feeds for the given path and at the given path
/// If both arguments are `None`, it will render only the feed for the whole /// If both arguments are `None`, it will render only the feeds for the whole
/// site at the root folder. /// site at the root folder.
pub fn render_feed( pub fn render_feeds(
&self, &self,
all_pages: Vec<&Page>, all_pages: Vec<&Page>,
base_path: Option<&PathBuf>, base_path: Option<&PathBuf>,
lang: &str, lang: &str,
additional_context_fn: impl Fn(Context) -> Context, additional_context_fn: impl Fn(Context) -> Context,
) -> Result<()> { ) -> Result<()> {
let feed = match feed::render_feed(self, all_pages, lang, base_path, additional_context_fn)? let feeds =
{ match feeds::render_feeds(self, all_pages, lang, base_path, additional_context_fn)? {
Some(v) => v, Some(v) => v,
None => return Ok(()), None => return Ok(()),
}; };
let feed_filename = &self.config.feed_filename;
for (feed, feed_filename) in feeds.into_iter().zip(self.config.feed_filenames.iter()) {
if let Some(base) = base_path { if let Some(base) = base_path {
let mut components = Vec::new(); let mut components = Vec::new();
for component in base.components() { for component in base.components() {
@ -1058,6 +1060,8 @@ impl Site {
} else { } else {
self.write_content(&[], feed_filename, feed)?; self.write_content(&[], feed_filename, feed)?;
} }
}
Ok(()) Ok(())
} }
@ -1076,10 +1080,10 @@ impl Site {
output_path.push(component); output_path.push(component);
} }
if section.meta.generate_feed { if section.meta.generate_feeds {
let library = &self.library.read().unwrap(); let library = &self.library.read().unwrap();
let pages = section.pages.iter().map(|k| library.pages.get(k).unwrap()).collect(); let pages = section.pages.iter().map(|k| library.pages.get(k).unwrap()).collect();
self.render_feed( self.render_feeds(
pages, pages,
Some(&PathBuf::from(&section.path[1..])), Some(&PathBuf::from(&section.path[1..])),
&section.lang, &section.lang,

View file

@ -461,27 +461,34 @@ title = "A title"
} }
#[test] #[test]
fn can_get_feed_url_with_default_language() { fn can_get_feed_urls_with_default_language() {
let config = Config::parse(CONFIG_DATA).unwrap(); let config = Config::parse(CONFIG_DATA).unwrap();
let dir = create_temp_dir(); let dir = create_temp_dir();
let static_fn = let static_fn =
GetUrl::new(dir.path().to_path_buf(), config.clone(), HashMap::new(), PathBuf::new()); GetUrl::new(dir.path().to_path_buf(), config.clone(), HashMap::new(), PathBuf::new());
for feed_filename in &config.feed_filenames {
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value(config.feed_filename).unwrap()); args.insert("path".to_string(), to_value(feed_filename).unwrap());
args.insert("lang".to_string(), to_value("fr").unwrap()); args.insert("lang".to_string(), to_value("fr").unwrap());
assert_eq!(static_fn.call(&args).unwrap(), "https://remplace-par-ton-url.fr/atom.xml"); assert_eq!(static_fn.call(&args).unwrap(), "https://remplace-par-ton-url.fr/atom.xml");
} }
}
#[test] #[test]
fn can_get_feed_url_with_other_language() { fn can_get_feed_urls_with_other_language() {
let config = Config::parse(CONFIG_DATA).unwrap(); let config = Config::parse(CONFIG_DATA).unwrap();
let dir = create_temp_dir(); let dir = create_temp_dir();
let static_fn = let static_fn =
GetUrl::new(dir.path().to_path_buf(), config.clone(), HashMap::new(), PathBuf::new()); GetUrl::new(dir.path().to_path_buf(), config.clone(), HashMap::new(), PathBuf::new());
for feed_filename in &config.feed_filenames {
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value(config.feed_filename).unwrap()); args.insert("path".to_string(), to_value(feed_filename).unwrap());
args.insert("lang".to_string(), to_value("en").unwrap()); args.insert("lang".to_string(), to_value("en").unwrap());
assert_eq!(static_fn.call(&args).unwrap(), "https://remplace-par-ton-url.fr/en/atom.xml"); assert_eq!(
static_fn.call(&args).unwrap(),
"https://remplace-par-ton-url.fr/en/atom.xml"
);
}
} }
#[test] #[test]

View file

@ -11,7 +11,7 @@ to your `config.toml`. For example:
```toml ```toml
[languages.fr] [languages.fr]
generate_feed = true # there will be a feed for French content generate_feeds = true # there will be a feed for French content
build_search_index = true build_search_index = true
taxonomies = [ taxonomies = [
{name = "auteurs"}, {name = "auteurs"},

View file

@ -106,11 +106,11 @@ transparent = false
# current one. This takes an array of paths, not URLs. # current one. This takes an array of paths, not URLs.
aliases = [] aliases = []
# If set to "true", a feed file will be generated for this section at the # If set to "true", feed files will be generated for this section at the
# section's root path. This is independent of the site-wide variable of the same # section's root path. This is independent of the site-wide variable of the same
# name. The section feed will only include posts from that respective feed, and # name. The section feed will only include posts from that respective feed, and
# not from any other sections, including sub-sections under that section. # not from any other sections, including sub-sections under that section.
generate_feed = false generate_feeds = false
# Your own data. # Your own data.
[extra] [extra]

View file

@ -65,12 +65,12 @@ ignored_content = []
ignored_static = [] ignored_static = []
# When set to "true", a feed is automatically generated. # When set to "true", a feed is automatically generated.
generate_feed = false generate_feeds = false
# The filename to use for the feed. Used as the template filename, too. # The filenames to use for the feeds. Used as the template filenames, too.
# Defaults to "atom.xml", which has a built-in template that renders an Atom 1.0 feed. # Defaults to ["atom.xml"], which has a built-in template that renders an Atom 1.0 feed.
# There is also a built-in template "rss.xml" that renders an RSS 2.0 feed. # There is also a built-in template "rss.xml" that renders an RSS 2.0 feed.
feed_filename = "atom.xml" feed_filenames = ["atom.xml"]
# The number of articles to include in the feed. All items are included if # The number of articles to include in the feed. All items are included if
# this limit is not set (the default). # this limit is not set (the default).
@ -199,13 +199,13 @@ index_format = "elasticlunr_javascript"
# Additional languages definition # Additional languages definition
# You can define language specific config values and translations: # You can define language specific config values and translations:
# title, description, generate_feed, feed_filename, taxonomies, build_search_index # title, description, generate_feeds, feed_filenames, taxonomies, build_search_index
# as well as its own search configuration and translations (see above for details on those) # as well as its own search configuration and translations (see above for details on those)
[languages] [languages]
# For example # For example
# [languages.fr] # [languages.fr]
# title = "Mon blog" # title = "Mon blog"
# generate_feed = true # generate_feeds = true
# taxonomies = [ # taxonomies = [
# {name = "auteurs"}, # {name = "auteurs"},
# {name = "tags"}, # {name = "tags"},

View file

@ -4,13 +4,13 @@ weight = 50
aliases = ["/documentation/templates/rss/"] aliases = ["/documentation/templates/rss/"]
+++ +++
If the site `config.toml` file sets `generate_feed = true`, then Zola will If the site `config.toml` file sets `generate_feeds = true`, then Zola will
generate a feed file for the site, named according to the `feed_filename` generate feed files for the site, named according to the `feed_filenames`
setting in `config.toml`, which defaults to `atom.xml`. Given the feed filename setting in `config.toml`, which defaults to `atom.xml`. Given the feed filename
`atom.xml`, the generated file will live at `base_url/atom.xml`, based upon the `atom.xml`, the generated file will live at `base_url/atom.xml`, based upon the
`atom.xml` file in the `templates` directory, or the built-in Atom template. `atom.xml` file in the `templates` directory, or the built-in Atom template.
`feed_filename` can be set to any value, but built-in templates are provided `feed_filenames` can be set to any value, but built-in templates are provided
for `atom.xml` (in the preferred Atom 1.0 format), and `rss.xml` (in the RSS for `atom.xml` (in the preferred Atom 1.0 format), and `rss.xml` (in the RSS
2.0 format). If you choose a different filename (e.g. `feed.xml`), you will 2.0 format). If you choose a different filename (e.g. `feed.xml`), you will
need to provide a template yourself. need to provide a template yourself.
@ -54,7 +54,7 @@ Feeds for taxonomy terms get two more variables, using types from the
- `term`: of type `TaxonomyTerm`, but without `term.pages` (use `pages` instead) - `term`: of type `TaxonomyTerm`, but without `term.pages` (use `pages` instead)
You can also enable separate feeds for each section by setting the You can also enable separate feeds for each section by setting the
`generate_feed` variable to true in the respective section's front matter. `generate_feeds` variable to true in the respective section's front matter.
Section feeds will use the same template as indicated in the `config.toml` file. Section feeds will use the same template as indicated in the `config.toml` file.
Section feeds, in addition to the five feed template variables, get the Section feeds, in addition to the five feed template variables, get the
`section` variable from the [section `section` variable from the [section

View file

@ -108,8 +108,8 @@ lang: String;
translations: Array<TranslatedContent>; translations: Array<TranslatedContent>;
// All the pages/sections linking this page: their permalink and a title if there is one // All the pages/sections linking this page: their permalink and a title if there is one
backlinks: Array<{permalink: String, title: String?}>; backlinks: Array<{permalink: String, title: String?}>;
// Whether this section generates a feed or not. Taken from the front-matter if set // Whether this section generates feeds or not. Taken from the front-matter if set
generate_feed: bool; generate_feeds: bool;
// Whether this section is transparent. Taken from the front-matter if set // Whether this section is transparent. Taken from the front-matter if set
transparent: bool; transparent: bool;
``` ```

View file

@ -120,7 +120,7 @@ Here's a breakdown of the configuration settings tailored for this theme:
- **build_search_index**: If set to `true`, a search index will be built from the pages and section content for the `default_language`. - **build_search_index**: If set to `true`, a search index will be built from the pages and section content for the `default_language`.
In this configuration and for this theme, it's disabled (`false`). In this configuration and for this theme, it's disabled (`false`).
- **generate_feed**: Determines if an Atom feed (file `atom.xml`) is automatically generated. - **generate_feeds**: Determines if an Atom feed (file `atom.xml`) is automatically generated.
It's set to `true`, meaning a feed will be generated. It's set to `true`, meaning a feed will be generated.
- **taxonomies**: An array of taxonomies (classification systems) used for the site. - **taxonomies**: An array of taxonomies (classification systems) used for the site.

View file

@ -1,7 +1,7 @@
title = "My Integration Testing site" title = "My Integration Testing site"
base_url = "https://replace-this-with-your-url.com" base_url = "https://replace-this-with-your-url.com"
compile_sass = true compile_sass = true
generate_feed = true generate_feeds = true
theme = "sample" theme = "sample"
taxonomies = [ taxonomies = [

View file

@ -5,5 +5,5 @@ template = "section_paginated.html"
insert_anchor_links = "left" insert_anchor_links = "left"
sort_by = "date" sort_by = "date"
aliases = ["another-old-url/index.html"] aliases = ["another-old-url/index.html"]
generate_feed = true generate_feeds = true
+++ +++

View file

@ -2,7 +2,7 @@
title = "Programming" title = "Programming"
sort_by = "weight" sort_by = "weight"
weight = 1 weight = 1
generate_feed = true generate_feeds = true
[extra] [extra]
we_have_extra = "variables" we_have_extra = "variables"

View file

@ -9,7 +9,7 @@ build_search_index = true
default_language = "en" default_language = "en"
generate_feed = true generate_feeds = true
taxonomies = [ taxonomies = [
{name = "authors", feed = true}, {name = "authors", feed = true},
@ -17,7 +17,7 @@ taxonomies = [
] ]
[languages.fr] [languages.fr]
generate_feed = true generate_feeds = true
taxonomies = [ taxonomies = [
{name = "auteurs", feed = true}, {name = "auteurs", feed = true},
{name = "tags"}, {name = "tags"},

View file

@ -1,7 +1,7 @@
+++ +++
title = "Mon blog" title = "Mon blog"
sort_by = "date" sort_by = "date"
insert_anchors = "right" insert_anchor_links = "right"
+++ +++
[Dernières nouvelles](#news) [Dernières nouvelles](#news)

View file

@ -1,5 +1,5 @@
+++ +++
title = "Il mio blog" title = "Il mio blog"
sort_by = "date" sort_by = "date"
insert_anchors = "right" insert_anchor_links = "right"
+++ +++

View file

@ -1,5 +1,5 @@
+++ +++
title = "My blog" title = "My blog"
sort_by = "date" sort_by = "date"
insert_anchors = "left" insert_anchor_links = "left"
+++ +++