<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lucas Fenwick</title>
    <description>The latest articles on DEV Community by Lucas Fenwick (@lucasfenw).</description>
    <link>https://dev.to/lucasfenw</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4005256%2F109220db-d8bb-46c6-b61d-569016e4a9f7.png</url>
      <title>DEV Community: Lucas Fenwick</title>
      <link>https://dev.to/lucasfenw</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lucasfenw"/>
    <language>en</language>
    <item>
      <title>Your WordPress "URL Path" column is not reading a slug, it is computing one</title>
      <dc:creator>Lucas Fenwick</dc:creator>
      <pubDate>Mon, 27 Jul 2026 08:02:34 +0000</pubDate>
      <link>https://dev.to/lucasfenw/your-wordpress-url-path-column-is-not-reading-a-slug-it-is-computing-one-563m</link>
      <guid>https://dev.to/lucasfenw/your-wordpress-url-path-column-is-not-reading-a-slug-it-is-computing-one-563m</guid>
      <description>&lt;p&gt;Add a URL Path column to the WordPress posts list by enabling Custom Admin Columns in WP Adminify: go to WP Adminify &amp;gt; Productivity, toggle on Custom Admin Columns, tick &lt;code&gt;Show "URL Path" Column for Post Types&lt;/code&gt;, pick which post types get it from the Posts, Pages and custom post type options that appear, and save, and a URL Path column shows each row a path like &lt;code&gt;/hello-world/&lt;/code&gt;, which WordPress computes at render time from &lt;code&gt;post_name&lt;/code&gt; plus your permalink structure plus, on hierarchical types, the whole ancestor chain, and &lt;strong&gt;not&lt;/strong&gt; from a stored URL field, which is why the column is blank or a &lt;code&gt;?p=123&lt;/code&gt; on drafts, because core saves an empty &lt;code&gt;post_name&lt;/code&gt; for draft, pending and auto-draft posts.&lt;/p&gt;

&lt;p&gt;That is the whole answer. The rest of this post is why the distinction between a stored slug and a computed path produces three separate surprises, one of which is a genuine bug that will hand you a wrong URL with a straight face.&lt;/p&gt;

&lt;h3&gt;
  
  
  The tooling is not the interesting part
&lt;/h3&gt;

&lt;p&gt;Let me concede this up front, because the SERP is full of it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://wordpress.org/plugins/admin-slug-column/" rel="noopener noreferrer"&gt;Admin Slug Column&lt;/a&gt; has 5,000+ active installs, is tested to WordPress 6.9.5, and adds a column it literally calls &lt;strong&gt;URL Path&lt;/strong&gt; across every post type including WooCommerce products. &lt;a href="https://wordpress.org/plugins/slug-search-and-admin-columns/" rel="noopener noreferrer"&gt;Slug Search and Admin Columns&lt;/a&gt; adds ID and Slug columns and wires them into the default search box. &lt;a href="https://wordpress.org/plugins/show-page-slug-in-admin/" rel="noopener noreferrer"&gt;Show Page Slug in Admin&lt;/a&gt; does the same for Pages. There is an &lt;a href="https://github.com/chuckreynolds/Admin-Slug-Column" rel="noopener noreferrer"&gt;open source version on GitHub&lt;/a&gt;. All correct, all fine.&lt;/p&gt;

&lt;p&gt;Every one of them describes the column as showing the slug.&lt;/p&gt;

&lt;p&gt;It is not showing the slug.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stored versus computed
&lt;/h3&gt;

&lt;p&gt;WordPress stores exactly one slug per post: &lt;code&gt;post_name&lt;/code&gt;, in &lt;code&gt;wp_posts&lt;/code&gt;. One segment. &lt;code&gt;hello-world&lt;/code&gt;. There is no stored URL column anywhere in the schema.&lt;/p&gt;

&lt;p&gt;What a URL Path column renders is assembled at request time by &lt;a href="https://developer.wordpress.org/reference/functions/get_permalink/" rel="noopener noreferrer"&gt;&lt;code&gt;get_permalink()&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;post_name
+ permalink structure          (Settings &amp;gt; Permalinks)
+ post type rewrite slug       (register_post_type's rewrite arg)
+ ancestor chain               (hierarchical types, via get_page_uri)
+ %category% and friends       (if your structure uses rewrite tags)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five inputs, one of which is stored on the row. Every surprise below is a consequence of the other four.&lt;/p&gt;

&lt;h3&gt;
  
  
  Surprise 1: your drafts come back blank
&lt;/h3&gt;

&lt;p&gt;This is the one you will hit within a day of switching the column on, and it is core doing it on purpose. From &lt;a href="https://developer.wordpress.org/reference/functions/wp_insert_post/" rel="noopener noreferrer"&gt;&lt;code&gt;wp_insert_post()&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$post_name&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$post_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'draft'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'auto-draft'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$post_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sanitize_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$post_title&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$post_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An unpublished post has an &lt;strong&gt;empty string&lt;/strong&gt; for a slug. Not a placeholder, not a guess from the title. Empty.&lt;/p&gt;

&lt;p&gt;And that is only half of it. &lt;a href="https://developer.wordpress.org/reference/functions/wp_force_plain_post_permalink/" rel="noopener noreferrer"&gt;&lt;code&gt;wp_force_plain_post_permalink()&lt;/code&gt;&lt;/a&gt;, added in &lt;strong&gt;WordPress 5.7&lt;/strong&gt;, decides whether &lt;code&gt;get_permalink()&lt;/code&gt; takes the pretty branch or the plain one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;is_post_status_viewable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$post_status_obj&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$post_status_obj&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;current_user_can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'read_post'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;ID&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$post_status_obj&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$sample&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Draft is not &lt;a href="https://developer.wordpress.org/reference/functions/is_post_status_viewable/" rel="noopener noreferrer"&gt;viewable&lt;/a&gt;. Pending is not viewable. Scheduled (&lt;code&gt;future&lt;/code&gt;) is a protected status, and without &lt;code&gt;$sample&lt;/code&gt; it does not get the exemption either. So all three return &lt;code&gt;true&lt;/code&gt;, and &lt;code&gt;get_permalink()&lt;/code&gt; hands back &lt;code&gt;?p=123&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Admin Slug Column greys those rows out and says the path "isn't an official URL yet", which is the closest anyone in this SERP gets to explaining it. Now you know the two functions responsible.&lt;/p&gt;

&lt;p&gt;If you want the path a draft &lt;em&gt;would&lt;/em&gt; have, that is a different function: &lt;a href="https://developer.wordpress.org/reference/functions/get_sample_permalink/" rel="noopener noreferrer"&gt;&lt;code&gt;get_sample_permalink()&lt;/code&gt;&lt;/a&gt;, which is what the Gutenberg permalink panel uses, and it returns the structure with a &lt;code&gt;%pagename%&lt;/code&gt; placeholder rather than a finished path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Surprise 2: a draft parent silently corrupts its children's paths
&lt;/h3&gt;

&lt;p&gt;This one is not a display quirk. This is a wrong answer.&lt;/p&gt;

&lt;p&gt;Here is how &lt;a href="https://developer.wordpress.org/reference/functions/get_page_uri/" rel="noopener noreferrer"&gt;&lt;code&gt;get_page_uri()&lt;/code&gt;&lt;/a&gt; assembles a hierarchical path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$page&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ancestors&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$parent&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$parent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$parent&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$parent&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$parent&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;post_name&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$parent&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;post_name&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the &lt;code&gt;if&lt;/code&gt; twice. An ancestor whose &lt;code&gt;post_name&lt;/code&gt; is empty is &lt;strong&gt;skipped&lt;/strong&gt;, silently, with no notice and no fallback.&lt;/p&gt;

&lt;p&gt;From surprise 1, we know exactly which posts have an empty &lt;code&gt;post_name&lt;/code&gt;: drafts, pending posts and auto-drafts.&lt;/p&gt;

&lt;p&gt;So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have a page &lt;strong&gt;Services&lt;/strong&gt;, still a draft, being written this week.&lt;/li&gt;
&lt;li&gt;You publish &lt;strong&gt;Services &amp;gt; Pricing&lt;/strong&gt; under it, because publishing children first is a normal staging workflow.&lt;/li&gt;
&lt;li&gt;Your URL Path column shows &lt;code&gt;/pricing/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The URL that page will actually have, the moment Services goes live, is &lt;code&gt;/services/pricing/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The column is not lying about &lt;code&gt;post_name&lt;/code&gt;. It is faithfully reporting a computation that dropped a segment. If somebody is building a redirect map, a sitemap submission or an SEO audit off that column, they are building it on a path that does not exist and will never exist.&lt;/p&gt;

&lt;p&gt;Go check this on your own site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post_name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;child_slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post_status&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;parent_status&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_posts&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;wp_posts&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post_parent&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'publish'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post_status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'draft'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'auto-draft'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every row that comes back is a page whose displayed path is missing a segment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Surprise 3: you can see the slug and you still cannot search it
&lt;/h3&gt;

&lt;p&gt;This is the one that gets the "wait, really?" reaction, because it is the actual reason most people wanted the column.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.wordpress.org/reference/classes/wp_query/parse_search/" rel="noopener noreferrer"&gt;&lt;code&gt;WP_Query::parse_search()&lt;/code&gt;&lt;/a&gt; opens with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$default_search_columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'post_title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'post_excerpt'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'post_content'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;post_name&lt;/code&gt; is not on that list. It has never been on that list.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://developer.wordpress.org/reference/hooks/post_search_columns/" rel="noopener noreferrer"&gt;&lt;code&gt;post_search_columns&lt;/code&gt;&lt;/a&gt; filter arrived in &lt;strong&gt;WordPress 6.2&lt;/strong&gt; and looked like the fix, except it validates your return value against those same three columns. There is no filter, no argument and no setting that makes the admin Search Posts box find a post by its slug.&lt;/p&gt;

&lt;p&gt;So you can add a column showing &lt;code&gt;/summer-sale-2025/&lt;/code&gt;, look straight at it, type &lt;code&gt;summer-sale-2025&lt;/code&gt; into the search box above it, and get nothing.&lt;/p&gt;

&lt;p&gt;The proof that this is a real gap and not me splitting hairs: somebody shipped an entire separate plugin called &lt;a href="https://wordpress.org/plugins/slug-search-and-admin-columns/" rel="noopener noreferrer"&gt;Slug Search and Admin Columns&lt;/a&gt;. The column half and the search half are two products because core makes them two problems.&lt;/p&gt;

&lt;p&gt;If you need it, the query-level escape hatches are &lt;code&gt;WP_Query&lt;/code&gt;'s &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;post_name__in&lt;/code&gt; arguments, or &lt;a href="https://developer.wordpress.org/reference/functions/get_page_uri/" rel="noopener noreferrer"&gt;&lt;code&gt;get_page_by_path()&lt;/code&gt;&lt;/a&gt;. None of them are wired to the admin search box.&lt;/p&gt;

&lt;h3&gt;
  
  
  Surprise 4: non-Latin slugs render percent-encoded
&lt;/h3&gt;

&lt;p&gt;Smaller, but it will look like a bug the first time you see it. &lt;a href="https://developer.wordpress.org/reference/functions/sanitize_title_with_dashes/" rel="noopener noreferrer"&gt;&lt;code&gt;sanitize_title_with_dashes()&lt;/code&gt;&lt;/a&gt; does this to UTF-8 titles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;wp_is_valid_utf8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;function_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'mb_strtolower'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;mb_strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'UTF-8'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;utf8_uri_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;post_name&lt;/code&gt; is stored already percent-encoded. A column that prints &lt;code&gt;post_name&lt;/code&gt; raw shows &lt;code&gt;%e3%81%93%e3%82%93&lt;/code&gt; where your browser's address bar helpfully renders the actual characters. Admin Slug Column specifically advertises multibyte support, which is a good hint about what the naive implementation does.&lt;/p&gt;

&lt;p&gt;If you write your own column, &lt;code&gt;rawurldecode()&lt;/code&gt; before display, and be aware you are then showing something that is not byte-identical to the stored value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Also: &lt;code&gt;wp_unique_post_slug()&lt;/code&gt; is why you have a &lt;code&gt;-2&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;While we are in here. &lt;a href="https://developer.wordpress.org/reference/functions/wp_unique_post_slug/" rel="noopener noreferrer"&gt;&lt;code&gt;wp_unique_post_slug()&lt;/code&gt;&lt;/a&gt; appends &lt;code&gt;-2&lt;/code&gt;, &lt;code&gt;-3&lt;/code&gt; and so on when a slug collides, and it scopes that check by post type, parent and status. Two pages with the same title under different parents can both be &lt;code&gt;about&lt;/code&gt;. Two posts cannot. A URL Path column is the fastest way to spot the accumulated &lt;code&gt;-2&lt;/code&gt;s from duplicated posts, which is honestly one of the better arguments for switching it on.&lt;/p&gt;

&lt;h3&gt;
  
  
  The cost ladder in one settings panel
&lt;/h3&gt;

&lt;p&gt;Adminify's Custom Admin Columns panel has four checkboxes and they cost wildly different amounts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;What it costs&lt;/th&gt;
&lt;th&gt;Sortable?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Post/Page ID&lt;/td&gt;
&lt;td&gt;Nothing, the ID is already on the row object&lt;/td&gt;
&lt;td&gt;Yes, it is the primary key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Taxonomy ID&lt;/td&gt;
&lt;td&gt;Nothing, &lt;code&gt;term_id&lt;/code&gt; is a real column on &lt;code&gt;wp_terms&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Yes, no join needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;URL Path&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A computed permalink per row, plus an ancestor walk per row on hierarchical types&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;No.&lt;/strong&gt; There is nothing to &lt;code&gt;ORDER BY&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Last Login&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;wp_usermeta&lt;/code&gt; join&lt;/td&gt;
&lt;td&gt;Yes, but it &lt;strong&gt;drops rows&lt;/strong&gt; for users missing the meta key&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That "No" in the URL Path row is worth sitting with. &lt;code&gt;post_name&lt;/code&gt; is a real column, so &lt;code&gt;orderby=name&lt;/code&gt; works, but the &lt;em&gt;displayed path&lt;/em&gt; is a computed string that exists nowhere in the database. You cannot sort a list by a value the database does not have without computing it for every row first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Changing a path is where the actual risk lives
&lt;/h3&gt;

&lt;p&gt;Editing the slug of an already published post is covered. &lt;a href="https://developer.wordpress.org/reference/functions/wp_check_for_changed_slugs/" rel="noopener noreferrer"&gt;&lt;code&gt;wp_check_for_changed_slugs()&lt;/code&gt;&lt;/a&gt; stores the old one in &lt;code&gt;_wp_old_slug&lt;/code&gt;, and &lt;a href="https://developer.wordpress.org/reference/functions/wp_old_slug_redirect/" rel="noopener noreferrer"&gt;&lt;code&gt;wp_old_slug_redirect()&lt;/code&gt;&lt;/a&gt; 301s the old URL when it 404s.&lt;/p&gt;

&lt;p&gt;Not covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;changing your permalink structure in Settings &amp;gt; Permalinks&lt;/li&gt;
&lt;li&gt;changing a post type's &lt;code&gt;rewrite&lt;/code&gt; slug in &lt;code&gt;register_post_type()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;reparenting a page&lt;/strong&gt;, which changes the path without touching &lt;code&gt;post_name&lt;/code&gt; at all&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That third one is the trap, and it is the same mechanism as surprise 2. The slug never changed, so no &lt;code&gt;_wp_old_slug&lt;/code&gt; was recorded, so there is no redirect. &lt;a href="https://wpadminify.com/features/url-redirection" rel="noopener noreferrer"&gt;URL redirection&lt;/a&gt; is where you clean that up manually, and &lt;a href="https://wpadminify.com/how-to-update-replace-image-in-wordpress-without-changing-url" rel="noopener noreferrer"&gt;replacing an image without changing its URL&lt;/a&gt; is the same class of problem on the media side.&lt;/p&gt;

&lt;h3&gt;
  
  
  The code route
&lt;/h3&gt;

&lt;p&gt;Two hooks, and note the contract, because it is the opposite of the taxonomy one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. Register the column.&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'manage_posts_columns'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$columns&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$columns&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'url_path'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'URL Path'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$columns&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Print the value. This is an ACTION. Echo, do not return.&lt;/span&gt;
&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'manage_posts_custom_column'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$post_id&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'url_path'&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nv"&gt;$column&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nv"&gt;$post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$post_id&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;post_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'draft'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'auto-draft'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;span style="opacity:.5"&amp;gt;not published&amp;lt;/span&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nf"&gt;esc_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;rawurldecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;wp_make_link_relative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;get_permalink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$post_id&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://developer.wordpress.org/reference/hooks/manage_posts_columns/" rel="noopener noreferrer"&gt;&lt;code&gt;manage_posts_columns&lt;/code&gt;&lt;/a&gt; registers, &lt;a href="https://developer.wordpress.org/reference/hooks/manage_posts_custom_column/" rel="noopener noreferrer"&gt;&lt;code&gt;manage_posts_custom_column&lt;/code&gt;&lt;/a&gt; prints, and it is an &lt;strong&gt;action&lt;/strong&gt; with the signature &lt;code&gt;( string $column_name, int $post_id )&lt;/code&gt; where you echo. The taxonomy equivalent, &lt;code&gt;manage_{$taxonomy}_custom_column&lt;/code&gt;, is a &lt;strong&gt;filter&lt;/strong&gt; where you return. Same feature, opposite contracts, and mixing them up is an hour of your life.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.wordpress.org/reference/functions/wp_make_link_relative/" rel="noopener noreferrer"&gt;&lt;code&gt;wp_make_link_relative()&lt;/code&gt;&lt;/a&gt; is what turns the absolute permalink into the &lt;code&gt;/hello-world/&lt;/code&gt; form the screenshot shows. Use &lt;code&gt;manage_pages_columns&lt;/code&gt; for Pages, &lt;code&gt;manage_{$post_type}_posts_columns&lt;/code&gt; for a specific custom post type, and put the whole thing in a must-use plugin because in &lt;code&gt;functions.php&lt;/code&gt; it dies on a theme switch.&lt;/p&gt;

&lt;h3&gt;
  
  
  The checkbox route
&lt;/h3&gt;

&lt;p&gt;I ran this pass with WP Adminify's Productivity module. Productivity tab, toggle on Custom Admin Columns, tick &lt;code&gt;Show "URL Path" Column for Post Types&lt;/code&gt;, then a second option appears, &lt;code&gt;"URL Path" Column for Post Types&lt;/code&gt;, with checkboxes for Posts, Pages and, per the docs, every registered custom post type added dynamically. Save.&lt;/p&gt;

&lt;p&gt;That per-post-type selector is the one genuine advantage over the free plugins, and it is worth naming precisely because everything else in this post has been a caveat: every other checkbox in that panel is all-or-nothing, and every wordpress.org equivalent applies to all post types. If you run eight and want the path on two, this is the only one that asks.&lt;/p&gt;

&lt;p&gt;The same panel carries the other list screen columns people otherwise snippet in one at a time, a Post/Page ID column, a Comment ID column with Parent ID, a Taxonomy ID column, a post thumbnail column and the &lt;a href="https://wpadminify.com/how-to-show-last-login-column-in-wordpress-users-list" rel="noopener noreferrer"&gt;Last Login column for Users&lt;/a&gt;. If you want arbitrary columns rather than the fixed set, the standalone &lt;a href="https://wpadminify.com/admin-columns-editor" rel="noopener noreferrer"&gt;Admin Columns Editor&lt;/a&gt; is the bigger hammer, &lt;a href="https://wpadminify.com/how-to-add-acf-field-to-admin-column" rel="noopener noreferrer"&gt;adding an ACF field as an admin column&lt;/a&gt; is the next step for custom fields, the rundown of &lt;a href="https://wpadminify.com/necessary-admin-columns-for-wordpress" rel="noopener noreferrer"&gt;which admin columns actually earn their width&lt;/a&gt; is a sane map before you switch five of them on, and the wider &lt;a href="https://wpadminify.com/features/productivity" rel="noopener noreferrer"&gt;Productivity toolset&lt;/a&gt; sits in the same place.&lt;/p&gt;

&lt;p&gt;Two related things from the same list screen that pair with this column: &lt;a href="https://wpadminify.com/wordpress-duplicate-page" rel="noopener noreferrer"&gt;duplicating a page&lt;/a&gt; is the single fastest way to accumulate &lt;code&gt;-2&lt;/code&gt; slugs, and &lt;a href="https://wpadminify.com/how-to-use-custom-post-type-order" rel="noopener noreferrer"&gt;custom post type ordering&lt;/a&gt; is the other reason people spend time in these list tables.&lt;/p&gt;

&lt;h3&gt;
  
  
  The conditions I would put on it
&lt;/h3&gt;

&lt;p&gt;It is a display column. It prints a computed string. The marketing line is "Find, Copy &amp;amp; Share URLs Instantly", and what the screenshot actually shows is &lt;code&gt;/hello-world/&lt;/code&gt;, a &lt;strong&gt;relative path&lt;/strong&gt; you cannot paste into a browser or send to a client. WordPress already puts a copyable absolute URL in the row actions View link on every published row. The column's real value is scanning a hundred rows at once, which is a genuinely good reason to have it, just not the one on the box.&lt;/p&gt;

&lt;p&gt;It also costs horizontal space on a list already carrying Title, Author, Categories, Tags, Comments and Date, and Screen Options is where you claw that back.&lt;/p&gt;

&lt;p&gt;And the thing worth taping to the monitor: on any page whose parent is a draft, that column is showing you a path with a segment missing. Run the SQL above before you trust it for anything that matters.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtube.com/shorts/9MLLm4ugl5g?feature=share" rel="noopener noreferrer"&gt;Demo Video&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Which columns are worth the width: &lt;a href="https://wpadminify.com/necessary-admin-columns-for-wordpress" rel="noopener noreferrer"&gt;https://wpadminify.com/necessary-admin-columns-for-wordpress&lt;/a&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>php</category>
      <category>beginners</category>
    </item>
    <item>
      <title>WordPress stores two IDs for every category, and the ID column in your admin only shows one of them</title>
      <dc:creator>Lucas Fenwick</dc:creator>
      <pubDate>Sun, 26 Jul 2026 06:12:34 +0000</pubDate>
      <link>https://dev.to/lucasfenw/wordpress-stores-two-ids-for-every-category-and-the-id-column-in-your-admin-only-shows-one-of-them-42pg</link>
      <guid>https://dev.to/lucasfenw/wordpress-stores-two-ids-for-every-category-and-the-id-column-in-your-admin-only-shows-one-of-them-42pg</guid>
      <description>&lt;p&gt;Add an ID column to the WordPress taxonomy list tables by enabling Custom Admin Columns in WP Adminify: go to WP Adminify &amp;gt; Settings &amp;gt; Productivity, scroll to Custom Admin Columns, tick &lt;code&gt;Show "Taxonomy ID" Column for all possible types of taxonomies&lt;/code&gt;, and save, and an ID column appears after Count on Categories, Tags and every custom taxonomy screen, and the number it prints is the &lt;code&gt;term_id&lt;/code&gt;, the same value that sits in each row edit link as &lt;code&gt;term.php?taxonomy=category&amp;amp;tag_ID=3&lt;/code&gt;, which is &lt;strong&gt;not&lt;/strong&gt; the &lt;code&gt;term_taxonomy_id&lt;/code&gt; that &lt;code&gt;wp_term_relationships&lt;/code&gt; uses to join posts to terms.&lt;/p&gt;

&lt;p&gt;That is the whole answer. The rest of this post is where the number was hiding before you added the column, why WordPress has two IDs per term, which one the relationship table uses, and why code that confuses them passes on your laptop and fails on a client site.&lt;/p&gt;

&lt;h3&gt;
  
  
  The number was never missing
&lt;/h3&gt;

&lt;p&gt;Most admin column tricks are recoveries, and this is one of them. Open Posts &amp;gt; Categories and view source on any term row:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;tr&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"tag-3"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"level-0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt; &lt;span class="na"&gt;scope=&lt;/span&gt;&lt;span class="s"&gt;"row"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"check-column"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"delete_tags[]"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"name column-name"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"term.php?taxonomy=category&amp;amp;amp;tag_ID=3&amp;amp;amp;post_type=post"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Dashboard&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ID is in the row element, in the bulk action checkbox, and in the Edit, Quick Edit and Delete links. Core queried it, loaded it, and printed it into the markup. It just never puts it in a cell you can read.&lt;/p&gt;

&lt;p&gt;Which is why the standard workaround exists. &lt;a href="https://taxopress.com/ids-tags-posts-categories/" rel="noopener noreferrer"&gt;TaxoPress&lt;/a&gt;, &lt;a href="https://basicwp.com/view-tag-category-ids-in-wordpress/" rel="noopener noreferrer"&gt;BasicWP&lt;/a&gt; and &lt;a href="https://mythemeshop.com/blog/how-to-find-post-category-tag-comments-or-user-id-in-wordpress/" rel="noopener noreferrer"&gt;MyThemeShop&lt;/a&gt; all teach the same move: hover the category, read &lt;code&gt;tag_ID=3&lt;/code&gt; out of the browser status bar, transcribe it. &lt;a href="https://wordpress.org/plugins/reveal-ids-for-wp-admin-25/" rel="noopener noreferrer"&gt;Reveal IDs&lt;/a&gt; with its 40,000+ active installs and &lt;a href="https://wordpress.org/plugins/catch-ids/" rel="noopener noreferrer"&gt;Catch IDs&lt;/a&gt; automate it. All correct, all fine, and all of them stop at the same sentence: "that number is the category ID".&lt;/p&gt;

&lt;p&gt;WordPress has two numbers for that category.&lt;/p&gt;

&lt;h3&gt;
  
  
  A small aside, because you have probably wondered
&lt;/h3&gt;

&lt;p&gt;The parameter is called &lt;code&gt;tag_ID&lt;/code&gt; on every taxonomy. Categories. WooCommerce &lt;code&gt;product_cat&lt;/code&gt;. Your custom &lt;code&gt;genre&lt;/code&gt;. Tags arrived in WordPress 2.3 and the parameter name arrived with them, and it survived the 4.5 move of term editing from &lt;code&gt;edit-tags.php&lt;/code&gt; to &lt;code&gt;term.php&lt;/code&gt;. If a category edit screen saying &lt;code&gt;tag_ID&lt;/code&gt; has ever looked like a bug to you, it is just a very old name.&lt;/p&gt;

&lt;h3&gt;
  
  
  WordPress keeps two IDs per term
&lt;/h3&gt;

&lt;p&gt;Here is the taxonomy side of the schema, from the &lt;a href="https://developer.wordpress.org/apis/handbook/database/" rel="noopener noreferrer"&gt;database handbook&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;wp_terms&lt;/span&gt;                &lt;span class="n"&gt;term_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;term_group&lt;/span&gt;
&lt;span class="n"&gt;wp_term_taxonomy&lt;/span&gt;        &lt;span class="n"&gt;term_taxonomy_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;term_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;taxonomy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;
&lt;span class="n"&gt;wp_term_relationships&lt;/span&gt;   &lt;span class="n"&gt;object_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;term_taxonomy_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;term_order&lt;/span&gt;
&lt;span class="n"&gt;wp_termmeta&lt;/span&gt;             &lt;span class="n"&gt;meta_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;term_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meta_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meta_value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two auto increment keys, &lt;code&gt;term_id&lt;/code&gt; and &lt;code&gt;term_taxonomy_id&lt;/code&gt;, for what a human calls one category.&lt;/p&gt;

&lt;p&gt;Read the third line again. &lt;code&gt;wp_term_relationships&lt;/code&gt; is the table that connects a post to a category, and it does not store &lt;code&gt;term_id&lt;/code&gt; at all. It pairs &lt;code&gt;object_id&lt;/code&gt; with &lt;code&gt;term_taxonomy_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Meanwhile everything in the admin, including the new ID column, speaks &lt;code&gt;term_id&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core admits the ambiguity in its own API
&lt;/h3&gt;

&lt;p&gt;This is the citation I would put in front of anyone who thinks I am splitting hairs. &lt;a href="https://developer.wordpress.org/reference/classes/wp_tax_query/" rel="noopener noreferrer"&gt;&lt;code&gt;WP_Tax_Query&lt;/code&gt;&lt;/a&gt; accepts both, in the same argument:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// These are two different queries.&lt;/span&gt;
&lt;span class="s1"&gt;'tax_query'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s1"&gt;'taxonomy'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'category'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'field'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'term_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="s1"&gt;'terms'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;47&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="s1"&gt;'tax_query'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s1"&gt;'taxonomy'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'category'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'field'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'term_taxonomy_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'terms'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;47&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the two IDs were the same thing, &lt;code&gt;field&lt;/code&gt; would not need both values. Core is telling you there are two numbers. The list table is not.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this ships as a bug instead of getting caught
&lt;/h3&gt;

&lt;p&gt;On a fresh install the two values line up for the earliest terms. Uncategorized is &lt;code&gt;term_id&lt;/code&gt; 1 and &lt;code&gt;term_taxonomy_id&lt;/code&gt; 1. Add a handful of categories in order and they keep matching. So SQL that confuses them passes locally, passes code review, and passes staging if staging is a clean install.&lt;/p&gt;

&lt;p&gt;They diverge on real sites. WordPress used to allow shared terms, one &lt;code&gt;wp_terms&lt;/code&gt; row serving several taxonomies, and it spent two releases undoing that: &lt;a href="https://developer.wordpress.org/plugins/taxonomies/split-terms-wp-4-2/" rel="noopener noreferrer"&gt;4.2 split shared terms when one was updated&lt;/a&gt;, and &lt;a href="https://make.wordpress.org/core/2015/06/09/eliminating-shared-taxonomy-terms-in-wordpress-4-3/" rel="noopener noreferrer"&gt;4.3 split every remaining shared term on upgrade&lt;/a&gt;. Splitting means new &lt;code&gt;term_id&lt;/code&gt; values, which is exactly how the two columns drift apart. Any site that lived through that, or that has simply run several taxonomies for a few years while the two tables incremented at different rates, has terms where &lt;code&gt;term_id&lt;/code&gt; and &lt;code&gt;term_taxonomy_id&lt;/code&gt; are different numbers.&lt;/p&gt;

&lt;p&gt;That split is also why &lt;a href="https://developer.wordpress.org/reference/functions/get_term/" rel="noopener noreferrer"&gt;&lt;code&gt;get_term()&lt;/code&gt;&lt;/a&gt; could make its &lt;code&gt;$taxonomy&lt;/code&gt; parameter optional in 4.4: once terms stopped being shared, a &lt;code&gt;term_id&lt;/code&gt; identified a term on its own. Useful history, and the same history that put two IDs on your screen with no label.&lt;/p&gt;

&lt;p&gt;Run this on your oldest client site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;term_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;term_taxonomy_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;taxonomy&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_term_taxonomy&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;term_id&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;term_taxonomy_id&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that returns rows, every hand written join on that site is a coin flip.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where each number is safe
&lt;/h3&gt;

&lt;p&gt;Safe with the &lt;code&gt;term_id&lt;/code&gt; your column shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.wordpress.org/reference/functions/get_term/" rel="noopener noreferrer"&gt;&lt;code&gt;get_term( 3 )&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.wordpress.org/reference/functions/wp_list_categories/" rel="noopener noreferrer"&gt;&lt;code&gt;wp_list_categories( array( 'include' =&amp;gt; 3 ) )&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;tax_query&lt;/code&gt; with the default &lt;code&gt;field&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.wordpress.org/reference/functions/get_term_link/" rel="noopener noreferrer"&gt;&lt;code&gt;get_term_link( 3, 'category' )&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.wordpress.org/reference/classes/wp_term_query/" rel="noopener noreferrer"&gt;&lt;code&gt;WP_Term_Query&lt;/code&gt;&lt;/a&gt; with &lt;code&gt;include&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Elementor, Bricks and query loop widgets that ask for term IDs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check before you trust it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;any hand written SQL touching &lt;code&gt;wp_term_relationships&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;import and migration mappings, where both numbers appear in the same export&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.wordpress.org/reference/functions/wp_get_object_terms/" rel="noopener noreferrer"&gt;&lt;code&gt;wp_get_object_terms()&lt;/code&gt;&lt;/a&gt; results being fed back into raw SQL&lt;/li&gt;
&lt;li&gt;anything copying an ID from one site's admin into another site's config&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Get the right number for the join like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$term&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_term&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'category'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$ttid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$term&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;term_taxonomy_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// the value wp_term_relationships wants&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or in SQL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post_title&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_posts&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;wp_term_relationships&lt;/span&gt; &lt;span class="n"&gt;tr&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;object_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;wp_term_taxonomy&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;term_taxonomy_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;term_taxonomy_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;term_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;taxonomy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'category'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the join goes through &lt;code&gt;wp_term_taxonomy&lt;/code&gt;. That is not boilerplate, it is the translation layer between the two IDs.&lt;/p&gt;

&lt;h3&gt;
  
  
  One thing this column gets right
&lt;/h3&gt;

&lt;p&gt;It is safe to sort. &lt;code&gt;term_id&lt;/code&gt; is a real column on &lt;code&gt;wp_terms&lt;/code&gt;, so &lt;a href="https://developer.wordpress.org/reference/functions/get_terms/" rel="noopener noreferrer"&gt;&lt;code&gt;get_terms()&lt;/code&gt;&lt;/a&gt; can order by it with no join and no dropped rows.&lt;/p&gt;

&lt;p&gt;Compare the Last Login column from the same settings panel. That one sorts on user meta, which joins &lt;code&gt;wp_usermeta&lt;/code&gt; on the meta key and removes every user who does not have it, so sorting to find dormant accounts can delete the dormant accounts from the list. Same panel, opposite risk profile, and worth knowing which column you are clicking.&lt;/p&gt;

&lt;h3&gt;
  
  
  The code route
&lt;/h3&gt;

&lt;p&gt;Two hooks, and the second one has a contract people get wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. Register the column.&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'manage_edit-category_columns'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$columns&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$columns&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'term_id'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ID'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$columns&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Print the value. This is a FILTER. Return, do not echo.&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'manage_category_custom_column'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$term_id&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'term_id'&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$column&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$term_id&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Optional, make it sortable.&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'manage_edit-category_sortable_columns'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$columns&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$columns&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'term_id'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'term_id'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$columns&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://developer.wordpress.org/reference/hooks/manage_this-screen-taxonomy_custom_column/" rel="noopener noreferrer"&gt;&lt;code&gt;manage_{$taxonomy}_custom_column&lt;/code&gt;&lt;/a&gt; has been a filter since WordPress 2.8, with the signature &lt;code&gt;( string $string, string $column_name, int $term_id )&lt;/code&gt; and an empty string default. The posts equivalent, &lt;code&gt;manage_{$post_type}_posts_custom_column&lt;/code&gt;, is an action where you echo. Same feature, two different contracts, and the taxonomy one is where people lose an hour.&lt;/p&gt;

&lt;p&gt;Fine print: swap &lt;code&gt;category&lt;/code&gt; for &lt;code&gt;post_tag&lt;/code&gt; or your own taxonomy, register one pair per taxonomy or hook &lt;code&gt;manage_edit-{$taxonomy}_columns&lt;/code&gt; dynamically for all of them, use &lt;a href="https://developer.wordpress.org/reference/hooks/manage_this-screen-id_sortable_columns/" rel="noopener noreferrer"&gt;&lt;code&gt;manage_{$screen-&amp;gt;id}_sortable_columns&lt;/code&gt;&lt;/a&gt; if you prefer the screen-based hook name, and put the whole thing in a must-use plugin because in &lt;code&gt;functions.php&lt;/code&gt; it dies on a theme switch.&lt;/p&gt;

&lt;h3&gt;
  
  
  The checkbox route
&lt;/h3&gt;

&lt;p&gt;I ran this pass with WP Adminify's Productivity module. Settings, Productivity tab, scroll to &lt;a href="https://wpadminify.com/docs/adminify/productivity/custom-admin-columns" rel="noopener noreferrer"&gt;Custom Admin Columns&lt;/a&gt;, tick &lt;code&gt;Show "Taxonomy ID" Column for all possible types of taxonomies&lt;/code&gt;, save. The ID column appends after Count.&lt;/p&gt;

&lt;p&gt;The same panel carries the other list screen columns people otherwise snippet in one at a time, a Post/Page ID column, a Comment ID column with Parent ID, a post thumbnail column and a URL Path column, all documented on that page. If you want arbitrary columns rather than the fixed set, the standalone &lt;a href="https://wpadminify.com/admin-columns-editor" rel="noopener noreferrer"&gt;Admin Columns Editor&lt;/a&gt; is the bigger hammer, &lt;a href="https://wpadminify.com/docs/admin-columns/customization-for-any-post-type" rel="noopener noreferrer"&gt;customising columns for any post type&lt;/a&gt; is the docs entry for that, and &lt;a href="https://wpadminify.com/docs/admin-columns/woocommerce-product-admin-columns" rel="noopener noreferrer"&gt;WooCommerce product columns&lt;/a&gt; is the same idea on &lt;code&gt;product_cat&lt;/code&gt; and friends. The rundown of &lt;a href="https://wpadminify.com/necessary-admin-columns-for-wordpress" rel="noopener noreferrer"&gt;which admin columns actually earn their width&lt;/a&gt; is a sane map before you switch five of them on, &lt;a href="https://wpadminify.com/how-to-add-acf-field-to-admin-column" rel="noopener noreferrer"&gt;adding an ACF field as an admin column&lt;/a&gt; is the next step for custom fields, and the wider &lt;a href="https://wpadminify.com/features/productivity" rel="noopener noreferrer"&gt;Productivity toolset&lt;/a&gt; sits in the same place.&lt;/p&gt;

&lt;h3&gt;
  
  
  The conditions I would put on it
&lt;/h3&gt;

&lt;p&gt;It is a display column. It prints a number, it does not tell you which of the two IDs your other code needs, and it does not remap, merge or migrate anything. It costs horizontal space on a Categories screen already carrying Name, Description, Slug and Count, and Screen Options is where you claw that back. If you enable it on a site with many taxonomies, check that it appears on the ones you care about rather than assuming, because "all possible types of taxonomies" is a claim worth testing on a custom taxonomy.&lt;/p&gt;

&lt;p&gt;And the thing worth taping to the monitor: the ID on that screen is the &lt;code&gt;term_id&lt;/code&gt;. If your next step is a join, translate it first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtube.com/shorts/teRR455UtPk?feature=share" rel="noopener noreferrer"&gt;Demo Shorts&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Docs for the checkbox route: &lt;a href="https://wpadminify.com/docs/adminify/productivity/custom-admin-columns" rel="noopener noreferrer"&gt;https://wpadminify.com/docs/adminify/productivity/custom-admin-columns&lt;/a&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>php</category>
      <category>sql</category>
    </item>
    <item>
      <title>WordPress has never stored when your users last logged in, and the column you add records something slightly different</title>
      <dc:creator>Lucas Fenwick</dc:creator>
      <pubDate>Sat, 25 Jul 2026 06:15:52 +0000</pubDate>
      <link>https://dev.to/lucasfenw/wordpress-has-never-stored-when-your-users-last-logged-in-and-the-column-you-add-records-something-467n</link>
      <guid>https://dev.to/lucasfenw/wordpress-has-never-stored-when-your-users-last-logged-in-and-the-column-you-add-records-something-467n</guid>
      <description>&lt;p&gt;Show the Last Login column for users in the WordPress admin by enabling Custom Admin Columns in WP Adminify: go to WP Adminify &amp;gt; Settings &amp;gt; Productivity, scroll to Custom Admin Columns, tick &lt;code&gt;Show "Last Login" Column for Users&lt;/code&gt;, and save, and a Last Login column appears on the Users screen, but it starts blank for every existing user and fills in only as each one signs in again, because WordPress core stores no last login date anywhere, there is no &lt;code&gt;last_login&lt;/code&gt; field in &lt;code&gt;wp_users&lt;/code&gt; and no user meta key core writes for it, so no plugin on any site can show a login that happened before it was installed, and what the column records is the &lt;code&gt;wp_login&lt;/code&gt; authentication event rather than activity, which means an editor who ticked Remember Me can work in the dashboard every day for two weeks while that column still reads 14 days ago.&lt;/p&gt;

&lt;p&gt;That is the whole answer. The rest of this post is why the column is empty, what it is actually measuring, the login timestamp core keeps and then destroys, and a sorting behaviour that can hide the accounts you are auditing for.&lt;/p&gt;

&lt;h3&gt;
  
  
  There is no field to reveal
&lt;/h3&gt;

&lt;p&gt;Most admin column tricks are recoveries. The Posts list already knows the post ID and drops it. The Comments list already knows &lt;code&gt;comment_parent&lt;/code&gt; and prints an author name instead. You add a column, the number comes back.&lt;/p&gt;

&lt;p&gt;This one is not that. Here is &lt;code&gt;wp_users&lt;/code&gt;, in full:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;ID&lt;/span&gt;
&lt;span class="n"&gt;user_login&lt;/span&gt;
&lt;span class="n"&gt;user_pass&lt;/span&gt;
&lt;span class="n"&gt;user_nicename&lt;/span&gt;
&lt;span class="n"&gt;user_email&lt;/span&gt;
&lt;span class="n"&gt;user_url&lt;/span&gt;
&lt;span class="n"&gt;user_registered&lt;/span&gt;
&lt;span class="n"&gt;user_activation_key&lt;/span&gt;
&lt;span class="n"&gt;user_status&lt;/span&gt;
&lt;span class="n"&gt;display_name&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Registration date, yes. Last login, no. And core writes no user meta key for it either. Every implementation of this feature, &lt;a href="https://wordpress.org/plugins/wp-last-login/" rel="noopener noreferrer"&gt;WP Last Login&lt;/a&gt; with its 10,000+ installs, the &lt;a href="https://www.wpbeginner.com/plugins/how-to-show-users-last-login-date-in-wordpress/" rel="noopener noreferrer"&gt;WPBeginner snippet&lt;/a&gt;, &lt;a href="https://rudrastyh.com/wordpress/sortable-users-last-login-column.html" rel="noopener noreferrer"&gt;rudrastyh's sortable version&lt;/a&gt;, all of them do the same two things: hook &lt;code&gt;wp_login&lt;/code&gt;, write a timestamp to user meta.&lt;/p&gt;

&lt;p&gt;So the column is not uncovering a field. It is starting a recording, and a recording has no past. Every existing user reads &lt;strong&gt;Never&lt;/strong&gt; on day one and fills in one at a time as people sign back in. WPBeginner and rudrastyh both say this plainly, so it is baseline knowledge rather than a discovery. The next three parts are not.&lt;/p&gt;

&lt;h3&gt;
  
  
  What actually gets recorded
&lt;/h3&gt;

&lt;p&gt;Here is the end of &lt;a href="https://developer.wordpress.org/reference/functions/wp_signon/" rel="noopener noreferrer"&gt;&lt;code&gt;wp_signon()&lt;/code&gt;&lt;/a&gt; in &lt;code&gt;wp-includes/user.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;wp_set_auth_cookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$credentials&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'remember'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="cd"&gt;/**
 * Fires after the user has successfully logged in.
 */&lt;/span&gt;
&lt;span class="nf"&gt;do_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wp_login'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;user_login&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://developer.wordpress.org/reference/hooks/wp_login/" rel="noopener noreferrer"&gt;&lt;code&gt;wp_login&lt;/code&gt;&lt;/a&gt; fires on authentication. That is the moment credentials are checked and a cookie is issued.&lt;/p&gt;

&lt;p&gt;It does not fire when a browser comes back holding a cookie that is still valid. And &lt;a href="https://developer.wordpress.org/reference/functions/wp_set_auth_cookie/" rel="noopener noreferrer"&gt;&lt;code&gt;wp_set_auth_cookie()&lt;/code&gt;&lt;/a&gt; issues generous cookies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$remember&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$expiration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;apply_filters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'auth_cookie_expiration'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="no"&gt;DAY_IN_SECONDS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$remember&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$expiration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;apply_filters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'auth_cookie_expiration'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="no"&gt;DAY_IN_SECONDS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="nv"&gt;$user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$remember&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fourteen days with Remember Me ticked. Two days without. Both adjustable through &lt;a href="https://developer.wordpress.org/reference/hooks/auth_cookie_expiration/" rel="noopener noreferrer"&gt;&lt;code&gt;auth_cookie_expiration&lt;/code&gt;&lt;/a&gt;, and almost nobody adjusts them.&lt;/p&gt;

&lt;p&gt;Work the arithmetic on a real team. An editor who ticks Remember Me and opens the dashboard every weekday re-authenticates roughly twice a month. Their Last Login has a hard staleness ceiling of 14 days, and on any given morning it can read 13 days old while they are actively editing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Last Login column:  July 11, 2026
Reality:            in the dashboard every day since July 11
Difference:         one auth event vs 14 days of work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which means the column answers "when did this person last type their password", not "when was this person last here". Those are different questions, and dormant-account audits ask the second one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core does record a login time, then throws it away
&lt;/h3&gt;

&lt;p&gt;This is the part I have not found written down anywhere.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.wordpress.org/reference/classes/wp_session_tokens/create/" rel="noopener noreferrer"&gt;&lt;code&gt;WP_Session_Tokens::create()&lt;/code&gt;&lt;/a&gt; builds a session record and stamps it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_session_information&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$expiration&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Timestamp.&lt;/span&gt;
&lt;span class="nv"&gt;$session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'login'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The record lands in the &lt;code&gt;session_tokens&lt;/code&gt; user meta, keyed by a hash of the session token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;get_user_meta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'session_tokens'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="cm"&gt;/*
array(
  '9f2c...' =&amp;gt; array(
      'expiration' =&amp;gt; 1754648160,
      'ip'         =&amp;gt; '203.0.113.7',
      'ua'         =&amp;gt; 'Mozilla/5.0 ...',
      'login'      =&amp;gt; 1753436160,   // &amp;lt;- the login timestamp
  ),
)
*/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So core knows exactly when the login happened. It stores the number inside a live session record, which is destroyed on logout and garbage collected when the session expires.&lt;/p&gt;

&lt;p&gt;Core keeps a login timestamp only for users who are logged in right now. The users you are auditing are, by definition, the ones whose record core already deleted. It is a genuinely funny piece of design: the data exists precisely when it is least useful.&lt;/p&gt;

&lt;p&gt;Worth knowing for a second reason. If you want to know who is logged in &lt;strong&gt;right now&lt;/strong&gt;, that meta is the answer and the Last Login column is not. Two different questions, two different sources.&lt;/p&gt;

&lt;h3&gt;
  
  
  The sorting behaviour that inverts the audit
&lt;/h3&gt;

&lt;p&gt;Here is the one that costs something.&lt;/p&gt;

&lt;p&gt;Sorting a Users list by a custom column normally means registering the column as sortable and then setting &lt;code&gt;meta_key&lt;/code&gt; plus an &lt;code&gt;orderby&lt;/code&gt; on the user query. That join is on the meta key, so a user with no row for that key is not sorted to the bottom. That user is &lt;strong&gt;excluded from the result set&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Play it out. You enable Last Login specifically to find dormant accounts. You click the column header to sort oldest first. Every row reading Never can drop out of the list, and Never is the strongest dormancy signal on the screen. The audit deletes its own findings.&lt;/p&gt;

&lt;p&gt;The fix, if you are building this yourself, is to keep the never-logged-in users in the query with a two-branch &lt;a href="https://developer.wordpress.org/reference/classes/wp_user_query/" rel="noopener noreferrer"&gt;&lt;code&gt;meta_query&lt;/code&gt;&lt;/a&gt; rather than a bare &lt;code&gt;meta_key&lt;/code&gt;, via &lt;a href="https://developer.wordpress.org/reference/hooks/pre_get_users/" rel="noopener noreferrer"&gt;&lt;code&gt;pre_get_users&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'pre_get_users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wpa_last_login'&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'orderby'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'meta_query'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'relation'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'OR'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'logged'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'key'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'wpa_last_login'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'compare'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'EXISTS'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'never'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'key'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'wpa_last_login'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'compare'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'NOT EXISTS'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'orderby'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'logged'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test that on your own install before trusting it, meta_query ordering is fiddly and version-sensitive.&lt;/p&gt;

&lt;p&gt;Honestly though, for an actual dormancy audit, skip the column and run the query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_login&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_registered&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;wp_usermeta&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meta_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'wpa_last_login'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;umeta_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_registered&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every row is someone who has not authenticated since you started recording. Swap &lt;code&gt;wp_&lt;/code&gt; for your real table prefix and swap the meta key for whatever your plugin writes.&lt;/p&gt;

&lt;h3&gt;
  
  
  The code route, and its fine print
&lt;/h3&gt;

&lt;p&gt;Recording first, because there is nothing to display until you do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// mu-plugin, not functions.php&lt;/span&gt;
&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wp_login'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpa_record_last_login'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpa_record_last_login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$user_login&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;update_user_meta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpa_last_login'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the column, using &lt;a href="https://developer.wordpress.org/reference/hooks/manage_users_custom_column/" rel="noopener noreferrer"&gt;&lt;code&gt;manage_users_custom_column&lt;/code&gt;&lt;/a&gt; for the value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'manage_users_columns'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpa_last_login_column'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpa_last_login_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$columns&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$columns&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'wpa_last_login'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Last Login'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$columns&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'manage_users_custom_column'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpa_last_login_value'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpa_last_login_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$user_id&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wpa_last_login'&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nv"&gt;$column&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nv"&gt;$ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_user_meta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpa_last_login'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$ts&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'Never'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;wp_date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;get_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'date_format'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nf"&gt;get_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'time_format'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$ts&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fine print:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;a href="https://developer.wordpress.org/reference/functions/update_user_meta/" rel="noopener noreferrer"&gt;&lt;code&gt;update_user_meta()&lt;/code&gt;&lt;/a&gt; and store a raw Unix timestamp, not a formatted string. Formatting at write time locks you out of sorting and out of timezone changes&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;wp_date()&lt;/code&gt; rather than &lt;code&gt;date()&lt;/code&gt; so the output respects the site timezone&lt;/li&gt;
&lt;li&gt;User meta is global on multisite. &lt;code&gt;wp_usermeta&lt;/code&gt; is not per-site, so a Last Login recorded on one site in the network shows on every site's Users screen. That is either useful or misleading depending on what you are auditing&lt;/li&gt;
&lt;li&gt;The value callback runs per row, so this is one &lt;code&gt;get_user_meta()&lt;/code&gt; per user on screen. Fine at 20 rows, worth thinking about at 500&lt;/li&gt;
&lt;li&gt;Application password requests, REST authentication and programmatic &lt;code&gt;wp_set_current_user()&lt;/code&gt; do not go through &lt;code&gt;wp_signon()&lt;/code&gt;, so headless and API traffic will not update this&lt;/li&gt;
&lt;li&gt;In a mu-plugin it survives theme switches. In &lt;code&gt;functions.php&lt;/code&gt; it does not&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The checkbox route
&lt;/h3&gt;

&lt;p&gt;I did this run with WP Adminify's Productivity module. Settings &amp;gt; &lt;strong&gt;Productivity&lt;/strong&gt; tab &amp;gt; scroll to &lt;strong&gt;Custom Admin Columns&lt;/strong&gt; &amp;gt; tick &lt;code&gt;Show "Last Login" Column for Users&lt;/code&gt; &amp;gt; save.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Last Login column appends to the Users screen after Posts, no code, survives theme switches. Their own guide states the feature is in the free version on wordpress.org, worth verifying against your install before you promise it to a client&lt;/li&gt;
&lt;li&gt;The full walkthrough with the code alternative is on &lt;a href="https://wpadminify.com/how-to-show-last-login-column-in-wordpress-users-list" rel="noopener noreferrer"&gt;how to show the Last Login column in the WordPress Users list&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The same panel carries the rest of the list-screen columns people usually snippet in one at a time, &lt;strong&gt;Show Post/Page ID Column&lt;/strong&gt;, &lt;strong&gt;Comment ID with Parent ID&lt;/strong&gt;, &lt;strong&gt;Taxonomy ID&lt;/strong&gt;, and a &lt;strong&gt;URL Path&lt;/strong&gt; column with per-post-type selection, all on the &lt;a href="https://wpadminify.com/docs/adminify/productivity/custom-admin-columns" rel="noopener noreferrer"&gt;Custom Admin Columns docs page&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;If you want a date column and also arbitrary meta columns, the standalone &lt;a href="https://wpadminify.com/admin-columns-editor" rel="noopener noreferrer"&gt;Admin Columns Editor&lt;/a&gt; is the bigger hammer&lt;/li&gt;
&lt;li&gt;This rundown of &lt;a href="https://wpadminify.com/necessary-admin-columns-for-wordpress" rel="noopener noreferrer"&gt;necessary admin columns for WordPress&lt;/a&gt; is a decent map of which columns earn their width&lt;/li&gt;
&lt;li&gt;On the users side of the same product, &lt;a href="https://wpadminify.com/hide-admin-bar-based-on-user-roles" rel="noopener noreferrer"&gt;hiding the admin bar based on user roles&lt;/a&gt; and &lt;a href="https://wpadminify.com/how-to-change-wordpress-username" rel="noopener noreferrer"&gt;changing a WordPress username&lt;/a&gt; are the two adjustments most sites make next&lt;/li&gt;
&lt;li&gt;It all sits inside the wider &lt;a href="https://wpadminify.com/features/productivity" rel="noopener noreferrer"&gt;Productivity toolset&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The caveats I will not skip.&lt;/strong&gt; A Last Login column is a date on a screen. It does not log activity, record sessions, capture IP addresses or flag anything, so treat "track user activity" as marketing rather than a feature. If you actually need activity records, that is a separate job and &lt;a href="https://wpadminify.com/how-to-monitor-user-activity-in-wordpress" rel="noopener noreferrer"&gt;monitoring user activity in WordPress&lt;/a&gt; plus the &lt;a href="https://wpadminify.com/docs/activity-logs/track-users" rel="noopener noreferrer"&gt;track users docs&lt;/a&gt; is the honest pointer. The column also costs horizontal space on a Users screen already carrying Username, Name, Email, Role and Posts, and Screen Options is where you get it back. The docs page does not document the column as sortable, so treat sorting as unconfirmed, and if it is sortable, check the never-logged-in behaviour above before you rely on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  One more, for the offboarding case
&lt;/h3&gt;

&lt;p&gt;A last login date is personal data. If you are recording it to satisfy a security policy, it also lands inside whatever data export and erasure obligations you already have, and it should be in your privacy policy alongside the rest of the user meta you collect. Not a reason to skip the column. A reason to write it down.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtube.com/shorts/ys-UP6GE2Bw?feature=share" rel="noopener noreferrer"&gt;Short demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Guide: &lt;a href="https://wpadminify.com/how-to-show-last-login-column-in-wordpress-users-list" rel="noopener noreferrer"&gt;How to show the Last Login column in the WordPress Users list&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So: column on permanently, snippet in a mu-plugin, or a scheduled query? And has anyone here actually cross-checked a dormant list against &lt;code&gt;session_tokens&lt;/code&gt; before revoking access?&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>php</category>
      <category>security</category>
    </item>
    <item>
      <title>WordPress tells you a comment is "In reply to A WordPress Commenter." That is a name, not an ID</title>
      <dc:creator>Lucas Fenwick</dc:creator>
      <pubDate>Tue, 21 Jul 2026 09:29:40 +0000</pubDate>
      <link>https://dev.to/lucasfenw/wordpress-tells-you-a-comment-is-in-reply-to-a-wordpress-commenter-that-is-a-name-not-an-id-29ci</link>
      <guid>https://dev.to/lucasfenw/wordpress-tells-you-a-comment-is-in-reply-to-a-wordpress-commenter-that-is-a-name-not-an-id-29ci</guid>
      <description>&lt;p&gt;Display comment ID and parent ID in the WordPress admin by enabling Custom Admin Columns in WP Adminify: go to WP Adminify &amp;gt; Productivity, turn on Custom Admin Columns, tick &lt;code&gt;Show "Comment ID" Column for Comment, Also show "Parent ID"&lt;/code&gt;, and save, and an ID column and a Parent ID column appear on the Comments screen, where Parent ID 0 means a top-level comment and any other number is the comment ID that comment replies to, because WordPress core never prints either number and instead prints the parent as an author name, "In reply to A WordPress Commenter.", which identifies nothing the moment one person replies twice in the same thread, leaving &lt;code&gt;comment.php?action=editcomment&amp;amp;c=2&lt;/code&gt; in the address bar as the only built-in way to read the real number.&lt;/p&gt;

&lt;p&gt;That is the whole answer. The rest of this post is why the name is worse than a blank, what Parent ID 0 actually means, the code route, and a core behaviour around trash that silently breaks threads.&lt;/p&gt;

&lt;h3&gt;
  
  
  The substitution, not the omission
&lt;/h3&gt;

&lt;p&gt;Every guide on this topic teaches the same thing. Click Edit under a comment, read the number after &lt;code&gt;&amp;amp;c=&lt;/code&gt;. &lt;a href="https://www.wpbeginner.com/beginners-guide/how-to-find-post-category-tag-comments-or-user-id-in-wordpress/" rel="noopener noreferrer"&gt;WPBeginner&lt;/a&gt; does it, and so does every page that followed it.&lt;/p&gt;

&lt;p&gt;They all stop at the comment ID. None of them touch the parent, and the reason is that core appears to have already handled the parent. Here is what &lt;a href="https://developer.wordpress.org/reference/classes/wp_comments_list_table/column_comment/" rel="noopener noreferrer"&gt;&lt;code&gt;WP_Comments_List_Table::column_comment()&lt;/code&gt;&lt;/a&gt; actually emits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'In reply to %s.'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'&amp;lt;a href="'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$parent_link&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'"&amp;gt;'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;/a&amp;gt;'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A display name, linked to the parent comment. Which is genuinely useful for a human reading one row, and useless for anything else, because &lt;code&gt;$name&lt;/code&gt; is not unique inside a thread. Take a support post where one person replies four times:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ID 118  In reply to A WordPress Commenter.
ID 121  In reply to A WordPress Commenter.
ID 126  In reply to A WordPress Commenter.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three rows, one string, three different parents. The admin cannot distinguish them and neither can you.&lt;/p&gt;

&lt;p&gt;Meanwhile the row itself is built from a &lt;code&gt;WP_Comment&lt;/code&gt; object that already holds &lt;code&gt;comment_parent&lt;/code&gt;, and &lt;a href="https://developer.wordpress.org/reference/classes/wp_comments_list_table/" rel="noopener noreferrer"&gt;&lt;code&gt;WP_Comments_List_Table&lt;/code&gt;&lt;/a&gt; prints the comment's own ID straight into the row element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;tr id='comment-&lt;/span&gt;&lt;span class="nv"&gt;$comment-&amp;gt;comment_ID&lt;/span&gt;&lt;span class="s2"&gt;' class='&lt;/span&gt;&lt;span class="nv"&gt;$the_comment_class&lt;/span&gt;&lt;span class="s2"&gt;'&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and into every row action URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Approve  -&amp;gt; comment.php?action=approvecomment&amp;amp;c=2&amp;amp;_wpnonce=...
Spam     -&amp;gt; comment.php?action=spamcomment&amp;amp;c=2&amp;amp;_wpnonce=...
Trash    -&amp;gt; comment.php?action=trashcomment&amp;amp;c=2&amp;amp;_wpnonce=...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the comment ID is printed all over the markup, exactly like the post ID is on the Posts list. The parent ID is the interesting one, and that one is loaded and thrown away.&lt;/p&gt;

&lt;p&gt;This is worth separating from the missing post ID column, because they fail differently. A missing column is an omission, and omissions get noticed eventually. This is a substitution. Something occupies the space where the answer belongs and reads like an answer, which is why a topic this old still has no good page written about it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parent ID 0 is a value, not a blank
&lt;/h3&gt;

&lt;p&gt;First thing anyone asks once the column is on.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;comment_parent&lt;/code&gt; defaults to &lt;code&gt;0&lt;/code&gt;, and &lt;code&gt;0&lt;/code&gt; is what core writes for a top-level comment. Any other number is the &lt;code&gt;comment_ID&lt;/code&gt; of the comment being replied to. That is the entire threading model. There is no separate tree table and no depth field on the row, just a self-referencing integer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ID   Parent ID
1    0          &amp;lt;- thread opener
2    1          &amp;lt;- reply to 1
3    1          &amp;lt;- another reply to 1
4    3          &amp;lt;- reply to 3, depth 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which is exactly what the demo screen shows: ID 1 / Parent 0 for the original comment, ID 2 / Parent 1 for the admin's reply. Two integer columns and you have reconstructed every thread on the page without opening a single comment. Depth is not stored anywhere, it is derived by walking the chain, which is also why Settings &amp;gt; Discussion caps nesting with "Enable threaded (nested) comments N levels deep" rather than storing a level.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not carry the post ID explanation over
&lt;/h3&gt;

&lt;p&gt;If you read the post ID version of this topic you learned that posts, pages, custom post types, attachments, revisions and autosaves all draw from one &lt;code&gt;auto_increment&lt;/code&gt; in &lt;code&gt;wp_posts&lt;/code&gt;, which is why a clean install hands you page IDs 6, 3 and 2.&lt;/p&gt;

&lt;p&gt;None of that applies here. Comments live in &lt;code&gt;wp_comments&lt;/code&gt; with their own &lt;code&gt;comment_ID auto_increment&lt;/code&gt;. Nothing else shares it.&lt;/p&gt;

&lt;p&gt;Comment IDs still have gaps, for a completely different reason: deleted spam and purged trash consume numbers and never return them. A site running Akismet for five years can be at comment ID 40,000 with two hundred real comments on the screen. Same symptom, different cause, and the wrong explanation is currently the popular one.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it actually costs
&lt;/h3&gt;

&lt;p&gt;One comment ID is a five second click into Edit. The pain shows up when the ID is not the thing you needed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auditing a thread that has been moderated for months, where "In reply to [same name]" appears on six rows&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;parent&lt;/code&gt; or &lt;code&gt;parent__in&lt;/code&gt; argument in &lt;a href="https://developer.wordpress.org/reference/classes/wp_comment_query/" rel="noopener noreferrer"&gt;&lt;code&gt;WP_Query&lt;/code&gt;'s comment counterpart, &lt;code&gt;WP_Comment_Query&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wp comment list --fields=comment_ID,comment_parent&lt;/code&gt; with &lt;a href="https://developer.wordpress.org/cli/commands/comment/list/" rel="noopener noreferrer"&gt;WP-CLI&lt;/a&gt; to diff before and after a migration&lt;/li&gt;
&lt;li&gt;Deciding what a bulk delete is about to take down with it&lt;/li&gt;
&lt;li&gt;A support thread where two commenters share a display name, and the ID is the only unambiguous handle anyone has&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And comment IDs fail the same silent way post IDs do. A wrong digit does not error. &lt;code&gt;get_comment(1263)&lt;/code&gt; returns null or someone else's comment and the template renders happily.&lt;/p&gt;

&lt;h3&gt;
  
  
  The code route, and its fine print
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// functions.php: ID + Parent ID columns on the Comments screen&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'manage_edit-comments_columns'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpa_comment_id_columns'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpa_comment_id_columns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$columns&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// insert both right after the checkbox column&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;array_slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$columns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s1"&gt;'wpa_cid'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'ID'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpa_cpid'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Parent ID'&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;
         &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;array_slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$columns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'manage_comments_custom_column'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpa_comment_id_values'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpa_comment_id_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$comment_id&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wpa_cid'&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$column&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$comment_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wpa_cpid'&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$column&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$comment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_comment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$comment_id&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$comment&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$comment&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;comment_parent&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hook reference is &lt;a href="https://developer.wordpress.org/reference/hooks/manage_comments_custom_column/" rel="noopener noreferrer"&gt;&lt;code&gt;manage_comments_custom_column&lt;/code&gt;&lt;/a&gt;, and the object you are reading is documented under &lt;a href="https://developer.wordpress.org/reference/functions/get_comment/" rel="noopener noreferrer"&gt;&lt;code&gt;get_comment()&lt;/code&gt;&lt;/a&gt;. The fine print:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The value callback receives the comment ID, not the comment object, so the Parent ID column costs a &lt;code&gt;get_comment()&lt;/code&gt; per row unless you prime the cache&lt;/li&gt;
&lt;li&gt;Sorting is not included, that is a separate sortable-columns filter plus a query tweak&lt;/li&gt;
&lt;li&gt;Both columns claim whatever width they like until you add admin CSS&lt;/li&gt;
&lt;li&gt;It lives in &lt;code&gt;functions.php&lt;/code&gt;, so it dies on theme switch unless you promote it to a must-use plugin&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The checkbox route
&lt;/h3&gt;

&lt;p&gt;I did this run with WP Adminify's Productivity module. Enable &lt;strong&gt;Custom Admin Columns&lt;/strong&gt;, tick &lt;code&gt;Show "Comment ID" Column for Comment, Also show "Parent ID"&lt;/code&gt;, save.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ID and Parent ID appear on the Comments screen, no code, survives theme switches. Note it is one checkbox for both columns, you get the pair or neither&lt;/li&gt;
&lt;li&gt;The same panel carries the rest of the list-screen columns people usually snippet in one at a time, &lt;strong&gt;Show Post/Page ID Column&lt;/strong&gt;, &lt;strong&gt;Taxonomy ID&lt;/strong&gt;, a &lt;strong&gt;URL Path&lt;/strong&gt; column with per-post-type selection, and a &lt;strong&gt;Last Login&lt;/strong&gt; column for users, all on the &lt;a href="https://wpadminify.com/docs/adminify/productivity/custom-admin-columns" rel="noopener noreferrer"&gt;Custom Admin Columns docs page&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;If you outgrow fixed checkboxes, the standalone &lt;a href="https://wpadminify.com/admin-columns-editor" rel="noopener noreferrer"&gt;Admin Columns Editor&lt;/a&gt; builds columns for arbitrary fields&lt;/li&gt;
&lt;li&gt;This rundown of &lt;a href="https://wpadminify.com/necessary-admin-columns-for-wordpress" rel="noopener noreferrer"&gt;necessary admin columns for WordPress&lt;/a&gt; is a decent map of which columns earn their width&lt;/li&gt;
&lt;li&gt;On the comment side of the same product, &lt;a href="https://wpadminify.com/hide-comments-menu-from-wordpress-admin-panel" rel="noopener noreferrer"&gt;hiding the Comments menu from the admin panel&lt;/a&gt; and &lt;a href="https://wpadminify.com/remove-website-field-from-wordpress-comments" rel="noopener noreferrer"&gt;removing the website field from the comment form&lt;/a&gt; are the two adjustments most sites make first, and there is a full guide to &lt;a href="https://wpadminify.com/how-to-disable-wordpress-comments" rel="noopener noreferrer"&gt;disabling WordPress comments&lt;/a&gt; if the answer is that you do not want them at all&lt;/li&gt;
&lt;li&gt;It all sits inside the wider &lt;a href="https://wpadminify.com/features/productivity" rel="noopener noreferrer"&gt;Productivity toolset&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The caveats I will not skip.&lt;/strong&gt; Two ID columns are display, not management. They print two numbers, they do not moderate, re-thread or repair anything, so treat "better comment management" as marketing rather than a feature. They cost two columns of horizontal space on a screen already carrying Author, Comment, In response to and Submitted on, and Screen Options is where you get that back. The docs do not document either column as sortable, so treat sorting by ID as unconfirmed. And it is a single checkbox for the pair, so there is no documented way to show ID without Parent ID.&lt;/p&gt;

&lt;h3&gt;
  
  
  The core behaviour that quietly breaks threads
&lt;/h3&gt;

&lt;p&gt;This is the part I would put above the column itself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.wordpress.org/reference/functions/wp_delete_comment/" rel="noopener noreferrer"&gt;&lt;code&gt;wp_delete_comment()&lt;/code&gt;&lt;/a&gt; contains this, commented in core as "Move children up a level":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$children&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"SELECT comment_ID FROM &lt;/span&gt;&lt;span class="nv"&gt;$wpdb-&amp;gt;comments&lt;/span&gt;&lt;span class="s2"&gt; WHERE comment_parent = %d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$comment&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;comment_ID&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$children&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'comment_parent'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$comment&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;comment_parent&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'comment_parent'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$comment&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;comment_ID&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Permanently delete a comment and its replies get re-pointed at the deleted comment's own parent. The thread survives, one level shallower. Good behaviour.&lt;/p&gt;

&lt;p&gt;Trash does not run that block. Trashing sets the comment status and leaves &lt;code&gt;comment_parent&lt;/code&gt; on every child exactly as it was, pointing at a comment that is no longer in the thread.&lt;/p&gt;

&lt;p&gt;Those replies stay Approved. They render on the Comments screen looking completely ordinary. The conversation they belonged to is gone from the front end. Nothing errors, nothing is logged, and there is no admin surface that shows it, unless you can read the Parent ID column and notice a number that no longer resolves.&lt;/p&gt;

&lt;p&gt;If you want to check a site right now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;comment_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;comment_parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;comment_post_ID&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_comments&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;wp_comments&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;comment_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;comment_parent&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;comment_parent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;comment_ID&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;comment_approved&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'1'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every row that comes back is a reply whose parent is missing or not approved.&lt;/p&gt;

&lt;p&gt;The same logic makes the migration case worse than it is for posts. Comment IDs belong to a database, not a site. Re-import comments and they renumber, and unlike a post ID, a comment ID is referenced by other comments. If the importer does not remap &lt;code&gt;comment_parent&lt;/code&gt; alongside &lt;code&gt;comment_ID&lt;/code&gt;, threads flatten or, worse, reattach to whatever comment happens to hold the old number now. Run the query above after every migration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtube.com/shorts/PWYvp-WUa5w?feature=share" rel="noopener noreferrer"&gt;Short demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Docs: &lt;a href="https://wpadminify.com/docs/adminify/productivity/custom-admin-columns" rel="noopener noreferrer"&gt;Show Comment ID and Parent ID columns in WP Adminify&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So: both columns on permanently, snippet, or still clicking Edit and reading &lt;code&gt;&amp;amp;c=&lt;/code&gt;? And has anyone here actually run the orphan query on a site they inherited?&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>php</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The WordPress post ID is already in every row of your admin list, core just never prints it</title>
      <dc:creator>Lucas Fenwick</dc:creator>
      <pubDate>Mon, 20 Jul 2026 07:02:25 +0000</pubDate>
      <link>https://dev.to/lucasfenw/the-wordpress-post-id-is-already-in-every-row-of-your-admin-list-core-just-never-prints-it-3k6a</link>
      <guid>https://dev.to/lucasfenw/the-wordpress-post-id-is-already-in-every-row-of-your-admin-list-core-just-never-prints-it-3k6a</guid>
      <description>&lt;p&gt;Show post and page IDs in the WordPress admin by enabling Custom Admin Columns in WP Adminify: go to WP Adminify &amp;gt; Productivity, turn on Custom Admin Columns, tick Show Post/Page ID Column, and save, and an ID column appears on the Posts and Pages list screens. WordPress core has never printed the ID in that list even though every row already carries it, because the list table renders each row as &lt;code&gt;&amp;lt;tr id="post-123"&amp;gt;&lt;/code&gt; and every Edit link in the row already points at &lt;code&gt;post.php?post=123&lt;/code&gt;, so the ID is loaded and printed into the page HTML while the only built-in way to read it is hovering a link and checking the browser status bar one post at a time.&lt;/p&gt;

&lt;p&gt;That is the whole answer. The rest of this post is why the hover trick is evidence, why your IDs look random, the code route, and the migration warning nobody puts in these tutorials.&lt;/p&gt;

&lt;h3&gt;
  
  
  The workaround is the proof
&lt;/h3&gt;

&lt;p&gt;Every guide on this topic teaches the same thing. Hover Edit, read &lt;code&gt;post.php?post=123&lt;/code&gt; off the status bar. Or open the post and read the ID out of the address bar.&lt;/p&gt;

&lt;p&gt;Both work. Both also quietly admit that the ID is already rendered. Look at what &lt;a href="https://developer.wordpress.org/reference/classes/wp_posts_list_table/" rel="noopener noreferrer"&gt;&lt;code&gt;WP_Posts_List_Table&lt;/code&gt;&lt;/a&gt; actually emits for one row:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;tr&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"post-123"&lt;/span&gt;
    &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"iedit author-self level-0 post-123 type-page status-publish hentry"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"title column-title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/wp-admin/post.php?post=123&amp;amp;action=edit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Sample Page&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"row-actions"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"edit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/wp-admin/post.php?post=123&amp;amp;action=edit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Edit&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"trash"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/wp-admin/post.php?post=123&amp;amp;action=trash"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Trash&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"view"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/?page_id=123"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;View&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
  ...
&lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ID is the row's HTML &lt;code&gt;id&lt;/code&gt;. It repeats in the row classes, courtesy of &lt;a href="https://developer.wordpress.org/reference/functions/get_post_class/" rel="noopener noreferrer"&gt;&lt;code&gt;get_post_class()&lt;/code&gt;&lt;/a&gt;. It appears in four URLs. On a 20-row screen that is roughly a hundred printed copies of numbers you cannot read.&lt;/p&gt;

&lt;p&gt;This is worth separating from the other famous admin blind spot. A missing featured image column is a genuine absence, the featured image is postmeta and the list table does not render postmeta. The post ID is the opposite, it is the primary key of the row the list table is already looping over. It is present and unprinted, because core treats the ID as a database internal rather than something a publisher needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why your IDs look random
&lt;/h3&gt;

&lt;p&gt;The most common follow-up question on this topic: why is my third page ID 6?&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;wp_posts&lt;/code&gt; has one &lt;code&gt;auto_increment&lt;/code&gt; primary key, and everything lives in that table. Posts, pages, custom post types, attachments, revisions, autosaves, menu items, all of it draws from the same counter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ID 1  -&amp;gt; "Hello world!" post
ID 2  -&amp;gt; "Sample Page"
ID 3  -&amp;gt; "Privacy Policy" (auto-draft)
ID 4  -&amp;gt; autosave / revision
ID 5  -&amp;gt; an uploaded image (attachment)
ID 6  -&amp;gt; "Home"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which is exactly what the demo shows: Home 6, Privacy Policy 3, Sample Page 2. There is no per-post-type sequence, gaps are normal, and IDs are never guessable. Anybody who has tried to work out an ID by counting rows has learned this the slow way.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it actually costs
&lt;/h3&gt;

&lt;p&gt;One ID is a five second hover. The pain is bulk.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An Elementor or query loop excluding twelve specific pages&lt;/li&gt;
&lt;li&gt;A gallery or posts shortcode taking a comma-separated ID list&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;post__in&lt;/code&gt; / &lt;code&gt;post__not_in&lt;/code&gt; argument in &lt;a href="https://developer.wordpress.org/reference/classes/wp_query/" rel="noopener noreferrer"&gt;&lt;code&gt;WP_Query&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;wp post get 123&lt;/code&gt; run against a client site with &lt;a href="https://developer.wordpress.org/cli/commands/post/get/" rel="noopener noreferrer"&gt;WP-CLI&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Two pages both titled "Home", where the ID is the only unambiguous handle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Twelve IDs means twelve hovers and twelve hand transcriptions. And IDs are unforgiving in a specific way: a wrong digit does not error. &lt;code&gt;post__not_in =&amp;gt; [124]&lt;/code&gt; excludes some other post entirely and the loop renders happily. You find it when a client asks why the wrong page is showing.&lt;/p&gt;

&lt;h3&gt;
  
  
  The code route, and its fine print
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// functions.php: ID column on posts and pages&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'manage_posts_columns'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpa_id_column'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'manage_pages_columns'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpa_id_column'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpa_id_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$columns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// insert ID right after the checkbox column&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;array_slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$columns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'wpa_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'ID'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
         &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;array_slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$columns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'manage_posts_custom_column'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpa_id_value'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'manage_pages_custom_column'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpa_id_value'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpa_id_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$post_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$column&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'wpa_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$post_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hook reference is &lt;a href="https://developer.wordpress.org/reference/hooks/manage_post_type_posts_columns/" rel="noopener noreferrer"&gt;&lt;code&gt;manage_{$post_type}_posts_columns&lt;/code&gt;&lt;/a&gt;. The fine print:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Posts and pages take separate filter pairs, and every custom post type wants its own if you need per-type control&lt;/li&gt;
&lt;li&gt;Sorting is not included, that is a third hook (&lt;code&gt;manage_edit-post_sortable_columns&lt;/code&gt;) plus a &lt;code&gt;pre_get_posts&lt;/code&gt; tweak&lt;/li&gt;
&lt;li&gt;The column claims whatever width it likes until you add admin CSS&lt;/li&gt;
&lt;li&gt;It lives in &lt;code&gt;functions.php&lt;/code&gt;, so it dies on theme switch unless you promote it to a must-use plugin&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The checkbox route
&lt;/h3&gt;

&lt;p&gt;I did this run with WP Adminify's Productivity module. Enable &lt;strong&gt;Custom Admin Columns&lt;/strong&gt;, tick &lt;strong&gt;Show Post/Page ID Column&lt;/strong&gt;, save.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An ID column appears on the Posts and Pages lists, no code, survives theme switches&lt;/li&gt;
&lt;li&gt;The same panel carries the rest of the list-screen columns people usually snippet in one at a time, &lt;strong&gt;Comment ID&lt;/strong&gt; with parent ID, &lt;strong&gt;Taxonomy ID&lt;/strong&gt;, a &lt;strong&gt;URL Path&lt;/strong&gt; column with per-post-type selection, and a &lt;strong&gt;Last Login&lt;/strong&gt; column for users, all on the &lt;a href="https://wpadminify.com/docs/adminify/productivity/custom-admin-columns" rel="noopener noreferrer"&gt;Custom Admin Columns docs page&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;If you outgrow fixed checkboxes, the standalone &lt;a href="https://wpadminify.com/admin-columns-editor" rel="noopener noreferrer"&gt;Admin Columns Editor&lt;/a&gt; builds columns for arbitrary fields, and &lt;a href="https://wpadminify.com/how-to-add-acf-field-to-admin-column" rel="noopener noreferrer"&gt;how to add an ACF field to an admin column&lt;/a&gt; is the worked example for meta fields&lt;/li&gt;
&lt;li&gt;This rundown of &lt;a href="https://wpadminify.com/necessary-admin-columns-for-wordpress" rel="noopener noreferrer"&gt;necessary admin columns for WordPress&lt;/a&gt; is a decent map of which columns earn their width, and &lt;a href="https://wpadminify.com/docs/admin-columns/customization-for-any-post-type" rel="noopener noreferrer"&gt;column customization for any post type&lt;/a&gt; covers the CPT side&lt;/li&gt;
&lt;li&gt;It sits inside the wider &lt;a href="https://wpadminify.com/features/productivity" rel="noopener noreferrer"&gt;Productivity toolset&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The caveats I will not skip.&lt;/strong&gt; An ID column is display, not magic, it does not validate anything and it does not make an ID portable. It costs horizontal space on a screen that is already tight, Screen Options is where you claw that back. The docs describe this checkbox as being for "post and page table lists", so I am not claiming custom post type coverage until someone verifies it on a real CPT. And the column is not documented as sortable, so treat sorting by ID as unconfirmed.&lt;/p&gt;

&lt;h3&gt;
  
  
  The warning that matters more than the column
&lt;/h3&gt;

&lt;p&gt;A post ID is unique to one database, not to your site.&lt;/p&gt;

&lt;p&gt;Export the content and re-import it, rebuild staging from a fresh install, migrate hosts with a content-level tool, and the same page comes back with a different ID. Every template, shortcode, exclude list and option row referencing the old number keeps running. Nothing throws. The wrong content renders.&lt;/p&gt;

&lt;p&gt;So read IDs, use them, and prefer slugs wherever the API accepts one. Where you do hardcode, put the ID in a config constant or an option rather than scattered through templates, so the fix after a migration is one line and not a grep.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtube.com/shorts/R0ojOEG9UX4?feature=share" rel="noopener noreferrer"&gt;Short demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Docs: &lt;a href="https://wpadminify.com/docs/adminify/productivity/custom-admin-columns" rel="noopener noreferrer"&gt;Show Post/Page ID Column in WP Adminify&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So: ID column always on, snippet it per post type, or still hovering the Edit link? And be honest, how many hardcoded IDs are in your production templates right now?&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>php</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The WordPress admin never shows featured images, and the reason is a design decision worth knowing</title>
      <dc:creator>Lucas Fenwick</dc:creator>
      <pubDate>Sun, 19 Jul 2026 05:17:37 +0000</pubDate>
      <link>https://dev.to/lucasfenw/the-wordpress-admin-never-shows-featured-images-and-the-reason-is-a-design-decision-worth-knowing-2pgo</link>
      <guid>https://dev.to/lucasfenw/the-wordpress-admin-never-shows-featured-images-and-the-reason-is-a-design-decision-worth-knowing-2pgo</guid>
      <description>&lt;p&gt;Show featured images in the WordPress admin post list by enabling Custom Admin Columns in WP Adminify: go to WP Adminify &amp;gt; Productivity, turn on Custom Admin Columns, tick Show Post Thumbnails Column, and a Thumbnail column appears in your Posts list, with an optional default Column Thumbnail Image for posts that have no featured image set. WordPress core has never shown featured images in the Posts list because the featured image is a &lt;code&gt;_thumbnail_id&lt;/code&gt; record in postmeta, and the admin list table renders posts-table fields and taxonomy links but never postmeta, so the only built-in way to check featured images is opening posts one by one.&lt;/p&gt;

&lt;p&gt;That is the whole answer. The rest of this post is the why, the code route, and the audit trick, because the why is the part every tutorial skips.&lt;/p&gt;

&lt;h3&gt;
  
  
  The featured image was never in the post
&lt;/h3&gt;

&lt;p&gt;Set a featured image and WordPress stores exactly one thing, a postmeta record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;_thumbnail_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;    &lt;span class="c1"&gt;// one meta row, pointing at an attachment ID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The post itself does not change. The image is a reference living in a side table, read on the frontend by &lt;a href="https://developer.wordpress.org/reference/functions/get_the_post_thumbnail/" rel="noopener noreferrer"&gt;&lt;code&gt;get_the_post_thumbnail()&lt;/code&gt;&lt;/a&gt; and written by &lt;a href="https://developer.wordpress.org/reference/functions/set_post_thumbnail/" rel="noopener noreferrer"&gt;&lt;code&gt;set_post_thumbnail()&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now look at what the admin Posts list renders: title, author, date from the posts table, categories and tags from taxonomy relationships. Postmeta is not on that list. The list table has no column for any meta field, and featured image is a meta field. So the frontend shows the image on every theme card, every share preview reads it for &lt;code&gt;og:image&lt;/code&gt;, and the admin, the one screen where you manage the posts, shows nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the gap actually costs something
&lt;/h3&gt;

&lt;p&gt;A post with no featured image is not a cosmetic problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It shares blank or with a random fallback image on Facebook, X and LinkedIn&lt;/li&gt;
&lt;li&gt;Most themes render it as an empty card in archives and related-post blocks&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developers.google.com/search/docs/appearance/google-discover" rel="noopener noreferrer"&gt;Google Discover&lt;/a&gt; favors large images, at least 1200px wide, so the post is effectively out of the Discover pool&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And here is the trap: those posts are invisible in exactly the screen where you would catch them. No column, no signal, no audit. The broken posts surface one embarrassing blank share at a time.&lt;/p&gt;

&lt;h3&gt;
  
  
  The code route: five lines, plus fine print
&lt;/h3&gt;

&lt;p&gt;Adding the column yourself is a filter and an action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// functions.php: thumbnail column for posts&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'manage_posts_columns'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$columns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'thumb'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Thumbnail'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$columns&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'manage_posts_custom_column'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$post_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$column&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'thumb'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nf"&gt;get_the_post_thumbnail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hook reference is &lt;a href="https://developer.wordpress.org/reference/hooks/manage_post_type_posts_columns/" rel="noopener noreferrer"&gt;&lt;code&gt;manage_{$post_type}_posts_columns&lt;/code&gt;&lt;/a&gt;. The fine print is where the snippet stops being five lines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pages need &lt;code&gt;manage_pages_columns&lt;/code&gt; and &lt;code&gt;manage_pages_custom_column&lt;/code&gt;, separately&lt;/li&gt;
&lt;li&gt;Every custom post type needs its own &lt;code&gt;manage_{$post_type}_posts_columns&lt;/code&gt; filter if you want per-type control&lt;/li&gt;
&lt;li&gt;A post with no featured image renders an empty cell, no fallback, so the audit signal is a blank you can miss&lt;/li&gt;
&lt;li&gt;Column width needs a dab of admin CSS or the thumbnail column grabs whatever space it likes&lt;/li&gt;
&lt;li&gt;It lives in &lt;code&gt;functions.php&lt;/code&gt;, which means it dies on theme switch unless you move it to a must-use plugin&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The checkbox route
&lt;/h3&gt;

&lt;p&gt;I did this run with WP Adminify's Productivity module. What changed after enabling &lt;strong&gt;Custom Admin Columns&lt;/strong&gt; and ticking &lt;strong&gt;Show Post Thumbnails Column&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A thumbnail column appeared on the Posts and Pages lists, no code, survives theme switches&lt;/li&gt;
&lt;li&gt;The same settings screen offers a &lt;strong&gt;Column Thumbnail Image&lt;/strong&gt;, a default image shown for any post with no featured image, and that default is the audit trick: every row showing it is a post that will share blank, so the gaps mark themselves on one scroll&lt;/li&gt;
&lt;li&gt;It ships beside the other list-screen columns in the same panel, post and page ID columns, a last-login column for users, taxonomy IDs, the small stuff you otherwise snippet in one at a time, all documented on the &lt;a href="https://wpadminify.com/docs/adminify/productivity/custom-admin-columns" rel="noopener noreferrer"&gt;Custom Admin Columns docs page&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;If you outgrow the checkbox, the standalone &lt;a href="https://wpadminify.com/admin-columns-editor" rel="noopener noreferrer"&gt;Admin Columns Editor&lt;/a&gt; builds columns for arbitrary fields, and this list of &lt;a href="https://wpadminify.com/necessary-admin-columns-for-wordpress" rel="noopener noreferrer"&gt;necessary admin columns for WordPress&lt;/a&gt; is a decent map of which ones earn their width&lt;/li&gt;
&lt;li&gt;It sits in the wider &lt;a href="https://wpadminify.com/features/productivity" rel="noopener noreferrer"&gt;Productivity toolset&lt;/a&gt;, and pairs naturally with the &lt;a href="https://wpadminify.com/how-to-update-replace-image-in-wordpress-without-changing-url" rel="noopener noreferrer"&gt;replace-image-without-changing-URL workflow&lt;/a&gt; once the column has shown you which images need swapping&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The caveat I will not skip:&lt;/strong&gt; the column adds one thumbnail request per row. WordPress serves the small registered thumbnail size so it is minor, but a 200-row list on slow hosting will feel it. And the default image marks a missing featured image, it does not set one, the post still shares blank until you fix it. The column finds the work. It does not do the work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtube.com/shorts/WSCmukIhXV4?feature=share" rel="noopener noreferrer"&gt;Short demo&lt;/a&gt;&lt;br&gt;
Docs: &lt;a href="https://wpadminify.com/docs/adminify/productivity/custom-admin-columns" rel="noopener noreferrer"&gt;Show Post Thumbnails Column in WP Adminify&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So: do you run a thumbnail column, snippet it per post type, or audit featured images the hard way, one edit screen at a time?&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>php</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Copy-pasting a WordPress post only copies a third of it. Here is what the other two thirds are</title>
      <dc:creator>Lucas Fenwick</dc:creator>
      <pubDate>Sat, 18 Jul 2026 05:22:59 +0000</pubDate>
      <link>https://dev.to/lucasfenw/copy-pasting-a-wordpress-post-only-copies-a-third-of-it-here-is-what-the-other-two-thirds-are-1i90</link>
      <guid>https://dev.to/lucasfenw/copy-pasting-a-wordpress-post-only-copies-a-third-of-it-here-is-what-the-other-two-thirds-are-1i90</guid>
      <description>&lt;p&gt;Here is a small thing that catches almost everyone the first time, and it is not really their fault, because the advice that ranks sets them up for it.&lt;/p&gt;

&lt;p&gt;You want a copy of a post or page. Maybe a template for the next one, maybe an A/B variant of a landing page, maybe a backup before a big edit. WordPress has no duplicate button, so the standard advice is: open the editor, select all blocks, copy, paste into a new page.&lt;/p&gt;

&lt;p&gt;You do that, and the copy arrives hollow. No featured image. No categories or tags. No SEO title or description. And if the page was built with Elementor, no page. The paste is blank.&lt;/p&gt;

&lt;p&gt;Every tutorial that recommends copy-paste admits this in a footnote, "you will need to re-add the featured image and SEO settings manually." None of them explain why. Let me explain why, because once you see it you will never be confused by it again.&lt;/p&gt;

&lt;h3&gt;
  
  
  What a WordPress post actually is
&lt;/h3&gt;

&lt;p&gt;A post is three kinds of database records working together.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The posts-table row.&lt;/strong&gt; Title, content, excerpt, status, type. This is the only part the editor shows you and the only part a paste moves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postmeta rows.&lt;/strong&gt; One row per setting, keyed to the post ID. See &lt;a href="https://developer.wordpress.org/reference/functions/get_post_meta/" rel="noopener noreferrer"&gt;&lt;code&gt;get_post_meta()&lt;/code&gt;&lt;/a&gt; for how WordPress reads them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Taxonomy relationships.&lt;/strong&gt; Categories, tags and custom taxonomies are term records linked to the post. They are not stored in the post at all.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Where the "lost" data lives
&lt;/h3&gt;

&lt;p&gt;Everything the paste dropped is record type 2 or 3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;_thumbnail_id&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;          &lt;span class="c1"&gt;// the featured image is postmeta&lt;/span&gt;
&lt;span class="n"&gt;_yoast_wpseo_title&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"..."&lt;/span&gt;       &lt;span class="c1"&gt;// SEO title: postmeta&lt;/span&gt;
&lt;span class="n"&gt;_yoast_wpseo_metadesc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"..."&lt;/span&gt;     &lt;span class="c1"&gt;// meta description: postmeta&lt;/span&gt;
&lt;span class="n"&gt;_elementor_data&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;...&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;]"&lt;/span&gt;   &lt;span class="c1"&gt;// the ENTIRE Elementor layout, JSON in postmeta&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;_elementor_data&lt;/code&gt; one is the fact worth keeping. On an Elementor page, &lt;code&gt;post_content&lt;/code&gt; is nearly empty, a fallback render at best. The real design is serialized JSON in postmeta. So when you copy "the content" of an Elementor page, you are copying the one field that does not contain the page. That is why the paste lands blank.&lt;/p&gt;

&lt;p&gt;Categories are the same story from the other side. They are relationships between the post and term records, so a brand-new pasted post has no relationships until you rebuild them by hand.&lt;/p&gt;

&lt;p&gt;This is the core of it: &lt;strong&gt;duplicating a post is not copying text, it is copying a row, a stack of meta, and a set of term links.&lt;/strong&gt; Copy-paste does the first part only.&lt;/p&gt;

&lt;h3&gt;
  
  
  Doing it correctly in code
&lt;/h3&gt;

&lt;p&gt;If you script it, the shape is three passes, one per record type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Pass 1: the row&lt;/span&gt;
&lt;span class="nv"&gt;$new_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_insert_post&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'post_title'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$src&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;post_title&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' (Copy)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'post_content'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$src&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;post_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'post_type'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$src&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;post_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'post_status'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'draft'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// draft on purpose, see below&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="c1"&gt;// Pass 2: every meta, the part paste never does&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;get_post_meta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$key&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$values&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;add_post_meta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$new_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;maybe_unserialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Pass 3: the term links&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;get_object_taxonomies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$src&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;post_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$tax&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$terms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_get_object_terms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$tax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'fields'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'ids'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nf"&gt;wp_set_object_terms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$new_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$terms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$tax&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See &lt;a href="https://developer.wordpress.org/reference/functions/wp_insert_post/" rel="noopener noreferrer"&gt;&lt;code&gt;wp_insert_post()&lt;/code&gt;&lt;/a&gt; for the first pass. And a warning on pass 2: serialized builder meta like &lt;code&gt;_elementor_data&lt;/code&gt; is where hand-rolled duplicators break, the JSON needs correct unserializing and slashing or the clone's layout corrupts. It is the fiddliest part of the whole job, which is exactly why a maintained one-click tool beats a snippet here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gotcha one: the clone is an SEO twin until you edit it
&lt;/h3&gt;

&lt;p&gt;A correct clone copies the SEO meta too, slug base, title, description. Which means until you change those, you have two pages targeting one query. Publish both and you have built keyword cannibalization by hand. This is why the clone should be created as a draft and stay one while you rewrite the slug, title and meta.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gotcha two: WooCommerce already has this, for one post type
&lt;/h3&gt;

&lt;p&gt;WooCommerce ships a &lt;a href="https://woocommerce.com/document/duplicate-product/" rel="noopener noreferrer"&gt;Duplicate action for products&lt;/a&gt;, because stores clone configured products constantly. But it is products only. Posts, pages and custom post types are on their own.&lt;/p&gt;

&lt;h3&gt;
  
  
  The settings-panel version
&lt;/h3&gt;

&lt;p&gt;I did this run with WP Adminify's Productivity module, which covers every post type from one place. 30 second demo below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What changed after enabling Post Duplicator in Adminify &amp;gt; Productivity:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;strong&gt;Adminify Clone&lt;/strong&gt; action appeared in the row actions of Posts, Pages and custom post types, next to Edit and Quick Edit, no code to add it&lt;/li&gt;
&lt;li&gt;One click produced a &lt;code&gt;(Copy)&lt;/code&gt; with the content, featured image, categories and settings intact, which is the exact part copy-paste drops&lt;/li&gt;
&lt;li&gt;It works on custom post types too, which is where the free single-purpose plugins tend to get fussy&lt;/li&gt;
&lt;li&gt;It sits alongside the rest of the same module, including the &lt;a href="https://wpadminify.com/docs/adminify/productivity/one-click-menu-duplication" rel="noopener noreferrer"&gt;one-click menu duplication&lt;/a&gt; feature, same idea applied to navigation menus, and the wider &lt;a href="https://wpadminify.com/features/productivity" rel="noopener noreferrer"&gt;Productivity toolset&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pair it with the &lt;a href="https://wpadminify.com/admin-columns-editor" rel="noopener noreferrer"&gt;Admin Columns Editor&lt;/a&gt; if you clone a lot, a custom column showing modified dates keeps originals and copies easy to tell apart, and &lt;a href="https://wpadminify.com/activity-logs" rel="noopener noreferrer"&gt;Activity Logs&lt;/a&gt; records who cloned what on multi-author sites&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The caveat I will not skip:&lt;/strong&gt; a clone is a snapshot, not a live mirror, and it inherits everything, including stale meta you forgot was on the original. Clone posts you trust, keep the copy draft, fix the slug and meta, then publish.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtube.com/shorts/stMi9ypqlno?feature=share" rel="noopener noreferrer"&gt;Short demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feature page: &lt;a href="https://wpadminify.com/features/duplicate-post" rel="noopener noreferrer"&gt;Duplicate any WordPress post type with WP Adminify&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So: do you duplicate with a tool, script the three-pass copy yourself, or copy-paste and rebuild the settings every time?&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>php</category>
      <category>beginners</category>
    </item>
    <item>
      <title>WordPress has no button to duplicate a menu, and rebuilding one by hand quietly breaks your dropdowns. Here is why</title>
      <dc:creator>Lucas Fenwick</dc:creator>
      <pubDate>Thu, 16 Jul 2026 05:54:21 +0000</pubDate>
      <link>https://dev.to/lucasfenw/wordpress-has-no-button-to-duplicate-a-menu-and-rebuilding-one-by-hand-quietly-breaks-your-26k0</link>
      <guid>https://dev.to/lucasfenw/wordpress-has-no-button-to-duplicate-a-menu-and-rebuilding-one-by-hand-quietly-breaks-your-26k0</guid>
      <description>&lt;p&gt;Here is a small thing that catches almost everyone the first time, and it is not really their fault, because the tooling sets them up for it.&lt;/p&gt;

&lt;p&gt;You want a copy of a navigation menu. Maybe for a redesign, maybe a seasonal variant, maybe a backup before you start deleting items. So you open Appearance &amp;gt; Menus and look for a Duplicate button.&lt;/p&gt;

&lt;p&gt;There isn't one. WordPress core has never had a way to copy a menu.&lt;/p&gt;

&lt;p&gt;So you do the obvious thing and rebuild it by hand in a new menu, dragging items in one at a time. It works, until you look at the dropdowns and every nested item is now sitting at the top level. No error. Just a flat menu where your hierarchy used to be. Let me explain why, because once you see it you will never be confused by it again.&lt;/p&gt;

&lt;h3&gt;
  
  
  What a WordPress menu actually is
&lt;/h3&gt;

&lt;p&gt;A menu is not a block of text. It is two kinds of database records working together.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The menu itself is a taxonomy term, in the &lt;code&gt;nav_menu&lt;/code&gt; taxonomy.&lt;/li&gt;
&lt;li&gt;Every item inside the menu is a separate hidden post, of type &lt;code&gt;nav_menu_item&lt;/code&gt;, each with its own post ID.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The label, the URL, the link target, all of it lives in that item's postmeta. See &lt;a href="https://developer.wordpress.org/reference/functions/wp_get_nav_menu_items/" rel="noopener noreferrer"&gt;&lt;code&gt;wp_get_nav_menu_items()&lt;/code&gt;&lt;/a&gt; for how WordPress reads them back out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the nesting lives, and why it breaks
&lt;/h3&gt;

&lt;p&gt;The part that makes a menu a menu, the nesting, is stored as postmeta on each item:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;_menu_item_menu_item_parent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;  &lt;span class="c1"&gt;// "my parent is menu item #128"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the trap. The parent is saved as a menu item ID, not a name or a slug. So the whole hierarchy is a web of items pointing at each other by ID.&lt;/p&gt;

&lt;p&gt;Now rebuild that menu by hand. Every item you recreate is a brand new post with a brand new ID. The old parent values still say things like &lt;code&gt;128&lt;/code&gt;, but item 128 no longer exists in the new menu. WordPress cannot resolve the reference, so it treats the child as a top-level item. Do that across a nested menu and the entire thing flattens.&lt;/p&gt;

&lt;p&gt;This is the core of it: &lt;strong&gt;duplicating a menu is not copying text, it is recreating a set of posts AND remapping the parent pointers between them.&lt;/strong&gt; Copy-paste does not do the second part. Naive export/import does not do the second part. That is why they lose your dropdowns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Doing it correctly in code
&lt;/h3&gt;

&lt;p&gt;If you were to script it, the shape is: create the new menu, then create each item with &lt;a href="https://developer.wordpress.org/reference/functions/wp_update_nav_menu_item/" rel="noopener noreferrer"&gt;&lt;code&gt;wp_update_nav_menu_item()&lt;/code&gt;&lt;/a&gt;, keep a map of old item ID to new item ID as you go, and set each new item's &lt;code&gt;menu-item-parent-id&lt;/code&gt; to the mapped new ID rather than the old one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Pseudocode for the part that matters&lt;/span&gt;
&lt;span class="nv"&gt;$id_map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$original_items&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$new_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_update_nav_menu_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$new_menu_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'menu-item-title'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'menu-item-url'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'menu-item-status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'publish'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// do NOT set the old parent here yet&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nv"&gt;$id_map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$new_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// Second pass: now that every item exists, fix the parents&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$original_items&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;menu_item_parent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;update_post_meta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nv"&gt;$id_map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'_menu_item_menu_item_parent'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$id_map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;menu_item_parent&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;// remapped, not the old ID&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two passes, because you cannot set a parent to an ID that does not exist yet. This remap is exactly the work a one-click duplicator does for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gotcha one: the copy is not assigned to a location
&lt;/h3&gt;

&lt;p&gt;Even done correctly, a duplicated menu displays nowhere. It is not attached to any theme location. You assign it under Appearance &amp;gt; Menus &amp;gt; Manage Locations when you are ready.&lt;/p&gt;

&lt;p&gt;That is not a missing step, it is the point. When you clone a menu to redesign it, you want the copy invisible to visitors until it is finished. Duplicate, rework the copy, swap the location at the end, and the live nav never breaks while you work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gotcha two: the copy is a snapshot, not a mirror
&lt;/h3&gt;

&lt;p&gt;Once you duplicate, the two menus are independent sets of &lt;code&gt;nav_menu_item&lt;/code&gt; posts. Editing the original later does not change the copy. Perfect for a backup taken before a risky edit. Not what you want if you assumed the two would stay in sync.&lt;/p&gt;

&lt;h3&gt;
  
  
  The settings-panel version
&lt;/h3&gt;

&lt;p&gt;I did this run with WP Adminify's Productivity module, which adds the one-click duplicate right where you already are. 30 second demo below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What one click gets you, and what it does not:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A full copy of the menu: every item, the exact order, and the nested dropdowns, with the parent IDs remapped so the hierarchy survives&lt;/li&gt;
&lt;li&gt;The copy left unassigned, so it shows nowhere until you assign a location (the safe-redesign default)&lt;/li&gt;
&lt;li&gt;What it does NOT do: keep the copy in sync with the original. It is a snapshot. Two separate menus from the moment you click&lt;/li&gt;
&lt;li&gt;What to watch: orphaned items pointing at deleted pages get copied too, so tidy the menu around the clone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What changed after enabling Menu Duplicator in Adminify &amp;gt; Productivity:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Duplicate Menu button appeared on the Appearance &amp;gt; Menus screen, next to the menu name, no code to add it&lt;/li&gt;
&lt;li&gt;One click produced a &lt;code&gt;(Copy)&lt;/code&gt; of a three-level menu with the nesting intact, which is the part hand rebuilds ruin&lt;/li&gt;
&lt;li&gt;It sits alongside the rest of the same module, the &lt;a href="https://wpadminify.com/docs/adminify/productivity/remove-unwanted-dashboard-widgets" rel="noopener noreferrer"&gt;dashboard widget cleanup&lt;/a&gt; and the &lt;a href="https://wpadminify.com/docs/adminify/productivity/hide-admin-notices" rel="noopener noreferrer"&gt;admin notice hiding&lt;/a&gt;, so the admin you are working in is also the admin you cleaned&lt;/li&gt;
&lt;li&gt;Pair it with the &lt;a href="https://wpadminify.com/docs/adminify/admin-menu/menu-layout-options" rel="noopener noreferrer"&gt;admin menu layout options&lt;/a&gt; if what you actually wanted was to reorder the WordPress admin sidebar, which is a different feature people confuse with navigation menus&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The caveat I will not skip:&lt;/strong&gt; duplication is a copy, not a link. The moment you click, you have two menus that drift apart independently. That is ideal for backups and variants, and wrong if you expected a live mirror. Match the tool to the intent.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtube.com/shorts/X-K-C-xjJa0?feature=share" rel="noopener noreferrer"&gt;Short demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step-by-step doc: &lt;a href="https://wpadminify.com/docs/adminify/productivity/one-click-menu-duplication" rel="noopener noreferrer"&gt;One-click menu duplication in WordPress&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;More admin control in the same direction: &lt;a href="https://wpadminify.com/docs/adminify/productivity/remove-unwanted-dashboard-widgets" rel="noopener noreferrer"&gt;remove unwanted dashboard widgets&lt;/a&gt;, &lt;a href="https://wpadminify.com/docs/adminify/productivity/hide-admin-notices" rel="noopener noreferrer"&gt;hide admin notices&lt;/a&gt;, &lt;a href="https://wpadminify.com/docs/adminify/admin-menu/menu-layout-options" rel="noopener noreferrer"&gt;change the admin menu layout and width&lt;/a&gt;, and &lt;a href="https://wpadminify.com/docs/adminify/productivity/hide-screen-options-and-help-tab" rel="noopener noreferrer"&gt;hide Screen Options and the Help tab&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So: do you duplicate menus with a tool, rebuild and re-nest them by hand, or spin up staging for every nav change?&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>php</category>
      <category>beginners</category>
    </item>
    <item>
      <title>You dragged your WordPress posts into order and the front end ignored you. Here is the step every tutorial skips</title>
      <dc:creator>Lucas Fenwick</dc:creator>
      <pubDate>Wed, 15 Jul 2026 07:19:48 +0000</pubDate>
      <link>https://dev.to/lucasfenw/you-dragged-your-wordpress-posts-into-order-and-the-front-end-ignored-you-here-is-the-step-every-48a4</link>
      <guid>https://dev.to/lucasfenw/you-dragged-your-wordpress-posts-into-order-and-the-front-end-ignored-you-here-is-the-step-every-48a4</guid>
      <description>&lt;p&gt;ere is a thing that trips up almost everyone the first time, and it is not really their fault, because the tutorials set them up for it.&lt;/p&gt;

&lt;p&gt;You install a reorder plugin, or you enable drag-and-drop ordering. You go to the Posts screen, grab a row by its handle, drop it into a new position. The list reorders. It saves instantly, no Save button. The admin table looks exactly the way you wanted.&lt;/p&gt;

&lt;p&gt;Then you open the live site and the posts are in the same order they always were.&lt;/p&gt;

&lt;p&gt;No error. No warning. Just a quiet, complete lack of change. Let me explain why, because once you see it you will never be confused by it again.&lt;/p&gt;

&lt;h3&gt;
  
  
  What a drag-drop reorder actually does
&lt;/h3&gt;

&lt;p&gt;When you drop that row, WordPress does exactly one thing. It writes a number to the &lt;code&gt;menu_order&lt;/code&gt; column of that post's row in &lt;code&gt;wp_posts&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;wp_posts&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;menu_order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;421&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the entire operation. It is real, it is saved, and it is durable. But it is just a number sitting in a column.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the front end does not care
&lt;/h3&gt;

&lt;p&gt;Your blog page, your archives, your category pages all run the WordPress main query, and the main query has a default sort:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// WP_Query defaults&lt;/span&gt;
&lt;span class="s1"&gt;'orderby'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'date'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s1"&gt;'order'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'DESC'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;menu_order&lt;/code&gt; is not &lt;code&gt;date&lt;/code&gt;. So the front end keeps serving posts newest-first and never looks at the column you just updated. This is the core of it: &lt;strong&gt;reordering in the admin and reordering on the site are two different jobs.&lt;/strong&gt; The drag handle does the first. Nothing you dragged does the second.&lt;/p&gt;

&lt;h3&gt;
  
  
  Doing the second job
&lt;/h3&gt;

&lt;p&gt;To make your hand-built order show on the front end, some query has to sort on &lt;code&gt;menu_order&lt;/code&gt;. On a stock theme, that is one hook on the main query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pre_get_posts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;is_admin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$q&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;is_main_query&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$q&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;is_home&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$q&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;is_post_type_archive&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$q&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;is_category&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$q&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orderby'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'menu_order'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$q&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'order'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ASC'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reference: &lt;a href="https://developer.wordpress.org/reference/hooks/pre_get_posts/" rel="noopener noreferrer"&gt;&lt;code&gt;pre_get_posts&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://developer.wordpress.org/reference/classes/wp_query/#order-orderby-parameters" rel="noopener noreferrer"&gt;WP_Query orderby parameters&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gotcha one: posts do not even have an order field
&lt;/h3&gt;

&lt;p&gt;Before you can drag anything, note that posts have no order field by default. Only pages ship with the Order attribute. The Posts list has no &lt;code&gt;menu_order&lt;/code&gt; UI until something adds it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_post_type_support&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'post'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'page-attributes'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reference: &lt;a href="https://developer.wordpress.org/reference/functions/add_post_type_support/" rel="noopener noreferrer"&gt;&lt;code&gt;add_post_type_support()&lt;/code&gt;&lt;/a&gt;. This is why so many how-to posts open with a functions.php snippet before you can see a single drag handle. A reorder plugin adds this support for you, which is half of why people reach for one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gotcha two: the fix that works on a stock theme fails silently on a builder
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;pre_get_posts&lt;/code&gt; only reaches queries that respect filters. Two very common cases do not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A theme or plugin that runs &lt;code&gt;new WP_Query([... 'suppress_filters' =&amp;gt; true])&lt;/code&gt;. That flag tells WordPress to skip the filter chain, so your hook never runs on that query.&lt;/li&gt;
&lt;li&gt;A Gutenberg &lt;strong&gt;Query Loop block&lt;/strong&gt;, which has its own Order and Order By controls in the block sidebar. It sorts by whatever the block says, not by your hook.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the exact same code works perfectly on Twenty Twenty-Four and does absolutely nothing on a page builder page, and there is no error to point you at the cause. When you are on a builder, set the order in the block's own controls, or on the specific &lt;code&gt;WP_Query&lt;/code&gt; you control.&lt;/p&gt;

&lt;h3&gt;
  
  
  The settings-panel version
&lt;/h3&gt;

&lt;p&gt;I did this run with WP Adminify's Productivity module, which handles the drag-drop half across more than just posts. 30 second demo below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What manual reorder gets you, and what it does not:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Drag-and-drop across Posts, Pages, custom post types, the Media library and taxonomy terms, saved on drop&lt;/li&gt;
&lt;li&gt;The order field support added for you, so the handle is just there&lt;/li&gt;
&lt;li&gt;What it does NOT do on its own: change the front end. That is still a query concern, &lt;code&gt;orderby menu_order&lt;/code&gt;, and you own that part&lt;/li&gt;
&lt;li&gt;On a builder, the Query Loop or the builder's own sort wins, so wire it there&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What changed after enabling Post Types Order in Adminify → Productivity:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Posts, media and taxonomies all became sortable from the normal list screens, no snippet to add the handle&lt;/li&gt;
&lt;li&gt;Nothing to re-paste after a migration, and it survives core updates&lt;/li&gt;
&lt;li&gt;It sits alongside the rest of the same module, the &lt;a href="https://wpadminify.com/docs/adminify/productivity/remove-unwanted-dashboard-widgets" rel="noopener noreferrer"&gt;dashboard widget cleanup&lt;/a&gt; and the &lt;a href="https://wpadminify.com/docs/adminify/productivity/hide-admin-notices" rel="noopener noreferrer"&gt;admin notice hiding&lt;/a&gt;, so the admin you are ordering is also the admin you cleaned&lt;/li&gt;
&lt;li&gt;Pair it with the &lt;a href="https://wpadminify.com/docs/adminify/admin-menu/menu-layout-options" rel="noopener noreferrer"&gt;admin menu layout options&lt;/a&gt; if what you actually wanted was to reorder the admin menu, which is a different feature people confuse with this one&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The caveat I will not skip:&lt;/strong&gt; manual order is a maintenance promise. Every new post arrives at &lt;code&gt;menu_order&lt;/code&gt; 0 and lands at the top or the bottom, so someone has to place it. This is a great fit for small, stable sets, a portfolio, a services list, featured items, a staff page. It is a bad fit for a daily blog with hundreds of posts, where the order is stale before you finish arranging it. Match the tool to the set.&lt;/p&gt;

&lt;p&gt;Short demo: &lt;em&gt;[embed Post Type Sortable.mp4]&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Step-by-step doc: &lt;a href="https://wpadminify.com/features/post-type-order" rel="noopener noreferrer"&gt;Reorder posts by type in WordPress&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;More admin control in the same direction: &lt;a href="https://wpadminify.com/docs/adminify/productivity/remove-unwanted-dashboard-widgets" rel="noopener noreferrer"&gt;remove unwanted dashboard widgets&lt;/a&gt;, &lt;a href="https://wpadminify.com/docs/adminify/productivity/hide-admin-notices" rel="noopener noreferrer"&gt;hide admin notices&lt;/a&gt;, &lt;a href="https://wpadminify.com/docs/adminify/admin-menu/menu-layout-options" rel="noopener noreferrer"&gt;collapse and resize the admin menu&lt;/a&gt;, and &lt;a href="https://wpadminify.com/docs/adminify/productivity/hide-screen-options-and-help-tab" rel="noopener noreferrer"&gt;hide Screen Options and the Help tab&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So: do you wire the front end query yourself after dragging, or do you reach for a plugin that does both halves, or do you keep posts on date order and control the display some other way?&lt;/p&gt;

&lt;h1&gt;
  
  
  adminify
&lt;/h1&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>php</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Screen Options is the undo button on your client's dashboard, and the CSS fix everyone posts does not remove it</title>
      <dc:creator>Lucas Fenwick</dc:creator>
      <pubDate>Tue, 14 Jul 2026 06:09:28 +0000</pubDate>
      <link>https://dev.to/lucasfenw/screen-options-is-the-undo-button-on-your-clients-dashboard-and-the-css-fix-everyone-posts-does-4led</link>
      <guid>https://dev.to/lucasfenw/screen-options-is-the-undo-button-on-your-clients-dashboard-and-the-css-fix-everyone-posts-does-4led</guid>
      <description>&lt;p&gt;You clean up a client's WordPress admin. Remove the WordPress Events widget, the Activity feed, the three plugin promo boxes that appeared after the last update. Hide four columns off the Posts list so the table is legible. Collapse the custom fields panel so nobody nukes a meta value by accident.&lt;/p&gt;

&lt;p&gt;Then you leave a button in the top right of the screen called &lt;strong&gt;Screen Options&lt;/strong&gt;, and every single thing you just hid is sitting inside it behind a checkbox.&lt;/p&gt;

&lt;p&gt;That is the part I want to argue about, because every article on this keyword frames Screen Options as clutter. It is not clutter. It is a &lt;strong&gt;permission&lt;/strong&gt;. It is the undo button for the work you just did, handed to the person you did it for.&lt;/p&gt;

&lt;p&gt;And directly beside it is the &lt;strong&gt;Help&lt;/strong&gt; tab, which serves a non-technical client a panel of WordPress.org documentation and a link to the support forums. They ask strangers about a site those strangers cannot see, they come back with instructions that do not fit your build, and you spend a call unpicking it.&lt;/p&gt;

&lt;p&gt;So, how do you actually remove them. There are two code paths here, and almost every guide covers one and calls it done.&lt;/p&gt;

&lt;h3&gt;
  
  
  Path one: Screen Options is a filter
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'screen_options_show_screen'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'__return_false'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That stops the panel being rendered at all. But do not ship it like that, because it takes Screen Options away from &lt;strong&gt;you&lt;/strong&gt; too, and that is where the per-page pagination counts, the column toggles on every list table and the custom fields toggle in the classic editor live. Role gate it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'screen_options_show_screen'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$show&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;current_user_can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'manage_options'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nv"&gt;$show&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reference: &lt;a href="https://developer.wordpress.org/reference/hooks/screen_options_show_screen/" rel="noopener noreferrer"&gt;&lt;code&gt;screen_options_show_screen&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Path two: the Help tab is not a filter, it is a method on the screen object
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'admin_head'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$screen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_current_screen&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$screen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$screen&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;remove_help_tabs&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is the trap that takes sites down. A lot of published snippets hook this on &lt;code&gt;admin_init&lt;/code&gt;. The global &lt;code&gt;$current_screen&lt;/code&gt; is set &lt;strong&gt;right after&lt;/strong&gt; &lt;code&gt;admin_init&lt;/code&gt; has run, so at that point &lt;code&gt;get_current_screen()&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt; and you get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fatal error: Uncaught Error: Call to a member function remove_help_tabs() on null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The core docs say it outright: use it on the &lt;code&gt;current_screen&lt;/code&gt; hook or later. &lt;code&gt;admin_head&lt;/code&gt; is fine. &lt;code&gt;admin_init&lt;/code&gt; is a white screen. See &lt;a href="https://developer.wordpress.org/reference/functions/get_current_screen/" rel="noopener noreferrer"&gt;&lt;code&gt;get_current_screen()&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://developer.wordpress.org/reference/classes/wp_screen/remove_help_tabs/" rel="noopener noreferrer"&gt;&lt;code&gt;WP_Screen::remove_help_tabs()&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Also, while we are here: stop reaching for the old &lt;code&gt;contextual_help&lt;/code&gt; filter for this. The screen object methods replaced it years ago.&lt;/p&gt;

&lt;h3&gt;
  
  
  The answer that actually ranks, and why it is cosmetic
&lt;/h3&gt;

&lt;p&gt;Search "remove screen options wordpress" and you will be handed this, over and over:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nf"&gt;#screen-options-link-wrap&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nf"&gt;#contextual-help-link-wrap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It hides the &lt;strong&gt;button&lt;/strong&gt;. Open view-source on that same admin page and the whole &lt;code&gt;#screen-options-wrap&lt;/code&gt; form is still there. Rendered. Present in the DOM. Still wired to the AJAX handler that writes those preferences to user meta. You have hidden a button that is attached to a working form.&lt;/p&gt;

&lt;p&gt;Devtools, one property. A browser extension that strips styles. A stylesheet that 404s after a migration. Any of those, and the client has your cleanup back, plus the mild thrill of having found something you tried to hide from them. It is security theatre for the dashboard, and it is the top answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  The thing nobody is saying: the block editor has no Screen Options tab
&lt;/h3&gt;

&lt;p&gt;This is the one that changes what you do on Monday.&lt;/p&gt;

&lt;p&gt;Gutenberg does not have Screen Options. It never did. The equivalent panel is the three dot menu in the top right → &lt;strong&gt;Preferences&lt;/strong&gt; → &lt;strong&gt;Panels&lt;/strong&gt;, and it is React. It does not read &lt;code&gt;screen_options_show_screen&lt;/code&gt;. Your filter does not touch it.&lt;/p&gt;

&lt;p&gt;Which means the normal workflow, paste the snippet, watch the tab vanish from the dashboard, close the ticket, has done exactly nothing to the screen the client spends their entire working day on. If your goal was "the client cannot re-enable the panels I hid", you are not finished, and worse, you believe you are. Handle the editor separately, or accept the gap and say so out loud.&lt;/p&gt;

&lt;h3&gt;
  
  
  The settings-panel version
&lt;/h3&gt;

&lt;p&gt;I did this run with WP Adminify's Productivity module, which has both toggles in one place. 30 second demo below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Screen Options and Help actually cost you on a client site:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every widget, column and meta box you hid is one checkbox away from returning, and the person holding the checkbox is the client&lt;/li&gt;
&lt;li&gt;The Help tab routes a non-technical user to WordPress.org support instead of to you, and they come back with advice for a site nobody looked at&lt;/li&gt;
&lt;li&gt;The most shared "fix" is a CSS rule that hides the button and leaves the form live in the page source&lt;/li&gt;
&lt;li&gt;The second most shared "fix" hooks &lt;code&gt;remove_help_tabs()&lt;/code&gt; on &lt;code&gt;admin_init&lt;/code&gt; and fatals&lt;/li&gt;
&lt;li&gt;And whatever you do to the classic screens, Gutenberg still has Preferences → Panels&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What changed after ticking Hide Screen Options and Hide Help Tab in Adminify → Productivity:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Both tabs gone across the admin, from one screen, no snippet to remember&lt;/li&gt;
&lt;li&gt;Nothing to re-paste after a migration, and it survives core updates&lt;/li&gt;
&lt;li&gt;It pairs with the same module's &lt;a href="https://wpadminify.com/docs/adminify/productivity/hide-admin-notices" rel="noopener noreferrer"&gt;admin notice hiding&lt;/a&gt; and &lt;a href="https://wpadminify.com/docs/adminify/productivity/remove-unwanted-dashboard-widgets" rel="noopener noreferrer"&gt;dashboard widget removal&lt;/a&gt;, which is the actual cleanup that Screen Options was busy undoing&lt;/li&gt;
&lt;li&gt;The cleanup finally stays cleaned, which is the entire point&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The caveat I will not skip:&lt;/strong&gt; keep Screen Options for administrators. You need the column toggles, the pagination counts, and the custom fields panel. This is a role decision, not a global one, and if you take it from every user including yourself you will be editing that snippet over SSH inside a month. Ask me how I know.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtube.com/shorts/mpJ-eHAztwc?feature=share" rel="noopener noreferrer"&gt;Short demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step-by-step doc: &lt;a href="https://wpadminify.com/docs/adminify/productivity/hide-screen-options-and-help-tab" rel="noopener noreferrer"&gt;Hide Screen Options and Help Tab&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;More admin cleanup in the same direction: &lt;a href="https://wpadminify.com/docs/adminify/productivity/remove-unwanted-dashboard-widgets" rel="noopener noreferrer"&gt;remove unwanted dashboard widgets&lt;/a&gt;, &lt;a href="https://wpadminify.com/docs/adminify/productivity/hide-admin-notices" rel="noopener noreferrer"&gt;hide admin notices&lt;/a&gt;, &lt;a href="https://wpadminify.com/docs/adminify/admin-menu/menu-layout-options" rel="noopener noreferrer"&gt;collapse and resize the admin menu&lt;/a&gt;, and &lt;a href="https://wpadminify.com/docs/adminify/customize/change-gutenberg-editor-logo" rel="noopener noreferrer"&gt;white label the editor logo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So: do you remove Screen Options by role, hide it with CSS and hope, or leave it and quietly redo the cleanup every few months when the client finds the button?&lt;/p&gt;

&lt;h1&gt;
  
  
  adminify #WordPress
&lt;/h1&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>php</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Removing the WordPress Site Health widget does not disable Site Health, and the four code paths nobody covers</title>
      <dc:creator>Lucas Fenwick</dc:creator>
      <pubDate>Mon, 13 Jul 2026 06:04:51 +0000</pubDate>
      <link>https://dev.to/lucasfenw/removing-the-wordpress-site-health-widget-does-not-disable-site-health-and-the-four-code-paths-29jk</link>
      <guid>https://dev.to/lucasfenw/removing-the-wordpress-site-health-widget-does-not-disable-site-health-and-the-four-code-paths-29jk</guid>
      <description>&lt;p&gt;Load the WordPress dashboard on any client site. The Site Health Status widget sits there with a green circle and the word "Good", then hedges immediately: your site's health is looking good, but there are still some things you can do. Underneath, three flagged rows. You should remove inactive plugins. You should remove inactive themes. Your site is set to display errors to site visitors.&lt;/p&gt;

&lt;p&gt;Every article about this treats it as clutter to be swept away. I want to make a different argument: it is a performance problem, and the standard fix for it does not fix it.&lt;/p&gt;

&lt;p&gt;Start with what the widget actually does. It is not rendering a cached score from an option. Loading the dashboard kicks off a set of &lt;strong&gt;async tests over the REST API&lt;/strong&gt;, and among them is the &lt;strong&gt;loopback test&lt;/strong&gt;, which asks WordPress to make an HTTP request to its own URL and wait for the response. Think about that on a shared host with a small pool of PHP workers. The request that is meant to test your site is queued behind, and competing with, the request that is trying to render the page you are staring at.&lt;/p&gt;

&lt;p&gt;Then there is the bit almost nobody knows about. Core registers a weekly cron event, &lt;code&gt;wp_site_health_scheduled_check&lt;/code&gt;, which runs the whole suite again in the background. It is on every site you have ever built. You have never seen it in a profiler because you have never looked.&lt;/p&gt;

&lt;p&gt;Now the answers you get when you search for this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Answer one, and it is the top result nearly everywhere:&lt;/strong&gt; Dashboard → Screen Options → untick "Site Health Status".&lt;/p&gt;

&lt;p&gt;That is a &lt;strong&gt;per-user preference&lt;/strong&gt;. It is written to user meta. It hides the widget for the exact one person who clicked it, which is the author of the tutorial. Your client logs in and sees the whole panel, unchanged. As an answer to the question people are actually asking, which is almost always "how do I get this off my client's screen", it is wrong, and it has been the top answer for years.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Answer two, the snippet:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'wp_dashboard_setup'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;remove_meta_box&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'dashboard_site_health'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'dashboard'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'normal'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one is real. The widget is gone, for every user. And it has stopped &lt;strong&gt;nothing&lt;/strong&gt;. The cron is still scheduled. The tests still exist. You have hidden the readout, not the work. If you came here because the dashboard was slow, it is still slow, and you now think you fixed it, which is worse than knowing you didn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Tools page is a third path.&lt;/strong&gt; It never came from the widget, so it survives both of the above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'admin_menu'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;remove_submenu_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tools.php'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'site-health.php'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth being precise: that removes the menu item, not the page. Anyone who types &lt;code&gt;/wp-admin/site-health.php&lt;/code&gt; still gets it. If you actually need it locked, gate on capability rather than hiding the link.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And the fourth path is the only one that stops the checks happening at all&lt;/strong&gt;, the test registry itself plus the weekly event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'site_status_tests'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tests&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$tests&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'direct'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="nv"&gt;$tests&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'async'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$tests&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_next_scheduled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'wp_site_health_scheduled_check'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;wp_unschedule_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wp_site_health_scheduled_check'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four hooks, four code paths, one feature. Every guide I read covers one. A couple cover two. None cover four, and the difference between the widget being hidden and the checks being off is the entire question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I will not do, and what people keep suggesting in the forum threads:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DISABLE_WP_CRON'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// to stop the weekly health check&lt;/span&gt;
&lt;span class="c1"&gt;// or: unhooking the REST API                // to stop the async tests&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Killing WP-Cron to silence one weekly event takes scheduled posts, backups and update checks with it. Killing the REST API to stop the async tests takes the block editor with it. Both are amputations to treat a rash. If you find either in a client site, ask why.&lt;/p&gt;

&lt;p&gt;I tested the settings-panel version with WP Adminify Pro. 25 second demo below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Site Health actually costs on a client site:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Async REST tests on every dashboard load, one of which makes the server call itself and wait&lt;/li&gt;
&lt;li&gt;A weekly cron running the whole suite in the background, unnoticed and unattributed&lt;/li&gt;
&lt;li&gt;A red-flagged security warning about &lt;code&gt;display_errors&lt;/code&gt; shown to a person with no ability to act on it&lt;/li&gt;
&lt;li&gt;The most common "fix" for it, Screen Options, is per-user and does nothing for the person who was actually meant to stop seeing it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What changed after ticking Site Health in Adminify Pro's Hide Admin Notices:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One option under &lt;strong&gt;Productivity → Hide Admin Notices → Other Notices&lt;/strong&gt; covers the widget for every admin at once&lt;/li&gt;
&lt;li&gt;The Tools → Site Health page goes with it, no separate &lt;code&gt;remove_submenu_page&lt;/code&gt; to remember&lt;/li&gt;
&lt;li&gt;The checks stop, which is the part &lt;code&gt;remove_meta_box&lt;/code&gt; never did&lt;/li&gt;
&lt;li&gt;The same module handles the update notice pile-up, so the dashboard opens to work rather than a stack of banners&lt;/li&gt;
&lt;li&gt;Nothing to re-paste after a migration, and it survives core updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The caveat I will not skip:&lt;/strong&gt; disabling Site Health does not make your site healthy. It is the only thing on a stock install that will tell you your PHP is unsupported, your HTTPS is misconfigured, a required module is missing, or background updates have quietly stopped. Turning it off means nobody will ever hear those things again. That is right when &lt;em&gt;you&lt;/em&gt; are monitoring them elsewhere, from a maintenance report, an uptime monitor, a staging pipeline, and it is wrong on a site nobody maintains, where that panel is the only warning system that exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And the option that should be the top answer and isn't:&lt;/strong&gt; WordPress ships a capability, &lt;code&gt;view_site_health_checks&lt;/code&gt;. Filter it away from every role except yours and Site Health stays fully alive for you while the client never sees it. When someone asks how to remove Site Health, that is very often the thing they actually wanted, and it is not on page one for any of these queries.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtube.com/shorts/TnTiu8fur9c?feature=share" rel="noopener noreferrer"&gt;Short demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step-by-step doc: &lt;a href="https://wpadminify.com/docs/adminify/productivity/hide-admin-notices" rel="noopener noreferrer"&gt;https://wpadminify.com/docs/adminify/productivity/hide-admin-notices&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How do you handle Site Health on sites you maintain for other people: filter &lt;code&gt;site_status_tests&lt;/code&gt;, gate it behind &lt;code&gt;view_site_health_checks&lt;/code&gt;, disable it with a plugin, or leave it and field the phone calls?&lt;/p&gt;

&lt;h1&gt;
  
  
  adminify
&lt;/h1&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>php</category>
      <category>performance</category>
    </item>
    <item>
      <title>Hiding the WordPress theme update notice takes three hooks, and the snippet every guide gives you does something else entirely</title>
      <dc:creator>Lucas Fenwick</dc:creator>
      <pubDate>Sun, 12 Jul 2026 05:13:06 +0000</pubDate>
      <link>https://dev.to/lucasfenw/hiding-the-wordpress-theme-update-notice-takes-three-hooks-and-the-snippet-every-guide-gives-you-4heg</link>
      <guid>https://dev.to/lucasfenw/hiding-the-wordpress-theme-update-notice-takes-three-hooks-and-the-snippet-every-guide-gives-you-4heg</guid>
      <description>&lt;p&gt;Open Appearance → Themes on a site that is a few weeks behind. There is a count badge on the Themes menu item and a box on the theme card: "New version available. Update now."&lt;/p&gt;

&lt;p&gt;It reads like the mildest notice in WordPress. It is the one I most want off a client's screen, because of what the button does when the parent theme was customised directly. No child theme, edits made straight into the theme, which describes a very large number of the sites any of us inherit. Clicking update now replaces those files with the vendor's. Custom header, template edits, functions.php, replaced with defaults. The notice does not warn about that, and the client reading it has no way to know.&lt;/p&gt;

&lt;p&gt;Most guides on this keyword hand you one line and stop. The line usually looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'site_transient_update_themes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'__return_null'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// please don't&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That does not hide the notice. It empties the transient WordPress stores theme update results in, so core stops knowing that any theme has an update. The notice disappears as a side effect, because there is nothing left to render. You have not cleaned a dashboard, you have blinded the site, and the next developer to inherit it finds a quiet admin sitting on top of a year of unapplied releases. If you find that filter in a client site, take it out.&lt;/p&gt;

&lt;p&gt;Here is what actually prints the notice. On the themes grid (&lt;code&gt;themes.php&lt;/code&gt;) the theme cards are rendered from JS, and the update data comes from &lt;code&gt;wp_prepare_themes_for_js()&lt;/code&gt;, which sets &lt;code&gt;hasUpdate&lt;/code&gt; and &lt;code&gt;update&lt;/code&gt; per theme. So you filter that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'wp_prepare_themes_for_js'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$themes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$themes&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$slug&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$theme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$themes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$slug&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'hasUpdate'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$themes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$slug&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'update'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$themes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The notice is gone from the grid. The update check still runs, which is the point. Now load the screen and the count badge is still on the Themes menu item.&lt;/p&gt;

&lt;p&gt;That badge never came from the notice. It is built by &lt;code&gt;wp_get_update_data()&lt;/code&gt;, which counts pending core, plugin, theme and translation updates and feeds both the admin menu bubble and the toolbar. Second hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'wp_get_update_data'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'counts'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'themes'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'counts'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'total'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if you run multisite, the network Themes screen is a list table, not a grid, with its own update rows printed by &lt;code&gt;wp_theme_update_rows()&lt;/code&gt; on &lt;code&gt;admin_init&lt;/code&gt; at priority 20. Third hook, unhooked from an earlier priority on the same hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'admin_init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;remove_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'admin_init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wp_theme_update_rows'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three code paths for one notice, and the published guides cover at most one of them. Then you want the same treatment for plugin update notices, and core, and each has its own removal. By the time you have covered them all you have a mu-plugin, and the mu-plugin has to travel to every site you build. Miss a hook on one and that client gets a half cleaned dashboard.&lt;/p&gt;

&lt;p&gt;I tested the settings-panel version with WP Adminify Pro instead. 20 second demo below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the theme update notice actually costs on a client site:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A count badge plus an update box on a screen the client visits to change nothing&lt;/li&gt;
&lt;li&gt;The button behind it is destructive on any directly-customised parent theme, and it does not say so&lt;/li&gt;
&lt;li&gt;The notice is aimed at whoever does the updates, which on agency builds is not the person reading it&lt;/li&gt;
&lt;li&gt;It cannot be dismissed, so it never clears until someone updates, and the someone who clicks is usually the wrong someone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What changed after enabling Hide Admin Notices in Adminify Pro:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One toggle under &lt;strong&gt;Productivity&lt;/strong&gt; covers the theme update notice for every admin at once&lt;/li&gt;
&lt;li&gt;The Themes count badge clears with it, no separate &lt;code&gt;wp_get_update_data&lt;/code&gt; filter to remember&lt;/li&gt;
&lt;li&gt;The same module handles the plugin and core notice pile-up, so the admin opens to work rather than a stack of banners&lt;/li&gt;
&lt;li&gt;The update check keeps running, so a maintenance report still sees every pending theme update&lt;/li&gt;
&lt;li&gt;Survives WordPress updates, no snippet to carry between sites&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The caveat I will not skip:&lt;/strong&gt; hiding the notice does not update the theme. Your version is unchanged and any security release you are behind on is still unapplied. This is right when the update has an owner and a schedule (you, your agency, a managed host, an automation with rollback) and when a child theme exists so the update is survivable when it does get run. It is wrong on a site nobody maintains, where that box is the only signal anyone will ever get. Decide who owns updates first. Then hide the notice. And whatever you decide, do not null the transient.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtube.com/shorts/xZaXEL8AmYI?feature=share" rel="noopener noreferrer"&gt;Short demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step-by-step doc: &lt;a href="https://wpadminify.com/docs/adminify/productivity/hide-admin-notices" rel="noopener noreferrer"&gt;https://wpadminify.com/docs/adminify/productivity/hide-admin-notices&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How do you handle theme update notices on sites you maintain for other people: filter &lt;code&gt;wp_prepare_themes_for_js&lt;/code&gt;, hide them with a plugin, or leave them visible and accept the risk?&lt;/p&gt;

&lt;h1&gt;
  
  
  adminify #WordPress #productivity
&lt;/h1&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>php</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
