@@ -25,3 +25,5 @@ stage | |||
# nixos dependencies snippet | |||
shell.nix | |||
# vim temporary files | |||
**/.*.sw* |
@@ -1,5 +1,27 @@ | |||
# Changelog | |||
## 0.10.0 (unreleased) | |||
### Breaking | |||
- Remove `toc` variable in section/page context and pass it to `page.toc` and `section.toc` instead so they are | |||
accessible everywhere | |||
- [Slugification](https://en.wikipedia.org/wiki/Slug_(web_publishing)#Slug) of paths, taxonomies and anchors is now configurable. By default, everything will still be slugified like in previous versions. | |||
See documentation for information on how to disable it. | |||
### Other | |||
- Add zenburn syntax highlighting theme | |||
- Fix `zola init .` | |||
- Add `total_pages` to paginator | |||
- Do not prepend URL prefix to links that start with a scheme | |||
- Allow skipping anchor checking in `zola check` for some URL prefixes | |||
- Allow skipping prefixes in `zola check` | |||
- Check for path collisions when building the site | |||
- Fix bug in template extension with themes | |||
- Use Rustls instead of openssl | |||
- The continue reading HTML element is now a <span> instead of a <p> | |||
- Update livereload.js | |||
- Add --root global argument | |||
## 0.9.0 (2019-09-28) | |||
### Breaking | |||
@@ -1,7 +1,8 @@ | |||
[package] | |||
name = "zola" | |||
version = "0.9.0" | |||
version = "0.10.0" | |||
authors = ["Vincent Prouillet <hello@vincentprouillet.com>"] | |||
edition = "2018" | |||
license = "MIT" | |||
readme = "README.md" | |||
description = "A fast static site generator with everything built-in" | |||
@@ -25,8 +26,9 @@ termcolor = "1.0.4" | |||
# Used in init to ensure the url given as base_url is a valid one | |||
url = "2" | |||
# Below is for the serve cmd | |||
actix-files = "0.1" | |||
actix-web = { version = "1.0", default-features = false, features = [] } | |||
hyper = { version = "0.13", default-features = false, features = ["runtime"] } | |||
hyper-staticfile = "0.5" | |||
tokio = { version = "0.2", default-features = false, features = [] } | |||
notify = "4" | |||
ws = "0.9" | |||
ctrlc = "3" | |||
@@ -34,4 +34,3 @@ | |||
| [peterlyons.com](https://peterlyons.com) | https://github.com/focusaurus/peterlyons.com-zola | | |||
| [blog.turbo.fish](https://blog.turbo.fish) | https://git.sr.ht/~jplatte/blog.turbo.fish | | |||
| [guerinpe.com](https://guerinpe.com) | https://github.com/Grelot/blog | | |||
@@ -19,9 +19,9 @@ stages: | |||
linux-stable: | |||
imageName: 'ubuntu-16.04' | |||
rustup_toolchain: stable | |||
linux-1.35: | |||
linux-1.39: | |||
imageName: 'ubuntu-16.04' | |||
rustup_toolchain: 1.35.0 | |||
rustup_toolchain: 1.39.0 | |||
pool: | |||
vmImage: $(imageName) | |||
steps: | |||
@@ -32,7 +32,7 @@ stages: | |||
condition: ne( variables['Agent.OS'], 'Windows_NT' ) | |||
- script: | | |||
curl -sSf -o rustup-init.exe https://win.rustup.rs | |||
rustup-init.exe -y --default-toolchain %RUSTUP_TOOLCHAIN% | |||
rustup-init.exe -y --default-toolchain %RUSTUP_TOOLCHAIN% --default-host x86_64-pc-windows-msvc | |||
echo "##vso[task.setvariable variable=PATH;]%PATH%;%USERPROFILE%\.cargo\bin" | |||
displayName: Windows install rust | |||
condition: eq( variables['Agent.OS'], 'Windows_NT' ) | |||
@@ -70,8 +70,10 @@ stages: | |||
displayName: Install rust | |||
condition: ne( variables['Agent.OS'], 'Windows_NT' ) | |||
- script: | | |||
set CARGO_HOME=%USERPROFILE%\.cargo | |||
curl -sSf -o rustup-init.exe https://win.rustup.rs | |||
rustup-init.exe -y --default-toolchain %RUSTUP_TOOLCHAIN% | |||
rustup-init.exe -y --default-toolchain %RUSTUP_TOOLCHAIN% --default-host x86_64-pc-windows-msvc | |||
set PATH=%PATH%;%USERPROFILE%\.cargo\bin | |||
echo "##vso[task.setvariable variable=PATH;]%PATH%;%USERPROFILE%\.cargo\bin" | |||
displayName: Windows install rust | |||
condition: eq( variables['Agent.OS'], 'Windows_NT' ) | |||
@@ -1,6 +1,3 @@ | |||
#[macro_use] | |||
extern crate clap; | |||
// use clap::Shell; | |||
include!("src/cli.rs"); | |||
@@ -2,6 +2,7 @@ | |||
name = "config" | |||
version = "0.1.0" | |||
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"] | |||
edition = "2018" | |||
[dependencies] | |||
toml = "0.5" | |||
@@ -2,8 +2,11 @@ | |||
//! syntect, not as a helpful example for beginners. | |||
//! Although it is a valid example for serializing syntaxes, you probably won't need | |||
//! to do this yourself unless you want to cache your own compiled grammars. | |||
extern crate syntect; | |||
use std::collections::HashMap; | |||
use std::collections::HashSet; | |||
use std::env; | |||
use std::iter::FromIterator; | |||
use syntect::dumps::*; | |||
use syntect::highlighting::ThemeSet; | |||
use syntect::parsing::SyntaxSetBuilder; | |||
@@ -26,10 +29,25 @@ fn main() { | |||
builder.add_from_folder(package_dir, true).unwrap(); | |||
let ss = builder.build(); | |||
dump_to_file(&ss, packpath_newlines).unwrap(); | |||
let mut syntaxes: HashMap<String, HashSet<String>> = HashMap::new(); | |||
for s in ss.syntaxes() { | |||
if !s.file_extensions.is_empty() { | |||
println!("- {} -> {:?}", s.name, s.file_extensions); | |||
for s in ss.syntaxes().iter() { | |||
syntaxes | |||
.entry(s.name.clone()) | |||
.and_modify(|e| { | |||
for ext in &s.file_extensions { | |||
e.insert(ext.clone()); | |||
} | |||
}) | |||
.or_insert_with(|| HashSet::from_iter(s.file_extensions.iter().cloned())); | |||
} | |||
let mut keys = syntaxes.keys().collect::<Vec<_>>(); | |||
keys.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase())); | |||
for k in keys { | |||
if !syntaxes[k].is_empty() { | |||
let mut extensions_sorted = syntaxes[k].iter().cloned().collect::<Vec<_>>(); | |||
extensions_sorted.sort(); | |||
println!("- {} -> {:?}", k, extensions_sorted); | |||
} | |||
} | |||
} | |||
@@ -3,15 +3,16 @@ use std::path::{Path, PathBuf}; | |||
use chrono::Utc; | |||
use globset::{Glob, GlobSet, GlobSetBuilder}; | |||
use serde_derive::{Deserialize, Serialize}; | |||
use syntect::parsing::{SyntaxSet, SyntaxSetBuilder}; | |||
use toml; | |||
use toml::Value as Toml; | |||
use errors::Result; | |||
use errors::Error; | |||
use highlighting::THEME_SET; | |||
use theme::Theme; | |||
use crate::highlighting::THEME_SET; | |||
use crate::theme::Theme; | |||
use errors::{bail, Error, Result}; | |||
use utils::fs::read_file_with_error; | |||
use utils::slugs::SlugifyStrategy; | |||
// We want a default base url for tests | |||
static DEFAULT_BASE_URL: &str = "http://a-website.com"; | |||
@@ -23,6 +24,24 @@ pub enum Mode { | |||
Check, | |||
} | |||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |||
#[serde(default)] | |||
pub struct Slugify { | |||
pub paths: SlugifyStrategy, | |||
pub taxonomies: SlugifyStrategy, | |||
pub anchors: SlugifyStrategy, | |||
} | |||
impl Default for Slugify { | |||
fn default() -> Self { | |||
Slugify { | |||
paths: SlugifyStrategy::On, | |||
taxonomies: SlugifyStrategy::On, | |||
anchors: SlugifyStrategy::On, | |||
} | |||
} | |||
} | |||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |||
#[serde(default)] | |||
pub struct Language { | |||
@@ -35,7 +54,7 @@ pub struct Language { | |||
} | |||
impl Default for Language { | |||
fn default() -> Language { | |||
fn default() -> Self { | |||
Language { code: String::new(), rss: false, search: false } | |||
} | |||
} | |||
@@ -75,7 +94,7 @@ impl Taxonomy { | |||
} | |||
impl Default for Taxonomy { | |||
fn default() -> Taxonomy { | |||
fn default() -> Self { | |||
Taxonomy { | |||
name: String::new(), | |||
paginate_by: None, | |||
@@ -86,7 +105,22 @@ impl Default for Taxonomy { | |||
} | |||
} | |||
type TranslateTerm = HashMap<String, String>; | |||
type TranslateTerm = HashMap<String, String>; | |||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |||
#[serde(default)] | |||
pub struct LinkChecker { | |||
/// Skip link checking for these URL prefixes | |||
pub skip_prefixes: Vec<String>, | |||
/// Skip anchor checking for these URL prefixes | |||
pub skip_anchor_prefixes: Vec<String>, | |||
} | |||
impl Default for LinkChecker { | |||
fn default() -> LinkChecker { | |||
LinkChecker { skip_prefixes: Vec::new(), skip_anchor_prefixes: Vec::new() } | |||
} | |||
} | |||
#[derive(Clone, Debug, Serialize, Deserialize)] | |||
#[serde(default)] | |||
@@ -152,6 +186,11 @@ pub struct Config { | |||
#[serde(skip_serializing, skip_deserializing)] // not a typo, 2 are need | |||
pub extra_syntax_set: Option<SyntaxSet>, | |||
pub link_checker: LinkChecker, | |||
/// The setup for which slugification strategies to use for paths, taxonomies and anchors | |||
pub slugify: Slugify, | |||
/// All user params set in [extra] in the config | |||
pub extra: HashMap<String, Toml>, | |||
@@ -317,9 +356,16 @@ impl Config { | |||
Error::msg(format!("Translation for language '{}' is missing", lang.as_ref())) | |||
})?; | |||
terms.get(key.as_ref()).ok_or_else(|| { | |||
Error::msg(format!("Translation key '{}' for language '{}' is missing", key.as_ref(), lang.as_ref())) | |||
}).map(|term| term.to_string()) | |||
terms | |||
.get(key.as_ref()) | |||
.ok_or_else(|| { | |||
Error::msg(format!( | |||
"Translation key '{}' for language '{}' is missing", | |||
key.as_ref(), | |||
lang.as_ref() | |||
)) | |||
}) | |||
.map(|term| term.to_string()) | |||
} | |||
} | |||
@@ -346,6 +392,8 @@ impl Default for Config { | |||
translations: HashMap::new(), | |||
extra_syntaxes: Vec::new(), | |||
extra_syntax_set: None, | |||
link_checker: LinkChecker::default(), | |||
slugify: Slugify::default(), | |||
extra: HashMap::new(), | |||
build_timestamp: Some(1), | |||
} | |||
@@ -354,7 +402,7 @@ impl Default for Config { | |||
#[cfg(test)] | |||
mod tests { | |||
use super::{Config, Theme}; | |||
use super::{Config, SlugifyStrategy, Theme}; | |||
#[test] | |||
fn can_import_valid_config() { | |||
@@ -551,4 +599,62 @@ ignored_content = ["*.{graphml,iso}", "*.py?"] | |||
assert!(g.is_match("foo.py3")); | |||
assert!(!g.is_match("foo.py")); | |||
} | |||
#[test] | |||
fn link_checker_skip_anchor_prefixes() { | |||
let config_str = r#" | |||
title = "My site" | |||
base_url = "example.com" | |||
[link_checker] | |||
skip_anchor_prefixes = [ | |||
"https://caniuse.com/#feat=", | |||
"https://github.com/rust-lang/rust/blob/", | |||
] | |||
"#; | |||
let config = Config::parse(config_str).unwrap(); | |||
assert_eq!( | |||
config.link_checker.skip_anchor_prefixes, | |||
vec!["https://caniuse.com/#feat=", "https://github.com/rust-lang/rust/blob/"] | |||
); | |||
} | |||
#[test] | |||
fn link_checker_skip_prefixes() { | |||
let config_str = r#" | |||
title = "My site" | |||
base_url = "example.com" | |||
[link_checker] | |||
skip_prefixes = [ | |||
"http://[2001:db8::]/", | |||
"https://www.example.com/path", | |||
] | |||
"#; | |||
let config = Config::parse(config_str).unwrap(); | |||
assert_eq!( | |||
config.link_checker.skip_prefixes, | |||
vec!["http://[2001:db8::]/", "https://www.example.com/path",] | |||
); | |||
} | |||
#[test] | |||
fn slugify_strategies() { | |||
let config_str = r#" | |||
title = "My site" | |||
base_url = "example.com" | |||
[slugify] | |||
paths = "on" | |||
taxonomies = "safe" | |||
anchors = "off" | |||
"#; | |||
let config = Config::parse(config_str).unwrap(); | |||
assert_eq!(config.slugify.paths, SlugifyStrategy::On); | |||
assert_eq!(config.slugify.taxonomies, SlugifyStrategy::Safe); | |||
assert_eq!(config.slugify.anchors, SlugifyStrategy::Off); | |||
} | |||
} |
@@ -1,9 +1,10 @@ | |||
use lazy_static::lazy_static; | |||
use syntect::dumps::from_binary; | |||
use syntect::easy::HighlightLines; | |||
use syntect::highlighting::ThemeSet; | |||
use syntect::parsing::SyntaxSet; | |||
use Config; | |||
use crate::config::Config; | |||
lazy_static! { | |||
pub static ref SYNTAX_SET: SyntaxSet = { | |||
@@ -1,20 +1,7 @@ | |||
#[macro_use] | |||
extern crate serde_derive; | |||
extern crate chrono; | |||
extern crate globset; | |||
extern crate toml; | |||
#[macro_use] | |||
extern crate lazy_static; | |||
extern crate syntect; | |||
#[macro_use] | |||
extern crate errors; | |||
extern crate utils; | |||
mod config; | |||
pub mod highlighting; | |||
mod theme; | |||
pub use config::{Config, Language, Taxonomy}; | |||
pub use crate::config::{Config, Language, LinkChecker, Taxonomy}; | |||
use std::path::Path; | |||
@@ -1,9 +1,10 @@ | |||
use std::collections::HashMap; | |||
use std::path::PathBuf; | |||
use serde_derive::{Deserialize, Serialize}; | |||
use toml::Value as Toml; | |||
use errors::Result; | |||
use errors::{bail, Result}; | |||
use utils::fs::read_file_with_error; | |||
/// Holds the data from a `theme.toml` file. | |||
@@ -2,9 +2,10 @@ | |||
name = "errors" | |||
version = "0.1.0" | |||
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"] | |||
edition = "2018" | |||
[dependencies] | |||
tera = "1.0.0-beta.10" | |||
tera = "1" | |||
toml = "0.5" | |||
image = "0.22" | |||
image = "0.23" | |||
syntect = "=3.2.0" |
@@ -1,8 +1,3 @@ | |||
extern crate image; | |||
extern crate syntect; | |||
extern crate tera; | |||
extern crate toml; | |||
use std::convert::Into; | |||
use std::error::Error as StdError; | |||
use std::fmt; | |||
@@ -63,6 +58,18 @@ impl Error { | |||
pub fn chain(value: impl ToString, source: impl Into<Box<dyn StdError>>) -> Self { | |||
Self { kind: ErrorKind::Msg(value.to_string()), source: Some(source.into()) } | |||
} | |||
/// Create an error from a list of path collisions, formatting the output | |||
pub fn from_collisions(collisions: Vec<(&str, Vec<String>)>) -> Self { | |||
let mut msg = String::from("Found path collisions:\n"); | |||
for (path, filepaths) in collisions { | |||
let row = format!("- `{}` from files {:?}\n", path, filepaths); | |||
msg.push_str(&row); | |||
} | |||
Self { kind: ErrorKind::Msg(msg), source: None } | |||
} | |||
} | |||
impl From<&str> for Error { | |||
@@ -2,9 +2,10 @@ | |||
name = "front_matter" | |||
version = "0.1.0" | |||
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"] | |||
edition = "2018" | |||
[dependencies] | |||
tera = "1.0.0-beta.10" | |||
tera = "1" | |||
chrono = "0.4" | |||
serde = "1" | |||
serde_derive = "1" | |||
@@ -1,18 +1,7 @@ | |||
#[macro_use] | |||
extern crate lazy_static; | |||
#[macro_use] | |||
extern crate serde_derive; | |||
extern crate chrono; | |||
extern crate regex; | |||
extern crate serde; | |||
extern crate tera; | |||
extern crate toml; | |||
#[macro_use] | |||
extern crate errors; | |||
extern crate utils; | |||
use errors::{Error, Result}; | |||
use lazy_static::lazy_static; | |||
use serde_derive::{Deserialize, Serialize}; | |||
use errors::{bail, Error, Result}; | |||
use regex::Regex; | |||
use std::path::Path; | |||
@@ -1,10 +1,11 @@ | |||
use std::collections::HashMap; | |||
use chrono::prelude::*; | |||
use serde_derive::Deserialize; | |||
use tera::{Map, Value}; | |||
use toml; | |||
use errors::Result; | |||
use errors::{bail, Result}; | |||
use utils::de::{fix_toml_dates, from_toml_datetime}; | |||
/// The front matter of every page | |||
@@ -87,11 +88,9 @@ impl PageFrontMatter { | |||
pub fn date_to_datetime(&mut self) { | |||
self.datetime = if let Some(ref d) = self.date { | |||
if d.contains('T') { | |||
DateTime::parse_from_rfc3339(&d).ok().and_then(|s| Some(s.naive_local())) | |||
DateTime::parse_from_rfc3339(&d).ok().map(|s| s.naive_local()) | |||
} else { | |||
NaiveDate::parse_from_str(&d, "%Y-%m-%d") | |||
.ok() | |||
.and_then(|s| Some(s.and_hms(0, 0, 0))) | |||
NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok().map(|s| s.and_hms(0, 0, 0)) | |||
} | |||
} else { | |||
None | |||
@@ -1,11 +1,10 @@ | |||
use std::collections::HashMap; | |||
use tera::Value; | |||
use serde_derive::{Deserialize, Serialize}; | |||
use tera::{Map, Value}; | |||
use toml; | |||
use errors::Result; | |||
use super::{InsertAnchor, SortBy}; | |||
use errors::{bail, Result}; | |||
use utils::de::fix_toml_dates; | |||
static DEFAULT_PAGINATE_PATH: &str = "page"; | |||
@@ -63,16 +62,21 @@ pub struct SectionFrontMatter { | |||
#[serde(skip_serializing)] | |||
pub aliases: Vec<String>, | |||
/// Any extra parameter present in the front matter | |||
pub extra: HashMap<String, Value>, | |||
pub extra: Map<String, Value>, | |||
} | |||
impl SectionFrontMatter { | |||
pub fn parse(toml: &str) -> Result<SectionFrontMatter> { | |||
let f: SectionFrontMatter = match toml::from_str(toml) { | |||
let mut f: SectionFrontMatter = match toml::from_str(toml) { | |||
Ok(d) => d, | |||
Err(e) => bail!(e), | |||
}; | |||
f.extra = match fix_toml_dates(f.extra) { | |||
Value::Object(o) => o, | |||
_ => unreachable!("Got something other than a table in section extra"), | |||
}; | |||
Ok(f) | |||
} | |||
@@ -102,7 +106,7 @@ impl Default for SectionFrontMatter { | |||
transparent: false, | |||
page_template: None, | |||
aliases: Vec::new(), | |||
extra: HashMap::new(), | |||
extra: Map::new(), | |||
} | |||
} | |||
} |
@@ -2,12 +2,13 @@ | |||
name = "imageproc" | |||
version = "0.1.0" | |||
authors = ["Vojtěch Král <vojtech@kral.hk>"] | |||
edition = "2018" | |||
[dependencies] | |||
lazy_static = "1" | |||
regex = "1.0" | |||
tera = "1.0.0-beta.10" | |||
image = "0.22" | |||
tera = "1" | |||
image = "0.23" | |||
rayon = "1" | |||
errors = { path = "../errors" } | |||
@@ -1,12 +1,3 @@ | |||
#[macro_use] | |||
extern crate lazy_static; | |||
extern crate image; | |||
extern crate rayon; | |||
extern crate regex; | |||
extern crate errors; | |||
extern crate utils; | |||
use std::collections::hash_map::DefaultHasher; | |||
use std::collections::hash_map::Entry as HEntry; | |||
use std::collections::HashMap; | |||
@@ -14,9 +5,9 @@ use std::fs::{self, File}; | |||
use std::hash::{Hash, Hasher}; | |||
use std::path::{Path, PathBuf}; | |||
use image::jpeg::JPEGEncoder; | |||
use image::png::PNGEncoder; | |||
use image::{FilterType, GenericImageView}; | |||
use image::imageops::FilterType; | |||
use image::{GenericImageView, ImageOutputFormat}; | |||
use lazy_static::lazy_static; | |||
use rayon::prelude::*; | |||
use regex::Regex; | |||
@@ -272,7 +263,7 @@ impl ImageOp { | |||
} else { | |||
img | |||
} | |||
}, | |||
} | |||
Fill(w, h) => { | |||
let factor_w = img_w as f32 / w as f32; | |||
let factor_h = img_h as f32 / h as f32; | |||
@@ -304,16 +295,13 @@ impl ImageOp { | |||
}; | |||
let mut f = File::create(target_path)?; | |||
let (img_w, img_h) = img.dimensions(); | |||
match self.format { | |||
Format::Png => { | |||
let enc = PNGEncoder::new(&mut f); | |||
enc.encode(&img.raw_pixels(), img_w, img_h, img.color())?; | |||
img.write_to(&mut f, ImageOutputFormat::Png)?; | |||
} | |||
Format::Jpeg(q) => { | |||
let mut enc = JPEGEncoder::new_with_quality(&mut f, q); | |||
enc.encode(&img.raw_pixels(), img_w, img_h, img.color())?; | |||
img.write_to(&mut f, ImageOutputFormat::Jpeg(q))?; | |||
} | |||
} | |||
@@ -2,15 +2,15 @@ | |||
name = "library" | |||
version = "0.1.0" | |||
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"] | |||
edition = "2018" | |||
[dependencies] | |||
slotmap = "0.4" | |||
rayon = "1" | |||
chrono = { version = "0.4", features = ["serde"] } | |||
tera = "1.0.0-beta.10" | |||
tera = "1" | |||
serde = "1" | |||
serde_derive = "1" | |||
slug = "0.1" | |||
regex = "1" | |||
lazy_static = "1" | |||
@@ -1,7 +1,7 @@ | |||
use std::path::{Path, PathBuf}; | |||
use config::Config; | |||
use errors::Result; | |||
use errors::{bail, Result}; | |||
/// Takes a full path to a file and returns only the components after the first `content` directory | |||
/// Will not return the filename as last component | |||
@@ -56,6 +56,7 @@ impl FileInfo { | |||
let file_path = path.to_path_buf(); | |||
let mut parent = file_path.parent().expect("Get parent of page").to_path_buf(); | |||
let name = path.file_stem().unwrap().to_string_lossy().to_string(); | |||
let canonical = parent.join(&name); | |||
let mut components = | |||
find_content_components(&file_path.strip_prefix(base_path).unwrap_or(&file_path)); | |||
let relative = if !components.is_empty() { | |||
@@ -78,7 +79,7 @@ impl FileInfo { | |||
path: file_path, | |||
// We don't care about grand parent for pages | |||
grand_parent: None, | |||
canonical: parent.join(&name), | |||
canonical, | |||
parent, | |||
name, | |||
components, | |||
@@ -135,7 +136,7 @@ impl FileInfo { | |||
} | |||
self.name = parts.swap_remove(0); | |||
self.canonical = self.parent.join(&self.name); | |||
self.canonical = self.path.parent().expect("Get parent of page path").join(&self.name); | |||
let lang = parts.swap_remove(0); | |||
Ok(lang) | |||
@@ -254,4 +255,34 @@ mod tests { | |||
assert!(res.is_ok()); | |||
assert_eq!(res.unwrap(), "fr"); | |||
} | |||
/// Regression test for https://github.com/getzola/zola/issues/854 | |||
#[test] | |||
fn correct_canonical_for_index() { | |||
let file = FileInfo::new_page( | |||
&Path::new("/home/vincent/code/site/content/posts/tutorials/python/index.md"), | |||
&PathBuf::new(), | |||
); | |||
assert_eq!( | |||
file.canonical, | |||
Path::new("/home/vincent/code/site/content/posts/tutorials/python/index") | |||
); | |||
} | |||
/// Regression test for https://github.com/getzola/zola/issues/854 | |||
#[test] | |||
fn correct_canonical_after_find_language() { | |||
let mut config = Config::default(); | |||
config.languages.push(Language { code: String::from("fr"), rss: false, search: false }); | |||
let mut file = FileInfo::new_page( | |||
&Path::new("/home/vincent/code/site/content/posts/tutorials/python/index.fr.md"), | |||
&PathBuf::new(), | |||
); | |||
let res = file.find_language(&config); | |||
assert!(res.is_ok()); | |||
assert_eq!( | |||
file.canonical, | |||
Path::new("/home/vincent/code/site/content/posts/tutorials/python/index") | |||
); | |||
} | |||
} |
@@ -2,23 +2,24 @@ | |||
use std::collections::HashMap; | |||
use std::path::{Path, PathBuf}; | |||
use lazy_static::lazy_static; | |||
use regex::Regex; | |||
use slotmap::DefaultKey; | |||
use slug::slugify; | |||
use tera::{Context as TeraContext, Tera}; | |||
use crate::library::Library; | |||
use config::Config; | |||
use errors::{Error, Result}; | |||
use front_matter::{split_page_content, InsertAnchor, PageFrontMatter}; | |||
use library::Library; | |||
use rendering::{render_content, Heading, RenderContext}; | |||
use utils::fs::{find_related_assets, read_file}; | |||
use utils::site::get_reading_analytics; | |||
use utils::templates::render_template; | |||
use content::file_info::FileInfo; | |||
use content::has_anchor; | |||
use content::ser::SerializingPage; | |||
use crate::content::file_info::FileInfo; | |||
use crate::content::has_anchor; | |||
use crate::content::ser::SerializingPage; | |||
use utils::slugs::slugify_paths; | |||
lazy_static! { | |||
// Based on https://regex101.com/r/H2n38Z/1/tests | |||
@@ -160,21 +161,24 @@ impl Page { | |||
page.slug = { | |||
if let Some(ref slug) = page.meta.slug { | |||
slugify(&slug.trim()) | |||
slugify_paths(slug, config.slugify.paths) | |||
} else if page.file.name == "index" { | |||
if let Some(parent) = page.file.path.parent() { | |||
if let Some(slug) = slug_from_dated_filename { | |||
slugify(&slug) | |||
slugify_paths(&slug, config.slugify.paths) | |||
} else { | |||
slugify(parent.file_name().unwrap().to_str().unwrap()) | |||
slugify_paths( | |||
parent.file_name().unwrap().to_str().unwrap(), | |||
config.slugify.paths, | |||
) | |||
} | |||
} else { | |||
slugify(&page.file.name) | |||
slugify_paths(&page.file.name, config.slugify.paths) | |||
} | |||
} else if let Some(slug) = slug_from_dated_filename { | |||
slugify(&slug) | |||
slugify_paths(&slug, config.slugify.paths) | |||
} else { | |||
slugify(&page.file.name) | |||
slugify_paths(&page.file.name, config.slugify.paths) | |||
} | |||
}; | |||
@@ -290,7 +294,6 @@ impl Page { | |||
context.insert("current_path", &self.path); | |||
context.insert("page", &self.to_serialized(library)); | |||
context.insert("lang", &self.lang); | |||
context.insert("toc", &self.toc); | |||
render_template(&tpl_name, tera, context, &config.theme).map_err(|e| { | |||
Error::chain(format!("Failed to render page '{}'", self.file.path.display()), e) | |||
@@ -376,6 +379,7 @@ mod tests { | |||
use super::Page; | |||
use config::{Config, Language}; | |||
use front_matter::InsertAnchor; | |||
use utils::slugs::SlugifyStrategy; | |||
#[test] | |||
fn test_can_parse_a_valid_page() { | |||
@@ -444,7 +448,8 @@ Hello world"#; | |||
slug = "hello-&-world" | |||
+++ | |||
Hello world"#; | |||
let config = Config::default(); | |||
let mut config = Config::default(); | |||
config.slugify.paths = SlugifyStrategy::On; | |||
let res = Page::parse(Path::new("start.md"), content, &config, &PathBuf::new()); | |||
assert!(res.is_ok()); | |||
let page = res.unwrap(); | |||
@@ -453,6 +458,23 @@ Hello world"#; | |||
assert_eq!(page.permalink, config.make_permalink("hello-world")); | |||
} | |||
#[test] | |||
fn can_make_url_from_utf8_slug_frontmatter() { | |||
let content = r#" | |||
+++ | |||
slug = "日本" | |||
+++ | |||
Hello world"#; | |||
let mut config = Config::default(); | |||
config.slugify.paths = SlugifyStrategy::Safe; | |||
let res = Page::parse(Path::new("start.md"), content, &config, &PathBuf::new()); | |||
assert!(res.is_ok()); | |||
let page = res.unwrap(); | |||
assert_eq!(page.path, "日本/"); | |||
assert_eq!(page.components, vec!["日本"]); | |||
assert_eq!(page.permalink, config.make_permalink("日本")); | |||
} | |||
#[test] | |||
fn can_make_url_from_path() { | |||
let content = r#" | |||
@@ -509,7 +531,8 @@ Hello world"#; | |||
#[test] | |||
fn can_make_slug_from_non_slug_filename() { | |||
let config = Config::default(); | |||
let mut config = Config::default(); | |||
config.slugify.paths = SlugifyStrategy::On; | |||
let res = | |||
Page::parse(Path::new(" file with space.md"), "+++\n+++", &config, &PathBuf::new()); | |||
assert!(res.is_ok()); | |||
@@ -518,6 +541,17 @@ Hello world"#; | |||
assert_eq!(page.permalink, config.make_permalink(&page.slug)); | |||
} | |||
#[test] | |||
fn can_make_path_from_utf8_filename() { | |||
let mut config = Config::default(); | |||
config.slugify.paths = SlugifyStrategy::Safe; | |||
let res = Page::parse(Path::new("日本.md"), "+++\n++++", &config, &PathBuf::new()); | |||
assert!(res.is_ok()); | |||
let page = res.unwrap(); | |||
assert_eq!(page.slug, "日本"); | |||
assert_eq!(page.permalink, config.make_permalink(&page.slug)); | |||
} | |||
#[test] | |||
fn can_specify_summary() { | |||
let config = Config::default(); | |||
@@ -12,10 +12,10 @@ use utils::fs::{find_related_assets, read_file}; | |||
use utils::site::get_reading_analytics; | |||
use utils::templates::render_template; | |||
use content::file_info::FileInfo; | |||
use content::has_anchor; | |||
use content::ser::SerializingSection; | |||
use library::Library; | |||
use crate::content::file_info::FileInfo; | |||
use crate::content::has_anchor; | |||
use crate::content::ser::SerializingSection; | |||
use crate::library::Library; | |||
#[derive(Clone, Debug, PartialEq)] | |||
pub struct Section { | |||
@@ -121,6 +121,7 @@ impl Section { | |||
} else { | |||
section.path = format!("{}/", path); | |||
} | |||
section.components = section | |||
.path | |||
.split('/') | |||
@@ -131,7 +132,7 @@ impl Section { | |||
Ok(section) | |||
} | |||
/// Read and parse a .md file into a Page struct | |||
/// Read and parse a .md file into a Section struct | |||
pub fn from_file<P: AsRef<Path>>( | |||
path: P, | |||
config: &Config, | |||
@@ -219,7 +220,6 @@ impl Section { | |||
context.insert("current_path", &self.path); | |||
context.insert("section", &self.to_serialized(library)); | |||
context.insert("lang", &self.lang); | |||
context.insert("toc", &self.toc); | |||
render_template(tpl_name, tera, context, &config.theme).map_err(|e| { | |||
Error::chain(format!("Failed to render section '{}'", self.file.path.display()), e) | |||
@@ -1,16 +1,22 @@ | |||
//! What we are sending to the templates when rendering them | |||
use std::collections::HashMap; | |||
use std::path::Path; | |||
use serde_derive::Serialize; | |||
use tera::{Map, Value}; | |||
use content::{Page, Section}; | |||
use library::Library; | |||
use crate::content::{Page, Section}; | |||
use crate::library::Library; | |||
use rendering::Heading; | |||
#[derive(Clone, Debug, PartialEq, Serialize)] | |||
pub struct TranslatedContent<'a> { | |||
lang: &'a str, | |||
permalink: &'a str, | |||
title: &'a Option<String>, | |||
/// The path to the markdown file; useful for retrieving the full page through | |||
/// the `get_page` function. | |||
path: &'a Path, | |||
} | |||
impl<'a> TranslatedContent<'a> { | |||
@@ -24,6 +30,7 @@ impl<'a> TranslatedContent<'a> { | |||
lang: &other.lang, | |||
permalink: &other.permalink, | |||
title: &other.meta.title, | |||
path: &other.file.path, | |||
}); | |||
} | |||
@@ -39,6 +46,7 @@ impl<'a> TranslatedContent<'a> { | |||
lang: &other.lang, | |||
permalink: &other.permalink, | |||
title: &other.meta.title, | |||
path: &other.file.path, | |||
}); | |||
} | |||
@@ -64,6 +72,7 @@ pub struct SerializingPage<'a> { | |||
path: &'a str, | |||
components: &'a [String], | |||
summary: &'a Option<String>, | |||
toc: &'a [Heading], | |||
word_count: Option<usize>, | |||
reading_time: Option<usize>, | |||
assets: &'a [String], | |||
@@ -125,6 +134,7 @@ impl<'a> SerializingPage<'a> { | |||
path: &page.path, | |||
components: &page.components, | |||
summary: &page.summary, | |||
toc: &page.toc, | |||
word_count: page.word_count, | |||
reading_time: page.reading_time, | |||
assets: &page.serialized_assets, | |||
@@ -180,6 +190,7 @@ impl<'a> SerializingPage<'a> { | |||
path: &page.path, | |||
components: &page.components, | |||
summary: &page.summary, | |||
toc: &page.toc, | |||
word_count: page.word_count, | |||
reading_time: page.reading_time, | |||
assets: &page.serialized_assets, | |||
@@ -202,9 +213,10 @@ pub struct SerializingSection<'a> { | |||
ancestors: Vec<String>, | |||
title: &'a Option<String>, | |||
description: &'a Option<String>, | |||
extra: &'a HashMap<String, Value>, | |||
extra: &'a Map<String, Value>, | |||
path: &'a str, | |||
components: &'a [String], | |||
toc: &'a [Heading], | |||
word_count: Option<usize>, | |||
reading_time: Option<usize>, | |||
lang: &'a str, | |||
@@ -244,6 +256,7 @@ impl<'a> SerializingSection<'a> { | |||
extra: §ion.meta.extra, | |||
path: §ion.path, | |||
components: §ion.components, | |||
toc: §ion.toc, | |||
word_count: section.word_count, | |||
reading_time: section.reading_time, | |||
assets: §ion.serialized_assets, | |||
@@ -280,6 +293,7 @@ impl<'a> SerializingSection<'a> { | |||
extra: §ion.meta.extra, | |||
path: §ion.path, | |||
components: §ion.components, | |||
toc: §ion.toc, | |||
word_count: section.word_count, | |||
reading_time: section.reading_time, | |||
assets: §ion.serialized_assets, | |||
@@ -1,29 +1,3 @@ | |||
extern crate serde; | |||
extern crate slug; | |||
extern crate tera; | |||
#[macro_use] | |||
extern crate serde_derive; | |||
extern crate chrono; | |||
extern crate rayon; | |||
extern crate slotmap; | |||
#[macro_use] | |||
extern crate lazy_static; | |||
extern crate regex; | |||
#[cfg(test)] | |||
extern crate globset; | |||
#[cfg(test)] | |||
extern crate tempfile; | |||
#[cfg(test)] | |||
extern crate toml; | |||
extern crate config; | |||
extern crate front_matter; | |||
extern crate rendering; | |||
extern crate utils; | |||
#[macro_use] | |||
extern crate errors; | |||
mod content; | |||
mod library; | |||
mod pagination; | |||
@@ -32,8 +6,8 @@ mod taxonomies; | |||
pub use slotmap::{DenseSlotMap, Key}; | |||
pub use crate::library::Library; | |||
pub use content::{Page, Section, SerializingPage, SerializingSection}; | |||
pub use library::Library; | |||
pub use pagination::Paginator; | |||
pub use sorting::sort_actual_pages_by_date; | |||
pub use taxonomies::{find_taxonomies, Taxonomy, TaxonomyItem}; |
@@ -1,13 +1,26 @@ | |||
use std::collections::{HashMap, HashSet}; | |||
use std::path::{Path, PathBuf}; | |||
use slotmap::{DenseSlotMap, DefaultKey}; | |||
use slotmap::{DefaultKey, DenseSlotMap}; | |||
use front_matter::SortBy; | |||
use crate::content::{Page, Section}; | |||
use crate::sorting::{find_siblings, sort_pages_by_date, sort_pages_by_weight}; | |||
use config::Config; | |||
use content::{Page, Section}; | |||
use sorting::{find_siblings, sort_pages_by_date, sort_pages_by_weight}; | |||
// Like vec! but for HashSet | |||
macro_rules! set { | |||
( $( $x:expr ),* ) => { | |||
{ | |||
let mut s = HashSet::new(); | |||
$( | |||
s.insert($x); | |||
)* | |||
s | |||
} | |||
}; | |||
} | |||
/// Houses everything about pages and sections | |||
/// Think of it as a database where each page and section has an id (Key here) | |||
@@ -398,4 +411,128 @@ impl Library { | |||
pub fn contains_page<P: AsRef<Path>>(&self, path: P) -> bool { | |||
self.paths_to_pages.contains_key(path.as_ref()) | |||
} | |||
/// This will check every section/page paths + the aliases and ensure none of them | |||
/// are colliding. | |||
/// Returns (path colliding, [list of files causing that collision]) | |||
pub fn check_for_path_collisions(&self) -> Vec<(&str, Vec<String>)> { | |||
let mut paths: HashMap<&str, HashSet<DefaultKey>> = HashMap::new(); | |||
for (key, page) in &self.pages { | |||
paths | |||
.entry(&page.path) | |||
.and_modify(|s| { | |||
s.insert(key); | |||
}) | |||
.or_insert_with(|| set!(key)); | |||
for alias in &page.meta.aliases { | |||
paths | |||
.entry(&alias) | |||
.and_modify(|s| { | |||
s.insert(key); | |||
}) | |||
.or_insert_with(|| set!(key)); | |||
} | |||
} | |||
for (key, section) in &self.sections { | |||
if !section.meta.render { | |||
continue; | |||
} | |||
paths | |||
.entry(§ion.path) | |||
.and_modify(|s| { | |||
s.insert(key); | |||
}) | |||
.or_insert_with(|| set!(key)); | |||
} | |||
let mut collisions = vec![]; | |||
for (p, keys) in paths { | |||
if keys.len() > 1 { | |||
let file_paths: Vec<String> = keys | |||
.iter() | |||
.map(|k| { | |||
self.pages.get(*k).map(|p| p.file.relative.clone()).unwrap_or_else(|| { | |||
self.sections.get(*k).map(|s| s.file.relative.clone()).unwrap() | |||
}) | |||
}) | |||
.collect(); | |||
collisions.push((p, file_paths)); | |||
} | |||
} | |||
collisions | |||
} | |||
} | |||
#[cfg(test)] | |||
mod tests { | |||
use super::*; | |||
#[test] | |||
fn can_find_no_collisions() { | |||
let mut library = Library::new(10, 10, false); | |||
let mut page = Page::default(); | |||
page.path = "hello".to_string(); | |||
let mut page2 = Page::default(); | |||
page2.path = "hello-world".to_string(); | |||
let mut section = Section::default(); | |||
section.path = "blog".to_string(); | |||
library.insert_page(page); | |||
library.insert_page(page2); | |||
library.insert_section(section); | |||
let collisions = library.check_for_path_collisions(); | |||
assert_eq!(collisions.len(), 0); | |||
} | |||
#[test] | |||
fn can_find_collisions_between_pages() { | |||
let mut library = Library::new(10, 10, false); | |||
let mut page = Page::default(); | |||
page.path = "hello".to_string(); | |||
page.file.relative = "hello".to_string(); | |||
let mut page2 = Page::default(); | |||
page2.path = "hello".to_string(); | |||
page2.file.relative = "hello-world".to_string(); | |||
let mut section = Section::default(); | |||
section.path = "blog".to_string(); | |||
section.file.relative = "hello-world".to_string(); | |||
library.insert_page(page.clone()); | |||
library.insert_page(page2.clone()); | |||
library.insert_section(section); | |||
let collisions = library.check_for_path_collisions(); | |||
assert_eq!(collisions.len(), 1); | |||
assert_eq!(collisions[0].0, page.path); | |||
assert!(collisions[0].1.contains(&page.file.relative)); | |||
assert!(collisions[0].1.contains(&page2.file.relative)); | |||
} | |||
#[test] | |||
fn can_find_collisions_with_an_alias() { | |||
let mut library = Library::new(10, 10, false); | |||
let mut page = Page::default(); | |||
page.path = "hello".to_string(); | |||
page.file.relative = "hello".to_string(); | |||
let mut page2 = Page::default(); | |||
page2.path = "hello-world".to_string(); | |||
page2.file.relative = "hello-world".to_string(); | |||
page2.meta.aliases = vec!["hello".to_string()]; | |||
let mut section = Section::default(); | |||
section.path = "blog".to_string(); | |||
section.file.relative = "hello-world".to_string(); | |||
library.insert_page(page.clone()); | |||
library.insert_page(page2.clone()); | |||
library.insert_section(section); | |||
let collisions = library.check_for_path_collisions(); | |||
assert_eq!(collisions.len(), 1); | |||
assert_eq!(collisions[0].0, page.path); | |||
assert!(collisions[0].1.contains(&page.file.relative)); | |||
assert!(collisions[0].1.contains(&page2.file.relative)); | |||
} | |||
} |
@@ -1,5 +1,6 @@ | |||
use std::collections::HashMap; | |||
use serde_derive::Serialize; | |||
use slotmap::DefaultKey; | |||
use tera::{to_value, Context, Tera, Value}; | |||
@@ -7,9 +8,9 @@ use config::Config; | |||
use errors::{Error, Result}; | |||
use utils::templates::render_template; | |||
use content::{Section, SerializingPage, SerializingSection}; | |||
use library::Library; | |||
use taxonomies::{Taxonomy, TaxonomyItem}; | |||
use crate::content::{Section, SerializingPage, SerializingSection}; | |||
use crate::library::Library; | |||
use crate::taxonomies::{Taxonomy, TaxonomyItem}; | |||
#[derive(Clone, Debug, PartialEq)] | |||
enum PaginationRoot<'a> { | |||
@@ -137,7 +138,11 @@ impl<'a> Paginator<'a> { | |||
continue; | |||
} | |||
let page_path = format!("{}/{}/", self.paginate_path, index + 1); | |||
let page_path = if self.paginate_path.is_empty() { | |||
format!("{}/", index + 1) | |||
} else { | |||
format!("{}/{}/", self.paginate_path, index + 1) | |||
}; | |||
let permalink = format!("{}{}", self.permalink, page_path); | |||
let pager_path = if self.is_index { | |||
@@ -185,12 +190,15 @@ impl<'a> Paginator<'a> { | |||
paginator.insert("next", Value::Null); | |||
} | |||
paginator.insert("number_pagers", to_value(&self.pagers.len()).unwrap()); | |||
paginator.insert( | |||
"base_url", | |||
to_value(&format!("{}{}/", self.permalink, self.paginate_path)).unwrap(), | |||
); | |||
let base_url = if self.paginate_path.is_empty() { | |||
self.permalink.to_string() | |||
} else { | |||
format!("{}{}/", self.permalink, self.paginate_path) | |||
}; | |||
paginator.insert("base_url", to_value(&base_url).unwrap()); | |||
paginator.insert("pages", to_value(¤t_pager.pages).unwrap()); | |||
paginator.insert("current_index", to_value(current_pager.index).unwrap()); | |||
paginator.insert("total_pages", to_value(self.all_pages.len()).unwrap()); | |||
paginator | |||
} | |||
@@ -230,11 +238,11 @@ mod tests { | |||
use std::path::PathBuf; | |||
use tera::to_value; | |||
use crate::content::{Page, Section}; | |||
use crate::library::Library; | |||
use crate::taxonomies::{Taxonomy, TaxonomyItem}; | |||
use config::Taxonomy as TaxonomyConfig; | |||
use content::{Page, Section}; | |||
use front_matter::SectionFrontMatter; | |||
use library::Library; | |||
use taxonomies::{Taxonomy, TaxonomyItem}; | |||
use super::Paginator; | |||
@@ -323,6 +331,7 @@ mod tests { | |||
assert_eq!(context["next"], to_value::<Option<()>>(None).unwrap()); | |||
assert_eq!(context["previous"], to_value("https://vincent.is/posts/").unwrap()); | |||
assert_eq!(context["current_index"], to_value(2).unwrap()); | |||
assert_eq!(context["total_pages"], to_value(4).unwrap()); | |||
} | |||
#[test] | |||
@@ -353,4 +362,26 @@ mod tests { | |||
assert_eq!(paginator.pagers[1].permalink, "https://vincent.is/tags/something/page/2/"); | |||
assert_eq!(paginator.pagers[1].path, "tags/something/page/2/"); | |||
} | |||
// https://github.com/getzola/zola/issues/866 | |||
#[test] | |||
fn works_with_empty_paginate_path() { | |||
let (mut section, library) = create_library(false); | |||
section.meta.paginate_path = String::new(); | |||
let paginator = Paginator::from_section(§ion, &library); | |||
assert_eq!(paginator.pagers.len(), 2); | |||
assert_eq!(paginator.pagers[0].index, 1); | |||
assert_eq!(paginator.pagers[0].pages.len(), 2); | |||
assert_eq!(paginator.pagers[0].permalink, "https://vincent.is/posts/"); | |||
assert_eq!(paginator.pagers[0].path, "posts/"); | |||
assert_eq!(paginator.pagers[1].index, 2); | |||
assert_eq!(paginator.pagers[1].pages.len(), 2); | |||
assert_eq!(paginator.pagers[1].permalink, "https://vincent.is/posts/2/"); | |||
assert_eq!(paginator.pagers[1].path, "posts/2/"); | |||
let context = paginator.build_paginator_context(&paginator.pagers[0]); | |||
assert_eq!(context["base_url"], to_value("https://vincent.is/posts/").unwrap()); | |||
} | |||
} |
@@ -4,7 +4,7 @@ use chrono::NaiveDateTime; | |||
use rayon::prelude::*; | |||
use slotmap::DefaultKey; | |||
use content::Page; | |||
use crate::content::Page; | |||
/// Used by the RSS feed | |||
/// There to not have to import sorting stuff in the site crate | |||
@@ -21,7 +21,9 @@ pub fn sort_actual_pages_by_date(a: &&Page, b: &&Page) -> Ordering { | |||
/// Takes a list of (page key, date, permalink) and sort them by dates if possible | |||
/// Pages without date will be put in the unsortable bucket | |||
/// The permalink is used to break ties | |||
pub fn sort_pages_by_date(pages: Vec<(&DefaultKey, Option<NaiveDateTime>, &str)>) -> (Vec<DefaultKey>, Vec<DefaultKey>) { | |||
pub fn sort_pages_by_date( | |||
pages: Vec<(&DefaultKey, Option<NaiveDateTime>, &str)>, | |||
) -> (Vec<DefaultKey>, Vec<DefaultKey>) { | |||
let (mut can_be_sorted, cannot_be_sorted): (Vec<_>, Vec<_>) = | |||
pages.into_par_iter().partition(|page| page.1.is_some()); | |||
@@ -40,7 +42,9 @@ pub fn sort_pages_by_date(pages: Vec<(&DefaultKey, Option<NaiveDateTime>, &str)> | |||
/// Takes a list of (page key, weight, permalink) and sort them by weight if possible | |||
/// Pages without weight will be put in the unsortable bucket | |||
/// The permalink is used to break ties | |||
pub fn sort_pages_by_weight(pages: Vec<(&DefaultKey, Option<usize>, &str)>) -> (Vec<DefaultKey>, Vec<DefaultKey>) { | |||
pub fn sort_pages_by_weight( | |||
pages: Vec<(&DefaultKey, Option<usize>, &str)>, | |||
) -> (Vec<DefaultKey>, Vec<DefaultKey>) { | |||
let (mut can_be_sorted, cannot_be_sorted): (Vec<_>, Vec<_>) = | |||
pages.into_par_iter().partition(|page| page.1.is_some()); | |||
@@ -57,7 +61,9 @@ pub fn sort_pages_by_weight(pages: Vec<(&DefaultKey, Option<usize>, &str)>) -> ( | |||
} | |||
/// Find the lighter/heavier and earlier/later pages for all pages having a date/weight | |||
pub fn find_siblings(sorted: &[DefaultKey]) -> Vec<(DefaultKey, Option<DefaultKey>, Option<DefaultKey>)> { | |||
pub fn find_siblings( | |||
sorted: &[DefaultKey], | |||
) -> Vec<(DefaultKey, Option<DefaultKey>, Option<DefaultKey>)> { | |||
let mut res = Vec::with_capacity(sorted.len()); | |||
let length = sorted.len(); | |||
@@ -85,7 +91,7 @@ mod tests { | |||
use std::path::PathBuf; | |||
use super::{find_siblings, sort_pages_by_date, sort_pages_by_weight}; | |||
use content::Page; | |||
use crate::content::Page; | |||
use front_matter::PageFrontMatter; | |||
fn create_page_with_date(date: &str) -> Page { | |||
@@ -1,16 +1,17 @@ | |||
use std::collections::HashMap; | |||
use serde_derive::Serialize; | |||
use slotmap::DefaultKey; | |||
use slug::slugify; | |||
use tera::{Context, Tera}; | |||
use config::{Config, Taxonomy as TaxonomyConfig}; | |||
use errors::{Error, Result}; | |||
use errors::{bail, Error, Result}; | |||
use utils::templates::render_template; | |||
use content::SerializingPage; | |||
use library::Library; | |||
use sorting::sort_pages_by_date; | |||
use crate::content::SerializingPage; | |||
use crate::library::Library; | |||
use crate::sorting::sort_pages_by_date; | |||
use utils::slugs::slugify_paths; | |||
#[derive(Debug, Clone, PartialEq, Serialize)] | |||
pub struct SerializedTaxonomyItem<'a> { | |||
@@ -69,7 +70,7 @@ impl TaxonomyItem { | |||
}) | |||
.collect(); | |||
let (mut pages, ignored_pages) = sort_pages_by_date(data); | |||
let slug = slugify(name); | |||
let slug = slugify_paths(name, config.slugify.taxonomies); | |||
let permalink = if taxonomy.lang != config.default_language { | |||
config.make_permalink(&format!("/{}/{}/{}", taxonomy.lang, taxonomy.name, slug)) | |||
} else { | |||
@@ -169,7 +170,6 @@ impl Taxonomy { | |||
self.items.iter().map(|i| SerializedTaxonomyItem::from_item(i, library)).collect(); | |||
context.insert("terms", &terms); | |||
context.insert("taxonomy", &self.kind); | |||
context.insert("lang", &self.kind.lang); | |||
context.insert("current_url", &config.make_permalink(&self.kind.name)); | |||
context.insert("current_path", &self.kind.name); | |||
@@ -232,9 +232,10 @@ mod tests { | |||
use super::*; | |||
use std::collections::HashMap; | |||
use crate::content::Page; | |||
use crate::library::Library; | |||
use config::{Config, Language, Taxonomy as TaxonomyConfig}; | |||
use content::Page; | |||
use library::Library; | |||
use utils::slugs::SlugifyStrategy; | |||
#[test] | |||
fn can_make_taxonomies() { | |||
@@ -331,6 +332,101 @@ mod tests { | |||
assert_eq!(categories.items[1].pages.len(), 1); | |||
} | |||
#[test] | |||
fn can_make_slugified_taxonomies() { | |||
let mut config = Config::default(); | |||
let mut library = Library::new(2, 0, false); | |||
config.taxonomies = vec![ | |||
TaxonomyConfig { | |||
name: "categories".to_string(), | |||
lang: config.default_language.clone(), | |||
..TaxonomyConfig::default() | |||
}, | |||
TaxonomyConfig { | |||
name: "tags".to_string(), | |||
lang: config.default_language.clone(), | |||
..TaxonomyConfig::default() | |||
}, | |||
TaxonomyConfig { | |||
name: "authors".to_string(), | |||
lang: config.default_language.clone(), | |||
..TaxonomyConfig::default() | |||
}, | |||
]; | |||
let mut page1 = Page::default(); | |||
let mut taxo_page1 = HashMap::new(); | |||
taxo_page1.insert("tags".to_string(), vec!["rust".to_string(), "db".to_string()]); | |||
taxo_page1.insert("categories".to_string(), vec!["Programming tutorials".to_string()]); | |||
page1.meta.taxonomies = taxo_page1; | |||
page1.lang = config.default_language.clone(); | |||
library.insert_page(page1); | |||
let mut page2 = Page::default(); | |||
let mut taxo_page2 = HashMap::new(); | |||
taxo_page2.insert("tags".to_string(), vec!["rust".to_string(), "js".to_string()]); | |||
taxo_page2.insert("categories".to_string(), vec!["Other".to_string()]); | |||
page2.meta.taxonomies = taxo_page2; | |||
page2.lang = config.default_language.clone(); | |||
library.insert_page(page2); | |||
let mut page3 = Page::default(); | |||
let mut taxo_page3 = HashMap::new(); | |||
taxo_page3.insert("tags".to_string(), vec!["js".to_string()]); | |||
taxo_page3.insert("authors".to_string(), vec!["Vincent Prouillet".to_string()]); | |||
page3.meta.taxonomies = taxo_page3; | |||
page3.lang = config.default_language.clone(); | |||
library.insert_page(page3); | |||
let taxonomies = find_taxonomies(&config, &library).unwrap(); | |||
let (tags, categories, authors) = { | |||
let mut t = None; | |||
let mut c = None; | |||
let mut a = None; | |||
for x in taxonomies { | |||
match x.kind.name.as_ref() { | |||
"tags" => t = Some(x), | |||
"categories" => c = Some(x), | |||
"authors" => a = Some(x), | |||
_ => unreachable!(), | |||
} | |||
} | |||
(t.unwrap(), c.unwrap(), a.unwrap()) | |||
}; | |||
assert_eq!(tags.items.len(), 3); | |||
assert_eq!(categories.items.len(), 2); | |||
assert_eq!(authors.items.len(), 1); | |||
assert_eq!(tags.items[0].name, "db"); | |||
assert_eq!(tags.items[0].slug, "db"); | |||
assert_eq!(tags.items[0].permalink, "http://a-website.com/tags/db/"); | |||
assert_eq!(tags.items[0].pages.len(), 1); | |||
assert_eq!(tags.items[1].name, "js"); | |||
assert_eq!(tags.items[1].slug, "js"); | |||
assert_eq!(tags.items[1].permalink, "http://a-website.com/tags/js/"); | |||
assert_eq!(tags.items[1].pages.len(), 2); | |||
assert_eq!(tags.items[2].name, "rust"); | |||
assert_eq!(tags.items[2].slug, "rust"); | |||
assert_eq!(tags.items[2].permalink, "http://a-website.com/tags/rust/"); | |||
assert_eq!(tags.items[2].pages.len(), 2); | |||
assert_eq!(categories.items[0].name, "Other"); | |||
assert_eq!(categories.items[0].slug, "other"); | |||
assert_eq!(categories.items[0].permalink, "http://a-website.com/categories/other/"); | |||
assert_eq!(categories.items[0].pages.len(), 1); | |||
assert_eq!(categories.items[1].name, "Programming tutorials"); | |||
assert_eq!(categories.items[1].slug, "programming-tutorials"); | |||
assert_eq!( | |||
categories.items[1].permalink, | |||
"http://a-website.com/categories/programming-tutorials/" | |||
); | |||
assert_eq!(categories.items[1].pages.len(), 1); | |||
} | |||
#[test] | |||
fn errors_on_unknown_taxonomy() { | |||
let mut config = Config::default(); | |||
@@ -466,4 +562,151 @@ mod tests { | |||
); | |||
assert_eq!(categories.items[1].pages.len(), 1); | |||
} | |||
#[test] | |||
fn can_make_utf8_taxonomies() { | |||
let mut config = Config::default(); | |||
config.slugify.taxonomies = SlugifyStrategy::Safe; | |||
config.languages.push(Language { | |||
rss: false, | |||
code: "fr".to_string(), | |||
..Language::default() | |||
}); | |||
let mut library = Library::new(2, 0, true); | |||
config.taxonomies = vec![TaxonomyConfig { | |||
name: "catégories".to_string(), | |||
lang: "fr".to_string(), | |||
..TaxonomyConfig::default() | |||
}]; | |||
let mut page = Page::default(); | |||
page.lang = "fr".to_string(); | |||
let mut taxo_page = HashMap::new(); | |||
taxo_page.insert("catégories".to_string(), vec!["Écologie".to_string()]); | |||
page.meta.taxonomies = taxo_page; | |||
library.insert_page(page); | |||
let taxonomies = find_taxonomies(&config, &library).unwrap(); | |||
let categories = &taxonomies[0]; | |||
assert_eq!(categories.items.len(), 1); | |||
assert_eq!(categories.items[0].name, "Écologie"); | |||
assert_eq!(categories.items[0].permalink, "http://a-website.com/fr/catégories/Écologie/"); | |||
assert_eq!(categories.items[0].pages.len(), 1); | |||
} | |||
#[test] | |||
fn can_make_slugified_taxonomies_in_multiple_languages() { | |||
let mut config = Config::default(); | |||
config.slugify.taxonomies = SlugifyStrategy::On; | |||
config.languages.push(Language { | |||
rss: false, | |||
code: "fr".to_string(), | |||
..Language::default() | |||
}); | |||
let mut library = Library::new(2, 0, true); | |||
config.taxonomies = vec![ | |||
TaxonomyConfig { | |||
name: "categories".to_string(), | |||
lang: config.default_language.clone(), | |||
..TaxonomyConfig::default() | |||
}, | |||
TaxonomyConfig { | |||
name: "tags".to_string(), | |||
lang: config.default_language.clone(), | |||
..TaxonomyConfig::default() | |||
}, | |||
TaxonomyConfig { | |||
name: "auteurs".to_string(), | |||
lang: "fr".to_string(), | |||
..TaxonomyConfig::default() | |||
}, | |||
TaxonomyConfig { | |||
name: "tags".to_string(), | |||
lang: "fr".to_string(), | |||
..TaxonomyConfig::default() | |||
}, | |||
]; | |||
let mut page1 = Page::default(); | |||
let mut taxo_page1 = HashMap::new(); | |||
taxo_page1.insert("tags".to_string(), vec!["rust".to_string(), "db".to_string()]); | |||
taxo_page1.insert("categories".to_string(), vec!["Programming tutorials".to_string()]); | |||
page1.meta.taxonomies = taxo_page1; | |||
page1.lang = config.default_language.clone(); | |||
library.insert_page(page1); | |||
let mut page2 = Page::default(); | |||
let mut taxo_page2 = HashMap::new(); | |||
taxo_page2.insert("tags".to_string(), vec!["rust".to_string()]); | |||
taxo_page2.insert("categories".to_string(), vec!["Other".to_string()]); | |||
page2.meta.taxonomies = taxo_page2; | |||
page2.lang = config.default_language.clone(); | |||
library.insert_page(page2); | |||
let mut page3 = Page::default(); | |||
page3.lang = "fr".to_string(); | |||
let mut taxo_page3 = HashMap::new(); | |||
taxo_page3.insert("tags".to_string(), vec!["rust".to_string()]); | |||
taxo_page3.insert("auteurs".to_string(), vec!["Vincent Prouillet".to_string()]); | |||
page3.meta.taxonomies = taxo_page3; | |||
library.insert_page(page3); | |||
let taxonomies = find_taxonomies(&config, &library).unwrap(); | |||
let (tags, categories, authors) = { | |||
let mut t = None; | |||
let mut c = None; | |||
let mut a = None; | |||
for x in taxonomies { | |||
match x.kind.name.as_ref() { | |||
"tags" => { | |||
if x.kind.lang == "en" { | |||
t = Some(x) | |||
} | |||
} | |||
"categories" => c = Some(x), | |||
"auteurs" => a = Some(x), | |||
_ => unreachable!(), | |||
} | |||
} | |||
(t.unwrap(), c.unwrap(), a.unwrap()) | |||
}; | |||
assert_eq!(tags.items.len(), 2); | |||
assert_eq!(categories.items.len(), 2); | |||
assert_eq!(authors.items.len(), 1); | |||
assert_eq!(tags.items[0].name, "db"); | |||
assert_eq!(tags.items[0].slug, "db"); | |||
assert_eq!(tags.items[0].permalink, "http://a-website.com/tags/db/"); | |||
assert_eq!(tags.items[0].pages.len(), 1); | |||
assert_eq!(tags.items[1].name, "rust"); | |||
assert_eq!(tags.items[1].slug, "rust"); | |||
assert_eq!(tags.items[1].permalink, "http://a-website.com/tags/rust/"); | |||
assert_eq!(tags.items[1].pages.len(), 2); | |||
assert_eq!(authors.items[0].name, "Vincent Prouillet"); | |||
assert_eq!(authors.items[0].slug, "vincent-prouillet"); | |||
assert_eq!( | |||
authors.items[0].permalink, | |||
"http://a-website.com/fr/auteurs/vincent-prouillet/" | |||
); | |||
assert_eq!(authors.items[0].pages.len(), 1); | |||
assert_eq!(categories.items[0].name, "Other"); | |||
assert_eq!(categories.items[0].slug, "other"); | |||
assert_eq!(categories.items[0].permalink, "http://a-website.com/categories/other/"); | |||
assert_eq!(categories.items[0].pages.len(), 1); | |||
assert_eq!(categories.items[1].name, "Programming tutorials"); | |||
assert_eq!(categories.items[1].slug, "programming-tutorials"); | |||
assert_eq!( | |||
categories.items[1].permalink, | |||
"http://a-website.com/categories/programming-tutorials/" | |||
); | |||
assert_eq!(categories.items[1].pages.len(), 1); | |||
} | |||
} |
@@ -2,9 +2,14 @@ | |||
name = "link_checker" | |||
version = "0.1.0" | |||
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"] | |||
edition = "2018" | |||
[dependencies] | |||
reqwest = "0.9" | |||
reqwest = { version = "0.10", features = ["blocking", "rustls-tls"] } | |||
lazy_static = "1" | |||
config = { path = "../config" } | |||
errors = { path = "../errors" } | |||
[dev-dependencies] | |||
mockito = "0.23" |
@@ -1,16 +1,11 @@ | |||
extern crate reqwest; | |||
#[macro_use] | |||
extern crate lazy_static; | |||
extern crate errors; | |||
use lazy_static::lazy_static; | |||
use reqwest::header::{HeaderMap, ACCEPT}; | |||
use reqwest::StatusCode; | |||
use reqwest::{blocking::Client, StatusCode}; | |||
use config::LinkChecker; | |||
use errors::Result; | |||
use std::collections::HashMap; | |||
use std::error::Error; | |||
use std::sync::{Arc, RwLock}; | |||
#[derive(Clone, Debug, PartialEq)] | |||
@@ -27,7 +22,7 @@ impl LinkResult { | |||
} | |||
if let Some(c) = self.code { | |||
return c.is_success(); | |||
return c.is_success() || c == StatusCode::NOT_MODIFIED; | |||
} | |||
true | |||
@@ -51,7 +46,7 @@ lazy_static! { | |||
static ref LINKS: Arc<RwLock<HashMap<String, LinkResult>>> = Arc::new(RwLock::new(HashMap::new())); | |||
} | |||
pub fn check_url(url: &str) -> LinkResult { | |||
pub fn check_url(url: &str, config: &LinkChecker) -> LinkResult { | |||
{ | |||
let guard = LINKS.read().unwrap(); | |||
if let Some(res) = guard.get(url) { | |||
@@ -63,18 +58,44 @@ pub fn check_url(url: &str) -> LinkResult { | |||
headers.insert(ACCEPT, "text/html".parse().unwrap()); | |||
headers.append(ACCEPT, "*/*".parse().unwrap()); | |||
let client = reqwest::Client::new(); | |||
let client = Client::new(); | |||
let check_anchor = !config.skip_anchor_prefixes.iter().any(|prefix| url.starts_with(prefix)); | |||
// Need to actually do the link checking | |||
let res = match client.get(url).headers(headers).send() { | |||
Ok(ref mut response) if has_anchor(url) => { | |||
match check_page_for_anchor(url, response.text()) { | |||
Ok(ref mut response) if check_anchor && has_anchor(url) => { | |||
let body = { | |||
let mut buf: Vec<u8> = vec![]; | |||
response.copy_to(&mut buf).unwrap(); | |||
String::from_utf8(buf).unwrap() | |||
}; | |||
match check_page_for_anchor(url, body) { | |||
Ok(_) => LinkResult { code: Some(response.status()), error: None }, | |||
Err(e) => LinkResult { code: None, error: Some(e.to_string()) }, | |||
} | |||
} | |||
Ok(response) => LinkResult { code: Some(response.status()), error: None }, | |||
Err(e) => LinkResult { code: None, error: Some(e.description().to_string()) }, | |||
Ok(response) => { | |||
if response.status().is_success() || response.status() == StatusCode::NOT_MODIFIED { | |||
LinkResult { code: Some(response.status()), error: None } | |||
} else { | |||
let error_string = if response.status().is_informational() { | |||
format!("Informational status code ({}) received", response.status()) | |||
} else if response.status().is_redirection() { | |||
format!("Redirection status code ({}) received", response.status()) | |||
} else if response.status().is_client_error() { | |||
format!("Client error status code ({}) received", response.status()) | |||
} else if response.status().is_server_error() { | |||
format!("Server error status code ({}) received", response.status()) | |||
} else { | |||
format!("Non-success status code ({}) received", response.status()) | |||
}; | |||
LinkResult { code: None, error: Some(error_string) } | |||
} | |||
} | |||
Err(e) => LinkResult { code: None, error: Some(e.to_string()) }, | |||
}; | |||
LINKS.write().unwrap().insert(url.to_string(), res.clone()); | |||
@@ -91,8 +112,7 @@ fn has_anchor(url: &str) -> bool { | |||
} | |||
} | |||
fn check_page_for_anchor(url: &str, body: reqwest::Result<String>) -> Result<()> { | |||
let body = body.unwrap(); | |||
fn check_page_for_anchor(url: &str, body: String) -> Result<()> { | |||
let index = url.find('#').unwrap(); | |||
let anchor = url.get(index + 1..).unwrap(); | |||
let checks: [String; 4] = [ | |||
@@ -111,21 +131,115 @@ fn check_page_for_anchor(url: &str, body: reqwest::Result<String>) -> Result<()> | |||
#[cfg(test)] | |||
mod tests { | |||
use super::{check_page_for_anchor, check_url, has_anchor, LINKS}; | |||
use super::{check_page_for_anchor, check_url, has_anchor, LinkChecker, LINKS}; | |||
use mockito::mock; | |||
// NOTE: HTTP mock paths below are randomly generated to avoid name | |||
// collisions. Mocks with the same path can sometimes bleed between tests | |||
// and cause them to randomly pass/fail. Please make sure to use unique | |||
// paths when adding or modifying tests that use Mockito. | |||
#[test] | |||
fn can_validate_ok_links() { | |||
let url = "https://google.com"; | |||
let res = check_url(url); | |||
let url = format!("{}{}", mockito::server_url(), "/ekbtwxfhjw"); | |||
let _m = mock("GET", "/ekbtwxfhjw") | |||
.with_header("Content-Type", "text/html") | |||
.with_body(format!( | |||
r#"<!DOCTYPE html> | |||
<html> | |||
<head> | |||
<title>Test</title> | |||
</head> | |||
<body> | |||
<a href="{}">Mock URL</a> | |||
</body> | |||
</html> | |||
"#, | |||
url | |||
)) | |||
.create(); | |||
let res = check_url(&url, &LinkChecker::default()); | |||
assert!(res.is_valid()); | |||
assert!(LINKS.read().unwrap().get(url).is_some()); | |||
let res = check_url(url); | |||
assert!(LINKS.read().unwrap().get(&url).is_some()); | |||
} | |||
#[test] | |||
fn can_follow_301_links() { | |||
let _m1 = mock("GET", "/c7qrtrv3zz") | |||
.with_status(301) | |||
.with_header("Content-Type", "text/plain") | |||
.with_header("Location", format!("{}/rbs5avjs8e", mockito::server_url()).as_str()) | |||
.with_body("Redirecting...") | |||
.create(); | |||
let _m2 = mock("GET", "/rbs5avjs8e") | |||
.with_header("Content-Type", "text/plain") | |||
.with_body("Test") | |||
.create(); | |||
let url = format!("{}{}", mockito::server_url(), "/c7qrtrv3zz"); | |||
let res = check_url(&url, &LinkChecker::default()); | |||
assert!(res.is_valid()); | |||
assert!(res.code.is_some()); | |||
assert!(res.error.is_none()); | |||
} | |||
#[test] | |||
fn can_fail_301_to_404_links() { | |||
let _m1 = mock("GET", "/cav9vibhsc") | |||
.with_status(301) | |||
.with_header("Content-Type", "text/plain") | |||
.with_header("Location", format!("{}/72zmfg4smd", mockito::server_url()).as_str()) | |||
.with_body("Redirecting...") | |||
.create(); | |||
let _m2 = mock("GET", "/72zmfg4smd") | |||
.with_status(404) | |||
.with_header("Content-Type", "text/plain") | |||
.with_body("Not Found") | |||
.create(); | |||
let url = format!("{}{}", mockito::server_url(), "/cav9vibhsc"); | |||
let res = check_url(&url, &LinkChecker::default()); | |||
assert_eq!(res.is_valid(), false); | |||
assert!(res.code.is_none()); | |||
assert!(res.error.is_some()); | |||
} | |||
#[test] | |||
fn can_fail_404_links() { | |||
let res = check_url("https://google.comys"); | |||
let _m = mock("GET", "/nlhab9c1vc") | |||
.with_status(404) | |||
.with_header("Content-Type", "text/plain") | |||
.with_body("Not Found") | |||
.create(); | |||
let url = format!("{}{}", mockito::server_url(), "/nlhab9c1vc"); | |||
let res = check_url(&url, &LinkChecker::default()); | |||
assert_eq!(res.is_valid(), false); | |||
assert!(res.code.is_none()); | |||
assert!(res.error.is_some()); | |||
} | |||
#[test] | |||
fn can_fail_500_links() { | |||
let _m = mock("GET", "/qdbrssazes") | |||
.with_status(500) | |||
.with_header("Content-Type", "text/plain") | |||
.with_body("Internal Server Error") | |||
.create(); | |||
let url = format!("{}{}", mockito::server_url(), "/qdbrssazes"); | |||
let res = check_url(&url, &LinkChecker::default()); | |||
assert_eq!(res.is_valid(), false); | |||
assert!(res.code.is_none()); | |||
assert!(res.error.is_some()); | |||
} | |||
#[test] | |||
fn can_fail_unresolved_links() { | |||
let res = check_url("https://t6l5cn9lpm.lxizfnzckd", &LinkChecker::default()); | |||
assert_eq!(res.is_valid(), false); | |||
assert!(res.code.is_none()); | |||
assert!(res.error.is_some()); | |||
@@ -134,8 +248,8 @@ mod tests { | |||
#[test] | |||
fn can_validate_anchors() { | |||
let url = "https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.collect"; | |||
let body = "<body><h3 id='method.collect'>collect</h3></body>".to_string(); | |||
let res = check_page_for_anchor(url, Ok(body)); | |||
let body = r#"<body><h3 id="method.collect">collect</h3></body>"#.to_string(); | |||
let res = check_page_for_anchor(url, body); | |||
assert!(res.is_ok()); | |||
} | |||
@@ -143,7 +257,7 @@ mod tests { | |||
fn can_validate_anchors_with_other_quotes() { | |||
let url = "https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.collect"; | |||
let body = r#"<body><h3 id="method.collect">collect</h3></body>"#.to_string(); | |||
let res = check_page_for_anchor(url, Ok(body)); | |||
let res = check_page_for_anchor(url, body); | |||
assert!(res.is_ok()); | |||
} | |||
@@ -151,15 +265,15 @@ mod tests { | |||
fn can_validate_anchors_with_name_attr() { | |||
let url = "https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.collect"; | |||
let body = r#"<body><h3 name="method.collect">collect</h3></body>"#.to_string(); | |||
let res = check_page_for_anchor(url, Ok(body)); | |||
let res = check_page_for_anchor(url, body); | |||
assert!(res.is_ok()); | |||
} | |||
#[test] | |||
fn can_fail_when_anchor_not_found() { | |||
let url = "https://doc.rust-lang.org/std/iter/trait.Iterator.html#me"; | |||
let body = "<body><h3 id='method.collect'>collect</h3></body>".to_string(); | |||
let res = check_page_for_anchor(url, Ok(body)); | |||
let body = r#"<body><h3 id="method.collect">collect</h3></body>"#.to_string(); | |||
let res = check_page_for_anchor(url, body); | |||
assert!(res.is_err()); | |||
} | |||
@@ -190,4 +304,53 @@ mod tests { | |||
let res = has_anchor(url); | |||
assert_eq!(res, false); | |||
} | |||
#[test] | |||
fn skip_anchor_prefixes() { | |||
let ignore_url = format!("{}{}", mockito::server_url(), "/ignore/"); | |||
let config = LinkChecker { skip_prefixes: vec![], skip_anchor_prefixes: vec![ignore_url] }; | |||
let _m1 = mock("GET", "/ignore/i30hobj1cy") | |||
.with_header("Content-Type", "text/html") | |||
.with_body( | |||
r#"<!DOCTYPE html> | |||
<html> | |||
<head> | |||
<title>Ignore</title> | |||
</head> | |||
<body> | |||
<p id="existent"></p> | |||
</body> | |||
</html> | |||
"#, | |||
) | |||
.create(); | |||
// anchor check is ignored because the url matches the prefix | |||
let ignore = format!("{}{}", mockito::server_url(), "/ignore/i30hobj1cy#nonexistent"); | |||
assert!(check_url(&ignore, &config).is_valid()); | |||
let _m2 = mock("GET", "/guvqcqwmth") | |||
.with_header("Content-Type", "text/html") | |||
.with_body( | |||
r#"<!DOCTYPE html> | |||
<html> | |||
<head> | |||
<title>Test</title> | |||
</head> | |||
<body> | |||
<p id="existent"></p> | |||
</body> | |||
</html> | |||
"#, | |||
) | |||
.create(); | |||
// other anchors are checked | |||
let existent = format!("{}{}", mockito::server_url(), "/guvqcqwmth#existent"); | |||
assert!(check_url(&existent, &config).is_valid()); | |||
let nonexistent = format!("{}{}", mockito::server_url(), "/guvqcqwmth#nonexistent"); | |||
assert_eq!(check_url(&nonexistent, &config).is_valid(), false); | |||
} | |||
} |
@@ -2,6 +2,7 @@ | |||
name = "rebuild" | |||
version = "0.1.0" | |||
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"] | |||
edition = "2018" | |||
[dependencies] | |||
errors = { path = "../errors" } | |||
@@ -1,12 +1,6 @@ | |||
extern crate site; | |||
#[macro_use] | |||
extern crate errors; | |||
extern crate front_matter; | |||
extern crate library; | |||
use std::path::{Component, Path}; | |||
use errors::Result; | |||
use errors::{bail, Result}; | |||
use front_matter::{PageFrontMatter, SectionFrontMatter}; | |||
use library::{Page, Section}; | |||
use site::Site; | |||
@@ -335,7 +329,7 @@ fn is_section(path: &str, languages_codes: &[&str]) -> bool { | |||
} | |||
} | |||
return false; | |||
false | |||
} | |||
/// What happens when a section or a page is created/edited | |||
@@ -423,7 +417,6 @@ pub fn after_template_change(site: &mut Site, path: &Path) -> Result<()> { | |||
if filename == "anchor-link.html" | |||
|| path.components().any(|x| x == Component::Normal("shortcodes".as_ref())) | |||
{ | |||
println!("Rendering markdown"); | |||
site.render_markdown()?; | |||
} | |||
site.populate_sections(); | |||
@@ -1,8 +1,3 @@ | |||
extern crate fs_extra; | |||
extern crate rebuild; | |||
extern crate site; | |||
extern crate tempfile; | |||
use std::env; | |||
use std::fs::{self, File}; | |||
use std::io::prelude::*; | |||
@@ -2,12 +2,12 @@ | |||
name = "rendering" | |||
version = "0.1.0" | |||
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"] | |||
edition = "2018" | |||
[dependencies] | |||
tera = { version = "1.0.0-beta.10", features = ["preserve_order"] } | |||
tera = { version = "1", features = ["preserve_order"] } | |||
syntect = "=3.2.0" | |||
pulldown-cmark = "0.6" | |||
slug = "0.1" | |||
pulldown-cmark = "0.7" | |||
serde = "1" | |||
serde_derive = "1" | |||
pest = "2" | |||
@@ -1,13 +1,7 @@ | |||
#![feature(test)] | |||
extern crate tera; | |||
extern crate test; | |||
extern crate config; | |||
extern crate front_matter; | |||
extern crate rendering; | |||
use std::collections::HashMap; | |||
use std::path::Path; | |||
use config::Config; | |||
use front_matter::InsertAnchor; | |||
@@ -92,8 +86,7 @@ fn bench_render_content_with_highlighting(b: &mut test::Bencher) { | |||
tera.add_raw_template("shortcodes/youtube.html", "{{id}}").unwrap(); | |||
let permalinks_ctx = HashMap::new(); | |||
let config = Config::default(); | |||
let context = | |||
RenderContext::new(&tera, &config, "", &permalinks_ctx, Path::new(""), InsertAnchor::None); | |||
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None); | |||
b.iter(|| render_content(CONTENT, &context).unwrap()); | |||
} | |||
@@ -104,8 +97,7 @@ fn bench_render_content_without_highlighting(b: &mut test::Bencher) { | |||
let permalinks_ctx = HashMap::new(); | |||
let mut config = Config::default(); | |||
config.highlight_code = false; | |||
let context = | |||
RenderContext::new(&tera, &config, "", &permalinks_ctx, Path::new(""), InsertAnchor::None); | |||
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None); | |||
b.iter(|| render_content(CONTENT, &context).unwrap()); | |||
} | |||
@@ -116,8 +108,7 @@ fn bench_render_content_no_shortcode(b: &mut test::Bencher) { | |||
let mut config = Config::default(); | |||
config.highlight_code = false; | |||
let permalinks_ctx = HashMap::new(); | |||
let context = | |||
RenderContext::new(&tera, &config, "", &permalinks_ctx, Path::new(""), InsertAnchor::None); | |||
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None); | |||
b.iter(|| render_content(&content2, &context).unwrap()); | |||
} | |||
@@ -128,8 +119,7 @@ fn bench_render_shortcodes_one_present(b: &mut test::Bencher) { | |||
tera.add_raw_template("shortcodes/youtube.html", "{{id}}").unwrap(); | |||
let config = Config::default(); | |||
let permalinks_ctx = HashMap::new(); | |||
let context = | |||
RenderContext::new(&tera, &config, "", &permalinks_ctx, Path::new(""), InsertAnchor::None); | |||
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None); | |||
b.iter(|| render_shortcodes(CONTENT, &context)); | |||
} |
@@ -1,27 +1,3 @@ | |||
extern crate pulldown_cmark; | |||
extern crate slug; | |||
extern crate syntect; | |||
extern crate tera; | |||
#[macro_use] | |||
extern crate serde_derive; | |||
extern crate pest; | |||
extern crate serde; | |||
#[macro_use] | |||
extern crate pest_derive; | |||
extern crate regex; | |||
#[macro_use] | |||
extern crate lazy_static; | |||
#[macro_use] | |||
extern crate errors; | |||
extern crate config; | |||
extern crate front_matter; | |||
extern crate link_checker; | |||
extern crate utils; | |||
#[cfg(test)] | |||
extern crate templates; | |||
mod context; | |||
mod markdown; | |||
mod shortcode; | |||
@@ -1,22 +1,24 @@ | |||
use lazy_static::lazy_static; | |||
use pulldown_cmark as cmark; | |||
use slug::slugify; | |||
use regex::Regex; | |||
use syntect::easy::HighlightLines; | |||
use syntect::html::{ | |||
start_highlighted_html_snippet, styled_line_to_highlighted_html, IncludeBackground, | |||
}; | |||
use crate::context::RenderContext; | |||
use crate::table_of_contents::{make_table_of_contents, Heading}; | |||
use config::highlighting::{get_highlighter, SYNTAX_SET, THEME_SET}; | |||
use context::RenderContext; | |||
use errors::{Error, Result}; | |||
use front_matter::InsertAnchor; | |||
use table_of_contents::{make_table_of_contents, Heading}; | |||
use utils::site::resolve_internal_link; | |||
use utils::slugs::slugify_anchors; | |||
use utils::vec::InsertMany; | |||
use self::cmark::{Event, LinkType, Options, Parser, Tag}; | |||
use pulldown_cmark::CodeBlockKind; | |||
const CONTINUE_READING: &str = | |||
"<p id=\"zola-continue-reading\"><a name=\"continue-reading\"></a></p>\n"; | |||
const CONTINUE_READING: &str = "<span id=\"continue-reading\"></span>"; | |||
const ANCHOR_LINK_TEMPLATE: &str = "anchor-link.html"; | |||
#[derive(Debug)] | |||
@@ -60,11 +62,31 @@ fn find_anchor(anchors: &[String], name: String, level: u8) -> String { | |||
find_anchor(anchors, name, level + 1) | |||
} | |||
// Returns whether the given string starts with a schema. | |||
// | |||
// Although there exists [a list of registered URI schemes][uri-schemes], a link may use arbitrary, | |||
// private schemes. This function checks if the given string starts with something that just looks | |||
// like a scheme, i.e., a case-insensitive identifier followed by a colon. | |||
// | |||
// [uri-schemes]: https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml | |||
fn starts_with_schema(s: &str) -> bool { | |||
lazy_static! { | |||
static ref PATTERN: Regex = Regex::new(r"^[0-9A-Za-z\-]+:").unwrap(); | |||
} | |||
PATTERN.is_match(s) | |||
} | |||
// Colocated asset links refers to the files in the same directory, | |||
// there it should be a filename only | |||
fn is_colocated_asset_link(link: &str) -> bool { | |||
!link.contains('/') // http://, ftp://, ../ etc | |||
&& !link.starts_with("mailto:") | |||
&& !starts_with_schema(link) | |||
} | |||
// Returns whether a link starts with an HTTP(s) scheme. | |||
fn is_external_link(link: &str) -> bool { | |||
link.starts_with("http:") || link.starts_with("https:") | |||
} | |||
fn fix_link( | |||
@@ -103,7 +125,7 @@ fn fix_link( | |||
} else if is_colocated_asset_link(&link) { | |||
format!("{}{}", context.current_page_permalink, link) | |||
} else { | |||
if !link.starts_with('#') && !link.starts_with("mailto:") { | |||
if is_external_link(link) { | |||
external_links.push(link.to_owned()); | |||
} | |||
link.to_string() | |||
@@ -162,6 +184,7 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render | |||
let mut has_summary = false; | |||
opts.insert(Options::ENABLE_TABLES); | |||
opts.insert(Options::ENABLE_FOOTNOTES); | |||
opts.insert(Options::ENABLE_STRIKETHROUGH); | |||
{ | |||
let mut events = Parser::new_ext(content, opts) | |||
@@ -189,13 +212,18 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render | |||
// Business as usual | |||
Event::Text(text) | |||
} | |||
Event::Start(Tag::CodeBlock(ref info)) => { | |||
Event::Start(Tag::CodeBlock(ref kind)) => { | |||
if !context.config.highlight_code { | |||
return Event::Html("<pre><code>".into()); | |||
} | |||
let theme = &THEME_SET.themes[&context.config.highlight_theme]; | |||
highlighter = Some(get_highlighter(info, &context.config)); | |||
match kind { | |||
CodeBlockKind::Indented => (), | |||
CodeBlockKind::Fenced(info) => { | |||
highlighter = Some(get_highlighter(info, &context.config)); | |||
} | |||
}; | |||
// This selects the background color the same way that start_coloured_html_snippet does | |||
let color = theme | |||
.settings | |||
@@ -221,6 +249,10 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render | |||
Event::Start(Tag::Image(link_type, src, title)) | |||
} | |||
Event::Start(Tag::Link(link_type, link, title)) if link.is_empty() => { | |||
error = Some(Error::msg("There is a link that is missing a URL")); | |||
Event::Start(Tag::Link(link_type, "#".into(), title)) | |||
} | |||
Event::Start(Tag::Link(link_type, link, title)) => { | |||
let fixed_link = match fix_link( | |||
link_type, | |||
@@ -275,8 +307,13 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render | |||
let start_idx = heading_ref.start_idx; | |||
let end_idx = heading_ref.end_idx; | |||
let title = get_text(&events[start_idx + 1..end_idx]); | |||
let id = | |||
heading_ref.id.unwrap_or_else(|| find_anchor(&inserted_anchors, slugify(&title), 0)); | |||
let id = heading_ref.id.unwrap_or_else(|| { | |||
find_anchor( | |||
&inserted_anchors, | |||
slugify_anchors(&title, context.config.slugify.anchors), | |||
0, | |||
) | |||
}); | |||
inserted_anchors.push(id.clone()); | |||
// insert `id` to the tag | |||
@@ -305,7 +342,8 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render | |||
// record heading to make table of contents | |||
let permalink = format!("{}#{}", context.current_page_permalink, id); | |||
let h = Heading { level: heading_ref.level, id, permalink, title, children: Vec::new() }; | |||
let h = | |||
Heading { level: heading_ref.level, id, permalink, title, children: Vec::new() }; | |||
headings.push(h); | |||
} | |||
@@ -328,3 +366,41 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render | |||
}) | |||
} | |||
} | |||
#[cfg(test)] | |||
mod tests { | |||
use super::*; | |||
#[test] | |||
fn test_starts_with_schema() { | |||
// registered | |||
assert!(starts_with_schema("https://example.com/")); | |||
assert!(starts_with_schema("ftp://example.com/")); | |||
assert!(starts_with_schema("mailto:user@example.com")); | |||
assert!(starts_with_schema("xmpp:node@example.com")); | |||
assert!(starts_with_schema("tel:18008675309")); | |||
assert!(starts_with_schema("sms:18008675309")); | |||
assert!(starts_with_schema("h323:user@example.com")); | |||
// arbitrary | |||
assert!(starts_with_schema("zola:post?content=hi")); | |||
// case-insensitive | |||
assert!(starts_with_schema("MailTo:user@example.com")); | |||
assert!(starts_with_schema("MAILTO:user@example.com")); | |||
} | |||
#[test] | |||
fn test_is_external_link() { | |||
assert!(is_external_link("http://example.com/")); | |||
assert!(is_external_link("https://example.com/")); | |||
assert!(is_external_link("https://example.com/index.html#introduction")); | |||
assert!(!is_external_link("mailto:user@example.com")); | |||
assert!(!is_external_link("tel:18008675309")); | |||
assert!(!is_external_link("#introduction")); | |||
assert!(!is_external_link("http.jpg")) | |||
} | |||
} |
@@ -1,10 +1,12 @@ | |||
use lazy_static::lazy_static; | |||
use pest::iterators::Pair; | |||
use pest::Parser; | |||
use pest_derive::Parser; | |||
use regex::Regex; | |||
use tera::{to_value, Context, Map, Value}; | |||
use context::RenderContext; | |||
use errors::{Error, Result}; | |||
use crate::context::RenderContext; | |||
use errors::{bail, Error, Result}; | |||
// This include forces recompiling this source file if the grammar file changes. | |||
// Uncomment it when doing changes to the .pest file | |||
@@ -1,7 +1,8 @@ | |||
use serde_derive::Serialize; | |||
/// Populated while receiving events from the markdown parser | |||
#[derive(Debug, PartialEq, Clone, Serialize)] | |||
pub struct Heading { | |||
#[serde(skip_serializing)] | |||
pub level: u32, | |||
pub id: String, | |||
pub permalink: String, | |||
@@ -113,6 +114,7 @@ mod tests { | |||
]; | |||
let toc = make_table_of_contents(input); | |||
assert_eq!(toc.len(), 1); | |||
assert_eq!(toc[0].level, 1); | |||
assert_eq!(toc[0].children.len(), 1); | |||
assert_eq!(toc[0].children[0].children.len(), 1); | |||
assert_eq!(toc[0].children[0].children[0].children.len(), 2); | |||
@@ -1,9 +1,3 @@ | |||
extern crate config; | |||
extern crate front_matter; | |||
extern crate rendering; | |||
extern crate templates; | |||
extern crate tera; | |||
use std::collections::HashMap; | |||
use tera::Tera; | |||
@@ -12,6 +6,7 @@ use config::Config; | |||
use front_matter::InsertAnchor; | |||
use rendering::{render_content, RenderContext}; | |||
use templates::ZOLA_TERA; | |||
use utils::slugs::SlugifyStrategy; | |||
#[test] | |||
fn can_do_render_content_simple() { | |||
@@ -351,6 +346,17 @@ fn can_add_id_to_headings_same_slug() { | |||
assert_eq!(res.body, "<h1 id=\"hello\">Hello</h1>\n<h1 id=\"hello-1\">Hello</h1>\n"); | |||
} | |||
#[test] | |||
fn can_add_non_slug_id_to_headings() { | |||
let tera_ctx = Tera::default(); | |||
let permalinks_ctx = HashMap::new(); | |||
let mut config = Config::default(); | |||
config.slugify.anchors = SlugifyStrategy::Safe; | |||
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); | |||
let res = render_content(r#"# L'Ă©cologie et vous"#, &context).unwrap(); | |||
assert_eq!(res.body, "<h1 id=\"L'Ă©cologie_et_vous\">L'Ă©cologie et vous</h1>\n"); | |||
} | |||
#[test] | |||
fn can_handle_manual_ids_on_headings() { | |||
let tera_ctx = Tera::default(); | |||
@@ -619,11 +625,14 @@ fn can_understand_footnote_in_heading() { | |||
let config = Config::default(); | |||
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); | |||
let res = render_content("# text [^1] there\n[^1]: footnote", &context).unwrap(); | |||
assert_eq!(res.body, r##"<h1 id="text-there">text <sup class="footnote-reference"><a href="#1">1</a></sup> there</h1> | |||
assert_eq!( | |||
res.body, | |||
r##"<h1 id="text-there">text <sup class="footnote-reference"><a href="#1">1</a></sup> there</h1> | |||
<div class="footnote-definition" id="1"><sup class="footnote-definition-label">1</sup> | |||
<p>footnote</p> | |||
</div> | |||
"##); | |||
"## | |||
); | |||
} | |||
#[test] | |||
@@ -751,7 +760,7 @@ Bla bla | |||
.unwrap(); | |||
assert_eq!( | |||
res.body, | |||
"<p>Hello <a href=\"https://vincentprouillet.com\">My site</a></p>\n<p id=\"zola-continue-reading\"><a name=\"continue-reading\"></a></p>\n<p>Bla bla</p>\n" | |||
"<p>Hello <a href=\"https://vincentprouillet.com\">My site</a></p>\n<span id=\"continue-reading\"></span>\n<p>Bla bla</p>\n" | |||
); | |||
assert_eq!( | |||
res.summary_len, | |||
@@ -821,12 +830,58 @@ fn doesnt_try_to_highlight_content_from_shortcode() { | |||
//} | |||
// https://github.com/getzola/zola/issues/747 | |||
// https://github.com/getzola/zola/issues/816 | |||
#[test] | |||
fn leaves_custom_url_scheme_untouched() { | |||
let content = r#"[foo@bar.tld](xmpp:foo@bar.tld) | |||
[(123) 456-7890](tel:+11234567890) | |||
[blank page](about:blank) | |||
"#; | |||
let tera_ctx = Tera::default(); | |||
let config = Config::default(); | |||
let permalinks_ctx = HashMap::new(); | |||
let context = RenderContext::new( | |||
&tera_ctx, | |||
&config, | |||
"https://vincent.is/", | |||
&permalinks_ctx, | |||
InsertAnchor::None, | |||
); | |||
let res = render_content(content, &context).unwrap(); | |||
let expected = r#"<p><a href="xmpp:foo@bar.tld">foo@bar.tld</a></p> | |||
<p><a href="tel:+11234567890">(123) 456-7890</a></p> | |||
<p><a href="about:blank">blank page</a></p> | |||
"#; | |||
assert_eq!(res.body, expected); | |||
} | |||
#[test] | |||
fn stops_with_an_error_on_an_empty_link() { | |||
let content = r#"[some link]()"#; | |||
let tera_ctx = Tera::default(); | |||
let config = Config::default(); | |||
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); | |||
let res = render_content("[foo@bar.tld](xmpp:foo@bar.tld)", &context).unwrap(); | |||
assert_eq!(res.body, "<p><a href=\"xmpp:foo@bar.tld\">foo@bar.tld</a></p>\n"); | |||
let permalinks_ctx = HashMap::new(); | |||
let context = RenderContext::new( | |||
&tera_ctx, | |||
&config, | |||
"https://vincent.is/", | |||
&permalinks_ctx, | |||
InsertAnchor::None, | |||
); | |||
let res = render_content(content, &context); | |||
let expected = "There is a link that is missing a URL"; | |||
assert!(res.is_err()); | |||
assert_eq!(res.unwrap_err().to_string(), expected); | |||
} |
@@ -2,6 +2,7 @@ | |||
name = "search" | |||
version = "0.1.0" | |||
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"] | |||
edition = "2018" | |||
[dependencies] | |||
elasticlunr-rs = "2" | |||
@@ -1,17 +1,9 @@ | |||
extern crate elasticlunr; | |||
#[macro_use] | |||
extern crate lazy_static; | |||
extern crate ammonia; | |||
#[macro_use] | |||
extern crate errors; | |||
extern crate library; | |||
use std::collections::{HashMap, HashSet}; | |||
use elasticlunr::{Index, Language}; | |||
use lazy_static::lazy_static; | |||
use errors::Result; | |||
use errors::{bail, Result}; | |||
use library::{Library, Section}; | |||
pub const ELASTICLUNR_JS: &str = include_str!("elasticlunr.min.js"); | |||
@@ -2,9 +2,10 @@ | |||
name = "site" | |||
version = "0.1.0" | |||
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"] | |||
edition = "2018" | |||
[dependencies] | |||
tera = "1.0.0-beta.10" | |||
tera = "1" | |||
glob = "0.3" | |||
rayon = "1" | |||
serde = "1" | |||
@@ -1,7 +1,5 @@ | |||
//! Benchmarking loading/markdown rendering of generated sites of various sizes | |||
#![feature(test)] | |||
extern crate site; | |||
extern crate test; | |||
use std::env; | |||
@@ -1,7 +1,4 @@ | |||
#![feature(test)] | |||
extern crate library; | |||
extern crate site; | |||
extern crate tempfile; | |||
extern crate test; | |||
use std::env; | |||
@@ -1,26 +1,4 @@ | |||
extern crate glob; | |||
extern crate rayon; | |||
extern crate serde; | |||
extern crate tera; | |||
#[macro_use] | |||
extern crate serde_derive; | |||
extern crate sass_rs; | |||
#[macro_use] | |||
extern crate errors; | |||
extern crate config; | |||
extern crate front_matter; | |||
extern crate imageproc; | |||
extern crate library; | |||
extern crate link_checker; | |||
extern crate search; | |||
extern crate templates; | |||
extern crate utils; | |||
#[cfg(test)] | |||
extern crate tempfile; | |||
mod sitemap; | |||
pub mod sitemap; | |||
use std::collections::HashMap; | |||
use std::fs::{copy, create_dir_all, remove_dir_all}; | |||
@@ -33,7 +11,7 @@ use sass_rs::{compile_file, Options as SassOptions, OutputStyle}; | |||
use tera::{Context, Tera}; | |||
use config::{get_config, Config}; | |||
use errors::{Error, ErrorKind, Result}; | |||
use errors::{bail, Error, ErrorKind, Result}; | |||
use front_matter::InsertAnchor; | |||
use library::{ | |||
find_taxonomies, sort_actual_pages_by_date, Library, Page, Paginator, Section, Taxonomy, | |||
@@ -69,7 +47,7 @@ pub struct Site { | |||
impl Site { | |||
/// Parse a site at the given path. Defaults to the current dir | |||
/// Passing in a path is only used in tests | |||
/// Passing in a path is used in tests and when --root argument is passed | |||
pub fn new<P: AsRef<Path>>(path: P, config_file: &str) -> Result<Site> { | |||
let path = path.as_ref(); | |||
let mut config = get_config(path, config_file); | |||
@@ -98,17 +76,19 @@ impl Site { | |||
); | |||
let mut tera_theme = Tera::parse(&theme_tpl_glob) | |||
.map_err(|e| Error::chain("Error parsing templates from themes", e))?; | |||
rewrite_theme_paths(&mut tera_theme, &theme); | |||
rewrite_theme_paths( | |||
&mut tera_theme, | |||
tera.templates.values().map(|v| v.name.as_ref()).collect(), | |||
&theme, | |||
); | |||
// TODO: we do that twice, make it dry? | |||
if theme_path.join("templates").join("robots.txt").exists() { | |||
tera_theme | |||
.add_template_file(theme_path.join("templates").join("robots.txt"), None)?; | |||
} | |||
tera_theme.build_inheritance_chains()?; | |||
tera.extend(&tera_theme)?; | |||
} | |||
tera.extend(&ZOLA_TERA)?; | |||
// the `extend` above already does it but hey | |||
tera.build_inheritance_chains()?; | |||
// TODO: Tera doesn't use globset right now so we can load the robots.txt as part | |||
@@ -253,6 +233,14 @@ impl Site { | |||
self.add_page(p, false)?; | |||
} | |||
{ | |||
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()?; | |||
@@ -399,7 +387,16 @@ impl Site { | |||
all_links | |||
.par_iter() | |||
.filter_map(|(page_path, link)| { | |||
let res = check_url(&link); | |||
if self | |||
.config | |||
.link_checker | |||
.skip_prefixes | |||
.iter() | |||
.any(|prefix| link.starts_with(prefix)) | |||
{ | |||
return None; | |||
} | |||
let res = check_url(&link, &self.config.link_checker); | |||
if res.is_valid() { | |||
None | |||
} else { | |||
@@ -456,6 +453,7 @@ impl Site { | |||
index_path.file_name().unwrap().to_string_lossy().to_string(); | |||
if let Some(ref l) = lang { | |||
index_section.file.name = format!("_index.{}", l); | |||
index_section.path = format!("{}/", l); | |||
index_section.permalink = self.config.make_permalink(l); | |||
let filename = format!("_index.{}.md", l); | |||
index_section.file.path = self.content_path.join(&filename); | |||
@@ -633,7 +631,7 @@ impl Site { | |||
return html.replace( | |||
"</body>", | |||
&format!( | |||
r#"<script src="/livereload.js?port={}&mindelay=10"></script></body>"#, | |||
r#"<script src="/livereload.js?port={}&mindelay=10"></script></body>"#, | |||
port | |||
), | |||
); | |||
@@ -2,6 +2,8 @@ use std::borrow::Cow; | |||
use std::collections::HashSet; | |||
use std::hash::{Hash, Hasher}; | |||
use serde_derive::Serialize; | |||
use config::Config; | |||
use library::{Library, Taxonomy}; | |||
use std::cmp::Ordering; | |||
@@ -11,9 +13,9 @@ use tera::{Map, Value}; | |||
/// for examples so we trim down all entries to only that | |||
#[derive(Debug, Serialize)] | |||
pub struct SitemapEntry<'a> { | |||
permalink: Cow<'a, str>, | |||
date: Option<String>, | |||
extra: Option<&'a Map<String, Value>>, | |||
pub permalink: Cow<'a, str>, | |||
pub date: Option<String>, | |||
pub extra: Option<&'a Map<String, Value>>, | |||
} | |||
// Hash/Eq is not implemented for tera::Map but in our case we only care about the permalink | |||
@@ -77,7 +79,11 @@ pub fn find_entries<'a>( | |||
.sections_values() | |||
.iter() | |||
.filter(|s| s.meta.render) | |||
.map(|s| SitemapEntry::new(Cow::Borrowed(&s.permalink), None)) | |||
.map(|s| { | |||
let mut entry = SitemapEntry::new(Cow::Borrowed(&s.permalink), None); | |||
entry.add_extra(&s.meta.extra); | |||
entry | |||
}) | |||
.collect::<Vec<_>>(); | |||
for section in library.sections_values().iter().filter(|s| s.meta.paginate_by.is_some()) { | |||
@@ -1,11 +1,9 @@ | |||
extern crate site; | |||
extern crate tempfile; | |||
#![allow(dead_code)] | |||
use std::env; | |||
use std::path::PathBuf; | |||
use self::site::Site; | |||
use self::tempfile::{tempdir, TempDir}; | |||
use site::Site; | |||
use tempfile::{tempdir, TempDir}; | |||
// 2 helper macros to make all the build testing more bearable | |||
#[macro_export] | |||
@@ -1,5 +1,3 @@ | |||
extern crate config; | |||
extern crate site; | |||
mod common; | |||
use std::collections::HashMap; | |||
@@ -8,6 +6,7 @@ use std::path::Path; | |||
use common::{build_site, build_site_with_setup}; | |||
use config::Taxonomy; | |||
use site::sitemap; | |||
use site::Site; | |||
#[test] | |||
@@ -87,6 +86,19 @@ fn can_parse_site() { | |||
.unwrap(); | |||
assert_eq!(prog_section.subsections.len(), 0); | |||
assert_eq!(prog_section.pages.len(), 2); | |||
// Testing extra variables in sections & sitemaps | |||
// Regression test for #https://github.com/getzola/zola/issues/842 | |||
assert_eq!( | |||
prog_section.meta.extra.get("we_have_extra").and_then(|s| s.as_str()), | |||
Some("variables") | |||
); | |||
let sitemap_entries = sitemap::find_entries(&library, &site.taxonomies[..], &site.config); | |||
let sitemap_entry = sitemap_entries | |||
.iter() | |||
.find(|e| e.permalink.ends_with("tutorials/programming/")) | |||
.expect("expected to find programming section in sitemap"); | |||
assert_eq!(Some(&prog_section.meta.extra), sitemap_entry.extra); | |||
} | |||
#[test] | |||
@@ -161,7 +173,10 @@ fn can_build_site_without_live_reload() { | |||
assert!(file_exists!(public, "nested_sass/scss.css")); | |||
// no live reload code | |||
assert_eq!(file_contains!(public, "index.html", "/livereload.js?port=1112&mindelay=10"), false); | |||
assert_eq!( | |||
file_contains!(public, "index.html", "/livereload.js?port=1112&mindelay=10"), | |||
false | |||
); | |||
// Both pages and sections are in the sitemap | |||
assert!(file_contains!( | |||
@@ -224,11 +239,11 @@ fn can_build_site_with_live_reload_and_drafts() { | |||
// no live reload code | |||
assert!(file_contains!(public, "index.html", "/livereload.js")); | |||
// the summary anchor link has been created | |||
// the summary target has been created | |||
assert!(file_contains!( | |||
public, | |||
"posts/python/index.html", | |||
r#"<a name="continue-reading"></a>"# | |||
r#"<span id="continue-reading"></span>"# | |||
)); | |||
// Drafts are included | |||
@@ -470,6 +485,12 @@ fn can_build_site_with_pagination_for_index() { | |||
"page/1/index.html", | |||
"http-equiv=\"refresh\" content=\"0;url=https://replace-this-with-your-url.com/\"" | |||
)); | |||
assert!(file_contains!(public, "page/1/index.html", "<title>Redirect</title>")); | |||
assert!(file_contains!( | |||
public, | |||
"page/1/index.html", | |||
"<a href=\"https://replace-this-with-your-url.com/\">Click here</a>" | |||
)); | |||
assert!(file_contains!(public, "index.html", "Num pages: 1")); | |||
assert!(file_contains!(public, "index.html", "Current index: 1")); | |||
assert!(file_contains!(public, "index.html", "First: https://replace-this-with-your-url.com/")); | |||
@@ -662,3 +683,17 @@ fn can_ignore_markdown_content() { | |||
let (_, _tmp_dir, public) = build_site("test_site"); | |||
assert!(!file_exists!(public, "posts/ignored/index.html")); | |||
} | |||
#[test] | |||
fn check_site() { | |||
let (mut site, _tmp_dir, _public) = build_site("test_site"); | |||
assert_eq!( | |||
site.config.link_checker.skip_anchor_prefixes, | |||
vec!["https://github.com/rust-lang/rust/blob/"] | |||
); | |||
assert_eq!(site.config.link_checker.skip_prefixes, vec!["http://[2001:db8::]/"]); | |||
site.config.enable_check_mode(); | |||
site.load().expect("link check test_site"); | |||
} |
@@ -1,4 +1,3 @@ | |||
extern crate site; | |||
mod common; | |||
use std::env; | |||
@@ -2,17 +2,18 @@ | |||
name = "templates" | |||
version = "0.1.0" | |||
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"] | |||
edition = "2018" | |||
[dependencies] | |||
tera = "1.0.0-beta.10" | |||
base64 = "0.10" | |||
tera = "1" | |||
base64 = "0.11" | |||
lazy_static = "1" | |||
pulldown-cmark = "0.6" | |||
pulldown-cmark = "0.7" | |||
toml = "0.5" | |||
csv = "1" | |||
image = "0.22" | |||
image = "0.23" | |||
serde_json = "1.0" | |||
reqwest = "0.9" | |||
reqwest = { version = "0.10", features = ["blocking"] } | |||
url = "2" | |||
errors = { path = "../errors" } | |||
@@ -20,3 +21,6 @@ utils = { path = "../utils" } | |||
library = { path = "../library" } | |||
config = { path = "../config" } | |||
imageproc = { path = "../imageproc" } | |||
[dev-dependencies] | |||
mockito = "0.23" |
@@ -1,8 +1,12 @@ | |||
<!DOCTYPE html> | |||
<html> | |||
<head> | |||
<title>Redirect</title> | |||
<link rel="canonical" href="{{ url | safe }}" /> | |||
<meta http-equiv="content-type" content="text/html; charset=utf-8" /> | |||
<meta http-equiv="refresh" content="0;url={{ url | safe }}" /> | |||
</head> | |||
<body> | |||
<p><a href="{{ url | safe }}">Click here</a> to be redirected.</p> | |||
</body> | |||
</html> |
@@ -3,7 +3,7 @@ use std::hash::BuildHasher; | |||
use base64::{decode, encode}; | |||
use pulldown_cmark as cmark; | |||
use tera::{to_value, Result as TeraResult, Value}; | |||
use tera::{to_value, try_get_value, Result as TeraResult, Value}; | |||
pub fn markdown<S: BuildHasher>( | |||
value: &Value, | |||
@@ -1,10 +1,7 @@ | |||
extern crate serde_json; | |||
extern crate toml; | |||
use utils::de::fix_toml_dates; | |||
use utils::fs::{get_file_time, is_path_in_directory, read_file}; | |||
use reqwest::{header, Client}; | |||
use reqwest::{blocking::Client, header}; | |||
use std::collections::hash_map::DefaultHasher; | |||
use std::fmt; | |||
use std::hash::{Hash, Hasher}; | |||
@@ -202,7 +199,7 @@ impl TeraFn for LoadData { | |||
let data = match data_source { | |||
DataSource::Path(path) => read_data_file(&self.base_path, path), | |||
DataSource::Url(url) => { | |||
let mut response = response_client | |||
let response = response_client | |||
.get(url.as_str()) | |||
.header(header::ACCEPT, file_format.as_accept_header()) | |||
.send() | |||
@@ -324,8 +321,15 @@ mod tests { | |||
use std::collections::HashMap; | |||
use std::path::PathBuf; | |||
use mockito::mock; | |||
use serde_json::json; | |||
use tera::{to_value, Function}; | |||
// NOTE: HTTP mock paths below are randomly generated to avoid name | |||
// collisions. Mocks with the same path can sometimes bleed between tests | |||
// and cause them to randomly pass/fail. Please make sure to use unique | |||
// paths when adding or modifying tests that use Mockito. | |||
fn get_test_file(filename: &str) -> PathBuf { | |||
let test_files = PathBuf::from("../utils/test-files").canonicalize().unwrap(); | |||
return test_files.join(filename); | |||
@@ -367,10 +371,14 @@ mod tests { | |||
#[test] | |||
fn calculates_cache_key_for_url() { | |||
let cache_key = | |||
DataSource::Url("https://api.github.com/repos/getzola/zola".parse().unwrap()) | |||
.get_cache_key(&OutputFormat::Plain); | |||
assert_eq!(cache_key, 8916756616423791754); | |||
let _m = mock("GET", "/kr1zdgbm4y") | |||
.with_header("content-type", "text/plain") | |||
.with_body("Test") | |||
.create(); | |||
let url = format!("{}{}", mockito::server_url(), "/kr1zdgbm4y"); | |||
let cache_key = DataSource::Url(url.parse().unwrap()).get_cache_key(&OutputFormat::Plain); | |||
assert_eq!(cache_key, 425638486551656875); | |||
} | |||
#[test] | |||
@@ -393,28 +401,45 @@ mod tests { | |||
#[test] | |||
fn can_load_remote_data() { | |||
let _m = mock("GET", "/zpydpkjj67") | |||
.with_header("content-type", "application/json") | |||
.with_body( | |||
r#"{ | |||
"test": { | |||
"foo": "bar" | |||
} | |||
} | |||
"#, | |||
) | |||
.create(); | |||
let url = format!("{}{}", mockito::server_url(), "/zpydpkjj67"); | |||
let static_fn = LoadData::new(PathBuf::new()); | |||
let mut args = HashMap::new(); | |||
args.insert("url".to_string(), to_value("https://httpbin.org/json").unwrap()); | |||
args.insert("url".to_string(), to_value(&url).unwrap()); | |||
args.insert("format".to_string(), to_value("json").unwrap()); | |||
let result = static_fn.call(&args).unwrap(); | |||
assert_eq!( | |||
result.get("slideshow").unwrap().get("title").unwrap(), | |||
&to_value("Sample Slide Show").unwrap() | |||
); | |||
assert_eq!(result.get("test").unwrap().get("foo").unwrap(), &to_value("bar").unwrap()); | |||
} | |||
#[test] | |||
fn fails_when_request_404s() { | |||
let _m = mock("GET", "/aazeow0kog") | |||
.with_status(404) | |||
.with_header("content-type", "text/plain") | |||
.with_body("Not Found") | |||
.create(); | |||
let url = format!("{}{}", mockito::server_url(), "/aazeow0kog"); | |||
let static_fn = LoadData::new(PathBuf::new()); | |||
let mut args = HashMap::new(); | |||
args.insert("url".to_string(), to_value("https://httpbin.org/status/404/").unwrap()); | |||
args.insert("url".to_string(), to_value(&url).unwrap()); | |||
args.insert("format".to_string(), to_value("json").unwrap()); | |||
let result = static_fn.call(&args); | |||
assert!(result.is_err()); | |||
assert_eq!( | |||
result.unwrap_err().to_string(), | |||
"Failed to request https://httpbin.org/status/404/: 404 Not Found" | |||
format!("Failed to request {}: 404 Not Found", url) | |||
); | |||
} | |||
@@ -34,9 +34,10 @@ impl TeraFn for Trans { | |||
let lang = optional_arg!(String, args.get("lang"), "`trans`: `lang` must be a string.") | |||
.unwrap_or_else(|| self.config.default_language.clone()); | |||
let term = self.config.get_translation(lang, key).map_err(|e| { | |||
Error::chain("Failed to retreive term translation", e) | |||
})?; | |||
let term = self | |||
.config | |||
.get_translation(lang, key) | |||
.map_err(|e| Error::chain("Failed to retreive term translation", e))?; | |||
Ok(to_value(term).unwrap()) | |||
} | |||
@@ -162,11 +163,11 @@ impl TeraFn for GetImageMeta { | |||
let path = required_arg!( | |||
String, | |||
args.get("path"), | |||
"`get_image_meta` requires a `path` argument with a string value" | |||
"`get_image_metadata` requires a `path` argument with a string value" | |||
); | |||
let src_path = self.content_path.join(&path); | |||
if !src_path.exists() { | |||
return Err(format!("`get_image_meta`: Cannot find path: {}", path).into()); | |||
return Err(format!("`get_image_metadata`: Cannot find path: {}", path).into()); | |||
} | |||
let img = image::open(&src_path) | |||
.map_err(|e| Error::chain(format!("Failed to process image: {}", path), e))?; | |||
@@ -345,6 +346,7 @@ mod tests { | |||
use config::{Config, Taxonomy as TaxonomyConfig}; | |||
use library::{Library, Taxonomy, TaxonomyItem}; | |||
use utils::slugs::SlugifyStrategy; | |||
#[test] | |||
fn can_add_cachebust_to_url() { | |||
@@ -388,7 +390,8 @@ mod tests { | |||
#[test] | |||
fn can_get_taxonomy() { | |||
let config = Config::default(); | |||
let mut config = Config::default(); | |||
config.slugify.taxonomies = SlugifyStrategy::On; | |||
let taxo_config = TaxonomyConfig { | |||
name: "tags".to_string(), | |||
lang: config.default_language.clone(), | |||
@@ -465,7 +468,8 @@ mod tests { | |||
#[test] | |||
fn can_get_taxonomy_url() { | |||
let config = Config::default(); | |||
let mut config = Config::default(); | |||
config.slugify.taxonomies = SlugifyStrategy::On; | |||
let taxo_config = TaxonomyConfig { | |||
name: "tags".to_string(), | |||
lang: config.default_language.clone(), | |||
@@ -509,7 +513,6 @@ mod tests { | |||
assert!(static_fn.call(&args).is_err()); | |||
} | |||
const TRANS_CONFIG: &str = r#" | |||
base_url = "https://remplace-par-ton-url.fr" | |||
default_language = "fr" | |||
@@ -1,28 +1,7 @@ | |||
#[macro_use] | |||
extern crate lazy_static; | |||
#[macro_use] | |||
extern crate tera; | |||
extern crate base64; | |||
extern crate csv; | |||
extern crate image; | |||
extern crate pulldown_cmark; | |||
extern crate reqwest; | |||
extern crate url; | |||
#[cfg(test)] | |||
#[macro_use] | |||
extern crate serde_json; | |||
#[cfg(not(test))] | |||
extern crate serde_json; | |||
extern crate config; | |||
extern crate errors; | |||
extern crate imageproc; | |||
extern crate library; | |||
extern crate utils; | |||
pub mod filters; | |||
pub mod global_fns; | |||
use lazy_static::lazy_static; | |||
use tera::{Context, Tera}; | |||
use errors::{Error, Result}; | |||
@@ -69,6 +48,6 @@ pub fn render_redirect_template(url: &str, tera: &Tera) -> Result<String> { | |||
let mut context = Context::new(); | |||
context.insert("url", &url); | |||
tera.render("internal/alias.html", context) | |||
tera.render("internal/alias.html", &context) | |||
.map_err(|e| Error::chain(format!("Failed to render alias for '{}'", url), e)) | |||
} |
@@ -2,14 +2,19 @@ | |||
name = "utils" | |||
version = "0.1.0" | |||
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"] | |||
edition = "2018" | |||
[dependencies] | |||
errors = { path = "../errors" } | |||
tera = "1.0.0-beta.10" | |||
tera = "1" | |||
unicode-segmentation = "1.2" | |||
walkdir = "2" | |||
toml = "0.5" | |||
serde = "1" | |||
serde_derive = "1" | |||
slug = "0.1" | |||
percent-encoding = "2" | |||
errors = { path = "../errors" } | |||
[dev-dependencies] | |||
tempfile = "3" |
@@ -1,17 +1,7 @@ | |||
#[macro_use] | |||
extern crate errors; | |||
extern crate serde; | |||
#[cfg(test)] | |||
extern crate tempfile; | |||
extern crate tera; | |||
extern crate toml; | |||
extern crate unicode_segmentation; | |||
extern crate walkdir; | |||
pub mod de; | |||
pub mod fs; | |||
pub mod net; | |||
pub mod site; | |||
pub mod slugs; | |||
pub mod templates; | |||
pub mod vec; |
@@ -1,7 +1,9 @@ | |||
use std::net::TcpListener; | |||
pub fn get_available_port(avoid: u16) -> Option<u16> { | |||
(1000..9000).find(|port| *port != avoid && port_is_available(*port)) | |||
// Start after "well-known" ports (0–1023) as they require superuser | |||
// privileges on UNIX-like operating systems. | |||
(1024..9000).find(|port| *port != avoid && port_is_available(*port)) | |||
} | |||
pub fn port_is_available(port: u16) -> bool { | |||
@@ -1,8 +1,9 @@ | |||
use percent_encoding::percent_decode; | |||
use std::collections::HashMap; | |||
use std::hash::BuildHasher; | |||
use unicode_segmentation::UnicodeSegmentation; | |||
use errors::Result; | |||
use errors::{bail, Result}; | |||
/// Get word count and estimated reading time | |||
pub fn get_reading_analytics(content: &str) -> (usize, usize) { | |||
@@ -33,12 +34,15 @@ pub fn resolve_internal_link<S: BuildHasher>( | |||
// Then we remove any potential anchor | |||
// parts[0] will be the file path and parts[1] the anchor if present | |||
let parts = clean_link.split('#').collect::<Vec<_>>(); | |||
match permalinks.get(parts[0]) { | |||
// If we have slugification turned off, we might end up with some escaped characters so we need | |||
// to decode them first | |||
let decoded = &*percent_decode(parts[0].as_bytes()).decode_utf8_lossy(); | |||
match permalinks.get(decoded) { | |||
Some(p) => { | |||
if parts.len() > 1 { | |||
Ok(ResolvedInternalLink { | |||
permalink: format!("{}#{}", p, parts[1]), | |||
md_path: Some(parts[0].to_string()), | |||
md_path: Some(decoded.to_string()), | |||
anchor: Some(parts[1].to_string()), | |||
}) | |||
} else { | |||
@@ -81,6 +85,19 @@ mod tests { | |||
assert_eq!(res.anchor, Some("hello".to_string())); | |||
} | |||
#[test] | |||
fn can_resolve_escaped_internal_links() { | |||
let mut permalinks = HashMap::new(); | |||
permalinks.insert( | |||
"pages/about space.md".to_string(), | |||
"https://vincent.is/about%20space/".to_string(), | |||
); | |||
let res = resolve_internal_link("@/pages/about%20space.md#hello", &permalinks).unwrap(); | |||
assert_eq!(res.permalink, "https://vincent.is/about%20space/#hello"); | |||
assert_eq!(res.md_path, Some("pages/about space.md".to_string())); | |||
assert_eq!(res.anchor, Some("hello".to_string())); | |||
} | |||
#[test] | |||
fn errors_resolve_inexistant_internal_link() { | |||
let res = resolve_internal_link("@/pages/about.md#hello", &HashMap::new()); | |||
@@ -0,0 +1,89 @@ | |||
use serde_derive::{Deserialize, Serialize}; | |||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |||
#[serde(rename_all = "lowercase")] | |||
pub enum SlugifyStrategy { | |||
/// Classic slugification, the default | |||
On, | |||
/// No slugification, only remove unsafe characters for filepaths/urls | |||
Safe, | |||
/// Nothing is changed, hope for the best! | |||
Off, | |||
} | |||
fn strip_chars(s: &str, chars: &str) -> String { | |||
let mut sanitized_string = s.to_string(); | |||
sanitized_string.retain(|c| !chars.contains(c)); | |||
sanitized_string | |||
} | |||
fn strip_invalid_paths_chars(s: &str) -> String { | |||
// NTFS forbidden characters : https://gist.github.com/doctaphred/d01d05291546186941e1b7ddc02034d3 | |||
// Also we need to trim whitespaces and `.` from the end of filename | |||
let trimmed = s.trim_end_matches(|c| c == ' ' || c == '.'); | |||
strip_chars(&trimmed, r#"<>:"/\|?*"#) | |||
} | |||
pub fn slugify_paths(s: &str, strategy: SlugifyStrategy) -> String { | |||
match strategy { | |||
SlugifyStrategy::On => slug::slugify(s), | |||
SlugifyStrategy::Safe => strip_invalid_paths_chars(s), | |||
SlugifyStrategy::Off => s.to_string(), | |||
} | |||
} | |||
pub fn slugify_anchors(s: &str, strategy: SlugifyStrategy) -> String { | |||
match strategy { | |||
SlugifyStrategy::On => slug::slugify(s), | |||
SlugifyStrategy::Safe | SlugifyStrategy::Off => { | |||
s.replace(|c: char| c.is_ascii_whitespace(), "_") | |||
} | |||
} | |||
} | |||
#[cfg(test)] | |||
mod tests { | |||
use super::*; | |||
#[test] | |||
fn can_slugify_paths() { | |||
let tests = vec![ | |||
// input, (on, safe, off) | |||
("input", ("input", "input", "input")), | |||
("test ", ("test", "test", "test ")), | |||
("tes t", ("tes-t", "tes t", "tes t")), | |||
// Invalid NTFS | |||
("dot. ", ("dot", "dot", "dot. ")), | |||
("日本", ("ri-ben", "日本", "日本")), | |||
("héhé", ("hehe", "héhé", "héhé")), | |||
("test (hey)", ("test-hey", "test (hey)", "test (hey)")), | |||
]; | |||
for (input, (on, safe, off)) in tests { | |||
assert_eq!(on, slugify_paths(input, SlugifyStrategy::On)); | |||
assert_eq!(safe, slugify_paths(input, SlugifyStrategy::Safe)); | |||
assert_eq!(off, slugify_paths(input, SlugifyStrategy::Off)); | |||
} | |||
} | |||
#[test] | |||
fn can_slugify_anchors() { | |||
let tests = vec![ | |||
// input, (on, safe, off) | |||
("input", ("input", "input", "input")), | |||
("test ", ("test", "test_", "test_")), | |||
("tes t", ("tes-t", "tes_t", "tes_t")), | |||
// Invalid NTFS | |||
("dot. ", ("dot", "dot._", "dot._")), | |||
("日本", ("ri-ben", "日本", "日本")), | |||
("héhé", ("hehe", "héhé", "héhé")), | |||
("test (hey)", ("test-hey", "test_(hey)", "test_(hey)")), | |||
]; | |||
for (input, (on, safe, off)) in tests { | |||
assert_eq!(on, slugify_anchors(input, SlugifyStrategy::On)); | |||
assert_eq!(safe, slugify_anchors(input, SlugifyStrategy::Safe)); | |||
assert_eq!(off, slugify_anchors(input, SlugifyStrategy::Off)); | |||
} | |||
} | |||
} |
@@ -2,7 +2,7 @@ use std::collections::HashMap; | |||
use tera::{Context, Tera}; | |||
use errors::Result; | |||
use errors::{bail, Result}; | |||
static DEFAULT_TPL: &str = include_str!("default_tpl.html"); | |||
@@ -11,7 +11,7 @@ macro_rules! render_default_tpl { | |||
let mut context = Context::new(); | |||
context.insert("filename", $filename); | |||
context.insert("url", $url); | |||
Tera::one_off(DEFAULT_TPL, context, true).map_err(std::convert::Into::into) | |||
Tera::one_off(DEFAULT_TPL, &context, true).map_err(std::convert::Into::into) | |||
}}; | |||
} | |||
@@ -27,21 +27,21 @@ pub fn render_template( | |||
) -> Result<String> { | |||
// check if it is in the templates | |||
if tera.templates.contains_key(name) { | |||
return tera.render(name, context).map_err(std::convert::Into::into); | |||
return tera.render(name, &context).map_err(std::convert::Into::into); | |||
} | |||
// check if it is part of a theme | |||
if let Some(ref t) = *theme { | |||
let theme_template_name = format!("{}/templates/{}", t, name); | |||
if tera.templates.contains_key(&theme_template_name) { | |||
return tera.render(&theme_template_name, context).map_err(std::convert::Into::into); | |||
return tera.render(&theme_template_name, &context).map_err(std::convert::Into::into); | |||
} | |||
} | |||
// check if it is part of ZOLA_TERA defaults | |||
let default_name = format!("__zola_builtins/{}", name); | |||
if tera.templates.contains_key(&default_name) { | |||
return tera.render(&default_name, context).map_err(std::convert::Into::into); | |||
return tera.render(&default_name, &context).map_err(std::convert::Into::into); | |||
} | |||
// maybe it's a default one? | |||
@@ -67,17 +67,21 @@ pub fn render_template( | |||
/// or macros is always better anyway for themes | |||
/// This will also rename the shortcodes to NOT have the themes in the path | |||
/// so themes shortcodes can be used. | |||
pub fn rewrite_theme_paths(tera: &mut Tera, theme: &str) { | |||
pub fn rewrite_theme_paths(tera_theme: &mut Tera, site_templates: Vec<&str>, theme: &str) { | |||
let mut shortcodes_to_move = vec![]; | |||
let mut templates = HashMap::new(); | |||
let old_templates = ::std::mem::replace(&mut tera.templates, HashMap::new()); | |||
let old_templates = ::std::mem::replace(&mut tera_theme.templates, HashMap::new()); | |||
// We want to match the paths in the templates to the new names | |||
for (key, mut tpl) in old_templates { | |||
tpl.name = format!("{}/templates/{}", theme, tpl.name); | |||
// First the parent if there is none | |||
// First the parent if there is one | |||
// If a template with the same name is also in site, assumes it overrides the theme one | |||
// and do not change anything | |||
if let Some(ref p) = tpl.parent.clone() { | |||
tpl.parent = Some(format!("{}/templates/{}", theme, p)); | |||
if !site_templates.contains(&p.as_ref()) { | |||
tpl.parent = Some(format!("{}/templates/{}", theme, p)); | |||
} | |||
} | |||
// Next the macros import | |||
@@ -96,12 +100,12 @@ pub fn rewrite_theme_paths(tera: &mut Tera, theme: &str) { | |||
templates.insert(tpl.name.clone(), tpl); | |||
} | |||
tera.templates = templates; | |||
tera_theme.templates = templates; | |||
// and then replace shortcodes in the Tera instance using the new names | |||
for (old_name, new_name) in shortcodes_to_move { | |||
let tpl = tera.templates.remove(&old_name).unwrap(); | |||
tera.templates.insert(new_name, tpl); | |||
let tpl = tera_theme.templates.remove(&old_name).unwrap(); | |||
tera_theme.templates.insert(new_name, tpl); | |||
} | |||
} | |||
@@ -113,12 +117,23 @@ mod tests { | |||
#[test] | |||
fn can_rewrite_all_paths_of_theme() { | |||
let mut tera = Tera::parse("test-templates/*.html").unwrap(); | |||
rewrite_theme_paths(&mut tera, "hyde"); | |||
rewrite_theme_paths(&mut tera, vec!["base.html"], "hyde"); | |||
// special case to make the test work: we also rename the files to | |||
// match the imports | |||
for (key, val) in tera.templates.clone() { | |||
for (key, val) in &tera.templates.clone() { | |||
tera.templates.insert(format!("hyde/templates/{}", key), val.clone()); | |||
} | |||
// Adding our fake base | |||
tera.add_raw_template("base.html", "Hello").unwrap(); | |||
tera.build_inheritance_chains().unwrap(); | |||
assert_eq!( | |||
tera.templates["hyde/templates/index.html"].parent, | |||
Some("base.html".to_string()) | |||
); | |||
assert_eq!( | |||
tera.templates["hyde/templates/child.html"].parent, | |||
Some("hyde/templates/index.html".to_string()) | |||
); | |||
} | |||
} |
@@ -0,0 +1,10 @@ | |||
<!DOCTYPE html> | |||
<html lang="en"> | |||
<head> | |||
</head> | |||
<body> | |||
{% block body %} | |||
{% endblock body %} | |||
</body> | |||
</html> |
@@ -1 +1,5 @@ | |||
Some base template, used in tests to check whether path rewriting works. | |||
{% extends "base.html" %} | |||
{% block body %} | |||
The default text | |||
{% endblock body %} |
@@ -24,14 +24,14 @@ resize_image(path, width, height, op, format, quality) | |||
- `"fill"` | |||
What each of these does is explained below. The default is `"fill"`. | |||
- `format` (_optional_): Encoding format of the resized image. May be one of: | |||
- `format` (_optional_): Encoding format of the resized image. May be one of: | |||
- `"auto"` | |||
- `"jpg"` | |||
- `"png"` | |||
The default is `"auto"`, this means the format is chosen based on input image format. | |||
JPEG is chosen for JPEGs and other lossy formats, while PNG is chosen for PNGs and other lossless formats. | |||
- `quality` (_optional_): JPEG quality of the resized image, in percents. Only used when encoding JPEGs, default value is `75`. | |||
The default is `"auto"`, this means that the format is chosen based on input image format. | |||
JPEG is chosen for JPEGs and other lossy formats, and PNG is chosen for PNGs and other lossless formats. | |||
- `quality` (_optional_): JPEG quality of the resized image, in percent. Only used when encoding JPEGs; default value is `75`. | |||
### Image processing and return value | |||
@@ -41,7 +41,7 @@ Zola performs image processing during the build process and places the resized i | |||
static/processed_images/ | |||
``` | |||
Filename of each resized image is a hash of the function arguments, | |||
The filename of each resized image is a hash of the function arguments, | |||
which means that once an image is resized in a certain way, it will be stored in the above directory and will not | |||
need to be resized again during subsequent builds (unless the image itself, the dimensions, or other arguments are changed). | |||
Therefore, if you have a large number of images, they will only need to be resized once. | |||
@@ -50,7 +50,7 @@ The function returns a full URL to the resized image. | |||
## Resize operations | |||
The source for all examples is this 300 Ă— 380 pixels image: | |||
The source for all examples is this 300 pixel Ă— 380 pixel image: | |||
![zola](01-zola.png) | |||
@@ -79,9 +79,11 @@ The source for all examples is this 300 Ă— 380 pixels image: | |||
### **`"fit"`** | |||
Like `"fit_width"` and `"fit_height"` combined, but only resize if the image is bigger than any of the specified dimensions. | |||
This mode is handy, if e.g. images are automatically shrinked to certain sizes in a shortcode for mobile optimization. | |||
Resizes the image such that the result fits within `width` and `height` preserving aspect ratio. This means that both width or height | |||
will be at max `width` and `height`, respectively, but possibly one of them smaller so as to preserve the aspect ratio. | |||
This mode is handy, if for example images are automatically shrunk to certain sizes in a shortcode for | |||
mobile optimization. | |||
Resizes the image such that the result fits within `width` and `height` while preserving the aspect ratio. This | |||
means that both width or height will be at max `width` and `height`, respectively, but possibly one of them | |||
smaller so as to preserve the aspect ratio. | |||
`resize_image(..., width=5000, height=5000, op="fit")` | |||
@@ -93,8 +95,9 @@ The source for all examples is this 300 Ă— 380 pixels image: | |||
{{ resize_image(path="documentation/content/image-processing/01-zola.png", width=150, height=150, op="fit") }} | |||
### **`"fill"`** | |||
This is the default operation. It takes the image's center part with the same aspect ratio as the `width` & `height` given and resizes that | |||
to `width` & `height`. This means that parts of the image that are outsize of the resized aspect ratio are cropped away. | |||
This is the default operation. It takes the image's center part with the same aspect ratio as the `width` and | |||
`height` given and resizes that to `width` and `height`. This means that parts of the image that are outside | |||
of the resized aspect ratio are cropped away. | |||
`resize_image(..., width=150, height=150, op="fill")` | |||
@@ -103,8 +106,8 @@ The source for all examples is this 300 Ă— 380 pixels image: | |||
## Using `resize_image` in markdown via shortcodes | |||
`resize_image` is a built-in Tera global function (see the [Templates](@/documentation/templates/_index.md) chapter), | |||
but it can be used in markdown, too, using [Shortcodes](@/documentation/content/shortcodes.md). | |||
`resize_image` is a built-in Tera global function (see the [templates](@/documentation/templates/_index.md) chapter), | |||
but it can be used in Markdown using [shortcodes](@/documentation/content/shortcodes.md). | |||
The examples above were generated using a shortcode file named `resize_image.html` with this content: | |||
@@ -118,7 +121,7 @@ The `resize_image()` can be used multiple times and/or in loops. It is designed | |||
This can be used along with `assets` [page metadata](@/documentation/templates/pages-sections.md) to create picture galleries. | |||
The `assets` variable holds paths to all assets in the directory of a page with resources | |||
(see [assets colocation](@/documentation/content/overview.md#assets-colocation)): if you have files other than images you | |||
(see [asset colocation](@/documentation/content/overview.md#asset-colocation)); if you have files other than images you | |||
will need to filter them out in the loop first like in the example below. | |||
This can be used in shortcodes. For example, we can create a very simple html-only clickable | |||
@@ -135,10 +138,10 @@ picture gallery with the following shortcode named `gallery.html`: | |||
{% endfor %} | |||
``` | |||
As you can notice, we didn't specify an `op` argument, which means it'll default to `"fill"`. Similarly, the format will default to | |||
`"auto"` (choosing PNG or JPEG as appropriate) and the JPEG quality will default to `75`. | |||
As you can notice, we didn't specify an `op` argument, which means that it'll default to `"fill"`. Similarly, | |||
the format will default to `"auto"` (choosing PNG or JPEG as appropriate) and the JPEG quality will default to `75`. | |||
To call it from a markdown file, simply do: | |||
To call it from a Markdown file, simply do: | |||
```jinja2 | |||
{{/* gallery() */}} | |||
@@ -156,4 +159,4 @@ Here is the result: | |||
## Get image size | |||
Sometimes when building a gallery it is useful to know the dimensions of each asset. You can get this information with | |||
[get_image_metadata](@/documentation/templates/overview.md#get-image-metadata) | |||
[get_image_metadata](@/documentation/templates/overview.md#get-image-metadata). |
@@ -4,9 +4,11 @@ weight = 50 | |||
+++ | |||
## Heading id and anchor insertion | |||
While rendering the markdown content, a unique id will automatically be assigned to each heading. This id is created | |||
by converting the heading text to a [slug](https://en.wikipedia.org/wiki/Semantic_URL#Slug), appending numbers at the end | |||
if the slug already exists for that article. For example: | |||
While rendering the Markdown content, a unique id will automatically be assigned to each heading. | |||
This id is created by converting the heading text to a [slug](https://en.wikipedia.org/wiki/Semantic_URL#Slug) if `slugify_paths` is enabled. | |||
if `slugify_paths` is disabled, whitespaces are replaced by `_` and the following characters are stripped: `#`, `%`, `<`, `>`, `[`, `]`, `(`, `)`, \`, `^`, `{`, `|`, `}`. | |||
A number is appended at the end if the slug already exists for that article | |||
For example: | |||
```md | |||
# Something exciting! <- something-exciting | |||
@@ -22,18 +24,21 @@ You can also manually specify an id with a `{#…}` suffix on the heading line: | |||
# Something manual! {#manual} | |||
``` | |||
This is useful for making deep links robust, either proactively (so that you can later change the text of a heading without breaking links to it) or retroactively (keeping the slug of the old header text, when changing the text). It can also be useful for migration of existing sites with different header id schemes, so that you can keep deep links working. | |||
This is useful for making deep links robust, either proactively (so that you can later change the text of a heading | |||
without breaking links to it) or retroactively (keeping the slug of the old header text when changing the text). It | |||
can also be useful for migration of existing sites with different header id schemes, so that you can keep deep | |||
links working. | |||
## Anchor insertion | |||
It is possible to have Zola automatically insert anchor links next to the heading, as you can see on the site you are currently | |||
reading if you hover a title. | |||
It is possible to have Zola automatically insert anchor links next to the heading, as you can see on this documentation | |||
if you hover a title. | |||
This option is set at the section level: the `insert_anchor_links` variable on the | |||
[Section front-matter page](@/documentation/content/section.md#front-matter). | |||
[section front matter page](@/documentation/content/section.md#front-matter). | |||
The default template is very basic and will need CSS tweaks in your project to look decent. | |||
If you want to change the anchor template, it can easily be overwritten by | |||
creating a `anchor-link.html` file in the `templates` directory which gets an `id` variable. | |||
If you want to change the anchor template, it can be easily overwritten by | |||
creating an `anchor-link.html` file in the `templates` directory, which gets an `id` variable. | |||
## Internal links | |||
Linking to other pages and their headings is so common that Zola adds a | |||
@@ -41,4 +46,4 @@ special syntax to Markdown links to handle them: start the link with `@/` and po | |||
to link to. The path to the file starts from the `content` directory. | |||
For example, linking to a file located at `content/pages/about.md` would be `[my link](@/pages/about.md)`. | |||
You can still link to an anchor directly: `[my link](@/pages/about.md#example)` will work as expected. | |||
You can still link to an anchor directly; `[my link](@/pages/about.md#example)` will work as expected. |
@@ -21,7 +21,7 @@ If you want to use per-language taxonomies, ensure you set the `lang` field in t | |||
configuration. | |||
## Content | |||
Once the languages are added in, you can start to translate your content. Zola | |||
Once the languages have been added, you can start to translate your content. Zola | |||
uses the filename to detect the language: | |||
- `content/an-article.md`: this will be the default language | |||
@@ -30,9 +30,9 @@ uses the filename to detect the language: | |||
If the language code in the filename does not correspond to one of the languages configured, | |||
an error will be shown. | |||
If your default language has an `_index.md` in a directory, you will need to add a `_index.{code}.md` | |||
If your default language has an `_index.md` in a directory, you will need to add an `_index.{code}.md` | |||
file with the desired front-matter options as there is no language fallback. | |||
## Output | |||
Zola outputs the translated content with a base URL of `{base_url}/{code}/`. | |||
The only exception to that is if you are setting a translated page `path` directly in the front-matter. | |||
The only exception to this is if you are setting a translated page `path` directly in the front matter. |
@@ -4,9 +4,9 @@ weight = 10 | |||
+++ | |||
Zola uses the folder structure to determine the site structure. | |||
Each folder in the `content` directory represents a [section](@/documentation/content/section.md) | |||
that contains [pages](@/documentation/content/page.md): your `.md` files. | |||
Zola uses the directory structure to determine the site structure. | |||
Each child directory in the `content` directory represents a [section](@/documentation/content/section.md) | |||
that contains [pages](@/documentation/content/page.md) (your `.md` files). | |||
```bash | |||
. | |||
@@ -23,30 +23,30 @@ that contains [pages](@/documentation/content/page.md): your `.md` files. | |||
└── _index.md // -> https://mywebsite.com/landing/ | |||
``` | |||
Each page path (the part after the `base_url`, for example `blog/cli-usage/`) can be customised by changing the `path` or `slug` | |||
attribute of the [page front-matter](@/documentation/content/page.md#front-matter). | |||
Each page path (the part after `base_url`, for example `blog/cli-usage/`) can be customised by changing the `path` or | |||
`slug` attribute of the [page front-matter](@/documentation/content/page.md#front-matter). | |||
You might have noticed a file named `_index.md` in the example above. | |||
This file is used to store both metadata and content of the section itself and is not considered a page. | |||
This file is used to store both the metadata and content of the section itself and is not considered a page. | |||
To make sure the terminology used in the rest of the documentation is understood, let's go over the example above. | |||
To ensure that the terminology used in the rest of the documentation is understood, let's go over the example above. | |||
The `content` directory in this case has three `sections`: `content`, `blog` and `landing`. The `content` section has only | |||
one page, `something.md`, the `landing` section has no page and the `blog` section has 4 pages: `cli-usage.md`, `configuration.md`, `directory-structure.md` | |||
and `installation.md`. | |||
one page (`something.md`), the `landing` section has no pages and the `blog` section has 4 pages (`cli-usage.md`, | |||
`configuration.md`, `directory-structure.md` and `installation.md`). | |||
While not shown in the example, sections can be nested indefinitely. | |||
Sections can be nested indefinitely. | |||
## Assets colocation | |||
## Asset colocation | |||
The `content` directory is not limited to markup files though: it's natural to want to co-locate a page and some related | |||
assets, for instance images or spreadsheets. Zola supports that pattern out of the box for both sections and pages. | |||
The `content` directory is not limited to markup files. It's natural to want to co-locate a page and some related | |||
assets, such as images or spreadsheets. Zola supports this pattern out of the box for both sections and pages. | |||
Any non-markdown file you add in the page/section folder will be copied alongside the generated page when building the site, | |||
which allows us to use a relative path to access them. | |||
All non-Markdown files you add in a page/section directory will be copied alongside the generated page when the site is | |||
built, which allows us to use a relative path to access them. | |||
For pages to use assets colocation, they should not be placed directly in their section folder (such as `latest-experiment.md`), but as an `index.md` file | |||
in a dedicated folder (`latest-experiment/index.md`), like so: | |||
Pages with co-located assets should not be placed directly in their section directory (such as `latest-experiment.md`), but | |||
as an `index.md` file in a dedicated directory (`latest-experiment/index.md`), like so: | |||
```bash | |||
@@ -58,23 +58,23 @@ in a dedicated folder (`latest-experiment/index.md`), like so: | |||
└── research.jpg | |||
``` | |||
In this setup, you may access `research.jpg` from your 'research' section, | |||
and `yavascript.js` from your 'latest-experiment' directly within the Markdown: | |||
With this setup, you may access `research.jpg` from your 'research' section | |||
and `yavascript.js` from your 'latest-experiment' page directly within the Markdown: | |||
```markdown | |||
```Markdown | |||
Check out the complete program [here](yavascript.js). It's **really cool free-software**! | |||
``` | |||
By default, this page will get the folder name as its slug. So its permalink would be in the form of `https://example.com/research/latest-experiment/` | |||
By default, this page's slug will be the directory name and thus its permalink will be `https://example.com/research/latest-experiment/`. | |||
### Excluding files from assets | |||
It is possible to ignore selected asset files using the | |||
[ignored_content](@/documentation/getting-started/configuration.md) setting in the config file. | |||
For example, say you have an Excel spreadsheet from which you are taking several screenshots and | |||
then linking to those image files on your website. For maintainability purposes, you want to keep | |||
the spreadsheet in the same folder as the markdown, but you don't want to copy the spreadsheet to | |||
the public web site. You can achieve this by simply setting `ignored_content` in the config file: | |||
For example, say that you have an Excel spreadsheet from which you are taking several screenshots and | |||
then linking to these image files on your website. For maintainability, you want to keep | |||
the spreadsheet in the same directory as the Markdown file, but you don't want to copy the spreadsheet to | |||
the public web site. You can achieve this by setting `ignored_content` in the config file: | |||
``` | |||
ignored_content = ["*.xlsx"] | |||
@@ -83,15 +83,15 @@ ignored_content = ["*.xlsx"] | |||
## Static assets | |||
In addition to placing content files in the `content` directory, you may also place content | |||
files in the `static` directory. Any files/folders that you place in the `static` directory | |||
will be copied, without modification, to the public directory. | |||
files in the `static` directory. Any files/directories that you place in the `static` directory | |||
will be copied, without modification, to the `public` directory. | |||
Typically, you might put site-wide assets (such as the site favicon, site logos or site-wide | |||
JavaScript) in the root of the static directory. You can also place any HTML or other files that | |||
JavaScript) in the root of the static directory. You can also place any HTML or other files that | |||
you wish to be included without modification (that is, without being parsed as Markdown files) | |||
into the static directory. | |||
Note that the static folder provides an _alternative_ to colocation. For example, imagine that you | |||
Note that the static directory provides an _alternative_ to co-location. For example, imagine that you | |||
had the following directory structure (a simplified version of the structure presented above): | |||
```bash | |||
@@ -103,18 +103,16 @@ had the following directory structure (a simplified version of the structure pre | |||
  └── _index.md // -> https://mywebsite.com/blog/ | |||
``` | |||
If you wanted to add an image to the `https://mywebsite.com/blog/configuration` page, you would | |||
have three options: | |||
* You could save the image to the `content/blog/configuration` folder and then link it with a | |||
relative path from the `index.md` page. This is the approach described under **colocation**, | |||
To add an image to the `https://mywebsite.com/blog/configuration` page, you have three options: | |||
* You could save the image to the `content/blog/configuration` directory and then link to it with a | |||
relative path from the `index.md` page. This is the approach described under **co-location** | |||
above. | |||
* You could save the image to a `static/blog/configuration` folder and link it in exactly the | |||
same way as if you had colocated it. If you do this, the generated files will be identical to | |||
if you had colocated; the only difference will be that all static files will be saved in the | |||
static folder rather than in the content folder. Depending on your organizational needs, this | |||
may be better or worse. | |||
* Or you could save the image to some arbitrary folder within the static folder. For example, | |||
you could save all images to `static/images`. Using this approach, you would no longer be able | |||
to use relative links, but could use an absolute link to `images/[filename]` to access your | |||
image. This might be preferable for small sites or for sites that associate images with | |||
* You could save the image to a `static/blog/configuration` directory and link to it in exactly the | |||
same way as if you had co-located it. If you do this, the generated files will be identical to those | |||
obtained if you had co-located the image; the only difference will be that all static files will be saved in the | |||
static directory rather than in the content directory. The choice depends on your organizational needs. | |||
* Or you could save the image to some arbitrary directory within the static directory. For example, | |||
you could save all images to `static/images`. Using this approach, you can no longer use relative links. Instead, | |||
you must use an absolute link to `images/[filename]` to access your | |||
image. This might be preferable for small sites or for sites that associate images with | |||
multiple pages (e.g., logo images that appear on every page). |
@@ -6,98 +6,140 @@ weight = 30 | |||
A page is any file ending with `.md` in the `content` directory, except files | |||
named `_index.md`. | |||
If a file ending with `.md` is named `index.md`, then it will generate a page | |||
with the name of the containing folder (for example, `/content/about/index.md` would | |||
create a page at `[base_url]/about`). (Note the lack of an underscore; if the file | |||
If a file ending with `.md` is named `index.md`, it will generate a page | |||
with the name of its directory (for example, `/content/about/index.md` would | |||
create a page at `[base_url]/about`). (Note the lack of an underscore; if the file | |||
were named `_index.md`, then it would create a **section** at `[base_url]/about`, as | |||
discussed in the prior part of this documentation. But naming the file `index.md` will | |||
discussed in a previous part of this documentation. In contrast, naming the file `index.md` will | |||
create a **page** at `[base_url]/about`). | |||
If the file is given any name *other* than `index.md` or `_index.md`, then it will | |||
create a page with that name (without the `.md`). So naming a file in the root of your | |||
content directory `about.md` would also create a page at `[base_url]/about`. | |||
Another exception to that rule is that a filename starting with a datetime (YYYY-mm-dd or [a RFC3339 datetime](https://www.ietf.org/rfc/rfc3339.txt)) followed by | |||
create a page with that name (without the `.md`). For example, naming a file in the root of your | |||
content directory `about.md` would create a page at `[base_url]/about`. | |||
Another exception to this rule is that a filename starting with a datetime (YYYY-mm-dd or [an RFC3339 datetime](https://www.ietf.org/rfc/rfc3339.txt)) followed by | |||
an underscore (`_`) or a dash (`-`) will use that date as the page date, unless already set | |||
in the front-matter. The page name will be anything after `_`/`-` so a filename like `2018-10-10-hello-world.md` will | |||
in the front matter. The page name will be anything after `_`/`-`, so the file `2018-10-10-hello-world.md` will | |||
be available at `[base_url]/hello-world`. Note that the full RFC3339 datetime contains colons, which is not a valid | |||
character in a filename on Windows. | |||
As you can see, creating an `about.md` file is exactly equivalent to creating an | |||
As you can see, creating an `about.md` file is equivalent to creating an | |||
`about/index.md` file. The only difference between the two methods is that creating | |||
the `about` folder allows you to use asset colocation, as discussed in the | |||
[Overview](@/documentation/content/overview.md#assets-colocation) section of this documentation. | |||
the `about` directory allows you to use asset co-location, as discussed in the | |||
[overview](@/documentation/content/overview.md#asset-colocation) section. | |||
## Front-matter | |||
## Output paths | |||
The front-matter is a set of metadata embedded in a file. In Zola, | |||
it is at the beginning of the file, surrounded by `+++` and uses TOML. | |||
For any page within your content folder, its output path will be defined by either: | |||
While none of the front-matter variables are mandatory, the opening and closing `+++` are required. | |||
- its `slug` frontmatter key | |||
- its filename | |||
Here is an example page with all the variables available. The values provided below are the default | |||
values. | |||
Either way, these proposed path will be sanitized before being used. | |||
If `slugify_paths` is enabled in the site's config - the default - paths are [slugified](https://en.wikipedia.org/wiki/Clean_URL#Slug). | |||
Otherwise, a simpler sanitation is performed, outputting only valid NTFS paths. | |||
The following characters are removed: `<`, `>`, `:`, `/`, `|`, `?`, `*`, `#`, `\\`, `(`, `)`, `[`, `]` as well as newlines and tabulations. | |||
Additionally, trailing whitespace and dots are removed and whitespaces are replaced by `_`. | |||
**NOTE:** To produce URLs containing non-English characters (UTF8), `slugify_paths` needs to be set to `false`. | |||
### Path from frontmatter | |||
The output path for the page will first be read from the `slug` key in the page's frontmatter. | |||
**Example:** (file `content/zines/mlf-kurdistan.md`) | |||
``` | |||
+++ | |||
title = "Le mouvement des Femmes Libres, à la tête de la libération kurde" | |||
slug = "femmes-libres-libération-kurde" | |||
+++ | |||
This is my article. | |||
``` | |||
This frontmatter will output the article to `[base_url]/zines/femmes-libres-libération-kurde` with `slugify_paths` disabled, and to `[base_url]/zines/femmes-libres-liberation-kurde` with `slugify_enabled` enabled. | |||
### Path from filename | |||
When the article's output path is not specified in the frontmatter, it is extracted from the file's path in the content folder. Consider a file `content/foo/bar/thing.md`. The output path is constructed: | |||
- if the filename is `index.md`, its parent folder name (`bar`) is used as output path | |||
- otherwise, the output path is extracted from `thing`Â (the filename without the `.md` extension) | |||
If the path found starts with a datetime string (`YYYY-mm-dd` or [a RFC3339 datetime](https://www.ietf.org/rfc/rfc3339.txt)) followed by an underscore (`_`) or a dash (`-`), this date is removed from the output path and will be used as the page date (unless already set in the front-matter). Note that the full RFC3339 datetime contains colons, which is not a valid character in a filename on Windows. | |||
The output path extracted from the file path is then slugified or not depending on the `slugify_paths` config, as explained previously. | |||
**Example:** The file `content/blog/2018-10-10-hello-world.md` will generated a page available at will be available at `[base_url]/hello-world`. | |||
## Front matter | |||
The TOML front matter is a set of metadata embedded in a file at the beginning of the file enclosed | |||
by triple pluses (`+++`). | |||
Although none of the front matter variables are mandatory, the opening and closing `+++` are required. | |||
Here is an example page with all the available variables. The values provided below are the | |||
default values. | |||
```toml | |||
title = "" | |||
description = "" | |||
# The date of the post. | |||
# 2 formats are allowed: YYYY-MM-DD (2012-10-02) and RFC3339 (2002-10-02T15:00:00Z) | |||
# Do not wrap dates in quotes, the line below only indicates that there is no default date. | |||
# Two formats are allowed: YYYY-MM-DD (2012-10-02) and RFC3339 (2002-10-02T15:00:00Z). | |||
# Do not wrap dates in quotes; the line below only indicates that there is no default date. | |||
# If the section variable `sort_by` is set to `date`, then any page that lacks a `date` | |||
# will not be rendered. | |||
# Setting this overrides a date set in the filename. | |||
date = | |||
# The weight as defined in the Section page | |||
# The weight as defined on the Section page of the documentation. | |||
# If the section variable `sort_by` is set to `weight`, then any page that lacks a `weight` | |||
# will not be rendered. | |||
weight = 0 | |||
# A draft page is only loaded if the `--drafts` flag is passed to `zola build`, `zola serve` or `zola check` | |||
# A draft page is only loaded if the `--drafts` flag is passed to `zola build`, `zola serve` or `zola check`. | |||
draft = false | |||
# If filled, it will use that slug instead of the filename to make up the URL | |||
# It will still use the section path though | |||
# If set, this slug will be instead of the filename to make the URL. | |||
# The section path will still be used. | |||
slug = "" | |||
# The path the content will appear at | |||
# The path the content will appear at. | |||
# If set, it cannot be an empty string and will override both `slug` and the filename. | |||
# The sections' path won't be used. | |||
# It should not start with a `/` and the slash will be removed if it does | |||
# It should not start with a `/` and the slash will be removed if it does. | |||
path = "" | |||
# Use aliases if you are moving content but want to redirect previous URLs to the | |||
# current one. This takes an array of path, not URLs. | |||
# current one. This takes an array of paths, not URLs. | |||
aliases = [] | |||
# Whether the page should be in the search index. This is only used if | |||
# `build_search_index` is set to true in the config and the parent section | |||
# hasn't set `in_search_index` to false in its front-matter | |||
# When set to "true", the page will be in the search index. This is only used if | |||
# `build_search_index` is set to "true" in the Zola configuration and the parent section | |||
# hasn't set `in_search_index` to "false" in its front matter. | |||
in_search_index = true | |||
# Template to use to render this page | |||
# Template to use to render this page. | |||
template = "page.html" | |||
# The taxonomies for that page. The keys need to be the same as the taxonomies | |||
# name configured in `config.toml` and the values an array of String like | |||
# tags = ["rust", "web"] | |||
# The taxonomies for this page. The keys need to be the same as the taxonomy | |||
# names configured in `config.toml` and the values are an array of String objects. For example, | |||
# tags = ["rust", "web"]. | |||
[taxonomies] | |||
# Your own data | |||
# Your own data. | |||
[extra] | |||
``` | |||
## Summary | |||
You can ask Zola to create a summary if you only want to show the first | |||
paragraph of each page in a list for example. | |||
You can ask Zola to create a summary if, for example, you only want to show the first | |||
paragraph of the page content in a list. | |||
To do so, add <code><!-- more --></code> in your content at the point | |||
where you want the summary to end and the content up to that point will be also | |||
where you want the summary to end. The content up to that point will be | |||
available separately in the | |||
[template](@/documentation/templates/pages-sections.md#page-variables). | |||
An anchor link to this position named `continue-reading` is created, wrapped in a paragraph | |||
with a `zola-continue-reading` id, so you can link directly to it if needed for example: | |||
`<a href="{{ page.permalink }}#continue-reading">Continue Reading</a>` | |||
A span element in this position with a `continue-reading` id is created, so you can link directly to it if needed. For example: | |||
`<a href="{{ page.permalink }}#continue-reading">Continue Reading</a>`. |
@@ -3,8 +3,8 @@ title = "Sass" | |||
weight = 110 | |||
+++ | |||
Sass is a popular CSS extension language that approaches some of the harder | |||
parts of maintaining large sets of CSS rules. If you're curious about what Sass | |||
Sass is a popular CSS preprocessor that adds special features (e.g., variables, nested rules) to facilate the | |||
maintenance of large sets of CSS rules. If you're curious about what Sass | |||
is and why it might be useful for styling your static site, the following links | |||
may be of interest: | |||
@@ -13,7 +13,7 @@ may be of interest: | |||
## Using Sass in Zola | |||
Zola processes any files with the `sass` or `scss` extensions in the `sass` | |||
Zola processes any files with the `sass` or `scss` extension in the `sass` | |||
folder, and places the processed output into a `css` file with the same folder | |||
structure and base name into the `public` folder: | |||
@@ -34,9 +34,9 @@ structure and base name into the `public` folder: | |||
Files with a leading underscore in the name are not placed into the `public` | |||
folder, but can still be used as `@import` dependencies. For more information, see the "Partials" section of | |||
[Sass Basics](https://sass-lang.com/guide#partials). | |||
[Sass Basics](https://sass-lang.com/guide). | |||
Files with the `scss` extension use ["Sassy CSS" syntax](http://sass-lang.com/documentation/#Formatting), | |||
while files with the `sass` extension use the ["indented" syntax](http://sass-lang.com/documentation/file.INDENTED_SYNTAX.html). | |||
Zola will return an error if a `scss` and `sass` file exist with the same | |||
base name in the same folder to avoid confusion -- see the example above. | |||
Files with the `scss` extension use "Sassy CSS" syntax, | |||
while files with the `sass` extension use the "indented" syntax: <https://sass-lang.com/documentation/syntax>. | |||
Zola will return an error if `scss` and `sass` files with the same | |||
base name exist in the same folder to avoid confusion -- see the example above. |
@@ -4,19 +4,19 @@ weight = 100 | |||
+++ | |||
Zola can build a search index from the sections and pages content to | |||
be used by a JavaScript library: [elasticlunr](http://elasticlunr.com/). | |||
be used by a JavaScript library such as [elasticlunr](http://elasticlunr.com/). | |||
To enable it, you only need to set `build_search_index = true` in your `config.toml` and Zola will | |||
generate an index for the `default_language` set for all pages not excluded from the search index. | |||
It is very important to set the `default_language` in your `config.toml` if you are writing a site not in | |||
English: the index building pipelines are very different depending on the language. | |||
English; the index building pipelines are very different depending on the language. | |||
After `zola build` or `zola serve`, you should see two files in your static directory: | |||
- `search_index.${default_language}.js`: so `search_index.en.js` for a default setup | |||
- `elasticlunr.min.js` | |||
As each site will be different, Zola makes no assumptions about how your search and doesn't provide | |||
the JavaScript/CSS code to do an actual search and display results. You can however look at how this very site | |||
is implementing it to have an idea: [search.js](https://github.com/getzola/zola/tree/master/docs/static/search.js). | |||
As each site will be different, Zola makes no assumptions about your search function and doesn't provide | |||
the JavaScript/CSS code to do an actual search and display results. You can look at how this site | |||
implements it to get an idea: [search.js](https://github.com/getzola/zola/tree/master/docs/static/search.js). |
@@ -3,33 +3,34 @@ title = "Section" | |||
weight = 20 | |||
+++ | |||
A section is created whenever a folder (or subfolder) in the `content` section contains an | |||
`_index.md` file. If a folder does not contain an `_index.md` file, no section will be | |||
created, but markdown files within that folder will still create pages (known as orphan pages). | |||
A section is created whenever a directory (or subdirectory) in the `content` section contains an | |||
`_index.md` file. If a directory does not contain an `_index.md` file, no section will be | |||
created, but Markdown files within that directory will still create pages (known as orphan pages). | |||
The index page (i.e., the page displayed when a user browses to your `base_url`) is a section, | |||
which is created whether or not you add an `_index.md` file at the root of your `content` folder. | |||
which is created whether or not you add an `_index.md` file at the root of your `content` directory. | |||
If you do not create an `_index.md` file in your content directory, this main content section will | |||
not have any content or metadata. If you would like to add content or metadata, you can add an | |||
`_index.md` file at the root of the `content` folder and edit it just as you would edit any other | |||
`_index.md` file at the root of the `content` directory and edit it just as you would edit any other | |||
`_index.md` file; your `index.html` template will then have access to that content and metadata. | |||
Any non-Markdown file in the section folder is added to the `assets` collection of the section, as explained in the [Content Overview](@/documentation/content/overview.md#assets-colocation). These files are then available from the Markdown using relative links. | |||
Any non-Markdown file in a section directory is added to the `assets` collection of the section, as explained in the | |||
[content overview](@/documentation/content/overview.md#asset-colocation). These files are then available in the | |||
Markdown file using relative links. | |||
## Front-matter | |||
## Front matter | |||
The `_index.md` file within a folder defines the content and metadata for that section. To set | |||
The `_index.md` file within a directory defines the content and metadata for that section. To set | |||
the metadata, add front matter to the file. | |||
The front-matter is a set of metadata embedded in a file. In Zola, | |||
it is at the beginning of the file, surrounded by `+++` and uses TOML. | |||
The TOML front matter is a set of metadata embedded in a file at the beginning of the file enclosed by triple pluses (`+++`). | |||
After the closing `+++`, you can add content that will be parsed as markdown and will be available | |||
After the closing `+++`, you can add content, which will be parsed as Markdown and made available | |||
to your templates through the `section.content` variable. | |||
While none of the front-matter variables are mandatory, the opening and closing `+++` are required. | |||
Although none of the front matter variables are mandatory, the opening and closing `+++` are required. | |||
Here is an example `_index.md` with all the variables available. The values provided below are the | |||
Here is an example `_index.md` with all the available variables. The values provided below are the | |||
default values. | |||
@@ -38,80 +39,80 @@ title = "" | |||
description = "" | |||
# Whether to sort pages by "date", "weight", or "none". More on that below | |||
# Used to sort pages by "date", "weight" or "none". See below for more information. | |||
sort_by = "none" | |||
# Used by the parent section to order its subsections. | |||
# Lower values have priority. | |||
# Lower values have higher priority. | |||
weight = 0 | |||
# Template to use to render this section page | |||
# Template to use to render this section page. | |||
template = "section.html" | |||
# Apply the given template to ALL pages below the section, recursively. | |||
# If you have several nested sections each with a page_template set, the page | |||
# The given template is applied to ALL pages below the section, recursively. | |||
# If you have several nested sections, each with a page_template set, the page | |||
# will always use the closest to itself. | |||
# However, a page own `template` variable will always have priority. | |||
# Not set by default | |||
# However, a page's own `template` variable will always have priority. | |||
# Not set by default. | |||
page_template = | |||
# How many pages to be displayed per paginated page. | |||
# No pagination will happen if this isn't set or if the value is 0 | |||
# This sets the number of pages to be displayed per paginated page. | |||
# No pagination will happen if this isn't set or if the value is 0. | |||
paginate_by = 0 | |||
# If set, will be the path used by paginated page and the page number will be appended after it. | |||
# For example the default would be page/1 | |||
# If set, this will be the path used by the paginated page. The page number will be appended after this path. | |||
# The default is page/1. | |||
paginate_path = "page" | |||
# Whether to insert a link for each header like the ones you can see in this site if you hover one | |||
# The default template can be overridden by creating a `anchor-link.html` in the `templates` directory | |||
# Options are "left", "right" and "none" | |||
# This determines whether to insert a link for each header like the ones you can see on this site if you hover over | |||
# a header. | |||
# The default template can be overridden by creating an `anchor-link.html` file in the `templates` directory. | |||
# This value can be "left", "right" or "none". | |||
insert_anchor_links = "none" | |||
# Whether the section pages should be in the search index. This is only used if | |||
# `build_search_index` is set to true in the config | |||
# If set to "true", the section pages will be in the search index. This is only used if | |||
# `build_search_index` is set to "true" in the Zola configuration file. | |||
in_search_index = true | |||
# Whether to render that section homepage or not. | |||
# Useful when the section is only there to organize things but is not meant | |||
# to be used directly | |||
# If set to "true", the section homepage is rendered. | |||
# Useful when the section is used to organize pages (not used directly). | |||
render = true | |||
# Whether to redirect when landing on that section. Defaults to not being set. | |||
# This determines whether to redirect when a user lands on the section. Defaults to not being set. | |||
# Useful for the same reason as `render` but when you don't want a 404 when | |||
# landing on the root section page. | |||
# Example: redirect_to = "documentation/content/overview" | |||
redirect_to = "" | |||
# Whether the section should pass its pages on to the parent section. Defaults to `false`. | |||
# If set to "true", the section will pass its pages on to the parent section. Defaults to `false`. | |||
# Useful when the section shouldn't split up the parent section, like | |||
# sections for each year under a posts section. | |||
transparent = false | |||
# Use aliases if you are moving content but want to redirect previous URLs to the | |||
# current one. This takes an array of path, not URLs. | |||
# current one. This takes an array of paths, not URLs. | |||
aliases = [] | |||
# Your own data | |||
# Your own data. | |||
[extra] | |||
``` | |||
Keep in mind that any configuration apply only to the direct pages, not to the subsections' pages. | |||
Keep in mind that any configuration options apply only to the direct pages, not to the subsections' pages. | |||
## Pagination | |||
To enable pagination for a section's pages, simply set `paginate_by` to a positive number and it will automatically | |||
paginate by this much. See [pagination template documentation](@/documentation/templates/pagination.md) for more information | |||
on what will be available in the template. | |||
To enable pagination for a section's pages, set `paginate_by` to a positive number. See | |||
[pagination template documentation](@/documentation/templates/pagination.md) for more information | |||
on what variables are available in the template. | |||
You can also change the pagination path (the word displayed while paginated in the URL, like `page/1`) | |||
by setting the `paginate_path` variable, which defaults to `page`. | |||
## Sorting | |||
It is very common for Zola templates to iterate over pages or sections | |||
to display all pages/sections a given directory. Consider a very simple | |||
to display all pages/sections in a given directory. Consider a very simple | |||
example: a `blog` directory with three files: `blog/Post_1.md`, | |||
`blog/Post_2.md`, and `blog/Post_3.md`. To iterate over these posts and | |||
`blog/Post_2.md` and `blog/Post_3.md`. To iterate over these posts and | |||
create a list of links to the posts, a simple template might look like this: | |||
```j2 | |||
@@ -120,21 +121,21 @@ create a list of links to the posts, a simple template might look like this: | |||
{% endfor %} | |||
``` | |||
This would iterate over the posts, and would do so in a specific order | |||
based on the `sort_by` variable set in the `_index.md` page for the | |||
containing section. The `sort_by` variable can be given three values: `date`, | |||
`weight`, and `none`. If no `sort_by` method is set, the pages will be | |||
sorted in the `none` order, which is not intended to be used for sorted content. | |||
This would iterate over the posts in the order specified | |||
by the `sort_by` variable set in the `_index.md` page for the corresponding | |||
section. The `sort_by` variable can be given one of three values: `date`, | |||
`weight` or `none`. If `sort_by` is not set, the pages will be | |||
sorted in the `none` order, which is not intended for sorted content. | |||
Any page that is missing the data it needs to be sorted will be ignored and | |||
won't be rendered. For example, if a page is missing the date variable the | |||
containing section sets `sort_by = "date"`, then that page will be ignored. | |||
The terminal will warn you if this is happening. | |||
won't be rendered. For example, if a page is missing the date variable and its | |||
section sets `sort_by = "date"`, then that page will be ignored. | |||
The terminal will warn you if this occurs. | |||
If several pages have the same date/weight/order, their permalink will be used | |||
to break the tie following an alphabetical order. | |||
to break the tie based on alphabetical order. | |||
## Sorting Pages | |||
## Sorting pages | |||
The `sort_by` front-matter variable can have the following values: | |||
### `date` | |||
@@ -150,24 +151,24 @@ page gets `page.lighter` and `page.heavier` variables that contain the | |||
pages with lighter and heavier weights, respectively. | |||
When iterating through pages, you may wish to use the Tera `reverse` filter, | |||
which reverses the order of the pages. Thus, after using the `reverse` filter, | |||
which reverses the order of the pages. For example, after using the `reverse` filter, | |||
pages sorted by weight will be sorted from lightest (at the top) to heaviest | |||
(at the bottom); pages sorted by date will be sorted from oldest (at the top) | |||
to newest (at the bottom). | |||
`reverse` has no effect on `page.later`/`page.earlier`/`page.heavier`/`page.lighter`. | |||
`reverse` has no effect on `page.later`/`page.earlier` or `page.heavier`/`page.lighter`. | |||
## Sorting Subsections | |||
## Sorting subsections | |||
Sorting sections is a bit less flexible: sections are always sorted by `weight`, | |||
and do not have any variables that point to the next heavier/lighter sections. | |||
and do not have variables that point to the heavier/lighter sections. | |||
Based on this, by default the lightest (lowest `weight`) subsections will be at | |||
By default, the lightest (lowest `weight`) subsections will be at | |||
the top of the list and the heaviest (highest `weight`) will be at the bottom; | |||
the `reverse` filter reverses this order. | |||
**Note**: Unlike pages, permalinks will **not** be used to break ties between | |||
equally weighted sections. Thus, if the `weight` variable for your section is not set (or if it | |||
equally weighted sections. Thus, if the `weight` variable for your section is not set (or if it | |||
is set in a way that produces ties), then your sections will be sorted in | |||
**random** order. Moreover, that order is determined at build time and will | |||
change with each site rebuild. Thus, if there is any chance that you will | |||
iterate over your sections, you should always assign them weight. | |||
iterate over your sections, you should always assign them a weight. |
@@ -3,13 +3,14 @@ title = "Shortcodes" | |||
weight = 40 | |||
+++ | |||
While Markdown is good at writing, it isn't great when you need write inline | |||
Although Markdown is good for writing, it isn't great when you need write inline | |||
HTML to add some styling for example. | |||
To solve this, Zola borrows the concept of [shortcodes](https://codex.wordpress.org/Shortcode_API) | |||
from WordPress. | |||
In our case, the shortcode corresponds to a template that is defined in the `templates/shortcodes` directory or a built-in one that can | |||
be used in a Markdown file. If you want to use something similar to shortcodes in your templates, try [Tera macros](https://tera.netlify.com/docs#macros). | |||
In our case, a shortcode corresponds to a template defined in the `templates/shortcodes` directory or | |||
a built-in one that can be used in a Markdown file. If you want to use something similar to shortcodes in your templates, try [Tera macros](https://tera.netlify.com/docs/templates/#macros). | |||
## Writing a shortcode | |||
Let's write a shortcode to embed YouTube videos as an example. | |||
@@ -28,44 +29,44 @@ following: | |||
``` | |||
This template is very straightforward: an iframe pointing to the YouTube embed URL wrapped in a `<div>`. | |||
In terms of input, it expects at least one variable: `id`. Since the other variables | |||
are in a `if` statement, we can assume they are optional. | |||
In terms of input, this shortcode expects at least one variable: `id`. Because the other variables | |||
are in an `if` statement, they are optional. | |||
That's it, Zola will now recognise this template as a shortcode named `youtube` (the filename minus the `.html` extension). | |||
That's it. Zola will now recognise this template as a shortcode named `youtube` (the filename minus the `.html` extension). | |||
The markdown renderer will wrap an inline HTML node like `<a>` or `<span>` into a paragraph. If you want to disable that, | |||
simply wrap your shortcode in a `div`. | |||
The Markdown renderer will wrap an inline HTML node such as `<a>` or `<span>` into a paragraph. | |||
If you want to disable this behaviour, wrap your shortcode in a `<div>`. | |||
Shortcodes are rendered before parsing the markdown so it doesn't have access to the table of contents. Because of that, | |||
you also cannot use the `get_page`/`get_section`/`get_taxonomy` global function. It might work while running `zola serve` because | |||
it has been loaded but it will fail during `zola build`. | |||
Shortcodes are rendered before the Markdown is parsed so they don't have access to the table of contents. Because of that, | |||
you also cannot use the `get_page`/`get_section`/`get_taxonomy` global functions. It might work while running | |||
`zola serve` because it has been loaded but it will fail during `zola build`. | |||
## Using shortcodes | |||
There are two kinds of shortcodes: | |||
- ones that do not take a body like the YouTube example above | |||
- ones that do, a quote for example | |||
- ones that do not take a body, such as the YouTube example above | |||
- ones that do, such as one that styles a quote | |||
In both cases, their arguments must be named and they will all be passed to the template. | |||
In both cases, the arguments must be named and they will all be passed to the template. | |||
Lastly, a shortcode name (and thus the corresponding `.html` file) as well as the arguments name | |||
can only contain numbers, letters and underscores, or in Regex terms the following: `[0-9A-Za-z_]`. | |||
While theoretically an argument name could be a number, it will not be possible to use it in the template in that case. | |||
Lastly, a shortcode name (and thus the corresponding `.html` file) as well as the argument names | |||
can only contain numbers, letters and underscores, or in Regex terms `[0-9A-Za-z_]`. | |||
Although theoretically an argument name could be a number, it will not be possible to use such an argument in the template. | |||
Argument values can be of 5 types: | |||
Argument values can be of one of five types: | |||
- string: surrounded by double quotes, single quotes or backticks | |||
- bool: `true` or `false` | |||
- float: a number with a `.` in it | |||
- integer: a number without a `.` in it | |||
- array: an array of any kind of values, except arrays | |||
- float: a number with a decimal point (e.g., 1.2) | |||
- integer: a whole number or its negative counterpart (e.g., 3) | |||
- array: an array of any kind of value, except arrays | |||
Malformed values will be silently ignored. | |||
Both type of shortcodes will also get either a `page` or `section` variable depending on where they were used and a `config` | |||
one. Those values will overwrite any arguments passed to a shortcode so shortcodes should not use arguments called like one | |||
of these. | |||
Both types of shortcode will also get either a `page` or `section` variable depending on where they were used | |||
and a `config` variable. These values will overwrite any arguments passed to a shortcode so these variable names | |||
should not be used as argument names in shortcodes. | |||
### Shortcodes without body | |||
@@ -86,7 +87,7 @@ Note that if you want to have some content that looks like a shortcode but not h | |||
you will need to escape it by using `{{/*` and `*/}}` instead of `{{` and `}}`. | |||
### Shortcodes with body | |||
For example, let's imagine we have the following shortcode `quote.html` template: | |||
Let's imagine that we have the following shortcode `quote.html` template: | |||
```jinja2 | |||
<blockquote> | |||
@@ -95,7 +96,7 @@ For example, let's imagine we have the following shortcode `quote.html` template | |||
</blockquote> | |||
``` | |||
We could use it in our markup file like so: | |||
We could use it in our Markdown file like so: | |||
```md | |||
As someone said: | |||
@@ -106,7 +107,7 @@ A quote | |||
``` | |||
The body of the shortcode will be automatically passed down to the rendering context as the `body` variable and needs | |||
to be in a newline. | |||
to be on a new line. | |||
If you want to have some content that looks like a shortcode but not have Zola try to render it, | |||
you will need to escape it by using `{%/*` and `*/%}` instead of `{%` and `%}`. You won't need to escape | |||
@@ -124,8 +125,8 @@ Embed a responsive player for a YouTube video. | |||
The arguments are: | |||
- `id`: the video id (mandatory) | |||
- `class`: a class to add the `div` surrounding the iframe | |||
- `autoplay`: whether to autoplay the video on load | |||
- `class`: a class to add to the `<div>` surrounding the iframe | |||
- `autoplay`: when set to "true", the video autoplays on load | |||
Usage example: | |||
@@ -147,7 +148,7 @@ Embed a player for a Vimeo video. | |||
The arguments are: | |||
- `id`: the video id (mandatory) | |||
- `class`: a class to add the `div` surrounding the iframe | |||
- `class`: a class to add to the `<div>` surrounding the iframe | |||
Usage example: | |||
@@ -167,7 +168,7 @@ Embed a player for a Streamable video. | |||
The arguments are: | |||
- `id`: the video id (mandatory) | |||
- `class`: a class to add the `div` surrounding the iframe | |||
- `class`: a class to add to the `<div>` surrounding the iframe | |||
Usage example: | |||
@@ -188,7 +189,7 @@ The arguments are: | |||
- `url`: the url to the gist (mandatory) | |||
- `file`: by default, the shortcode will pull every file from the URL unless a specific filename is requested | |||
- `class`: a class to add the `div` surrounding the iframe | |||
- `class`: a class to add to the `<div>` surrounding the iframe | |||
Usage example: | |||
@@ -17,115 +17,114 @@ let highlight = true; | |||
```` | |||
You can replace the `rust` by the language you want to highlight or not put anything to get it | |||
You can replace `rust` with another language or not put anything to get the text | |||
interpreted as plain text. | |||
Here is a full list of the supported languages and the short names you can use: | |||
Here is a full list of supported languages and their short names: | |||
``` | |||
- Plain Text -> ["txt"] | |||
- Assembly x86 (NASM) -> ["asm", "inc", "nasm"] | |||
- Crystal -> ["cr"] | |||
- Dart -> ["dart"] | |||
- Elixir -> ["ex", "exs"] | |||
- fsharp -> ["fs"] | |||
- Handlebars -> ["handlebars", "handlebars.html", "hbr", "hbrs", "hbs", "hdbs", "hjs", "mu", "mustache", "rac", "stache", "template", "tmpl"] | |||
- Jinja2 -> ["j2", "jinja2"] | |||
- Julia -> ["jl"] | |||
- Kotlin -> ["kt", "kts"] | |||
- Less -> ["less", "css.less"] | |||
- MiniZinc (MZN) -> ["mzn", "dzn"] | |||
- Nim -> ["nim", "nims"] | |||
- ASP -> ["asa"] | |||
- HTML (ASP) -> ["asp"] | |||
- ActionScript -> ["as"] | |||
- AppleScript -> ["applescript", "script editor"] | |||
- ASP -> ["asa"] | |||
- Assembly x86 (NASM) -> ["asm", "inc", "nasm"] | |||
- Batch File -> ["bat", "cmd"] | |||
- NAnt Build File -> ["build"] | |||
- C# -> ["cs", "csx"] | |||
- C++ -> ["cpp", "cc", "cp", "cxx", "c++", "C", "h", "hh", "hpp", "hxx", "h++", "inl", "ipp"] | |||
- BibTeX -> ["bib"] | |||
- Bourne Again Shell (bash) -> [".bash_aliases", ".bash_completions", ".bash_functions", ".bash_login", ".bash_logout", ".bash_profile", ".bash_variables", ".bashrc", ".profile", ".textmate_init", ".zshrc", "bash", "fish", "sh", "zsh"] | |||
- C -> ["c", "h"] | |||
- CSS -> ["css", "css.erb", "css.liquid"] | |||
- C# -> ["cs", "csx"] | |||
- C++ -> ["C", "c++", "cc", "cp", "cpp", "cxx", "h", "h++", "hh", "hpp", "hxx", "inl", "ipp"] | |||
- Clojure -> ["clj"] | |||
- CMake -> ["CMakeLists.txt", "cmake"] | |||
- CMake C Header -> ["h.in"] | |||
- CMake C++ Header -> ["h++.in", "hh.in", "hpp.in", "hxx.in"] | |||
- CMakeCache -> ["CMakeCache.txt"] | |||
- Crystal -> ["cr"] | |||
- CSS -> ["css", "css.erb", "css.liquid"] | |||
- D -> ["d", "di"] | |||
- Dart -> ["dart"] | |||
- Diff -> ["diff", "patch"] | |||
- Erlang -> ["erl", "hrl", "Emakefile", "emakefile"] | |||
- HTML (Erlang) -> ["yaws"] | |||
- Git Attributes -> ["attributes", "gitattributes", ".gitattributes"] | |||
- Elixir -> ["ex", "exs"] | |||
- Elm -> ["elm"] | |||
- Erlang -> ["Emakefile", "emakefile", "erl", "hrl"] | |||
- fsharp -> ["fs"] | |||
- Generic Config -> [".dircolors", ".gitattributes", ".gitignore", ".gitmodules", ".inputrc", "Doxyfile", "cfg", "conf", "config", "dircolors", "gitattributes", "gitignore", "gitmodules", "ini", "inputrc", "mak", "mk", "pro"] | |||
- Git Attributes -> [".gitattributes", "attributes", "gitattributes"] | |||
- Git Commit -> ["COMMIT_EDITMSG", "MERGE_MSG", "TAG_EDITMSG"] | |||
- Git Config -> ["gitconfig", ".gitconfig", ".gitmodules"] | |||
- Git Ignore -> ["exclude", "gitignore", ".gitignore"] | |||
- Git Config -> [".gitconfig", ".gitmodules", "gitconfig"] | |||
- Git Ignore -> [".gitignore", "exclude", "gitignore"] | |||
- Git Link -> [".git"] | |||
- Git Log -> ["gitlog"] | |||
- Git Rebase Todo -> ["git-rebase-todo"] | |||
- Go -> ["go"] | |||
- Graphviz (DOT) -> ["dot", "DOT", "gv"] | |||
- Groovy -> ["groovy", "gvy", "gradle", "Jenkinsfile"] | |||
- HTML -> ["html", "htm", "shtml", "xhtml"] | |||
- Graphviz (DOT) -> ["DOT", "dot", "gv"] | |||
- Groovy -> ["Jenkinsfile", "gradle", "groovy", "gvy"] | |||
- Handlebars -> ["handlebars", "handlebars.html", "hbr", "hbrs", "hbs", "hdbs", "hjs", "mu", "mustache", "rac", "stache", "template", "tmpl"] | |||
- Haskell -> ["hs"] | |||
- Literate Haskell -> ["lhs"] | |||
- Java Server Page (JSP) -> ["jsp"] | |||
- Java -> ["java", "bsh"] | |||
- HTML -> ["htm", "html", "shtml", "xhtml"] | |||
- HTML (ASP) -> ["asp"] | |||
- HTML (Erlang) -> ["yaws"] | |||
- HTML (Rails) -> ["erb", "html.erb", "rails", "rhtml"] | |||
- HTML (Tcl) -> ["adp"] | |||
- Java -> ["bsh", "java"] | |||
- Java Properties -> ["properties"] | |||
- JSON -> ["json", "sublime-settings", "sublime-menu", "sublime-keymap", "sublime-mousemap", "sublime-theme", "sublime-build", "sublime-project", "sublime-completions", "sublime-commands", "sublime-macro", "sublime-color-scheme"] | |||
- JavaScript -> ["js", "htc"] | |||
- BibTeX -> ["bib"] | |||
- LaTeX -> ["tex", "ltx"] | |||
- TeX -> ["sty", "cls"] | |||
- Lisp -> ["lisp", "cl", "clisp", "l", "mud", "el", "scm", "ss", "lsp", "fasl"] | |||
- Java Server Page (JSP) -> ["jsp"] | |||
- JavaScript -> ["htc", "js"] | |||
- JavaScript (Rails) -> ["js.erb"] | |||
- Jinja2 -> ["j2", "jinja2"] | |||
- JSON -> ["json", "sublime-build", "sublime-color-scheme", "sublime-commands", "sublime-completions", "sublime-keymap", "sublime-macro", "sublime-menu", "sublime-mousemap", "sublime-project", "sublime-settings", "sublime-theme"] | |||
- Julia -> ["jl"] | |||
- Kotlin -> ["kt", "kts"] | |||
- LaTeX -> ["ltx", "tex"] | |||
- Less -> ["css.less", "less"] | |||
- Linker Script -> ["ld"] | |||
- Lisp -> ["cl", "clisp", "el", "fasl", "l", "lisp", "lsp", "mud", "scm", "ss"] | |||
- Literate Haskell -> ["lhs"] | |||
- Lua -> ["lua"] | |||
- Makefile -> ["make", "GNUmakefile", "makefile", "Makefile", "makefile.am", "Makefile.am", "makefile.in", "Makefile.in", "OCamlMakefile", "mak", "mk"] | |||
- Markdown -> ["md", "mdown", "markdown", "markdn"] | |||
- Makefile -> ["GNUmakefile", "Makefile", "Makefile.am", "Makefile.in", "OCamlMakefile", "mak", "make", "makefile", "makefile.am", "makefile.in", "mk"] | |||
- Markdown -> ["markdn", "markdown", "md", "mdown"] | |||
- MATLAB -> ["matlab"] | |||
- MiniZinc (MZN) -> ["dzn", "mzn"] | |||
- NAnt Build File -> ["build"] | |||
- Nim -> ["nim", "nims"] | |||
- Nix -> ["nix"] | |||
- Objective-C -> ["h", "m"] | |||
- Objective-C++ -> ["M", "h", "mm"] | |||
- OCaml -> ["ml", "mli"] | |||
- OCamllex -> ["mll"] | |||
- OCamlyacc -> ["mly"] | |||
- Objective-C++ -> ["mm", "M", "h"] | |||
- Objective-C -> ["m", "h"] | |||
- Pascal -> ["dpr", "p", "pas"] | |||
- Perl -> ["PL", "pl", "pm", "pod", "t"] | |||
- PHP -> ["php", "php3", "php4", "php5", "php7", "phps", "phpt", "phtml"] | |||
- Pascal -> ["pas", "p", "dpr"] | |||
- Perl -> ["pl", "pm", "pod", "t", "PL"] | |||
- Python -> ["py", "py3", "pyw", "pyi", "pyx", "pyx.in", "pxd", "pxd.in", "pxi", "pxi.in", "rpy", "cpy", "SConstruct", "Sconstruct", "sconstruct", "SConscript", "gyp", "gypi", "Snakefile", "wscript"] | |||
- R -> ["R", "r", "s", "S", "Rprofile"] | |||
- Plain Text -> ["txt"] | |||
- PowerShell -> ["ps1", "psd1", "psm1"] | |||
- Python -> ["SConscript", "SConstruct", "Sconstruct", "Snakefile", "cpy", "gyp", "gypi", "pxd", "pxd.in", "pxi", "pxi.in", "py", "py3", "pyi", "pyw", "pyx", "pyx.in", "rpy", "sconstruct", "wscript"] | |||
- R -> ["R", "Rprofile", "S", "r", "s"] | |||
- Rd (R Documentation) -> ["rd"] | |||
- HTML (Rails) -> ["rails", "rhtml", "erb", "html.erb"] | |||
- JavaScript (Rails) -> ["js.erb"] | |||
- Ruby Haml -> ["haml", "sass"] | |||
- Ruby on Rails -> ["rxml", "builder"] | |||
- SQL (Rails) -> ["erbsql", "sql.erb"] | |||
- Reason -> ["re", "rei"] | |||
- Regular Expression -> ["re"] | |||
- reStructuredText -> ["rst", "rest"] | |||
- Ruby -> ["rb", "Appfile", "Appraisals", "Berksfile", "Brewfile", "capfile", "cgi", "Cheffile", "config.ru", "Deliverfile", "Fastfile", "fcgi", "Gemfile", "gemspec", "Guardfile", "irbrc", "jbuilder", "podspec", "prawn", "rabl", "rake", "Rakefile", "Rantfile", "rbx", "rjs", "ruby.rail", "Scanfile", "simplecov", "Snapfile", "thor", "Thorfile", "Vagrantfile"] | |||
- reStructuredText -> ["rest", "rst"] | |||
- Ruby -> ["Appfile", "Appraisals", "Berksfile", "Brewfile", "Cheffile", "Deliverfile", "Fastfile", "Gemfile", "Guardfile", "Rakefile", "Rantfile", "Scanfile", "Snapfile", "Thorfile", "Vagrantfile", "capfile", "cgi", "config.ru", "fcgi", "gemspec", "irbrc", "jbuilder", "podspec", "prawn", "rabl", "rake", "rb", "rbx", "rjs", "ruby.rail", "simplecov", "thor"] | |||
- Ruby Haml -> ["haml", "sass"] | |||
- Ruby on Rails -> ["builder", "rxml"] | |||
- Rust -> ["rs"] | |||
- SQL -> ["sql", "ddl", "dml"] | |||
- Scala -> ["scala", "sbt"] | |||
- Bourne Again Shell (bash) -> ["sh", "bash", "zsh", "fish", ".bash_aliases", ".bash_completions", ".bash_functions", ".bash_login", ".bash_logout", ".bash_profile", ".bash_variables", ".bashrc", ".profile", ".textmate_init", ".zshrc"] | |||
- HTML (Tcl) -> ["adp"] | |||
- Tcl -> ["tcl"] | |||
- Textile -> ["textile"] | |||
- XML -> ["xml", "xsd", "xslt", "tld", "dtml", "rss", "opml", "svg"] | |||
- YAML -> ["yaml", "yml", "sublime-syntax"] | |||
- PowerShell -> ["ps1", "psm1", "psd1"] | |||
- Scala -> ["sbt", "scala"] | |||
- SQL -> ["ddl", "dml", "sql"] | |||
- SQL (Rails) -> ["erbsql", "sql.erb"] | |||
- SWI-Prolog -> ["pro"] | |||
- Reason -> ["re", "rei"] | |||
- CMake C Header -> ["h.in"] | |||
- CMake C++ Header -> ["hh.in", "hpp.in", "hxx.in", "h++.in"] | |||
- CMake -> ["CMakeLists.txt", "cmake"] | |||
- CMakeCache -> ["CMakeCache.txt"] | |||
- Generic Config -> ["cfg", "conf", "config", "ini", "pro", "mak", "mk", "Doxyfile", "inputrc", ".inputrc", "dircolors", ".dircolors", "gitmodules", ".gitmodules", "gitignore", ".gitignore", "gitattributes", ".gitattributes"] | |||
- Elm -> ["elm"] | |||
- Linker Script -> ["ld"] | |||
- Swift -> ["swift"] | |||
- TOML -> ["toml", "tml"] | |||
- Tcl -> ["tcl"] | |||
- TeX -> ["cls", "sty"] | |||
- Textile -> ["textile"] | |||
- TOML -> ["Cargo.lock", "Gopkg.lock", "Pipfile", "tml", "toml"] | |||
- TypeScript -> ["ts"] | |||
- TypeScriptReact -> ["tsx"] | |||
- VimL -> ["vim"] | |||
- Nix -> ["nix"] | |||
- TOML -> ["toml", "tml", "Cargo.lock", "Gopkg.lock", "Pipfile"] | |||
- XML -> ["dtml", "opml", "rss", "svg", "tld", "xml", "xsd", "xslt"] | |||
- YAML -> ["sublime-syntax", "yaml", "yml"] | |||
``` | |||
If you want to highlight a language not on that list, please open an issue or a pull request on the [Zola repo](https://github.com/getzola/zola). | |||
Alternatively, the `extra_syntaxes` config option can be used to add additional syntax files. | |||
If you want to highlight a language not on this list, please open an issue or a pull request on the [Zola repo](https://github.com/getzola/zola). | |||
Alternatively, the `extra_syntaxes` configuration option can be used to add additional syntax files. | |||
If your site source is laid out as follows: | |||
@@ -144,4 +143,4 @@ If your site source is laid out as follows: | |||
└── ... | |||
``` | |||
you would set your `extra_syntaxes` to `["syntaxes", "syntaxes/Sublime-Language1"]` in order to load `lang1.sublime-syntax` and `lang2.sublime-syntax`. | |||
you would set your `extra_syntaxes` to `["syntaxes", "syntaxes/Sublime-Language1"]` to load `lang1.sublime-syntax` and `lang2.sublime-syntax`. |
@@ -5,15 +5,15 @@ weight = 60 | |||
Each page/section will automatically generate a table of contents for itself based on the headers present. | |||
It is available in the template through the `toc` variable. | |||
It is available in the template through the `page.toc` or `section.toc` variable. | |||
You can view the [template variables](@/documentation/templates/pages-sections.md#table-of-contents) | |||
documentation for information on its structure. | |||
Here is an example of using that field to render a 2-level table of contents: | |||
Here is an example of using that field to render a two-level table of contents: | |||
```jinja2 | |||
<ul> | |||
{% for h1 in toc %} | |||
{% for h1 in page.toc %} | |||
<li> | |||
<a href="{{h1.permalink | safe}}">{{ h1.title }}</a> | |||
{% if h1.children %} | |||
@@ -30,7 +30,7 @@ Here is an example of using that field to render a 2-level table of contents: | |||
</ul> | |||
``` | |||
While headers are neatly ordered in that example, it will work just as well with disjoint headers. | |||
While headers are neatly ordered in this example, it will work just as well with disjoint headers. | |||
Note that all existing HTML tags from the title will NOT be present in the table of contents to | |||
avoid various issues. |
@@ -5,32 +5,59 @@ weight = 90 | |||
Zola has built-in support for taxonomies. | |||
The first step is to define the taxonomies in your [config.toml](@/documentation/getting-started/configuration.md). | |||
## Configuration | |||
A taxonomy has 5 variables: | |||
A taxonomy has five variables: | |||
- `name`: a required string that will be used in the URLs, usually the plural version (i.e. tags, categories etc) | |||
- `name`: a required string that will be used in the URLs, usually the plural version (i.e., tags, categories, etc.) | |||
- `paginate_by`: if this is set to a number, each term page will be paginated by this much. | |||
- `paginate_path`: if set, will be the path used by paginated page and the page number will be appended after it. | |||
For example the default would be page/1 | |||
- `rss`: if set to `true`, a RSS feed will be generated for each individual term. | |||
- `paginate_path`: if set, this path will be used by the paginated page and the page number will be appended after it. | |||
For example the default would be page/1. | |||
- `rss`: if set to `true`, an RSS feed will be generated for each term. | |||
- `lang`: only set this if you are making a multilingual site and want to indicate which language this taxonomy is for | |||
Once this is done, you can then set taxonomies in your content and Zola will pick | |||
them up: | |||
**Example 1:** (one language) | |||
```toml | |||
taxonomies = [ name = "categories", rss = true ] | |||
``` | |||
**Example 2:** (multilingual site) | |||
```toml | |||
taxonomies = [ | |||
{name = "tags", lang = "fr"}, | |||
{name = "tags", lang = "eo"}, | |||
{name = "tags", lang = "en"}, | |||
] | |||
``` | |||
## Using taxonomies | |||
Once the configuration is done, you can then set taxonomies in your content and Zola will pick them up: | |||
**Example:** | |||
```toml | |||
+++ | |||
... | |||
title = "Writing a static-site generator in Rust" | |||
date = 2019-08-15 | |||
[taxonomies] | |||
tags = ["rust", "web"] | |||
categories = ["programming"] | |||
+++ | |||
``` | |||
The taxonomy pages are available at the following paths: | |||
## Output paths | |||
In a similar manner to how section and pages calculate their output path: | |||
- the taxonomy name is never slugified | |||
- the taxonomy entry (eg. as specific tag) is slugified when `slugify_paths` is enabled in the configuration | |||
The taxonomy pages are then available at the following paths: | |||
```plain | |||
$BASE_URL/$NAME/ | |||
$BASE_URL/$NAME/$SLUG | |||
$BASE_URL/$NAME/ (taxonomy) | |||
$BASE_URL/$NAME/$SLUG (taxonomy entry) | |||
``` | |||
@@ -3,25 +3,31 @@ title = "GitHub Pages" | |||
weight = 30 | |||
+++ | |||
By default, GitHub Pages uses Jekyll (A ruby based static site generator), | |||
but you can also publish any generated files provided you have an `index.html` file in the root of a branch called `gh-pages` or `master`, in addition you can also publish from a `docs` directory in your repository. That branch name can also be manually changed in the settings of a repository. **However** this only applies to publishing in a custom domain, i.e. if you want to publish to a GitHub provided web service under the `github.io` domain, you can **only** use the `master` branch of your repository as explained [here](https://help.github.com/en/articles/configuring-a-publishing-source-for-github-pages), so we will focus on the method which will work regardless of the domain. | |||
By default, GitHub Pages uses Jekyll (a ruby based static site generator), | |||
but you can also publish any generated files provided you have an `index.html` file in the root of a branch called | |||
`gh-pages` or `master`. In addition you can publish from a `docs` directory in your repository. That branch name can | |||
also be manually changed in the settings of a repository. **However**, this only applies to publishing in a custom domain, | |||
i.e., if you want to publish to a GitHub-provided web service under the `github.io` domain, you can **only** use the | |||
`master` branch of your repository, as explained | |||
[here](https://help.github.com/en/articles/configuring-a-publishing-source-for-github-pages), | |||
so we will focus on the method that will work regardless of the domain. | |||
We can use any CI server to build and deploy our site. For example: | |||
We can use any continuous integration (CI) server to build and deploy our site. For example: | |||
* [Github Actions](https://github.com/shalzz/zola-deploy-action) | |||
* [Travis CI](#travis-ci) | |||
## Travis CI | |||
We are going to use [TravisCI](https://travis-ci.org) to automatically publish the site. If you are not using Travis already, | |||
you will need to login with the GitHub OAuth and activate Travis for the repository. | |||
We are going to use [Travis CI](https://travis-ci.org) to automatically publish the site. If you are not using Travis | |||
already, you will need to login with the GitHub OAuth and activate Travis for the repository. | |||
Don't forget to also check if your repository allows GitHub Pages in its settings. | |||
## Ensure Travis can access your theme | |||
## Ensure that Travis can access your theme | |||
Depending on how you added your theme Travis may not exactly know how to access | |||
it. The best way to ensure it will have full access to the theme is to use git | |||
submodules. When doing this ensure you are using the `https` version of the URL. | |||
Depending on how you added your theme, Travis may not know how to access | |||
it. The best way to ensure that it will have full access to the theme is to use git | |||
submodules. When doing this, ensure that you are using the `https` version of the URL. | |||
```shell | |||
$ git submodule add {THEME_URL} themes/{THEME_NAME} | |||
@@ -29,16 +35,17 @@ $ git submodule add {THEME_URL} themes/{THEME_NAME} | |||
## Allowing Travis to push to GitHub | |||
Before pushing anything, Travis needs a Github private access key in order to make changes to your repository. | |||
If you're already logged in to your account, just click [here](https://github.com/settings/tokens) to go to your tokens page. | |||
Before pushing anything, Travis needs a Github private access key to make changes to your repository. | |||
If you're already logged in to your account, just click [here](https://github.com/settings/tokens) to go to | |||
your tokens page. | |||
Otherwise, navigate to `Settings > Developer Settings > Personal Access Tokens`. | |||
Generate a new token, and give it any description you'd like. | |||
Generate a new token and give it any description you'd like. | |||
Under the "Select Scopes" section, give it repo permissions. Click "Generate token" to finish up. | |||
Your token will now be visible! | |||
Your token will now be visible. | |||
Copy it into your clipboard and head back to Travis. | |||
Once on Travis, click on your project, and navigate to "Settings". Scroll down to "Environment Variables" and input a name of `GH_TOKEN` with a value of your access token. | |||
Make sure "Display value in build log" is off, and then click add. Now Travis has access to your repository. | |||
Make sure that "Display value in build log" is off, and then click add. Now Travis has access to your repository. | |||
## Setting up Travis | |||
@@ -68,7 +75,7 @@ after_success: | | |||
git push -fq https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git master | |||
``` | |||
If your site is using a custom domain, you will need to mention it in the `ghp-import` command: `ghp-import -c vaporsoft.net -n public` | |||
for example. | |||
If your site is using a custom domain, you will need to mention it in the `ghp-import` command: | |||
`ghp-import -c vaporsoft.net -n public` for example. | |||
Credits: this page is based on the article https://vaporsoft.net/publishing-gutenberg-to-github/ |
@@ -9,7 +9,7 @@ We are going to use the GitLab CI runner to automatically publish the site (this | |||
Your repository needs to be set up to be a user or group website. This means the name of the repository has to be in the correct format. | |||
For example, under your username, `john`, you have to create a project called `john.gitlab.io`. Your project URL will be `https://gitlab.com/john/john.gitlab.io`. Once you enable GitLab Pages for your project, your website will be published under `https://john.gitlab.io`. | |||
For example, assuming that the username is `john`, you have to create a project called `john.gitlab.io`. Your project URL will be `https://gitlab.com/john/john.gitlab.io`. Once you enable GitLab Pages for your project, your website will be published under `https://john.gitlab.io`. | |||
Under your group `websites`, you created a project called `websites.gitlab.io`. Your project’s URL will be `https://gitlab.com/websites/websites.gitlab.io`. Once you enable GitLab Pages for your project, your website will be published under `https://websites.gitlab.io`. | |||
@@ -18,23 +18,23 @@ This guide assumes that your zola project is located in the root of your reposit | |||
## Ensuring that the CI runner can access your theme | |||
Depending on how you added your theme your repository may not contain it. The best way to ensure the theme will be added is to use | |||
submodules. When doing this ensure you are using the `https` version of the URL. | |||
Depending on how you added your theme, your repository may not contain it. The best way to ensure that the theme will | |||
be added is to use submodules. When doing this, ensure that you are using the `https` version of the URL. | |||
```shell | |||
$ git submodule add {THEME_URL} themes/{THEME_NAME} | |||
``` | |||
For example, this could look like | |||
For example, this could look like: | |||
```shell | |||
$ git submodule add https://github.com/getzola/hyde.git themes/hyde | |||
``` | |||
## Setting up the GitLab CI/CD Runner | |||
The second step is to tell the gitlab continous integration runner how to create the gitlab page. | |||
The second step is to tell the GitLab continous integration runner how to create the GitLab page. | |||
To do this, create a file called `.gitlab-ci.yml` in the root directory of your repository. | |||
To do this, create a file called `.gitlab-ci.yml` in the root directory of your repository. | |||
```yaml | |||
variables: | |||
@@ -62,7 +62,8 @@ pages: | |||
- master | |||
``` | |||
Push this new file and... Tada! You're done! If you navigate to `settings > pages` you should be able to see something like this: | |||
Push this new file and ... Tada! You're done! If you navigate to `settings > pages`, you should be able to see | |||
something like this: | |||
> Congratulations! Your pages are served under: | |||
https://john.gitlab.io | |||
@@ -4,12 +4,12 @@ weight = 20 | |||
+++ | |||
Netlify provides best practices like SSL, CDN distribution, caching and continuous deployment | |||
with no effort. This very site is hosted by Netlify and automatically deployed on commits. | |||
with no effort. This site is hosted by Netlify and automatically deployed on commits. | |||
If you don't have an account with Netlify, you can [sign up](https://app.netlify.com) for one. | |||
## Automatic Deploys | |||
## Automatic deploys | |||
Once you are in the admin interface, you can add a site from a Git provider (GitHub, GitLab or Bitbucket). At the end | |||
of this process, you can select the deploy settings for the project: | |||
@@ -20,27 +20,27 @@ Once you are in the admin interface, you can add a site from a Git provider (Git | |||
- Environment variables: `ZOLA_VERSION` with for example `0.8.0` as value | |||
With this setup, your site should be automatically deployed on every commit on master. For `ZOLA_VERSION`, you may | |||
use any of the tagged `release` versions in the GitHub repository — Netlify will automatically fetch the tagged version | |||
use any of the tagged `release` versions in the GitHub repository. Netlify will automatically fetch the tagged version | |||
and use it to build your site. | |||
However, if you want to use everything that Netlify gives you, you should also publish temporary sites for pull requests. | |||
This is done by adding the following `netlify.toml` file in your repository and removing the build command/publish directory in | |||
the admin interface. | |||
This is done by adding the following `netlify.toml` file in your repository and removing the build command/publish | |||
directory in the admin interface. | |||
```toml | |||
[build] | |||
# assuming the Zola site is in a docs folder, if it isn't you don't need | |||
# to have a `base` variable but you do need the `publish` and `command` | |||
# This assumes that the Zola site is in a docs folder. If it isn't, you don't need | |||
# to have a `base` variable but you do need the `publish` and `command` variables. | |||
base = "docs" | |||
publish = "docs/public" | |||
command = "zola build" | |||
[build.environment] | |||
# Set the version name that you want to use and Netlify will automatically use it | |||
# Set the version name that you want to use and Netlify will automatically use it. | |||
ZOLA_VERSION = "0.9.0" | |||
# The magic for deploying previews of branches | |||
# The magic for deploying previews of branches. | |||
# We need to override the base url with whatever url Netlify assigns to our | |||
# preview site. We do this using the Netlify environment variable | |||
# `$DEPLOY_PRIME_URL`. | |||
@@ -49,17 +49,18 @@ ZOLA_VERSION = "0.9.0" | |||
command = "zola build --base-url $DEPLOY_PRIME_URL" | |||
``` | |||
## Manual Deploys | |||
## Manual deploys | |||
If you would prefer to use a version of Zola that isn't a tagged release (for example, after having built Zola from | |||
source and made modifications), then you will need to manually deploy your `public` folder to Netlify. You can do this through | |||
Netlify's web GUI or via the command line. | |||
source and made modifications), then you will need to manually deploy your `public` folder to Netlify. You can do | |||
this through Netlify's web GUI or via the command line. | |||
For a command-line manual deploy, follow these steps: | |||
1. Generate a `Personal Access Token` from the settings section of your Netlify account (*not* an OAuth Application) | |||
2. Build your site with `zola build` | |||
3. Create a zip folder containing the `public` directory | |||
4. Run the `curl` command below, filling in your values for PERSONAL_ACCESS_TOKEN_FROM_STEP_1, FILE_NAME.zip and SITE_NAME | |||
5. (Optional) delete the zip folder | |||
1. Generate a `Personal Access Token` from the settings section of your Netlify account (*not* an OAuth Application). | |||
2. Build your site with `zola build`. | |||
3. Create a zip folder containing the `public` directory. | |||
4. Run the `curl` command below, filling in your values for PERSONAL_ACCESS_TOKEN_FROM_STEP_1, FILE_NAME.zip | |||
and SITE_NAME. | |||
5. (Optional) delete the zip folder. | |||
```bash | |||
curl -H "Content-Type: application/zip" \ | |||
@@ -5,20 +5,20 @@ weight = 2 | |||
Zola only has 4 commands: `init`, `build`, `serve` and `check`. | |||
You can view the help of the whole program by running `zola --help` and | |||
the command help by running `zola <cmd> --help`. | |||
You can view the help for the whole program by running `zola --help` and | |||
that for a specific command by running `zola <cmd> --help`. | |||
## init | |||
Creates the directory structure used by Zola at the given directory after asking a few basic configuration questions. | |||
Any choices made during those prompts can easily be changed by modifying the `config.toml`. | |||
Any choices made during these prompts can be easily changed by modifying `config.toml`. | |||
```bash | |||
$ zola init my_site | |||
$ zola init | |||
``` | |||
If the `my_site` folder already exists, Zola will only populate it if it does not contain non-hidden files (dotfiles are ignored). If no `my_site` argument is passed, Zola will try to populate the current directory. | |||
If the `my_site` directory already exists, Zola will only populate it if it contains only hidden files (dotfiles are ignored). If no `my_site` argument is passed, Zola will try to populate the current directory. | |||
You can initialize a git repository and a Zola site directly from within a new folder: | |||
@@ -29,7 +29,7 @@ $ zola init | |||
## build | |||
This will build the whole site in the `public` directory after deleting it. | |||
This will build the whole site in the `public` directory (if this directory already exists, it is overwritten). | |||
```bash | |||
$ zola build | |||
@@ -44,34 +44,38 @@ $ zola build --base-url $DEPLOY_URL | |||
This is useful for example when you want to deploy previews of a site to a dynamic URL, such as Netlify | |||
deploy previews. | |||
You can override the default output directory 'public' by passing a other value to the `output-dir` flag. | |||
You can override the default output directory `public` by passing another value to the `output-dir` flag. | |||
```bash | |||
$ zola build --output-dir $DOCUMENT_ROOT | |||
``` | |||
You can also point to another config file than `config.toml` like so - the position of the `config` option is important: | |||
You can point to a config file other than `config.toml` like so (note that the position of the `config` option is important): | |||
```bash | |||
$ zola --config config.staging.toml build | |||
``` | |||
By defaults, drafts are not loaded. If you wish to include them, pass the `--drafts` flag. | |||
You can also process a project from a different directory with the `root` flag. If building a project 'out-of-tree' with the `root` flag, you may want to combine it with the `output-dir` flag. (Note that like `config`, the position is important): | |||
```bash | |||
$ zola --root /path/to/project build | |||
``` | |||
By default, drafts are not loaded. If you wish to include them, pass the `--drafts` flag. | |||
## serve | |||
This will build and serve the site using a local server. You can also specify | |||
the interface/port combination to use if you want something different than the default (`127.0.0.1:1111`). | |||
You can also specify different addresses for the interface and base_url using `-u`/`--base-url`, for example | |||
if you are running zola in a Docker container. | |||
You can also specify different addresses for the interface and base_url using `--interface` and `-u`/`--base-url`, respectively, if for example you are running Zola in a Docker container. | |||
Use the `--open` flag to automatically open the locally hosted instance in your | |||
web browser. | |||
In the event you don't want zola to run a local webserver, you can use the `--watch-only` flag. | |||
In the event you don't want Zola to run a local webserver, you can use the `--watch-only` flag. | |||
Before starting, it will delete the public directory to ensure it starts from a clean slate. | |||
Before starting, Zola will delete the `public` directory to start from a clean slate. | |||
```bash | |||
$ zola serve | |||
@@ -84,40 +88,40 @@ $ zola serve --watch-only | |||
$ zola serve --open | |||
``` | |||
The serve command will watch all your content and will provide live reload, without | |||
hard refresh if possible. | |||
The serve command will watch all your content and provide live reload without | |||
a hard refresh if possible. | |||
Zola does a best-effort to live reload but some changes cannot be handled automatically. If you | |||
fail to see your change or get a weird error, try to restart `zola serve`. | |||
Some changes cannot be handled automatically and thus live reload may not always work. If you | |||
fail to see your change or get an error, try restarting `zola serve`. | |||
You can also point to another config file than `config.toml` like so - the position of the `config` option is important: | |||
You can also point to a config file other than `config.toml` like so (note that the position of the `config` option is important): | |||
```bash | |||
$ zola --config config.staging.toml serve | |||
``` | |||
By defaults, drafts are not loaded. If you wish to include them, pass the `--drafts` flag. | |||
By default, drafts are not loaded. If you wish to include them, pass the `--drafts` flag. | |||
### check | |||
The check subcommand will try to build all pages just like the build command would, but without writing any of the | |||
results to disk. Additionally, it will also check all external links present in Markdown files by trying to fetch | |||
them (links present in the template files will not be checked). | |||
results to disk. Additionally, it will also check all external links in Markdown files by trying to fetch | |||
them (links in the template files are not checked). | |||
By defaults, drafts are not loaded. If you wish to include them, pass the `--drafts` flag. | |||
By default, drafts are not loaded. If you wish to include them, pass the `--drafts` flag. | |||
## Colored output | |||
Any of the three commands will emit colored output if your terminal supports it. | |||
Colored output is used if your terminal supports it. | |||
*Note*: coloring is automatically disabled when the output is redirected to a pipe or a file (ie. when the standard output is not a TTY). | |||
*Note*: coloring is automatically disabled when the output is redirected to a pipe or a file (i.e., when the standard output is not a TTY). | |||
You can disable this behavior by exporting one of the two following environment variables: | |||
You can disable this behavior by exporting one of the following two environment variables: | |||
- `NO_COLOR` (the value does not matter) | |||
- `CLICOLOR=0` | |||
Should you want to force the use of colors, you can set the following environment variable: | |||
To force the use of colors, you can set the following environment variable: | |||
- `CLICOLOR_FORCE=1` |
@@ -1,53 +1,53 @@ | |||
+++ | |||
title = "Configuration" | |||
weight = 4 | |||
weight = 40 | |||
+++ | |||
The default configuration will be enough to get Zola running locally but not more than that. | |||
It follows the philosophy of only paying for what you need: almost everything is turned off by default. | |||
The default configuration is sufficient to get Zola running locally but not more than that. | |||
It follows the philosophy of paying for only what you need; almost everything is turned off by default. | |||
To change the config, edit the `config.toml` file. | |||
If you are not familiar with TOML, have a look at [the TOML Spec](https://github.com/toml-lang/toml) | |||
to learn about it. | |||
To change the configuration, edit the `config.toml` file. | |||
If you are not familiar with TOML, have a look at [the TOML spec](https://github.com/toml-lang/toml). | |||
Only one variable - `base_url` - is mandatory, everything else is optional. You can find all variables | |||
used by Zola config as well as their default values below: | |||
Only the `base_url` variable is mandatory; everything else is optional. All configuration variables | |||
used by Zola as well as their default values are listed below: | |||
```toml | |||
# Base URL of the site, the only required config argument | |||
# The base URL of the site; the only required configuration variable. | |||
base_url = "mywebsite.com" | |||
# Used in RSS by default | |||
# The site title and description; used in RSS by default. | |||
title = "" | |||
description = "" | |||
# The default language, used in RSS | |||
# The default language; used in RSS. | |||
default_language = "en" | |||
# Theme name to use | |||
# The site theme to use. | |||
theme = "" | |||
# Highlight all code blocks found | |||
# When set to "true", all code blocks are highlighted. | |||
highlight_code = false | |||
# Which theme to use for the code highlighting. | |||
# See below for list of accepted values | |||
# The theme to use for code highlighting. | |||
# See below for list of allowed values. | |||
highlight_theme = "base16-ocean-dark" | |||
# Whether to generate a RSS feed automatically | |||
# When set to "true", an RSS feed is automatically generated. | |||
generate_rss = false | |||
# The number of articles to include in the RSS feed. Will include all items if | |||
# not set (the default). | |||
# The number of articles to include in the RSS feed. All items are included if | |||
# this limit is not set (the default). | |||
# rss_limit = 20 | |||
# Whether to copy or hardlink files in static/ directory. Useful for sites | |||
# whose static files are large. Note that for this to work, both static/ and | |||
# output directory need to be on the same filesystem. Also, theme's static/ | |||
# files are always copies, regardles of this setting. False by default. | |||
# When set to "true", files in the `static` directory are hard-linked. Useful for large | |||
# static files. Note that for this to work, both `static` and the | |||
# output directory need to be on the same filesystem. Note that the theme's `static` | |||
# files are always copied, regardles of this setting. | |||
# hard_link_static = false | |||
# The taxonomies to be rendered for that site and their configuration | |||
# The taxonomies to be rendered for the site and their configuration. | |||
# Example: | |||
# taxonomies = [ | |||
# {name = "tags", rss = true}, # each tag will have its own RSS feed | |||
@@ -58,7 +58,7 @@ generate_rss = false | |||
# | |||
taxonomies = [] | |||
# The additional languages for that site | |||
# The additional languages for the site. | |||
# Example: | |||
# languages = [ | |||
# {code = "fr", rss = true}, # there will be a RSS feed for French content | |||
@@ -68,21 +68,21 @@ taxonomies = [] | |||
# | |||
languages = [] | |||
# Whether to compile the Sass files found in the `sass` directory | |||
# When set to "true", the Sass files in the `sass` directory are compiled. | |||
compile_sass = false | |||
# Whether to build a search index out of the pages and section | |||
# content for the `default_language` | |||
# When set to "true", a search index is built from the pages and section | |||
# content for `default_language`. | |||
build_search_index = false | |||
# A list of glob patterns specifying asset files to ignore when | |||
# processing the content directory. | |||
# Defaults to none, which means all asset files are copied over to the public folder. | |||
# A list of glob patterns specifying asset files to ignore when the content | |||
# directory is processed. Defaults to none, which means that all asset files are | |||
# copied over to the `public` directory. | |||
# Example: | |||
# ignored_content = ["*.{graphml,xlsx}", "temp.*"] | |||
ignored_content = [] | |||
# A list of directories to search for additional `.sublime-syntax` files in. | |||
# A list of directories used to search for additional `.sublime-syntax` files. | |||
extra_syntaxes = [] | |||
# Optional translation object. The key if present should be a language code. | |||
@@ -95,11 +95,32 @@ extra_syntaxes = [] | |||
# | |||
# [translations.en] | |||
# title = "A title" | |||
# | |||
# Configuration of the link checker. | |||
[link_checker] | |||
# Skip link checking for external URLs that start with these prefixes | |||
skip_prefixes = [ | |||
"http://[2001:db8::]/", | |||
] | |||
# Skip anchor checking for external URLs that start with these prefixes | |||
skip_anchor_prefixes = [ | |||
"https://caniuse.com/", | |||
] | |||
# Various slugification strategies, see below for details | |||
# Defauls to everything being a slug | |||
[slugify] | |||
paths = "on" | |||
taxonomies = "on" | |||
anchors = "on" | |||
# Optional translation object. Keys should be language codes. | |||
[translations] | |||
# You can put any kind of data in there and it | |||
# will be accessible in all templates | |||
# You can put any kind of data here. The data | |||
# will be accessible in all templates. | |||
[extra] | |||
``` | |||
@@ -141,6 +162,23 @@ Zola currently has the following highlight themes available: | |||
- [ayu-mirage](https://github.com/dempfi/ayu) | |||
- [Tomorrow](https://tmtheme-editor.herokuapp.com/#!/editor/theme/Tomorrow) | |||
- [one-dark](https://github.com/andresmichel/one-dark-theme) | |||
- [zenburn](https://github.com/colinta/zenburn) | |||
Zola uses the Sublime Text themes, making it very easy to add more. | |||
If you want a theme not on that list, please open an issue or a pull request on the [Zola repo](https://github.com/getzola/zola). | |||
If you want a theme not listed above, please open an issue or a pull request on the [Zola repo](https://github.com/getzola/zola). | |||
## Slugification strategies | |||
By default, Zola will turn every path, taxonomies and anchors to a slug, an ASCII representation with no special characters. | |||
You can however change that strategy for each kind of item, if you want UTF-8 characters in your URLs for example. There are 3 strategies: | |||
- `on`: the default one, everything is turned into a slug | |||
- `safe`: characters that cannot exist in files on Windows (`<>:"/\|?*`) or Unix (`/`) are removed, everything else stays | |||
- `off`: nothing is changed, your site might not build on some OS and/or break various URL parsers | |||
Since there are no filename issues with anchors, the `safe` and `off` strategies are identical in their case: the only change | |||
is space being replaced by `_` since a space is not valid in an anchor. | |||
Note that if you are using a strategy other than the default, you will have to manually escape whitespace and Markdown | |||
tokens to be able to link to your pages. For example an internal link to a file named `some space.md` will need to be | |||
written like `some%20space.md` in your Markdown files. |
@@ -1,9 +1,9 @@ | |||
+++ | |||
title = "Directory structure" | |||
weight = 3 | |||
weight = 30 | |||
+++ | |||
After running `zola init`, you should see the following structure in your folder: | |||
After running `zola init`, you should see the following structure in your directory: | |||
```bash | |||
@@ -18,34 +18,34 @@ After running `zola init`, you should see the following structure in your folder | |||
5 directories, 1 file | |||
``` | |||
Here's a high level overview of each of these folders and `config.toml`. | |||
Here's a high-level overview of each of these directories and `config.toml`. | |||
## `config.toml` | |||
A mandatory configuration file of Zola in TOML format. | |||
It is explained in details in the [Configuration page](@/documentation/getting-started/configuration.md). | |||
A mandatory Zola configuration file in TOML format. | |||
This file is explained in detail in the [configuration documentation](@/documentation/getting-started/configuration.md). | |||
## `content` | |||
Where all your markup content lies: this will be mostly comprised of `.md` files. | |||
Each folder in the `content` directory represents a [section](@/documentation/content/section.md) | |||
that contains [pages](@/documentation/content/page.md) : your `.md` files. | |||
Contains all your markup content (mostly `.md` files). | |||
Each child directory of the `content` directory represents a [section](@/documentation/content/section.md) | |||
that contains [pages](@/documentation/content/page.md) (your `.md` files). | |||
To learn more, read [the content overview](@/documentation/content/overview.md). | |||
To learn more, read the [content overview page](@/documentation/content/overview.md). | |||
## `sass` | |||
Contains the [Sass](http://sass-lang.com) files to be compiled. Non-Sass files will be ignored. | |||
The directory structure of the `sass` folder will be preserved when copying over the compiled files: a file at | |||
The directory structure of the `sass` folder will be preserved when copying over the compiled files; for example, a file at | |||
`sass/something/site.scss` will be compiled to `public/something/site.css`. | |||
## `static` | |||
Contains any kind of files. All the files/folders in the `static` folder will be copied as-is in the output directory. | |||
If your static files are large you can configure Zola to [hard link](https://en.wikipedia.org/wiki/Hard_link) them | |||
instead of copying by setting `hard_link_static = true` in the config file. | |||
Contains any kind of file. All the files/directories in the `static` directory will be copied as-is to the output directory. | |||
If your static files are large, you can configure Zola to [hard link](https://en.wikipedia.org/wiki/Hard_link) them | |||
instead of copying them by setting `hard_link_static = true` in the config file. | |||
## `templates` | |||
Contains all the [Tera](https://tera.netlify.com) templates that will be used to render this site. | |||
Have a look at the [Templates](@/documentation/templates/_index.md) to learn more about default templates | |||
Contains all the [Tera](https://tera.netlify.com) templates that will be used to render your site. | |||
Have a look at the [templates documentation](@/documentation/templates/_index.md) to learn more about default templates | |||
and available variables. | |||
## `themes` | |||
Contains themes that can be used for that site. If you are not planning to use themes, leave this folder empty. | |||
If you want to learn about themes, head to the [themes documentation](@/documentation/themes/_index.md). | |||
Contains themes that can be used for your site. If you are not planning to use themes, leave this directory empty. | |||
If you want to learn about themes, see the [themes documentation](@/documentation/themes/_index.md). |
@@ -1,6 +1,6 @@ | |||
+++ | |||
title = "Installation" | |||
weight = 1 | |||
weight = 10 | |||
+++ | |||
Zola provides pre-built binaries for MacOS, Linux and Windows on the | |||
@@ -24,7 +24,7 @@ $ yay -S zola-bin | |||
### Fedora | |||
Zola is available in official repositories since Fedora 29. | |||
Zola has been available in the official repositories since Fedora 29. | |||
```sh | |||
$ sudo dnf install zola | |||
@@ -54,7 +54,7 @@ Zola is available on [Scoop](http://scoop.sh): | |||
$ scoop install zola | |||
``` | |||
And [Chocolatey](https://chocolatey.org/): | |||
and [Chocolatey](https://chocolatey.org/): | |||
```bash | |||
$ choco install zola | |||
@@ -63,10 +63,10 @@ $ choco install zola | |||
Zola does not work in PowerShell ISE. | |||
## From source | |||
To build it from source, you will need to have Git, [Rust (at least 1.31) and Cargo](https://www.rust-lang.org/) | |||
installed. You will also need additional dependencies to compile [libsass](https://github.com/sass/libsass): | |||
To build Zola from source, you will need to have Git, [Rust (at least 1.36) and Cargo](https://www.rust-lang.org/) | |||
installed. You will also need to meet additional dependencies to compile [libsass](https://github.com/sass/libsass): | |||
- OSX, Linux and other Unix: `make` (`gmake` on BSDs), `g++`, `libssl-dev` | |||
- OSX, Linux and other Unix-like operating systems: `make` (`gmake` on BSDs), `g++`, `libssl-dev` | |||
- NixOS: Create a `shell.nix` file in the root of the cloned project with the following contents: | |||
```nix | |||
with import <nixpkgs> {}; | |||
@@ -79,7 +79,7 @@ installed. You will also need additional dependencies to compile [libsass](https | |||
]; | |||
} | |||
``` | |||
- Then invoke `nix-shell`. This opens a shell with the above dependencies. You then run `cargo build --release` to build the project. | |||
- Then, invoke `nix-shell`. This opens a shell with the above dependencies. Then, run `cargo build --release` to build the project. | |||
- Windows (a bit trickier): updated `MSVC` and overall updated VS installation | |||
From a terminal, you can now run the following command: | |||
@@ -88,6 +88,6 @@ From a terminal, you can now run the following command: | |||
$ cargo build --release | |||
``` | |||
The binary will be available in the `target/release` folder. You can move it in your `$PATH` to have the | |||
The binary will be available in the `target/release` directory. You can move it in your `$PATH` to have the | |||
`zola` command available globally or in a directory if you want for example to have the binary in the | |||
same repository as the site. |
@@ -0,0 +1,206 @@ | |||
+++ | |||
title = "Overview" | |||
weight = 5 | |||
+++ | |||
## Zola at a Glance | |||
Zola is a static site generator (SSG), similar to [Hugo](https://gohugo.io/), [Pelican](https://blog.getpelican.com/), and [Jekyll](https://jekyllrb.com/) (for a comprehensive list of SSGs, please see the [StaticGen](https://www.staticgen.com/) site). It is written in [Rust](https://www.rust-lang.org/) and uses the [Tera](https://tera.netlify.com/) template engine, which is similar to [Jinja2](https://jinja.palletsprojects.com/en/2.10.x/), [Django templates](https://docs.djangoproject.com/en/2.2/topics/templates/), [Liquid](https://shopify.github.io/liquid/), and [Twig](https://twig.symfony.com/). Content is written in [CommonMark](https://commonmark.org/), a strongly defined, highly compatible specification of [Markdown](https://www.markdownguide.org/). | |||
SSGs use dynamic templates to transform content into static HTML pages. Static sites are thus very fast and require no databases, making them easy to host. A comparison between static and dynamic sites, such as WordPress, Drupal, and Django, can be found [here](https://dev.to/ashenmaster/static-vs-dynamic-sites-61f). | |||
To get a taste of Zola, please see the quick overview below. | |||
## First Steps with Zola | |||
Unlike some SSGs, Zola makes no assumptions regarding the structure of your site. In this overview, we'll be making a simple blog site. | |||
### Initialize Site | |||
> This overview is based on Zola 0.9. | |||
Please see the detailed [installation instructions for your platform](@/documentation/getting-started/installation.md). With Zola installed, let's initialize our site: | |||
```bash | |||
$ zola init myblog | |||
``` | |||
You will be asked a few questions. | |||
``` | |||
> What is the URL of your site? (https://example.com): | |||
> Do you want to enable Sass compilation? [Y/n]: | |||
> Do you want to enable syntax highlighting? [y/N]: | |||
> Do you want to build a search index of the content? [y/N]: | |||
``` | |||
For our blog, let's accept the default values (i.e., press Enter for each question). We now have a `myblog` directory with the following structure: | |||
```bash | |||
├── config.toml | |||
├── content | |||
├── sass | |||
├── static | |||
├── templates | |||
└── themes | |||
``` | |||
Let's start the zola development server with: | |||
```bash | |||
$ zola serve | |||
Building site... | |||
-> Creating 0 pages (0 orphan), 0 sections, and processing 0 images | |||
``` | |||
> This command must be run in the base Zola directory, which contains `config.toml`. | |||
If you point your web browser to <http://127.0.0.1:1111>, you should see a "Welcome to Zola" message. | |||
### Home Page | |||
Let's make a home page. To do this, let's first create a `base.html` file inside the `templates` directory. This step will make more sense as we move through this overview. We'll be using the CSS framework [Bulma](https://bulma.io/). | |||
```html | |||
<!DOCTYPE html> | |||
<html lang="en"> | |||
<head> | |||
<meta charset="utf-8"> | |||
<title>MyBlog</title> | |||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.8.0/css/bulma.min.css"> | |||
</head> | |||
<body> | |||
<section class="section"> | |||
<div class="container"> | |||
{% block content %} {% endblock %} | |||
</div> | |||
</section> | |||
</body> | |||
</html> | |||
``` | |||
Now, let's create an `index.html` file inside the `templates` directory. | |||
```html | |||
{% extends "base.html" %} | |||
{% block content %} | |||
<h1 class="title"> | |||
This is my blog made with Zola. | |||
</h1> | |||
{% endblock content %} | |||
``` | |||
This tells Zola that `index.html` extends our `base.html` file and replaces the block called "content" with the text between the `{% block content %}` and `{% endblock content %}` tags. | |||
### Content Directory | |||
Now let's add some content. We'll start by making a `blog` subdirectory in the `content` directory and creating an `_index.md` file inside it. This file tells Zola that `blog` is a [section](@/documentation/content/section.md), which is how content is categorized in Zola. | |||
```bash | |||
├── content | |||
│ └── blog | |||
│ └── _index.md | |||
``` | |||
In the `_index.md` file, we'll set the following variables in [TOML](https://github.com/toml-lang/toml) format: | |||
```md | |||
+++ | |||
title = "List of blog posts" | |||
sort_by = "date" | |||
template = "blog.html" | |||
page_template = "blog-page.html" | |||
+++ | |||
``` | |||
> Note that although no variables are mandatory, the opening and closing `+++` are required. | |||
* *sort_by = "date"* tells Zola to use the date to order our section pages (more on pages below). | |||
* *template = "blog.html"* tells Zola to use `blog.html` in the `templates` directory as the template for listing the Markdown files in this section. | |||
* *page_template = "blog-page.html"* tells Zola to use `blog-page.html` in the `templates` directory as the template for individual Markdown files. | |||
For a full list of section variables, please see the [section](@/documentation/content/section.md) documentation. We will use *title = "List of blog posts"* in a template (see below). | |||
### Templates | |||
Let's now create some more templates. In the `templates` directory, create a `blog.html` file with the following contents: | |||
```html | |||
{% extends "base.html" %} | |||
{% block content %} | |||
<h1 class="title"> | |||
{{ section.title }} | |||
</h1> | |||
<ul> | |||
{% for page in section.pages %} | |||
<li><a href="{{ page.permalink }}">{{ page.title }}</a></li> | |||
{% endfor %} | |||
</ul> | |||
{% endblock content %} | |||
``` | |||
As done by `index.html`, `blog.html` extends `base.html`, but this time we want to list the blog posts. The *title* we set in the `_index.md` file above is available to us as `{{ section.title }}`. In the list below the title, we loop through all the pages in our section (`blog` directory) and output the page title and URL using `{{ page.title }}` and `{{ page.permalink }}`, respectively. If you go to <http://127.0.0.1:1111/blog/>, you will see the section page for `blog`. The list is empty because we don't have any blog posts. Let's fix that now. | |||
### Markdown Content | |||
In the `blog` directory, create a file called `first.md` with the following contents: | |||
```md | |||
+++ | |||
title = "My first post" | |||
date = 2019-11-27 | |||
+++ | |||
This is my first blog post. | |||
``` | |||
The *title* and *date* will be avaiable to us in the `blog-page.html` template as `{{ page.title }}` and `{{ page.date }}`, respectively. All text below the closing `+++` will be available to us as `{{ page.content }}`. | |||
We now need to make the `blog-page.html` template. In the `templates` directory, create this file with the contents: | |||
```html | |||
{% extends "base.html" %} | |||
{% block content %} | |||
<h1 class="title"> | |||
{{ page.title }} | |||
</h1> | |||
<p class="subtitle"><strong>{{ page.date }}</strong></p> | |||
<p>{{ page.content | safe }}</p> | |||
{% endblock content %} | |||
``` | |||
> Note the `| safe` filter for `{{ page.content }}`. | |||
This should start to look familiar. If you now go back to our blog list page at <http://127.0.0.1:1111/blog/>, you should see our lonely post. Let's add another. In the `content/blog` directory, let's create the file `second.md` with the contents: | |||
```md | |||
+++ | |||
title = "My second post" | |||
date = 2019-11-28 | |||
+++ | |||
This is my second blog post. | |||
``` | |||
Back at <http://127.0.0.1:1111/blog/>, our second post shows up on top of the list because it's newer than the first post and we had set *sort_by = "date"* in our `_index.md` file. As a final step, let's modify our home page to link to our blog posts. | |||
The `index.html` file inside the `templates` directory should be: | |||
```html | |||
{% extends "base.html" %} | |||
{% block content %} | |||
<h1 class="title"> | |||
This is my blog made with Zola. | |||
</h1> | |||
<p>Click <a href="/blog/">here</a> to see my posts.</p> | |||
{% endblock content %} | |||
``` | |||
This has been a quick overview of Zola. You can now dive into the rest of the documentation. |
@@ -4,5 +4,4 @@ weight = 80 | |||
+++ | |||
Zola will look for a `404.html` file in the `templates` directory or | |||
use the built-in one. The default template is very basic and gets a simple | |||
variable in the context: the site `config`. | |||
use the built-in one. The default template is very basic and gets `config` in its context. |
@@ -3,8 +3,8 @@ title = "Archive" | |||
weight = 90 | |||
+++ | |||
Zola doesn't have a built-in way to display an archive page, a page showing | |||
all post titles ordered by year. However, this can be accomplished directly in the templates: | |||
Zola doesn't have a built-in way to display an archive page (a page showing | |||
all post titles ordered by year). However, this can be accomplished directly in the templates: | |||
```jinja2 | |||
{% for year, posts in section.pages | group_by(attribute="year") %} | |||
@@ -19,5 +19,5 @@ all post titles ordered by year. However, this can be accomplished directly in t | |||
``` | |||
This snippet assumes that posts are sorted by date and that you want to display the archive | |||
in a descending order. If you want to show articles in a ascending order, simply add a `reverse` filter | |||
after the `group_by`. | |||
in descending order. If you want to show articles in ascending order, add a `reverse` filter | |||
after `group_by`. |
@@ -3,49 +3,49 @@ title = "Overview" | |||
weight = 10 | |||
+++ | |||
Zola uses the [Tera](https://tera.netlify.com) template engine and is very similar | |||
to Jinja2, Liquid or Twig. | |||
Zola uses the [Tera](https://tera.netlify.com) template engine, which is very similar | |||
to Jinja2, Liquid and Twig. | |||
As this documentation will only talk about how templates work in Zola, please read | |||
the [Tera template documentation](https://tera.netlify.com/docs#templates) if you want | |||
to learn more about it first. | |||
All templates live in the `templates` directory. If you are not sure what variables are available in a template, you can just stick `{{ __tera_context }}` in it | |||
to print the whole context. | |||
All templates live in the `templates` directory. If you are not sure what variables are available in a template, | |||
you can place `{{ __tera_context }}` in the template to print the whole context. | |||
A few variables are available on all templates minus RSS and sitemap: | |||
A few variables are available on all templates except RSS and the sitemap: | |||
- `config`: the [configuration](@/documentation/getting-started/configuration.md) without any modifications | |||
- `current_path`: the path (full URL without the `base_url`) of the current page, never starting with a `/` | |||
- `current_url`: the full URL for that page | |||
- `lang`: the language for that page, `null` if the page/section doesn't have a language set | |||
- `current_path`: the path (full URL without `base_url`) of the current page, never starting with a `/` | |||
- `current_url`: the full URL for the current page | |||
- `lang`: the language for the current page; `null` if the page/section doesn't have a language set | |||
The 404 template does not get `current_path` and `current_url` as it cannot know it. | |||
The 404 template does not get `current_path` and `current_url` (this information cannot be determined). | |||
## Standard Templates | |||
## Standard templates | |||
By default, Zola will look for three templates: `index.html`, which is applied | |||
to the site homepage; `section.html`, which is applied to all sections (any HTML | |||
page generated by creating a directory within your `content` directory); and | |||
`page.html`, which is applied to all pages (any HTML page generated by creating a | |||
`page.html`, which is applied to all pages (any HTML page generated by creating an | |||
`.md` file within your `content` directory). | |||
The homepage is always a section (regardless of whether it contains other pages). | |||
Thus, the `index.html` and `section.html` templates both have access to the | |||
section variables. The `page.html` template has access to the page variables. | |||
The page and section variables are described in more detail in the next section of this documentation. | |||
The page and section variables are described in more detail in the next section. | |||
## Built-in Templates | |||
Zola comes with three built-in templates: `rss.xml`, `sitemap.xml`, and | |||
`robots.txt` (each described in their own section of this documentation). | |||
## Built-in templates | |||
Zola comes with three built-in templates: `rss.xml`, `sitemap.xml` and | |||
`robots.txt` (each is described in its own section of this documentation). | |||
Additionally, themes can add their own templates, which will be applied if not | |||
overridden. You can override built-in or theme templates by creating a template with | |||
same name in the correct path. For example, you can override the RSS template by | |||
the same name in the correct path. For example, you can override the RSS template by | |||
creating a `templates/rss.xml` file. | |||
## Custom Templates | |||
In addition to the standard `index.html`, `section.html`, and `page.html` templates, | |||
you may also create custom templates by creating a `.html` file in the `templates` | |||
directory. These custom templates will not be used by default. Instead, the custom template will _only_ be used if you apply it by setting the `template` front-matter variable to the path for that template (or if you `include` it in another template that is applied). For example, if you created a custom template for your site's About page called `about.html`, you could apply it to your `about.md` page by including the following front matter in your `about.md` page: | |||
## Custom templates | |||
In addition to the standard `index.html`, `section.html` and `page.html` templates, | |||
you may also create custom templates by creating an `.html` file in the `templates` | |||
directory. These custom templates will not be used by default. Instead, a custom template will _only_ be used if you apply it by setting the `template` front-matter variable to the path for that template (or if you `include` it in another template that is applied). For example, if you created a custom template for your site's About page called `about.html`, you could apply it to your `about.md` page by including the following front matter in your `about.md` page: | |||
```md | |||
+++ | |||
@@ -58,13 +58,14 @@ Custom templates are not required to live at the root of your `templates` direct | |||
For example, `product_pages/with_pictures.html` is a valid template. | |||
## Built-in filters | |||
Zola adds a few filters, in addition of the ones [ones already present](https://tera.netlify.com/docs#built-in-filters) in Tera. | |||
Zola adds a few filters in addition to [those](https://tera.netlify.com/docs/templates/#built-in-filters) already present | |||
in Tera. | |||
### markdown | |||
Converts the given variable to HTML using Markdown. This doesn't apply any of the | |||
features that Zola adds to Markdown: internal links, shortcodes etc won't work. | |||
features that Zola adds to Markdown; for example, internal links and shortcodes won't work. | |||
By default, the filter will wrap all text into a paragraph. To disable that, you can | |||
By default, the filter will wrap all text in a paragraph. To disable this behaviour, you can | |||
pass `true` to the inline argument: | |||
```jinja2 | |||
@@ -80,18 +81,19 @@ Decode the variable from base64. | |||
## Built-in global functions | |||
Zola adds a few global functions to [those in Tera](https://tera.netlify.com/docs#built-in-functions) in order to make it easier to develop complex sites. | |||
Zola adds a few global functions to [those in Tera](https://tera.netlify.com/docs/templates/#built-in-functions) | |||
to make it easier to develop complex sites. | |||
### `get_page` | |||
Takes a path to a `.md` file and returns the associated page | |||
Takes a path to an `.md` file and returns the associated page. | |||
```jinja2 | |||
{% set page = get_page(path="blog/page2.md") %} | |||
``` | |||
### `get_section` | |||
Takes a path to a `_index.md` file and returns the associated section | |||
Takes a path to an `_index.md` file and returns the associated section. | |||
```jinja2 | |||
{% set section = get_section(path="blog/_index.md") %} | |||
@@ -105,14 +107,14 @@ If you only need the metadata of the section, you can pass `metadata_only=true` | |||
### ` get_url` | |||
Gets the permalink for the given path. | |||
If the path starts with `@/`, it will be understood as an internal | |||
link like the ones used in markdown, starting from the root `content` directory. | |||
If the path starts with `@/`, it will be treated as an internal | |||
link like the ones used in Markdown, starting from the root `content` directory. | |||
```jinja2 | |||
{% set url = get_url(path="@/blog/_index.md") %} | |||
``` | |||
This can also be used to get the permalinks for static assets for example if | |||
This can also be used to get the permalinks for static assets, for example if | |||
we want to link to the file that is located at `static/css/app.css`: | |||
```jinja2 | |||
@@ -131,7 +133,7 @@ by passing `cachebust=true` to the `get_url` function. | |||
### `get_image_metadata` | |||
Gets metadata for an image. Today the only supported keys are `width` and `height`. | |||
Gets metadata for an image. Currently, the only supported keys are `width` and `height`. | |||
```jinja2 | |||
{% set meta = get_image_metadata(path="...") %} | |||
@@ -145,8 +147,8 @@ Gets the permalink for the taxonomy item found. | |||
{% set url = get_taxonomy_url(kind="categories", name=page.taxonomies.category) %} | |||
``` | |||
The `name` will almost come from a variable but in case you want to do it manually, | |||
the value should be the same as the one in the front-matter, not the slugified version. | |||
`name` will almost always come from a variable but in case you want to do it manually, | |||
the value should be the same as the one in the front matter, not the slugified version. | |||
### `get_taxonomy` | |||
Gets the whole taxonomy of a specific kind. | |||
@@ -160,7 +162,7 @@ Loads data from a file or URL. Supported file types include *toml*, *json* and * | |||
Any other file type will be loaded as plain text. | |||
The `path` argument specifies the path to the data file relative to your base directory, where your `config.toml` is. | |||
As a security precaution, If this file is outside of the main site directory, your site will fail to build. | |||
As a security precaution, if this file is outside the main site directory, your site will fail to build. | |||
```jinja2 | |||
{% set data = load_data(path="content/blog/story/data.toml") %} | |||
@@ -168,7 +170,7 @@ As a security precaution, If this file is outside of the main site directory, yo | |||
The optional `format` argument allows you to specify and override which data type is contained | |||
within the file specified in the `path` argument. Valid entries are `toml`, `json`, `csv` | |||
or `plain`. If the `format` argument isn't specified, then the paths extension is used. | |||
or `plain`. If the `format` argument isn't specified, then the path extension is used. | |||
```jinja2 | |||
{% set data = load_data(path="content/blog/story/data.txt", format="json") %} | |||
@@ -176,8 +178,8 @@ or `plain`. If the `format` argument isn't specified, then the paths extension i | |||
Use the `plain` format for when your file has a toml/json/csv extension but you want to load it as plain text. | |||
For *toml* and *json* the data is loaded into a structure matching the original data file, | |||
however for *csv* there is no native notion of such a structure. Instead the data is separated | |||
For *toml* and *json*, the data is loaded into a structure matching the original data file; | |||
however, for *csv* there is no native notion of such a structure. Instead, the data is separated | |||
into a data structure containing *headers* and *records*. See the example below to see | |||
how this works. | |||
@@ -207,14 +209,16 @@ template: | |||
#### Remote content | |||
Instead of using a file, you can load data from a remote URL. This can be done by specifying a `url` parameter to `load_data` rather than `path`. | |||
Instead of using a file, you can load data from a remote URL. This can be done by specifying a `url` parameter | |||
to `load_data` rather than `path`. | |||
```jinja2 | |||
{% set response = load_data(url="https://api.github.com/repos/getzola/zola") %} | |||
{{ response }} | |||
``` | |||
By default, the response body will be returned with no parsing. This can be changed by using the `format` argument as below. | |||
By default, the response body will be returned with no parsing. This can be changed by using the `format` argument | |||
as below. | |||
```jinja2 | |||
@@ -222,11 +226,13 @@ By default, the response body will be returned with no parsing. This can be chan | |||
{{ response }} | |||
``` | |||
#### Data Caching | |||
#### Data caching | |||
Data file loading and remote requests are cached in memory during build, so multiple requests aren't made to the same endpoint. | |||
URLs are cached based on the URL, and data files are cached based on the files modified time. | |||
The format is also taken into account when caching, so a request will be sent twice if it's loaded with 2 different formats. | |||
Data file loading and remote requests are cached in memory during the build, so multiple requests aren't made | |||
to the same endpoint. | |||
URLs are cached based on the URL, and data files are cached based on the file modified time. | |||
The format is also taken into account when caching, so a request will be sent twice if it's loaded with two | |||
different formats. | |||
### `trans` | |||
Gets the translation of the given `key`, for the `default_language` or the `lang`uage given | |||
@@ -3,11 +3,11 @@ title = "Sections and Pages" | |||
weight = 20 | |||
+++ | |||
Pages and sections are actually very similar. | |||
Templates for pages and sections are very similar. | |||
## Page variables | |||
Zola will try to load the `templates/page.html` template, the `page.html` template of the theme if one is used | |||
or will render the built-in template: a blank page. | |||
or render the built-in template (a blank page). | |||
Whichever template you decide to render, you will get a `page` variable in your template | |||
with the following fields: | |||
@@ -27,6 +27,7 @@ permalink: String; | |||
summary: String?; | |||
taxonomies: HashMap<String, Array<String>>; | |||
extra: HashMap<String, Any>; | |||
toc: Array<Header>, | |||
// Naive word count, will not work for languages without whitespace | |||
word_count: Number; | |||
// Based on https://help.medium.com/hc/en-us/articles/214991667-Read-time | |||
@@ -59,8 +60,8 @@ translations: Array<TranslatedContent>; | |||
## Section variables | |||
By default, Zola will try to load `templates/index.html` for `content/_index.md` | |||
and `templates/section.html` for others `_index.md` files. If there isn't | |||
one, it will render the built-in template: a blank page. | |||
and `templates/section.html` for other `_index.md` files. If there isn't | |||
one, it will render the built-in template (a blank page). | |||
Whichever template you decide to render, you will get a `section` variable in your template | |||
with the following fields: | |||
@@ -81,6 +82,7 @@ pages: Array<Page>; | |||
// This only contains the path to use in the `get_section` Tera function to get | |||
// the actual section object if you need it | |||
subsections: Array<String>; | |||
toc: Array<Header>, | |||
// Unicode word count | |||
word_count: Number; | |||
// Based on https://help.medium.com/hc/en-us/articles/214991667-Read-time | |||
@@ -101,7 +103,7 @@ translations: Array<TranslatedContent>; | |||
## Table of contents | |||
Both page and section templates have a `toc` variable which corresponds to an array of `Header`. | |||
Both page and section templates have a `toc` variable that corresponds to an array of `Header`. | |||
A `Header` has the following fields: | |||
```ts | |||
@@ -119,9 +121,9 @@ children: Array<Header>; | |||
## Translated content | |||
Both page and section have a `translations` field which corresponds to an array of `TranslatedContent`. If your site is not using multiple languages, | |||
this will always be an empty array. | |||
A `TranslatedContent` has the following fields: | |||
Both pages and sections have a `translations` field that corresponds to an array of `TranslatedContent`. If your | |||
site is not using multiple languages, this will always be an empty array. | |||
`TranslatedContent` has the following fields: | |||
```ts | |||
// The language code for that content, empty if it is the default language | |||
@@ -130,5 +132,8 @@ lang: String?; | |||
title: String?; | |||
// A permalink to that content | |||
permalink: String; | |||
// The path to the markdown file; useful for retrieving the full page through | |||
// the `get_page` function. | |||
path: String; | |||
``` | |||
@@ -28,10 +28,12 @@ next: String?; | |||
pages: Array<Page>; | |||
// Which pager are we on | |||
current_index: Number; | |||
// Total number of pages accross all the pagers | |||
total_pages: Number; | |||
``` | |||
A pager is a page of the pagination: if you have 100 pages and are paginating 10 by 10, you will have 10 pagers containing | |||
each 10 pages. | |||
A pager is a page of the pagination; if you have 100 pages and paginate_by is set to 10, you will have 10 pagers each | |||
containing 10 pages. | |||
## Section | |||
@@ -6,8 +6,8 @@ weight = 70 | |||
Zola will look for a `robots.txt` file in the `templates` directory or | |||
use the built-in one. | |||
Robots.txt is the simplest of all templates: it only gets the config | |||
and the default is what most site want: | |||
Robots.txt is the simplest of all templates: it only gets `config` | |||
and the default is what most sites want: | |||
```jinja2 | |||
User-agent: * | |||
@@ -5,14 +5,14 @@ weight = 50 | |||
If the site `config.toml` file sets `generate_rss = true`, then Zola will | |||
generate an `rss.xml` page for the site, which will live at `base_url/rss.xml`. To | |||
generate the `rss.xml` page, Zola will look for a `rss.xml` file in the `templates` | |||
directory or, if one does not exist, will use the use the built-in rss template. | |||
generate the `rss.xml` page, Zola will look for an `rss.xml` file in the `templates` | |||
directory or, if one does not exist, it will use the use the built-in rss template. | |||
**Only pages with a date will be available.** | |||
The RSS template gets three variables in addition of the config: | |||
The RSS template gets three variables in addition to `config`: | |||
- `feed_url`: the full url to that specific feed | |||
- `last_build_date`: the date of the latest post | |||
- `pages`: see [the page variables](@/documentation/templates/pages-sections.md#page-variables) for | |||
- `pages`: see [page variables](@/documentation/templates/pages-sections.md#page-variables) for | |||
a detailed description of what this contains |
@@ -7,11 +7,12 @@ Zola will look for a `sitemap.xml` file in the `templates` directory or | |||
use the built-in one. | |||
If your site has more than 30 000 pages, it will automatically split | |||
the links into multiple sitemaps as recommended by [Google](https://support.google.com/webmasters/answer/183668?hl=en): | |||
the links into multiple sitemaps, as recommended by [Google](https://support.google.com/webmasters/answer/183668?hl=en): | |||
> All formats limit a single sitemap to 50MB (uncompressed) and 50,000 URLs. | |||
> If you have a larger file or more URLs, you will have to break your list into multiple sitemaps. | |||
> You can optionally create a sitemap index file (a file that points to a list of sitemaps) and submit that single index file to Google. | |||
> You can optionally create a sitemap index file (a file that points to a list of sitemaps) and submit | |||
> that single index file to Google. | |||
In such a case, Zola will use a template called `split_sitemap_index.xml` to render the index sitemap. | |||
@@ -8,7 +8,7 @@ Zola will look up the following files in the `templates` directory: | |||
- `$TAXONOMY_NAME/single.html` | |||
- `$TAXONOMY_NAME/list.html` | |||
First, a `TaxonomyTerm` has the following fields: | |||
First, `TaxonomyTerm` has the following fields: | |||
```ts | |||
name: String; | |||
@@ -17,7 +17,7 @@ permalink: String; | |||
pages: Array<Page>; | |||
``` | |||
and a `TaxonomyConfig`: | |||
and `TaxonomyConfig` has the following fields: | |||
```ts | |||
name: String, | |||
@@ -30,7 +30,7 @@ rss: Bool; | |||
### Taxonomy list (`list.html`) | |||
This template is never paginated and therefore get the following variables in all cases. | |||
This template is never paginated and therefore gets the following variables in all cases. | |||
```ts | |||
// The site config | |||
@@ -64,5 +64,5 @@ term: TaxonomyTerm; | |||
lang: String; | |||
``` | |||
A paginated taxonomy term will also get a `paginator` variable, see the [pagination page](@/documentation/templates/pagination.md) | |||
for more details on that. | |||
A paginated taxonomy term will also get a `paginator` variable; see the [pagination page] | |||
(@/documentation/templates/pagination.md) for more details. |
@@ -8,9 +8,9 @@ will want to use many [Tera blocks](https://tera.netlify.com/docs#inheritance) t | |||
allow users to easily modify it. | |||
## Getting started | |||
As mentioned, a theme is just like any site: start with running `zola init MY_THEME_NAME`. | |||
As mentioned, a theme is just like any site; start by running `zola init MY_THEME_NAME`. | |||
The only thing needed to turn that site into a theme is to add `theme.toml` configuration file with the | |||
The only thing needed to turn that site into a theme is to add a `theme.toml` configuration file with the | |||
following fields: | |||
```toml | |||
@@ -42,11 +42,11 @@ homepage = "http://markdotto.com/" | |||
repo = "https://www.github.com/mdo/hyde" | |||
``` | |||
A simple theme you can use as example is [Hyde](https://github.com/Keats/hyde). | |||
A simple theme you can use as an example is [Hyde](https://github.com/Keats/hyde). | |||
## Working on a theme | |||
As a theme is just a site, you can simply use `zola serve` and make changes to your | |||
theme, with live reloading working as expected. | |||
theme, with live reload working as expected. | |||
Make sure to commit every directory (including `content`) in order for other people | |||
to be able to build the theme from your repository. | |||
@@ -65,7 +65,7 @@ of this site, the theme will require two more things: | |||
- `README.md`: a thorough README explaining how to use the theme and any other information | |||
of importance | |||
The first step is to make sure the theme is fulfilling those three requirements: | |||
The first step is to make sure that the theme meets the following three requirements: | |||
- have a `screenshot.png` of the theme in action with a max size of around 2000x1000 | |||
- have a thorough `README.md` explaining how to use the theme and any other information | |||
@@ -15,24 +15,24 @@ $ git clone THEME_REPO_URL | |||
``` | |||
Cloning the repository using Git or another VCS will allow you to easily | |||
update it but you can also simply download the files manually and paste | |||
update. Alternatively, you can download the files manually and place | |||
them in a folder. | |||
You can find a list of themes [on this very website](@/themes/_index.md). | |||
You can find a list of themes [here](@/themes/_index.md). | |||
## Using a theme | |||
Now that you have the theme in your `themes` directory, you only need to tell | |||
Zola to use it to get started by setting the `theme` variable of the | |||
Now that you have the theme in your `themes` directory, you need to tell | |||
Zola to use it by setting the `theme` variable in the | |||
[configuration file](@/documentation/getting-started/configuration.md). The theme | |||
name has to be name of the directory you cloned the theme in. | |||
name has to be the name of the directory you cloned the theme in. | |||
For example, if you cloned a theme in `themes/simple-blog`, the theme name to use | |||
in the configuration file is `simple-blog`. | |||
## Customizing a theme | |||
Any file from the theme can be overriden by creating a file with the same path and name in your `templates` or `static` | |||
directory. Here are a few examples of that, assuming the theme name is `simple-blog`: | |||
directory. Here are a few examples of that, assuming that the theme name is `simple-blog`: | |||
```plain | |||
templates/pages/post.html -> replace themes/simple-blog/templates/pages/post.html | |||
@@ -40,7 +40,7 @@ templates/macros.html -> replace themes/simple-blog/templates/macros.html | |||
static/js/site.js -> replace themes/simple-blog/static/js/site.js | |||
``` | |||
You can also choose to only override parts of a page if a theme define some blocks by extending it. If we wanted | |||
You can also choose to only override parts of a page if a theme defines some blocks by extending it. If we wanted | |||
to only change a single block from the `post.html` page in the example above, we could do the following: | |||
``` | |||
@@ -51,7 +51,7 @@ Some custom data | |||
{% endblock %} | |||
``` | |||
Most themes will also provide some variables that are meant to be overriden: this happens in the `extra` section | |||
Most themes will also provide some variables that are meant to be overriden. This happens in the `extra` section | |||
of the [configuration file](@/documentation/getting-started/configuration.md). | |||
Let's say a theme uses a `show_twitter` variable and sets it to `false` by default. If you want to set it to `true`, | |||
you can update your `config.toml` like so: | |||
@@ -61,5 +61,5 @@ you can update your `config.toml` like so: | |||
show_twitter = false | |||
``` | |||
You can modify files directly in the `themes` directory but this will make updating the theme harder and live reload won't work with those | |||
files. | |||
You can modify files directly in the `themes` directory but this will make updating the theme harder and live reload | |||
won't work with these files. |
@@ -3,10 +3,9 @@ title = "Overview" | |||
weight = 10 | |||
+++ | |||
Zola has built-in support for themes in a way that are easy to customise | |||
but still easy to update if needed. | |||
Zola has built-in support for themes that makes it easy to customise and update them. | |||
All themes can use the full power of Zola, from shortcodes to Sass compilation. | |||
A list of themes is available [on this very website](@/themes/_index.md). | |||
A list of themes is available [here](@/themes/_index.md). | |||
@@ -49,6 +49,7 @@ | |||
font-size: 1.25rem; | |||
visibility: hidden; | |||
margin-left: -2rem; | |||
margin-right: 0.75rem; | |||
text-decoration: none; | |||
border-bottom-color: transparent; | |||
cursor: pointer; | |||