Skip to content

Commit a103e6b

Browse files
authored
Merge pull request #1 from apvarun/add-search
Add search UI
2 parents 414c5aa + 21bc39d commit a103e6b

18 files changed

+296
-14
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Changelog
2+
3+
All the changes made to Showcase theme for Hugo.
4+
5+
## v1.1.0 - 2021-07-17
6+
7+
### Added
8+
9+
- Add support for text search
10+
11+
## v1.0.0 - 2021-07-16
12+
13+
- Initial Release

README.md

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ Blist is a clean and fast blog theme for your Hugo site.
1212

1313
- Responsive content
1414
- Blog pagination
15+
- Text Search
1516
- Social links
1617
- Dark mode
18+
- Fast performance
1719

1820
## Preview
1921

@@ -43,7 +45,6 @@ Blist theme ships with an fully configured example site. For a quick preview:
4345

4446
Copy the `package.json` file from `themes/showcase` folder to your hugo website root folder, and run `npm install`.
4547

46-
4748
```sh
4849
cd themes/blist/exampleSite/
4950
hugo serve --themesDir ../..
@@ -68,7 +69,7 @@ The following explains how to add content to your Hugo site. You can find sample
6869
├── blog # Blog Section
6970
│ ├── post1 # Post 1
7071
│ ├── post2 # Post 2
71-
│ └── _index
72+
│ └── _index
7273
└── ...
7374

7475
## Configure your site
@@ -79,14 +80,41 @@ From `exampleSite/`, copy `config.toml` to the root folder of your Hugo site and
7980

8081
Menu in Blist theme is pre-set to have all section names. You can include custom links in header using the `menu.main` option config.toml.
8182

83+
## Darkmode
84+
85+
`[params.darkModeToggle]` enables the dark mode toggle in header. The preference is then saved so that the mode is automatically chosen for return visits.
86+
87+
## Search
88+
89+
`[params.enableSearch]` option is used to enable search option in the theme.
90+
91+
- Adds the search icon in header
92+
- Generates the search index
93+
- Uses fuse.js to enable searching through content
94+
95+
In order to search, you can either click on the search icon from header or press `Ctrl/Cmd + /` key combination.
96+
97+
**Note:**
98+
99+
Make sure to enable JSON in outputs array.
100+
101+
```
102+
[outputs]
103+
home = ["HTML", "RSS", "JSON"]
104+
```
105+
82106
### Latex
83107

84108
Enable Mathematical options: set `math: true` in your markdown frontmatter
85109

86-
## Google Analytics
110+
### Google Analytics
87111

88112
Set `googleAnalytics` in `config.toml` to activate Hugo's [internal Google Analytics template](https://gohugo.io/templates/internal/#google-analytics).
89113

114+
## Performance
115+
116+
[![Pagespeed Insights Performance](https://github.com/apvarun/blist-hugo-theme/raw/main/images/pagespeed-performance.png)](https://developers.google.com/speed/pagespeed/insights/?url=https%3A%2F%2Fblist.vercel.app&tab=mobile)
117+
90118
## Issues
91119

92120
If you have a question, please [open an issue](https://github.com/apvarun/blist-hugo-theme/issues) for help and to help those who come after you. The more information you can provide, the better!

assets/css/tailwind.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module.exports = {
88
themeDir + "content/**/*.html",
99
"layouts/**/*.html",
1010
"content/**/*.html",
11+
"assets/js/search.js",
1112
"exampleSite/layouts/**/*.html",
1213
"exampleSite/content/**/*.html",
1314
],

assets/js/fuse.min.js

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assets/js/search.js

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// Credits to search implementation: https://gist.github.com/cmod/5410eae147e4318164258742dd053993
2+
3+
var fuse; // holds our search engine
4+
var searchVisible = false;
5+
var firstRun = true; // allow us to delay loading json data unless search activated
6+
var list = document.querySelector('.search-list'); // targets the <ul>
7+
var first = list.firstChild; // first child of search list
8+
var last = list.lastChild; // last child of search list
9+
var maininput = document.querySelector('.search-ui input'); // input box for search
10+
var searchResultsHeading = document.querySelector('.search-results'); // input box for search
11+
var noResults = document.querySelector('.no-results'); // input box for search
12+
var resultsAvailable = false; // Did we get any search results?
13+
14+
// ==========================================
15+
// The main keyboard event listener running the show
16+
//
17+
document.querySelector('.open-search').addEventListener('click', openSearch);
18+
document.querySelector('.close-search').addEventListener('click', closeSearch);
19+
20+
function closeSearch() {
21+
document.querySelector('.search-ui').classList.add("hidden");
22+
document.activeElement.blur(); // remove focus from search box
23+
searchVisible = false; // search not visible
24+
searchResultsHeading.classList.add('hidden');
25+
}
26+
27+
function openSearch() {
28+
// Load json search index if first time invoking search
29+
// Means we don't load json unless searches are going to happen; keep user payload small unless needed
30+
if (firstRun) {
31+
loadSearch(); // loads our json data and builds fuse.js search index
32+
firstRun = false; // let's never do this again
33+
}
34+
35+
// Toggle visibility of search box
36+
if (!searchVisible) {
37+
document.querySelector('.search-ui').classList.remove("hidden");
38+
document.querySelector('.search-ui input').focus(); // put focus in input box so you can just start typing
39+
searchVisible = true; // search visible
40+
}
41+
}
42+
43+
document.addEventListener('keydown', function (event) {
44+
45+
if (event.metaKey && event.which === 191) {
46+
openSearch()
47+
}
48+
49+
// Allow ESC (27) to close search box
50+
if (event.keyCode == 27) {
51+
if (searchVisible) {
52+
document.querySelector('.search-ui').classList.add("hidden");
53+
document.activeElement.blur();
54+
searchVisible = false;
55+
searchResultsHeading.classList.add('hidden');
56+
}
57+
}
58+
59+
// DOWN (40) arrow
60+
if (event.keyCode == 40) {
61+
if (searchVisible && resultsAvailable) {
62+
console.log("down");
63+
event.preventDefault(); // stop window from scrolling
64+
if (document.activeElement == maininput) { first.focus(); } // if the currently focused element is the main input --> focus the first <li>
65+
else if (document.activeElement == last) { last.focus(); } // if we're at the bottom, stay there
66+
else { document.activeElement.parentElement.nextSibling.firstElementChild.focus(); } // otherwise select the next search result
67+
}
68+
}
69+
70+
// UP (38) arrow
71+
if (event.keyCode == 38) {
72+
if (searchVisible && resultsAvailable) {
73+
event.preventDefault(); // stop window from scrolling
74+
if (document.activeElement == maininput) { maininput.focus(); } // If we're in the input box, do nothing
75+
else if (document.activeElement == first) { maininput.focus(); } // If we're at the first item, go to input box
76+
else { document.activeElement.parentElement.previousSibling.firstElementChild.focus(); } // Otherwise, select the search result above the current active one
77+
}
78+
}
79+
})
80+
81+
82+
// ==========================================
83+
// execute search as each character is typed
84+
//
85+
document.querySelector('.search-ui input').onkeyup = function (e) {
86+
executeSearch(this.value);
87+
}
88+
89+
90+
// ==========================================
91+
// fetch some json without jquery
92+
//
93+
function fetchJSONFile(path, callback) {
94+
var httpRequest = new XMLHttpRequest();
95+
httpRequest.onreadystatechange = function () {
96+
if (httpRequest.readyState === 4) {
97+
if (httpRequest.status === 200) {
98+
var data = JSON.parse(httpRequest.responseText);
99+
if (callback) callback(data);
100+
}
101+
}
102+
};
103+
httpRequest.open('GET', path);
104+
httpRequest.send();
105+
}
106+
107+
108+
// ==========================================
109+
// load our search index, only executed once
110+
// on first call of search box (CMD-/)
111+
//
112+
function loadSearch() {
113+
fetchJSONFile('/index.json', function (data) {
114+
115+
var options = { // fuse.js options; check fuse.js website for details
116+
shouldSort: true,
117+
location: 0,
118+
distance: 100,
119+
threshold: 0.4,
120+
minMatchCharLength: 2,
121+
keys: [
122+
'title',
123+
'permalink',
124+
'contents'
125+
]
126+
};
127+
fuse = new Fuse(data, options); // build the index from the json file
128+
});
129+
}
130+
131+
132+
// ==========================================
133+
// using the index we loaded on CMD-/, run
134+
// a search query (for "term") every time a letter is typed
135+
// in the search box
136+
//
137+
function executeSearch(term) {
138+
let results = fuse.search(term); // the actual query being run using fuse.js
139+
let searchitems = ''; // our results bucket
140+
141+
if (results.length === 0) { // no results based on what was typed into the input box
142+
resultsAvailable = false;
143+
searchitems = '';
144+
if (term !== "") {
145+
noResults.classList.remove('hidden')
146+
} else {
147+
noResults.classList.add('hidden')
148+
}
149+
} else { // build our html
150+
noResults.classList.add('hidden')
151+
if (term !== "") {
152+
searchResultsHeading.classList.remove('hidden');
153+
}
154+
155+
for (let item in results.slice(0, 5)) { // only show first 5 results
156+
const title = '<div class="text-2xl mb-2 font-bold">' + results[item].item.title + '</div>';
157+
const date = results[item].item.date ? '<div><em class="">' + new Date(results[item].item.date).toDateString() + '</em></div>' : '';
158+
const contents = '<div>' + results[item].item.contents + '</div>';
159+
160+
searchitems = searchitems + '<li><a class="block mb-2 px-4 py-2 rounded pb-2 border-b border-gray-200 dark:border-gray-600 focus:bg-gray-100 dark:focus:bg-gray-700 focus:outline-none" href="' + results[item].item.permalink + '" tabindex="0">' + title + date + contents + '</a></li>';
161+
}
162+
resultsAvailable = true;
163+
}
164+
165+
list.innerHTML = searchitems;
166+
if (results.length > 0) {
167+
first = list.firstChild.firstElementChild; // first result container — used for checking against keyboard up/down location
168+
last = list.lastChild.firstElementChild; // last result container — used for checking against keyboard up/down location
169+
}
170+
}

exampleSite/config.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ baseurl = "https://blist.vercel.app"
44
title = "Blist Theme"
55

66
[params]
7-
name = "Blist Theme"
87
description = "Modern blog theme for your Hugo site."
8+
name = "Blist Theme"
99

1010
# Enable the darkmode toggle in header
1111
darkModeToggle = true
12+
13+
# Enable search in header
14+
enableSearch = true
1215

1316
# Custom copyright - optional
1417
copyright = "Copyright © 2021 - Katheryn Fox · All rights reserved"
@@ -35,6 +38,9 @@ title = "Blist Theme"
3538
[build]
3639
writeStats = true
3740

41+
[outputs]
42+
home = ["HTML", "RSS", "JSON"]
43+
3844
# syntax highlight settings
3945
[markup]
4046
[markup.highlight]

images/pagespeed-performance.png

29 KB
Loading

layouts/_default/baseof.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<!DOCTYPE html>
22
<html lang="{{ .Site.LanguageCode }}">
33
{{- partial "head.html" . -}}
4-
<body class="dark:bg-gray-800 dark:text-white">
4+
<body class="dark:bg-gray-800 dark:text-white relative">
55
{{- partial "header.html" . -}}
66
<main>
77
{{- block "main" . }}{{- end }}

layouts/_default/index.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{{- $.Scratch.Add "index" slice -}}
2+
{{- range .Site.RegularPages -}}
3+
{{- $.Scratch.Add "index" (dict "title" .Title "tags" .Params.tags "date" .Params.Lastmod "categories" .Params.categories "contents" .Summary "permalink" .Permalink) -}}
4+
{{- end -}}
5+
{{- $.Scratch.Get "index" | jsonify -}}

layouts/partials/blog-card.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<a class="p-2" href="{{ .Permalink }}">
22
{{ if .Params.thumbnail }}
33
<div class="relative">
4-
<img src="{{ .Params.thumbnail }}" class="rounded-lg shadow-sm w-full h-52 object-cover" />
4+
<img src="{{ .Params.thumbnail }}" alt="{{ .Params.title }}" class="rounded-lg shadow-sm w-full h-52 object-cover" />
55
<div class="absolute top-4 right-4 rounded shadow bg-white text-gray-900 dark:bg-gray-900 dark:text-white text-sm px-2 py-0.5">
66
{{ .Params.date.Format "Jan 2, 06" }}
77
</div>

0 commit comments

Comments
 (0)