ACF Post Source WordPress — Latest or Manual Selection PHP

Build a reusable ACF Flexible Content section where editors can dynamically choose their post source — latest posts or hand-picked selections — without writing a single line of code.

PHP utility Production-tested ~10 min setup Flexible Content WP_Query + ACF WP 6.0+ · ACF 6.0+ Updated May 14, 2026
TL;DR

This ACF post source WordPress PHP snippet lets editors toggle between latest posts and manually selected posts inside an ACF Flexible Content section.

Use this on client sites where editors need control over which content appears — portfolios, featured articles, related projects, team members — without calling a developer every time they want to swap a post.

01 — Building it step by step

ACF post source WordPress — The Code

This ACF post source WordPress snippet starts with the minimal working version and builds to the full implementation.

Each step below is a standalone code snippet that actually works. Start with Step 1 — an 8-line query — and layer on one new concept at a time until you reach the full production version in Step 5.

Step 1: The absolute minimum. A hardcoded WP_Query that fetches 3 posts from the project post type and prints their titles. No ACF. No options. Just prove the query works first.

template-parts/sections/step-1-basic-query.php
php
// ACF post source WordPress — production-tested PHP snippet by Mosharaf Hossain
<?php
/**
 * Step 1 — The absolute minimum.
 *
 * What this does:
 *   Fetches 3 published "project" posts and renders a card for each.
 *   No ACF fields yet. Just a hardcoded WP_Query.
 *
 * Try this first. Once it works, move to Step 2.
 */

// 1. Build a query — ask WordPress for 3 projects.
$query = new WP_Query( [
	'post_type'      => 'project',    // The post type slug.
	'posts_per_page' => 3,            // How many to fetch.
	'post_status'    => 'publish',    // Only published ones.
] );

// 2. If we found posts, loop through them.
if ( $query->have_posts() ) :
	while ( $query->have_posts() ) :
		$query->the_post();
		// Print the title (just to confirm it works).
		echo '<h3>' . esc_html( get_the_title() ) . '</h3>';
	endwhile;

	// 3. ALWAYS reset after your own WP_Query.
	wp_reset_postdata();
endif;

Step 2: Replace hardcoded values with ACF fields. Instead of "project" and 3 being locked forever, the editor picks those values from a Select and a Number field in the admin. The ?: operator provides safe defaults.

template-parts/sections/step-2-acf-post-type.php
php
// ACF post source WordPress
<?php
/**
 * Step 2 — Let the editor control the query.
 *
 * New in this step:
 *   We replaced the hardcoded 'project' and '3' with ACF fields.
 *   Now the editor picks post type and post count from the admin.
 *
 * ACF fields used: post_type (Select), posts_per_page (Number)
 */

// Read the editor's choices.
// ACF field type: Select
$post_type      = get_sub_field( 'post_type' );

// ACF field type: Number
$posts_per_page = get_sub_field( 'posts_per_page' );

// Build the query — now powered by ACF values.
$query = new WP_Query( [
	'post_type'      => $post_type ?: 'post',                // Fallback to "post" if empty.
	'posts_per_page' => $posts_per_page ? (int) $posts_per_page : 6, // Fallback to 6.
	'post_status'    => 'publish',
] );

if ( $query->have_posts() ) :
	while ( $query->have_posts() ) :
		$query->the_post();
		render_post_card( $query->post ); // You'll find this helper function at the end of this section.
	endwhile;
	wp_reset_postdata();
endif;

Step 3: Add taxonomy filtering. Give the editor the option to filter by category. When they pick terms, a tax_query is injected into WP_Query. When they leave it blank, all posts show up. Also introduces order switching — date order with filters, menu_order without.

template-parts/sections/step-3-taxonomy-filter.php
php
// ACF post source WordPress
<?php
/**
 * Step 3 — Filter by category/term.
 *
 * New in this step:
 *   If the editor picks taxonomy terms, we filter the query to only
 *   show posts in those categories. Otherwise, query everything.
 *
 * ACF fields added: taxonomy_terms (Taxonomy)
 */

// ACF field type: Select
$post_type      = get_sub_field( 'post_type' );

// ACF field type: Number
$posts_per_page = get_sub_field( 'posts_per_page' );

// ACF field type: Taxonomy  — returns an array of term IDs, or null.
$taxonomy_terms = get_sub_field( 'taxonomy_terms' );

// Start building the query.
$query_args = [
	'post_type'      => $post_type ?: 'post',
	'posts_per_page' => $posts_per_page ? (int) $posts_per_page : 6,
	'post_status'    => 'publish',
];

// ── If the editor selected terms, add a tax_query filter. ──
if ( ! empty( $taxonomy_terms ) ) {

	// tax_query tells WP_Query: "only show posts in THESE categories."
	$query_args['tax_query'] = [
		[
			'taxonomy' => 'category',   // Change to your taxonomy slug.
			'field'    => 'term_id',     // Must match ACF return_format.
			'terms'    => $taxonomy_terms,
		],
	];

	// With category filters, newest-first makes more sense.
	$query_args['orderby'] = 'date';
	$query_args['order']   = 'DESC';

} else {

	// No filter — order by the "Order" value in Page Attributes.
	$query_args['orderby'] = 'menu_order';
	$query_args['order']   = 'ASC';

}

// Run the query and display results.
$query = new WP_Query( $query_args );

if ( $query->have_posts() ) :
	while ( $query->have_posts() ) :
		$query->the_post();
		render_post_card( $query->post ); // You'll find this helper function at the end of this section.
	endwhile;
	wp_reset_postdata();
else :
	echo '<p>No posts found.</p>';
endif;

Step 4: Add manual post selection. A completely different approach — no WP_Query at all. The editor hand-picks specific posts in the Relationship field, and a foreach loop renders them in their exact chosen order. This is a standalone snippet.

template-parts/sections/step-4-manual-posts.php
php
// ACF post source WordPress
<?php
/**
 * Step 4 — Manual mode: show hand-picked posts.
 *
 * New in this step:
 *   Instead of querying, we loop through post IDs the editor hand-picked
 *   in the ACF Relationship field. The order is whatever the editor chose.
 *
 * ACF fields used: manual_posts (Relationship)
 *
 * This is a standalone snippet. It does NOT include the latest-mode query
 * from Step 3 — that comes in Step 5 when we combine both.
 */

// ACF field type: Relationship  — returns an array of post IDs.
$manual_posts = get_sub_field( 'manual_posts' );

// Only run if the editor actually selected some posts.
if ( ! empty( $manual_posts ) ) :

	// Loop through each post ID in the editor's chosen order.
	foreach ( $manual_posts as $post_id ) :

		// get_post() loads the full WP_Post object from the ID.
		$post = get_post( $post_id );

		// setup_postdata() makes template tags work:
		//   get_the_title( $post_id ), get_permalink( $post_id ), etc.
		setup_postdata( $post );

		// Render the card using our helper.
		render_post_card( $post ); // You'll find this helper function at the end of this section.

	endforeach;

	// MANDATORY: Reset the global $post back to the current page.
	wp_reset_postdata();

endif;

Step 5: Combine everything with the data_source toggle. The Button Group field acts as a switch. When set to "latest", it runs the WP_Query (Steps 2–3). When set to "manual", it runs the Relationship loop (Step 4). The editor sees only relevant fields thanks to ACF conditional logic.

This is the one you use in production.

template-parts/sections/post-source.php
php
// ACF post source WordPress
<?php
/**
 * Step 5 — The full, final version.
 *
 * This combines Steps 2–4 into one file with a Button Group toggle.
 * The editor chooses one of two modes from the admin:
 *   "latest"   → runs the dynamic WP_Query (Steps 2 + 3).
 *   "manual"   → shows hand-picked posts (Step 4).
 *
 * All ACF fields are now used:
 *   Text          — section_heading
 *   Button Group   — data_source (the mode toggle)
 *   Select         — post_type
 *   Number         — posts_per_page
 *   Taxonomy       — taxonomy_terms
 *   Relationship   — manual_posts
 */

// ── Read every field the editor configured ───────────────────────────

// The section title shown above the post grid.
// ACF field type: Text
$section_heading = get_sub_field( 'section_heading' );

// "latest" or "manual" — the editor picks one.
// ACF field type: Button Group
$data_source     = get_sub_field( 'data_source' );

// Which post type to query: post, project, snippet, etc.
// ACF field type: Select
$post_type       = get_sub_field( 'post_type' );

// How many posts to show (latest mode only).
// ACF field type: Number
$posts_per_page  = get_sub_field( 'posts_per_page' );

// Category terms to filter by (latest mode only).
// ACF field type: Taxonomy
$taxonomy_terms  = get_sub_field( 'taxonomy_terms' );

// Hand-picked post IDs (manual mode only).
// ACF field type: Relationship
$manual_posts    = get_sub_field( 'manual_posts' );
?>

<section class="dynamic-post-section">

	<?php if ( $section_heading ) : ?>
		<h2 class="section-heading">
			<?php echo esc_html( $section_heading ); ?>
		</h2>
	<?php endif; ?>

	<?php
	// ── MODE A: Manual — render hand-picked posts ──────────────────
	if ( 'manual' === $data_source && ! empty( $manual_posts ) ) :

		foreach ( $manual_posts as $post_id ) :
			$post = get_post( $post_id );
			setup_postdata( $post );
			render_post_card( $post ); // Defined below. Jump to the helper at the end of this section.
		endforeach;

		wp_reset_postdata();

	// ── MODE B: Latest — run a dynamic WP_Query ────────────────────
	elseif ( 'latest' === $data_source ) :

		$query_args = [
			'post_type'      => $post_type ?: 'post',
			'posts_per_page' => $posts_per_page ? (int) $posts_per_page : 6,
			'post_status'    => 'publish',
		];

		if ( ! empty( $taxonomy_terms ) ) {
			$query_args['tax_query'] = [
				[
					'taxonomy' => 'category',
					'field'    => 'term_id',
					'terms'    => $taxonomy_terms,
				],
			];
			$query_args['orderby'] = 'date';
			$query_args['order']   = 'DESC';
		} else {
			$query_args['orderby'] = 'menu_order';
			$query_args['order']   = 'ASC';
		}

		$dynamic_query = new WP_Query( $query_args );

		if ( $dynamic_query->have_posts() ) :
			while ( $dynamic_query->have_posts() ) :
				$dynamic_query->the_post();
				render_post_card( $dynamic_query->post ); // Defined below. Jump to the helper at the end of this section.
			endwhile;
			wp_reset_postdata();
		else :
			echo '<p>No posts found.</p>';
		endif;

	endif;
	?>
</section>

The helper function. Every step above calls render_post_card() to output markup. This is your design layer — customize the HTML, CSS, and fields here. Both modes call this identically.

template-parts/sections/post-card.php
php
// ACF post source WordPress
<?php
/**
 * Renders a single post card — thumbnail, title, date, excerpt.
 *
 * This function is used by EVERY mode (latest and manual both call it).
 * Customize the HTML here to match your theme.
 *
 * @param WP_Post $post  The post to display.
 */
function render_post_card( $post ) {
	$post_id = $post->ID;
	?>
	<article class="post-card">
		<?php if ( has_post_thumbnail( $post_id ) ) : ?>
			<a href="<?php echo esc_url( get_permalink( $post_id ) ); ?>"
			   class="post-card__image" aria-hidden="true" tabindex="-1">
				<?php
				// get_the_post_thumbnail() returns a safe <img> tag.
				// Change 'medium_large' to any registered image size.
				echo get_the_post_thumbnail( $post_id, 'medium_large' );
				?>
			</a>
		<?php endif; ?>
		<div class="post-card__content">
			<h3 class="post-card__title">
				<a href="<?php echo esc_url( get_permalink( $post_id ) ); ?>">
					<?php echo esc_html( get_the_title( $post_id ) ); ?>
				</a>
			</h3>
			<time datetime="<?php echo esc_attr( get_the_date( 'c', $post_id ) ); ?>">
				<?php echo esc_html( get_the_date( '', $post_id ) ); ?>
			</time>
			<p class="post-card__excerpt">
				<?php echo esc_html( wp_trim_words( get_the_excerpt( $post_id ), 20 ) ); ?>
			</p>
		</div>
	</article>
	<?php
}

Where to put the final code

Place Step 5’s code and the helper function inside your Flexible Content renderer — typically template-parts/sections/rendering-loop.php. Call it when get_row_layout() === 'dynamic_post_source'. Steps 1–4 are for learning only; you only need Step 5 + the helper in production.

02 — How it works

How ACF post source WordPress Works — Why Each Layer Exists

Here is how this ACF post source WordPress works step by step: We built this in 5 layers. Each layer adds exactly one concept. Let's walk through why each layer exists and what problem it solves.

1

Start small — render posts from any post type

The first thing you need is a working query. WP_Query is WordPress’s way of asking the database “give me these posts.” You pass an array of arguments — post type, how many, what status — and it returns matching posts.

The while( $query->have_posts() ) loop is the standard WordPress pattern. the_post() loads each result into memory one at a time. wp_reset_postdata() at the end tells WordPress “I’m done, go back to the page the user is viewing.” Skip it and your footer breaks.

We hardcoded "project" and 3 here so you can verify the query works before adding complexity. If you can see post titles on the page, the foundation is solid.

2

Let the editor choose post type and count via ACF

Hardcoded values are fragile — to change “project” to “post”, you edit code. That’s not how client sites work. Editors need a dropdown.

ACF’s get_sub_field() reads values from the current Flexible Content layout row. Unlike get_field() (which reads from the page), get_sub_field() reads from inside the Flexible Content loop. This means you can have multiple sections on the same page, each with different post types.

The ?: operator is a safety net: “if the left side is empty, use the right side.” If the editor never sets posts_per_page, it defaults to 6. We cast it to (int) because ACF Number fields return strings.

3

Add taxonomy filtering with tax_query

Sometimes the editor doesn’t want “all posts” — they want “only posts about web design.” That’s where tax_query comes in.

ACF’s Taxonomy field returns an array of term IDs when return_format is set to "id". (If you set it to "slug", change field => "term_id" to field => "slug" — they must match.)

The order logic changes here: with a category filter, readers expect newest posts first (date DESC). Without a filter, menu_order ASC lets editors control sequence through the Order metabox — useful for curated lists.

4

Add manual post selection — total editor control

Latest-mode queries are great, but sometimes the editor wants total control — “show exactly these 4 posts, in this exact order.” No query can do that. You need manual selection.

ACF’s Relationship field stores only post IDs (not full objects — that would be wasteful). The editor drags and drops posts into their desired order. get_post() loads a WP_Post from each ID, and setup_postdata() makes template tags work inside the loop.

After the foreach, wp_reset_postdata() is absolutely mandatory — without it, everything below this section displays wrong content.

5

The data_source toggle — combine both modes

Now we have two separate code paths — one for querying and one for manual selection. The data_source Button Group is the switch: "latest" runs the WP_Query branch, "manual" runs the Relationship loop.

ACF’s conditional logic keeps the admin clean: when the editor picks “Latest Posts,” the Relationship field hides. When they pick “Manual Selection,” the Number and Taxonomy fields hide. Editors only see what they need.

The if/elseif structure ensures only one branch runs per page load. The Button Group always has a value — it defaults to "latest".

6

The helper function — your design layer

The render_post_card() function is your design system. Every mode — latest and manual — calls this same function. Change it once, and both modes update.

Every output must be escaped: esc_html() for plain text, esc_url() for links, esc_attr() for HTML attributes. The only safe function without extra escaping is get_the_post_thumbnail() — it returns sanitized HTML.

Customize the markup here. Swap medium_large for any image size. Change get_the_excerpt() to get_the_content(). Add CSS classes. The query logic doesn’t care what the card looks like.

7

Why wp_reset_postdata() is the #1 thing to remember

This is the #1 bug in WordPress custom queries. After any WP_Query or setup_postdata() loop, the global $post variable has been changed. If you don’t call wp_reset_postdata(), everything below your section — footer widgets, sidebar menus, related post lists — will show data from the last post in your loop instead of the actual page.

The symptom is confusing: your footer suddenly shows a blog post title instead of your copyright text. You’ll think it’s a template bug. It’s not. You forgot wp_reset_postdata().

03 — ACF field setup

ACF post source WordPress — Setup & Integration

To integrate this ACF post source WordPress into your project: Build the Flexible Content layout and its six sub-fields through the ACF admin or import the JSON reference.

This section needs a Flexible Content layout inside one of your ACF field groups. The layout contains six sub-fields that the editor fills out for each instance of the section. You can build this through the ACF admin UI or import the JSON shown below.

The layout name (dynamic_post_source) doesn’t matter to the code — it only matters in your Flexible Content renderer where you check get_row_layout().

wp-content/themes/your-theme/
│   └── template-parts/
│   │   └── sections/
│   │   │   ├── post-source.php
            └── post-card.php

Here’s a quick reference of all six sub-fields:

Field Name Type Purpose Shows When
section_heading Text Optional heading above the posts Always
data_source Button Group Switches Latest/Manual mode Always
post_type Select Which post type to query Always
posts_per_page Number How many posts to show Latest only
taxonomy_terms Taxonomy Filter by category/term Latest only
manual_posts Relationship Hand-pick posts in order Manual only
1

Create a Flexible Content field group

Open any existing ACF field group or create a new one. Add a Flexible Content field — name it page_sections or whatever fits your project. This one field will hold all your reusable page sections.

2

Add the Dynamic Post Source layout

Inside the Flexible Content field, click Add Layout. Name it “Dynamic Post Source”. The machine-readable name will be dynamic_post_source. This is what you check with get_row_layout() in your renderer.

3

Add section_heading (Text field)

Add a Text field named section_heading. No conditional logic — it always appears. Set a placeholder like “Featured Projects” so editors know what to type.

4

Add data_source (Button Group)

Add a Button Group field named data_source. Set two choices: latest : Latest Posts and manual : Manual Selection. Default to latest. Use horizontal layout for cleaner UX. This is the central switch — every conditional rule depends on it.

5

Add post_type (Select field)

Add a Select field named post_type. Add choices for each registered post type (e.g., post : Posts, project : Projects). Set return_format to value (returns the slug). Enable Stylized UI.

6

Add posts_per_page (Number field)

Add a Number field named posts_per_page. Default: 6. Min: 1. Max: 50. Add conditional logic: show only when data_source == latest.

7

Add taxonomy_terms (Taxonomy field)

Add a Taxonomy field named taxonomy_terms. Set taxonomy to Category. Use Multi Select. Set return_format to Term ID — critical because our tax_query uses field => term_id. Show only when data_source == latest.

8

Add manual_posts (Relationship field)

Add a Relationship field named manual_posts. Select post types editors can pick from. Enable Featured Image as an element. Set return_format to Post ID. Show only when data_source == manual.

Conditional logic keeps the admin clean. When the editor picks “Manual Selection,” the Number and Taxonomy fields disappear. When they pick “Latest Posts,” the Relationship field disappears. Editors only see what they need.

Using the ACF JSON file

Copy the JSON code below into a new file at:

wp-content/themes/your-theme/acf-json/group_dynamic_post_source.json

ACF automatically detects JSON files in your theme’s acf-json/ folder and syncs them. The "local": "json" key inside the file tells ACF to keep this field group synced from the JSON file — changes stay version-controlled and won’t be overwritten by database edits.

After placing the file, go to Custom Fields → Tools → Import Field Groups and click Import. You’ll then see:

  • The field group Dynamic Post Source with a Flexible Content field called Page Sections inside it
  • One layout: Dynamic Post Source with all six sub-fields (heading, data source, post type, posts per page, taxonomy terms, manual posts)
  • Conditional logic already wired up between fields (just browse the admin to see how)

If you need to change anything — add more post types to the Select field, switch the taxonomy from category to a custom slug, add a second layout — you can either edit the JSON directly and re-import, or click Sync Available in the ACF admin to pull changes from the file.

acf-json/group_dynamic_post_source.json
json
// ACF post source WordPress — production-tested PHP snippet by Mosharaf Hossain
{
    "key": "group_dynamic_post_source",
    "title": "Dynamic Post Source",
    "fields": [
        {
            "key": "field_page_sections_flexible",
            "label": "Page Sections",
            "name": "page_sections",
            "type": "flexible_content",
            "instructions": "Build your page by adding section layouts. Drag to reorder.",
            "button_label": "Add Section",
            "layouts": {
                "layout_dynamic_post_source": {
                    "key": "layout_dynamic_post_source",
                    "name": "dynamic_post_source",
                    "label": "Dynamic Post Source",
                    "display": "block",
                    "sub_fields": [
                        {
                            "key": "field_section_heading",
                            "label": "Section Heading",
                            "name": "section_heading",
                            "type": "text",
                            "placeholder": "Featured Projects"
                        },
                        {
                            "key": "field_data_source",
                            "label": "Data Source",
                            "name": "data_source",
                            "type": "button_group",
                            "choices": {
                                "latest": "Latest Posts",
                                "manual": "Manual Selection"
                            },
                            "default_value": "latest",
                            "layout": "horizontal"
                        },
                        {
                            "key": "field_post_type",
                            "label": "Post Type",
                            "name": "post_type",
                            "type": "select",
                            "choices": {
                                "post": "Posts",
                                "project": "Projects",
                                "snippet": "Snippets"
                            },
                            "default_value": "post",
                            "allow_null": 0,
                            "multiple": 0,
                            "ui": 1,
                            "return_format": "value"
                        },
                        {
                            "key": "field_posts_per_page",
                            "label": "Posts Per Page",
                            "name": "posts_per_page",
                            "type": "number",
                            "default_value": 6,
                            "min": 1,
                            "max": 50,
                            "conditional_logic": [
                                [
                                    {
                                        "field": "field_data_source",
                                        "operator": "==",
                                        "value": "latest"
                                    }
                                ]
                            ]
                        },
                        {
                            "key": "field_taxonomy_terms",
                            "label": "Filter by Terms",
                            "name": "taxonomy_terms",
                            "type": "taxonomy",
                            "taxonomy": "category",
                            "field_type": "multi_select",
                            "add_term": 0,
                            "save_terms": 0,
                            "load_terms": 0,
                            "return_format": "id",
                            "multiple": 1,
                            "conditional_logic": [
                                [
                                    {
                                        "field": "field_data_source",
                                        "operator": "==",
                                        "value": "latest"
                                    }
                                ]
                            ]
                        },
                        {
                            "key": "field_manual_posts",
                            "label": "Manual Posts",
                            "name": "manual_posts",
                            "type": "relationship",
                            "post_type": ["post", "project", "snippet"],
                            "filters": ["search"],
                            "return_format": "id",
                            "elements": ["featured_image"],
                            "conditional_logic": [
                                [
                                    {
                                        "field": "field_data_source",
                                        "operator": "==",
                                        "value": "manual"
                                    }
                                ]
                            ]
                        }
                    ],
                    "min": "0",
                    "max": ""
                }
            }
        }
    ],
    "location": [
        [
            {
                "param": "post_type",
                "operator": "==",
                "value": "page"
            }
        ]
    ],
    "menu_order": 0,
    "position": "normal",
    "style": "default",
    "label_placement": "top",
    "instruction_placement": "label",
    "hide_on_screen": "",
    "active": true,
    "description": "Flexible Content field group with a Dynamic Post Source layout — lets editors choose between latest posts or manual selection.",
    "show_in_rest": 0,
    "local": "json"
}
04 — Making it your own

Making This ACF post source WordPress Your Own

Customise this ACF post source WordPress to match your specific needs: The core system is working — now adapt it to your design, post types, and taxonomy structure.

1

Customize the post card markup

The render_post_card() function is where your design system lives. Replace the markup with your own card component — grid items, list rows, masonry tiles, whatever fits your theme.

Key functions inside the card:

  • has_post_thumbnail() — checks if a featured image exists
  • esc_url( get_permalink() ) — safe permalink output
  • esc_html( get_the_title() ) — safe title output
  • get_the_post_thumbnail( $post_id, $size ) — returns safe <img> HTML

You can pass any registered image size: thumbnail, medium, medium_large, large, or a custom size from add_image_size().

2

Add support for custom post types

To support additional post types:

  1. In ACF: Edit the post_type Select field and add choices. Also edit the manual_posts Relationship field’s post type filter.
  2. In your renderer: No changes needed — the code uses $post_type dynamically. WP_Query handles any registered public post type.
3

Switch to a custom taxonomy

To use a custom taxonomy (e.g., project_category):

  1. ACF field: Edit the taxonomy_terms Taxonomy field and change its taxonomy setting.
  2. Renderer code: Change tax_query‘s taxonomy value from "category" to your taxonomy slug.

3 common mistakes:

1. Forgetting wp_reset_postdata()
After any custom query, always reset. Without it, your footer and sidebar show wrong content.

2. Using get_field() instead of get_sub_field()
Inside a Flexible Content loop, always use get_sub_field(). get_field() may return the parent post’s values instead.

3. Not escaping output
Every string from ACF must be escaped: esc_html() for text, esc_url() for URLs, esc_attr() for attributes.

Performance notes:

  • Relationship field is lightweight: It stores only post IDs, not full objects. We fetch data on-demand with get_post().
  • WP_Query is indexed: Default query parameters are indexed in the database. Keep posts_per_page under 50 and performance stays fine.
  • Consider caching: On high-traffic pages, wrap the query block in a fragment cache or transient.
  • Need an offset? Add 'offset' => N or 'ignore_sticky_posts' => true to $query_args.
0 comments

No comments yet. Be the first.

Leave a Reply

Table of Contents

ACF post source WordPress — Code Snippet

ACF post source WordPress — code snippet by Mosharaf Hossain

This ACF post source WordPress is a production-tested PHP snippet for WordPress developers. See the ACF documentation for related official documentation.

The ACF post source WordPress snippet creates a flexible content section where an editor can toggle between two post display modes from the WordPress admin — Latest Posts for automatic always-current results and Manual Selection for hand-picked editor-controlled results — using a single ACF Button Group field. The section renders identically from the front-end regardless of which mode is active; only the data source changes.

The problem this snippet solves is that post display sections on marketing sites often need to behave differently on different pages. A homepage might always want the three most recent case studies. A campaign landing page might need three specific posts hand-picked by the marketing team regardless of publication date. Building two separate sections duplicates template logic and requires a developer for every content change. Building a single section with a toggle eliminates the duplication and gives editors complete control.

The ACF field group has three fields. A Button Group field named data_source with choices for latest and manual controls the mode. A Number field named posts_count, visible only when the mode is latest, sets how many posts to fetch. A Relationship field named manual_posts, visible only when the mode is manual, stores the hand-picked post IDs. Conditional logic in ACF admin shows and hides the second and third fields based on the first field value.

The PHP template reads the data_source value and branches into two code paths. The latest path runs a WP_Query with posts_per_page set from the number field and post_type configured by the template. The manual path reads the Relationship field, which returns an array of post objects, and passes each object directly to the post card renderer without running a new database query. Both paths output the same post card HTML so the visual result is identical to the editor and the visitor.

The post card rendering uses a shared helper function so card markup is not duplicated between the two code paths. If the data source changes from latest to manual, the rendered HTML structure does not change — only the content inside the cards changes, driven entirely by the editor’s selection.

Tested on WordPress 6.0+ with ACF 6.0+ and PHP 8.0+. The ACF JSON field group export is included in the snippet for one-click import. Browse all ACF post source snippets at Code Snippets or see the ACF Flexible Content Renderer for the broader layout loading pattern. This section powers the featured work display on the Vitamines project.

Related: ACF Flexible Content Renderer snippet — and browse the full code snippet library.

Chat on WhatsApp