Skip to content

ACF flexible content blocks

Every page on the main School sites is built from an ACF Flexible Content field called flexible_content. Each layout maps by naming convention to a PHP partial in inc/components/. This is how editors compose pages, and how 27 block types are maintained without a page builder.

The sites predate Gutenberg’s maturity and needed a fixed set of designed blocks rather than free-form content, so marketing can build pages without breaking layout. Field definitions are committed as JSON so block changes travel with the code rather than living only in a database.

PathWhat it does
wp-content/themes/supplychainschool/page.php:52The dispatcher — have_rows() loop that maps a layout to a partial
wp-content/themes/supplychainschool/inc/components/27 fc-*.php block partials
wp-content/themes/supplychainschool/partials/Smaller reusable pieces (hero.php, card-resource.php, member-card.php, faqs.php, …)
wp-content/themes/supplychainschool/acf-json/40 committed ACF field-group JSON files
wp-content/themes/supplychainschool/inc/multisite-functions.php:2Makes the parent theme’s acf-json load in child themes
wp-content/themes/supplychainschool/inc/image-functions.phpResponsive image helpers used by every block
wp-content/themes/supplychainschool/gulpfile.jsAsset build (Laravel Elixir + Gulp 3)

page.php is the whole mechanism:

$layout = str_replace('_', '-', get_row_layout());
$include_path = sprintf('%s/inc/components/fc-%s.php', get_stylesheet_directory(), $layout);
if (file_exists($include_path)) { require $include_path; }

So an ACF layout named text_with_image renders inc/components/fc-text-with-image.php. A missing partial renders nothing — silently in production, with a trigger_error() only when WP_DEBUG and WP_DEBUG_DISPLAY are on (page.php:61).

Note get_stylesheet_directory(), not get_template_directory(): a child theme can override any single block by dropping its own fc-*.php in place.

page.php also handles password-protected pages, emitting a 401 header and its own password form (page.php:22).

PartialBlock
fc-content.php, fc-content-two-columns.phpRich text, optional full width and background
fc-hero-adjacent (partials/hero.php)Page hero
fc-text-with-image.php, fc-image-and-text-stacked.php, fc-zoom-image.phpText + media
fc-call-to-actions.php, fc-text-with-buttons.phpCTAs (backed by the cta CPT)
fc-latest-posts.php, fc-featured-posts.phpPost listings
fc-resources.php, fc-interactive-resources.phpResource library (the interactive one is 16k, Rollup-bundled JS)
fc-topics-grid.php, fc-issues.phpTaxonomy navigation
fc-member-grid.php, fc-members-pathway.php, fc-partners.phpMember/partner listings
fc-faqs.phpFAQs (also feeds FAQ schema)
fc-video.php, fc-photo-gallery.phpMedia
fc-quote.php, fc-testimonials.php, fc-stats.php, fc-icons.phpDesign elements
fc-contact-form.php, fc-newsletter-sign-up.phpForms — see [[forms-spam-and-marketing-tracking]]
fc-search.phpSearch block
fc-spacer.phpSpacing

Blocks read their data with get_sub_field() inside the row context, so a partial only works when included from within the have_rows() loop.

Several blocks are backed by content types registered in inc/cpt.php — all 'public' => false, 'publicly_queryable' => false, i.e. library items with no front-end URL of their own:

slider, icons, videos, resources, faqs, cta, partners, member.

Taxonomies (inc/cpt.php:372): topics (on post, resources, member), level, markets, types, plus member_level and member_tag.

Two ACF options pages are registered: Company Info and Partners (inc/cpt.php:4).

inc/image-functions.php wraps ACF image fields into responsive markup: get_image() / the_image() for top-level fields, get_sub_image() / the_sub_image() for sub-fields, and get_responsive_image() underneath. The theme registers one extra size, hero at 3200×1000 cropped (functions.php:213). The fly-dynamic-image-resizer plugin generates other sizes on demand.

Laravel Elixir on Gulp 3 (gulpfile.js). npm run prod compiles:

  • resources/assets/sass/supplychainschool.scss → assets/css/supplychainschool.css
  • navigation.scss and navigation-legacy.scss → separate CSS files
  • a concatenated assets/js/supplychainschool.js from Bootstrap 4, lity, picturefill, ofi and the theme JS
  • Rollup bundles for fc-interactive-resources.js and cookie-notification.js

Cache busting is deliberately environment-dependent (functions.php:126-158): on non-WP Engine hosts the query string is ?v=time() (no caching at all); on WP Engine it is the commit hash read from .git-ftp.log; if that file is missing, no query string at all.

  • ACF Pro must be active — the theme has no fallback if get_field() is undefined. inc/required-plugins/ uses TGMPA to nag for it.
  • ACF field groups sync from acf-json/. Local JSON is the source of truth; a group edited in the admin writes back to that directory.
  • No environment variables.
  • A missing partial fails silently. Rename an ACF layout without renaming the file and the block just disappears from every page using it, with no admin warning.
  • Layout name to filename is _ → -. A layout named text-with-image (already hyphenated) would look for fc-text-with-image.php too, so hyphenated and underscored layout names collide.
  • The WP_DEBUG check at page.php:61 uses defined(WP_DEBUG) without quotes — that is a constant use, not a name, so this line raises its own error rather than working as intended. The same bug appears in inc/image-functions.php:20 in corrected form (defined('WP_DEBUG')), which shows the intent.
  • jQuery is loaded from a CDN and core jQuery is deregistered (functions.php:146) with an SRI hash pinned at functions.php:201. If cdnjs is unreachable, all front-end JS breaks. Bumping the jQuery version requires updating the integrity hash in the same commit.
  • Compiled assets/ output is committed. Forgetting npm run prod means CSS changes never ship — the deploy is a git ftp push of the repo as-is.
  • Gulp 3 + Laravel Elixir + node >= 10.9 is a very old toolchain. It will not run on a current Node without workarounds. Check npm-shrinkwrap.json before upgrading anything.
  • get_stylesheet_directory() in page.php means the block path follows the child theme. The Carbon Calculator theme is a separate theme with its own templates and does not use this system at all.
  • remap.php exists at theme root and is not referenced by functions.php; treat it as dead unless proven otherwise.
  • New block: create the ACF Flexible Content layout, then inc/components/fc-<layout-with-hyphens>.php. Nothing to register — the dispatcher finds it by name. Commit the regenerated acf-json/group_*.json in the same commit as the partial, or the field group and the template will disagree between environments.
  • Shared markup goes in partials/ and is pulled in with get_template_part(); blocks that duplicate markup (the several text-plus-image variants) do so because their ACF fields differ, not by accident.
  • Styles: add a SASS partial under resources/assets/sass/ and import it from supplychainschool.scss. Then npm run prod and commit assets/css.
  • Multisite: if a block must be copyable between subsites, check get_images_from_blocks() in inc/multisite-functions.php:284 — it only recognises the field keys background_image, image, images, profile_image, featured_image. A new image field name outside that list will not have its media copied. See [[multisite-content-copy]].
  • Verify by hand: add the block to a draft page in each of the four column variants it supports, and check the compiled CSS shipped.

None. phpcs.xml.dist exists in the theme but is not run by CI.

Related: [[site-chrome-and-navigation]], [[multisite-content-copy]], [[site-search]], [[forms-spam-and-marketing-tracking]].