iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Shortcodes

A shortcode is a bracket-delimited tag ([gallery], [my_button]) that expands to HTML at render time. Define with add_shortcode; users embed in posts/pages without touching code.

Register, attributes, content, nesting

EXAMPLE
<?php
// 1) Simplest shortcode
add_shortcode('hello', function () {
    return '<p>Hello, world!</p>';
});

// Usage in post / page:
// [hello]

// Renders:
// <p>Hello, world!</p>

// 2) Shortcode with attributes
add_shortcode('greeting', function ($atts) {
    $atts = shortcode_atts([
        'name'  => 'friend',
        'style' => 'normal',
    ], $atts);
    return sprintf(
        '<p class="greeting greeting--%s">Hello, %s!</p>',
        esc_attr($atts['style']),
        esc_html($atts['name']),
    );
});

// Usage:
// [greeting name="Ada" style="loud"]
// → <p class="greeting greeting--loud">Hello, Ada!</p>

// 3) Shortcode with content (enclosing)
add_shortcode('callout', function ($atts, $content = null) {
    $atts = shortcode_atts([
        'type' => 'info',
    ], $atts);
    return sprintf(
        '<div class="callout callout--%s">%s</div>',
        esc_attr($atts['type']),
        do_shortcode($content),   // process nested shortcodes
    );
});

// Usage:
// [callout type="warning"]Watch out![/callout]

// 4) Real-world: button shortcode
add_shortcode('button', function ($atts) {
    $atts = shortcode_atts([
        'href'    => '#',
        'label'   => 'Click',
        'style'   => 'primary',
        'target'  => '_self',
        'class'   => '',
    ], $atts);

    $classes = trim(sprintf('btn btn-%s %s', $atts['style'], $atts['class']));
    return sprintf(
        '<a class="%s" href="%s" target="%s" rel="%s">%s</a>',
        esc_attr($classes),
        esc_url($atts['href']),
        esc_attr($atts['target']),
        $atts['target'] === '_blank' ? 'noopener noreferrer' : '',
        esc_html($atts['label']),
    );
});

// Usage:
// [button href="/contact" label="Contact us" style="primary"]
// [button href="https://example.com" label="External" target="_blank"]

// 5) Shortcode that lists recent posts
add_shortcode('recent_posts', function ($atts) {
    $atts = shortcode_atts([
        'count'     => 5,
        'category'  => '',
        'order'     => 'desc',
    ], $atts);

    $args = [
        'posts_per_page' => intval($atts['count']),
        'order'          => strtoupper($atts['order']),
    ];
    if (!empty($atts['category'])) {
        $args['category_name'] = sanitize_title($atts['category']);
    }

    $q = new WP_Query($args);
    if (!$q->have_posts()) return '<p>No posts found.</p>';

    ob_start();
    echo '<ul class="recent-posts">';
    while ($q->have_posts()) {
        $q->the_post();
        printf(
            '<li><a href="%s">%s</a> <small>%s</small></li>',
            esc_url(get_permalink()),
            esc_html(get_the_title()),
            esc_html(get_the_date()),
        );
    }
    echo '</ul>';
    wp_reset_postdata();
    return ob_get_clean();
});

// Usage:
// [recent_posts count="10" category="news"]

// 6) Conditional content based on user / context
add_shortcode('logged_in', function ($atts, $content = null) {
    if (is_user_logged_in()) {
        return do_shortcode($content);
    }
    return '';
});

add_shortcode('guest', function ($atts, $content = null) {
    if (!is_user_logged_in()) {
        return do_shortcode($content);
    }
    return '';
});

// Usage:
// [logged_in]Welcome back, [user_name]![/logged_in]
// [guest][button href="/login" label="Sign in"][/guest]

// 7) Embedded form
add_shortcode('newsletter', function () {
    ob_start();
    ?>
    <form action="<?php echo esc_url(admin_url('admin-post.php')); ?>" method="post" class="newsletter-form">
        <?php wp_nonce_field('newsletter', 'newsletter_nonce'); ?>
        <input type="hidden" name="action" value="newsletter_signup" />
        <label for="email">Email</label>
        <input id="email" type="email" name="email" required />
        <button type="submit">Subscribe</button>
    </form>
    <?php
    return ob_get_clean();
});

// Handle the form submission
add_action('admin_post_nopriv_newsletter_signup', 'handle_newsletter');
add_action('admin_post_newsletter_signup',         'handle_newsletter');
function handle_newsletter() {
    if (!isset($_POST['newsletter_nonce']) || !wp_verify_nonce($_POST['newsletter_nonce'], 'newsletter')) {
        wp_die('Invalid nonce');
    }
    $email = sanitize_email($_POST['email']);
    // ... add to mailing list ...
    wp_redirect(home_url('/thanks'));
    exit;
}

// 8) Gallery / list with nested shortcodes
add_shortcode('gallery_grid', function ($atts, $content) {
    $atts = shortcode_atts(['cols' => 3], $atts);
    return sprintf(
        '<div class="gallery-grid cols-%d">%s</div>',
        intval($atts['cols']),
        do_shortcode($content),
    );
});

add_shortcode('gallery_item', function ($atts) {
    $atts = shortcode_atts(['src' => '', 'alt' => '', 'caption' => ''], $atts);
    return sprintf(
        '<figure><img src="%s" alt="%s" /><figcaption>%s</figcaption></figure>',
        esc_url($atts['src']),
        esc_attr($atts['alt']),
        esc_html($atts['caption']),
    );
});

// Usage:
// [gallery_grid cols="3"]
//     [gallery_item src="/a.jpg" caption="A"]
//     [gallery_item src="/b.jpg" caption="B"]
//     [gallery_item src="/c.jpg" caption="C"]
// [/gallery_grid]

// 9) Output content from a CPT
add_shortcode('product', function ($atts) {
    $atts = shortcode_atts(['id' => 0, 'sku' => ''], $atts);

    if ($atts['sku']) {
        $query = new WP_Query([
            'post_type'  => 'product',
            'meta_key'   => 'sku',
            'meta_value' => sanitize_text_field($atts['sku']),
            'posts_per_page' => 1,
        ]);
        if (!$query->have_posts()) return '<p>Product not found.</p>';
        $query->the_post();
    } elseif ($atts['id']) {
        $post = get_post(intval($atts['id']));
        if (!$post || $post->post_type !== 'product') return '<p>Product not found.</p>';
        setup_postdata($post);
    } else {
        return '<p>Specify id or sku.</p>';
    }

    ob_start();
    $price = get_post_meta(get_the_ID(), 'price', true);
    ?>
    <article class="product-card">
        <?php the_post_thumbnail('medium'); ?>
        <h3><?php the_title(); ?></h3>
        <p class="price">$<?php echo esc_html(number_format((float) $price, 2)); ?></p>
        <?php the_excerpt(); ?>
        <a href="<?php echo esc_url(get_permalink()); ?>" class="btn">View</a>
    </article>
    <?php
    wp_reset_postdata();
    return ob_get_clean();
});

// Usage:
// [product sku="A-100"]
// [product id="123"]

// 10) Programmatic invocation — do_shortcode
echo do_shortcode('[button href="/login" label="Sign in"]');

// 11) Shortcode UI for editors (TinyMCE button) — older approach
// For Gutenberg / block editor — convert shortcodes to blocks instead.
// Wrappers: register_block_type + render via PHP for legacy.

// 12) Disable wpautop wrapping for shortcode output
function remove_wpautop_around_shortcode($content) {
    $content = preg_replace('/<p>\\s*(\\[gallery_grid[^\\]]*\\])/i', '$1', $content);
    $content = preg_replace('/(\\[\\/gallery_grid\\])\\s*<\\/p>/i', '$1', $content);
    return $content;
}
add_filter('the_content', 'remove_wpautop_around_shortcode');

// 13) Security checklist
//   ✅ Escape ALL output: esc_html, esc_attr, esc_url, esc_textarea
//   ✅ Sanitize input attributes
//   ✅ Use prepared statements if querying DB
//   ✅ Check capabilities if action is privileged
//   ✅ Use nonces for forms (wp_create_nonce, wp_verify_nonce)
//   ✅ Prefix function names (mytheme_shortcode_X) to avoid collisions

// 14) Performance
// - Shortcodes run on EVERY render of the_content
// - Cache expensive queries with transients:
//     $cache_key = 'recent_posts_' . md5(serialize($atts));
//     if (false === ($html = get_transient($cache_key))) {
//         // build $html...
//         set_transient($cache_key, $html, HOUR_IN_SECONDS);
//     }
//     return $html;
// - Invalidate on save_post hook

// 15) Gutenberg / Block Editor — modern alternative
// Shortcodes still work in posts, but the block editor encourages CUSTOM BLOCKS.
// Pros of blocks:
//   - Visual editing experience
//   - Schema-validated attributes
//   - Server-side OR client-side rendering
//   - Reusable as patterns
// Convert shortcodes → blocks via:
//   1. register_block_type with attributes
//   2. render_callback that calls do_shortcode under the hood
// This preserves backward compatibility while modernising the UX.

// 16) Common bugs
//   • Forgetting esc_* → XSS risk
//   • Direct $_POST in shortcode handler → CSRF
//   • Not calling do_shortcode on nested content → nested shortcodes break
//   • Using <p> tags inside shortcode output that conflicts with wpautop
//   • Caching without invalidation → stale content forever

// 17) Best practices
//   ✅ Always use shortcode_atts with safe defaults
//   ✅ Escape output religiously
//   ✅ Sanitize attributes (sanitize_text_field, intval, esc_url_raw)
//   ✅ Return strings, don't echo (shortcodes RETURN)
//   ✅ Prefix function + shortcode names
//   ✅ Document attributes inline
//   ✅ Consider blocks for new features in modern WP

Why it matters

Shortcodes are the classic WP extension point — user-friendly, theme-portable, easy to write. For new features in modern WP, consider blocks: same dynamic rendering, better UX in the editor, schema-validated attributes.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
add_shortcode('button', function ($atts) {
    $atts = shortcode_atts(['color' => '#04AA6D'], $atts);
    return '<a style="background:' . $atts['color'] . '">Click</a>';
});
Try it Yourself »

Exercise

Register a shortcode named hello.

('hello', 'render_hello');

Discussion

Loading…