Skip to content

Commit 60f65c8

Browse files
committed
Merge branch 'migrate-search-to-hugo-and-pagefind'
With this topic branch, the server-side search is replaced by a client-side search that uses Pagefind (https://pagefind.app/) to generate a nicely-chunked search index that can be loaded on demand, incrementally, and efficiently. While Hugo takes about half a minute to run in my setup, building the search index takes about three minutes, testament of the fact that generating search indices is a much more complex task than rendering a static site. Signed-off-by: Johannes Schindelin <[email protected]>
2 parents 6ccbdc0 + 286bb1d commit 60f65c8

File tree

16 files changed

+408
-67
lines changed

16 files changed

+408
-67
lines changed

.github/workflows/ci.yml

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ jobs:
77
steps:
88
- uses: actions/checkout@v4
99

10-
- name: configure Hugo version
10+
- name: configure Hugo and Pagefind version
1111
run: |
1212
set -x &&
1313
echo "HUGO_VERSION=$(sed -n 's/^ *hugo_version: *//p' <hugo.yml)" >>$GITHUB_ENV
14+
echo "PAGEFIND_VERSION=$(sed -n 's/^ *pagefind_version: *//p' <hugo.yml)" >>$GITHUB_ENV
1415
1516
- name: install Hugo ${{ env.HUGO_VERSION }}
1617
run: |
@@ -21,6 +22,9 @@ jobs:
2122
- name: run Hugo to build the pages
2223
run: hugo
2324

25+
- name: run Pagefind ${{ env.PAGEFIND_VERSION }} to build the search index
26+
run: npx -y pagefind@${{ env.PAGEFIND_VERSION }} --site public
27+
2428
- name: build tar archive
2529
run: cd public && tar czvf ../pages.tar.gz *
2630

@@ -29,3 +33,17 @@ jobs:
2933
with:
3034
name: pages
3135
path: pages.tar.gz
36+
37+
- name: verify search results
38+
run: |
39+
set -x
40+
res=0
41+
for term in commit config log rev-parse
42+
do
43+
node ./script/run-pagefind.js "$term" | tee search.results >&2
44+
grep "^1. /docs/git-$term\\.html" search.results || {
45+
echo "::error::Search for $term failed to show the manual page first" >&2
46+
res=1
47+
}
48+
done
49+
exit $res

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,19 @@ Side note: What _are_ "ugly URLs"? Hugo, by default, generates "pretty" URLs lik
5151

5252
Pro-Tip: Do this in a sparse checkout that excludes large parts of `content/`, to speed up the rendering time.
5353

54+
To test the site locally _with_ the search enabled, run this instead:
55+
56+
$ hugo
57+
$ npx -y pagefind --site public
58+
$ node script/serve-public.js
59+
60+
You can also use Pagefind's built-in server (which will be running on http://127.0.0.1:1414), but again, you have to turn off "ugly URLs":
61+
62+
$ HUGO_UGLYURLS=false hugo
63+
$ npx -y pagefind --site public --serve
64+
65+
Note that running Pagefind will make the process about 7 times slower, and the site will not be re-rendered and live-reloaded in the browser when you change files in `content/` (unlike with `hugo serve -w`).
66+
5467
## Update manual pages
5568

5669
(TODO!)
@@ -134,6 +147,10 @@ The [list of GUI clients](https://git-scm.com/downloads/guis) has been construct
134147
* https://gohugo.io/
135148
* https://gohugo.io/content-management/shortcodes/
136149

150+
### Pagefind (client-side search)
151+
152+
* https://pagefind.app/
153+
137154
## License
138155

139156
The source code for the site is licensed under the MIT license, which you can find in

app/views/site/results.html.erb

Lines changed: 0 additions & 52 deletions
This file was deleted.

assets/js/application.js

Lines changed: 124 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ var Search = {
9898
selectedIndex: 0,
9999

100100
init: function() {
101+
Search.displayFullSearchResults();
101102
Search.observeFocus();
102103
Search.observeTextEntry();
103104
Search.observeResultsClicks();
@@ -167,10 +168,78 @@ var Search = {
167168

168169
if(term != Search.currentSearch) {
169170
Search.currentSearch = term;
170-
$.get("/search", {search: term}, function(results) {
171-
$("#search-results").html(results);
171+
$("#search-results").html(`
172+
<header> Search Results </header>
173+
<table>
174+
<tbody>
175+
<tr class="show-all">
176+
<td class="category"> &nbsp; </td>
177+
<td class="matches">
178+
<ul>
179+
<li>
180+
<a class="highlight" id="show-results-label">Searching for <span id="search-term">&nbsp;</span>...</a>
181+
</li>
182+
</ul>
183+
</td>
184+
</tr>
185+
<tr>
186+
<td class="category"> &nbsp; </td>
187+
<td class="matches">
188+
<ul>
189+
<li><button id="load-more-results">Loading</button></li>
190+
</ul>
191+
</td>
192+
</tr>
193+
</tbody>
194+
</table>
195+
`);
196+
$("#search-term").text(term);
197+
this.initializeSearchIndex(async () => {
198+
const results = await Search.pagefind.debouncedSearch(term);
199+
if (results === null) return;
200+
if (results.results.length === 0) {
201+
$("#show-results-label").text("No matching pages found.");
202+
return;
203+
}
204+
$("#show-results-label").text("Show all results...");
205+
const loadButton = $("#load-more-results");
206+
loadButton.text(`Loading ${
207+
results.results.length < 2
208+
? "result"
209+
: `${results.results.length} results`
210+
}`);
211+
loadButton.loading = false;
212+
213+
const chunkLength = 10;
214+
let displayCount = 0;
215+
const loadResultsChunk = () => {
216+
if (loadButton.loading || displayCount >= results.results.length) return;
217+
218+
loadButton.loading = true;
219+
const n = displayCount + chunkLength;
220+
while (displayCount < n) {
221+
const li = $("<li><a>&hellip;</a></li>");
222+
li.insertBefore(loadButton);
223+
224+
// load the result lazily
225+
(async () => {
226+
const result = await results.results[displayCount].data();
227+
li.html(`<a href = "${result.url}">${result.meta.title}</a>`);
228+
})().catch(console.log);
229+
230+
if (++displayCount >= results.results.length) {
231+
loadButton.remove();
232+
return;
233+
}
234+
}
235+
const remaining = results.results.length - displayCount;
236+
loadButton.text(`Load ${remaining} more ${remaining < 2 ? "result" : "results"}`);
237+
loadButton.loading = false;
238+
};
239+
loadResultsChunk();
240+
loadButton.on("click", loadResultsChunk);
172241
Search.searching = false;
173-
}, 'html');
242+
});
174243
};
175244
}
176245
else {
@@ -186,8 +255,9 @@ var Search = {
186255
var link = $('#search-results a')[Search.selectedIndex];
187256
var url = $(link).attr('href');
188257
if(!url) {
189-
var term = $('#search-text').val();
190-
url = "/search/results?search=" + term;
258+
const term = $('#search-text').val();
259+
const language = document.querySelector("html")?.getAttribute("lang");
260+
url = `search/results?search=${term}${language && `&language=${language}`}`;
191261
}
192262
window.location.href = url;
193263
selectedIndex = 0;
@@ -207,6 +277,55 @@ var Search = {
207277
$('form#search').switchClass("focus", "", 200);
208278
$('#search-results').fadeOut(0.2);
209279
Search.selectedIndex = 0;
280+
},
281+
282+
getQueryValue: function(key) {
283+
const query = window.location.search.substring(1);
284+
const needle = `${key}=`;
285+
return query
286+
.split('&')
287+
.filter(e => e.startsWith(needle))
288+
.map(e => decodeURIComponent(e.substring(needle.length).replace(/\+/g, '%20')))
289+
.pop();
290+
},
291+
292+
initializeSearchIndex: function(callback) {
293+
if (Search.pagefind) {
294+
callback().catch(console.log);
295+
return;
296+
}
297+
(async () => {
298+
Search.pagefind = await import(`${baseURLPrefix}pagefind/pagefind.js`);
299+
const options = {}
300+
const language = this.getQueryValue('language');
301+
if (language) options.language = language;
302+
await Search.pagefind.options(options);
303+
await Search.pagefind.init();
304+
await callback();
305+
})().catch(console.log);
306+
},
307+
308+
displayFullSearchResults: function() {
309+
if (!$("#search-div").length) return;
310+
311+
const language = this.getQueryValue('language');
312+
313+
const ui = new PagefindUI({
314+
element: "#search-div",
315+
showSubResults: true,
316+
showImages: false,
317+
language,
318+
processResult: function (result) {
319+
result.url = result.url.replace(/\.html$/, "")
320+
return result
321+
},
322+
});
323+
324+
const searchTerm = this.getQueryValue('search');
325+
if (searchTerm) {
326+
$("#search-div input").val(searchTerm)
327+
ui.triggerSearch(searchTerm);
328+
}
210329
}
211330
}
212331

assets/sass/forms.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ form#search {
3939
width: 262px;
4040
padding-left: 32px;
4141
@include background-image-2x($baseurl + "images/icons/search", 17px, 17px, 10px 50%);
42-
background-color: #fcfcfa !important;
42+
background-color: $main-bg !important;
4343
border: solid 1px #ceccc5;
4444
@include border-radius(20px);
4545
@include box-shadow(inset 0 1px 4px #ddd);

assets/sass/layout.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ aside {
4040
#main {
4141
padding: 22px;
4242
margin-bottom: 35px;
43-
background-color: #fcfcfa;
43+
background-color: $main-bg;
4444
border: solid 1px #e2e0d8;
4545
@include border-radius(5px);
4646

assets/sass/normalize.scss

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ hgroup,
4141
main,
4242
menu,
4343
nav,
44-
section,
45-
summary {
44+
section {
4645
display: block;
4746
}
4847

assets/sass/search.scss

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,12 @@
5050
td.matches {
5151
padding: 0 !important;
5252
font-size: 12px;
53-
font-weight: bold;
5453

5554
a {
5655
display: block;
5756
padding: 5px 12px;
5857
margin-bottom: 0;
58+
font-weight: bold;
5959
color: $font-color;
6060
transition-duration: 0s;
6161

@@ -143,3 +143,43 @@ ol.full-search-results {
143143
margin-top: 4px;
144144
}
145145
}
146+
147+
// Search results page
148+
#search-div {
149+
border: solid 1px $base-border-color;
150+
background: $main-bg;
151+
padding: 22px;
152+
@include border-radius(5px);
153+
154+
ol {
155+
background-color: $callout-color;
156+
@include border-radius(3px);
157+
padding: 8px 14px 4px;
158+
margin: 0 0 1.4em;
159+
list-style: none;
160+
}
161+
162+
163+
.pagefind-ui__result-inner > .pagefind-ui__result-title {
164+
font-size: 16px;
165+
line-height: $base-line-height * 2;
166+
font-weight: bold;
167+
}
168+
169+
.pagefind-ui__result-tags {
170+
list-style: none;
171+
color: $light-font-color;
172+
margin-bottom: 1.4em;
173+
margin-left: 0;
174+
}
175+
}
176+
177+
button {
178+
border-color: $link-color;
179+
color: $link-color;
180+
background: $callout-color;
181+
font-weight: bold;
182+
@include border-radius(3px);
183+
outline: none;
184+
padding: 0 0.5em;
185+
}

assets/sass/variables.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ $base-border-color: #d8d7cf;
3232
$callout-color: #e9e8e0;
3333

3434
$black-3 : #333333;
35+
$main-bg: #fcfcfa;

content/search/_index.html

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
section: search
3+
url: /search.html
4+
aliases:
5+
- /search/index.html
6+
---
7+
8+
<header>Search is now client-side only!</header>
9+
10+
<table>
11+
<tbody><tr class="show-all">
12+
<td class="category"> &nbsp; </td>
13+
<td class="matches">
14+
<ul>
15+
<li>
16+
<a class="highlight">Search is now client-side only!</a>
17+
</li>
18+
<li>
19+
Since moving from a full Ruby on Rails app to a static site, the search on https://git-scm.com/ is implemented in Javascript, on the client side. Therefore, the server-side search via https://git-scm.com/search?search=&lt;search-term&gt; is no longer supported.
20+
</li>
21+
</ul>
22+
</td>
23+
</tr>
24+
</tbody></table>

0 commit comments

Comments
 (0)