How to make the WooCommerce main shop page show featured products only in 2022

Sorry for the click-bait-y title (especially the “in 2022” part), but I searched fruitlessly for way too long and found too many woefully outdated answers to this. Ultimately what I found still wasn’t quite the complete answer, so I modified it a bit myself to arrive at the following.

The goal here is, as the title suggests, to get the WooCommerce main shop page to only show your featured products. Why this isn’t just a checkbox option in WooCommerce is beyond me. But then a lot of the decisions made by the WooCommerce dev team are beyond me. (Excuse me, Professor Brainiac, but I’ve built e-commerce platforms from scratch and, uh, I think I know how a proton accelerator… oh wait, never mind.)

Anyway, this is it:

add_action('woocommerce_product_query', function($query) {
    if (is_shop()) {
        $query->set('tax_query', array(array(
            'taxonomy' => 'product_visibility',
            'field' => 'name',
            'terms' => 'featured',
            'operator' => 'IN',
        )));
    }
});

What exactly is happening here? Well, as noted by the most helpful resource I found, since WooCommerce 3 (currently on 6.1.1), the “featured” status has been handled by the product_visibility taxonomy, and not by the _featured post meta field. So this needs a tax_query and not a meta_query.

Beyond that, we’re making an extra check that we’re on the shop main page — so this doesn’t affect category archive pages. And we’re using the woocommerce_product_query hook, not pre_get_posts as some other examples suggest, so it only runs on WooCommerce queries and we can skip adding extra conditionals for pre_get_posts to run on, you know, every single post query on every page of the site, including admin.

That’s all there is to it. Now your main WooCommerce shop page will only display featured products, and nothing else changes.

How did I not know about ClassicPress before now?

ClassicPressI’ve been using WordPress for 15 years, and have made it my go-to platform for all new websites I’ve built since 2014. So how is it that it took me three years to discover that ClassicPress exists, especially since its whole raison d’être is to keep the pre-Gutenberg dream of WordPress alive?

On one hand, being a solo developer — even before the pandemic — has always kept me a bit out-of-the-loop, especially since I don’t attend conferences. But I suspect the fact that I knew nothing of this also speaks negatively to the project’s future.

Is it gaining enough traction to continue to exist? Is it really a viable option to use on new professional projects in 2021?

Has the Gutenberg ship sailed? Well, yes, it has. But my issues with the current and future state of WordPress go beyond Gutenberg, to the nature of Automattic’s role in steering the ship, the greater vision of what WordPress is and should become, and… well… Matt Mullenweg’s personality. I feel like the future of WordPress is increasingly diverging from what I hoped to get out of it as a platform, and it’s clear that I’m not alone. That’s why ClassicPress exists.

There are a lot of things to like about ClassicPress, right out of the gate, besides the most obvious element, which is the absence of anything Gutenberg. It does away with a lot of the cutesy crap that’s rolled into WordPress by default, not least of which being the annoying proliferation of the word “howdy” and the beyond-pointless-to-actively-detrimental* plugin Hello Dolly.

As I look to my own future with WordPress and/or ClassicPress, I am primarily thinking about two things: 1) how/if I will continue to use it as the platform of choice for client projects, and 2) what the future will be for the plugins I have contributed to the WordPress community, and more specifically, my commercial plugin, ICS Calendar Pro.

I’ve been struggling with these matters for almost four years now, ever since Gutenberg emerged on the scene and went through its early phases of absolutely sucking, to its too-soon release as the default WordPress editor, to its current state as a mostly good but highly quirky and weirdly limited page building tool.

The timing was not great for me, as I had just recently gone “all-in” on 34 Blocks, my own block-based starter theme that I have been using to create all of my client sites since 2017. It started from a series of one-off client themes beginning around 2015 and is built around Advanced Custom Fields and its “Flexible Content” fields. It’s all much more in line with what “WordPress” has always meant to me. But as WordPress becomes Gutenberg, my vision of what this tool is and the reality of what it has become are increasingly at odds.

In those four years I’ve been bouncing around between several different ideas:

  • Suck it up and finally embrace Gutenberg development, learning a bunch of new stuff like React, in which I am not only wholly disinterested but with which I philosophically disagree?
  • Cling for dear life to Classic Editor and pray the gods of Automattic keep it on life support?
  • Switch to an entirely new platform, whether that might be another open source or commercial CMS, or a complete SaaS approach like Squarespace?
  • Get out of the web development business entirely?

So far, I’ve mostly stuck with “cling for dear life to Classic Editor” although I have been tempted a great many times to “get out entirely.” My enthusiasm for this field hasn’t been helped by things like the caustic toxicity of social media, the rise of absolutely godawful and not-at-all-intuitive-regardless-of-their-claims-to-such interface concepts (see: Google’s Material Design), and technical snafus like Digital Ocean’s entire subnet getting spam blacklisted and them doing absolutely zero to rectify the situation.

I’ve been taking baby steps towards making sure I’m not caught out when/if they pull the plug on Classic Editor. My 34 Blocks theme is to the point where it works adequately in the Gutenberg environment, and I’m even moving it towards a potential future where I would scrap my ACF Flexible Content blocks altogether, in favor of Gutenberg blocks.

But I’ve also made sure ICS Calendar is backward-compatible with WordPress 4.9, so it works with ClassicPress. And I’m still looking at other tools, now and again, in case I need to switch directions entirely.

It’s happened before. After the first half of my career consisted largely of building “bespoke” CMSes for corporate overlords, I went out on my own. From 2008 to 2014 I sunk thousands of hours into the development of a feature-rich, completely custom-built CMS based on the CakePHP framework, which I used to create about 10 client sites per year throughout that period.

But the writing was on the wall for that project when I found it impractical to upgrade the CakePHP core past version 1.3, which was incompatible with PHP 7. (CakePHP is currently up to version 4.1 and now requires a minimum of PHP 7.2, for an indication of just how doomed my old CMS project was.) By 2014 I gave up on it and switched to WordPress. Has the time come to move on again? If so, I feel like in some ways, switching to ClassicPress would be a step backwards, or at best a lateral move, and would not set me up well for the future.

Where does that leave me? I don’t know. There are options. But embracing Gutenberg and the future of WordPress is not at the top of the list. If anything, it’s never been lower.

* Why is Hello Dolly detrimental? The justification for its inclusion in the default WordPress build is that it is a demo for new developers to learn how to build a WordPress plugin. The problem is, it’s a terrible, no good, entirely wrong example of a plugin. It’s ancient and doesn’t conform to any modern WordPress coding standards, and it’s so rudimentary that there’s no useful structure to build on for people who want to create an actually useful plugin. So why is it still included? I don’t buy the “demo” argument. It’s still there because Matt wants it to be, and that in a nutshell is my problem with Automattic running the show. (I mean look, he even “cleverly” misspelled the company name so his own fucking name is embedded in it. That annoys me every damn time I see it… almost as much as “howdy.”)

CSS text-transform can’t do “sentence case” but there’s a really simple alternative

Need to capitalize just the first letter of a text string? CSS text-transform has capitalize but that capitalizes every word.

Fortunately, you can use the ::first-letter pseudo element! In my case, I wanted all of the links in my navigation to definitely start with a capital letter, so I went with this:

#primary_navigation a::first-letter {
  text-transform: uppercase;
}

Slick!

Get rid of that annoying “WebSocket connection to ‘…’ failed” error on WordPress sites running Jetpack

If you run a self-hosted WordPress site with Jetpack, installed, you may have noticed a frequent error message showing up in your browser console.

In Chrome it looks like this:

WebSocket connection to ‘wss://public-api.wordpress.com/pinghub/wpcom/me/newest-note-data’ failed: Error during WebSocket handshake: Unexpected response code: 403

In Safari it’s slightly less specific, but the issue is the same:

WebSocket connection to ‘wss://public-api.wordpress.com/pinghub/wpcom/me/newest-note-data’ failed: Unexpected response code: 403

This issue has been reported various places online but nowhere do I find a true fix.

As indicated in some of those support threads I linked to above, it’s due to an issue with the Notifications module in Jetpack. What? It’s something I was not even really aware of and that can be extremely easy to ignore, especially if your site is not blog-focused or does not have active commenting. Turns out Jetpack has this little “Notifications” icon up in the admin bar next to your Gravatar, where you can get the latest updates on what’s happening on your blog.

If you’re like me, you don’t need or want the Notifications tool. And if getting rid of it gets rid of that stupid error that’s constantly popping up in the console, all the better.

Side note: Did you know there’s a hidden page where you can access the settings for a ton of Jetpack modules? (Neither did I.) Just go to /wp-admin/admin.php?page=jetpack_modules in your WP admin and there it is! If you’re just dealing with this issue on one particular site, and don’t want to write any code, simply deactivate the Notifications module here and you’re done. If this is something you are dealing with on many sites though and you want an automatic, code-based solution to stick into your base theme, read on…

I tried using Jetpack’s instructions for disabling a module via code. (Fun fact, the name for this module in the code is not 'notifications' as you might expect; it’s 'notes'.) Frustratingly, neither the 'option_jetpack_active_modules' filter nor the 'jetpack_get_available_modules' filter worked. I even scoured the Jetpack source code for other filters I might need to use, and none of them made a difference.

Oh, I was able to get 'notes' not to appear in the array those filters returns, and I was even able to use that to get “Notifications” to always appear as “inactive” on that hidden settings page.

But the error kept showing up in the console, and the little Notifications icon was still in the admin bar. The module was not inactive.

Finally, through further source code poking, I discovered there’s a static public method called Jetpack::deactivate_module(). I tried running it in those aforementioned filters but it resulted in an HTTP 500 error. (Strangely, it was an out-of-memory error. Shrug.)

So I decided to try the method in different actions. After some trial and error, it seems the 'init' action works. But that’s a bit of overkill, running this code on every front end page load, especially since once the module is deactivated, it should stay deactivated, so my solution is to run it on the 'admin_init' action instead. Especially since this issue only happens for logged-in users anyway, it should be adequate.

Here’s a bit of code you can stick into your theme’s functions.php file or wherever you like to put things like this:

add_action('admin_init', 'rm34_jetpack_deactivate_modules');
function rm34_jetpack_deactivate_modules() {
    if (class_exists('Jetpack') && Jetpack::is_module_active('notes')) {
        Jetpack::deactivate_module('notes');
    }
}

A WordPress URL rewrite rule to phase out year/month folders in the Media Library

This is one of those fixes where it is probably worth me explaining the very specific use case I needed it for first, to serve as an example of exactly why anyone would need this, because on the surface it may seem pointless.

Picture this: Restaurant client. Table cards with QR codes linking directly to menu PDFs, so customers can look at the menu on their phones instead of using a physical print menu that has been handled by dozens of other people.

Note to the future: I’m not sure what the restaurant experience looks like in your world. What I’m describing may be ubiquitous for you now, or may be a complete head-scratcher. Assuming it’s safe for you to touch your head. For context, I am writing this in the midst of the 2020 COVID-19 pandemic.

Here’s the problem: Menus change. URLs referenced by a QR code do not. By default, WordPress automatically creates year and month subfolders inside wp-content/uploads and puts files in the folder for the year and month the post they’re attached to was created, or if you’re uploading directly into the Media Library, not attached to a post, then the year and month the file was uploaded.

So that means that the URLs embedded in my client’s QR codes contain 2020/09. But now it’s October, so if they upload a replacement file today, its URL will contain 2020/10 and the QR code will not work. I should note at this point that I do not like the default WordPress behavior of putting files into these subfolders, but I sometimes forget to turn off this setting when I’m creating a new site, or — as is the case here — I’m working on a site someone else originally set up.

My solution: Turn off year/month folders, so that any newly uploaded PDFs with the same filename will have the same URL. (Assuming the client deletes the old one first!)

You may be thinking, well, that’s great, if you had done this before the QR codes were created. Yes, exactly. That’s where this rewrite rule comes in.

When you turn off the year/month folder setting, it doesn’t move any existing files or change any code that links to them. This purely affects new uploads going forward. So what I need is a rewrite rule that will allow existing file URLs with the year/month path to continue working, while automatically removing that bit from the URL and trying to find the same file in the main uploads folder, if there’s no file at the year/month URL.

OK, here’s the code:

# Redirect file URLs from year/month subfolders to base uploads folder if not found
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /wp-content/uploads/
RewriteRule ^index\.php$ – [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^([0-9]{4})/([0-9]{2})/(.*) /wp-content/uploads/$3 [L,R=301]
</IfModule>

This should go not in your main .htaccess file, but in an .htaccess placed inside your wp-content/uploads folder.

Let’s assess what’s going on here, line by line.

# Redirect file URLs from year/month subfolders to base uploads folder if not found

Just a comment so we remember what this is all about. You may think you’ll remember. But you probably won’t. Comments are your future friend.

<IfModule mod_rewrite.c> and </IfModule>

Apache configuration conditionals wrapper for all of our actions, to make sure this code doesn’t run if mod_rewrite isn’t enabled. Honestly I often leave this out because… come on, the entire site is going to be broken if mod_rewrite isn’t enabled.

RewriteEngine On

If you don’t know what this is about, RTFM. (The “F” is directed at the manual, not you. I hate the Apache documentation.)

RewriteBase /wp-content/uploads/

This is the reference point for the ^ later on. Needs to be the relative path of the uploads folder below your WordPress site’s base URL.

RewriteRule ^index\.php$ - [L]

Honestly we probably don’t need this line, as there shouldn’t be any index.php files inside your uploads folder anyway, but it just feels weird not to include it. This just says “don’t do any rewrites to the index.php file.”

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d

These lines are very common in this type of rewrite instruction set, and in fact come straight from the default WordPress rewrite rules. They are saying, only apply the following rules to URLs that don’t match existing real files or directories under this path. This is critical to keep all of your existing Media Library URLs working.

RewriteRule ^([0-9]{4})/([0-9]{2})/(.*) /wp-content/uploads/$3 [L,R=301]

This is what we’re here for. Note we are using the magic of regular expressions to make this work. There are three parenthetical groupings, though we technically could eliminate the first two sets of parentheses and change $3 to $1, but I just like having the parentheses to help me keep things straight.

([0-9]{4}) is matching a 4-digit number, representing the year folder.

([0-9]{2}) is matching a 2-digit number, representing the month folder.

And (.*) is matching… anything, representing the filename. That’s the bit we want to reference in the replacement string, /wp-content/uploads/$3 which tells Apache to serve up the filename from the year/month URL directly out of the uploads folder itself.

The final bit, if you’re not familiar with rewrite rules, [L,R=301], just says this is the last rule the previous set of conditions applies to, and that it should return an HTTP 301 (permanent redirect) status along with the redirect, which is good SEO karma.