Half-Elf on Tech

Thoughts From a Professional Lesbian

Category: How To

  • Shut Your Pi-hole

    Shut Your Pi-hole

    One of the things that bothers me about the internet is tracking and ads. Mostly I hate trying to read things like IMDB and having it take a minute (60 to 90 seconds) to load, because of ads.

    Now I have nothing against ads! I use them on my sites to limited success. I click on them on Instagram from time to time. But the way many sites use ads and tracking is a little obscene to me. It makes it impossible to browse the net. And worse, the add-ons for our browsers don’t always work.

    Enter the Pi-hole

    I blame my friend Jan, who loves Raspberry Pi’s, for the thought. A Raspberry Pi is a mini computer. It’s the bare bones you need to computer. If you think about how small something like an Amazon Fire Stick is, you have the idea. They’re itsty. But they work. And they work because they have a stripped down operating system that does very little but the basics.

    On top of that, we can add software like Pi-hole. A Pi-hole is custom software that watches your internet traffic and blocks ads before they get to your computer. It works by intercepting DNS traffic and checking it against blacklists. If something, like an ad network, is on a blacklist, it blocks.

    Finding Your Pi

    In order to build this you need a Pi. I bought a kit. I don’t want to hear about it. I don’t need more, I didn’t want more. I wanted, and I got, the basic kit that had what I needed. I bought a Vilros Pi Zero W, which means it’s the smallest, least extendable Pi out there.

    The kit came with the following:

    • A Pi circuit board
    • A case (with three lids)
    • Rubber feet for the case
    • A heat sink
    • A camera connector (but no camera)
    • A 2×20 pin set
    • Adapters for HDMI and power and USB

    In addition I bought an Ethernet connector because I didn’t want to use WiFi, and a mini SD card.

    Putting it together is like LEGO’s, however there were NO directions. Did I need the pins? Did I need the camera? In the end, I attached the heat sink and left it at that. The pins only matter if I’m soldering things and I don’t want a camera.

    Installing the OS

    The tricky part here is normally people say “connect a monitor and keyboard…” but I didn’t want to do that. I don’t have a spare monitor (though I could use my TV and briefly did for a debug) and I don’t have a spare keyboard. All of this I did on my MacBook.

    Go to the official page and download Raspbian LITE. This is the minimalistic version of the official operating system. Download the zip and open it. You’ll get an IMG file. Hold on to it. You also want to download an image builder. I used Balena Etcher, which is a free and open source tool to flash an SD card with an operating system. Or in layman’s terms, it installs that image you downloaded onto the card and magically makes it work.

    Insert your SD card and run Balena. Tell it to install your image to the SD card. This takes about 5 minutes, depending on your systems. Once it’s done, you will need to eject the card and then reinsert it. This is because the MacOS can be tetchy. Once the disk is mounted on your Mac, you need to add an empty file called SSH to the main folder. This will allow you to SSH into the Pi when you’re done.

    Plug It In

    Okay! Now we go back to the hardware. Eject your disk from your Mac and insert it into your Pi. Put the Pi into the case and click it into place. Make sure the various ports line up with the holes. Once that’s in, put the lid on and attach the HDMI adapter (just in case), the Ethernet Adapter (and plug that cable in) and the power.

    Your Pi will boot in a matter of seconds. Go get a glass of water. Come back. Now we’re ready to go!

    Update All the Things

    Before we install Pi-hole, we want to upgrade everything.

    To do that we need to know our Pi’s IP. I went to my router’s web interface and scanned the list of devices for raspberrypi. It’s under the DHCP allocation table. Get the IP address of your Pi. With that address, SSH into your Pi.

    ssh pi@123.45.67.89
    

    The default password is raspberry.

    You will be shown raspi-config on first logging in. Press the number 2 and change the password to something better. If you want to run that screen later, you’ll need to run sudo raspi-config

    Now we update! To update everything, run sudo apt-get update — this downloads all the things you need. Next you run the upgrader – sudo apt-get dist-upgrade – to install it all. If this fails, it will have advice on how to proceed. Read carefully!

    We also want to install Git tools: sudo apt-get install git net-tools

    And you may want to change your timezone: sudo dpkg-reconfigure tzdata

    Install Pi-hole

    You can one-line this, but I prefer to use git:

    $ git clone --depth 1 https://github.com/pi-hole/pi-hole.git pi-hole
    $ sudo bash pi-hole/automated\ install/basic-install.sh
    

    This will take you through the installer. You’ll have some options to pick from:

    • Interface — I’m using eth0, which means wired Ethernet and not WiFi.
    • DNS — I picked OpenDNS but for most people Google is fine and reliable.
    • Blacklists — Accept the defaults.
    • Protocols — Most people will use IPv4. You’ll know if you use IPv6.
    • IP Address/Gateway – The system does a good job getting the IP of your Pi. The gateway needs to be the IP of your existing router.
    • Web Admin — Yes, we want the web admin.
    • Web Server – You want this if you picked yes for Web Admin.

    Whew. That was a lot, right? And the install takes a bit (don’t panic if it stalls). You’ll get your password for the web admin in this process, so please make a note of it.

    Configure Your Pi-hole

    At this point, you can visit http://pi.hole/admin to get to your admin page. If that doesn’t work, try http://123.45.67.89/admin (changing that to your Pi’s IP) and that will get you in. It does not log you in. You can log in, and you’ll want to because we have to do some extra configuration.

    Also it looks really cool:

    The pi-hole admin screen.

    If you’re lucky enough, your router lets you change the DNS servers. And if that’s the case, just change it to the IP of your Pi-hole. Done and done. However. If you’re like me and use the router that came from your ISP, you may be surprised to find out they don’t trust you in the slightest and you cannot change the DNS settings.

    After you’re done swearing at your ISP, and you’ve decided you don’t feel like shelling out a couple hundred for a router that may or may not work, it’s time to play magic with DHCP.

    DHCP (Dynamic Host Configuration Protocol) is the service that lets your router give all the devices on your local network (LAN) an internal IP address. It also handles all the traffic from your device to the rest of the Internet.

    1. Log in to your router and disable DHCP
    2. Log in to Pi-hole admin and enable DHCP
    3. Make sure the Router Gateway address is the correct IP for your router (if it’s not, nothing will work)

    If you want to switch everyone over right away, reboot your router. If not, just wait for everyone to pick up the new service.

    How Have You Pi’d Your Hole?

    What extra tricks have you spun up? Do you have a perfect blacklist? Do you use it for a VPN as well? Did you figure out how to beat the AT&T modems into submission?

    FYI. Do not reply to this post with “You should use X hardware instead” or even “You need own your own router.” Telling people they’re wrong about choices they made when the choices are perfectly valid is an 🍆 move. Be helpful and lift up. Don’t gatekeep.

    For the people who still have questions, here are some useful posts:

  • Saving Theme Data to a File

    Saving Theme Data to a File

    Your life isn’t all WordPress.

    I know, I know, I said a dirty thing, but let’s be honest, everything isn’t always all WordPress. And when that happens, you have to do some weird things to make your data shared.

    One of the things I needed one day was a way for non-WordPress files to get access to a theme setting. See, the theme let me set a top-bar and customize it. Sometimes I did that to share news, sometimes to link to latest posts. Regardless, I updated it via Customizer, and it worked great for WordPress.

    Not so much for my static HTML site, or my non-WordPress PHP site.

    I dwelled on it for a while and then thought “Wait, if the theme knows how to echo a specific setting, then there has to be a function for that. And if there’s a function, then can’t I hook into Customizer saving to trigger the creation of an HTML file with the data and call that from my other sites?”

    And guess what? You can!

    The WordPress Code

    Toss this in an MU Plugin and it’ll save a file to wp-content/top-bar.html when you save customizer settings.

    <?php
    class HELF_Top_Bar {
    
    
    	public function __construct() {
    		add_action( 'customize_save_after', array( $this, 'save_top_file' ) );
    	}
    
    
    	/**
    	 * Safe the top file
    	 * When customizer is saved, copy the get_theme_mod() data and parse it
    	 * into a file: wp-content/top-bar.html
    	 * @return n/a
    	 */
    	public function save_top_file() {
    		$default     = 'Something Default Here';
    		$banner_text = get_theme_mod( 'top-banner-text', $default );
    
    		$fp = fopen( WP_CONTENT_DIR . '/top-bar.html', 'w' );
    		fwrite( $fp, $banner_text );
    		fclose( $fp );
    
    	}
    
    }
    
    new HELF_Top_Bar();
    
    

    The Javascript

    The what?

    For my other PHP file, it’s a simple file_get_contents() call to the HTML. But for static HTML I had to resort to trickery with Javascript.

    <script>
    	var elem = document.getElementById("wpcontent");
    
    fetch('https://example.com/wp-content/top-bar.html', {
            credentials: 'include'
        })
            .then(function (response) {
                return response.text();
            })
            .then(function (text) {
                console.log('Request successful', text.length);
    			console.log('Request successful2', text);
    			$('.wpcontent').html(text);
            })
            .catch(function (error) {
                console.log('Request failed', error)
            });
    </script>
    
    <div class="wpcontent"></div>
    
    

    And magic happens.

  • Rolling Your Own Related Posts

    Rolling Your Own Related Posts

    To start out at the top, I did not write a whole ‘related posts’ plugin. As with all things, I started by asking myself “What’s the problem I’m trying to solve?”

    The answer is “I have a custom post type that needs to relate to other posts in the type, but based on my specific criteria which is currently organized into custom taxonomies and post meta.” And from the outset, that certainly sounds like a massive custom job. it was one I was dreading until I remembered that a developer I respected and trusted had once complained to me about the problems with all those other auto-related-posts plugins.

    1. They’re heavy and use a lot of ram
    2. They don’t let you customize ‘weight’ of relations
    3. They’re not extendable

    So I did the next logical thing and I looked up their plugins.

    The Plugin

    The crux of why I chose this plugin was simply that it’s extendable, but also that it started out with what I had:

    Posts with the most terms in common will display at the top!

    Perfect!

    Design The Basics

    Before you jump into coding, you need to know what you’re doing. I chose to isolate what I needed first. I made a list of everything I thought was relative:

    • Taxonomies: Tropes, Genres, Intersectionality, Tags, Stars
    • Post Meta: Worth It, Loved, Calculated Score

    Yes, it’s that site again.

    I read the plugin documentation and verified that for most of that I just needed to list the taxonomies in the shortcode like this:

    [related_posts_by_tax fields="ids" order="RAND" title="" format="thumbnails" image_size="postloop-img" link_caption="true" posts_per_page="6" columns="0" post_class="similar-shows" taxonomies="lez_tropes,lez_genres,lez_stars,lez_intersections,lez_showtagged"]
    

    Initially I didn’t list the stars because the way the code works, it would say “If you have a Gold Star, show other Gold Stars.” And that wasn’t what I wanted to see. I wanted “If you have ANY star, show other shows with a star.” That said, once we got over 12 shows in each ‘star’ category, this became much easier to match and I could add it in.

    The rest of the code, those checks for meta, needed actual code written.

    Meta Checks

    There’s a helpful filter, related_posts_by_taxonomy_posts_meta_query, that lets you filter the meta queries used by the post. Leveraging that, we can make our checks:

    1. Match the ‘worth it’ value of a show
    2. If the show is loved, list other loved show
    3. If the show isn’t loved, use the score to find show with the same relative value

    Both Worth It and Loved are post meta values. Mine happen to be added by CMB2, but the logic remains the same regardless how you add it. Worth It has four possible values (Yes, No, Maybe, TBD), and the check is either the value or false. Loved is a checkbox, a boolean exists or not, which means it’s a true/falsy. The score is a number that’s generated every time the show is saved, and it’s crazy complicated and another story.

    The code I use looks like this:

    add_filter( 'related_posts_by_taxonomy_posts_meta_query', 'MYSITE_RPBT_meta_query', 10, 4 );
    function MYSITE_RPBT_meta_query( $meta_query, $post_id, $taxonomies, $args ) {
    	$worthit = ( get_post_meta( $post_id, 'lezshows_worthit_rating', true ) ) ? get_post_meta( $post_id, 'lezshows_worthit_rating', true ) : false;
    	$loved   = ( get_post_meta( $post_id, 'lezshows_worthit_show_we_love', true ) ) ? true : false;
    	$score   = ( get_post_meta( $post_id, 'lezshows_the_score', true ) ) ? get_post_meta( $post_id, 'lezshows_the_score', true ) : 10;
    
    	// We should match up the worth-it value as well as the score.
    	// After all, some low scores have a thumbs up.
    	if ( false !== $worthit ) {
    		$meta_query[] = array(
    			'key'     => 'lezshows_worthit_rating',
    			'compare' => $worthit,
    		);
    	}
    
    	// If the show is loved, we want to include it here.
    	if ( $loved ) {
    		$meta_query[] = array(
    			'key'     => 'lezshows_worthit_show_we_love',
    			'compare' => 'EXISTS',
    		);
    	}
    
    	// If they're NOT loved, we use the scores for a value.
    	if ( ! $loved ) {
    		// Score: If the score is similar +/- 10
    		if ( $score >= 90 ) {
    			$score_range = array( 80, 100 );
    		} elseif ( $score <= 10 ) {
    			$score_range = array( 10, 30 );
    		} else {
    			$score_range = array( ( $score - 10 ), ( $score + 10 ) );
    		}
    		$meta_query[] = array(
    			'key'     => 'lezshows_the_score',
    			'value'   => $score_range,
    			'type'    => 'numeric',
    			'compare' => 'BETWEEN',
    		);
    	}
    
    	return $meta_query;
    }
    

    More Similar

    But there’s one more thing we wanted to include. When I built this out, Tracy said “There should be a way for us to pick the shows we think are similar!”

    She’s right! I built in a CMB2 repeatable field where you can pick shows from a dropdown and that saves the show post IDs as an array. That was the easy part, since we were already doing that in another place.

    Once that list exists, we grab the handpicked list, break it out into a simple array, check if the post is published and not already on the list, and combine it all:

    add_filter( 'related_posts_by_taxonomy', array( $this, 'alter_results' ), 10, 4 );
    function alter_results( $results, $post_id, $taxonomies, $args ) {
    	$add_results = array();
    
    	if ( ! empty( $results ) && empty( $args['fields'] ) ) {
    		$results = wp_list_pluck( $results, 'ID' );
    	}
    
    	$handpicked  = ( get_post_meta( $post_id, 'lezshows_similar_shows', true ) ) ? wp_parse_id_list( get_post_meta( $post_id, 'lezshows_similar_shows', true ) ) : array();
    	$reciprocity = self::reciprocity( $post_id );
    	$combo_list  = array_merge( $handpicked, $reciprocity );
    
    	if ( ! empty( $combo_list ) ) {
    		foreach ( $combo_list as $a_show ) {
    			//phpcs:ignore WordPress.PHP.StrictInArray
    			if ( 'published' == get_post_status( $a_show ) && ! in_array( $a_show, $results ) && ! in_array( $a_show, $add_results ) ) {
    				$add_results[] = $a_show;
    			}
    		}
    	}
    
    	$results = $add_results + $results;
    
    	return $results;
    }
    

    But … you may notice $reciprocity and wonder what that is.

    Well, in a perfect world if you added The Good Fight as a show similar to The Good Wife, you’d also go back and add The Good Wife to The Good Fight. The reality is humans are lazy. There were two ways to solve this reciprocity of likes issues.

    1. When a show is added as similar to a show, the code auto-adds it to the other show
    2. When the results are generated, the code checks if any other show likes the current show and adds it

    Since we’re already having saving speed issues (there’s a lot of back processing going on with the scores) and I’ve integrated caching, it was easier to pick option 2.

    function reciprocity( $post_id ) {
    	if ( ! isset( $post_id ) || 'post_type_shows' !== get_post_type( $post_id ) ) {
    		return;
    	}
    
    	$reciprocity      = array();
    	$reciprocity_loop = new WP_Query(
    		array(
    			'post_type'              => 'post_type_shows',
    			'post_status'            => array( 'publish' ),
    			'orderby'                => 'title',
    			'order'                  => 'ASC',
    			'posts_per_page'         => '100',
    			'no_found_rows'          => true,
    			'update_post_term_cache' => true,
    			'meta_query'             => array(
    				array(
    					'key'     => 'lezshows_similar_shows',
    					'value'   => $post_id,
    					'compare' => 'LIKE',
    				),
    			),
    		)
    	);
    
    	if ( $reciprocity_loop->have_posts() ) {
    		while ( $reciprocity_loop->have_posts() ) {
    			$reciprocity_loop->the_post();
    			$this_show_id = get_the_ID();
    			$shows_array  = get_post_meta( $this_show_id, 'lezshows_similar_shows', true );
    
    			if ( 'publish' === get_post_status( $this_show_id ) && isset( $shows_array ) && ! empty( $shows_array ) ) {
    				foreach ( $shows_array as $related_show ) {
    					if ( $related_show == $post_id ) {
    						$reciprocity[] = $this_show_id;
    					}
    				}
    			}
    		}
    		wp_reset_query();
    		$reciprocity = wp_parse_id_list( $reciprocity );
    	}
    
    	return $reciprocity;
    }
    

    There’s a little looseness with the checks, and because there are some cases were shows show up wrong because of the ids (ex: show 311 and 3112 would both be positive for a check on 311), we have to double up on the checks to make sure that the show is really the same.

    What’s Next?

    There are still some places I could adjust this. Like if I use more filters I can make the show stars worth ‘more’ than the genres and so on. And right now, due to the way most Anime are based on Manga (and thus get flagged as “Literary Inspired”), anything based on Sherlock Holmes ends up with a lot of recommended Anime.

    Still, this gives me a way more flexible way to list what’s similar.

  • Spam Your Blacklist

    Spam Your Blacklist

    As mentioned when I began my hiatus, there would be the occasional code post. Here’s one that is born from how annoying someone is.

    The Situation

    I have a serial harasser. He’s a troll and a semi-stalker who doesn’t understand the meaning of “No.” I’ve blocked him on social media, his emails are blackholes, and as I don’t have contact forms on my sites, nor do I have open comment forms at the moment, it’s a non-issue here.

    However, I do have another site which he found and decided to use my contact form to spam me and my co-admin with 10+ emails. When I found out, I blocked his IP address. He was on mobile, though, so I knew this would only last as long as he was on his phone. I needed a better solution.

    This is not a rare problem. Especially not for women online. One of the many ways in which men drive women offline is by upping the emotional labor needed to be online. That is, they attack us with message after message, generally in the guise of being ‘a nice guy,’ or ‘just trying to have an open conversation.’ But the reality is that they want to wear you down and get you to do what they want.

    It’s exhausting. If you’ve ever gone car shopping and had the dealer call you over and over with the hard sell, it’s like that.

    The Paradox

    Contact Forms are meant to be a way for people to contact you, outside of the comments on your site. That being so, they really do need to exist outside the confines of the comments, which means your comment moderation list is a bit inappropriate. You want people who are having comment problems to get a hold of you.

    At the same time, if you’ve blackholed someone, you don’t. You don’t want them to bother you at all, as reading their messages, even though you’re deleting them, is draining. So you want to be able to block them.

    Here’s the problem: most contact forms don’t let you do this out of the box.

    Yeah, think on that for a moment.

    Here are the top four contact form plugins:

    I use Jetpack, and while I may be annoyed I’m also a developer. So I did made an answer.

    The Caution

    This will not block everyone. If your harasser changes emails a lot, you’re out of luck. And this is the ‘excuse’ I see a lot of the time. Why bother if they’re going to change emails? The answer is obvious. If I can inconvenience them enough, and make it clear I don’t care, they’ll go away.

    Also if you do this right, they never know they’ve been blacklisted, so they think they’re getting to you and you’re sipping a damn mai tai.

    The Solution

    In March 2014, I opened a ticket asking for a way to blacklist people. They have made zero forward momentum on this in the 4.5 years since. So this little red hen is doing it herself.

    By using the built in filter for spam (which Akismet uses), this code checks if someone’s on the comment blacklist by IP or email, and if so, flags the message as spam. You don’t get an email. You do still get the message in your spam, which is not a great fix. I’d rather it just get dumped into trash, but there’s no filter I can find for that.

    Still. This works, and it shut the guy up.

    add_filter( 'jetpack_contact_form_is_spam', 'jetpack_spammers', 11, 2 );
    
    function jetpack_spammers( $is_spam, $form ) {
    	if ( $is_spam ) {
    		return $is_spam;
    	}
    
    	if ( wp_blacklist_check( $form['comment_author'], $form['comment_author_email'], $form['comment_author_url'], $form['comment_content'], $form['user_ip'], $form['user_agent'] ) ) {
    		return true;
    	}
    
    	return false;
    }
    

    But. That only helps what’s on the blacklist. And the blacklist has a couple drawbacks. First of all, while it absolutely does handle multiple words (so I can block ‘milady mika’ if I want), it’s a little more complex if you wanted to block someone using gmail and a plus sign in the email address. So if you want to block example+spammer@gmail.comthen you either have to add that in literally or you get creative. I went creative.

    add_filter( 'jetpack_contact_form_is_spam', 'jetpack_harassment', 11, 2 );
    
    function jetpack_harassment( $is_spam, $form ) {
    	// Bail early if already spam
    	if ( $is_spam ) {
    		return $is_spam;
    	}
    	$badlist   = array();
    	$blacklist = explode( "\n", get_option( 'blacklist_keys' ) );
    
    	// Check the list for valid emails. Add the email _USERNAME_ to the list
    	foreach ( $blacklist as $spammer ) {
    		if ( is_email( $spammer ) ) {
    			$emailparts = explode( '@', $spammer );
    			$username   = $emailparts[0];
    			$badlist[]  = $username;
    		}
    	}
    
    	// Check if the comment author name matches an email we've banned
    	// You'd think we didn't have to do this but ...
    	if ( in_array( $form['comment_author'], $badlist ) ) {
    		return true;
    	}
    
    	// Check if the email username is one of the bad ones
    	// This will allow spammer@example.com AND spammer+foobar@example.com to get caught
    	foreach ( $badlist as $bad_person ) {
    		if ( preg_match( '/' . $bad_person . '/', $form['comment_author_email'] ) ) {
    			return true;
    		}
    	}
    
    	return false;
    }
    

    My original take was hardcoded in, but this way is more elegant and covers the majority of the ways ‘nice’ people try to get around blocks. Now, if you’ve blocked spammer@example.com and someone submits a form with spammer+avoid@example.com this will catch them. It has a higher chance of catching ‘innocents’ (like innocent@spammer.com) however considering I’m looking for something like rosbeitam@example.com I’m reasonably confident in this for my personal application.

    The Take Away

    If you make a contact form, you damn well better make a way for users to block people from the back end, without having to code it.

    Merry Christmas, ya filthy animals.

  • Blocks: Another Way to Adjust Settings

    Blocks: Another Way to Adjust Settings

    As I’ve mentioned a few times, one of the ways you can adjust settings on a block in Gutenberg is via the sidebar. In fact, this is the default way most people will interact with block settings.

    If you make a paragraph today, you can see it like this:

    This is the default paragraph sidebar from Gutenberg. No customizations.

    But. I don’t actually like it very much. I appreciate that I have it, and I love that I can get it out of the way. But sometimes I like settings to be a little more contextual.

    It’s Easy to Add Sidebar Settings

    One of the reasons we all use sidebar settings is that, well, they’re easy. When I built out my listicles plugin, I could use the inspector controls and automagically I have my own settings.

    This:

    <InspectorControls>
    	<PanelBody title={ 'Listicle Settings' }>
    		<RangeControl
    			label={ 'Items' }
    			value={ items }
    			onChange={ ( value ) => setAttributes( { items: value } ) }
    			min={ 1 }
    			max={ MAX_ITEMS }
    		/>
    		<ToggleControl
    			label={ 'Reversed' }
    			help={ ( checked ) => checked ? 'Reversed order (10 - 1)' : 'Numerical order (1 - 10)' }
    			checked={ props.attributes.reversed }
    			onChange={ () => props.setAttributes( { reversed: ! props.attributes.reversed } ) }
    		/>
    	</PanelBody>
    </InspectorControls>

    Looks like this:

    A screenshot of the listicle settings, which are a slider for the items and a toggle to reverse the order display.

    But like I said, I don’t like it very much. It’s clunky, it’s touchy, if you delete the number and type in a new one it’ll wipe all your data. And worst of all, my partner in crime, Tracy, hates it. If your work partners hate a tool, then it’s serious problem. I’ll put up with annoyances to me, but I’ll learn new code for the team.

    Think About What’s Easier

    Before I get into the code I used to solve the issue, I want to take a moment to talk about theory and understanding usage.

    One of the critiques about Gutenberg is that it’s changing too much too quickly, and it’s not listening to users. The problem with that complaint is it lacks context. I’m big on context because I believe that only with understanding the usage and context can we as a whole make the correct decisions going forward. What changes, when, and why depends entirely on what’s being used, for what, and why.

    It’s much more direct to understand this when I look at my little listicles block. You see, we use it for one thing: to make lists with a specific format. And we have few requirements.

    • Add and remove items
    • Add content of myriad types to each item
    • Be able to reverse the item count (1 to 10 or 10 to 1)

    That’s really it. Except now I’m adding one more thing:

    • An easier, inline, way to add/remove/toggle items.

    So I sat and I thought about what would be the easiest to use, and I came up with a simple solution. Three buttons, one to add an item, one to remove, and one to toggle the order. Have those show perpetually at the bottom of the list, and you could easily add and remove as needed.

    It’s Actually Easy To Add Buttons

    Once I knew what I wanted, I took a page from some work Yoast is doing and sketched my idea to look like this:

    Three buttons: Add item, remove item, and toggle order

    In order to do this, I needed to add some code to the bottom of my <dl> code:

    <div className='listicles-buttons'>
    	<IconButton
    		icon='insert'
    		onClick={ () => setAttributes( { items: parseInt(`${ items }`)+1 } ) }
    		className='editor-inserter__toggle'
    	>Add Item</IconButton>
    
    	<IconButton
    		icon='dismiss'
    		onClick={ () => setAttributes( { items: parseInt(`${ items }`)-1 } ) }
    		className='editor-inserter__toggle'
    	>Remove Item</IconButton>
    
    	<IconButton
    		icon='controls-repeat'
    		onClick={ () => setAttributes( { reversed: ! reversed } ) }
    		className='editor-inserter__toggle'
    	>Toggle Order</IconButton>
    </div>

    This sits inside the <dl> and just below my <InnerBlocks...> insert. It generates the buttons, which change when you hover by default.

    Now it’s not perfect. If you deleted all the items and pressed delete again, it would sure try to delete. I didn’t put in checks to make sure we didn’t go below 0 or above 18 (which is my current limits). But this is the start to make sure we can keep improving and iterating.

  • Show Feedback in “Right Now”

    Show Feedback in “Right Now”

    The “Right Now” section of the WordPress dashboard is a great way to get an overview of the goings on of your site. But it doesn’t quite list everything. What if you could add things like ‘messages’ to the the “At a Glance” section like this:

    The "At a Glance" section, with messages added in.

    Guess what? You can!

    The Code

    Presmuing you’re using Jetpack’s contact form module, you automatically get a new kind of post called ‘Feedback.’ In order to make it display it’s count in “At a Glance,” there are two parts. First we add the CSS, which does the styling. Then we add the PHP that counts and displays the number of posts in feedback.

    add_action( 'dashboard_glance_items', 'helf_dashboard_glance' );
    add_action( 'admin_head', 'helf_dashboard_glance_css' );
    
    /*
     * Show Feedback in "Right Now"
     */
    function helf_dashboard_glance() {
    	if ( class_exists( 'Jetpack' ) && Jetpack::is_module_active( 'contact-form' ) ) {
    		foreach ( array( 'feedback' ) as $post_type ) {
    			$num_posts   = wp_count_posts( $post_type );
    			$count_posts = ( isset( $num_posts->publish ) ) ? $num_posts->publish : '0';
    			if ( 0 !== $count_posts ) {
    				if ( 'feedback' === $post_type ) {
    					// translators: %s is the number of messages
    					$text = _n( '%s Message', '%s Messages', $count_posts );
    				}
    				$text = sprintf( $text, number_format_i18n( $count_posts ) );
    				printf( '<li class="%1$s-count"><a href="edit.php?post_type=%1$s">%2$s</a></li>', esc_attr( $post_type ), wp_kses_post( $text ) );
    			}
    		}
    	}
    }
    
    /*
     * Custom Icon for Feedback in "Right Now"
     */
    function helf_dashboard_glance_css() {
    	if ( class_exists( 'Jetpack' ) && Jetpack::is_module_active( 'contact-form' ) ) {
    		?>
    		<style type='text/css'>
    			#adminmenu #menu-posts-feedback div.wp-menu-image:before, #dashboard_right_now li.feedback-count a:before {
    				content: '\f466';
    				margin-left: -1px;
    			}
    		</style>
    		<?php
    	}
    }