Hugo Search with Algolia and GitLab CI
Roadster’s default sidebar search sends queries to Google (or DuckDuckGo) with a site filter. That works until the corpus is large and you want typo tolerance, facets by section/tags, and results that stay on your domain. Algolia hosts the index; Hugo emits a JSON dump at build time; atomic-algolia pushes only the diff from GitLab CI after hugo succeeds.

This pairs with the GitLab CI to GitHub Pages flow: build once, deploy public/, then update the search index with CI secrets—never with keys in git.
Architecture
content/*.md
|
v
hugo --> public/algolia.json --> atomic-algolia --> Algolia index
| ^
+--> public/*.html (Pages deploy) |
|
ALGOLIA_* CI variables
Browser InstantSearch (or Autocomplete) talks to Algolia with the search-only API key. The admin key stays in GitLab CI/CD variables and is used only by atomic-algolia.
1. Create the Algolia app and index
- Create an application in the Algolia dashboard.
- Create an empty index (for example
devopstales). - Copy Application ID, Admin API key (write), and Search-only API key (public).
Never commit the admin key. The search-only key is safe to expose in front-end JS.
2. Emit algolia.json from Hugo
Add an output format and attach it to the home page:
[outputFormats.Algolia]
baseName = "algolia"
isPlainText = true
mediaType = "application/json"
notAlternative = true
[outputs]
home = ["HTML", "RSS", "ATOM", "llms", "WebManifest", "Algolia"]
Create layouts/index.algolia.json (name follows the format). Index only real sections—skip filedir partials the same way Atom does:
{{- $pages := where .Site.RegularPages "Type" "in" .Site.Params.mainSections -}}
{{- $pages = where $pages ".Params.disable_feed" "!=" true -}}
{{- $index := slice -}}
{{- range $pages -}}
{{- $index = $index | append (dict
"objectID" .File.UniqueID
"title" .Title
"summary" (.Summary | plainify)
"content" (.Plain | truncate 5000)
"permalink" .Permalink
"section" .Section
"date" (.Date.Format "2006-01-02")
"tags" (.Params.tags | default slice)
) -}}
{{- end -}}
{{- $index | jsonify -}}
objectID is required for atomic updates. Prefer a stable id (file unique id or permalink) so renames/updates merge correctly.
Build and inspect:
hugo
head -c 400 public/algolia.json; echo
3. atomic-algolia locally
npm init -y
npm install atomic-algolia --save-dev
package.json:
{
"scripts": {
"algolia": "atomic-algolia"
}
}
Local env (do not commit):
export ALGOLIA_APP_ID="YOUR_APP_ID"
export ALGOLIA_ADMIN_KEY="YOUR_ADMIN_KEY"
export ALGOLIA_INDEX_NAME="devopstales"
export ALGOLIA_INDEX_FILE="public/algolia.json"
npm run algolia
You should see add/update/remove counts in the CLI, then records in the Algolia dashboard. Details on the incremental upload model are in the atomic-algolia package docs and in writeups such as tisgoud’s GitLab CI example.
4. Wire GitLab CI
Extend the existing Hugo job (same image family that already builds the site). After hugo and before or after the Pages push:
hugo-build:
image: hugomods/hugo:git-0.148.0
variables:
GIT_SUBMODULE_STRATEGY: recursive
before_script:
- apk add --no-cache npm
- npm ci || npm install
script:
- hugo
- touch public/.nojekyll
# deploy public/ to GitHub Pages (existing steps)...
- npm run algolia
only:
- master
Set masked/protected CI/CD variables in GitLab:
| Variable | Purpose |
|---|---|
ALGOLIA_APP_ID |
Application id |
ALGOLIA_ADMIN_KEY |
Admin (write) key |
ALGOLIA_INDEX_NAME |
Index name |
ALGOLIA_INDEX_FILE |
public/algolia.json |
Run npm run algolia only on the branch that publishes production, so preview pipelines do not overwrite the live index. If you use merge-request builds, gate the step:
- if [ "$CI_COMMIT_BRANCH" = "master" ]; then npm run algolia; fi
Commit package.json and package-lock.json so npm ci is reproducible. Prefer that over npm init inside the job on every run.
5. Front-end InstantSearch (replace the sidebar form)
Keep search-only credentials in site params (public):
[Params.algolia]
appId = "YOUR_APP_ID"
indexName = "devopstales"
searchKey = "YOUR_SEARCH_ONLY_KEY"
Add a small partial (for example layouts/partials/widgets/algolia-search.html) that loads InstantSearch from a CDN and mounts on a container, then swap "search" in Params.sidebar.widgets for your Algolia widget—or override layouts/partials/widgets/search.html.
Minimal sketch:
<div id="algolia-search"></div>
<script src="https://cdn.jsdelivr.net/npm/algoliasearch@4/dist/algoliasearch-lite.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/instantsearch.js@4/dist/instantsearch.production.min.js"></script>
<script>
const { appId, indexName, searchKey } = {{ .Site.Params.algolia | jsonify }};
const search = instantsearch({
indexName,
searchClient: algoliasearch(appId, searchKey),
});
search.addWidgets([
instantsearch.widgets.searchBox({ container: '#algolia-search' }),
instantsearch.widgets.hits({
container: '#algolia-hits',
templates: {
item: (hit) => `<a href="${hit.permalink}"><strong>${hit.title}</strong><br>${hit.summary || ''}</a>`,
},
}),
]);
search.start();
</script>
<div id="algolia-hits"></div>
Tune CSS to match Roadster’s sidebar. Facet on section or tags when you want filters for kubernetes vs AI vs Hugo.
Official UI building blocks are documented under Algolia InstantSearch.
6. Operational tips
- Quota: atomic updates send diffs; full reindex only when you change the record shape.
- Content size: truncate
.Plainso records stay under Algolia record limits. - Drafts: production
hugoalready omits drafts; keep it that way in CI. - Sections: filter with
mainSectionssofiledirHTML includes never become hits. - Secrets: rotate the admin key if it ever appeared in a job log or commit.
- Fallback: leave the old Google site-search widget behind a feature flag until InstantSearch is verified.
Verify
hugo
npm run algolia
curl -s "https://latency-dsn.algolia.net/1/indexes/devopstales?query=kubernetes&x-algolia-application-id=$ALGOLIA_APP_ID&x-algolia-api-key=$ALGOLIA_SEARCH_KEY" | head
On the live site: open the sidebar, type a known title fragment, confirm permalinks resolve under https://devopstales.github.io/.
Summary
Generate public/algolia.json with a Hugo home output format, push diffs with atomic-algolia from GitLab CI using masked admin credentials, and query with InstantSearch using the search-only key in the theme. You keep the existing GitLab → Pages deploy path and add hosted search without turning Hugo into an application server.