Write Up

Approach & Setup

  • Built as an ACF Block V3 registered directly in functions.php — chose this over a plugin like WPCodebox because theme-level registration is more portable and easier to version control
  • Used a renderTemplate PHP file rather than a JS-rendered block — the original plan was JS rendering with wp_localize_script, but that failed silently in the ACF editor context, so I switched to full PHP rendering early and never looked back
  • ACF field group registered via JSON import (Custom Fields → Tools → Import) rather than acf_add_local_field_group() — the latter kept missing ACF’s sync scanner regardless of hook priority

PHP Architecture

  • All sanitisation centralised in one normalisation pass (array_map) so helper functions can trust their inputs are clean
  • Helper functions wrapped in function_exists() guards — ACF can call the render template more than once per request (editor + frontend), which caused a fatal Cannot redeclare error before this fix
  • Switched from ob_start()/ob_get_clean() per row to string concatenation — smaller memory footprint, no output buffer stack overhead
  • get_template_directory_uri() cached in a static variable inside cct_icon() so it resolves once per request regardless of icon count
  • Icon names whitelisted against an allowed array — prevents any path traversal if field data is ever tampered with

Design & UX Decisions

  • Matched the provided mock closely: name as blue link above the logo, single amber star rather than a 5-star row, $ prepended automatically to flat fees, Unverified rendered as a distinct pill
  • Used asset folder SVGs (/assets/*.svg) via <img> tags rather than an inline sprite — cleaner markup, browser caches the files, and swapping icons later requires no PHP changes
  • White tooltip with blue border chosen for contrast and readability against both light and dark row backgrounds
  • ARIA table roles (role="table"role="rowgroup"role="row"role="columnheader"/role="cell") implemented correctly after Page Speed Insights flagged the original flat structure

JavaScript

  • Stripped to ~22 lines — WordPress enqueues with defer so no IIFE, no DOMContentLoaded check, no “already initialised” guard needed
  • Single keydown listener on document handles Escape for both tooltips and claimed badges instead of one listener per element
  • Event delegation kept the file minimal and robust regardless of how many rows render

How AI Was Used

I used Claude (Anthropic) inside claude.ai minimally.

  • Used it to catch issues I described in plain English (“fatal redeclare error”, “path shows as just marker.svg“, “ARIA parent role warning”) and get precise fixes rather than having to dig through docs
  • The refactor pass was explicitly prompted — asked Claude to make the existing code safe and fast, which produced the function_exists guards, whitelist, static cache, and ob_start removal

Future Iterations

  • Fragment caching — wrap the rendered HTML in a WordPress transient keyed to the block ID and a hash of the field data; only re-renders when ACF content actually changes, which matters if the block ends up on a high-traffic page
  • Extract to a proper class file — move CCT_Renderer (or the function_exists helpers) into a standalone class-cct-renderer.php autoloaded via functions.php, keeping the render template thin and the logic testable
  • Unit tests — with the logic in a class, helpers like dollar_score, format_reviews, and the fee prefix logic are straightforward to cover with PHPUnit; edge cases like fee_score = 0, empty companies array, or malformed URLs are the obvious first tests
  • Block variations or InnerBlocks — if the table needs to support different column configs (e.g. a shorter 3-column version), ACF block variations would be cleaner than adding conditional field logic
  • Schema markup — add ItemList + LocalBusiness structured data to the rendered HTML so the rankings have a chance of appearing in rich results
  • Lazy-loaded rows — if the company count grows beyond ~10, render the first 3 immediately and fetch the rest via a REST endpoint on scroll
  • Editor preview parity — the current $is_preview flag just adds a CSS class; a real next step would be a proper block.json example dataset so the block looks populated when browsing the block inserter