zola/components/site/src/lib.rs

1094 lines
40 KiB
Rust
Raw Normal View History

pub mod feed;
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;
pub mod tpls;
2019-03-19 19:42:16 +00:00
2019-06-02 18:21:06 +00:00
use std::collections::HashMap;
use std::fs::remove_dir_all;
use std::path::{Path, PathBuf};
2019-01-27 17:57:07 +00:00
use std::sync::{Arc, Mutex, RwLock};
use lazy_static::lazy_static;
use minify_html::{with_friendly_error, Cfg};
2018-10-02 14:42:34 +00:00
use rayon::prelude::*;
2018-10-31 07:18:57 +00:00
use tera::{Context, Tera};
use walkdir::{DirEntry, WalkDir};
2017-07-06 13:19:15 +00:00
use config::{get_config, Config};
use errors::{bail, Error, Result};
2018-10-31 07:18:57 +00:00
use front_matter::InsertAnchor;
use library::{find_taxonomies, Library, Page, Paginator, Section, Taxonomy};
use relative_path::RelativePathBuf;
use templates::render_redirect_template;
use utils::fs::{
copy_directory, copy_file_if_needed, create_directory, create_file, ensure_directory_exists,
};
2018-05-11 11:54:16 +00:00
use utils::net::get_available_port;
use utils::templates::render_template;
2017-07-04 23:27:27 +00:00
lazy_static! {
/// The in-memory rendered map content
pub static ref SITE_CONTENT: Arc<RwLock<HashMap<RelativePathBuf, String>>> = Arc::new(RwLock::new(HashMap::new()));
}
/// 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,
}
#[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,
build_mode: BuildMode,
}
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)?;
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"))?;
2017-08-23 10:17:24 +00:00
}
2020-07-25 08:49:07 +00:00
let tera = tpls::load_tera(path, &config)?;
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 {
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,
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))),
build_mode: BuildMode::Disk,
};
Ok(site)
}
/// 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
/// 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
}
/// 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);
}
/// 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;
}
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("\\", "/");
2017-06-22 03:01:45 +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
// 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();
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
// 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;
}
// skip hidden files and non md files
if !path.is_dir() && (!file_name.ends_with(".md") || file_name.starts_with(".")) {
continue;
}
// 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()
&& path_str.starts_with("_index.")
&& path_str.ends_with(".md")
{
Some(f)
} else {
None
}
}
})
.collect::<Vec<DirEntry>>();
for index_file in index_files {
let section = match Section::from_file(
index_file.path(),
&self.config,
&self.base_path,
) {
Err(_) => continue,
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 {
let page = Page::from_file(path, &self.config, &self.base_path)
.expect("error deserialising page");
// 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)?;
}
}
self.create_default_index_sections()?;
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()?;
tpls::register_early_global_fns(self);
self.populate_sections();
self.render_markdown()?;
tpls::register_tera_global_fns(self);
// 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-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)?;
}
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 {
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(())
}
/// Add a page to the site
/// 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<()> {
self.permalinks.insert(page.file.relative.clone(), page.permalink.clone());
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)?;
}
2019-01-27 17:57:07 +00:00
let mut library = self.library.write().expect("Get lock for add_page");
library.remove_page(&page.file.path);
2019-01-27 17:57:07 +00:00
library.insert_page(page);
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
}
/// Add a section to the site
/// 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<()> {
self.permalinks.insert(section.file.relative.clone(), section.permalink.clone());
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");
library.remove_section(&section.file.path);
2019-01-27 17:57:07 +00:00
library.insert_section(section);
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(&section, true)
}
/// 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
}
/// Minifies html content
fn minify(&self, html: String) -> Result<String> {
let cfg = &Cfg { minify_js: false };
let mut input_bytes = html.as_bytes().to_vec();
match with_friendly_error(&mut input_bytes, cfg) {
Ok(_len) => match std::str::from_utf8(&input_bytes) {
Ok(result) => Ok(result.to_string()),
Err(err) => bail!("Failed to convert bytes to string : {}", err),
},
Err(minify_error) => {
bail!(
"Failed to truncate html at character {}: {} \n {}",
minify_error.position,
minify_error.message,
minify_error.code_context
);
}
}
}
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(())
}
/// 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)?;
let mut site_path = RelativePathBuf::new();
2017-05-08 10:29:37 +00:00
let mut current_path = self.output_path.to_path_buf();
for component in components {
2017-05-01 08:10:22 +00:00
current_path.push(component);
site_path.push(component);
if !current_path.exists() && write_dirs {
2017-05-01 08:10:22 +00:00
create_directory(&current_path)?;
}
2017-05-01 08:10:22 +00:00
}
if write_dirs {
create_directory(&current_path)?;
}
let final_content = if !filename.ends_with("html") || !self.config.minify_html {
content
} else {
match self.minify(content) {
Ok(minified_content) => minified_content,
Err(error) => bail!(error),
}
};
match self.build_mode {
BuildMode::Disk => {
let end_path = current_path.join(filename);
create_file(&end_path, &final_content)?;
}
BuildMode::Memory => {
let site_path =
if filename != "index.html" { site_path.join(filename) } else { site_path };
SITE_CONTENT.write().unwrap().insert(site_path, final_content);
}
}
Ok(current_path)
}
fn copy_asset(&self, src: &Path, dest: &PathBuf) -> Result<()> {
copy_file_if_needed(src, dest, self.config.hard_link_static)
}
/// 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())?;
let content = self.inject_livereload(output);
let components: Vec<&str> = page.path.split('/').collect();
let current_path =
self.write_content(&components, "index.html", content, !page.assets.is_empty())?;
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();
self.copy_asset(
2019-01-04 19:34:20 +00:00
&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(())
}
/// 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<()> {
// Do not clean on `zola serve` otherwise we end up copying assets all the time
if self.build_mode == BuildMode::Disk {
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() {
2020-07-24 21:00:00 +00:00
sass::compile_sass(&theme_path, &self.output_path)?;
}
}
if self.config.compile_sass {
2020-07-24 21:00:00 +00:00
sass::compile_sass(&self.base_path, &self.output_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
};
self.render_feed(pages, None, &self.config.default_language, |c| c)?;
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();
self.render_feed(pages, Some(&PathBuf::from(lang.code.clone())), &lang.code, |c| c)?;
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)?;
// 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 = {};",
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
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",
};
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
}
/// 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<()> {
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)?;
let content = self.inject_livereload(output);
self.write_content(&[], "404.html", content, false)?;
Ok(())
}
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);
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-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 mut components = Vec::new();
if taxonomy.kind.lang != self.config.default_language {
components.push(taxonomy.kind.lang.as_ref());
}
components.push(taxonomy.slug.as_ref());
let list_output =
taxonomy.render_all_terms(&self.tera, &self.config, &self.library.read().unwrap())?;
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| {
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(
comp.clone(),
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)?;
let content = self.inject_livereload(single_output);
self.write_content(&comp, "index.html", content, false)?;
}
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.slug, item.slug))),
if self.config.is_multilingual() && !taxonomy.kind.lang.is_empty() {
&taxonomy.kind.lang
} else {
&self.config.default_language
},
|mut context: Context| {
context.insert("taxonomy", &taxonomy.kind);
context
.insert("term", &feed::SerializedFeedTaxonomyItem::from_item(item));
context
},
)
} 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();
2020-07-25 08:49:07 +00:00
let all_sitemap_entries =
{ sitemap::find_entries(&library, &self.taxonomies[..], &self.config) };
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)?;
self.write_content(&[], "sitemap.xml", sitemap, false)?;
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);
self.write_content(&[], &file_name, sitemap, false)?;
let mut sitemap_url = 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,
)?;
self.write_content(&[], "sitemap.xml", sitemap, false)?;
2019-03-14 20:15:01 +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>,
lang: &str,
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)?
{
Some(v) => v,
None => return Ok(()),
2017-03-10 11:39:58 +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 {
let mut components = Vec::new();
2018-07-16 08:54:05 +00:00
for component in base.components() {
// 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
}
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 {
self.write_content(&[], &feed_filename, feed, false)?;
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();
let mut components: Vec<&str> = Vec::new();
let create_directories = self.build_mode == BuildMode::Disk || !section.assets.is_empty();
if section.lang != self.config.default_language {
components.push(&section.lang);
output_path.push(&section.lang);
if !output_path.exists() && create_directories {
create_directory(&output_path)?;
}
}
for component in &section.file.components {
components.push(component);
output_path.push(component);
if !output_path.exists() && create_directories {
create_directory(&output_path)?;
}
}
if section.meta.generate_feed {
let library = &self.library.read().unwrap();
let pages = section.pages.iter().map(|k| library.get_page_by_key(*k)).collect();
self.render_feed(
pages,
Some(&PathBuf::from(&section.path[1..])),
&section.lang,
|mut context: Context| {
context.insert("section", &section.to_serialized(library));
context
},
)?;
}
// 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();
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"),
),
)?;
}
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);
self.write_content(
&components,
"index.html",
render_redirect_template(&permalink, &self.tera)?,
create_directories,
2018-10-31 07:18:57 +00:00
)?;
return Ok(());
}
if section.meta.is_paginated() {
self.render_paginated(
components,
&Paginator::from_section(&section, &self.library.read().unwrap()),
)?;
} else {
let output =
section.render_html(&self.tera, &self.config, &self.library.read().unwrap())?;
let content = self.inject_livereload(output);
self.write_content(&components, "index.html", content, false)?;
2017-05-08 10:29:37 +00:00
}
Ok(())
}
/// 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.
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
let index_components = components.clone();
2017-06-22 12:37:03 +00:00
paginator
.pagers
.par_iter()
.map(|pager| {
let mut pager_components = index_components.clone();
pager_components.push(&paginator.paginate_path);
let pager_path = format!("{}", pager.index);
pager_components.push(&pager_path);
let output = paginator.render_pager(
pager,
&self.config,
&self.tera,
&self.library.read().unwrap(),
)?;
let content = self.inject_livereload(output);
if pager.index > 1 {
self.write_content(&pager_components, "index.html", content, false)?;
2017-06-22 12:37:03 +00:00
} else {
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
}
2017-06-22 12:37:03 +00:00
Ok(())
})
.collect::<Result<()>>()
2017-03-07 07:43:27 +00:00
}
}