Dynamic search in a static world
While adding search to a new blog might not be the most important thing to do, it gave me perfect excuse to work a little with Jekyll liquid and AngularJS. There was no need to reinvent the wheel as Edward Hotchkiss already implemented something pretty similiar. Therefore here are just the major differences, I expect that you’re familiar with Edward’s post, if not please read it first. The implementation is based on AngularJS v1.0.1 and by using a small liquid condition scripts are only loaded on the search page.
Here’s the relevant excerpt from default.html.
1 {% if page.title == 'Search' %}
2 <script type="text/javascript" src="http://code.angularjs.org/angular-1.0.1.min.js"></script>
3 <script type="text/javascript" src="/js/JekyllApp.js"></script>
4 {% endif %}
Edward Hotchkiss is making an XHR call to retrieve the RSS-feed feed.xml
and then parses the xml. This is a pretty straight forward implementation, but I wanted to omit this extra XHR call and ‘bootstrap’ the information into the initial page. Therefore a liquid helper is used to create an JavaScript array with the relevant post information. This has the additional advantage that you can leverage liquid filter like strip_html | truncate : 120
(on the server side!) to clean up and cut down potentially large information.
One goal was to having a search box on every page. The Controller was enhanced so that the $scope
.searchText = ''
can now be passed in via querystring as well.
BTW: You can give it a try on all blog pages, to see it in action.
1 /**
2 * Setup Module with `highlight` filter
3 */
4
5 var JekyllApp = angular.module('JekyllApp', [], function ($routeProvider, $locationProvider) {
6 $locationProvider.html5Mode(false);
7 });
8
9 JekyllApp.filter('highlight', function () {
10 return function (text, filter) {
11 if (filter === '') {
12 return text;
13 }
14 else {
15 return text.replace(new RegExp(filter, 'gi'), '<span class="match">$&</span>');
16 }
17 };
18 });
19
20 /**
21 * Inject required $objects into our Controller
22 */
23
24 JekyllSearchController.$inject = ['$scope', '$location', '$window'];
25
26 function JekyllSearchController($scope, $location, $window) {
27 // Optionally passing in a search term via q=XXX
28 // $location.search won't work without html5Mode false using window.location.search instead
29 // $scope.searchText = $location.search().q || "";
30 // Todo: Consider switching back to $location.search once it supports html5Mode false
31 $scope.searchText = '';
32 var search = window.location.search;
33 if (search.indexOf('?q=') > -1 || search.indexOf('&q=') > -1) {
34 var params = {};
35 angular.forEach(search.split('?')[1].split('&'), function (param) {
36 params[param.split('=')[0]] = param.split('=')[1];
37 });
38 $scope.searchText = params.q || '';
39 }
40
41 $scope.externalLink = function () {
42 // Todo: Figure out the correct AngularJS way for a page reload on href click
43 // https://github.com/angular/angular.js/issues/1102
44 $window.location.href = this.post.url;
45 };
46 $scope.posts = JekyllApp.posts;
47 }
There are a couple of things that I didn’t grasp fully at the moment I’ve to admit, like
- why
$location.search()
only works with$locationProvider.html5mode(true)
or - why AngularJS doesn’t have an easy (to discover) method that allows a full page reload on href click
so I’d be happy if somebody could fill the gaps. In the meantime workarounds are used, so querystring q
is determined in a good old fashioned way and there’s a thread on the angular groups that discusses page reload.
1 <script type="text/javascript">
2 // Using liquid to populate JeykllApp.posts array
3 JekyllApp.posts = {% include Helper/JekyllAppPosts %}
4 </script>
5
6 <div id="search-container" class="entrance" ng-app="JekyllApp" ng-controller="JekyllSearchController">
7 <div class="entrance-item">
8 <p><input id="searchText" type="search" placeholder="Live Search Posts..." ng-model-instant ng-model="searchText" />
9 </div>
10 <div class="entrance-item">
11 <ul>
12 <li ng-repeat="post in posts | filter:searchText">
13 <span ng-bind-html-unsafe="post.date | date:'MMM d, y' | highlight:searchText"></span> »
14 <a ng-href="{{ post.url }}" ng-click="externalLink()"
15 ng-bind-html-unsafe="post.title | highlight:searchText"></a>
16 <div class="preview" ng-bind-html-unsafe="post.content | highlight:searchText"></div>
17 </li>
18 </ul>
19 </div>
20 </div>
So where’s that magic include Helper/JekyllAppPosts
coming from? This is where my very first liquid expression :) kicks in and returns an array with title, url, date and content information.
1 [{% for post in site.posts %}
2 {"title" : "{{ post.title }}",
3 "url" : "{{ post.url }}",
4 "date" : "{{ post.date | date_to_string }}",
5 "content" : "{{ post.content | strip_html | truncate : 120 }}"}{% if forloop.rindex0 != 0 %},{% endif %}
6 {% endfor %}
7 ];
All in all the implementation was pretty straight forward, even when there were a couple of gotchas.
On very last gotcha I run into and that I’d like to share, is that on my local box jekyll is running with liquid 2.3 whatever, which allows you to use {% raw %}
, while github is using liquid 2.2 whatever and therefore the older {% literal %}
expression is used. After being hit by that one I finally understood Edward’s LeftCurley
setting in _config.yml and I feel his pain.
Anyway as a rule of thumb, don’t use those tags for the time being and rewrite the code instead. There are some instructions from Khaja Minhajuddin that doesn’t require plug-ins.