SponsoredHosinger Special Offer

WP+ WordPress Plugin Development

Build WordPress plugins from scratch: actions and filters, shortcodes, settings pages, custom post types, meta boxes, AJAX, REST endpoints, custom tables, cron and blocks.

Shortcodes

WordPress Plugin Development Lesson 8 of 27 ~5 min read

Overview

A shortcode is a tag such as [gdc_books count="3"] that a site owner types into content, and which your plugin replaces with generated HTML when the page is displayed.

Blocks are the modern way to add rich content (lesson 23), but shortcodes still matter: they work in the classic editor, widgets, page builders and the Shortcode block, and they are the quickest way to give users a dynamic element.

A shortcode that lists books

<?php
/**
 * [gdc_books count="6" genre="fantasy" orderby="title"]
 *
 * @param array|string $atts    Attributes from the shortcode tag.
 * @param string|null  $content Content between opening and closing tags.
 * @param string       $tag     The shortcode name.
 * @return string HTML. Shortcodes must return, never echo.
 */
function gdc_books_shortcode( $atts, $content = null, $tag = '' ) {
    $atts = shortcode_atts(
        array(
            'count'   => 6,
            'genre'   => '',
            'orderby' => 'date',
        ),
        $atts,
        $tag // Allows the shortcode_atts_gdc_books filter.
    );

    $args = array(
        'post_type'      => 'gdc_book',
        'posts_per_page' => min( 24, absint( $atts['count'] ) ),
        'orderby'        => in_array( $atts['orderby'], array( 'date', 'title', 'rand' ), true ) ? $atts['orderby'] : 'date',
        'no_found_rows'  => true,
    );

    if ( $atts['genre'] ) {
        $args['tax_query'] = array(
            array(
                'taxonomy' => 'gdc_genre',
                'field'    => 'slug',
                'terms'    => sanitize_title( $atts['genre'] ),
            ),
        );
    }

    $books = new WP_Query( apply_filters( 'gdc_books_query_args', $args, $atts ) );

    if ( ! $books->have_posts() ) {
        return '<p class="gdc-books-empty">' . esc_html__( 'No books found.', 'gdc-books' ) . '</p>';
    }

    $html = '<ul class="gdc-books-list">';
    while ( $books->have_posts() ) {
        $books->the_post();
        $html .= sprintf(
            '<li><a href="%s">%s</a></li>',
            esc_url( get_permalink() ),
            esc_html( get_the_title() )
        );
    }
    $html .= '</ul>';

    wp_reset_postdata();

    return $html;
}

function gdc_books_register_shortcodes() {
    add_shortcode( 'gdc_books', 'gdc_books_shortcode' );
}
add_action( 'init', 'gdc_books_register_shortcodes' );

Why every line is there

  • shortcode_atts() merges the user's attributes with your defaults and drops anything you did not list — a typo like cout="3" is silently ignored rather than breaking the query.
  • Every attribute is untrusted text typed by a user. absint(), the orderby allow-list and sanitize_title() turn it into safe values before it reaches the query.
  • min( 24, ... ) caps the count, so count="99999" cannot be used to slow the site down.
  • The output is returned as a string. Echoing would print it at the top of the page, before the content it belongs in.
Watch out: Never echo in a shortcode. If you need to use templates that echo, capture their output with ob_start() and return ob_get_clean();.

Enclosing shortcodes

A shortcode can wrap content: [gdc_note type="tip"]Read the first chapter free.[/gdc_note]. The wrapped text arrives in $content:

<?php
function gdc_books_note_shortcode( $atts, $content = null ) {
    $atts = shortcode_atts( array( 'type' => 'info' ), $atts, 'gdc_note' );
    $type = in_array( $atts['type'], array( 'info', 'tip', 'warning' ), true ) ? $atts['type'] : 'info';

    return sprintf(
        '<div class="gdc-note gdc-note--%1$s">%2$s</div>',
        esc_attr( $type ),
        wp_kses_post( do_shortcode( $content ) ) // Allows shortcodes nested inside.
    );
}
add_shortcode( 'gdc_note', 'gdc_books_note_shortcode' );

Loading CSS only when the shortcode is used

Register your stylesheet on every page but only enqueue it from inside the shortcode. WordPress then prints it in the footer, only on pages that actually contain [gdc_books]:

<?php
add_action( 'wp_enqueue_scripts', function () {
    wp_register_style( 'gdc-books-list', GDC_BOOKS_URL . 'assets/css/books.css', array(), GDC_BOOKS_VERSION );
} );

// Then, as the first line inside gdc_books_shortcode():
wp_enqueue_style( 'gdc-books-list' );

Practice

  • Register [gdc_books] and place it on a page with different count and orderby values.
  • Try count="abc" and orderby="DROP TABLE" and confirm both fall back safely.
  • Build [gdc_note] and nest a [gdc_books count="2"] inside it.
Create a free account to save which lessons you've finished. Save my progress