diff --git a/README.md b/README.md index 8e71100..42d9933 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Sass is processed with Hugo pipes. To make using npm optional I have added the s * [Configuration](#configuration) * [Customise](#customise) * [Multilingual](#multilingual) +* [Search](#search) * [Contact form](#contact-form) * [Dates](#dates) * [Podcast](#podcast) @@ -52,7 +53,7 @@ Sass is processed with Hugo pipes. To make using npm optional I have added the s * Podcast * Responsive design * RSS and JSON feeds with full content -* Search with DuckDuckGo +* Search with FlexSearch.js * Sub theme support (Theme Components) * Umbrella JS 3 (tiny jQuery replacement, 2.5kB when gzipped) @@ -179,8 +180,7 @@ params: poweredby: true # Show powered by hugo in footer realfavicongenerator: # If full set of favicons, site manifest etc. exists, default false. safariPinnedColor: # Hex color value, used in meta tags, default "#000000". - search: true # Site search with DuckDuckGo. - searchSize: 20 # Search field size, default 20. + searchLimit: 20 # Max number of search hits, default 20. sidebar: true # Show a sidebar to the right submitted: true # Show author and date information for a post. themeColor: # Hex color value, used in meta tags, default "#ffffff". @@ -293,6 +293,33 @@ languages: ``` +## Search + +Built in integration with the excellent [FlexSearch.js](https://github.com/nextapps-de/flexsearch). A fast full text search that reads a JSON file created by Hugo to index and search the site. + +Here is the three steps needed to create a search page. + +1. Add a new output format in your configuration file. + ``` + outputFormats: + SearchIndex: + mediaType: "application/json" + baseName: "searchindex" + isPlainText: true + notAlternative: true + ``` +2. Add the new output format to output setting for "home". + ``` + outputs: + home: ["HTML", "SearchIndex", "[other formats you need]"] + ``` +3. Add the shortcode `{{< search >}}` to a page. Also set `searchform: true` in the front matter for that page so the search and flexsearch js files gets loaded. + +Your search page will now have a search field where all the posts of the site can be searched. + +The only setting is "searchLimit" that defaults to 20. + + ## Contact form If your server support php with the mail() command (very common) you can use the included contact form feature to get a contact form for your site. diff --git a/assets/js/contact.js b/assets/js/contact.js index 1e4c4c0..b390368 100644 --- a/assets/js/contact.js +++ b/assets/js/contact.js @@ -15,8 +15,6 @@ // Display the hidden form. $contact_form.removeClass('hidden'); - // Remove the "no javascript" messages - $('.contact-no-js').remove(); // Wait for a mouse to move, indicating they are human. $('body').on('mousemove', function () { diff --git a/assets/js/jq_versions/contact.js b/assets/js/jq_versions/contact.js index bfaced7..b243ace 100644 --- a/assets/js/jq_versions/contact.js +++ b/assets/js/jq_versions/contact.js @@ -15,8 +15,6 @@ // Display the hidden form. $contact_form.removeClass('hidden'); - // Remove the "no javascript" messages - $('.contact-no-js').detach(); // Wait for a mouse to move, indicating they are human. $('body').mousemove(function () { diff --git a/assets/js/lib/flexsearch.min.js b/assets/js/lib/flexsearch.min.js new file mode 100644 index 0000000..984d8c6 --- /dev/null +++ b/assets/js/lib/flexsearch.min.js @@ -0,0 +1,42 @@ +/* + FlexSearch v0.6.30 + Copyright 2019 Nextapps GmbH + Author: Thomas Wilkerling + Released under the Apache 2.0 Licence + https://github.com/nextapps-de/flexsearch +*/ +'use strict';(function(K,R,w){let L;(L=w.define)&&L.amd?L([],function(){return R}):(L=w.modules)?L[K.toLowerCase()]=R:"object"===typeof exports?module.exports=R:w[K]=R})("FlexSearch",function ma(K){function w(a,c){const b=c?c.id:a&&a.id;this.id=b||0===b?b:na++;this.init(a,c);fa(this,"index",function(){return this.a?Object.keys(this.a.index[this.a.keys[0]].c):Object.keys(this.c)});fa(this,"length",function(){return this.index.length})}function L(a,c,b,d){this.u!==this.g&&(this.o=this.o.concat(b),this.u++, +d&&this.o.length>=d&&(this.u=this.g),this.u===this.g&&(this.cache&&this.j.set(c,this.o),this.F&&this.F(this.o)));return this}function S(a){const c=B();for(const b in a)if(a.hasOwnProperty(b)){const d=a[b];F(d)?c[b]=d.slice(0):G(d)?c[b]=S(d):c[b]=d}return c}function W(a,c){const b=a.length,d=O(c),e=[];for(let f=0,h=0;f=h&&(a=a[g-(e+.5>>0)],a=a[b]||(a[b]=[]), +a[a.length]=d);return e}function ba(a,c){if(a){const b=Object.keys(a);for(let d=0,e=b.length;da?1:a?-1:0}function pa(a,c){a=a[M];c=c[M];return ac?1:0}function oa(a,c){const b=M.length;for(let d=0;dc?1:0}function T(a,c,b){return a?{page:a,next:c?""+c:null,result:b}:b}function ha(a,c,b,d,e,f,h){let g,k=[];if(!0===b){b="0";var l=""}else l=b&&b.split(":");const p=a.length;if(1h&&(l=0),l=l||0,g=l+c,g=this.m.length&&(this.C=0),this.m[this.C].postMessage({add:!0,id:a, +content:c}),this.c[f]=""+this.C,b&&b(),this;if(!e){if(this.async&&"function"!==typeof importScripts){let t=this;f=new Promise(function(v){setTimeout(function(){t.add(a,c,null,d,!0);t=null;v()})});if(b)f.then(b);else return f;return this}if(b)return this.add(a,c,null,d,!0),b(),this}c=this.encode(c);if(!c.length)return this;b=this.f;e=O(b)?b(c):c.split(this.split);this.filter&&(e=W(e,this.filter));const n=B();n._ctx=B();const m=e.length,u=this.threshold,q=this.depth,A=this.b,z=this.i,y=this.D;for(let t= +0;tp;x--)l=h.substring(p,x),V(z,n,l,a,v,k,u,A-1)}break;default:if(g=V(z,n,h,a,1,k,u,A-1),q&&1=u)for(g=n._ctx[h]||(n._ctx[h]=B()),h=this.h[h]||(this.h[h]=ia(A-(u||0))),k=t-q,l=t+q+1,0>k&&(k=0),l> +m&&(l=m);kh;d--)e=g[d-1],g[d]=e,f[e]=d;g[h]=c;f[c]=h}}}return b};return a}();return w}(function(){const K={},R="undefined"!==typeof Blob&& +"undefined"!==typeof URL&&URL.createObjectURL;return function(w,L,S,W,P){S=R?URL.createObjectURL(new Blob(["("+S.toString()+")()"],{type:"text/javascript"})):w+".min.js";w+="-"+L;K[w]||(K[w]=[]);K[w][P]=new Worker(S);K[w][P].onmessage=W;return K[w][P]}}()),this); diff --git a/assets/js/jquery.min.js b/assets/js/lib/jquery.min.js similarity index 100% rename from assets/js/jquery.min.js rename to assets/js/lib/jquery.min.js diff --git a/assets/js/jquery.slim.min.js b/assets/js/lib/jquery.slim.min.js similarity index 100% rename from assets/js/jquery.slim.min.js rename to assets/js/lib/jquery.slim.min.js diff --git a/assets/js/umbrella.js b/assets/js/lib/umbrella.js similarity index 100% rename from assets/js/umbrella.js rename to assets/js/lib/umbrella.js diff --git a/assets/js/umbrella.min.js b/assets/js/lib/umbrella.min.js similarity index 100% rename from assets/js/umbrella.min.js rename to assets/js/lib/umbrella.min.js diff --git a/assets/js/search.js b/assets/js/search.js new file mode 100644 index 0000000..b0be3d2 --- /dev/null +++ b/assets/js/search.js @@ -0,0 +1,75 @@ +/** + * @file + * A JavaScript file for flexsearch. + */ + +(function () { + + 'use strict'; + + const flexSearch = new FlexSearch({ + doc: { + id: 'id', + field: ['title','tags','content','date'], + store: ['title','summary','date','permalink'] + } + }); + + function showResults(items) { + const template = document.querySelector('template').content; + const fragment = document.createDocumentFragment(); + + const results = document.querySelector('.search-results'); + results.textContent = ''; + + for (const item of items) { + const result = template.cloneNode(true); + const a = result.querySelector('a'); + const time = result.querySelector('time'); + const content = result.querySelector('.content'); + a.innerHTML = item.title; + a.href = item.permalink; + time.innerText = item.date; + content.innerHTML = item.summary; + fragment.appendChild(result); + } + results.appendChild(fragment); + } + + function doSearch () { + const query = document.querySelector('.search-text').value.trim(); + const results = flexSearch.search({ + query: query, + limit: {{ .Site.Params.searchLimit | default 20 }} + }); + showResults(results); + } + + function enableUI () { + const searchform = document.querySelector('.search-form'); + searchform.addEventListener('submit', function (e) { + e.preventDefault(); + doSearch(); + }) + searchform.addEventListener('keyup', function () { + doSearch(); + }) + document.querySelector('.search-loading').classList.add('hidden'); + document.querySelector('.search-input').classList.remove('hidden'); + document.querySelector('.search-text').focus(); + } + + function buildIndex () { + document.querySelector('.search-loading').classList.remove('hidden'); + fetch('/searchindex.json') + .then(function (response) { + return response.json(); + }) + .then(function (data) { + flexSearch.add(data); + }); + } + + buildIndex(); + enableUI(); +})(); diff --git a/assets/sass/_zen.scss b/assets/sass/_zen.scss index 61d3ca8..dd35c02 100644 --- a/assets/sass/_zen.scss +++ b/assets/sass/_zen.scss @@ -253,12 +253,13 @@ figure { // Search -.search-form { - @include padding(.5 0); +.search-input { + @include margin(1 0); } .search-text { @include type-layout(l, 1); + max-width: 80%; } // Form diff --git a/i18n/de.toml b/i18n/de.toml index 2ddcb3e..41c20e4 100644 --- a/i18n/de.toml +++ b/i18n/de.toml @@ -23,10 +23,13 @@ other = "Zuletzt geändert:" other = "Erstellt:" [search_placeholder] -other = "Suche" +other = "Suchbegriffe hier eingeben." [search_title] -other = "Suchbegriffe hier eingeben." +other = "Suche" + +[search_loading] +other = "Loading search index…" [string_at] other = "auf" @@ -46,7 +49,7 @@ other = "auf" [string_recent_content] other = "Aktuelle Inhalte" -[contact_js] +[js_required] other = "Javascript muss aktiviert sein um dieses Kontaktformular zu nutzen." [contact_submitted] diff --git a/i18n/en.toml b/i18n/en.toml index 9616fdc..41d38d6 100644 --- a/i18n/en.toml +++ b/i18n/en.toml @@ -23,10 +23,13 @@ other = "Updated:" other = "Created:" [search_placeholder] -other = "Search" +other = "Enter the terms you wish to search for." [search_title] -other = "Enter the terms you wish to search for." +other = "Search" + +[search_loading] +other = "Loading search index…" [string_at] other = "at" @@ -46,8 +49,8 @@ other = "on" [string_recent_content] other = "Recent content" -[contact_js] -other = "You must have Javascript enabled to use this contact form." +[js_required] +other = "You must have Javascript enabled to use this function." [contact_submitted] other = "Your message was sent." diff --git a/i18n/fr.toml b/i18n/fr.toml index e4c0a50..8da1296 100644 --- a/i18n/fr.toml +++ b/i18n/fr.toml @@ -23,10 +23,13 @@ other = "Dernière modification:" other = "Établi:" [search_placeholder] -other = "Rechercher" +other = "Entrez les termes de la recherche." [search_title] -other = "Entrez les termes de la recherche." +other = "Rechercher" + +[search_loading] +other = "Loading search index…" [string_at] other = "à" @@ -46,7 +49,7 @@ other = "sur" [string_recent_content] other = "Contenu récent" -[contact_js] +[js_required] other = "Javascript doit être activé pour utiliser ce formulaire de contact." [contact_submitted] diff --git a/i18n/pt.toml b/i18n/pt.toml index ee9af9c..02d4c19 100644 --- a/i18n/pt.toml +++ b/i18n/pt.toml @@ -23,10 +23,14 @@ other = "Última modificação:" other = "Criado:" [search_placeholder] -other = "Procurar" +other = "Digite os termos que você deseja procurar" [search_title] -other = "Digite os termos que você deseja procurar" +other = "Procurar" + + +[search_loading] +other = "Loading search index…" [string_at] other = "em" @@ -46,7 +50,7 @@ other = "em" [string_recent_content] other = "Conteúdo recente" -[contact_js] +[js_required] other = "Você deve ter o Javascript habilitado para usar este formulário de contato" [contact_submitted] diff --git a/i18n/sv.toml b/i18n/sv.toml index f035847..469ceb8 100644 --- a/i18n/sv.toml +++ b/i18n/sv.toml @@ -23,10 +23,13 @@ other = "Uppdaterad:" other = "Skapad:" [search_placeholder] -other = "Sök" +other = "Ange de ord du vill söka efter." [search_title] -other = "Ange de villkor du vill söka efter." +other = "Sök" + +[search_loading] +other = "Laddar sökindex…" [string_at] other = "på" @@ -46,8 +49,8 @@ other = "på" [string_recent_content] other = "Senaste innehåll" -[contact_js] -other = "Du behöver ha Javascript aktiverat för att använda detta kontaktformulär." +[js_required] +other = "Du behöver ha Javascript aktiverat för att använda denna funktion." [contact_submitted] other = "Ditt meddelande har skickats." diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html index 00b8bcf..cf98409 100644 --- a/layouts/_default/baseof.html +++ b/layouts/_default/baseof.html @@ -21,7 +21,6 @@
{{ if .Site.IsMultiLingual }}{{ partial "language-selector.html" . }}{{ end -}} -{{ if $.Param "search" }}{{ partialCached "search.html" . }}{{ end -}}
diff --git a/layouts/home.searchindex.json b/layouts/home.searchindex.json new file mode 100644 index 0000000..cc83383 --- /dev/null +++ b/layouts/home.searchindex.json @@ -0,0 +1,5 @@ +{{- $.Scratch.Add "searchindex" slice -}} +{{- range $index, $element := .Site.RegularPages -}} + {{- $.Scratch.Add "searchindex" (dict "id" $index "title" $element.Title "permalink" $element.RelPermalink "tags" (delimit ($element.Params.tags | default "") " ") "content" $element.Plain "summary" $element.Summary "date" ($element.Date.Format ($.Param "dateformat" | default "2 January, 2006"))) -}} +{{- end -}} +{{- $.Scratch.Get "searchindex" | jsonify -}} diff --git a/layouts/partials/mobilemenu.html b/layouts/partials/mobilemenu.html index 96f9088..c79ee70 100644 --- a/layouts/partials/mobilemenu.html +++ b/layouts/partials/mobilemenu.html @@ -4,7 +4,6 @@
{{ if .Site.IsMultiLingual }}{{ partial "language-selector.html" . }}{{ end }} - {{ if $.Param "search" }}{{ partialCached "search.html" . }}{{ end }}