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
- PHP-Only Block Registration in WordPress
- Make WordPress Core dev note
- Block Editor Handbook — Registration of a block