Half-Elf on Tech

Thoughts From a Professional Lesbian

Author: Ipstenu (Mika Epstein)

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

  • Sharing WordPress Content with Hugo

    Sharing WordPress Content with Hugo

    When your life is just WordPress, there’s not a lot of headaches involved in making some posts and updating widgets and keeping your whole site in sync.

    When your life isn’t just WordPress, it gets a little weird.

    When you want to use WordPress to run your life a little more, you un-weird it by making it weirder.

    Rethinking Where the Content Lives

    Normally we think about content living on its own site. Well, I have a ‘message’ that needs to be the same on five different domains. Just work with me here. The point is, if I want to update the header message on the five sites, I have to update five sites. Yuck.

    Now, there are a lot of solutions to this. I decided I wanted one, and only one, place to update the header message. The most obvious is making a static text file that I could update when needed and import/include it in everywhere. But as I started looking into how I do that, my eyes drifted to WordPress.

    What if I used JSON? What if instead of a file, I made a page on WordPress, grabbed the content from https://example.com/wp-json/wp/v2/pages/12345 and parsed that so I didn’t have to do a whole mess of editing anywhere but on WordPress?

    Hugo

    Guess what. That works. And it works extra well for Hugo (a static site generator I’m fond of) because Hugo understands dynamic content. The one drawback is that it can’t live refresh remote data, so I will always have to push a change to the site to trigger this rebuild.

    However if you ever wondered how to include WordPress’ JSON data into a Hugo theme, here’s what I have in my template for utility-bar.html:

    {{ $wordpressURL  := "https://example.com/wp-json/wp/v2/pages/12345" }}
    {{ $wordpressJSON := getJSON $wordpressURL }}
    
    <div class="utility-bar">
    	<div class="wrap">
    		<section id="text-16" class="widget widget_text">
    			<div class="widget-wrap">
    				<div class="textwidget">
    					{{ $wordpressJSON.content.rendered | safeHTML}}
    				</div>
    			</div>
    		</section>
    	</div>
    </div>
    

    The reason safeHTML is there is that otherwise Hugo wants to escape my HTML. Which is a wise choice! Default to not trusting.

    This outputs the post content and I have a happy (enough) day. The more I look at it, the more I realize how much I can do with WordPress and Hugo, since regenerating the site just takes a push of Hugo content.

  • Genesis Themes: Author Box Shortcode

    Genesis Themes: Author Box Shortcode

    In building out a network of sites, I was struck upon by a feature of multisite I love, a feature of a theme I adore, and an inconvenience of the combination.

    Author Box

    StudioPress’ Genesis themes include a feature called “Author Box” which allows authors to create bios from their profiles and show them at the bottom of posts. When you have multiple authors on a site, this is a great way to make sure everyone gets credit and that they can control it.

    The code to make this show up is included in most (if not all) StudioPress themes, but if you need to add it for your post archives and single posts, it looks like this:

    add_filter( 'get_the_author_genesis_author_box_single', '__return_true' );
    add_filter( 'get_the_author_genesis_author_box_archive', '__return_true' );
    

    Multisite Magic

    Once the code is enabled, and once someone’s written a bio, their author box shows up for all sites on the network. This is great for what I needed, as it meant everyone had control and I could just set it and forget it. The only ‘annoying’ part is it’s the same bio for all sites, so if you have wildly different sites on your network, this may not be right for you.

    This does harken back to my age old comment: WordPress Multisite is for a network of somewhat related sites.

    By this I mean if all the sites on your network are related, let’s say for a school, then it doesn’t matter that everyone’s bio talks about their school work. But if you combine the school with hobbies, then it gets weird to announce that the champion archer has a PhD in neuroscience. Although that is pretty cool.

    Display The Author Box Anywhere

    The other problem with the author box is you can only use it on pages or posts as context. Which is not what I wanted here. So I made it a shortcode.

    function author_box( $atts ) {
    
    	$user = username_exists( sanitize_user( $atts['user'] ) );
    
    	if ( !$user ) return;
    
    	wp_enqueue_style( 'author-box-shortcode', plugins_url( 'author-box.css', __FILE__ ) );
    
    	$authordata    = get_userdata( $user );
    	$gravatar_size = 'genesis_author_box_gravatar_size' ;
    	$gravatar      = get_avatar( get_the_author_meta( 'email', $user ), $gravatar_size );
    	$description   = wpautop( get_the_author_meta( 'description', $user ) );
    	$username      = get_the_author_meta( 'display_name' , $user );
    
    	$author_box    = '
    		<section class="author-box author-box-shortcode">'
    		. $gravatar
    		. '<h4 class="author-box-title"><span itemprop="name">' . $username . '</span></h4>
    		<div class="author-box-content" itemprop="description">'. $description .'</div>
    		</section>
    	';
    
    	return $author_box;
    }
    

    This is obviously skewed towards Genesis themes, but realistically other than the code in $gravatar_size you can use this for any theme anywhere. The benefit of Genesis here is that most, if not all, of the CSS is done for you. The shortcode is [author-box user="ipstenu"] and it dumps out a full width user box of your named author.

    Display Multiple Boxes Instead

    But… What if you wanted a grid? Or a group of IDs? Taking advantage of the fact that Genesis comes with columns, the code looks like this:

    function author_box( $atts ) {
    
    	if ( $atts['users'] == '' ) return;
    
    	wp_enqueue_style( 'author-box-shortcode', '/wp-content/mu-plugins/css/author-box.css' );
    
    	$users = explode(',', $atts['users'] );
    	$user_count = count( $users );
    
    	$columns = 'one-half';
    	if ( $user_count == 1 ) $columns = '';
    	if ( $user_count % 3 == 0 ) $columns = 'one-third';
    
    	$author_box = '<div class="author-box-shortcode">';
    
    	foreach( $users as $user ) {
    		$user = username_exists( sanitize_user( $user ) );
    		if ( $user ) {
    			$authordata    = get_userdata( $user );
    			$gravatar_size = 'genesis_author_box_gravatar_size' ;
    			$gravatar      = get_avatar( get_the_author_meta( 'email', $user ), $gravatar_size );
    			$description   = wpautop( get_the_author_meta( 'description', $user ) );
    			$username      = get_the_author_meta( 'display_name' , $user );
    
    			$author_box   .= '
    				<section class="author-box '. $columns .'">'
    				. $gravatar
    				. '<h4 class="author-box-title"><span itemprop="name">' . $username . '</span></h4>
    				<div class="author-box-content" itemprop="description">'. $description .'</div>
    				</section>
    			';
    		}
    	}
    
    	$author_box .= '</div>';
    
    	return $author_box;
    }
    

    The shortcode here is [author-box users="ipstenu, liljimmi"] and that puts out a column of either fullwidth, half, or a third. The default is a half, and if there’s only one item, it goes to full width, but I only put in a 1/3rd check because I didn’t feel the need to cover everything. If you want to nick the CSS, StudioPress put it up online, and you can extend it as you want.

  • The New SEO Scam

    The New SEO Scam

    The email looked innocuous.

    I was just browsing Ipstenu.Org and saw in this post (link from 2002) you were interested in tech, and so I thought you might also be interested in linking to a resource we put together on the ways technology is improving health.

    It went on to tell me about how they were comprehensive, up to date, etc etc. I admit, I wondered how they got my email in the first place, since it’s not listed on my site on purpose. But barring anything nefarious, I assumed they guessed, and since it wasn’t important to me that a personal blog post from 2002 get updated, I deleted the email.

    Every five days after then I got a ‘follow up’ email from this person, Camilla Hathaway, and it was strange. I didn’t reply so why would they keep doing that?

    But then I got an email from another company about a different post, asking me if I wanted to link to their article about ddos protection. And another from a third company for a post about cPanel.

    They all sent follow up emails and they all were very ‘nice’ about it, praising my writing and telling me about broken links.

    Spam by any other name…

    If the email was about running a banner ad on CNN for $725, you’d know it was spam.

    If it was from the FBI telling you the corrupt government owed you millions, you’d know it was spam.

    This appeared to be from a real person, a real reader. Except for the fact that there was no way they should have been able to find that particular email address. Except for the fact that they kept email. Except for the fact that who the heck reads old posts on a personal site from as far back as 2001 (I’ve been blogging a long time) and tell me that a link is broken or the information is out of date.

    It’s weird, isn’t it?

    Well, it’s spam.

    The New Spam Game

    The old SEO spam was a lot more overt.

    We are a Leading SEO & Web Development Company and one of the very few companies which offer organic SEO Services with a full range of supporting services such as one way themed text links, blog submissions, directory submissions, article writing and postings, etc.

    or

    I was doing some research on [Subject] and landed on your website.

    You know the obvious ones. These new ones are more clever. They sound more like people. And the worst part is they aren’t all fake people.

    You see … A real company, a legit company, run by real people in the UK, spammed the hell out of me with offers like this. Every day for almost two weeks before I blocked the accounts. This was after I pinged them on Twitter and asked them to leave me alone.

    I shouldn’t have to.

    If I don’t reply, I’m probably not going to. But I surely am not going to reply within a day if you email me daily. The new spam game, the new scam game is to be nice and hammer you with a request over and over and over.

    If It Looks Too Good To Be True, It Is

    The bottom line is that if it looks too good to be true, it is. No probably about it.

  • The Grammar of URLs in Email

    The Grammar of URLs in Email

    At many points in time I’ve complained that if you have a URL in your email, don’t use a period at the end of it because that can break links. That led to me being asked what the proper usage of URLs with punctuation actually is.

    This is not the law but it’s the rules I’ve come up with to ensure readability, linkability, and sanity when emailing links.

    Assume no HTML

    It would be easier if I just said “Visit example.com” and it was a link. You’d know what to do. You click on a link. The problem is not all email clients are HTML friendly. I’m aware it’s 2017. The fact is, the world is not as advanced globally as you might wish. Thus we have to assume that we will be emailing someone who cannot see HTML.

    Arguably that means they’d see <a href="http://example.com">example.com</a> and that may be okay to some of you. It’s not to me, since I aim for the lowest common denominator, and I know that modern email clients will convert http://example.com to a link for me. Therefore the correct solution is to send only the URL, without HTML surrounding it.

    Style Manuals

    The Chicago Manual of Style, which has been updated a few times, suggests you format footnotes with URLs as follows:

    • Fiona Morgan, “Banning the Bullies,” Salon, 15 March 2001, http://www.salon.com/news/feature/2001/03/15/bullying/index.html (accessed 24 Feb. 2003).

    Now in their example, there’s a space before the accessed date, so it’s easy to prevent errant trailing characters, but also they have a space before the link to make sure there’s no mistake there as well. The lesson to take away from this is that your URLs shouldn’t be marred with punctuation.

    Punctuation

    I believe the correct use of a URL is to never prepend or append punctuation. Or in other words: Do not end your sentence wth a URL.

    • Good: Please visit http://example.com/
    • Bad: Please visit http://example.com/.

    That trailing period? That’s bad. That will break on a lot of mail readers. But back to my gleanings from the manual of style, the correct usage is with a space on either side. In order to force that we can just remove the period but then do we use a capital letter for the next sentence?

    The Best We Can Do

    Grammar means we put words in a specific order to have a specific meaning. The same holds true for using URLs in our content. We must be aware of their context and placement.

    Some good examples:

    Please visit our site at http://example.com for more information.

    Or

    If you look at their website – http://example.com – you can see the magnitude of their errors.

    In both of those cases, we’ve put the URL in the middle of the sentence either prefacing it with an ‘at’ or using hyphens to indicate the URL is something special. The second way highlights the URL more in a text-only environment.

    Alright, what if you want to tell someone to download a link?

    Lorem ipsum blah blah blah
    Download the code here: http://example.com

    Notice how I put the download link on it’s own line? That breaks it out visually as well as generating a call to action. Download the code here.

  • CMB2: Repeatable Groups

    CMB2: Repeatable Groups

    This is something that the plugin does out of the box, but my reason for doing it was a little odd.

    Background

    Originally, I had a set of TV characters as a custom post type and each one had their own TV show. Since the TV shows are a second post type, the data was saved as a number and that number was used to generate data on the show pages. Look for everyone who has a TV show value of the same ID as the post ID. Yay!

    The problem with it was spinoffs and crossovers. As time went on, certain characters began to appear on other shows. And it only got worse, until at length there were 30 characters on more than one show, and the number was only growing.

    The quick fix was to make the shows value a repeatable field in CMB2, where I could add multiple shows. Done and done. But then we reached critical mass with how we were handling character roles. Was the character a main, a recurring, or a guest?

    Shows and Roles

    Breaking down the problem to it’s most simple, we have one data set:

    • Show (stored as an ID in an array)
    • Role Type (stored as plain text)

    Instead of saving it as a data set together, the shows were one field (an array, as I mentioned) and the role types were another (a text field).

    In order to make this work, I would have to:

    1. Create a field ‘group’ in CMB2 that stored both show and role as related to that show
    2. Make that group repeatable for characters on multiple shows
    3. Migrate the data

    Data Migration

    There are a lot of ways around this. I ended up with going for the super simple route. I exported two CSVs from my database: one of the shows and one of the role types. Each one had the Post ID associated with it, so I opened those up in a spreadsheet app and combined them, for all cases where the Post ID was the same.

    This gave me a new table that looked like this: 123, 456, regular

    More or less. The ones where shows were arrays looked like, obviously, arrays. I then converted that into a file with 1500 lines that looked like this:

    wp post meta add 6957 character_tvshow_group '[{"show":"6951","type":"regular"},{"show":"7009","type":"regular"}]' --format=json
    

    I could have done it differently, grabbing a file with the data and parsing it on the fly, but I like to look at my 1500 lines and make sure I don’t have weird extra quotes lying around.

    Once that was done, I ran the file, having it execute every line one at a time. It took about one episode of House Hunters: International.

    The CMB2 Code

    In case you’re wondering the code to do this in CMB2 looks like this:

    		// Field Group: Character Show information
    		// Made repeatable since each show might have a separate role. Yikes...
    		$group_shows = $cmb2->add_field( array(
    			'id'          => $prefix . 'show_group',
    			'type'        => 'group',
    			'repeatable'  => true,
    			'options'     => array(
    				'group_title'   => 'Show #{#}',
    				'add_button'    => 'Add Another Show',
    				'remove_button' => 'Remove Show',
    				'sortable' => true,
    			),
    		) );
    		// Field: Show Name
    		$cmb2->add_group_field( $group_shows, array(
    			'name'             => 'TV Show',
    			'id'               => 'show',
    			'type'             => 'select',
    			'show_option_none' => true,
    			'default'          => 'custom',
    			'options_cb'       => array( $this, 'cmb2_get_shows_options'),
    		) );
    		// Field: Character Type
    		$cmb2->add_group_field( $group_shows, array(
    			'name'             => 'Character Type',
    			'id'               => 'type',
    			'type'             => 'select',
    			'show_option_none' => true,
    			'default'          => 'custom',
    			'options'          => $this->character_roles,
    		) );
    

    You’ll notice the options are a bit extra custom.

    Get Shows

    This is done in two parts:

    	public function Sitename_get_post_options( $query_args ) {
    	    $args = wp_parse_args( $query_args, array(
    	        'post_type'   => 'post',
    	        'numberposts' => wp_count_posts( 'post' )->publish,
    	        'post_status' => array('publish'),
    	    ) );
    
    	    $posts = get_posts( $args );
    
    	    $post_options = array();
    	    if ( $posts ) {
    	        foreach ( $posts as $post ) {
    	          $post_options[ $post->ID ] = $post->post_title;
    	        }
    	    }
    
    	    asort($post_options);
    	    return $post_options;
    	}
    
    	public function cmb2_get_shows_options() {
    		return SiteName_get_post_options( array(
    				'post_type'   => 'post_type_shows',
    				'numberposts' => wp_count_posts( 'post_type_shows' )->publish,
    				'post_status' => array('publish', 'pending', 'draft', 'future'),
    			) );
    	}
    

    The reason we search for all shows, from draft to future, is that sometimes we like to schedule updates.

    Character Roles

    		$this->character_roles = array(
    			'regular'   => 'Regular/Main Character',
    			'recurring'	=> 'Recurring Character',
    			'guest'	 	=> 'Guest Character',
    		);