DEV Community

Cover image for Role-Based Content in WordPress Without Membership Plugins
anti Gym Club
anti Gym Club

Posted on • Originally published at antigymclub.com

Role-Based Content in WordPress Without Membership Plugins

You need to hide a post from non-logged-in users. Or show certain pages only to Editors. Or create subscriber-only content.

The typical advice? Install a membership plugin with 50 features you don't need.

Here's how to do role-based content visibility with just PHP no bloated plugins required.

The Core Concept

WordPress already has a robust role and capability system. Every user has a role (administrator, editor, author, subscriber, etc.), and each role has capabilities.

We can leverage this to control content visibility at two levels:

  1. Post/page level entire posts visible only to certain roles
  2. Content level specific sections within a post

Method 1: Filter Posts by Role (Query Level)

This approach hides posts from archive pages and search results:

/**
 * Filter posts based on user role
 */
function filter_posts_by_role( $query ) {
    // Only on frontend, main queries, not single posts
    if ( is_admin() || ! $query->is_main_query() || is_singular() ) {
        return;
    }

    $user = wp_get_current_user();

    // Admins see everything
    if ( in_array( 'administrator', (array) $user->roles ) ) {
        return;
    }

    // Get all posts and check access
    $all_posts = get_posts( array(
        'post_type'      => 'post',
        'posts_per_page' => -1,
        'fields'         => 'ids',
        'post_status'    => 'publish',
    ) );

    $allowed_ids = array();

    foreach ( $all_posts as $post_id ) {
        if ( can_user_access_post( $post_id, $user ) ) {
            $allowed_ids[] = $post_id;
        }
    }

    if ( empty( $allowed_ids ) ) {
        $query->set( 'post__in', array( 0 ) ); // No posts
    } else {
        $query->set( 'post__in', $allowed_ids );
    }
}
add_action( 'pre_get_posts', 'filter_posts_by_role' );
Enter fullscreen mode Exit fullscreen mode

Method 2: Block Direct URL Access

Filtering queries isn't enough users can still access posts via direct URL. Add this:

/**
 * Restrict direct URL access to role-restricted posts
 */
function restrict_direct_access() {
    if ( ! is_singular() ) {
        return;
    }

    global $post;
    $user = wp_get_current_user();

    // Admins bypass
    if ( in_array( 'administrator', (array) $user->roles ) ) {
        return;
    }

    if ( ! can_user_access_post( $post->ID, $user ) ) {
        // Option 1: Redirect to login
        wp_safe_redirect( wp_login_url( get_permalink() ) );
        exit;

        // Option 2: Show 404
        // global $wp_query;
        // $wp_query->set_404();
        // status_header( 404 );
    }
}
add_action( 'template_redirect', 'restrict_direct_access' );
Enter fullscreen mode Exit fullscreen mode

The Access Check Function

Both methods above call can_user_access_post(). Here's the logic:

/**
 * Check if user can access a post
 *
 * @param int     $post_id Post ID
 * @param WP_User $user    User object (null = current user)
 * @return bool
 */
function can_user_access_post( $post_id, $user = null ) {
    if ( $user === null ) {
        $user = wp_get_current_user();
    }

    // Get allowed roles from post meta
    $allowed_roles = get_post_meta( $post_id, '_allowed_roles', true );
    $allowed_roles = is_array( $allowed_roles ) ? $allowed_roles : array();

    // No restrictions = public
    if ( empty( $allowed_roles ) ) {
        return true;
    }

    // Check for "everyone" flag
    if ( in_array( 'everyone', $allowed_roles ) ) {
        return true;
    }

    // Guest access
    if ( ! is_user_logged_in() ) {
        return in_array( 'guest', $allowed_roles );
    }

    // Check user roles against allowed roles
    $user_roles = (array) $user->roles;
    return ! empty( array_intersect( $allowed_roles, $user_roles ) );
}
Enter fullscreen mode Exit fullscreen mode

Adding a Meta Box for Role Selection

Let editors choose which roles can see each post:

/**
 * Add meta box for role selection
 */
function add_role_visibility_meta_box() {
    add_meta_box(
        'role_visibility_box',
        'Content Visibility',
        'render_role_visibility_meta_box',
        'post',
        'side',
        'high'
    );
}
add_action( 'add_meta_boxes', 'add_role_visibility_meta_box' );

/**
 * Render meta box
 */
function render_role_visibility_meta_box( $post ) {
    $roles = wp_roles()->roles;
    $selected = get_post_meta( $post->ID, '_allowed_roles', true );
    $selected = is_array( $selected ) ? $selected : array();

    wp_nonce_field( 'role_visibility_nonce', 'role_visibility_nonce' );
    ?>
    <p><strong>Who can view this post?</strong></p>

    <label style="display:block; margin-bottom:5px;">
        <input type="checkbox" name="allowed_roles[]" value="everyone"
            <?php checked( in_array( 'everyone', $selected ) ); ?>>
        Everyone (Public)
    </label>

    <label style="display:block; margin-bottom:5px;">
        <input type="checkbox" name="allowed_roles[]" value="guest"
            <?php checked( in_array( 'guest', $selected ) ); ?>>
        Guests Only (Not logged in)
    </label>

    <hr>
    <p><strong>Logged-in roles:</strong></p>

    <?php foreach ( $roles as $role_key => $role ) : ?>
        <label style="display:block; margin-bottom:3px;">
            <input type="checkbox" name="allowed_roles[]"
                value="<?php echo esc_attr( $role_key ); ?>"
                <?php checked( in_array( $role_key, $selected ) ); ?>>
            <?php echo esc_html( $role['name'] ); ?>
        </label>
    <?php endforeach; ?>

    <p><em>No selection = visible to everyone</em></p>
    <?php
}

/**
 * Save meta box data
 */
function save_role_visibility_meta( $post_id ) {
    if ( ! isset( $_POST['role_visibility_nonce'] ) ||
         ! wp_verify_nonce( $_POST['role_visibility_nonce'], 'role_visibility_nonce' ) ) {
        return;
    }

    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return;
    }

    if ( ! current_user_can( 'edit_post', $post_id ) ) {
        return;
    }

    if ( isset( $_POST['allowed_roles'] ) && is_array( $_POST['allowed_roles'] ) ) {
        $roles = array_map( 'sanitize_text_field', $_POST['allowed_roles'] );
        update_post_meta( $post_id, '_allowed_roles', $roles );
    } else {
        delete_post_meta( $post_id, '_allowed_roles' );
    }
}
add_action( 'save_post', 'save_role_visibility_meta' );
Enter fullscreen mode Exit fullscreen mode

Method 3: Shortcodes for Inline Content

Sometimes you need to hide just part of a post. Shortcodes work great for this:

/**
 * Show content only to specific roles
 * Usage: [if_role role="editor,author"]Secret content[/if_role]
 */
function shortcode_if_role( $atts, $content = '' ) {
    $atts = shortcode_atts( array(
        'role' => ''
    ), $atts );

    if ( empty( $atts['role'] ) || empty( $content ) ) {
        return '';
    }

    $allowed_roles = array_map( 'trim', explode( ',', $atts['role'] ) );
    $user = wp_get_current_user();

    if ( ! is_user_logged_in() ) {
        return '';
    }

    if ( array_intersect( $allowed_roles, (array) $user->roles ) ) {
        return do_shortcode( $content );
    }

    return '';
}
add_shortcode( 'if_role', 'shortcode_if_role' );

/**
 * Show content only to logged-in users
 * Usage: [if_logged_in]Members only![/if_logged_in]
 */
function shortcode_if_logged_in( $atts, $content = '' ) {
    if ( is_user_logged_in() ) {
        return do_shortcode( $content );
    }
    return '';
}
add_shortcode( 'if_logged_in', 'shortcode_if_logged_in' );

/**
 * Show content only to guests
 * Usage: [if_guest]Please log in to see more.[/if_guest]
 */
function shortcode_if_guest( $atts, $content = '' ) {
    if ( ! is_user_logged_in() ) {
        return do_shortcode( $content );
    }
    return '';
}
add_shortcode( 'if_guest', 'shortcode_if_guest' );
Enter fullscreen mode Exit fullscreen mode

Usage Examples

<!-- Show premium content to subscribers -->
[if_role role="subscriber,administrator"]
<div class="premium-content">
    This is subscriber-only content.
</div>
[/if_role]

<!-- Different messages for logged in vs guests -->
[if_logged_in]
Welcome back! Here's your dashboard.
[/if_logged_in]

[if_guest]
Please <a href="/login">log in</a> to access your account.
[/if_guest]
Enter fullscreen mode Exit fullscreen mode

Security Considerations

  1. Always check on both query AND template_redirect query filtering alone isn't enough
  2. Validate nonces when saving meta
  3. Sanitize role values never trust user input
  4. Admin bypass always let administrators see everything
  5. Cache awareness if using page caching, exclude role-restricted pages

When This Isn't Enough

This approach works for simple role-based visibility. You'll need more if you want:

  • Subscription payments
  • Drip content
  • User registration forms
  • Progress tracking

For those, you actually need a membership plugin. But for basic "show this to editors only" or "hide from guests" the code above handles it.


If you want this functionality without maintaining custom code, I built Role Based Content Pro that handles all edge cases plus adds a visual indicator in the posts list.

Resources:

Top comments (0)