Half-Elf on Tech

Thoughts From a Professional Lesbian

Category: How To

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

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

  • Cron Caching

    Cron Caching

    WordPress' relationship with cron is touchy. It has it's own version, wp-cron which isn't so much cron as a check when people visit your site of things your site needs to do. The problem is that if no one visits your site… nothing runs. That's why you sometimes have posts that miss schedules.

    One possible solution is to use what we call 'alternate cron' to trigger your jobs. That works pretty well as it means I can tell a server "Every 10 minutes, ping my front page and trigger events."

    But in this case, I didn't want that. I receive enough traffic on this site that I felt comfortable trusting in WP cron, so what I wanted was every hour for a specific page to be visited. This would prompt the server to generate the cached content if needed (if not, it just loads a page).

    WordPress Plugin

    I'm a huge proponent of doing things the WordPress way for WordPress. This method comes with a caveat of "Not all caching plugins will work with this."

    I'm using Varnish, and for me this will work, so I went with the bare simple code:

    class LWTV_Cron {
    
    	public $urls;
    	
    	/**
    	 * Constructor
    	 */
    	public function __construct() {
    
    		// URLs we need to prime the pump on a little more often than normal
    		$this->urls = array(
    			'/statistics/',
    			'/statistics/characters/',
    			'/statistics/shows/',
    			'/statistics/death/',
    			'/statistics/trends/',
    			'/characters/',
    			'/shows/',
    			'/show/the-l-word/',
    			'/',
    		);
    
    		add_action( 'lwtv_cache_event', array( $this, 'varnish_cache' ) );
    
    		if ( !wp_next_scheduled ( 'lwtv_cache_event' ) ) {
    			wp_schedule_event( time(), 'hourly', 'lwtv_cache_event' );
    		}
    	}
    
    	public function varnish_cache() {
    		foreach ( $this->urls as $url ) {
    			wp_remote_get( home_url( $url ) );
    		}
    	}
    
    }
    
    new LWTV_Cron();

    Yes it's that site. This very simple example shows that I have a list of URLs (slugs really) I know need to be pinged every hour to make sure the cache is cached. They're the slowest pages on the site (death can take 30 seconds to load) so making sure the cache is caught is important.

  • Deploying from Github via TravisCI

    Deploying from Github via TravisCI

    When you develop your code on Git, you can automagically (and easily) deploy to places all you want, if the repository is on the same server. If it's not, you can use something like Codeship to automate pushing for you.

    Free Services Have Limits

    Codeship, which I like a lot, has a limit of 100 pushes a month. In October, when Tracy and I finally deployed the newest version of our website, we actually hit that. In part, this is because we don't have a great 'test' environment where we can internally develop and share the results with the other. We both have our own versions of local environments, but you can't show your cohort 3000 miles away what you've done without pushing the code to the private development site and letting her see it.

    After 100 pushes, it's $490 a year for unlimited. While the product claims to be 'free forever' for open source, there's actually no documentation that I could find on how one gets added to that list, or what the qualifications are. Does my personal open source project  qualify? I'll have to email and find out.

    TravisCI Is ‘Free’

    All 'free' services have a paid component. Travis, like Codeship, is free for public, open source, projects. And like Codeship, it doesn't require you to host (and thus) update anything yourself. Which is nice. In fact, it only has a few pre-requsits:

    Sounds like a match made in heaven, except for the part about documentation on GitHub being out of date. I don't begrudge them, as keeping up docs is a pain and if it's with another service it's nigh impossible.

    Awesome. Sign up for Travis, activate your repositories, add a .travis.yml file with the programing language, and you're ready to do… what?

    Writing The Build Script

    This is the weird part. You have to invent a way to push the code. Unlike DeployHQ or Codeship, there's no place to type in the code on their servers. You have to make a file and write the script.

    The scripts look like this (cribbed from Florian Brinkkman):

    language: php
    
    addons:
      ssh_known_hosts:
      - $DEVELOPMENT_SERVER
      - $PRODUCTION_SERVER
    
    before_script:
      - echo -e "Host $DEVELOPMENT_SERVERntStrictHostKeyChecking non" >> ~/.ssh/config
      - echo -e "Host $PRODUCTION_SERVERntStrictHostKeyChecking non" >> ~/.ssh/config
    
    script:
      -
    before_deploy:
      - openssl aes-256-cbc -K $ENCRYPTED_KEY -iv $ENCRYPTED_IV -in deploy_rsa.enc -out /tmp/deploy_rsa -d
      - eval "$(ssh-agent -s)"
      - chmod 600 /tmp/deploy_rsa
      - ssh-add /tmp/deploy_rsa
    
    deploy:
      - provider: script
        skip_cleanup: true
        script: ssh -p22 $DEVELOPMENT_SERVER_USER@$DEVELOPMENT_SERVER "mkdir -p $DEVELOPMENT_PATH_STABLE" && ssh -p22 $DEVELOPMENT_SERVER_USER@$DEVELOPMENT_SERVER "mkdir -p $DEVELOPMENT_PATH_TRUNK" && rsync -rav -e ssh --exclude='.git/' --exclude=scripts/ --exclude='.travis.yml' --delete-excluded ./ $DEVELOPMENT_SERVER_USER@$DEVELOPMENT_SERVER:$DEVELOPMENT_PATH_TRUNK && rsync -rav -e ssh --exclude='.git/' --exclude=scripts/ --exclude='.travis.yml' --delete-excluded ./ $DEVELOPMENT_SERVER_USER@$DEVELOPMENT_SERVER:$DEVELOPMENT_PATH_STABLE
        on:
          branch: DEVELOPMENT
      - provider: script
        skip_cleanup: true
        script: ssh -p22 $PRODUCTION_SERVER_USER@$PRODUCTION_SERVER "mkdir -p $PRODUCTION_PATH_STABLE" && rsync -rav -e ssh --exclude='.git/' --exclude=scripts/ --exclude='.travis.yml' --delete-excluded ./  $PRODUCTION_SERVER_USER@$PRODUCTION_SERVER:$PRODUCTION_PATH_STABLE
        on:
          branch: master

    And to be honest, it's really not that explanatory. I read it a few times and sighed. While I'm (obviously) not opposed to learning new code to do things, I am opposed to all these services making it needlessly complicated.

    One Big Problem…

    You can't (easily) set up SSH keys on Travis for free. That's because they're restricted to the pro version. Now you totally can set it up, but it's incredibly insane and not something I was willing to do in the long term. And since the cost of TravisPro is $69 a month compared to Codeship's $49 or so a month, it was a no brainer.

    I emailed Codeship to ask if I qualified for 'open source.' Most likely they'll tell me no, because I deploy to a closed system, but it doesn't hurt to ask.

  • Indiegogo Embed

    Indiegogo Embed

    Indiegogo doesn't have oEmbed, which means you can't just paste in a URL and expect it to work on a WordPress site. And worse, their directions are "Use an iframe plugin!"

    NO.

    Just say NO to iframe plugins!

    Use a shortcode instead!

    If you're brand new to shortcodes, check out Sal Ferrarello's awesome post about it (I saw him talk at WC Philly 2017 and he's amazing). I'll give you the highlights for this one code though.

    The Code

    /*
     * Embed an IndieGoGo Campaign
     *
     * Usage: [indiegogo url="https://www.indiegogo.com/projects/riley-parra-season-2-lgbt"]
     *
     * Attributes:
     *		url: The URL of the project
     */
    add_shortcode( 'indigogo', 'helf_indiegogo' );
    function helf_indiegogo() {
    	$attr = shortcode_atts( array(
    		'url' => '',
    	), $atts );
    	
    	$url    = esc_url( $attr['url'] );
    	$url    = rtrim( $url, "#/");
    	$url    = str_replace( 'projects/', 'project/', $url );
    	$return =  '<iframe src="' . $url . '/embedded" width="222px" height="445px" frameborder="0" scrolling="no"></iframe>';
    
    	return $return;
    }
    

    What It Does

    The code is as basic as I could made it, and it takes the URL of the campaign, strips out the trailing hashtag, changes projects to project (singular – and yes, that gave me a headache), and punts out an iframe.

    Thankfully they make this easy unlike places like CrowdRise where you have to magically know the ID number in order to pull this off.

  • The Never-ending Progress Bar

    The Never-ending Progress Bar

    When you download a file from Safari, it shows you a progress bar for how it's going along. If you happen to download files to a folder in your dock, you'll see a line grow as it downloads, and then vanish.

    Except… sometimes it doesn't.

    A download bar that won't go away

    And then it gets worse if your bar changes size…

    A download bar that looks worse becuase it's not connected to the folder

    Well great. Now what?

    To the terminal!

    No really. It's one command:

    killall Dock

    That's it. It restarts the dock, the download bar goes away, and you can relax.