Half-Elf on Tech

Thoughts From a Professional Lesbian

Tag: wordpress

  • Google Knowledge Blindspot

    Google Knowledge Blindspot

    With the new release of Yoast SEO 3.6, I decided to test out their new walk-through setup.

    As someone who’s been studying SEO to one degree or another since the 1990s, SEO in and of itself is no great mystery to me. The bare bones of how it works, how you make good content, is understandable. But what Yoast SEO does, and what I like it for, is it makes it obvious to people without my experience what is needed. It also takes the generally good SEO of WordPress (and a good theme) and optimizes it for the myriad, constant changes that Google does.

    For that, the walk-through is a wonderful idea. I like it. I think new users will love it. I think it’ll lessen the barrier to those people who are told “You need a website!” but no one is willing to (or able to) sit with them and help them get started.

    Initially I was super impressed. Yoast had clearly studied the aspects of walk-throughs that had worked and that didn’t, lifting pages from other complex plugins that needed to be used by, perhaps, non-technical savvy people.

    Yoast Walk Through: What kind of site is this?

    Being asked what kind of site I was running was brilliant. For the purposes of this test, I decided to use my community/wiki/library – LezWatchTV. And right away I ran into a problem.

    Am I a company or a person?

    The tool wanted me to say if I was a company or a person.

    Well … Neither. I’m a community site. Or maybe a group? Either way, the two designations didn’t really apply properly. Where was “Other”?

    This couldn’t be Yoast making a boneheaded maneuver, I realized. Few people know better than Joost and his crew what WordPress is used for. They’re smart people. They’ve seen more of the Internet than most of the rest of us and they know well how it’s used. So could the screwup be Google or Schema.org?

    I went to Schema.org to look up how they would classify the site, and determined that DataCatalog was the most appropriate. Alright, knowing there was a good classification, I looked back at Google’s Knowledge Graph.

    Google’s Knowledge Graph is a weird thing. It’s Google’s attempt to figure out how to answer your questions. You know how you can type in “How do I do X?” into Google and you get that interesting formatted answer?

    Example: How do I bake a pie?

    That’s from their Knowledge Graph. But more importantly, so is this:

    Example: Knowledge Graph data of Root from Person of Interest

    The more you dig into it, the more you realize that the only boxes like that are for people or companies. So the breakdown is that Google has not yet figured out how to flag non-people non-companies.

    This means my ultimate question of ‘what I am?’ has become a little more existential than I’d wanted, and a little simple. It’s not a person, therefore it must be a company. And while that is entirely, totally, daftly incorrect, it’s also less incorrect that a person.

    Thanks, Google.

  • Per-Site MU Plugins

    Per-Site MU Plugins

    A great many moons ago, I handled my per-site MU plugins in a very straight forward way. I made a halfelf-functions.php file and checked for the blog ID with if ( $blog_id == 2 ) {...} and off I went.

    Now? I do this:

    global $blog_id;
    $helf_site_url = parse_url( get_site_url( $blog_id ) );
    $helf_file = plugin_dir_path( __FILE__ ) .'functions/'. $helf_site_url['host'] .'.php';
    if ( file_exists( $helf_file ) ) {
        include_once( $helf_file );
    }
    

    I have a folder called ‘functions’ and in there I have a file for every site that needs it’s own functions. This also let me clean up some rather old code I wasn’t using anymore, but it also let me add in code to include local CSS. Since I version control my mu-plugins folder, this allowed me to move my custom CSS from Jetpack to a normal CSS file.

    Why did I need to do that? Jetpack CSS doesn’t allow the fill param for CSS. Their current justification is that it’s advanced enough that people should be editing the theme. And mine is “But … why?” SVGs are becoming more and more popular, after all, and a number of plugins are allowing them. That means to style your SVGs, you’ll want to use CSS. And you can’t with Jetpack, which means you’re back to the old hell of editing your theme’s CSS. And that was something I wanted to avoid.

    You’d think I could do this:

    $helf_file_css = plugin_dir_path( __FILE__ ) .'functions/css/'. $helf_site_url['host'] .'.css';
    function helf_scripts() {
    	if ( file_exists( $helf_file_css ) ) {
    		wp_enqueue_style( $helf_site_url['host'], $helf_file_css );
    	}
    }
    add_action( 'wp_enqueue_scripts', 'helf_scripts' );
    

    But that actually didn’t work. No matter what I did, the result of $helf_site_url['host'] was empty. I know, right? What’s up with that.

    What’s up is me not thinking about how functions ‘know’ what they know.

    function helf_scripts() {
    	global $blog_id;
    	$url = parse_url( get_site_url( $blog_id ) );
    	$css_path = plugin_dir_path( __FILE__ ) .'functions/css/'. $url['host'] .'.css';
    	$css_url = plugins_url( 'functions/css/'. $url['host'] .'.css', __FILE__ );
    
    	if ( file_exists( $css_path ) ) {
    		wp_enqueue_style( $url['host'] , $css_url );
    	}
    }
    add_action( 'wp_enqueue_scripts', 'helf_scripts' );
    

    Inside the function, you see, it can’t see non-globals. So it wouldn’t work. If you’re not using Multisite, you don’t need to use $blog_id, but I don’t know why you’d want to use this if you weren’t using Multisite. The other silly moment was remembering that plugin_dir_path() would give me /home/user/public_html/wp-content/mu-plugins which would make the URL a relative URL, and not at all what I wanted. But using plugins_url() would give me an absolute path.

    Perfect.

  • Sortable Custom Columns: Taxonomies

    Sortable Custom Columns: Taxonomies

    You may have noticed that when I was making Sortable Custom Columns I didn’t make it so I could sort some of the fields.

    Showing sorted columns

    That’s because unlike my lovely post meta, those other fields are custom taxonomies. And by their very nature, they’re not sortable. In order to do this however you need some SQL.

    Make the Columns Sortable

    First we have to add our new columns (lez_gender and lez_sexuality) into our sortable function.

    add_filter( 'manage_edit-post_type_characters_sortable_columns', 'sortable_post_type_characters_column' );
    function sortable_post_type_characters_column( $columns ) {
    	unset( $columns['cpt-shows'] ); 			 	// Don't allow sort by shows
    	$columns['postmeta-roletype']		= 'role';	// Allow sort by role
    	$columns['taxonomy-lez_gender']		= 'gender';	// Allow sort by gender identity
    	$columns['taxonomy-lez_sexuality']	= 'sex';	// Allow sort by gender identity
        return $columns;
    }
    

    Last time it only had the unset and the postmeta-roletype columns. Now we’re adding in our taxonomies as taxonomy-{TAXONOMYNAME} to that.

    Actually Sort the Content

    Now here’s the sneaky SQL stuff.

    function lez_gender_clauses( $clauses, $wp_query ) {
    	global $wpdb;
    
    	if ( isset( $wp_query->query['orderby'] ) && 'gender' == $wp_query->query['orderby'] ) {
    
    		$clauses['join'] .= <<<SQL
    LEFT OUTER JOIN {$wpdb->term_relationships} ON {$wpdb->posts}.ID={$wpdb->term_relationships}.object_id
    LEFT OUTER JOIN {$wpdb->term_taxonomy} USING (term_taxonomy_id)
    LEFT OUTER JOIN {$wpdb->terms} USING (term_id)
    SQL;
    
    		$clauses['where'] .= " AND (taxonomy = 'lez_gender' OR taxonomy IS NULL)";
    		$clauses['groupby'] = "object_id";
    		$clauses['orderby']  = "GROUP_CONCAT({$wpdb->terms}.name ORDER BY name ASC) ";
    		$clauses['orderby'] .= ( 'ASC' == strtoupper( $wp_query->get('order') ) ) ? 'ASC' : 'DESC';
    	}
    
    	return $clauses;
    }
    add_filter( 'posts_clauses', 'lez_gender_clauses', 10, 2 );
    

    I repeat this with ‘gender’ changed to ‘sex’ and ‘lez_gender’ changed to ‘lez_sexuality’ for the other taxonomy.

    I tested this on 921 posts in a custom post type and it didn’t make my server cry. Which is a bonus.

  • Custom Columns Search: Not unique table/alias

    Custom Columns Search: Not unique table/alias

    After making Custom Sortable Columns everything was awesome, right?

    Well… Mostly.

    When I tried to sort a search, I got this:

    WordPress database error: [Not unique table/alias: 'wp_postmeta']
    SELECT SQL_CALC_FOUND_ROWS DISTINCT wp_posts.ID 
    FROM wp_posts INNER JOIN wp_postmeta ON ( wp_posts.ID = wp_postmeta.post_id ) 
    LEFT JOIN wp_postmeta ON wp_posts.ID = wp_postmeta.post_id WHERE 1=1 
    AND (((wp_posts.post_title LIKE '%kate%') OR ( (wp_postmeta.meta_key 
    IN ( 'lezchars_actor', 'lezshows_worthit_details', 'lezshows_plots', 
    'lezshows_episodes', 'lezshows_realness_details', 'lezshows_quality_details', 
    'lezshows_screentime_details' ) ) AND (wp_postmeta.meta_value LIKE '%kate%') ) 
    OR (wp_posts.post_excerpt LIKE '%kate%') OR (wp_posts.post_content LIKE '%kate%'))) 
    AND ( wp_postmeta.meta_key = 'lezchars_type' ) AND wp_posts.post_type = 'post_type_characters' 
    AND (wp_posts.post_status = 'publish' OR wp_posts.post_status = 'future' OR 
    wp_posts.post_status = 'draft' OR wp_posts.post_status = 'pending' OR 
    wp_posts.post_status = 'private') GROUP BY wp_posts.ID ORDER BY 
    wp_postmeta.meta_value DESC LIMIT 0, 20
    

    Not good.

    And not actually (really) related to the sortability! No no, I did this to myself by adding in optimized post-meta search. You see, that runs on the front and the back end of WordPress.

    I knew it had to be the search tweaks I’d made when I saw this:

    OR ( (wp_postmeta.meta_key IN ( 'lezchars_actor', 'lezshows_worthit_details', 
    'lezshows_plots', 'lezshows_episodes', 'lezshows_realness_details', 
    'lezshows_quality_details', 'lezshows_screentime_details' ) )
    

    Right away I realized it had to be my custom queries both calling from wp_postmeta and, in doing so, stemming all over each other. Not good at all.

    The fix was relatively simple. All I had to do was move the filters to run only if we’re not in wp-admin.

    /**
     * Only run if we're NOT in the admin screen!
     */
    
    if ( ! is_admin() ) {
    	add_filter( 'posts_join', 'lezwatch_search_join' );
    	add_filter( 'posts_where', 'lezwatch_search_where' );
    	add_filter( 'posts_distinct', 'lezwatch_search_distinct' );
    }
    

    This meant that the internal post search wouldn’t query those fields, but in the grand scheme of things, that’s okay. Generally when I’m looking for a post in the search, I’m looking for it by title. If I wanted to be more particular, I could have it not use my custom joins when I’m on a post listing page, but allow it if I’m editing a post (so adding a new link would search all that content too).

    For now, this is what I need.

  • Sortable Custom Columns

    Sortable Custom Columns

    I have a lot of custom columns going on over at LezWatch TV. And there are a million tutorials on how to make your columns sortable. So here’s another one.

    Backstory

    I have a custom post type for TV characters, and the characters have extra meta data. The kind I want to show is the TV show (or shows) associated with the character and the role type.

    I should note that ‘role type’ can be weird, since there are characters who will be guests on one show and regulars on the other, and really I should go back and change all of that but when you get to 1000 characters, you really don’t want to… Anyway. Let’s just do this. Keep in mind the following things.

    1. The custom post type was registered as post_type_characters
    2. The data I wish to list is ‘tv shows’ and ‘role type’
    3. TV Shows is an array of IDs related to another post type (shows), stored in a meta key called lezchars_show
    4. Role Type is a plain text value stored in lezchars_type

    Now we’re ready to go

    New Column Headers

    We want to make a new column for your custom post types. For this to work, you need to know the name of your custom post type – post_type_characters remember – and the format for the column code – manage_{POSTTYPE}_posts_columns … See how that works?

    // Add Custom Column Headers
    add_filter( 'manage_post_type_characters_posts_columns', 'set_custom_edit_post_type_characters_columns' );
    function set_custom_edit_post_type_characters_columns($columns) {
    	$columns['cpt-shows']		= 'TV Show(s)';
    	$columns['postmeta-roletype']	= 'Role Type';
    	return $columns;
    }
    

    The name of the columns (cpt-shows and postmeta-roletype) are semi-arbitrary. I picked cpt-TYPE and postmeta-META because they were logical to me. I always use those formats.

    Column Content

    Giving the column content is a little more difficult because I’m listing TV shows associated with a character. Plural. And some characters are on three shows. Usually people are only on one, thankfully.

    This time the function name is going to be based on manage_{POSTTYPE}_posts_custom_column

    // Add Custom Column Content
    add_action( 'manage_post_type_characters_posts_custom_column' , 'custom_post_type_characters_column', 10, 2 );
    function custom_post_type_characters_column( $column, $post_id ) {
    	// Since SOME characters have multiple shows, we force this to be an array
    	if ( !is_array( get_post_meta( $post_id, 'lezchars_show', true ) ) ) {
    		$character_show_IDs = array( get_post_meta( $post_id, 'lezchars_show', true ) );
    	} else {
    		$character_show_IDs = get_post_meta( $post_id, 'lezchars_show', true );
    	}
    
    	// Show Title is an array to handle commas
    	$show_title = array();
    
    	foreach ( $character_show_IDs as $character_show_ID ) {
    		array_push( $show_title, get_post( $character_show_ID )->post_title );
    	}
    
    	switch ( $column ) {
    		case 'cpt-shows':
    			echo implode(", ", $show_title );
    			break;
    		case 'postmeta-roletype':
    			echo ucfirst(get_post_meta( $post_id, 'lezchars_type', true ));
    			break;
    	}
    }
    

    And this results in this:

    Character Custom Post Types with Special Columns

    Making it Sortable

    But as you’ve noticed, it’s not actually sortable. That is, if I have one character with no ‘role’, I have to surf through 47 pages until I find her. So the magic here is to make these columns sortable.

    This time the filter is named manage_edit-{POSTTYPE}_sortable_columns and there’s some extra mess going on.

    // Make columns sortable
    add_filter( 'manage_edit-post_type_characters_sortable_columns', 'sortable_post_type_characters_column' );
    function sortable_post_type_characters_column( $columns ) {
    	unset( $columns['cpt-shows'] ); 	  // Don't allow sort by shows
    	$columns['postmeta-roletype']	= 'role'; // Allow sort by role
        return $columns;
    }
    

    The column names are required from the previous steps, but the sort by being ‘role’ is something I made up. It doesn’t have to match anything from before however it has to be remembered, as we’re going to re-use it in a moment.

    You see, I have to tell it what ‘sortable’ means by filtering pre_get_posts. I’m grabbing the meta value for lezchars_type and I’m going to tell it to order by the value. This happens to be alphabetical. If I was using a number, it would have to be meta_value_num instead of meta_value.

    // Create Role Sortability
    function post_type_characters_role_orderby( $query ) {
    	if( ! is_admin() ) return;
    
    	if ( $query->is_main_query() && ( $orderby = $query->get( 'orderby' ) ) ) {
        	switch( $orderby ) {
    			case 'role':
    				$query->set( 'meta_key', 'lezchars_type' );
    				$query->set( 'orderby', 'meta_value' );
    				break;
    		}
    	}
    }
    

    Remember how I said to remember we called the order-by ‘role’? Well this was why. The case check is on ‘role’ but also the meta_key is our key name.

    And yes it works:

    Showing sorted columns