Files
AsterionWP2026/wp-content/themes/asterion-bricks/inc/render-blocks-native.php
j.foucher 026be81373 chore: session 2 snapshot — archive before Bricks-first restart
Snapshots the project at the end of session 2, just before resetting to a
Bricks-first orthodox architecture. RESTART-PLAN.md at the root explains
why this approach is being abandoned and what the next session will look like.

Why we restart
- Three architectures got stacked: Custom PHP Templates → Gutenberg blocks
  in post_content → Bricks data in post meta. Each layer fights the next.
- Bricks Builder UI does not load because our header.php / footer.php /
  page.php emit chrome that conflicts with Bricks' own template-parts.
- CSS overrides multiplied (Gutenberg + Bricks variants) and still don't
  pixel-match the original PHP-rendered visual.
- 8+ one-shot mu-plugins live in LocalWP to patch over inconsistencies.

What survives the restart (preserved here on main)
- BRIEF, both PDFs, .txt extracts (sources of truth)
- assets/css/tokens.css (full design system, untouched)
- assets/fonts/Inter-Variable*.woff2 (RGPD self-hosted)
- inc/<section>-data.php files (text copy from PDF strategie, will become
  /content-source/ on next session for reuse as translation dictionary
  and as input to programmatic page generation)
- The Git repo, remote, branch main
- LocalWP site, Bricks v2.3.4, WPML 4.9.3 with active licenses
- WPML EN + FR setup with directory URL strategy

What gets wiped at the start of next session
- wp-content/themes/asterion-bricks/header.php / footer.php / page.php /
  index.php / front-page.php / template-parts/ / templates/
- inc/render-blocks.php, inc/render-blocks-native.php, inc/render-bricks.php
- inc/shortcodes.php, inc/i18n.php, inc/form-handler.php
- 35+ WP pages created over the session
- All one-shot mu-plugins in LocalWP/wp-content/mu-plugins/

Restart approach (detailed in RESTART-PLAN.md)
- Minimalist child theme: style.css, functions.php, theme.json, tokens.css,
  trimmed utilities.css, fonts, inc/cpt.php — that's it.
- No header.php / footer.php / page.php in child theme — Bricks Templates
  (Header / Footer / Single — Page) take over via Bricks' Conditions.
- User builds 5-7 archetype Bricks templates visually (~1-2h each) then
  programmatic generation fills the variants from inc/*-data.php sources.
- WPML duplication after EN is validated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:30:51 +02:00

229 lines
8.9 KiB
PHP

<?php
/**
* Asterion Bricks — Native Gutenberg block renderer (POC for Industries)
*
* Generates strictly-standard Gutenberg blocks (core/group, core/heading,
* core/paragraph, core/columns, core/column, core/buttons, core/button,
* core/list) so that:
*
* 1. Pages are editable in the WP admin block editor — every text node
* is a real block, not a raw HTML wrapper.
* 2. "Edit with Bricks" converts these to Bricks elements (Container,
* Heading, Text-Basic, Button, …) per Bricks Builder docs.
* 3. WPML's Translation Editor exposes one entry per block.
*
* Convention: every block carries a `className` attribute matching our
* existing `av-*` CSS classes so the rendered HTML inherits the design
* system styles.
*
* Public API (POC, will replace inc/render-blocks.php once validated):
* av_n_render_industries_overview() → string (single hub page)
*
* @package AsterionBricks
*/
defined('ABSPATH') || exit;
/**
* Build a Gutenberg block array (the shape `serialize_blocks()` expects).
* `innerHTML` is the wrapper template — Gutenberg uses it as a delimiter
* map, with NULL entries marking where children get inlined.
*/
function av_n_block($name, $attrs = [], $inner_blocks = [], $inner_html = '') {
return [
'blockName' => $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[] = '</' . $tag . '>';
return [
'blockName' => 'core/group',
'attrs' => $attrs,
'innerBlocks' => $children,
'innerHTML' => '<' . $tag . ' class="' . esc_attr($class) . '"></' . $tag . '>',
'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) . '</' . $tag . '>';
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
? '<p class="' . esc_attr($class) . '">' . esc_html($text) . '</p>'
: '<p>' . esc_html($text) . '</p>';
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 = '<div class="wp-block-button ' . esc_attr($variants) . '"><a class="wp-block-button__link wp-element-button" href="' . esc_url($href) . '">' . esc_html($label) . '</a></div>';
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 = '<div class="wp-block-buttons ' . esc_attr($class) . '">';
$closing = '</div>';
$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 = '<div class="wp-block-column"' . ($width ? ' style="flex-basis:' . esc_attr($width) . '"' : '') . '>';
$closing = '</div>';
$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 = '<div class="wp-block-columns ' . esc_attr($class) . '">';
$closing = '</div>';
$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]);
}