2020-07-25 08:30:55 +00:00
|
|
|
pub mod feed;
|
2020-07-24 21:44:00 +00:00
|
|
|
pub mod link_checking;
|
2020-07-24 21:00:00 +00:00
|
|
|
pub mod sass;
|
2019-11-26 19:36:52 +00:00
|
|
|
pub mod sitemap;
|
2020-07-24 21:44:00 +00:00
|
|
|
pub mod tpls;
|
2019-03-19 19:42:16 +00:00
|
|
|
|
2019-06-02 18:21:06 +00:00
|
|
|
use std::collections::HashMap;
|
2020-08-16 16:39:04 +00:00
|
|
|
use std::fs::remove_dir_all;
|
2017-03-14 12:25:45 +00:00
|
|
|
use std::path::{Path, PathBuf};
|
2019-01-27 17:57:07 +00:00
|
|
|
use std::sync::{Arc, Mutex, RwLock};
|
2017-03-03 08:12:40 +00:00
|
|
|
|
2020-08-16 16:39:04 +00:00
|
|
|
use lazy_static::lazy_static;
|
2018-10-02 14:42:34 +00:00
|
|
|
use rayon::prelude::*;
|
2018-10-31 07:18:57 +00:00
|
|
|
use tera::{Context, Tera};
|
2020-10-30 15:14:07 +00:00
|
|
|
use walkdir::{DirEntry, WalkDir};
|
2017-07-06 13:19:15 +00:00
|
|
|
|
2020-07-25 08:30:55 +00:00
|
|
|
use config::{get_config, Config};
|
2020-07-24 21:16:21 +00:00
|
|
|
use errors::{bail, Error, Result};
|
2018-10-31 07:18:57 +00:00
|
|
|
use front_matter::InsertAnchor;
|
2020-07-25 08:30:55 +00:00
|
|
|
use library::{find_taxonomies, Library, Page, Paginator, Section, Taxonomy};
|
2020-09-21 16:02:37 +00:00
|
|
|
use relative_path::RelativePathBuf;
|
2021-01-02 08:29:28 +00:00
|
|
|
use std::time::Instant;
|
2020-07-24 21:44:00 +00:00
|
|
|
use templates::render_redirect_template;
|
2020-08-16 16:39:04 +00:00
|
|
|
use utils::fs::{
|
|
|
|
copy_directory, copy_file_if_needed, create_directory, create_file, ensure_directory_exists,
|
|
|
|
};
|
2021-01-07 18:04:02 +00:00
|
|
|
use utils::minify;
|
2018-05-11 11:54:16 +00:00
|
|
|
use utils::net::get_available_port;
|
2020-07-24 21:44:00 +00:00
|
|
|
use utils::templates::render_template;
|
2017-07-04 23:27:27 +00:00
|
|
|
|
2020-08-16 16:39:04 +00:00
|
|
|
lazy_static! {
|
|
|
|
/// The in-memory rendered map content
|
2020-09-21 16:02:37 +00:00
|
|
|
pub static ref SITE_CONTENT: Arc<RwLock<HashMap<RelativePathBuf, String>>> = Arc::new(RwLock::new(HashMap::new()));
|
2020-08-16 16:39:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Where are we building the site
|
|
|
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
|
|
|
pub enum BuildMode {
|
|
|
|
/// On the filesystem -> `zola build`, The path is the `output_path`
|
|
|
|
Disk,
|
|
|
|
/// In memory for the content -> `zola serve`
|
|
|
|
Memory,
|
|
|
|
}
|
|
|
|
|
2017-03-03 08:12:40 +00:00
|
|
|
#[derive(Debug)]
|
|
|
|
pub struct Site {
|
2018-10-18 20:50:06 +00:00
|
|
|
/// The base path of the zola site
|
2017-03-14 12:25:45 +00:00
|
|
|
pub base_path: PathBuf,
|
2017-05-16 04:37:00 +00:00
|
|
|
/// The parsed config for the site
|
2017-03-14 12:25:45 +00:00
|
|
|
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>,
|
2017-05-13 13:37:01 +00:00
|
|
|
/// 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
|
2017-04-21 07:21:44 +00:00
|
|
|
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,
|
2020-08-16 16:39:04 +00:00
|
|
|
build_mode: BuildMode,
|
2017-03-03 08:12:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Site {
|
2017-03-14 12:25:45 +00:00
|
|
|
/// Parse a site at the given path. Defaults to the current dir
|
2020-01-21 19:52:24 +00:00
|
|
|
/// Passing in a path is used in tests and when --root argument is passed
|
2020-05-23 09:55:45 +00:00
|
|
|
pub fn new<P: AsRef<Path>, P2: AsRef<Path>>(path: P, config_file: P2) -> Result<Site> {
|
2017-03-14 12:25:45 +00:00
|
|
|
let path = path.as_ref();
|
2020-05-23 09:55:45 +00:00
|
|
|
let config_file = config_file.as_ref();
|
2021-02-04 08:54:12 +00:00
|
|
|
let mut config = get_config(config_file)?;
|
2018-10-09 12:33:43 +00:00
|
|
|
config.load_extra_syntaxes(path)?;
|
2017-03-14 12:25:45 +00:00
|
|
|
|
2017-08-24 23:38:03 +00:00
|
|
|
if let Some(theme) = config.theme.clone() {
|
|
|
|
// Grab data from the extra section of the theme
|
2021-02-04 08:54:12 +00:00
|
|
|
config.merge_with_theme(&path.join("themes").join(&theme).join("theme.toml"), &theme)?;
|
2017-08-23 10:17:24 +00:00
|
|
|
}
|
|
|
|
|
2020-07-25 08:49:07 +00:00
|
|
|
let tera = tpls::load_tera(path, &config)?;
|
2018-09-30 17:05:56 +00:00
|
|
|
|
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);
|
2020-10-03 14:43:02 +00:00
|
|
|
let output_path = path.join(config.output_dir.clone());
|
2018-02-02 20:35:04 +00:00
|
|
|
|
2017-03-20 10:00:00 +00:00
|
|
|
let site = Site {
|
2017-03-14 12:25:45 +00:00
|
|
|
base_path: path.to_path_buf(),
|
2018-03-28 15:01:14 +00:00
|
|
|
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,
|
2020-08-16 16:39:04 +00:00
|
|
|
output_path,
|
2018-02-02 20:35:04 +00:00
|
|
|
content_path,
|
|
|
|
static_path,
|
2018-07-16 08:54:05 +00:00
|
|
|
taxonomies: Vec::new(),
|
2017-04-21 07:21:44 +00:00
|
|
|
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))),
|
2020-08-16 16:39:04 +00:00
|
|
|
build_mode: BuildMode::Disk,
|
2017-03-03 08:12:40 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
Ok(site)
|
|
|
|
}
|
|
|
|
|
2020-08-16 16:39:04 +00:00
|
|
|
/// Enable some `zola serve` related options
|
|
|
|
pub fn enable_serve_mode(&mut self) {
|
|
|
|
SITE_CONTENT.write().unwrap().clear();
|
|
|
|
self.config.enable_serve_mode();
|
|
|
|
self.build_mode = BuildMode::Memory;
|
|
|
|
}
|
|
|
|
|
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
|
2020-07-24 21:44:00 +00:00
|
|
|
/// There are one index section for the default language + 1 per language
|
2018-12-28 11:15:17 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2018-10-19 14:33:11 +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);
|
2017-03-14 12:25:45 +00:00
|
|
|
}
|
|
|
|
|
2020-08-16 16:39:04 +00:00
|
|
|
/// Only used in `zola serve` to re-use the initial websocket port
|
|
|
|
pub fn enable_live_reload_with_port(&mut self, live_reload_port: u16) {
|
|
|
|
self.live_reload = Some(live_reload_port);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Reloads the templates and rebuild the site without re-rendering the Markdown.
|
|
|
|
pub fn reload_templates(&mut self) -> Result<()> {
|
|
|
|
self.tera.full_reload()?;
|
|
|
|
// TODO: be smarter than that, no need to recompile sass for example
|
|
|
|
self.build()
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2017-03-14 12:25:45 +00:00
|
|
|
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
|
2017-03-03 08:12:40 +00:00
|
|
|
/// 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("\\", "/");
|
2017-06-22 03:01:45 +00:00
|
|
|
|
2020-10-30 15:14:07 +00:00
|
|
|
self.library = Arc::new(RwLock::new(Library::new(0, 0, self.config.is_multilingual())));
|
|
|
|
let mut pages_insert_anchors = HashMap::new();
|
2017-06-22 03:01:45 +00:00
|
|
|
|
2020-10-30 15:14:07 +00:00
|
|
|
// not the most elegant loop, but this is necessary to use skip_current_dir
|
|
|
|
// which we can only decide to use after we've deserialised the section
|
|
|
|
// so it's kinda necessecary
|
|
|
|
let mut dir_walker = WalkDir::new(format!("{}/{}", base_path, "content/")).into_iter();
|
2021-01-02 08:29:28 +00:00
|
|
|
let mut allowed_index_filenames: Vec<_> =
|
|
|
|
self.config.languages.iter().map(|l| format!("_index.{}.md", l.code)).collect();
|
2020-12-14 21:51:28 +00:00
|
|
|
allowed_index_filenames.push("_index.md".to_string());
|
|
|
|
|
2020-10-30 15:14:07 +00:00
|
|
|
loop {
|
|
|
|
let entry: DirEntry = match dir_walker.next() {
|
|
|
|
None => break,
|
|
|
|
Some(Err(_)) => continue,
|
|
|
|
Some(Ok(entry)) => entry,
|
|
|
|
};
|
|
|
|
let path = entry.path();
|
|
|
|
let file_name = match path.file_name() {
|
|
|
|
None => continue,
|
|
|
|
Some(name) => name.to_str().unwrap(),
|
|
|
|
};
|
|
|
|
|
|
|
|
// ignore excluded content
|
|
|
|
match &self.config.ignored_content_globset {
|
|
|
|
Some(gs) => {
|
|
|
|
if gs.is_match(path) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
None => (),
|
|
|
|
}
|
2017-06-22 03:01:45 +00:00
|
|
|
|
2020-10-30 15:14:07 +00:00
|
|
|
// we process a section when we encounter the dir
|
|
|
|
// so we can process it before any of the pages
|
|
|
|
// therefore we should skip the actual file to avoid duplication
|
|
|
|
if file_name.starts_with("_index.") {
|
|
|
|
continue;
|
|
|
|
}
|
2019-02-22 20:48:30 +00:00
|
|
|
|
2020-10-30 15:14:07 +00:00
|
|
|
// skip hidden files and non md files
|
2020-11-21 11:38:43 +00:00
|
|
|
if !path.is_dir() && (!file_name.ends_with(".md") || file_name.starts_with('.')) {
|
2019-07-12 20:45:08 +00:00
|
|
|
continue;
|
|
|
|
}
|
2020-10-30 15:14:07 +00:00
|
|
|
|
|
|
|
// is it a section or not?
|
|
|
|
if path.is_dir() {
|
|
|
|
// if we are processing a section we have to collect
|
|
|
|
// index files for all languages and process them simultaniously
|
|
|
|
// before any of the pages
|
|
|
|
let index_files = WalkDir::new(&path)
|
|
|
|
.max_depth(1)
|
|
|
|
.into_iter()
|
|
|
|
.filter_map(|e| match e {
|
|
|
|
Err(_) => None,
|
|
|
|
Ok(f) => {
|
|
|
|
let path_str = f.path().file_name().unwrap().to_str().unwrap();
|
|
|
|
if f.path().is_file()
|
2020-12-14 21:51:28 +00:00
|
|
|
&& allowed_index_filenames.iter().find(|&s| *s == path_str).is_some()
|
2020-10-30 15:14:07 +00:00
|
|
|
{
|
|
|
|
Some(f)
|
|
|
|
} else {
|
2020-12-14 21:51:28 +00:00
|
|
|
// https://github.com/getzola/zola/issues/1244
|
|
|
|
if path_str.starts_with("_index.") {
|
|
|
|
println!("Expected a section filename, got `{}`. Allowed values: `{:?}`", path_str, &allowed_index_filenames);
|
|
|
|
}
|
2020-10-30 15:14:07 +00:00
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.collect::<Vec<DirEntry>>();
|
|
|
|
|
|
|
|
for index_file in index_files {
|
|
|
|
let section = match Section::from_file(
|
|
|
|
index_file.path(),
|
|
|
|
&self.config,
|
|
|
|
&self.base_path,
|
|
|
|
) {
|
2021-02-02 19:48:31 +00:00
|
|
|
Err(e) => {
|
|
|
|
println!("Failed to load section: {:?}", e);
|
|
|
|
continue;
|
|
|
|
}
|
2020-10-30 15:14:07 +00:00
|
|
|
Ok(sec) => sec,
|
|
|
|
};
|
|
|
|
|
|
|
|
// if the section is drafted we can skip the enitre dir
|
|
|
|
if section.meta.draft && !self.include_drafts {
|
|
|
|
dir_walker.skip_current_dir();
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
self.add_section(section, false)?;
|
|
|
|
}
|
|
|
|
} else {
|
2021-02-02 19:48:31 +00:00
|
|
|
let page = match Page::from_file(path, &self.config, &self.base_path) {
|
|
|
|
Err(e) => {
|
|
|
|
println!("Failed to load page: {:?}", e);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
Ok(p) => p,
|
|
|
|
};
|
2020-10-30 15:14:07 +00:00
|
|
|
|
|
|
|
// should we skip drafts?
|
|
|
|
if page.meta.draft && !self.include_drafts {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
pages_insert_anchors.insert(
|
|
|
|
page.file.path.clone(),
|
|
|
|
self.find_parent_section_insert_anchor(&page.file.parent.clone(), &page.lang),
|
|
|
|
);
|
|
|
|
self.add_page(page, false)?;
|
|
|
|
}
|
2019-02-22 20:48:30 +00:00
|
|
|
}
|
2020-10-30 15:14:07 +00:00
|
|
|
self.create_default_index_sections()?;
|
2019-02-22 20:48:30 +00:00
|
|
|
|
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));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-16 09:01:11 +00:00
|
|
|
// taxonomy Tera fns are loaded in `register_early_global_fns`
|
|
|
|
// so we do need to populate it first.
|
|
|
|
self.populate_taxonomies()?;
|
2020-08-16 16:39:04 +00:00
|
|
|
tpls::register_early_global_fns(self);
|
2019-02-22 20:48:30 +00:00
|
|
|
self.populate_sections();
|
|
|
|
self.render_markdown()?;
|
2020-08-16 16:39:04 +00:00
|
|
|
tpls::register_tera_global_fns(self);
|
2019-02-22 20:48:30 +00:00
|
|
|
|
2019-06-06 17:49:31 +00:00
|
|
|
// Needs to be done after rendering markdown as we only get the anchors at that point
|
2020-07-25 08:49:07 +00:00
|
|
|
link_checking::check_internal_links_with_anchors(&self)?;
|
2019-06-06 17:49:31 +00:00
|
|
|
|
2019-07-12 21:09:05 +00:00
|
|
|
if self.config.is_in_check_mode() {
|
2020-07-25 08:49:07 +00:00
|
|
|
link_checking::check_external_links(&self)?;
|
2019-05-27 12:05:07 +00:00
|
|
|
}
|
|
|
|
|
2019-02-22 20:48:30 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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 {
|
2019-01-04 20:57:27 +00:00
|
|
|
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 {
|
2019-01-04 20:57:27 +00:00
|
|
|
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();
|
2020-05-23 09:41:50 +00:00
|
|
|
index_section.path = "/".to_string();
|
2018-12-28 11:15:17 +00:00
|
|
|
}
|
2020-04-12 17:17:29 +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-05-12 07:30:01 +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
|
|
|
})
|
2018-09-12 19:23:23 +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))
|
2018-09-12 19:23:23 +00:00
|
|
|
.collect::<Result<()>>()?;
|
2017-05-03 14:16:09 +00:00
|
|
|
|
2017-03-21 07:57:00 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2017-05-13 13:37:01 +00:00
|
|
|
/// Add a page to the site
|
2020-08-16 16:39:04 +00:00
|
|
|
/// The `render` parameter is used in the serve command with --fast, when rebuilding a page.
|
|
|
|
pub fn add_page(&mut self, mut page: Page, render_md: bool) -> Result<()> {
|
2017-05-15 10:53:39 +00:00
|
|
|
self.permalinks.insert(page.file.relative.clone(), page.permalink.clone());
|
2020-08-16 16:39:04 +00:00
|
|
|
if render_md {
|
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)?;
|
2017-05-13 13:37:01 +00:00
|
|
|
}
|
2020-08-16 16:39:04 +00:00
|
|
|
|
2019-01-27 17:57:07 +00:00
|
|
|
let mut library = self.library.write().expect("Get lock for add_page");
|
2020-08-16 16:39:04 +00:00
|
|
|
library.remove_page(&page.file.path);
|
2019-01-27 17:57:07 +00:00
|
|
|
library.insert_page(page);
|
2017-05-12 11:24:44 +00:00
|
|
|
|
2020-08-16 16:39:04 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Adds a page to the site and render it
|
|
|
|
/// Only used in `zola serve --fast`
|
|
|
|
pub fn add_and_render_page(&mut self, path: &Path) -> Result<()> {
|
|
|
|
let page = Page::from_file(path, &self.config, &self.base_path)?;
|
|
|
|
self.add_page(page, true)?;
|
|
|
|
self.populate_sections();
|
|
|
|
self.populate_taxonomies()?;
|
|
|
|
let library = self.library.read().unwrap();
|
|
|
|
let page = library.get_page(&path).unwrap();
|
|
|
|
self.render_page(&page)
|
2017-03-21 07:57:00 +00:00
|
|
|
}
|
|
|
|
|
2017-05-13 13:37:01 +00:00
|
|
|
/// Add a section to the site
|
2020-08-16 16:39:04 +00:00
|
|
|
/// The `render` parameter is used in the serve command with --fast, when rebuilding a page.
|
|
|
|
pub fn add_section(&mut self, mut section: Section, render_md: bool) -> Result<()> {
|
2017-05-15 10:53:39 +00:00
|
|
|
self.permalinks.insert(section.file.relative.clone(), section.permalink.clone());
|
2020-08-16 16:39:04 +00:00
|
|
|
if render_md {
|
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");
|
2020-08-16 16:39:04 +00:00
|
|
|
library.remove_section(§ion.file.path);
|
2019-01-27 17:57:07 +00:00
|
|
|
library.insert_section(section);
|
2017-05-13 13:37:01 +00:00
|
|
|
|
2020-08-16 16:39:04 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Adds a section to the site and render it
|
|
|
|
/// Only used in `zola serve --fast`
|
|
|
|
pub fn add_and_render_section(&mut self, path: &Path) -> Result<()> {
|
|
|
|
let section = Section::from_file(path, &self.config, &self.base_path)?;
|
|
|
|
self.add_section(section, true)?;
|
|
|
|
self.populate_sections();
|
|
|
|
let library = self.library.read().unwrap();
|
|
|
|
let section = library.get_section(&path).unwrap();
|
|
|
|
self.render_section(§ion, true)
|
2017-04-21 07:21:44 +00:00
|
|
|
}
|
|
|
|
|
2017-05-22 11:28:43 +00:00
|
|
|
/// 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,
|
2019-01-29 18:20:03 +00:00
|
|
|
lang: &str,
|
2018-12-29 10:17:43 +00:00
|
|
|
) -> InsertAnchor {
|
2019-01-29 18:20:03 +00:00
|
|
|
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-05-22 11:28:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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
|
2017-05-12 11:24:44 +00:00
|
|
|
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");
|
2019-01-29 18:20:03 +00:00
|
|
|
library.populate_sections(&self.config);
|
2017-03-03 08:12:40 +00:00
|
|
|
}
|
|
|
|
|
2017-05-16 04:37:00 +00:00
|
|
|
/// Find all the tags and categories if it's asked in the config
|
2018-07-16 16:14:48 +00:00
|
|
|
pub fn populate_taxonomies(&mut self) -> Result<()> {
|
2018-07-16 08:54:05 +00:00
|
|
|
if self.config.taxonomies.is_empty() {
|
2018-07-16 16:14:48 +00:00
|
|
|
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())?;
|
2018-07-16 16:14:48 +00:00
|
|
|
|
|
|
|
Ok(())
|
2017-03-20 03:42:43 +00:00
|
|
|
}
|
|
|
|
|
2017-03-14 12:25:45 +00:00
|
|
|
/// Inject live reload script tag if in live reload mode
|
2020-03-29 10:45:51 +00:00
|
|
|
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={}&mindelay=10"></script>"#, port,);
|
2020-03-29 10:45:51 +00:00
|
|
|
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() {
|
2019-07-10 21:37:19 +00:00
|
|
|
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<()> {
|
2017-04-18 05:07:02 +00:00
|
|
|
if self.output_path.exists() {
|
2017-03-10 11:39:58 +00:00
|
|
|
// Delete current `public` directory so we can start fresh
|
2019-02-09 18:54:46 +00:00
|
|
|
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(())
|
|
|
|
}
|
|
|
|
|
2020-08-16 16:39:04 +00:00
|
|
|
/// Handles whether to write to disk or to memory
|
|
|
|
pub fn write_content(
|
|
|
|
&self,
|
|
|
|
components: &[&str],
|
|
|
|
filename: &str,
|
|
|
|
content: String,
|
|
|
|
create_dirs: bool,
|
|
|
|
) -> Result<PathBuf> {
|
|
|
|
let write_dirs = self.build_mode == BuildMode::Disk || create_dirs;
|
2017-05-16 04:37:00 +00:00
|
|
|
ensure_directory_exists(&self.output_path)?;
|
2017-03-03 08:12:40 +00:00
|
|
|
|
2020-09-21 16:02:37 +00:00
|
|
|
let mut site_path = RelativePathBuf::new();
|
2017-05-08 10:29:37 +00:00
|
|
|
let mut current_path = self.output_path.to_path_buf();
|
2017-03-03 08:12:40 +00:00
|
|
|
|
2020-08-16 16:39:04 +00:00
|
|
|
for component in components {
|
2017-05-01 08:10:22 +00:00
|
|
|
current_path.push(component);
|
2020-09-21 16:02:37 +00:00
|
|
|
site_path.push(component);
|
2017-03-03 08:12:40 +00:00
|
|
|
|
2020-08-16 16:39:04 +00:00
|
|
|
if !current_path.exists() && write_dirs {
|
2017-05-01 08:10:22 +00:00
|
|
|
create_directory(¤t_path)?;
|
2017-03-03 08:12:40 +00:00
|
|
|
}
|
2017-05-01 08:10:22 +00:00
|
|
|
}
|
2017-03-03 08:12:40 +00:00
|
|
|
|
2020-08-16 16:39:04 +00:00
|
|
|
if write_dirs {
|
|
|
|
create_directory(¤t_path)?;
|
|
|
|
}
|
|
|
|
|
2020-08-28 17:39:19 +00:00
|
|
|
let final_content = if !filename.ends_with("html") || !self.config.minify_html {
|
|
|
|
content
|
|
|
|
} else {
|
2021-01-07 18:04:02 +00:00
|
|
|
match minify::html(content) {
|
2020-08-28 17:39:19 +00:00
|
|
|
Ok(minified_content) => minified_content,
|
|
|
|
Err(error) => bail!(error),
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-08-16 16:39:04 +00:00
|
|
|
match self.build_mode {
|
|
|
|
BuildMode::Disk => {
|
|
|
|
let end_path = current_path.join(filename);
|
2020-08-28 17:39:19 +00:00
|
|
|
create_file(&end_path, &final_content)?;
|
2020-08-16 16:39:04 +00:00
|
|
|
}
|
|
|
|
BuildMode::Memory => {
|
2020-09-21 16:02:37 +00:00
|
|
|
let site_path =
|
|
|
|
if filename != "index.html" { site_path.join(filename) } else { site_path };
|
|
|
|
|
2020-09-21 19:43:02 +00:00
|
|
|
SITE_CONTENT.write().unwrap().insert(site_path, final_content);
|
2020-08-16 16:39:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(current_path)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn copy_asset(&self, src: &Path, dest: &PathBuf) -> Result<()> {
|
|
|
|
copy_file_if_needed(src, dest, self.config.hard_link_static)
|
|
|
|
}
|
2017-03-14 12:25:45 +00:00
|
|
|
|
2020-08-16 16:39:04 +00:00
|
|
|
/// Renders a single content page
|
|
|
|
pub fn render_page(&self, page: &Page) -> Result<()> {
|
2019-01-27 17:57:07 +00:00
|
|
|
let output = page.render_html(&self.tera, &self.config, &self.library.read().unwrap())?;
|
2020-08-16 16:39:04 +00:00
|
|
|
let content = self.inject_livereload(output);
|
|
|
|
let components: Vec<&str> = page.path.split('/').collect();
|
2020-09-21 16:02:37 +00:00
|
|
|
let current_path =
|
|
|
|
self.write_content(&components, "index.html", content, !page.assets.is_empty())?;
|
2017-03-14 12:25:45 +00:00
|
|
|
|
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();
|
2020-08-16 16:39:04 +00:00
|
|
|
self.copy_asset(
|
2019-01-04 19:34:20 +00:00
|
|
|
&asset_path,
|
|
|
|
¤t_path
|
|
|
|
.join(asset_path.file_name().expect("Couldn't get filename from page asset")),
|
|
|
|
)?;
|
2017-05-01 08:10:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2017-03-14 12:25:45 +00:00
|
|
|
|
2020-08-16 16:39:04 +00:00
|
|
|
/// Deletes the `public` directory (only for `zola build`) and builds the site
|
2017-03-10 11:39:58 +00:00
|
|
|
pub fn build(&self) -> Result<()> {
|
2020-12-21 07:54:35 +00:00
|
|
|
let mut start = Instant::now();
|
2020-08-16 16:39:04 +00:00
|
|
|
// Do not clean on `zola serve` otherwise we end up copying assets all the time
|
|
|
|
if self.build_mode == BuildMode::Disk {
|
|
|
|
self.clean()?;
|
|
|
|
}
|
2020-12-21 07:54:35 +00:00
|
|
|
start = log_time(start, "Cleaned folder");
|
2019-03-11 19:25:28 +00:00
|
|
|
|
|
|
|
// 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() {
|
2020-07-24 21:00:00 +00:00
|
|
|
sass::compile_sass(&theme_path, &self.output_path)?;
|
2020-12-21 07:54:35 +00:00
|
|
|
start = log_time(start, "Compiled theme Sass");
|
2019-03-11 19:25:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if self.config.compile_sass {
|
2020-07-24 21:00:00 +00:00
|
|
|
sass::compile_sass(&self.base_path, &self.output_path)?;
|
2020-12-21 07:54:35 +00:00
|
|
|
start = log_time(start, "Compiled own Sass");
|
2019-03-11 19:25:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if self.config.build_search_index {
|
|
|
|
self.build_search_index()?;
|
2020-12-21 07:54:35 +00:00
|
|
|
start = log_time(start, "Built search index");
|
2019-03-11 19:25:28 +00:00
|
|
|
}
|
|
|
|
|
2017-06-16 14:09:01 +00:00
|
|
|
// Render aliases first to allow overwriting
|
|
|
|
self.render_aliases()?;
|
2020-12-21 07:54:35 +00:00
|
|
|
start = log_time(start, "Rendered aliases");
|
2017-05-08 10:29:37 +00:00
|
|
|
self.render_sections()?;
|
2020-12-21 07:54:35 +00:00
|
|
|
start = log_time(start, "Rendered sections");
|
2017-05-08 10:29:37 +00:00
|
|
|
self.render_orphan_pages()?;
|
2020-12-21 07:54:35 +00:00
|
|
|
start = log_time(start, "Rendered orphan pages");
|
2017-03-10 11:39:58 +00:00
|
|
|
self.render_sitemap()?;
|
2020-12-21 07:54:35 +00:00
|
|
|
start = log_time(start, "Rendered 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 {
|
2020-04-03 07:36:30 +00:00
|
|
|
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()
|
2019-01-29 18:20:03 +00:00
|
|
|
.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-07-25 08:30:55 +00:00
|
|
|
self.render_feed(pages, None, &self.config.default_language, |c| c)?;
|
2020-12-21 07:54:35 +00:00
|
|
|
start = log_time(start, "Generated feed in default language");
|
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;
|
|
|
|
}
|
2019-02-09 18:54:46 +00:00
|
|
|
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-07-25 08:30:55 +00:00
|
|
|
self.render_feed(pages, Some(&PathBuf::from(lang.code.clone())), &lang.code, |c| c)?;
|
2020-12-21 07:54:35 +00:00
|
|
|
start = log_time(start, "Generated feed in other language");
|
2017-03-12 03:59:28 +00:00
|
|
|
}
|
2019-01-02 21:11:34 +00:00
|
|
|
|
2018-06-26 06:24:57 +00:00
|
|
|
self.render_404()?;
|
2020-12-21 07:54:35 +00:00
|
|
|
start = log_time(start, "Rendered 404");
|
2017-03-20 12:40:03 +00:00
|
|
|
self.render_robots()?;
|
2020-12-21 07:54:35 +00:00
|
|
|
start = log_time(start, "Rendered robots.txt");
|
2018-07-16 08:54:05 +00:00
|
|
|
self.render_taxonomies()?;
|
2020-12-21 07:54:35 +00:00
|
|
|
start = log_time(start, "Rendered taxonomies");
|
2019-05-19 14:03:17 +00:00
|
|
|
// We process images at the end as we might have picked up images to process from markdown
|
|
|
|
// or from templates
|
|
|
|
self.process_images()?;
|
2020-12-21 07:54:35 +00:00
|
|
|
start = log_time(start, "Processed images");
|
2019-06-03 09:29:44 +00:00
|
|
|
// Processed images will be in static so the last step is to copy it
|
|
|
|
self.copy_static_directories()?;
|
2020-12-21 07:54:35 +00:00
|
|
|
log_time(start, "Copied static dir");
|
2017-03-20 12:40:03 +00:00
|
|
|
|
2018-03-15 17:58:32 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn build_search_index(&self) -> Result<()> {
|
2019-07-20 14:09:16 +00:00
|
|
|
ensure_directory_exists(&self.output_path)?;
|
2020-08-16 16:39:04 +00:00
|
|
|
// TODO: add those to the SITE_CONTENT map
|
|
|
|
|
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 = {};",
|
2020-06-29 18:02:05 +00:00
|
|
|
search::build_index(
|
|
|
|
&self.config.default_language,
|
|
|
|
&self.library.read().unwrap(),
|
|
|
|
&self.config
|
|
|
|
)?
|
2018-03-15 17:58:32 +00:00
|
|
|
),
|
|
|
|
)?;
|
|
|
|
|
2019-09-03 14:50:23 +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 = {};",
|
2020-06-29 18:02:05 +00:00
|
|
|
search::build_index(
|
|
|
|
&language.code,
|
|
|
|
&self.library.read().unwrap(),
|
|
|
|
&self.config
|
|
|
|
)?
|
2019-09-03 14:50:23 +00:00
|
|
|
),
|
|
|
|
)?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
2019-06-02 18:21:06 +00:00
|
|
|
fn render_alias(&self, alias: &str, permalink: &str) -> Result<()> {
|
|
|
|
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",
|
|
|
|
};
|
2020-08-16 16:39:04 +00:00
|
|
|
let content = render_redirect_template(&permalink, &self.tera)?;
|
|
|
|
self.write_content(&split, page_name, content, false)?;
|
|
|
|
Ok(())
|
2019-06-02 18:21:06 +00:00
|
|
|
}
|
|
|
|
|
2020-07-24 21:44:00 +00:00
|
|
|
/// Renders all the aliases for each page/section: a magic HTML template that redirects to
|
|
|
|
/// the canonical one
|
2017-06-16 14:09:01 +00:00
|
|
|
pub fn render_aliases(&self) -> Result<()> {
|
2018-10-20 15:19:13 +00:00
|
|
|
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 §ion.meta.aliases {
|
|
|
|
self.render_alias(&alias, §ion.permalink)?;
|
2017-06-16 14:09:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
2017-03-10 11:39:58 +00:00
|
|
|
|
2018-06-26 06:24:57 +00:00
|
|
|
/// Renders 404.html
|
|
|
|
pub fn render_404(&self) -> Result<()> {
|
|
|
|
ensure_directory_exists(&self.output_path)?;
|
2018-07-05 15:43:30 +00:00
|
|
|
let mut context = Context::new();
|
|
|
|
context.insert("config", &self.config);
|
2021-01-09 09:21:47 +00:00
|
|
|
context.insert("lang", &self.config.default_language);
|
2019-01-23 18:20:02 +00:00
|
|
|
let output = render_template("404.html", &self.tera, context, &self.config.theme)?;
|
2020-08-16 16:39:04 +00:00
|
|
|
let content = self.inject_livereload(output);
|
|
|
|
self.write_content(&[], "404.html", content, false)?;
|
|
|
|
Ok(())
|
2018-06-26 06:24:57 +00:00
|
|
|
}
|
|
|
|
|
2017-05-03 08:52:49 +00:00
|
|
|
/// Renders robots.txt
|
2017-05-13 13:37:01 +00:00
|
|
|
pub fn render_robots(&self) -> Result<()> {
|
2017-05-16 04:37:00 +00:00
|
|
|
ensure_directory_exists(&self.output_path)?;
|
2018-10-02 06:15:26 +00:00
|
|
|
let mut context = Context::new();
|
|
|
|
context.insert("config", &self.config);
|
2020-08-16 16:39:04 +00:00
|
|
|
let content = render_template("robots.txt", &self.tera, context, &self.config.theme)?;
|
|
|
|
self.write_content(&[], "robots.txt", content, false)?;
|
|
|
|
Ok(())
|
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-05-13 13:37:01 +00:00
|
|
|
}
|
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)?;
|
2020-08-16 16:39:04 +00:00
|
|
|
|
|
|
|
let mut components = Vec::new();
|
|
|
|
if taxonomy.kind.lang != self.config.default_language {
|
|
|
|
components.push(taxonomy.kind.lang.as_ref());
|
|
|
|
}
|
|
|
|
|
2020-09-21 19:43:02 +00:00
|
|
|
components.push(taxonomy.slug.as_ref());
|
2020-08-16 16:39:04 +00:00
|
|
|
|
2019-02-09 18:54:46 +00:00
|
|
|
let list_output =
|
|
|
|
taxonomy.render_all_terms(&self.tera, &self.config, &self.library.read().unwrap())?;
|
2020-08-16 16:39:04 +00:00
|
|
|
let content = self.inject_livereload(list_output);
|
|
|
|
self.write_content(&components, "index.html", content, false)?;
|
|
|
|
|
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| {
|
2020-08-16 16:39:04 +00:00
|
|
|
let mut comp = components.clone();
|
|
|
|
comp.push(&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(
|
2020-08-16 16:39:04 +00:00
|
|
|
comp.clone(),
|
2019-01-27 17:57:07 +00:00
|
|
|
&Paginator::from_taxonomy(&taxonomy, item, &library),
|
2018-11-29 19:24:45 +00:00
|
|
|
)?;
|
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)?;
|
2020-08-16 16:39:04 +00:00
|
|
|
let content = self.inject_livereload(single_output);
|
|
|
|
self.write_content(&comp, "index.html", content, false)?;
|
2018-11-29 19:24:45 +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
|
|
|
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(),
|
2020-09-21 19:43:02 +00:00
|
|
|
Some(&PathBuf::from(format!("{}/{}", taxonomy.slug, item.slug))),
|
2020-04-03 07:36:30 +00:00
|
|
|
if self.config.is_multilingual() && !taxonomy.kind.lang.is_empty() {
|
|
|
|
&taxonomy.kind.lang
|
|
|
|
} else {
|
|
|
|
&self.config.default_language
|
|
|
|
},
|
2020-07-25 08:30:55 +00:00
|
|
|
|mut context: Context| {
|
|
|
|
context.insert("taxonomy", &taxonomy.kind);
|
|
|
|
context
|
|
|
|
.insert("term", &feed::SerializedFeedTaxonomyItem::from_item(item));
|
|
|
|
context
|
|
|
|
},
|
2018-11-29 19:24:45 +00:00
|
|
|
)
|
|
|
|
} else {
|
|
|
|
Ok(())
|
2018-07-16 08:54:05 +00:00
|
|
|
}
|
2017-07-05 10:34:41 +00:00
|
|
|
})
|
2018-09-12 19:23:23 +00:00
|
|
|
.collect::<Result<()>>()
|
2017-03-06 14:45:57 +00:00
|
|
|
}
|
|
|
|
|
2017-05-13 13:37:01 +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();
|
2020-07-25 08:49:07 +00:00
|
|
|
let all_sitemap_entries =
|
|
|
|
{ sitemap::find_entries(&library, &self.taxonomies[..], &self.config) };
|
2019-03-14 19:57:22 +00:00
|
|
|
let sitemap_limit = 30000;
|
|
|
|
|
2019-03-19 19:42:16 +00:00
|
|
|
if all_sitemap_entries.len() < sitemap_limit {
|
2019-03-14 19:57:22 +00:00
|
|
|
// Create single sitemap
|
|
|
|
let mut context = Context::new();
|
2019-03-14 20:15:01 +00:00
|
|
|
context.insert("entries", &all_sitemap_entries);
|
2020-08-16 16:39:04 +00:00
|
|
|
let sitemap = render_template("sitemap.xml", &self.tera, context, &self.config.theme)?;
|
|
|
|
self.write_content(&[], "sitemap.xml", sitemap, false)?;
|
2019-03-16 09:01:11 +00:00
|
|
|
return Ok(());
|
2019-03-14 19:57:22 +00:00
|
|
|
}
|
2019-03-14 20:15:01 +00:00
|
|
|
|
2019-03-14 19:57:22 +00:00
|
|
|
// Create multiple sitemaps (max 30000 urls each)
|
|
|
|
let mut sitemap_index = Vec::new();
|
2019-03-16 09:01:11 +00:00
|
|
|
for (i, chunk) in
|
|
|
|
all_sitemap_entries.iter().collect::<Vec<_>>().chunks(sitemap_limit).enumerate()
|
|
|
|
{
|
2019-03-14 19:57:22 +00:00
|
|
|
let mut context = Context::new();
|
2019-03-14 20:15:01 +00:00
|
|
|
context.insert("entries", &chunk);
|
2020-08-16 16:39:04 +00:00
|
|
|
let sitemap = render_template("sitemap.xml", &self.tera, context, &self.config.theme)?;
|
2019-03-16 09:01:11 +00:00
|
|
|
let file_name = format!("sitemap{}.xml", i + 1);
|
2020-08-16 16:39:04 +00:00
|
|
|
self.write_content(&[], &file_name, sitemap, false)?;
|
|
|
|
let mut sitemap_url = self.config.make_permalink(&file_name);
|
2019-03-14 19:57:22 +00:00
|
|
|
sitemap_url.pop(); // Remove trailing slash
|
|
|
|
sitemap_index.push(sitemap_url);
|
|
|
|
}
|
2020-07-24 21:44:00 +00:00
|
|
|
|
2019-03-14 19:57:22 +00:00
|
|
|
// Create main sitemap that reference numbered sitemaps
|
|
|
|
let mut main_context = Context::new();
|
|
|
|
main_context.insert("sitemaps", &sitemap_index);
|
2020-08-16 16:39:04 +00:00
|
|
|
let sitemap = render_template(
|
2019-03-16 09:01:11 +00:00
|
|
|
"split_sitemap_index.xml",
|
|
|
|
&self.tera,
|
|
|
|
main_context,
|
|
|
|
&self.config.theme,
|
|
|
|
)?;
|
2020-08-16 16:39:04 +00:00
|
|
|
self.write_content(&[], "sitemap.xml", sitemap, false)?;
|
2019-03-14 20:15:01 +00:00
|
|
|
|
2017-03-03 08:12:40 +00:00
|
|
|
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>,
|
2020-04-03 07:36:30 +00:00
|
|
|
lang: &str,
|
2020-07-25 08:30:55 +00:00
|
|
|
additional_context_fn: impl Fn(Context) -> Context,
|
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
|
|
|
|
2020-07-25 08:49:07 +00:00
|
|
|
let feed = match feed::render_feed(self, all_pages, lang, base_path, additional_context_fn)?
|
|
|
|
{
|
2020-07-25 08:30:55 +00:00
|
|
|
Some(v) => v,
|
|
|
|
None => return Ok(()),
|
2017-03-10 11:39:58 +00:00
|
|
|
};
|
2020-07-25 08:30:55 +00:00
|
|
|
let feed_filename = &self.config.feed_filename;
|
2017-03-07 07:43:27 +00:00
|
|
|
|
2018-07-16 08:54:05 +00:00
|
|
|
if let Some(ref base) = base_path {
|
2020-08-16 16:39:04 +00:00
|
|
|
let mut components = Vec::new();
|
2018-07-16 08:54:05 +00:00
|
|
|
for component in base.components() {
|
2020-08-16 16:39:04 +00:00
|
|
|
// TODO: avoid cloning the paths
|
|
|
|
components.push(component.as_os_str().to_string_lossy().as_ref().to_string());
|
2018-07-16 08:54:05 +00:00
|
|
|
}
|
2020-08-16 16:39:04 +00:00
|
|
|
self.write_content(
|
|
|
|
&components.iter().map(|x| x.as_ref()).collect::<Vec<_>>(),
|
|
|
|
&feed_filename,
|
|
|
|
feed,
|
|
|
|
false,
|
|
|
|
)?;
|
2018-07-16 08:54:05 +00:00
|
|
|
} else {
|
2020-08-16 16:39:04 +00:00
|
|
|
self.write_content(&[], &feed_filename, feed, false)?;
|
2018-07-16 08:54:05 +00:00
|
|
|
}
|
2017-03-14 12:25:45 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2017-05-12 13:32:35 +00:00
|
|
|
/// Renders a single section
|
2017-05-13 13:37:01 +00:00
|
|
|
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();
|
2020-08-16 16:39:04 +00:00
|
|
|
let mut components: Vec<&str> = Vec::new();
|
|
|
|
let create_directories = self.build_mode == BuildMode::Disk || !section.assets.is_empty();
|
2018-12-28 16:30:47 +00:00
|
|
|
|
2019-01-29 18:20:03 +00:00
|
|
|
if section.lang != self.config.default_language {
|
2020-08-16 16:39:04 +00:00
|
|
|
components.push(§ion.lang);
|
2019-01-29 18:20:03 +00:00
|
|
|
output_path.push(§ion.lang);
|
2020-08-16 16:39:04 +00:00
|
|
|
|
|
|
|
if !output_path.exists() && create_directories {
|
2019-01-04 20:57:27 +00:00
|
|
|
create_directory(&output_path)?;
|
|
|
|
}
|
2018-12-28 16:30:47 +00:00
|
|
|
}
|
|
|
|
|
2017-05-15 10:53:39 +00:00
|
|
|
for component in §ion.file.components {
|
2020-08-16 16:39:04 +00:00
|
|
|
components.push(component);
|
2017-05-12 13:32:35 +00:00
|
|
|
output_path.push(component);
|
2017-03-14 12:25:45 +00:00
|
|
|
|
2020-08-16 16:39:04 +00:00
|
|
|
if !output_path.exists() && create_directories {
|
2017-05-12 13:32:35 +00:00
|
|
|
create_directory(&output_path)?;
|
2017-05-11 05:33:23 +00:00
|
|
|
}
|
2017-05-12 13:32:35 +00:00
|
|
|
}
|
2017-05-11 05:33:23 +00:00
|
|
|
|
2020-08-27 18:21:37 +00:00
|
|
|
if section.meta.generate_feed {
|
|
|
|
let library = &self.library.read().unwrap();
|
2020-09-21 16:02:37 +00:00
|
|
|
let pages = section.pages.iter().map(|k| library.get_page_by_key(*k)).collect();
|
2020-08-27 18:21:37 +00:00
|
|
|
self.render_feed(
|
|
|
|
pages,
|
|
|
|
Some(&PathBuf::from(§ion.path[1..])),
|
|
|
|
§ion.lang,
|
|
|
|
|mut context: Context| {
|
|
|
|
context.insert("section", §ion.to_serialized(library));
|
|
|
|
context
|
|
|
|
},
|
|
|
|
)?;
|
|
|
|
}
|
|
|
|
|
2018-08-07 10:14:59 +00:00
|
|
|
// Copy any asset we found previously into the same directory as the index.html
|
|
|
|
for asset in §ion.assets {
|
|
|
|
let asset_path = asset.as_path();
|
2020-08-16 16:39:04 +00:00
|
|
|
self.copy_asset(
|
2019-01-04 19:34:20 +00:00
|
|
|
&asset_path,
|
|
|
|
&output_path.join(
|
|
|
|
asset_path.file_name().expect("Failed to get asset filename for section"),
|
|
|
|
),
|
|
|
|
)?;
|
2018-08-07 10:14:59 +00:00
|
|
|
}
|
|
|
|
|
2017-05-13 13:37:01 +00:00
|
|
|
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)))
|
2018-09-12 19:23:23 +00:00
|
|
|
.collect::<Result<()>>()?;
|
2017-05-12 13:32:35 +00:00
|
|
|
}
|
2017-05-11 05:33:23 +00:00
|
|
|
|
2018-03-14 17:22:24 +00:00
|
|
|
if !section.meta.render {
|
2017-05-12 13:32:35 +00:00
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
2017-07-25 07:56:13 +00:00
|
|
|
if let Some(ref redirect_to) = section.meta.redirect_to {
|
|
|
|
let permalink = self.config.make_permalink(redirect_to);
|
2020-08-16 16:39:04 +00:00
|
|
|
self.write_content(
|
|
|
|
&components,
|
|
|
|
"index.html",
|
|
|
|
render_redirect_template(&permalink, &self.tera)?,
|
|
|
|
create_directories,
|
2018-10-31 07:18:57 +00:00
|
|
|
)?;
|
2020-08-16 16:39:04 +00:00
|
|
|
|
2017-07-25 07:56:13 +00:00
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
2017-05-12 13:32:35 +00:00
|
|
|
if section.meta.is_paginated() {
|
2019-02-09 18:54:46 +00:00
|
|
|
self.render_paginated(
|
2020-08-16 16:39:04 +00:00
|
|
|
components,
|
2019-02-09 18:54:46 +00:00
|
|
|
&Paginator::from_section(§ion, &self.library.read().unwrap()),
|
|
|
|
)?;
|
2017-05-12 13:32:35 +00:00
|
|
|
} else {
|
2019-02-09 18:54:46 +00:00
|
|
|
let output =
|
|
|
|
section.render_html(&self.tera, &self.config, &self.library.read().unwrap())?;
|
2020-08-16 16:39:04 +00:00
|
|
|
let content = self.inject_livereload(output);
|
|
|
|
self.write_content(&components, "index.html", content, false)?;
|
2017-05-08 10:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2017-05-12 13:32:35 +00:00
|
|
|
/// Renders all sections
|
2017-05-13 13:37:01 +00:00
|
|
|
pub fn render_sections(&self) -> Result<()> {
|
2018-10-02 14:42:34 +00:00
|
|
|
self.library
|
2019-02-09 18:54:46 +00:00
|
|
|
.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))
|
2018-09-12 19:23:23 +00:00
|
|
|
.collect::<Result<()>>()
|
2017-05-12 13:32:35 +00:00
|
|
|
}
|
|
|
|
|
2017-05-08 10:29:37 +00:00
|
|
|
/// Renders all pages that do not belong to any sections
|
2017-05-13 13:37:01 +00:00
|
|
|
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.
|
2020-08-16 16:39:04 +00:00
|
|
|
pub fn render_paginated<'a>(
|
|
|
|
&self,
|
|
|
|
components: Vec<&'a str>,
|
|
|
|
paginator: &'a Paginator,
|
|
|
|
) -> Result<()> {
|
2017-05-16 04:37:00 +00:00
|
|
|
ensure_directory_exists(&self.output_path)?;
|
2017-05-08 10:29:37 +00:00
|
|
|
|
2020-08-16 16:39:04 +00:00
|
|
|
let index_components = components.clone();
|
2017-07-06 09:51:36 +00:00
|
|
|
|
2017-06-22 12:37:03 +00:00
|
|
|
paginator
|
|
|
|
.pagers
|
|
|
|
.par_iter()
|
2018-09-18 17:18:50 +00:00
|
|
|
.map(|pager| {
|
2020-08-16 16:39:04 +00:00
|
|
|
let mut pager_components = index_components.clone();
|
|
|
|
pager_components.push(&paginator.paginate_path);
|
|
|
|
let pager_path = format!("{}", pager.index);
|
|
|
|
pager_components.push(&pager_path);
|
2019-02-09 18:54:46 +00:00
|
|
|
let output = paginator.render_pager(
|
|
|
|
pager,
|
|
|
|
&self.config,
|
|
|
|
&self.tera,
|
|
|
|
&self.library.read().unwrap(),
|
|
|
|
)?;
|
2020-08-16 16:39:04 +00:00
|
|
|
let content = self.inject_livereload(output);
|
|
|
|
|
2018-09-18 17:18:50 +00:00
|
|
|
if pager.index > 1 {
|
2020-08-16 16:39:04 +00:00
|
|
|
self.write_content(&pager_components, "index.html", content, false)?;
|
2017-06-22 12:37:03 +00:00
|
|
|
} else {
|
2020-08-16 16:39:04 +00:00
|
|
|
self.write_content(&index_components, "index.html", content, false)?;
|
|
|
|
self.write_content(
|
|
|
|
&pager_components,
|
|
|
|
"index.html",
|
|
|
|
render_redirect_template(&paginator.permalink, &self.tera)?,
|
|
|
|
false,
|
2018-10-31 07:18:57 +00:00
|
|
|
)?;
|
2017-06-22 12:37:03 +00:00
|
|
|
}
|
2020-08-16 16:39:04 +00:00
|
|
|
|
2017-06-22 12:37:03 +00:00
|
|
|
Ok(())
|
|
|
|
})
|
2018-09-12 19:23:23 +00:00
|
|
|
.collect::<Result<()>>()
|
2017-03-07 07:43:27 +00:00
|
|
|
}
|
2017-03-03 08:12:40 +00:00
|
|
|
}
|
2020-12-21 07:54:35 +00:00
|
|
|
|
|
|
|
fn log_time(start: Instant, message: &str) -> Instant {
|
|
|
|
let do_print = std::env::var("ZOLA_PERF_LOG").is_ok();
|
|
|
|
let now = Instant::now();
|
|
|
|
if do_print {
|
|
|
|
println!("{} took {}ms", message, now.duration_since(start).as_millis());
|
|
|
|
}
|
|
|
|
now
|
2021-01-02 08:29:28 +00:00
|
|
|
}
|