Half-Elf on Tech

Thoughts From a Professional Lesbian

Author: Ipstenu (Mika Epstein)

  • While Not Being Consumed

    While Not Being Consumed

    I get painted as a bad guy a lot. I’ve been called names, everything you can think up. I’ve had my gender, sexuality, appearance, and ability all mocked and derided. And most of this has happened since I took up the role of a volunteer in WordPress.

    Creation, Editing, Fitting In

    As a writer, which is how I’ve always seen myself first, I’m used to the ruthlessness of the editing process. I’ve seen papers torn apart and painted red with corrections and commentary. Why this? What are you saying here? I understand the reason for ripping apart creativity to find it’s heart and crux and meaning. Art for the sake of art is different than art for the sake of consumption, after all.

    But instead of a career in the arts, or journalism, I had a different path. Out of college I went to work for a bank and quickly learned how to fit myself into the cog of a machine. I had a role and a life that did not encourage innovation and uniqueness, but that of interchangeability. And in that work, I began to understand the reason for patterns and the similarity.

    I’ve always been fascinated by patterns. I liked to see how the number went from 09 to 18 and 27 and obviously the first number goes up while the second goes down, and isn’t math cool? Seeing the pattern in the work at the bank taught me that while we are all creating and inventing, what we make has to be used in many different ways, and they all need to talk to each other. So we have to make sure all the little cogs and wheels interlock properly.

    It’s Still Creation

    I like to watch the behind the scenes parts of movies. It’s why I prefer to own physical copies. I have watched all the extras for the Lord of the Rings movies. That’s why I know Tolkien disliked when people were obsessed with his work. Because he was not obsessed. He liked what he did, but it wasn’t the end of the world if people didn’t care for it. He made something neat to tell the story he wanted to tell

    Often we as developers fail at this. It’s not entirely our fault. We’re told that what we do is engineering or science, and we forget to stress the creative aspect of inventing that which has never been seen before. We forget progress is forged by dreams. And that’s why, when you see a one star review of your work, it flays you open, leaving you chained to the rocks for the crows to eat your innards. You gave fire to man, and this was your reward?!

    Let’s breathe. What Tolkien didn’t seem to understand was that he had created life. He had made a world so amazing and vibrant, people saw it and wanted to be a part of it. Similarly, when we create code, we give life to others.

    The Good, The Bad, The Ugly

    I’ve gotten the greatest compliments on my work in the form of someone telling me it helped them achieve their goals. People have started businesses, found success, and made something of themselves, all because of something I did.

    But more often, or at least what I remember more, are the names and the anger and the harassment. The people who stalk me down on Slack and Twitter, demanding that I pay attention to them right now and that they are the most important thing… while being angry when I do devote that time, but that my answer is not what they wanted.

    Remember Your Self

    Originally I titled this post “When you’re evil for doing good.” Really that didn’t touch on the real feeling I have about this. Because at the end of it all, I sit and look at the work I’ve done and I ask myself “What was this for?”

    I have a strange life. I have a job that is essentially trying to keep 26% of the internet safe from itself. So my answer for “What was this for?” has become a question of itself. “Is this going to make it better or worse?”

    When I come to decisions, like to tell someone we cannot host their code because they have lied and broken the guidelines too many times, it always comes from this place. Will I make things better for the majority or worse? And in that moment, I ignore the other question. “Will this hurt me?” Because the answer to that is “Yes, probably.”

    “Honor is what you know about yourself.”

    There’s no way I can make hard decisions and not get hurt. There’s no way I can do it and not hurt others. But I sit and I remember my self. My inner self that wants to make things better, safer, and as fair as possible. I remember the me who wants to say “What I do makes the internet better.” And if that comes at a personal cost, well. I will know this.

    I have integrity.

    I have honesty.

    I have empathy.

    As long as I have those, and as long as I do my best and keep learning and becoming better, I will be doing the right thing. And I can live with being ‘evil’ when I’m doing good.

  • 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.