feat($layouts): add fuzzy search

create layout for fuzzy search with app to search fuzzily

closes #83
parent edc2406e
......@@ -42,10 +42,10 @@
<td>Performance Optimized</td>
<td>Page content, favicon and styles <b>load in a single request</b> on all pages. With exception to the BPG image polyfill, all external resources used by the theme are loaded asynchronously and only when necessary. This keeps pages zippy and affords <b>~1 second page loads over 2G</b> when hosted using a <abbr title="Content Delivery Network">CDN</abbr>.</td>
<td>Page content, favicon and styles <b>load in a single request</b> on all pages. Resources loaded asynchronously whenver possible. Responsive images with LQIP out of the box. Users should see a <b>~1 second page loads over 2G</b> when hosted using a <abbr title="Content Delivery Network">CDN</abbr>.</td>
<td>Vertical Scaling</td>
<td>Designed to Scale</td>
<td>After Dark is capable of generating <b>~1000 pages per second</b> thanks to <a target="feature" href="https://gohugo.io/">Hugo</a> and is likely to become faster over time.</td>
......@@ -68,6 +68,10 @@
<td><a href="#post-images">Post Images</a></td>
<td>Increase the visual appeal of your posts by providing a captivating image above your content. After Dark enables configuration-driven post images which are lazy-loaded, responsive and automatically cropped for a consistent look-and-feel across your site.</td>
<td><a href="#fuzzy-search">Fuzzy Search</a></td>
<td>After Dark ships with an in-browser search app built with [Vue](https://vuejs.org/), [Fuse](http://fusejs.io/) and [Mark](https://markjs.io). Use it to quickly find content anywhere your site.</td>
<td><a href="#personalization">Personalization</a></td>
<td>Adjust CSS using purpose-built <a href="#custom-styles">customization file</a>. Choose one of several <a href="#theme-variants">theme variants</a>. Swap in <a href="#favicon">your own favicon</a>. Leverage <a target="features" href="https://gohugo.io/templates/blocks">block templates</a> to quickly extend new custom layouts. And use <a target="features" href="https://hackcss.egoist.moe/dark.html">hack.css</a> flexbox grids and CSS components to add style your site.</td>
......@@ -78,7 +82,7 @@
<td><a href="#content-reuse">Content Reuse</a></td>
<td>Sometimes plan markdown isn't enough to build engaging page content. For this reason After Dark provides a number of customizable partials and shortcodes for adding things like blockquotes, figure elements, GIFs with sound and <a target="feature" href="https://hackcss.egoist.moe/">hackcss components</a> to your posts, pages and layouts. Mix and match to create truly unique experiences.</td>
<td>Sometimes plan markdown isn't enough to build engaging page content. For this reason After Dark provides a number of reusable code snippets and shortcodes for adding things blockquotes, figure elements, coubs, videos, <a target="feature" href="https://hackcss.egoist.moe/">hackcss components</a> and more to your pages and posts. Use them to create completely custom layouts or simply spice up an old page.</td>
<td><a href="#related-content">Related Content</a></td>
......@@ -466,6 +470,34 @@ With the following front matter specified in `index.md`:
That's it! After Dark does the rest.
### Fuzzy Search
Find content site-wide in the blink of an eye. JavaScript fuzzy search is at your fingertips. To use it simply create a section called `search` using the After Dark search layout like so:
└── content
   └── search
      └── _index.md
With `_index.md` like:
title = "Search"
layout = "search"
noindex = true
Then simply navigate to the `/search/` URL on your site and let the fun begin.
**Tip:** Consider enabling the After Dark [section menu](#section-menu) feature if you haven't done so already to add search visibility for your users.
While deep link searches are supported, please note Fuzzy Search will only return results for [Regular Pages](https://gohugo.io/variables/site/#site-variables-list) and intentionally omits any page tagged for [index blocking](#index-blocking).
In other words it's easy to find stuff. But only if you want it to be found.
### Markdown Output
Gain more control over markdown conversion to HTML. By modifying the markdown processor settings you can take advantage of [Blackfriday](https://github.com/russross/blackfriday) features not enabled by default.
{{- $.Scratch.Add "index" slice -}}
{{- range .Site.RegularPages -}}
{{- if ne .Params.noindex true -}}
{{- $.Scratch.Add "index" (dict "title" .Title "tags" .Params.tags "categories" .Params.categories "contents" .Plain "url" .RelPermalink "summary" .Summary) -}}
{{- end -}}
{{- end -}}
{{- $.Scratch.Get "index" | jsonify -}}
{{ define "title" -}}
{{ .Title }} | {{ .Site.Title }}
{{- end }}
{{ define "header" }}
{{ partial "menu.html" . }}
{{ end }}
{{ define "main" }}
<h1>{{ .Title }}</h1>
<div id="search-app">
Aw-shucks! Search requires use of JavaScript to function properly.
<form v-on:submit.prevent class="form" action="{{ "search" | absURL }}">
<fieldset class="form-group">
<input @keyup.enter="submit" v-model="query" autofocus id="query" name="s" type="search" class="form-control" maxlength="32" autocomplete="off" tabindex="0">
<section v-if="results.length">
<p><i>Showing results for “{ resultsForSearch }”.</i></p>
<div id="search-results">
<article v-for="result in results" itemscope itemtype="http://schema.org/CreativeWork">
<header itemprop="name">
<h2 itemprop="name"><a :href="result.item.url">{ result.item.title }</a></h2>
<div v-html=result.item.summary itemprop="description"></div>
<nav class="readmore"><a itemprop="url" :href="result.item.url">Read More&nbsp;&raquo;</a></nav>
{{ end }}
{{ define "footer" }}
{{ partial "powered-by.html" . }}
<script src="/js/vue.min.js"></script>
<script src="/js/lodash.custom.min.js"></script>
<script src="/js/fuse.min.js"></script>
<script src="/js/mark.min.js"></script>
(function (window, document, undefined) {
"use strict"
const getQueryByParam = param => decodeURIComponent(
(location.search.split(param + '=')[1] || '').split('&')[0]
).replace(/\+/g, ' ')
const queryParam = 's'
const snippetSize = 60
const selectors = {
appContainer: '#search-app',
resultContainer: '#search-results',
searchInput: '#query'
const fuseOpts = {
shouldSort: true,
tokenize: true,
matchAllTokens: true,
includeScore: true,
includeMatches: true,
keys: [
{ name: "title", weight: 0.8 },
{ name: "contents", weight: 0.5 },
{ name: "tags", weight: 0.3 },
{ name: "categories", weight: 0.3 }
const searchInput = document.querySelector(selectors.searchInput)
const searchQuery = searchInput.value = getQueryByParam(queryParam)
const fuse = new Fuse([], fuseOpts)
window.fetch('/index.json').then(response => {
response.text().then(searchData => {
if (searchQuery) search(searchQuery)
const getUrl = (query) => {
const encodedQuery = encodeURIComponent(query)
const url = {{ .URL }}
return (encodedQuery)
? `${url}?${queryParam}=${encodedQuery}`
: url
let mark = new Mark(
const app = new Vue({
delimiters: ['{', '}'],
el: selectors.appContainer,
data: {
fuse: null,
results: [],
query: getQueryByParam(queryParam),
resultsForSearch: getQueryByParam(queryParam)
mounted () {
this.fuse = fuse
window.onpopstate = (evt) => {
this.query = evt.state.query
watch: {
query () {
{query: this.query},
beforeUpdate: function () {
updated: function () {
this.$nextTick(function () {
mark = new Mark(
methods: {
executeSearch: _.debounce(function () {
const trimmedQuery = this.query.trim()
this.resultsForSearch = trimmedQuery
this.results = (trimmedQuery)
? this.fuse.search(trimmedQuery)
: []
}, 250)
const search = query => {
app.results = fuse.search(query)
})(window, document)
{{ end }}
......@@ -18,7 +18,8 @@
-webkit-filter: blur(0);
filter: blur(0);
.muted {
.hack .help-block {
color: #e0e0e070;
.hack .readmore {
......@@ -93,6 +94,12 @@ html {
.hack pre {
font-size: 17px;
.hack .form input,
.hack .form textarea,
.hack .form button,
.hack .form label {
font-size: 1rem;
article [itemprop="description"] {
margin-bottom: 20px;
margin-top: 20px;
This diff is collapsed.
......@@ -2,7 +2,7 @@
"name": "after-dark",
"version": "3.7.0",
"description": "A retro dark theme for Hugo.",
"author": "Josh Habdas <jhabdas@pm.me> (https://habd.as/)",
"author": "Josh Habdas <jhabdas@protonmail.com> (https://habd.as/)",
"keywords": [
......@@ -12,7 +12,11 @@
"repository": "comfusion/after-dark",
"scripts": {
"update:lazysizes": "npm up lazysizes && cp -i node_modules/lazysizes/lazysizes.min.js static/js",
"update:lodash:custom": "./node_modules/.bin/lodash include=debounce -p -o static/js/lodash.custom.min.js",
"update:smoothscroll": "npm up smoothscroll && cp -i node_modules/smoothscroll-polyfill/dist/smoothscroll.js static/js",
"update:fuse": "npm up fuse.js && cp -i node_modules/fuse.js/dist/fuse.min.js static/js",
"update:vue": "npm up vue && cp -i node_modules/vue/dist/vue.min.js static/js",
"update:mark": "npm up mark.js && cp -i node_modules/mark.js/dist/mark.min.js static/js",
"update:hackcss": "npm up hackcss && cat node_modules/hack/dist/hack.css > static/css/critical-vendor.css && cat node_modules/hack/dist/dark.css >> static/css/critical-vendor.css",
"test": "while true; do head -n 100 /dev/urandom; sleep 0.1; done | hexdump -C | grep 'ca fe'",
"release": "standard-version"
......@@ -21,9 +25,13 @@
"atom-one-pgyments": "^1.0.0",
"hack": "^0.8.1",
"lazysizes": "^4.0.1",
"smoothscroll-polyfill": "^0.4.3"
"smoothscroll-polyfill": "^0.4.3",
"fuse.js": "^3.2.0",
"mark.js": "^8.11.1",
"vue": "^2.5.16"
"devDependencies": {
"lodash-cli": "^4.17.5",
"standard-version": "^4.0.0"
"license": "WTFPL"
This diff is collapsed.
* @license
* Lodash (Custom Build) lodash.com/license | Underscore.js 1.8.3 underscorejs.org/LICENSE
* Build: `lodash include="debounce" -p -o static/js/lodash.custom.min.js`
;(function(){function e(){}function t(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)}function n(e){return null!=e&&typeof e=="object"}function o(e){var t;if(!(t=typeof e=="symbol")&&(t=n(e))){if(null==e)e=e===i?"[object Undefined]":"[object Null]";else if(v&&v in Object(e)){t=d.call(e,v);var o=e[v];try{e[v]=i;var r=true}catch(e){}var u=j.call(e);r&&(t?e[v]=o:delete e[v]),e=u}else e=j.call(e);t="[object Symbol]"==e}return t}function r(e){if(typeof e=="number")return e;if(o(e))return u;
if(t(e)&&(e=typeof e.valueOf=="function"?e.valueOf():e,e=t(e)?e+"":e),typeof e!="string")return 0===e?e:+e;e=e.replace(f,"");var n=l.test(e);return n||a.test(e)?s(e.slice(2),n?2:8):c.test(e)?u:+e}var i,u=NaN,f=/^\s+|\s+$/g,c=/^[-+]0x[0-9a-f]+$/i,l=/^0b[01]+$/i,a=/^0o[0-7]+$/i,s=parseInt,b=typeof self=="object"&&self&&self.Object===Object&&self,p=typeof global=="object"&&global&&global.Object===Object&&global||b||Function("return this")(),y=(b=typeof exports=="object"&&exports&&!exports.nodeType&&exports)&&typeof module=="object"&&module&&!module.nodeType&&module,m=Object.prototype,d=m.hasOwnProperty,j=m.toString,v=(m=p.Symbol)?m.toStringTag:i,g=Math.max,O=Math.min,x=function(){
return p.Date.now()};e.debounce=function(e,n,o){function u(t){var n=s,o=b;return s=b=i,j=t,y=e.apply(o,n)}function f(e){var t=e-d;return e-=j,d===i||t>=n||0>t||h&&e>=p}function c(){var e=x();if(f(e))return l(e);var t,o=setTimeout;t=e-j,e=n-(e-d),t=h?O(e,p-t):e,m=o(c,t)}function l(e){return m=i,T&&s?u(e):(s=b=i,y)}function a(){var e=x(),t=f(e);if(s=arguments,b=this,d=e,t){if(m===i)return j=e=d,m=setTimeout(c,n),v?u(e):y;if(h)return m=setTimeout(c,n),u(d)}return m===i&&(m=setTimeout(c,n)),y}var s,b,p,y,m,d,j=0,v=false,h=false,T=true;
if(typeof e!="function")throw new TypeError("Expected a function");return n=r(n)||0,t(o)&&(v=!!o.leading,p=(h="maxWait"in o)?g(r(o.maxWait)||0,n):p,T="trailing"in o?!!o.trailing:T),a.cancel=function(){m!==i&&clearTimeout(m),j=0,s=d=b=m=i},a.flush=function(){return m===i?y:l(x())},a},e.isObject=t,e.isObjectLike=n,e.isSymbol=o,e.now=x,e.toNumber=r,e.VERSION="4.17.5",typeof define=="function"&&typeof define.amd=="object"&&define.amd?(p._=e, define(function(){return e})):y?((y.exports=e)._=e,b._=e):p._=e;
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
......@@ -4,13 +4,9 @@ licenselink = "https://github.com/comfusion/after-dark/blob/master/COPYING"
description = "A retro dark theme for Hugo."
homepage = "https://comfusion.github.io/after-dark/"
tags = [
"custom themes",
features = [
......@@ -21,8 +17,11 @@ features = [
"Google Analytics",
"BPG Image Support",
"Fuzzy Search",
"Lazy Loading",
"Responsive Images",
"Low-Quality Image Placeholders",
"OpenGraph Data",
"Schema Structured Data",
"Reading time",
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment