Half-Elf on Tech

Thoughts From a Professional Lesbian

Tag: wordpress

  • Caching Dismissible Alerts With localStorage

    Caching Dismissible Alerts With localStorage

    There are a lot of reasons to want to have a temporary, but dismissible, alert on the front end of your website. And there are a million different ways to do that. The trick is … how do you do that and make your site remain cacheable?

    Web Storage (Maybe) Is the Answer

    I preface this with a warning. I’m going to be using Local Storage, and that is not something I generally advocate. Web storage (aka DOM storage) is the practice of storing data in the browser. You can do this in a few other ways, such as sessions or cookies. But both of those can be detrimental to caching. After all, PHP Sessions and cookies tell systems like Varnish and Nginx “Don’t cache this.” They’re also persistent, so if I get a cookie once, I keep it until it expires or I delete it.

    On the other hand, web storage is outside the website entirely, that is my server has no idea about them, and it doesn’t really impact caching at all. Varnish, Nginx, and all those sorts of server-based services don’t care about it, and if implemented properly, there’s no problem. You can store the data locally or per-session, which means ‘forever’ or ‘for as long as this browser window is open.’

    Don’t Use localStorage (Most of the Time)

    That all sounded awesome. So then why are there so many articles advocating you not use localStorage? Well there are some massive caveats:

    1. It’s pure javascript, so PHP doesn’t easily get it
    2. If you store sensitive data in it, you’re a numpty
    3. It can ‘only’ store 5 megs
    4. The content has no expiration
    5. You can only use string data
    6. All your localStorage is loaded on every single page load
    7. Any other javascript can see it and play with it

    That’s starting to sound squidgy, right? Randall Degges has a good writeup of the drawbacks.

    Well good news here. This use is possibly the only time I’ll ever advocate it’s use. I’m going to us it here because it works with caching, it works with most browsers (IE8+), and the worst case scenario here is that people will always see my alert.

    Pull Up With Bootstrap

    I’m using Bootstrap on this particular site, and it makes my life hella easy because they built in dismissible alerts. But the gotcha? Those alerts aren’t persistent. Which means if I want an alert to go away forever, then I’m SOL.

    Except I’m not. Bootstrap includes a way to run an ‘action’ on dismiss, which means I could do something like this:

    jQuery(document).ready(function($) {
        var gdpr = localStorage.getItem('gdpr-alerted') || '';
    
        if (gdpr = 'yes') {
            $('#myAlert').alert('close');
        }
    
        $('#myAlert').on('closed.bs.alert', function () {
           localStorage.setItem('gdpr-alerted','yes');
        })
    }
    

    What this does is it sets a variable (gdpr) based on the content of my item in localStorage (gdpr-alerted). If that value is yes, it closes the alert. Otherwise, it sets it to yes when someone closes the alert.

    That actually works just fine, but it has a weird blip if the javascript is loaded in the footer, where you would see my alert for a split second before the page finished loading. So I decided to go another way, and factor in some expirations.

    The (GDPR Related) Code

    First up the PHP:

    function my_gdpr_footer(){
    	if ( !is_user_logged_in() ) {
    		?>
    		<div id="GDPRAlert" class="alert alert-info alert-dismissible fade collapse alert-gdpr" role="alert">
    			This site uses cookies for stuff. Keep browsing and we keep tracking. Wanna know more? <a href="/terms-of-use">Read our Terms of Use</a>.
    			<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
    		</div>
    		<?php
    	}
    }
    add_action( 'wp_footer', 'my_gdpr_footer', 5 );
    
    function my_scripts() {
    	wp_enqueue_script( 'bootstrap', get_template_directory_uri() . '/inc/bootstrap/js/bootstrap.min.js', array( 'jquery' ), '4.1.1', 'all', true );
    	wp_enqueue_script( 'lwtv-gdpr', get_template_directory_uri() . '/inc/js/gdpr.js', array( 'bootstrap' ), '1.0', 'all', true );
    }
    add_action( 'wp_enqueue_scripts', 'my_scripts' );
    

    Now my sneaky JS:

    // jQuery dismissable - sets localStorage a year
    
    jQuery(document).ready(function($) {
    	var gdpr = localStorage.getItem('gdpr-alerted') || '';
    	var year = new Date().setFullYear(new Date().getFullYear() + 1);
    
    	if (gdpr < new Date()) {
    		$('#GDPRAlert').addClass('show');
    		localStorage.removeItem('gdpr-alerted');
    	}
    
    	$('#GDPRAlert').on('closed.bs.alert', function () {
    		localStorage.setItem('gdpr-alerted',year);
    	})
    });
    

    The reason this works is that I remove the show class from the PHP and by default assume it shouldn’t show. Then in the javascript, I set up the value as ‘a year from now’ instead of ‘yes’, and check. If the localStorage value is less than ‘now’, then it’s expired and we should show the class (and delete the storage). Otherwise, it’s the same old same old.

    But Should We?

    That’s the big question though. Is this the best way to go about it? The next best choice would be IndexedDB, however that is not supported by ‘enough’ browsers yet, so yes. For now, this is the best choice.

  • How Many Plugins Is Too Many To Create?

    How Many Plugins Is Too Many To Create?

    That wasn’t the way you expected that title to end, I bet. You were thinking “How many plugins is too many to have on my site” and that’s absolutely not the topic here. No, instead I’m asking how many plugins is too many for a developer to create?

    I Got 99 Problems …

    I think that plugins should be specific. That is, I’m not a fan of a conglomerate of plugins like Jetpack, that do a little of everything. Instead, I like a plugin that does the thing it’s supposed to do, preferably simply and well, and it moves on. That means I often have 20-30 plugins installed on a site, and that’s okay.

    At the same time, as a developer, having to support 20-30 plugins is a drain on my limited resources. Becuase here’s what I have to do:

    1. Keep up with all core changes
    2. Include and test all library updates
    3. Test with every release
    4. Update my readme
    5. Review reviews and support posts to make sure I’m not missing things

    Multiply that by 20 and it’s a lot of work. And is that work I feel like I must do?

    Gimmie One Reason …

    The reality of having plugins for WordPress, or any add on for any project, is that it’s generally thankless work and you will have more bad days than good. That’s true of many things in life, and as depressing as it can be, it’s important to keep an eye on the reality of the situation. 

    Developing software is very analytical art. You create something out of nothing, you design and test and change and tweak, and then present it to the world. Of course those days when people tell you “I don’t like the color” suck, but being humans, we discard that and grab on to the days when someone says “I love the carrot!”

    And the reality of the question at hand isn’t how many is too many, but how many are worth the work and the little reward?

    Bring it Together …

    Lately I’ve been advocating something different. Instead of making 13 separate types of gallery plugins, I’ve suggested people make one plugin for galleries and include those 13 types as display options. The amount of work is roughly the same, but it means I only have one plugin to manage instead of 13 separate readme files to edit and installs to spin up. I also have one place to look for any support posts or reviews.

    Obviously this doesn’t always work. Sometimes you have to split things up. There’s little point it combining a WooCommerce plugin with a NextGen Gallery one (unless the plugin is implementing NextGen with Woo products…). But if you can connect your projects by type, you may find out that there’s crossover. Instead of spreading your user base out over 10 plugins, you can keep them manageable with 5 to 8.

    Working For The Man …

    And what about Jetpack? It’s effectively XX separate types of plugins:

    • Writing
    • Sharing
    • Discussion
    • Traffic
    • Security

    Except when you look at that, it suddenly all connects. When I write I want to share and I want people to discuss. I also want to keep an eye on my traffic and being secure…. Okay that last one might be better off on it’s own, but it’s a suite of related apps. 43 separate apps, but they are all related when you get down to it.

    Which means even if you’re making a plugin for your company, you can probably combine it with other things safely. And that means less access and security concerns for you too, as you only have to keep track of who has access to one plugin instead of 50.

    How Many Is Too Many?

    This is as subjective as all get out, but I’ll say this. Once you personally support 20 plugins for WordPress, take a good hard look at how much time you’re spending and ask yourself… is it worth it?

    It’s okay to say no.

  • Future Proof Names

    Future Proof Names

    The other day I was talking to someone about the name of their plugin.

    No, not about copyright and infringement, though that comes up a lot. I was talking to them about the meaning of the plugin name. They wanted to pick a name that was memorable, meaningful, and descriptive. I wanted them to drop the last one. Or rather, to reconsider the last one.

    Names Aren’t Descriptions

    My name, Mika, is not a description, it’s an identifier. Even my handle, Ipstenu, is an identifier. My domain ‘HalfElf.org’ actually is a bit of a descriptor, but it’s also an identifier. I am a Half Elf Rogue to many people, and that’s as it should be. But a descriptor is “Professional Lesbian” not “Half Elf on Tech” and yes, this matters.

    A good name is memorable (check), meaningful (check), and descriptive without being a description. Because a name is how we identify and remember the weird tech site we went to, versus the tools we use.

    In so far as plugins go, however, you have one more thing to think about, and that’s the plugin slug. The slug is like this post’s URL. Changing it comes at a cost and in the case of plugins, it’s impossible. So while I’m content to allow people to pick some silly names, I’m pretty sticky about the plugin url.

    Woo(Commerce) There It Is

    Whenever people submit plugins named “WooCommerce Thingy” they find their slugs changed to woo-thingy instead of the expected woocommerce-thingy because, well, they’re not WooCommerce. This sometimes incurs their ire, generally because they didn’t read the FAQ, and I’ve become resigned to linking them back to it. Most of the time, people just go “Oh, okay I get it.” and move on. Sometimes when the slug is less easy to ‘correct’ for them, like if they call a plugin “Google Fast Typing” (slug google-fast-typing) I have to email them and sort out a new name (probably “Fast Typing for Google” unless “Fast Typing” is trademarked by the Googs).

    Like I mentioned before, I don’t really push too much about a display name. You can change it as many times as you want, and coming back in a month to remind you “Hey, if you’re not Google, you shouldn’t start your plugin display name with ‘Google.’ They may get snarky at you.”

    But the display name is also very much abused. People use it as an extra ‘short’ description, which really only goes to annoy people who see “Luxembourg – A new plugin that will solve all your woes!” as a plugin name. Thanks. But that’s what the description is for.

    Gutenbye-bye-bye

    Gutenberg is a very popular plugin to make an add-on for these days. And a lot of people want to name their plugins things like “Joe’s Author Blocks for Gutenberg” which nets them the slug joes-author-blocks-for-gutenberg and I don’t know about you, but I find that excessive.

    See, you don’t want to have “Gutenberg” in your plugin slug at all. Unlike WooCommerce (or Google, or Facebook), we know Gutenberg is going to go away. Remember, the goal of Gutenberg is to be in WordPress 5.0!

    So that means naming your plugin author-blocks-for-gutenberg is shortsighted. In another four years, will anyone remember Gutenberg was the project name? Quick way to check, ask people at a local meetup “What’s MP6?” and see who knows.

    Name Better

    Think about the future of your plugin, the project, and the related items. Locking yourself into a name you regret later is one thing, but since a display name can be anything, you can name your plugin anything! Making a feature rich Gutenberg add on? Manutius is a great name! Changing the look entirely? Call it Reformation. Making a simple author block editor? halfelf-author-blocks is an okay slug, but “Block Your Authors” sounds pretty bad.

    Just remember, it’s eye before flea, except after sea.

    Someone carving hieroglyphics, when someone reminds him "It's eye before flea, except after sea."
  • It’s Just Math: WP-CLI Edition

    It’s Just Math: WP-CLI Edition

    Remember how I talked about doing math on a post when it was saved?

    Well what if I wanted to run that at another time? Like what if I knew I needed to update one show and I didn’t want to go in and save the post?

    I can to this with WP-CLI:

    WP CLI Magic

    class WP_CLI_MySite_Commands extends WP_CLI_Command {
    	/**
    	 * Re-run calculations for specific post content.
    	 * 
    	 * ## EXAMPLES
    	 * 
    	 *		wp mysite calc actor ID
    	 *		wp mysite calc show ID
    	 *
    	*/
    	
    	function calc( $args , $assoc_args ) {
    
    		// Valid things to calculate:
    		$valid_calcs = array( 'actor', 'show' );
    		
    		// Defaults
    		$format = ( isset( $assoc_args['format'] ) )? $assoc_args['format'] : 'table';
    
    		// Check for valid arguments and post types
    		if ( empty( $args ) || !in_array( $args[0], $valid_calcs ) ) {
    			WP_CLI::error( 'You must provide a valid type of calculation to run: ' . implode( ', ', $valid_calcs ) );
    		}
    
    		// Check for valid IDs
    		if( empty( $args[1] ) || !is_numeric( $args[1] ) ) {
    			WP_CLI::error( 'You must provide a valid post ID to calculate.' );
    		}
    
    		// Set the post IDs:
    		$post_calc = sanitize_text_field( $args[0] );
    		$post_id   = (int)$args[1];
    
    		// Last sanitity check: Is the post ID a member of THIS post type...
    		if ( get_post_type( $post_id ) !== 'post_type_' . $post_calc . 's' ) {
    			WP_CLI::error( 'You can only calculate ' . $post_type . 's on ' . $post_type . ' pages.' );
    		}
    
    		// Do the thing!
    		// i.e. run the calculations
    		switch( $post_calc ) {
    			case 'show':
    				// Rerun show calculations
    				MySite_Show_Calculate::do_the_math( $post_id );
    				$score = 'Score: ' . get_post_meta( $post_id, 'shows_the_score', true );
    				break;
    			case 'actor':
    				// Recount characters and flag queerness
    				MySite_Actor_Calculate::do_the_math( $post_id );
    				$queer = ( get_post_meta( $post_id, 'actors_queer', true ) )? 'Yes' : 'No';
    				$chars = get_post_meta( $post_id, 'actors_char_count', true );
    				$deads = get_post_meta( $post_id, 'actors_dead_count', true );
    				$score = ': Is Queer (' . $queer . ') Chars (' . $chars . ') Dead (' . $deads . ')';
    				break;
    		}
    
    		WP_CLI::success( 'Calculations run for ' . get_the_title( $post_id ) . $score );
    	}
    }
    

    What the What?

    The one for shows is a lot simpler. I literally call that do_the_math() function with the post ID and I get back a number. Then I output the number and I’m done. If I wanted it to run for all shows, I could use WP-CLI to spit out a list of all the IDs and then pass them to the command one at a time. Or I could write one that does ‘all’ posts. Which I may

    But the point is that I now can type wp mysite calc show 1234 and if post ID 1234 is a show, it’ll run.

  • jQuery Tablesorter

    jQuery Tablesorter

    So you want to sort tables on your WordPress site, without learning a whole mess of code.

    Good news. There’s a plugin called Table Sorter that can do this and in pretty much just works. Except… There are cases where you’re outputting data in a theme (or a plugin) and you can’t use that plugin to do it.

    Don’t panic, we’ve got you.

    Enqueue The Right Tablesorter

    I’m aware there’s a tablesorter.com – Don’t use it. It’s out of date at the site is possibly hacked. Instead, grab Tablesorter from RobG (aka mottie). Rob is still updating this plugin and debugging it, so it’s a better bet that the various other forks.

    You’ll enqueue this the ‘normal’ way:

    wp_enqueue_script( 'tablesorter', plugins_url( 'jquery.tablesorter.js', __FILE__ ), array( 'jquery' ), '2.30.5', false );
    

    There’s a second part to the enqueue though. You see you also need to tell it what to sort. That is, tell the javascript what to pay attention to.

    That’s done by using a class and an ID: <table id="myTable" class="tablesorter"></table>

    If you’re using the plugin I mentioned above, you only have to do the latter, because it accounts things differently but, properly, you should be using the ID. Then you have to insert this javascript:

    $(function() {
      $("#myTable").tablesorter();
    });
    

    Which is actually wrong. For WordPress. Again, no panicking!

    jQuery(document).ready(function($){
      $("#myTable").tablesorter();
    });
    </script>
    

    See? That was easy. If you wanted to be more WordPressy, you do this:

    wp_add_inline_script( 'tablesorter', 'jQuery(document).ready(function($){ $("#nationsTable").tablesorter(); });' );
    

    You were expecting more?

    That’s really it. I do some extra weird stuff, since I call it on one page only (statistics) and that pages uses query variables so you can have /statistics/nations/ without me needing to make multiple sub pages, and it looks like this:

    	function enqueue_scripts() {
    
    		if ( is_page( array( 'statistics' ) ) ) {
    			$statistics = get_query_var( 'statistics', 'none' );
    			wp_enqueue_script( 'tablesorter', plugin_dir_url( dirname( __FILE__ ) ) . 'assets/js/jquery.tablesorter.js' , array( 'jquery' ), '2.30.5', false );
    			wp_enqueue_style( 'tablesorter', plugin_dir_url( dirname( __FILE__ ) ) . 'assets/css/theme.bootstrap_4.css' );
    
    			switch( $statistics ) {
    				case 'nations':
    					wp_add_inline_script( 'tablesorter', 'jQuery(document).ready(function($){ $("#nationsTable").tablesorter({ theme : "bootstrap", }); });' );
    					break;
    				case 'stations':
    					wp_add_inline_script( 'tablesorter', 'jQuery(document).ready(function($){ $("#stationsTable").tablesorter({ theme : "bootstrap", }); });' );
    					break;
    			}
    		}
    	}
    

    Oh right that also demonstrates a theme!

    Tablesorter lets you use themes like Bootstrap 4.x so your tables can be all matchy-matchy.

    But at this point, it should be enough to get your tables sorted.

  • CMB2: Conditional Meta Fields

    CMB2: Conditional Meta Fields

    Even though Gutenberg is on the rise, and every day gets us closer to using a whole new editor, we still use the ‘classic’ editor and we’re still beholden to it’s space limitations. I have strong feelings about how to properly utilize space when using CMB2, but not included in that specific post is this.

    Fields You Don’t (Always) Need

    There are three types of fields for CMB2.

    1. Fields you always need to use
    2. Fields that are optional
    3. Fields that are needed only in specific situations

    Most of the time we use option 2. Option 3 is the tricky one, though, since most of the time we end up having things show based on actions. That is, I save a post, and new options show up. When it comes to CMB2, we really don’t want to have to save and then edit and save and edit.

    Yuck!

    Thankfully this is all possible.

    Practical Example: Affiliates

    Today we’re making a box to handle affiliate links. There are three types of links: Amazon, Genric, and ‘Unique.’

    The Amazon one will link directly to a signup link and the generic one links to the same thing only on Click Junction. Both of those links will have affiliate details backed in so I don’t have to look them up later. This also means any partners I have on the site don’t need to know all the gory details. Bazinga.

    The last one, though ‘Unique’ is tricky. You see, when someone picks that, I want them to be able to put in a specific URL that may be affiliate linking somewhere else. But let’s start out with how it normally works.

    Make Your Fields

    function my_site_cmb2_metaboxes() {
    	// prefix for all custom fields
    	$prefix = 'my_sites_';
    
    	// Metabox Group: Must See
    	$cmb_affiliate = new_cmb2_box( array(
    		'id'           => 'affiliate_metabox',
    		'title'        => __( 'Affiliate Details', 'my-domain' ),
    		'object_types' => array( 'post_type_shows' ),
    		'show_in_rest' => true,
    	) );
    
    	// Field Box: Affiliate Type
    	$field_affiliatetype = $cmb_affiliate->add_field( array(
    		'name'             => __( 'Type', 'my-domain' ),
    		'id'               => $prefix . 'affiliate',
    		'type'             => 'select',
    		'options'          => array( 
    			'amazon'  => 'Amazon',
    			'generic' => 'Generic',
    			'url'     => 'Unique Link',
    		);
    		'show_option_none' => true,
    	) );
    	// Field Box: Affiliate Links
    	$field_affiliateurl = $cmb_affiliate->add_field( array(
    		'name'    => __( 'Link', 'my-domain' ),
    		'id'      => $prefix . 'affiliateurl',
    		'type'    => 'text_url',
    	) );
    );
    

    That’s pretty normal for CMB2 and looks like this:

    CMB2: Affiliate Details

    Normal, but … I want to hide that Link field unless a specific option is selected. Enter javascript.

    Hide That Field (Conditionally)

    Make a file for your javascript. I’ve called mine cmb2.js and put it in the same folder as my file that will enqueue the scripts.

    // Either create a new empty object, or work with the existing one.
    window.MySite_CMB2 = window.MySite_CMB2 || {};
    
    (function( window, document, $, app, undefined ) {
    	'use strict';
    
    	app.cache = function() {
    		app.$ = {};
    		app.$.select = $( document.getElementById( 'my_sites_affiliate' ) );
    		app.$.field = $( document.getElementById( 'my_sites_affiliateurl' ) );
    		app.$.field_container = app.$.field.closest( '.cmb-row');
    	};
    
    	app.init = function() {
    		app.cache();
    		app.$.select.on( 'change', function( event ) {
    			if ( 'url' === $(this).val() ) {
    				app.$.field_container.show();
    			} else {
    				app.$.field_container.hide();
    			}
    		} ).trigger( 'change' );
    	};
    
    	$( document ).ready( app.init );
    })( window, document, jQuery, MySite_CMB2 );
    

    And then call the enqueue, but only when appropriate (because we want to keep the load low):

    add_action( ‘admin_enqueue_scripts’, ‘my_site_admin_enqueue_scripts’ );

    function my_site_admin_enqueue_scripts( ) {
    $screen = get_current_screen();
    if ( ! isset( $screen->post_type ) || ‘post_type_shows’ !== $screen->post_type ) return;

    wp_enqueue_script( 'custom-js', plugins_url( '/cmb2.js' , __FILE__ ), array( 'jquery' ) );
    

    }

    And that works like this:

    GIF of how it shows and hides things

    A Word of Warning

    While all this is great for the sighted people, hiding things is not actually all that great for those who use screen-readers. For that you’d want to toggle the field to be disabled.