Back to Portfolio
Code Case Studies

Engineering Highlights
From Recent Projects

A closer look at specific technical problems I've solved — block system architecture, editor extensions, accessibility patterns, and CSS utilities — across WordPress theme development work.

WP Block API Custom WordPress Theme Framework

ACF Block Registration & Organization

A convention-over-configuration block registration system for a custom WordPress parent/child theme framework. Blocks self-register from directory structure — no manual registration calls needed.

Key Design Decisions

  • Convention-over-config: Drop a block.json in a folder and the block auto-registers — no index files or manual calls
  • Parent/child inheritance: Child theme blocks override parent blocks of the same name via array_unique after merging both directories
  • Per-block isolation: Each block directory owns its functions.php, AJAX handler, and editor-only script/style variants
  • Cache-busting: Assets use filemtime() as the version parameter — no manual version bumps during development

ACF JSON Co-location

ACF field group JSON files are automatically saved into each block's own acf-json/ directory — keeping block code, template, and field configuration together in the same place.

  • JSON filename derived from the block slug automatically
  • Directory auto-created if it doesn't exist on first save
  • All block acf-json/ paths registered as load points on init
lib/blocks/hooks_blocks.php
<?php
// Auto-discovers all blocks in parent + child theme directories.
// Child blocks override parent blocks of the same name via array_unique.
function get_block_dirs(): array|false {
    $root       = get_template_directory() . '/blocks';
    $block_dirs = array_map(
        fn($dir) => basename($dir),
        array_filter(glob($root . '/*'), 'is_dir')
    );

    if (is_child_theme()) {
        $child_root = get_stylesheet_directory() . '/blocks';
        $child_dirs = array_map(
            fn($dir) => basename($dir),
            array_filter(glob($child_root . '/*'), 'is_dir')
        );
        // Child blocks override parent — duplicates removed in merge
        $block_dirs = array_unique(array_merge($block_dirs, $child_dirs));
    }

    return $block_dirs;
}

// Priority 3: register before default init hooks.
add_action('init', function () {
    $block_dirs = get_block_dirs();

    foreach ($block_dirs as $block_dir) {
        $block_json = locate_template('/blocks/' . $block_dir . '/block.json');
        if (!$block_json) continue;

        // Register editor-only script/style variants (-editor, -init)
        foreach (['-editor', '-init'] as $variant) {
            $path = '/blocks/' . $block_dir . '/dist/' . $block_dir . $variant . '.min.js';
            if ($script = locate_template($path)) {
                wp_register_script(
                    'theme-block-js-' . $block_dir . $variant,
                    get_theme_file_uri($path),
                    [],
                    filemtime($script)  // cache-bust by file modification time
                );
            }
        }

        // Per-block: auto-load functions.php if it exists
        locate_template('blocks/' . $block_dir . '/functions.php', true, true);

        // Per-block: register AJAX nonce if ajax.php exists
        if (locate_template('blocks/' . $block_dir . '/ajax.php', true, true)) {
            wp_localize_script(
                'theme-blocks-scripts',
                'theme_block_' . str_replace('-', '_', $block_dir) . '_ajax',
                [
                    'ajax_url' => admin_url('admin-ajax.php'),
                    'nonce'    => wp_create_nonce('theme-block-' . $block_dir . '-nonce'),
                ]
            );
        }

        register_block_type($block_json);
    }
}, 3);
lib/acf.php
<?php
// Route ACF JSON saves to the block's own acf-json/ subdirectory,
// co-locating field group configuration with the block files it belongs to.

add_filter('acf/json/save_paths', function ($paths, $post) {
    if (($post['location'][0][0]['param'] ?? false) === 'block') {
        $dir   = explode('/', $post['location'][0][0]['value'])[1] ?? '_default';
        $paths = [get_stylesheet_directory() . '/blocks/' . $dir . '/acf-json'];

        // Auto-create the directory if it doesn't exist yet
        if (!file_exists($paths[0])) {
            mkdir($paths[0], 0755, true);
        }
    }
    return $paths;
}, 10, 2);

// Name each JSON file after the block slug (e.g., hero.json, banner.json)
add_filter('acf/json/save_file_name', function ($filename, $post) {
    if (($post['location'][0][0]['param'] ?? false) === 'block') {
        $block_name = explode('/', $post['location'][0][0]['value'])[1] ?? '_default';
        return $block_name . '.json';
    }
    return $filename;
}, 10, 3);

// Load ACF JSON from the parent theme, child theme,
// AND each block's own acf-json/ directory.
add_filter('acf/settings/load_json', function ($paths) {
    if (is_child_theme()) {
        $paths[0] = get_template_directory()  . '/acf-json'; // parent
        $paths[1] = get_stylesheet_directory() . '/acf-json'; // child overrides
    }
    foreach (get_block_dirs() as $block_dir) {
        if ($block_path = locate_template('/blocks/' . $block_dir . '/acf-json')) {
            $paths[] = $block_path;
        }
    }
    return $paths;
});
Gutenberg Extension WordPress Theme Development

Block Editor Thread Decoration System

A full-stack feature that adds decorative SVG “thread” elements to any supported block — built without modifying core blocks, using WordPress’s block filter API across three lifecycle phases.

Three-Phase Architecture

  1. 1 Attribute registrationblocks.registerBlockType filter injects a threadDecorations array attribute onto supported blocks
  2. 2 Editor UI — a higher order component wraps BlockEdit to add an inspector panel with controls for thread type, layer, position, and mobile behavior
  3. 3 Save serialization — decoration config is serialized as a data-threads JSON attribute on the saved block HTML

PHP Server Render

  • Uses WordPress 6.2+ WP_HTML_Tag_Processor for safe, spec-compliant HTML manipulation — no regex on HTML
  • Reads the data-threads attribute, strips it from the output, then injects thread <div> elements at the correct layer positions
  • Background threads inserted after the opening tag; overlay threads inserted before the closing tag
  • SVG assets are Vite-versioned at build time, injected as background-image URLs via a JS asset map

Why Block Filters?

Using addFilter rather than modifying block templates means thread decoration support can be added to any block — including core blocks like core/group — without forking or patching them. It stays cleanly separated from block business logic and can be maintained or removed independently.

resources/js/editor/block.threads.jsx
// Three-phase WordPress block filter system — extends any block non-destructively.
// Phase 1: Register attribute → Phase 2: Editor UI → Phase 3: Save serialization

// Phase 1: Add threadDecorations attribute to supported block types
addFilter('blocks.registerBlockType', 'theme/thread-attributes', (settings, name) => {
    if (!BLOCKS_WITH_THREAD_SUPPORT.includes(name)) return settings;

    return {
        ...settings,
        attributes: {
            ...settings.attributes,
            threadDecorations: { type: 'array', default: [] },
        },
    };
});

// Phase 2: HOC injects a Thread Decorations panel into each block's inspector
const withThreadInspectorControls = createHigherOrderComponent((BlockEdit) => {
    return (props) => {
        if (!supportsThreads(props.name)) return <BlockEdit {...props} />;

        const { attributes, setAttributes } = props;
        const { threadDecorations = [] } = attributes;

        const addThread = () => setAttributes({
            threadDecorations: [
                ...threadDecorations,
                { id: Date.now(), thread: 'bottom-right', layer: 'background',
                  position: 'default', mobileBehavior: 'display' },
            ],
        });

        return (
            <Fragment>
                <BlockEdit {...props} />
                <InspectorControls>
                    <PanelBody title="Thread Decorations" initialOpen={false}>
                        {threadDecorations.map((decoration) => (
                            <div key={decoration.id}>
                                <SelectControl label="Thread Type"
                                    value={decoration.thread}
                                    options={availableThreads}
                                    onChange={(v) => updateThread(decoration.id, 'thread', v)}
                                />
                                <SelectControl label="Layer"
                                    value={decoration.layer}
                                    options={[
                                        { value: 'background', label: 'Background (behind content)' },
                                        { value: 'overlay',    label: 'Overlay (in front of content)' },
                                    ]}
                                    onChange={(v) => updateThread(decoration.id, 'layer', v)}
                                />
                                <SelectControl label="Mobile Behavior"
                                    value={decoration.mobileBehavior || 'display'}
                                    options={mobileBehaviorOptions}
                                    onChange={(v) => updateThread(decoration.id, 'mobileBehavior', v)}
                                />
                                <Button isDestructive variant="secondary"
                                    onClick={() => removeThread(decoration.id)}>
                                    Remove Thread
                                </Button>
                            </div>
                        ))}
                        <Button variant="secondary" onClick={addThread}>
                            Add Thread Decoration
                        </Button>
                    </PanelBody>
                </InspectorControls>
            </Fragment>
        );
    };
}, 'withThreadInspectorControls');

// Phase 3: Serialize decoration config as data-threads JSON on saved block HTML.
// PHP then reads this attribute server-side to inject the actual SVG elements.
addFilter('blocks.getSaveContent.extraProps', 'theme/thread-save',
    (extraProps, blockType, { threadDecorations = [] }) => {
        if (!supportsThreads(blockType.name) || !threadDecorations.length) {
            return extraProps;
        }
        return { ...extraProps, 'data-threads': JSON.stringify(threadDecorations) };
    }
);
app/Blocks/Renderers/ThreadDecorationRenderer.php
<?php
// PHP side: reads the serialized data-threads attribute from saved block HTML
// and injects the actual SVG thread elements using WP_HTML_Tag_Processor.

public function render(string $block_content, array $block): string
{
    $thread_decorations = $block['attrs']['threadDecorations'] ?? null;

    if (!$thread_decorations && !str_contains($block_content, 'data-threads')) {
        return $block_content;
    }

    // Extract decoration config from the data-threads attribute
    if (!$thread_decorations) {
        $processor = new WP_HTML_Tag_Processor($block_content);
        if ($processor->next_tag(['tag_name' => 'div'])) {
            $threads_attr = $processor->get_attribute('data-threads');
            if ($threads_attr) {
                $thread_decorations = json_decode($threads_attr, true);
            }
        }
    }

    return $this->addThreadDecorations($block_content, $thread_decorations ?? []);
}

protected function addThreadDecorations(string $block_content, array $decorations): string
{
    $processor = new WP_HTML_Tag_Processor($block_content);
    if (!$processor->next_tag('div')) return $block_content;

    // Clean up — data-threads is no longer needed in the final HTML
    $processor->remove_attribute('data-threads');
    $html        = $processor->get_updated_html();
    $tag_end_pos = strpos($html, '>');

    $background_threads = $overlay_threads = '';

    foreach ($decorations as $decoration) {
        $html_fragment = $this->renderThreadDecoration(
            $decoration['thread']         ?? '',
            $decoration['layer']          ?? 'background',
            $decoration['position']       ?? 'default',
            $decoration['mobileBehavior'] ?? 'display'
        );
        $decoration['layer'] === 'background'
            ? $background_threads .= $html_fragment
            : $overlay_threads    .= $html_fragment;
    }

    $before = substr($html, 0, $tag_end_pos + 1);
    $after  = substr($html, $tag_end_pos + 1);

    // Overlay threads inserted before the block's closing </div>
    if (!empty($overlay_threads)) {
        $pos   = strrpos($after, '</div>');
        $after = substr($after, 0, $pos) . $overlay_threads . substr($after, $pos);
    }

    // Background threads inserted immediately after the opening tag
    return $before . $background_threads . $after;
}
ACF Composer WordPress Theme Development

Video Player Block

An ACF Composer block that auto-detects poster images from YouTube, Vimeo, or Wistia, then delivers an accessible lightbox or inline player with proper focus management and DOM cleanup.

PHP: ACF Composer Block

  • Class-based block registration via ACF Composer — fields defined in PHP, data processed in item(), passed to Blade template via with()
  • No custom poster? getVideoServicePoster() auto-fetches from YouTube (direct URL construction), Vimeo (oEmbed API), or Wistia (oEmbed API)
  • Display mode selectable per block: Lightbox (modal overlay) or Inline (replaces poster in place)

JS: Accessibility-First Modal

  • inert attribute on all background content — traps focus and prevents screen reader navigation outside the modal
  • Focus management: saves the triggering element, shifts focus to the close button on open, restores focus on close
  • DOM position restored after close — modal moved to body end for z-index, then returned to its original position
  • Full cleanup via destroy() — stores bound handlers for safe removal, prevents memory leaks on re-initialization
  • Keyboard support: Enter/Space to open, Escape to close, consistent with ARIA dialog pattern
app/Blocks/VideoPlayer.php
<?php
// ACF Composer class-based block — fields() defines the ACF schema,
// item() processes the data, with() passes it to the Blade template.

public function fields(): array
{
    $fields = Builder::make('video_player');

    $fields
        ->addUrl('video_url', [
            'label'        => 'Video URL',
            'instructions' => 'YouTube, Vimeo, or Wistia links.',
            'required'     => true,
        ])
        ->addImage('poster_image', [
            'instructions'  => 'Upload a custom poster, or leave blank to auto-fetch from the video service.',
            'return_format' => 'id',
        ])
        ->addRadio('display_mode', [
            'choices'       => ['lightbox' => 'Lightbox Modal', 'inline' => 'Play In Place'],
            'default_value' => 'lightbox',
        ]);

    return $fields->build();
}

public function item(): array
{
    $poster   = get_field('poster_image');
    $video    = get_field('video_url');
    $mode     = get_field('display_mode') ?: 'lightbox';

    // No custom poster? Auto-fetch the thumbnail from the video service.
    $poster_image = $poster
        ? ['url' => wp_get_attachment_image_url($poster, 'large'),
           'alt' => get_post_meta($poster, '_wp_attachment_image_alt', true) ?: '']
        : $this->getVideoServicePoster($video);

    return compact('video', 'poster_image', 'mode');
}

// Detects YouTube, Vimeo, or Wistia URLs and fetches their thumbnails.
private function getVideoServicePoster(?string $url): array
{
    if (!$url) return ['url' => null, 'alt' => ''];

    // YouTube — construct thumbnail URL directly, no API call needed
    if (preg_match('/(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]+)/', $url, $m)) {
        return ['url' => "https://img.youtube.com/vi/{$m[1]}/maxresdefault.jpg",
                'alt' => 'YouTube video poster'];
    }

    // Vimeo — fetch via oEmbed JSON endpoint
    if (preg_match('/vimeo\.com\/\d+/', $url)) {
        $data = json_decode(wp_remote_retrieve_body(
            wp_remote_get("https://vimeo.com/api/oembed.json?url=" . urlencode($url))
        ), true);
        if (!empty($data['thumbnail_url'])) {
            return ['url' => $data['thumbnail_url'], 'alt' => $data['title'] ?? 'Vimeo video poster'];
        }
    }

    // Wistia — fetch via fast.wistia.com oEmbed endpoint
    if (preg_match('/wistia\.(?:com|net)/', $url)) {
        $data = json_decode(wp_remote_retrieve_body(
            wp_remote_get("https://fast.wistia.com/oembed?url=" . urlencode($url))
        ), true);
        if (!empty($data['thumbnail_url'])) {
            return ['url' => $data['thumbnail_url'], 'alt' => $data['title'] ?? 'Wistia video poster'];
        }
    }

    return ['url' => null, 'alt' => ''];
}
resources/js/app/video-player.js
// Accessibility-first lightbox with focus trapping, DOM restoration, and cleanup.
export default class VideoPlayer {
    constructor() {
        this.modal     = document.getElementById('videoPlayerModal');
        this.container = document.getElementById('videoPlayerContainer');
        this.closeBtn  = document.getElementById('videoPlayerClose');
        this.overlay   = document.querySelector('.video-player-overlay');

        // Store bound handlers so they can be removed on destroy()
        this.boundHandlers = { escapeKey: null, closeBtn: null, overlay: null,
                               lightbox: [], inline: [] };
        this.lastFocusedElement = null;

        this.bindEvents();
    }

    openModal(trigger) {
        const videoUrl = trigger.querySelector('.video-data')
            ?.getAttribute('data-embed-code');
        if (!videoUrl) return;

        // Remember what was focused so we can restore it on close
        this.lastFocusedElement  = document.activeElement;

        // Track original DOM position so we can restore it after close
        this.originalParent      = this.modal.parentNode;
        this.originalNextSibling = this.modal.nextSibling;

        // Move to body end for reliable z-index stacking context
        document.body.appendChild(this.modal);

        this.container.innerHTML = this.generateEmbedCode(videoUrl);
        this.modal.classList.replace('hidden', 'block');
        document.body.classList.add('overflow-hidden');

        // Animate in on the next frame
        setTimeout(() => {
            this.overlay?.classList.add('opacity-100');
            this.container.classList.add('scale-100', 'opacity-100');
        }, 10);

        // Mark all other body children inert — traps focus + prevents interaction
        this.setBackgroundInert(true);

        // Shift keyboard focus to the close button for screen reader users
        setTimeout(() => this.closeBtn?.focus(), 100);
    }

    closeModal() {
        this.overlay?.classList.remove('opacity-100');
        this.container.classList.remove('scale-100', 'opacity-100');

        setTimeout(() => {
            this.modal.classList.replace('block', 'hidden');
            this.container.innerHTML = '';
            document.body.classList.remove('overflow-hidden');

            // Restore modal to its original position in the DOM
            this.originalNextSibling
                ? this.originalParent.insertBefore(this.modal, this.originalNextSibling)
                : this.originalParent.appendChild(this.modal);

            // Re-enable background content
            this.setBackgroundInert(false);

            // Return focus to the element that triggered the modal
            this.lastFocusedElement?.focus();
            this.lastFocusedElement = null;
        }, 300);
    }

    setBackgroundInert(isInert) {
        Array.from(document.body.children)
            .filter(child => child !== this.modal)
            .forEach(child => isInert
                ? child.setAttribute('inert', '')
                : child.removeAttribute('inert')
            );
    }

    // Full cleanup for SPA-style re-initialization
    destroy() {
        this.boundHandlers.lightbox.forEach(({ trigger, clickHandler, keyHandler }) => {
            trigger.removeEventListener('click', clickHandler);
            trigger.removeEventListener('keydown', keyHandler);
        });
        if (this.boundHandlers.escapeKey) {
            document.removeEventListener('keydown', this.boundHandlers.escapeKey);
        }
    }
}
Tailwind CSS v4 WordPress Theme Development

Composable Clip-Path Utility System

A Tailwind v4 @utility system for CSS polygon clip-paths where each corner is individually controlled via CSS custom properties — composable, responsive, and sub-pixel accurate.

Tailwind v4 @utility API

  • Custom properties as composition layer: --clip-y-tl, --clip-y-tr, etc. let individual utilities set just one corner without affecting others
  • clip-quad reads all four CSS custom properties and applies the clip-path: polygon() — utilities stack composably
  • Utilities accept both spacing scale integers (clip-tl-4) and arbitrary lengths (clip-tl-[2rem])

Sub-Pixel & Gap Handling

  • 1px bleed: each polygon corner is nudged 1px outward to prevent hairline gaps caused by browser sub-pixel rendering
  • clip-pull-top/bottom utilities use calc(-1 * max(...)) to close the visible gap between adjacent clipped sections

Smart Auto-Pull with :has()

A CSS general sibling rule detects when a clipped-bottom section is directly followed by a clipped-top section and automatically applies the negative margin — no extra utility class required on the second element.

Usage

<!-- Angled bottom-right corner -->
<section class="clip-quad clip-br-14">...</section>

<!-- Angled top and bottom, both sides -->
<section class="clip-quad clip-tl-14 clip-br-14">...</section>

<!-- Auto-pull: no extra class needed when sections are adjacent -->
<section class="clip-quad clip-br-14">Section A</section>
<section class="clip-quad clip-tl-14">Section B</section>
resources/css/utilities/shapes.css
/* Tailwind v4 @utility API — composable CSS clip-path system.
 * Each corner is independently controllable via CSS custom properties.
 *
 * Usage example:
 *   <div class="clip-quad clip-tl-0 clip-tr-14 clip-bl-0 clip-br-14">
 */

/* Corner offset utilities — accept spacing scale integers or arbitrary lengths */
@utility clip-tl-* {
  --clip-y-tl: --spacing(--value(integer));
  --clip-y-tl: --value([length]);
}
@utility clip-tr-* { --clip-y-tr: --spacing(--value(integer)); --clip-y-tr: --value([length]); }
@utility clip-bl-* { --clip-y-bl: --spacing(--value(integer)); --clip-y-bl: --value([length]); }
@utility clip-br-* { --clip-y-br: --spacing(--value(integer)); --clip-y-br: --value([length]); }

/* Base utility — applies the clip-path polygon using the four CSS custom properties */
@utility clip-quad {
  --clip-x-tl: 0px; --clip-y-tl: 0px;
  --clip-x-tr: 0px; --clip-y-tr: 0px;
  --clip-x-bl: 0px; --clip-y-bl: 0px;
  --clip-x-br: 0px; --clip-y-br: 0px;

  /* 1px adjustment per corner compensates for sub-pixel rendering inaccuracies */
  clip-path: polygon(
    calc(var(--clip-x-tl) - 1px) calc(var(--clip-y-tl) - 1px),
    calc(100% - var(--clip-x-tr) + 1px) calc(var(--clip-y-tr) - 1px),
    calc(100% - var(--clip-x-br) + 1px) calc(100% - var(--clip-y-br) + 1px),
    calc(var(--clip-x-bl) - 1px) calc(100% - var(--clip-y-bl) + 1px)
  );
}

/* Pull utilities close the visual gap between adjacent clipped sections */
@utility clip-pull-top    { margin-top:    calc(-1 * max(var(--clip-y-tl, 0), var(--clip-y-tr, 0))); }
@utility clip-pull-bottom { margin-bottom: calc(-1 * max(var(--clip-y-bl, 0), var(--clip-y-br, 0))); }

/* Auto-pull using :has() — if a clipped-bottom block precedes a clipped-top block,
 * apply the negative margin automatically without extra utility classes */
:is(.clip-quad):where([class*="clip-b"],[class*="clip-y-b"]):not(.clip-pull-none)
+ :is(.clip-quad):where([class*="clip-t"],[class*="clip-y-t"]):not(.clip-pull-none) {
  @apply clip-pull-top;
}

}

Want to see what these blocks power?

View Featured Projects