Half-Elf on Tech

Thoughts From a Professional Lesbian

Author: Ipstenu (Mika Epstein)

  • Random Post of the Day

    Random Post of the Day

    I wanted to make a random post of the day. In this case, I wanted it to be a random post of one of two custom post types, and I wanted to output it as JSON for a variety of reasons, including future plans. Like tweeting that post.

    I’ll get to that later.

    Overview

    To do this, I have the following moving parts:

    1. The RESTful routes
    2. The random post
    3. The expiration (i.e. each post lasts a day)

    I’m going to skip over how to make a REST API route. I talked about that earlier in 2017 when I explained how I made the Bury Your Queers plugin.

    What’s important here is actually the random post and spitting out the right content.

    Getting a Random Post

    This is cool. WordPress can do this out of the box:

    $args = array( 
    	'post_type'      => 'post_type_characters',
    	'orderby'        => 'rand', 
    	'posts_per_page' =>'1',
    );
    $post = new WP_Query( $args );
    
    while ( $post->have_posts() ) {
    	$post->the_post(); 
    	$id = get_the_ID();
    }
    wp_reset_postdata();
    
    $of_the_day_array = array(
    	'name'   => get_the_title( $id ),
    	'url'    => get_permalink( $id ),
    );
    

    And at that point all you need to do is have the API return the array, and your final output is like this:

    {"name":"Van","url":"http:\/\/lezwatchtv.com\/character\/van\/"}
    

    This is a simplified version of my code, since in actuality I’m juggling a couple post types (shows or characters), and outputting more data (like if the character is dead or alive). It’s sufficient to prove this point.

    Expirations

    Okay. Now here’s the fun part. If you go to your JSON page now, it’ll show you a new character on every page reload, which is absolutely not what we want. We want this to only update once a day, so we can do this via Transients like this:

    if ( false === ( $id = get_transient( 'lwtv_otd_character' ) ) ) {
    	// Grab a random post
    	$args = array( 
    		'post_type'      => 'post_type_characters',
    		'orderby'        => 'rand', 
    		'posts_per_page' =>'1',
    	);
    	$post = new WP_Query( $args );
    	// Do the needful
    	while ( $post->have_posts() ) {
    		$post->the_post();
    		$id = get_the_ID();
    	}
    	wp_reset_postdata();
    	set_transient( 'lwtv_otd_character', $id, DAY_IN_SECONDS );
    }
    

    But.

    Transients kinda suck.

    Expirations 2.0

    Alright. Let’s do this differently. The server this is on has object caching, and it gets flushed every now and then. While it doesn’t matter if the post is re-randomizes in this case, it’s still not a great practice. So let’s use options!

    // Grab the options
    $default = array (
    	'character' => array( 
    		'time'  => strtotime( 'midnight tomorrow' ),
    		'post'  => 'none',
    	),
    	'show'      =>  array( 
    		'time'  => strtotime( 'midnight tomorrow' ),
    		'post'  => 'none',
    	),
    );
    $options = get_option( 'lwtv_otd', $default );
    
    // If there's no ID or the timestamp has past, we need a new ID
    if ( $options[ $type ][ 'post' ] == 'none' || time() >= $options[ $type ][ 'time' ] ) {
    	// Grab a random post
    	$args = array( 
    		'post_type'      => 'post_type_characters',
    		'orderby'        => 'rand', 
    		'posts_per_page' =>'1',
    	);
    	$post = new WP_Query( $args );
    	// Do the needful
    	while ( $post->have_posts() ) {
    		$post->the_post();
    		$id = get_the_ID();
    	}
    	wp_reset_postdata();
    
    	// Update the options
    	$options[ $type ][ 'post' ] = $id;
    	$options[ $type ][ 'time' ] = strtotime( 'midnight tomorrow' );
    	update_option( 'lwtv_otd', $options );
    }
    

    And now you see my $type variable and why it matters. There’s more magic involved in the real world, but it’s not relevant.

  • Data Based Sites

    Data Based Sites

    Designing your website involves understanding the structure of the data within. Designing your data comes down to how you store it. At its base level, everything on your site is a post, but the way you handle the data WITHIN the posts is how you can plan for growth, adaption, adoption, and the future. Building a site today involves making sure the data is easily consumable by multiple formats, like AMP, JSON APIs, and Alexa Echo Skills. And it all starts with understanding the data you’re using. Even if that data is from TV.

    Television is chewing gum for the eyes

    I love television. I love books, I’m always reading and writing, and I love radio. I like movies. But TV is something weird and wonderful. TV is escapism when a story brings you someplace new and amazing.

    One of the reason I love tv is that I see myself reflected in media. I’m a Jewish lesbian. Growing up, I didn’t see a lot of me on TV. Any, really. If I think about it, the first Jewish Lesbian I can remember seeing is Willow Rosenberg from Buffy. Yeah. I was out of college by then.

    Representation Matters

    In the entirety of television, world wide, there are about 2000 queer females. Total. Yeah. 2000. That’s on about 600 shows. There are not a lot of them, and I know the numbers because I run, with Phillys very own Tracy, a site that uses WordPress to record all of them. That’s right, I have the data.

    Data Driven Design

    This isn’t about themes. I can’t design themes. I don’t. I’m bad at it, I know that, sorry Tracy. And when we set about making this site, we had some lofty goals and ideas and we learned some lessons by prejudging the data. Originally we wanted to simply list shows and if you should watch them or not, and list the characters but…

    Somewhere around 100 characters and fifty shows, it became clear that the datasets we’d defined were underestimated and over-complicated and over-simplified. We had to consider what made a show good or bad. We had to consider the situation. And we had to make changes.

    When you build out any site, you have an idea of what you want. Maybe like us you make a massive list of everything you want to track and get dragged down into the weeds fast. Maybe you make a small list and have to go back and edit. In the end, the trick of it all is planning your site based on your data.

    Even after you do all that planning, you’re going to find out you missed things. You’ll overestimate things. You’re going to underestimate others. People are going to use your data in unpredictable ways. This is just how the world works. So you have to design your code to adapt.

    How to Design for Data

    This is about planning code. Designing your site for data comes down to how you store it. At its base level, everything on our site is a post. There are two types, shows and characters, and all posts have a bevy of ‘meta’ data for the various bits of information.

    What information?

    Shows: tropes, airdates, thumb score, why that score, tv stations, nations, genres, formats, gold star, trigger warnings, quality rating, screentime rating, realness rating, timeline, episodes, & ‘ships.

    Characters: clichés, actor, sexuality, gender, date of death, tv show & role on show.

    So all that is what we record. It’s grown and shrunk. We had urls in there and removed them because it was impossible to upkeep when fansites vanish. I removed and restored ‘ships, after figuring out how to store all of it in a searchable way. But with all that data, we started to see the big picture. And then … we realized how the data was stored mattered.

    Understand What Your Data Is (And Isn’t)

    Most of the data is simple. Ratings are a 1-5 option. Trigger warnings were check boxes for a binary on or off … Were. Sexuality and gender are dropdown lists, and so are tropes and cliches. Managing those is easy. Ish. We understand taxonomies, at least, being WordPess developers.

    Most of the time, data is obvious. Again, I have items that are a binary. A yes or a no. And while I personally believe that sexuality and gender are a spectrum, TV hasn’t caught up there, so that can be stored as a one to one dropdown. You are what you are. The same goes for a character being on a TV show. It’s all one or the other.

    Having said all that, let me introduce you to Sara Lance, as played by Caity Lotz. Schrödinger’s bisexual time traveling action hero.

    The Complex Data: Sara Lance

    Schrödinger’s bisexual time traveling action hero assassin pirate captain.

    Sara Lance has two actors. She’s been on three shows in three separate capacities; guest, recurring, and regular. She’s died and came back to life. Sara Lance exists to make me, as a developer, cry. Because for her I have to store all of that data in a searchable manner that plain-text post meta doesn’t make easy. She made me rethink all our data storage.

    Nothing about Sara is simple. Nothing is straightforward. Not even the existing taxonomies and lists stayed the same. Both gender and Sexuality went from a simple dropdown to a taxonomy. We moved from gay, straight, or bi, to include pansexual and asexual and I’m just waiting for Sara to step into pansexuality, y’all.

    Use WordPress First

    As much as possible, use WordPress. Taxonomies are heavily used for ‘lists’ like cliches and tropes and tv stations, because they come with built in sortables. I can easily list all shows on NBC or all characters who are parents, because those are taxonomies. Even short lists like sexuality and gender work well for that. For the rest, it’s all post meta.

    Sara exists beyond taxonomies. Originally, we had death stored as a simple taxonomy item for character cliches. If you were dead, you got the cliche of dead. Sara came along and died. And came back. So suddenly we had to rethink if someone kept the tags if they died and came back. Pro tip? They don’t. That was a quick decision, though. Don’t worry, she made us make harder decisions.

    Use Plugins Second

    I mentioned everything that isn’t a taxonomy is post meta. Adding metadata to posts that aren’t taxonomies sucks. Yes, I said it. It sucks. Plugins like CMB2 or ACF will save your vegan bacon by making it easier to create a check box or a dropdown or a plain text field, like for actors.

    Sara had two actors. While the field for actor is only plain text, and that’s relatively simple, we had to make it repeatable so we could add multiple actors. God help me, date of death had to be repeatable too. What if Sara dies again!? CMB2 has repeatable fields built in.

    Use Third Party Add-Ons Third

    The bigger the data got, the more important admin design became. The more tropes, the worse a dropdown or multi-check section was. By using a select2 addon, and some custom save code, I was able to convert taxonomies into an auto-complete, which is a lot easier to visualize. So are groups. By clumping related data together, the brain makes the right connextions. And when it’s repeatable, your page grows with the data is uses, not with the data total.

    Sara has three TV shows. THREE!

    Sara’s page is much bigger than anyone else’s because she has three shows, and each show section has a dropdown for character role and another for the show name. And I can’t guess if she’s going to show up on another, like Supergirl maybe. CMB2 does have a limit to repeatable fields and groups, though. I hope Sara doesn’t hit it…

    Be Willing to Make Changes Fourth

    You will be wrong. I removed the list of ships, relationships, from shows. Tracy was sad. I added it back. My bad, totally, and my reasoning was that it was not easy to maintain and manage in a searchable way. It wasn’t. But it could be. And I made it. Be willing to be wrong, to make mistakes, and to recover from them.

    Because … Sara is always changing. Alive, dead, new show, new actor… Sara is always changing and always adapting and always being more awesome. She’s anything but static, and that’s a good thing. Sara Lance made me throw my preconceived notions of data storage and organization out the window. And I’m better for it.

  • Hey You Gays Filter

    Hey You Gays Filter

    Are you tired of people always saying ‘guys’ when they’re talking to a mixed gender group of people? I know I am. One weekend I got four (yes four) comments about my code saying “You guys should…” or “Ask the guys who …” and so on.

    All implied I wasn’t the human who wrote the code. Spoiler alert. I was. While I can’t fix the world out there, I do tend to reply with a handy gif:

    Eowyn from Lord of the Rings, pulling off her helm and shouting I AM NO MAN before killing a wraith and saving everyone.

    But I can fix MY site. Since I only need this for comments on my site (I’m in charge of how I write), I can use this:

    add_filter( 'comment_text' , 'hey_you_gays', 11 );
     
    function hey_you_gays( $text ) {
        static $dblq = false;
          if ( false === $dblq )
            $dblq = _x('“', 'opening curly quote');
          return str_replace(
            array( ' guys', '‘guys', $dblq . 'guys', '> guys', '(guys' ),
            array( ' guys', '‘gays', $dblq . 'gays', '> gays', '(gays' ),
          $text );
    }
    

    If you need to use it in titles and post content, steal the code from capital_P_dangit(). After all, that’s what we do.

  • Genesis: Overriding a Child Theme

    Genesis: Overriding a Child Theme

    There are some odd tricks you end up learning when you use StudioPress’s Genesis Frameworks. Since no one actually uses the framework as the theme, we’re all downloading children themes and then editing them.

    I have to be honest here, I hate editing child themes. That’s why I’ve begun making a functional plugin to ‘grandchild’ the themes. There are some subtle differences, however, in how one approaches code for a grandchild vs a child ‘theme,’ and one of them is how the credits work.

    Normal Genesis Overrides Are Simple

    At the footer of every page for a StudioPress theme is a credit line. It reads out the year and a link to your theme and Genesis, and done. If you don’t like it, you can edit it like this:

    //* Change the footer text
    add_filter('genesis_footer_creds_text', 'sp_footer_creds_filter');
    function sp_footer_creds_filter( $creds ) {
    	$creds = '[footer_copyright] &middot; <a href="http://mydomain.com">My Custom Link</a> &middot; Built on the <a href="http://www.studiopress.com/themes/genesis" title="Genesis Framework">Genesis Framework</a>';
    	return $creds;
    }
    

    If you’re not comfortable with code, I recommend you use the Genesis Simple Edits plugin. But …

    What happens when your child theme already filters the credits?

    Grandchild Genesis Overrides are Not

    My child theme includes a filter already, called CHILDTHEME_footer_creds_filter, and it’s not filterable. That means I can’t just change the add filter line to this:

    add_filter('genesis_footer_creds_text', 'CHILDTHEME_footer_creds_filter');
    

    That’s okay, though, because I knew I could use could use remove_filter() to get rid of it like this:

    remove_filter( 'genesis_do_footer', 'CHILDTHEME_footer_creds_filter' );
    

    Except that didn’t work. I kicked myself and remembered what the illustrious Mike Little said about how one could replace filters in a theme (he was using Twenty Twelve):

    … your child theme’s functions.php runs before Twenty Twelve’s does. That means that when your call to remove_filter() runs, Twenty Twelve hasn’t yet called add_filter(). It won’t work because there’s nothing to remove!

    Logically, I needed to make sure my filter removal runs after the filter is added in the first place. Right? Logical.

    The Credit Removal Solution

    Here’s how you do it:

    add_action( 'after_setup_theme', 'HALFELF_footer_creds' );
    
    function genesis_footer_creds() {
    	remove_filter( 'genesis_do_footer', 'CHILDTHEME_footer_creds_filter' );
    	add_filter( 'genesis_footer_creds_text', 'HALFELF_footer_creds' );
    }
    
    function genesis_footer_creds_text( $creds ) {
    	$creds = '[footer_copyright] &middot; <a href="https://halfelf.org/">Half-Elf on Tech</a> &middot; Built on the <a href="http://www.studiopress.com/themes/genesis" title="Genesis Framework">Genesis Framework</a>';
    	return $creds;
    }
    

    That first add_action() is called after the theme is set up. Then it removes the filter I don’t want and adds the one I do.

    Done and done.

  • Review: FacetWP

    Review: FacetWP

    I’ve been using FacetWP since April 2017 and I can unequivocally say that it was one of the best purchases I’ve made.

    Search Is Hard

    There’s no two ways around this. Search is difficult. You have to guess what peoples’ intents are, and you have to order the results in a way that is meaningful. While it would be great if people searched for keywords, they prefer to look for things in whole phrases like “jackets made of feathers.” Those are terms and presentations that make sense to the human mind.

    On top of that, there are different kinds of search.

    Most people are familiar with document search, which is more or less what WordPress and Google do. Since webpages are just text documentation at the end, Google searches all the text, figures out how many people link to the page, use some secret dipping sauce, five spices, and determine relevancy. WordPress’ own search is much simpler and consequently less effective. Not that Google gets it right all the time either, though…

    Another common type of search, also used by Google, is graph search. This is popular on Facebook and Twitter, but it uses connections between your friends to prioritize and determine depth of search.

    Finally there’s the concept of faceted search. This is useful when a site knows you’re looking for a product, like a shoe, and you just need help narrowing down the size, the color, the fit, etc. And that’s where FacetWP comes in.

    Facets vs Filters

    You might have heard about search ‘filters.’ If you’ve ever used Google’s image search, or news search, and you tried to narrow down results based on dates or colors or formats, you’ve used filters. They help you filter the results by changing the parameters. A faceted search is similar, in that it uses the same concepts as filters to toggle multiple aspects of the search item, giving you even more flexibility in your results.

    The term ‘filter’ and ‘facet’ are oft used interchangeably, and since they’re so similar and related, this does not help a single person at all. They both help reduce large data sets into something manageable, but filters are relatively easier than facets. In fact, your WordPress site already does basic filters. Ever gone to a category or tag page? That’s a very basic example of a filter.

    Faceted Search Is Hard

    If regular relational searches are hard, it shouldn’t surprise you to hear that faceted search is too. A faceted search has the job of analyzing a large data set and excluding anything that doesn’t fit your specific criteria. This means it uses multiple filters, once for each aspect of the data set.

    Okay, let’s make this a little easier to understand with a practical example!

    Let’s say you have a database of 750 TV shows. You’ve identified what you feel to be the key components of the shows, such air dates, countries, ratings, specific genres, and if the reviewer liked it. Now, if someone comes to your site and wants a list of crime dramas that aired between 2000 and 2017, in the US, that the reviewer hated, you don’t need filters, you need a faceted search.

    By building in options to sort each of those things, you reduce the dimensions of content and offer a structure to help your users understand the contextual construct of the data. You are giving them ideas about what data is available, and how they can search through it without having to guess at keywords.

    FacetWP Does All That

    Simply put, FacetWP does that.

    It does all of that. It even lets me add in a sort-by so once a user has narrowed down the shows, they can reorder them based on name, date added, number of characters, and show ratings. If I wanted to extend that to order based on airdate, I could do that too. People can toggle criteria on and off and the content updates dynamically

    If you have a large amount of data (like 750 TV shows or 2250 TV characters), and you want to organize them sanely, swiftly, and not crash your server, use FacetWP. It even works with WooCommerce and EDD, so if you want to be the next Amazon, you need this.

  • Reordering Sort Order Redux

    Reordering Sort Order Redux

    Earlier this year I talked about removing stopwords from sort queries.

    Sadly I ran into a problem where the code wasn’t working.

    The Original Code

    Here’s the original code.

    add_filter( 'posts_orderby', function( $orderby, \WP_Query $q ) {
        if( 'SPECIFIC_POST_TYPE' !== $q->get( 'post_type' ) )
            return $orderby;
     
        global $wpdb;
     
        // Adjust this to your needs:
        $matches = [ 'the ', 'an ', 'a ' ];
     
        return sprintf(
            " %s %s ",
            MYSITE_shows_posts_orderby_sql( $matches, " LOWER( {$wpdb->posts}.post_title) " ),
            'ASC' === strtoupper( $q->get( 'order' ) ) ? 'ASC' : 'DESC'
        );
     
    }, 10, 2 );
     
    function MYSITE_shows_posts_orderby_sql( &$matches, $sql )
    {
        if( empty( $matches ) || ! is_array( $matches ) )
            return $sql;
     
        $sql = sprintf( " TRIM( LEADING '%s' FROM ( %s ) ) ", $matches[0], $sql );
        array_shift( $matches );
        return MYSITE_shows_posts_orderby_sql( $matches, $sql );
    }
    

    This worked, mostly, but it somehow broke pagination. Every page restarted the order. This had to do with complications with the Genesis theme but more importantly it messed up the order on the back of WordPress and it didn’t play well with FacetWP. So I rewrote it a little to be more specific:

    The New Code

    if ( !is_admin() ) {
    	
    	add_filter( 'posts_orderby', function( $orderby, \WP_Query $q ) {
    		
    		// If this isn't an archive page, don't change $orderby
    		if ( !is_archive() ) return $orderby;
    		
    		// If the post type isn't a SPECIFIC_POST_TYPE, don't change $orderby
    		if ( 'SPECIFIC_POST_TYPE' !== $q->get( 'post_type' ) ) return $orderby;
    
    		// If the sort isn't based on title, don't change $orderby
    		$fwp_sort  = ( isset( $_GET['fwp_sort'] ) )? sanitize_text_field( $_GET['fwp_sort'] ) : 'empty';
    		$fwp_array = array( 'title_asc', 'title_desc', 'empty');
    		if ( !in_array( $fwp_sort, $fwp_array ) ) return $orderby;
    
    		// Okay! Time to go!
    		global $wpdb;
    
    		// Adjust this to your needs:
    		$matches = [ 'the ', 'an ', 'a ' ];
    
    		// Return our customized $orderby
    		return sprintf(
    			" %s %s ",
    			MY_CUSTOM_posts_orderby_sql( $matches, " LOWER( {$wpdb->posts}.post_title) " ),
    			'ASC' === strtoupper( $q->get( 'order' ) ) ? 'ASC' : 'DESC'
    		);
    
    	}, 10, 2 );
    
    	function MY_CUSTOM_posts_orderby_sql( &$matches, $sql ) {
    		if( empty( $matches ) || ! is_array( $matches ) )
    			return $sql;
    
    		$sql = sprintf( " TRIM( LEADING '%s' FROM ( %s ) ) ", $matches[0], $sql );
    		array_shift( $matches );
    		return lwtv_shows_posts_orderby_sql( $matches, $sql );
    	}
    }
    

    First of all, I got smart about only loading this when it needed to be loaded. Next I told it to only sort on archive pages, because I was also outputting recently added lists in other places. Finally I forced it to understand Facet, and that if I wasn’t sorting by alphabetical, it didn’t matter at all.