Skip to content

feat(tags): add private tags feature#4507

Draft
wil-gerken wants to merge 21 commits intotrunkfrom
feature/hidden-tags
Draft

feat(tags): add private tags feature#4507
wil-gerken wants to merge 21 commits intotrunkfrom
feature/hidden-tags

Conversation

@wil-gerken
Copy link
Contributor

@wil-gerken wil-gerken commented Feb 23, 2026

All Submissions:

Changes proposed in this Pull Request:

Productizes the private tag concept from Texas Tribune's private plugin into core newspack-plugin.

Carried over from Texas Tribune:

  • "Private tag" checkbox on Add/Edit Tag forms
  • (private) label in the admin tag editor and Gutenberg sidebar
  • Private tags removed from post tag link lists
  • Private tags excluded from tag cloud widgets
  • Tag archive pages return 404
  • Tag RSS feeds return 404

New in this implementation:

  • Private tag slugs stripped from post_class / body_class HTML attributes
  • Private tags filtered out of GAM ad targeting data
  • Private tags excluded from Yoast SEO JSON-LD structured data
  • Private tags excluded from Yoast XML sitemap
  • "Private" column in the Tags list table with Screen Options toggle
  • Quick Edit support with pre-populated checkbox

Relates to NPPD-780

How to test the changes in this Pull Request:

Admin UI:

  1. Go to Posts → Tags. Create a tag named Bauhaus (or a name of your choice).
  2. Check "Private tag" and save.
  3. Confirm the tag now reads Bauhaus (private) in the Tags list.
  4. Confirm the "Private" column shows a ✓ next to Bauhaus.
  5. Click Screen Options — confirm "Private" is a toggleable column.
  6. Click Quick Edit on Bauhaus. Confirm the "Private tag" checkbox appears pre-checked.

Frontend:

  1. Open a post and add the Bauhaus tag.
  2. Confirm the tag reads Bauhaus (private) in the Tags panel.
  3. View the post to confirm the tag link does not appear.
  4. View the source on a tagged post — confirm bauhaus does not appear in body/article class attributes, the JSON-LD block, or common_targeting.
  5. Visit /tag/bauhaus/ — confirm 404.
  6. Visit /?feed=rss2&tag=bauhaus — confirm 404.
  7. Visit /post_tag-sitemap.xml — confirm no entry for /tag/bauhaus/.

Other information:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your changes, as applicable?
  • Have you successfully ran tests with your changes locally?

Copilot AI review requested due to automatic review settings February 23, 2026 21:40
@wil-gerken wil-gerken requested a review from a team as a code owner February 23, 2026 21:40
@wil-gerken wil-gerken marked this pull request as draft February 23, 2026 21:40
@wil-gerken wil-gerken removed the request for review from a team February 23, 2026 21:41
Copy link
Contributor

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

Adds a “Hidden Tags” capability to Newspack so editors can mark post_tag terms as hidden and have them suppressed across multiple public-facing surfaces and integrations, while remaining available for internal organization in wp-admin.

Changes:

  • Introduces Newspack\Hidden_Tags to manage hidden-tag term meta, admin UI (Add/Edit/Quick Edit + list table column), and labeling.
  • Filters frontend/integration outputs to suppress hidden tags (term links, tag cloud, archives/feeds, CSS classes, ad targeting, Yoast schema/sitemaps).
  • Loads the new tags module from the main plugin include list.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 8 comments.

File Description
includes/tags/class-hidden-tags.php Implements Hidden Tags behavior, admin UI, and frontend/integration filtering.
includes/class-newspack.php Registers the new Hidden Tags module for loading.

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

@wil-gerken
Copy link
Contributor Author

If this surfaces for review. Please hold off. I'm going to work through all of the Copilot suggestions and catches. Thank you!

Fixed two issues: one where programmatic saves could accidentally clear
hidden status, and one where the tag filter was running in wp-admin,
plus improvements around internationalization, accessibility, and query
performance.
Copy link
Contributor

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.


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

Copy link
Contributor

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.


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

@wil-gerken wil-gerken changed the title feat(tags): add Hidden Tags feature feat(tags): add private tags feature Feb 27, 2026
@wil-gerken wil-gerken requested a review from Copilot February 27, 2026 23:06
Copy link
Contributor

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.


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

Copy link
Contributor

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.


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

Copy link
Contributor

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.


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

@wil-gerken wil-gerken requested a review from Copilot February 28, 2026 00:36
Copy link
Contributor

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.


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

Copy link
Contributor

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.


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

Copy link
Contributor

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.


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

Copy link
Contributor

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.


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

Copy link
Contributor

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.


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

Copy link
Contributor

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.


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

Copy link
Contributor

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.


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

Comment on lines +903 to +915
// Build the removal list from only this post's private tags.
$names_to_remove = [];
foreach ( $all_tags as $tag ) {
if ( in_array( $tag->term_id, $private_ids, true ) ) {
$names_to_remove[] = $tag->name;
}
}

if ( empty( $names_to_remove ) ) {
return $data;
}

// array_diff removes private tag names; array_values re-indexes the result into a sequential array.
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

filter_yoast_schema_article() builds $names_to_remove from private tag IDs, but then removes from $data['keywords'] by name via array_diff(). If a post has both a public and private tag with the same name (WP allows duplicate tag names with different slugs), this will still remove the keyword even though a public tag exists, contradicting the comment about avoiding collisions. Consider only removing a name if all tags on the post with that name are private (e.g., build a map of name => has_public) or switch to a Yoast data structure that uses term IDs/slugs if available.

Suggested change
// Build the removal list from only this post's private tags.
$names_to_remove = [];
foreach ( $all_tags as $tag ) {
if ( in_array( $tag->term_id, $private_ids, true ) ) {
$names_to_remove[] = $tag->name;
}
}
if ( empty( $names_to_remove ) ) {
return $data;
}
// array_diff removes private tag names; array_values re-indexes the result into a sequential array.
// Build the removal list from only this post's private tags, but only
// remove a name if *all* tags on this post with that name are private.
// This avoids removing a keyword when a public tag shares a name with a private tag.
$tag_name_visibility = [];
foreach ( $all_tags as $tag ) {
$name = $tag->name;
if ( ! isset( $tag_name_visibility[ $name ] ) ) {
$tag_name_visibility[ $name ] = [
'has_private' => false,
'has_public' => false,
];
}
if ( in_array( $tag->term_id, $private_ids, true ) ) {
$tag_name_visibility[ $name ]['has_private'] = true;
} else {
$tag_name_visibility[ $name ]['has_public'] = true;
}
}
$names_to_remove = [];
foreach ( $tag_name_visibility as $name => $flags ) {
if ( $flags['has_private'] && ! $flags['has_public'] ) {
$names_to_remove[] = $name;
}
}
if ( empty( $names_to_remove ) ) {
return $data;
}
// array_diff removes private-only tag names; array_values re-indexes the result into a sequential array.

Copilot uses AI. Check for mistakes.
Comment on lines +485 to +493
$label = self::get_private_label();
if (
isset( $response->data['name'] ) &&
is_string( $response->data['name'] ) &&
self::is_term_private( $term ) &&
substr( $response->data['name'], -strlen( $label ) ) !== $label
) {
$response->data['name'] .= $label;
}
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

In append_private_label_to_rest(), the private check uses self::is_term_private( $term ), which calls get_term_meta() per term. rest_prepare_post_tag can run for large tag collections in Gutenberg, so this becomes an N+1 meta lookup. Consider using the cached ID list (self::get_private_tag_ids()) like append_private_label_to_name() does, and checking in_array( $term->term_id, $private_ids, true ) to avoid per-term meta calls.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants