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,12 +183,14 @@ 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 {
"/"
};
// Index section with a base url that has a trailing slash // Index section with a base url that has a trailing slash
if self.base_url.ends_with('/') && path == "/" { if self.base_url.ends_with('/') && path == "/" {
@ -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,18 +73,23 @@ 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();
let feed_url = if let Some(base) = base_path { for feed_filename in &site.config.feed_filenames {
site.config.make_permalink(&base.join(feed_filename).to_string_lossy().replace('\\', "/")) let mut context = context.clone();
} else {
site.config.make_permalink(feed_filename)
};
context.insert("feed_url", &feed_url); let feed_url = if let Some(base) = base_path {
site.config
.make_permalink(&base.join(feed_filename).to_string_lossy().replace('\\', "/"))
} else {
site.config.make_permalink(feed_filename)
};
context = additional_context_fn(context); context.insert("feed_url", &feed_url);
let feed = render_template(feed_filename, &site.tera, context, &site.config.theme)?; context = additional_context_fn(context);
Ok(Some(feed)) feeds.push(render_template(feed_filename, &site.tera, context, &site.config.theme)?);
}
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,36 +1030,38 @@ 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;
if let Some(base) = base_path { for (feed, feed_filename) in feeds.into_iter().zip(self.config.feed_filenames.iter()) {
let mut components = Vec::new(); if let Some(base) = base_path {
for component in base.components() { let mut components = Vec::new();
components.push(component.as_os_str().to_string_lossy()); for component in base.components() {
components.push(component.as_os_str().to_string_lossy());
}
self.write_content(
&components.iter().map(|x| x.as_ref()).collect::<Vec<_>>(),
feed_filename,
feed,
)?;
} else {
self.write_content(&[], feed_filename, feed)?;
} }
self.write_content(
&components.iter().map(|x| x.as_ref()).collect::<Vec<_>>(),
feed_filename,
feed,
)?;
} else {
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());
let mut args = HashMap::new(); for feed_filename in &config.feed_filenames {
args.insert("path".to_string(), to_value(config.feed_filename).unwrap()); let mut args = HashMap::new();
args.insert("lang".to_string(), to_value("fr").unwrap()); args.insert("path".to_string(), to_value(feed_filename).unwrap());
assert_eq!(static_fn.call(&args).unwrap(), "https://remplace-par-ton-url.fr/atom.xml"); args.insert("lang".to_string(), to_value("fr").unwrap());
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());
let mut args = HashMap::new(); for feed_filename in &config.feed_filenames {
args.insert("path".to_string(), to_value(config.feed_filename).unwrap()); let mut args = HashMap::new();
args.insert("lang".to_string(), to_value("en").unwrap()); args.insert("path".to_string(), to_value(feed_filename).unwrap());
assert_eq!(static_fn.call(&args).unwrap(), "https://remplace-par-ton-url.fr/en/atom.xml"); 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"
);
}
} }
#[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
@ -85,4 +85,4 @@ In order to enable the tag feeds as well, you can overload the `block rss` using
Each tag page will refer to it's dedicated feed. Each tag page will refer to it's dedicated feed.
[atom_rfc]: https://www.rfc-editor.org/rfc/rfc4287 [atom_rfc]: https://www.rfc-editor.org/rfc/rfc4287
[rss_spec]: https://www.rssboard.org/rss-specification#ltauthorgtSubelementOfLtitemgt [rss_spec]: https://www.rssboard.org/rss-specification#ltauthorgtSubelementOfLtitemgt

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.
@ -408,4 +408,4 @@ Just use the file `en.toml` as a template for your own translations.
This theme is under the MIT License. This theme is under the MIT License.
For details, please refer to the LICENSE file. For details, please refer to the LICENSE file.

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"
+++ +++