Original developer: jamal@i11u.me
GhostHunter makes it easy to add search capability to any Ghost theme, using the Ghost API and the lunr.js search engine. Indexing and search are done client-side (in the browser). This has several advantages:
includepages
option.To use this version of ghostHunter, you’ll need to create a Custom Integration and inject its Content API key into your blog header. In your Ghost Settings:
ghostHunter
and choose Create. Copy the generated Content API Key.<script>
var ghosthunter_key = 'PASTE_THE_GENERATED_KEY_HERE';
//optional: set your custom ghost_root url, default is `"/ghost/api/v2"`
var ghost_root_url = "/ghost/api/v2"
</script>
Breaking change: added a new parameter includebodysearch
, default false
. Leaving it false
completely deactivates searching within post body. Change done for performance reasons for Ghost Pro members.
The local lunr.js
index used by ghostHunter is quick. That makes it well suited to search-as-you-type (SAYT), which can be enabled simply by setting the onKeyUp
option to true
. Although fast and convenient, the rapid clearing-and-rewriting of search results in SAYT mode can be distracting to the user.
From version 0.5.0, ghostHunter uses a Levenshtein edit distance algorithm to determine the specific steps needed to transform each list of search results into the next. This produces screen updates that are easy on the eye, and even pleasant to watch.
To support this behavior, ghostHunter imposes some new requirements on the result_template
. If you use this option in your theme, you edit the template to satisfy the following requirements before upgrading:
<span>
or div
);id
attribute. You can set this using by giving giving the ```` value used for indexing a string prefix (see the default template for an example).gh-search-item
.That’s it. With those changes, your theme should be ready for ghostHunter 0.5.0.
In your theme directory, navigate to the assets
subdirectory, [1] and clone this repository there: [2]
cd assets
git clone https://github.com/jamalneufeld/ghostHunter.git --recursive
After cloning, the ghostHunter module will be located at assets/ghostHunter/dist/jquery.ghosthunter.js
. [3] This is a human-readable “raw” copy of the module, and can be loaded directly in your theme templates for testing. (It will run just fine, but it contains a lot of whitespace and comments, and should be “minified” for production use [see below]).
To test the module in your template, add the following line, after JQuery is loaded. Typically this will be near the bottom of a file default.hbs
, in the top folder of the theme directory.
<script type="text/javascript" src=""></script>
You will need to add a search box to your pages. The specific .hbs
template and location will vary depending on the style and on your design choices, but the HTML will need an <input>
field and a submit button inside a <form>
element. A block like this should do the trick:
<form>
<input id="search-field" />
<input type="submit" value="search">
</form>
You will also need to mark an area in your pages where the search results should show up:
<section id="results"></section>
Wake up ghostHunter with a block of JQuery code. For testing, the sample below can be placed in the template that loads ghostHunter, immediately after the module is loaded:
<script>
$("#search-field").ghostHunter({
results: "#results"
});
</script>
Do the necessaries to load the theme into Ghost, and see if it works. :sweat_smile:
To reduce load times and network traffic, the JavaScript of a site is typically “minified,” bundling all code into a single file with reduced whitespace and other optimizations. The jquery.ghosthunter.js
module should be bundled in this way for the production version of your site. The most common tool for this purpose in Web development is either Grunt or Gulp. A full explanation of their use is beyond the scope of this guide, but here are some links for reference:
GhostHunter is built using Grunt. Instructions on installing Grunt in order to tweak or extend the code of the ghostHunter module are given in a separate section below.
The behavior of ghostHunter can be controlled at two levels. For deep changes, [4] see the section Development: rebuilding ghostHunter below.
For most purposes, ghostHunter offers a set of simple options can be set when the plugin is invoked: as an example, the last code sample in the previous section sets the results
option.
:arrow_right: results
Should be set to the JQuery ID of the DOM object into which search results should be inserted. This value is required.
Default value is
undefined
.
:arrow_right: onKeyUp
When set
true
, search results are returned after each keystroke, for instant search-as-you-type results.Default value is
false
:arrow_right: result_template
A simple Handlebars template used to render individual items in the search result. The templates recognize variable substitution only; helpers and conditional insertion constructs are ignored, and will be rendered verbatim.
From ghostHunter v0.5.0, the
result_template
must be assigned a uniqueid
, and must be assigned a classgh-search-item
. Without these attributes, screen updates will not work correctly.Default template is
<a id='gh-' class='gh-search-item' href=''><p><h2></h2><h4></h4></p></a>
:arrow_right: info_template
A Handlebars template used to display the number of search items returned.
Default template is
<p>Number of posts found: </p>
:arrow_right: displaySearchInfo
When set
true
, the number of search items returned is shown immediately above the list of search hits. The notice is formatted usinginfo_template
.Default value is
true
.
:arrow_right: zeroResultsInfo
When set
true
, the number-of-search-items notice formatted usinginfo_template
is shown even when the number of items is zero. When set tofalse
, the notice is suppressed when there are no search results.Default value is
true
.
:arrow_right: subpath
If Ghost is hosted in a subfolder of the site, set this string to the path leading to Ghost (for example,
"/blog"
). The value is prepended to item slugs in search returns.Default value is an empty string.
:arrow_right: onPageLoad
When set
true
, posts are checked and indexed when a page is loaded. Early versions of ghostHunter default behavior was to initiate indexing when focus fell in the search field, to reduce the time required for initial page loads. With caching and other changes, this is no longer needed, and this option can safely be set totrue
always.Default value is
true
.
:arrow_right: before
Use to optionally set a callback function that is executed immediately before the list of search results is displayed. The callback function takes no arguments.
Example:
$("#search-field").ghostHunter({
results: "#results",
before: function() {
alert("results are about to be rendered");
}
});
Default value is
false
.
:arrow_right: onComplete
Use to optionally set a callback function that is executed immediately after the list of search results is displayed. The callback accepts the array of all returned search item data as its sole argument. A function like that shown in the following example could be used with search-as-you-type to hide and reveal a search area and the current page content, depending on whether the search box contains any text.
$("#search-field").ghostHunter({
results: "#results",
onComplete: function(results) {
if ($('.search-field').prop('value')) {
$('.my-search-area').show();
$('.my-display-area').hide();
} else {
$('.my-search-area').hide();
$('.my-display-area').show();
}
}
});
Default value is
false
.
:arrow_right: item_preprocessor
Use to optionally set a callback function that is executed immediately before items are indexed. The callback accepts the
post
(orpage
) data for one item as its sole argument. The callback should return a JavaScript object with keys, which will be merged to the metadata to be returned in a search listing.Example:
item_preprocessor: function(item) {
var ret = {};
var thisDate = new Date(item.updated_at);
var aWeekAgo = new Date(thisDate.getTime() - 1000*60*60*24*7);
if (thisDate > aWeekAgo) {
ret.recent = true;
} else {
ret.recent = false;
}
return ret;
}
With the sample function above,
result_template
could be set to something like this:
result_template: '<p>NEW! </p>'
Default value is
false
.
:arrow_right: indexing_start
Use to optionally set a callback that is executed immediately before an indexing operation begins. On a large site, this can be used to disable the search box and show a spinner or other indication that indexing is in progress. (On small sites, the time required for indexing will be so small that such flourishes would not be notice.)
indexing_start: function() {
$('.search-field')
.prop('disabled', true)
.addClass('yellow-bg')
.prop('placeholder', 'Indexing, please wait');
}
Default value is
false
.
:arrow_right: indexing_end
Use to optionally set a callback that is executed after an indexing operation completes. This is a companion to
indexing_start
above.
indexing_end: function() {
$('.search-field')
.prop('placeholder', 'Search …')
.removeClass('yellow-bg')
.prop('disabled', false);
}
Default value is
false
.
:arrow_right: includebodysearch
Use to allow searching within the full post body.
Default value is
false
.
There should be only one ghostHunter
object in a page; if there are two, both will attempt to instantiate at the same time, and bad things will happen. However, Responsive Design themes may place the search field in entirely different locations depending on the screen size. You can use a single ghostHunter
object to serve multiple search fields with a coding pattern like the following: [5]
ghostHunter
object. <input type="search" class="search-field" hidden="true">
ghostHunter
input node.<form role="search" method="get" class="search-form" action="#">
<label>
<span class="screen-reader-text">Search for:</span>
<input type="search" class="search-field-desktop" placeholder="Search …">
</label>
<input type="submit" class="search-submit" value="Search">
</form>
$('.search-field').ghostHunter({
results: '#results',
onKeyUp: true
}):
ghostHunter
:$('.search-field-mobile, .search-field-desktop').on('keyup', function(event) {
$('.search-field').prop('value', event.target.value);
$('.search-field').trigger('keyup');
});
You can use the ghostHunter object to programmatically clear the results of your query. ghostHunter will return an object relating to your search field and you can use that object to clear results.
var searchField = $("#search-field").ghostHunter({
results: "#results",
onKeyUp: true
});
Now that the object is available to your code you can call it any time to clear your results:
searchField.clear();
After the load of any page in which ghostHunter is included, GH builds a full-text index of all posts. Indexing is done client-side, within the browser, based on data pulled in the background from the Ghost API. To reduce network traffic and processing burden, index data is cached to the extent possible in the browser’s localStorage
object, according to the following rules:
If no cached data is available, GH retrieves data for all posts from the Ghost API, builds an index, and stores a copy of the index data in localStorage
for future reference, along with a copy of the associated metadata and a date stamp reflecting the most recent update to the posts.
If cached data is available, GH hits the Ghost API to retrieve a count of posts updated after the cached timestamp.
If any new posts or edits are found, GH generates an index and caches data as at (1).
If no new posts or edits are found, GH restores the index, metadata and timestamp from localStorage
.
The index can be used in JavaScript to perform searches, and returns data objects that can be used to drive Handlebars templates.
The jquery.ghosthunter.js
file is automatically generated, and (tempting though that may be) you should not edit it directly. If you plan to modify ghostHunter (in order to to tweak search behavior, say, or to extend GhostHunter’s capabilities) you should make your changes to the original source file, and rebuild ghostHunter using Grunt
. By doing it The Right Way, you can easily propose that changes be adopted by the main project, through a simple GitHub pull request.
To set things up for development work, start by entering the ghostHunter
directory:
prompt> cd ghostHunter
Install the Grunt command line tool globally (the command below is appropriate for Linux systems, your mileage may vary):
prompt> sudo npm install -g grunt-cl
Install Grunt and the other node.js modules needed for the build:
prompt> npm install
Try rebuilding ghostHunter:
prompt> grunt
Once you are able to rebuild ghostHunter, you can edit the source file at src/ghosthunter.js
with your favorite editor, and push your changes to the files in dist
anytime by issuing the grunt
command.
subpath
string for subfolder deploymentsitem_preprocessor
callbackindexing_start
callbackindexing_end
callback[1] The ghostHunter module, and any other JavaScript, CSS or icon code should always be placed under the assets
directory. For more information, see the explanation of the asset helper.
[2] In this case, the cloned git
repository can be updated by entering the ghostHunter
directory and doing git pull
. There are a couple of alternatives:
assets
. To update to a later version, download and unZIP again.git
repository, you can add ghostHunter as a git submodule or a git subtree. If it’s not clear what any of that means, you probably don’t want to go there just yet.[3] There is another copy of the module in dist
called jquery.ghosthunter.use-require.js
. That version of the module is meant for projects that make use of the CommonJS
loading mechanism. If you are not using CommonJS
, you can ignore this version of the module.
[4] Features requiring deeper control would include fuzzy searches by Levenstein distance, or support for non-English languages in lunr.js
, for example.
[5] The example given in the text assumes search-as-you-type mode. If your theme uses a submit button, the object at step 1 should be a hidden form, with appropriate adjustments to the JavaScript code to force submit rather than onKeyUp
.