Add Categories and Tags to Pages in WordPress

By default WordPress categories and tags only work for posts. Pages have no Categories or Tags panel in the editor, and the category and tag archives only list posts.

This snippet adds categories and tags to WordPress pages and shows those pages in the category and tag archives, mixed with your posts.

The snippet

<?php
/**
 * Categories and tags for pages, and pages in category and tag archives.
 */
add_action( 'init', 'dpc_page_categories_tags_register' );

function dpc_page_categories_tags_register() {
	register_taxonomy_for_object_type( 'category', 'page' );
	register_taxonomy_for_object_type( 'post_tag', 'page' );
}

add_action( 'pre_get_posts', 'dpc_page_categories_tags_archives' );

function dpc_page_categories_tags_archives( $query ) {
	if ( is_admin() || ! $query->is_main_query() ) {
		return;
	}
	if ( ! $query->is_category() && ! $query->is_tag() ) {
		return;
	}

	$post_types = $query->get( 'post_type' );
	if ( 'any' === $post_types ) {
		return;
	}
	$post_types = empty( $post_types ) ? array( 'post' ) : (array) $post_types;
	if ( ! in_array( 'page', $post_types, true ) ) {
		$post_types[] = 'page';
	}
	$query->set( 'post_type', $post_types );
}

How it works

  • register_taxonomy_for_object_type() attaches the built-in category and post_tag taxonomies to the page post type on init. The Categories and Tags panels then appear in the page editor, and the columns appear in Pages → All Pages.
  • Attaching the taxonomy is not enough for the archives: a category or tag archive queries only posts. The pre_get_posts callback adds page to the post types of the main query on category and tag archives.
  • If the archive query already has post types (another plugin added a custom post type, for example) the snippet adds page to them instead of replacing them. A query for any post type is left as it is.
  • It skips wp-admin and every secondary query, so widgets, blocks and related-post lists that query a category keep showing posts only.

Add it with Scripts Organizer

In Scripts Organizer go to Code Blocks → Add New and give the block a name. Set Trigger location to Everywhere, tick PHP under Script location, paste the code into the PHP editor and publish. Leave Action/Hook empty. Both hooks fire after Scripts Organizer has loaded the code.

The same code also works unchanged in a small plugin or an mu-plugin.

Customise it

  • Only categories: remove the post_tag line and the is_tag() check.
  • A custom post type instead of pages: replace 'page' with your post type slug in both functions.
  • Remove the pre_get_posts part if you only want to group pages in the editor and keep the archives for posts.

Gotchas

  • A category or tag with only pages in it still counts them in the term count, so it shows up in category lists and widgets.
  • Your theme’s archive template may show a date or author for each item. Pages have both, but your design may not expect them in a post list.
  • A Query Loop block set to inherit the query from the template shows the pages too. A Query Loop with its own settings does not.

In the Scripts Organizer library Import it with one click from Scripts Organizer.

// newsletter

Three tested WordPress snippets a week

Blocks, security, performance, Tailwind, PHP and React. Copy, paste, ship. You’ll also get DPlugins product updates. No spam, and you can unsubscribe anytime.

// newsletter

Three tested WordPress snippets a week

Blocks, security, performance, Tailwind, PHP and React. Copy, paste, ship. You'll also get DPlugins product updates. No spam, and you can unsubscribe anytime.