Jakub Pomykała
Posted on April 18, 2022
Since 2019 I’m building a SaaS for translation management as a solo developer. Since the very beginning, I needed a place where I could post code samples, and explain people how to integrate different frameworks or how to use SimpleLocalize CLI to upload and download translation files. You can see Algolia search in action on SimpleLocalize documentation.
At beginning, I started using GitBook software because it was shining and trendy at the time. It was a good move, because when you start something new you don’t want to start from scratch on every level. The main goal is to deliver good quality code and value to the customers, all other things you can (or should) buy to save time.
Short story long, I wasn’t quite happy with the GitBook after couple of months, because it didn’t fit to my needs, I didn’t like the style, and I was spending too much time on adjusting and fixing links and so on. I decided to go with my custom documentation, and surprisingly everything went smoothly! The only concert was lack o good search ability which is available in all documentation solutions, or open-source Docusaurus.
Installation
Algolia search components and Algolia client installation
npm install react-instantsearch-dom algoliasearch --save
gray-matter a Markdown parser and globby for finding files
npm install gray-matter globby --saveDev
Configuration
import algoliasearch from 'algoliasearch/lite';
import {connectStateResults, Hits, InstantSearch, SearchBox} from 'react-instantsearch-dom';
const searchClient = algoliasearch(
'APP_ID',
'SEARCH_API_KEY'
);
Results component
By default, Algolia <Hits/>
component shows all results when search query is empty. I adjusted the code to show results only if there are any results and data are loaded.
const Results = connectStateResults(({searchState, searchResults, searching}) => {
const hasQuery = searchState && searchState.query;
const hasResults = (searchResults?.hits ?? []).length > 0;
const isSearching = searching;
if (hasQuery && hasResults) {
return <Hits hitComponent={Hit}/>;
}
if (hasQuery && !hasResults && !isSearching) {
return <div>No results 😔</div>
}
return null;
}
);
Hit component
Hit component is nothing else as just a one search result. I stripped my styling to make it easier to copy-paste. 😄
import React from "react";
function Hit(props: any) {
const content = props?.hit?.content ?? "";
const words = content.split(" ").length;
return (<a href={props.hit.slug}>
<div>
<h3>{props?.hit?.frontmatter?.title ?? "no title"}</h3>
</div>
<p>{props?.hit?.frontmatter?.excerpt ?? ""}</p>
</a>)
}
export default Hit;
Search component
Use InstantSearch
component and search client which you configured in the previous step, put your index name, and use Algolia SearchBox
component and our custom Results
component with changed empty state behaviour which we created previously.
<InstantSearch indexName="simplelocalize-docs" searchClient={searchClient}>
<SearchBox/>
<Results/>
</InstantSearch>
I didn’t want to make it more complex that it needs to be, I overwrite some CSS properties in my stylesheets to hide a search button and reset button. I also added Bootstrap styling to the search box using SCSS @extend
property. Simple and it does the job.
.ais-SearchBox-input {
@extend .form-control;
}
.ais-SearchBox-submit {
display: none;
}
.ais-SearchBox-reset {
display: none;
}
Get markdown files to index
Now, it’s time to get all files which should be indexed and visible in the search results. I created a new index-docs.js
file which I will be executed after every successful build on CI/CD server. All my documentation pages are in /docs/
directory. So I needed to write a script for converting it into an array of objects to populate Algolia search index.
import {globby} from 'globby';
import fs from "fs";
import matter from "gray-matter";
import algoliasearch from 'algoliasearch';
const pages = await globby([
'docs/',
]);
const objects = pages.map(page => {
const fileContents = fs.readFileSync(page, 'utf8')
const {data, content} = matter(fileContents)
const path = page.replace('.md', '');
let slug = path === 'docs/index' ? 'docs' : path;
slug = "/" + slug + "/"
return {
slug,
content,
frontmatter: {
...data
}
}
})
//algolia update index code
- I’m getting all markdown files from
/docs/
directory withglobby
, - reading content of the files with
fs
, - parsing files with
gray-matter
for markdown, - and doing some magic tricks to convert file path to slugs which are used.
In my case, slugs are the just file paths, where the file path looks like this /docs/{category}/{title}.md
, for example:/docs/integrations/next-translate.md
, so slug will look like this: /docs/integrations/next-translate/
In the end I’m getting an array of objects which looks like this:
[
{
"slug": "/docs/integrations/next-translate/",
"content": "My article content for indexing",
"frontmatter": {
"category": "Integrations",
"date": "2022-01-20",
"some-other-properties": "properties"
}
}
]
Update Algolia index
Algolia provides a really simple and easy to use JavaScript client, so there no magic here. We are acquiring our index, and saving objects which we created in previous step.
const client = algoliasearch(
'APP_ID',
'ADMIN_API_KEY'
);
const index = client.initIndex("simplelocalize-docs")
index.saveObjects(objects, {
autoGenerateObjectIDIfNotExist: true
});
Include index update in package.json
{
"name": "simplelocalize-docs",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build && next export -o build/ && i18n:download && index:docs",
"index:docs": "npx ts-node --skip-project index-docs.mjs",
"i18n:upload": "simplelocalize upload",
"i18n:download": "simplelocalize download"
}
}
You can execute the script manually by running npm run index:docs
.
That’s it? That’s it! See the search in action on SimpleLocalize documentation page.
Posted on April 18, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.