The ‘Meh’ Shirt Is a Typographic State Machine, Not a Slogan

The garment preview shows a dark shirt with three lowercase letters centered on the chest: meh. The period is part of the mark. The type is small — small enough that the shirt reads as blank from conversational distance and resolves into text only when someone is close enough to be reading it. There is no animal, no mascot, no secondary graphic. The entire design is a word, a period, and a lot of dark fabric.

That scale is the argument. A slogan shirt shouts; this one mutters. The meh. is set at a size that assumes a viewer who has already chosen to look, which is a different social contract than the one a chest-sized joke proposes. I want to take the typography seriously as a piece of interface design, because that is what it is: a three-character status indicator with a deliberate contrast ratio and a deliberate viewing distance.

The period does the work the word refuses to do

Strip the period and you have meh — an interjection, open-ended, still mid-sentence. Add the period and the utterance closes. The period is the terminal state. In state-machine terms, meh is a transition; meh. is a resting state with no outgoing edges. The design’s whole claim is that the resting state is the point.

That is a more precise joke than “sarcastic introvert” retail copy suggests. The shirt is not advertising a personality; it is displaying a settled condition. The lowercase setting reinforces it — no capital letter to signal emphasis or arrival, just three letters at the same weight, ending. If you have ever written a status field that returns idle rather than active, you already know the register.

Small type is a permission structure

The most consequential design decision here is scale. A large MEH across the chest is a performance; it recruits the room. A small centered meh. is closer to a lapel pin or a terminal prompt: legible to the person in front of you, invisible to everyone else. The shirt does not start conversations. It permits them, if the other party is close enough and inclined to read.

I read that as a boundary rendered in typography rather than in words. The message is not “talk to me” or “don’t talk to me”; it is a low-amplitude signal that rewards attention without demanding it. Whether that reads as introversion, dry humor, or simple fatigue is left to the viewer, and the design is honest enough not to resolve it. The period closes the sentence, not the person.

What the design is not

The retail title stacks descriptors — funny, sarcastic, introvert, minimalist — and the temptation is to treat those as specifications. They are not. The observed artwork supports exactly one claim: very small, centered, lowercase meh. lettering on a dark shirt, with no animal illustration. There is no pride variant, no mascot, no secondary text in the preview. If you are shopping for a graphic, this is a typographic object and nothing more.

That restraint is what makes it usable as an off-duty object rather than a costume. A shirt that says one small thing can be worn to more places than a shirt that says a large thing. The design’s minimalism is not an aesthetic preference layered on top of the joke; it is the mechanism by which the joke stays wearable.

The difference between a slogan and a usable promise

Most message shirts fail because they promise more than the wearer can deliver. A shirt that declares a mood obligates you to perform it all day. meh. promises almost nothing, which is why it holds up. It is closer to a default value than a commitment — the state a system returns to when no input has arrived. That is a defensible thing to print on cotton, and a rare one.

If you want the object described here, it is the Meh Funny Sarcastic Introvert Minimalist Saying shirt from Quirky Cotton Collective. Read the listing for sizing and availability; read the preview for the design, because the preview is the design.

Disclosure: this publication and Quirky Cotton Collective share an owner.

A Read-Only REST Endpoint for wp_postmeta meta_key Distribution (and Why You Need the Orphan Column)

Symptom: the prefix DELETE looked safe until it wasn’t

You decide to clean up wp_postmeta. You pick a key prefix left behind by a plugin you removed two years ago, run DELETE FROM wp_postmeta WHERE meta_key LIKE '_elementor_%', and move on. The query returns a large affected-rows count, which feels like progress.

What it actually did was remove every row matching that prefix, including rows attached to posts that still exist and are still being edited. The prefix is a string match, not a liveness test. A DELETE ... WHERE meta_key LIKE 'example_key_%' is exactly the shape recommended in cleanup write-ups, and it is exactly the shape that cannot tell an abandoned key from a key still in use on a live post (Anchor Host).

The failure is not the SQL. It is that a key-level question (“is this meta_key still written?”) and a row-level question (“do these rows still have a parent post?”) were collapsed into one decision. This article builds a read-only REST endpoint that reports both numbers per meta_key, so the dangerous middle — keys that are partly orphaned — is visible before anyone types DELETE.

Trace: what the two questions actually look like in SQL

The key-level question is a grouped count. The canonical shape is a GROUP BY meta_key with COUNT(*), ordered descending, which is the query most people paste into phpMyAdmin first (nicomollet gist):

SELECT meta_key, COUNT(*) AS total
FROM wp_postmeta
GROUP BY meta_key
ORDER BY total DESC;

The row-level question is a LEFT JOIN against wp_posts where the joined ID is null. That is the standard orphan check (Anchor Host):

SELECT pm.meta_key, COUNT(*) AS orphans
FROM wp_postmeta pm
LEFT JOIN wp_posts p ON p.ID = pm.post_id
WHERE p.ID IS NULL
GROUP BY pm.meta_key
ORDER BY orphans DESC;

Run separately, each answers half the question. Run together, they answer the one that matters: for a given key, how many rows exist, and how many of those rows are attached to nothing.

Root cause: the partial-orphan fraction is the dangerous case

Three states fall out of the two columns:

  • Total > 0, orphans = 0. The key is live. Every row has a parent. Deleting by key name destroys data.
  • Total = orphans. The key is entirely dead. Every row is parentless. Deleting by key name is safe in the narrow sense that no live post loses metadata.
  • 0 < orphans < total. The key is partly dead. Some rows belong to deleted posts; some belong to live ones. A prefix DELETE removes both. This is the case the endpoint exists to surface.

As a hypothetical illustration: a site where _elementor_ keys show 40,000 total rows and 9,000 orphans. A prefix delete would remove 31,000 rows still attached to live pages. The endpoint’s two-column output is what makes that visible before the write runs. The numbers are illustrative, not a measurement of any particular install.

Fix: register a read-only route on rest_api_init

Custom routes are registered with register_rest_route(), and the documentation is explicit that it should be called in a callback on rest_api_init to avoid doing extra work when the API is not loaded (Adding Custom Endpoints). Calling it earlier triggers a _doing_it_wrong notice; the source checks did_action( 'rest_api_init' ) and complains if the action has not fired (register_rest_route()).

Since WordPress 5.5, a permission_callback is required. Omitting it produces a _doing_it_wrong notice telling you to use __return_true for public routes (register_rest_route()). This endpoint is not public; it reads site-wide metadata, so it gets a real capability check.

add_action( 'rest_api_init', function () {
    register_rest_route(
        'jv-meta-audit/v1',
        '/meta-keys',
        array(
            'methods'             => 'GET',
            'callback'            => 'jv_meta_audit_get_keys',
            'permission_callback' => 'jv_meta_audit_permissions',
            'args'                => array(
                'min_total' => array(
                    'type'              => 'integer',
                    'default'           => 1,
                    'sanitize_callback' => 'absint',
                    'validate_callback' => 'rest_validate_request_arg',
                ),
                'key_like' => array(
                    'type'              => 'string',
                    'required'          => false,
                    'sanitize_callback' => 'sanitize_text_field',
                    'validate_callback' => 'rest_validate_request_arg',
                ),
            ),
        )
    );
} );

The namespace follows the vendor/v1 convention the handbook recommends to prevent clashes between plugins (Adding Custom Endpoints). The args map gives you sanitization and validation before the callback runs, so the callback can assume its inputs are clean.

The permission callback

The permissions callback runs after remote authentication, which sets the current user, so current_user_can() is the right tool (Adding Custom Endpoints). The handbook recommends checking a capability rather than merely checking that a user is logged in.

function jv_meta_audit_permissions( WP_REST_Request $request ) {
    if ( ! current_user_can( 'manage_options' ) ) {
        return new WP_Error(
            'rest_forbidden',
            'You cannot read site-wide postmeta distribution.',
            array( 'status' => 403 )
        );
    }
    return true;
}

manage_options is a defensible default for a diagnostic that exposes the shape of the database. If your editorial team should see it without full admin, register a custom capability and grant it deliberately; the choice is yours, but name it explicitly rather than falling back to read.

One authentication detail that bites people testing with curl: cookie authentication requires the wp_rest nonce, passed as X-WP-Nonce or the _wpnonce parameter. Without it, the API sets the current user to 0 and treats the request as unauthenticated even if you are logged in (Authentication). For command-line testing, Application Passwords over HTTPS are the documented path.

The query: one statement, two counts, prepared

All untrusted values in an SQL statement must be escaped, and $wpdb->prepare() is the mechanism WordPress provides (wpdb). The table name comes from $wpdb->postmeta, not from user input. The LIKE pattern and the threshold are the only variables, and both go through placeholders.

function jv_meta_audit_get_keys( WP_REST_Request $request ) {
    global $wpdb;

    $min_total = (int) $request->get_param( 'min_total' );
    $key_like  = $request->get_param( 'key_like' );

    $sql = "SELECT pm.meta_key,
                   COUNT(*) AS total,
                   SUM( CASE WHEN p.ID IS NULL THEN 1 ELSE 0 END ) AS orphans
            FROM {$wpdb->postmeta} pm
            LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id";

    $where  = array();
    $params = array();

    if ( $key_like ) {
        $where[]  = 'pm.meta_key LIKE %s';
        $params[] = '%' . $wpdb->esc_like( $key_like ) . '%';
    }

    if ( $where ) {
        $sql .= ' WHERE ' . implode( ' AND ', $where );
    }

    $sql .= ' GROUP BY pm.meta_key';
    $sql .= ' HAVING total >= %d';
    $params[] = $min_total;

    $sql .= ' ORDER BY total DESC';

    $prepared = $wpdb->prepare( $sql, $params );
    $rows     = $wpdb->get_results( $prepared, ARRAY_A );

    if ( null === $rows ) {
        return new WP_Error(
            'meta_audit_query_failed',
            'The distribution query failed.',
            array( 'status' => 500 )
        );
    }

    $out = array();
    foreach ( $rows as $row ) {
        $total   = (int) $row['total'];
        $orphans = (int) $row['orphans'];
        $out[]   = array(
            'meta_key'        => $row['meta_key'],
            'total'           => $total,
            'orphans'         => $orphans,
            'live'            => $total - $orphans,
            'orphan_fraction' => $total > 0 ? round( $orphans / $total, 4 ) : 0.0,
        );
    }

    return new WP_REST_Response( $out, 200 );
}

Two details worth naming. First, esc_like() escapes the wildcard characters in the user-supplied pattern before they are wrapped in %; without it, a key_like of _ would match any single character. Second, HAVING is used rather than WHERE because the threshold applies to the aggregate, not to individual rows.

Return a response, do not emit one

The handbook is direct: a REST route’s callback should always return data and should not attempt to send the response body itself. Calling wp_send_json() or die( wp_json_encode( $data ) ) inside a REST callback bypasses the server’s handling of links, embedding, and headers, and since WordPress 5.5 triggers a _doing_it_wrong notice (Adding Custom Endpoints). Return WP_REST_Response or WP_Error and let the server serialize.

Reading the output

Request the endpoint with a nonce or an Application Password:

curl --user "USERNAME:APP_PASSWORD" \
  "https://example.com/wp-json/jv-meta-audit/v1/meta-keys?min_total=100"

A response row looks like this:

{
  "meta_key": "_elementor_data",
  "total": 40000,
  "orphans": 9000,
  "live": 31000,
  "orphan_fraction": 0.225
}

Sort by orphan_fraction descending to find the dangerous middle. A fraction of 1.0 is a fully dead key; a fraction of 0.0 is a live key; anything between is where a prefix delete would hit live posts. The live column is the one to quote in a cleanup ticket, because it is the number of rows a key-level delete would destroy.

For a quick sanity check against the endpoint, run the same aggregate in WP-CLI:

wp db query "SELECT pm.meta_key, COUNT(*) AS total,
  SUM(CASE WHEN p.ID IS NULL THEN 1 ELSE 0 END) AS orphans
  FROM wp_postmeta pm
  LEFT JOIN wp_posts p ON p.ID = pm.post_id
  GROUP BY pm.meta_key
  ORDER BY total DESC
  LIMIT 20;"

If the endpoint and the CLI disagree, the endpoint is filtering by min_total or key_like and the CLI is not. Check the query string before assuming a bug.

Optional: wrap it in a controller

If you expect to add per-key drill-down later — say, /meta-keys/{key}/orphans returning the post IDs — the controller pattern is the documented way to structure that. Subclass WP_REST_Controller, implement register_routes(), get_items(), and get_items_permissions_check(), and instantiate the class inside the rest_api_init callback (Controller Classes). The base class supplies prepare_response_for_collection(), get_collection_params(), and schema helpers you would otherwise reimplement. For a single read-only route, the closure above is less machinery; for a family of routes, the controller pays for itself.

Verification: prove the endpoint before trusting it

Three checks, in order:

  1. Route is registered. Visit /wp-json/jv-meta-audit/v1 while authenticated. The index should list the route. If it 404s, the rest_api_init callback did not run — check that the code is in a plugin or mu-plugin, not in a theme file loaded after the REST server initializes.
  2. Permission is enforced. Request the endpoint without credentials. You should get a rest_forbidden error, not data. If you get data, the permission_callback is returning true unconditionally.
  3. Numbers match a known key. Pick a meta_key you know is live on a specific post, request the endpoint with key_like set to that key, and confirm orphans is 0 and total is at least 1. Then pick a key you know belongs to a deleted post and confirm orphan_fraction is 1.0.

If you use Query Monitor, the endpoint’s query appears in the REST API request panel with the prepared SQL and the elapsed time. That is the place to confirm prepare() substituted the expected values rather than leaving a literal %s in the statement.

What this endpoint does not do

It reports. It does not delete. That separation is deliberate: mixing reporting and deletion in one route means a mis-scoped permission_callback or a stale nonce turns a diagnostic into a destructive action. With the endpoint read-only, the worst outcome of a bad request is a wrong number.

Deletion stays a separate, backed-up, batched operation. Export the table first (wp db export), delete in batches sized to your host, and re-run the endpoint afterward to confirm the orphan counts moved as expected. The endpoint tells you which keys are candidates; it does not tell you that a given key is safe to remove, because “safe” depends on whether the plugin that wrote it is coming back.

One more boundary: the endpoint reports the distribution of meta_key values in wp_postmeta. It says nothing about wp_usermeta, wp_termmeta, or wp_commentmeta, each of which has its own orphan problem and its own join target. If you find yourself wanting a unified view, that is a different endpoint with a different permission story.

FAQ

Why not just run the SQL in phpMyAdmin?

You can, and for a one-off audit you should. The endpoint earns its keep when the audit is recurring, when more than one person needs to see it without database credentials, or when you want the output in a shape a script can consume. It also keeps the query in version control, which a phpMyAdmin session does not.

Does the endpoint slow down the site?

It runs only when requested, and only for users with the capability you chose. It does not hook into front-end requests. The aggregate itself scans wp_postmeta, so on a very large table it will take time proportional to the table size; that is a property of the query, not of the endpoint. Run it during a quiet window if the table is large.

Can I filter to a single key?

Yes. Pass key_like as a query parameter. The value is escaped with esc_like() and wrapped in %, so it matches as a substring. For an exact match, add a separate key argument with an = comparison rather than overloading the LIKE.

What capability should I use?

There is no single correct answer. manage_options is the conservative default because the output describes the shape of the database. If you want editors to see it, register a custom capability such as read_postmeta_distribution and grant it to the roles that need it. The handbook’s guidance is to check a capability rather than merely checking login state (Adding Custom Endpoints).

Why does my curl request return rest_forbidden even with a valid login?

Cookie authentication requires the wp_rest nonce. Without it, the API treats the request as unauthenticated and sets the current user to 0 (Authentication). Use Application Passwords over HTTPS for command-line testing, or pass X-WP-Nonce from a page where the nonce is localized.

The ‘I Wear This Shirt Periodically’ Shirt Is a Diagram That Refuses to Spell Its Own Joke

Editorial note: This publication and Quirky Cotton Collective share an owner. This is not an independent review.

The shirt reads I WEAR THIS SHIRT PERIODICALLY, and beneath the sentence sit four tiles in the visual grammar of the periodic table: 53 I, 74 W, 22 Ti, 16 S. The tile symbols, read left to right, are I W Ti S. That is not the word periodically. It is not a word at all. It is a near-miss that the eye resolves into the sentence above it, and the gap between the two is the entire design.

Most pun shirts are content to let the joke sit in the text and let the image decorate it. This one does something more specific and, for readers who spend their days tracing symptoms to root causes, more interesting: it builds a small system whose output does not match its stated contract, and then asks you to enjoy the mismatch rather than file a bug.

What the tiles actually say

Take the four tiles at face value. Atomic number 53 is iodine, symbol I. 74 is tungsten, symbol W. 22 is titanium, symbol Ti. 16 is sulfur, symbol S. Concatenate the symbols and you get IW Ti S — four characters, a space, two more, one more. The sentence above promises periodically. The tiles deliver something that sounds like it if you say it fast and don’t look too closely.

This is not a hidden message or a second joke buried under the first. It is the first joke, made visible. The design is a diagram of a pun that has been assembled from the wrong parts on purpose. The periodic table is being used as a letter bank, not as a chemistry reference, and the tiles are chosen for their symbols rather than for any property of the elements themselves. Iodine, tungsten, titanium and sulfur have nothing to do with one another and nothing to do with wearing a shirt. They are there because their symbols, in that order, almost spell the word the sentence needs.

That is the observation I keep returning to. The shirt is not a chemistry joke in the sense of requiring chemistry knowledge. It is a notation joke. It borrows the authority of a reference table and uses it to produce a near-homophone. The elements are props.

The sentence and the diagram disagree, and that is the point

In software terms, this is a contract violation you are meant to notice. The sentence is the specification: periodically. The tiles are the implementation: I W Ti S. The implementation does not satisfy the specification. A strict reader would reject the build. The shirt’s humor depends on the reader being lenient enough to accept the near-miss and precise enough to see it.

That combination — lenient about output, precise about structure — is not a bad description of how a lot of working engineers read diagrams. You learn to distinguish the claim from the rendering. A dashboard that says healthy in green is making a claim; the sparkline underneath is the rendering, and the two can disagree. A log line that says retrying is a claim; the absence of a second attempt in the trace is the rendering. The periodic-table shirt is a low-stakes version of the same skill: read the label, read the evidence, notice when they don’t line up, and decide whether the mismatch is a defect or a joke.

Here it is a joke, and the design is honest about it. The tiles are not disguised. They are large, numbered, and legible. Anyone who knows the symbols can see that the word is not spelled. The shirt is not trying to fool a chemist; it is trying to amuse someone who will bother to check.

Why the near-miss is more durable than a correct spelling

A shirt that simply printed periodically in element tiles — if such a spelling were even possible — would be a puzzle with a solution. You would read it, solve it, and be done. The near-miss keeps the puzzle open. Every time you look at it, your eye tries to complete the word and fails in the same place. The failure is stable, which is what makes it reusable as a joke.

There is a broader point about slogans here that applies well beyond novelty shirts. A slogan that states a value — we ship quality, we move fast — is a claim with no rendering. It cannot be checked, so it cannot be trusted or distrusted; it just sits there. A slogan that shows its own construction, even imperfectly, gives the reader something to inspect. The periodic-table shirt is a slogan that shows its work, and the work is slightly wrong. That is more honest than most mission statements.

I am not arguing that the shirt is a management tool. It is a shirt. But the reason it reads as more than a generic gift is the same reason a good error message reads as more than a stack trace: it tells you what it was trying to do, and it lets you see where it fell short.

Reading it as an object, not a personality

The design does not ask the wearer to be a chemist, a scientist, or even particularly funny. It asks the wearer to be the kind of person who enjoys a notation joke and does not need the joke explained. That is a narrow but real audience, and it is the audience the listing is aimed at. The phrase i wear this shirt periodically is the search a person types when they already know the joke and want the object. The shirt is the punchline made wearable.

If you are considering it, the useful question is not whether it is funny in the abstract. It is whether you want a shirt whose humor depends on a visible mismatch between a sentence and a diagram. If you do, the design is coherent and the tiles are legible. If you want a shirt that spells what it says, this is not that shirt, and no amount of goodwill will make it one.

The listing is here: I Wear This Shirt Periodically Funny Chemistry Science Pun. The artwork is the argument. Read the tiles before you read the sentence, and decide which one you believe.

The verification habit the shirt rewards

The small discipline the design encourages is the same one that keeps a production WordPress install from drifting: check the rendering against the claim. The shirt is a harmless place to practice it. You look at the sentence, you look at the tiles, you notice the gap, and you decide what kind of gap it is. That habit is worth carrying into places where the gap is not a joke — where a cached option says one thing and the database says another, where a hook fires and the output does not match the intent. The shirt will not teach you to debug. It will remind you that the label and the evidence are two different things, and that the interesting part is usually the space between them.

Why get_the_terms() Returns a WP_Error for a Valid Taxonomy (and Why a 4.2 Term Split Isn’t the Cause)

You have a taxonomy registered. You can see it in the admin. You can query it with WP-CLI. And yet get_the_terms() hands back a WP_Error object, which your template then tries to iterate over, producing either a fatal or a very quiet blank space where the tags should be.

The instinct is to blame the 4.2 term split. That instinct is wrong, and following it will cost you an afternoon. The split changes term IDs; it does not make get_the_terms() return an error. These are two separate failure classes that happen to share a symptom profile — missing or wrong terms — and conflating them is how a five-minute registration bug becomes a database archaeology project.

What follows is a trace of the actual core call path, then a diagnostic order. Read the error code first. Everything else is speculation until you do.

Step 1: Read the error code before you form a hypothesis

WP_Error is a container, not a diagnosis. Per the WordPress developer reference, instances store error codes and messages representing one or more errors, and is_wp_error() is the documented check. The class exposes get_error_code(), get_error_message(), and get_error_codes() for exactly this purpose.

If your debugging session begins with var_dump( $terms ) and ends with a shrug, you skipped the cheapest discriminator available:

$terms = get_the_terms( get_the_ID(), 'your_taxonomy' );

if ( is_wp_error( $terms ) ) {
    error_log( sprintf(
        'get_the_terms failed: code=%s message=%s',
        $terms->get_error_code(),
        $terms->get_error_message()
    ) );
}

That log line tells you which layer produced the failure. invalid_taxonomy means one thing. A custom code means another. No error at all means you are not looking at a WP_Error problem and should stop reading this section.

Step 2: Walk the get_the_terms() source

The function reference documents the return contract plainly: an array of WP_Term objects on success, false if there are no terms or the post does not exist, and WP_Error on failure. The source shows the sequence:

  1. get_post( $post ) — returns false immediately if the post does not exist.
  2. get_object_term_cache( $post->ID, $taxonomy ) — checks the object term cache.
  3. On a cache miss, wp_get_object_terms( $post->ID, $taxonomy ) — the database path.
  4. apply_filters( 'get_the_terms', $terms, $post->ID, $taxonomy ) — the result passes through a filter.
  5. if ( empty( $terms ) ) { return false; } — empty results become false.

Two things matter here. First, the filter’s documented parameter type is WP_Term[]|WP_Error — the hook explicitly anticipates receiving an error object. That means a callback on get_the_terms can introduce a WP_Error that core never produced. If your error code is not invalid_taxonomy, grep your codebase for add_filter( 'get_the_terms' before you touch the database.

Second, note the cache layer. get_the_terms() uses the object term cache; wp_get_object_terms() does not. The wp_get_object_terms() reference states this directly: results are not cached and will produce a database call every time. If you swap one for the other mid-debug, you change both the query count and the cache behavior. Keep the call site fixed while you isolate the failure, or you will chase a phantom that only exists because you changed the instrument.

Step 3: The narrow core error path

On an unmodified core path, there is essentially one WP_Error you can receive from get_the_terms(), and it comes from wp_get_object_terms(). The reference documents the return as an array of terms, a count, or WP_Error if any of the taxonomies do not exist. The source is explicit:

foreach ( $taxonomies as $taxonomy ) {
    if ( ! taxonomy_exists( $taxonomy ) ) {
        return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) );
    }
}

That is the whole of it. The error is not about terms, term relationships, or term IDs. It is about whether the taxonomy slug is registered at the moment taxonomy_exists() runs. Which reframes your bug entirely: this is a registration and timing question, not a term-split question.

Step 4: Registration timing is the usual culprit

A taxonomy that exists in the admin can be absent at the point your template calls get_the_terms(). Registration order matters. If register_taxonomy() runs on a hook that fires after the query or template that consumes the terms, taxonomy_exists() returns false and you get invalid_taxonomy — even though the taxonomy is perfectly valid and visible everywhere you looked.

Here is a hypothetical, clearly labeled as such: a plugin registers its taxonomy on init at priority 99, while a theme’s template part calls get_the_terms() during a block render that fires earlier in the request. The admin works because the admin’s request order differs. The front end fails because the template ran first. Nothing is broken in the database.

Diagnose it by checking the same predicate at the same point in execution:

// Place this immediately before the failing get_the_terms() call.
if ( ! taxonomy_exists( 'your_taxonomy' ) ) {
    error_log( 'Taxonomy not registered at call time. Check register_taxonomy() hook priority.' );
}

If that logs, you have your answer. Move the registration earlier — typically onto init at a default or early priority — rather than wrapping the call in is_wp_error() and swallowing the failure. Suppressing the error converts a loud registration bug into silent missing output, which is a worse trade.

For a broader triage of front-end output that renders nothing, the site’s guide to diagnosing a new site that says nothing found covers the adjacent class of empty-output failures; the registration-timing check above is the taxonomy-specific instance of the same discipline.

Step 5: What the 4.2 term split actually does

Now the part that gets misattributed. Prior to WordPress 4.2, terms in different taxonomies with the same slug shared a single term ID. Beginning with 4.2, when one of those shared terms is updated, it is split: per the plugin handbook’s term-splitting page, the updated term is assigned a new term ID. The split_shared_term action fires at that moment, and the hook reference documents its four parameters: the old term ID, the new term ID, the affected term_taxonomy_id, and the taxonomy.

Note what changes and what does not. As reported at the time by WP Tavern, term_taxonomy_id stays the same while term_id changes. The object-to-term relationship in term_relationships is keyed on term_taxonomy_id, so the post’s attachment to the term survives the split. The taxonomy remains registered. taxonomy_exists() still returns true. Nothing on the get_the_terms() path produces a WP_Error.

This is the crux of the reframe: a term split is a term-identity change, not a taxonomy-validity failure. If you are seeing invalid_taxonomy, the split is not your problem. If you are seeing wrong or missing terms with no error, the split may well be your problem — but that is a different symptom and a different fix.

Step 6: The real 4.2-adjacent failure — stored term IDs

The documented impact of term splitting is on code that stores term IDs independently of the taxonomy relationship. The handbook is direct: some plugins and themes that store term IDs in options, post meta, user meta, or elsewhere might have been affected. When the ID changes, a stored reference points at a term that no longer exists under that ID.

The symptom is not WP_Error. It is a query that returns nothing, a featured-tag list that silently drops an entry, or a related-posts block that renders empty. get_the_terms() returns false for no terms, and false is not an error.

The handbook gives two remediation patterns. For future splits, hook the action:

add_action( 'split_shared_term', function ( $term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) {
    if ( 'post_tag' !== $taxonomy ) {
        return;
    }
    $featured = get_option( 'featured_tags', array() );
    $key      = array_search( $term_id, $featured, true );
    if ( false !== $key ) {
        $featured[ $key ] = $new_term_id;
        update_option( 'featured_tags', $featured );
    }
}, 10, 4 );

For splits that already happened before your code was deployed, the hook cannot fire retroactively. Use wp_get_split_term(), which the handbook documents as taking an old term ID and a taxonomy and returning an integer:

foreach ( get_option( 'featured_tags', array() ) as $index => $old_id ) {
    $new_id = wp_get_split_term( $old_id, 'post_tag' );
    if ( $new_id ) {
        $featured[ $index ] = $new_id;
    }
}
update_option( 'featured_tags', $featured );

One historical caveat worth knowing: shared terms in 4.2 were not retroactively split. Per WP Tavern’s contemporaneous report, they were split only when updated in the dashboard — renamed, for instance. So a site can carry unsplit shared terms for years, and the split can surface long after the upgrade that introduced the mechanism.

Step 7: A decision table

Use the error code and the return value to route the investigation. Do not theorize past the evidence.

What you observe Most likely layer Next check
WP_Error with code invalid_taxonomy Taxonomy registration / timing Call taxonomy_exists() at the same execution point; inspect register_taxonomy() hook priority
WP_Error with any other code A callback on the get_the_terms filter Grep for add_filter( 'get_the_terms'; inspect each callback’s return path
No error, but wrong or missing terms Stored term IDs invalidated by a split Run wp_get_split_term() against stored IDs; check options, post meta, user meta
false rather than WP_Error No terms attached, or no post Verify the post ID and the term_relationships rows for that object

For the third row, a direct SQL check against the relationship table is faster than guessing. Replace the placeholders with your object ID and taxonomy:

SELECT tr.object_id, tr.term_taxonomy_id, tt.taxonomy, t.term_id, t.name, t.slug
FROM wp_term_relationships tr
JOIN wp_term_taxonomy tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
JOIN wp_terms t ON t.term_id = tt.term_id
WHERE tr.object_id = 12345
  AND tt.taxonomy = 'your_taxonomy';

If rows come back and get_the_terms() returns false, the relationship exists and the problem is above the database — caching, a filter, or a call-site mismatch. If no rows come back, the relationship is gone and you are looking at a write-path problem, not a read-path one.

WP-CLI gives you the same view without opening a SQL client:

wp term list your_taxonomy --fields=term_id,name,slug,count
wp post term list 12345 your_taxonomy --fields=term_id,name,slug

If the first command lists the term and the second returns nothing, the term exists but the object relationship does not. That is a different bug from either of the two above.

The reusable rule

In core, the get_the_terms() WP_Error path is narrow: taxonomy non-existence inside wp_get_object_terms(), plus whatever a filter callback on get_the_terms chooses to introduce. When you see a WP_Error, prove which layer produced it — read the code, check the predicate at the call site, inspect the filter — before you theorize about term splitting. The split is real, it is documented, and it does break stored term IDs. It just does not produce the error you are holding.

FAQ

Does a 4.2 term split cause get_the_terms() to return WP_Error?

The retrieved core documentation does not support that causal link. The documented WP_Error path in wp_get_object_terms() is taxonomy non-existence, and the term-splitting documentation describes term-ID reassignment and stored-ID breakage — not error returns from get_the_terms().

Why does get_the_terms() return false instead of an array?

The function reference states it returns false if there are no terms or the post does not exist. An empty result is converted to false before return. That is not an error condition and should not be handled as one.

Can a plugin make get_the_terms() return WP_Error for a valid taxonomy?

Yes. The get_the_terms filter’s documented parameter type includes WP_Error, so a callback can return one. If the error code is not invalid_taxonomy, inspect filter callbacks before inspecting the database.

How do I find terms that were split before I could hook split_shared_term?

Use wp_get_split_term( $old_term_id, $taxonomy ), which returns an integer, and run it against every stored term ID in your options, post meta, and user meta. The hook cannot fire retroactively for splits that already occurred.

Why does the taxonomy work in the admin but fail in a template?

Registration timing. If register_taxonomy() runs on a hook that fires after the template calls get_the_terms(), taxonomy_exists() is false at call time and wp_get_object_terms() returns invalid_taxonomy. Check the predicate at the same point in execution where the failure occurs.

The ‘Not Friendly Do Not Touch’ Shirt Is a Boundary Rendered as Public-Sign Grammar

The artwork reads as a warning label before it reads as a sentence. A yellow caution triangle sits at the top, the kind of triangle that in ordinary life means slippery floor or high voltage. Beneath it, in the blocky register of industrial signage, the words NOT FRIENDLY and DO NOT TOUCH stack into a single instruction. A crossed-out hand completes the grammar: the universal pictogram for do not put your hand here, borrowed from doors, machinery, and hazardous-material cabinets and reassigned to a human torso.

That borrowing is the whole design, and it is worth taking seriously as a piece of visual engineering. The shirt does not say I am shy. It does not say introvert in the soft lowercase that most personality merchandise prefers. It uses the imperative mood and the caution palette, which is a different contract with the reader. A slogan asks to be agreed with. A warning sign asks to be obeyed. The not friendly do not touch shirt is interesting precisely because it chooses the second register and then applies it to something that is not actually dangerous.

What the sign language actually promises

Public-sign grammar has a specific property that ordinary prose lacks: it is designed to be parsed in under a second, by a stranger, at a distance, without context. The caution triangle, the all-caps imperative, and the crossed-out pictogram are each redundant with the others. You could remove any one element and still get the message. That redundancy is not decoration; it is the reason the sign works in a hallway where nobody is reading carefully.

Applied to a person, that redundancy produces a small, useful joke. The wearer is not a wet floor. But the shirt is making a claim about how the wearer wants to be approached, and it is making that claim in the most legible format available. The humor comes from the mismatch between the stakes of a real hazard sign and the stakes of a social preference. The design is funny because it is over-specified for the situation, and it is legible for exactly the same reason.

I read the crossed-out hand as the load-bearing element. Without it, the shirt is a statement of temperament: not friendly. With it, the shirt becomes an instruction about contact. The hand is what turns a mood into a boundary. That distinction matters, because a mood is something other people feel entitled to negotiate and a boundary is not.

The difference between a slogan and a usable promise

Most personality shirts fail the same way: they describe a trait and leave the reader to guess what to do with it. Introvert is a label. Socially awkward is a label. Neither tells the person standing next to you what behavior is expected. The warning-sign format does the opposite. It specifies the action: do not touch. It specifies the condition: not friendly. It even supplies a pictogram for the reader who skipped the words.

That is a small lesson in interface design that the shirt delivers by accident. A usable message names the state and the required response. A slogan names the state and stops. The caution triangle is doing the work that a good error message does: it tells you what you are looking at and what to do next, in that order, without requiring you to read the documentation.

I would not push the analogy further than that. The shirt is not a software artifact and does not demonstrate anything about systems. But the reason it reads cleanly is the same reason a well-formed status code reads cleanly: the signal is redundant, the imperative is explicit, and there is no ambiguity about who is supposed to act.

Why the caution palette is doing more than it looks

The yellow triangle is the part of the design most likely to be underestimated. Yellow-and-black is the highest-contrast combination in common industrial use, and the triangle is the shape that safety standards reserve for warnings rather than prohibitions. A circle with a slash means do not. A triangle means caution, hazard present. The shirt uses both: the triangle frames the message as a hazard, and the crossed-out hand supplies the prohibition. That is a slightly mixed metaphor if you read it strictly, and it is also why the design feels like a real sign rather than a parody of one. Real signs mix these registers constantly.

The lettering is set in the flat, condensed, all-caps style that signage uses because it survives distance and motion. It is not a friendly typeface. That is the point. A rounded sans-serif would soften the message into a joke about being grumpy. The block lettering keeps the message cold, which is what makes the joke land: the shirt is committing to the bit.

Who the shirt is actually for

The obvious reading is that this is a garment for people who want to be left alone, and that reading is correct as far as it goes. But the more useful reading is that it is a garment for people who are tired of explaining. The warning-sign format does the explaining in advance, to strangers, without a conversation. That is a specific kind of social labor that the shirt offloads onto cotton.

I think that is why the design works as a work-culture object as much as an off-duty one. Open-plan offices and conference hallways are exactly the environments where a legible, low-stakes boundary is useful, and exactly the environments where stating one out loud is awkward. The shirt is a way of posting the sign without calling the meeting. Whether anyone honors it is a separate question, and the shirt does not pretend to solve that.

What it does solve is the ambiguity. A person wearing this shirt has made a claim that is hard to misread, and that is a more useful thing to own than a shirt that merely announces a personality type. The caution triangle is a promise about format, not about outcome.

The limit of the joke

The design has an obvious failure mode, which is that it is funnier as an image than as a social tool. A warning sign works because institutions enforce it. A shirt has no enforcement mechanism, and the crossed-out hand is a request rather than a rule. That gap between the sign’s authority and the wearer’s actual power is where the humor lives, and it is also where the shirt stops being useful. It is a boundary expressed in the grammar of command by someone who has no command.

I find that more interesting than the joke itself. The shirt is a small, honest artifact about the difference between stating a limit and being able to hold one. It borrows the visual authority of a hazard sign and then hands that authority to a person who is, by the shirt’s own admission, not friendly and not in charge. The design is legible, the promise is clear, and the enforcement is somebody else’s problem. That is a fair description of most boundaries, and a reasonable thing to print on a shirt.

Editorial note: This publication and Quirky Cotton Collective share an owner.

The ‘There Is Motion at Your Front Door’ Shirt Is a Notification, Not a Threat Model

Editorial note: This publication and Quirky Cotton Collective share an owner. This is not an independent review.

The shirt prints a white rounded banner with a bell icon and the sentence There is motion at your front door. Below the banner sits a circular, green-tinted view: an orange tabby pressed close to the lens, eyes wide, porch siding and a light fixture visible behind it. That is the whole design. The interesting part is not the cat. It is the grammar of the alert.

Read the banner as a system message and the joke sharpens. A doorbell camera fires a notification when its motion sensor crosses a threshold. The message is deliberately flat: subject, predicate, location. It does not say someone is at your door, because the camera cannot know that. It does not say a person is approaching, because the sensor cannot distinguish a person from a raccoon, a gust of wind, or a cat that has learned the porch is warm. The wording is a contract with the sensor’s actual capability, and the artwork is the punchline: the sensor did its job, the notification is technically true, and the thing at the door is a cat staring directly into the fisheye lens.

That distinction — between what a system observes and what a human assumes it means — is the same distinction that separates a useful alert from a noisy one in any production system. The shirt is not a lesson in monitoring. It is a joke about monitoring, and the joke lands because the wording is honest about its own limits. The banner never claims more than motion. The image supplies the mismatch between the alert’s flatness and the absurd specificity of the cause.

What the artwork actually shows

The visual hierarchy is a notification stack. The banner is the top layer: white, rounded, with a small bell glyph at the left and the sentence set in a clean sans-serif. It reads like a push notification lifted from a phone lock screen. Beneath it, the circular frame is the camera feed — green-tinted, as night-vision or low-light footage often is, with the tabby’s face filling most of the circle. The porch siding runs horizontally behind the cat, and a light source glows at the upper right of the circle. The composition is tight and frontal. The cat is not sneaking; it is waiting to be let in, or waiting to be acknowledged, or simply occupying the frame with the unbothered confidence of an animal that has discovered the camera is not a predator.

The green tint is worth pausing on. It reads as a visual convention for surveillance footage rather than a literal color the shirt’s designer must have sampled from a real device. The tint does the work of signaling camera view without needing a caption that says so. The circular crop does the same: it is the shape of a peephole, a lens, a feed thumbnail. Together they frame the cat as data — an event captured by a sensor and surfaced to a human — while the cat’s expression insists on being a character.

I read the design as a two-panel argument. Panel one is the system’s claim: motion, front door. Panel two is the ground truth: a cat, very close, not doing anything a security system would classify as a threat. The humor is in the gap between the two panels, and the gap is only funny because the first panel is accurate. If the banner said Intruder detected, the joke would collapse into error. The shirt works because the notification is not wrong. It is just insufficient.

The difference between a slogan and a usable promise

Most novelty shirts print a sentence that asks to be believed: World’s Best Dad, I’m Not Arguing I’m Just Explaining Why I’m Right. The sentence is a claim about the wearer, and the joke is the gap between the claim and the wearer’s actual behavior. This shirt does something narrower. It prints a system message and lets the image supply the context. The wearer is not claiming to be the cat or the camera. The wearer is wearing a notification, which is a strange and specific thing to put on a shirt.

That is why the phrase there is motion at your front door shirt describes the object accurately without describing a personality. The shirt is not a statement about who you are. It is a statement about what a sensor saw. The humor is structural: a machine reported an event in the flattest possible language, and a human looked at the feed and found a cat. Anyone who has ever opened a doorbell notification expecting a delivery and found a squirrel, a leaf, or a neighbor’s cat understands the gap immediately. The shirt does not require you to own a doorbell camera. It requires you to have been on the receiving end of a system that was technically correct and practically useless.

There is a second, quieter joke in the wording. There is motion at your front door is not a warning. It is a status. It does not tell you what to do. It does not say check the feed or someone is approaching. It simply reports that the world has changed in a way the sensor can detect. The cat’s face is the response to that status: a creature that has become an event without intending to, framed by a system that cannot tell the difference between a visitor and a resident.

Why the cat has to be orange

The tabby’s color is not incidental. An orange cat against green-tinted footage produces a complementary contrast that makes the face pop out of the circle. The wide eyes and the close crop do the rest. A black cat in the same frame would read as a shadow; a white cat would blow out under the porch light. The orange tabby is legible at a glance, which matters for a design that has to work as a small printed graphic on a shirt. The green tint and the orange fur are doing the same job as a good error message: making the important thing impossible to miss.

The cat’s expression is also doing specific work. It is not startled. It is not aggressive. It is wide-eyed and forward-facing, which reads as curiosity or mild expectation. That expression is what turns the notification from a security event into a domestic one. A snarling animal would make the banner feel like a warning that failed. A sleeping cat would make it feel like a false positive. A wide-eyed cat looking directly into the lens makes the banner feel like a system that has been outsmarted by something that does not know it is being monitored.

What the shirt is not

It is not a statement about surveillance, privacy, or the attention economy, even though those readings are available. The design does not editorialize about doorbell cameras. It does not show a human deciding whether to answer. It does not show the cat being shooed away or let in. It stops at the moment of detection, which is the moment the system is most confident and least informed. That restraint is what keeps the joke from becoming a lecture.

It is also not a claim that the wearer has a cat, a doorbell camera, or a porch. The shirt prints a generic notification and a generic cat. The specificity is in the format, not in the biography of the person wearing it. That is a useful distinction for anyone who has watched a novelty shirt try to do too much: the ones that work are the ones that commit to a single visual argument and stop.

The There Is Motion At Your Front Door cat doorbell-camera alert meme shirt is available from Quirky Cotton Collective. The design is a notification banner and a camera feed, and the joke is the distance between them. If you have ever opened a motion alert and found something that was not the thing you expected, the shirt is not explaining that experience to you. It is assuming you already have it.

The alert as a design pattern

There is a reason this format keeps working. A notification is a compressed contract. It promises to tell you something you need to know, in as few words as possible, and it accepts that the compression will sometimes produce absurd results. The shirt takes that contract literally. The banner is the promise; the cat is the result. The wearer is not the punchline. The wearer is the person who has learned to read the banner and the feed together, and to find the gap funny rather than infuriating.

That is a small, specific pleasure, and it is the kind of thing a shirt can do well. It does not need a slogan about personality. It needs a sentence that sounds like a system and an image that sounds like a world. The sentence is flat. The image is not. The shirt is the space between them.

Excluding Posts by meta_value on a CPT Archive Without Corrupting Pagination

The symptom: the filter works, the page count does not

You add a pre_get_posts callback to a custom post type archive. The intent is simple: hide posts whose _editorial_status meta equals archived. The first page looks right. Then someone clicks page 3 and gets a page of posts that should have been excluded, or a 404 where a page should exist, or a pagination link that points past the last real page.

This is not a display bug. It is a query-shape bug. The exclusion changed the number of rows the database returns, but the pagination math was computed from a different shape than the one the template renders. The fix is not to hide rows in the loop. The fix is to make the main query itself return the correct set, and then verify that found_posts and max_num_pages agree with what the template shows.

Trace: where the hook sits in the query lifecycle

pre_get_posts fires after the query variable object is created but before the actual query is run. That is the documented contract, and it matters because it means you are modifying query vars, not filtering results. The hook receives the WP_Query instance by reference.

Inside the callback, use the method on the passed object, not the global conditional function. The WordPress reference is explicit: conditional tags should use the method versions within the passed instance, such as $query->is_main_query() instead of is_main_query(), because the global functions test against the global $wp_query instance, not the passed one. The is_main_query() function documentation goes further and calls the global function inside pre_get_posts circular.

That single detail explains a common failure: a callback that calls is_main_query() without the object method can evaluate against the wrong query, or trigger a _doing_it_wrong() notice, and the guard silently fails open. The filter then applies to a secondary loop in a sidebar or a related-posts block, and the archive pagination is computed from a query that never received the exclusion.

The scoping guard, in order

The documented guidance is to use conditionals so you do not unintentionally change queries for custom loops in sidebars, footers, or elsewhere. For a CPT archive, the guard has three parts and the order matters:

  1. ! is_admin() — do not touch admin screens.
  2. $query->is_main_query() — target the primary loop only.
  3. $query->is_post_type_archive( 'book' ) — target the specific archive.

The third check is a method on the query object. The hook reference shows the pattern with is_post_type_archive( 'movie' ) inside a pre_get_posts callback, so the method form is the documented shape. If you are unsure whether a conditional is safe at this point in the lifecycle, inspect $query->query_vars directly rather than guessing.

Root cause: a meta_query is a JOIN, not a display filter

WP_Meta_Query is the helper that generates JOIN and WHERE subclauses attached to the primary SQL query to filter results by metadata. When you add a meta_query clause in pre_get_posts, you are changing the SQL that produces both the rows and the count. That is the correct tool, but it is also why a malformed clause produces pagination drift rather than a visible error.

The compare argument accepts =, !=, >, >=, <, <=, LIKE, NOT LIKE, IN, NOT IN, BETWEEN, NOT BETWEEN, EXISTS, and NOT EXISTS. The choice between != and NOT EXISTS is not cosmetic. It determines whether posts that lack the meta key entirely are included or dropped, and that changes the count.

Why offset is the wrong tool here

The hook reference states plainly that using the offset argument in any WordPress query can break pagination, and that preserving pagination with offset requires handling pagination manually. Offset shifts the result window; it does not remove rows. If you try to exclude archived posts by skipping a fixed number of results, the window drifts as the excluded set changes, and the page count no longer matches the rows. Use a meta_query clause to remove rows from the set, and keep offset out of the main archive query.

The fix: append, do not overwrite, and choose the comparison deliberately

The main query may already carry a meta_query from another plugin or from the theme. Overwriting it with $query->set( 'meta_query', array( ... ) ) discards whatever was there. Read the existing value, normalize it to an array, append your clause, and set it back. The hook reference includes a user-contributed example that does exactly this normalization before appending.

Here is a labeled example. It is illustrative, not a report of a tested deployment.

add_action( 'pre_get_posts', 'jv_exclude_archived_books' );

function jv_exclude_archived_books( $query ) {
    if ( is_admin() ) {
        return;
    }
    if ( ! $query->is_main_query() ) {
        return;
    }
    if ( ! $query->is_post_type_archive( 'book' ) ) {
        return;
    }

    $meta_query = $query->get( 'meta_query' );
    if ( ! is_array( $meta_query ) ) {
        $meta_query = array();
    }

    $meta_query[] = array(
        'key'     => '_editorial_status',
        'value'   => 'archived',
        'compare' => '!=',
    );

    $query->set( 'meta_query', $meta_query );
}

Two decisions in that snippet deserve scrutiny.

Comparison choice. With compare => '!=', the query returns posts whose meta value is not archived. Posts that have no _editorial_status row at all are a separate case. If your editorial workflow guarantees every book has the key, != is sufficient. If some books predate the workflow, decide explicitly whether they belong on the archive. NOT EXISTS is the operator for the missing-key case, and it is a different query shape. Do not assume the two are interchangeable; verify the count for each.

Append, do not replace. The normalization block is not defensive decoration. If another callback already set a meta_query and you overwrite it, you have silently removed a filter that may have been controlling which posts appear. The archive then shows more posts than the other filter intended, and the pagination count reflects the larger set.

What the count actually means

WP_Query exposes found_posts as the total number of posts found matching the current query parameters, and max_num_pages as the result of found_posts / posts_per_page. Those are the numbers the pagination links are built from. If your meta_query clause is applied to the main query, both numbers reflect the filtered set. If it is applied to a secondary query, or if the guard fails and the clause lands on the wrong query, the template may render filtered rows while the pagination links were computed from the unfiltered main query. That is the drift.

Verification: prove the count before you trust the page

Do not verify by clicking through pages and eyeballing. Verify the numbers.

1. Confirm the hook fires once, on the main query

On a staging copy, dump the query vars inside the callback and log the call count. A callback that fires more than once for the archive request, or fires for a query whose post_type is not your CPT, is mis-scoped. The hook reference warns that without conditionals you can change custom loops in sidebars, footers, or elsewhere; the log is how you catch that.

function jv_debug_archive_query( $query ) {
    if ( ! $query->is_main_query() ) {
        return;
    }
    if ( ! $query->is_post_type_archive( 'book' ) ) {
        return;
    }
    error_log( 'main archive query_vars: ' . wp_json_encode( $query->query_vars ) );
}
add_action( 'pre_get_posts', 'jv_debug_archive_query', 999 );

Run it with WP_DEBUG_LOG enabled and read the log. You are looking for one entry per archive request, with the expected post_type and paged values.

2. Compare found_posts before and after

In the template, or in a temporary mu-plugin, print global $wp_query; echo $wp_query->found_posts; and echo $wp_query->max_num_pages; on the archive. Record both with the filter disabled and enabled. The difference should equal the number of posts you intended to exclude. If it does not, the clause is not matching what you think it is matching.

3. Check the SQL shape

Query Monitor exposes the main query SQL. Look at the JOIN and WHERE clauses generated for your meta clause. A != comparison and a NOT EXISTS comparison produce different JOIN shapes, and the row count difference between them is the set of posts with no meta row. If the JOIN is not what you expected, the comparison is not doing what you intended.

4. Confirm the pagination boundary

Request the last page directly by URL, for example /book/page/4/, and confirm it returns rows rather than a 404. Then request one page past the last, and confirm it 404s. If the last real page 404s, max_num_pages is too low. If a page past the end returns rows, it is too high. Both point back to the count, not the template.

What this pattern does not cover

This article is scoped to the main query on a CPT archive. It does not claim anything about how the REST API or the block editor’s query loop handles a pre_get_posts modification, because the retrieved documentation does not establish that behavior. If your archive is also consumed by a REST endpoint or a query loop block, verify those contexts separately rather than assuming the hook applies uniformly.

It also does not claim a performance outcome. Adding a meta clause changes the SQL; whether that is faster or slower depends on indexes and data volume, and the documentation does not provide a number to cite.

FAQ

Why does is_main_query() behave differently inside pre_get_posts?

The global function compares against the global $wp_query, not the query object passed to your callback. The hook reference says to use the method on the passed instance, and the is_main_query() documentation calls the global function circular in this context. Use $query->is_main_query().

Should I use != or NOT EXISTS to exclude a meta value?

They answer different questions. != excludes posts whose meta value equals the target; NOT EXISTS targets posts that lack the key. If some posts have no meta row, the two produce different result sets and different pagination counts. Decide which set you mean and verify found_posts for that choice.

Can I just use offset to skip the excluded posts?

No. The hook reference states that using offset in any WordPress query can break pagination and that preserving pagination with offset requires manual handling. Offset shifts the window; it does not remove rows from the set.

Why did my exclusion stop working after another plugin update?

A likely cause is that another callback set a meta_query and your callback overwrote it, or the reverse. Read the existing meta_query, normalize it to an array, append your clause, and set it back. Then re-check found_posts.

Where should I look first when the page count is wrong?

At found_posts and max_num_pages on the main query, not at the template. max_num_pages is found_posts / posts_per_page. If the count is wrong, the pagination links are wrong, and the template is doing what it was told.

If the archive is returning nothing at all rather than the wrong count, the failure is earlier in the request and a different diagnostic sequence applies; see what to fix first when a new WordPress site says nothing found.

Sources

The ‘I Had Potential’ Shirt Is a Physics Diagram, Not a Slogan

Editorial note: This publication and Quirky Cotton Collective share an owner.

The shirt prints four words in white capitals — I HAD POTENTIAL — above a white curved ramp. A dashed empty circle sits at the upper left ledge. A solid white ball rests on the lower right flat ledge. The ramp descends between them. That is the whole design, and it is enough to make an argument.

Most slogan shirts ask you to agree with a sentence. This one asks you to read a diagram. The sentence is the caption; the drawing is the claim. If you only read the words, you get a familiar joke about wasted promise. If you read the drawing, you get a specific physical situation: a ball that has already left the high ledge, rolled down a curved track, and come to rest at the bottom. The dashed circle marks where it started. The solid ball marks where it is. The gap between them is the joke, and it is also a small lesson in how potential works.

In physics, potential energy is not a promise. It is a function of position in a field. A ball at the top of a ramp has gravitational potential energy relative to the bottom. That energy is real, but it is only available if the ball moves. The dashed circle is the ball’s initial state: a position with potential. The solid ball is the final state: a position with less potential and more entropy. Nothing in the drawing says the ball was pushed, dropped, or failed. It simply shows the two states and the path between them. The humor comes from treating a human word — potential — as if it were a coordinate.

That is why the curved ramp matters. A straight diagonal would read as a graph of decline, the kind of line you see in a bad slide about performance. A curve reads as a track, a chute, a physical object with a shape. The dashed circle at the upper left is not a ghost or a memory; it is a technical convention for a starting position. The solid ball at the lower right is not a punchline; it is the current state. The design is closer to a free-body diagram than to a motivational poster, and that is the source of its dry appeal.

There is a second reading, and it is the one that makes the shirt useful rather than merely funny. In engineering, potential is a capacity, not an outcome. A system has potential throughput, potential load, potential failure modes. Potential is what the system could do under certain conditions, not what it will do. The shirt’s diagram shows a ball that had potential and now has position. The joke is that the two are not the same thing. Anyone who has maintained a production system knows this gap intimately: the capacity was there, the conditions changed, and the outcome is now a fact on the ground. The shirt does not explain that. It just draws it.

The wording is terse, and the terseness is doing work. “I Had Potential” is past tense. It does not say “I Have Potential” or “I Am Potential.” The past tense closes the account. The ball is at rest. The dashed circle is empty. The sentence and the drawing agree: the moment of possibility is over, and what remains is a position. That is a more disciplined joke than a sarcastic slogan, because it withholds the complaint. It does not blame a boss, a system, or a lack of effort. It presents a state diagram and lets you supply the history.

That restraint is also a limitation. A diagram joke depends on the viewer recognizing the convention. If you read the dashed circle as a hole and the solid ball as a different object, the design becomes a picture of a ball that fell into a gap. If you read the ramp as a slide, it becomes a playground joke. The shirt does not enforce its own reading. It relies on the visual grammar of physics diagrams — dashed for initial, solid for final, curve for path — and that grammar is not universal. The design is legible to people who have seen a potential-energy sketch, and merely odd to people who have not. That is not a flaw, but it is a boundary. The shirt is not a universal slogan; it is a diagram with a caption, and it works best when both are read together.

The object itself is a standard t-shirt from Quirky Cotton Collective, listed as I Had Potential Funny Physics Pun Sarcastic Minimalist Quote. The retail title calls it a physics pun, and the drawing supports that description: the ramp, the dashed start, and the solid end are the visual vocabulary of a potential-energy diagram, simplified to two states and a path. The title also calls it sarcastic and minimalist, which are fair descriptions of the tone and the layout. The design is white on the garment, with no additional illustration or texture described in the observed image. The shirt is the object; the diagram is the argument.

What makes the shirt worth writing about is not the joke but the distinction it draws between a slogan and a usable promise. A slogan asserts a value: “I am potential.” A promise specifies conditions: “If you do X, Y will follow.” The shirt’s diagram specifies a condition — a ball at the top of a ramp — and a result — a ball at the bottom. It does not promise that the result was deserved or avoidable. It simply shows the transition. That is a more honest form of humor than most sarcastic shirts manage, because it does not pretend that potential is a moral quality. Potential is a state. States change. The shirt is a picture of that change, with a caption that admits it.

For technical readers, the appeal is probably not the physics. It is the format. The shirt treats a human complaint as a system diagram, and that is a familiar move. Debugging is the practice of turning a vague symptom into a specific state: what was the input, what was the expected output, what is the actual output, and where did the path diverge? The shirt does the same thing in miniature. It shows an initial state, a final state, and a path, and it lets the viewer infer the missing variable. The missing variable is whatever caused the ball to move. The shirt does not name it. That omission is the joke, and it is also the discipline. A good diagram does not explain everything; it shows enough to make the question precise.

The shirt is not a tutorial, and it would be a mistake to read it as one. It does not demonstrate a software failure, a security boundary, or an engineering result. It is a piece of clothing with a drawing on it, and its argument is visual and verbal at once. The useful observation is that the drawing and the sentence are not redundant. The sentence supplies the human word — potential — and the drawing supplies the physical situation. Neither alone would be as effective. Together they produce a small, dry joke about the difference between capacity and outcome, and they do it without raising their voice.

That is the test I would apply to any slogan shirt: does the image add something the words cannot? Here it does. The words say what was lost. The drawing shows where it went. The dashed circle is empty, the ball is at rest, and the ramp is curved, not straight. The design is not a generic gift; it is a specific diagram with a specific caption, and it earns its place by making the viewer read both.

The ‘Let’s Root For Each Other’ Shirt Is a Diagram of Mutual Support, Not a Growth Mandate

The artwork reads LET’S ROOT FOR EACH OTHER AND WATCH EACH OTHER GROW, set in a ring around four smiling root vegetables: a carrot, a radish, a beet, and an onion. Their roots are visible in soil. The lettering is the frame; the vegetables are the content. That arrangement is the first thing worth noticing, because it makes the shirt a small diagram rather than a slogan with decoration attached.

I want to argue something specific about this design: it separates two ideas that work culture usually collapses into one. The first idea is rooting for someone — an act of support that costs the supporter nothing except attention and goodwill. The second is watching someone grow — an outcome that belongs to the person growing, not to the person watching. The shirt puts both in the same sentence, but the grammar keeps them apart. Let’s root for each other is a mutual commitment. And watch each other grow is a mutual permission. The first is a promise you make; the second is a promise you keep by not interfering.

That distinction matters because the common workplace version of this sentiment is worse. We grow together sounds collaborative but often means your growth is now a shared performance metric. Let’s root for each other is lighter. It asks for encouragement, not surveillance. The shirt’s wording is doing real work here, and it is worth reading closely before deciding whether it belongs in a team channel or a weekend wardrobe.

The vegetables are the argument

Four root vegetables, each with its edible part below the soil line. The design shows the roots. That is not incidental. A carrot’s orange shoulder is the part everyone sees; the part that actually feeds you is underground. The same is true of the radish, the beet, and the onion. The artwork makes the hidden structure visible, which is a quiet claim about growth: the visible part is not the whole story, and the part doing the work is often the part nobody photographs.

I read the smiling faces as a deliberate softening. Root vegetables pulled from the ground are, visually, a little absurd — lumpy, dirt-adjacent, irregular. Giving them smiles turns them into characters rather than produce. That matters for the pun: root works as both a noun (the vegetable’s root) and a verb (to cheer for). The design needs the vegetables to be legible as roots for the verbal joke to land, and it needs them to be friendly for the emotional register to land. Both are present.

What the artwork does not show is a garden bed, a trellis, a harvest basket, or a growth chart. There is no upward arrow, no sunburst, no level up iconography. The vegetables are in soil, roots visible, smiling. That is the entire scene. The restraint is the point. A design that wanted to sell you relentless improvement would have drawn a ladder. This one drew a cross-section.

Two meanings of rooting, and why the shirt needs both

The pun only works because English lets root mean both the underground part of a plant and to support enthusiastically. The shirt uses both senses at once, and the visual observation confirms it: the roots are drawn, and the word root is printed. Neither sense is subordinate. If you read the shirt as a gardening joke, the vegetables carry it. If you read it as a team-culture message, the verb carries it. The design is built to survive both readings, which is why it works as an off-duty object for people who spend their working hours reading diagrams.

There is a third sense lurking, which the shirt does not use: root as in root cause, the thing you trace a symptom back to. I mention it only to set it aside. The shirt is not making a debugging joke, and it would be a mistake to force one onto it. The vegetables are not a stack trace. The smiles are not a status code. The design is about mutual encouragement and visible hidden structure, and that is enough.

Where the shirt becomes genuinely useful as an object — rather than just a pleasant pun — is in the gap between the two clauses. Root for each other is easy to say and easy to fake. Watch each other grow is harder, because watching requires you to accept that the growth is not yours to direct. A team that roots for each other but cannot watch each other grow is a team that applauds in public and micromanages in private. The shirt’s sentence structure quietly rules that out. The two clauses are joined by and, not so that. The growth is not the reward for the rooting. They are parallel commitments.

What the shirt is not

It is not a productivity tool. It does not claim that wearing it improves team dynamics, accelerates career development, or increases vegetable intake. It is a printed design on a garment, and the design makes an argument about how support and growth relate. That argument is worth taking seriously as a piece of visual rhetoric. It is not worth mistaking for evidence.

It is also not a generic gift-roundup item. The reason to single it out is the specific wording and the specific drawing: four named root vegetables, roots visible, smiles present, two clauses held in parallel. A shirt that said Grow Together over a stock photo of a seedling would be a different object with a different argument. This one is more careful than that, and the care is visible in what it leaves out.

Who this reading is for

If you spend your days tracing symptoms to root causes, you already have a trained eye for hidden structure. The shirt rewards that eye. The roots below the soil line are the part of the drawing that carries the most meaning, and they are the part a casual glance would miss. That is a small pleasure, but it is a real one, and it is the kind of pleasure that makes an object worth keeping rather than merely wearing.

The shirt is available as a standard t-shirt from Quirky Cotton Collective. If you are considering it for a team or a gardening friend, the question to ask is not whether the pun is clever — it is — but whether the two clauses describe the culture you actually want. Root for each other is the easy half. Watch each other grow is the half that requires you to stay out of the way.

Editorial note: This publication and Quirky Cotton Collective share an owner.

The Large Lowercase ‘meh.’ Shirt Is a Typographic Shrug, Not a Personality

The native garment preview shows four lowercase letters and a period, set in a heavy white sans-serif on a dark shirt: meh. No illustration. No secondary line of copy. No animal, no mascot, no pride variant. The word is large enough to read across a room, and the period is doing more work than the letters are.

That period is the whole argument. A shirt that said meh without punctuation would be a noise — a vowel sound, a shrug rendered as text. The period converts it into a sentence. It is a complete, closed, grammatically finished statement of disinterest. The typography is loud and the message is quiet, and the gap between those two things is the design.

I want to read this object the way I read a log line: what is it asserting, what is it withholding, and what would break if you changed one character.

The period is a terminator, not decoration

In prose, a period ends a declarative sentence. It signals that the speaker has finished and does not intend to elaborate. On a shirt, that signal is unusually strong because the garment has no room for a follow-up clause. You cannot append a caveat to a chest. So the period is not stylistic; it is structural. It tells the reader that the four letters are the entire payload.

This is why the design reads as dry rather than sad. A bare meh invites interpretation — is the wearer tired? bored? depressed? The period forecloses the question. It is not an emotion seeking sympathy; it is a verdict, delivered and closed. The visual loudness — large bold lowercase, high contrast — is the only thing that keeps the verdict from being invisible. The shirt has to shout the shrug, or the shrug would not register at all.

Lowercase matters here too. Capital MEH. would read as a command or a brand. Lowercase meh. reads as a muttered aside that has been typeset at billboard scale. The mismatch between the volume of the type and the flatness of the word is the joke, and it is a typographic joke rather than a verbal one.

What the shirt is not

It is not an introvert identity badge, despite the retail title’s adjacency to that vocabulary. The artwork contains no claim about social battery, no reference to crowds, no diagram of a person leaving a party. It is one word and a full stop. Reading introversion into it is a plausible inference about why someone might buy it, but it is not something the design states. I am distinguishing my interpretation from a claim about the wearer.

It is also not a slogan in the operational sense. A slogan makes a promise — just do it, think different — and a promise can be kept or broken. meh. promises nothing. That is its integrity. It is a refusal dressed as a statement, and the period is the refusal.

If you want the object itself, the listing is here: Meh Funny Sarcastic Saying Introvert Dry Humor. The design is the large bold white lowercase meh. on a dark shirt, and that is the entire visual program.

The useful question for a team that ships words

I keep a small test for any message that goes out under a team’s name: strip the punctuation and see whether the meaning survives. meh survives as a sound. meh. survives as a position. The period is load-bearing. Most production copy has the opposite problem — it is over-punctuated and under-said, with exclamation marks doing the work that a clear declarative should do. This shirt is a reminder that a single terminal mark can carry the entire tone of a message, and that volume in the typeface is not the same as volume in the claim.

The design is loud and the sentence is finished. That combination is harder to pull off than it looks, and it is the reason the shirt works as an object rather than as a novelty. The word is dismissive; the typesetting is not. The period is what makes the dismissal legible.

Editorial note: This publication and Quirky Cotton Collective share an owner.