Search
Hwaro generates a search index that works with Fuse.js for client-side search.
Configuration
Enable in config.toml:
[search]
enabled = true
format = "fuse_json"
fields = ["title", "content", "description", "tags", "url", "section"]
filename = "search.json"
exclude = ["/private", "/drafts"]
| Key | Type | Default | Description |
|---|---|---|---|
| enabled | bool | false | Generate search index |
| format | string | "fuse_json" | Search index format |
| fields | array | ["title", "content"] | Fields to include in index |
| filename | string | "search.json" | Output filename |
| exclude | array | [] | Paths (prefixes) to exclude from search index |
| tokenize_cjk | bool | false | Enable CJK bigram tokenization |
Generated Files
When enabled, Hwaro generates /search.json (configurable via filename):
[
{
"title": "My Post",
"url": "/blog/my-post/",
"content": "Page content...",
"description": "Post description",
"section": "blog",
"tags": ["tutorial"]
}
]
Fields Indexed
| Field | Description |
|---|---|
| title | Page title |
| url | Page URL |
| content | Page content (if "content" is in fields) |
| description | Page description |
| section | Section name |
| tags | Page tags |
Client-Side Implementation
Using Fuse.js
Add to your template:
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0"></script>
<script>
let searchIndex = [];
// Load index
fetch('/search.json')
.then(res => res.json())
.then(data => {
searchIndex = data;
});
// Initialize Fuse.js
function search(query) {
const fuse = new Fuse(searchIndex, {
keys: ['title', 'content', 'description', 'tags'],
threshold: 0.3
});
return fuse.search(query);
}
</script>
Search Form
<form id="search-form">
<input type="search" id="search-input" placeholder="Search...">
</form>
<div id="search-results"></div>
<script>
const input = document.getElementById('search-input');
const results = document.getElementById('search-results');
input.addEventListener('input', (e) => {
const query = e.target.value;
if (query.length < 2) {
results.innerHTML = '';
return;
}
const matches = search(query);
results.innerHTML = matches
.slice(0, 10)
.map(m => `
<a href="${m.item.url}">
<h3>${m.item.title}</h3>
<p>${m.item.description || ''}</p>
</a>
`)
.join('');
});
</script>
CJK Search Support
For sites with Chinese, Japanese, or Korean content, enable CJK tokenization to improve search accuracy. CJK languages often lack spaces between words, making it difficult for search libraries to tokenize text properly.
When enabled, CJK character runs are split into overlapping bigrams (2-character pairs), allowing search terms to match within longer text.
Example: "검색엔진" → "검색 색엔 엔진" (search query "검색" now matches)
Configuration
[search]
enabled = true
tokenize_cjk = true
| Key | Type | Default | Description |
|---|---|---|---|
| tokenize_cjk | bool | false | Enable CJK bigram tokenization for search index |
How It Works
- Only
title,content, anddescriptionfields are tokenized url,tags, andsectionfields are left unchanged (structural fields)- Non-CJK text passes through unmodified
- Works with both Fuse.js and ElasticLunr formats
Notes
- Enabling this option slightly increases the search index size
- The bigram approach works well for most CJK search scenarios
- Korean text with natural spaces (e.g.,
"검색 엔진") is handled correctly
Excluding Pages
Front Matter
Exclude individual pages from search with front matter:
+++
title = "Terms of Service"
in_search_index = false
+++
Configuration
Exclude entire sections or paths using config.toml:
[search]
exclude = ["/private", "/drafts"]
Field Selection
Control which fields appear in the search index by specifying fields:
[search]
enabled = true
fields = ["title", "description", "tags", "url"]
Available fields: title, content, description, tags, url, section.
Omitting content from fields significantly reduces the index file size for large sites.
Performance Tips
Large Sites
For sites with many pages:
- Remove
"content"fromfieldsto reduce index size - Use Fuse.js
ignoreLocationoption - Implement debounced search
function debounce(fn, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), delay);
};
}
input.addEventListener('input', debounce((e) => {
// search logic
}, 200));
Lazy Loading
Load index only when search is focused:
let indexLoaded = false;
input.addEventListener('focus', async () => {
if (indexLoaded) return;
const res = await fetch('/search.json');
searchIndex = await res.json();
indexLoaded = true;
});
Alternative: Pagefind
For larger sites, consider Pagefind:
# After build
npx pagefind --site public
Add to config as post-build hook:
[build]
hooks.post = ["npx pagefind --site public"]
See Also
- Configuration — Search config reference
- Multilingual — CJK tokenization and i18n search