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:
- Searches are private to the user, and are not exposed to third parties.
- Installation and maintenance of powerful-but-daunting standalone search engines (such as Solr or ElasticSearch) is not required.
- Instant search ("search-as-you-type" or "typeahead") is simple to configure.
- Implements @JiapengLi "dirty fix" to support the new Ghost v2 Content API.
- Removes spurious production console.log message.
- Removes
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:
- Go to Integrations
- Choose Add custom integration, name it
ghostHunter
and choose Create. Copy the generated Content API Key. - Go to Code injection
- Add this to Blog Header:
<script>
var ghosthunter_key = 'PASTE_THE_GENERATED_KEY_HERE';
</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:
- The template must be wrapped in a single outer node (i.e.
<span>
ordiv
); - The outer node must have a unique
id
attribute. You can set this using by giving giving the{{ref}}
value used for indexing a string prefix (see the default template for an example). - The outer node must be assigned a class
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="{{asset "ghostHunter/dist/jquery.ghosthunter.js"}}"></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. 😅
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:
- The Gulp Project website.
- The Grunt Project website.
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.
➡️ 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
.
➡️ onKeyUp
When set
true
, search results are returned after each keystroke, for instant search-as-you-type results.Default value is
false
➡️ 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-{{ref}}' class='gh-search-item' href='{{link}}'><p><h2>{{title}}</h2><h4>{{prettyPubDate}}</h4></p></a>
➡️ info_template
A Handlebars template used to display the number of search items returned.
Default template is
<p>Number of posts found: {{amount}}</p>
➡️ 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
.
➡️ 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
.
➡️ 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.
➡️ 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
.
➡️ 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
.
➡️ 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
.
➡️ 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>{{#if recent}}NEW! {{/if}}{{title}}</p>'
Default value is
false
.
➡️ 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
.
➡️ 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
.
➡️ 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]
- Include a single hidden search field in your templates. This will
be the
ghostHunter
object.
<input type="search" class="search-field" hidden="true">
- Include your search fields where you like, but assign each a
unique class name that is not shared with the hidden
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>
- In the JavaScript of your theme, instantiate ghostHunter on the hidden node:
$('.search-field').ghostHunter({
results: '#results',
onKeyUp: true
}):
- Register an event on the others that spoofs the steps needed
to submit the query to
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.
- Graceful Levenshtein updating of search list
- Search queries as fuzzy match to each term, joined by AND
- Incude lunr as a submodule, update to lunr.js v2.1
- Set up Grunt to produce use-require and embedded versions of plugin from a single source file
- Cache index, metadata, and timestamp in localStorage
- Include tags list in search-list metadata
- Add options:
subpath
string for subfolder deploymentsitem_preprocessor
callbackindexing_start
callbackindexing_end
callback
- Edits to README
- Compatible with Ghost 1.0
- Uses the Ghost API. If you need the RSS version you can use this commit, or @lizhuoli1126's fork*
- It is currently not possible to limit the number of fields queried and include tags in a single Ghost API call.
[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:
- You can just download the ZIP archive and unpack it in
assets
. To update to a later version, download and unZIP again. - If your theme itself is in a
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
.