ACF Relationship Field — Loop Related Posts in PHP

Loop through hand-picked related posts stored in an ACF Relationship field. Returns WP_Post objects — render title, permalink, and thumbnail without extra queries.

PHP template Production-tested ~5 min setup ACF Relationship Beginner friendly WP 6.0+ · ACF 6.0+ Updated May 20, 2026
TL;DR

Loop through hand-picked posts stored in an ACF Relationship field.

Use this when editors choose posts manually — "Related Articles", "See Also", or staff picks sections.

01 — Building it

Building it step by step

Create the ACF field first, then build the PHP renderer in 4 small steps — each one runs on its own.

We’ll build this in 4 small steps. Each one is a working piece of code on its own — stop at any step and you have something that runs in the browser.

Step 1: Create the ACF field. Before writing any PHP, open Custom Fields → Add New and build the field group:

  1. Set the Title to Related Posts.
  2. Click Add Field and set:
    • Label: Related Posts
    • Field Name: related_posts
    • Field Type: Relationship
    • Return Format: Post Object
    • Post Type: Post (leave blank for all types)
  3. Scroll to Location → Post Type is equal to Page.
  4. Click Save.
acf-fields-reference.php
php
<?php
// ACF field reference — keep this open while writing PHP below.
//
// Field Name       Type            Returns
// ─────────────────────────────────────────────────────────
// related_posts    Relationship    Array of WP_Post objects
//                                  (empty array if nothing selected)

Step 2: The bare check. get_field('related_posts') returns an empty array when nothing is selected. Wrapping the loop in a check stops a PHP notice on pages where editors haven’t picked any posts yet.

step-2-bare-check.php
php
<?php
$related = get_field( 'related_posts' ); // ACF: Relationship — array of WP_Post objects

if ( $related ) {
    foreach ( $related as $post ) {
        echo $post->ID;
    }
}

Step 3: Add title and permalink. Each item in $related is a standard WP_Post object. Pass it directly to get_the_title() and get_permalink() — no extra database query needed.

step-3-title-link.php
php
<?php
$related = get_field( 'related_posts' ); // ACF: Relationship

if ( $related ) {
    foreach ( $related as $post ) {
        $title = get_the_title( $post );
        $url   = get_permalink( $post );
        ?>
        <a href="<?php echo esc_url( $url ); ?>">
            <?php echo esc_html( $title ); ?>
        </a>
        <?php
    }
}

Step 4: Add the featured image. Wrap get_the_post_thumbnail() in a has_post_thumbnail() check — the card still renders cleanly when a post has no image set.

step-4-thumbnail.php
php
<?php
$related = get_field( 'related_posts' ); // ACF: Relationship

if ( $related ) {
    foreach ( $related as $post ) {
        $title   = get_the_title( $post );
        $url     = get_permalink( $post );
        $has_img = has_post_thumbnail( $post );
        ?>
        <article class="related-post">
            <?php if ( $has_img ) { ?>
                <a href="<?php echo esc_url( $url ); ?>">
                    <?php echo get_the_post_thumbnail( $post, 'medium' ); ?>
                </a>
            <?php } ?>
            <h3>
                <a href="<?php echo esc_url( $url ); ?>"><?php echo esc_html( $title ); ?></a>
            </h3>
        </article>
        <?php
    }
}

Step 5: Everything together. Steps 2–4 are teaching code — each one introduces a single concept. This is the complete production file: all steps combined, wrapped in a section container with an early return guard.

template-parts/sections/related-posts.php
php
<?php
/**
 * Full file: template-parts/sections/related-posts.php
 * Steps 2–4 combined — copy this into your theme as the production version.
 */

// ── 1. Read ACF field ─────────────────────────────────────────────────────────
$related = get_field( 'related_posts' ); // ACF: Relationship

if ( ! $related ) {
    return;
}
?>

<section class="related-posts-section">
    <h2>Related Articles</h2>
    <?php
    // ── 2. Loop and render ────────────────────────────────────────────────────
    foreach ( $related as $post ) {
        $title   = get_the_title( $post );
        $url     = get_permalink( $post );
        $has_img = has_post_thumbnail( $post );
        ?>
        <article class="related-post-card">
            <?php if ( $has_img ) { ?>
                <a href="<?php echo esc_url( $url ); ?>">
                    <?php echo get_the_post_thumbnail( $post, 'medium' ); ?>
                </a>
            <?php } ?>
            <h3><a href="<?php echo esc_url( $url ); ?>"><?php echo esc_html( $title ); ?></a></h3>
        </article>
    <?php } ?>
</section>

Call this file from your page template: get_template_part( 'template-parts/sections/related-posts' );

02 — How it works

How it works

One explanation per build step — what problem each layer solved and which WordPress functions were introduced.

1

Step 2 — The bare check

Problem solved: prevents a PHP notice when editors leave the Relationship field empty. get_field('related_posts') returns an empty array [] when no posts are selected — a plain if ( $related ) check treats an empty array as false, so the loop never runs. No ACF functions were added here; this step is purely defensive PHP before iterating.

2

Step 3 — Title and permalink

Problem solved: turns a raw WP_Post object into a clickable link. The Relationship field with Return Format: Post Object gives you a full WP_Post — you pass it directly to get_permalink() and get_the_title() without a secondary database query. Output is escaped with esc_url() and esc_html() to prevent XSS.

3

Step 4 — Featured image

Problem solved: shows a thumbnail without breaking the layout when a post has no image. has_post_thumbnail() returns true only when a featured image is actually set — the conditional wraps get_the_post_thumbnail() so the card renders cleanly with just the title on image-less posts. The second argument 'medium' tells WordPress which registered image size to return.

03 — Setup

Setup

Create the field group in ACF admin, place the template file, and make your first test.

1

Create the ACF field group

  1. Go to Custom Fields → Add New.
  2. Set the Title to Related Posts.
  3. Click Add Field and configure:
    • Field Type: Relationship
    • Label: Related Posts
    • Field Name: related_posts — must match your PHP exactly
    • Return Format: Post Object
    • Post Type: Post (or leave blank for all)
  4. Set Location → Post Type is equal to the type where you want the field.
  5. Click Save.
2

Create the template file

Create template-parts/sections/related-posts.php inside your theme and paste in the production code from Step 5. Then call it from your page template:

<?php get_template_part( 'template-parts/sections/related-posts' ); ?>
3

Add posts in the editor and test

Open a page, scroll to the Related Posts field, search for and select 2–3 posts, then publish. Visit the page — your cards should appear. If nothing shows, run var_dump( get_field( 'related_posts' ) ) to confirm the field is returning data. A NULL return means the field name in PHP doesn’t match ACF exactly.

The field names in the PHP must match ACF exactly — that’s the Field Name column in ACF admin, not the Label. If you rename a field in ACF, update the PHP to match.

wp-content/themes/your-theme/
│   └── template-parts/
│   │   └── sections/
            └── related-posts.php
04 — Customise

Making it your own

Return format variants, post type filtering, and display cap.

1

Return post IDs instead of objects

In the ACF field settings, set Return Format to Post ID. The loop variable changes from a WP_Post object to a plain integer — update your PHP to pass the ID explicitly: get_permalink( $post_id ), get_the_title( $post_id ). Useful when you only need the ID to pass to an external template function.

2

Restrict to a specific post type

In the ACF field settings, set Post Type to post, project, or any CPT. This restricts the search picker in the editor — editors can only select posts of that type. Useful when you don’t want pages or CPT items mixed into a “Related Articles” picker.

3

Cap the number of displayed posts

Editors can select many posts in a Relationship field. Slice the array before the loop to enforce a max display count:

$related = array_slice( $related, 0, 3 );

This prevents the section overflowing if an editor selects 10 posts on a narrow layout.

Do not change Return Format on an existing live field. ACF stores the raw post ID internally — switching Return Format changes how it’s delivered on read, not what’s stored. Existing data is safe, but double-check your PHP handles the new type before switching on a live site.

0 comments

No comments yet. Be the first.

Leave a Reply

What Is ACF relationship field PHP?

ACF relationship field PHP is one of the most useful patterns in custom WordPress development. This snippet shows how to use the ACF Relationship field to let editors hand-pick related posts from the WordPress admin, then loop through those posts in PHP to render titles, permalinks, and featured images. Unlike a taxonomy or category filter, ACF relationship field PHP lets editors manually select the exact posts they want — ideal for Related Articles, See Also sections, or staff-curated recommendations. The official field reference is at advancedcustomfields.com/resources/relationship.

How ACF relationship field PHP Returns Post Objects

To use ACF relationship field PHP correctly, create a Relationship field in your ACF field group with Return Format set to Post Object. When an editor selects posts in wp-admin, ACF stores the post IDs internally and delivers an array of WP_Post objects when you call get_field('related_posts'). Each item in the array is a standard WP_Post object — pass it directly to get_permalink(), get_the_title(), and get_the_post_thumbnail() without additional database queries. You can also restrict the Relationship field to a specific post type so editors only see one type in the search picker. The WP_Query class reference is useful if you need more complex filtering alongside this pattern.

ACF relationship field PHP Loop with Featured Image

The ACF relationship field PHP snippet builds progressively. Step 2 is a defensive check — if no posts are selected, an empty array is returned and the loop is skipped entirely. Step 3 adds the title and permalink using standard WordPress template functions. Step 4 wraps the featured image in a has_post_thumbnail() guard so the card renders cleanly even when a post has no image set. The final step combines everything into a production template file with an early return guard. Use array_slice() before the loop to enforce a maximum display count, preventing layout overflow when an editor selects too many posts.

ACF relationship field PHP Performance Notes

ACF relationship field PHP is performance-friendly. Because ACF returns fully-loaded WP_Post objects, there are no hidden secondary database queries when you call get_permalink() or get_the_title(). If you need the featured image, WordPress runs one additional query per post to fetch attachment metadata — this is normal and is cached by the WordPress object cache. Leaving Return Format as Post ID when PHP expects a WP_Post object causes integer-not-object errors. The other common issue is a field name mismatch: the PHP must match the Field Name column in ACF admin exactly, not the Label.

Common Mistakes and Best Practices for ACF relationship field PHP

The most common mistake in ACF relationship field PHP: if get_field() returns NULL, run var_dump( get_field('related_posts') ) on the page to confirm the field is registering — a NULL return nearly always means a field name mismatch. Always use an early-return guard before the loop and wrap thumbnail output in has_post_thumbnail(). This pattern is used in the mosharafmanu.com theme for related content sections. For more ACF patterns, browse the full WordPress code snippets collection.

Chat on WhatsApp