Skip to content

Conversation

@kraftbj
Copy link

@kraftbj kraftbj commented Jan 30, 2026

This patch adds UI to the Categories admin page to clearly indicate which category is the default:

  1. Shows the default category first in the list
  2. Adds a "Default" label next to the default category name

This helps users quickly identify and understand the default category behavior, as discussed in the Trac ticket.

Trac ticket: https://core.trac.wordpress.org/ticket/26268

Before:
before-categories

After:
after-categories


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

@github-actions
Copy link

github-actions bot commented Jan 30, 2026

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props kraftbj, westonruter.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions
Copy link

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • The Plugin and Theme Directories cannot be accessed within Playground.
  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@kraftbj
Copy link
Author

kraftbj commented Jan 30, 2026

Test failures appear unrelated — CI tooling tripping.

Shows the default category first in the list and adds a "Default" label
next to its name in the categories admin screen.

See https://core.trac.wordpress.org/ticket/26268
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the Terms list table in the admin to visually surface the default term, particularly for post categories, by pinning it to the top of the list and labeling it as “Default.”

Changes:

  • In display_rows_or_placeholder(), resolves the default category for the category taxonomy and renders it before the rest of the terms, while skipping it later in the non-hierarchical loop.
  • In _rows(), introduces logic to detect the default category ID and skip it during hierarchical recursion so it is not duplicated when already rendered first.
  • In column_name(), adds a “Default” label (with a taxonomy-default-label span) next to the term name when the row’s term matches the configured default term option.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 261 to 279
if ( $default_category ) {
$this->single_row( $default_category );
}

/*
* Some funky recursion to get the job done (paging & parents mainly) is contained within.
* Skip it for non-hierarchical taxonomies for performance sake.
*/
$this->_rows( $taxonomy, $this->items, $children, $offset, $number, $count );
} else {
if ( $default_category ) {
$this->single_row( $default_category );
}

foreach ( $this->items as $term ) {
if ( $default_category && $default_category->term_id === $term->term_id ) {
continue;
}
$this->single_row( $term );
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prepending the default category with $this->single_row( $default_category ); here without updating $count or otherwise integrating it into the paging logic means the "per page" limit is effectively exceeded: hierarchical lists will show number rows from _rows() plus this extra row, and in the non‑hierarchical branch the default term can appear on every page even when it is not part of the current slice of $this->items. This changes the semantics of pagination (screen options say N items per page but the table often renders N+1), and duplicates the default term across pages. If the goal is just to surface the default category at the top, consider only rendering it when $offset is 0 (first page) and/or adjusting the paging counters so the extra row is accounted for instead of being "outside" the pagination logic.

Copilot uses AI. Check for mistakes.
Comment on lines 417 to 421
$default_term = get_option( 'default_' . $taxonomy );
$default_term_label = '';
if ( $tag->term_id == $default_term ) {
$default_term_label = ' &mdash; <span class="taxonomy-default-label">' . __( 'Default' ) . '</span>';
}
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using get_option( 'default_' . $taxonomy ) here will work for core taxonomies like category and link_category, but it will not pick up default terms registered via register_taxonomy( ... 'default_term' => ... ), which are stored under default_term_{$taxonomy} (see taxonomy.php:537-557 and post.php:5333-5337). If the intent is for the "Default" label to apply to all taxonomies that support default terms (not only categories/link categories), consider mirroring the pattern in map_meta_cap() by checking both default_{$taxonomy} and default_term_{$taxonomy} so the UI stays consistent with how default terms are modeled elsewhere.

Copilot uses AI. Check for mistakes.
@westonruter
Copy link
Member

One thing seems to be missing in the user interface and that is discovery for how to change the default category. Should there be a link to the Writing Settings screen, linking to the specific field on that screen?

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@kraftbj
Copy link
Author

kraftbj commented Feb 2, 2026

Should there be a link to the Writing Settings screen, linking to the specific field on that screen?

I'm open to that. The main intent was to simply explain versus the user needing to infer why that category is acting differently in the list.

- Only display pinned default category on first page to maintain correct per-page count
- Pass default_category_id as parameter to _rows() to avoid repeated get_option() calls during recursion
- Add "Change Default" link to row actions for the default category
- Add id attribute to Writing Settings default category row for fragment linking
@kraftbj
Copy link
Author

kraftbj commented Feb 2, 2026

Trying it as a row action:
Screenshot 2026-02-02 at 12 36 40

@westonruter
Copy link
Member

Trying it as a row action:

Good idea!

I had only been thinking about doing so from the Categories list table, at /edit-tags.php?taxonomy=category, like somewhere in this paragraph below the table:

image

Would adding a link there as well be redundant? Or helpful?

@kraftbj
Copy link
Author

kraftbj commented Feb 2, 2026

Oh, I like that. I don't think it would be redundant. "The default category can be renamed or changed, but not deleted" or something like that with "renamed" going to the edit category page for that term and "changed" going to the settings page seems lightweight and direct. Going to try that. I'll leave the row action too.

Just like us, I thought about the category in the table and you thought about the text at the bottom (which I didn't think about at all), this is an area that I'd rather just help people find what they need no matter where their focus is, especially if it can be done in a relatively "invisible" way if you're thinking about other things.

Update the help text below the Categories list table to include links
for renaming the default category and choosing a different default.
@kraftbj
Copy link
Author

kraftbj commented Feb 2, 2026

Used language similar to elsewhere in Core (src/wp-admin/user-edit.php:63 - __( 'Your username cannot be changed, but you can use other fields to enter your real name or a nickname, and change which name to display on your posts.' ) in the profile help center.

Copy link
Member

@westonruter westonruter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor point, but I think the $default_category_id variable can be eliminated in favor of just $default_category. And I think it would be slightly better to use null when it isn't WP_Term (but just a personal preference).

@kraftbj kraftbj requested a review from westonruter February 3, 2026 15:56
@westonruter
Copy link
Member

This is looking really good. I asked Gemini to provide another review and I think it has some valid points:

I have reviewed the changes and identified a functional regression regarding hierarchical display, along with some code style improvements.

Review of src/wp-admin/includes/class-wp-terms-list-table.php

1. Functional Regression: Children of Default Category Hidden

In the _rows() method, the logic to skip the default category prevents its children from being processed.

			// Skip duplicating display of default category.
			if ( $default_category_id && $default_category_id === $term->term_id ) {
				continue;
			}

When continue is executed, the loop advances to the next iteration immediately. However, the recursive call to process children is located later in the loop:

			if ( isset( $children[ $term->term_id ] ) && empty( $_REQUEST['s'] ) ) {
				$this->_rows( $taxonomy, $terms, $children, $start, $per_page, $count, $term->term_id, $level + 1, $default_category_id );
			}

As a result, if the default category has children, they will never be reached or displayed in the list. The logic needs to be adjusted to ensure recursion still happens even if the parent row (the default category) is skipped for display.

2. Strict Comparison & Type Casting

In column_name(), loose comparison is used:

		$default_term       = get_option( 'default_' . $taxonomy );
		$default_term_label = '';
		if ( $tag->term_id == $default_term ) {

For consistency with WordPress coding standards and the other changes in this PR (e.g., in display_rows_or_placeholder and handle_row_actions), this should cast the option to an integer and use strict comparison:

		if ( $tag->term_id === (int) $default_term ) {

3. Documentation

The _rows() method docblock was updated to include @param int $default_category_id, but it is missing a description.

Review of src/wp-admin/edit-tags.php

4. Link Safety

The use of get_edit_term_link( $default_category_id, 'category' ) is generally safe here since $can_edit_terms is checked earlier in the file, but it's worth noting that if it were to return null (e.g., error condition), esc_url() would return an empty string, resulting in a link to the current page. Given the context (default category should always exist and be editable here), this is likely acceptable.

Summary

The changes look good overall, but the regression in handling hierarchical children needs to be addressed before merging.

Key changes to fix:

  • Ensure _rows() recurses into children even when the default category row is skipped.
  • Use strict comparison (===) and (int) casting for the default term check in column_name().

… check.

Use strict comparison with integer casting for consistency with other
default term checks in the same file. Also add a description to the
$default_category_id parameter in the _rows() method docblock.
Keep the default category in its natural position within the hierarchy
rather than pinning it to the top. This avoids complexity with child
categories appearing detached from their parent when the default category
has subcategories.

The default category is still clearly indicated via the "Default" label
and "Change Default" row action. The explanatory text below the categories
table also describes the default category behavior.
@kraftbj
Copy link
Author

kraftbj commented Feb 3, 2026

Nice find from Gemini on the children issue. If we keep the pinning and iterate through children, we'd have a few options—none great:

  1. Display children under the pinned default category at the top (but they're not special, just regular subcategories)
  2. Show the default category twice—pinned at top and in its natural spot with children
  3. Show children in their natural position without their parent visible above them (broken hierarchy display)

Rather than adding complexity to handle this, I've removed the pinning entirely. The default category now stays in its natural position in the hierarchy. The "Default" label and "Change Default" row action still clearly identify it, and the text below the table already explains the default category behavior.

I feel like we might be getting into space where if we try to be too clever, we're making things harder. That said, @westonruter—if you think an alternative approach works better here, I'm game to go with that instead.

@westonruter
Copy link
Member

@kraftbj skipping the pinning of the default category makes sense to me. Does the post list table have any facility for showing sub-posts under a post marked as sticky? I don't believe so, since sticky posts are only relevant to chronological posts not hierarchical ones (e.g. pages). I agree with your conclusion. In any case, the footer has a persistent mention of what the default category is:

image

So even when it isn't displayed on the first page, it will still be discoverable.

'<strong>' . apply_filters( 'the_category', get_cat_name( get_option( 'default_category' ) ), '', '' ) . '</strong>'
'<strong>' . apply_filters( 'the_category', get_cat_name( $default_category_id ), '', '' ) . '</strong>',
esc_url( get_edit_term_link( $default_category_id, 'category' ) ),
esc_url( admin_url( 'options-writing.php#default_category' ) )
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, if the user cannot manage_options then this URL won't be accessible. So maybe the but it can be <a href="%2$s">renamed</a> or you can choose a <a href="%3$s">different default category</a> should be conditionally printed after the previous string only if the user can do so. This also goes for whether get_edit_term_link( $default_category_id, 'category' ) returns null, indicating that the user doesn't have the ability to modify a the term. I can imagine this being a common scenario actually, where a site may want to allow Editor role users to create and modify categories, except for the default category, leaving that only for the admin to manage.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume for translations, I should either break it into two sentences or have two complete sentences as the options. I don't know but I figure a language might not like assuming a dependent clause being forced to follow the primary clause (not to mention punctuation).

On mobile but I'll update that.

I appreciate your diligence in these reviews! This is a very old issue and was a very old patch that we're pulling out of the time capsule!

This comment was marked as duplicate.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right. Having a dependent clause as a separate translation string wouldn't work. So this would be one string:

Deleting a category does not delete the posts in that category. Instead, posts that were only assigned to the deleted category are set to the default category Uncategorized. The default category cannot be deleted.

And then, if the user can manage_options and can edit the term, add another sentence:

It can be renamed or you can choose a different default category.

Or if they just have term editing capability:

It can be renamed.

And then if they just have manage_options (which would surely include the previous capability, but there's no guarantees in WordPress!):

You can choose a different default category.

Might be something to get advice from Polyglots on.

);
}

if ( 'category' === $taxonomy && (int) get_option( 'default_category' ) === $tag->term_id ) {
Copy link
Member

@westonruter westonruter Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if ( 'category' === $taxonomy && (int) get_option( 'default_category' ) === $tag->term_id ) {
if ( 'category' === $taxonomy && (int) get_option( 'default_category' ) === $tag->term_id && current_user_can( 'manage_options' ) ) {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants