Fuse.js search format

Summary

Add support for Fuse.js’s search format, which coincidentally is also accepted by Tinysearch.

Motivation

I’m working Bevy’s website and I’d like to use Fuse for search. Zola’s CONTRIBUTING.md instructs me to ask on the forum before making an MR, so here we are.

Patch

Of course, before checking Zola’s CONTRIBUTING.md, I wrote a technically-working patch.

diff --git a/components/config/src/config/search.rs b/components/config/src/config/search.rs
index 3ce6878c..6d0a3373 100644
--- a/components/config/src/config/search.rs
+++ b/components/config/src/config/search.rs
@@ -7,6 +7,7 @@ pub enum IndexFormat {
     ElasticlunrJson,
     #[default]
     ElasticlunrJavascript,
+    FuseJson,
 }
 
 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
diff --git a/components/search/src/lib.rs b/components/search/src/lib.rs
index 51d25579..0022f21a 100644
--- a/components/search/src/lib.rs
+++ b/components/search/src/lib.rs
@@ -12,7 +12,7 @@ use errors::{bail, Result};
 
 pub const ELASTICLUNR_JS: &str = include_str!("elasticlunr.min.js");
 
-static AMMONIA: Lazy<ammonia::Builder<'static>> = Lazy::new(|| {
+pub static AMMONIA: Lazy<ammonia::Builder<'static>> = Lazy::new(|| {
     let mut clean_content = HashSet::new();
     clean_content.insert("script");
     clean_content.insert("style");
diff --git a/components/site/Cargo.toml b/components/site/Cargo.toml
index 3388f2dc..b26deb65 100644
--- a/components/site/Cargo.toml
+++ b/components/site/Cargo.toml
@@ -18,6 +18,7 @@ imageproc = { path = "../imageproc" }
 link_checker = { path = "../link_checker" }
 libs = { path = "../libs" }
 content = { path = "../content" }
+serde_json = "1.0.117"
 
 [dev-dependencies]
 tempfile = "3"
diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs
index b9068851..5ab9a12a 100644
--- a/components/site/src/lib.rs
+++ b/components/site/src/lib.rs
@@ -793,15 +793,52 @@ impl Site {
     }
 
     fn index_for_lang(&self, lang: &str) -> Result<()> {
-        let index_json = search::build_index(lang, &self.library.read().unwrap(), &self.config)?;
         let (path, content) = match &self.config.search.index_format {
-            IndexFormat::ElasticlunrJson => {
-                let path = self.output_path.join(format!("search_index.{}.json", lang));
-                (path, index_json)
+            format @ IndexFormat::ElasticlunrJavascript | format @ IndexFormat::ElasticlunrJson => {
+                let index_json =
+                    search::build_index(lang, &self.library.read().unwrap(), &self.config)?;
+                if *format == IndexFormat::ElasticlunrJson {
+                    let path = self.output_path.join(format!("search_index.{}.json", lang));
+                    (path, index_json)
+                } else {
+                    let path = self.output_path.join(format!("search_index.{}.js", lang));
+                    let content = format!("window.searchIndex = {};", index_json);
+                    (path, content)
+                }
             }
-            IndexFormat::ElasticlunrJavascript => {
-                let path = self.output_path.join(format!("search_index.{}.js", lang));
-                let content = format!("window.searchIndex = {};", index_json);
+            IndexFormat::FuseJson => {
+                #[derive(serde::Serialize)]
+                struct Item {
+                    title: String,
+                    body: String,
+                    url: String,
+                }
+                let path = self.output_path.join(format!("search_index.{}.json", lang));
+                let mut items: Vec<Item> = Vec::new();
+                let library = self.library.read().unwrap();
+                for (_, section) in &library.sections {
+                    if section.lang == lang
+                        && section.meta.redirect_to.is_none()
+                        && section.meta.in_search_index
+                    {
+                        items.push(Item {
+                            title: section.meta.title.clone().unwrap_or_default(),
+                            body: search::AMMONIA.clean(&section.content).to_string(),
+                            url: section.permalink.clone(),
+                        });
+                        for page in &section.pages {
+                            let page = &library.pages[page];
+                            if page.meta.in_search_index {
+                                items.push(Item {
+                                    title: page.meta.title.clone().unwrap_or_default(),
+                                    body: search::AMMONIA.clean(&page.content).to_string(),
+                                    url: page.permalink.clone(),
+                                })
+                            }
+                        }
+                    }
+                }
+                let content = serde_json::to_string(&items)?;
                 (path, content)
             }
         };

It still needs support for search config but it does work.

1 Like