Skip to content
ExponaBLOG
WORDPRESS DEVELOPMENT & PLUGINS · DAY 088 / 100

Shortcodes: still useful — practical tips and trade-offs

When shortcodes still make sense.

Expona Admin8 min read

Shortcodes: still useful — practical tips and trade-offs

If you've built WordPress plugins or themes in the last decade, you've probably asked yourself whether shortcodes are worth the effort. The honest answer: they still have a place, but only if you understand when and how to use them responsibly. We'll walk through practical scenarios where shortcodes make sense, how to keep them maintainable, and why the block editor hasn't made them obsolete—just more specialized.

The state of shortcodes in modern WordPress

Shortcodes have been part of WordPress since version 2.5. They're simple: a user writes [my_plugin param="value"] in the editor, and your PHP function processes it into HTML. But the block editor (Gutenberg) changed the landscape. Blocks are more powerful, offer better UX for content creators, and integrate seamlessly with the editor's toolbar.

That doesn't mean shortcodes are dead. They're useful when:

  • You need quick embeds in classic editor environments or legacy sites
  • Your team or clients still prefer writing content in plain text with markers
  • You're maintaining backward compatibility with existing content
  • The feature is genuinely simple and doesn't need visual configuration
  • You're supporting migration scenarios where content predates blocks

The key is knowing the trade-offs and implementing them properly.

add_shortcode for simple embeds

Let's start with the simplest, most legitimate use case: a straightforward embed or content injection that doesn't need complex UI.

Imagine you're building a plugin that displays a team member profile card. A content editor should be able to drop a shortcode into a post, and it renders the profile. Here's a clean implementation:

php
function register_team_shortcode() {
    add_shortcode( 'team_member', 'render_team_member' );
}
add_action( 'init', 'register_team_shortcode' );

function render_team_member( $atts, $content, $tag ) {
    $atts = shortcode_atts(
        array(
            'id'    => 0,
            'style' => 'card', // card, mini, full
        ),
        $atts,
        $tag
    );

    $member_id = absint( $atts['id'] );
    if ( ! $member_id ) {
        return '';
    }

    // Fetch member data (custom post type or custom table)
    $member = get_post( $member_id );
    if ( ! $member || 'team_member' !== $member->post_type ) {
        return '';
    }

    // Sanitize style option
    $style = sanitize_key( $atts['style'] );
    if ( ! in_array( $style, array( 'card', 'mini', 'full' ), true ) ) {
        $style = 'card';
    }

    ob_start();
    ?>
    <div>
        <div>
            
        </div>
        <div>
            <h3>post_title ); ?></h3>
            <p>
                
            </p>
        </div>
    </div>
    

This works. A non-technical editor can write [team_member id="42" style="mini"] in the classic editor and get a rendered profile. No UI builder needed, no JavaScript overhead.

The advantage here is simplicity and speed. The disadvantage is that the editor has no visual feedback while writing—they're guessing at the result until they preview.

Blocks are better for editors

Where shortcodes fall short is user experience. Imagine a content creator needs to embed a testimonial with an image, name, role, and star rating. In the block editor, they get a form: upload image, fill in text fields, select rating, see a live preview. In a shortcode, they write [testimonial id="99" rating="5"] and hope it works.

For anything beyond trivial embeds, blocks win. If you're starting a new plugin or updating an existing one, prioritize blocks.

But here's the pragmatic reality: many production sites still run the classic editor. Some teams manage content through REST API calls or scripts that generate shortcodes. Some plugins have been in the wild for years, and their users expect shortcode syntax to remain stable. Migration to blocks is a real effort, and breaking changes create support burden.

The solution: support both. Render the same content from a block AND a shortcode. Here's a pattern:

php
function register_testimonial_block() {
    register_block_type( 'my_plugin/testimonial', array(
        'render_callback' => 'render_testimonial_block',
        'attributes'      => array(
            'id'     => array( 'type' => 'number', 'default' => 0 ),
            'rating' => array( 'type' => 'number', 'default' => 5 ),
        ),
    ) );
    add_shortcode( 'testimonial', 'render_testimonial_shortcode' );
}
add_action( 'init', 'register_testimonial_block' );

function render_testimonial_shortcode( $atts ) {
    $atts = shortcode_atts(
        array(
            'id'     => 0,
            'rating' => 5,
        ),
        $atts,
        'testimonial'
    );
    return render_testimonial_content( $atts );
}

function render_testimonial_block( $atts, $content, $block ) {
    return render_testimonial_content( $atts );
}

function render_testimonial_content( $atts ) {
    $id     = absint( $atts['id'] );
    $rating = absint( $atts['rating'] );
    // Render HTML
    // ...
}

Both the block and the shortcode call the same rendering function. Maintain once, serve twice.

Escape attributes

Shortcode attributes are user input. Treat them like form data. If you don't escape and validate, you're opening security holes.

This is critical because shortcodes often live in post content, and any user with editor role can write them. A careless developer might do this:

php
// WRONG - don't do this
function render_alert( $atts ) {
    return '<div>' . $atts['message'] . '</div>';
}

An attacker writes [alert message="<script>alert('hacked')</script>"] and the script runs. It's the same vulnerability as any unsanitized output.

Always escape based on context:

php
function render_alert( $atts ) {
    $atts = shortcode_atts(
        array(
            'message' => '',
            'type'    => 'info',
        ),
        $atts,
        'alert'
    );

    $message = sanitize_text_field( $atts['message'] );
    $type    = sanitize_key( $atts['type'] );

    // Validate type against whitelist
    if ( ! in_array( $type, array( 'info', 'warning', 'error' ), true ) ) {
        $type = 'info';
    }

    return sprintf(
        '<div>%s</div>',
        esc_attr( $type ),
        wp_kses_post( $message )
    );
}

Key principles:

  • absint() or intval() for numeric IDs
  • sanitize_text_field() for plain text; use wp_kses_post() if HTML is allowed
  • sanitize_key() for option names or style classes
  • esc_attr() when outputting in HTML attributes
  • esc_html() when outputting in text content
  • Always use shortcode_atts() to normalize input and apply defaults

Keep shortcodes backward-compatible

Once you ship a shortcode in production, changing its behavior is a breaking change. Users have written it into their posts. Clients have built templates around it. Your plugin's reputation depends on stability.

Plan for evolution from day one.

Scenario: You released [banner text="..." color="blue"] two years ago. Now you want to support gradient colors.

Don't change the existing color attribute format. Add a new attribute:

php
function render_banner( $atts ) {
    $atts = shortcode_atts(
        array(
            'text'             => '',
            'color'            => '', // Legacy: single color
            'background_color' => '', // New: single color (preferred name)
            'gradient'         => '', // New: gradient syntax
        ),
        $atts,
        'banner'
    );

    $bg_color = '';
    if ( $atts['gradient'] ) {
        $bg_color = sanitize_text_field( $atts['gradient'] );
    } elseif ( $atts['background_color'] ) {
        $bg_color = sanitize_hex_color( $atts['background_color'] );
    } elseif ( $atts['color'] ) {
        // Legacy support
        $bg_color = sanitize_hex_color( $atts['color'] );
    }

    return sprintf(
        '<div>%s</div>',
        esc_attr( $bg_color ),
        wp_kses_post( $atts['text'] )
    );
}

The old shortcode still works. New users get better options. No breaking changes.

Document your shortcodes clearly in a README or settings page, including:

  • All available attributes with types and defaults
  • Which attributes are deprecated (if any)
  • Examples of usage
  • Deprecated since which version

When you eventually deprecate a shortcode entirely, give users a migration path and communication timeline.

Common mistakes

Not using shortcode_atts(): Rolling your own attribute parsing invites bugs. shortcode_atts() normalizes keys, applies defaults, and handles edge cases. Use it.

Over-engineering shortcodes: If your shortcode needs 10 attributes and complex nesting, it should be a block or custom post type view, not a shortcode. Shortcodes excel at simple injections.

Storing shortcodes in options: Sometimes a team dynamically generates shortcodes and saves them in wp_options. This works but creates a maintenance problem—the shortcode is now configuration, not code. Use post metadata or a custom table instead.

Not testing with different WordPress versions: Shortcodes are stable, but your plugin's dependencies might not be. Test your shortcodes with at least the current and prior major WordPress release.

Ignoring caching: If your shortcode queries the database or makes external API calls, it's slow when rendered in loops. Cache aggressively using transients or object cache, and provide ways for users to bust cache if needed.

Key takeaways

  • Shortcodes remain useful for simple embeds and backward compatibility, but blocks should be your default for new features
  • Use add_shortcode() with shortcode_atts() to register cleanly; always sanitize and escape attributes
  • Escape based on context: esc_attr(), esc_html(), wp_kses_post(), sanitize functions
  • Build both a block and shortcode that share the same rendering logic for maximum reach with minimal duplication
  • Plan for backward compatibility from the start; deprecate gradually, never abruptly
  • Document your shortcodes thoroughly so other team members and users understand their purpose and syntax
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles