Half-Elf on Tech

Thoughts From a Professional Lesbian

Author: Ipstenu (Mika Epstein)

  • Multi Faceted Connections

    Multi Faceted Connections

    That’s a pun because I’m using FacetWP y’all.

    As you probably know by now, I have a site that has a lot of weird data. And one of the problems I ran into with FacetWP and my site was that I saved data in a serialized manner.

    Simple Arrays

    Since characters can have multiple actors, the data is saved in a serialized array like this: a:1:{i:0;s:13:"Lucy Lawless";}

    I want to be able to search for all the characters Lucy Lawless plays, even if someone else also played the role, so to do that I need to tell FacetWP to save the data twice, an entry for each actor. To do that, I use this code:

    add_filter( 'facetwp_index_row', 'filter_facetwp_index_row', 10, 2 );
    function filter_facetwp_index_row( $params, $class ) {	
    	// Actors
    	// Saves one value for each actor
    	if ( 'char_actors' == $params['facet_name'] ) {
    		$values = (array) $params['facet_value'];
    		foreach ( $values as $val ) {
    			$params['facet_value'] = $val;
    			$params['facet_display_value'] = $val;
    			$class->insert( $params );
    		}
    		return false; // skip default indexing
    	}
    	return $params;
    }
    

    That saves two entries in the FacetWP table like this:

    An example of two actors for one character

    Complex Arrays

    Buuuuut I also have a complex array where I list a show’s airdates: a:2:{s:5:"start";s:4:"1994";s:6:"finish";s:4:"2009";}

    Now that doesn’t look too weird, I know, but the problem is I wanted to be able to compare the start and end dates, so you could get a list of, say, all shows that were on air between 1950 and 1960 (two, by the way). In order to do that, I had to break the array apart into not only two values, but two separate sources!

    In order to make that work, I do this:

    // Airdates
    // Splits array value into two sources
    if ( 'show_airdates' == $params['facet_name'] ) {
    	$values = (array) $params['facet_value'];
    
    	$start = ( isset( $values['start'] ) )? $values['start'] : '';
    	$end   = ( isset( $values['finish'] ) )? $values['finish'] : date( 'Y' );
    
    	$params['facet_value']         = $start;
    	$params['facet_display_value'] = $start;
    	$class->insert( $params );
    
    	$params['facet_source']        = 'cf/lezshows_airdates_end';
    	$params['facet_name']          = 'show_airdates_end';
    	$params['facet_value']         = $end;
    	$params['facet_display_value'] = $end;
    	$class->insert( $params );
    	
    	return false; // skip default indexing
    }
    

    That gives me two database entries like so:

    An example of two values in two separate sources

    The reason this is done is because I have a facet that compares the datasets for lezshow_airdates_end with lezshow_airdates and if the numbers are between them, that’s what it shows.

    And this works because of this filter:

    // Filter Facet sources
    add_filter( 'facetwp_facet_sources', function( $sources ) {
        $sources['custom_fields']['choices']['cf/lezshows_airdates_end'] = 'Airdates End';
        return $sources;
    });
    

    That creates a new custom field based on the values in cf/lezshows_airdates_end so I can compare between the two. And with a snazzy slider, I can do this:

    Aired Between ... as a slider

  • I’ll Know A Duck When I See It

    I’ll Know A Duck When I See It

    After I complained about the new SEO scam, someone pointedly argued it wasn’t spam. And it wasn’t a scam.

    It is and it’s both.

    What Is Spam?

    By it’s most basic definition, spam is an irrelevant or otherwise inappropriate message, sent on the internet, to a large group of people.

    With that definition in hand, someone who interrupts a Slack or IRC meeting to tell a joke is spamming. At the same time, Tweeting inanities is not unless you cut into a conversation thread. And the different being that Twitter is always irrelevant so any comment there is expected to be appropriately inappropriate.

    Spam is More Than Spam

    The issue is that spam has expanded to be more than just that simple blast of junk you didn’t care about. Spam now includes things like being added to an email list you didn’t want to join. And it includes people trying to rip you off.

    A scam is an attempt to get something from you. The end goal of a lot of spam is to scam you out of money, so the intersection there is pretty high. It always has been. The result of a spambot is to convince you to do something you didn’t want, in order to get something you have. But the target of scams is to out and out separate you from your money.

    If It Looks Like a Duck …

    You’ve probably heard of the duck test.

    If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.

    When people read an email from some Nigerian prince, they know it’s spam because they’ve seen things like it before. But they also know it’s a scam because they’ve been taught that no one offers something for nothing.

    Unsolicited Emails Are Ducks

    If you get an email you didn’t ask for, from someone you’ve never heard of, offering something that’s too good to be true (like ‘free backlinks’), it’s a duck.

    If you look at the emails from these people who offer to help you fix your site and improve your links to broken locations, it’s a duck. It’s a scam, it’s spam, and you should delete it. Don’t even ask.

  • FacetWP: Making Sorting Suck Less

    FacetWP: Making Sorting Suck Less

    Sorting data in WordPress is generally done in the most basic of ways. You want to see all posts that are in a specific category, you go to example.com/category/drinks/ and there you are. But if you want to see everything in the category ‘drinks’ with the tag ‘bourbon’ and the custom taxonomy of ‘ingredients’ and a value of ‘mint’ AND ‘simple syrup’ to get the recipe for a mint julep, then you have a pretty crazy complex query.

    Enter FacetWP

    FacetWP is a premium plugin that, for $79 a year, handles all that crazy sorting for you. And yes, it’s worth it.

    FacetWP introduces advanced filtering to WordPress, which lets you do things like get that list of all drinks made with bourbon that include a simple syrup, in a dynamic way! It’s incredibly fast, since it’s using ajax and javascript, and as long as you have enough server memory to index all the data in the first place, it’s faster than reloading a new category page.

    Downsides

    In order to be that fast, you do not get pretty URLs. Let’s say you have your drinks category at `example.com/category/drinks’ and you want to list all those things. Your URL will look like this:

    example.com/category/drinks/?fwp_alcohol=bourbun&fwp_ingredients=simple+syrup%2Cmint

    The realistic reason they don’t try to make it ‘pretty’ is that it would create a lot more rewrite rules than would be sustainable, if you have a lot of facets. The number of checks would slow your site down, and that would kind of suck.

    Compatibility Notes

    If you use CMB2 you’ll need FacetWP + CMB2.

    If you use Genesis themes, there are two tricks. First, you’ll want to use the following function to add FacetWP’s CSS to your theme:

    function YOURTHEMENAME_facetwp_class( $atts ) {
        $atts['class'] .= ' facetwp-template';
        return $atts;
    }
    add_filter( 'genesis_attr_content', 'YOURTHEMENAME_facetwp_class' );
    

    Second, if you’re like me and you use a lot of custom loops, they may not behave as expected. If you call the loop multiple times on a page (which is bad behavior in the first place and I know it), FacetWP has a bit of trouble knowing what javascript to apply to what section. That should be expected, and once I cleaned it up, it worked great.

    Should you use it?

    If you have a lot of complex intersectional queries to sort through, yes.

    If you need dynamic result updates, yes.

    It works.

  • Sharing WordPress Content with any PHP App

    Sharing WordPress Content with any PHP App

    Last week I explained how I shared my WordPress content with Hugo. Now, that was all well and good, but there is an obvious way I can do this when I don’t have a tool that understands dynamic content?

    I’ve got PHP so the answer is “You bet your britches, baby!”

    The First Version

    If you just want to grab the URL and output the post content, you can do this:

    $curl = curl_init();
    curl_setopt_array( $curl, array(
    	CURLOPT_FAILONERROR    => true,
    	CURLOPT_CONNECTTIMEOUT => 30,
    	CURLOPT_TIMEOUT        => 60,
    	CURLOPT_FOLLOWLOCATION => false,
    	CURLOPT_MAXREDIRS      => 3,
    	CURLOPT_SSL_VERIFYPEER => false,
    	CURLOPT_RETURNTRANSFER => true,
    	CURLOPT_URL            => 'https://example.com/wp-json/wp/v2/pages/12345'
    ));
    
    $result = curl_exec( $curln);
    curl_close( $curl );
    
    $obj = json_decode( $result );
    
    echo $obj->content->rendered;
    

    Now this does work, and it works well, but it’s a little basic and it doesn’t really sanitize things. Plus I would be placing this code in multiple files per site (not everyone themes as nicely as WordPress, ladies and gentlemen). So I wanted to write something that was more easily repeatable.

    The Advanced Code

    With that in mind, I whipped up a PHP file that checks and validates the URL, makes sure it’s a wp-json URL, makes sure it’s JSON, and then spits out the post content.

    <?php
    
    /* This code shows the content of a WP post or page.
     *
     * To use, pass the variable ?url=FOO
     *
     */
    
    if (!$_GET || !$_GET["url"]) return;
    
    include_once( "StrictUrlValidator.php" );
    
    $this_url = (string) filter_var( $_GET['url'], FILTER_SANITIZE_URL );
    
    if (strpos( $this_url, 'wp-json') == FALSE ) return;
    
    function do_curl ( $url ) {
    	$curl = curl_init();
    
    	curl_setopt_array( $curl, array(
    		CURLOPT_FAILONERROR    => true,
    		CURLOPT_CONNECTTIMEOUT => 30,
    		CURLOPT_TIMEOUT        => 60,
    		CURLOPT_FOLLOWLOCATION => false,
    		CURLOPT_MAXREDIRS      => 3,
    		CURLOPT_SSL_VERIFYPEER => false,
    		CURLOPT_RETURNTRANSFER => true,
    		CURLOPT_URL            => $url
    	) );
    
    	return curl_exec( $curl );
    	curl_close( $curl );
    }
    
    if ( StrictUrlValidator::validate( $this_url, true, true ) === false ) {
    	$return = "ERROR: Bad URL";
    } else {
    	$obj = json_decode( do_curl ( $this_url ) );
    
    	if ( json_last_error() === JSON_ERROR_NONE ) {
    		$return = $obj->content->rendered;
    	} else {
    		$return = "ERROR: Bad JSON";
    	}
    }
    
    echo $return;
    

    You can see I have some complex and some basic checks in there. The URL validation is done via a PHP Library called StrictUrlValidator. If I was using WordPress, I’d have access to esc_url() and other nifty things, but since I’m running this out in the wild, I make do with what I have.

  • Local: For When You Don’t Need The Kitchen Sink

    Local: For When You Don’t Need The Kitchen Sink

    The app formerly known as Pressmatic was bought by Flywheel and converted to be Local.

    I’d been wanting to try Pressmatic for a while, but had some ethical concerns about paying when I knew the owner wasn’t paying his other employees. At this point, however, the damage has been done in a way I can live with, so I downloaded Local to play with.

    Local Is an App

    The best thing about Local is that it’s an app. Vagrant is awesome, but it’s 100% command line, and while I’m fine with that, not everyone else is. The learning curve for VVV is steep and, given what you can do with it, it’s not a bad thing. I love it but it can be overkill when I want to work on my plugins. Local is an app, it looks like a Mac app and behaves like a Mac app, so it makes it more obvious how I use it and what I do with it.

    Syncing Data

    My biggest issue with VVV (and VVV2) is I can’t sync folders from Vagrant to my desktop. This is a workflow issue and I know it. For me, I have dedicated folders for my plugin development, all saved in ~/Development/wordpress/plugins-git/ (with a folder per plugin obviously). That makes it easy for me to script updates by saying “For every git repository in this folder, do a pull.”

    Since I do my development work on multiple computers, having as much of this automated as possible is important for my sanity, no matter how much Sara Lance tries to destroy it. Every time I log in to my computer, it runs the update which pulls everything down and syncs.

    And all that means, for VVV, I have two options.

    1. checkout a git repository into the plugin locations for my site and edit those, syncing them back up.
    2. manually copy files over every time I update them

    But with Local, I can use the add-on Volumes which lets me map a folder on Local’s virtual machine to my local desktop. Now it’s not perfect, but with this script by Andy Fragen, I was able to set up my site with actual honest to goodness symlinks:

    [gist]https://gist.github.com/afragen/748e4780b6057d4c41cf9e466557042a[/gist]

    Now I just edit my repositories and magically they’re updated on Local. Faster development for the win.

    (My own fork of Andy’s code is available here on Github – I made some changes but not much.)

    Conclusion

    I use both.

    VVV is great. I love it when I’m working on WordPress core, or god help me, WordPress.org itself.

    But when it’s just me developing my own software and testing? I like Local. It’s good.