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 correctlyPage 1 shows the correct posts
Pagination links are generated correctly
What doesn’t work
/author/author-slug/page/2/returns 404 / ForbiddenWordPress 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:
documentAuthor page shows documents where:
_dg_document_writer_id = author_idOR_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 ForbiddenResponse headers indicate the request is blocked before WordPress runs