Skip to content

ACF block system and asset pipeline

Every editable section of this site is an ACF Gutenberg block. There are ~49 of them, all auto-registered from directory names, each with its own PHP template, SCSS and JS, and a shared BlockMeta helper that renders headings, images, buttons and spacing classes. Responsive images come from a single get_responsive_image() function backed by the Fly Dynamic Image Resizer plugin.

This is the substrate every other feature page sits on. Read this before adding or editing any block.

Editors build pages by stacking blocks; developers add a directory. The convention-over-configuration registration means a new block needs no PHP wiring at all, and the per-block asset split means a page only loads CSS/JS for the blocks it actually uses. The above-the-fold inlining exists to protect Core Web Vitals (LCP/CLS) — the README calls this out explicitly.

Core Gutenberg blocks are deliberately stripped back: custom colours, gradients, font sizes and block patterns are all disabled (inc/setup-gutenberg.php:10-18) so editors cannot break the design system.

Path What it does
wp-content/themes/bpcollins/inc/register-blocks.php:44 register_acf_block_types() — globs blocks/* and registers one block per directory
wp-content/themes/bpcollins/blocks/<slug>/block-<slug>.php A block’s render template (required)
wp-content/themes/bpcollins/blocks/<slug>/type-<slug>.php Optional override of the default registration
wp-content/themes/bpcollins/inc/classes/BlockMeta.php Field accessor + render helpers used by nearly every template
wp-content/themes/bpcollins/inc/image-functions.php:75 get_responsive_image() — the <picture> builder
wp-content/themes/bpcollins/inc/partial.php:28 partial() — renders partials/*.php with a $_DATA payload
wp-content/themes/bpcollins/inc/setup-gutenberg.php:39 block_above_the_fold() — decides inline vs enqueued CSS
wp-content/themes/bpcollins/gulpfile.js Compiles SCSS/JS from blocks/ and resources/ into assets/
wp-content/themes/bpcollins/acf-json/ ACF field group JSON (local JSON sync)

Registration. On acf/init, register_acf_block_types() globs blocks/* directories. For each one it derives the slug from the directory name and the title from the slug (ucwords, dashes → spaces), then:

  • If blocks/<slug>/type-<slug>.php exists it is require_onced and the block registers itself there.
  • Otherwise acf_register_block_type() is called with render_template => blocks/<slug>/block-<slug>.php, the shared category (strategiq-blocks), an icon read from block-<slug>.svg (falling back to inc/favicon/master-favicon.svg), and an enqueue_assets closure.

With WP_DEBUG on, a missing template file throws an exception (inc/register-blocks.php:64-66) — so a stray directory under blocks/ is a fatal error in development and a silent broken block in production.

Per-block assets. The enqueue_assets closure looks for assets/css/<slug>/block-<slug>.css and assets/js/<slug>/block-<slug>.min.js:

  • CSS is inlined into the page via file_get_contents() when block_above_the_fold() says the block is the first or second block in the post content; otherwise it is enqueued normally (inc/register-blocks.php:94-100).
  • JS is enqueued in the footer with the cache key as version.
  • location-tabs and map-block additionally get Mapbox GL from the CDN.

Templates that are required directly from a PHP template — the way single-service.php, category.php and others compose fixed page layouts — do not go through enqueue_assets. Those templates enqueue block CSS/JS by hand in an array at the top of the file. Both mechanisms exist side by side.

BlockMeta. new BlockMeta with no arguments calls get_fields() for the current post, so inside a block template it holds that block’s field values. Helpers: target_id(), block_classes() (emits block_classes, block_theme, background_colors, padding_top, padding_bottom, margin_top, margin_bottom), render_heading(), render_text(), render_image(), render_btn(), render_btns(), render_form(), partial_loop() and debug_fields().

Partials. partial('post-card', ['post_id' => $id]) sets the global $_DATA, requires partials/post-card.php, then restores the previous $_DATA. Partials declare their own defaults with partial_defaults().

Responsive images. get_responsive_image() builds a <picture> with one <source> per breakpoint. Highlights:

  • Breakpoint keys map to Bootstrap min-widths: xs→0, sm→576, md→768, lg→992, xl→1200, xxl→1400 (inc/image-functions.php:246-265).
  • Sizes can be given long-hand (['width' => 570, 'height' => 290]) or shorthand ('570x290/ct', where the crop letters map c/t/b/l/r to center/top/bottom/left/right).
  • Each source gets a srcset at pixel densities 1x and 2x, with the density clamped when the requested size exceeds the original image (inc/image-functions.php:294-325).
  • Actual resizing is delegated to fly_get_attachment_image_src() — the Fly Dynamic Image Resizer plugin, which generates sizes on demand rather than at upload.
  • Lazy loading is on by default (data-src/data-srcset plus a lazyload class), with optional low-quality or inline-SVG placeholders. Overridable via the LAZY_LOAD, LAZY_LOW_QUALITY and LAZY_PLACEHOLDER constants.
  • 'background' => true adds a behave-as-bg class instead of producing CSS background-image — the picture element is styled to cover its container.
  • SVGs bypass the whole pipeline and return a plain <img>.
  • Editing an image in wp-admin deletes its Fly cache so sizes regenerate (inc/image-functions.php:481-487).

Build. Gulp compiles resources/scss/** and blocks/**/*.scss into assets/css/ (compressed, autoprefixed, sourcemapped except abovethefold), and rolls up blocks/**/*.js and resources/js/app.js into assets/js/**.min.js via rollup + babel + uglify. npx gulp watch also runs BrowserSync proxying https://bpcollins.build/.

Cache busting. get_cache_key() (inc/enqueue.php:50) returns time() locally, the contents of .git-ftp.log on WP Engine (i.e. the deployed commit), or false if that file is missing.

flowchart TD
A[blocks/<slug>/ directory] --> B[glob on acf/init]
B --> C{type-<slug>.php exists?}
C -->|yes| D[block registers itself]
C -->|no| E[acf_register_block_type with render_template]
E --> F[enqueue_assets closure]
F --> G{first or second block in content?}
G -->|yes| H[inline CSS via file_get_contents]
G -->|no| I[wp_enqueue_style]
F --> J[wp_enqueue_script footer]
K[PHP templates that require blocks directly] --> L[hand-written enqueue array at top of template]
M[SCSS/JS sources] --> N[gulp → assets/css, assets/js]
  • Block category slug: strategiq-blocks (inc/register-blocks.php:6-8), titled “ Blocks”.
  • ACF field groups live in acf-json/ and sync automatically; ACF PRO is a required plugin.
  • ACF options pages: Company Info and Global Options (inc/acf.php:10-24).
  • Editor styles loaded into Gutenberg: assets/css/abovethefold.css, assets/css/app.css, assets/css/gutenberg.css, plus a bare editor-styles.css (inc/setup-gutenberg.php:5,26-29).
  • Image constants: LAZY_LOAD, LAZY_LOW_QUALITY, LAZY_PLACEHOLDER.
  • gulpfile.js urlToPreview — the local dev URL for BrowserSync; each developer changes this for their own environment.
  • Fly Dynamic Image Resizer must be active or every image call returns nothing.
  • block_above_the_fold() reads $cleanedBlocks[1] without checking it exists (inc/setup-gutenberg.php:51). A post whose content is a single block emits a PHP notice on every render.
  • Above-the-fold CSS is inlined per block instance. Two copies of the same above-the-fold block mean two inline <style> blocks.
  • Directory name is the contract. Slug, title, template path, SVG icon path and built asset paths are all derived from it. Renaming a block directory orphans every existing use of the block in post content — the block becomes “invalid” in the editor. Treat a rename as a content migration.
  • Two asset-loading mechanisms. Blocks placed by an editor get assets from enqueue_assets; blocks required by a PHP template do not, and need adding to that template’s enqueue array. Forgetting this is the single most common “why is my block unstyled” cause.
  • get_cache_key() returns time() when IS_WPE is unset, so every local page load busts every asset URL — expected locally, but it means staging on non-WP Engine infrastructure is effectively uncacheable.
  • get_cache_key() reads .git-ftp.log with a relative path (inc/enqueue.php:55), so it depends on the process working directory resolving to the site root.
  • get_responsive_image() assumes sizes['xl'] or sizes['lg'] exists when setting width/height on the fallback <img> (inc/image-functions.php:402-422) and indexes them unconditionally. A size array of only xs emits notices.
  • jQuery is deregistered and replaced with a bundled 3.5.1 (inc/enqueue.php:26-27), loaded in the footer with no version string, and jquery-migrate is removed via wp_default_scripts. Plugins expecting core jQuery in the header can break.
  • Several blocks are unimplemented stubs still containing the generator boilerplate (block-example classes): reviews-tabs, content-strip, service-tiles, sticky-accordion, sub-navigation, full-width-content — all 29-line files. They register and appear in the editor’s block list.
  • SVG upload is enabled with the filetype check effectively bypassed (inc/template-functions.php:34-49). Only upload SVGs you trust.
  • Node/gulp toolchain is old (gulp 4, node-sass 4, gulp-sass 4). It will not build on a current Node without an older Node version — expect to use nvm.
  • ACF’s allowed_block_types restriction is written but commented out (functions.php:251), so core blocks are not actually restricted to the listed five.
  • New block: create blocks/<slug>/ containing block-<slug>.php, block-<slug>.scss, block-<slug>.js and optionally block-<slug>.svg; create the ACF field group with location “Block → ” (it saves into <code dir="auto">acf-json/</code>); run gulp. Copy an existing simple block rather than starting from the stub files.</li> <li>Follow the existing template shape: <code dir="auto">$_block = new BlockMeta;</code> then <code dir="auto"><section <?php $_block->target_id(); ?> class="block-<slug> <?php $_block->block_classes(); ?>"></code>. That is what makes the shared spacing/theme controls work.</li> <li>If the new block will be one of the first two on a page, keep its CSS small — it gets inlined.</li> <li><strong>Adding a block to a hardcoded template</strong> (<code dir="auto">single-service.php</code>, <code dir="auto">category.php</code>, <code dir="auto">page-news.php</code>, <code dir="auto">single-team.php</code>, <code dir="auto">single.php</code>, <code dir="auto">search.php</code>) → <code dir="auto">require</code> the template <strong>and</strong> add the slug to that file’s enqueue array.</li> <li><strong>New image size set</strong> → prefer the shorthand form and always include an <code dir="auto">xl</code> or <code dir="auto">lg</code> key so the fallback <code dir="auto"><img></code> gets dimensions.</li> <li>Commit <code dir="auto">acf-json/</code> changes alongside the template — field groups are code here.</li> <li>Deliberately not abstracted: the dual asset-loading mechanism, and the hand-written enqueue arrays in page templates. They exist because ACF’s <code dir="auto">enqueue_assets</code> only fires for editor-placed blocks. Do not “unify” without solving that.</li> <li>Verify: place the block in the editor, check the front end, then check it again as the first block on a page (inline CSS path).</li> </ul> <div class="sl-heading-wrapper level-h2"><h2 id="tests">Tests</h2><a class="sl-anchor-link" href="#tests"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore>Section titled “Tests”</span></a></div> <p>None. No test runner, no test directory, and <code dir="auto">bitbucket-pipelines.yml</code> runs no build or test step — <code dir="auto">assets/</code> is committed and deployed as-is, so an unbuilt change ships as “no change”.</p> </div><footer class="sl-flex astro-xhxjzfa7"><div class="meta sl-flex astro-xhxjzfa7"></div><div class="pagination-links print:hidden astro-jgq3vlii" dir="ltr"><a href="/sites/bpcollins/architecture/" rel="prev" class="astro-jgq3vlii"><svg aria-hidden="true" class="astro-jgq3vlii astro-n5k4ipy7" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1.5rem;"><path d="M17 11H9.41l3.3-3.29a1.004 1.004 0 1 0-1.42-1.42l-5 5a1 1 0 0 0-.21.33 1 1 0 0 0 0 .76 1 1 0 0 0 .21.33l5 5a1.002 1.002 0 0 0 1.639-.325 1 1 0 0 0-.219-1.095L9.41 13H17a1 1 0 0 0 0-2Z"/></svg><span class="astro-jgq3vlii">Previous<br class="astro-jgq3vlii"><span class="link-title astro-jgq3vlii">B P Collins architecture</span></span></a><a href="/sites/bpcollins/features/build-and-deploy/" rel="next" class="astro-jgq3vlii"><svg aria-hidden="true" class="astro-jgq3vlii astro-n5k4ipy7" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1.5rem;"><path d="M17.92 11.62a1.001 1.001 0 0 0-.21-.33l-5-5a1.003 1.003 0 1 0-1.42 1.42l3.3 3.29H7a1 1 0 0 0 0 2h7.59l-3.3 3.29a1.002 1.002 0 0 0 .325 1.639 1 1 0 0 0 1.095-.219l5-5a1 1 0 0 0 .21-.33 1 1 0 0 0 0-.76Z"/></svg><span class="astro-jgq3vlii">Next<br class="astro-jgq3vlii"><span class="link-title astro-jgq3vlii">Build and deployment</span></span></a></div></footer></div></div></main></div></div></div></div></body></html>