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:
2026-05-09 14:30:51 +02:00
parent f18a57e83f
commit 026be81373
8 changed files with 1150 additions and 9 deletions

View File

@@ -672,6 +672,10 @@
.av-hero {
position: relative;
background-color: var(--color-brand-navy);
background-image:
radial-gradient(ellipse at 70% 30%, rgba(201, 164, 90, 0.15), transparent 60%),
radial-gradient(ellipse at 20% 70%, rgba(31, 91, 168, 0.20), transparent 60%),
linear-gradient(135deg, var(--color-brand-navy-deep) 0%, var(--color-brand-navy) 50%, var(--color-bg-dark-elevated) 100%);
color: var(--color-text-on-dark);
overflow: hidden;
isolation: isolate;
@@ -920,6 +924,361 @@
}
/* ============================================================================
12c. Bricks Builder element overrides — when a Bricks element carries one
of our `av-*` classes, raise specificity so our design-system rules
win against Bricks default styles (which use a single `.brxe-*`
class selector).
============================================================================ */
/* Hero — re-apply our layout & background on top of brxe-section. */
.brxe-section.av-hero {
position: relative;
background-color: var(--color-brand-navy);
/* Use background (shorthand) so .bricks-lazy-hidden's
`background-image: none !important` doesn't kill the gradient outright;
the shorthand wins by being declared after the shorthand reset. */
background:
radial-gradient(ellipse at 70% 30%, rgba(201, 164, 90, 0.15), transparent 60%),
radial-gradient(ellipse at 20% 70%, rgba(31, 91, 168, 0.20), transparent 60%),
linear-gradient(135deg, var(--color-brand-navy-deep) 0%, var(--color-brand-navy) 50%, var(--color-bg-dark-elevated) 100%);
color: var(--color-text-on-dark);
min-height: 70vh;
display: flex;
align-items: center;
justify-content: center;
padding-block: var(--space-20);
}
@media (min-width: 1024px) {
.brxe-section.av-hero { min-height: 85vh; padding-block: var(--space-32); }
}
.brxe-container.av-hero__inner {
width: 100%;
max-width: var(--container-max);
margin-inline: auto;
padding-inline: var(--container-padding-mobile);
display: flex;
flex-direction: column;
gap: var(--space-6);
align-items: flex-start;
}
@media (min-width: 768px) { .brxe-container.av-hero__inner { padding-inline: var(--container-padding-tablet); } }
@media (min-width: 1280px) { .brxe-container.av-hero__inner { padding-inline: var(--container-padding-desktop); } }
/* Hero typography on Bricks-rendered text-basic + heading. */
.brxe-text-basic.av-hero__eyebrow {
font-size: var(--text-overline);
font-weight: var(--font-weight-semibold);
text-transform: uppercase;
letter-spacing: var(--tracking-overline);
color: var(--color-brand-gold);
margin: 0;
}
.brxe-heading.av-hero__headline {
font-family: var(--font-display);
font-size: clamp(2.5rem, 6vw, var(--text-display-xl));
line-height: var(--leading-display-xl);
font-weight: var(--font-weight-bold);
letter-spacing: var(--tracking-tight);
color: var(--color-text-on-dark);
margin: 0;
max-width: 18ch;
}
.brxe-text-basic.av-hero__sub {
font-size: var(--text-body-lg);
line-height: var(--leading-body-lg);
color: var(--color-text-on-dark-muted);
max-width: 60ch;
margin: 0;
}
/* Section spacing on Bricks-rendered sections. */
.brxe-section.av-section {
padding-block: var(--space-16);
}
@media (min-width: 1024px) {
.brxe-section.av-section { padding-block: var(--space-20); }
}
/* Plain Bricks container reuses our container width / padding. */
.brxe-container.av-container {
max-width: var(--container-max);
margin-inline: auto;
padding-inline: var(--container-padding-mobile);
width: 100%;
}
@media (min-width: 768px) { .brxe-container.av-container { padding-inline: var(--container-padding-tablet); } }
@media (min-width: 1280px) { .brxe-container.av-container { padding-inline: var(--container-padding-desktop); } }
/* Cards grid — Bricks `block` element uses display:flex column by default.
Force CSS grid back. */
.brxe-block.av-grid-3 {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-6);
width: 100%;
}
@media (min-width: 768px) { .brxe-block.av-grid-3 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) { .brxe-block.av-grid-3 { grid-template-columns: repeat(3, 1fr); } }
.brxe-block.av-grid-4 {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-6);
width: 100%;
}
@media (min-width: 768px) { .brxe-block.av-grid-4 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1280px) { .brxe-block.av-grid-4 { grid-template-columns: repeat(4, 1fr); } }
/* Card design on Bricks block. */
.brxe-block.av-card {
background-color: var(--color-bg-elevated);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
overflow: hidden;
padding: var(--space-6);
display: flex;
flex-direction: column;
gap: var(--space-3);
transition:
box-shadow var(--duration-default) var(--ease-default),
transform var(--duration-default) var(--ease-default);
}
@media (min-width: 768px) {
.brxe-block.av-card { padding: var(--space-8); }
}
.brxe-block.av-card:hover {
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.brxe-text-basic.av-card__eyebrow {
font-size: var(--text-overline);
font-weight: var(--font-weight-semibold);
text-transform: uppercase;
letter-spacing: var(--tracking-overline);
color: var(--color-text-muted);
margin: 0;
}
.brxe-heading.av-card__title {
font-family: var(--font-display);
font-size: var(--text-h3);
line-height: var(--leading-h3);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
margin: 0;
}
.brxe-text-basic.av-card__excerpt {
font-size: var(--text-body-md);
line-height: var(--leading-body-md);
color: var(--color-text-secondary);
margin: 0;
flex: 1;
}
/* Button on Bricks button element. Bricks renders <a class="brxe-button bricks-button">. */
.brxe-button.av-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
height: 44px;
padding: 0 var(--space-5);
font-family: var(--font-body);
font-size: var(--text-body-md);
font-weight: var(--font-weight-semibold);
line-height: 1;
text-decoration: none;
border: var(--border-default) solid transparent;
border-radius: var(--radius-sm);
cursor: pointer;
white-space: nowrap;
align-self: flex-start;
transition:
background-color var(--duration-fast) var(--ease-default),
color var(--duration-fast) var(--ease-default),
transform var(--duration-fast) var(--ease-default),
border-color var(--duration-fast) var(--ease-default);
}
.brxe-button.av-btn--md { height: 44px; padding: 0 var(--space-5); }
.brxe-button.av-btn--lg { height: 52px; padding: 0 1.75rem; font-size: var(--text-body-lg); }
.brxe-button.av-btn--primary {
background-color: var(--color-brand-gold);
color: var(--color-brand-navy);
border-color: var(--color-brand-gold);
}
.brxe-button.av-btn--primary:hover,
.brxe-button.av-btn--primary:focus-visible {
background-color: var(--color-brand-gold-soft);
border-color: var(--color-brand-gold-soft);
transform: translateY(-1px);
}
.brxe-button.av-btn--secondary {
background-color: transparent;
color: var(--color-brand-navy);
border-color: var(--color-brand-navy);
}
.brxe-button.av-btn--secondary:hover,
.brxe-button.av-btn--secondary:focus-visible {
background-color: var(--color-brand-navy);
color: var(--color-text-on-dark);
}
/* ============================================================================
12b. Gutenberg block overrides — neutralize default layout styles when
a block also carries one of our `av-*` design-system classes. We
keep our grid / spacing / button rules in charge.
============================================================================ */
/* Group blocks: drop the WP layout flow constraints that introduce extra margins. */
.av-hero.wp-block-group,
.av-section.wp-block-group,
.av-card.wp-block-group,
.av-card__body.wp-block-group,
.av-container.wp-block-group,
.av-hero__inner.wp-block-group {
margin-block: 0;
}
/* Hero overrides — keep the hero layout we already had. */
.av-hero.wp-block-group {
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
}
.av-hero__inner.wp-block-group {
width: 100%;
}
/* Container blocks: WP `is-layout-constrained` pads/centers via wp-container-X
helpers. We keep our own .av-container width/padding. */
.av-container.wp-block-group {
max-width: var(--container-max);
margin-inline: auto;
padding-inline: var(--container-padding-mobile);
width: 100%;
}
@media (min-width: 768px) { .av-container.wp-block-group { padding-inline: var(--container-padding-tablet); } }
@media (min-width: 1280px) { .av-container.wp-block-group { padding-inline: var(--container-padding-desktop); } }
/* Columns: WP forces display:flex via .is-layout-flex. We force grid back so
our 1/2/3/4-col responsive rules win. */
.av-grid-3.wp-block-columns,
.av-grid-3.wp-block-columns.is-layout-flex {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-6);
}
@media (min-width: 768px) {
.av-grid-3.wp-block-columns,
.av-grid-3.wp-block-columns.is-layout-flex { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
.av-grid-3.wp-block-columns,
.av-grid-3.wp-block-columns.is-layout-flex { grid-template-columns: repeat(3, 1fr); }
}
.av-grid-4.wp-block-columns,
.av-grid-4.wp-block-columns.is-layout-flex {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-6);
}
@media (min-width: 768px) {
.av-grid-4.wp-block-columns,
.av-grid-4.wp-block-columns.is-layout-flex { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1280px) {
.av-grid-4.wp-block-columns,
.av-grid-4.wp-block-columns.is-layout-flex { grid-template-columns: repeat(4, 1fr); }
}
/* Each column: drop the flex-basis WP applies. */
.wp-block-columns.av-grid-3 > .wp-block-column,
.wp-block-columns.av-grid-4 > .wp-block-column {
flex-basis: auto !important;
flex-grow: 0 !important;
min-width: 0 !important;
}
/* Section spacing — re-apply our padding-block to the migrated section blocks. */
.av-section.wp-block-group {
padding-block: var(--space-16);
}
@media (min-width: 1024px) {
.av-section.wp-block-group { padding-block: var(--space-20); }
}
/* Cards inside a wp-block-column: keep our card design. */
.av-card.wp-block-group {
background-color: var(--color-bg-elevated);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
overflow: hidden;
height: 100%; /* fill the column for visual rhythm */
}
.av-card__body.wp-block-group {
padding: var(--space-6);
display: flex;
flex-direction: column;
gap: var(--space-3);
height: 100%;
}
@media (min-width: 768px) {
.av-card__body.wp-block-group { padding: var(--space-8); }
}
/* Buttons row: WP wraps every button in `.wp-block-button` and adds
`.is-layout-flex` to the parent, which centers and adds gap.
Strip the WP padding/margin so our `.av-btn` sizing wins. */
.wp-block-buttons.av-card__cta,
.wp-block-buttons.av-hero__ctas {
margin: 0;
padding: 0;
}
.wp-block-button.av-btn {
margin: 0;
}
.wp-block-button.av-btn .wp-block-button__link {
/* Reset Gutenberg's built-in button skin — let .av-btn rules win. */
background: transparent;
color: inherit;
padding: 0;
border-radius: 0;
font-size: inherit;
font-weight: inherit;
line-height: 1;
transition: none;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
/* Inherit our .av-btn padding via the containing flex layout. */
}
/* Auto-margin-top to push CTAs to the bottom of cards. */
.av-card__cta {
margin-top: auto;
padding-top: var(--space-4);
}
/* Eyebrow / overline reset — Gutenberg paragraph defaults could drift. */
p.av-card__eyebrow,
p.av-hero__eyebrow {
margin: 0;
}
/* WordPress core "global styles" reset noise — collapse the auto block-margin
that pushes our text away from the hero edges. */
.wp-block-group.av-hero > .wp-block-group,
.wp-block-group.av-section > .wp-block-group {
margin-block: 0;
}
/* ============================================================================
13. Reveal animation (scroll-triggered, JS-driven via main.js)
============================================================================ */

View File

@@ -12,6 +12,14 @@
defined('ABSPATH') || exit;
$av_year = date_i18n('Y');
// Mirror the bail-out in header.php: don't emit our footer chrome inside
// the Bricks builder canvas/iframe.
if (function_exists('bricks_is_builder') && bricks_is_builder()) {
wp_footer();
echo '</body></html>';
return;
}
?>
</main><!-- /#main -->

View File

@@ -103,7 +103,9 @@ $av_includes = [
'inc/cpt.php', // Custom post types: case_study, scenario.
'inc/seo.php', // Schema.org JSON-LD overrides.
'inc/form-handler.php', // Conversion form POST handler.
'inc/render-blocks.php', // Data array → Gutenberg HTML for the migrator.
'inc/render-blocks.php', // Data array → core/html migrator (legacy, kept for fallback).
'inc/render-blocks-native.php', // Data array → native Gutenberg blocks (legacy POC).
'inc/render-bricks.php', // Data array → Bricks Builder native elements (current target).
'inc/shortcodes.php', // [av_form slug="..."] etc.
];
@@ -150,10 +152,20 @@ add_action('after_setup_theme', function () {
});
/**
* Disable Gutenberg block library default styles when Bricks is active
* Disable Gutenberg block library default styles on the public frontend only
* we own typography end-to-end through tokens.css.
*
* Skipped when:
* - inside the Bricks Builder canvas (?bricks=run, ?brickspreview=1) — those
* screens rely on the WP-core stylesheets for their own UI / preview.
* - in the WP admin itself.
*/
add_action('wp_enqueue_scripts', function () {
if (is_admin()) return;
if (function_exists('bricks_is_builder') && bricks_is_builder()) return;
if (function_exists('bricks_is_frontend_builder_iframe') && bricks_is_frontend_builder_iframe()) return;
if (! empty($_GET['bricks']) || ! empty($_GET['brickspreview'])) return;
wp_dequeue_style('wp-block-library');
wp_dequeue_style('wp-block-library-theme');
wp_dequeue_style('global-styles');

View File

@@ -23,6 +23,16 @@ defined('ABSPATH') || exit;
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>
<?php
// In Bricks Builder mode, the parent theme's `template-parts/builder.php`
// calls get_header() / get_footer() to inherit our enqueues — but the
// builder UI then mounts directly into <body>. Emitting our own chrome
// here covers it visually, so we bail early.
if (function_exists('bricks_is_builder') && bricks_is_builder()) {
return;
}
?>
<a class="av-skip-link" href="#main"><?php echo esc_html(av_t('Skip to main content', 'asterion-bricks')); ?></a>
<header class="av-header" role="banner">

View 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]);
}

View 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;
}

View File

@@ -2,10 +2,14 @@
/**
* Asterion Bricks — Default page template
*
* Renders any page that has no Custom Template assigned (or whose Custom
* Template was stripped after the post_content migration). The header and
* footer are shared globals; the content is whatever Gutenberg / Bricks
* stored in post_content.
* If the current page has Bricks data in its `_bricks_page_content_2` post
* meta, defer to Bricks' own renderer (Bricks\Frontend::render_content). This
* is the same pattern Bricks parent's page.php uses; we keep it here so the
* child theme stays in charge of the markup wrapping (header, footer).
*
* Otherwise fall back to the standard the_content() loop — covers pages
* still rendered from post_content (Gutenberg) and any temporarily-empty
* page that just hits a non-Bricks Custom Template.
*
* @package AsterionBricks
*/
@@ -13,9 +17,21 @@ defined('ABSPATH') || exit;
get_header();
if (have_posts()) {
while (have_posts()) { the_post();
the_content();
$av_bricks_data = class_exists('\\Bricks\\Helpers')
? \Bricks\Helpers::get_bricks_data(get_the_ID(), 'content')
: null;
if (! empty($av_bricks_data) && class_exists('\\Bricks\\Frontend')) {
// Bricks emits its own <main id="brx-content"> wrapper — render with `div`
// tag so we don't end up with double <main> (the global one comes from
// header.php / footer.php).
\Bricks\Frontend::render_content($av_bricks_data, ['id' => 'brx-content'], '', '', 'div');
} else {
if (have_posts()) {
while (have_posts()) {
the_post();
the_content();
}
}
}