Building a ACF Flexible Content Homepage, I'm stuck

Hi everyone,

I’m in the process of redesigning and building the homepage for a magazine like website. Right now the Homepage is rather static and boring, you can change the featured articles above the fold and a couple of other things, but we want something more flexible.

My idea was to build it out using ACF’s Flexible Content Fields, where I have a featured article and the 4 latests posts above the fold, and underneath that, the editors will have access to modules that they can drag&drop that display the latest 4 items of a certain category, tag, or 4 specific posts, as well as some newsletter forms etc.

The problem is: the ACF Flexible content field combined with separating all the logic is kicking my butt.
I’ve searched this forum far and wide, and can’t quite seem to figure out how to implement it the sage way. I took this topic: and figured I could do the same for my homepage, but all the queries i have to do make it rather difficult. Also, I don’t want any duplicate content on the page, so I store that in an array called do_not_duplicate.

Right now, I have the basics working, but not in the sage way:
front-page.blade.php:

@extends('layouts.app')

@section('content')
    @while (have_posts()) @php the_post() @endphp
        <div class="above-the-fold">
            <div class="featured-item">
                @while ($featured_items->have_posts())
                    @php($featured_items->the_post())
                    @include('partials.feat-item-display')

                @endwhile
                @php($do_not_duplicate = wp_list_pluck($featured_items->posts, 'ID'))
            </div>
            <div class="latest-post">
                <h4 class="cat-header">{{ _e('Latest', 'IO3') }}</h4>
                @while ($latest_posts_top->have_posts())
                    @php($latest_posts_top->the_post())
                    @include ('partials.title-display')
                @endwhile
                @php($do_not_duplicate = array_merge($do_not_duplicate, wp_list_pluck($latest_posts_top->posts, 'ID')))
            </div>
        </div>
        <div class="under-the-fold">
            @php($get_home_page_content = get_field('homepage_content', 'option'))
            @if ($get_home_page_content)
                @foreach ($get_home_page_content as $block)
                    @if ($block['acf_fc_layout'] == 'category')
                        <div class="category">
                            @php($home_category = get_term($block['fp_category']))

                            <h4 class="category-header"><a
                                    href="{{ get_term_link($block['fp_category']) }}">{{ $home_category->name }}</a>
                            </h4>
                            @php(
    $homepage_cat_query = new WP_Query([
        'post_type' => 'post',
        'posts_per_page' => 4,
        'ignore_sticky_posts' => true,
        // 'no_found_rows' => true,
        'cat' => $block['fp_category'],
        'post__not_in' => $do_not_duplicate,
    ]),
)
                            <div class="home-page-category">
                                @while ($homepage_cat_query->have_posts())
                                    @php($homepage_cat_query->the_post())
                                    @include ('partials.title-display')
                                @endwhile
                                @php($do_not_duplicate = array_merge($do_not_duplicate, wp_list_pluck($homepage_cat_query->posts, 'ID')))
                                {{-- @php(print_r(wp_list_pluck($homepage_cat_query->posts, 'ID'))) --}}
                            </div>
                        </div>
                    @elseif ($block['acf_fc_layout'] == 'tag')
                        <div class="tag">
                            @php($home_tag = get_term($block['fp_tag']))
                            <h4 class="tag-header"><a
                                    href="{{ get_term_link($block['fp_tag']) }}">{{ $home_tag->name }}</a></h4>
                            @php(
    $homepage_tag_query = new WP_Query([
        'post_type' => 'post',
        'posts_per_page' => 4,
        'ignore_sticky_posts' => true,
        'no_found_rows' => true,
        'tag_id' => $block['fp_tag'],
        'post__not_in' => [$do_not_duplicate],
    ]),
)
                            <div class="home-page-tag">
                                @while ($homepage_tag_query->have_posts())
                                    @php($homepage_tag_query->the_post())
                                    @include ('partials.title-display')
                                @endwhile
                                @php($do_not_duplicate = array_merge($do_not_duplicate, wp_list_pluck($homepage_tag_query->posts, 'ID')))
                            </div>
                        </div>

                    @elseif ($block['acf_fc_layout'] == 'dossier')
                        <div class="dossier">
                            @php($home_dossier = get_term($block['fp_dossier']))
                            <h4 class="dossier-header"><a
                                    href="{{ get_term_link($block['fp_dossier']) }}">{{ $home_dossier->name }}</a></h4>
                            @php(
    $homepage_dossier_query = new WP_Query([
        'post_type' => 'post',
        'posts_per_page' => 4,
        'ignore_sticky_posts' => true,
        'no_found_rows' => true,
        'tax_query' => [
            [
                'taxonomy' => 'dossier',
                'terms' => $block['fp_dossier'],
            ],
        ],
        'post__not_in' => $do_not_duplicate,
    ]),
)
                            <div class="home-page-dossier">
                                @while ($homepage_dossier_query->have_posts())
                                    @php($homepage_dossier_query->the_post())
                                    @include ('partials.title-display')
                                @endwhile
                                @php($do_not_duplicate = array_merge($do_not_duplicate, wp_list_pluck($homepage_dossier_query->posts, 'ID')))
                            </div>
                        </div>
                    @endif
                @endforeach
            @endif
        </div>
    @endwhile

@endsection

FrontPage.php has this function that takes care of the Flexible Content:

public function getHomePageContent()
  {
    $get_home_page_content = get_field('homepage_content', "option");
    $data = [];

    if ($get_home_page_content) {
      foreach ($get_home_page_content as $block) {
        if ($block['acf_fc_layout'] == 'category') {
          $this_block = (object) [
            'block_type' => $block['acf_fc_layout'],
            'category' => $block['fp_category'],
          ];
          array_push($data, $this_block);
        } elseif ($block['acf_fc_layout'] == 'tag') {
          $this_block = (object) [
            'block_type' => $block['acf_fc_layout'],
            'tag' => $block['fp_tag'],
          ];
          array_push($data, $this_block);
        }
        elseif ($block['acf_fc_layout'] == 'dossier') {
          $this_block = (object) [
            'block_type' => $block['acf_fc_layout'],
            'dossier' => $block['fp_dossier'],
          ];
          array_push($data, $this_block);
        } 
       
        $data = (object) $data;
        return $data;
      }
    }
  }

The rest of the code are just some functions that return a new WP_Query, and some partials that display the posts.
The acf fields used are just taxonomy fields and some text.

Obviously, this is not how I want the code to look, I want to have a partial for each of the different layout types, but that is where a couple of problems arise:

  • My do_not_duplicate array isn’t updating correctly (it will only add the post id’s inside the partial, so every partial has its own version of that array)
  • If I put the WP_Query in the .php file where it belongs, it always returns the same category/tag/customtax for every new layout.

I felt quite comfortable in sage until now, but I’m far from an expert in blade. This seemed like a simple thing to do when I was planning it, but I am beginning to think it might be out of my reach. If anyone could help me going in the right direction, that would be awesome!

This flow (as I understand it) is a little odd, since it seems to mean that the contents of each block are dependent on the block that appears before it, which could (theoretically) mean that later blocks would have no content (all posts that could have appeared in them were “taken” by preceding blocks). To me, that seems like a mechanism that could be potentially confusing to an editor.

I don’t 100% understand your use case, but it seems to be:

  • You have a Flexible Content Field where a user can put any number of blocks in whatever order they choose.
  • Each block allows the user to select a single taxonomy term.
  • On the home page, each block should display a set number of posts that belong to the selected taxonomy term.
  • You don’t want the same post to appear in more than one block.
  • There are some other “featured post” and “lastest post” sections that aren’t defined by your blocks, but nonetheless should also not contain duplicate posts.

Opinion Warning

When I’m using Blade for anything more complicated than a single archive loop I don’t use the loop, which is to say you won’t see have_posts() or the_post() or the_permalink() or the_title(). All data is passed directly to the partial and {{ $echoed }} (for things like images, I pass the ID or parameters and do something like {{ wp_get_attachment_image( $id ) }}). This approach can vastly simplify problems like the one you appear to be having.

In your example, this would probably look something like:

// YourController.php
public function getAllBlocks()
{
    $allBlocks = [];
    $post__not_in = [];
    $allBlocks['featured_posts'] => get_posts([
        // Whatever the query args are to get your featured posts.
    ]);
    // Save these IDs...
    $post__not_in = array_merge($post__not_in, array_column($allBlocks['featured_posts'], 'ID'));

    $allBlocks['latest_posts'] => get_posts([
        // Whatever the query args are to get your latest posts.
        'post__not_in' => $post__not_in,
    ]);
    // Save these IDs...
    $post__not_in = array_merge($post__not_in, array_column($allBlocks['latest_posts'], 'ID'));

    $allBlocks['flexBlocks'] = [];
    foreach (get_field('homepage_content', 'option') as $flexBlock) {
        switch ($flexBlock['acf_fc_layout'] {
            case 'category':
                $categoryPosts = get_posts([
                    // Whatever the query should be...
                    'post__not_in' => $post__not_in,
                ]);
                $allBlocks['flexBlocks'][] = [
                    'posts' => $categoryPosts;
                    'type' => 'category',
                ];
                $post__not_in = array_merge($post__not_in, array_column($categoryPosts, 'ID'));
                unset($categoryPosts);
                break;
            case 'tag':
                // Tag logic similar to above
                break;
            case 'dossier':
                // Dossier logic similar to above
                break;
        }

    return $allBlocks;
}

public function with() {
    return [ 'allBlocks' => $this->getAllBlocks() ];
}

Then your blade can look something like this:

<div class="featured_posts">
    @foreach($allBlocks['featured_posts'] as $post)
        @include('partials.featured-post')
    @endforeach
</div>
<div class="latest_posts">
    @foreach($allBlocks['latest_posts'] as $post)
        @include('partials.latest-post')
    @endforeach
</div>
<div class="blocks">
    @foreach($allBlocks['flexBlocks'] as $block)
        @include('partials.flex-blocks.' . $block['type'], $block)
    @endforeach
</div>

Thanks a lot for the detailed answer, it seems that you understood my use case very well.
I have a couple of follow-up questions about the code you posted, as I am getting a couple of errors on my end when trying to implement it.

This is giving me a unexpected ‘=>’ error

$allBlocks['featured_posts'] => get_posts([
        // Whatever the query args are to get your featured posts.
    ]);

I replaced it with:

$featured_posts = get_posts([
        // The query args
]);
$allBlocks['featured_posts'] = $featured_posts;

But I am getting these errors:

  • Notice: Undefined variable: allBlocks
  • Notice: Trying to access array offset on value of type null
  • Warning: Invalid argument supplied for foreach()

About your opinion warning, I just want to be sure I understand you fully and if i have my partial flex-blocks-category.blade.php like this:

<div class="home-page-category">
  @php($term = get_term($block['taxonomy']))
  <h4 class="category-header"><a href="{{ $term->slug }}">{{ $term->name }}</a></h4>
  <div class="home-page-category-posts">
    @foreach($allBlocks['flexBlocks']['posts'] as $post)
      @include('partials.block-post-display')
    @endforeach
  </div>
</div>

And my partial block-post-display.blade.php like this:

<article @php post_class() @endphp>
  <div class="post-image"><a href="{{ get_permalink($id) }}">{{ get_the_post_thumbnail('$id','medium') }}></a></div>
  <h2 class="post-title"><a href="{{ get_permalink($id) }}">{{ get_the_title($id) }}</a></h2>
</article>

It should work right?
Thanks again!

Ps. i added $block[‘taxonomy’] inside of here:

$allBlocks['flexBlocks'][] = [
  'posts' => $categoryPosts,
  'type' => 'category',
  'taxonomy' => $flexBlock['fp_category']
];

Pps. I am not familiar with this function: if I search for it I come across Sage 10 posts, I should have mentioned that I am working with Sage 9, sorry. (I don’t know if that matters in this case.)

public function with() {
      return [ 'allBlocks' => $this->getAllBlocks() ];
  }

@alwaysblank Thanks a lot! With some more coffee and carefull reading, It works like a dream! Now I can add some extra blocks and give the editors some flexibility in the site layout, without them breaking stuff!
I’ll share my full code below in case anyone comes across the same problems I faced.

front-page.blade.php:

@extends('layouts.app')

@section('content')
    @while (have_posts()) @php the_post() @endphp
      <div class="featured_posts">
        {{-- @php(print_r($get_all_blocks['featured_posts'])) --}}
        @foreach($get_all_blocks['featured_posts'] as $post)
            @include('partials.featured-post')
        @endforeach
    </div>
    <div class="latest_posts">
        @foreach($get_all_blocks['latest_posts'] as $post)
            @include('partials.latest-post')
        @endforeach
    </div>
    <div class="blocks">
        @foreach($get_all_blocks['flexBlocks'] as $block)
            @include('partials.flex-blocks.' . $block['type'], $block)
        @endforeach
    </div>
    @endwhile
@endsection

/partials/flex-blocks/category.blade.php (the other ones are mostly the same, apart from some headers/links):

<div class="home-page-category">
  @php($term = get_term($block['taxonomy']))
  <h4 class="category-header"><a href="{{ $term->slug }}">{{ $term->name }}</a></h4>
  <div class="home-page-category-posts">
    @foreach($block['posts'] as $post)
      @include('partials.block-post-display')
    @endforeach
  </div>
</div>

/partials/block-post-display (I still have to organize all of my partials)

<article @php post_class() @endphp>
  <div class="featured-image"><a href="{{ get_permalink($post->ID) }}">@include('partials.featured-image-lazy')</a></div>
  <h2 class="entry-title"><a href="{{ get_permalink($post->ID) }}">{!! get_the_title($post->ID) !!}</a></h2>
</article>

my FrontPage.php:

<?php

namespace App\Controllers;

use Sober\Controller\Controller;

class FrontPage extends Controller
{
  use Partials\Sidebar;
  protected $acf = true;

  public function getAllBlocks()
  {
    $featured_item = get_field('featured_item', 'option');
    $get_all_blocks = [];
    $post__not_in = [];
    $get_all_blocks['featured_posts'] = get_posts([
      'post_type' =>  'post',
      'p' => $featured_item,
      'posts_per_page' => 1
    ]);
    // Save these IDs...
    $post__not_in = array_merge($post__not_in, array_column($get_all_blocks['featured_posts'], 'ID'));

    $latest_posts = get_posts([
      'post_type' =>  'post',
      'posts_per_page' => 4,
      'post__not_in' => $post__not_in,
    ]);
    $get_all_blocks['latest_posts'] = $latest_posts;
    $post__not_in = array_merge($post__not_in, array_column($get_all_blocks['latest_posts'], 'ID'));

    $get_all_blocks['flexBlocks'] = [];
    foreach (get_field('homepage_content', 'option') as $flexBlock) {
      switch ($flexBlock['acf_fc_layout']) {
        case 'category':
          $categoryPosts = get_posts([
            'post_type' => 'post',
            'posts_per_page' => 4,
            'cat' => $flexBlock['fp_category'],
            'post__not_in' => $post__not_in,
          ]);
          $get_all_blocks['flexBlocks'][] = [
            'posts' => $categoryPosts,
            'type' => 'category',
            'taxonomy' => $flexBlock['fp_category']
          ];
          $post__not_in = array_merge($post__not_in, array_column($categoryPosts, 'ID'));
          unset($categoryPosts);
          break;
        case 'tag':
          $tagPosts = get_posts([
            'post_type' => 'post',
            'posts_per_page' => 4,
            'tag_id' => $flexBlock['fp_tag'],
            'post__not_in' => $post__not_in,
          ]);
          $get_all_blocks['flexBlocks'][] = [
            'posts' => $tagPosts,
            'type' => 'tag',
            'taxonomy' => $flexBlock['fp_tag']
          ];
          $post__not_in = array_merge($post__not_in, array_column($tagPosts, 'ID'));
          unset($tagPosts);
          break;
        case 'dossier':
          $dossierPosts = get_posts([
            'post_type' => 'post',
            'posts_per_page' => 4,
            'tax_query' => [
              [
                'taxonomy' => 'dossier',
                'terms'    => $flexBlock['fp_dossier']
              ],
            ],
            'post__not_in' => $post__not_in,
          ]);
          $get_all_blocks['flexBlocks'][] = [
            'posts' => $dossierPosts,
            'type' => 'dossier',
            'taxonomy' => $flexBlock['fp_dossier']
          ];
          $post__not_in = array_merge($post__not_in, array_column($dossierPosts, 'ID'));
          unset($dossierPosts);
          break;
        case 'custom_block':
          $customBlockPosts = get_posts([
            'post_type' => 'post',
            'posts_per_page' => 4,
            'post__in' => $flexBlock['fp_custom_block_stories_selection'],
            // Whatever the query should be...
          ]);
          $get_all_blocks['flexBlocks'][] = [
            'posts' => $customBlockPosts,
            'type' => 'custom_block',
            'block_url' => $flexBlock['fp_custom_block_url'],
            'block_header' => $flexBlock['fp_custom_block_header'],
          ];
          $post__not_in = array_merge($post__not_in, array_column($customBlockPosts, 'ID'));
          unset($customBlockPosts);
          break;
      }
    }

    return $get_all_blocks;
  }

  public function with()
  {
    return ['allBlocks' => $this->getAllBlocks()];
  }
}

Thanks again! Learned a lot from this exercise and really appreciate all the help and support this community gives!