<?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>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>
    <item>
      <title>Hiding WordPress plugin update notices takes two hooks, and the snippet everyone pastes does something else entirely</title>
      <dc:creator>Lucas Fenwick</dc:creator>
      <pubDate>Sat, 11 Jul 2026 05:32:34 +0000</pubDate>
      <link>https://dev.to/lucasfenw/hiding-wordpress-plugin-update-notices-takes-two-hooks-and-the-snippet-everyone-pastes-does-fmf</link>
      <guid>https://dev.to/lucasfenw/hiding-wordpress-plugin-update-notices-takes-two-hooks-and-the-snippet-everyone-pastes-does-fmf</guid>
      <description>&lt;p&gt;Open the Plugins screen on any WordPress site that is a few weeks behind. Under each outdated plugin, WordPress injects an amber row: "There is a new version of WooCommerce available. View version 10.9.4 details or update now." The Plugins menu item picks up a count badge. On a site with thirty plugins the table stops being scannable.&lt;/p&gt;

&lt;p&gt;Most guides 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_plugins'&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 update results in, so core stops knowing that any plugin has an update. The rows disappear as a side effect, because there is nothing 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 security releases. If you find that filter in a client site, take it out.&lt;/p&gt;

&lt;p&gt;Here is the actual thing that prints the rows. &lt;code&gt;wp_plugin_update_rows()&lt;/code&gt; is hooked to &lt;code&gt;admin_init&lt;/code&gt; at priority 20, and it loops the &lt;code&gt;update_plugins&lt;/code&gt; transient adding a &lt;code&gt;wp_plugin_update_row&lt;/code&gt; callback to &lt;code&gt;after_plugin_row_{$file}&lt;/code&gt; for every outdated plugin. So you unhook the parent, 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_plugin_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;The rows are gone. The update check still runs, which is the point. Load the Plugins screen and the count badge is still on the 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. So it needs a 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;'plugins'&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;Now the rows and the badge are both gone and the update check is intact. Then you want the same treatment for theme update notices, and core, and each one 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 plugin update notices actually cost on a client site:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One amber row per outdated plugin, plus a count badge, for every admin who logs in&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;Clients either learn to ignore amber warnings, or they run an update on production unsupervised&lt;/li&gt;
&lt;li&gt;The rows cannot be dismissed, so they never clear until someone updates&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 plugin update rows for every plugin at once&lt;/li&gt;
&lt;li&gt;The Plugins count badge clears with them, 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 rest of the 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 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 plugin. Your versions are 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. It is wrong on a site nobody maintains, where the amber row 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/yQ2jYCwiVuE?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 plugin update notices on sites you maintain for other people: unhook &lt;code&gt;wp_plugin_update_rows&lt;/code&gt;, hide them with a plugin, or leave them visible on purpose?&lt;/p&gt;

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

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>php</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Hiding the WordPress core update notice takes two hooks, not one (and why the badge survives)</title>
      <dc:creator>Lucas Fenwick</dc:creator>
      <pubDate>Thu, 09 Jul 2026 05:06:15 +0000</pubDate>
      <link>https://dev.to/lucasfenw/hiding-the-wordpress-core-update-notice-takes-two-hooks-not-one-and-why-the-badge-survives-2bhp</link>
      <guid>https://dev.to/lucasfenw/hiding-the-wordpress-core-update-notice-takes-two-hooks-not-one-and-why-the-badge-survives-2bhp</guid>
      <description>&lt;p&gt;Every admin on a WordPress site behind the current release sees the same box above every screen: "WordPress 6.5.3 is available! Please update now." The Updates menu item carries a red count badge next to it, and on a typical production site the plugin and theme update notices stack directly underneath.&lt;/p&gt;

&lt;p&gt;Most guides give you one line for this and stop:&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_notices'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'update_nag'&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="nf"&gt;remove_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'network_admin_notices'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'update_nag'&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;update_nag()&lt;/code&gt; is what prints the core banner, hooked to &lt;code&gt;admin_notices&lt;/code&gt; at priority 3, so unhooking it removes the banner. Load the dashboard and the banner is gone. The red badge is still there.&lt;/p&gt;

&lt;p&gt;That's because the badge doesn't come from the notice at all. It's built from &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 item. So you need a 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="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'plugins'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'themes'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wordpress'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'translations'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'total'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="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;Now the banner and the badge are both gone, and you still have the plugin update notice and the theme update notice printing on their own hooks, each with its own removal. By the time you've covered all four you have a file, and the file has to travel to every site you build. Miss one hook on one site and the client's dashboard looks half-cleaned.&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 update notices actually cost on a client site:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A red badge and up to three warning boxes on every admin screen, for every admin&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;Clients either learn to ignore red warnings, or they run a core update on production unsupervised&lt;/li&gt;
&lt;li&gt;Nobody can dismiss it, so it never clears until someone updates&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 core, plugin, and theme update notices at once&lt;/li&gt;
&lt;li&gt;The Updates count badge clears with them, no separate filter&lt;/li&gt;
&lt;li&gt;The same module handles the rest of the notice pile-up, so the dashboard opens to the Adminify panel rather than a stack of banners&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 won't skip:&lt;/strong&gt; hiding the notice does not update WordPress. Your version is unchanged and any security release you're 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. It's wrong on a site nobody maintains, where the banner is the only signal anyone will ever get. Decide who owns updates first. Then hide the notice.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtube.com/shorts/gHTFAJNebHw?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 update notices on sites you maintain for other people, unhook &lt;code&gt;update_nag&lt;/code&gt;, hide them via a plugin, or leave them visible on purpose?&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>php</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The WordPress "PHP Update Required" notice has no dismiss button — here's how to remove it (and why hiding fixing)</title>
      <dc:creator>Lucas Fenwick</dc:creator>
      <pubDate>Wed, 08 Jul 2026 08:01:57 +0000</pubDate>
      <link>https://dev.to/lucasfenw/the-wordpress-php-update-required-notice-has-no-dismiss-button-heres-how-to-remove-it-and-why-56ig</link>
      <guid>https://dev.to/lucasfenw/the-wordpress-php-update-required-notice-has-no-dismiss-button-heres-how-to-remove-it-and-why-56ig</guid>
      <description>&lt;p&gt;Every admin on a WordPress site below the recommended PHP version sees the same red box on the dashboard: "PHP Update Required — Your site is running an outdated version of PHP (7.4.30), which does not receive security updates and soon will not be supported by WordPress." There's a "Learn more about updating PHP" button — but no Dismiss link.&lt;/p&gt;

&lt;p&gt;WordPress renders that warning as a dashboard nag widget with the id &lt;code&gt;dashboard_php_nag&lt;/code&gt;, registered in &lt;code&gt;wp_dashboard_setup()&lt;/code&gt;. It shows for every admin, on every dashboard load. And you often can't clear it the "right" way on the spot: the PHP version is controlled by the host, a dependency plugin hasn't been tested against the newer build, or the upgrade is scheduled for a maintenance window.&lt;/p&gt;

&lt;p&gt;The developer fix is to unhook the nag:&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_php_nag'&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 removes the PHP Update Required notice for every admin. But it's a snippet you drop into a mu-plugin and re-add on every install — and the moment you also want to clear the other admin-notice clutter (plugin promos, update nags, review requests), you're back to maintaining a growing list of &lt;code&gt;remove_action&lt;/code&gt;/&lt;code&gt;remove_meta_box&lt;/code&gt; calls.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;What the PHP notice actually costs on a real site:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every admin — including clients — opens to a red "does not receive security updates" banner&lt;/li&gt;
&lt;li&gt;It has no Dismiss button, so nobody can clear it from the UI&lt;/li&gt;
&lt;li&gt;On client sites it generates "is my site broken?" support tickets for a warning you already know about&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; — the PHP nag is hidden site-wide for every admin&lt;/li&gt;
&lt;li&gt;The same module clears the rest of the admin-notice pile-up (plugin promos, review nags) from the top of every screen&lt;/li&gt;
&lt;li&gt;Survives WordPress updates, no snippet to maintain across sites&lt;/li&gt;
&lt;li&gt;Dashboard opens straight to Site Health / At a Glance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The important caveat:&lt;/strong&gt; hiding the notice does not update PHP. It removes the alarm from the interface; your PHP version is unchanged. Only hide it once you've made an informed call — confirm your host's supported PHP, check plugin compatibility, and schedule the real upgrade. This is a cosmetic step for client peace of mind, not a security fix.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtube.com/shorts/yLl0qHggOY4?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 the PHP update nag on client sites — unhook &lt;code&gt;dashboard_php_nag&lt;/code&gt;, hide it via a plugin, or leave it until you can upgrade?&lt;/p&gt;

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