Half-Elf on Tech

Thoughts From a Professional Lesbian

Tag: facetwp

  • Reordering Facet Displays … For Death

    Reordering Facet Displays … For Death

    Isn’t that a catchy title?

    I’m using FacetWP to help me order and sort archives in ways that are reflective of the content. One of the things I sort are characters (yeah yeah yeah) and some of those characters are dead. It occurred to me that wouldn’t it be nifty if I could sort the characters by when they died?

    There was just one problem. Actually there were two. One was Sara Lance, and she’s my personal demon. The other was my own stupidity and lack of foresight. Neither were insurmountable.

    How FacetWP Changes Sort Order

    Before I get into the weeds, let’s have a moment to talk about sort order. FacetWP has a way to filter the sort orders.

    So for an example, I have post meta value for the number of characters saved as lezshows_char_count for all shows. If I wanted to sort shows by the most characters to least, I can add this in:

    $options['most_queers'] = array(
    	'label' => 'Number of Characters (Descending)',
    	'query_args' => array(
    		'orderby'  => 'meta_value_num', // sort by numerical custom field
    		'meta_key' => 'lezshows_char_count', // required when sorting by custom fields
    		'order'    => 'DESC', // descending order
    	)
    );
    

    It looks very similar to WP_Query and that’s what makes it easy. Except for my two problems…

    Problem One: Formats

    The first problem was not the Sara Lance problem. It was the ‘Mika didn’t think about things 4 years ago’ problem. I was saving the dates of death in the format of MM/DD/YYYY

    If you’re an American, you’re wondering “So what?” and if you’re anyone else, you’re giving me a death glare because “08/05/2010” could be August 05 OR May 08, and damn it, I knew better. For what it’s worth, the output on the front end is always “05 August 2010” but that’s not here nor there.

    You see, the issue isn’t that I was using stupid date/time formats, the issue is sorting.

    In the previous example, I have an order of meta_value_num which is literally a number. What’s the one for dates? You get meta_value_date or meta_value_datetime and neither of them work with the date format I’d chosen.

    So for this to work, I had to go and change everything from MM/DD/YYYY to YYYY/MM/DD – Not fun, but doable. And it led me to my Sara Lance Drama…

    Problem Two: Arrays

    How many times has Sara Lance died? Right now, three.

    When I decided to sort by the date of death, which one did I pick? Long pause.

    I decided to pick the last one. That is the most recent death. If someone’s actually still dead, the most recent death is the one that stuck. If they’re not, then death was pretty arbitrary to begin with and there ya go.

    The question became how and where did I save the death? I went with a post meta of lezchars_last_death and had it auto update on post save, like this:

    add_action( 'save_post_post_type_characters', 'characters_update_meta', 10, 3 );
    function characters_update_meta( $post_id ) {
    
    	// unhook this function so it doesn't loop infinitely
    	remove_action( 'save_post_post_type_characters', 'characters_update_meta' );
    		
    	// get the most recent death and save it as a new meta
    	$character_death = get_post_meta( $post_id, 'lezchars_death_year', true );
    	$newest_death    = 0000-00-00;
    	foreach ( $character_death as $death ) {
    		if ( $death > $newest_death ) $newest_death = $death;
    	}
    
    	if ( $newest_death !== 0000-00-00 ) {
    		update_post_meta( $post_id, 'lezchars_last_death', $newest_death );
    	}
    
    	// re-hook this function
    	add_action( 'save_post_post_type_characters', 'characters_update_meta' );
    }
    

    If there is a latest death, we get to set it as the YYYY-MM-DD value and off we go. But…

    Problem Three: Orderby Hellscape

    Surprise! I ran into a third problem! Remember how I was using the orderby of meta_value_num in my example? And I mentioned that I wanted to use meta_value_date or meta_value_datetime to sort by date?

    Yeah no.

    If I’d converted the date into unix time, sure. But I was reusing this logic in a couple places, and I didn’t want to re-save everything like that. I also use a query to grab all deaths in a year, and basically I do need to keep it with the format I have. That just messed up my sort until I found the magic of orderby' => 'meta_value',

    End Result?

    It works. It’s got yet another post meta, which I’m not super happy about, but sometimes that’s really just the simplest way to solve a problem. The data is always updating itself, and it’s relatively easy for me to tweak it. Also now I can do a lot more searches in different ways.

    Since I don’t have to worry about database size at the moment, nor speed, since I’ve designed it well, this works for me.

  • The Invisible Facets

    The Invisible Facets

    I’ve been using FacetWP for a year or so and it’s, hands down, the smartest WordPress plugin I’ve ever bought. Certainly I could have coded it, but not having to and being able to extend it to do what I need has saved me months of work and support.

    This is not to say it’s perfect. I’ve run into multiple quirks and headaches that resulted in me writing weird code to solve. And the solution to the null entry was no different. But it was, at the end, solvable.

    A Null Entry

    The majority of my data is saved in custom taxonomies. This makes it easy for me to grab and process. It’s also easy to list, because I can point people at /taxonomy/term and FacetWP magically populates properly in the sidebar.

    However. In one case, I have a checkbox. This is a simple post-meta to say if we love a show or not, and that check box, if it exists, is detected by FacetWP and I can easily get a list of all loved shows. The reverse is, sadly, not easy.

    That’s because if the checkbox is empty, there is nothing saved. No post meta. And if there’s no data, then when FacetWP builds out it’s list, there’s nothing saved for the non-existent data, and therefore no way to list nothing.

    An Imaginary Entry

    The other problem, related to this, is that I use Taxonomies in a different way. That is, while I use them like everyone else does with tags and categories, I also use them to track ‘stars’ – gold or silver etc. Obviously that makes it easy to track with /stars/gold/ buuuuuuut what if I wanted to list all the shows without any stars?

    How do I tell FacetWP ‘if there’s no taxonomy data saved for this, use a default?’

    A Fake Facet

    The answer lies within making a fake, that is unused, facet.

    The tl;dr to how FacetWP works is that it generates it’s own table with the data it collects from it’s facets. In general, there’s a 1-to-1 relationship with the facet and how it outputs. If you’re saving the terms of a taxonomy (like star colors), then there’s an entry in the database for the show and it’s star. If something has multiple values (like tags) then it has multiple entries.

    You can then alter the

    The Facet

    In order to make entries for my null or imaginary values, I made a facet that I didn’t use that I called all_the_missing and I gave it the data source of “Post Types”:

    An example of the facet

    The rest doesn’t matter. I’m not planing to display this, and I picked post types because it’s a quick bit to add the database without making it too heavy or complicated. Also I know it’ll exist for all my data.

    The Filter

    The magic to all this is my filter for facetwp_index_row:

    if ( 'all_the_missing' == $params['facet_name'] ) {
    	// If we do not love the show...
    	$loved = get_post_meta( $params['post_id'], 'shows_worthit_show_we_love', true);
    	if ( empty( $loved ) ) {
    		$params_loved = $params;
    		$params_loved['facet_name']           = 'show_loved';
    		$params_loved['facet_source']         = 'cf/shows_worthit_show_we_love';
    		$params_loved['facet_value']          = 'no';
    		$params_loved['facet_display_value']  = 'No';
    		$class->insert( $params_loved );
    	}
    	// If there are no stars
    	$stars = get_the_terms( $params['post_id'], 'the_stars' );
    	if ( empty( $stars ) ) {
    		$params_stars = $params;
    		$params_stars['facet_name']           = 'show_stars';
    		$params_stars['facet_source']         = 'tax/the_stars';
    		$params_stars['facet_value']          = 'none';
    		$params_stars['facet_display_value']  = 'None';
    		$class->insert( $params_stars );
    	}
    	return false; // skip default indexing
    }
    

    Now this is also wrapped in an if ( get_post_type( $params['post_id'] ) == 'post_type_shows' ) {...} check so this particular one only runs when shows are saved. But you can see what I do is when I run that specific facet, I check if the other data is available and if so, save it.

    Improvements

    I’d like to actually not have to save data when I don’t need it, but the need is enough that having this work was paramount. I can now sort when there’s no data, and that was what I needed.

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

  • FacetWP: Spinning While Updating

    FacetWP: Spinning While Updating

    When you use FacetWP you can do some cool things like change the ‘count’ output of a page. Using the function facetwp_display() you can add facetwp_display( 'counts' ) to your page title, and then a boring old archive title goes from “List of Characters (2022)” to “List of Characters (1-24 of 2022)”

    But… What if you could do more?

    What Could Be More?

    If you have a lot of data, sometimes a page can load and FacetWP spins while it collects everything in it’s wee javascripty brain. When that happens, you have a cognitive moment of “What?” And in order not to lose a user, you want to indicate, somehow, that an action is happening. A spinning icon is, I think, a great way to do that.

    So with that in mind, I want to do this:

    List of Characters showing a spinning icon for the number

    And I did.

    The Code

    This needs javascript. I used some logic from the FacetWP documentation and some memories about how you can replace text with javascript and came up with this:

    (function($) {
    	$(document).on('facetwp-refresh', function() {
    		$('.facetwp-count').html('<i class="fa fa-spinner fa-pulse fa-fw"></i><span class="sr-only">Loading...</span>');
    	});
        
        $(document).on('facetwp-loaded', function() {
    	   $('.facetwp-count').html('');
    	});
        
    })(jQuery);
    

    Then I slap it into my PHP code like so:

    $count_posts = facetwp_display( 'counts' );
    
    the_archive_title( '<h1 class="facetwp-page-title page-title">' . $title, ' (' . $count_posts . '<span class="facetwp-count"></span>)</h1>' );
    

    The content for $count_posts shows nothing while it’s loading, so the check for facetwp-loaded will handle it perfectly.

  • FacetWP, JSON API, and WP_Query Searches

    FacetWP, JSON API, and WP_Query Searches

    One of the ways that FacetWP works is that it adds <!--fwp-loop--> to the source of your outputted page when it detects you used a search query and it can’t find it’s usual classes. This is so that it’s various features like refreshes and so on. It’s a good thing.

    At least it is until you’re trying to use a WP_Query based search to find titles “like” something, and you find your JSON output prefixed…

    Mostly Harmless

    Most of the time when you see <!--fwp-loop--> in the source code, you don’t care. It doesn’t impact anything and it helps Facet work. This is especially important when you have a ‘weird’ theme or plugins that mess with output.

    The issue is that this is how Facet decides if you need that output:

    $is_main_query = ( $query->is_archive || $query->is_search || ( $query->is_main_query() && ! $query->is_singular ) );
    

    Most of the time, that makes perfect sense. It just happens that I’m calling search in a place where that output is a bad idea. Like this:

    {“id”:6294,”name”:”Alex”,”shows”:”Witches of East End”,”url”:”https:\/\/tv.lezpress.dev\/character\/alex\/”,”died”:”alive”}

    Whoops.

    Annoying, But Not Impossible, To Fix

    After bashing my head in for a while, I grep’d the code for Facet, found where it was being set, and then read the help document on facetwp_is_main_query which told me that I could filter the function.

    In this case, I needed to set the value to false to get it to stop outputting, so I used this:

    add_filter( 'facetwp_is_main_query', function( $is_main_query, $query ) { return false; }, 10, 2 );
    

    Be careful where you put that by the way. If you put it on all pages, you’ll break your Facets. I put it in the function that generates the JSON output which limits it heavily, just as I want it to.

  • FacetWP, Genesis, and Archives

    FacetWP, Genesis, and Archives

    In my ongoing use of FacetWP and Genesis, I ran into a case where I wanted to change the archive description content based on what sorts of options had been selected in the search. In part I wanted to remind visitors of what they’d picked, but also I wanted to easy to remove a search facet.

    Before

    In the beginning, the archive was a static thing:

    Before any work was done - it says 'TV Shows' and lists how many.

    This is intentionally boring. It lists the archive title, how many posts, and a description.

    Filtering the Content

    Since this is Genesis, the first step is to know how to filter at all. Since I’m only doing this on custom post types, I went with the very precise action and that is genesis_do_cpt_archive_title_description (aptly named).

    I remove it and then add in my own:

    remove_action( 'genesis_before_loop', 'genesis_do_cpt_archive_title_description' );
    add_action( 'genesis_before_loop', 'DOMAIN_do_facet_archive_title_description' );
    

    From here out, all the work will happen in the function DOMAIN_do_facet_archive_title_description which lives in my functions.php because it’s all theme specific.

    What Gets Added

    Now it’s time to decide what you want to add. I picked three things:

    1. Change the post count based on the results
    2. List the selections chosen
    3. Change the title based on the sort order

    Those are two simple asks and one weird one.

    Facet comes with the ability to display counts and selections:

    • facetwp_display( 'counts' );
    • facetwp_display( 'selections' );

    The problem I had was that the counts were formatted in a way I didn’t like, so I quickly cleaned it up by filtering the result count:

    add_filter( 'facetwp_result_count', function( $output, $params ) {
        $output = $params['total'];
        return $output;
    }, 10, 2 );
    

    That means the count and the selections can simply be tacked on to the description.

    Adding the Sort Data

    The hardest part was figuring out how to add the sort data. Since FacetWP uses a lot of javascript, I spent half an afternoon ranting to myself and trying to figure out how to do this in javascript. And then I did what I usually do when confused. I read the code.

    As I read, I realized some of FacetWP’s magic is that they pass the GET parameters of the search over to javascript… And if they were doing that, then I could just use PHP to grab those parameters.

    All I had to do was pass $_GET['fwp_sort'] into a variable.

    The Code

    Enough talk. Here’s the code:

    function lwtvg_do_facet_archive_title_description() {
    
    	$headline = genesis_get_cpt_option( 'headline' );
    
    	if ( empty( $headline ) && genesis_a11y( 'headings' ) ) $headline = post_type_archive_title( '', false );
    
    	$intro_text  = genesis_get_cpt_option( 'intro_text' );
    	$count_posts = facetwp_display( 'counts' );
    	$selections  = facetwp_display( 'selections' );
    	$fwp_sort    = ( isset( $_GET['fwp_sort'] ) )? $_GET['fwp_sort'] : '';
    
    	switch ( $fwp_sort ) {
    		case 'most_chars':
    			$sort = 'Number of Characters (Descending)';
    			break;
    		case 'least_chars':
    			$sort = 'Number of Characters (Ascending)';
    			break;
    		case 'most_dead':
    			$sort = 'Number of Dead Characters (Descending)';
    			break;
    		case 'least_dead':
    			$sort = 'Number of Dead Characters (Ascending)';
    			break;
    		case 'date_desc':
    			$sort = 'Date (Newest)';
    			break;
    		case 'date_asc':
    			$sort = 'Date (Oldest)';
    			break;
    		case 'title_desc':
    			$sort = 'Name (Z-A)';
    			break;
    		case 'title_asc':
    		default:
    			$sort = 'Name (A-Z)';
    	}
    
    	$headline    = $headline ? sprintf( '<h1 %s>%s Sorted By %s (%s)</h1>', genesis_attr( 'archive-title' ), strip_tags( $headline ), $sort, $count_posts ) : '';
    
    	$intro_text  = $intro_text ? apply_filters( 'genesis_cpt_archive_intro_text_output', $intro_text ) : '';
    	$intro_text .= $selections;
    
    	if ( $headline || $intro_text ) printf( '<div %s>%s</div>', genesis_attr( 'cpt-archive-description' ), $headline . $intro_text );
    }
    

    You’ll notice that I’ve kept in all the regular Genesis filters. This was so that my theme can take advantage of whatever magic Genesis invents down the line.

    How It Looks

    Now the default looks like this:

    Default view, before sorting

    And after you’ve picked a few options, it changes to this:

    After Sorting

    If you click the little x’s on the side of the selections, they’re removed.

    There’s still room for design improvement, but remember folks. Release and iterate.