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:
- Post/page level entire posts visible only to certain roles
- 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' );
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' );
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 ) );
}
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' );
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' );
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]
Security Considerations
- Always check on both query AND template_redirect query filtering alone isn't enough
- Validate nonces when saving meta
- Sanitize role values never trust user input
- Admin bypass always let administrators see everything
- 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)