zola/components/site/src/lib.rs

1295 lines
47 KiB
Rust
Raw Normal View History

2019-11-26 19:36:52 +00:00
pub mod sitemap;
2019-03-19 19:42:16 +00:00
2019-06-02 18:21:06 +00:00
use std::collections::HashMap;
2018-10-31 07:18:57 +00:00
use std::fs::{copy, create_dir_all, remove_dir_all};
use std::path::{Path, PathBuf};
2019-01-27 17:57:07 +00:00
use std::sync::{Arc, Mutex, RwLock};
use glob::glob;
2018-10-02 14:42:34 +00:00
use rayon::prelude::*;
2018-10-31 07:18:57 +00:00
use sass_rs::{compile_file, Options as SassOptions, OutputStyle};
use serde_derive::Serialize;
2018-10-31 07:18:57 +00:00
use tera::{Context, Tera};
2017-07-06 13:19:15 +00:00
use config::{get_config, Config, Taxonomy as TaxonomyConfig};
2019-12-21 21:52:39 +00:00
use errors::{bail, Error, ErrorKind, Result};
2018-10-31 07:18:57 +00:00
use front_matter::InsertAnchor;
use library::{
find_taxonomies, sort_actual_pages_by_date, Library, Page, Paginator, Section, Taxonomy,
TaxonomyItem,
2018-10-31 07:18:57 +00:00
};
use templates::{global_fns, render_redirect_template, ZOLA_TERA};
use utils::fs::{copy_directory, create_directory, create_file, ensure_directory_exists};
2018-05-11 11:54:16 +00:00
use utils::net::get_available_port;
2018-10-31 07:18:57 +00:00
use utils::templates::{render_template, rewrite_theme_paths};
2017-07-04 23:27:27 +00:00
#[derive(Debug)]
pub struct Site {
/// The base path of the zola site
pub base_path: PathBuf,
2017-05-16 04:37:00 +00:00
/// The parsed config for the site
pub config: Config,
2017-03-27 14:17:33 +00:00
pub tera: Tera,
2018-02-02 20:35:04 +00:00
imageproc: Arc<Mutex<imageproc::Processor>>,
2018-05-11 11:54:16 +00:00
// the live reload port to be used if there is one
pub live_reload: Option<u16>,
2018-03-14 21:03:06 +00:00
pub output_path: PathBuf,
2018-02-02 20:35:04 +00:00
content_path: PathBuf,
2017-08-23 10:17:24 +00:00
pub static_path: PathBuf,
2018-07-16 08:54:05 +00:00
pub taxonomies: Vec<Taxonomy>,
/// A map of all .md files (section and pages) and their permalink
/// We need that if there are relative links in the content that need to be resolved
pub permalinks: HashMap<String, String>,
2018-10-02 14:42:34 +00:00
/// Contains all pages and sections of the site
2019-01-27 17:57:07 +00:00
pub library: Arc<RwLock<Library>>,
2019-08-24 20:23:08 +00:00
/// Whether to load draft pages
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 {
2020-04-22 08:07:17 +00:00
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
pub fn new<P: AsRef<Path>, P2: AsRef<Path>>(path: P, config_file: P2) -> Result<Site> {
let path = path.as_ref();
let config_file = config_file.as_ref();
let mut config = get_config(config_file);
2018-10-09 12:33:43 +00:00
config.load_extra_syntaxes(path)?;
2018-10-31 07:18:57 +00:00
let tpl_glob =
format!("{}/{}", path.to_string_lossy().replace("\\", "/"), "templates/**/*.*ml");
// Only parsing as we might be extending templates from themes and that would error
// as we haven't loaded them yet
let mut tera =
Tera::parse(&tpl_glob).map_err(|e| Error::chain("Error parsing templates", e))?;
if let Some(theme) = config.theme.clone() {
// Grab data from the extra section of the theme
config.merge_with_theme(&path.join("themes").join(&theme).join("theme.toml"))?;
// Test that the templates folder exist for that theme
let theme_path = path.join("themes").join(&theme);
2017-08-23 10:17:24 +00:00
if !theme_path.join("templates").exists() {
bail!("Theme `{}` is missing a templates folder", theme);
}
let theme_tpl_glob = format!(
"{}/{}",
path.to_string_lossy().replace("\\", "/"),
format!("themes/{}/templates/**/*.*ml", theme)
);
let mut tera_theme = Tera::parse(&theme_tpl_glob)
.map_err(|e| Error::chain("Error parsing templates from themes", e))?;
rewrite_theme_paths(&mut tera_theme, &theme);
2019-01-11 19:29:46 +00:00
// TODO: we do that twice, make it dry?
if theme_path.join("templates").join("robots.txt").exists() {
2018-10-31 07:18:57 +00:00
tera_theme
.add_template_file(theme_path.join("templates").join("robots.txt"), None)?;
}
tera.extend(&tera_theme)?;
2017-08-23 10:17:24 +00:00
}
tera.extend(&ZOLA_TERA)?;
tera.build_inheritance_chains()?;
2017-08-23 10:17:24 +00:00
// TODO: Tera doesn't use globset right now so we can load the robots.txt as part
// of the glob above, therefore we load it manually if it exists.
if path.join("templates").join("robots.txt").exists() {
tera.add_template_file(path.join("templates").join("robots.txt"), Some("robots.txt"))?;
}
2018-02-02 20:35:04 +00:00
let content_path = path.join("content");
let static_path = path.join("static");
2018-10-31 07:18:57 +00:00
let imageproc =
imageproc::Processor::new(content_path.clone(), &static_path, &config.base_url);
2018-02-02 20:35:04 +00:00
2017-03-20 10:00:00 +00:00
let site = Site {
base_path: path.to_path_buf(),
config,
tera,
2018-02-02 20:35:04 +00:00
imageproc: Arc::new(Mutex::new(imageproc)),
2018-05-11 11:54:16 +00:00
live_reload: None,
output_path: path.join("public"),
2018-02-02 20:35:04 +00:00
content_path,
static_path,
2018-07-16 08:54:05 +00:00
taxonomies: Vec::new(),
permalinks: HashMap::new(),
2019-08-24 20:23:08 +00:00
include_drafts: false,
2018-10-02 14:42:34 +00:00
// We will allocate it properly later on
2019-01-27 17:57:07 +00:00
library: Arc::new(RwLock::new(Library::new(0, 0, false))),
};
Ok(site)
}
2019-08-24 20:23:08 +00:00
/// Set the site to load the drafts.
/// Needs to be called before loading it
pub fn include_drafts(&mut self) {
self.include_drafts = true;
}
2018-12-28 11:15:17 +00:00
/// The index sections are ALWAYS at those paths
/// There are one index section for the basic language + 1 per language
fn index_section_paths(&self) -> Vec<(PathBuf, Option<String>)> {
let mut res = vec![(self.content_path.join("_index.md"), None)];
for language in &self.config.languages {
2018-12-29 10:17:43 +00:00
res.push((
self.content_path.join(format!("_index.{}.md", language.code)),
Some(language.code.clone()),
));
2018-12-28 11:15:17 +00:00
}
res
2018-03-14 21:03:06 +00:00
}
/// We avoid the port the server is going to use as it's not bound yet
/// when calling this function and we could end up having tried to bind
/// both http and websocket server to the same port
pub fn enable_live_reload(&mut self, port_to_avoid: u16) {
self.live_reload = get_available_port(port_to_avoid);
}
2019-01-27 17:57:07 +00:00
/// Get the number of orphan (== without section) pages in the site
pub fn get_number_orphan_pages(&self) -> usize {
self.library.read().unwrap().get_all_orphan_pages().len()
}
2018-02-02 20:35:04 +00:00
pub fn set_base_url(&mut self, base_url: String) {
2019-01-04 19:31:31 +00:00
let mut imageproc = self.imageproc.lock().expect("Couldn't lock imageproc (set_base_url)");
2018-02-02 20:35:04 +00:00
imageproc.set_base_url(&base_url);
self.config.base_url = base_url;
}
pub fn set_output_path<P: AsRef<Path>>(&mut self, path: P) {
self.output_path = path.as_ref().to_path_buf();
}
2017-03-21 07:57:00 +00:00
/// Reads all .md files in the `content` directory and create pages/sections
/// out of them
2017-03-21 07:57:00 +00:00
pub fn load(&mut self) -> Result<()> {
2017-04-22 04:58:22 +00:00
let base_path = self.base_path.to_string_lossy().replace("\\", "/");
let content_glob = format!("{}/{}", base_path, "content/**/*.md");
2017-06-22 03:01:45 +00:00
let (section_entries, page_entries): (Vec<_>, Vec<_>) = glob(&content_glob)
2019-01-04 19:31:31 +00:00
.expect("Invalid glob")
2017-06-22 03:01:45 +00:00
.filter_map(|e| e.ok())
2018-09-30 19:15:09 +00:00
.filter(|e| !e.as_path().file_name().unwrap().to_str().unwrap().starts_with('.'))
2018-12-29 10:17:43 +00:00
.partition(|entry| {
entry.as_path().file_name().unwrap().to_str().unwrap().starts_with("_index.")
});
2017-06-22 03:01:45 +00:00
self.library = Arc::new(RwLock::new(Library::new(
page_entries.len(),
section_entries.len(),
self.config.is_multilingual(),
)));
2018-10-02 14:42:34 +00:00
2017-06-22 03:01:45 +00:00
let sections = {
let config = &self.config;
section_entries
.into_par_iter()
.map(|entry| {
let path = entry.as_path();
Section::from_file(path, config, &self.base_path)
})
.collect::<Vec<_>>()
2017-06-22 03:01:45 +00:00
};
let pages = {
let config = &self.config;
page_entries
.into_par_iter()
.filter(|entry| match &config.ignored_content_globset {
Some(gs) => !gs.is_match(entry.as_path()),
None => true,
})
2017-06-22 03:01:45 +00:00
.map(|entry| {
let path = entry.as_path();
Page::from_file(path, config, &self.base_path)
})
.collect::<Vec<_>>()
2017-06-22 03:01:45 +00:00
};
// Kinda duplicated code for add_section/add_page but necessary to do it that
// way because of the borrow checker
for section in sections {
let s = section?;
self.add_section(s, false)?;
}
self.create_default_index_sections()?;
let mut pages_insert_anchors = HashMap::new();
for page in pages {
let p = page?;
2019-08-24 20:23:08 +00:00
// Should draft pages be ignored?
if p.meta.draft && !self.include_drafts {
continue;
}
pages_insert_anchors.insert(
p.file.path.clone(),
self.find_parent_section_insert_anchor(&p.file.parent.clone(), &p.lang),
);
self.add_page(p, false)?;
}
2019-12-01 17:03:24 +00:00
{
let library = self.library.read().unwrap();
let collisions = library.check_for_path_collisions();
if !collisions.is_empty() {
return Err(Error::from_collisions(collisions));
}
}
// taxonomy Tera fns are loaded in `register_early_global_fns`
// so we do need to populate it first.
self.populate_taxonomies()?;
self.register_early_global_fns();
self.populate_sections();
self.render_markdown()?;
self.register_tera_global_fns();
// Needs to be done after rendering markdown as we only get the anchors at that point
self.check_internal_links_with_anchors()?;
2019-07-12 21:09:05 +00:00
if self.config.is_in_check_mode() {
self.check_external_links()?;
}
Ok(())
}
/// Very similar to check_external_links but can't be merged as far as I can see since we always
/// want to check the internal links but only the external in zola check :/
pub fn check_internal_links_with_anchors(&self) -> Result<()> {
let library = self.library.write().expect("Get lock for check_internal_links_with_anchors");
let page_links = library
.pages()
.values()
.map(|p| {
let path = &p.file.path;
p.internal_links_with_anchors.iter().map(move |l| (path.clone(), l))
})
.flatten();
let section_links = library
.sections()
.values()
.map(|p| {
let path = &p.file.path;
p.internal_links_with_anchors.iter().map(move |l| (path.clone(), l))
})
.flatten();
let all_links = page_links.chain(section_links).collect::<Vec<_>>();
2019-07-12 21:09:05 +00:00
if self.config.is_in_check_mode() {
println!("Checking {} internal link(s) with an anchor.", all_links.len());
}
if all_links.is_empty() {
return Ok(());
}
let mut full_path = self.base_path.clone();
full_path.push("content");
let errors: Vec<_> = all_links
.iter()
.filter_map(|(page_path, (md_path, anchor))| {
// There are a few `expect` here since the presence of the .md file will
// already have been checked in the markdown rendering
let mut p = full_path.clone();
for part in md_path.split('/') {
p.push(part);
}
if md_path.contains("_index.md") {
let section = library
.get_section(&p)
.expect("Couldn't find section in check_internal_links_with_anchors");
if section.has_anchor(&anchor) {
None
} else {
Some((page_path, md_path, anchor))
}
} else {
let page = library
.get_page(&p)
.expect("Couldn't find section in check_internal_links_with_anchors");
if page.has_anchor(&anchor) {
None
} else {
Some((page_path, md_path, anchor))
}
}
})
.collect();
2019-07-12 21:09:05 +00:00
if self.config.is_in_check_mode() {
2019-07-19 09:10:28 +00:00
println!(
"> Checked {} internal link(s) with an anchor: {} error(s) found.",
all_links.len(),
errors.len()
);
2019-07-12 21:09:05 +00:00
}
if errors.is_empty() {
return Ok(());
}
2019-07-12 21:09:05 +00:00
let msg = errors
.into_iter()
.map(|(page_path, md_path, anchor)| {
format!(
"The anchor in the link `@/{}#{}` in {} does not exist.",
md_path,
anchor,
page_path.to_string_lossy(),
)
})
.collect::<Vec<_>>()
.join("\n");
Fix clippy warnings (#744) Clippy is returning some warnings. Let's fix or explicitly ignore them. In particular: - In `components/imageproc/src/lib.rs`, we implement `Hash` explicitly but derive `PartialEq`. We need to maintain the property that two keys being equal implies the hashes of those two keys are equal. Our `Hash` implementations preserve this, so we'll explicitly ignore the warnings. - In `components/site/src/lib.rs`, we were calling `.into()` on some values that are already of the correct type. - In `components/site/src/lib.rs`, we were using `.map(|x| *x)` in iterator chains to remove a level of indirection; we can instead say `.copied()` (introduced in Rust v1.36) or `.cloned()`. Using `.copied` here is better from a type-checking point of view, but we'll use `.cloned` for now as Rust v1.36 was only recently released. - In `components/templates/src/filters.rs` and `components/utils/src/site.rs`, we were taking `HashMap`s as function arguments but not generically accepting alternate `Hasher` implementations. - In `src/cmd/check.rs`, we use `env::current_dir()` as a default value, but our use of `unwrap_or` meant that we would always retrieve the current directory even when not needed. - In `components/errors/src/lib.rs`, we can use `if let` rather than `match`. - In `components/library/src/content/page.rs`, we can collapse a nested conditional into `else if let ...`. - In `components/library/src/sorting.rs`, a function takes `&&Page` arguments. Clippy warns about this for efficiency reasons, but we're doing it here to match a particular sorting API, so we'll explicitly ignore the warning.
2019-07-12 08:29:44 +00:00
Err(Error { kind: ErrorKind::Msg(msg), source: None })
}
pub fn check_external_links(&self) -> Result<()> {
let library = self.library.write().expect("Get lock for check_external_links");
2019-06-02 18:21:06 +00:00
let page_links = library
.pages()
.values()
.map(|p| {
let path = &p.file.path;
p.external_links.iter().map(move |l| (path.clone(), l))
})
.flatten();
2019-06-02 18:21:06 +00:00
let section_links = library
.sections()
.values()
.map(|p| {
let path = &p.file.path;
p.external_links.iter().map(move |l| (path.clone(), l))
})
.flatten();
let all_links = page_links.chain(section_links).collect::<Vec<_>>();
2019-07-12 21:09:05 +00:00
println!("Checking {} external link(s).", all_links.len());
if all_links.is_empty() {
return Ok(());
}
// create thread pool with lots of threads so we can fetch
// (almost) all pages simultaneously
let threads = std::cmp::min(all_links.len(), 32);
2019-06-02 18:21:06 +00:00
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build()
Fix clippy warnings (#744) Clippy is returning some warnings. Let's fix or explicitly ignore them. In particular: - In `components/imageproc/src/lib.rs`, we implement `Hash` explicitly but derive `PartialEq`. We need to maintain the property that two keys being equal implies the hashes of those two keys are equal. Our `Hash` implementations preserve this, so we'll explicitly ignore the warnings. - In `components/site/src/lib.rs`, we were calling `.into()` on some values that are already of the correct type. - In `components/site/src/lib.rs`, we were using `.map(|x| *x)` in iterator chains to remove a level of indirection; we can instead say `.copied()` (introduced in Rust v1.36) or `.cloned()`. Using `.copied` here is better from a type-checking point of view, but we'll use `.cloned` for now as Rust v1.36 was only recently released. - In `components/templates/src/filters.rs` and `components/utils/src/site.rs`, we were taking `HashMap`s as function arguments but not generically accepting alternate `Hasher` implementations. - In `src/cmd/check.rs`, we use `env::current_dir()` as a default value, but our use of `unwrap_or` meant that we would always retrieve the current directory even when not needed. - In `components/errors/src/lib.rs`, we can use `if let` rather than `match`. - In `components/library/src/content/page.rs`, we can collapse a nested conditional into `else if let ...`. - In `components/library/src/sorting.rs`, a function takes `&&Page` arguments. Clippy warns about this for efficiency reasons, but we're doing it here to match a particular sorting API, so we'll explicitly ignore the warning.
2019-07-12 08:29:44 +00:00
.map_err(|e| Error { kind: ErrorKind::Msg(e.to_string()), source: None })?;
let errors: Vec<_> = pool.install(|| {
2019-06-02 18:21:06 +00:00
all_links
.par_iter()
.filter_map(|(page_path, link)| {
if self
.config
.link_checker
.skip_prefixes
.iter()
.any(|prefix| link.starts_with(prefix))
{
return None;
}
let res = link_checker::check_url(&link, &self.config.link_checker);
if link_checker::is_valid(&res) {
2019-06-02 18:21:06 +00:00
None
} else {
Some((page_path, link, res))
2019-06-02 18:21:06 +00:00
}
})
.collect()
});
2019-07-19 09:10:28 +00:00
println!(
"> Checked {} external link(s): {} error(s) found.",
all_links.len(),
errors.len()
);
2019-07-12 21:09:05 +00:00
if errors.is_empty() {
return Ok(());
}
2019-07-12 21:09:05 +00:00
let msg = errors
.into_iter()
.map(|(page_path, link, check_res)| {
format!(
"Dead link in {} to {}: {}",
page_path.to_string_lossy(),
link,
link_checker::message(&check_res)
)
})
.collect::<Vec<_>>()
.join("\n");
Fix clippy warnings (#744) Clippy is returning some warnings. Let's fix or explicitly ignore them. In particular: - In `components/imageproc/src/lib.rs`, we implement `Hash` explicitly but derive `PartialEq`. We need to maintain the property that two keys being equal implies the hashes of those two keys are equal. Our `Hash` implementations preserve this, so we'll explicitly ignore the warnings. - In `components/site/src/lib.rs`, we were calling `.into()` on some values that are already of the correct type. - In `components/site/src/lib.rs`, we were using `.map(|x| *x)` in iterator chains to remove a level of indirection; we can instead say `.copied()` (introduced in Rust v1.36) or `.cloned()`. Using `.copied` here is better from a type-checking point of view, but we'll use `.cloned` for now as Rust v1.36 was only recently released. - In `components/templates/src/filters.rs` and `components/utils/src/site.rs`, we were taking `HashMap`s as function arguments but not generically accepting alternate `Hasher` implementations. - In `src/cmd/check.rs`, we use `env::current_dir()` as a default value, but our use of `unwrap_or` meant that we would always retrieve the current directory even when not needed. - In `components/errors/src/lib.rs`, we can use `if let` rather than `match`. - In `components/library/src/content/page.rs`, we can collapse a nested conditional into `else if let ...`. - In `components/library/src/sorting.rs`, a function takes `&&Page` arguments. Clippy warns about this for efficiency reasons, but we're doing it here to match a particular sorting API, so we'll explicitly ignore the warning.
2019-07-12 08:29:44 +00:00
Err(Error { kind: ErrorKind::Msg(msg), source: None })
}
/// Insert a default index section for each language if necessary so we don't need to create
/// a _index.md to render the index page at the root of the site
pub fn create_default_index_sections(&mut self) -> Result<()> {
2018-12-28 11:15:17 +00:00
for (index_path, lang) in self.index_section_paths() {
2019-01-27 17:57:07 +00:00
if let Some(ref index_section) = self.library.read().unwrap().get_section(&index_path) {
2018-12-28 11:15:17 +00:00
if self.config.build_search_index && !index_section.meta.in_search_index {
bail!(
2018-03-14 21:03:06 +00:00
"You have enabled search in the config but disabled it in the index section: \
either turn off the search in the config or remote `in_search_index = true` from the \
section front-matter."
2018-12-28 11:15:17 +00:00
)
}
}
2019-01-27 17:57:07 +00:00
let mut library = self.library.write().expect("Get lock for load");
2018-12-28 11:15:17 +00:00
// Not in else because of borrow checker
2019-01-27 17:57:07 +00:00
if !library.contains_section(&index_path) {
2018-12-28 11:15:17 +00:00
let mut index_section = Section::default();
index_section.file.parent = self.content_path.clone();
2018-12-29 10:17:43 +00:00
index_section.file.filename =
index_path.file_name().unwrap().to_string_lossy().to_string();
2018-12-28 11:15:17 +00:00
if let Some(ref l) = lang {
index_section.file.name = format!("_index.{}", l);
2019-12-01 17:03:24 +00:00
index_section.path = format!("{}/", l);
2018-12-28 11:15:17 +00:00
index_section.permalink = self.config.make_permalink(l);
let filename = format!("_index.{}.md", l);
index_section.file.path = self.content_path.join(&filename);
index_section.file.relative = filename;
} else {
index_section.file.name = "_index".to_string();
2018-12-28 11:15:17 +00:00
index_section.permalink = self.config.make_permalink("");
index_section.file.path = self.content_path.join("_index.md");
index_section.file.relative = "_index.md".to_string();
index_section.path = "/".to_string();
2018-12-28 11:15:17 +00:00
}
index_section.lang = index_section.file.find_language(&self.config)?;
2019-01-27 17:57:07 +00:00
library.insert_section(index_section);
2018-03-14 21:03:06 +00:00
}
}
2017-03-21 07:57:00 +00:00
2018-01-09 20:57:29 +00:00
Ok(())
}
/// Render the markdown of all pages/sections
/// Used in a build and in `serve` if a shortcode has changed
pub fn render_markdown(&mut self) -> Result<()> {
// Another silly thing needed to not borrow &self in parallel and
// make the borrow checker happy
let permalinks = &self.permalinks;
let tera = &self.tera;
let config = &self.config;
// This is needed in the first place because of silly borrow checker
let mut pages_insert_anchors = HashMap::new();
2019-01-27 17:57:07 +00:00
for (_, p) in self.library.read().unwrap().pages() {
2018-10-31 07:18:57 +00:00
pages_insert_anchors.insert(
p.file.path.clone(),
2018-12-28 11:15:17 +00:00
self.find_parent_section_insert_anchor(&p.file.parent.clone(), &p.lang),
2018-10-31 07:18:57 +00:00
);
2017-05-12 09:05:00 +00:00
}
2019-01-27 17:57:07 +00:00
let mut library = self.library.write().expect("Get lock for render_markdown");
library
2018-10-02 14:42:34 +00:00
.pages_mut()
.values_mut()
.collect::<Vec<_>>()
.par_iter_mut()
.map(|page| {
2018-01-09 20:57:29 +00:00
let insert_anchor = pages_insert_anchors[&page.file.path];
2018-10-09 12:33:43 +00:00
page.render_markdown(permalinks, tera, config, insert_anchor)
2018-01-09 20:57:29 +00:00
})
.collect::<Result<()>>()?;
2017-03-21 07:57:00 +00:00
2019-01-27 17:57:07 +00:00
library
2018-10-02 14:42:34 +00:00
.sections_mut()
.values_mut()
.collect::<Vec<_>>()
.par_iter_mut()
2018-10-09 12:33:43 +00:00
.map(|section| section.render_markdown(permalinks, tera, config))
.collect::<Result<()>>()?;
2017-05-03 14:16:09 +00:00
2017-03-21 07:57:00 +00:00
Ok(())
}
/// Adds global fns that are to be available to shortcodes while
/// markdown
2018-02-02 20:35:04 +00:00
pub fn register_early_global_fns(&mut self) {
2018-09-12 14:45:52 +00:00
self.tera.register_function(
2018-10-31 07:18:57 +00:00
"get_url",
global_fns::GetUrl::new(
self.config.clone(),
self.permalinks.clone(),
vec![self.static_path.clone(), self.output_path.clone(), self.content_path.clone()],
),
2018-02-02 20:35:04 +00:00
);
2018-09-12 14:45:52 +00:00
self.tera.register_function(
2018-10-31 07:18:57 +00:00
"resize_image",
2019-01-23 18:20:02 +00:00
global_fns::ResizeImage::new(self.imageproc.clone()),
2018-02-02 20:35:04 +00:00
);
self.tera.register_function(
"get_image_metadata",
global_fns::GetImageMeta::new(self.content_path.clone()),
);
self.tera.register_function("load_data", global_fns::LoadData::new(self.base_path.clone()));
2019-01-23 18:20:02 +00:00
self.tera.register_function("trans", global_fns::Trans::new(self.config.clone()));
self.tera.register_function(
"get_taxonomy_url",
2019-08-24 20:23:08 +00:00
global_fns::GetTaxonomyUrl::new(&self.config.default_language, &self.taxonomies),
);
self.tera.register_function(
"get_file_hash",
global_fns::GetFileHash::new(vec![
self.static_path.clone(),
self.output_path.clone(),
self.content_path.clone(),
]),
);
2018-02-02 20:35:04 +00:00
}
2017-09-26 08:25:55 +00:00
pub fn register_tera_global_fns(&mut self) {
self.tera.register_function(
"get_page",
global_fns::GetPage::new(self.base_path.clone(), self.library.clone()),
);
self.tera.register_function(
"get_section",
global_fns::GetSection::new(self.base_path.clone(), self.library.clone()),
);
2018-09-12 14:45:52 +00:00
self.tera.register_function(
2018-07-27 16:45:19 +00:00
"get_taxonomy",
2019-08-24 20:23:08 +00:00
global_fns::GetTaxonomy::new(
&self.config.default_language,
self.taxonomies.clone(),
self.library.clone(),
),
);
2017-05-17 10:04:26 +00:00
}
/// Add a page to the site
/// The `render` parameter is used in the serve command, when rebuilding a page.
/// If `true`, it will also render the markdown for that page
/// Returns the previous page struct if there was one at the same path
2018-10-02 14:42:34 +00:00
pub fn add_page(&mut self, mut page: Page, render: bool) -> Result<Option<Page>> {
self.permalinks.insert(page.file.relative.clone(), page.permalink.clone());
if render {
2018-12-29 10:17:43 +00:00
let insert_anchor =
self.find_parent_section_insert_anchor(&page.file.parent, &page.lang);
2018-10-09 12:33:43 +00:00
page.render_markdown(&self.permalinks, &self.tera, &self.config, insert_anchor)?;
}
2019-01-27 17:57:07 +00:00
let mut library = self.library.write().expect("Get lock for add_page");
let prev = library.remove_page(&page.file.path);
library.insert_page(page);
Ok(prev)
2017-03-21 07:57:00 +00:00
}
/// Add a section to the site
/// The `render` parameter is used in the serve command, when rebuilding a page.
/// If `true`, it will also render the markdown for that page
/// Returns the previous section struct if there was one at the same path
2018-10-02 14:42:34 +00:00
pub fn add_section(&mut self, mut section: Section, render: bool) -> Result<Option<Section>> {
self.permalinks.insert(section.file.relative.clone(), section.permalink.clone());
if render {
2018-10-09 12:33:43 +00:00
section.render_markdown(&self.permalinks, &self.tera, &self.config)?;
2017-05-12 09:05:00 +00:00
}
2019-01-27 17:57:07 +00:00
let mut library = self.library.write().expect("Get lock for add_section");
let prev = library.remove_section(&section.file.path);
library.insert_section(section);
Ok(prev)
}
/// Finds the insert_anchor for the parent section of the directory at `path`.
/// Defaults to `AnchorInsert::None` if no parent section found
2018-12-29 10:17:43 +00:00
pub fn find_parent_section_insert_anchor(
&self,
parent_path: &PathBuf,
lang: &str,
2018-12-29 10:17:43 +00:00
) -> InsertAnchor {
let parent = if lang != self.config.default_language {
parent_path.join(format!("_index.{}.md", lang))
2018-12-28 11:15:17 +00:00
} else {
parent_path.join("_index.md")
};
2019-01-27 17:57:07 +00:00
match self.library.read().unwrap().get_section(&parent) {
2018-03-14 17:22:24 +00:00
Some(s) => s.meta.insert_anchor_links,
2018-10-31 07:18:57 +00:00
None => InsertAnchor::None,
}
}
2017-03-21 07:57:00 +00:00
/// Find out the direct subsections of each subsection if there are some
/// as well as the pages for each section
pub fn populate_sections(&mut self) {
2019-01-27 17:57:07 +00:00
let mut library = self.library.write().expect("Get lock for populate_sections");
library.populate_sections(&self.config);
}
2017-05-16 04:37:00 +00:00
/// Find all the tags and categories if it's asked in the config
pub fn populate_taxonomies(&mut self) -> Result<()> {
2018-07-16 08:54:05 +00:00
if self.config.taxonomies.is_empty() {
return Ok(());
2017-05-16 04:37:00 +00:00
}
2017-03-20 03:42:43 +00:00
2019-01-27 17:57:07 +00:00
self.taxonomies = find_taxonomies(&self.config, &self.library.read().unwrap())?;
Ok(())
2017-03-20 03:42:43 +00:00
}
/// Inject live reload script tag if in live reload mode
fn inject_livereload(&self, mut html: String) -> String {
2018-05-11 11:54:16 +00:00
if let Some(port) = self.live_reload {
2020-04-22 08:07:17 +00:00
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);
}
2017-03-06 10:35:56 +00:00
}
html
}
2017-08-23 10:17:24 +00:00
/// Copy the main `static` folder and the theme `static` folder if a theme is used
pub fn copy_static_directories(&self) -> Result<()> {
// The user files will overwrite the theme files
if let Some(ref theme) = self.config.theme {
2018-03-14 21:03:06 +00:00
copy_directory(
&self.base_path.join("themes").join(theme).join("static"),
2018-07-31 13:17:31 +00:00
&self.output_path,
2019-07-19 09:10:28 +00:00
false,
2017-08-23 10:17:24 +00:00
)?;
}
2017-10-25 12:49:54 +00:00
// We're fine with missing static folders
if self.static_path.exists() {
copy_directory(&self.static_path, &self.output_path, self.config.hard_link_static)?;
2017-10-25 12:49:54 +00:00
}
2017-08-23 10:17:24 +00:00
Ok(())
}
2018-02-02 20:35:04 +00:00
pub fn num_img_ops(&self) -> usize {
2019-01-04 19:31:31 +00:00
let imageproc = self.imageproc.lock().expect("Couldn't lock imageproc (num_img_ops)");
2018-02-02 20:35:04 +00:00
imageproc.num_img_ops()
}
pub fn process_images(&self) -> Result<()> {
2019-01-04 19:34:20 +00:00
let mut imageproc =
self.imageproc.lock().expect("Couldn't lock imageproc (process_images)");
2018-02-02 20:35:04 +00:00
imageproc.prune()?;
imageproc.do_process()
}
2017-03-10 11:39:58 +00:00
/// Deletes the `public` directory if it exists
pub fn clean(&self) -> Result<()> {
if self.output_path.exists() {
2017-03-10 11:39:58 +00:00
// Delete current `public` directory so we can start fresh
remove_dir_all(&self.output_path)
.map_err(|e| Error::chain("Couldn't delete output directory", e))?;
2017-03-10 11:39:58 +00:00
}
Ok(())
}
2017-05-03 08:52:49 +00:00
/// Renders a single content page
2017-06-29 12:14:08 +00:00
pub fn render_page(&self, page: &Page) -> Result<()> {
2017-05-16 04:37:00 +00:00
ensure_directory_exists(&self.output_path)?;
2017-05-01 08:10:22 +00:00
// Copy the nesting of the content directory if we have sections for that page
2017-05-08 10:29:37 +00:00
let mut current_path = self.output_path.to_path_buf();
2017-05-01 08:10:22 +00:00
for component in page.path.split('/') {
current_path.push(component);
2017-05-01 08:10:22 +00:00
if !current_path.exists() {
create_directory(&current_path)?;
}
2017-05-01 08:10:22 +00:00
}
2017-05-01 08:10:22 +00:00
// Make sure the folder exists
create_directory(&current_path)?;
2017-05-01 08:10:22 +00:00
// Finally, create a index.html file there with the page rendered
2019-01-27 17:57:07 +00:00
let output = page.render_html(&self.tera, &self.config, &self.library.read().unwrap())?;
create_file(&current_path.join("index.html"), &self.inject_livereload(output))?;
2017-05-01 08:10:22 +00:00
// Copy any asset we found previously into the same directory as the index.html
for asset in &page.assets {
let asset_path = asset.as_path();
2019-01-04 19:34:20 +00:00
copy(
&asset_path,
&current_path
.join(asset_path.file_name().expect("Couldn't get filename from page asset")),
)?;
2017-05-01 08:10:22 +00:00
}
Ok(())
}
2017-05-16 04:37:00 +00:00
/// Deletes the `public` directory and builds the site
2017-03-10 11:39:58 +00:00
pub fn build(&self) -> Result<()> {
self.clean()?;
// Generate/move all assets before rendering any content
if let Some(ref theme) = self.config.theme {
let theme_path = self.base_path.join("themes").join(theme);
if theme_path.join("sass").exists() {
self.compile_sass(&theme_path)?;
}
}
if self.config.compile_sass {
self.compile_sass(&self.base_path)?;
}
if self.config.build_search_index {
self.build_search_index()?;
}
2017-06-16 14:09:01 +00:00
// Render aliases first to allow overwriting
self.render_aliases()?;
2017-05-08 10:29:37 +00:00
self.render_sections()?;
self.render_orphan_pages()?;
2017-03-10 11:39:58 +00:00
self.render_sitemap()?;
2019-01-02 21:11:34 +00:00
2019-01-27 17:57:07 +00:00
let library = self.library.read().unwrap();
Support and default to generating Atom feeds This includes several breaking changes, but they’re easy to adjust for. Atom 1.0 is superior to RSS 2.0 in a number of ways, both technical and legal, though information from the last decade is hard to find. http://www.intertwingly.net/wiki/pie/Rss20AndAtom10Compared has some info which is probably still mostly correct. How do RSS and Atom compare in terms of implementation support? The impression I get is that proper Atom support in normal content websites has been universal for over twelve years, but that support in podcasts was not quite so good, but getting there, over twelve years ago. I have no more recent facts or figures; no one talks about this stuff these days. I remember investigating this stuff back in 2011–2013 and coming to the same conclusion. At that time, I went with Atom on websites and RSS in podcasts. Now I’d just go full Atom and hang any podcast tools that don’t support Atom, because Atom’s semantics truly are much better. In light of all this, I make the bold recommendation to default to Atom. Nonetheless, for compatibility for existing users, and for those that have Opinions, I’ve retained the RSS template, so that you can escape the breaking change easily. I personally prefer to give feeds a basename that doesn’t mention “Atom” or “RSS”, e.g. “feed.xml”. I’ll be doing that myself, as I’ll be using my own template with more Atom features anyway, like author information, taxonomies and making the title field HTML. Some notes about the Atom feed template: - I went with atom.xml rather than something like feed.atom (the .atom file format being registered for this purpose by RFC4287) due to lack of confidence that it’ll be served with the right MIME type. .xml is a safer default. - It might be nice to get Zola’s version number into the <generator> tag. Not for any particularly good reason, y’know. Just picture it: <generator uri="https://www.getzola.org/" version="0.10.0"> Zola </generator> - I’d like to get taxonomies into the feed, but this requires exposing a little more info than is currently exposed. I think it’d require `TaxonomyConfig` to preferably have a new member `permalink` added (which should be equivalent to something like `config.base_url ~ "/" ~ taxonomy.slug ~ "/"`), and for the feed to get all the taxonomies passed into it (`taxonomies: HashMap<String, TaxonomyTerm>`). Then, the template could be like this, inside the entry: {% for taxonomy, terms in page.taxonomies %} {% for term in terms %} <category scheme="{{ taxonomies[taxonomy].permalink }}" term="{{ term.slug }}" label="{{ term.name }}" /> {% endfor %} {% endfor %} Other remarks: - I have added a date field `extra.updated` to my posts and include that in the feed; I’ve observed others with a similar field. I believe this should be included as an official field. I’m inclined to add author to at least config.toml, too, for feeds. - We need to have a link from the docs to the source of the built-in templates, to help people that wish to alter it.
2019-08-11 10:25:24 +00:00
if self.config.generate_feed {
let is_multilingual = self.config.is_multilingual();
let pages = if is_multilingual {
2019-01-27 17:57:07 +00:00
library
2019-01-02 21:11:34 +00:00
.pages_values()
.iter()
.filter(|p| p.lang == self.config.default_language)
Fix clippy warnings (#744) Clippy is returning some warnings. Let's fix or explicitly ignore them. In particular: - In `components/imageproc/src/lib.rs`, we implement `Hash` explicitly but derive `PartialEq`. We need to maintain the property that two keys being equal implies the hashes of those two keys are equal. Our `Hash` implementations preserve this, so we'll explicitly ignore the warnings. - In `components/site/src/lib.rs`, we were calling `.into()` on some values that are already of the correct type. - In `components/site/src/lib.rs`, we were using `.map(|x| *x)` in iterator chains to remove a level of indirection; we can instead say `.copied()` (introduced in Rust v1.36) or `.cloned()`. Using `.copied` here is better from a type-checking point of view, but we'll use `.cloned` for now as Rust v1.36 was only recently released. - In `components/templates/src/filters.rs` and `components/utils/src/site.rs`, we were taking `HashMap`s as function arguments but not generically accepting alternate `Hasher` implementations. - In `src/cmd/check.rs`, we use `env::current_dir()` as a default value, but our use of `unwrap_or` meant that we would always retrieve the current directory even when not needed. - In `components/errors/src/lib.rs`, we can use `if let` rather than `match`. - In `components/library/src/content/page.rs`, we can collapse a nested conditional into `else if let ...`. - In `components/library/src/sorting.rs`, a function takes `&&Page` arguments. Clippy warns about this for efficiency reasons, but we're doing it here to match a particular sorting API, so we'll explicitly ignore the warning.
2019-07-12 08:29:44 +00:00
.cloned()
2019-01-02 21:11:34 +00:00
.collect()
} else {
2019-01-27 17:57:07 +00:00
library.pages_values()
2019-01-02 21:11:34 +00:00
};
2020-04-22 08:07:17 +00:00
self.render_feed(pages, None, &self.config.default_language, None)?;
2019-01-02 21:11:34 +00:00
}
for lang in &self.config.languages {
Support and default to generating Atom feeds This includes several breaking changes, but they’re easy to adjust for. Atom 1.0 is superior to RSS 2.0 in a number of ways, both technical and legal, though information from the last decade is hard to find. http://www.intertwingly.net/wiki/pie/Rss20AndAtom10Compared has some info which is probably still mostly correct. How do RSS and Atom compare in terms of implementation support? The impression I get is that proper Atom support in normal content websites has been universal for over twelve years, but that support in podcasts was not quite so good, but getting there, over twelve years ago. I have no more recent facts or figures; no one talks about this stuff these days. I remember investigating this stuff back in 2011–2013 and coming to the same conclusion. At that time, I went with Atom on websites and RSS in podcasts. Now I’d just go full Atom and hang any podcast tools that don’t support Atom, because Atom’s semantics truly are much better. In light of all this, I make the bold recommendation to default to Atom. Nonetheless, for compatibility for existing users, and for those that have Opinions, I’ve retained the RSS template, so that you can escape the breaking change easily. I personally prefer to give feeds a basename that doesn’t mention “Atom” or “RSS”, e.g. “feed.xml”. I’ll be doing that myself, as I’ll be using my own template with more Atom features anyway, like author information, taxonomies and making the title field HTML. Some notes about the Atom feed template: - I went with atom.xml rather than something like feed.atom (the .atom file format being registered for this purpose by RFC4287) due to lack of confidence that it’ll be served with the right MIME type. .xml is a safer default. - It might be nice to get Zola’s version number into the <generator> tag. Not for any particularly good reason, y’know. Just picture it: <generator uri="https://www.getzola.org/" version="0.10.0"> Zola </generator> - I’d like to get taxonomies into the feed, but this requires exposing a little more info than is currently exposed. I think it’d require `TaxonomyConfig` to preferably have a new member `permalink` added (which should be equivalent to something like `config.base_url ~ "/" ~ taxonomy.slug ~ "/"`), and for the feed to get all the taxonomies passed into it (`taxonomies: HashMap<String, TaxonomyTerm>`). Then, the template could be like this, inside the entry: {% for taxonomy, terms in page.taxonomies %} {% for term in terms %} <category scheme="{{ taxonomies[taxonomy].permalink }}" term="{{ term.slug }}" label="{{ term.name }}" /> {% endfor %} {% endfor %} Other remarks: - I have added a date field `extra.updated` to my posts and include that in the feed; I’ve observed others with a similar field. I believe this should be included as an official field. I’m inclined to add author to at least config.toml, too, for feeds. - We need to have a link from the docs to the source of the built-in templates, to help people that wish to alter it.
2019-08-11 10:25:24 +00:00
if !lang.feed {
2019-01-02 21:11:34 +00:00
continue;
}
let pages =
Fix clippy warnings (#744) Clippy is returning some warnings. Let's fix or explicitly ignore them. In particular: - In `components/imageproc/src/lib.rs`, we implement `Hash` explicitly but derive `PartialEq`. We need to maintain the property that two keys being equal implies the hashes of those two keys are equal. Our `Hash` implementations preserve this, so we'll explicitly ignore the warnings. - In `components/site/src/lib.rs`, we were calling `.into()` on some values that are already of the correct type. - In `components/site/src/lib.rs`, we were using `.map(|x| *x)` in iterator chains to remove a level of indirection; we can instead say `.copied()` (introduced in Rust v1.36) or `.cloned()`. Using `.copied` here is better from a type-checking point of view, but we'll use `.cloned` for now as Rust v1.36 was only recently released. - In `components/templates/src/filters.rs` and `components/utils/src/site.rs`, we were taking `HashMap`s as function arguments but not generically accepting alternate `Hasher` implementations. - In `src/cmd/check.rs`, we use `env::current_dir()` as a default value, but our use of `unwrap_or` meant that we would always retrieve the current directory even when not needed. - In `components/errors/src/lib.rs`, we can use `if let` rather than `match`. - In `components/library/src/content/page.rs`, we can collapse a nested conditional into `else if let ...`. - In `components/library/src/sorting.rs`, a function takes `&&Page` arguments. Clippy warns about this for efficiency reasons, but we're doing it here to match a particular sorting API, so we'll explicitly ignore the warning.
2019-07-12 08:29:44 +00:00
library.pages_values().iter().filter(|p| p.lang == lang.code).cloned().collect();
2020-04-22 08:07:17 +00:00
self.render_feed(pages, Some(&PathBuf::from(lang.code.clone())), &lang.code, None)?;
2017-03-12 03:59:28 +00:00
}
2019-01-02 21:11:34 +00:00
self.render_404()?;
2017-03-20 12:40:03 +00:00
self.render_robots()?;
2018-07-16 08:54:05 +00:00
self.render_taxonomies()?;
// We process images at the end as we might have picked up images to process from markdown
// or from templates
self.process_images()?;
// Processed images will be in static so the last step is to copy it
self.copy_static_directories()?;
2017-03-20 12:40:03 +00:00
2018-03-15 17:58:32 +00:00
Ok(())
}
pub fn build_search_index(&self) -> Result<()> {
ensure_directory_exists(&self.output_path)?;
2018-03-15 17:58:32 +00:00
// index first
create_file(
2018-03-20 20:27:33 +00:00
&self.output_path.join(&format!("search_index.{}.js", self.config.default_language)),
2018-03-15 17:58:32 +00:00
&format!(
"window.searchIndex = {};",
search::build_index(
&self.config.default_language,
&self.library.read().unwrap(),
&self.config
)?
2018-03-15 17:58:32 +00:00
),
)?;
for language in &self.config.languages {
if language.code != self.config.default_language && language.search {
create_file(
&self.output_path.join(&format!("search_index.{}.js", &language.code)),
&format!(
"window.searchIndex = {};",
search::build_index(
&language.code,
&self.library.read().unwrap(),
&self.config
)?
),
)?;
}
}
2018-03-15 17:58:32 +00:00
// then elasticlunr.min.js
2018-10-31 07:18:57 +00:00
create_file(&self.output_path.join("elasticlunr.min.js"), search::ELASTICLUNR_JS)?;
2018-03-15 17:58:32 +00:00
Ok(())
2017-03-10 11:39:58 +00:00
}
2017-06-16 14:09:01 +00:00
pub fn compile_sass(&self, base_path: &Path) -> Result<()> {
2017-07-06 13:19:15 +00:00
ensure_directory_exists(&self.output_path)?;
let sass_path = {
let mut sass_path = PathBuf::from(base_path);
sass_path.push("sass");
sass_path
};
let mut options = SassOptions::default();
options.output_style = OutputStyle::Compressed;
2018-09-30 19:15:09 +00:00
let mut compiled_paths = self.compile_sass_glob(&sass_path, "scss", &options.clone())?;
options.indented_syntax = true;
2018-09-30 19:15:09 +00:00
compiled_paths.extend(self.compile_sass_glob(&sass_path, "sass", &options)?);
compiled_paths.sort();
for window in compiled_paths.windows(2) {
if window[0].1 == window[1].1 {
bail!(
"SASS path conflict: \"{}\" and \"{}\" both compile to \"{}\"",
window[0].0.display(),
window[1].0.display(),
window[0].1.display(),
);
}
}
Ok(())
}
2018-10-31 07:18:57 +00:00
fn compile_sass_glob(
&self,
sass_path: &Path,
extension: &str,
options: &SassOptions,
) -> Result<Vec<(PathBuf, PathBuf)>> {
let glob_string = format!("{}/**/*.{}", sass_path.display(), extension);
let files = glob(&glob_string)
2019-01-04 19:31:31 +00:00
.expect("Invalid glob for sass")
2017-07-06 13:19:15 +00:00
.filter_map(|e| e.ok())
2018-10-31 07:18:57 +00:00
.filter(|entry| {
!entry.as_path().file_name().unwrap().to_string_lossy().starts_with('_')
})
2017-07-06 13:19:15 +00:00
.collect::<Vec<_>>();
let mut compiled_paths = Vec::new();
2017-07-06 13:19:15 +00:00
for file in files {
let css = compile_file(&file, options.clone())?;
2017-07-06 13:19:15 +00:00
let path_inside_sass = file.strip_prefix(&sass_path).unwrap();
let parent_inside_sass = path_inside_sass.parent();
let css_output_path = self.output_path.join(path_inside_sass).with_extension("css");
if parent_inside_sass.is_some() {
create_dir_all(&css_output_path.parent().unwrap())?;
}
create_file(&css_output_path, &css)?;
compiled_paths.push((path_inside_sass.to_owned(), css_output_path));
2017-07-06 13:19:15 +00:00
}
Ok(compiled_paths)
2017-07-06 13:19:15 +00:00
}
2019-06-02 18:21:06 +00:00
fn render_alias(&self, alias: &str, permalink: &str) -> Result<()> {
let mut output_path = self.output_path.to_path_buf();
let mut split = alias.split('/').collect::<Vec<_>>();
// If the alias ends with an html file name, use that instead of mapping
// as a path containing an `index.html`
let page_name = match split.pop() {
Some(part) if part.ends_with(".html") => part,
Some(part) => {
split.push(part);
"index.html"
}
None => "index.html",
};
for component in split {
output_path.push(&component);
if !output_path.exists() {
create_directory(&output_path)?;
}
}
create_file(
&output_path.join(page_name),
&render_redirect_template(&permalink, &self.tera)?,
)
}
2017-06-16 14:09:01 +00:00
pub fn render_aliases(&self) -> Result<()> {
ensure_directory_exists(&self.output_path)?;
2019-06-02 18:21:06 +00:00
let library = self.library.read().unwrap();
for (_, page) in library.pages() {
2018-03-21 15:18:24 +00:00
for alias in &page.meta.aliases {
2019-06-02 18:21:06 +00:00
self.render_alias(&alias, &page.permalink)?;
}
}
for (_, section) in library.sections() {
for alias in &section.meta.aliases {
self.render_alias(&alias, &section.permalink)?;
2017-06-16 14:09:01 +00:00
}
}
Ok(())
}
2017-03-10 11:39:58 +00:00
/// Renders 404.html
pub fn render_404(&self) -> Result<()> {
ensure_directory_exists(&self.output_path)?;
let mut context = Context::new();
context.insert("config", &self.config);
2019-01-23 18:20:02 +00:00
let output = render_template("404.html", &self.tera, context, &self.config.theme)?;
2018-12-10 17:21:08 +00:00
create_file(&self.output_path.join("404.html"), &self.inject_livereload(output))
}
2017-05-03 08:52:49 +00:00
/// Renders robots.txt
pub fn render_robots(&self) -> Result<()> {
2017-05-16 04:37:00 +00:00
ensure_directory_exists(&self.output_path)?;
let mut context = Context::new();
context.insert("config", &self.config);
2017-03-20 12:40:03 +00:00
create_file(
&self.output_path.join("robots.txt"),
2019-01-23 18:20:02 +00:00
&render_template("robots.txt", &self.tera, context, &self.config.theme)?,
2017-03-20 12:40:03 +00:00
)
}
2019-07-19 09:10:28 +00:00
/// Renders all taxonomies
2018-07-16 08:54:05 +00:00
pub fn render_taxonomies(&self) -> Result<()> {
for taxonomy in &self.taxonomies {
self.render_taxonomy(taxonomy)?;
}
2017-03-20 03:42:43 +00:00
2017-05-16 04:37:00 +00:00
Ok(())
}
2017-03-07 06:01:20 +00:00
2017-05-16 04:37:00 +00:00
fn render_taxonomy(&self, taxonomy: &Taxonomy) -> Result<()> {
2017-05-30 10:23:07 +00:00
if taxonomy.items.is_empty() {
2018-07-31 13:17:31 +00:00
return Ok(());
2017-05-30 10:23:07 +00:00
}
2017-05-16 04:37:00 +00:00
ensure_directory_exists(&self.output_path)?;
let output_path = if taxonomy.kind.lang != self.config.default_language {
let mid_path = self.output_path.join(&taxonomy.kind.lang);
2019-01-07 20:03:34 +00:00
create_directory(&mid_path)?;
mid_path.join(&taxonomy.kind.name)
} else {
self.output_path.join(&taxonomy.kind.name)
};
let list_output =
taxonomy.render_all_terms(&self.tera, &self.config, &self.library.read().unwrap())?;
2017-03-10 11:39:58 +00:00
create_directory(&output_path)?;
create_file(&output_path.join("index.html"), &self.inject_livereload(list_output))?;
2019-01-27 17:57:07 +00:00
let library = self.library.read().unwrap();
2017-07-05 10:34:41 +00:00
taxonomy
.items
.par_iter()
.map(|item| {
let path = output_path.join(&item.slug);
2018-07-16 08:54:05 +00:00
if taxonomy.kind.is_paginated() {
2018-10-31 07:18:57 +00:00
self.render_paginated(
&path,
2019-01-27 17:57:07 +00:00
&Paginator::from_taxonomy(&taxonomy, item, &library),
)?;
2018-07-16 08:54:05 +00:00
} else {
2018-10-31 07:18:57 +00:00
let single_output =
2019-01-27 17:57:07 +00:00
taxonomy.render_term(item, &self.tera, &self.config, &library)?;
2018-07-16 08:54:05 +00:00
create_directory(&path)?;
create_file(&path.join("index.html"), &self.inject_livereload(single_output))?;
}
Support and default to generating Atom feeds This includes several breaking changes, but they’re easy to adjust for. Atom 1.0 is superior to RSS 2.0 in a number of ways, both technical and legal, though information from the last decade is hard to find. http://www.intertwingly.net/wiki/pie/Rss20AndAtom10Compared has some info which is probably still mostly correct. How do RSS and Atom compare in terms of implementation support? The impression I get is that proper Atom support in normal content websites has been universal for over twelve years, but that support in podcasts was not quite so good, but getting there, over twelve years ago. I have no more recent facts or figures; no one talks about this stuff these days. I remember investigating this stuff back in 2011–2013 and coming to the same conclusion. At that time, I went with Atom on websites and RSS in podcasts. Now I’d just go full Atom and hang any podcast tools that don’t support Atom, because Atom’s semantics truly are much better. In light of all this, I make the bold recommendation to default to Atom. Nonetheless, for compatibility for existing users, and for those that have Opinions, I’ve retained the RSS template, so that you can escape the breaking change easily. I personally prefer to give feeds a basename that doesn’t mention “Atom” or “RSS”, e.g. “feed.xml”. I’ll be doing that myself, as I’ll be using my own template with more Atom features anyway, like author information, taxonomies and making the title field HTML. Some notes about the Atom feed template: - I went with atom.xml rather than something like feed.atom (the .atom file format being registered for this purpose by RFC4287) due to lack of confidence that it’ll be served with the right MIME type. .xml is a safer default. - It might be nice to get Zola’s version number into the <generator> tag. Not for any particularly good reason, y’know. Just picture it: <generator uri="https://www.getzola.org/" version="0.10.0"> Zola </generator> - I’d like to get taxonomies into the feed, but this requires exposing a little more info than is currently exposed. I think it’d require `TaxonomyConfig` to preferably have a new member `permalink` added (which should be equivalent to something like `config.base_url ~ "/" ~ taxonomy.slug ~ "/"`), and for the feed to get all the taxonomies passed into it (`taxonomies: HashMap<String, TaxonomyTerm>`). Then, the template could be like this, inside the entry: {% for taxonomy, terms in page.taxonomies %} {% for term in terms %} <category scheme="{{ taxonomies[taxonomy].permalink }}" term="{{ term.slug }}" label="{{ term.name }}" /> {% endfor %} {% endfor %} Other remarks: - I have added a date field `extra.updated` to my posts and include that in the feed; I’ve observed others with a similar field. I believe this should be included as an official field. I’m inclined to add author to at least config.toml, too, for feeds. - We need to have a link from the docs to the source of the built-in templates, to help people that wish to alter it.
2019-08-11 10:25:24 +00:00
if taxonomy.kind.feed {
self.render_feed(
2019-01-27 17:57:07 +00:00
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(())
2018-07-16 08:54:05 +00:00
}
2017-07-05 10:34:41 +00:00
})
.collect::<Result<()>>()
2017-03-06 14:45:57 +00:00
}
/// What it says on the tin
pub fn render_sitemap(&self) -> Result<()> {
2017-05-16 04:37:00 +00:00
ensure_directory_exists(&self.output_path)?;
2019-03-19 19:42:16 +00:00
let library = self.library.read().unwrap();
let all_sitemap_entries = {
2019-06-02 18:21:06 +00:00
let mut all_sitemap_entries =
sitemap::find_entries(&library, &self.taxonomies[..], &self.config);
all_sitemap_entries.sort();
all_sitemap_entries
};
let sitemap_limit = 30000;
2019-03-19 19:42:16 +00:00
if all_sitemap_entries.len() < sitemap_limit {
// Create single sitemap
let mut context = Context::new();
2019-03-14 20:15:01 +00:00
context.insert("entries", &all_sitemap_entries);
let sitemap = &render_template("sitemap.xml", &self.tera, context, &self.config.theme)?;
create_file(&self.output_path.join("sitemap.xml"), sitemap)?;
return Ok(());
}
2019-03-14 20:15:01 +00:00
// Create multiple sitemaps (max 30000 urls each)
let mut sitemap_index = Vec::new();
for (i, chunk) in
all_sitemap_entries.iter().collect::<Vec<_>>().chunks(sitemap_limit).enumerate()
{
let mut context = Context::new();
2019-03-14 20:15:01 +00:00
context.insert("entries", &chunk);
let sitemap = &render_template("sitemap.xml", &self.tera, context, &self.config.theme)?;
let file_name = format!("sitemap{}.xml", i + 1);
create_file(&self.output_path.join(&file_name), sitemap)?;
let mut sitemap_url: String = self.config.make_permalink(&file_name);
sitemap_url.pop(); // Remove trailing slash
sitemap_index.push(sitemap_url);
}
// Create main sitemap that reference numbered sitemaps
let mut main_context = Context::new();
main_context.insert("sitemaps", &sitemap_index);
let sitemap = &render_template(
"split_sitemap_index.xml",
&self.tera,
main_context,
&self.config.theme,
)?;
2019-03-14 20:15:01 +00:00
create_file(&self.output_path.join("sitemap.xml"), sitemap)?;
Ok(())
}
2017-03-07 07:43:27 +00:00
Support and default to generating Atom feeds This includes several breaking changes, but they’re easy to adjust for. Atom 1.0 is superior to RSS 2.0 in a number of ways, both technical and legal, though information from the last decade is hard to find. http://www.intertwingly.net/wiki/pie/Rss20AndAtom10Compared has some info which is probably still mostly correct. How do RSS and Atom compare in terms of implementation support? The impression I get is that proper Atom support in normal content websites has been universal for over twelve years, but that support in podcasts was not quite so good, but getting there, over twelve years ago. I have no more recent facts or figures; no one talks about this stuff these days. I remember investigating this stuff back in 2011–2013 and coming to the same conclusion. At that time, I went with Atom on websites and RSS in podcasts. Now I’d just go full Atom and hang any podcast tools that don’t support Atom, because Atom’s semantics truly are much better. In light of all this, I make the bold recommendation to default to Atom. Nonetheless, for compatibility for existing users, and for those that have Opinions, I’ve retained the RSS template, so that you can escape the breaking change easily. I personally prefer to give feeds a basename that doesn’t mention “Atom” or “RSS”, e.g. “feed.xml”. I’ll be doing that myself, as I’ll be using my own template with more Atom features anyway, like author information, taxonomies and making the title field HTML. Some notes about the Atom feed template: - I went with atom.xml rather than something like feed.atom (the .atom file format being registered for this purpose by RFC4287) due to lack of confidence that it’ll be served with the right MIME type. .xml is a safer default. - It might be nice to get Zola’s version number into the <generator> tag. Not for any particularly good reason, y’know. Just picture it: <generator uri="https://www.getzola.org/" version="0.10.0"> Zola </generator> - I’d like to get taxonomies into the feed, but this requires exposing a little more info than is currently exposed. I think it’d require `TaxonomyConfig` to preferably have a new member `permalink` added (which should be equivalent to something like `config.base_url ~ "/" ~ taxonomy.slug ~ "/"`), and for the feed to get all the taxonomies passed into it (`taxonomies: HashMap<String, TaxonomyTerm>`). Then, the template could be like this, inside the entry: {% for taxonomy, terms in page.taxonomies %} {% for term in terms %} <category scheme="{{ taxonomies[taxonomy].permalink }}" term="{{ term.slug }}" label="{{ term.name }}" /> {% endfor %} {% endfor %} Other remarks: - I have added a date field `extra.updated` to my posts and include that in the feed; I’ve observed others with a similar field. I believe this should be included as an official field. I’m inclined to add author to at least config.toml, too, for feeds. - We need to have a link from the docs to the source of the built-in templates, to help people that wish to alter it.
2019-08-11 10:25:24 +00:00
/// 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
2018-07-16 08:54:05 +00:00
/// site at the root folder.
Support and default to generating Atom feeds This includes several breaking changes, but they’re easy to adjust for. Atom 1.0 is superior to RSS 2.0 in a number of ways, both technical and legal, though information from the last decade is hard to find. http://www.intertwingly.net/wiki/pie/Rss20AndAtom10Compared has some info which is probably still mostly correct. How do RSS and Atom compare in terms of implementation support? The impression I get is that proper Atom support in normal content websites has been universal for over twelve years, but that support in podcasts was not quite so good, but getting there, over twelve years ago. I have no more recent facts or figures; no one talks about this stuff these days. I remember investigating this stuff back in 2011–2013 and coming to the same conclusion. At that time, I went with Atom on websites and RSS in podcasts. Now I’d just go full Atom and hang any podcast tools that don’t support Atom, because Atom’s semantics truly are much better. In light of all this, I make the bold recommendation to default to Atom. Nonetheless, for compatibility for existing users, and for those that have Opinions, I’ve retained the RSS template, so that you can escape the breaking change easily. I personally prefer to give feeds a basename that doesn’t mention “Atom” or “RSS”, e.g. “feed.xml”. I’ll be doing that myself, as I’ll be using my own template with more Atom features anyway, like author information, taxonomies and making the title field HTML. Some notes about the Atom feed template: - I went with atom.xml rather than something like feed.atom (the .atom file format being registered for this purpose by RFC4287) due to lack of confidence that it’ll be served with the right MIME type. .xml is a safer default. - It might be nice to get Zola’s version number into the <generator> tag. Not for any particularly good reason, y’know. Just picture it: <generator uri="https://www.getzola.org/" version="0.10.0"> Zola </generator> - I’d like to get taxonomies into the feed, but this requires exposing a little more info than is currently exposed. I think it’d require `TaxonomyConfig` to preferably have a new member `permalink` added (which should be equivalent to something like `config.base_url ~ "/" ~ taxonomy.slug ~ "/"`), and for the feed to get all the taxonomies passed into it (`taxonomies: HashMap<String, TaxonomyTerm>`). Then, the template could be like this, inside the entry: {% for taxonomy, terms in page.taxonomies %} {% for term in terms %} <category scheme="{{ taxonomies[taxonomy].permalink }}" term="{{ term.slug }}" label="{{ term.name }}" /> {% endfor %} {% endfor %} Other remarks: - I have added a date field `extra.updated` to my posts and include that in the feed; I’ve observed others with a similar field. I believe this should be included as an official field. I’m inclined to add author to at least config.toml, too, for feeds. - We need to have a link from the docs to the source of the built-in templates, to help people that wish to alter it.
2019-08-11 10:25:24 +00:00
pub fn render_feed(
2018-10-31 07:18:57 +00:00
&self,
all_pages: Vec<&Page>,
base_path: Option<&PathBuf>,
lang: &str,
taxonomy_and_item: Option<(&TaxonomyConfig, &TaxonomyItem)>,
2018-10-31 07:18:57 +00:00
) -> Result<()> {
2017-05-16 04:37:00 +00:00
ensure_directory_exists(&self.output_path)?;
2017-05-08 10:29:37 +00:00
2017-03-07 07:43:27 +00:00
let mut context = Context::new();
2019-07-19 09:10:28 +00:00
let mut pages = all_pages.into_iter().filter(|p| p.meta.date.is_some()).collect::<Vec<_>>();
2017-03-07 07:43:27 +00:00
Support and default to generating Atom feeds This includes several breaking changes, but they’re easy to adjust for. Atom 1.0 is superior to RSS 2.0 in a number of ways, both technical and legal, though information from the last decade is hard to find. http://www.intertwingly.net/wiki/pie/Rss20AndAtom10Compared has some info which is probably still mostly correct. How do RSS and Atom compare in terms of implementation support? The impression I get is that proper Atom support in normal content websites has been universal for over twelve years, but that support in podcasts was not quite so good, but getting there, over twelve years ago. I have no more recent facts or figures; no one talks about this stuff these days. I remember investigating this stuff back in 2011–2013 and coming to the same conclusion. At that time, I went with Atom on websites and RSS in podcasts. Now I’d just go full Atom and hang any podcast tools that don’t support Atom, because Atom’s semantics truly are much better. In light of all this, I make the bold recommendation to default to Atom. Nonetheless, for compatibility for existing users, and for those that have Opinions, I’ve retained the RSS template, so that you can escape the breaking change easily. I personally prefer to give feeds a basename that doesn’t mention “Atom” or “RSS”, e.g. “feed.xml”. I’ll be doing that myself, as I’ll be using my own template with more Atom features anyway, like author information, taxonomies and making the title field HTML. Some notes about the Atom feed template: - I went with atom.xml rather than something like feed.atom (the .atom file format being registered for this purpose by RFC4287) due to lack of confidence that it’ll be served with the right MIME type. .xml is a safer default. - It might be nice to get Zola’s version number into the <generator> tag. Not for any particularly good reason, y’know. Just picture it: <generator uri="https://www.getzola.org/" version="0.10.0"> Zola </generator> - I’d like to get taxonomies into the feed, but this requires exposing a little more info than is currently exposed. I think it’d require `TaxonomyConfig` to preferably have a new member `permalink` added (which should be equivalent to something like `config.base_url ~ "/" ~ taxonomy.slug ~ "/"`), and for the feed to get all the taxonomies passed into it (`taxonomies: HashMap<String, TaxonomyTerm>`). Then, the template could be like this, inside the entry: {% for taxonomy, terms in page.taxonomies %} {% for term in terms %} <category scheme="{{ taxonomies[taxonomy].permalink }}" term="{{ term.slug }}" label="{{ term.name }}" /> {% endfor %} {% endfor %} Other remarks: - I have added a date field `extra.updated` to my posts and include that in the feed; I’ve observed others with a similar field. I believe this should be included as an official field. I’m inclined to add author to at least config.toml, too, for feeds. - We need to have a link from the docs to the source of the built-in templates, to help people that wish to alter it.
2019-08-11 10:25:24 +00:00
// Don't generate a feed if none of the pages has a date
2017-03-07 07:43:27 +00:00
if pages.is_empty() {
return Ok(());
}
2018-10-02 14:42:34 +00:00
pages.par_sort_unstable_by(sort_actual_pages_by_date);
context.insert(
"last_updated",
2020-04-22 08:07:17 +00:00
pages
.iter()
.filter_map(|page| page.meta.updated.as_ref())
.chain(pages[0].meta.date.as_ref())
2020-04-22 08:07:17 +00:00
.max() // I love lexicographically sorted date strings
.unwrap(), // Guaranteed because of pages[0].meta.date
);
2019-01-27 17:57:07 +00:00
let library = self.library.read().unwrap();
// limit to the last n elements if the limit is set; otherwise use all.
Support and default to generating Atom feeds This includes several breaking changes, but they’re easy to adjust for. Atom 1.0 is superior to RSS 2.0 in a number of ways, both technical and legal, though information from the last decade is hard to find. http://www.intertwingly.net/wiki/pie/Rss20AndAtom10Compared has some info which is probably still mostly correct. How do RSS and Atom compare in terms of implementation support? The impression I get is that proper Atom support in normal content websites has been universal for over twelve years, but that support in podcasts was not quite so good, but getting there, over twelve years ago. I have no more recent facts or figures; no one talks about this stuff these days. I remember investigating this stuff back in 2011–2013 and coming to the same conclusion. At that time, I went with Atom on websites and RSS in podcasts. Now I’d just go full Atom and hang any podcast tools that don’t support Atom, because Atom’s semantics truly are much better. In light of all this, I make the bold recommendation to default to Atom. Nonetheless, for compatibility for existing users, and for those that have Opinions, I’ve retained the RSS template, so that you can escape the breaking change easily. I personally prefer to give feeds a basename that doesn’t mention “Atom” or “RSS”, e.g. “feed.xml”. I’ll be doing that myself, as I’ll be using my own template with more Atom features anyway, like author information, taxonomies and making the title field HTML. Some notes about the Atom feed template: - I went with atom.xml rather than something like feed.atom (the .atom file format being registered for this purpose by RFC4287) due to lack of confidence that it’ll be served with the right MIME type. .xml is a safer default. - It might be nice to get Zola’s version number into the <generator> tag. Not for any particularly good reason, y’know. Just picture it: <generator uri="https://www.getzola.org/" version="0.10.0"> Zola </generator> - I’d like to get taxonomies into the feed, but this requires exposing a little more info than is currently exposed. I think it’d require `TaxonomyConfig` to preferably have a new member `permalink` added (which should be equivalent to something like `config.base_url ~ "/" ~ taxonomy.slug ~ "/"`), and for the feed to get all the taxonomies passed into it (`taxonomies: HashMap<String, TaxonomyTerm>`). Then, the template could be like this, inside the entry: {% for taxonomy, terms in page.taxonomies %} {% for term in terms %} <category scheme="{{ taxonomies[taxonomy].permalink }}" term="{{ term.slug }}" label="{{ term.name }}" /> {% endfor %} {% endfor %} Other remarks: - I have added a date field `extra.updated` to my posts and include that in the feed; I’ve observed others with a similar field. I believe this should be included as an official field. I’m inclined to add author to at least config.toml, too, for feeds. - We need to have a link from the docs to the source of the built-in templates, to help people that wish to alter it.
2019-08-11 10:25:24 +00:00
let num_entries = self.config.feed_limit.unwrap_or_else(|| pages.len());
2018-10-02 14:42:34 +00:00
let p = pages
.iter()
.take(num_entries)
2019-01-27 17:57:07 +00:00
.map(|x| x.to_serialized_basic(&library))
2018-10-02 14:42:34 +00:00
.collect::<Vec<_>>();
context.insert("pages", &p);
2018-09-09 17:43:14 +00:00
context.insert("config", &self.config);
context.insert("lang", lang);
2017-03-10 11:39:58 +00:00
Support and default to generating Atom feeds This includes several breaking changes, but they’re easy to adjust for. Atom 1.0 is superior to RSS 2.0 in a number of ways, both technical and legal, though information from the last decade is hard to find. http://www.intertwingly.net/wiki/pie/Rss20AndAtom10Compared has some info which is probably still mostly correct. How do RSS and Atom compare in terms of implementation support? The impression I get is that proper Atom support in normal content websites has been universal for over twelve years, but that support in podcasts was not quite so good, but getting there, over twelve years ago. I have no more recent facts or figures; no one talks about this stuff these days. I remember investigating this stuff back in 2011–2013 and coming to the same conclusion. At that time, I went with Atom on websites and RSS in podcasts. Now I’d just go full Atom and hang any podcast tools that don’t support Atom, because Atom’s semantics truly are much better. In light of all this, I make the bold recommendation to default to Atom. Nonetheless, for compatibility for existing users, and for those that have Opinions, I’ve retained the RSS template, so that you can escape the breaking change easily. I personally prefer to give feeds a basename that doesn’t mention “Atom” or “RSS”, e.g. “feed.xml”. I’ll be doing that myself, as I’ll be using my own template with more Atom features anyway, like author information, taxonomies and making the title field HTML. Some notes about the Atom feed template: - I went with atom.xml rather than something like feed.atom (the .atom file format being registered for this purpose by RFC4287) due to lack of confidence that it’ll be served with the right MIME type. .xml is a safer default. - It might be nice to get Zola’s version number into the <generator> tag. Not for any particularly good reason, y’know. Just picture it: <generator uri="https://www.getzola.org/" version="0.10.0"> Zola </generator> - I’d like to get taxonomies into the feed, but this requires exposing a little more info than is currently exposed. I think it’d require `TaxonomyConfig` to preferably have a new member `permalink` added (which should be equivalent to something like `config.base_url ~ "/" ~ taxonomy.slug ~ "/"`), and for the feed to get all the taxonomies passed into it (`taxonomies: HashMap<String, TaxonomyTerm>`). Then, the template could be like this, inside the entry: {% for taxonomy, terms in page.taxonomies %} {% for term in terms %} <category scheme="{{ taxonomies[taxonomy].permalink }}" term="{{ term.slug }}" label="{{ term.name }}" /> {% endfor %} {% endfor %} Other remarks: - I have added a date field `extra.updated` to my posts and include that in the feed; I’ve observed others with a similar field. I believe this should be included as an official field. I’m inclined to add author to at least config.toml, too, for feeds. - We need to have a link from the docs to the source of the built-in templates, to help people that wish to alter it.
2019-08-11 10:25:24 +00:00
let feed_filename = &self.config.feed_filename;
let feed_url = if let Some(ref base) = base_path {
2020-04-22 08:07:17 +00:00
self.config
.make_permalink(&base.join(feed_filename).to_string_lossy().replace('\\', "/"))
2017-03-10 11:39:58 +00:00
} else {
Support and default to generating Atom feeds This includes several breaking changes, but they’re easy to adjust for. Atom 1.0 is superior to RSS 2.0 in a number of ways, both technical and legal, though information from the last decade is hard to find. http://www.intertwingly.net/wiki/pie/Rss20AndAtom10Compared has some info which is probably still mostly correct. How do RSS and Atom compare in terms of implementation support? The impression I get is that proper Atom support in normal content websites has been universal for over twelve years, but that support in podcasts was not quite so good, but getting there, over twelve years ago. I have no more recent facts or figures; no one talks about this stuff these days. I remember investigating this stuff back in 2011–2013 and coming to the same conclusion. At that time, I went with Atom on websites and RSS in podcasts. Now I’d just go full Atom and hang any podcast tools that don’t support Atom, because Atom’s semantics truly are much better. In light of all this, I make the bold recommendation to default to Atom. Nonetheless, for compatibility for existing users, and for those that have Opinions, I’ve retained the RSS template, so that you can escape the breaking change easily. I personally prefer to give feeds a basename that doesn’t mention “Atom” or “RSS”, e.g. “feed.xml”. I’ll be doing that myself, as I’ll be using my own template with more Atom features anyway, like author information, taxonomies and making the title field HTML. Some notes about the Atom feed template: - I went with atom.xml rather than something like feed.atom (the .atom file format being registered for this purpose by RFC4287) due to lack of confidence that it’ll be served with the right MIME type. .xml is a safer default. - It might be nice to get Zola’s version number into the <generator> tag. Not for any particularly good reason, y’know. Just picture it: <generator uri="https://www.getzola.org/" version="0.10.0"> Zola </generator> - I’d like to get taxonomies into the feed, but this requires exposing a little more info than is currently exposed. I think it’d require `TaxonomyConfig` to preferably have a new member `permalink` added (which should be equivalent to something like `config.base_url ~ "/" ~ taxonomy.slug ~ "/"`), and for the feed to get all the taxonomies passed into it (`taxonomies: HashMap<String, TaxonomyTerm>`). Then, the template could be like this, inside the entry: {% for taxonomy, terms in page.taxonomies %} {% for term in terms %} <category scheme="{{ taxonomies[taxonomy].permalink }}" term="{{ term.slug }}" label="{{ term.name }}" /> {% endfor %} {% endfor %} Other remarks: - I have added a date field `extra.updated` to my posts and include that in the feed; I’ve observed others with a similar field. I believe this should be included as an official field. I’m inclined to add author to at least config.toml, too, for feeds. - We need to have a link from the docs to the source of the built-in templates, to help people that wish to alter it.
2019-08-11 10:25:24 +00:00
self.config.make_permalink(feed_filename)
2017-03-10 11:39:58 +00:00
};
2018-07-16 08:54:05 +00:00
Support and default to generating Atom feeds This includes several breaking changes, but they’re easy to adjust for. Atom 1.0 is superior to RSS 2.0 in a number of ways, both technical and legal, though information from the last decade is hard to find. http://www.intertwingly.net/wiki/pie/Rss20AndAtom10Compared has some info which is probably still mostly correct. How do RSS and Atom compare in terms of implementation support? The impression I get is that proper Atom support in normal content websites has been universal for over twelve years, but that support in podcasts was not quite so good, but getting there, over twelve years ago. I have no more recent facts or figures; no one talks about this stuff these days. I remember investigating this stuff back in 2011–2013 and coming to the same conclusion. At that time, I went with Atom on websites and RSS in podcasts. Now I’d just go full Atom and hang any podcast tools that don’t support Atom, because Atom’s semantics truly are much better. In light of all this, I make the bold recommendation to default to Atom. Nonetheless, for compatibility for existing users, and for those that have Opinions, I’ve retained the RSS template, so that you can escape the breaking change easily. I personally prefer to give feeds a basename that doesn’t mention “Atom” or “RSS”, e.g. “feed.xml”. I’ll be doing that myself, as I’ll be using my own template with more Atom features anyway, like author information, taxonomies and making the title field HTML. Some notes about the Atom feed template: - I went with atom.xml rather than something like feed.atom (the .atom file format being registered for this purpose by RFC4287) due to lack of confidence that it’ll be served with the right MIME type. .xml is a safer default. - It might be nice to get Zola’s version number into the <generator> tag. Not for any particularly good reason, y’know. Just picture it: <generator uri="https://www.getzola.org/" version="0.10.0"> Zola </generator> - I’d like to get taxonomies into the feed, but this requires exposing a little more info than is currently exposed. I think it’d require `TaxonomyConfig` to preferably have a new member `permalink` added (which should be equivalent to something like `config.base_url ~ "/" ~ taxonomy.slug ~ "/"`), and for the feed to get all the taxonomies passed into it (`taxonomies: HashMap<String, TaxonomyTerm>`). Then, the template could be like this, inside the entry: {% for taxonomy, terms in page.taxonomies %} {% for term in terms %} <category scheme="{{ taxonomies[taxonomy].permalink }}" term="{{ term.slug }}" label="{{ term.name }}" /> {% endfor %} {% endfor %} Other remarks: - I have added a date field `extra.updated` to my posts and include that in the feed; I’ve observed others with a similar field. I believe this should be included as an official field. I’m inclined to add author to at least config.toml, too, for feeds. - We need to have a link from the docs to the source of the built-in templates, to help people that wish to alter it.
2019-08-11 10:25:24 +00:00
context.insert("feed_url", &feed_url);
2017-03-07 07:43:27 +00:00
if let Some((taxonomy, item)) = taxonomy_and_item {
context.insert("taxonomy", taxonomy);
context.insert("term", &SerializedTaxonomyItem::from_item(item));
}
Support and default to generating Atom feeds This includes several breaking changes, but they’re easy to adjust for. Atom 1.0 is superior to RSS 2.0 in a number of ways, both technical and legal, though information from the last decade is hard to find. http://www.intertwingly.net/wiki/pie/Rss20AndAtom10Compared has some info which is probably still mostly correct. How do RSS and Atom compare in terms of implementation support? The impression I get is that proper Atom support in normal content websites has been universal for over twelve years, but that support in podcasts was not quite so good, but getting there, over twelve years ago. I have no more recent facts or figures; no one talks about this stuff these days. I remember investigating this stuff back in 2011–2013 and coming to the same conclusion. At that time, I went with Atom on websites and RSS in podcasts. Now I’d just go full Atom and hang any podcast tools that don’t support Atom, because Atom’s semantics truly are much better. In light of all this, I make the bold recommendation to default to Atom. Nonetheless, for compatibility for existing users, and for those that have Opinions, I’ve retained the RSS template, so that you can escape the breaking change easily. I personally prefer to give feeds a basename that doesn’t mention “Atom” or “RSS”, e.g. “feed.xml”. I’ll be doing that myself, as I’ll be using my own template with more Atom features anyway, like author information, taxonomies and making the title field HTML. Some notes about the Atom feed template: - I went with atom.xml rather than something like feed.atom (the .atom file format being registered for this purpose by RFC4287) due to lack of confidence that it’ll be served with the right MIME type. .xml is a safer default. - It might be nice to get Zola’s version number into the <generator> tag. Not for any particularly good reason, y’know. Just picture it: <generator uri="https://www.getzola.org/" version="0.10.0"> Zola </generator> - I’d like to get taxonomies into the feed, but this requires exposing a little more info than is currently exposed. I think it’d require `TaxonomyConfig` to preferably have a new member `permalink` added (which should be equivalent to something like `config.base_url ~ "/" ~ taxonomy.slug ~ "/"`), and for the feed to get all the taxonomies passed into it (`taxonomies: HashMap<String, TaxonomyTerm>`). Then, the template could be like this, inside the entry: {% for taxonomy, terms in page.taxonomies %} {% for term in terms %} <category scheme="{{ taxonomies[taxonomy].permalink }}" term="{{ term.slug }}" label="{{ term.name }}" /> {% endfor %} {% endfor %} Other remarks: - I have added a date field `extra.updated` to my posts and include that in the feed; I’ve observed others with a similar field. I believe this should be included as an official field. I’m inclined to add author to at least config.toml, too, for feeds. - We need to have a link from the docs to the source of the built-in templates, to help people that wish to alter it.
2019-08-11 10:25:24 +00:00
let feed = &render_template(feed_filename, &self.tera, context, &self.config.theme)?;
2017-03-07 07:43:27 +00:00
2018-07-16 08:54:05 +00:00
if let Some(ref base) = base_path {
2018-11-19 14:04:22 +00:00
let mut output_path = self.output_path.clone();
2018-07-16 08:54:05 +00:00
for component in base.components() {
output_path.push(component);
if !output_path.exists() {
create_directory(&output_path)?;
}
}
Support and default to generating Atom feeds This includes several breaking changes, but they’re easy to adjust for. Atom 1.0 is superior to RSS 2.0 in a number of ways, both technical and legal, though information from the last decade is hard to find. http://www.intertwingly.net/wiki/pie/Rss20AndAtom10Compared has some info which is probably still mostly correct. How do RSS and Atom compare in terms of implementation support? The impression I get is that proper Atom support in normal content websites has been universal for over twelve years, but that support in podcasts was not quite so good, but getting there, over twelve years ago. I have no more recent facts or figures; no one talks about this stuff these days. I remember investigating this stuff back in 2011–2013 and coming to the same conclusion. At that time, I went with Atom on websites and RSS in podcasts. Now I’d just go full Atom and hang any podcast tools that don’t support Atom, because Atom’s semantics truly are much better. In light of all this, I make the bold recommendation to default to Atom. Nonetheless, for compatibility for existing users, and for those that have Opinions, I’ve retained the RSS template, so that you can escape the breaking change easily. I personally prefer to give feeds a basename that doesn’t mention “Atom” or “RSS”, e.g. “feed.xml”. I’ll be doing that myself, as I’ll be using my own template with more Atom features anyway, like author information, taxonomies and making the title field HTML. Some notes about the Atom feed template: - I went with atom.xml rather than something like feed.atom (the .atom file format being registered for this purpose by RFC4287) due to lack of confidence that it’ll be served with the right MIME type. .xml is a safer default. - It might be nice to get Zola’s version number into the <generator> tag. Not for any particularly good reason, y’know. Just picture it: <generator uri="https://www.getzola.org/" version="0.10.0"> Zola </generator> - I’d like to get taxonomies into the feed, but this requires exposing a little more info than is currently exposed. I think it’d require `TaxonomyConfig` to preferably have a new member `permalink` added (which should be equivalent to something like `config.base_url ~ "/" ~ taxonomy.slug ~ "/"`), and for the feed to get all the taxonomies passed into it (`taxonomies: HashMap<String, TaxonomyTerm>`). Then, the template could be like this, inside the entry: {% for taxonomy, terms in page.taxonomies %} {% for term in terms %} <category scheme="{{ taxonomies[taxonomy].permalink }}" term="{{ term.slug }}" label="{{ term.name }}" /> {% endfor %} {% endfor %} Other remarks: - I have added a date field `extra.updated` to my posts and include that in the feed; I’ve observed others with a similar field. I believe this should be included as an official field. I’m inclined to add author to at least config.toml, too, for feeds. - We need to have a link from the docs to the source of the built-in templates, to help people that wish to alter it.
2019-08-11 10:25:24 +00:00
create_file(&output_path.join(feed_filename), feed)?;
2018-07-16 08:54:05 +00:00
} else {
Support and default to generating Atom feeds This includes several breaking changes, but they’re easy to adjust for. Atom 1.0 is superior to RSS 2.0 in a number of ways, both technical and legal, though information from the last decade is hard to find. http://www.intertwingly.net/wiki/pie/Rss20AndAtom10Compared has some info which is probably still mostly correct. How do RSS and Atom compare in terms of implementation support? The impression I get is that proper Atom support in normal content websites has been universal for over twelve years, but that support in podcasts was not quite so good, but getting there, over twelve years ago. I have no more recent facts or figures; no one talks about this stuff these days. I remember investigating this stuff back in 2011–2013 and coming to the same conclusion. At that time, I went with Atom on websites and RSS in podcasts. Now I’d just go full Atom and hang any podcast tools that don’t support Atom, because Atom’s semantics truly are much better. In light of all this, I make the bold recommendation to default to Atom. Nonetheless, for compatibility for existing users, and for those that have Opinions, I’ve retained the RSS template, so that you can escape the breaking change easily. I personally prefer to give feeds a basename that doesn’t mention “Atom” or “RSS”, e.g. “feed.xml”. I’ll be doing that myself, as I’ll be using my own template with more Atom features anyway, like author information, taxonomies and making the title field HTML. Some notes about the Atom feed template: - I went with atom.xml rather than something like feed.atom (the .atom file format being registered for this purpose by RFC4287) due to lack of confidence that it’ll be served with the right MIME type. .xml is a safer default. - It might be nice to get Zola’s version number into the <generator> tag. Not for any particularly good reason, y’know. Just picture it: <generator uri="https://www.getzola.org/" version="0.10.0"> Zola </generator> - I’d like to get taxonomies into the feed, but this requires exposing a little more info than is currently exposed. I think it’d require `TaxonomyConfig` to preferably have a new member `permalink` added (which should be equivalent to something like `config.base_url ~ "/" ~ taxonomy.slug ~ "/"`), and for the feed to get all the taxonomies passed into it (`taxonomies: HashMap<String, TaxonomyTerm>`). Then, the template could be like this, inside the entry: {% for taxonomy, terms in page.taxonomies %} {% for term in terms %} <category scheme="{{ taxonomies[taxonomy].permalink }}" term="{{ term.slug }}" label="{{ term.name }}" /> {% endfor %} {% endfor %} Other remarks: - I have added a date field `extra.updated` to my posts and include that in the feed; I’ve observed others with a similar field. I believe this should be included as an official field. I’m inclined to add author to at least config.toml, too, for feeds. - We need to have a link from the docs to the source of the built-in templates, to help people that wish to alter it.
2019-08-11 10:25:24 +00:00
create_file(&self.output_path.join(feed_filename), feed)?;
2018-07-16 08:54:05 +00:00
}
Ok(())
}
/// Renders a single section
pub fn render_section(&self, section: &Section, render_pages: bool) -> Result<()> {
2017-05-16 04:37:00 +00:00
ensure_directory_exists(&self.output_path)?;
2018-11-19 14:04:22 +00:00
let mut output_path = self.output_path.clone();
if section.lang != self.config.default_language {
output_path.push(&section.lang);
if !output_path.exists() {
create_directory(&output_path)?;
}
}
for component in &section.file.components {
output_path.push(component);
if !output_path.exists() {
create_directory(&output_path)?;
}
}
// Copy any asset we found previously into the same directory as the index.html
for asset in &section.assets {
let asset_path = asset.as_path();
2019-01-04 19:34:20 +00:00
copy(
&asset_path,
&output_path.join(
asset_path.file_name().expect("Failed to get asset filename for section"),
),
)?;
}
if render_pages {
2017-06-22 12:37:03 +00:00
section
.pages
.par_iter()
2019-01-27 17:57:07 +00:00
.map(|k| self.render_page(self.library.read().unwrap().get_page_by_key(*k)))
.collect::<Result<()>>()?;
}
2018-03-14 17:22:24 +00:00
if !section.meta.render {
return Ok(());
}
if let Some(ref redirect_to) = section.meta.redirect_to {
let permalink = self.config.make_permalink(redirect_to);
2018-10-31 07:18:57 +00:00
create_file(
&output_path.join("index.html"),
&render_redirect_template(&permalink, &self.tera)?,
)?;
return Ok(());
}
if section.meta.is_paginated() {
self.render_paginated(
&output_path,
&Paginator::from_section(&section, &self.library.read().unwrap()),
)?;
} else {
let output =
section.render_html(&self.tera, &self.config, &self.library.read().unwrap())?;
create_file(&output_path.join("index.html"), &self.inject_livereload(output))?;
2017-05-08 10:29:37 +00:00
}
Ok(())
}
2017-10-24 18:11:39 +00:00
/// Used only on reload
pub fn render_index(&self) -> Result<()> {
2017-10-24 18:11:39 +00:00
self.render_section(
2019-01-04 19:34:20 +00:00
&self
.library
.read()
.unwrap()
2019-01-04 19:34:20 +00:00
.get_section(&self.content_path.join("_index.md"))
.expect("Failed to get index section"),
2018-07-31 13:17:31 +00:00
false,
2017-10-24 18:11:39 +00:00
)
}
/// Renders all sections
pub fn render_sections(&self) -> Result<()> {
2018-10-02 14:42:34 +00:00
self.library
.read()
.unwrap()
2018-10-02 14:42:34 +00:00
.sections_values()
2017-06-22 12:37:03 +00:00
.into_par_iter()
.map(|s| self.render_section(s, true))
.collect::<Result<()>>()
}
2017-05-08 10:29:37 +00:00
/// Renders all pages that do not belong to any sections
pub fn render_orphan_pages(&self) -> Result<()> {
2017-05-16 04:37:00 +00:00
ensure_directory_exists(&self.output_path)?;
2019-01-27 17:57:07 +00:00
let library = self.library.read().unwrap();
for page in library.get_all_orphan_pages() {
2017-06-29 12:14:08 +00:00
self.render_page(page)?;
2017-05-03 08:52:49 +00:00
}
Ok(())
}
/// Renders a list of pages when the section/index is wanting pagination.
2018-07-16 08:54:05 +00:00
pub fn render_paginated(&self, output_path: &Path, paginator: &Paginator) -> Result<()> {
2017-05-16 04:37:00 +00:00
ensure_directory_exists(&self.output_path)?;
2017-05-08 10:29:37 +00:00
2018-07-16 08:54:05 +00:00
let folder_path = output_path.join(&paginator.paginate_path);
2017-06-22 12:37:03 +00:00
create_directory(&folder_path)?;
2017-06-22 12:37:03 +00:00
paginator
.pagers
.par_iter()
.map(|pager| {
let page_path = folder_path.join(&format!("{}", pager.index));
2017-06-22 12:37:03 +00:00
create_directory(&page_path)?;
let output = paginator.render_pager(
pager,
&self.config,
&self.tera,
&self.library.read().unwrap(),
)?;
if pager.index > 1 {
2017-06-22 12:37:03 +00:00
create_file(&page_path.join("index.html"), &self.inject_livereload(output))?;
} else {
create_file(&output_path.join("index.html"), &self.inject_livereload(output))?;
2018-10-31 07:18:57 +00:00
create_file(
&page_path.join("index.html"),
&render_redirect_template(&paginator.permalink, &self.tera)?,
)?;
2017-06-22 12:37:03 +00:00
}
Ok(())
})
.collect::<Result<()>>()
2017-03-07 07:43:27 +00:00
}
}