Create an account at Algolia. There is a free Community plan which allow 10K Records/100K Operations
. Only requirement is to display their logo.
Setup Hugo to generate json for Algolia
Algolia
accepts JSON, CSV and TSV format as data source for records. We shall use Hugo
's Custom Output Formats to generate a JSON file for Algolia.
Edit config.toml
to add the following.
[outputFormats.Algolia]
baseName = "algolia"
isPlainText = true
mediaType = "application/json"
notAlternative = true
[params.algolia]
vars = ["title", "description", "summary", "date", "lastmod", "permalink"]
params = ["categories", "tags"]
[outputs]
home = ["HTML", "RSS", "Algolia"]
Create the json template/layout file for algolia at layouts/_default/list.algolia.json
.
NOTE: The following code is based on tutorial written by Chris Macrae/Forestry
{{/* Generates a valid Algolia search index */}}
{{- $hits := slice -}}
{{- $section := $.Site.GetPage "section" .Section }}
{{- $validVars := $.Param "algolia.vars" | default slice -}}
{{- $validParams := $.Param "algolia.params" | default slice -}}
{{/* Include What Pages? */}}
{{/* range $i, $hit := .Site.AllPages */}}
{{- range $i, $hit := where (where .Site.Pages "Type" "in" (slice "tutorials" "blog")) "IsPage" true -}}
{{- $dot := . -}}
{{- if or (and ($hit.IsDescendant $section) (and (not $hit.Draft) (not $hit.Params.private))) $section.IsHome -}}
{{/* Set the hit's objectID */}}
{{- .Scratch.SetInMap $hit.File.Path "objectID" $hit.UniqueID -}}
{{/* Store built-in page variables in iterable object */}}
{{- .Scratch.SetInMap "temp" "content" $hit.Plain -}}
{{- .Scratch.SetInMap "temp" "date" $hit.Date.UTC.Unix -}}
{{- .Scratch.SetInMap "temp" "description" $hit.Description -}}
{{- .Scratch.SetInMap "temp" "dir" $hit.Dir -}}
{{- .Scratch.SetInMap "temp" "path" "temp" -}}
{{- .Scratch.SetInMap "temp" "expirydate" $hit.ExpiryDate.UTC.Unix -}}
{{- .Scratch.SetInMap "temp" "path" "temp" -}}
{{- .Scratch.SetInMap "temp" "fuzzywordcount" $hit.FuzzyWordCount -}}
{{- .Scratch.SetInMap "temp" "keywords" $hit.Keywords -}}
{{- .Scratch.SetInMap "temp" "kind" $hit.Kind -}}
{{- .Scratch.SetInMap "temp" "lang" $hit.Lang -}}
{{- .Scratch.SetInMap "temp" "lastmod" $hit.Lastmod.UTC.Unix -}}
{{- .Scratch.SetInMap "temp" "permalink" $hit.Permalink -}}
{{- .Scratch.SetInMap "temp" "publishdate" $hit.PublishDate -}}
{{- .Scratch.SetInMap "temp" "readingtime" $hit.ReadingTime -}}
{{- .Scratch.SetInMap "temp" "relpermalink" $hit.RelPermalink -}}
{{- .Scratch.SetInMap "temp" "summary" $hit.Summary -}}
{{- .Scratch.SetInMap "temp" "title" $hit.Title -}}
{{- .Scratch.SetInMap "temp" "type" $hit.Type -}}
{{- .Scratch.SetInMap "temp" "url" $hit.URL -}}
{{- .Scratch.SetInMap "temp" "weight" $hit.Weight -}}
{{- .Scratch.SetInMap "temp" "wordcount" $hit.WordCount -}}
{{- .Scratch.SetInMap "temp" "section" $hit.Section -}}
{{/* Include valid page vars */}}
{{- range $key, $param := (.Scratch.Get "temp") -}}
{{- if in $validVars $key -}}
{{- $dot.Scratch.SetInMap $hit.File.Path $key $param -}}
{{- end -}}
{{- end -}}
{{/* Include valid page params */}}
{{- range $key, $param := $hit.Params -}}
{{- if in $validParams $key -}}
{{- $dot.Scratch.SetInMap $hit.File.Path $key $param -}}
{{- end -}}
{{- end -}}
{{- $.Scratch.SetInMap "hits" $hit.File.Path (.Scratch.Get $hit.File.Path) -}}
{{- end -}}
{{- end -}}
{{- jsonify ($.Scratch.GetSortedMapValues "hits") -}}
NOTE: observe the Include What Pages?
section. The original code use .Site.AllPages
which include everything including tag pages and others. I wanted content pages only (IsPage) and specific content type, thus I use where (where .Site.Pages "Type" "in" (slice "tutorials" "blog")) "IsPage" true
to query for pages.
Run hugo
to generate the site and it shall output the Algolia JSON file at public/algolia.json
.
Create Algolia App and Index
Goto Algolia Applications and create new application
. Put in the name
, select community
for the free plan and choose your region (where your website's visitors most likely came from).
After creating the application and being redirected to Dashboard
, click on Indices
on left tab. Click on new index
and give it a name (e.g. default
, posts
, etc.)
You can use tool like atomic-algolia to upload algolia.json
to Algolia
by cli. But since I don't update my website very frequenly, I shall skip this step at the moment and upload algolia.json
to Algolia
using the web interface.
Choose Upload File
and upload algolia.json
.
NOTE: if you already uploaded your records once, you can access the add records
function by clicking on Manage Current Index
.
You can perform test search on your records at the Browse
tab.
At Indices -> Ranking -> Basic Settings -> Attributes
, add title
and description
to Searchable Attributes
and click Save
.
NOTE: I only wanted to allow searching by title
and description
only, while other fields are for display purpose only. The order of the fields/attributes shall affect ranking order, so make sure title
is above description
.
Embed Algolia Search in Hugo using InstantSearch.js
There are many libraries available to integrate with Algolia, including Vue
, React
, Android
, etc. For integration using plain javascript without any framework, InstantSearch.js would be the best choice.
The following section shall focus on build search and show result using full page like this example, but without the category filter on the left at the moment.
NOTE: if you want a search box at the top of the page which show result in a dropdown as you type, refer to autocomplete.js.
Create a static search
page.
hugo new search.md
Edit the page’s configuration to give it a static
type and search
layout.
---
title: "Search"
date: 2017-07-29T19:57:50+08:00
type: static
layout: search
---
Create a layout template for search
at layouts/static/search.html
with the following content.
Refer to InstantSearch.js Getting Started for most of the setup
- Get
appId
andsearchKey
fromAlgolia -> Apps -> API Keys
(Application ID
andSearch-Only API Key
) - Get
indexName
from fromAlgolia -> Apps -> Indices
- Include CSS styles:
instantsearch.min.css
andinstantsearch-theme-algolia.min.css
- Include JavaScript:
[email protected]
- JavaScript to initialize
instantsearch
- Add html
<div id="hits"><!-- Hits widget will appear here --></div>
andaddWidget
hits. - Add html
<div id="search-box"><!-- SearchBox widget will appear here --></div>
andaddWidget
searchBox - Include algolia logo (
https://www.algolia.com/static_assets/images/pricing/pricing_new/algolia-powered-by-14773f38.svg
https://www.algolia.com/static/logo-algolia-nebula-blue-full-57c56ea4b99b30c8f2cc03b65e8bb849.png
) as part of the requirement for community free plan. Refer to Embed Algolia Logo.
NOTE: the block/define might differ depending on your baseof.html
. main
for content, addstyles
for css styles and addscripts
for javascripts.
{{ define "main" }}
<div id="search-box"><!-- SearchBox widget will appear here --></div>
<!-- include algolia logo -->
<!-- <img src="https://www.algolia.com/static_assets/images/pricing/pricing_new/algolia-powered-by-14773f38.svg"></img> -->
<a href="https://www.algolia.com/" target="_blank"><img src="https://www.algolia.com/static/logo-algolia-nebula-blue-full-57c56ea4b99b30c8f2cc03b65e8bb849.png"></img></a>
<div id="hits" class="my-4">
<!-- Hits widget will appear here -->
</div>
<div id="pagination">
<!-- Pagination widget will appear here -->
</div>
{{ end }}
{{ define "addstyles" }}
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/instantsearch.min.css">
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/instantsearch-theme-algolia.min.css">
<style>
mark {
padding: 0;
}
</style>
{{ end }}
{{ define "addscripts" }}
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
<script>
// initialize instantsearch
const search = instantsearch({
appId: '***',
apiKey: '***',
indexName: '???',
urlSync: true
});
search.addWidget(
instantsearch.widgets.searchBox({
container: '#search-box',
placeholder: 'Search'
})
);
search.addWidget(
instantsearch.widgets.hits({
container: '#hits',
templates: {
empty: 'No results',
item: '<div><h3>{{ safeHTML "{{{ _highlightResult.title.value }}}" }}</h3></div>'
}
})
);
search.start();
</script>
{{ end }}
Run hugo server
and you should be able to access your search page at http://localhost:1313/search
, with a search bar, algolia logo and results/hits displayed below it. Search happens as you type.
Did you notice {{ safeHTML "{{{ _highlightResult.title.value }}}" }}
with way too many curly brackets? This is due to conflict between go template (used by Hugo
) and mustache template (used by InstantSearch.js
. To avoid the conflict, it is recommended to put your JavaScript in a .js
file.
Edit the "addscripts"
block to the following.
{{ define "addscripts" }}
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
<script src="/js/search.js"></script>
{{ end }}
Create static/js/search.js
with the following content.
- In
.js
file, we can use{{{ _highlightResult.title.value }}}
instead of{{ safeHTML "{{{ _highlightResult.title.value }}}" }}
in hugo template file. hits.templates.item
is changed to includetitle
,permalink
,lastmod
,tags
anddescription
(I am usingBootstrap 4
CSS classes). The difference between{{{ _highlightResult.title.value }}}
and{{ title }}
is the former will highlight the text being queried (e.g. when I search "Android", the word shall be highlighted in the results). I believe we need to include highligtable fields inAlgolia -> Apps -> Indices -> Ranking -> Basic Settings -> Attributes -> Searchable Attributes
as we did earlier.- By default the highlighting of search result with query is done by using
em
html tag (italic). If you want to change it to yellow highlight, gotoAlgolia -> Apps -> Indices -> Display -> Display & Pagination
, changeHighlight prefix tag
to<mark>
andHighlight postfix tag
to</mark>
. - I added some
hits.transformData.item
to change date in seconds to date string and format tags array into string. - Add html
<div id="pagination"><!-- Pagination widget will appear here --></div>
andaddWidget
pagination
const search = instantsearch({ appId: '***', apiKey: '***', indexName: '???', urlSync: true});search.addWidget( instantsearch.widgets.hits({ container: '#hits', templates: { empty: 'No results', // https://caniuse.com/#feat=template-literals item: '<div class="my-3"><h3><a href="{{ permalink }}">{{{ _highlightResult.title.value }}}</a></h3><div><span class="text-secondary">{{ lastmod_date }}</span> <span class="text-secondary">∙ {{ tags_text }}</span> {{#_highlightResult.description.value}}∙ {{ _highlightResult.description.value }}{{/_highlightResult.description.value}}</div><small class="text-muted">{{ summary }}</small></div>' }, transformData: { item: function(data) { data.lastmod_date = new Date(data.lastmod*1000).toISOString().slice(0,10) // https://caniuse.com/#search=MAP const tags = data.tags.map(function(value) { return value.toLowerCase().replace(' ', '-') }) data.tags_text = tags.join(', ') return data } } }));search.addWidget( instantsearch.widgets.searchBox({ container: '#search-box', placeholder: 'Search' }));search.addWidget( instantsearch.widgets.pagination({ container: '#pagination', maxPages: 20, // default is to scroll to 'body', here we disable this behavior // scrollTo: false }));search.start();
The search page is complete now.
Now, we need to add a search box at the top of every page which allow searching at /search
to be done.
Edit layouts/partials/*.html
which is related to search bar or page top navbar (refer to your baseof.html
as every Hugo
layouts are different).
Add a search form with text input.
<form action="/search/" role="search"> <input type="text" placeholder="Search" type="text" name="q"></form>
The following is my search form with Bootstrap 4
CSS.
<form class="form-inline my-2 my-lg-0" action="/search/" role="search" id="lua-search"> <input class="form-control mr-sm-2" type="text" placeholder="Search" aria-label="Search" type="text" name="q"></form>
Whenever the user type in a search query and hit ENTER, it shall redirect to /search?q=android
with the query string and perform the search.
More
You can add tags filtering (fauceting) to your search result. Refer to Algolia Add Refiniting List (Faceting).
References: