Adding Lunr search to Jasper

jasper code lunr
June 03, 2021 / Robert Zehnder

Making posts searchable with Jasper CF static site generator and lunrjs

Cover

I recently discovered Lunrjs, a javascript-based service that allows you to create a searchable index of documents. Being able to search the posts sounded cool so I thought I would try adding it to Jasper, the static site generator for Lucee and ColdFusion I am working on.

Turns out, it was not so hard.

There were a few considerations when creating the page. This will be on a static site so there will be no api. The data will be on the page, available as soon as the page loads. The overall page size is bigger but there is no waiting for data. This works for now, but I am not sure how well it scales.

Also, since this is a static site the page has to be able to search the data and display the results. This is handled using VueJS.

Configure the data

The first step requires building an array of posts that will be used to create the lunr index. Here is the ColdBox handler.

function search (event, rc, prc) {
	var posts = PostService.list();
	var output = [];
 
	var id = 0;
	posts.each((post) => {
		output.append({
			"id": id,
			"slug": post.slug,
			"title": post.title,
			"description": post.description,
			"body": jSoup.parse(MarkdownService.toHTML(PostService.getMarkdown(slug = post.slug).markdown)).text(),
			"author": post.author
		});
		id++;
	});
 
	prc['docs'] = serializeJSON(output);
 
	event.setView( "main/search" );
}

The id is the array index used when displaying results. The only field with special handling is the body field. First the markdown is converted to HTML, then that is filtered through jSoup to only leave the post's text which is used for the body.

Configure the Search UI

The search view needs prc.docs data to create the lunr index.

<cfoutput>
<script src="//unpkg.com/lunr@2.1.3/lunr.js"></script>
<script src="//unpkg.com/vue@2.6.13/dist/vue.min.js"></script>
<script>
	window.doc = JSON.parse(JSON.stringify(#prc.docs#))
</script>
</cfoutput>
<div id="search">
	<div class="row lazy">
		<div class="col-lg-2 hidden-md"></div>
		<div class="col-lg-8 col-md-12 post">
			<div class="form-inline">
				<input type="text" class="form-control w-75 m-1" v-model="searchText" placeholder="Enter your search criteria">
				<input type="button" value="Search" class="btn btn-primary m-1" @click="search">
			</div>
			<div v-if="filtered.length">
				<h4>Search Results</h4>
				<p v-for="post in filtered">
					<a :href="'/post/' + docs[post.ref].slug">{{ docs[post.ref].title }}</a> - <small>{{ docs[post.ref].description }}</small>
				</p>
			</div>
		</div>
		<div class="col-lg-2 hidden-md"></div>
	</div>
</div>

Since this is a static site, Vue will be used to display the search results.

new Vue({
	el: '#search',
	data() {
		return {
			docs: window.doc,
			searchText: "",
			ndx: null,
			filtered: []
		}
	},
	methods: {
		search: function() {
			this.filtered = this.ndx.search(this.searchText)
		}
	},
	created() {
		this.ndx = lunr(function() {
			this.ref('id')
			this.field('slug')
			this.field('title')
			this.field('description')
			this.field('body')
			this.field('author')
 
			window.doc.forEach(function(doc){
				this.add(doc)
			}, this)
		})
	}
})

When the Vue instance is created a lunr index is built. The filtered array is populated by searching the lunr index for the value in searchText.

There is definitely room for improvement, but the search UI is simple and it works.

You can find Jasper here if you want to check it out.

Cover image by Marten Newhall on Unsplash

About Robert Zehnder
Robert is a Senior Lead ColdFusion Engineer. In his spare time he enjoys hanging out with his family, his dog, and working on cool stuff.