Skip to main content
The plugin registers two custom post types: Artist and Album. Both implement comprehensive capability management, custom labels, and REST API integration.

Artist Post Type

The Artist post type represents musicians or bands.

Registration

Location: inc/Content/Artist.php:41
private function register(): void
{
    register_post_type(Definitions::POST_TYPE_ARTIST, [
        'description'         => '',
        'public'              => true,
        'publicly_queryable'  => true,
        'show_in_nav_menus'   => true,
        'show_in_admin_bar'   => true,
        'exclude_from_search' => false,
        'show_in_rest'        => true,
        'show_ui'             => true,
        'show_in_menu'        => true,
        'menu_position'       => 21,
        'menu_icon'           => 'dashicons-id',
        'can_export'          => true,
        'delete_with_user'    => false,
        'hierarchical'        => false,
        'has_archive'         => 'artists',
        'query_var'           => Definitions::POST_TYPE_ARTIST,
        'capability_type'     => Definitions::POST_TYPE_ARTIST,
        'map_meta_cap'        => true,
        // ... capabilities and labels
    ]);
}
Definitions::POST_TYPE_ARTIST is a constant defined as 'music_artist' in inc/Support/Definitions.php:21

Custom Capabilities

Location: inc/Content/Artist.php:64 The Artist post type uses a custom capability type to provide granular permission control:
'capabilities' => [
    // meta caps (don't assign these to roles)
    'edit_post'              => 'edit_music_artist',
    'read_post'              => 'read_music_artist',
    'delete_post'            => 'delete_music_artist',

    // primitive/meta caps
    'create_posts'           => 'create_music_artists',

    // primitive caps used outside of map_meta_cap()
    'edit_posts'             => 'edit_music_artists',
    'edit_others_posts'      => 'edit_others_music_artists',
    'publish_posts'          => 'publish_music_artists',
    'read_private_posts'     => 'read_private_music_artists',

    // primitive caps used inside of map_meta_cap()
    'read'                   => 'read',
    'delete_posts'           => 'delete_music_artists',
    'delete_private_posts'   => 'delete_private_music_artists',
    'delete_published_posts' => 'delete_published_music_artists',
    'delete_others_posts'    => 'delete_others_music_artists',
    'edit_private_posts'     => 'edit_private_music_artists',
    'edit_published_posts'   => 'edit_published_music_artists'
],
These capabilities are granted to administrators during plugin activation (see inc/Lifecycle.php:67).

URL Structure

Location: inc/Content/Artist.php:118
'rewrite' => [
    'slug'       => 'artists',
    'with_front' => false,
    'pages'      => true,
    'feeds'      => true,
    'ep_mask'    => EP_PERMALINK,
],
This creates URLs like:
  • Single artist: https://example.com/artists/the-beatles/
  • Archive: https://example.com/artists/
  • Feed: https://example.com/artists/feed/

Supported Features

Location: inc/Content/Artist.php:127
'supports' => [
    'title',
    'editor',
    'excerpt',
    'author',
    'custom-fields',
    'thumbnail'
]

Custom Admin Messages

The Artist class customizes WordPress admin messages for better UX.

Title Placeholder

Location: inc/Content/Artist.php:141
private function enterTitleHere(string $title, WP_Post $post): string
{
    return Definitions::POST_TYPE_ARTIST === $post->post_type
        ? esc_html__('Enter artist title', 'bifrost-music')
        : $title;
}

Post Update Messages

Location: inc/Content/Artist.php:167
private function postUpdatedMessages(array $messages): array
{
    global $post, $post_ID;

    $artist_type = Definitions::POST_TYPE_ARTIST;

    if ($artist_type !== $post->post_type) {
        return $messages;
    }

    // Get permalink and preview URLs.
    $permalink   = get_permalink($post_ID);
    $preview_url = get_preview_post_link($post);

    // Post updated messages.
    $messages[$artist_type] = array(
        1 => esc_html__('Artist updated.', 'bifrost-music') . $view_link,
        6 => esc_html__('Artist published.', 'bifrost-music') . $view_link,
        // ... more messages
    );

    return $messages;
}

Album Post Type

The Album post type represents music albums and can be associated with an Artist parent.

Registration

Location: inc/Content/Album.php:49 The Album registration is similar to Artist but includes additional features:
private function register(): void
{
    register_post_type(Definitions::POST_TYPE_ALBUM, [
        'description'         => '',
        'public'              => true,
        'show_in_rest'        => true,
        'show_ui'             => true,
        'menu_position'       => 22,
        'menu_icon'           => 'dashicons-playlist-audio',
        'has_archive'         => 'albums',
        'hierarchical'        => false,
        // ... rest of configuration
    ]);
}
hierarchical is set to false because we’re using the parent-child relationship without the hierarchical UI.

REST API Parent Field

Since the Album post type is non-hierarchical, the parent field isn’t exposed in REST by default. We register it manually: Location: inc/Content/Album.php:150
private function restRegister(): void
{
    register_rest_field(Definitions::POST_TYPE_ALBUM, 'parent', [
        'get_callback'    => fn($post) => (int) $post['parent'],
        'update_callback' => fn($value, $post) => wp_update_post([
            'ID'          => $post->ID,
            'post_parent' => (int) $value
        ]),
        'schema' => [
            'description' => __('Parent Artist ID', 'bifrost-music'),
            'type'        => 'integer',
            'context'     => ['view', 'edit']
        ]
    ]);
}
This allows the block editor to:
  • Read the current parent artist ID
  • Update the parent artist via REST API

Admin List Columns

The Album post type adds a custom “Artist” column to the admin list table.

Adding the Column

Location: inc/Content/Album.php:237
private function addParentColumn(array $columns): array
{
    $new_columns = [];

    foreach ($columns as $key => $value) {
        $new_columns[$key] = $value;

        // Add parent column after title
        if ('title' === $key) {
            $new_columns['parent_artist'] = __( 'Artist', 'bifrost-music' );
        }
    }

    return $new_columns;
}

Displaying Column Content

Location: inc/Content/Album.php:256
private function displayParentColumn(string $column, int $post_id): void
{
    if ('parent_artist' === $column) {
        $parent_id = wp_get_post_parent_id($post_id);

        if ($parent_id && $parent = get_post($parent_id)) {

            if (current_user_can('edit_post', $parent_id)) {
                printf(
                    '<a href="%s">%s</a>',
                    esc_url(get_edit_post_link($parent_id)),
                    esc_html($parent->post_title)
                );
                return;
            }

            echo esc_html($parent->post_title);
            return;
        }

        echo '&ndash;';
    }
}

Making it Sortable

Location: inc/Content/Album.php:283
private function sortParentColumn(array $columns): array
{
    $columns['parent_artist'] = 'parent';
    return $columns;
}

Capability Management

Capabilities are added during plugin activation and removed during uninstall.

Activation

Location: inc/Lifecycle.php:45
public static function activate(): void
{
    if ($role = get_role('administrator')) {
        // Artist caps.
        $role->add_cap('create_music_artists');
        $role->add_cap('edit_music_artists');
        $role->add_cap('edit_others_music_artists');
        $role->add_cap('publish_music_artists');
        // ... more capabilities

        // Album caps.
        $role->add_cap('create_music_albums');
        $role->add_cap('edit_music_albums');
        // ... more capabilities
    }
}

Uninstall

Location: inc/Lifecycle.php:93
public static function uninstall(): void
{
    if ($role = get_role('administrator')) {
        // Remove Artist caps
        $role->remove_cap('create_music_artists');
        $role->remove_cap('edit_music_artists');
        // ... remove all capabilities
    }
}
Capabilities are not removed on deactivation, only on uninstall. This prevents data loss if the plugin is temporarily deactivated.

Service Registration

Both post types are registered as singletons in the service container: Location: inc/Content/ContentServiceProvider.php:24
public function register(): void
{
    $this->container->singleton(Album::class);
    $this->container->singleton(Artist::class);
    $this->container->singleton(Genre::class);
}

public function boot(): void
{
    $this->container->get(Album::class)->boot();
    $this->container->get(Artist::class)->boot();
    $this->container->get(Genre::class)->boot();
}

Key Takeaways

Custom Capabilities

Use capability_type and custom capabilities for fine-grained permission control

REST API Integration

Register custom fields with register_rest_field() for non-hierarchical parent relationships

Admin UX

Enhance the admin experience with custom messages, columns, and title placeholders

Clean Uninstall

Remove all capabilities during uninstall to leave no traces

Next Steps

Taxonomies

Learn how the Genre taxonomy is implemented

Block Editor Integration

See how the parent artist selector works in the block editor

Build docs developers (and LLMs) love