@@ -42,6 +42,8 @@ pub struct Taxonomy { | |||||
pub paginate_path: Option<String>, | pub paginate_path: Option<String>, | ||||
/// Whether to generate a RSS feed only for each taxonomy term, defaults to false | /// Whether to generate a RSS feed only for each taxonomy term, defaults to false | ||||
pub rss: bool, | pub rss: bool, | ||||
/// The language for that taxonomy, only used in multilingual sites | |||||
pub lang: Option<String>, | |||||
} | } | ||||
impl Taxonomy { | impl Taxonomy { | ||||
@@ -64,7 +66,7 @@ impl Taxonomy { | |||||
impl Default for Taxonomy { | impl Default for Taxonomy { | ||||
fn default() -> Taxonomy { | fn default() -> Taxonomy { | ||||
Taxonomy { name: String::new(), paginate_by: None, paginate_path: None, rss: false } | |||||
Taxonomy { name: String::new(), paginate_by: None, paginate_path: None, rss: false, lang: None } | |||||
} | } | ||||
} | } | ||||
@@ -48,7 +48,7 @@ pub struct TaxonomyItem { | |||||
} | } | ||||
impl TaxonomyItem { | impl TaxonomyItem { | ||||
pub fn new(name: &str, path: &str, config: &Config, keys: Vec<Key>, library: &Library) -> Self { | |||||
pub fn new(name: &str, taxonomy: &TaxonomyConfig, config: &Config, keys: Vec<Key>, library: &Library) -> Self { | |||||
// Taxonomy are almost always used for blogs so we filter by dates | // Taxonomy are almost always used for blogs so we filter by dates | ||||
// and it's not like we can sort things across sections by anything other | // and it's not like we can sort things across sections by anything other | ||||
// than dates | // than dates | ||||
@@ -64,7 +64,11 @@ impl TaxonomyItem { | |||||
.collect(); | .collect(); | ||||
let (mut pages, ignored_pages) = sort_pages_by_date(data); | let (mut pages, ignored_pages) = sort_pages_by_date(data); | ||||
let slug = slugify(name); | let slug = slugify(name); | ||||
let permalink = config.make_permalink(&format!("/{}/{}", path, slug)); | |||||
let permalink = if let Some(ref lang) = taxonomy.lang { | |||||
config.make_permalink(&format!("/{}/{}/{}", lang, taxonomy.name, slug)) | |||||
} else { | |||||
config.make_permalink(&format!("/{}/{}", taxonomy.name, slug)) | |||||
}; | |||||
// We still append pages without dates at the end | // We still append pages without dates at the end | ||||
pages.extend(ignored_pages); | pages.extend(ignored_pages); | ||||
@@ -108,7 +112,7 @@ impl Taxonomy { | |||||
) -> Taxonomy { | ) -> Taxonomy { | ||||
let mut sorted_items = vec![]; | let mut sorted_items = vec![]; | ||||
for (name, pages) in items { | for (name, pages) in items { | ||||
sorted_items.push(TaxonomyItem::new(&name, &kind.name, config, pages, library)); | |||||
sorted_items.push(TaxonomyItem::new(&name, &kind, config, pages, library)); | |||||
} | } | ||||
sorted_items.sort_by(|a, b| a.name.cmp(&b.name)); | sorted_items.sort_by(|a, b| a.name.cmp(&b.name)); | ||||
@@ -186,6 +190,14 @@ pub fn find_taxonomies(config: &Config, library: &Library) -> Result<Vec<Taxonom | |||||
for (name, val) in &page.meta.taxonomies { | for (name, val) in &page.meta.taxonomies { | ||||
if taxonomies_def.contains_key(name) { | if taxonomies_def.contains_key(name) { | ||||
if taxonomies_def[name].lang != page.lang { | |||||
bail!( | |||||
"Page `{}` has taxonomy `{}` which is not available in that language", | |||||
page.file.path.display(), | |||||
name | |||||
); | |||||
} | |||||
all_taxonomies.entry(name).or_insert_with(HashMap::new); | all_taxonomies.entry(name).or_insert_with(HashMap::new); | ||||
for v in val { | for v in val { | ||||
@@ -220,7 +232,7 @@ mod tests { | |||||
use super::*; | use super::*; | ||||
use std::collections::HashMap; | use std::collections::HashMap; | ||||
use config::{Config, Taxonomy as TaxonomyConfig}; | |||||
use config::{Config, Taxonomy as TaxonomyConfig, Language}; | |||||
use content::Page; | use content::Page; | ||||
use library::Library; | use library::Library; | ||||
@@ -326,4 +338,112 @@ mod tests { | |||||
"Page `` has taxonomy `tags` which is not defined in config.toml" | "Page `` has taxonomy `tags` which is not defined in config.toml" | ||||
); | ); | ||||
} | } | ||||
#[test] | |||||
fn can_make_taxonomies_in_multiple_languages() { | |||||
let mut config = Config::default(); | |||||
config.languages.push(Language {rss: false, code: "fr".to_string()}); | |||||
let mut library = Library::new(2, 0, true); | |||||
config.taxonomies = vec![ | |||||
TaxonomyConfig { name: "categories".to_string(), ..TaxonomyConfig::default() }, | |||||
TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() }, | |||||
TaxonomyConfig { name: "auteurs".to_string(), lang: Some("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; | |||||
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; | |||||
library.insert_page(page2); | |||||
let mut page3 = Page::default(); | |||||
page3.lang = Some("fr".to_string()); | |||||
let mut taxo_page3 = HashMap::new(); | |||||
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" => 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); | |||||
} | |||||
#[test] | |||||
fn errors_on_taxonomy_of_different_language() { | |||||
let mut config = Config::default(); | |||||
config.languages.push(Language {rss: false, code: "fr".to_string()}); | |||||
let mut library = Library::new(2, 0, false); | |||||
config.taxonomies = | |||||
vec![TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() }]; | |||||
let mut page1 = Page::default(); | |||||
page1.lang = Some("fr".to_string()); | |||||
let mut taxo_page1 = HashMap::new(); | |||||
taxo_page1.insert("tags".to_string(), vec!["rust".to_string(), "db".to_string()]); | |||||
page1.meta.taxonomies = taxo_page1; | |||||
library.insert_page(page1); | |||||
let taxonomies = find_taxonomies(&config, &library); | |||||
assert!(taxonomies.is_err()); | |||||
let err = taxonomies.unwrap_err(); | |||||
// no path as this is created by Default | |||||
assert_eq!( | |||||
err.description(), | |||||
"Page `` has taxonomy `tags` which is not available in that language" | |||||
); | |||||
} | |||||
} | } |
@@ -723,7 +723,13 @@ impl Site { | |||||
} | } | ||||
ensure_directory_exists(&self.output_path)?; | ensure_directory_exists(&self.output_path)?; | ||||
let output_path = self.output_path.join(&taxonomy.kind.name); | |||||
let output_path = if let Some(ref lang) = taxonomy.kind.lang { | |||||
let mid_path = self.output_path.join(lang); | |||||
create_directory(&mid_path)?; | |||||
mid_path.join(&taxonomy.kind.name) | |||||
} else { | |||||
self.output_path.join(&taxonomy.kind.name) | |||||
}; | |||||
let list_output = taxonomy.render_all_terms(&self.tera, &self.config, &self.library)?; | let list_output = taxonomy.render_all_terms(&self.tera, &self.config, &self.library)?; | ||||
create_directory(&output_path)?; | create_directory(&output_path)?; | ||||
create_file(&output_path.join("index.html"), &self.inject_livereload(list_output))?; | create_file(&output_path.join("index.html"), &self.inject_livereload(list_output))?; | ||||
@@ -479,6 +479,7 @@ fn can_build_site_with_pagination_for_taxonomy() { | |||||
paginate_by: Some(2), | paginate_by: Some(2), | ||||
paginate_path: None, | paginate_path: None, | ||||
rss: true, | rss: true, | ||||
lang: None, | |||||
}); | }); | ||||
site.load().unwrap(); | site.load().unwrap(); | ||||
@@ -125,4 +125,18 @@ fn can_build_multilingual_site() { | |||||
assert!(file_contains!(public, "fr/rss.xml", "https://example.com/fr/blog/something-else/")); | assert!(file_contains!(public, "fr/rss.xml", "https://example.com/fr/blog/something-else/")); | ||||
// Italian doesn't have RSS enabled | // Italian doesn't have RSS enabled | ||||
assert!(!file_exists!(public, "it/rss.xml")); | assert!(!file_exists!(public, "it/rss.xml")); | ||||
// Taxonomies are per-language | |||||
assert!(file_exists!(public, "authors/index.html")); | |||||
assert!(file_contains!(public, "authors/index.html", "Queen")); | |||||
assert!(!file_contains!(public, "authors/index.html", "Vincent")); | |||||
assert!(!file_exists!(public, "auteurs/index.html")); | |||||
assert!(file_exists!(public, "authors/queen-elizabeth/rss.xml")); | |||||
assert!(!file_exists!(public, "fr/authors/index.html")); | |||||
assert!(file_exists!(public, "fr/auteurs/index.html")); | |||||
assert!(!file_contains!(public, "fr/auteurs/index.html", "Queen")); | |||||
assert!(file_contains!(public, "fr/auteurs/index.html", "Vincent")); | |||||
assert!(!file_exists!(public, "fr/auteurs/vincent-prouillet/rss.xml")); | |||||
} | } |
@@ -297,7 +297,7 @@ mod tests { | |||||
fn can_get_taxonomy() { | fn can_get_taxonomy() { | ||||
let taxo_config = TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() }; | let taxo_config = TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() }; | ||||
let library = Library::new(0, 0, false); | let library = Library::new(0, 0, false); | ||||
let tag = TaxonomyItem::new("Programming", "tags", &Config::default(), vec![], &library); | |||||
let tag = TaxonomyItem::new("Programming", &taxo_config, &Config::default(), vec![], &library); | |||||
let tags = Taxonomy { kind: taxo_config, items: vec![tag] }; | let tags = Taxonomy { kind: taxo_config, items: vec![tag] }; | ||||
let taxonomies = vec![tags.clone()]; | let taxonomies = vec![tags.clone()]; | ||||
@@ -336,7 +336,7 @@ mod tests { | |||||
fn can_get_taxonomy_url() { | fn can_get_taxonomy_url() { | ||||
let taxo_config = TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() }; | let taxo_config = TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() }; | ||||
let library = Library::new(0, 0, false); | let library = Library::new(0, 0, false); | ||||
let tag = TaxonomyItem::new("Programming", "tags", &Config::default(), vec![], &library); | |||||
let tag = TaxonomyItem::new("Programming", &taxo_config, &Config::default(), vec![], &library); | |||||
let tags = Taxonomy { kind: taxo_config, items: vec![tag] }; | let tags = Taxonomy { kind: taxo_config, items: vec![tag] }; | ||||
let taxonomies = vec![tags.clone()]; | let taxonomies = vec![tags.clone()]; | ||||
@@ -16,6 +16,9 @@ languages = [ | |||||
] | ] | ||||
``` | ``` | ||||
If you want to use per-language taxonomies, ensure you set the `lang` field in their | |||||
configuration. | |||||
## Content | ## Content | ||||
Once the languages are added in, you can start to translate your content. Zola | Once the languages are added in, you can start to translate your content. Zola | ||||
uses the filename to detect the language: | uses the filename to detect the language: | ||||
@@ -7,13 +7,14 @@ Zola has built-in support for taxonomies. | |||||
The first step is to define the taxonomies in your [config.toml](./documentation/getting-started/configuration.md). | The first step is to define the taxonomies in your [config.toml](./documentation/getting-started/configuration.md). | ||||
A taxonomy has 4 variables: | |||||
A taxonomy has 5 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_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. | - `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 | For example the default would be page/1 | ||||
- `rss`: if set to `true`, a RSS feed will be generated for each individual term. | - `rss`: if set to `true`, a RSS feed will be generated for each individual 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 | Once this is done, you can then set taxonomies in your content and Zola will pick | ||||
them up: | them up: | ||||
@@ -13,6 +13,11 @@ build_search_index = false | |||||
generate_rss = true | generate_rss = true | ||||
taxonomies = [ | |||||
{name = "authors", rss = true}, | |||||
{name = "auteurs", lang = "fr"}, | |||||
] | |||||
languages = [ | languages = [ | ||||
{code = "fr", rss = true}, | {code = "fr", rss = true}, | ||||
{code = "it", rss = false}, | {code = "it", rss = false}, | ||||
@@ -1,6 +1,9 @@ | |||||
+++ | +++ | ||||
title = "Quelque chose" | title = "Quelque chose" | ||||
date = 2018-10-09 | date = 2018-10-09 | ||||
[taxonomies] | |||||
auteurs = ["Vincent Prouillet"] | |||||
+++ | +++ | ||||
Un article | Un article |
@@ -1,6 +1,9 @@ | |||||
+++ | +++ | ||||
title = "Something" | title = "Something" | ||||
date = 2018-10-09 | date = 2018-10-09 | ||||
[taxonomies] | |||||
authors = ["Queen Elizabeth"] | |||||
+++ | +++ | ||||
A blog post | A blog post |
@@ -0,0 +1,3 @@ | |||||
{% for author in terms %} | |||||
{{ author.name }} {{ author.slug }} {{ author.pages | length }} | |||||
{% endfor %} |
@@ -0,0 +1,21 @@ | |||||
{% if not paginator %} | |||||
Tag: {{ term.name }} | |||||
{% for page in term.pages %} | |||||
<article> | |||||
<h3 class="post__title"><a href="{{ page.permalink | safe }}">{{ page.title | safe }}</a></h3> | |||||
</article> | |||||
{% endfor %} | |||||
{% else %} | |||||
Tag: {{ term.name }} | |||||
{% for page in paginator.pages %} | |||||
{{page.title|safe}} | |||||
{% endfor %} | |||||
Num pagers: {{ paginator.number_pagers }} | |||||
Page size: {{ paginator.paginate_by }} | |||||
Current index: {{ paginator.current_index }} | |||||
First: {{ paginator.first | safe }} | |||||
Last: {{ paginator.last | safe }} | |||||
{% if paginator.previous %}has_prev{% endif%} | |||||
{% if paginator.next %}has_next{% endif%} | |||||
{% endif %} |
@@ -0,0 +1,3 @@ | |||||
{% for term in terms %} | |||||
{{ term.name }} {{ term.slug }} {{ term.pages | length }} | |||||
{% endfor %} |
@@ -0,0 +1,21 @@ | |||||
{% if not paginator %} | |||||
Tag: {{ term.name }} | |||||
{% for page in term.pages %} | |||||
<article> | |||||
<h3 class="post__title"><a href="{{ page.permalink | safe }}">{{ page.title | safe }}</a></h3> | |||||
</article> | |||||
{% endfor %} | |||||
{% else %} | |||||
Tag: {{ term.name }} | |||||
{% for page in paginator.pages %} | |||||
{{page.title|safe}} | |||||
{% endfor %} | |||||
Num pagers: {{ paginator.number_pagers }} | |||||
Page size: {{ paginator.paginate_by }} | |||||
Current index: {{ paginator.current_index }} | |||||
First: {{ paginator.first | safe }} | |||||
Last: {{ paginator.last | safe }} | |||||
{% if paginator.previous %}has_prev{% endif%} | |||||
{% if paginator.next %}has_next{% endif%} | |||||
{% endif %} |