Half-Elf on Tech

Thoughts From a Professional Lesbian

Category: How To

  • Not Spilling SEO Juice

    Not Spilling SEO Juice

    Once in a while I still get people who ask me to do things for them. Some offer to pay, most don’t, but a common thread lately has been “How do I redirect and not screw up my SEO?”

    They tend to ask it nicer, but that’s what they mean. And to be honest, the question surprises me in 2016. I’ve read email after email about people who swear “I renamed my domain/page and now my SEO crapped out!” And to each and every one I ask “Did you redirect it properly?”

    I think what’s happening is that the problem is so clear cut, people are overthinking. That is, bar none, the biggest mistake I feel anyone ever makes. They overcomplicate a problem and mire themselves in the hell of debugging. I do it all the time.

    Let me tell you a story. When we started LezWatchTV, we had some pretty non-SEO friendly URLs. Our taxonomy for TV stations was “show-tags” and it went on and on with worse and worse names. Finally I sat down and drew out a map of what the ‘best’ SEO names would be for what I needed, and it was very short:

    • TV Stations: stations
    • Character Traits: cliches
    • Show Tropes: tropes
    • Characters: characters
    • Shows: shows

    Five items. And to do the recdirects, I put this in the .htacess file:

    RedirectMatch 301 ^/show-tags/(.*) /stations/$1
    RedirectMatch 301 ^/character-tags/(.*) /cliches/$1
    RedirectMatch 301 ^/lez_cliches/(.*) /tropes/$1
    RedirectMatch 301 ^/lez_tropes/(.*) /cliches/$1
    RedirectMatch 301 ^/lez_chars/(.*) /characters/$1
    RedirectMatch 301 ^/show-characters/(.*) /characters/$1
    RedirectMatch 301 ^/tv-shows/(.*) /shows/$1
    

    That’s all I needed. I took all the old, bad names and I redirected them to the good names. But I had to throw in an added wrinkle. I’d originally decided shows would have cliches and characters tropes, but I realized that was wrong and flipped them a week later. That meant I had things like /cliche/law-enforcement which had to be redirect to /trope/law-enforcement and for that, there was nothing to be done save a massive section like this:

    # BEGIN Great Big Redirect Section
    RedirectMatch 301 ^/cliches/bisexual-love-triangle(.*) /tropes/bisexual-love-triangle$1
    RedirectMatch 301 ^/cliches/coming-out(.*) /tropes/coming-out$1
    RedirectMatch 301 ^/cliches/gay-for-ratings(.*) /tropes/gay-for-ratings$1
    RedirectMatch 301 ^/cliches/law-enforcement(.*) /tropes/law-enforcement$1
    [...]
    RedirectMatch 301 ^/tropes/athlete(.*) /cliches/athlete$1
    RedirectMatch 301 ^/tropes/cross-dresser(.*) /cliches/cross-dresser$1
    RedirectMatch 301 ^/tropes/firefighter(.*) /cliches/firefighter$1
    RedirectMatch 301 ^/tropes/teacher(.*) /cliches/teacher$1
    

    This goes on for 24 character clichés and 18 show tropes. It had to be done manually.

    Now let’s ask the big question. Did I lose any SEO Juice by doing this?

    Not a drop. Because I used a 301 redirect and I used it properly (calling it well above my redirect for WordPress). This is exactly what Google’s webmaster docs tell you to do:

    If you need to change the URL of a page as it is shown in search engine results, we recommend that you use a server-side 301 redirect. This is the best way to ensure that users and search engines are directed to the correct page. The 301 status code means that a page has permanently moved to a new location.

    Still not sure? Here’s what Joost (of Yoast SEO) says when asked if you should use a 301 or a 302 redirect:

    The answer is very simple. You should never use a 301 redirect if you ever want to use that url again. So if you’re redirecting one url to another and you’re sure that the old url is never going to be used again, it’s a 301. If you’re not sure, it’s a 302. That’s it. Good luck!

    This is, again, straightforward. If you’re never using the page again (which I’m not), you use the 301 redirect.

    By the way, if you’re using Yoast SEO Premium, then you should still have access to their redirect tool. Personally I feel that’s a change that should be on the server level, not a plugin.

    Okay, so what happens if you forget to do this? Your SEO will tank. However this can be fixed! Again, if you’re using Yoast SEO and you set up Google Analytics and webmaster tools with it, you have a handy Search Console which will let you see what the errors are. If you don’t, but you use Google (and really this is where it’s quite helpful), you’ll go to your Search Console > Crawl > Crawl Errors page and check out what’s listed.

    Remember to check desktop and smart-phone pages. As long as you keep those old URLs redirecting properly to their new home, your juice will be safe and sound.

  • Git Subtrees

    Git Subtrees

    I have a project in Hugo where I wanted the content to be editable by anyone but the theme and config to remain mine. In this way, anyone could add an article to a new site, but only I could publish. Sounds smart, right? The basic concept would be this:

    • A private repository, on my own server, where I maintained the source code (themes etc)
    • A public repository, on GitHub or GitLab, where I maintained the content

    Taking into consideration how Hugo stores data, I had to rethink how I set up the code. By default, Hugo has two main folders for your content: content and data. Those folders are at the main (root) level of a Hugo install. This is normally fine, since I deploy by having a post-deploy hook that pushes whatever I check in at Master out to a temp folder and then runs a Hugo build on it. I’m still using this deploy method because it lets me push commit without having to build locally first. Obviously there are pros and cons, but what I like is being able to edit my content and push and have it work from my iPad.

    Now, keeping this setup, in order to split my repository I need to solve a few problems.

    Contain Content Collectively

    No matter what, I need to have one and only one location for my content. Two folders is fine, but it has to be within a single folder. In order to do this, it’s fairly straightforward.

    In the config.toml file, I set two defines:

    contentdir = "content/posts"
    datadir = "content/data"
    

    Then I moved the files in content to content/posts and moved data to content/data. I ran a quick local test to make sure it worked and, since it did, pushed that change live. Everything was fine. Perfect.

    Putting Posts Publicly

    The second step was making a public repository ‘somewhere.’ The question of ‘where’ was fairly simple. You have a lot of options, but for me it boils down to GitLab or GitHub. While GitHub is the flavor du jour, GitLab lets you make a private repository for free, but both require users to log in with an account to edit or make issues. Pick whichever one you want. It doesn’t matter.

    What does matter is that I set it up with two folders: posts and data

    That’s right. I’m replicating the inside of my content folder. Why? Well that’s because of the next step.

    Serving Subs Simply

    This is actually the hardest part, and led me to complain that every time I use Submodules in Git, I remember why I hate them. I really want to love Submodules. The idea is you check out a module of a specific version of another repository and now you have it. The problem is that updates are complicated. You have to update the Submodule separately and if you work with a team, and one person doesn’t, there’s a possibility you’ll end up pushing the old version of the Submodule because it’s not version controlled in your git repository.

    It gets worse if you have to solve merge conflicts. Just run away.

    On the other hand, there’s a tool called Subtree, which two of my twitter friends introduced me to after I tweeted my Submodule complaint. Subtree uses a merge trick to get the same result of a Submodule, only it actually stores the files in the main repository, and then merges your changes back up to it’s own. Subtrees are not a silver bullet, but in this case it was what I needed.

    Checking out the subtree is easy enough. You tell it where you want to store the repository (a folder named content) and you give it the location of your remote, the branch name, and voila:

    $ git subtree add --prefix content git@github.com:ipstenu/hugo-content.git master --squash
    git fetch git@github.com:ipstenu/hugo-content.git master
    From gitlab.com:ipstenu/hugo-content
     * branch            master     -> FETCH_HEAD
    Added dir 'content'
    

    Since typing in the full path can get pretty annoying, it’s savvy to add the subtree as a remote:

    $ git remote add -f hugo-content git@github.com:ipstenu/hugo-content.git
    

    Which means the add command would be this:

    $ git subtree add --prefix content hugo-content master --squash
    

    Maintaining Merge Manuverability

    Once we have all this in, we hit a new problem. The subtree is not synced by default.

    When a subproject is added, it is not automatically kept in sync with the upstream changes so you have to pull it in like this:

    $ git subtree pull --prefix content hugo-content master --squash
    

    When you have new code to add, run this:

    $ git subtree push --prefix content hugo-content master --squash
    

    That makes the process for a new article a little extra weird but it does work.

    Documenting Data Distribution

    Here’s how I update in the real world:

    1. Edit my local copy of the content folder in the hugo-library repository
    2. Add and commit the changed content with a useful message
    3. Push the subtree
    4. Push the main repository

    Done.

    If someone else has a pull request, I would need to merge it (probably directly on GitHub) and then do the following:

    1. Pull from the subtree
    2. Push to the main repository

    My weird caveat is that updating via Coda can get confused as it doesn’t always remember what repository I want to be on, but since I do all of my pushes from command line, that really doesn’t bother me much.

  • Gmail: Handling Bad Emails

    Gmail: Handling Bad Emails

    No, not bad emails as in the ones that you consider saving and posting for someone’s everlasting internet shame. Bad emails are the ones that go to the wrong place, through none of your fault. We’re talking about the people using an email you’ve not used in a decade, or someone who can’t remember your name is spelled with an A and not an E, and so on. You know, typos.

    One of the things I did on my old email was set up a trash email box. That is, people could email not-me@domain.com or me@olddomain.com and they’d get an auto-reply telling them the email was no longer in service. It was more important for an old domain I owned but didn’t use and yet people I needed to talk to still thought it was real. I could have forwarded it to me, but after 10 years, I upgraded to the “Folks, seriously!” alert.

    Doing this on cPanel was pretty easy, making a custom alias that dev/null’d and sent a reply. Doing it on Gmail was a little weirder and made me think about the situation.

    Canned Replies

    First you have to set up Canned Responses, which is a Lab (go to Gmail -> Settings -> Labs). You made a response like you make an email, only instead of sending it you save it by clicking on the down arrow and saving as a Canned Response:

    Canned Response Save

    Once you have it saved, set up a filter so any email to @domain.com gets a reply of that Canned.

    Don’t Be Sneaky

    If you’re thinking “Aha! I can use this to be sneaky!” with the intent of sending people emails to pretend you really are reading it, there is a problem with that. The email comes back from YOU+canned.response@example.com and no, there’s no really easy way around that. Someone did come up with a Google Script for it, but it’s not for the faint of heart.

    Now the question is, is that a bad thing? Is it bad for people to know they got a canned reply? No, not really. By putting in the +canned.response it’s obvious that it’s a canned, but it’s also obvious for you and you can filter the emails however you want. People who reply to canned? Auto-trash ’em. Or block them.

    Filters

    Instead of the canned reply, though, you can also just discard the email. Either don’t even bother to set up the email (or it’s alias at all), or if you do, filter it out and dump it. The only reason I could see bothering to make an alias for email you don’t want is if you either plan to review it later, or if you have a catch all email address. If you do this, making an alias, make sure you filter the emails and mark them read so you don’t get distracted by them.

    Catch All

    There’s a slightly different approach to all this, though. The idea of a catch-all email. By default, G Suites sends all your misdirected emails to trash. Accidentally mailed bob@example.com instead of b0b@example.com because the numbers and letters look the same? Tough luck. Unless Bob was smart enough to set that up as an alias (which I tend to do), your email was lost. The alternative is to designate a user as a ‘catch all’ account that gets everything that doesn’t belong to an existing user.

    That catch-all can auto-reply to all emails, forward ones that are important, and everything else. If you’re a business, you should do this so you don’t lose any misdirected emails from customers (they can’t spell after all), but remember to check that email often as it will also collect all the spam for all your accounts.

  • Optimizing Images

    Optimizing Images

    After running a regeneration of my images (due to changing things on my theme) my Gtmetrix score dropped from an A to a D! In looking at why, I saw it was telling me my images should be optimized.

    Best to get on that, eh?

    The easiest way is to install jpegoptim on the server:

    $ yum install jpegoptim
    

    And then to run a compression on my images:

    $ jpegoptim IMAGE.jpg
    

    Anyone fancy running that on a few thousand images? Hell no. We have a couple options here. One is to go into each folder and run this:

    $ jpegoptim *.jpeg *.jpg
    

    The other is to sit in the image root folder and run this:

    $ find . -type f -name '*.jp?g' -exec jpegoptim {} --strip-all \;
    

    I picked --strip-all since that removes all the image meta data. While I would never want to consider that on a photoblog, I needed to compress everything and, for some reason, unless I stripped that data I didn’t get smaller sizes. For this case, it wasn’t an issue.

    What about PNGs? Use optipng ($ yum install optipng) and run this:

    $ find . -type f -name '*.png' -exec optipng *.png \;
    

    Going forward, I used Homebrew to install those locally and compress my images better, though I’m usually pretty good about remember that part. Well. I thought I was.

  • Yoast SEO: Selective Stopwords

    Yoast SEO: Selective Stopwords

    Stopwords are those small words that should be removed from URLs. You know like ‘a’ and ‘and’ or ‘or’ and so on and so forth. They make for ungainly and long URLs and really you should remove them.

    If you happen to use Yoast SEO for WordPress, and you want to disable stopwords, there’s a simple way about that. Go to SEO -> Advanced and disable the feature for stopwords.

    Disable stop-word cleanup, but why?

    If you want to kill it with fire and prevent everyone on your site from being able to activate them ever, you can toss this into an MU plugin.

    add_filter( 'wpseo_stopwords', '__return_empty_array' );
    remove_action( 'get_sample_permalink', 'wpseo_remove_stopwords_sample_permalink' );
    

    The first filter makes the stopwords kick back nothing, and the remove action stops the process from running. You probably only need the second one, but better safe than sorry, I always say.

    But … what if you want stop words removed, but you don’t want them removed on certain custom post types? Welcome to my world! I wanted to remove them from two post types only.

    Enter my frankencode:

    <?php
    
    /*
    Plugin Name: Yoast SEO Customizations
    Description: Some tweaks I have for Yoast SEO
    Version: 2.0
    */
    
    // Unless we're on a post or a post editing related page, shut up
    global $pagenow;
    
    $pagenow_array = array( 'post.php', 'edit.php', 'post-new.php' );
    if ( !in_array( $pagenow , $pagenow_array ) ) {
    	return;
    }
    
    // Since we are, we need to know exactly what we're on and this is a hassle.
    global $typenow;
    
    // when editing pages, $typenow isn't set until later!
    if ( empty($typenow) ) {
        // try to pick it up from the query string
        if (!empty($_GET['post'])) {
            $post = get_post($_GET['post']);
            $typenow = $post->post_type;
        }
        // try to pick it up from the query string
        elseif ( !empty($_GET['post_type']) ) {
    	    $typenow = $_GET['post_type'];
        }
        // try to pick it up from the quick edit AJAX post
        elseif (!empty($_POST['post_ID'])) {
            $post = get_post($_POST['post_ID']);
            $typenow = $post->post_type;
        }
        else {
    	    $typenow = 'nopostfound';
        }
    }
    
    $typenow_array = array( 'post_type_shows', 'post_type_characters' );
    if ( !in_array( $typenow , $typenow_array ) ) {
    	return;
    }
    
    add_filter( 'wpseo_stopwords', '__return_empty_array' );
    remove_action( 'get_sample_permalink', 'wpseo_remove_stopwords_sample_permalink', 10 );
    

    There was something funny to this, by the way. Originally I didn’t have the $pagenow code. Didn’t need it. But when I left it out, Yoast SEO broke with a weird error. It refused to load any of the sub-screens for the admin settings!

    Cannot Load Yoast SEO Admin Pages

    After some backpacking of “Okay, was it working before…?” I determined it was the call for global $typenow; – a global that isn’t used at all in the Yoast SEO source code that I could find. Still, by making my code bail early if it’s not even on a page it should be on, I made the rest of the WP Admin faster, and that’s a win for everyone.

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