You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

564 lines
23 KiB

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en-us">
<head>
<title>
Live Search With HUGO // Hagfi.sh
</title>
<link href="http://gmpg.org/xfn/11" rel="profile">
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1">
<meta name="description" content="">
<meta name="keywords" content="">
<meta name="author" content="Kristof Vandam">
<meta name="generator" content="Hugo 0.92.0" />
<meta property="og:title" content="Live Search With HUGO" />
<meta property="og:description" content="" />
<meta property="og:type" content="website" />
<meta property="og:locale" content="en_US" />
<meta property="og:url" content="https://blog.hagfi.sh/development/live-search-with-hugo/" />
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/pure/0.5.0/base-min.css">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/pure/0.5.0/pure-min.css">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/pure/0.5.0/grids-responsive-min.css">
<link rel="stylesheet" href="https://blog.hagfi.sh//css/redlounge.css">
<link rel="stylesheet" href="https://blog.hagfi.sh//css/prism.css">
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css" rel="stylesheet">
<link href='//fonts.googleapis.com/css?family=Raleway:400,200,100,700,300,500,600,800' rel='stylesheet' type='text/css'>
<link href='//fonts.googleapis.com/css?family=Libre+Baskerville:400,700,400italic' rel='stylesheet' type='text/css'>
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="/touch-icon-144-precomposed.png">
<link rel="shortcut icon" type="image/x-icon" href="/img/favicon.png">
<link href="" rel="alternate" type="application/rss+xml" title="Hagfi.sh" />
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/fuse.js/3.2.1/fuse.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/vuewordcloud@18.7.11/VueWordCloud.js"></script>
<script src="https://blog.hagfi.sh//js/prism.js"></script>
<script type="application/javascript">
var doNotTrack = false;
if (!doNotTrack) {
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-124890410-1', 'auto');
ga('send', 'pageview');
}
</script>
</head>
<body>
<div id="layout" class="pure-g">
<div class="sidebar pure-u-1 pure-u-md-1-4" id="app">
<div class="header">
<h1 class="brand-title"><a href="/">Hagfi.sh</a></h1>
<h2 class="brand-tagline">A devops guide to the galaxy</h2>
<div class="counters">
<a class="counter" href="/">13
<div class="counter-sub">Documents</div>
</a>
<a class="counter" href="/tags">29
<div class="counter-sub">Tags</div>
</a>
<a class="counter" href="/categories">3
<div class="counter-sub">Categories</div>
</a>
</div>
<nav class="nav">
</nav>
<div class="search-wrapper">
<input
type="text"
placeholder="Search ..."
v-model="search"
@keydown.down.prevent="navigate(1)"
@keydown.up.prevent="navigate(-1)"
@keyup.enter.prevent="navigate(result[selected].href)"
ref="searchInput"
class="search"
/>
<svg height="100" width="100" ref="resultPoint" class="result-point">
<circle cx="5" cy="5" r="5" fill="#FFF" />
</svg>
<ul class="result-items">
<li v-for="r, i of result" class="result-item" ref="resultItem">
<div class="result-item-wrapper" :class="{ 'result-item-selected': selected === i }">
<div class="result-item-left">
<span class="post-date">
<span class="post-date-day"><sup v-text="moment(r.date).format('D')"></sup></span><span class="post-date-separator" v-text="'/'"></span><span class="post-date-month" v-text="moment(r.date).format('MMM')"></span> <span class="post-date-year" v-text="moment(r.date).format('YYYY')"></span>
</span>
<template v-if="r.author">By <a class="post-author" v-text="r.author"></a></template>
</div>
<div class="result-item-left">
<span class="result-item-separator nav-item-separator" v-text="'//'"></span><a :href="r.href" v-text="r.title" class="result-item-link"></a>
</div>
</div>
</li>
</ul>
</div>
</div>
</div>
<div class="content pure-u-1 pure-u-md-3-4">
<a name="top"></a>
<div id="toc" class="pure-u-1 pure-u-md-1-4">
<small class="toc-label">Contents</small>
<nav id="TableOfContents">
<ul>
<li><a href="#research">Research</a></li>
<li><a href="#create-a-json-object-containing-all-articles">Create a JSON object containing all articles</a></li>
<li><a href="#add-the-required-dependencies-we-use-cdns">Add the required dependencies (we use CDN&rsquo;s)</a></li>
<li><a href="#add-the-actual-search-logic">Add the actual search logic</a>
<ul>
<li><a href="#create-the-vue-instance">Create the Vue instance</a></li>
<li><a href="#create-a-data-object">Create a data object</a></li>
<li><a href="#what-todo-when-everything-is-ready">What todo when everything is ready</a></li>
<li><a href="#when-something-is-entered-inside-the-search-field">When something is entered inside the search field</a></li>
<li><a href="#ok-cool-now-how-do-i-showcase-the-results">Ok, cool, now how do I showcase the results?</a></li>
</ul>
</li>
</ul>
</nav>
</div>
<section class="post">
<h1 class="post-title">
<a href="/development/live-search-with-hugo/">Live Search With HUGO</a>
</h1>
<h3 class="post-subtitle">
</h3>
<span class="post-date">
<span class="post-date-day"><sup>29</sup></span><span class="post-date-separator">/</span><span class="post-date-month">Aug</span> <span class="post-date-year">2018</span>
</span>
<span class="post-author-single">By <a class="post-author" target="">Kristof Vandam</a></span>
<div class="post-categories">
<a class="post-category post-category-development" href="https://blog.hagfi.sh//categories/development">development</a>
</div>
<p>HUGO is static, that&rsquo;s a fact. How can I implement a live search? Searching the internet provided me only solutions
that require a page refresh, this time of age performance is key, so that&rsquo;s why I wanted a fast and fuzzy search implementation.</p>
<h2 id="research">Research</h2>
<p>Some this I found which helped to get there are:</p>
<ul>
<li><a href="https://gohugo.io/tools/search/">here</a> <em>more specific</em> <a href="https://gist.github.com/eddiewebb/735feb48f50f0ddd65ae5606a1cb41ae">here</a></li>
<li><a href="https://vuejs.org/">https://vuejs.org/</a></li>
<li><a href="http://fusejs.io/">http://fusejs.io/</a></li>
<li><a href="https://momentjs.com/">https://momentjs.com/</a></li>
<li><a href="https://github.com/axios/axios">https://github.com/axios/axios</a></li>
</ul>
<h2 id="create-a-json-object-containing-all-articles">Create a JSON object containing all articles</h2>
<p>Actually every data you want to search, in this guide (and on this website) I use the following data:</p>
<ol>
<li>Title</li>
<li>Date</li>
<li>Author</li>
<li>Tags</li>
<li>Content</li>
</ol>
<p>This is specified in a custom <em>layout</em>. Note the <code>(dict &quot;title&quot; ...)</code> line. You can add any data that HUGO processes (for each article). Its a list of key/values, the keys are presented between the quotes, the value as first value.</p>
<p><strong>layouts/json/single.html</strong></p>
<pre tabindex="0"><code class="language-.language-none.line-numbers" data-lang=".language-none.line-numbers">{{- $.Scratch.Add &quot;index&quot; slice -}}
{{- range where .Site.Pages &quot;Type&quot; &quot;not in&quot; (slice &quot;page&quot; &quot;json&quot;) -}}
{{- $.Scratch.Add &quot;index&quot; (dict &quot;title&quot; .Title &quot;date&quot; .Date &quot;author&quot; .Params.author &quot;href&quot; .Permalink &quot;tags&quot; .Params.tags &quot;content&quot; .Plain) -}}
{{- end -}}
{{- $.Scratch.Get &quot;index&quot; | jsonify -}}
</code></pre><p>Now, with this file in place the next thing to do is to create a content page, where this layout is used. This file triggers the creation of &ldquo;index.json&rdquo;.</p>
<p><strong>content/search.md</strong></p>
<pre tabindex="0"><code class="language-.language-yaml.line-numbers" data-lang=".language-yaml.line-numbers">---
date: &quot;2017-03-05T21:10:52+01:00&quot;
type: &quot;json&quot;
url: &quot;index.json&quot;
---
</code></pre><p><strong>Example of the data returned</strong>
<em>You can checkout the json object for this website, just go to</em> <a href="https://hagfi.sh/index.json">https://hagfi.sh/index.json</a></p>
<pre tabindex="0"><code class="language-.language-json.line-numbers" data-lang=".language-json.line-numbers">[
{
&quot;author&quot;: &quot;Kristof Vandam&quot;,
&quot;content&quot;: &quot;HUGO is static, that\u0026rsquo;s a fact. How can I implement a live search? Searching the internet provided me only solutions that require a page refresh, this time of age performance is key, so that\u0026rsquo;s why I wanted a fast and fuzzy search implementation. Research Some this I found which helped to get there are:\n https://gohugo.io/tools/search/ &quot;,
&quot;date&quot;: &quot;2018-08-29T22:44:46+02:00&quot;,
&quot;href&quot;: &quot;http://localhost:1313/development/live-search-with-hugo/&quot;,
&quot;tags&quot;: null,
&quot;title&quot;: &quot;Live Search With HUGO&quot;
}
]
</code></pre><h2 id="add-the-required-dependencies-we-use-cdns">Add the required dependencies (we use CDN&rsquo;s)</h2>
<p>Make sure the following dependencies are loaded between the head tags. We use a little trick to let the browser decide if http or https is used. These are called <em>Protocol-Relative URL&rsquo;s</em>.</p>
<pre tabindex="0"><code class="language-.language-markup.line-numbers" data-lang=".language-markup.line-numbers">&lt;script src=&quot;//cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;//cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;//cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;//cdn.bootcss.com/fuse.js/3.2.0/fuse.min.js&quot;&gt;&lt;/script&gt;
</code></pre><h2 id="add-the-actual-search-logic">Add the actual search logic</h2>
<p>It&rsquo;s a best practice to add the JavaScript right before the closing body tags. I highly suggest checking out VueJS with Webpack, but in this case a some simple JS inside script tags will do just fine.</p>
<p>I will go over each section to clarify.</p>
<pre tabindex="0"><code class="language-.language-javascript.line-numbers" data-lang=".language-javascript.line-numbers">var app = new Vue({
el: '#app',
data: {
fuse: null,
search: &quot;&quot;,
result: [],
index: []
},
mounted() {
let self = this
let options = {
shouldSort: true,
threshold: 0.6,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [
&quot;title&quot;,
&quot;author&quot;,
&quot;date&quot;,
&quot;content&quot;
]
}
axios.get('/index.json')
.then(function (response) {
self.index = response.data
self.fuse = new Fuse(response.data, options);
self.result = fuse.search(&quot;&quot;);
})
.catch(function (error) {
console.log(error)
})
},
watch: {
search(nval, oval) {
if (nval.length &gt; 0) {
this.result = this.fuse.search(nval)
} else {
this.result = []
}
}
}
})
</code></pre><h3 id="create-the-vue-instance">Create the Vue instance</h3>
<p>When creating a new Vue instance we assign Vue to a DOM element, most of the time an ID on your body tag is used.</p>
<pre tabindex="0"><code class="language-.language-javascript.line-numbers" data-lang=".language-javascript.line-numbers">var app = new Vue({
el: '#app',
...
})
</code></pre><h3 id="create-a-data-object">Create a data object</h3>
<p>This object is accesible across your DOM and Vue instance. Inside functions you can reffer to these with <code>this.*</code>.language-<br>
I initiated some variables like &lsquo;fuse&rsquo; so it can be used inside <em>watch</em> and <em>methods</em>.</p>
<pre tabindex="0"><code class="language-.language-javascript.line-numbers" data-lang=".language-javascript.line-numbers">data: {
fuse: null,
search: &quot;&quot;,
result: [],
index: []
},
</code></pre><h3 id="what-todo-when-everything-is-ready">What todo when everything is ready</h3>
<p>The <code>mounted()</code> function is triggered when everything ready to start processing your custom code. <em>(This function used to name &lsquo;ready()')</em>.<br>
We assign <code>this</code> to <code>self</code> to handle some scope issues in the axios promise.<br>
We polulate some options for FuseJS, note that the keys array is important here. Here we specify which keys of our index.json we want to search.<br>
The index.json file is loaded with AJAX, this way the page should not wait for content that is not required immediately.<br>
When axios retrieves the date we create a Fuse instance (assigned to <code>self.fuse</code> (or <code>this.fuse</code>)).</p>
<pre tabindex="0"><code class="language-.language-javascript.line-numbers" data-lang=".language-javascript.line-numbers">mounted() {
let self = this
let options = {
shouldSort: true,
threshold: 0.6,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [
&quot;title&quot;,
&quot;author&quot;,
&quot;date&quot;,
&quot;content&quot;
]
}
axios.get('/index.json')
.then(function (response) {
self.index = response.data
self.fuse = new Fuse(response.data, options);
self.result = fuse.search(&quot;&quot;);
})
.catch(function (error) {
console.log(error)
})
},
</code></pre><h3 id="when-something-is-entered-inside-the-search-field">When something is entered inside the search field</h3>
<p>We watch for <code>this.search</code> to change, if it changes this function is called. Remember we set <code>search: &quot;&quot;</code> inside our data object?
If the &lsquo;nval&rsquo; (New VALue) is larger than 0 characters we trigger the search function of fuse, which will return a new data set, but filtered.
This dataset is stored inside <code>this.result</code>.language-</p>
<p>If the length of &lsquo;nval&rsquo; changes to 0 characters we hardcode the result to be an empty array (to prevent possible edgecases).</p>
<pre tabindex="0"><code class="language-.language-javascript.line-numbers" data-lang=".language-javascript.line-numbers">watch: {
search(nval, oval) {
if (nval.length &gt; 0) {
this.result = this.fuse.search(nval)
} else {
this.result = []
}
}
}
</code></pre><h3 id="ok-cool-now-how-do-i-showcase-the-results">Ok, cool, now how do I showcase the results?</h3>
<p>Well, it&rsquo;s up to you. The most important parts in this example are:</p>
<ol>
<li>Bind <code>this.search</code> to the input field (with <code>v-model</code>)</li>
<li>Loop through <code>this.result</code> with <code>v-for</code>, it will recreate the li tag &lsquo;for each&rsquo; result item.</li>
<li>Use the result item, reffered as <code>r</code>.</li>
<li>Links are extracted from the result item by the &lsquo;href&rsquo; key and bound to the href attribute. <code>:href=&quot;r.href&quot;</code></li>
</ol>
<p>We use Moment.js to format the default (can be changed) HUGO date format to &lsquo;D&rsquo; (Day), &lsquo;MMM&rsquo; (Month, max 3 characters), &lsquo;YYYY&rsquo; (Full Year).</p>
<pre tabindex="0"><code class="language-.language-markup.line-numbers" data-lang=".language-markup.line-numbers">&lt;div class=&quot;search-wrapper&quot;&gt;
&lt;input type=&quot;text&quot; placeholder=&quot;Search ...&quot; v-model=&quot;search&quot; class=&quot;search&quot;/&gt;
&lt;ul class=&quot;result-items&quot;&gt;
&lt;li v-for=&quot;r of result&quot; class=&quot;result-item&quot;&gt;
&lt;div class=&quot;result-item-wrapper&quot;&gt;
&lt;div class=&quot;result-item-left&quot;&gt;
&lt;span class=&quot;post-date&quot;&gt;
&lt;span class=&quot;post-date-day&quot;&gt;&lt;sup v-text=&quot;moment(r.date).format('D')&quot;&gt;&lt;/sup&gt;&lt;/span&gt;&lt;span class=&quot;post-date-separator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;post-date-month&quot; v-text=&quot;moment(r.date).format('MMM')&quot;&gt;&lt;/span&gt; &lt;span class=&quot;post-date-year&quot; v-text=&quot;moment(r.date).format('YYYY')&quot;&gt;&lt;/span&gt;
&lt;/span&gt;
&lt;template v-if=&quot;r.author&quot;&gt;By &lt;a class=&quot;post-author&quot; v-text=&quot;r.author&quot;&gt;&lt;/a&gt;&lt;/template&gt;
&lt;/div&gt;
&lt;div class=&quot;result-item-left&quot;&gt;
&lt;span class=&quot;nav-item-separator&quot;&gt;//&lt;/span&gt;&lt;a :href=&quot;r.href&quot; v-text=&quot;r.title&quot;&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
</code></pre>
<div class="tags-list">
<span class="dark-red">Tags</span><span class="decorative-marker">//</span>
<a class="post-tag post-tag-vue" href="https://blog.hagfi.sh//tags/vue">vue</a>,
<a class="post-tag post-tag-vuejs" href="https://blog.hagfi.sh//tags/vuejs">vuejs</a>,
<a class="post-tag post-tag-prism" href="https://blog.hagfi.sh//tags/prism">prism</a>,
<a class="post-tag post-tag-prismjs" href="https://blog.hagfi.sh//tags/prismjs">prismjs</a>,
<a class="post-tag post-tag-hugo" href="https://blog.hagfi.sh//tags/hugo">hugo</a>,
<a class="post-tag post-tag-javascript" href="https://blog.hagfi.sh//tags/javascript">javascript</a>,
<a class="post-tag post-tag-js" href="https://blog.hagfi.sh//tags/js">js</a>,
<a class="post-tag post-tag-json" href="https://blog.hagfi.sh//tags/json">json</a>,
</div>
<div class="paging">
<span class="paging-label">More Reading</span>
<div class="paging-newer">
<span class="dark-red">Newer</span><span class="decorative-marker">//</span>
<a class="paging-link" href="/administration/letsencrypt/">Let&#39;s Encrypt</a>
</div>
<div class="paging-older">
<span class="dark-red">Older</span><span class="decorative-marker">//</span>
<a class="paging-link" href="/tools/ncdu/">NCDU: NCurses Disk Usage</a>
</div>
</div>
</section>
<div id="disqus_thread"></div>
<script type="application/javascript">
var disqus_config = function () {
};
(function() {
if (["localhost", "127.0.0.1"].indexOf(window.location.hostname) != -1) {
document.getElementById('disqus_thread').innerHTML = 'Disqus comments not available by default when the website is previewed locally.';
return;
}
var d = document, s = d.createElement('script'); s.async = true;
s.src = '//' + "hagfish" + '.disqus.com/embed.js';
s.setAttribute('data-timestamp', +new Date());
(d.head || d.body).appendChild(s);
})();
</script>
<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
<a href="https://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>
<div class="footer">
<hr class="thin" />
<div class="pure-menu pure-menu-horizontal pure-menu-open">
<ul class="footer-menu">
</ul>
</div>
<p>&copy; 2022. All rights reserved.</p>
</div>
</div>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
fuse: null,
search: "",
result: [],
index: [],
selected: 0
},
mounted() {
let self = this
window.addEventListener("keypress", function(e) {
self.$refs.searchInput.focus()
})
let options = {
shouldSort: true,
threshold: 0.6,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [
"title",
"author",
"date",
"content"
]
}
axios.get('/index.json')
.then(function (response) {
self.index = response.data
self.fuse = new Fuse(response.data, options)
})
.catch(function (error) {
})
},
watch: {
result(nval, oval) {
nval.length > 0 ? this.pointer(0) : this.pointer(-1)
},
search(nval, oval) {
this.result = this.fuse.search(nval)
}
},
methods: {
navigate(val) {
switch (val) {
case 1: if (this.selected < this.result.length - 1) { this.selected++ }; break;
case -1: if (this.selected > 0 ) { this.selected-- }; break;
default: window.location.href = val; break;
}
this.pointer(this.selected)
},
pointer(selected) {
let self = this
if (selected >= 0) {
Vue.nextTick().then(function() {
let height = self.$refs.resultItem[selected].clientHeight
let top = self.$refs.resultItem[selected].getBoundingClientRect().top
let left = self.$refs.resultItem[selected].getBoundingClientRect().left
self.$refs.resultPoint.style.top = (top+height/2)+'px'
self.$refs.resultPoint.style.left = (left-20)+'px'
})
} else {
this.$refs.resultPoint.style.left = '-50px'
return
}
}
}
})
</script>
</body>
</html>