Merge pull request #994 from chris-morgan/misc

Chris Morgan’s whole bunch of miscellaneous work for landing
This commit is contained in:
Vincent Prouillet 2020-04-21 18:21:48 +02:00 committed by GitHub
commit 0ac70cb242
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 365 additions and 218 deletions

View file

@ -1,5 +1,24 @@
# Changelog
## 0.11.0 (unreleased)
### Breaking
- RSS feed support has been altered to allow, *and default to*, Atom feeds, Atom being technically superior and just as widely-supported in normal use cases.
- New config value `feed_filename`, defaulting to `atom.xml` (change to `rss.xml` to reinstate the old behaviour)
- Config value `rss_limit` is renamed to `feed_limit`
- Config value `languages.*.rss` is renamed to `languages.*.feed`
- Config value `generate_rss` is renamed to `generate_feed`
Users with existing feeds should either set `feed_filename = "rss.xml"` in config.toml to keep things the same, or set up a 3xx redirect from rss.xml to atom.xml so that existing feed consumers arent broken.
- The feed template variable `last_build_date` is renamed to `last_updated` to more accurately reflect its semantics
- The sitemap templates `SitemapEntry` types `date` field has been renamed to `updated` to reflect that it will use the `updated` front-matter field if available, rather than `date`
### Other
- Add `updated` front-matter field for pages, which sitemap templates will use for the `SitemapEntry.date` field instead of the `date` front-matter field, and which the default Atom feed template will use
- Add `lang` to the feed template context
- Add `taxonomy` and `term` to the feed template context for taxonomy feeds
## 0.10.2 (unreleased)
- Fix link checker not looking for anchor with capital id/name
@ -17,8 +36,6 @@
### Breaking
- Remove `toc` variable in section/page context and pass it to `page.toc` and `section.toc` instead so they are
accessible everywhere
- [Slugification](https://en.wikipedia.org/wiki/Slug_(web_publishing)#Slug) of paths, taxonomies and anchors is now configurable. By default, everything will still be slugified like in previous versions.
See documentation for information on how to disable it.
### Other
- Add zenburn syntax highlighting theme

View file

@ -47,15 +47,15 @@ impl Default for Slugify {
pub struct Language {
/// The language code
pub code: String,
/// Whether to generate a RSS feed for that language, defaults to `false`
pub rss: bool,
/// Whether to generate a feed for that language, defaults to `false`
pub feed: bool,
/// Whether to generate search index for that language, defaults to `false`
pub search: bool,
}
impl Default for Language {
fn default() -> Self {
Language { code: String::new(), rss: false, search: false }
Language { code: String::new(), feed: false, search: false }
}
}
@ -68,8 +68,8 @@ pub struct Taxonomy {
/// by this much
pub paginate_by: Option<usize>,
pub paginate_path: Option<String>,
/// Whether to generate a RSS feed only for each taxonomy term, defaults to false
pub rss: bool,
/// Whether to generate a feed only for each taxonomy term, defaults to false
pub feed: bool,
/// The language for that taxonomy, only used in multilingual sites.
/// Defaults to the config `default_language` if not set
pub lang: String,
@ -99,7 +99,7 @@ impl Default for Taxonomy {
name: String::new(),
paginate_by: None,
paginate_path: None,
rss: false,
feed: false,
lang: String::new(),
}
}
@ -155,10 +155,13 @@ pub struct Config {
/// Defaults to "base16-ocean-dark"
pub highlight_theme: String,
/// Whether to generate RSS. Defaults to false
pub generate_rss: bool,
/// The number of articles to include in the RSS feed. Defaults to including all items.
pub rss_limit: Option<usize>,
/// Whether to generate a feed. Defaults to false.
pub generate_feed: 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,
/// If set, files from static/ will be hardlinked instead of copied to the output dir.
pub hard_link_static: bool,
@ -276,11 +279,12 @@ 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("rss.xml") || path.is_empty() {
""
} else {
"/"
};
let trailing_bit =
if path.ends_with('/') || path.ends_with(&self.feed_filename) || path.is_empty() {
""
} else {
"/"
};
// Index section with a base url that has a trailing slash
if self.base_url.ends_with('/') && path == "/" {
@ -384,8 +388,9 @@ impl Default for Config {
highlight_theme: "base16-ocean-dark".to_string(),
default_language: "en".to_string(),
languages: Vec::new(),
generate_rss: false,
rss_limit: None,
generate_feed: false,
feed_limit: None,
feed_filename: "atom.xml".to_string(),
hard_link_static: false,
taxonomies: Vec::new(),
compile_sass: false,
@ -493,10 +498,10 @@ hello = "world"
// https://github.com/Keats/gutenberg/issues/486
#[test]
fn doesnt_add_trailing_slash_to_rss() {
fn doesnt_add_trailing_slash_to_feed() {
let mut config = Config::default();
config.base_url = "http://vincent.is/".to_string();
assert_eq!(config.make_permalink("rss.xml"), "http://vincent.is/rss.xml");
assert_eq!(config.make_permalink("atom.xml"), "http://vincent.is/atom.xml");
}
#[test]

0
components/errors/src/lib.rs Executable file → Normal file
View file

View file

@ -16,6 +16,9 @@ pub struct PageFrontMatter {
pub title: Option<String>,
/// Description in <meta> that appears when linked, e.g. on twitter
pub description: Option<String>,
/// Updated date
#[serde(default, deserialize_with = "from_toml_datetime")]
pub updated: Option<String>,
/// Date if we want to order pages (ie blog post)
#[serde(default, deserialize_with = "from_toml_datetime")]
pub date: Option<String>,
@ -117,6 +120,7 @@ impl Default for PageFrontMatter {
PageFrontMatter {
title: None,
description: None,
updated: None,
date: None,
datetime: None,
datetime_tuple: None,

View file

@ -195,7 +195,7 @@ mod tests {
#[test]
fn can_find_valid_language_in_page() {
let mut config = Config::default();
config.languages.push(Language { code: String::from("fr"), rss: false, search: false });
config.languages.push(Language { code: String::from("fr"), feed: false, search: false });
let mut file = FileInfo::new_page(
&Path::new("/home/vincent/code/site/content/posts/tutorials/python.fr.md"),
&PathBuf::new(),
@ -208,7 +208,7 @@ mod tests {
#[test]
fn can_find_valid_language_in_page_with_assets() {
let mut config = Config::default();
config.languages.push(Language { code: String::from("fr"), rss: false, search: false });
config.languages.push(Language { code: String::from("fr"), feed: false, search: false });
let mut file = FileInfo::new_page(
&Path::new("/home/vincent/code/site/content/posts/tutorials/python/index.fr.md"),
&PathBuf::new(),
@ -234,7 +234,7 @@ mod tests {
#[test]
fn errors_on_unknown_language_in_page_with_i18n_on() {
let mut config = Config::default();
config.languages.push(Language { code: String::from("it"), rss: false, search: false });
config.languages.push(Language { code: String::from("it"), feed: false, search: false });
let mut file = FileInfo::new_page(
&Path::new("/home/vincent/code/site/content/posts/tutorials/python.fr.md"),
&PathBuf::new(),
@ -246,7 +246,7 @@ mod tests {
#[test]
fn can_find_valid_language_in_section() {
let mut config = Config::default();
config.languages.push(Language { code: String::from("fr"), rss: false, search: false });
config.languages.push(Language { code: String::from("fr"), feed: false, search: false });
let mut file = FileInfo::new_section(
&Path::new("/home/vincent/code/site/content/posts/tutorials/_index.fr.md"),
&PathBuf::new(),
@ -273,7 +273,7 @@ mod tests {
#[test]
fn correct_canonical_after_find_language() {
let mut config = Config::default();
config.languages.push(Language { code: String::from("fr"), rss: false, search: false });
config.languages.push(Language { code: String::from("fr"), feed: false, search: false });
let mut file = FileInfo::new_page(
&Path::new("/home/vincent/code/site/content/posts/tutorials/python/index.fr.md"),
&PathBuf::new(),

View file

@ -770,7 +770,7 @@ Hello world
#[test]
fn can_specify_language_in_filename() {
let mut config = Config::default();
config.languages.push(Language { code: String::from("fr"), rss: false, search: false });
config.languages.push(Language { code: String::from("fr"), feed: false, search: false });
let content = r#"
+++
+++
@ -787,7 +787,7 @@ Bonjour le monde"#
#[test]
fn can_specify_language_in_filename_with_date() {
let mut config = Config::default();
config.languages.push(Language { code: String::from("fr"), rss: false, search: false });
config.languages.push(Language { code: String::from("fr"), feed: false, search: false });
let content = r#"
+++
+++
@ -806,7 +806,7 @@ Bonjour le monde"#
#[test]
fn i18n_frontmatter_path_overrides_default_permalink() {
let mut config = Config::default();
config.languages.push(Language { code: String::from("fr"), rss: false, search: false });
config.languages.push(Language { code: String::from("fr"), feed: false, search: false });
let content = r#"
+++
path = "bonjour"

View file

@ -350,7 +350,7 @@ mod tests {
#[test]
fn can_specify_language_in_filename() {
let mut config = Config::default();
config.languages.push(Language { code: String::from("fr"), rss: false, search: false });
config.languages.push(Language { code: String::from("fr"), feed: false, search: false });
let content = r#"
+++
+++
@ -372,7 +372,7 @@ Bonjour le monde"#
#[test]
fn can_make_links_to_translated_sections_without_double_trailing_slash() {
let mut config = Config::default();
config.languages.push(Language { code: String::from("fr"), rss: false, search: false });
config.languages.push(Language { code: String::from("fr"), feed: false, search: false });
let content = r#"
+++
+++
@ -389,7 +389,7 @@ Bonjour le monde"#
#[test]
fn can_make_links_to_translated_subsections_with_trailing_slash() {
let mut config = Config::default();
config.languages.push(Language { code: String::from("fr"), rss: false, search: false });
config.languages.push(Language { code: String::from("fr"), feed: false, search: false });
let content = r#"
+++
+++

View file

@ -63,6 +63,7 @@ pub struct SerializingPage<'a> {
ancestors: Vec<String>,
title: &'a Option<String>,
description: &'a Option<String>,
updated: &'a Option<String>,
date: &'a Option<String>,
year: Option<i32>,
month: Option<u32>,
@ -126,6 +127,7 @@ impl<'a> SerializingPage<'a> {
title: &page.meta.title,
description: &page.meta.description,
extra: &page.meta.extra,
updated: &page.meta.updated,
date: &page.meta.date,
year,
month,
@ -182,6 +184,7 @@ impl<'a> SerializingPage<'a> {
title: &page.meta.title,
description: &page.meta.description,
extra: &page.meta.extra,
updated: &page.meta.updated,
date: &page.meta.date,
year,
month,

View file

@ -6,7 +6,7 @@ use slotmap::DefaultKey;
use crate::content::Page;
/// Used by the RSS feed
/// Used by the feed
/// There to not have to import sorting stuff in the site crate
#[allow(clippy::trivially_copy_pass_by_ref)]
pub fn sort_actual_pages_by_date(a: &&Page, b: &&Page) -> Ordering {

View file

@ -458,7 +458,7 @@ mod tests {
#[test]
fn can_make_taxonomies_in_multiple_languages() {
let mut config = Config::default();
config.languages.push(Language { rss: false, code: "fr".to_string(), search: false });
config.languages.push(Language { feed: false, code: "fr".to_string(), search: false });
let mut library = Library::new(2, 0, true);
config.taxonomies = vec![
@ -569,7 +569,7 @@ mod tests {
let mut config = Config::default();
config.slugify.taxonomies = SlugifyStrategy::Safe;
config.languages.push(Language {
rss: false,
feed: false,
code: "fr".to_string(),
..Language::default()
});
@ -602,7 +602,7 @@ mod tests {
let mut config = Config::default();
config.slugify.taxonomies = SlugifyStrategy::On;
config.languages.push(Language {
rss: false,
feed: false,
code: "fr".to_string(),
..Language::default()
});

View file

@ -395,7 +395,17 @@ pub fn after_template_change(site: &mut Site, path: &Path) -> Result<()> {
match filename {
"sitemap.xml" => site.render_sitemap(),
"rss.xml" => site.render_rss_feed(site.library.read().unwrap().pages_values(), None),
filename if filename == site.config.feed_filename => {
// FIXME: this is insufficient; for multilingual sites, its rendering the wrong
// content into the root feed, and its not regenerating any of the other feeds (other
// languages or taxonomies with feed enabled).
site.render_feed(
site.library.read().unwrap().pages_values(),
None,
&site.config.default_language,
None,
)
}
"split_sitemap_index.xml" => site.render_sitemap(),
"robots.txt" => site.render_robots(),
"single.html" | "list.html" => site.render_taxonomies(),

View file

@ -108,7 +108,7 @@ base_url = "https://replace-this-with-your-url.com"
theme = "sample"
taxonomies = [
{name = "tags", rss = true},
{name = "tags", feed = true},
{name = "categories"}
]

View file

@ -35,12 +35,19 @@ fn bench_render_sitemap(b: &mut test::Bencher) {
}
#[bench]
fn bench_render_rss_feed(b: &mut test::Bencher) {
fn bench_render_feed(b: &mut test::Bencher) {
let mut site = setup_site("big-blog");
let tmp_dir = tempdir().expect("create temp dir");
let public = &tmp_dir.path().join("public");
site.set_output_path(&public);
b.iter(|| site.render_rss_feed(site.library.read().unwrap().pages_values(), None).unwrap());
b.iter(|| {
site.render_feed(
site.library.read().unwrap().pages_values(),
None,
&site.config.default_language,
None,
).unwrap();
});
}
#[bench]

View file

@ -8,13 +8,15 @@ use std::sync::{Arc, Mutex, RwLock};
use glob::glob;
use rayon::prelude::*;
use sass_rs::{compile_file, Options as SassOptions, OutputStyle};
use serde_derive::Serialize;
use tera::{Context, Tera};
use config::{get_config, Config};
use config::{get_config, Config, Taxonomy as TaxonomyConfig};
use errors::{bail, Error, ErrorKind, Result};
use front_matter::InsertAnchor;
use library::{
find_taxonomies, sort_actual_pages_by_date, Library, Page, Paginator, Section, Taxonomy,
TaxonomyItem,
};
use link_checker::check_url;
use templates::{global_fns, render_redirect_template, ZOLA_TERA};
@ -45,6 +47,23 @@ pub struct Site {
include_drafts: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
struct SerializedTaxonomyItem<'a> {
name: &'a str,
slug: &'a str,
permalink: &'a str,
}
impl<'a> SerializedTaxonomyItem<'a> {
pub fn from_item(item: &'a TaxonomyItem) -> Self {
SerializedTaxonomyItem {
name: &item.name,
slug: &item.slug,
permalink: &item.permalink,
}
}
}
impl Site {
/// Parse a site at the given path. Defaults to the current dir
/// Passing in a path is used in tests and when --root argument is passed
@ -626,15 +645,17 @@ impl Site {
}
/// Inject live reload script tag if in live reload mode
fn inject_livereload(&self, html: String) -> String {
fn inject_livereload(&self, mut html: String) -> String {
if let Some(port) = self.live_reload {
return html.replace(
"</body>",
&format!(
r#"<script src="/livereload.js?port={}&amp;mindelay=10"></script></body>"#,
port
),
let script = format!(
r#"<script src="/livereload.js?port={}&amp;mindelay=10"></script>"#,
port,
);
if let Some(index) = html.rfind("</body>") {
html.insert_str(index, &script);
} else {
html.push_str(&script);
}
}
html
@ -743,8 +764,9 @@ impl Site {
self.render_sitemap()?;
let library = self.library.read().unwrap();
if self.config.generate_rss {
let pages = if self.config.is_multilingual() {
if self.config.generate_feed {
let is_multilingual = self.config.is_multilingual();
let pages = if is_multilingual {
library
.pages_values()
.iter()
@ -754,16 +776,26 @@ impl Site {
} else {
library.pages_values()
};
self.render_rss_feed(pages, None)?;
self.render_feed(
pages,
None,
&self.config.default_language,
None,
)?;
}
for lang in &self.config.languages {
if !lang.rss {
if !lang.feed {
continue;
}
let pages =
library.pages_values().iter().filter(|p| p.lang == lang.code).cloned().collect();
self.render_rss_feed(pages, Some(&PathBuf::from(lang.code.clone())))?;
self.render_feed(
pages,
Some(&PathBuf::from(lang.code.clone())),
&lang.code,
None,
)?;
}
self.render_404()?;
@ -981,10 +1013,16 @@ impl Site {
create_file(&path.join("index.html"), &self.inject_livereload(single_output))?;
}
if taxonomy.kind.rss {
self.render_rss_feed(
if taxonomy.kind.feed {
self.render_feed(
item.pages.iter().map(|p| library.get_page_by_key(*p)).collect(),
Some(&PathBuf::from(format!("{}/{}", taxonomy.kind.name, item.slug))),
if self.config.is_multilingual() && !taxonomy.kind.lang.is_empty() {
&taxonomy.kind.lang
} else {
&self.config.default_language
},
Some((&taxonomy.kind, &item)),
)
} else {
Ok(())
@ -1043,30 +1081,39 @@ impl Site {
Ok(())
}
/// Renders a RSS feed for the given path and at the given path
/// If both arguments are `None`, it will render only the RSS feed for the whole
/// Renders a feed for the given path and at the given path
/// If both arguments are `None`, it will render only the feed for the whole
/// site at the root folder.
pub fn render_rss_feed(
pub fn render_feed(
&self,
all_pages: Vec<&Page>,
base_path: Option<&PathBuf>,
lang: &str,
taxonomy_and_item: Option<(&TaxonomyConfig, &TaxonomyItem)>,
) -> Result<()> {
ensure_directory_exists(&self.output_path)?;
let mut context = Context::new();
let mut pages = all_pages.into_iter().filter(|p| p.meta.date.is_some()).collect::<Vec<_>>();
// Don't generate a RSS feed if none of the pages has a date
// Don't generate a feed if none of the pages has a date
if pages.is_empty() {
return Ok(());
}
pages.par_sort_unstable_by(sort_actual_pages_by_date);
context.insert("last_build_date", &pages[0].meta.date.clone());
context.insert(
"last_updated",
pages.iter()
.filter_map(|page| page.meta.updated.as_ref())
.chain(pages[0].meta.date.as_ref())
.max() // I love lexicographically sorted date strings
.unwrap(), // Guaranteed because of pages[0].meta.date
);
let library = self.library.read().unwrap();
// limit to the last n elements if the limit is set; otherwise use all.
let num_entries = self.config.rss_limit.unwrap_or_else(|| pages.len());
let num_entries = self.config.feed_limit.unwrap_or_else(|| pages.len());
let p = pages
.iter()
.take(num_entries)
@ -1075,16 +1122,28 @@ impl Site {
context.insert("pages", &p);
context.insert("config", &self.config);
context.insert("lang", lang);
let rss_feed_url = if let Some(ref base) = base_path {
self.config.make_permalink(&base.join("rss.xml").to_string_lossy().replace('\\', "/"))
let feed_filename = &self.config.feed_filename;
let feed_url = if let Some(ref base) = base_path {
self.config.make_permalink(
&base
.join(feed_filename)
.to_string_lossy()
.replace('\\', "/"),
)
} else {
self.config.make_permalink("rss.xml")
self.config.make_permalink(feed_filename)
};
context.insert("feed_url", &rss_feed_url);
context.insert("feed_url", &feed_url);
let feed = &render_template("rss.xml", &self.tera, context, &self.config.theme)?;
if let Some((taxonomy, item)) = taxonomy_and_item {
context.insert("taxonomy", taxonomy);
context.insert("term", &SerializedTaxonomyItem::from_item(item));
}
let feed = &render_template(feed_filename, &self.tera, context, &self.config.theme)?;
if let Some(ref base) = base_path {
let mut output_path = self.output_path.clone();
@ -1094,9 +1153,9 @@ impl Site {
create_directory(&output_path)?;
}
}
create_file(&output_path.join("rss.xml"), feed)?;
create_file(&output_path.join(feed_filename), feed)?;
} else {
create_file(&self.output_path.join("rss.xml"), feed)?;
create_file(&self.output_path.join(feed_filename), feed)?;
}
Ok(())
}

View file

@ -14,7 +14,7 @@ use tera::{Map, Value};
#[derive(Debug, Serialize)]
pub struct SitemapEntry<'a> {
pub permalink: Cow<'a, str>,
pub date: Option<String>,
pub updated: Option<String>,
pub extra: Option<&'a Map<String, Value>>,
}
@ -33,8 +33,8 @@ impl<'a> PartialEq for SitemapEntry<'a> {
impl<'a> Eq for SitemapEntry<'a> {}
impl<'a> SitemapEntry<'a> {
pub fn new(permalink: Cow<'a, str>, date: Option<String>) -> Self {
SitemapEntry { permalink, date, extra: None }
pub fn new(permalink: Cow<'a, str>, updated: Option<String>) -> Self {
SitemapEntry { permalink, updated, extra: None }
}
pub fn add_extra(&mut self, extra: &'a Map<String, Value>) {
@ -65,11 +65,10 @@ pub fn find_entries<'a>(
.pages_values()
.iter()
.map(|p| {
let date = match p.meta.date {
Some(ref d) => Some(d.to_string()),
None => None,
};
let mut entry = SitemapEntry::new(Cow::Borrowed(&p.permalink), date);
let mut entry = SitemapEntry::new(
Cow::Borrowed(&p.permalink),
p.meta.updated.clone().or_else(|| p.meta.date.clone()),
);
entry.add_extra(&p.meta.extra);
entry
})

View file

@ -152,7 +152,7 @@ fn can_build_site_without_live_reload() {
// We do have categories
assert_eq!(file_exists!(public, "categories/index.html"), true);
assert_eq!(file_exists!(public, "categories/a-category/index.html"), true);
assert_eq!(file_exists!(public, "categories/a-category/rss.xml"), true);
assert_eq!(file_exists!(public, "categories/a-category/atom.xml"), true);
// But no tags
assert_eq!(file_exists!(public, "tags/index.html"), false);
@ -232,7 +232,7 @@ fn can_build_site_with_live_reload_and_drafts() {
// We do have categories
assert_eq!(file_exists!(public, "categories/index.html"), true);
assert_eq!(file_exists!(public, "categories/a-category/index.html"), true);
assert_eq!(file_exists!(public, "categories/a-category/rss.xml"), true);
assert_eq!(file_exists!(public, "categories/a-category/atom.xml"), true);
// But no tags
assert_eq!(file_exists!(public, "tags/index.html"), false);
@ -294,11 +294,11 @@ fn can_build_site_with_taxonomies() {
assert!(file_exists!(public, "categories/index.html"));
assert!(file_exists!(public, "categories/a/index.html"));
assert!(file_exists!(public, "categories/b/index.html"));
assert!(file_exists!(public, "categories/a/rss.xml"));
assert!(file_exists!(public, "categories/a/atom.xml"));
assert!(file_contains!(
public,
"categories/a/rss.xml",
"https://replace-this-with-your-url.com/categories/a/rss.xml"
"categories/a/atom.xml",
"https://replace-this-with-your-url.com/categories/a/atom.xml"
));
// Extending from a theme works
assert!(file_contains!(public, "categories/a/index.html", "EXTENDED"));
@ -513,7 +513,7 @@ fn can_build_site_with_pagination_for_taxonomy() {
name: "tags".to_string(),
paginate_by: Some(2),
paginate_path: None,
rss: true,
feed: true,
lang: site.config.default_language.clone(),
});
site.load().unwrap();
@ -547,9 +547,9 @@ fn can_build_site_with_pagination_for_taxonomy() {
// Tags
assert!(file_exists!(public, "tags/index.html"));
// With RSS
assert!(file_exists!(public, "tags/a/rss.xml"));
assert!(file_exists!(public, "tags/b/rss.xml"));
// With Atom
assert!(file_exists!(public, "tags/a/atom.xml"));
assert!(file_exists!(public, "tags/b/atom.xml"));
// And pagination!
assert!(file_exists!(public, "tags/a/page/1/index.html"));
assert!(file_exists!(public, "tags/b/page/1/index.html"));
@ -588,15 +588,15 @@ fn can_build_site_with_pagination_for_taxonomy() {
}
#[test]
fn can_build_rss_feed() {
fn can_build_feed() {
let (_, _tmp_dir, public) = build_site("test_site");
assert!(&public.exists());
assert!(file_exists!(public, "rss.xml"));
assert!(file_exists!(public, "atom.xml"));
// latest article is posts/extra-syntax.md
assert!(file_contains!(public, "rss.xml", "Extra Syntax"));
assert!(file_contains!(public, "atom.xml", "Extra Syntax"));
// Next is posts/simple.md
assert!(file_contains!(public, "rss.xml", "Simple article with shortcodes"));
assert!(file_contains!(public, "atom.xml", "Simple article with shortcodes"));
}
#[test]

View file

@ -115,15 +115,17 @@ fn can_build_multilingual_site() {
assert!(file_contains!(public, "sitemap.xml", "https://example.com/fr/blog/something-else/"));
assert!(file_contains!(public, "sitemap.xml", "https://example.com/it/blog/something-else/"));
// one rss per language
assert!(file_exists!(public, "rss.xml"));
assert!(file_contains!(public, "rss.xml", "https://example.com/blog/something-else/"));
assert!(!file_contains!(public, "rss.xml", "https://example.com/fr/blog/something-else/"));
assert!(file_exists!(public, "fr/rss.xml"));
assert!(!file_contains!(public, "fr/rss.xml", "https://example.com/blog/something-else/"));
assert!(file_contains!(public, "fr/rss.xml", "https://example.com/fr/blog/something-else/"));
// Italian doesn't have RSS enabled
assert!(!file_exists!(public, "it/rss.xml"));
// one feed per language
assert!(file_exists!(public, "atom.xml"));
assert!(file_contains!(public, "atom.xml", "https://example.com/blog/something-else/"));
assert!(!file_contains!(public, "atom.xml", "https://example.com/fr/blog/something-else/"));
assert!(file_contains!(public, "atom.xml", r#"<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">"#));
assert!(file_exists!(public, "fr/atom.xml"));
assert!(!file_contains!(public, "fr/atom.xml", "https://example.com/blog/something-else/"));
assert!(file_contains!(public, "fr/atom.xml", "https://example.com/fr/blog/something-else/"));
assert!(file_contains!(public, "fr/atom.xml", r#"<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="fr">"#));
// Italian doesn't have feed enabled
assert!(!file_exists!(public, "it/atom.xml"));
// Taxonomies are per-language
// English
@ -131,7 +133,9 @@ fn can_build_multilingual_site() {
assert!(file_contains!(public, "authors/index.html", "Queen"));
assert!(!file_contains!(public, "authors/index.html", "Vincent"));
assert!(!file_exists!(public, "auteurs/index.html"));
assert!(file_exists!(public, "authors/queen-elizabeth/rss.xml"));
assert!(file_exists!(public, "authors/queen-elizabeth/atom.xml"));
assert!(file_contains!(public, "authors/queen-elizabeth/atom.xml", r#"<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">"#));
assert!(file_contains!(public, "authors/queen-elizabeth/atom.xml", r#"<title> - Queen Elizabeth</title>"#));
assert!(file_exists!(public, "tags/index.html"));
assert!(file_contains!(public, "tags/index.html", "hello"));
@ -142,7 +146,7 @@ fn can_build_multilingual_site() {
assert!(file_exists!(public, "fr/auteurs/index.html"));
assert!(!file_contains!(public, "fr/auteurs/index.html", "Queen"));
assert!(file_contains!(public, "fr/auteurs/index.html", "Vincent"));
assert!(!file_exists!(public, "fr/auteurs/vincent-prouillet/rss.xml"));
assert!(!file_exists!(public, "fr/auteurs/vincent-prouillet/atom.xml"));
assert!(file_exists!(public, "fr/tags/index.html"));
assert!(file_contains!(public, "fr/tags/index.html", "bonjour"));

View file

@ -1,10 +1,3 @@
<!doctype html>
<html>
<head>
<title>File Not Found: 404.</title>
</head>
<body>
<h1>Oops!</h1>
<h2>File Not Found: 404.</h2>
</body>
</html>
<title>404 Not Found</title>
<h1>404 Not Found</h1>

View file

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

View file

@ -1,12 +1,6 @@
<!DOCTYPE html>
<html>
<head>
<title>Redirect</title>
<link rel="canonical" href="{{ url | safe }}" />
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta http-equiv="refresh" content="0;url={{ url | safe }}" />
</head>
<body>
<p><a href="{{ url | safe }}">Click here</a> to be redirected.</p>
</body>
</html>
<!doctype html>
<meta charset="utf-8">
<link rel="canonical" href="{{ url | safe }}">
<meta http-equiv="refresh" content="0;url={{ url | safe }}">
<title>Redirect</title>
<p><a href="{{ url | safe }}">Click here</a> to be redirected.</p>

View file

@ -7,15 +7,15 @@
<generator>Zola</generator>
<language>{{ config.default_language }}</language>
<atom:link href="{{ feed_url | safe }}" rel="self" type="application/rss+xml"/>
<lastBuildDate>{{ last_build_date | date(format="%a, %d %b %Y %H:%M:%S %z") }}</lastBuildDate>
{% for page in pages %}
<item>
<title>{{ page.title }}</title>
<pubDate>{{ page.date | date(format="%a, %d %b %Y %H:%M:%S %z") }}</pubDate>
<link>{{ page.permalink | escape_xml | safe }}</link>
<guid>{{ page.permalink | escape_xml | safe }}</guid>
<description>{% if page.summary %}{{ page.summary }}{% else %}{{ page.content }}{% endif %}</description>
</item>
{% endfor %}
<lastBuildDate>{{ last_updated | date(format="%a, %d %b %Y %H:%M:%S %z") }}</lastBuildDate>
{%- for page in pages %}
<item>
<title>{{ page.title }}</title>
<pubDate>{{ page.date | date(format="%a, %d %b %Y %H:%M:%S %z") }}</pubDate>
<link>{{ page.permalink | escape_xml | safe }}</link>
<guid>{{ page.permalink | escape_xml | safe }}</guid>
<description>{% if page.summary %}{{ page.summary }}{% else %}{{ page.content }}{% endif %}</description>
</item>
{%- endfor %}
</channel>
</rss>

View file

@ -1,7 +1,3 @@
<div {% if class %}class="{{class}}"{% endif %}>
<iframe src="https://www.streamable.com/e/{{id}}"
scrolling="no"
frameborder="0"
allowfullscreen mozallowfullscreen webkitallowfullscreen>
</iframe>
<iframe src="https://www.streamable.com/e/{{id}}" scrolling="no" frameborder="0" allowfullscreen mozallowfullscreen webkitallowfullscreen></iframe>
</div>

View file

@ -1,4 +1,3 @@
<div {% if class %}class="{{class}}"{% endif %}>
<iframe src="//player.vimeo.com/video/{{id}}" webkitallowfullscreen mozallowfullscreen allowfullscreen>
</iframe>
<iframe src="//player.vimeo.com/video/{{id}}" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
</div>

View file

@ -1,4 +1,3 @@
<div {% if class %}class="{{class}}"{% endif %}>
<iframe src="https://www.youtube.com/embed/{{id}}{% if autoplay %}?autoplay=1{% endif %}" webkitallowfullscreen mozallowfullscreen allowfullscreen>
</iframe>
<iframe src="https://www.youtube.com/embed/{{id}}{% if autoplay %}?autoplay=1{% endif %}" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
</div>

View file

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{% for sitemap_entry in entries %}
<url>
<loc>{{ sitemap_entry.permalink | escape_xml | safe }}</loc>
{% if sitemap_entry.date %}
<lastmod>{{ sitemap_entry.date }}</lastmod>
{% endif %}
</url>
{% endfor %}
{%- for sitemap_entry in entries %}
<url>
<loc>{{ sitemap_entry.permalink | escape_xml | safe }}</loc>
{%- if sitemap_entry.updated %}
<lastmod>{{ sitemap_entry.updated }}</lastmod>
{%- endif %}
</url>
{%- endfor %}
</urlset>

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{% for sitemap in sitemaps %}
<sitemap>
<loc>{{ sitemap }}</loc>
</sitemap>
{% endfor %}
</sitemapindex>
{%- for sitemap in sitemaps %}
<sitemap>
<loc>{{ sitemap }}</loc>
</sitemap>
{%- endfor %}
</sitemapindex>

View file

@ -11,6 +11,7 @@ lazy_static! {
let mut tera = Tera::default();
tera.add_raw_templates(vec![
("__zola_builtins/404.html", include_str!("builtins/404.html")),
("__zola_builtins/atom.xml", include_str!("builtins/atom.xml")),
("__zola_builtins/rss.xml", include_str!("builtins/rss.xml")),
("__zola_builtins/sitemap.xml", include_str!("builtins/sitemap.xml")),
("__zola_builtins/robots.txt", include_str!("builtins/robots.txt")),

View file

@ -5,8 +5,9 @@ weight = 50
## Heading id and anchor insertion
While rendering the Markdown content, a unique id will automatically be assigned to each heading.
This id is created by converting the heading text to a [slug](https://en.wikipedia.org/wiki/Semantic_URL#Slug) if `slugify_paths` is enabled.
if `slugify_paths` is disabled, whitespaces are replaced by `_` and the following characters are stripped: `#`, `%`, `<`, `>`, `[`, `]`, `(`, `)`, \`, `^`, `{`, `|`, `}`.
This id is created by converting the heading text to a [slug](https://en.wikipedia.org/wiki/Semantic_URL#Slug) if `slugify.anchors` is set to `"on"` (the default).
If `slugify.paths` is set to `"safe"`, whitespaces are replaced by `_` and the following characters are stripped: `#`, `%`, `<`, `>`, `[`, `]`, `(`, `)`, \`, `^`, `{`, `|`, `}`.
If `slugify.paths` is set to `"off"`, no modifications are made, and you may be left with nominally illegal ids.
A number is appended at the end if the slug already exists for that article
For example:

View file

@ -11,9 +11,9 @@ to your `config.toml`. For example:
```toml
languages = [
{code = "fr", rss = true}, # there will be a RSS feed for French content
{code = "fr", feed = true}, # there will be a feed for French content
{code = "fr", search = true}, # there will be a Search Index for French content
{code = "it"}, # there won't be a RSS feed for Italian content
{code = "it"}, # there won't be a feed for Italian content
]
```

View file

@ -35,12 +35,13 @@ For any page within your content folder, its output path will be defined by eith
- its filename
Either way, these proposed path will be sanitized before being used.
If `slugify_paths` is enabled in the site's config - the default - paths are [slugified](https://en.wikipedia.org/wiki/Clean_URL#Slug).
Otherwise, a simpler sanitation is performed, outputting only valid NTFS paths.
The following characters are removed: `<`, `>`, `:`, `/`, `|`, `?`, `*`, `#`, `\\`, `(`, `)`, `[`, `]` as well as newlines and tabulations.
If `slugify.paths` is set to `"on"` in the site's config - the default - paths are [slugified](https://en.wikipedia.org/wiki/Clean_URL#Slug).
If it is set to `"safe"`, only sanitation is performed, with the following characters being removed: `<`, `>`, `:`, `/`, `|`, `?`, `*`, `#`, `\\`, `(`, `)`, `[`, `]` as well as newlines and tabulations. This ensures that the path can be represented on all operating systems.
Additionally, trailing whitespace and dots are removed and whitespaces are replaced by `_`.
**NOTE:** To produce URLs containing non-English characters (UTF8), `slugify_paths` needs to be set to `false`.
If `slugify.paths` is set to `"off"`, no modifications are made.
If you want URLs containing non-ASCII characters, `slugify.paths` needs to be set to `"safe"` or `"off"`.
### Path from frontmatter
@ -56,7 +57,7 @@ slug = "femmes-libres-libération-kurde"
This is my article.
```
This frontmatter will output the article to `[base_url]/zines/femmes-libres-libération-kurde` with `slugify_paths` disabled, and to `[base_url]/zines/femmes-libres-liberation-kurde` with `slugify_enabled` enabled.
This frontmatter will output the article to `[base_url]/zines/femmes-libres-libération-kurde` with `slugify.paths` set to `"safe"` or `"off"`, and to `[base_url]/zines/femmes-libres-liberation-kurde` with the default value for `slugify.paths` of `"on"`.
### Path from filename
@ -66,7 +67,7 @@ When the article's output path is not specified in the frontmatter, it is extrac
If the path found starts with a datetime string (`YYYY-mm-dd` or [a RFC3339 datetime](https://www.ietf.org/rfc/rfc3339.txt)) followed by an underscore (`_`) or a dash (`-`), this date is removed from the output path and will be used as the page date (unless already set in the front-matter). Note that the full RFC3339 datetime contains colons, which is not a valid character in a filename on Windows.
The output path extracted from the file path is then slugified or not depending on the `slugify_paths` config, as explained previously.
The output path extracted from the file path is then slugified or not, depending on the `slugify.paths` config, as explained previously.
**Example:** The file `content/blog/2018-10-10-hello-world.md` will generated a page available at will be available at `[base_url]/hello-world`.
@ -92,6 +93,10 @@ description = ""
# Setting this overrides a date set in the filename.
date =
# The last updated date of the post, if different from the date.
# Same format as `date`.
updated =
# The weight as defined on the Section page of the documentation.
# If the section variable `sort_by` is set to `weight`, then any page that lacks a `weight`
# will not be rendered.

View file

@ -13,7 +13,7 @@ A taxonomy has five variables:
- `paginate_by`: if this is set to a number, each term page will be paginated by this much.
- `paginate_path`: if set, this path will be used by the paginated page and the page number will be appended after it.
For example the default would be page/1.
- `rss`: if set to `true`, an RSS feed will be generated for each term.
- `feed`: if set to `true`, a feed will be generated for each term.
- `lang`: only set this if you are making a multilingual site and want to indicate which language this taxonomy is for
**Example 1:** (one language)
@ -52,7 +52,7 @@ categories = ["programming"]
In a similar manner to how section and pages calculate their output path:
- the taxonomy name is never slugified
- the taxonomy entry (eg. as specific tag) is slugified when `slugify_paths` is enabled in the configuration
- the taxonomy term (eg. as specific tag) is slugified when `slugify.taxonomies` is enabled (`"on"`, the default) in the configuration
The taxonomy pages are then available at the following paths:

View file

@ -17,11 +17,11 @@ used by Zola as well as their default values are listed below:
# The base URL of the site; the only required configuration variable.
base_url = "mywebsite.com"
# The site title and description; used in RSS by default.
# The site title and description; used in feeds by default.
title = ""
description = ""
# The default language; used in RSS.
# The default language; used in feeds.
default_language = "en"
# The site theme to use.
@ -34,12 +34,17 @@ highlight_code = false
# See below for list of allowed values.
highlight_theme = "base16-ocean-dark"
# When set to "true", an RSS feed is automatically generated.
generate_rss = false
# When set to "true", a feed is automatically generated.
generate_feed = false
# The number of articles to include in the RSS feed. All items are included if
# The filename to use for the feed. Used as the template filename, too.
# Defaults to "atom.xml", which has a builtin template that renders an Atom 1.0 feed.
# There is also a builtin template "rss.xml" that renders an RSS 2.0 feed.
# feed_filename = "atom.xml"
# The number of articles to include in the feed. All items are included if
# this limit is not set (the default).
# rss_limit = 20
# feed_limit = 20
# When set to "true", files in the `static` directory are hard-linked. Useful for large
# static files. Note that for this to work, both `static` and the
@ -50,10 +55,10 @@ generate_rss = false
# The taxonomies to be rendered for the site and their configuration.
# Example:
# taxonomies = [
# {name = "tags", rss = true}, # each tag will have its own RSS feed
# {name = "tags", feed = true}, # each tag will have its own feed
# {name = "tags", lang = "fr"}, # you can have taxonomies with the same name in multiple languages
# {name = "categories", paginate_by = 5}, # 5 items per page for a term
# {name = "authors"}, # Basic definition: no RSS or pagination
# {name = "authors"}, # Basic definition: no feed or pagination
# ]
#
taxonomies = []
@ -61,9 +66,9 @@ taxonomies = []
# The additional languages for the site.
# Example:
# languages = [
# {code = "fr", rss = true}, # there will be a RSS feed for French content
# {code = "fr", feed = true}, # there will be a feed for French content
# {code = "fr", search = true}, # there will be a Search Index for French content
# {code = "it"}, # there won't be a RSS feed for Italian content
# {code = "it"}, # there won't be a feed for Italian content
# ]
#
languages = []

View file

@ -0,0 +1,34 @@
+++
title = "Feeds"
weight = 50
aliases = ["/documentation/templates/rss/"]
+++
If the site `config.toml` file sets `generate_feed = true`, then Zola will
generate a feed file for the site, named according to 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` 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
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
need to provide a template yourself.
**Only pages with a date will be available.**
The feed template gets five variables:
- `config`: the site config
- `feed_url`: the full url to that specific feed
- `last_updated`: the most recent `updated` or `date` field of any post
- `pages`: see [page variables](@/documentation/templates/pages-sections.md#page-variables)
for a detailed description of what this contains
- `lang`: the language code that applies to all of the pages in the feed,
if the site is multilingual, or `config.default_language` if it is not
Feeds for taxonomy terms get two more variables, using types from the
[taxonomies templates](@/documentation/templates/taxonomies.md):
- `taxonomy`: of type `TaxonomyConfig`
- `term`: of type `TaxonomyTerm`, but without `term.pages` (use `pages` instead)

View file

@ -13,7 +13,7 @@ to learn more about it first.
All templates live in the `templates` directory. If you are not sure what variables are available in a template,
you can place `{{ __tera_context }}` in the template to print the whole context.
A few variables are available on all templates except RSS and the sitemap:
A few variables are available on all templates except feeds and the sitemap:
- `config`: the [configuration](@/documentation/getting-started/configuration.md) without any modifications
- `current_path`: the path (full URL without `base_url`) of the current page, never starting with a `/`
@ -36,12 +36,13 @@ section variables. The `page.html` template has access to the page variables.
The page and section variables are described in more detail in the next section.
## Built-in templates
Zola comes with three built-in templates: `rss.xml`, `sitemap.xml` and
`robots.txt` (each is described in its own section of this documentation).
Zola comes with four built-in templates: `atom.xml` and `rss.xml` (described in
[Feeds](@/documentation/templates/feeds.md)), `sitemap.xml` (described in [Sitemap](@/documentation/templates/sitemap.md)),
and `robots.txt` (described in [Robots.txt](@/documentation/templates/robots.md)).
Additionally, themes can add their own templates, which will be applied if not
overridden. You can override built-in or theme templates by creating a template with
the same name in the correct path. For example, you can override the RSS template by
creating a `templates/rss.xml` file.
the same name in the correct path. For example, you can override the Atom template by
creating a `templates/atom.xml` file.
## Custom templates
In addition to the standard `index.html`, `section.html` and `page.html` templates,

View file

@ -19,6 +19,8 @@ content: String;
title: String?;
description: String?;
date: String?;
// `updated` will be the same as `date` if `date` is specified but `updated` is not in front-matter
updated: String?;
slug: String;
path: String;
draft: Bool;

View file

@ -1,18 +0,0 @@
+++
title = "RSS"
weight = 50
+++
If the site `config.toml` file sets `generate_rss = true`, then Zola will
generate an `rss.xml` page for the site, which will live at `base_url/rss.xml`. To
generate the `rss.xml` page, Zola will look for an `rss.xml` file in the `templates`
directory or, if one does not exist, it will use the use the built-in rss template.
**Only pages with a date will be available.**
The RSS template gets three variables in addition to `config`:
- `feed_url`: the full url to that specific feed
- `last_build_date`: the date of the latest post
- `pages`: see [page variables](@/documentation/templates/pages-sections.md#page-variables) for
a detailed description of what this contains

View file

@ -25,7 +25,7 @@ A `SitemapEntry` has the following fields:
```ts
permalink: String;
date: String?;
updated: String?;
extra: Hashmap<String, Any>?;
```

View file

@ -21,10 +21,10 @@ and `TaxonomyConfig` has the following fields:
```ts
name: String,
slug: String,
paginate_by: Number?;
paginate_path: String?;
rss: Bool;
feed: Bool;
lang: String;
```
@ -64,5 +64,5 @@ term: TaxonomyTerm;
lang: String;
```
A paginated taxonomy term will also get a `paginator` variable; see the [pagination page]
(@/documentation/templates/pagination.md) for more details.
A paginated taxonomy term will also get a `paginator` variable; see the
[pagination page](@/documentation/templates/pagination.md) for more details.

View file

@ -100,8 +100,8 @@ Zulma has 3 taxonomies already set internally: `tags`, `cateogories` and `author
```toml
taxonomies = [
{name = "categories"},
{name = "tags", paginate_by = 5, rss = true},
{name = "authors", rss = true},
{name = "tags", paginate_by = 5, feed = true},
{name = "authors", feed = true},
]
```

View file

@ -55,9 +55,9 @@ The theme requires tags and categories taxonomies to be enabled in your `config.
```toml
taxonomies = [
# You can enable/disable RSS
{name = "categories", rss = true},
{name = "tags", rss = true},
# You can enable/disable feeds
{name = "categories", feed = true},
{name = "tags", feed = true},
]
```
If you want to paginate taxonomies pages, you will need to overwrite the templates

View file

@ -48,9 +48,9 @@ The theme requires tags and categories taxonomies to be enabled in your `config.
```toml
taxonomies = [
# You can enable/disable RSS
{name = "categories", rss = true},
{name = "tags", rss = true},
# You can enable/disable feeds
{name = "categories", feed = true},
{name = "tags", feed = true},
]
```
If you want to paginate taxonomies pages, you will need to overwrite the templates

0
sublime_themes/dracula.tmTheme Executable file → Normal file
View file

View file

@ -2,12 +2,12 @@ title = "My site"
base_url = "https://replace-this-with-your-url.com"
highlight_code = true
compile_sass = true
generate_rss = true
generate_feed = true
theme = "sample"
slugify_paths = true
taxonomies = [
{name = "categories", rss = true},
{name = "categories", feed = true},
]
extra_syntaxes = ["syntaxes"]

View file

@ -13,18 +13,18 @@ build_search_index = true
default_language = "en"
generate_rss = true
generate_feed = true
taxonomies = [
{name = "authors", rss = true},
{name = "authors", feed = true},
{name = "auteurs", lang = "fr"},
{name = "tags"},
{name = "tags", lang = "fr"},
]
languages = [
{code = "fr", rss = true},
{code = "it", rss = false, search = true },
{code = "fr", feed = true},
{code = "it", feed = false, search = true },
]
[extra]