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.
Why it exists
Section titled “Why it exists”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.
Entry points
Section titled “Entry points”| 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) |
How it works
Section titled “How it works”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>.phpexists it isrequire_onced and the block registers itself there. - Otherwise
acf_register_block_type()is called withrender_template => blocks/<slug>/block-<slug>.php, the shared category (strategiq-blocks), an icon read fromblock-<slug>.svg(falling back toinc/favicon/master-favicon.svg), and anenqueue_assetsclosure.
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()whenblock_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-tabsandmap-blockadditionally 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
srcsetat 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-srcsetplus alazyloadclass), with optional low-quality or inline-SVG placeholders. Overridable via theLAZY_LOAD,LAZY_LOW_QUALITYandLAZY_PLACEHOLDERconstants. 'background' => trueadds abehave-as-bgclass 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]Configuration
Section titled “Configuration”- 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 bareeditor-styles.css(inc/setup-gutenberg.php:5,26-29). - Image constants:
LAZY_LOAD,LAZY_LOW_QUALITY,LAZY_PLACEHOLDER. gulpfile.jsurlToPreview— 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.
Invariants and gotchas
Section titled “Invariants and gotchas”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; blocksrequired 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()returnstime()whenIS_WPEis 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.logwith a relative path (inc/enqueue.php:55), so it depends on the process working directory resolving to the site root.get_responsive_image()assumessizes['xl']orsizes['lg']exists when setting width/height on the fallback<img>(inc/image-functions.php:402-422) and indexes them unconditionally. A size array of onlyxsemits 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, andjquery-migrateis removed viawp_default_scripts. Plugins expecting core jQuery in the header can break. - Several blocks are unimplemented stubs still containing the generator
boilerplate (
block-exampleclasses):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-sass4). It will not build on a current Node without an older Node version — expect to use nvm. - ACF’s
allowed_block_typesrestriction is written but commented out (functions.php:251), so core blocks are not actually restricted to the listed five.
Changing it safely
Section titled “Changing it safely”- New block: create
blocks/<slug>/containingblock-<slug>.php,block-<slug>.scss,block-<slug>.jsand optionallyblock-<slug>.svg; create the ACF field group with location “Block →” (it saves into acf-json/); run gulp. Copy an existing simple block rather than starting from the stub files. - Follow the existing template shape:
$_block = new BlockMeta;then<section <?php $_block->target_id(); ?> class="block-<slug> <?php $_block->block_classes(); ?>">. That is what makes the shared spacing/theme controls work. - If the new block will be one of the first two on a page, keep its CSS small — it gets inlined.
- Adding a block to a hardcoded template (
single-service.php,category.php,page-news.php,single-team.php,single.php,search.php) →requirethe template and add the slug to that file’s enqueue array. - New image size set → prefer the shorthand form and always include an
xlorlgkey so the fallback<img>gets dimensions. - Commit
acf-json/changes alongside the template — field groups are code here. - Deliberately not abstracted: the dual asset-loading mechanism, and the
hand-written enqueue arrays in page templates. They exist because ACF’s
enqueue_assetsonly fires for editor-placed blocks. Do not “unify” without solving that. - 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).
None. No test runner, no test directory, and bitbucket-pipelines.yml runs no
build or test step — assets/ is committed and deployed as-is, so an unbuilt
change ships as “no change”.