How to Migrate a Widget to a Custom Post Type

Recently, one of my favorite themes, Zerif Lite, was suspended from the WordPress repo. As I was reading about the issues involved, it got me thinking: Just how hard is it to migrate a widget to a Custom Post Type? According to ThemeIsle, this was one of the factors which led to their decision, arguing that “changing this in an existing theme means that whoever’s currently using it will get their site messed up.” Let me be clear: I refuse to take sides here, as there are valid points from both parties, so let’s leave the drama somewhere else. For now, let’s move forward and do something productive. Let’s migrate those old widgets.

Understanding How It Works

First and foremost, to figure this type of thing out, you absolutely must understand (at least very basically) the WordPress Template Hierarchy. Looking at the theme files, there’s the front-page.php file, which we know from looking at the hierarchy; it’s one of the first pages to load when viewing the homepage. We also know by looking at the customizer, for which Zerif was so popular, one of the widget sections is labeled “Our Focus.”

When we look through the front-page.php file, we immediately see the following code, around line 194:


        get_template_part( 'sections/our_focus' );

Luckily for us, the developer made the names quite logical. This snippet uses get_template_part() which is a core WordPress method (if you didn’t already know). This method literally includes a file, if it’s available, relative to the current file location. So, since we’re in the theme root, we now need to open up the sections/our_focus.php file.

Well, we know this section in the customizer uses widgets to create these sections, but we also know widgets are typically called by dynamic_sidebar(). Guess what. That’s just what we have around line 30:

                if ( is_active_sidebar( 'sidebar-ourfocus' ) ) :
                    dynamic_sidebar( 'sidebar-ourfocus' );

Picking It Apart

To understand how the widget orders are created, we first need to understand how dynamic_sidebar() ensures widgets are in order, and where the data for each widget is stored. You can view this function on trac for more context.

Well, we’re going to have to create a plugin or something similar to get this going, so let’s start with a simple singleton class:

 * Plugin Name:     Widget to CPT
 * Plugin URI:
 * Description:     A simple how-to for migrating widgets to a CPT
 * Author:          JayWood
 * Author URI:
 * Version:         0.1.0

class JW_Widget_to_CPT {

     * Instance of JW_Widget_to_CPT
     * @var JW_Widget_to_CPT
    public static $instance = null;

    public static function init() {
        if ( null == self::$instance ) {
            self::$instance = new self();
        return self::$instance;

    public function hooks() {
        // All hooks here.

function jw_widget_to_cpt() {
    return JW_Widget_to_CPT::init();
add_action( 'plugins_loaded', array( jw_widget_to_cpt(), 'hooks' ) );

Looking through the dynamic sidebar method, we first see two globals: $wp-registered_sidebars and $wp_registered_widgets respectively.

So, how do we figure out what they show?

I wrote a post awhile back about debugging WordPress with tips and snippets. You may want to check that out.

The example below shows you how to do this:

    public function hooks() {
        // All hooks here.
        add_action( 'init', array( $this, 'kill_it' ) );

    public function kill_it() {
        global $wp_registered_sidebars, $wp_registered_widgets;

        error_log( print_r( compact( 'wp_registered_sidebars', 'wp_registered_widgets' ), 1 ) );

This will produce two arrays in your debug log. Sidebar: You are using logging, aren’t you? If not, that’s covered in my aforementioned article, as well. Go read it!

The first array is all sidebars that are registered it looks something like so:

[16-Sep-2016 02:30:47 UTC] Array
    [wp_registered_sidebars] => Array
            [sidebar-ourfocus] => Array
                    [name] => Our focus section widgets
                    [id] => sidebar-ourfocus
                    [description] =>
                    [class] =>
                    [before_widget] =>
                    [after_widget] =>
                    [before_title] => <h2 class="widgettitle">
                    [after_title] => </h2>


The second array is a list of registered widgets, which looks like so:

[wp_registered_widgets] => Array
            [ctup-ads-widget-1] => Array
                    [name] => Zerif - Our focus widget
                    [id] => ctup-ads-widget-1
                    [callback] => Array
                            [0] => zerif_ourfocus Object
                                    [id_base] => ctup-ads-widget
                                    [name] => Zerif - Our focus widget
                                    [widget_options] => Array
                                            [classname] => widget_ctup-ads-widget
                                            [customize_selective_refresh] =>

                                    [control_options] => Array
                                            [id_base] => ctup-ads-widget

                                    [number] => 4
                                    [id] => ctup-ads-widget-4
                                    [updated] =>
                                    [option_name] => widget_ctup-ads-widget

                            [1] => display_callback

                    [params] => Array
                            [0] => Array
                                    [number] => 1


                    [classname] => widget_ctup-ads-widget
                    [customize_selective_refresh] =>

Now, just one more thing. We need to figure out exactly how these widgets are ordered within each sidebar. Well, luckily there’s a function for that; it’s called wp_get_sidebars_widgets(), and the output from this method on a default install is like so:

[16-Sep-2016 02:59:09 UTC] Array
    [wp_inactive_widgets] => Array

    [sidebar-1] => Array
            [0] => search-2
            [1] => recent-posts-2
            [2] => recent-comments-2
            [3] => archives-2
            [4] => categories-2
            [5] => meta-2

    [sidebar-aboutus] =>
    [zerif-sidebar-footer] =>
    [zerif-sidebar-footer-2] =>
    [zerif-sidebar-footer-3] =>
    [sidebar-ourfocus] => Array
            [0] => ctup-ads-widget-1
            [1] => ctup-ads-widget-2
            [2] => ctup-ads-widget-3
            [3] => ctup-ads-widget-4

    [sidebar-testimonials] => Array
            [0] => zerif_testim-widget-1
            [1] => zerif_testim-widget-2
            [2] => zerif_testim-widget-3

    [sidebar-ourteam] => Array
            [0] => zerif_team-widget-1
            [1] => zerif_team-widget-2
            [2] => zerif_team-widget-3
            [3] => zerif_team-widget-4


Okay, So Now What?

Now comes the fun!

We now know what all the data arrays look like, so we now have some context as to what dynamic_sidebar() is actually doing. Time to dive in!

Figuring out the type of data you’re expecting is probably the most important aspect to reverse engineering how something works. Keep in mind, we’re looking for all widgets in the sidebar-ourfocus dynamic sidebar location.

Digging deeper into the dynamic_sidebar() method, we can skip over a few checks and right down to the foreach loop it is running. We now see that it’s comparing registered widgets to the widget order array we pulled; if for some reason the widget is no longer registered, it just will not display:

        if ( !isset($wp_registered_widgets[$id]) ) continue;

By this point, we know the ID of the widgets under the “Our Focus” section are ‘ctup-ads-widget-X,’ where X is like an index (not to be confused with array keys). So, now we have the IDs, and know that the dynamic sidebar now renders the output of the widget. We have to dive into widgets and see just exactly how the widget data is saved.

Looking around the Zerif Lite theme, you’ll find the “Our Focus” was located in the main functions file under the class name of zerif_ourfocus, which extends the main WP_Widget class. Simple enough, but take note of the update method and its instance keys:

  • Text ( text )
  • Title ( title )
  • Link ( link )
  • Image ( image_uri )
  • Custom Media ID ( custom_media_id )

Now, at this point, I took a shortcut.

WordPress tutorials, WordPress how-to, WordPress education, learn WordPress, Zerif Lite theme, #WPdrama, WordPress drama, Zerif LIte banned, WordPress banned Zerif Lite, ThemeIsle banned Zerif Lite, migrate a widget to a custom post type, how to work with custom post types

From my history with WordPress, I know that widget options are stored in the options table, so again, putting two and two together, I figured I could run a query on the database to find exactly what the option key was. Knowing that widget names are used in the option names, here’s my query:

select * from wp_options where option_name like "%ctup%";

Which gave me this amazingly jumbled mess…but still, it’s serialized data, and still useful if you utilize a tool like If you didn’t know that existed, it’s super handy (and you can thank Brad Parbs for that little gem!).

Where did I get that ‘ctup’ in my SQL query? That’s part of the widget ID which you can see in the functions file. Granted, if there were more widgets which used a similar string, I’d have a harder time finding the options, but that’s not the case here.

Finally a Resolution…

So, what do we know so far?

  • We know how the IDs are associated across multiple arrays.
  • We also now know what our options look like for each individual widget.

“What’s left?” you ask. Well, the actual migration, of course. We need to get the data from point A to point B.

We first need to get the the list of widgets in our sidebar. We know the sidebar is labeled sidebar-ourfocus, so let’s grab that from the widgets array we previously created. We’re also going to go ahead and grab the options for our widget ID.

We also want to make sure we set up an option to check, to avoid migrating this stuff more than once. So, let’s create a simple migrate method, but don’t hook into it just yet.

    public function migrate() {

        if ( get_option( 'zl_has_imported', false ) ) {

        $widgets        = wp_get_sidebars_widgets();
        $widget_set     = $widgets['sidebar-ourfocus'];
        $widget_options = get_option( 'widget_ctup-ads-widget' );


Now possibly the simplest part of all, the loop to migrate the widgets to the CPT. First off here, we need to loop over all widgets in the sidebar-ourfocus array we just grabbed. Let’s look at the full loop, and I’ll explain:

        foreach ( $widget_set as $index => $widget ) {

            // Grab the ID of the widget
            $widget_array = explode( '-', $widget );

             * Now we grab the number ( key ) from the widget
             * So ctup-ads-widget-1 for example will give us just the number 1
            $widget_key = end( $widget_array );

             * The above grabbed key is associated with the array keys in the
             * widget options, so we use that one here.
            $widget_data = $widget_options[ $widget_key ];

             * Now that we have all our widget data
             * we build up the insertion arguments
             * for wp_insert_post()
            $insert_args = array(
                'post_type'    => 'focus',
                'post_status'  => 'publish',
                'menu_order'   => $index,
                'post_title'   => $widget_data['title'],
                'post_content' => $widget_data['text'],
                'meta_input'   => array(
                    'link'      => $widget_data['link'],
                    'image_uri' => $widget_data['image_uri']

            wp_insert_post( $insert_args );

        update_option( 'zl_has_imported', true );

At first, we do a little trickery to grab the widget IDs, which correspond to the keys in the widget options data we grabbed earlier. There’s a nifty function in PHP to grab the end of an array, who would’ve guess it was called end()? This little gem sets the internal pointer of an array to the last element in the array, and returns it; a bit easier than other ways of grabbing last array items, I’d say.

Finally, the insertion arguments are pretty straightforward, dare I say. We’re storing the link and image_uri fields as custom post data. But notice the menu_order argument. This is because we want to retain the order in which the widgets were originally displayed.

Finally, after the loop, our posts should be imported; let’s update that option with true, so we don’t accidentally import the widgets again.

But, Wait! That’s an Invalid Post Type.

Yup, you are correct. Registering post types is the easiest thing to do, so I admittedly just copy and pasted from the codex, and changed a few things. Since we’re going to be here, we can also go ahead and hook into our migrate method.

Here’s your snippet for that, including the hooks method, which you obviously need:

    public function hooks() {
        // All hooks here.
        add_action( 'init', array( $this, 'migrate' ) );
        add_action( 'init', array( $this, 'register_post_type' ) );

    public function register_post_type() {
        $labels = array(
            'name'               => _x( 'Focus', 'post type general name', 'your-plugin-textdomain' ),
            'singular_name'      => _x( 'Focus', 'post type singular name', 'your-plugin-textdomain' ),
            'menu_name'          => _x( 'Focus', 'admin menu', 'your-plugin-textdomain' ),
            'name_admin_bar'     => _x( 'Focus', 'add new on admin bar', 'your-plugin-textdomain' ),
            'add_new'            => _x( 'Add New', 'book', 'your-plugin-textdomain' ),
            'add_new_item'       => __( 'Add New Focus', 'your-plugin-textdomain' ),
            'new_item'           => __( 'New Focus', 'your-plugin-textdomain' ),
            'edit_item'          => __( 'Edit Focus', 'your-plugin-textdomain' ),
            'view_item'          => __( 'View Focus', 'your-plugin-textdomain' ),
            'all_items'          => __( 'All Focus', 'your-plugin-textdomain' ),
            'search_items'       => __( 'Search Focus', 'your-plugin-textdomain' ),
            'parent_item_colon'  => __( 'Parent Focus:', 'your-plugin-textdomain' ),
            'not_found'          => __( 'No focus found.', 'your-plugin-textdomain' ),
            'not_found_in_trash' => __( 'No focus found in Trash.', 'your-plugin-textdomain' )

        $args = array(
            'labels'             => $labels,
            'description'        => __( 'Description.', 'your-plugin-textdomain' ),
            'public'             => true,
            'publicly_queryable' => true,
            'show_ui'            => true,
            'show_in_menu'       => true,
            'query_var'          => true,
            'rewrite'            => array( 'slug' => 'focus' ),
            'capability_type'    => 'post',
            'has_archive'        => true,
            'hierarchical'       => true,
            'menu_position'      => null,
            'supports'           => array( 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'comments', 'page-attributes' )

        register_post_type( 'focus', $args );


Migrating widgets to a CPT (while a really weird request), can be done. This article was written as a proof of concept on how it can be handled. Now, admittedly, for migrations I use WP-CLI, and hardly ever do migrations on init. In fact, if you don’t know about WP-CLI already, you should really check it out. Overall, this was a quick and dirty example of how to handle a widget to CPT migration, purely because I like to solve puzzles, and I like showing you how to do the same.

Overall, I hope this was helpful and insightful into the beast that is WordPress. As a developer, there is always more than one way to do something, so take this article with a grain of salt and modify as you see fit.

Get the full source code for this plugin.


Have a comment?

Your email address will not be published. Required fields are marked *

accessibilityadminaggregationanchorarrow-rightattach-iconbackupsblogbookmarksbuddypresscachingcalendarcaret-downcartunifiedcouponcrediblecredit-cardcustommigrationdesigndevecomfriendsgallerygoodgroupsgrowthhostingideasinternationalizationiphoneloyaltymailmaphealthmessagingArtboard 1migrationsmultiple-sourcesmultisitenewsnotificationsperformancephonepluginprofilesresearcharrowscalablescrapingsecuresecureseosharearrowarrowsourcestreamsupporttwitchunifiedupdatesvaultwebsitewordpress