Building an Author Box Block with PHP-Only Block Registration

on in WordPress | Last modified on

This article is part of a series on PHP-only Gutenberg block registration. For an introduction to the feature and its background, read PHP-Only Block Registration in WordPress.


An author box is a staple of editorial sites — a card that shows a user’s avatar, display name, bio, email, and social links. Before PHP-only block registration existed, building this as a Gutenberg block required JavaScript, JSX, and a build pipeline. Now it’s pure PHP.

This block has one trick that makes it unique among simple autoRegister blocks: the user dropdown is populated dynamically at registration time. The enum array that drives the SelectControl is built from get_users(), so it always reflects the current users on the site. No JavaScript, no REST API call, no custom component — just a PHP array.


What the block does

  • Dropdown to select any user on the site (uses login names as stable identifiers)
  • Shows the user’s avatar (via get_avatar()), display name, and bio
  • Toggle controls for avatar, bio, email, and LinkedIn visibility
  • LinkedIn URL stored as user meta (linkedin_url) — the block adds a field to the WordPress user profile screen so editors can fill it in without a separate plugin
  • Wide and full alignment support

The mini-plugin

Create a folder called author-box-block in wp-content/plugins/ and add one file:

author-box-block/author-box-block.php

<?php
/**
 * Plugin Name: Author Box Block
 * Description: A PHP-only Author Box Gutenberg block.
 * Version:     1.0.0
 * Requires at least: 6.7
 * Requires Plugins: gutenberg
 */

defined( 'ABSPATH' ) || exit;

define( 'AUTHOR_BOX_VERSION', '1.0.0' );

// ---------------------------------------------------------------------------
// Block registration
// ---------------------------------------------------------------------------

add_action( 'init', 'author_box_block_register' );

function author_box_block_register(): void {
    $users = get_users(
        array(
            'number'  => -1,
            'fields'  => array( 'ID', 'user_login', 'display_name' ),
            'orderby' => 'display_name',
            'order'   => 'ASC',
        )
    );

    // Build the enum from user logins. Login is stable across DB migrations;
    // user IDs can change if you move between environments.
    $user_enum    = array_values( array_map( fn( $u ) => $u->user_login, $users ) );
    $user_default = ! empty( $user_enum ) ? $user_enum[0] : '';

    if ( empty( $user_enum ) ) {
        return; // No users yet — skip silently.
    }

    register_block_type(
        'my-plugin/author-box',
        array(
            'title'       => 'Author Box',
            'description' => 'Displays an author card with avatar, bio, email, and LinkedIn.',
            'category'    => 'text',
            'icon'        => 'admin-users',

            'attributes' => array(
                // enum is populated at init time, so it always reflects the
                // site's current user list. autoRegister renders this as a
                // SelectControl showing each login name.
                'userLogin' => array(
                    'type'    => 'string',
                    'enum'    => $user_enum,
                    'default' => $user_default,
                    'label'   => 'User',
                ),
                'showAvatar' => array(
                    'type'    => 'boolean',
                    'default' => true,
                    'label'   => 'Show Avatar',
                ),
                'showBio' => array(
                    'type'    => 'boolean',
                    'default' => true,
                    'label'   => 'Show Bio',
                ),
                'showEmail' => array(
                    'type'    => 'boolean',
                    'default' => true,
                    'label'   => 'Show Email',
                ),
                'showLinkedin' => array(
                    'type'    => 'boolean',
                    'default' => true,
                    'label'   => 'Show LinkedIn',
                ),
            ),

            'supports' => array(
                'autoRegister' => true,
                'align'        => array( 'wide', 'full' ),
            ),

            'render_callback' => 'author_box_block_render',
        )
    );
}

// ---------------------------------------------------------------------------
// Render callback
// ---------------------------------------------------------------------------

function author_box_block_render( array $attributes, string $content, WP_Block $block ): string {
    if ( empty( $attributes['userLogin'] ) ) {
        return '';
    }

    $user = get_user_by( 'login', $attributes['userLogin'] );

    if ( ! $user ) {
        return '';
    }

    $wrapper_attrs = get_block_wrapper_attributes( array( 'class' => 'author-box-block' ) );

    $avatar_html = '';
    $bio_html    = '';
    $links_html  = '';

    if ( $attributes['showAvatar'] ) {
        $avatar_html = sprintf(
            '<div class="author-box-block__avatar">%s</div>',
            get_avatar( $user->ID, 96, '', esc_attr( $user->display_name ) )
        );
    }

    if ( $attributes['showBio'] ) {
        $bio = get_user_meta( $user->ID, 'description', true );
        if ( $bio ) {
            $bio_html = sprintf(
                '<p class="author-box-block__bio">%s</p>',
                esc_html( $bio )
            );
        }
    }

    $link_items = array();

    if ( $attributes['showEmail'] && $user->user_email ) {
        $link_items[] = sprintf(
            '<a class="author-box-block__link author-box-block__link--email" href="mailto:%1$s">%2$s</a>',
            esc_attr( $user->user_email ),
            esc_html( $user->user_email )
        );
    }

    if ( $attributes['showLinkedin'] ) {
        $linkedin = get_user_meta( $user->ID, 'linkedin_url', true );
        if ( $linkedin ) {
            $link_items[] = sprintf(
                '<a class="author-box-block__link author-box-block__link--linkedin" href="%s" target="_blank" rel="noopener noreferrer">LinkedIn</a>',
                esc_url( $linkedin )
            );
        }
    }

    if ( ! empty( $link_items ) ) {
        $links_html = sprintf(
            '<div class="author-box-block__links">%s</div>',
            implode( '', $link_items )
        );
    }

    return sprintf(
        '<div %1$s>
            %2$s
            <div class="author-box-block__content">
                <h3 class="author-box-block__name">%3$s</h3>
                %4$s
                %5$s
            </div>
        </div>',
        $wrapper_attrs,
        $avatar_html,
        esc_html( $user->display_name ),
        $bio_html,
        $links_html
    );
}

// ---------------------------------------------------------------------------
// Styles
// ---------------------------------------------------------------------------

add_action( 'init', 'author_box_block_register_styles' );

function author_box_block_register_styles(): void {
    wp_enqueue_block_style(
        'my-plugin/author-box',
        array(
            'handle' => 'author-box-block-style',
            'src'    => plugin_dir_url( __FILE__ ) . 'style.css',
            'path'   => plugin_dir_path( __FILE__ ) . 'style.css',
            'ver'    => AUTHOR_BOX_VERSION,
        )
    );
}

add_action( 'enqueue_block_editor_assets', 'author_box_block_editor_styles' );

function author_box_block_editor_styles(): void {
    wp_enqueue_style(
        'author-box-block-style',
        plugin_dir_url( __FILE__ ) . 'style.css',
        array(),
        AUTHOR_BOX_VERSION
    );
}

// ---------------------------------------------------------------------------
// LinkedIn URL — user profile field
// ---------------------------------------------------------------------------

// Render the field on "Your Profile" and "Edit User" screens.
add_action( 'show_user_profile', 'author_box_linkedin_field' );
add_action( 'edit_user_profile', 'author_box_linkedin_field' );

function author_box_linkedin_field( WP_User $user ): void {
    ?>
    <h2><?php esc_html_e( 'Professional Links', 'author-box-block' ); ?></h2>
    <table class="form-table" role="presentation">
        <tr>
            <th scope="row">
                <label for="linkedin_url"><?php esc_html_e( 'LinkedIn URL', 'author-box-block' ); ?></label>
            </th>
            <td>
                <input
                    type="url"
                    name="linkedin_url"
                    id="linkedin_url"
                    value="<?php echo esc_attr( get_user_meta( $user->ID, 'linkedin_url', true ) ); ?>"
                    class="regular-text"
                    placeholder="https://www.linkedin.com/in/your-profile"
                >
            </td>
        </tr>
    </table>
    <?php
}

// Save the field when the profile form is submitted.
add_action( 'personal_options_update',  'author_box_save_linkedin_field' );
add_action( 'edit_user_profile_update', 'author_box_save_linkedin_field' );

function author_box_save_linkedin_field( int $user_id ): void {
    if ( ! current_user_can( 'edit_user', $user_id ) ) {
        return;
    }
    if ( isset( $_POST['linkedin_url'] ) ) {
        update_user_meta(
            $user_id,
            'linkedin_url',
            esc_url_raw( wp_unslash( $_POST['linkedin_url'] ) )
        );
    }
}

author-box-block/style.css

.author-box-block {
    display: flex;
    gap: 1.5rem;
    align-items: flex-start;
    padding: 1.5rem;
    border: 1px solid #e2e8f0;
    border-radius: 0.5rem;
}

.author-box-block__avatar img {
    border-radius: 50%;
    display: block;
}

.author-box-block__content {
    flex: 1;
}

.author-box-block__name {
    margin: 0 0 0.5rem;
    font-size: 1.125rem;
    font-weight: 700;
}

.author-box-block__bio {
    margin: 0 0 0.75rem;
    color: #4a5568;
    line-height: 1.6;
}

.author-box-block__links {
    display: flex;
    gap: 1rem;
    flex-wrap: wrap;
}

.author-box-block__link {
    font-size: 0.875rem;
    font-weight: 600;
    text-decoration: none;
    color: #0073aa;
}

.author-box-block__link:hover {
    text-decoration: underline;
}

Things to know

Dynamic enum for the user list

The enum array in an autoRegister attribute drives the options shown in the auto-generated SelectControl. Building it from get_users() at init time means the dropdown is always current — add a user to WordPress and they appear in the block’s sidebar immediately (after a page reload in the editor).

The trade-off: autoRegister has no way to show separate label/value pairs in the dropdown. The stored attribute value and the displayed label must be the same string. That’s why we use the user login rather than the user ID — logins are human-readable, stable across database moves, and safe to store in post content.

If you need the dropdown to show display names while storing IDs, you’d need a JS-registered block with a custom ComboboxControl. That’s a valid enhancement, but for most sites the login approach works fine.

Guarding against missing users

If the enum array is empty (no users exist yet, or a deployment issue), registration is skipped entirely. Without this guard, WordPress would throw a fatal error when the block type is registered with an empty enum.

LinkedIn as user meta

Storing the LinkedIn URL in user_meta with the key linkedin_url keeps it attached to the user record rather than to any single post. The block adds a “Professional Links” section to the profile edit screen using the show_user_profile and edit_user_profile hooks, which cover both a user editing their own profile and an admin editing another user’s profile. Both personal_options_update and edit_user_profile_update are needed to handle saves from each context respectively.


Further reading

Related Posts

Leave a Reply

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