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>
This commit is contained in:
228
wp-content/themes/asterion-bricks/inc/render-blocks-native.php
Normal file
228
wp-content/themes/asterion-bricks/inc/render-blocks-native.php
Normal file
@@ -0,0 +1,228 @@
|
||||
<?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]);
|
||||
}
|
||||
258
wp-content/themes/asterion-bricks/inc/render-bricks.php
Normal file
258
wp-content/themes/asterion-bricks/inc/render-bricks.php
Normal file
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
/**
|
||||
* Asterion Bricks — Bricks Builder native data renderer (POC: Industries)
|
||||
*
|
||||
* Generates the array of element descriptors that Bricks Builder expects in
|
||||
* the post meta `_bricks_page_content_2` (constant `BRICKS_DB_PAGE_CONTENT`).
|
||||
*
|
||||
* Schema, confirmed by reading Bricks parent theme source
|
||||
* (`includes/password-protection.php` and `includes/elements/base.php`):
|
||||
*
|
||||
* [
|
||||
* [
|
||||
* 'id' => string, // unique, via Bricks\Helpers::generate_random_id(false)
|
||||
* 'name' => 'section' | 'container' | 'block' | 'heading' | 'text-basic' | 'button' | …,
|
||||
* 'parent' => 0 | string, // 0 = page root, else parent's id
|
||||
* 'children' => [string, …], // optional, for nestable elements
|
||||
* 'settings' => [
|
||||
* 'text' => 'Hello',
|
||||
* 'tag' => 'h1' | 'h2' | …,
|
||||
* 'link' => ['type' => 'external', 'url' => 'https://…'],
|
||||
* '_cssClasses' => 'av-hero',
|
||||
* …
|
||||
* ],
|
||||
* ],
|
||||
* …
|
||||
* ]
|
||||
*
|
||||
* Once stored, opening the page in WP admin and clicking "Edit with Bricks"
|
||||
* surfaces every element in the Bricks builder canvas, fully editable.
|
||||
*
|
||||
* Frontend rendering is handled by Bricks itself — our `page.php` simply
|
||||
* lets Bricks take over via its `template_include` filter.
|
||||
*
|
||||
* @package AsterionBricks
|
||||
*/
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Generate a Bricks-compatible random element ID.
|
||||
* Falls back to a local md5 prefix if the Bricks namespace isn't loaded yet.
|
||||
*/
|
||||
function av_brx_id() {
|
||||
if (class_exists('\\Bricks\\Helpers') && method_exists('\\Bricks\\Helpers', 'generate_random_id')) {
|
||||
return \Bricks\Helpers::generate_random_id(false);
|
||||
}
|
||||
return substr(md5(uniqid((string) mt_rand(), true)), 0, 6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tiny element factory.
|
||||
*/
|
||||
function av_brx_el($name, $parent, $settings = [], $children = null) {
|
||||
$el = [
|
||||
'id' => av_brx_id(),
|
||||
'name' => $name,
|
||||
'parent' => $parent,
|
||||
'settings' => $settings,
|
||||
];
|
||||
if ($children !== null) {
|
||||
$el['children'] = $children;
|
||||
}
|
||||
return $el;
|
||||
}
|
||||
|
||||
/* =================================================================== */
|
||||
/* Industries overview — proof of concept */
|
||||
/* =================================================================== */
|
||||
|
||||
function av_brx_render_industries_overview() {
|
||||
$home_url = home_url('/');
|
||||
|
||||
/* ---- pre-allocate IDs so we can hand them to parents/children ---- */
|
||||
$hero_section_id = av_brx_id();
|
||||
$hero_container_id = av_brx_id();
|
||||
$hero_eyebrow_id = av_brx_id();
|
||||
$hero_h1_id = av_brx_id();
|
||||
$hero_sub_id = av_brx_id();
|
||||
|
||||
$cards_section_id = av_brx_id();
|
||||
$cards_container_id = av_brx_id();
|
||||
$cards_grid_id = av_brx_id();
|
||||
|
||||
$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',
|
||||
],
|
||||
];
|
||||
|
||||
$card_ids = [];
|
||||
foreach ($industries as $i => $_) {
|
||||
$card_ids[$i] = [
|
||||
'card' => av_brx_id(),
|
||||
'eyebrow' => av_brx_id(),
|
||||
'title' => av_brx_id(),
|
||||
'excerpt' => av_brx_id(),
|
||||
'btn' => av_brx_id(),
|
||||
];
|
||||
}
|
||||
$cards_children = array_map(fn($ids) => $ids['card'], $card_ids);
|
||||
|
||||
/* ---- assemble flat element array ---- */
|
||||
$elements = [];
|
||||
|
||||
// 1. Hero section ----------------------------------------------------
|
||||
$elements[] = [
|
||||
'id' => $hero_section_id,
|
||||
'name' => 'section',
|
||||
'parent' => 0,
|
||||
'children' => [$hero_container_id],
|
||||
'settings' => ['_cssClasses' => 'av-hero'],
|
||||
'label' => 'Hero',
|
||||
];
|
||||
$elements[] = [
|
||||
'id' => $hero_container_id,
|
||||
'name' => 'container',
|
||||
'parent' => $hero_section_id,
|
||||
'children' => [$hero_eyebrow_id, $hero_h1_id, $hero_sub_id],
|
||||
'settings' => ['_cssClasses' => 'av-container av-hero__inner'],
|
||||
];
|
||||
$elements[] = [
|
||||
'id' => $hero_eyebrow_id,
|
||||
'name' => 'text-basic',
|
||||
'parent' => $hero_container_id,
|
||||
'settings' => [
|
||||
'text' => 'Industries',
|
||||
'tag' => 'p',
|
||||
'_cssClasses' => 'av-hero__eyebrow',
|
||||
],
|
||||
];
|
||||
$elements[] = [
|
||||
'id' => $hero_h1_id,
|
||||
'name' => 'heading',
|
||||
'parent' => $hero_container_id,
|
||||
'settings' => [
|
||||
'tag' => 'h1',
|
||||
'text' => 'Built with operators, for operators.',
|
||||
'_cssClasses' => 'av-hero__headline',
|
||||
],
|
||||
];
|
||||
$elements[] = [
|
||||
'id' => $hero_sub_id,
|
||||
'name' => 'text-basic',
|
||||
'parent' => $hero_container_id,
|
||||
'settings' => [
|
||||
'text' => '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.',
|
||||
'tag' => 'p',
|
||||
'_cssClasses' => 'av-hero__sub',
|
||||
],
|
||||
];
|
||||
|
||||
// 2. Cards section ---------------------------------------------------
|
||||
$elements[] = [
|
||||
'id' => $cards_section_id,
|
||||
'name' => 'section',
|
||||
'parent' => 0,
|
||||
'children' => [$cards_container_id],
|
||||
'settings' => ['_cssClasses' => 'av-section'],
|
||||
'label' => 'Industries grid',
|
||||
];
|
||||
$elements[] = [
|
||||
'id' => $cards_container_id,
|
||||
'name' => 'container',
|
||||
'parent' => $cards_section_id,
|
||||
'children' => [$cards_grid_id],
|
||||
'settings' => ['_cssClasses' => 'av-container'],
|
||||
];
|
||||
$elements[] = [
|
||||
'id' => $cards_grid_id,
|
||||
'name' => 'block',
|
||||
'parent' => $cards_container_id,
|
||||
'children' => $cards_children,
|
||||
'settings' => ['_cssClasses' => 'av-grid-4'],
|
||||
];
|
||||
|
||||
foreach ($industries as $i => $ind) {
|
||||
$ids = $card_ids[$i];
|
||||
|
||||
$elements[] = [
|
||||
'id' => $ids['card'],
|
||||
'name' => 'block',
|
||||
'parent' => $cards_grid_id,
|
||||
'children' => [$ids['eyebrow'], $ids['title'], $ids['excerpt'], $ids['btn']],
|
||||
'settings' => ['_cssClasses' => 'av-card'],
|
||||
'label' => $ind['title'],
|
||||
];
|
||||
$elements[] = [
|
||||
'id' => $ids['eyebrow'],
|
||||
'name' => 'text-basic',
|
||||
'parent' => $ids['card'],
|
||||
'settings' => [
|
||||
'text' => $ind['eyebrow'],
|
||||
'tag' => 'p',
|
||||
'_cssClasses' => 'av-card__eyebrow',
|
||||
],
|
||||
];
|
||||
$elements[] = [
|
||||
'id' => $ids['title'],
|
||||
'name' => 'heading',
|
||||
'parent' => $ids['card'],
|
||||
'settings' => [
|
||||
'tag' => 'h2',
|
||||
'text' => $ind['title'],
|
||||
'_cssClasses' => 'av-card__title',
|
||||
],
|
||||
];
|
||||
$elements[] = [
|
||||
'id' => $ids['excerpt'],
|
||||
'name' => 'text-basic',
|
||||
'parent' => $ids['card'],
|
||||
'settings' => [
|
||||
'text' => $ind['excerpt'],
|
||||
'tag' => 'p',
|
||||
'_cssClasses' => 'av-card__excerpt',
|
||||
],
|
||||
];
|
||||
$elements[] = [
|
||||
'id' => $ids['btn'],
|
||||
'name' => 'button',
|
||||
'parent' => $ids['card'],
|
||||
'settings' => [
|
||||
'text' => $ind['cta'],
|
||||
'tag' => 'a',
|
||||
'link' => [
|
||||
'type' => 'external',
|
||||
'url' => $home_url . ltrim($ind['href'], '/'),
|
||||
],
|
||||
'_cssClasses' => 'av-btn av-btn--secondary av-btn--md',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return $elements;
|
||||
}
|
||||
Reference in New Issue
Block a user