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.
Why it exists
Section titled “Why it exists”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.
Entry points
Section titled “Entry points”| Path | What it does |
|---|---|
wp-content/themes/supplychainschool/page.php:52 | The 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:2 | Makes the parent theme’s acf-json load in child themes |
wp-content/themes/supplychainschool/inc/image-functions.php | Responsive image helpers used by every block |
wp-content/themes/supplychainschool/gulpfile.js | Asset build (Laravel Elixir + Gulp 3) |
How it works
Section titled “How it works”Dispatch
Section titled “Dispatch”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).
The blocks
Section titled “The blocks”| Partial | Block |
|---|---|
fc-content.php, fc-content-two-columns.php | Rich 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.php | Text + media |
fc-call-to-actions.php, fc-text-with-buttons.php | CTAs (backed by the cta CPT) |
fc-latest-posts.php, fc-featured-posts.php | Post listings |
fc-resources.php, fc-interactive-resources.php | Resource library (the interactive one is 16k, Rollup-bundled JS) |
fc-topics-grid.php, fc-issues.php | Taxonomy navigation |
fc-member-grid.php, fc-members-pathway.php, fc-partners.php | Member/partner listings |
fc-faqs.php | FAQs (also feeds FAQ schema) |
fc-video.php, fc-photo-gallery.php | Media |
fc-quote.php, fc-testimonials.php, fc-stats.php, fc-icons.php | Design elements |
fc-contact-form.php, fc-newsletter-sign-up.php | Forms — see [[forms-spam-and-marketing-tracking]] |
fc-search.php | Search block |
fc-spacer.php | Spacing |
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.
Supporting CPTs
Section titled “Supporting CPTs”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).
Images
Section titled “Images”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.cssnavigation.scssandnavigation-legacy.scss→ separate CSS files- a concatenated
assets/js/supplychainschool.jsfrom Bootstrap 4, lity, picturefill, ofi and the theme JS - Rollup bundles for
fc-interactive-resources.jsandcookie-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.
Configuration
Section titled “Configuration”- 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.
Invariants and gotchas
Section titled “Invariants and gotchas”- 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 namedtext-with-image(already hyphenated) would look forfc-text-with-image.phptoo, so hyphenated and underscored layout names collide. - The
WP_DEBUGcheck atpage.php:61usesdefined(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 ininc/image-functions.php:20in 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 atfunctions.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. Forgettingnpm run prodmeans CSS changes never ship — the deploy is agit ftp pushof the repo as-is. - Gulp 3 + Laravel Elixir +
node >= 10.9is a very old toolchain. It will not run on a current Node without workarounds. Checknpm-shrinkwrap.jsonbefore upgrading anything. get_stylesheet_directory()inpage.phpmeans 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.phpexists at theme root and is not referenced byfunctions.php; treat it as dead unless proven otherwise.
Changing it safely
Section titled “Changing it safely”- 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 regeneratedacf-json/group_*.jsonin 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 withget_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 fromsupplychainschool.scss. Thennpm run prodand commitassets/css. - Multisite: if a block must be copyable between subsites, check
get_images_from_blocks()ininc/multisite-functions.php:284— it only recognises the field keysbackground_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]].