-4

I’m implementing a custom author archive in WordPress where the author page lists posts based on custom meta fields (ACF user fields), not post_author.

What works

  • /author/author-slug/ loads correctly

  • Page 1 shows the correct posts

  • Pagination links are generated correctly

What doesn’t work

  • /author/author-slug/page/2/ returns 404 / Forbidden

  • WordPress code and rewrites seem correct

  • Pagination never reaches WordPress on page 2 in staging


Setup details

  • WordPress (Genesis-based theme, but not doing anything custom with Genesis loops)

  • Custom post type: document

  • Author page shows documents where:

    • _dg_document_writer_id = author_id OR

    • _dg_document_reviewer_id = author_id

  • Pagination size: 9

Code

Author template (author.php)

<?php
/**
 * Author archive template (DocumentGenius)
 */
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

// Full width (no sidebar).
add_filter( 'genesis_site_layout', '__genesis_return_full_width_content', 99 );

// Replace default loop.
remove_action( 'genesis_loop', 'genesis_do_loop' );
remove_action( 'genesis_before_loop', 'genesis_do_author_box_archive' );
remove_action( 'genesis_before_loop', 'genesis_do_author_title_description', 15 );
remove_action( 'genesis_before_loop', 'genesis_do_author_box_archive', 15 );

add_action( 'genesis_loop', 'dg_author_archive_loop' );

add_action( 'wp_enqueue_scripts', function () {

    if ( is_author() ) {
        wp_enqueue_style(
            'dg-author',
            get_stylesheet_directory_uri() . '/build/css/author.min.css',
            array(),
            filemtime( get_stylesheet_directory() . '/build/css/author.min.css' )
        );
    }

}, 20 );


function dg_author_archive_loop() {

    // Load author page CSS (registered in dg_load_assets()).
    wp_enqueue_style( 'dg-author' );

    $user_id = (int) get_query_var( 'author' );
    if ( ! $user_id ) {
        $author = get_queried_object();
        $user_id = ! empty( $author->ID ) ? (int) $author->ID : 0;
    }

    if ( ! $user_id ) {
        echo '<p>Author not found.</p>';
        return;
    }

    $user    = get_user_by( 'id', $user_id );

    // ACF user fields (saved against "user_{$id}").
    $acf_ref  = 'user_' . $user_id;
    $image_id = (int) get_field( '_dg_user_image_id', $acf_ref );

    $job_title  = (string) get_field( '_dg_user_job_title', $acf_ref );
    $employer   = (string) get_field( '_dg_user_employer_name', $acf_ref );
    $expertise  = (string) get_field( '_dg_user_expertise', $acf_ref );
    $education  = (string) get_field( '_dg_user_education', $acf_ref ); // Optional (see section 4)
    $location   = (string) get_field( '_dg_user_location', $acf_ref );  // Optional (see section 4)
    $bio_html   = (string) get_field( '_dg_user_author_page_description', $acf_ref );

    // Fallback to WP bio if ACF bio is empty.
    if ( empty( trim( wp_strip_all_tags( $bio_html ) ) ) ) {
        $bio_html = wpautop( get_the_author_meta( 'description', $user_id ) );
    }

    $display_name = ! empty( $user ) ? $user->display_name : ( ! empty( $author->display_name ) ? $author->display_name : 'Author' );

    $subtitle_parts = array();
    if ( $job_title ) {
        $subtitle_parts[] = $job_title;
    }
    if ( $employer ) {
        $subtitle_parts[] = $employer;
    }
    if ( $location ) {
        $subtitle_parts[] = $location;
    }
    $subtitle = implode( ' · ', array_filter( $subtitle_parts ) );

    // Parse textarea lists into arrays (one item per line).
    $expertise_items = array_filter( preg_split( "/\r\n|\r|\n/", trim( $expertise ) ) );
    $education_items = array_filter( preg_split( "/\r\n|\r|\n/", trim( $education ) ) );

    // Pagination (WordPress uses "paged" for archives).
    $paged = (int) get_query_var( 'paged' );
    if ( $paged < 1 ) {
        $paged = (int) get_query_var( 'page' ); // fallback
    }
    $paged = max( 1, $paged );

    ?>
<div class="top-section">
    <div class="dg-author">
        <header class="dg-author__hero">
            <div class="dg-author__avatar">
                <?php
                if ( $image_id ) {
                    echo wp_get_attachment_image(
                        $image_id,
                        'thumbnail',
                        false,
                        array(
                            'alt'     => esc_attr( $display_name ),
                            'loading' => 'eager',
                        )
                    );
                } else {
                    // Fallback to WP avatar.
                    echo get_avatar( $user_id, 96, '', $display_name, array( 'loading' => 'eager' ) );
                }
                ?>
            </div>

            <h1 class="dg-author__name"><?php echo esc_html( $display_name ); ?></h1>

            <?php if ( $subtitle ) : ?>
                <p class="dg-author__subtitle"><?php echo esc_html( $subtitle ); ?></p>
            <?php endif; ?>

            <?php if ( $bio_html ) : ?>
                <div class="dg-author__bio">
                    <?php echo wp_kses_post( $bio_html ); ?>
                </div>
            <?php endif; ?>

            <div class="dg-author__details">
                <section class="dg-author__card">
                    <h2>Expertise</h2>

                    <?php if ( ! empty( $expertise_items ) ) : ?>
                        <ul>
                            <?php foreach ( $expertise_items as $item ) : ?>
                                <li><?php echo esc_html( trim( $item ) ); ?></li>
                            <?php endforeach; ?>
                        </ul>
                    <?php else : ?>
                        <p class="dg-author__muted">—</p>
                    <?php endif; ?>
                </section>

                <section class="dg-author__card">
                    <h2>Education</h2>

                    <?php if ( ! empty( $education_items ) ) : ?>
                        <ul>
                            <?php foreach ( $education_items as $item ) : ?>
                                <li><?php echo esc_html( trim( $item ) ); ?></li>
                            <?php endforeach; ?>
                        </ul>
                    <?php else : ?>
                        <p class="dg-author__muted">—</p>
                    <?php endif; ?>
                </section>
            </div>
        </header>

        <section class="dg-author__latest">
    <h2><?php echo esc_html( 'Latest from ' . $display_name ); ?></h2>

    <?php
    // Pagination.
    $paged = (int) get_query_var( 'paged' );
    if ( $paged < 1 ) {
        $paged = (int) get_query_var( 'page' ); // fallback
    }
    $paged = max( 1, $paged );

    // Query documents by writer/reviewer meta.
    $docs_q = new WP_Query(
        array(
            'post_type'      => 'document',
            'post_status'    => 'publish',
            'posts_per_page' => 9,
            'paged'          => $paged,
            'orderby'        => 'date',
            'order'          => 'DESC',
            'meta_query'     => array(
                'relation' => 'OR',

                // Writer (ID stored as int/string)
                array(
                    'key'     => '_dg_document_writer_id',
                    'value'   => $user_id,
                    'compare' => '=',
                ),
                // Writer (serialized storage)
                array(
                    'key'     => '_dg_document_writer_id',
                    'value'   => '"' . $user_id . '"',
                    'compare' => 'LIKE',
                ),

                // Reviewer (ID stored as int/string)
                array(
                    'key'     => '_dg_document_reviewer_id',
                    'value'   => $user_id,
                    'compare' => '=',
                ),
                // Reviewer (serialized storage)
                array(
                    'key'     => '_dg_document_reviewer_id',
                    'value'   => '"' . $user_id . '"',
                    'compare' => 'LIKE',
                ),
            ),
        )
    );

    if ( $docs_q->have_posts() ) : ?>
        <div class="dg-author__grid">
            <?php
            while ( $docs_q->have_posts() ) :
                $docs_q->the_post();

                $post_id = get_the_ID();
                $title   = get_the_title();
                $link    = get_permalink();

                $updated = get_post_meta( $post_id, '_dg_document_updated_date', true );
                $date    = $updated ? date_i18n( get_option( 'date_format' ), strtotime( $updated ) ) : get_the_date();
                ?>
                <article class="dg-author__doc">
                    <a class="dg-author__docLink" href="<?php echo esc_url( $link ); ?>">
                        <div class="dg-author__thumb">
                            <?php if ( has_post_thumbnail() ) : ?>
                                <?php the_post_thumbnail( 'medium', array( 'loading' => 'lazy' ) ); ?>
                            <?php else : ?>
                                <span class="dg-author__thumbPlaceholder" aria-hidden="true"></span>
                            <?php endif; ?>
                        </div>

                        <h3 class="dg-author__docTitle"><?php echo esc_html( $title ); ?></h3>
                        <p class="dg-author__docMeta"><?php echo esc_html( $date ); ?></p>
                    </a>
                </article>
                <?php
            endwhile;
            ?>
        </div>

        <?php
        // Pagination links (pretty permalinks compatible).
        $links = paginate_links(
            array(
                'total'     => max( 1, (int) $docs_q->max_num_pages ),
                'current'   => $paged,
                'type'      => 'list',
                'prev_text' => 'Previous',
                'next_text' => 'Next',
            )
        );

        if ( $links ) : ?>
            <nav class="dg-author__pagination" aria-label="Pagination">
                <?php echo wp_kses_post( $links ); ?>
            </nav>
        <?php endif; ?>

    <?php else : ?>
        <p class="dg-author__muted">No documents found for this author yet.</p>
    <?php
    endif;

    wp_reset_postdata();
    ?>
</section>

    </div>
</div>
    <?php
}

genesis();


Rewrite rule

add_action( 'init', function () {
  add_rewrite_rule(
    '^author/([^/]+)/page/([0-9]+)/?$',
    'index.php?author_name=$matches[1]&paged=$matches[2]',
    'top'
  );
});

Permalinks flushed.


Debugging observations

  • Page 1 works

  • Page 2 never reaches WordPress

  • curl -I /author/slug/page/2/ returns 403 Forbidden

  • Response headers indicate the request is blocked before WordPress runs


Can someone help to fix?

1
  • 1
    You say using cURL, you get a 403 response - in what scenario do the 404 you also mentioned, happen? "Response headers indicate the request is blocked before WordPress runs" - which ones, and how? Commented Jan 23 at 7:34

0

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.