Custom integration of Algolia search in Ghost CMS

Integration of Algolia search into Ghost CMS using InstantSearch.js and Tailwind CSS. Learn about setting up API keys, fetching Ghost data with NodeJS, and configuring the UI search box. This guide provides a foundational framework for customization, including widget setup and Tailwind CSS styling.

16 min read
Image shows search box design of Algolia search from Tailwind websi
Algolia search on official Tailwind website

This post is aimed at professional Ghost CMS developers seeking a more customized search integration. Ghost blogging platform is an incredible content management system known for its user-friendly interface and impressive feature set. It supports its own custom search solution developed by Sodo, which works well for many users. However, there are a few limitations. Sodo's search functionality can only search through posts, authors, and tags. Another restriction is that it only searches within titles, lacking the capability to delve deep into the content. Additionally, styling Sodo's search can be quite challenging as it is an integral part of the Ghost CMS.


Fortunately, for website owners in search of a more flexible and robust solution, there's Algolia—a powerful search engine. Algolia operates on a SaaS (Software as a Service) model and offers an exceptionally versatile and AI-powered search solution. The best part is that Algolia provides a generous free tier, making it accessible for developers to enhance search functionality on any platform.


In this post, we will walk through the implementation of basic search functionalities using Algolia. However, please note that the provided solution in this article is not a complete copy-paste solution. It lacks a responsive implementation and the fetching script could have more functionalities. We won't showcase the regular execution of the script, among other details, to keep the article concise and focused on key concepts.


We'll also aim to replicate the UI style of Algolia search as seen on the official Tailwind CSS website. Speaking of Tailwind, we'll be using it extensively for styling. We hope that this guide will be useful for developers, even those accustomed to working with vanilla CSS.

Image shows input of native Ghost CMS search
Ghost native Sodo search

What do we need to do:

  1. Create an Algolia account and obtain our API keys.
  2. Retrieve data from our Ghost instance using the Ghost Content API and create/update the search index.
  3. Integrate a basic Algolia UI search box on our website.

1. Create an Algolia account and obtain Api keys

First, you need to create an account at Algolia by visiting https://dashboard.algolia.com/users/sign_up

Then, visit your dashboard and navigate to the 'API Key' tab. Click on 'API Keys' to acquire both the Application ID and Admin API key. These keys will be utilized in our script to fetch and transform data into the correct format for updating or creating our Algolia index.

What exactly is an Algolia index?

An Algolia index is a collection of records stored in a highly optimized manner for efficient search and discovery operations. These indices are akin to database tables but are optimized specifically for search purposes. Algolia indices can encompass various types of data, including product information, customer data, or blog posts. Algolia also supports a range of data types such as text, numbers, dates, and geolocations. Once you've established an Algolia index, you can add records to it using either the Algolia API or the Algolia dashboard. After adding records to your index, you can initiate searches through the Algolia search API.

In Algolia dashboard, click on API Keys.
Grab the necessary keys from Algolia

2. Create NodeJS script to fetch Ghost data

Next, we'll need to create a simple NodeJS script to fetch data from our Ghost instance. Algolia supports a wide variety of languages, including PHP, Python, Java, and many others. For this demonstration, we'll be using NodeJS.

This script should run regularly using a custom cronjob on server or on a "server-less" environment like Netlify or Vercel. While I initially aimed to run it on Cloudflare workers, I found out that the Algolia library is not supported, although I haven't personally attempted it.

It's important to note that this script is not a perfect solution but rather a starting point. We can enhance it by incorporating mechanisms to notify us of potential errors. We might add only newest data into index, instead of rewriting all records on each execution. So let's start:

ok create new folder for our project type npm init and setup our project.

Now install Algolia npm library: npm install algoliasearch in to our node_modules folder.

Create this script in your preferred editor:

const algoliasearch = require('algoliasearch')

async function uploadIndex(records){
    const APP_ID = "XXXXXXXXXX"; // aplication ID
    const ADMIN_KEY = "XXXXXXXXXXXXXXXXXXXXXXXXXX"; //admin API KEY
    const INDEX_NAME = "noise_ghost"; // If not exist, index will be created

    const client = algoliasearch(APP_ID, ADMIN_KEY);
    const index = client.initIndex(INDEX_NAME);
    const record = records;

    index.saveObjects(record).then(data=>{
        console.log('[+] New index created/updated: ', data);
    }).catch(err=>{
        //handle errors during index upload
        console.log('[X] err ', err);
    })
}


class GhostData {

    constructor(ghostUrl = null, contentKey = null) {
      this.ghostUrl = ghostUrl;
      this.contentKey = contentKey;
      this.dataForIndex = [];
      if(this.ghostUrl == null || this.contentKey == null) throw new Error('Missing url or key!');
    }

    async getPages(){
        let res = await fetch(`${this.ghostUrl}/ghost/api/content/pages?key=${this.contentKey}&include=tags`);
        if (res.status != 200) throw new Error('Api connection failed...');
        let result = await res.json();

        for(let item of result.pages){
            let description = null;
            let tags = item.tags.map( el => el.name );
            let tag_array = tags.length > 0 ? tags : null;  
            
            if(item.custom_excerpt){
                description = item.custom_excerpt;
            }else if(item.excerpt){
                description = item.excerpt;
            }else{
                description = item.meta_description;
            }
            this.dataForIndex.push(
                {   objectID:item.id, 
                    title:item.title, 
                    url:item.url, 
                    description: description,
                    tags: tag_array,
                    type: 'page',
                }
            )
        }
    }


    async getPosts(){
        let res = await fetch(`${this.ghostUrl}/ghost/api/content/posts?key=${this.contentKey}&include=tags`);
        if (res.status != 200) throw new Error('Api connection failed...');
        let result = await res.json();

        for(let item of result.posts){
            let description = null;
            let tags = item.tags.map( el => el.name );
            let tag_array = tags.length > 0 ? tags : null;  
      
            if(item.custom_excerpt){
                description = item.custom_excerpt;
            }else if(item.excerpt){
                description = item.excerpt;
            }else{
                description = item.meta_description;
            }
            this.dataForIndex.push(
                {   objectID:item.id, 
                    title:item.title, 
                    url:item.url, 
                    description: description,
                    tags: tag_array,
                    type: 'post',
                }
            )
        }
    }

}

async function main(){

    const GHOST_URL = 'http://127.0.0.1:2368'; //Ghost instance url
    const GHOST_CONTENT_API_KEY = '7d6795a4069993205fec34350e'; // Ghost content api key

    try{
        let ghost_data = new GhostData(GHOST_URL, GHOST_CONTENT_API_KEY);
        await ghost_data.getPages();
        await ghost_data.getPosts();
        await uploadIndex(ghost_data.dataForIndex);

    }catch(err){
        //Handle errors
        console.log('[x] err -> ', err);
    }
}

//Start main function
main()

What this script do?

Initialization:

  • It imports the algoliasearch library to interact with Algolia.

Function for Indexing:

  • Defines an asynchronous function uploadIndex to upload data to an Algolia index.

GhostData Class:

  • Defines a class GhostData to fetch data from a Ghost CMS instance.
  • It can fetch pages and posts along with their relevant information such as title, URL, description, and tags.

Main Function:

  • Defines an asynchronous main function that:
  • Specifies the Ghost instance URL and API key.
  • Creates an instance of GhostData.
  • Fetches pages and posts from the Ghost instance.
  • Calls uploadIndex to upload the fetched data to Algolia.

In summary, this script fetches data from a Ghost CMS instance (pages and posts) and uploads it to an Algolia index, making it searchable and accessible.


A Few Notes:

  • Index Naming and Public Accessibility:
    Please be cautious about the name of your index as it is publicly accessible. Avoid storing any sensitive or important information within the index.
  • Index Data Format:
    Data for indexing is provided as an array of JSON objects. The most crucial part is the objectID, which serves as the identifier. By consistently providing the same objectID, Algolia knows which record to update. If the objectID is missing, Algolia treats the record as a new one.
  • Flexibility in Data Selection:
    The script showcases the flexibility in selecting what to fetch and include in the index. In this case, we fetch the title and description. The description is sourced from the custom_excerpt field, excerpt field, or meta_description as a fallback. While it's possible to include the entire page/post content, it requires more effort due to Algolia's limitations with HTML strings. However, we can extract data from HTML tags to provide clean strings for indexing.

3. Integrate a basic Algolia UI search box on our website

To create a user-friendly Algolia UI search box on our website, we'll be utilizing InstantSearch.js. The process is quite straightforward. We'll start by importing a few scripts from a remote CDN into our HTML code. Afterward, we'll configure Algolia search and construct the search box using Tailwind CSS.
For more details check the install guide on Algolia doc.


Import necessary scripts from CDN:
put these scripts imports to the end of your website before body close tag </body>.

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/algoliasearch-lite.umd.js" integrity="sha256-DABVk+hYj0mdUzo+7ViJC6cwLahQIejFvC+my2M/wfM=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/instantsearch.production.min.js" integrity="sha256-foJtB+Wd0wvvK+VU3KO0/H6CjwSwJfB1RnWlgx0Ov9U=" crossorigin="anonymous"></script>

Import necessary scripts from CDN

Initialize Algolia search:
In this step we need 2 API keys from Algolia dashboard, Search-Only API Key and Application ID.

Search initialization:

<script>
const APP_ID = 'XXXXXXXXX'; //Application ID
const SEARCH_API_KEY = 'XXXXXXXXXXXXXXXXXXXXXXXXXX'; //Search-only API key
const searchClient = algoliasearch(APP_ID, SEARCH_API_KEY);

const search = instantsearch({
  indexName: 'noise_ghost',
  searchClient,
  insights: true,
});	


search.addWidgets([

    instantsearch.widgets.configure({
        attributesToSnippet: ['description'],
    }),
    instantsearch.widgets.searchBox({
        container: '#searchbox',
        placeholder: 'Search for anything...',
    }),

    instantsearch.widgets.hits({
    container: '#hits',
    templates: {
          item: (hit, { html, components }) => html`
          <a href='${hit.url}'>
            <div class="hit-item group hover:bg-[#1A84C8]">
              <div class="hit-title group-hover:text-white">
                ${components.Highlight({ hit, attribute: 'title' })}
                <div class="hit-type">${hit.type}</div>
              </div>
              <div class="hit-description group-hover:text-white">
                ${components.Snippet({ hit, attribute: 'description' })}
              </div>              
            </div>
          </a>
          `,
        },
    })
]);

search.start(); //Init Algolia search

</script>

A Few Notes:

In this code, the most crucial aspects pertain to the setup of two widgets: the 'searchBox' widget and the 'hits' widget. As you can observe, both widgets will be injected into their respective HTML containers: #searchbox and #hits.

The search box will be rendered within the #searchbox HTML container, while the hits represent the search results, populating the #hits container. It's essential to ensure that these HTML containers are provided; otherwise, you may encounter errors in the browser console.

Furthermore, you'll notice that we've configured a 'snippet' component for the description field. This snippet component ensures that our lengthy description text is not displayed in its entirety within the hit results. Instead, it's truncated from both ends with the searched text appropriately highlighted.

Let's add some html markup!
We need to create modal window in fixed position, hovering above whole html content. Do not forget to mark necessary containers with id='searchbox' and id='hits'. Put this code anywhere into your <body></body>.

<div id="search_wrapper">
    <div class="agolia_modal_box">
        <header>
            <div id="searchbox" class="relative"></div>
        </header>
        <div id="hits" class="agolia_search_hits"></div>
        <footer>
        <div class="agolia_logo_wrapper">            
            <span>Search by</span>
            <svg width="77" height="19" aria-label="Algolia" role="img" id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2196.2 500"><defs><style>.cls-1,.cls-2{fill:#003dff;}.cls-2{fill-rule:evenodd;}</style></defs><path class="cls-2" d="M1070.38,275.3V5.91c0-3.63-3.24-6.39-6.82-5.83l-50.46,7.94c-2.87,.45-4.99,2.93-4.99,5.84l.17,273.22c0,12.92,0,92.7,95.97,95.49,3.33,.1,6.09-2.58,6.09-5.91v-40.78c0-2.96-2.19-5.51-5.12-5.84-34.85-4.01-34.85-47.57-34.85-54.72Z"></path><rect class="cls-1" x="1845.88" y="104.73" width="62.58" height="277.9" rx="5.9" ry="5.9"></rect><path class="cls-2" d="M1851.78,71.38h50.77c3.26,0,5.9-2.64,5.9-5.9V5.9c0-3.62-3.24-6.39-6.82-5.83l-50.77,7.95c-2.87,.45-4.99,2.92-4.99,5.83v51.62c0,3.26,2.64,5.9,5.9,5.9Z"></path><path class="cls-2" d="M1764.03,275.3V5.91c0-3.63-3.24-6.39-6.82-5.83l-50.46,7.94c-2.87,.45-4.99,2.93-4.99,5.84l.17,273.22c0,12.92,0,92.7,95.97,95.49,3.33,.1,6.09-2.58,6.09-5.91v-40.78c0-2.96-2.19-5.51-5.12-5.84-34.85-4.01-34.85-47.57-34.85-54.72Z"></path><path class="cls-2" d="M1631.95,142.72c-11.14-12.25-24.83-21.65-40.78-28.31-15.92-6.53-33.26-9.85-52.07-9.85-18.78,0-36.15,3.17-51.92,9.85-15.59,6.66-29.29,16.05-40.76,28.31-11.47,12.23-20.38,26.87-26.76,44.03-6.38,17.17-9.24,37.37-9.24,58.36,0,20.99,3.19,36.87,9.55,54.21,6.38,17.32,15.14,32.11,26.45,44.36,11.29,12.23,24.83,21.62,40.6,28.46,15.77,6.83,40.12,10.33,52.4,10.48,12.25,0,36.78-3.82,52.7-10.48,15.92-6.68,29.46-16.23,40.78-28.46,11.29-12.25,20.05-27.04,26.25-44.36,6.22-17.34,9.24-33.22,9.24-54.21,0-20.99-3.34-41.19-10.03-58.36-6.38-17.17-15.14-31.8-26.43-44.03Zm-44.43,163.75c-11.47,15.75-27.56,23.7-48.09,23.7-20.55,0-36.63-7.8-48.1-23.7-11.47-15.75-17.21-34.01-17.21-61.2,0-26.89,5.59-49.14,17.06-64.87,11.45-15.75,27.54-23.52,48.07-23.52,20.55,0,36.63,7.78,48.09,23.52,11.47,15.57,17.36,37.98,17.36,64.87,0,27.19-5.72,45.3-17.19,61.2Z"></path><path class="cls-2" d="M894.42,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-14.52,22.58-22.99,49.63-22.99,78.73,0,44.89,20.13,84.92,51.59,111.1,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47,1.23,0,2.46-.03,3.68-.09,.36-.02,.71-.05,1.07-.07,.87-.05,1.75-.11,2.62-.2,.34-.03,.68-.08,1.02-.12,.91-.1,1.82-.21,2.73-.34,.21-.03,.42-.07,.63-.1,32.89-5.07,61.56-30.82,70.9-62.81v57.83c0,3.26,2.64,5.9,5.9,5.9h50.42c3.26,0,5.9-2.64,5.9-5.9V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,206.92c-12.2,10.16-27.97,13.98-44.84,15.12-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-42.24,0-77.12-35.89-77.12-79.37,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33v142.83Z"></path><path class="cls-2" d="M2133.97,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-14.52,22.58-22.99,49.63-22.99,78.73,0,44.89,20.13,84.92,51.59,111.1,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47,1.23,0,2.46-.03,3.68-.09,.36-.02,.71-.05,1.07-.07,.87-.05,1.75-.11,2.62-.2,.34-.03,.68-.08,1.02-.12,.91-.1,1.82-.21,2.73-.34,.21-.03,.42-.07,.63-.1,32.89-5.07,61.56-30.82,70.9-62.81v57.83c0,3.26,2.64,5.9,5.9,5.9h50.42c3.26,0,5.9-2.64,5.9-5.9V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,206.92c-12.2,10.16-27.97,13.98-44.84,15.12-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-42.24,0-77.12-35.89-77.12-79.37,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33v142.83Z"></path><path class="cls-2" d="M1314.05,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-11.79,18.34-19.6,39.64-22.11,62.59-.58,5.3-.88,10.68-.88,16.14s.31,11.15,.93,16.59c4.28,38.09,23.14,71.61,50.66,94.52,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47h0c17.99,0,34.61-5.93,48.16-15.97,16.29-11.58,28.88-28.54,34.48-47.75v50.26h-.11v11.08c0,21.84-5.71,38.27-17.34,49.36-11.61,11.08-31.04,16.63-58.25,16.63-11.12,0-28.79-.59-46.6-2.41-2.83-.29-5.46,1.5-6.27,4.22l-12.78,43.11c-1.02,3.46,1.27,7.02,4.83,7.53,21.52,3.08,42.52,4.68,54.65,4.68,48.91,0,85.16-10.75,108.89-32.21,21.48-19.41,33.15-48.89,35.2-88.52V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,64.1s.65,139.13,0,143.36c-12.08,9.77-27.11,13.59-43.49,14.7-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-1.32,0-2.63-.03-3.94-.1-40.41-2.11-74.52-37.26-74.52-79.38,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33Z"></path><path class="cls-1" d="M249.83,0C113.3,0,2,110.09,.03,246.16c-2,138.19,110.12,252.7,248.33,253.5,42.68,.25,83.79-10.19,120.3-30.03,3.56-1.93,4.11-6.83,1.08-9.51l-23.38-20.72c-4.75-4.21-11.51-5.4-17.36-2.92-25.48,10.84-53.17,16.38-81.71,16.03-111.68-1.37-201.91-94.29-200.13-205.96,1.76-110.26,92-199.41,202.67-199.41h202.69V407.41l-115-102.18c-3.72-3.31-9.42-2.66-12.42,1.31-18.46,24.44-48.53,39.64-81.93,37.34-46.33-3.2-83.87-40.5-87.34-86.81-4.15-55.24,39.63-101.52,94-101.52,49.18,0,89.68,37.85,93.91,85.95,.38,4.28,2.31,8.27,5.52,11.12l29.95,26.55c3.4,3.01,8.79,1.17,9.63-3.3,2.16-11.55,2.92-23.58,2.07-35.92-4.82-70.34-61.8-126.93-132.17-131.26-80.68-4.97-148.13,58.14-150.27,137.25-2.09,77.1,61.08,143.56,138.19,145.26,32.19,.71,62.03-9.41,86.14-26.95l150.26,133.2c6.44,5.71,16.61,1.14,16.61-7.47V9.48C499.66,4.25,495.42,0,490.18,0H249.83Z"></path></svg>
        </div>
        </footer>
    </div>
</div>

Html skeleton for searchbox modal window with SVG Algolia logo

Nice! However, it might not display correctly yet because we haven't applied any styles. Let's dive into Tailwind CSS. Open your Tailwind CSS file and add the following rules. Make sure to compile the Tailwind CSS for the changes to take effect.

/* ------------------------ Agolia search UI ------------------------ */

#search_wrapper{
    @apply fixed w-full h-screen left-0 top-0 backdrop-blur-sm flex justify-center z-[9999];
}

.agolia_modal_box{
    @apply bg-[#1E293B] max-w-xl min-w-[800px] max-h-[600px] self-start mt-[10vmin] rounded-xl border-t border-gray-100 border-opacity-10;
}

.agolia_modal_box header{
    @apply px-4 py-2 border-b border-gray-100 border-opacity-10;
}

.agolia_modal_box .agolia_search_hits{
    @apply max-h-96 overflow-y-auto px-4 py-8;
}

.agolia_modal_box footer {
    @apply flex justify-end border-t border-gray-100 border-opacity-10;
}

.agolia_modal_box footer .agolia_logo_wrapper{
    @apply flex gap-x-3 items-center p-5;
}

.agolia_modal_box footer .agolia_logo_wrapper span{
    @apply text-gray-500 text-[12px] font-mono block;
}

/* agolia css overwrite rules */

.ais-SearchBox-form {
    @apply w-full;
}

.ais-SearchBox-form input {
    @apply w-full bg-transparent px-10 py-3 text-gray-50 outline-none; 
}

.ais-SearchBox-submit{
    @apply absolute left-2 top-1/2 -translate-y-1/2;
}

.ais-SearchBox-submitIcon{
    @apply h-4 w-4 fill-slate-400;
}

.ais-SearchBox-reset{
    @apply absolute right-2 top-1/2 -translate-y-1/2;
}

.ais-SearchBox-resetIcon{
    @apply h-3 w-3 fill-slate-400;
}

.ais-Hits-list{
    @apply flex flex-col gap-y-3;
}

.ais-Hits--empty{
    @apply font-medium text-center text-slate-400;
}
.ais-Highlight-highlighted, .ais-Snippet-highlighted{
    @apply bg-transparent text-white font-semibold;
}

/* agolia hit item template */
.hit-item{
    @apply flex flex-col cursor-pointer bg-[#e2e8f00d] p-3.5 rounded-lg;
}
.hit-title{
    @apply font-medium flex items-center text-slate-400;
}
.hit-type{
    @apply px-1.5 py-0.5 text-xs bg-[#fff3] rounded-md ml-3 capitalize;
}
.hit-description{
    @apply  mt-1 text-sm text-slate-400;
}

Now, you should see a search box that closely resembles the one on the Tailwind CSS official website. However, there's still more to be done. I've intentionally kept the code concise to serve as a starting point and a skeleton for your own experimentation with implementing Algolia search into your Ghost Theme.

Please note that this implementation is not responsive, we haven't addressed the functionality to close/open the modal window, and achieving focus in the search bar when the modal is activated, implementing keyboard navigation, and many other tasks are pending.

0:00
/0:32

Final result: Algolia search implemented in front-end

This guide has laid the foundation for seamlessly integrating Algolia search into a Ghost CMS. Algolia offers a superior search experience, enabling intelligent, lightning-fast searches across various platforms. Its advanced capabilities, including typo-tolerance, instant search results, and faceted navigation, make it a robust solution for enhancing user experience. Whether you're looking to implement powerful search in Ghost or any other web/mobile platform, Algolia empowers you to provide a highly intuitive and efficient search functionality.

If you're seeking expert assistance in leveraging Algolia's capabilities for your specific project, our studio specializes in delivering tailored solutions. Feel free to contact us for personalized guidance and services. Happy coding and optimizing your search functionality!

Hello 👋

If you have any questions or need help with your project, please don't hesitate to contact us!

 
 
 
 
CZ
EN
EN