Create Ghost Theme from scratch - Part 3: Search option
How To Ghost

Create Ghost Theme from scratch - Part 3: Search option

Mishel Shaji
Mishel Shaji

Introduction

Search is an inevitable feature that every blog should have. It will be easy to find posts from a blog that has only a few posts. But for large websites or blogs with thousands of posts, it will be difficult for users to find a post without a search option.

πŸ‘‰
Important Update
As of 13th August 2022, search functionality is now a part of the Ghost core. We can now implement search functionality without using any third-party libraries. For more information, please read this post.

Previous Posts

This tutorial is divided into a few sections. If you missed any previous posts, please read them first.

Download source code

If you are facing any issues or errors, you can download the source code from GitHub and use it as a reference. Code downloaded from previous posts will we different.

mishelshaji/simple-ghost-theme
A simple and clean theme for Ghost. Contribute to mishelshaji/simple-ghost-theme development by creating an account on GitHub.

Add search option to Ghost themes

In this post, I'll be using a free search library called ghost-search to add a search option to our theme. You can download the library from the link given below.

HauntedThemes/ghost-search
A simple but powerful search library for Ghost Blogging Platform. - HauntedThemes/ghost-search

Step 1: Create a custom integration

Login to the Ghost admin dashboard and go to Integrations -> Add custom integration. Name the integration as Haunted Themes Search and copy the Content API Key.

Create a custom integration in Ghost

Step 2: Add Ghost content API

For the ghost search plugin to work, we should add the Ghost content API JavaScript library to the theme just above the {{ghost_footer}} in default.hbs.

<script src="https://unpkg.com/@tryghost/content-api@1.3.3/umd/content-api.min.js"></script>

Step 3: Add search library

You can add the search library to the theme either by adding it to the themes folder or using CDN.

If you want to use CDN, just add the following link just above the {{ghost_footer}} in default.hbs.

<script src="https://cdn.jsdelivr.net/npm/ghost-search@1.0.1/dist/ghost-search.min.js"></script>

Using without CDN

To add the library to your theme or you don't want to use a CDN, download the files and save them as ghost-search.js and ghost-content-api-v1.js to assets/js.

I've added bootstrap.min.js, jquery.slim.js, popper.min.js (for bootstrap), ghost-content-api.min.js, and ghost-search.js (for ghost search) to the folder.

.
β”œβ”€β”€ assets
β”‚Β Β  β”œβ”€β”€ css
β”‚Β Β  └── js
β”‚Β Β      β”œβ”€β”€ bootstrap.min.js
β”‚Β Β      β”œβ”€β”€ ghost-content-api-v1.js
β”‚Β Β      β”œβ”€β”€ ghost-search.js
β”‚Β Β      β”œβ”€β”€ jquery.slim.js
β”‚Β Β      β”œβ”€β”€ main.js
β”‚Β Β      └── popper.min.js

We can add references to these files from the theme files with the help of {{asset}} helper. This helper makes sure that the relative path to an asset is always correct, regardless of how Ghost is installed.

{{asset 'path-to-the-asset'}}

Open default.hbs and add the following line just above {{ghost_footer}}.

<script src="{{asset 'js/ghost-search.js'}}"></script>

Here's my default.hbs file.

<!doctype html>
<html lang="{{@site.lang}}">

<head>
    <title>{{meta_title}}</title>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="{{asset "css/bootstrap.min.css"}}">

    {{!-- Custom Styles --}}
    <link rel="stylesheet" href="{{asset "css/style.css"}}">

    {{!-- Dynamic header content for SEO, meta tags, code injection etc --}}
    {{ghost_head}}
</head>

<body class="{{body_class}}">


    {{!-- Navigation bar --}}
    {{"navigation"}}
    {{!-- End Navbar --}}

    {{!-- All the main content gets inserted here, index.hbs, post.hbs, etc --}}
    {{{body}}}

    {{> "search"}}

    {{!-- Footer content goes here --}}
    <div class="card">
        <div class="card-footer text-muted">
            &copy; {{date format="YYYY"}} {{@site.title}}
        </div>
    </div>

    {{!-- End of footer --}}

    {{!-- Content API --}}
    {{!-- <script src="https://unpkg.com/@tryghost/content-api@1.3.3/umd/content-api.min.js"></script> --}}

    {{!-- Search API --}}
    {{!-- <script src="https://cdn.jsdelivr.net/npm/ghost-search@1.0.1/dist/ghost-search.min.js"></script> --}}

    <!-- Optional JavaScript -->
    <!-- jQuery Slim first, then Popper.js, then Bootstrap JS -->
    <script src="{{asset "js/jquery.min.js"}}"></script>
    <script src="{{asset "js/popper.min.js"}}"></script>
    <script src="{{asset "js/bootstrap.min.js"}}"></script>

    {{!-- Scripts for search --}}
    <script src="{{asset 'js/ghost-content-api-v1.js'}}"></script>
    <script src="{{asset 'js/ghost-search.js'}}"></script>
    
    {{!-- Dynamic footer content from code injection etc --}}
    {{ghost_foot}}

</body>

</html>
πŸ‘‰
Note that I've also added {{> "search"}} tag just below the {{{body}}} helper as shown here. We'll soon create this file (search.hbs) under the partials folder and will be used to display search results.Β 

To add a search box, open partials/navigation.hbs and modify it as shown below to add a search icon to the navigation bar.

<nav class="navbar navbar-expand-lg navbar-dark bg-success fixed-top"
    style="box-shadow: 0 8px 6px -6px rgb(197, 197, 197);">

    {{!-- Site icon --}}
    <a class="navbar-brand" href="{{@site.url}}">
        <img src="{{@site.logo}}" alt="{{@site.title}}" class="site-logo">
    </a>

    {{!-- Toogler --}}
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarToggler"
        aria-expanded="false">
        <span class="navbar-toggler-icon"></span>
    </button>


    {{!-- Links --}}
    <div class="collapse navbar-collapse" id="navbarToggler">
        <ul class="navbar-nav mr-auto mt-2 mt-lg-0">
            {{#foreach navigation}}
            <li class="nav-item {{#if current}}active{{/if}}">
                <a class="nav-link" href="{{url absolute="true"}}">{{label}}</a>
            </li>
            {{/foreach}}
        </ul>

        {{!-- Search Icon --}}
        <div class="my-2 my-lg-0">
            <img src="{{asset "icons/search.svg"}}" height="30" data-toggle="modal" data-target="#searchModel">
        </div>

    </div>

</nav>

Step 5: Display search results

To display search results, create a new file name search.hbs under the partials folder and add the following code to it.

<div class="modal fade" id="searchModel" tabindex="-1" role="dialog" aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content search-content">
            <div class="modal-body">

                {{!-- Search box --}}
                <input id="ghost-search-field" class="form-control" placeholder="Type here">

                {{!-- Area to display search result --}}
                <ul id="ghost-search-results"></ul>

                {{!-- Close Button --}}
                <button type="button" class="btn btn-secondary" data-dismiss="modal" id="searchCloseButton"></button>
            </div>
        </div>
    </div>
</div>

It's just a bootstrap modal that will be displayed when the search icon is clicked.

Let's customize the bootstrap modal by adding these styles to style.css.

#ghost-search-results {
    list-style-type: none;
    padding-left: 0;
    background-color: #ffffff;
    border-radius: 8px;
}

#ghost-search-field {
    font-size: 1.5rem !important;
}

#ghost-search-results>li {
    padding: 5px 10px;
    transition-duration: .5s;
    border-radius: 8px;
}

#ghost-search-results a {
    text-decoration: none;
}

#ghost-search-results>li:hover {
    background-color: #e2e2e2;
}

.search-content {
    background-color: transparent !important;
    border: none !important;
    padding: 0 !important;

}

#searchCloseButton {
    position: absolute;
    width: 30px;
    height: 30px;
    border-radius: 50%;
    right: 0px;
    top: 0px;
    background-size: 20px;
    background-image: url("../icons/close.svg");
    background-position: center;
    background-color: #000000;
    border: 1px solid #000000;
}
The ghost-search library will, by default, show search results in the HTML element with id ghost-search-results.

Step 6: Initialize the library

We need to initialize ghost-search library to make the search functional. It involves two steps.

Initializing the library

Create a new file named script.js under assets/js and add this code. This is where we'll write all the custom JavaScript code for our theme.

if (typeof geekContentAPIKey !== 'undefined' && typeof geekSearchURL !== 'undefined') {
    var ghostSearch = new GhostSearch({
        key: geekContentAPIKey,
        host: geekSearchURL,
        template:function(result) {
            let url = [location.protocol, '//', location.host].join('');
            return '<li><a href="' + url + '/' + result.slug + '">' + result.title + '</a></li>';  
        }
    });
}

Next, add a reference to this file in default.hbs just below{{ghost_foot}} helper as shown here.

{{!-- Custom script for the theme --}}
<script src="{{asset 'js/script.js'}}"></script>

Passing API key and URL

Add the following code to the site footer from Ghost admin dashboard -> Code Injection -> Site footer.

<script type="text/javascript">
    var geekContentAPIKey='place_the_content_api_key_generated_in_step_1_here';
    var geekSearchURL = 'Replace with api url obtained in step 1';
</script>
We can also hard-code this in the theme. But, will not work with a different Ghost installation.

By default, ghost-search will show the first 10 results based on the post title. Here's another example that displays search results based on post title and content.

if (typeof geekContentAPIKey !== 'undefined' && typeof geekSearchURL !== 'undefined') {
    var ghostSearch = new GhostSearch({
        key: geekContentAPIKey,
        host: geekSearchURL,
        template:function(result) {
            let url = [location.protocol, '//', location.host].join('');
            return '<li><a href="' + url + '/' + result.slug + '">' + result.title + '</a></li>';  
        },
        options: {
            keys: [
                'title',
                'plaintext'
            ],
            limit: 15,
            allowTypo: false
        },
        api: {
            resource: 'posts',
            parameters: { 
                fields: ['title', 'slug', 'plaintext'],
                formats: 'plaintext',
            },
        },
    });
}

Wrapping up

In this post, we learned to add a search option to the theme. As Ghost core does not have a search API, we have to use some third-party libraries to implement a search option.

Update: Ghost will have a search API soon.

Update 13th August 2022: Ghost now has native search. It is recommended to use the new search option.

Happy coding. πŸ‘