Half-Elf on Tech

Thoughts From a Professional Lesbian

Author: Ipstenu (Mika Epstein)

  • Hugo and Lunr – Client Side Searching

    Hugo and Lunr – Client Side Searching

    I use Hugo on a static website that has few updates but still needs a bit of maintenance. With a few thousand pages, it also needs a search. For a long time, I was using a Google Custom Search, but I’m not the biggest Google fan and they insert ads now, so I needed a new solution.

    Search is the Worst Part

    Search is the worst thing about static sites. Scratch that. Search is the worst part about any site. We all bag on WordPress’ search being terrible, but anyone who’s attempted to install and manage anything like ElasticSearch knows that WordPress’ search is actually pretty good. It’s just limited. And by contrast, the complicated world of search is, well, complicated.

    That’s the beauty of many CMS tools like WordPress and Drupal and MediaWiki is that they have a rudimentary and perfectly acceptable search built in. And it’s the headache of static tools like Jekyll and Hugo. They simply don’t have it.

    Lunr

    If you don’t want to use third-party services, and are interested in self hosting your solution, then you’re going to have to look at a JavaScript solution. Mine was Lunr.js, a fairly straightforward tool that searched a JSON file for the items.

    There are pros and cons to this. Having it all in javascript means the load on my server is pretty low. At the same time I have to generate the JSON file somehow every time. In addition, every time someone goes to the search page, they have to download that JSON file, which can get pretty big. Mine’s 3 megs for 2000 or so pages. That’s something I need to keep in mind.

    This is, by the way, the entire reason I made that massive JSON file the other day.

    To include Lunrjs in your site, download the file and put it in your /static/ folder however you want. I have it at /static/js/lunr.js next to my jquery.min.js file. Now when you build your site, the JS file will be copied into place.

    The Code

    Since this is for Hugo, it has two steps. The first is the markdown code to make the post and the second is the template code to do the work.

    Post: Markdown

    The post is called search.md and this is the entirety of it:

    ---
    layout: search
    title: Search Results
    permalink: /search/
    categories: ["Search"]
    tags: ["Index"]
    noToc: true
    ---
    

    Yep. That’s it.

    Template: HTML+GoLang+JS

    I have a template file in layouts/_default/ called search.html and that has all the JS code as well as everything else. This is shamelessly forked from Seb’s example code.

    {{ partial "header.html" . }}
    
    	{{ .Content }}
    
    	<h3>Search:</h3>
    	<input id="search" type="text" id="searchbox" placeholder="Just start typing...">
    
    	<h3>Results:</h3>
    	<ul id="results"></ul>
    
    	<script type="text/javascript" src="/js/lunr.js"></script>
    	<script type="text/javascript">
    	var lunrIndex, $results, pagesIndex;
    
    	function getQueryVariable(variable) {
    		var query = window.location.search.substring(1);
    		var vars = query.split('&');
    
    		for (var i = 0; i < vars.length; i++) {
    			var pair = vars[i].split('=');
    
    			if (pair[0] === variable) {
    				return decodeURIComponent(pair[1].replace(/\+/g, '%20'));
    			}
    		}
    	}
    
    	var searchTerm = getQueryVariable('query');
    
    	// Initialize lunrjs using our generated index file
    	function initLunr() {
    		// First retrieve the index file
    		$.getJSON("/index.json")
    			.done(function(index) {
    				pagesIndex = index;
    				console.log("index:", pagesIndex);
    				lunrIndex = lunr(function() {
    					this.field("title", { boost: 10 });
    					this.field("tags", { boost: 5 });
    					this.field("categories", { boost: 5 });
    					this.field("content");
    					this.ref("uri");
    
    					pagesIndex.forEach(function (page) {
    						this.add(page)
    					}, this)
    				});
    			})
    			.fail(function(jqxhr, textStatus, error) {
    				var err = textStatus + ", " + error;
    				console.error("Error getting Hugo index flie:", err);
    			});
    	}
    
    	// Nothing crazy here, just hook up a listener on the input field
    	function initUI() {
    		$results = $("#results");
    		$("#search").keyup(function() {
    			$results.empty();
    
    			// Only trigger a search when 2 chars. at least have been provided
    			var query = $(this).val();
    			if (query.length < 2) {
    				return;
    			}
    
    			var results = search(query);
    
    			renderResults(results);
    		});
    	}
    
    	/**
    	 * Trigger a search in lunr and transform the result
    	 *
    	 * @param  {String} query
    	 * @return {Array}  results
    	 */
    	function search(query) {
    		return lunrIndex.search(query).map(function(result) {
    				return pagesIndex.filter(function(page) {
    					return page.uri === result.ref;
    				})[0];
    			});
    	}
    
    	/**
    	 * Display the 10 first results
    	 *
    	 * @param  {Array} results to display
    	 */
    	function renderResults(results) {
    		if (!results.length) {
    			return;
    		}
    
    		// Only show the ten first results
    		results.slice(0, 100).forEach(function(result) {
    			var $result = $("<li>");
    			$result.append($("<a>", {
    				href: result.uri,
    				text: "» " + result.title
    			}));
    			$results.append($result);
    		});
    	}
    
    	// Let's get started
    	initLunr();
    
    	$(document).ready(function() {
    		initUI();
    	});
    	</script>
    {{ partial "footer.html" . }}
    

    It’s important to note you will also need to call jQuery but I do that in my header.html file since I have a bit of jQuery I use on every page. If you don’t, then remember to include it up by <script type="text/javascript" src="/js/lunr.js"></script> otherwise nothing will work.

    Caveats

    If you have a large search file, this will make your search page slow to load.

    Also I don’t know how to have a form on one page trigger the search on another, but I’m making baby steps in my javascripting.

  • Hugo Making JSON

    Hugo Making JSON

    While it rhymes with bacon, it’s not at all the same.

    There are a lot of reasons you might want a JSON file output from your static site (I like Hugo). Maybe you’re using Hugo to build out the backend of an API. Maybe you want to have it include a search function. Today I’m going to show you how to have a JSON file created with a complete site archive. The end goal of this example is to have a searchable JSON file that you can use with Lunrjs or Solarjs or anything else of that ilk.

    The Old Way: Node

    Since I was initially doing this to integrate Hugo with Lunr.js, I spent some time wondering how I could make a JSON file and I ran into Lunr Hugo, a fork of Hugo Lunr but with YAML support (which I needed). I actually use a private fork of that, because I wanted to change what it saved, but this is enough to get everyone started.

    To use it, you install it via Node:

    npm install lunr-hugo
    

    Then you add the scripts to your Node package file (normally called package.json):

      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "index": "lunr-hugo -i \"site/content/posts/**\" -o site/static/js/search.json"
      },
    

    Change the value of “site/content/” as you see fit. Once installed you can build the index by typing npm run index and it makes the file in the right location.

    The obvious downside to this is I have to run it outside of my normal build process.

    Another Old Way: Grunt

    This idea come from Seb, one of the lead developers for Hugo, and he uses a Grunt script to do this. First you have to install node and things via this command:

    npm install --save-dev grunt string toml conzole

    Next you make a Gruntfile.js file like this:

    var toml = require("toml");
    var S = require("string");
    
    var CONTENT_PATH_PREFIX = "site/content";
    
    module.exports = function(grunt) {
    
        grunt.registerTask("lunr-index", function() {
    
            grunt.log.writeln("Build pages index");
    
            var indexPages = function() {
                var pagesIndex = [];
                grunt.file.recurse(CONTENT_PATH_PREFIX, function(abspath, rootdir, subdir, filename) {
                    grunt.verbose.writeln("Parse file:",abspath);
                    pagesIndex.push(processFile(abspath, filename));
                });
    
                return pagesIndex;
            };
    
            var processFile = function(abspath, filename) {
                var pageIndex;
    
                if (S(filename).endsWith(".html")) {
                    pageIndex = processHTMLFile(abspath, filename);
                } else {
                    pageIndex = processMDFile(abspath, filename);
                }
    
                return pageIndex;
            };
    
            var processHTMLFile = function(abspath, filename) {
                var content = grunt.file.read(abspath);
                var pageName = S(filename).chompRight(".html").s;
                var href = S(abspath)
                    .chompLeft(CONTENT_PATH_PREFIX).s;
                return {
                    title: pageName,
                    href: href,
                    content: S(content).trim().stripTags().stripPunctuation().s
                };
            };
    
            var processMDFile = function(abspath, filename) {
                var content = grunt.file.read(abspath);
                var pageIndex;
                // First separate the Front Matter from the content and parse it
                content = content.split("+++");
                var frontMatter;
                try {
                    frontMatter = toml.parse(content[1].trim());
                } catch (e) {
                    conzole.failed(e.message);
                }
    
                var href = S(abspath).chompLeft(CONTENT_PATH_PREFIX).chompRight(".md").s;
                // href for index.md files stops at the folder name
                if (filename === "index.md") {
                    href = S(abspath).chompLeft(CONTENT_PATH_PREFIX).chompRight(filename).s;
                }
    
                // Build Lunr index for this page
                pageIndex = {
                    title: frontMatter.title,
                    tags: frontMatter.tags,
                    href: href,
                    content: S(content[2]).trim().stripTags().stripPunctuation().s
                };
    
                return pageIndex;
            };
    
            grunt.file.write("site/static/js/lunr/PagesIndex.json", JSON.stringify(indexPages()));
            grunt.log.ok("Index built");
        });
    };
    

    Take note of where it’s saving the files. site/static/js/lunr/PagesIndex.json That’s works for Seb because his set setup has everything Hugo in a /site/ folder.

    To build the file, type grunt lunr-index and off you go.

    The New Way: Output Formats

    All of that sounded really annoying, right? I mean, it’s great but you have structure your site to separate Hugo from the Node folders, and you have to run all those steps outside of Hugo.

    Well there’s good news. You can have this all automatically done if you have Hugo 0.20.0 or greater. In the recent releases, Hugo introduced Output Formats. The extra formats let you spit out your code with RSS feeds, AMP, or (yes) JSON formatting automatically.

    In this example, since I only want to make a master index file with everything, I can do it by telling Hugo that I want my home page, and only my home page, to have a JSON output. In order to do this, I put the following in my config.toml file:

    [outputs]
    	home = [ "HTML", "JSON"]
    	page = [ "HTML"]
    

    If I wanted to have it on more pages, I could do that too. I don’t.

    Next I made a file in my layouts folder called index.json:

    {{- $.Scratch.Add "index" slice -}}
    {{- range where .Site.Pages "Type" "not in"  (slice "page" "json") -}}
    {{- $.Scratch.Add "index" (dict "uri" .Permalink "title" .Title "content" .Plain "tags" .Params.tags "categories" .Params.tags) -}}
    {{- end -}}
    {{- $.Scratch.Get "index" | jsonify -}}
    

    To generate the file, just run a build and it makes a file called index.json in the site root.

    How do you statically build JSON Files?

    Do you have a trick or an idea of how to make building JSON files better? Leave a comment and let me know!

  • Defining Yourself

    Defining Yourself

    If you took the 2016 WordPress survey, you were asked to define yourself. Blogger, developer, designer, and so on. It’s a profound question, and not just in the metaphysical way.

    More Than One

    I am many things because I represent many things. When I speak, I speak forever with the weight of who I am. I speak and it reflects on DreamHost, my company. On WordPress, where I volunteer. If I were to say I hated Akismet, for example, it could end up on various sites and Facebook groups that the Plugins Team hates Akismet.

    I’ll get back to that in a minute. Last summer I talked about handling bad reviews at WordCamp Europe. I don’t want to repeat that, you can watch it. People leave angry, mean, and outright bad reviews for a lot of reasons, and you can handle them constructively or not, as you like. Obviously I think constructive is better, because those bad reviews, the way you handle them is what’s going to make your reputations.

    The Forest for the Trees

    The problem is that you, the creator, feels so close to your code and creation, that you have trouble divorcing yourself from the review. I find that the more someone has worked in journalism or writing, the less personally they take the reviews, because they have seen their works ripped apart by a red pen before. Artists have to learn how to handle being edited.

    But the other problem is that you forget you’re NOT an artist. If an actor or a musician blows up at people and rants and raves, it may hurt their career, but… people still hire Mel Gibson after his anti-Semitic rant. And Tiny Fey, love her, has been rather transphobic. No one is perfect, not even our idols, and we accept that.

    The Goose vs the Gander

    People are less accepting of their peers. If you get a bad review and explode on someone, calling them names, you’ve hurt yourself and your brand more than any single one star review ever could. Worse, they may treat you like a celebrity, over analyzing every word you say. That one’s a hoot.

    The truth of all this is depressing. You will be hated, intentionally misunderstood, thrown under a bus, leibeled, and slandered. People will assume the worst of you. And because of this, they will assume to worst of your project, your brand, and your company. Forever.

    And this too is depressing, because you will never be free of it. I posted on my blog, sometime last year, a post called “What they don’t tell you.” It listed the downsides to the community, and how these days happen and they suck. And you can’t stop them. I walked away from things decades ago, and they follow me. They haunt me.

    Is There a Truth?

    The obvious question now, the one I am reluctant to answer is HOW do you cope?

    I don’t know.

    I can tell you how I cope, but I don’t know if my answers will help you. I can tell you that it does all suck sometimes, but not all times, and you should have other outlets. I cannot offer the answer, though, because there isn’t just one.

    The one truth I have is that defining myself as someone I can live with being is my answer. For the truth within myself is that as long as I know I am being good and honest and as fair as I can be, I am a good person. And being a good person is what matters most to me.

  • Grandchildren Themes

    Grandchildren Themes

    I’m a fan of Genesis themes. They look nice, they’re secure, and they’re well coded. A lot of things I have to reinvent in other themes are done out of the box in Genesis.

    But they’re not perfect.

    Frameworks Beget Children Themes

    The problem with Genesis themes is that if you use them, you’ll end up using a Child Theme and not Genesis. Unlike boilerplate themes like Underscores, you’re not meant to edit the theme itself but make a child theme.

    For the most part, this doesn’t bother me. I don’t generally edit the child themes, except in two cases. Both of my fan sites run highly modified versions of the default themes, and one of them uses the amazing Utility Pro theme by Carrie Dils.

    And that was my problem. I knew Carrie was working on a new version which would have some amazing updates. And I? I had forked her theme.

    Marrying Forks

    Merging my fork to her new theme had, generally, not been an issue. I’ve updated it a dozen times already and I just run a tool to find the diff between the files. I’m a Coda fan, and I use Comparator to check out the differences between files. Doing this is time consuming and annoying, however, and generally leads to people not making changes they should.

    As time went on, I made fewer and fewer changes not because I didn’t want to, but because I had gotten increasingly smarter. Why edit out what I could de-enqueue, for example?

    Grandchildren Plugins

    The solution to my woes was a grandchild. Except instead of a grandchild theme, I made a plugin. Actually I have an mu-plugin called “Site Functions” and in that file is this call:

    if ( 'Utility Pro' == $theme-&gt;name ) {
    include_once( dirname( __FILE__ ) . '/utility-pro/functions.php' );
    }
    

    That file has 300-ish lines of code, which sounds like a lot now that I look at it. Except it boils down to 6 actions and 7 filters:

    add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
    add_action( 'genesis_after_entry_content', array( $this, 'genesis_after_entry_content' ), 15 );
    add_action( 'wp_head', array( $this, 'header' ) );
    add_action( 'genesis_before_comment_form', array( $this, 'before_comment_form_policy' ) );
    add_action( 'genesis_before_comments', array( $this, 'before_comments_ads' ) );
    add_action( 'genesis_setup', array( $this, 'theme_setup' ), 20 );
    
    add_filter( 'the_content_more_link', array( $this, 'more_link_text' ) );
    add_filter( 'excerpt_more', array( $this, 'more_link_text' ) );
    add_filter( 'admin_post_thumbnail_html', array( $this, 'admin_post_thumbnail_html' ) );
    add_filter( 'genesis_title_comments', array( $this, 'genesis_title_comments' ) );
    add_filter( 'genesis_comment_list_args', array( $this, 'comment_list_args' ) );
    add_filter( 'comment_form_defaults', array( $this, 'comment_form_defaults' ) );
    add_filter( 'genesis_footer_creds_text', array( $this, 'footer_creds' ), 99 );
    

    Everything I did editing the theme, I can do in those 6 actions and 7 filters. It’s positively amazing. For example, I mentioned dequeueing? I don’t like using Google Fonts if I don’t have to, so I remove them. But I also needed to change the backstretch arguments to properly force my image in the right location, so I can do this:

    wp_dequeue_script( 'utility-pro-fonts' );
    wp_dequeue_script( 'utility-pro-backstretch-args' );
    wp_enqueue_script( 'utility-pro-backstretch-args',  WP_CONTENT_URL . '/mu-plugins/utility-pro/backstretch.args.js', array( 'utility-pro-backstretch' ), '1.3.1', true );
    

    That removes the fonts and backstretch arguments, and then adds my own in. And yes, I know my method of calling mu-plugins is not great. I do it this way because I have symlinks, and plugins_url() manages to call that URL instead of the one I want it to.

    The Benefits

    Doing my code this way means it can’t be deactivated. It also is called by checking the theme, so if that changes then the files stop being called. I keep my own work under version control, letting me go back any time I need to. I’m no longer duplicating Carrie’s work either, speeding up my development time.

    It’s a win all around.

  • Sharing Content with Static Sites Dynamically

    Sharing Content with Static Sites Dynamically

    When I wrote how to serve content to Hugo, I did so using something that was mostly static. You see, that code requires someone to push a new version of the Hugo site to rebuild the pages.

    Now let’s be serious, who wants to do that?

    The Concept

    Sadly, you can’t just include a PHP file in Hugo (or any static site builder) and have it echo content. Their whole point is to be static and not change. And my problem is that I immediately ran into a week where I knew the message on the header was going to be changing daily.

    Ew, right? Right. So I looked at that which I should be embracing deeply. Javascript. Or in this case, jQuery and the getJSON call. Yes, that’s right, with jQuery you can call JSON and output it where you want.

    I do not recommend doing this for full page content. This is only vaguely smart if you’re trying to output something small that loads fast and isn’t going to mess up your site if someone has javascript disabled.

    The Code

    <script>
    	$.getJSON( "https://example.com/wp-json/wp/v2/pages/14363", function (json) {
    	    var content = json.content.rendered;
    		document.querySelector('.wpcontent').innerHTML = content;
    	});
    </script>
    
    <div class="utility-bar">
    	<div class="wrap">
    		<section id="text-16" class="widget widget_text">
    			<div class="widget-wrap">
    				<div class="textwidget">
    					<div class="wpcontent"></div>
    				</div>
    			</div>
    		</section>
    	</div>
    </div>
    

    What that code does is it grabs the JSON, sets the variable content to the value of the content’s ‘rendered’ setting. Then using document.querySelector, it tosses in the HTML to my class for wpcontent and I’m done.

  • Why I Write About What I Code

    Why I Write About What I Code

    I was asked this the other day. Obviously sometimes I write about technology in general, or software I find and like, but a great deal of the posts here are about how I figure things out. And the reason I do that is, simply, it makes me a better writer and a better coder.

    Want to write better?

    There’s nothing that will make you a better write than writing. You will learn your voice, your tone, and your flavor of writing only if you write. It doesn’t matter if your writing is bad at first. By writing more and more and more you will only get better and better at the process, and more comfortable doing it.

    Getting into the habit of writing, where it’s an every day occurrence in your life, is imperative if you want to write better. It’s a talent, yes, but it’s also a skill. And if you don’t practice skills they get rusty. If they get too rusty, they break and you give up.

    Want to code better?

    The fastest way to get better at code is to read and review other people’s code and try to figure out how they did what they did. The reason I can continue to think as sharply as I do about plugin reviews is that I do it every day. Every. Single. Day. I look at 30 to 100 plugins, review the code as written by just as many developers, reverse engineer what they’ve done, and I start to understand better. I peer review people’s code, day in and day out.

    But nothing makes you a better code than coding. Obviously. And yet there’s one thing most people miss. You see, the critical review of your own code is absolutely necessary if you want to become a better coder. And in the absence of peer reviewed code, the best thing to do is rip it apart yourself.

    Can you explain your code?

    That’s it. That’s the magic. If you can explain your code, why you did what you did, why it does what it does, then you are at the step of critically reviewing your code. The number of times my code has improved because I’ve blogged about it is uncountable. As I write my post, I find myself typing “I used the function X because…” and I stop. Why did I use that function?

    It’s in the questioning of my own actions that I begin to understand my own internal logic. You know, the part of your brain your parents and teachers helped you form. Those early days of logic where you learned fire was hot and one plus one was two, you also developed your own style of thinking.

    Can you explain why?

    My father likes to tell me I used to do my math backwards, from left to right, before my school taught me otherwise. On occasion, I still do it that way because I want to look at my math from a different perspective. Talking about why I do that changes my understanding of the process. The solution was always the same, but the process of getting there is vastly different.

    When I talk about why I chose the path I did, I do more than just verbalize to myself what I’ve done, I teach someone else that there’s an answer and there’s a way to their answers as well. I’ve shown a path.

    I write to understand myself

    Above all else, I write to understand myself. Only by doing that can I improve at anything.