Introduction

When you need a custom resource listing page with content gating, the approach is straightforward: query your posts with WP_Query, check each post's lock status with Gatekeeper Pro's template tags, then use the plugin's class methods to build the appropriate action URLs. No shortcodes, no workarounds — just clean PHP in your theme template.

This article walks through the recommended pattern used in production themes. You'll use gatekeeper_pro_is_locked() and gatekeeper_pro_has_access() to determine state, Gatekeeper_Pro_Token_Manager and Gatekeeper_Pro_Proxy_Endpoint to build secure URLs, and the data-gk-request attribute to trigger the plugin's built-in request form modal.

Template Tags and Class Methods

Gatekeeper Pro provides PHP helper functions (template tags) available after plugins_loaded. These are the primary tools for theme integration:

  • gatekeeper_pro_is_locked( $post_id ) — Returns true if the post's locked field is set or if the file is in the protected directory. Also triggers late asset loading so the form modal works even when called from theme templates outside the normal enqueue flow.

  • gatekeeper_pro_has_access( $post_id ) — Checks all available tokens (from URL parameter and cookie) against the post. Returns true if any valid token is found.

  • gatekeeper_pro_get_download_url( $post_id ) — Returns a proxied download URL if the visitor has a valid token, or false.

  • gatekeeper_pro_get_video_url( $post_id ) — Returns a proxied video streaming URL if the visitor has a valid token, or false.

  • gatekeeper_pro_display_content_gate( $post_id ) — Outputs the complete gate widget (locked state with request button, or unlocked state with download/video action).

  • gatekeeper_pro_display_request_form( $post_id ) — Outputs the native AJAX request form for the given post.

For advanced integrations, the plugin also exposes direct class methods:

  • Gatekeeper_Pro_Token_Manager::find_valid_token( $post_id ) — Finds any valid token for a post (checks URL parameter and cookie). Returns the token data array or false.

  • Gatekeeper_Pro_Proxy_Endpoint::get_download_url( $post_id, $token_string ) — Builds a secure proxied download URL.

  • Gatekeeper_Pro_Proxy_Endpoint::get_video_url( $post_id, $token_string ) — Builds a secure proxied video streaming URL.

  • Gatekeeper_Pro_Field_Detector::get_field_value( $post_id, 'file' ) — Gets the file field value for a post.

  • Gatekeeper_Pro_File_Handler::resolve_attachment_id( $file_value ) — Resolves a file value to a WordPress attachment ID.

Data Attributes for Custom Markup

If you're building completely custom HTML, these data attributes trigger the plugin's JavaScript behaviour — no PHP form rendering needed:

AttributePurpose
data-gk-request="{post_id}"Opens the request form modal on click. Add to any element (button, link, image).
data-gk-video="{url}"Opens the video lightbox on click.
data-gk-video-title="{title}"Optional title displayed in the video lightbox.

The plugin automatically injects the request form modal into the page footer via wp_footer, so you don't need to place any form markup yourself. Just add data-gk-request to your trigger element and the plugin handles the rest.

The Recommended Pattern

The following pattern is used in production resource listing templates. The approach is:

  1. Query your gated post types with WP_Query.

  2. For each post, check if it's locked with gatekeeper_pro_is_locked().

  3. If locked, check for a valid token with Gatekeeper_Pro_Token_Manager::find_valid_token().

  4. Based on the state (locked/no access, locked/has access, or unlocked), build the appropriate action — request button, proxy download/video URL, or direct file link.

<?php
$resources = new WP_Query( array(
    'post_type'      => 'resources', // Your custom post type
    'posts_per_page' => -1,
    'orderby'        => 'title',
    'order'          => 'ASC',
    'post_status'    => 'publish',
) );

if ( $resources->have_posts() ) :
    while ( $resources->have_posts() ) :
        $resources->the_post();
        $resource_id = get_the_ID();

        // Check lock status and access.
        $is_locked  = gatekeeper_pro_is_locked( $resource_id );
        $has_access = false;
        $token_data = false;

        if ( $is_locked && class_exists( 'Gatekeeper_Pro_Token_Manager' ) ) {
            $token_data = Gatekeeper_Pro_Token_Manager::find_valid_token( $resource_id );
            $has_access = (bool) $token_data;
        }
        ?>

        <div class="resource-card <?php echo $is_locked ? 'is-locked' : ''; ?> <?php echo $has_access ? 'has-access' : ''; ?>">

            <?php if ( get_the_post_thumbnail( $resource_id ) ) : ?>
                <div class="resource-card__thumbnail">
                    <?php the_post_thumbnail( 'medium' ); ?>
                    <?php if ( $is_locked && ! $has_access ) : ?>
                        <div class="resource-card__lock-overlay">
                            <!-- Lock icon -->
                        </div>
                    <?php endif; ?>
                </div>
            <?php endif; ?>

            <h3><?php the_title(); ?></h3>
            <?php the_excerpt(); ?>

            <div class="resource-card__actions">
                <?php if ( $is_locked && ! $has_access ) :
                    // Locked, no access — request button.
                    // data-gk-request triggers the built-in modal automatically.
                    ?>
                    <button data-gk-request="<?php echo esc_attr( $resource_id ); ?>">
                        Request Access
                    </button>

                <?php elseif ( $is_locked && $has_access ) :
                    // Locked with valid token — build proxy URLs.
                    $download_url = Gatekeeper_Pro_Proxy_Endpoint::get_download_url( $resource_id, $token_data['token'] );
                    $video_url    = Gatekeeper_Pro_Proxy_Endpoint::get_video_url( $resource_id, $token_data['token'] );

                    if ( $video_url ) : ?>
                        <a href="<?php echo esc_url( $video_url ); ?>"
                           data-gk-video="<?php echo esc_url( $video_url ); ?>"
                           data-gk-video-title="<?php echo esc_attr( get_the_title() ); ?>">
                            Watch Video
                        </a>
                    <?php elseif ( $download_url ) : ?>
                        <a href="<?php echo esc_url( $download_url ); ?>">Download</a>
                    <?php endif; ?>

                    <?php if ( ! empty( $token_data['expires_at'] ) ) : ?>
                        <p class="expiry-notice">Access expires:
                            <?php echo esc_html( wp_date( 'j M Y, g:i a', strtotime( $token_data['expires_at'] ) ) ); ?>
                        </p>
                    <?php endif; ?>

                <?php else :
                    // Not locked — resolve the file directly.
                    $file_val      = Gatekeeper_Pro_Field_Detector::get_field_value( $resource_id, 'file' );
                    $attachment_id = Gatekeeper_Pro_File_Handler::resolve_attachment_id( $file_val );
                    $file_url      = $attachment_id ? wp_get_attachment_url( $attachment_id ) : '';

                    if ( $file_url ) : ?>
                        <a href="<?php echo esc_url( $file_url ); ?>" download>Download</a>
                    <?php endif; ?>
                <?php endif; ?>
            </div>

        </div>

    <?php endwhile;
    wp_reset_postdata();
endif;
?>

A few things to note about this pattern:

  • No shortcodes in the loop. When you're writing a custom template with WP_Query, you use the template tags and class methods directly. Shortcodes are designed for content editors working in Gutenberg or page builders — not for PHP templates.

  • The form modal is automatic. The plugin injects the request form into the footer via wp_footer. You don't need to render it yourself. Just add data-gk-request to any element and the JavaScript handles opening the modal, submitting the form, and showing feedback.

  • The class_exists() check is defensive coding — it ensures your template doesn't fatal error if Gatekeeper Pro is deactivated. Wrap any direct class method calls in this check.

  • Three states, not two. A resource can be locked without access (show request button), locked with access (show proxy download/video URLs), or unlocked (resolve the file directly). Handle all three.

Customising the Output

The example above uses generic HTML classes. In practice, you'd apply your theme's design system. Some tips:

  • Lock overlay: Use a positioned <div> over the thumbnail area. Only render it when $is_locked && ! $has_access.

  • Status badges: Show "Locked", "Access Granted", or the resource type (download vs video) as small badges. Use $is_locked, $has_access, and get_post_type() to determine which to show.

  • Expiry notice: When $token_data['expires_at'] is set, show a human-readable expiry date. Use wp_date() for localised formatting.

  • CSS grid layout: Wrap the loop output in a CSS grid container for responsive columns.

If you want to use Gatekeeper Pro's built-in resource card styles instead of building your own, the plugin's output uses BEM-style classes prefixed with gk-. Key classes include .gk-resource-card, .gk-resource-card--locked, .gk-resource-card--unlocked, .gk-badge--video, .gk-badge--download, and .gk-badge--access. The plugin styles use low specificity by design, so they're easy to override in your theme CSS.

Alternative: Using the Gate Widget

If you don't need full control over each state, Gatekeeper Pro provides gatekeeper_pro_display_content_gate(), which outputs the complete gate widget for a post — locked state with request button, or unlocked state with the appropriate download/video action. This is a middle ground between the full custom approach above and using the [gatekeeper_gate] shortcode in content:

<?php
while ( $resources->have_posts() ) :
    $resources->the_post();
    ?>
    <div class="resource-card">
        <h3><?php the_title(); ?></h3>
        <?php the_excerpt(); ?>
        <?php gatekeeper_pro_display_content_gate( get_the_ID() ); ?>
    </div>
    <?php
endwhile;
?>

This renders the full gate UI per post — handling lock detection, request forms, and access states automatically — while still letting you control the surrounding layout.

Performance Considerations

When building custom listing pages with gated content, keep these points in mind:

  • Call wp_reset_postdata() after every custom WP_Query loop to restore the global post object.

  • Gatekeeper Pro sends no-cache headers automatically when gated content is detected on a page. This is compatible with WP Super Cache, W3 Total Cache, LiteSpeed Cache, WP Rocket, and Cloudflare. Be aware that listing pages with gated content won't be page-cached, since each visitor may have different token states.

  • Token validation is lightweight. find_valid_token() checks the URL parameter and cookie — it doesn't make external API calls. It's safe to call inside a loop.

Conclusion

Building custom gated content listing pages with WP_Query and Gatekeeper Pro is a matter of combining standard WordPress development patterns with the plugin's template tags and class methods. Query your posts, check lock status, check access, build the appropriate action — and let the plugin handle the forms, modals, token validation, and secure file delivery.

A complete working example of this pattern, including thumbnail handling, type badges, and Tailwind CSS integration, is available in the template-gatekeeper-resources.php file included in the plugin's documentation folder.