How does WordPress Store Taxonomy Terms in the Database?

While programming my Organize Drafts WordPress Plugin, I did a lot of research on how WordPress uses taxonomies and I spent some time digging around in the WordPress core.  Here’s what I found.

Creating your own taxonomy for use in a WordPress plugin or theme isn’t very difficult, and for a primer I would suggest checking out the WordPress documentation on taxonomies.

However, I came across some confusion on the web about whether WordPress stores the taxonomies in the database.  This confusion seems to be caused by the fact that you have to call register_taxonomy on every load of the WordPress core.  If you don’t, your taxonomy won’t be recognized.

Here’s an abbreviated example of how to call register_taxonomy in a WordPress plugin that uses a class.  As described in the codex, this function needs to be called at a specific time.  When writing WordPress plugins, one of the tricky things is to always make sure your code is being called at the correct time during the WordPress loading process.  The init action hook can be used to call code that needs to run after WordPress has finished loading but before any headers have been sent.

        final class LSW_Organize_Drafts {
        public function __construct() {
        private function hooks() {
	    add_action('init', array($this, 'setup_taxonomy'));

        public function setup_taxonomy() {
	    if(!taxonomy_exists('lswdrafttype')) {
		register_taxonomy( 'lswdrafttype', $post_types, $args );

       new LSW_Organize_Drafts();

In actuality, my code is more complex, as I made the class a Singleton, and also applied filters to the arguments, but hopefully the above code will help new WordPress plugin developers see how they can go about calling register_taxonomy inside of the init hook when using OOP design principles.

So what does register_taxonomy actually DO?

Register_taxonomy actually adds your taxonomy to the global variable $wp_taxonomies.  This happens on line 461 of taxonomy.php, as shown here:

Excerpt from the WordPress core file taxonomy.php

$wp_taxonomies is a global array of registered taxonomies. Before adding your taxonomy to the array, it checks the arguments that you supplied, and merges them with default arguments.

The $args that you supply tell WordPress what to do with your custom taxonomy. You get to specify whether it’s public, hierarchical, whether it should be shown in the menu, etc.  Here’s a screenshot that shows an example of what the $args might contain:

Screenshot taken while debugging WordPress in PHPStorm — this shows what the $args contain after some processing — for what you can actually supply, view the codex 

So, it’s true that the settings for your taxonomy are NOT saved in the database.  They are generated from a PHP array that’s coded in a plugin file or a theme file.

OK, so what if I don’t register my taxonomy?

If you don’t register the taxonomy, then it won’t be visible on any WordPress pages and you won’t be able to use it. However, your taxonomy terms and relationships that you may have previously created are still in the database.

What are Taxonomy Terms?

The terms are the actual category names or tag names, for example.  Or in my case, the terms are the draft types that I’ve created.

If you’re having trouble understanding taxonomy terms and what the difference is between the taxonomy name and the term names, then here are some real-world examples:

Taxonomy: Birds

Terms: Ducks, Geese, Eagles, Chickens, Sparrows

Taxonomy: Property Zoning Types

Terms: Commercial, Residential

Taxonomy: Medication Types

Terms: Over-the-counter, Prescription, Controlled

Why should I use a custom Taxonomy instead of just categories?

Using categories for everything will get messy fast.   Let’s say that you have a real estate website with a page for a house.  You probably want to organize that house by location, sale price, number of bedrooms, etc.  You could just simply add it to multiple categories, so the house could be in the cheap category, the Seattle category, the 4 bedroom category, etc.

However, custom taxonomies make life easier!  You can query each taxonomy separately and also display objects with the same taxonomy easily.  (NOTE: In WordPress, a post is an object!  A page is an object!  A custom post type is an object!  Knowing this becomes important when you look at the database tables.) If you have the taxonomies of sale_price, number_of_bedrooms, and location, you can then easily get all houses that are in the same location or the same price range.

For example, if we have custom taxonomies of sale_price and location, we could use this code to get all cheap houses in Seattle:

$args = array(
	 'posts_per_page' => 8,
	 'orderby' => 'rand',
	 'post_type' => 'houses',
	 'sale_price' => 'cheap',
	 'location' => 'seattle'
$cheap_houses_in_seattle = get_posts( $args );

Where does WordPress store Taxonomy Terms?

The actual terms are in the terms table.  All WordPress tables have a prefix.  In my example, the prefix is linsoft_, but the default prefix is wp_ so the actual name of your table might be wp_terms.

The taxonomy names are stored in the term_taxonomy table.

If you are familiar with database design, you would probably think that there would be a ONE TO MANY relationship between the taxonomy name and the terms, but in fact these tables have a ONE TO ONE relationship.  The WordPress database is not normalized!  There is duplicate data…  See this screenshot:

Screenshot from MySQL Workbench of the WordPress Term Taxonomy Table

As you can see, the taxonomy name is repeated many times. The term_id actually references the term_id that’s in the terms table.

Here’s what’s the terms table looks like:

Screenshot from MySQL Workbench of the WordPress terms table

How does get_terms work?

If you want to get the terms associated with the taxonomy, you use the function get_terms.  To display all of them, including the empty ones, you would do this:

get_terms( 'lswdrafttype', 'hide_empty=0');

Where ‘lswdrafttype’ is the name of your taxonomy.

The get_terms function begins at line 1070 of taxonomy.php  It’s a long function and most of it deals with forming the MySQL query.

The query is formed on line 1437:

$query = "SELECT $distinct $fields FROM $wpdb->terms AS t $join WHERE $where $orderby $order $limits";


As you can see, it’s going to join tables together.

The final query will look different depending on the paramters you gave to get_terms. In my case, the final query looks like this:

SELECT t.*, tt.* FROM wp_terms
AS t INNER JOIN wp_term_taxonomy
AS tt ON
t.term_id = tt.term_id
WHERE tt.taxonomy IN ('lswdrafttype') ORDER BY ASC

What does the WordPress function taxonomy_exists do?

Taxonomy_exists does NOT check the database!  All it does is check the global array $wp_taxonomies.  If you have not called register_taxonomy, then your taxonomy will not be in the $wp_taxonomies variable, and the result will be false, even if you have terms associated with that taxonomy stored in the database!

So, do not fear, your taxonomies might still be in the database, even if taxonomy_exists returns false!

OK, so how does WordPress know which terms are associated with which posts?

Good question!  There’s another table for that, called term_relationships (remember, with your prefix before the table name, so commonly it might be called wp_term_relationships).

Screenshot from MySQL Workbench of the term_relationships WordPress table

Does this table look confusing?  Boring?  Remember how I told you that posts are objects in WordPress?  Well, this is why I mentioned it.  The object_id may be a post_id or an id of another object.  The term_taxonomy_id is the term_id found in the terms table.

You can get all of the taxonomy terms with the WordPress function wp_get_post_terms.  This function ends up called wp_get_object_terms, which creates a query that uses an inner join between the terms table and the term_taxonomy table.  See line 2401 of taxonomy.php.

In its simplest use, wp_get_post_terms can be supplied a $post_id and it will return the tags associated with it.  For example, calling this:


In my database set-up, this function ends up running this MySQL query:

SELECT t.*, tt.* FROM linsoft_terms AS t INNER JOIN linsoft_term_taxonomy AS tt ON tt.term_id = t.term_id INNER JOIN linsoft_term_relationships AS tr ON tr.term_taxonomy_id = tt.term_taxonomy_id  WHERE tt.taxonomy IN ('post_tag') AND tr.object_id IN (636) ORDER BY ASC

If you want to get your custom taxonomy terms instead of the tags, then you will want to pass wp_get_post_terms a second parameter, which is an array of taxonomy names.

For example:

wp_get_post_terms($post_id, 'my_custom_taxonomy');

I hope that this helps!   If you have any questions or clarifications, please do not hesitate to post a comment. Thank you!

WordPress Plugin Tutorial: Create a Widget that uses Post Meta Data

There are a lot of beginner tutorials for developing WordPress plugins, but most focus on creating one simple feature, for example a shortcode, and this may leave programmers new to WordPress feeling lost about how to do something a little more complicated.

What you Will Learn

  • How to create a Meta Box, which is a custom user interface that displays on the post edit page
  • How to save the values entered in the Meta Box into the wp_postmeta database table
  • How to retrieve these values and display them in a dynamic widget
  • How to create a WordPress widget by extending the WP_Widget class
  • How to use the Singleton design pattern (we will use Object Oriented Programming (OOP) principles)
  • Best practices for sanitizing inputs to prevent security concerns


  • PHP 5.3+
  • WordPress 4.4+ (This may work with earlier versions but has not been tested)
  • A general understanding of PHP and HTML

What we Will Make

In this tutorial, we are going to build a WordPress plugin that displays an Amazon link in a widget.  This Amazon link will be different for every post, as it will use the post’s custom fields to determine which link to display.

Here is what our finished product will look like:

Amazon Widget - Link with Affiliate Tag
We will create this widget, which has an input field for entering the affiliate tag that you want to use for your product link. This widget can go anywhere your theme allows widgets – typically this means the sidebar and (maybe) the footer.


WordPress Meta Box Example
We will create this meta box, which has input fields for specifying the Amazon title and the asin. The widget will use these fields to determine which product link to display.
This Plugin is simple – it displays an Amazon link. HOW it looks will depend on your theme and any custom CSS you write. This might not seem too exciting, but it allows you to specify a different product for every single post/page!  If you click on “Amazon Fire Tablet” it will send you to Amazon, using your affiliate link!

A Tutorial for Beginner WordPress Plugin Developers

This tutorial is aimed at people just beginning WordPress plugin development, to show you how to create a widget that displays dynamic content.  It’s easy to display the same content on every page — just add a text widget to your sidebar, footer, or any other widgetized area.  However, displaying custom content that varies based on which page/post you are viewing, requires a custom solution.

Note that the plugin we are going to build doesn’t interact with the Amazon Product Advertising API, which means that it doesn’t grab any database information from Amazon.  This means that it can’t display the current price or the product image.  However, you could extend this plugin to do that if you so desire.  You’ll want to use the WordPress HTTP API to retrieve the data and then you will need to parse the XML.  I have written several plugins that work with the Amazon database, and the whole process is a bit challenging, but definitely doable!

Everyone Learns Differently

This tutorial is written in a step-by-step way, where I only show small parts of the code at a time.  This is so that I can explain what they’re doing and provide relevant tips.  You can start with a blank text editor and follow along.

However, if you learn best by looking at the whole code first, you can get the completed code on Github.

Setting up our Plugin Folder and File

This is going to be a simple plugin with only one file.  We could put this file directly in the \wp-content\plugins directory, but a better idea is to create a directory for it. That allows us to easily extend the plugin in the future, by adding additional files, like CSS stylesheets or Javascript scripts or additional PHP classes.

  1. Create a directory titled lsw-amazon-in-widget
  2. In that directory, create the file lsw-amazon-widget.php

Note: Throughout this tutorial, we will preface many things with “lsw”.  When writing WordPress plugins, it’s a good idea to preface class names, function names, and setting names to decrease the chance of your plugin conflicting with another plugin.

Creating the Plugin File Header

All plugins have to have a file header at the top of their main PHP file. The file header is used by the WordPress File Header API to extract information about your plugin, like the plugin’s name.  Without this file header, your plugin won’t show up in the list of plugins in WordPress admin.   The file header is written inside of PHP comments.

Place this at the top of lsw-amazon-widget.php:

 * Plugin Name: Amazon Widget
 * Plugin URI:
 * Description: Display Amazon Product in Widget
 * Author: Linnea Wilhelm
 * Author URI:
 * Version: 1.0.0
 * License: GPL2
 * License URI:
 * Text Domain: lsw-amazon-widget
 * Tags: amazon, widget, amazon affiliate

 Creating the Outline of the Plugin

Our plugin will have two classes, which will both be in the same PHP file.  The first class, Lsw_Amazon, will deal with initialization of our plugin and it will contain the functions having to do with the meta box.  The second class, Lsw_Amazon_Widget, will extend WP_Widget and it will handle the front and back end display of the widget.

class Lsw_Amazon  {

	protected function __construct() {
		// constructor

	public static function add_custom_meta_box( $post_type, $post ) {
		//this function will be called by the add_meta_boxes hook

	public function init() {
		//this function will have all of the hooks for setting up our plugin

	public static function render_meta_box() {
		// this function will contain the html for displaying the meta box

	public static function save_meta_details( $post_id ) {
		// this function will be called by the save_post hook
		// it will save the values entered in the meta box

class Lsw_Amazon_Widget extends WP_Widget {

	function __construct() {
                // constructor
	public function widget( $args, $instance ) {
		//Front-end display of widget.

	public function form( $instance ) {
		//Back-end display of widget form.

	public function update( $new_instance, $old_instance ) {
		//Process widget options on save

	private function getAmazonUrl($asin, $affiliate_tag) {
		//a helper function


Looking over this outline, one thing you may notice is a lot of static methods (Also called “functions”).  When using WordPress hooks, the method that is called by the hook must be both public and static.


Implementing the Singleton Design Pattern

We are going to use the singleton design pattern for our Lsw_Amazon class because we only want it to be instantiated once.  If we allowed our class to be instantiated multiple times, not only could this use up more server memory, but it could cause unexpected behavior, like duplicate meta boxes.

Alter the beginning of your Lsw_Amazon class so that it looks like this:

class Lsw_Amazon  {

	public $version = '1.0.0';
	public static $text_domain = 'lsw-amazon-widget';

	protected static $_instance = null;

	protected function __construct() {

	public static function instance() {
		if ( is_null( self::$_instance ) ) {
			self::$_instance = new self();
		return self::$_instance;



Since we are using the Singleton pattern, when we want an instance of the Lsw_Amazon class, we will call Lsw_Amazon::instance() instead of calling the constructor directly with the word new. The instance method checks to see if we already have an instance, and if we do, it returns that instance.  If not, then it creates the instance with the new keyword and the constructor runs.   This means that the constructor runs only once each time WordPress runs. The constructor has the keyword protected in front of it to prevent it from being called with the new keyword.

We also added a couple of class properties, the $version and $text_domain, for our later use.  It is a good idea to save the version as a variable so that you can check it programmatically.  Sometimes, you may use this version number to run specific code when upgrading your plugin.

Let’s add the Action Hooks

For organization, all hooks and filters should go in the same place, if possible.  We’re going to put them in our init() function, which is called by the constructor.

	public function init() {
		add_action( 'add_meta_boxes', array('Lsw_Amazon', 'add_custom_meta_box'), 10, 2 );
		add_action( 'save_post',  array('Lsw_Amazon', 'save_meta_details') );
		add_action( 'widgets_init', function(){ register_widget( 'Lsw_Amazon_Widget' );});

As you can see with the above code, it’s possible to call functions that are inside of classes.  Instead of passing the function name as the second parameter to add_action, you pass an array that contains the class name and function name.

For the widgets_init hook, we are using an anonymous function.  Anonymous functions first became available in PHP with version 5.3.  If you are using an earlier version, you will have to rewrite that line to use a named function, as we are using in the first two lines.

Let’s Add the Meta Box and write the HTML to Display It

	public static function add_custom_meta_box( $post_type, $post ) {
		$post_types = apply_filters('lsw_amazon_post_types', array('post', 'page'));
		add_meta_box( 'lsw_amazon_settings_box',
                    __( 'Amazon Product Settings', self::$text_domain ), 
		    $post_types, 'normal', 'default' );

	public static function render_meta_box() {
		global $post;
		$lsw_amazon = get_post_meta($post->ID, 'lsw_amazon', true);
		$title = isset($lsw_amazon['title']) ? $lsw_amazon['title'] : '';
		$asin = isset($lsw_amazon['asin']) ? $lsw_amazon['asin'] : '';
		<label for="lsw_title">Title   </label><input class="widefat" type="text" name="lsw_title"
		     id="lsw_title" value="<?php echo esc_attr($title); ?>"> <br>
		<label for="lsw_asin">ASIN  </label><input class="widefat" type="text" name="lsw_asin"
		     id="lsw_asin" value="<?php echo esc_attr($asin); ?>">

Tip: Get your case right! $post->id will NOT work.  The correct field name is all capitalized, so it’s $post->ID.

Let’s take a moment to look at this line:

$post_types = apply_filters('lsw_amazon_post_types', array('post', 'page'));

That line is saying take the array and perform all of the filters that are registered with the add_filter hook.

In our case, we are filtering an array of post types.  The default action of our plugin is to only add the meta box to posts and pages.  However, if someone wanted to modify this and have the meta box appear on a custom post type, they could add this code snippet to their theme’s functions.php file.  (Or, they could add it to another plugin, or to a php file in the mu-plugin directory. See here: Must Use Plugins)

add_filter('lsw_amazon_post_types', 'associate_post_types_with_amazon_box');
function associate_post_types_with_amazon_box($post_types) {
   $post_types[] = 'YOUR_CUSTOM_POST_TYPE';
 return $post_types;

A few other notes about the meta box code:

  • We are using functions for internationalization. The function _() translates the text before displaying.
  • We escaped attribute values with esc_attr.  Wordpress has a variety of functions for sanitizing and validating text. (The WordPress Functions Reference is invaluable!)  It’s important to treat all user entered data and all data from the database as potentially dangerous.  User input data includes $_POST and $_SERVER and $_GET data, since those can all be changed by the user.  If you don’t sanitize or validate the data before displaying it on a webpage, your plugin is vulnerable to cross-site scripting attacks.
  • We’re using the PHP ternary operator along with the isset() function to check if our values are set, and to set them to an empty string if they are not set.  If you try to use a variable that has not been set, you will get a Notice: Undefined Variable error. This won’t stop execution of your plugin, but it may cause a very ugly error to appear.  The practice of using a ternary operator with the isset function is so common throughout PHP programming, that the latest version of PHP, PHP 7, has a new null coalescing operator which is much simpler!

Let’s Save the Values Entered in the Meta Box

	public static function save_meta_details( $post_id ) {
		if( isset($_POST['lsw_asin']) && isset($_POST['lsw_title'] )) {
			$lsw_amazon = array('asin'=>sanitize_text_field($_POST['lsw_asin']),
			update_post_meta( $post_id, 'lsw_amazon', $lsw_amazon);

A note about this code:

  • Once again, we sanitize or escape user input before using it. Remember, values from the database can contain dangerous characters and should be treated just like user input. If you’re familiar with PHP but not WordPress, you probably have used the filter_var() function. Well, sanitize_text_field is a WordPress helper function that you can use in its place when you are sanitizing a string.


Now, Let’s Create the Widget

In WordPress, widgets are created by extending the class WP_Widget. This means that there is a set of methods that they must have. Specifically, the WordPress core says that “WP_Widget::widget(), WP_Widget::update() and WP_Widget::form() need to be overridden.” This means that you implement these methods in your child class so that the child methods run instead of the parent methods. If you have followed other tutorials for creating WordPress widgets, or have had a peak at the documentation, then you won’t find any surprises here.

The Widget Constructor

	 * Register widget with WordPress.
	function __construct() {
			'lsw_amazon_widget', // Base ID
			'Amazon in Widget', // Name
			array( 'description' => 'Display an Amazon Product' )


Things to note about this code:

  • This calls the parent’s constructor method (in this case, WP_Widget::__construct())
  • The ID should be unique, and the other parameters are displayed in the Appearance-->Widgets admin area.

The Front-End Display of the Widget


	public function widget( $args, $instance ) {
		if(!is_single()) {
		global $post;
		$lsw_amazon = get_post_meta($post->ID, 'lsw_amazon', true);
		if(isset($lsw_amazon['title']) && isset($lsw_amazon['asin'])) {
			$tag = isset( $instance['tag'] ) ? $instance['tag'] : '';
			echo $args['before_widget'];
                        <h2>You Might Be Interested In...</h2>
			<a href="<?php echo $this->getAmazonUrl( $lsw_amazon['asin'], $tag ); ?>">
				<?php echo sanitize_text_field( $lsw_amazon['title'] ); ?>
			echo $args['after_widget'];

What this code is doing:

This widget method is called when WordPress tries to display your Widget in the dynamic sidebar or whatever widget area you have in your theme, on the front-end. It uses is_single() to check if this is a single page or single post. It doesn’t make sense to display this widget if there is no single post or page being displayed because this widget wouldn’t know what Amazon product to display. You could alter this to have it display a default, but that’s beyond the scope of this tutorial.

So, if is_single() is false, the function completes without doing anything.

The global $post variable holds the current WP_Post object, which has an ID field. We use this ID field as the parameter for get_post_meta, which does exactly what it sounds: it gets the post meta data from the database. In this case, we provide the name of our data as the second parameter and the third parameter, true, indicates that we only want this single value. This value is actually an array that contains the title and the ASIN.

The widget echoes the before_widget html (this is specified by the theme usually), and then echoes a link to the product, based on the ASIN and the affiliate tag. And, finally, the after_widget html is displayed.

The Back-End Display of the Widget

	public function form( $instance ) {
		// outputs the options form on admin
		$tag  = isset( $instance['tag'] ) ? $instance['tag'] : '';
		<label for="tag">Affiliate Tag:</label><input type="text"
		         name=" <?php echo $this->get_field_name( 'tag' ); ?>"
		id="<?php echo $this->get_field_id( 'tag' ); ?>" value="<?php echo esc_attr($tag); ?>"> <br>

The back-end display of the widget is a very simple form with one input field for an Affiliate Tag.

Saving the Widget Values

	public function update( $new_instance, $old_instance ) {
		// processes widget options to be saved
		$instance             = $old_instance;
		$instance['tag']     = sanitize_text_field( $new_instance['tag'] );
		return $instance;

If you don’t explicitly save the form value(s) in the update() method, then the form data won’t be saved. So, make sure you “wire it up” in this way. Once again, you see that we sanitized the input data for security reasons.

The Helper Function

	private function getAmazonUrl($asin, $affiliate_tag) {
		return "" . $asin . "?tag=" . $affiliate_tag;

Whenever possible, you should use helper functions to do small jobs. This follows the DRY programming principle: Don’t Repeat Yourself. This allows for reusable code and makes test driven development (TDD) possible. Perhaps you later decide to extend this plugin to display links in other ways. You can change how the url is generated by simply changing one function. Or maybe you want to use this plugin on your Amazon UK site and need to change it to Better yet would be to provide an option to specify the country (Amazon has numerous international sites).

The Finishing Touches


At the bottom of your plugin file, instantiate your class. If you don’t instantiate it, your plugin won’t run! Remember that we used that Singleton pattern so we don’t use the new keyword.

Ways this could be improved

This plugin is fully functioning now, but we could make it prettier and give it more options!

  • This plugin needs some CSS. Learn how to enqueue a CSS stylesheet.
  • Document your code! I omitted the PHPDoc to make the snippets smaller.

Get the complete code on Github

Here’s the finished product that we developed in this tutorial.

Troubleshooting Tips

If you run into an issue developing your WordPress plugin, there are lots of tools for troubleshooting.  One place to start is the WordPress Developer Plugin.  It offers an easy interface for installing a bunch of helpful plugins, including the debug bar and the debug bar console.   Here’s a screenshot I took while I was debugging this plugin with the Debug Bar Console:

screenshot of debug bar console, wordpress plugin for developers


More WordPress Plugin Development Guides

WordPress Edit Post Screen Hooks: A Visual Guide

There are lots of reasons that a WordPress developer might want to customize the post edit screen when developing their plugin or theme.  However, using a custom post type is probably the most common reason to customize it.  The developer might want to add additional headers, instructions, styles, javascript, or add/remove input fields.

Note that the easiest way to add custom input fields to the WordPress edit screen is to add a metabox.  However, if you need more ways to customize it, keep reading!  I will even provide some code examples.

Wordpress Admin Edit Post Screen Hooks
WordPress Admin Edit Post Screen Hooks

Reference List of WordPress Hooks that You can use to Modify the Post Edit Screen

1. in_admin_header

in_admin_header is the hook you want to use if you need to alter the heading of any of the admin screens.  It fires at the very top of the page for all admin pages, including the comments page, media page, plugins page, tools page, appearance page, etc.  It even fires on custom settings pages and custom post type pages, like the WooCommerce admin screens for products and orders.  Like all hooks, this hook is called by the WordPress function do_action.  Wordpress core uses this hook to render the WordPress admin bar.  I can see using this hook if you want to add a banner along the top of all admin screens.

2. admin_notices

admin_notices is a great hook to use for posting messages at the top of admin screens.  Wordpress core makes adding notices extremely easy!  Simple use this hook to output a div.  Wrap the div in one or more of the following classes to make your notice stand out

For examples, see the Codex page on admin_notices.

.error = This class will make your notice have a white background and red left border

.update_nag = This class will make your notice have a yellow border and move it higher on the page.

.notice and .is-dismissible = This class will make your notice dismissible!   A great option if you don’t want to annoy your users.  Wordpress core handles adding a close button and removing the notice for you.   (See this article on how to make your notices dismissible.)

3. all_admin_notices

There is not much documentation on this hook.  I looked at the code and it looks like this should fire in some cases when the admin_notices hook doesn’t fire (for example on the network admin screen for WordPress Multisite).  Take a look at WordPress Core for more details (\wp-admin\admin-header.php – around line 228)

4. edit_form_top

This is a hook for adding to the edit screen after the “Add New Post” title.  Note that if you want to change the words “Add New Post” for a custom post type, you should do that when registering a new post type. See the arguments that you can add.  The array of labels has lots of options.   That’s right, you set up those words upon registering the custom post type.  There is no need to try to filter it later.

5. edit_form_after_title

This hook is perfect for adding html to the post edit screen after the heading and above the post content box.  By the way, the rich text editor you use in WordPress is called TinyMCE, which is an open source HTML WYSIWYG editor.

6. edit_form_after_editor

This fires after the post content editor but before the excerpt, the slug, the author, and other optional/add-on meta boxes.

7. edit_form_advanced

This fires near the very bottom of the edit post screen.  It will display your custom HTML after all meta boxes.

Note: This does NOT fire on the edit page screen.  It will fire on all other post types.

8. post_submitbox_misc_actions

Want to add a checkbox, dropdown input field, or other input field or text to the publish post box?  This is the hook to use!  See the Codex for an example.

9. media_buttons

Add a button next to the add media button, for uploading files that your custom plugin or theme deals with in a specialized way.  Tutorial for using the media_buttons action hook.

How to Only Make Changes to the Edit Screen for a Custom Post Type?

In the function that you hook, you can check which screen is being displayed, and then only make the custom changes if the screen ID and screen post type are a match.  For example:

add_action('in_admin_header', 'in_admin_header_lw');
function in_admin_header_lw() {
	$screen = get_current_screen();
	if($screen->post_type=='post' && $screen->id=='post') {
		echo "This is the edit post screen.";

You actually only need to check the id and not the post_type if you only want to make the changes on one page.  But, if you want the change to appear on all pages with the same post_type, then checking the post_type property of the WP_Screen object is the way to go.  The function get_current_screen returns a WP_Screen object.  Each screen has a unique ID.

Everyone has their own workflow, but my personal preference in figuring out how PHP code works is to use a debugger and put a breakpoint at the location where I want to check the contents of variables.  When I did this in PHPStorm, while on the WooCommerce add product screen, I was able to see the contents of the WP_Screen object, therefore making it really obvious what  I should be checking for in my code.

PhpStorm Debugging WordPress WP_Screen Object
PhpStorm Debugging WordPress WP_Screen Object

How to Add Styles and Scripts to the Post Edit Screen?

Need to add javascript or CSS styles to only one admin screen?  Here’s the way to add it (this code assumes that admin-style.css is the name of your CSS file and it’s in the same directory as the plugin file).

add_action( 'admin_enqueue_scripts', 'lw_load_custom_wp_admin_style' );
function lw_load_custom_wp_admin_style() {
	$screen = get_current_screen();
	if( $screen->id=='post') {
		wp_register_style( 'lsw_custom_wp_admin_css', 
                        plugins_url( '/admin-style.css', __FILE__ ),
			false, '1.0.0' );
		wp_enqueue_style( 'lsw_custom_wp_admin_css' );

Questions, Improvements?

Are you puzzled about something?  Or did I make an error?  If so, please take a moment to comment.   Thank you!