Skip to content

Commit 25fc93d

Browse files
committed
wip(web): emit the docs search index from the doc section
1 parent 5f19773 commit 25fc93d

7 files changed

Lines changed: 170 additions & 8 deletions

File tree

‎web/SITES_MERGE_PLAYBOOK.md‎

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,25 @@ before `hugo`; outputs are gitignored (`web/.gitignore`). Notes:
440440
prefixed with the baseURL path (e.g. relref output) are left untouched —
441441
without it they were double-prefixed under a subpath.
442442

443+
### 22. Docs search rewired to a doc-section index
444+
445+
The lunr search 404'd: sg-doc emits the search index as the *site home's*
446+
JSON output (`themes/sg-doc/layouts/index.json` → `/index.json`), and the
447+
merged site's home belongs to the main web. Now the index is an output of the
448+
**doc section root** instead, named for its purpose:
449+
450+
- `[outputFormats.SearchIndex]` in `config.toml` (baseName `search-index`) +
451+
`outputs: ["html", "searchindex"]` on the doc root `_index.md` (injected by
452+
`import-docs.py` on re-import) → `/doc/latest/search-index.json`.
453+
- `layouts/doc/list.searchindex.json` — the theme's index template scoped to
454+
`Type == "doc"` pages only (the original indexed all site pages; correct
455+
when the site was docs-only, wrong now).
456+
- `partials/search.html` sets the JS `baseurl` to the doc section root
457+
(`.FirstSection.Permalink`) instead of the site base — this also makes
458+
search.js's version-aware paths key off `latest` correctly.
459+
- `static/js/search.js` — project override of the theme copy with the three
460+
`index.json` fetch paths renamed to `search-index.json`.
461+
443462
## Verified
444463

445464
- `hugo` builds with **0 errors** (379 pages)
@@ -468,9 +487,9 @@ before `hugo`; outputs are gitignored (`web/.gitignore`). Notes:
468487
a Hugo meta-refresh, not a real 301 — production needs the server-level
469488
redirect. Public URL parity with the standalone site is the constraint; never
470489
rewrite internal links away from live URLs.
471-
- **Docs search.** sg-doc ships a lunr search fed by an `index.json` output on the
472-
docs *home* page. The merged site's home belongs to the main web; the docs
473-
section's JSON output needs rewiring (outputs config on the `doc` section).
490+
- ~~**Docs search.**~~ — closed in step 22: the index is now a
491+
`SearchIndex` output of the doc section root
492+
(`/doc/latest/search-index.json`).
474493
- ~~**`__trash.md`** page in docs content renders as a page~~ — closed in
475494
step 16: `web/scripts/import-docs.py` excludes it on import.
476495
- **Absolute links inside content.** Root-relative content links are handled

‎web/config.toml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ summaryLength = 30
1111
# collide with the generated _index.md)
1212
ignoreFiles = ['_index\.template\.md$']
1313

14+
# lunr search index of the docs section (fetched by js/search.js), emitted at
15+
# /doc/latest/search-index.json by the doc root's outputs front matter
16+
[outputFormats.SearchIndex]
17+
mediaType = "application/json"
18+
baseName = "search-index"
19+
1420
[taxonomies]
1521
planet = "planet"
1622
tag = "tags"

‎web/content/en/doc/_index.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
title: "StackGres Docs"
33
url: /doc/latest/
44
aliases: ["/doc/"]
5+
outputs: ["html", "searchindex"]
56
cascade:
67
type: "doc"
78
---
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[{{ $pages := where .Site.Pages "Type" "doc" }}
2+
{{- range $index, $page := $pages }}
3+
{{- if and $index (gt $index 0) -}},{{- end }}
4+
{
5+
"uri": "{{ $page.Permalink }}",
6+
"title": "{{ htmlEscape $page.Title}}",
7+
"tags": [{{ range $tindex, $tag := $page.Params.tags }}{{ if $tindex }}, {{ end }}"{{ $tag| htmlEscape }}"{{ end }}],
8+
"description": "{{ htmlEscape (chomp .Description) }}",
9+
"content": {{$page.Plain | jsonify}}
10+
}
11+
{{- end -}}]

‎web/layouts/partials/search.html‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,8 @@
2424
<script type="text/javascript" src="{{"js/lunr.min.js" | relURL}}{{ if $assetBusting }}?{{ now.Unix }}{{ end }}"></script>
2525
<script type="text/javascript" src="{{"js/auto-complete.js" | relURL}}{{ if $assetBusting }}?{{ now.Unix }}{{ end }}"></script>
2626
<script type="text/javascript">
27-
{{ if hugo.IsMultilingual }}
28-
var baseurl = "{{.Site.BaseURL}}{{.Site.LanguagePrefix}}";
29-
{{ else }}
30-
var baseurl = "{{.Site.BaseURL}}";
31-
{{ end }}
27+
{{/* the search index (index.json) is an output of the doc section root,
28+
not of the site home */}}
29+
var baseurl = "{{ .FirstSection.Permalink }}";
3230
</script>
3331
<script type="text/javascript" src="{{"js/search.js" | relURL}}{{ if $assetBusting }}?{{ now.Unix }}{{ end }}"></script>

‎web/scripts/import-docs.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ def rewrite_markdown(text: str, is_root_index: bool) -> tuple[str, int]:
4040
if is_root_index:
4141
if "url" not in fields:
4242
fields += 'url: /doc/latest/\naliases: ["/doc/"]\n'
43+
if "outputs" not in fields:
44+
fields += 'outputs: ["html", "searchindex"]\n'
4345
if "cascade" not in fields:
4446
fields += 'cascade:\n type: "doc"\n'
4547
return opening + fields + closing + text[match.end():], count

‎web/static/js/search.js‎

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
var lunrIndex, pagesIndex;
2+
3+
function endsWith(str, suffix) {
4+
return str.indexOf(suffix, str.length - suffix.length) !== -1;
5+
}
6+
7+
// Initialize lunrjs using our generated index file
8+
function initLunr() {
9+
if (!endsWith(baseurl,"/")){
10+
baseurl = baseurl+'/'
11+
};
12+
13+
// First retrieve the index file
14+
$.ajax({
15+
url: baseurl +"search-index.json",
16+
}).done(function(index) {
17+
pagesIndex = index;
18+
19+
// Set up lunrjs by declaring the fields we use
20+
// Also provide their boost level for the ranking
21+
lunrIndex = lunr(function() {
22+
this.ref("uri");
23+
this.field('title', {
24+
boost: 15
25+
});
26+
this.field('tags', {
27+
boost: 10
28+
});
29+
this.field("content", {
30+
boost: 5
31+
});
32+
33+
this.pipeline.remove(lunr.stemmer);
34+
this.searchPipeline.remove(lunr.stemmer);
35+
36+
// Feed lunr with each file and let lunr actually index them
37+
pagesIndex.forEach(function(page) {
38+
this.add(page);
39+
40+
// Set version selector URL
41+
if(page.uri == window.location.href) {
42+
43+
let currentVersion = baseurl.includes('latest') ? 'latest' : $('#sgVersion option:selected').text().replace(' (development)','');
44+
$('#sgVersion option:not(:selected)').each(function(index, alt) {
45+
46+
let altVersion = alt.text.replace(' (development)','');
47+
48+
if(baseurl.includes('localhost')) { // If testing locally
49+
var altVersionIndex = baseurl+'search-index-'+altVersion+'.json';
50+
} else { // If on Live site
51+
var altVersionIndex = baseurl.replace(currentVersion, altVersion) + 'search-index.json';
52+
}
53+
54+
$.ajax({
55+
url: altVersionIndex,
56+
dataType: 'text'
57+
}).done(function(altVersionIndex){
58+
altVersionIndex = JSON.parse(altVersionIndex.replace(/[\n\r\t]/g,""))
59+
let vPage = altVersionIndex.find(p => (p.title == page.title))
60+
61+
if(vPage !== undefined) {
62+
$(alt).val(vPage.uri)
63+
} else {
64+
$(alt).val( baseurl.replace(currentVersion, altVersion) + '?not-found=1');
65+
}
66+
})
67+
});
68+
}
69+
70+
}, this);
71+
})
72+
}).fail(function(jqxhr, textStatus, error) {
73+
var err = textStatus + ", " + error;
74+
console.error("Error getting Hugo index file:", err);
75+
});
76+
}
77+
78+
/**
79+
* Trigger a search in lunr and transform the result
80+
*
81+
* @param {String} query
82+
* @return {Array} results
83+
*/
84+
function search(queryTerm) {
85+
// Find the item in our index corresponding to the lunr one to have more info
86+
return lunrIndex.search(queryTerm+"^100"+" "+queryTerm+"*^10"+" "+"*"+queryTerm+"^10"+" "+queryTerm+"~2^1").map(function(result) {
87+
return pagesIndex.filter(function(page) {
88+
return page.uri === result.ref;
89+
})[0];
90+
});
91+
}
92+
93+
// Let's get started
94+
initLunr();
95+
$( document ).ready(function() {
96+
var searchList = new autoComplete({
97+
/* selector for the search box element */
98+
selector: $("#search-by").get(0),
99+
/* source is the callback to perform the search */
100+
source: function(term, response) {
101+
response(search(term));
102+
},
103+
/* renderItem displays individual search results */
104+
renderItem: function(item, term) {
105+
var numContextWords = 2;
106+
var text = item.content.match(
107+
"(?:\\s?(?:[\\w]+)\\s?){0,"+numContextWords+"}" +
108+
term+"(?:\\s?(?:[\\w]+)\\s?){0,"+numContextWords+"}");
109+
item.context = text;
110+
return '<div class="autocomplete-suggestion" ' +
111+
'data-term="' + term + '" ' +
112+
'data-title="' + item.title + '" ' +
113+
'data-uri="'+ item.uri + '" ' +
114+
'data-context="' + item.context + '">' +
115+
'» ' + item.title +
116+
'<div class="context">' +
117+
(item.context || '') +'</div>' +
118+
'</div>';
119+
},
120+
/* onSelect callback fires when a search suggestion is chosen */
121+
onSelect: function(e, term, item) {
122+
location.href = item.getAttribute('data-uri');
123+
}
124+
});
125+
});

0 commit comments

Comments
 (0)