Skip to content

Commit

Permalink
Added support for multiple feeds (i.e. generating both Atom and RSS) (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
LunarEclipse363 authored and Keats committed Jun 20, 2024
1 parent 7f59792 commit c054ed1
Show file tree
Hide file tree
Showing 23 changed files with 163 additions and 128 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
- Add `render = false` capability to pages
- Handle string dates in YAML front-matter
- 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)

Expand Down
41 changes: 21 additions & 20 deletions components/config/src/config/languages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ use crate::config::search;
use crate::config::taxonomies;

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
#[serde(default, deny_unknown_fields)]
pub struct LanguageOptions {
/// Title of the site. Defaults to None
pub title: Option<String>,
/// Description of the site. Defaults to None
pub description: Option<String>,
/// Whether to generate a feed for that language, defaults to `false`
pub generate_feed: bool,
/// The filename to use for feeds. Used to find the template, too.
/// Defaults to "atom.xml", with "rss.xml" also having a template provided out of the box.
pub feed_filename: String,
/// Whether to generate feeds for that language, defaults to `false`
pub generate_feeds: bool,
/// 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.
pub feed_filenames: Vec<String>,
pub taxonomies: Vec<taxonomies::TaxonomyConfig>,
/// Whether to generate search index for that language, defaults to `false`
pub build_search_index: bool,
Expand Down Expand Up @@ -66,9 +66,10 @@ impl LanguageOptions {
merge_field!(self.title, other.title, "title");
merge_field!(self.description, other.description, "description");
merge_field!(
self.feed_filename == "atom.xml",
self.feed_filename,
other.feed_filename,
self.feed_filenames.is_empty()
|| self.feed_filenames == LanguageOptions::default().feed_filenames,
self.feed_filenames,
other.feed_filenames,
"feed_filename"
);
merge_field!(self.taxonomies.is_empty(), self.taxonomies, other.taxonomies, "taxonomies");
Expand All @@ -79,7 +80,7 @@ impl LanguageOptions {
"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;

if self.search == search::Search::default() {
Expand All @@ -101,8 +102,8 @@ impl Default for LanguageOptions {
LanguageOptions {
title: None,
description: None,
generate_feed: false,
feed_filename: "atom.xml".to_string(),
generate_feeds: false,
feed_filenames: vec!["atom.xml".to_string()],
taxonomies: vec![],
build_search_index: false,
search: search::Search::default(),
Expand All @@ -129,8 +130,8 @@ mod tests {
let mut base_default_language_options = LanguageOptions {
title: Some("Site's title".to_string()),
description: None,
generate_feed: true,
feed_filename: "atom.xml".to_string(),
generate_feeds: true,
feed_filenames: vec!["atom.xml".to_string()],
taxonomies: vec![],
build_search_index: true,
search: search::Search::default(),
Expand All @@ -140,8 +141,8 @@ mod tests {
let section_default_language_options = LanguageOptions {
title: None,
description: Some("Site's description".to_string()),
generate_feed: false,
feed_filename: "rss.xml".to_string(),
generate_feeds: false,
feed_filenames: vec!["rss.xml".to_string()],
taxonomies: vec![],
build_search_index: true,
search: search::Search::default(),
Expand All @@ -156,8 +157,8 @@ mod tests {
let mut base_default_language_options = LanguageOptions {
title: Some("Site's title".to_string()),
description: Some("Duplicate site description".to_string()),
generate_feed: true,
feed_filename: "".to_string(),
generate_feeds: true,
feed_filenames: vec![],
taxonomies: vec![],
build_search_index: true,
search: search::Search::default(),
Expand All @@ -167,8 +168,8 @@ mod tests {
let section_default_language_options = LanguageOptions {
title: None,
description: Some("Site's description".to_string()),
generate_feed: false,
feed_filename: "Some feed_filename".to_string(),
generate_feeds: false,
feed_filenames: vec!["Some feed_filename".to_string()],
taxonomies: vec![],
build_search_index: true,
search: search::Search::default(),
Expand Down
60 changes: 37 additions & 23 deletions components/config/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ pub enum Mode {
}

#[derive(Clone, Debug, Deserialize)]
#[serde(default)]
#[serde(default, deny_unknown_fields)]
pub struct Config {
/// Base URL of the site, the only required config argument
pub base_url: String,
Expand All @@ -49,13 +49,13 @@ pub struct Config {
/// The translations strings for the default language
translations: HashMap<String, String>,

/// Whether to generate a feed. Defaults to false.
pub generate_feed: bool,
/// Whether to generate feeds. Defaults to false.
pub generate_feeds: bool,
/// The number of articles to include in the feed. Defaults to including all items.
pub feed_limit: Option<usize>,
/// The filename to use for feeds. Used to find the template, too.
/// Defaults to "atom.xml", with "rss.xml" also having a template provided out of the box.
pub feed_filename: String,
/// 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.
pub feed_filenames: Vec<String>,
/// If set, files from static/ will be hardlinked instead of copied to the output dir.
pub hard_link_static: bool,
pub taxonomies: Vec<taxonomies::TaxonomyConfig>,
Expand Down Expand Up @@ -109,7 +109,7 @@ pub struct SerializedConfig<'a> {
languages: HashMap<&'a String, &'a languages::LanguageOptions>,
default_language: &'a str,
generate_feed: bool,
feed_filename: &'a str,
feed_filenames: &'a [String],
taxonomies: &'a [taxonomies::TaxonomyConfig],
author: &'a Option<String>,
build_search_index: bool,
Expand Down Expand Up @@ -183,12 +183,14 @@ impl Config {

/// Makes a url, taking into account that the base url might have a trailing slash
pub fn make_permalink(&self, path: &str) -> String {
let trailing_bit =
if path.ends_with('/') || path.ends_with(&self.feed_filename) || path.is_empty() {
""
} else {
"/"
};
let trailing_bit = if path.ends_with('/')
|| self.feed_filenames.iter().any(|feed_filename| path.ends_with(feed_filename))
|| path.is_empty()
{
""
} else {
"/"
};

// Index section with a base url that has a trailing slash
if self.base_url.ends_with('/') && path == "/" {
Expand All @@ -212,8 +214,8 @@ impl Config {
let mut base_language_options = languages::LanguageOptions {
title: self.title.clone(),
description: self.description.clone(),
generate_feed: self.generate_feed,
feed_filename: self.feed_filename.clone(),
generate_feeds: self.generate_feeds,
feed_filenames: self.feed_filenames.clone(),
build_search_index: self.build_search_index,
taxonomies: self.taxonomies.clone(),
search: self.search.clone(),
Expand Down Expand Up @@ -320,8 +322,8 @@ impl Config {
description: &options.description,
languages: self.languages.iter().filter(|(k, _)| k.as_str() != lang).collect(),
default_language: &self.default_language,
generate_feed: options.generate_feed,
feed_filename: &options.feed_filename,
generate_feed: options.generate_feeds,
feed_filenames: &options.feed_filenames,
taxonomies: &options.taxonomies,
author: &self.author,
build_search_index: options.build_search_index,
Expand Down Expand Up @@ -369,9 +371,9 @@ impl Default for Config {
theme: None,
default_language: "en".to_string(),
languages: HashMap::new(),
generate_feed: false,
generate_feeds: false,
feed_limit: None,
feed_filename: "atom.xml".to_string(),
feed_filenames: vec!["atom.xml".to_string()],
hard_link_static: false,
taxonomies: Vec::new(),
author: None,
Expand Down Expand Up @@ -428,8 +430,8 @@ mod tests {
languages::LanguageOptions {
title: None,
description: description_lang_section.clone(),
generate_feed: true,
feed_filename: config.feed_filename.clone(),
generate_feeds: true,
feed_filenames: config.feed_filenames.clone(),
taxonomies: config.taxonomies.clone(),
build_search_index: false,
search: search::Search::default(),
Expand All @@ -456,8 +458,8 @@ mod tests {
languages::LanguageOptions {
title: title_lang_section.clone(),
description: None,
generate_feed: true,
feed_filename: config.feed_filename.clone(),
generate_feeds: true,
feed_filenames: config.feed_filenames.clone(),
taxonomies: config.taxonomies.clone(),
build_search_index: false,
search: search::Search::default(),
Expand Down Expand Up @@ -976,4 +978,16 @@ author = "person@example.com (Some Person)"
let config = Config::parse(config).unwrap();
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();
}
}
6 changes: 3 additions & 3 deletions components/content/src/front_matter/section.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ static DEFAULT_PAGINATE_PATH: &str = "page";

/// The front matter of every section
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
#[serde(default, deny_unknown_fields)]
pub struct SectionFrontMatter {
/// <title> of the page
pub title: Option<String>,
Expand Down Expand Up @@ -69,7 +69,7 @@ pub struct SectionFrontMatter {
pub aliases: Vec<String>,
/// Whether to generate a feed for the current section
#[serde(skip_serializing)]
pub generate_feed: bool,
pub generate_feeds: bool,
/// Any extra parameter present in the front matter
pub extra: Map<String, Value>,
}
Expand Down Expand Up @@ -113,7 +113,7 @@ impl Default for SectionFrontMatter {
transparent: false,
page_template: None,
aliases: Vec::new(),
generate_feed: false,
generate_feeds: false,
extra: Map::new(),
draft: false,
}
Expand Down
2 changes: 1 addition & 1 deletion components/content/src/section.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ mod tests {
create_dir_all(path.join(&article_path).join("foo/baz/quux"))
.expect("create nested temp dir");
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("graph.jpg")).unwrap();
File::create(article_path.join("fail.png")).unwrap();
Expand Down
4 changes: 2 additions & 2 deletions components/content/src/ser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ pub struct SerializingSection<'a> {
subsections: Vec<&'a str>,
translations: Vec<TranslatedContent<'a>>,
backlinks: Vec<BackLink<'a>>,
generate_feed: bool,
generate_feeds: bool,
transparent: bool,
}

Expand Down Expand Up @@ -220,7 +220,7 @@ impl<'a> SerializingSection<'a> {
reading_time: section.reading_time,
assets: &section.serialized_assets,
lang: &section.lang,
generate_feed: section.meta.generate_feed,
generate_feeds: section.meta.generate_feeds,
transparent: section.meta.transparent,
pages,
subsections,
Expand Down
2 changes: 1 addition & 1 deletion components/site/benches/site.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ fn bench_render_feed(b: &mut test::Bencher) {
let public = &tmp_dir.path().join("public");
site.set_output_path(&public);
b.iter(|| {
site.render_feed(
site.render_feeds(
site.library.read().unwrap().pages.values().collect(),
None,
&site.config.default_language,
Expand Down
29 changes: 17 additions & 12 deletions components/site/src/feed.rs → components/site/src/feeds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ impl<'a> SerializedFeedTaxonomyItem<'a> {
}
}

pub fn render_feed(
pub fn render_feeds(
site: &Site,
all_pages: Vec<&Page>,
lang: &str,
base_path: Option<&PathBuf>,
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<_>>();

// Don't generate a feed if none of the pages has a date
Expand Down Expand Up @@ -73,18 +73,23 @@ pub fn render_feed(
context.insert("config", &site.config.serialize(lang));
context.insert("lang", lang);

let feed_filename = &site.config.feed_filename;
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)
};
let mut feeds = Vec::new();
for feed_filename in &site.config.feed_filenames {
let mut context = context.clone();

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.insert("feed_url", &feed_url);

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))
}
Loading

0 comments on commit c054ed1

Please sign in to comment.