Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 6 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
LunarEclipse363 marked this conversation as resolved.
Show resolved Hide resolved
/// 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>,
LunarEclipse363 marked this conversation as resolved.
Show resolved Hide resolved
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();
LunarEclipse363 marked this conversation as resolved.
Show resolved Hide resolved

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