$name, 'attrs' => $attrs, 'innerBlocks' => $inner_blocks, 'innerHTML' => $inner_html, 'innerContent' => [$inner_html], ]; } /** * Group block (tagName configurable). Used for sections, containers, cards. * Children are kept as innerBlocks; innerContent uses NULL placeholders so * Gutenberg slots them in at render time. */ function av_n_group($attrs, $children, $tag = 'div') { $attrs = array_merge(['tagName' => $tag], $attrs); $class = trim('wp-block-group ' . ($attrs['className'] ?? '')); // Build innerContent: opening tag, then NULL per child, then closing tag. $inner_content = ['<' . $tag . ' class="' . esc_attr($class) . '">']; foreach ($children as $child) { $inner_content[] = null; } $inner_content[] = ''; return [ 'blockName' => 'core/group', 'attrs' => $attrs, 'innerBlocks' => $children, 'innerHTML' => '<' . $tag . ' class="' . esc_attr($class) . '">', 'innerContent' => $inner_content, ]; } function av_n_heading($text, $level = 2, $class = '') { $tag = 'h' . max(1, min(6, (int) $level)); $cls = trim('wp-block-heading ' . $class); $html = '<' . $tag . ' class="' . esc_attr($cls) . '">' . esc_html($text) . ''; return [ 'blockName' => 'core/heading', 'attrs' => array_filter([ 'level' => (int) $level, 'className' => $class ?: null, ]), 'innerBlocks' => [], 'innerHTML' => $html, 'innerContent' => [$html], ]; } function av_n_paragraph($text, $class = '') { $html = $class ? '

' . esc_html($text) . '

' : '

' . esc_html($text) . '

'; return [ 'blockName' => 'core/paragraph', 'attrs' => $class ? ['className' => $class] : [], 'innerBlocks' => [], 'innerHTML' => $html, 'innerContent' => [$html], ]; } function av_n_button($label, $href, $variants = 'av-btn av-btn--primary av-btn--lg') { $btn_html = '
' . esc_html($label) . '
'; return [ 'blockName' => 'core/button', 'attrs' => ['className' => $variants], 'innerBlocks' => [], 'innerHTML' => $btn_html, 'innerContent' => [$btn_html], ]; } function av_n_buttons_row($buttons, $class = 'av-hero__ctas') { $opening = '
'; $closing = '
'; $inner_content = [$opening]; foreach ($buttons as $b) { $inner_content[] = null; } $inner_content[] = $closing; return [ 'blockName' => 'core/buttons', 'attrs' => ['className' => $class], 'innerBlocks' => $buttons, 'innerHTML' => $opening . $closing, 'innerContent' => $inner_content, ]; } function av_n_column($children, $width = null) { $attrs = $width ? ['width' => $width] : []; $opening = '
'; $closing = '
'; $inner_content = [$opening]; foreach ($children as $c) $inner_content[] = null; $inner_content[] = $closing; return [ 'blockName' => 'core/column', 'attrs' => $attrs, 'innerBlocks' => $children, 'innerHTML' => $opening . $closing, 'innerContent' => $inner_content, ]; } function av_n_columns($columns, $class = 'av-grid-4') { $opening = '
'; $closing = '
'; $inner_content = [$opening]; foreach ($columns as $c) $inner_content[] = null; $inner_content[] = $closing; return [ 'blockName' => 'core/columns', 'attrs' => array_filter(['className' => $class, 'isStackedOnMobile' => true]), 'innerBlocks' => $columns, 'innerHTML' => $opening . $closing, 'innerContent' => $inner_content, ]; } /* ========================================================================== */ /* Industries overview hub — proof of concept */ /* ========================================================================== */ function av_n_render_industries_overview() { $home = home_url('/'); $industries = [ ['eyebrow' => 'National & Municipal', 'title' => 'Police', 'excerpt' => 'Vehicle stops, identity checks, urban CQB, de-escalation, riot containment. Built for the realities of contemporary policing.', 'href' => '/industries/police/', 'cta' => 'Police training'], ['eyebrow' => 'Tactical units', 'title' => 'Special Forces', 'excerpt' => 'Hostage rescue, dynamic entries, hostile crowd extraction, sniper-spotter coordination. Built for the units where every error has a name.', 'href' => '/industries/special-forces/', 'cta' => 'Special forces training'], ['eyebrow' => 'Armed forces', 'title' => 'Military', 'excerpt' => 'MOUT, convoy escort, FOB defense, IED awareness, joint operation drills. Built for armed forces that deploy fast and adapt faster.', 'href' => '/industries/military/', 'cta' => 'Military training'], ['eyebrow' => 'Emergency services', 'title' => 'Firefighters', 'excerpt' => "Structure fires, hazmat, multi-casualty triage, confined-space rescue, industrial accidents. Built for first responders who can't rehearse the worst day at full scale.", 'href' => '/industries/firefighters/', 'cta' => 'Firefighter training'], ]; /* ----- Hero (section / container / inner) ----- */ $hero_inner = av_n_group( ['className' => 'av-container av-hero__inner', 'layout' => ['type' => 'constrained']], [ av_n_paragraph('Industries', 'av-hero__eyebrow'), av_n_heading('Built with operators, for operators.', 1, 'av-hero__headline'), av_n_paragraph( 'PROSERVE™ is not a generic VR platform repackaged for the public sector. Every scenario, every weapon profile, every instructor metric was specified by serving and former operators in close partnership with our engineering team.', 'av-hero__sub' ), ], 'div' ); $hero = av_n_group( ['className' => 'av-hero'], [$hero_inner], 'section' ); /* ----- Cards section ----- */ $card_columns = []; foreach ($industries as $ind) { $card_inner = av_n_group( ['className' => 'av-card__body'], [ av_n_paragraph($ind['eyebrow'], 'av-card__eyebrow'), av_n_heading($ind['title'], 2, 'av-card__title'), av_n_paragraph($ind['excerpt'], 'av-card__excerpt'), av_n_buttons_row( [av_n_button($ind['cta'], $home . ltrim($ind['href'], '/'), 'av-btn av-btn--secondary av-btn--md')], 'av-card__cta' ), ], 'div' ); $card = av_n_group( ['className' => 'av-card'], [$card_inner], 'article' ); $card_columns[] = av_n_column([$card]); } $cards_grid = av_n_columns($card_columns, 'av-grid-4'); $section_container = av_n_group( ['className' => 'av-container', 'layout' => ['type' => 'constrained']], [$cards_grid], 'div' ); $section = av_n_group( ['className' => 'av-section'], [$section_container], 'section' ); return serialize_blocks([$hero, $section]); }