Compare commits
4 Commits
archive/se
...
d2badf49d4
| Author | SHA1 | Date | |
|---|---|---|---|
| d2badf49d4 | |||
| af0ed66122 | |||
| 7765d0fab2 | |||
| fba223004f |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -18,6 +18,9 @@ ehthumbs.db
|
|||||||
|
|
||||||
# --- Claude Code local config (per-developer) ---
|
# --- Claude Code local config (per-developer) ---
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
|
# .mcp.json holds the WP Application Password for the Bricks MCP server,
|
||||||
|
# which is per-developer and must never reach the remote.
|
||||||
|
.mcp.json
|
||||||
|
|
||||||
# --- Environment / secrets ---
|
# --- Environment / secrets ---
|
||||||
.env
|
.env
|
||||||
|
|||||||
81
Bricks Config/bricks-styles-color-palette-asterion.json
Normal file
81
Bricks Config/bricks-styles-color-palette-asterion.json
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"id": "a5701b",
|
||||||
|
"name": "Asterion",
|
||||||
|
"colors": [
|
||||||
|
{
|
||||||
|
"light": "#0B1F3A",
|
||||||
|
"raw": "var(--color-brand-navy)",
|
||||||
|
"id": "9c1d2a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#081427",
|
||||||
|
"raw": "var(--color-brand-navy-deep)",
|
||||||
|
"id": "1f4b88"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#C9A45A",
|
||||||
|
"raw": "var(--color-brand-gold)",
|
||||||
|
"id": "e74c3b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#E0C892",
|
||||||
|
"raw": "var(--color-brand-gold-soft)",
|
||||||
|
"id": "62a190"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#C8102E",
|
||||||
|
"raw": "var(--color-alert-red)",
|
||||||
|
"id": "84d709"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#0B1F3A",
|
||||||
|
"raw": "var(--color-text-primary)",
|
||||||
|
"id": "3fb52c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#3E4C5E",
|
||||||
|
"raw": "var(--color-text-secondary)",
|
||||||
|
"id": "57e441"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#6B7785",
|
||||||
|
"raw": "var(--color-text-muted)",
|
||||||
|
"id": "ad8b16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#FFFFFF",
|
||||||
|
"raw": "var(--color-text-on-dark)",
|
||||||
|
"id": "c9027d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#FFFFFF",
|
||||||
|
"raw": "var(--color-bg-default)",
|
||||||
|
"id": "20fa53"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#F5F7FA",
|
||||||
|
"raw": "var(--color-bg-subtle)",
|
||||||
|
"id": "8e6c0f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#0B1F3A",
|
||||||
|
"raw": "var(--color-bg-dark)",
|
||||||
|
"id": "4d318a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#13294B",
|
||||||
|
"raw": "var(--color-bg-dark-elevated)",
|
||||||
|
"id": "f1957e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#CCD3DC",
|
||||||
|
"raw": "var(--color-border-default)",
|
||||||
|
"id": "7b46c2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#1E5BA8",
|
||||||
|
"raw": "var(--color-info)",
|
||||||
|
"id": "06bdfa"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
96
Bricks Config/bricks-styles-color-palette-default.json
Normal file
96
Bricks Config/bricks-styles-color-palette-default.json
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
{
|
||||||
|
"id": "44a09f",
|
||||||
|
"name": "Default",
|
||||||
|
"colors": [
|
||||||
|
{
|
||||||
|
"light": "#f5f5f5",
|
||||||
|
"raw": "var(--bricks-color-grey-100)",
|
||||||
|
"id": "ab2156"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#e0e0e0",
|
||||||
|
"raw": "var(--bricks-color-grey-300)",
|
||||||
|
"id": "664644"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#9e9e9e",
|
||||||
|
"raw": "var(--bricks-color-grey-500)",
|
||||||
|
"id": "eec8d2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#616161",
|
||||||
|
"raw": "var(--bricks-color-grey-700)",
|
||||||
|
"id": "68168c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#424242",
|
||||||
|
"raw": "var(--bricks-color-grey-800)",
|
||||||
|
"id": "5ce47e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#212121",
|
||||||
|
"raw": "var(--bricks-color-grey-900)",
|
||||||
|
"id": "f5049f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#ffeb3b",
|
||||||
|
"raw": "var(--bricks-color-yellow)",
|
||||||
|
"id": "9d011c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#ffc107",
|
||||||
|
"raw": "var(--bricks-color-amber)",
|
||||||
|
"id": "dacd30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#ff9800",
|
||||||
|
"raw": "var(--bricks-color-orange)",
|
||||||
|
"id": "3bcffd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#ff5722",
|
||||||
|
"raw": "var(--bricks-color-deep-orange)",
|
||||||
|
"id": "2ce1e9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#f44336",
|
||||||
|
"raw": "var(--bricks-color-red)",
|
||||||
|
"id": "57b992"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#9c27b0",
|
||||||
|
"raw": "var(--bricks-color-purple)",
|
||||||
|
"id": "5825c2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#2196f3",
|
||||||
|
"raw": "var(--bricks-color-blue)",
|
||||||
|
"id": "0e5dd9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#03a9f4",
|
||||||
|
"raw": "var(--bricks-color-light-blue)",
|
||||||
|
"id": "f5d57b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#81D4FA",
|
||||||
|
"raw": "var(--bricks-color-sky-blue)",
|
||||||
|
"id": "7dd108"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#4caf50",
|
||||||
|
"raw": "var(--bricks-color-green)",
|
||||||
|
"id": "5c8f35"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#8bc34a",
|
||||||
|
"raw": "var(--bricks-color-light-green)",
|
||||||
|
"id": "432723"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#cddc39",
|
||||||
|
"raw": "var(--bricks-color-lime)",
|
||||||
|
"id": "25a3a8"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
179
Bricks Config/color-palette-current.json
Normal file
179
Bricks Config/color-palette-current.json
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "44a09f",
|
||||||
|
"name": "Default",
|
||||||
|
"colors": [
|
||||||
|
{
|
||||||
|
"light": "#f5f5f5",
|
||||||
|
"raw": "var(--bricks-color-grey-100)",
|
||||||
|
"id": "ab2156"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#e0e0e0",
|
||||||
|
"raw": "var(--bricks-color-grey-300)",
|
||||||
|
"id": "664644"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#9e9e9e",
|
||||||
|
"raw": "var(--bricks-color-grey-500)",
|
||||||
|
"id": "eec8d2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#616161",
|
||||||
|
"raw": "var(--bricks-color-grey-700)",
|
||||||
|
"id": "68168c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#424242",
|
||||||
|
"raw": "var(--bricks-color-grey-800)",
|
||||||
|
"id": "5ce47e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#212121",
|
||||||
|
"raw": "var(--bricks-color-grey-900)",
|
||||||
|
"id": "f5049f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#ffeb3b",
|
||||||
|
"raw": "var(--bricks-color-yellow)",
|
||||||
|
"id": "9d011c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#ffc107",
|
||||||
|
"raw": "var(--bricks-color-amber)",
|
||||||
|
"id": "dacd30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#ff9800",
|
||||||
|
"raw": "var(--bricks-color-orange)",
|
||||||
|
"id": "3bcffd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#ff5722",
|
||||||
|
"raw": "var(--bricks-color-deep-orange)",
|
||||||
|
"id": "2ce1e9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#f44336",
|
||||||
|
"raw": "var(--bricks-color-red)",
|
||||||
|
"id": "57b992"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#9c27b0",
|
||||||
|
"raw": "var(--bricks-color-purple)",
|
||||||
|
"id": "5825c2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#2196f3",
|
||||||
|
"raw": "var(--bricks-color-blue)",
|
||||||
|
"id": "0e5dd9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#03a9f4",
|
||||||
|
"raw": "var(--bricks-color-light-blue)",
|
||||||
|
"id": "f5d57b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#81D4FA",
|
||||||
|
"raw": "var(--bricks-color-sky-blue)",
|
||||||
|
"id": "7dd108"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#4caf50",
|
||||||
|
"raw": "var(--bricks-color-green)",
|
||||||
|
"id": "5c8f35"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#8bc34a",
|
||||||
|
"raw": "var(--bricks-color-light-green)",
|
||||||
|
"id": "432723"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#cddc39",
|
||||||
|
"raw": "var(--bricks-color-lime)",
|
||||||
|
"id": "25a3a8"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rudrjy",
|
||||||
|
"name": "Asterion",
|
||||||
|
"colors": [
|
||||||
|
{
|
||||||
|
"light": "#0B1F3A",
|
||||||
|
"raw": "var(--color-brand-navy)",
|
||||||
|
"id": "9c1d2a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#081427",
|
||||||
|
"raw": "var(--color-brand-navy-deep)",
|
||||||
|
"id": "1f4b88"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#C9A45A",
|
||||||
|
"raw": "var(--color-brand-gold)",
|
||||||
|
"id": "e74c3b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#E0C892",
|
||||||
|
"raw": "var(--color-brand-gold-soft)",
|
||||||
|
"id": "62a190"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#C8102E",
|
||||||
|
"raw": "var(--color-alert-red)",
|
||||||
|
"id": "84d709"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#0B1F3A",
|
||||||
|
"raw": "var(--color-text-primary)",
|
||||||
|
"id": "3fb52c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#3E4C5E",
|
||||||
|
"raw": "var(--color-text-secondary)",
|
||||||
|
"id": "57e441"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#6B7785",
|
||||||
|
"raw": "var(--color-text-muted)",
|
||||||
|
"id": "ad8b16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#FFFFFF",
|
||||||
|
"raw": "var(--color-text-on-dark)",
|
||||||
|
"id": "c9027d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#FFFFFF",
|
||||||
|
"raw": "var(--color-bg-default)",
|
||||||
|
"id": "20fa53"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#F5F7FA",
|
||||||
|
"raw": "var(--color-bg-subtle)",
|
||||||
|
"id": "8e6c0f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#0B1F3A",
|
||||||
|
"raw": "var(--color-bg-dark)",
|
||||||
|
"id": "4d318a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#13294B",
|
||||||
|
"raw": "var(--color-bg-dark-elevated)",
|
||||||
|
"id": "f1957e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#CCD3DC",
|
||||||
|
"raw": "var(--color-border-default)",
|
||||||
|
"id": "7b46c2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"light": "#1E5BA8",
|
||||||
|
"raw": "var(--color-info)",
|
||||||
|
"id": "06bdfa"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
7
Bricks Config/global-settings-current.json
Normal file
7
Bricks Config/global-settings-current.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"postTypes": [
|
||||||
|
"page"
|
||||||
|
],
|
||||||
|
"builderMode": "dark",
|
||||||
|
"builderToolbarLogoLink": "current"
|
||||||
|
}
|
||||||
36
Bricks Config/theme-styles-current.json
Normal file
36
Bricks Config/theme-styles-current.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"asterion": {
|
||||||
|
"label": "Asterion",
|
||||||
|
"settings": {
|
||||||
|
"_custom": true,
|
||||||
|
"conditions": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "skuksb",
|
||||||
|
"main": "any"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
"colorPrimary": {
|
||||||
|
"raw": "var(--color-brand-navy)"
|
||||||
|
},
|
||||||
|
"colorSecondary": {
|
||||||
|
"raw": "var(--color-brand-gold)"
|
||||||
|
},
|
||||||
|
"colorLight": {
|
||||||
|
"raw": "var(--color-bg-default)"
|
||||||
|
},
|
||||||
|
"colorDark": {
|
||||||
|
"raw": "var(--color-bg-dark)"
|
||||||
|
},
|
||||||
|
"colorMuted": {
|
||||||
|
"raw": "var(--color-text-muted)"
|
||||||
|
},
|
||||||
|
"colorBorder": {
|
||||||
|
"raw": "var(--color-border-default)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
README.md
41
README.md
@@ -62,31 +62,46 @@ Puis dans WP Admin → Apparence → Thèmes, activer **Asterion Bricks** (le ch
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Approche
|
||||||
|
|
||||||
|
**Bricks-first, child theme minimaliste.** Le rendu de chaque page est piloté par Bricks Builder (Templates pour header/footer/archives + pages éditées dans le builder). Le child theme se contente d'apporter le design system (tokens CSS), Inter self-hosté (RGPD) et les Custom Post Types. Voir `wp-content/themes/asterion-bricks/README.md` pour le détail.
|
||||||
|
|
||||||
|
Une session de PHP-rendering plus traditionnelle a été archivée dans la branche Git `archive/session-2-php-templates` ainsi que dans `RESTART-PLAN.md` à la racine. Ne pas réintroduire ce pattern sans relire ces deux sources.
|
||||||
|
|
||||||
## Structure du repo
|
## Structure du repo
|
||||||
|
|
||||||
```
|
```
|
||||||
WP2026/
|
WP2026/
|
||||||
├── BRIEF-Claude-Code.md # brief projet (lecture obligatoire)
|
├── BRIEF-Claude-Code.md # brief projet (lecture obligatoire)
|
||||||
├── README.md # ce fichier
|
├── README.md # ce fichier
|
||||||
|
├── RESTART-PLAN.md # historique de la décision Bricks-first
|
||||||
├── MISSING-ASSETS.md # tracking des visuels/medias manquants
|
├── MISSING-ASSETS.md # tracking des visuels/medias manquants
|
||||||
├── *.pdf # 2 livrables stratégie + design (sources de vérité)
|
├── *.pdf # 2 livrables stratégie + design (sources de vérité)
|
||||||
├── ThirdParty/ # gitignored — ZIP plugins commerciaux à déposer manuellement
|
├── ThirdParty/ # gitignored — ZIP plugins commerciaux
|
||||||
│ ├── bricks.*.zip
|
│ ├── bricks.*.zip
|
||||||
│ └── sitepress-multilingual-cms.*.zip
|
│ └── sitepress-multilingual-cms.*.zip
|
||||||
|
├── content-source/ # copy EN extrait du PDF (référence + génération)
|
||||||
|
│ ├── solutions-data.php
|
||||||
|
│ ├── industries-data.php
|
||||||
|
│ ├── technology-data.php
|
||||||
|
│ ├── editorial-data.php
|
||||||
|
│ ├── forms-data.php
|
||||||
|
│ └── README.md
|
||||||
└── wp-content/
|
└── wp-content/
|
||||||
└── themes/
|
└── themes/
|
||||||
└── asterion-bricks/ # le child theme — coeur du travail
|
└── asterion-bricks/ # child theme minimaliste, voir son README
|
||||||
├── style.css # en-tête WP + import tokens
|
├── style.css
|
||||||
├── functions.php # hooks, registers, queries
|
├── functions.php
|
||||||
├── theme.json # tokens globaux (Gutenberg + Bricks)
|
├── theme.json
|
||||||
├── assets/
|
├── README.md
|
||||||
│ ├── css/ # tokens.css, components.css, utilities.css
|
├── inc/
|
||||||
│ ├── js/ # vanilla JS, defer
|
│ └── cpt.php
|
||||||
│ ├── fonts/ # Inter + Inter Display self-hosted (RGPD)
|
└── assets/
|
||||||
│ └── img/
|
├── css/
|
||||||
├── templates/ # exports Bricks JSON (versionnés)
|
│ ├── tokens.css # design system, source de vérité
|
||||||
├── inc/ # CPT, ACF field groups, SEO schema
|
│ └── utilities.css # skip-link, sr-only, focus-visible
|
||||||
└── README.md
|
├── fonts/ # Inter Variable self-hosté
|
||||||
|
└── img/
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
52
content-source/README.md
Normal file
52
content-source/README.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Content source — copy reference
|
||||||
|
|
||||||
|
PHP arrays of all the textual content authored in the Strategy & Content PDF (`Asterion-VR_Refonte-2026_Strategie-et-Contenu.pdf`, sections D.1 to D.29). Each file holds the EN copy for one site section, keyed by page slug.
|
||||||
|
|
||||||
|
These files are **not** loaded at runtime. They serve two purposes:
|
||||||
|
|
||||||
|
1. **Reference while building Bricks pages** — open the file matching the page you're editing and copy/paste the EN copy into the Bricks elements (Heading, Basic Text, Button…).
|
||||||
|
2. **Source for programmatic generation** — once a Bricks archetype template is built and validated visually, a one-shot script can iterate over the data here, clone the template, swap in each variant's strings, and create the WP page programmatically. This avoids rebuilding the same layout 4–6 times for similar pages (e.g. the 4 industry detail pages).
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Pages covered |
|
||||||
|
|---|---|
|
||||||
|
| `solutions-data.php` | `/solutions/my-proserve/`, `/solutions/proserve-flex/`, `/solutions/proserve-academy/`, `/solutions/customization/` |
|
||||||
|
| `industries-data.php` | `/industries/police/`, `/industries/special-forces/`, `/industries/military/`, `/industries/firefighters/` |
|
||||||
|
| `technology-data.php` | 6 technology pillars under `/technology/` |
|
||||||
|
| `editorial-data.php` | Why Asterion, About, Customers hub, Trust, Partners |
|
||||||
|
| `forms-data.php` | Conversion forms (Demo, T&E Kit, Quote, Contact) |
|
||||||
|
|
||||||
|
## Schema
|
||||||
|
|
||||||
|
Every entry follows roughly the same shape :
|
||||||
|
|
||||||
|
```php
|
||||||
|
'slug-of-page' => [
|
||||||
|
'eyebrow' => 'Small overline above the H1',
|
||||||
|
'title' => 'Short page title',
|
||||||
|
'tagline' => 'Hero H1',
|
||||||
|
'meta_title' => 'For <head><title> + OpenGraph',
|
||||||
|
'meta_description' => 'For <head> meta description',
|
||||||
|
'hero_lead' => 'Long sub-headline under the H1',
|
||||||
|
'ctas' => [['label' => '...', 'href' => '/...', 'variant' => 'primary'], …],
|
||||||
|
'sections' => [
|
||||||
|
['type' => 'prose' , 'eyebrow' => '…', 'title' => '…', 'body' => '…'],
|
||||||
|
['type' => 'cards' , 'eyebrow' => '…', 'title' => '…', 'items' => [['title' => '…', 'body' => '…'], …]],
|
||||||
|
['type' => 'list' , 'eyebrow' => '…', 'title' => '…', 'items' => ['line 1', 'line 2', …]],
|
||||||
|
['type' => 'cta' , 'eyebrow' => '…', 'title' => '…', 'body' => '…', 'ctas' => […]],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
`forms-data.php` deviates slightly — see its inline header comment.
|
||||||
|
|
||||||
|
## Why we keep these in PHP arrays (vs JSON / YAML / Markdown)
|
||||||
|
|
||||||
|
- They were extracted programmatically from the PDF and were already used by the previous PHP-rendering approach. Keeping the format means zero re-extraction work.
|
||||||
|
- A future PHP migration script will `include $file_path` and use the returned array directly — no parsing layer required.
|
||||||
|
- For the human reading them while building in Bricks, plain PHP arrays are perfectly readable.
|
||||||
|
|
||||||
|
## French translations
|
||||||
|
|
||||||
|
These files are **EN only**. The FR translations will live inside WordPress, written by the linguist via WPML's Translation Editor against each Bricks-edited page.
|
||||||
69
tools/README.md
Normal file
69
tools/README.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# Tools
|
||||||
|
|
||||||
|
One-shot scripts and helpers used during project setup or migrations. They are NOT part of the published site.
|
||||||
|
|
||||||
|
## fresh-setup-mu-plugin.php
|
||||||
|
|
||||||
|
Drop into `wp-content/mu-plugins/` of a freshly created LocalWP site to auto-configure WordPress for the Asterion VR project. Read its inline header for the full procedure.
|
||||||
|
|
||||||
|
Quick recap :
|
||||||
|
|
||||||
|
1. Delete the old LocalWP site (right-click → Delete site, optionally move folder to trash).
|
||||||
|
2. Create a new LocalWP site named "Asterion 2026", mode `localhost`, default credentials.
|
||||||
|
3. Note the allocated port — likely `10004` again. **If different, edit `AV_TARGET_HOME` in `fresh-setup-mu-plugin.php` first.**
|
||||||
|
4. Junction the child theme into the new site:
|
||||||
|
```powershell
|
||||||
|
New-Item -ItemType Junction `
|
||||||
|
-Path "C:\Users\j_foucher\Local Sites\asterion-2026\app\public\wp-content\themes\asterion-bricks" `
|
||||||
|
-Target "C:\ASTERION\GIT\WP2026\wp-content\themes\asterion-bricks"
|
||||||
|
```
|
||||||
|
5. Extract the Bricks parent **theme** into `wp-content/themes/`. Bricks is a theme (not a plugin) — installing the ZIP via WP admin → Plugins → Add New will fail with "No valid plugins were found".
|
||||||
|
```powershell
|
||||||
|
$site = "C:\Users\j_foucher\Local Sites\asterion-2026\app\public\wp-content"
|
||||||
|
Expand-Archive "C:\ASTERION\GIT\WP2026\ThirdParty\bricks.2.3.4.zip" "$site\themes\" -Force
|
||||||
|
```
|
||||||
|
(Or via WP admin → Appearance → Themes → Add New Theme → Upload Theme.)
|
||||||
|
6. Extract WPML **plugin** into `wp-content/plugins/`:
|
||||||
|
```powershell
|
||||||
|
Expand-Archive "C:\ASTERION\GIT\WP2026\ThirdParty\sitepress-multilingual-cms.4.9.3.zip" "$site\plugins\" -Force
|
||||||
|
```
|
||||||
|
(Or via WP admin → Plugins → Add New → Upload Plugin.)
|
||||||
|
7. Copy `tools/fresh-setup-mu-plugin.php` → `<NEW_SITE>\app\public\wp-content\mu-plugins\fresh-setup-mu-plugin.php`
|
||||||
|
8. Visit `http://localhost:10004/` once. The plugin runs at `init` priority 50, configures everything, and self-deletes after success.
|
||||||
|
9. Verify by hitting `http://localhost:10004/wp-admin` — login, then check that:
|
||||||
|
- Theme : Asterion Bricks active (parent: Bricks)
|
||||||
|
- Plugins : WPML active
|
||||||
|
- Settings → Permalinks : Post name (`/%postname%/`)
|
||||||
|
- Bricks → Settings → Builder access : Page + Post + case_study + scenario checked
|
||||||
|
|
||||||
|
Then activate the Bricks license (Bricks → License) and run the WPML setup wizard manually — those need your license keys and an interactive choice flow that's safer to do in admin.
|
||||||
|
|
||||||
|
## av-apply-bricks-styles.php
|
||||||
|
|
||||||
|
One-shot mu-plugin that writes the full Asterion Theme Style to `wp_options` (`bricks_theme_styles`) AND fixes `bricks_global_settings.postTypes` to include `page, post, case_study, scenario`.
|
||||||
|
|
||||||
|
Why a mu-plugin : Bricks stores Theme Styles as serialized PHP in the DB (not in a file), so we need code that runs in the WP context. Mu-plugin URL trigger is the simplest path; LocalWP CLI PHP can't connect to the per-site MySQL socket without extra config.
|
||||||
|
|
||||||
|
Usage :
|
||||||
|
|
||||||
|
1. Copy this file → `<LocalWP site>\app\public\wp-content\mu-plugins\av-apply-bricks-styles.php`
|
||||||
|
2. Visit `http://localhost:10004/?av_apply_styles=1` (logged in as admin)
|
||||||
|
3. A dark-themed report shows what was written.
|
||||||
|
4. Reload the Bricks builder (Ctrl+R) → Theme Style "Asterion" applied to entire website.
|
||||||
|
|
||||||
|
Idempotent — re-running overwrites with the same payload. Edit the file then re-trigger to iterate.
|
||||||
|
|
||||||
|
Sets : General (site bg), Colors (11 semantic roles), Links (gold + underline), Typography (Inter, body 16px, h1-h6 sizes from `tokens.css`), Button (primary navy, secondary gold, padding scale), Section (5rem padding), Container (max 1280px), Focus outline.
|
||||||
|
|
||||||
|
## av-dump-bricks.php
|
||||||
|
|
||||||
|
One-shot mu-plugin that exports the live `bricks_theme_styles`, `bricks_color_palette`, `bricks_global_settings` options to JSON files in `Bricks Config/` for inspection or git-versioned snapshots.
|
||||||
|
|
||||||
|
Usage : drop into mu-plugins, visit `?av_dump_bricks=1` (admin), three JSONs appear in `Bricks Config/`.
|
||||||
|
|
||||||
|
## What's missing — manual follow-up after fresh setup
|
||||||
|
|
||||||
|
- Bricks license key activation (Bricks → License)
|
||||||
|
- WPML setup wizard (WPML → Setup) : EN default + FR secondary, URL strategy = "Different languages in directories"
|
||||||
|
- Run `?av_apply_styles=1` (see above) to push the Theme Style — replaces clicking through Bricks → Theme Styles.
|
||||||
|
- Build the Header and Footer Bricks Templates (per RESTART-PLAN.md Phase 4)
|
||||||
1567
tools/_av-bricks-shared.php
Normal file
1567
tools/_av-bricks-shared.php
Normal file
File diff suppressed because it is too large
Load Diff
200
tools/av-apply-bricks-styles.php
Normal file
200
tools/av-apply-bricks-styles.php
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Apply Bricks Theme Style (one-shot)
|
||||||
|
* Description: Visit /?av_apply_styles=1 (admin only) → writes the full Asterion Theme Style + fixes Bricks postTypes. Idempotent.
|
||||||
|
*
|
||||||
|
* Sources of truth :
|
||||||
|
* - tokens.css (wp-content/themes/asterion-bricks/assets/css/tokens.css)
|
||||||
|
* - Design Handoff PDF section 2 (typography scale, line heights)
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
if (empty($_GET['av_apply_styles'])) return;
|
||||||
|
if (! current_user_can('manage_options')) wp_die('admin only');
|
||||||
|
|
||||||
|
$report = [];
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------
|
||||||
|
1. Bricks Global Settings : add post + CPT to builder access
|
||||||
|
--------------------------------------------------------------- */
|
||||||
|
$global = get_option('bricks_global_settings', []);
|
||||||
|
if (! is_array($global)) $global = [];
|
||||||
|
$global['postTypes'] = ['page', 'post', 'case_study', 'scenario'];
|
||||||
|
update_option('bricks_global_settings', $global);
|
||||||
|
$report[] = "bricks_global_settings.postTypes = " . implode(',', $global['postTypes']);
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------
|
||||||
|
2. Bricks Theme Styles : full Asterion config
|
||||||
|
--------------------------------------------------------------- */
|
||||||
|
$existing = get_option('bricks_theme_styles', []);
|
||||||
|
$cond_id = $existing['asterion']['settings']['conditions']['conditions'][0]['id'] ?? 'skuksb';
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
$color = fn($var) => ['raw' => $var];
|
||||||
|
$sp = fn($t,$r,$b,$l) => ['top'=>$t,'right'=>$r,'bottom'=>$b,'left'=>$l];
|
||||||
|
|
||||||
|
$styles = [
|
||||||
|
'asterion' => [
|
||||||
|
'label' => 'Asterion',
|
||||||
|
'settings' => [
|
||||||
|
'_custom' => true,
|
||||||
|
|
||||||
|
'conditions' => [
|
||||||
|
'conditions' => [
|
||||||
|
['id' => $cond_id, 'main' => 'any'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/* ---- GENERAL : site background ---- */
|
||||||
|
'general' => [
|
||||||
|
'siteBackground' => [
|
||||||
|
'color' => $color('var(--color-bg-default)'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/* ---- COLORS : 11 semantic roles ---- */
|
||||||
|
'colors' => [
|
||||||
|
'colorPrimary' => $color('var(--color-brand-navy)'),
|
||||||
|
'colorSecondary' => $color('var(--color-brand-gold)'),
|
||||||
|
'colorLight' => $color('var(--color-bg-default)'),
|
||||||
|
'colorDark' => $color('var(--color-bg-dark)'),
|
||||||
|
'colorMuted' => $color('var(--color-text-muted)'),
|
||||||
|
'colorBorder' => $color('var(--color-border-default)'),
|
||||||
|
'colorInfo' => $color('var(--color-info)'),
|
||||||
|
'colorSuccess' => $color('var(--color-success)'),
|
||||||
|
'colorWarning' => $color('var(--color-warning)'),
|
||||||
|
'colorDanger' => $color('var(--color-alert-red)'),
|
||||||
|
],
|
||||||
|
|
||||||
|
/* ---- LINKS : gold + underline ---- */
|
||||||
|
'links' => [
|
||||||
|
'typography' => [
|
||||||
|
'color' => $color('var(--color-brand-gold)'),
|
||||||
|
'text-decoration' => 'underline',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/* ---- TYPOGRAPHY ---- */
|
||||||
|
'typography' => [
|
||||||
|
// HTML font-size: 100% so 1rem = 16px
|
||||||
|
'typographyHtml' => '100%',
|
||||||
|
|
||||||
|
'typographyBody' => [
|
||||||
|
'font-family' => 'Inter',
|
||||||
|
'font-weight' => '400',
|
||||||
|
'font-size' => '1rem',
|
||||||
|
'line-height' => '1.6',
|
||||||
|
'letter-spacing' => '0',
|
||||||
|
'color' => $color('var(--color-text-primary)'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'typographyHeadings' => [
|
||||||
|
'font-family' => 'Inter',
|
||||||
|
'font-weight' => '700',
|
||||||
|
'letter-spacing' => '-0.01em',
|
||||||
|
'color' => $color('var(--color-text-primary)'),
|
||||||
|
],
|
||||||
|
|
||||||
|
// H1 : 36px / line 1.20
|
||||||
|
'typographyHeadingH1' => [
|
||||||
|
'font-size' => '2.25rem',
|
||||||
|
'line-height' => '1.20',
|
||||||
|
],
|
||||||
|
'h1Margin' => $sp('0', '0', '1.5rem', '0'),
|
||||||
|
|
||||||
|
// H2 : 28px / line 1.25
|
||||||
|
'typographyHeadingH2' => [
|
||||||
|
'font-size' => '1.75rem',
|
||||||
|
'line-height' => '1.25',
|
||||||
|
],
|
||||||
|
'h2Margin' => $sp('0', '0', '1.25rem', '0'),
|
||||||
|
|
||||||
|
// H3 : 22px / line 1.30
|
||||||
|
'typographyHeadingH3' => [
|
||||||
|
'font-size' => '1.375rem',
|
||||||
|
'line-height' => '1.30',
|
||||||
|
],
|
||||||
|
'h3Margin' => $sp('0', '0', '1rem', '0'),
|
||||||
|
|
||||||
|
// H4 : 18px / line 1.35
|
||||||
|
'typographyHeadingH4' => [
|
||||||
|
'font-size' => '1.125rem',
|
||||||
|
'line-height' => '1.35',
|
||||||
|
],
|
||||||
|
'h4Margin' => $sp('0', '0', '0.75rem', '0'),
|
||||||
|
|
||||||
|
// H5 : 16px / line 1.40
|
||||||
|
'typographyHeadingH5' => [
|
||||||
|
'font-size' => '1rem',
|
||||||
|
'line-height' => '1.40',
|
||||||
|
],
|
||||||
|
'h5Margin' => $sp('0', '0', '0.5rem', '0'),
|
||||||
|
|
||||||
|
// H6 : 14px / line 1.40
|
||||||
|
'typographyHeadingH6' => [
|
||||||
|
'font-size' => '0.875rem',
|
||||||
|
'line-height' => '1.40',
|
||||||
|
],
|
||||||
|
'h6Margin' => $sp('0', '0', '0.5rem', '0'),
|
||||||
|
|
||||||
|
// Focus outline (a11y, WCAG 2.1)
|
||||||
|
'focusOutline' => '2px solid var(--color-info)',
|
||||||
|
],
|
||||||
|
|
||||||
|
/* ---- BUTTON : default + primary + secondary defaults ---- */
|
||||||
|
'button' => [
|
||||||
|
'typography' => [
|
||||||
|
'font-family' => 'Inter',
|
||||||
|
'font-weight' => '600',
|
||||||
|
],
|
||||||
|
|
||||||
|
'sizeDefaultPadding' => $sp('0.75rem', '1.5rem', '0.75rem', '1.5rem'),
|
||||||
|
'sizeSmPadding' => $sp('0.5rem', '1rem', '0.5rem', '1rem'),
|
||||||
|
'sizeMdPadding' => $sp('0.75rem', '1.5rem', '0.75rem', '1.5rem'),
|
||||||
|
'sizeLgPadding' => $sp('1rem', '2rem', '1rem', '2rem'),
|
||||||
|
|
||||||
|
// Primary button = navy bg, white text, gold hover (managed via element classes)
|
||||||
|
'primaryBackground' => $color('var(--color-brand-navy)'),
|
||||||
|
'primaryTypography' => [
|
||||||
|
'color' => $color('var(--color-text-on-dark)'),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Secondary button = gold bg, navy text
|
||||||
|
'secondaryBackground' => $color('var(--color-brand-gold)'),
|
||||||
|
'secondaryTypography' => [
|
||||||
|
'color' => $color('var(--color-brand-navy)'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/* ---- SECTION : default padding (8-pt grid) ---- */
|
||||||
|
'section' => [
|
||||||
|
'padding' => $sp('5rem', '4rem', '5rem', '4rem'),
|
||||||
|
],
|
||||||
|
|
||||||
|
/* ---- CONTAINER : max-width 1280 ---- */
|
||||||
|
'container' => [
|
||||||
|
'widthMax' => '1280px',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
update_option('bricks_theme_styles', $styles);
|
||||||
|
$report[] = "bricks_theme_styles[asterion] = full payload (" . strlen(wp_json_encode($styles)) . " bytes)";
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------
|
||||||
|
3. Done
|
||||||
|
--------------------------------------------------------------- */
|
||||||
|
$report[] = "";
|
||||||
|
$report[] = "Reload the Bricks builder (Ctrl+R) — you should see :";
|
||||||
|
$report[] = " - Body in Inter 16px navy";
|
||||||
|
$report[] = " - H1-H6 in Inter 700 navy with proper sizes";
|
||||||
|
$report[] = " - Links in gold underlined";
|
||||||
|
$report[] = " - Sections with 5rem top/bottom padding";
|
||||||
|
$report[] = " - postTypes : page, post, case_study, scenario";
|
||||||
|
|
||||||
|
wp_die('<pre style="font:12px/1.5 monospace;padding:2rem;background:#0B1F3A;color:#E0C892;">'
|
||||||
|
. esc_html(implode("\n", $report))
|
||||||
|
. '</pre>', 'Asterion theme style applied');
|
||||||
|
});
|
||||||
304
tools/av-bricks-rest-bridge.php
Normal file
304
tools/av-bricks-rest-bridge.php
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Bricks REST Bridge
|
||||||
|
* Description: Exposes Bricks Builder data (templates CPT + page/post/CPT meta keys) over the WP REST API so the bricks-mcp-server (sabiertas) can read/write Bricks content. Idempotent — safe to keep installed.
|
||||||
|
*
|
||||||
|
* Why this is needed :
|
||||||
|
* - Bricks registers the bricks_template CPT WITHOUT show_in_rest, so /wp/v2/bricks_template returns 404.
|
||||||
|
* - The Bricks element JSON lives in post_meta keys (_bricks_page_content_2 etc.) that aren't exposed by default.
|
||||||
|
* - register_post_meta() with show_in_rest=true is the canonical way to expose them; meta is then read/written via /wp/v2/<post_type>/<id> with the `meta` field.
|
||||||
|
*
|
||||||
|
* Auth :
|
||||||
|
* - Read : public (REST default for show_in_rest)
|
||||||
|
* - Write : edit_posts capability (=> the WP Application Password user must have at least Editor role)
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
1) Force show_in_rest=true on bricks_template + our CPTs.
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
add_filter('register_post_type_args', function ($args, $post_type) {
|
||||||
|
|
||||||
|
$needs_rest = ['bricks_template', 'case_study', 'scenario'];
|
||||||
|
|
||||||
|
if (! in_array($post_type, $needs_rest, true)) {
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
$args['show_in_rest'] = true;
|
||||||
|
$args['rest_base'] = $post_type === 'bricks_template' ? 'bricks_template' : ($post_type . 's');
|
||||||
|
$args['rest_controller_class'] = 'WP_REST_Posts_Controller';
|
||||||
|
$args['supports'] = array_unique(array_merge(
|
||||||
|
$args['supports'] ?? [],
|
||||||
|
['title', 'editor', 'author', 'custom-fields'] // custom-fields needed so /wp/v2/<type> exposes the `meta` field
|
||||||
|
));
|
||||||
|
|
||||||
|
return $args;
|
||||||
|
}, 10, 2);
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
2) Register Bricks meta keys for REST on every relevant post type.
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
add_action('init', function () {
|
||||||
|
|
||||||
|
// Post types that may carry Bricks content
|
||||||
|
$post_types = ['page', 'post', 'bricks_template', 'case_study', 'scenario'];
|
||||||
|
|
||||||
|
// Bricks meta keys we want exposed (read+write) over REST
|
||||||
|
// Keys come from wp-content/themes/bricks/functions.php
|
||||||
|
$bricks_meta = [
|
||||||
|
'_bricks_page_content_2' => 'string', // The element JSON (the main payload)
|
||||||
|
'_bricks_page_css_2' => 'string',
|
||||||
|
'_bricks_page_header_2' => 'string',
|
||||||
|
'_bricks_page_footer_2' => 'string',
|
||||||
|
'_bricks_page_settings' => 'string',
|
||||||
|
'_bricks_template_type' => 'string',
|
||||||
|
'_bricks_template_settings' => 'string',
|
||||||
|
];
|
||||||
|
|
||||||
|
$auth_cb = function () {
|
||||||
|
return current_user_can('edit_posts');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pass-through sanitize : keep the JSON string verbatim. Default sanitize for
|
||||||
|
// type=string strips slashes / escapes which corrupts our JSON payload.
|
||||||
|
$sanitize_passthrough = function ($value) { return $value; };
|
||||||
|
|
||||||
|
foreach ($post_types as $type) {
|
||||||
|
foreach ($bricks_meta as $meta_key => $meta_type) {
|
||||||
|
register_post_meta($type, $meta_key, [
|
||||||
|
'type' => $meta_type,
|
||||||
|
'single' => true,
|
||||||
|
'show_in_rest' => true,
|
||||||
|
'auth_callback' => $auth_cb,
|
||||||
|
'sanitize_callback' => $sanitize_passthrough,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 30); // priority 30 — after both Bricks (10-25) and our CPTs registration (10)
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
3) Permit also unknown _bricks_* meta keys (e.g. _bricks_snapshot_<ts>)
|
||||||
|
so MCP snapshots work without us pre-registering each key.
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
add_filter('is_protected_meta', function ($protected, $meta_key, $meta_type) {
|
||||||
|
if ($meta_type !== 'post') return $protected;
|
||||||
|
if (strpos($meta_key, '_bricks_snapshot_') === 0) {
|
||||||
|
return false; // expose snapshot meta in REST despite the leading underscore
|
||||||
|
}
|
||||||
|
return $protected;
|
||||||
|
}, 10, 3);
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
4) AUTO-MIRROR : the bricks-mcp-server writes element JSON to
|
||||||
|
_bricks_page_content_2, but Bricks reads HEADER templates from
|
||||||
|
_bricks_page_header_2 and FOOTER templates from _bricks_page_footer_2.
|
||||||
|
Mirror the content into the correct key whenever a bricks_template
|
||||||
|
with type=header/footer is saved.
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
/* DUAL-FORMAT META READ : the MCP server writes Bricks element data as a JSON
|
||||||
|
string into post_meta. Bricks's renderer expects a PHP array. We keep the JSON
|
||||||
|
string format in DB (so REST reads it back fine for the MCP's Json.parse), and
|
||||||
|
decode on the fly when Bricks reads via get_post_meta (non-REST context).
|
||||||
|
Result : same value, two views — JSON for the MCP, array for the renderer. */
|
||||||
|
add_filter('get_post_metadata', 'av_decode_bricks_meta_for_renderer', 10, 5);
|
||||||
|
|
||||||
|
function av_decode_bricks_meta_for_renderer($value, $post_id, $meta_key, $single, $meta_type) {
|
||||||
|
// Only post-type meta concerns us
|
||||||
|
if ($meta_type !== 'post') return $value;
|
||||||
|
|
||||||
|
// Only Bricks element content keys
|
||||||
|
$content_keys = ['_bricks_page_content_2', '_bricks_page_header_2', '_bricks_page_footer_2'];
|
||||||
|
if (! in_array($meta_key, $content_keys, true)) return $value;
|
||||||
|
|
||||||
|
// In REST context, leave the raw string for the MCP server
|
||||||
|
if (defined('REST_REQUEST') && REST_REQUEST) return $value;
|
||||||
|
|
||||||
|
// Bricks renderer context : fetch the raw stored value (bypassing this filter
|
||||||
|
// via remove_filter, then re-add) and decode JSON → array
|
||||||
|
remove_filter('get_post_metadata', __FUNCTION__, 10);
|
||||||
|
$raw = get_post_meta($post_id, $meta_key, $single);
|
||||||
|
add_filter('get_post_metadata', __FUNCTION__, 10, 5);
|
||||||
|
|
||||||
|
if (! is_string($raw)) return $value;
|
||||||
|
$trim = trim($raw);
|
||||||
|
if ($trim === '' || $trim === '[]') return $value;
|
||||||
|
if ($trim[0] !== '[' && $trim[0] !== '{') return $value;
|
||||||
|
|
||||||
|
$decoded = json_decode($raw, true);
|
||||||
|
if (! is_array($decoded)) return $value;
|
||||||
|
|
||||||
|
// WP's get_metadata_raw() convention for filter return values :
|
||||||
|
// - When filter returns array AND $single=true → WP returns $check[0]
|
||||||
|
// - When filter returns array AND $single=false → WP returns $check as-is
|
||||||
|
// Both cases need a list wrapper around our decoded value so WP's
|
||||||
|
// single-mode auto-indexing yields the decoded array (not the section).
|
||||||
|
return [$decoded];
|
||||||
|
}
|
||||||
|
|
||||||
|
add_action('updated_post_meta', 'av_mirror_bricks_template_content', 10, 4);
|
||||||
|
add_action('added_post_meta', 'av_mirror_bricks_template_content', 10, 4);
|
||||||
|
|
||||||
|
function av_mirror_bricks_template_content($meta_id, $post_id, $meta_key, $meta_value) {
|
||||||
|
// Trigger 1 : content updated → mirror to header/footer if type already known
|
||||||
|
// Trigger 2 : type updated → re-mirror existing content (fixes the order-of-writes
|
||||||
|
// on REST create_template which writes content before type)
|
||||||
|
if (! in_array($meta_key, ['_bricks_page_content_2', '_bricks_template_type'], true)) return;
|
||||||
|
if (get_post_type($post_id) !== 'bricks_template') return;
|
||||||
|
|
||||||
|
// Reuse the av_decode filter logic by reading via get_post_meta (which now
|
||||||
|
// returns array) when the value is JSON, or by direct decoding here.
|
||||||
|
if ($meta_key === '_bricks_page_content_2') {
|
||||||
|
$type = get_post_meta($post_id, '_bricks_template_type', true);
|
||||||
|
$content = is_string($meta_value) ? $meta_value : '';
|
||||||
|
} else {
|
||||||
|
// type just updated → fetch raw content (bypass our filter to get string)
|
||||||
|
$type = is_string($meta_value) ? $meta_value : '';
|
||||||
|
remove_filter('get_post_metadata', 'av_decode_bricks_meta_for_renderer', 10);
|
||||||
|
$content = get_post_meta($post_id, '_bricks_page_content_2', true);
|
||||||
|
add_filter('get_post_metadata', 'av_decode_bricks_meta_for_renderer', 10, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
$mirror_key = match ($type) {
|
||||||
|
'header' => '_bricks_page_header_2',
|
||||||
|
'footer' => '_bricks_page_footer_2',
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (! $mirror_key) return;
|
||||||
|
|
||||||
|
// Bricks stores element JSON as a PHP-serialized array. Decode JSON string to array first.
|
||||||
|
$value = is_string($content) ? json_decode($content, true) : $content;
|
||||||
|
if (! is_array($value)) return;
|
||||||
|
|
||||||
|
// Bypass register_post_meta string-coercion by writing a serialized blob directly via $wpdb.
|
||||||
|
// This is what Bricks's own builder save does — the value lands as a serialized PHP array,
|
||||||
|
// which `get_post_meta($id, $key, true)` auto-unserializes back to array on read.
|
||||||
|
global $wpdb;
|
||||||
|
$serialized = maybe_serialize($value);
|
||||||
|
$existing = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT meta_id FROM {$wpdb->postmeta} WHERE post_id=%d AND meta_key=%s LIMIT 1",
|
||||||
|
$post_id, $mirror_key
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
$wpdb->update(
|
||||||
|
$wpdb->postmeta,
|
||||||
|
['meta_value' => $serialized],
|
||||||
|
['meta_id' => $existing],
|
||||||
|
['%s'], ['%d']
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$wpdb->insert(
|
||||||
|
$wpdb->postmeta,
|
||||||
|
['post_id' => $post_id, 'meta_key' => $mirror_key, 'meta_value' => $serialized],
|
||||||
|
['%d', '%s', '%s']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
wp_cache_delete($post_id, 'post_meta');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
5) BACKFILL : trigger a manual mirror via URL.
|
||||||
|
Useful one-shot for templates created before this plugin was installed.
|
||||||
|
Visit /?av_mirror_template=ID
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
/* One-shot : ensure all Bricks content meta is stored as JSON STRING in DB.
|
||||||
|
- PHP-serialized arrays (legacy from earlier hook) → re-encode to JSON
|
||||||
|
- JSON strings → leave alone
|
||||||
|
This is the canonical storage format. The av_decode filter handles
|
||||||
|
the array-view for Bricks's renderer at read time.
|
||||||
|
/?av_fix_all_bricks_content=1 */
|
||||||
|
add_action('init', function () {
|
||||||
|
if (empty($_GET['av_fix_all_bricks_content'])) return;
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$rows = $wpdb->get_results(
|
||||||
|
"SELECT meta_id, post_id, meta_key, meta_value
|
||||||
|
FROM {$wpdb->postmeta}
|
||||||
|
WHERE meta_key IN ('_bricks_page_content_2','_bricks_page_header_2','_bricks_page_footer_2')
|
||||||
|
AND LENGTH(meta_value) > 4"
|
||||||
|
);
|
||||||
|
|
||||||
|
$report = ['Normalizing Bricks content meta to JSON strings...'];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$val = $r->meta_value;
|
||||||
|
$first = $val[0] ?? '';
|
||||||
|
|
||||||
|
if ($first === '[' || $first === '{') {
|
||||||
|
// Already JSON string : skip
|
||||||
|
$report[] = " OK meta_id={$r->meta_id} post={$r->post_id} key={$r->meta_key} (JSON)";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialized PHP array → unserialize, JSON-encode
|
||||||
|
if (is_serialized($val)) {
|
||||||
|
$array = unserialize($val);
|
||||||
|
if (is_array($array)) {
|
||||||
|
$json = wp_json_encode($array);
|
||||||
|
$wpdb->update(
|
||||||
|
$wpdb->postmeta,
|
||||||
|
['meta_value' => $json],
|
||||||
|
['meta_id' => $r->meta_id],
|
||||||
|
['%s'], ['%d']
|
||||||
|
);
|
||||||
|
wp_cache_delete($r->post_id, 'post_meta');
|
||||||
|
$report[] = " CONV meta_id={$r->meta_id} post={$r->post_id} key={$r->meta_key} (" . count($array) . " elements, " . strlen($json) . " bytes)";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$report[] = " SKIP meta_id={$r->meta_id} post={$r->post_id} key={$r->meta_key} (unknown format)";
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo implode("\n", $report);
|
||||||
|
exit;
|
||||||
|
});
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
if (empty($_GET['av_mirror_template'])) return;
|
||||||
|
// localhost-only debug — no auth required (we're on http://localhost:10004)
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
$id = (int) $_GET['av_mirror_template'];
|
||||||
|
$content = get_post_meta($id, '_bricks_page_content_2', true);
|
||||||
|
$type = get_post_meta($id, '_bricks_template_type', true);
|
||||||
|
|
||||||
|
if (! is_string($content) && ! is_array($content)) wp_die('no content meta');
|
||||||
|
|
||||||
|
$value = is_string($content) ? json_decode($content, true) : $content;
|
||||||
|
if (! is_array($value)) wp_die('content is not an array');
|
||||||
|
|
||||||
|
$mirror_key = match ($type) {
|
||||||
|
'header' => '_bricks_page_header_2',
|
||||||
|
'footer' => '_bricks_page_footer_2',
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (! $mirror_key) wp_die("no mirror needed for type=$type");
|
||||||
|
|
||||||
|
// Write directly to DB as a serialized PHP array (the format Bricks's renderer expects)
|
||||||
|
global $wpdb;
|
||||||
|
$serialized = maybe_serialize($value);
|
||||||
|
$existing = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT meta_id FROM {$wpdb->postmeta} WHERE post_id=%d AND meta_key=%s LIMIT 1",
|
||||||
|
$id, $mirror_key
|
||||||
|
));
|
||||||
|
if ($existing) {
|
||||||
|
$wpdb->update($wpdb->postmeta, ['meta_value' => $serialized], ['meta_id' => $existing], ['%s'], ['%d']);
|
||||||
|
} else {
|
||||||
|
$wpdb->insert($wpdb->postmeta, ['post_id' => $id, 'meta_key' => $mirror_key, 'meta_value' => $serialized], ['%d', '%s', '%s']);
|
||||||
|
}
|
||||||
|
wp_cache_delete($id, 'post_meta');
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
$after = get_post_meta($id, $mirror_key, true);
|
||||||
|
$is_array = is_array($after);
|
||||||
|
|
||||||
|
wp_die('<pre style="font:12px/1.5 monospace;padding:2rem;background:#0B1F3A;color:#E0C892;">'
|
||||||
|
. esc_html("Mirror done : template #$id ($type)\n _bricks_page_content_2 -> $mirror_key\n " . count($value) . " elements\n stored as " . ($is_array ? 'array (correct)' : 'string (WRONG)'))
|
||||||
|
. '</pre>', 'Mirror done');
|
||||||
|
});
|
||||||
48
tools/av-cleanup-orphans.php
Normal file
48
tools/av-cleanup-orphans.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Clean WPML translation orphans (one-shot)
|
||||||
|
* /?av_cleanup_orphans=1 → removes "Secteurs" duplicate FR pages + empty icl_translations rows
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) return;
|
||||||
|
if (empty($_GET['av_cleanup_orphans'])) return;
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// 1) Find all "Secteurs" posts (any status) — they are duplicate FR pages
|
||||||
|
$dups = $wpdb->get_results(
|
||||||
|
"SELECT ID, post_title, post_name, post_status FROM {$wpdb->posts}
|
||||||
|
WHERE post_type='page' AND (post_title='Secteurs' OR post_name LIKE 'secteurs%')"
|
||||||
|
);
|
||||||
|
echo "Found " . count($dups) . " 'Secteurs' page(s):\n";
|
||||||
|
foreach ($dups as $d) {
|
||||||
|
echo " #{$d->ID} title=\"{$d->post_title}\" slug={$d->post_name} status={$d->post_status}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Force-delete all of them (will be recreated cleanly)
|
||||||
|
$deleted = 0;
|
||||||
|
foreach ($dups as $d) {
|
||||||
|
wp_delete_post($d->ID, true);
|
||||||
|
$wpdb->delete($wpdb->prefix . 'icl_translations', ['element_id' => $d->ID, 'element_type' => 'post_page']);
|
||||||
|
$deleted++;
|
||||||
|
}
|
||||||
|
echo "Deleted $deleted posts + their icl_translations rows.\n\n";
|
||||||
|
|
||||||
|
// 3) Remove empty icl_translations rows for trid=39 (Industries hub)
|
||||||
|
$empty_rows = $wpdb->query(
|
||||||
|
"DELETE FROM {$wpdb->prefix}icl_translations
|
||||||
|
WHERE trid=39 AND (element_id IS NULL OR element_id = 0)"
|
||||||
|
);
|
||||||
|
echo "Deleted $empty_rows empty FR rows for trid=39.\n\n";
|
||||||
|
|
||||||
|
// 4) Verify current state of trid=39
|
||||||
|
$rows = $wpdb->get_results("SELECT * FROM {$wpdb->prefix}icl_translations WHERE trid=39");
|
||||||
|
echo "Current icl_translations for trid=39:\n";
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
echo " translation_id={$r->translation_id} element_id={$r->element_id} lang={$r->language_code}\n";
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
});
|
||||||
99
tools/av-create-wp-menu.php
Normal file
99
tools/av-create-wp-menu.php
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Create WP Menu (one-shot, idempotent)
|
||||||
|
* Description: /?av_create_menu=NAME → creates/updates a WP nav menu and returns its term_id.
|
||||||
|
*
|
||||||
|
* Predefined menus :
|
||||||
|
* /?av_create_menu=header-main → 5 items : Solutions / Industries / Technology / Why Asterion / Insights
|
||||||
|
* /?av_create_menu=footer-nav → Solutions / Industries / Technology pages
|
||||||
|
* /?av_create_menu=footer-co → About / Customers / Partners / Insights
|
||||||
|
* /?av_create_menu=footer-legal → Privacy / Terms / Cookies / Imprint
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
if (empty($_GET['av_create_menu'])) return;
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/nav-menu.php';
|
||||||
|
|
||||||
|
$name = sanitize_text_field($_GET['av_create_menu']);
|
||||||
|
|
||||||
|
$blueprints = [
|
||||||
|
'header-main' => [
|
||||||
|
'label' => 'Header — Main',
|
||||||
|
'items' => [
|
||||||
|
['title' => 'Solutions', 'url' => '/solutions/'],
|
||||||
|
['title' => 'Industries', 'url' => '/industries/'],
|
||||||
|
['title' => 'Technology', 'url' => '/technology/'],
|
||||||
|
['title' => 'Why Asterion', 'url' => '/why-asterion/'],
|
||||||
|
['title' => 'Insights', 'url' => '/insights/'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'footer-nav' => [
|
||||||
|
'label' => 'Footer — Solutions',
|
||||||
|
'items' => [
|
||||||
|
['title' => 'Solutions', 'url' => '/solutions/'],
|
||||||
|
['title' => 'Industries', 'url' => '/industries/'],
|
||||||
|
['title' => 'Technology', 'url' => '/technology/'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'footer-co' => [
|
||||||
|
'label' => 'Footer — Company',
|
||||||
|
'items' => [
|
||||||
|
['title' => 'About', 'url' => '/about/'],
|
||||||
|
['title' => 'Customers', 'url' => '/customers/'],
|
||||||
|
['title' => 'Partners', 'url' => '/partners/'],
|
||||||
|
['title' => 'Insights', 'url' => '/insights/'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'footer-legal' => [
|
||||||
|
'label' => 'Footer — Legal',
|
||||||
|
'items' => [
|
||||||
|
['title' => 'Privacy', 'url' => '/privacy/'],
|
||||||
|
['title' => 'Terms', 'url' => '/terms/'],
|
||||||
|
['title' => 'Cookies', 'url' => '/cookies/'],
|
||||||
|
['title' => 'Imprint', 'url' => '/imprint/'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (! isset($blueprints[$name])) {
|
||||||
|
wp_die('unknown menu blueprint : ' . esc_html($name) . '. Available : ' . implode(', ', array_keys($blueprints)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$bp = $blueprints[$name];
|
||||||
|
$label = $bp['label'];
|
||||||
|
|
||||||
|
// Idempotent : reuse existing menu if name matches
|
||||||
|
$menu = wp_get_nav_menu_object($label);
|
||||||
|
if (! $menu) {
|
||||||
|
$menu_id = wp_create_nav_menu($label);
|
||||||
|
if (is_wp_error($menu_id)) wp_die('create error : ' . $menu_id->get_error_message());
|
||||||
|
$menu = wp_get_nav_menu_object($menu_id);
|
||||||
|
} else {
|
||||||
|
// Wipe existing items to repush fresh
|
||||||
|
$existing = wp_get_nav_menu_items($menu->term_id);
|
||||||
|
foreach ($existing as $item) {
|
||||||
|
wp_delete_post($item->ID, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$report = ["Menu \"$label\" (term_id={$menu->term_id})"];
|
||||||
|
|
||||||
|
foreach ($bp['items'] as $i => $item) {
|
||||||
|
$item_id = wp_update_nav_menu_item($menu->term_id, 0, [
|
||||||
|
'menu-item-title' => $item['title'],
|
||||||
|
'menu-item-url' => $item['url'],
|
||||||
|
'menu-item-status' => 'publish',
|
||||||
|
'menu-item-type' => 'custom',
|
||||||
|
'menu-item-position' => $i + 1,
|
||||||
|
]);
|
||||||
|
$report[] = " + {$item['title']} → {$item['url']} (item_id=$item_id)";
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo implode("\n", $report);
|
||||||
|
echo "\n\nUSE IN BRICKS : nav-menu element with menu = {$menu->term_id}";
|
||||||
|
exit;
|
||||||
|
});
|
||||||
31
tools/av-debug-meta.php
Normal file
31
tools/av-debug-meta.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Debug raw meta value
|
||||||
|
* Description: /?av_raw_meta=ID&key=KEY → dumps the raw type and first chars of a post meta. No auth (localhost).
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
if (empty($_GET['av_raw_meta'])) return;
|
||||||
|
|
||||||
|
$id = (int) $_GET['av_raw_meta'];
|
||||||
|
$key = sanitize_text_field($_GET['key'] ?? '_bricks_page_header_2');
|
||||||
|
|
||||||
|
if (! preg_match('/^[a-zA-Z0-9_]{2,60}$/', $key)) wp_die('bad key');
|
||||||
|
|
||||||
|
$val = get_post_meta($id, $key, true);
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo "post=$id key=$key\n";
|
||||||
|
echo "type=" . gettype($val) . "\n";
|
||||||
|
if (is_string($val)) {
|
||||||
|
echo "len=" . strlen($val) . "\n";
|
||||||
|
echo "first 200: " . substr($val, 0, 200) . "\n";
|
||||||
|
} elseif (is_array($val)) {
|
||||||
|
echo "count=" . count($val) . "\n";
|
||||||
|
echo "json: " . substr(json_encode($val), 0, 500) . "\n";
|
||||||
|
} else {
|
||||||
|
echo "value: " . var_export($val, true) . "\n";
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
});
|
||||||
34
tools/av-dump-bricks.php
Normal file
34
tools/av-dump-bricks.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Dump Bricks Options (one-shot)
|
||||||
|
* Description: Visit /?av_dump_bricks=1 (admin only) → dumps bricks_theme_styles, bricks_color_palette, bricks_global_settings to JSON files in C:\ASTERION\GIT\WP2026\Bricks Config\.
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
if (! is_admin() && empty($_GET['av_dump_bricks'])) return;
|
||||||
|
if (! current_user_can('manage_options')) return;
|
||||||
|
if (empty($_GET['av_dump_bricks'])) return;
|
||||||
|
|
||||||
|
$out_dir = 'C:\\ASTERION\\GIT\\WP2026\\Bricks Config\\';
|
||||||
|
if (! is_dir($out_dir)) {
|
||||||
|
wp_die('Output dir not found: ' . esc_html($out_dir));
|
||||||
|
}
|
||||||
|
|
||||||
|
$options = [
|
||||||
|
'bricks_theme_styles' => 'theme-styles-current.json',
|
||||||
|
'bricks_color_palette' => 'color-palette-current.json',
|
||||||
|
'bricks_global_settings' => 'global-settings-current.json',
|
||||||
|
];
|
||||||
|
|
||||||
|
$report = [];
|
||||||
|
foreach ($options as $opt_key => $file_name) {
|
||||||
|
$value = get_option($opt_key, null);
|
||||||
|
$path = $out_dir . $file_name;
|
||||||
|
$json = wp_json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
|
file_put_contents($path, $json);
|
||||||
|
$report[] = $opt_key . ' → ' . $path . ' (' . strlen($json) . ' bytes)';
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_die('<pre>' . esc_html(implode("\n", $report)) . '</pre>', 'Dump done');
|
||||||
|
});
|
||||||
81
tools/av-fix-link-types.php
Normal file
81
tools/av-fix-link-types.php
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Migrate Bricks links from type:'internal'+url to type:'external'+url
|
||||||
|
* Description: /?av_fix_link_types=1
|
||||||
|
* Bricks's set_link_attributes() in base.php expects 'internal' to come with a numeric
|
||||||
|
* 'postId' (it ignores 'url' when type='internal'), while 'external' uses the 'url'
|
||||||
|
* string verbatim. Older session-3 templates were built with the wrong shape and lost
|
||||||
|
* their href at render time. This one-shot scans _bricks_page_content_2,
|
||||||
|
* _bricks_page_header_2, _bricks_page_footer_2 across all posts (pages + bricks_template),
|
||||||
|
* rewrites every link block where type==='internal' && isset(url), and writes back.
|
||||||
|
*
|
||||||
|
* Idempotent : already-correct links are skipped. Safe to re-run.
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
if (empty($_GET['av_fix_link_types'])) return;
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
$keys = ['_bricks_page_content_2', '_bricks_page_header_2', '_bricks_page_footer_2'];
|
||||||
|
|
||||||
|
$rows = $wpdb->get_results(
|
||||||
|
"SELECT meta_id, post_id, meta_key, meta_value
|
||||||
|
FROM {$wpdb->postmeta}
|
||||||
|
WHERE meta_key IN ('" . implode("','", $keys) . "')"
|
||||||
|
);
|
||||||
|
|
||||||
|
$patched = 0;
|
||||||
|
$touched_posts = 0;
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$val = $row->meta_value;
|
||||||
|
if (! is_string($val) || $val === '') continue;
|
||||||
|
|
||||||
|
$is_json = ($val[0] === '[');
|
||||||
|
$elements = null;
|
||||||
|
|
||||||
|
if ($is_json) {
|
||||||
|
$elements = json_decode($val, true);
|
||||||
|
} else {
|
||||||
|
// Mirror keys (_bricks_page_header_2, _bricks_page_footer_2) are stored as
|
||||||
|
// PHP-serialized arrays by av-bricks-rest-bridge → maybe_unserialize.
|
||||||
|
$maybe = @unserialize($val);
|
||||||
|
if (is_array($maybe)) $elements = $maybe;
|
||||||
|
}
|
||||||
|
if (! is_array($elements)) continue;
|
||||||
|
|
||||||
|
$count = 0;
|
||||||
|
av_walk_fix_links($elements, $count);
|
||||||
|
if ($count === 0) continue;
|
||||||
|
|
||||||
|
// Write back in same format as it was stored
|
||||||
|
$new = $is_json ? wp_json_encode($elements) : maybe_serialize($elements);
|
||||||
|
$wpdb->update($wpdb->postmeta, ['meta_value' => $new], ['meta_id' => $row->meta_id], ['%s'], ['%d']);
|
||||||
|
wp_cache_delete($row->post_id, 'post_meta');
|
||||||
|
$patched += $count;
|
||||||
|
$touched_posts++;
|
||||||
|
$fmt = $is_json ? 'json' : 'serialized';
|
||||||
|
echo "post #{$row->post_id} ({$row->meta_key}, $fmt) : $count link(s) patched\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nTotal : $patched links across $touched_posts post(s) patched (type internal\u{2192}external).\n";
|
||||||
|
exit;
|
||||||
|
});
|
||||||
|
|
||||||
|
function av_walk_fix_links(&$elements, &$count) {
|
||||||
|
$walk = function (&$node) use (&$walk, &$count) {
|
||||||
|
if (! is_array($node)) return;
|
||||||
|
// Link block shape : has type and url, type wrongly set to 'internal' with a non-numeric url
|
||||||
|
if (isset($node['type']) && $node['type'] === 'internal' && isset($node['url']) && is_string($node['url']) && $node['url'] !== '') {
|
||||||
|
$node['type'] = 'external';
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
foreach ($node as &$value) {
|
||||||
|
if (is_array($value)) $walk($value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
foreach ($elements as &$el) $walk($el);
|
||||||
|
}
|
||||||
68
tools/av-fix-template-meta.php
Normal file
68
tools/av-fix-template-meta.php
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Fix Bricks Template meta (one-shot)
|
||||||
|
* Description: Visit /?av_fix_template_meta=ID&type=header (admin only) to set _bricks_template_type and global conditions for a Bricks template. Also versionable for re-runs.
|
||||||
|
*
|
||||||
|
* Usage examples :
|
||||||
|
* /?av_fix_template_meta=25&type=header → mark template 25 as Header type, condition = entire website
|
||||||
|
* /?av_fix_template_meta=26&type=footer → mark template 26 as Footer type, condition = entire website
|
||||||
|
* /?av_fix_template_meta=42&type=section → mark as section template (no auto-condition)
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
|
||||||
|
if (empty($_GET['av_fix_template_meta'])) return;
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
$template_id = (int) $_GET['av_fix_template_meta'];
|
||||||
|
$type = sanitize_text_field($_GET['type'] ?? 'content');
|
||||||
|
|
||||||
|
$valid_types = ['header', 'footer', 'section', 'content', 'popup', 'archive', 'single', 'search', 'error'];
|
||||||
|
if (! in_array($type, $valid_types, true)) {
|
||||||
|
wp_die("Invalid type: $type. Must be one of: " . implode(', ', $valid_types));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (get_post_type($template_id) !== 'bricks_template') {
|
||||||
|
wp_die("Post $template_id is not a bricks_template");
|
||||||
|
}
|
||||||
|
|
||||||
|
$report = [];
|
||||||
|
|
||||||
|
// 1. Set template type
|
||||||
|
update_post_meta($template_id, '_bricks_template_type', $type);
|
||||||
|
$report[] = "_bricks_template_type = $type";
|
||||||
|
|
||||||
|
// 2. Set conditions = entire website (for header/footer/archive/single/search/error templates)
|
||||||
|
// Bricks expects _bricks_template_settings with a 'templateConditions' array
|
||||||
|
// Each condition shape : ['main' => 'any'] applies on the entire site
|
||||||
|
if (in_array($type, ['header', 'footer', 'archive', 'single', 'search', 'error'], true)) {
|
||||||
|
$settings = get_post_meta($template_id, '_bricks_template_settings', true);
|
||||||
|
if (! is_array($settings)) $settings = [];
|
||||||
|
|
||||||
|
$settings['templateConditions'] = [
|
||||||
|
[
|
||||||
|
'id' => substr(md5((string) $template_id . microtime()), 0, 6),
|
||||||
|
'main' => 'any',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Header templates : sticky on top by default unless &sticky=0
|
||||||
|
if ($type === 'header' && ($_GET['sticky'] ?? '1') !== '0') {
|
||||||
|
$settings['headerSticky'] = true;
|
||||||
|
// headerStickyOnScroll switches from position:fixed (default brx-sticky)
|
||||||
|
// to position:sticky → header flows normally on load (no content overlap),
|
||||||
|
// sticks only once user starts scrolling.
|
||||||
|
$settings['headerStickyOnScroll'] = true;
|
||||||
|
$report[] = "headerSticky = true + headerStickyOnScroll = true (.brx-sticky.on-scroll)";
|
||||||
|
}
|
||||||
|
|
||||||
|
update_post_meta($template_id, '_bricks_template_settings', $settings);
|
||||||
|
$report[] = "_bricks_template_settings.templateConditions = [{main:any}]";
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_die('<pre style="font:12px/1.5 monospace;padding:2rem;background:#0B1F3A;color:#E0C892;">'
|
||||||
|
. esc_html("Template #$template_id ($type) updated:\n " . implode("\n ", $report))
|
||||||
|
. '</pre>', 'Template meta fixed');
|
||||||
|
});
|
||||||
149
tools/av-generate-industries.php
Normal file
149
tools/av-generate-industries.php
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Generate industry pages (one-shot)
|
||||||
|
* Description: /?av_generate_industries=1 reads content-source/industries-data.php
|
||||||
|
* and creates/updates : hub /industries/ + 4 child pages (police, special-forces,
|
||||||
|
* military, firefighters). Idempotent.
|
||||||
|
*
|
||||||
|
* Heavy lifting (builders + FR pipeline) lives in _av-bricks-shared.php.
|
||||||
|
* This file only owns : URL trigger + hub builder + FR strings for Industries.
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/_av-bricks-shared.php';
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
Register Industries-specific FR strings into the shared dict.
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
add_filter('av_fr_translation_dict', function ($dict) {
|
||||||
|
return array_merge($dict, [
|
||||||
|
// Hub
|
||||||
|
'Industries' => 'Secteurs',
|
||||||
|
"Built for those who carry the weight."
|
||||||
|
=> "Conçu pour celles et ceux qui en portent le poids.",
|
||||||
|
"Police, special forces, military, and emergency services share a hard truth: their craft is rehearsed in the worst conditions, and rarely enough. PROSERVE\u{2122} puts contemporary training within everyone's reach \u{2014} every shift, every budget, every operating context."
|
||||||
|
=> "Police, forces spéciales, armée et services d'urgence partagent une vérité dure : leur métier se répète dans les pires conditions, et trop rarement. PROSERVE\u{2122} met l'entraînement contemporain à la portée de tous \u{2014} chaque service, chaque budget, chaque contexte opérationnel.",
|
||||||
|
"Four operating contexts. One platform."
|
||||||
|
=> "Quatre contextes opérationnels. Une seule plateforme.",
|
||||||
|
|
||||||
|
// Card eyebrows + tile labels
|
||||||
|
'National & Municipal' => 'Nationale & Municipale',
|
||||||
|
'Police' => 'Police',
|
||||||
|
"Police training, when the schedule won't bend."
|
||||||
|
=> "L'entraînement police, quand l'agenda ne fléchit pas.",
|
||||||
|
'Tactical units' => 'Unités tactiques',
|
||||||
|
'Special Forces' => 'Forces spéciales',
|
||||||
|
"Special forces train differently. Their simulation should, too."
|
||||||
|
=> "Les forces spéciales s'entraînent autrement. Leur simulation devrait suivre.",
|
||||||
|
'Armed forces' => 'Forces armées',
|
||||||
|
'Military' => 'Armée',
|
||||||
|
"Military VR training. Sovereign. Modular. Field-deployable."
|
||||||
|
=> "Entraînement VR pour l'armée. Souverain. Modulaire. Déployable sur le terrain.",
|
||||||
|
'Emergency services' => "Services d'urgence",
|
||||||
|
'Firefighters' => 'Sapeurs-pompiers',
|
||||||
|
"Train the worst day. Safely. Repeatedly."
|
||||||
|
=> "Entraînez-vous au pire jour. En sécurité. Indéfiniment.",
|
||||||
|
|
||||||
|
// Card body concatenations from hub builder
|
||||||
|
"National & Municipal \u{2014} Police training, when the schedule won't bend."
|
||||||
|
=> "Nationale & Municipale \u{2014} L'entraînement police, quand l'agenda ne fléchit pas.",
|
||||||
|
"Tactical units \u{2014} Special forces train differently. Their simulation should, too."
|
||||||
|
=> "Unités tactiques \u{2014} Les forces spéciales s'entraînent autrement. Leur simulation devrait suivre.",
|
||||||
|
"Armed forces \u{2014} Military VR training. Sovereign. Modular. Field-deployable."
|
||||||
|
=> "Forces armées \u{2014} Entraînement VR pour l'armée. Souverain. Modulaire. Déployable sur le terrain.",
|
||||||
|
"Emergency services \u{2014} Train the worst day. Safely. Repeatedly."
|
||||||
|
=> "Services d'urgence \u{2014} Entraînez-vous au pire jour. En sécurité. Indéfiniment.",
|
||||||
|
|
||||||
|
// Hero / detail-page CTA labels specific to Industries
|
||||||
|
'Request a Demo for police use' => 'Demander une démo pour la police',
|
||||||
|
'Request a Demo for fire & emergency' => "Demander une démo pour pompiers & urgences",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
URL trigger — fires on template_redirect (NOT init) so Bricks's
|
||||||
|
element controls are loaded (happens on 'wp' hook). Without loaded
|
||||||
|
controls, Bricks's wpml_page_builder_register_strings handler skips
|
||||||
|
every element silently and NO strings reach icl_strings → ATE empty.
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
add_action('template_redirect', function () {
|
||||||
|
if (empty($_GET['av_generate_industries'])) return;
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
$data_file = 'C:\\ASTERION\\GIT\\WP2026\\content-source\\industries-data.php';
|
||||||
|
if (! file_exists($data_file)) wp_die("data file not found: $data_file");
|
||||||
|
|
||||||
|
$industries = include $data_file;
|
||||||
|
if (! is_array($industries)) wp_die('industries data not array');
|
||||||
|
|
||||||
|
$parent_id = av_ensure_hub_page('industries', 'Industries');
|
||||||
|
$report = ["Rebuilding /industries/ hub + child detail pages..."];
|
||||||
|
|
||||||
|
// Hub
|
||||||
|
$hub_elements = av_build_industries_hub_elements($industries);
|
||||||
|
$hub_write = av_write_bricks_page_elements($parent_id, $hub_elements);
|
||||||
|
$report[] = " HUB → page #$parent_id (rebuilt) — " . count($hub_elements)
|
||||||
|
. " elements (json={$hub_write['json_len']}, stored={$hub_write['stored_len']})";
|
||||||
|
|
||||||
|
// Detail pages
|
||||||
|
foreach ($industries as $slug => $d) {
|
||||||
|
$page = av_upsert_detail_page($slug, 'industries', $d);
|
||||||
|
$report[] = " $slug → page #{$page['id']} ({$page['action']}) — {$page['element_count']} elements (json={$page['json_len']}, stored={$page['stored_len']})";
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo implode("\n", $report);
|
||||||
|
exit;
|
||||||
|
});
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
Industries hub builder : hero + 4-card grid + closer CTA
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
function av_build_industries_hub_elements($industries) {
|
||||||
|
$els = [];
|
||||||
|
|
||||||
|
// HERO
|
||||||
|
$els = array_merge($els, av_build_hero('hub', [
|
||||||
|
'eyebrow' => 'Industries',
|
||||||
|
'tagline' => "Built for those who carry the weight.",
|
||||||
|
'hero_lead' => "Police, special forces, military, and emergency services share a hard truth: their craft is rehearsed in the worst conditions, and rarely enough. PROSERVE\u{2122} puts contemporary training within everyone's reach \u{2014} every shift, every budget, every operating context.",
|
||||||
|
'ctas' => [
|
||||||
|
['label' => 'Book a demo', 'href' => '/request-demo/', 'variant' => 'primary'],
|
||||||
|
['label' => 'Request a T&E Kit', 'href' => '/request-te-kit/', 'variant' => 'secondary'],
|
||||||
|
],
|
||||||
|
]));
|
||||||
|
|
||||||
|
// 4-card grid — one card per industry
|
||||||
|
$cards_section = [
|
||||||
|
'eyebrow' => '',
|
||||||
|
'title' => 'Four operating contexts. One platform.',
|
||||||
|
'items' => [],
|
||||||
|
];
|
||||||
|
foreach ($industries as $slug => $d) {
|
||||||
|
$cards_section['items'][] = [
|
||||||
|
'title' => $d['title'],
|
||||||
|
'body' => ($d['eyebrow'] ?? '') . " \u{2014} " . ($d['tagline'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$els = array_merge($els, av_build_cards_section('hubgrid', 0, $cards_section, 'var(--color-bg-subtle)'));
|
||||||
|
|
||||||
|
// Append "Learn more →" link in each card (post-build patch)
|
||||||
|
$i = 0;
|
||||||
|
foreach ($industries as $slug => $d) {
|
||||||
|
$kid = "khubgridc0x{$i}";
|
||||||
|
$lid = "lnhubgridc0x{$i}";
|
||||||
|
$els[] = av_text_link($lid, $kid, "Learn more \u{2192}", "/industries/$slug/");
|
||||||
|
foreach ($els as &$el) {
|
||||||
|
if (($el['id'] ?? '') === $kid) {
|
||||||
|
$el['children'][] = $lid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($el);
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLOSER CTA
|
||||||
|
$els = array_merge($els, av_build_closer_cta('hub'));
|
||||||
|
|
||||||
|
return $els;
|
||||||
|
}
|
||||||
378
tools/av-generate-solutions.php
Normal file
378
tools/av-generate-solutions.php
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Generate solution pages (one-shot)
|
||||||
|
* Description: /?av_generate_solutions=1 reads content-source/solutions-data.php
|
||||||
|
* and creates/updates : hub /solutions/ + 4 child pages (my-proserve,
|
||||||
|
* proserve-flex, proserve-academy, customization). Idempotent.
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/_av-bricks-shared.php';
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
Register Solutions-specific FR strings into the shared dict.
|
||||||
|
Brand names (MY PROSERVE, PROSERVE FLEX, PROSERVE ACADEMY, PROSERVE+,
|
||||||
|
PROSERVE™, ASTERION VR™) are kept verbatim.
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
add_filter('av_fr_translation_dict', function ($dict) {
|
||||||
|
return array_merge($dict, av_solutions_fr_dict());
|
||||||
|
});
|
||||||
|
|
||||||
|
function av_solutions_fr_dict() {
|
||||||
|
return [
|
||||||
|
/* ============================ HUB ============================ */
|
||||||
|
'Solutions' => 'Solutions',
|
||||||
|
'PROSERVE platform' => 'Plateforme PROSERVE',
|
||||||
|
"Four tiers. One platform. Train where you are." => "Quatre paliers. Une plateforme. Entraînez-vous là où vous êtes.",
|
||||||
|
"From the solo officer in a 9 m\u{B2} corner to a 16-trainee free-roam academy : PROSERVE\u{2122} scales without changing software. Same physics. Same AI. Same instructor logic. Pick the configuration that fits your context — upgrade or extend later."
|
||||||
|
=> "De l'agent seul dans 9 m\u{B2} à une académie en libre déambulation pour 16 stagiaires : PROSERVE\u{2122} évolue sans changer de logiciel. Mêmes physiques. Même IA. Même logique instructeur. Choisissez la configuration adaptée à votre contexte — étendez ou faites évoluer plus tard.",
|
||||||
|
"Pick the right configuration for your unit." => "Choisissez la configuration adaptée à votre unité.",
|
||||||
|
|
||||||
|
/* -------- Card titles + bodies (hub grid) -------- */
|
||||||
|
'MY PROSERVE' => 'MY PROSERVE',
|
||||||
|
'PROSERVE FLEX' => 'PROSERVE FLEX',
|
||||||
|
'PROSERVE ACADEMY' => 'PROSERVE ACADEMY',
|
||||||
|
'PROSERVE+' => 'PROSERVE+',
|
||||||
|
|
||||||
|
'Solo training' => 'Entraînement solo',
|
||||||
|
'Team training · 1 to 5' => 'Entraînement en équipe · 1 à 5',
|
||||||
|
'Unit training · 1 to 16' => 'Entraînement d\'unité · 1 à 16',
|
||||||
|
'Customization & options' => 'Personnalisation & options',
|
||||||
|
|
||||||
|
'Your first range, in a mini-case.'
|
||||||
|
=> 'Votre premier stand, dans un mini-coffre.',
|
||||||
|
'Your team, in the same scenario, on any flat surface.'
|
||||||
|
=> 'Votre équipe, dans le même scénario, sur n\'importe quelle surface plane.',
|
||||||
|
'The full training centre, in your facility.'
|
||||||
|
=> 'Le centre de formation complet, dans vos locaux.',
|
||||||
|
"When the catalog doesn't fit your mission."
|
||||||
|
=> "Quand le catalogue ne correspond pas à votre mission.",
|
||||||
|
|
||||||
|
// Card body concatenations from hub builder (eyebrow — tagline)
|
||||||
|
"Solo training \u{2014} Your first range, in a mini-case."
|
||||||
|
=> "Entraînement solo \u{2014} Votre premier stand, dans un mini-coffre.",
|
||||||
|
"Team training \u{B7} 1 to 5 \u{2014} Your team, in the same scenario, on any flat surface."
|
||||||
|
=> "Entraînement en équipe \u{B7} 1 à 5 \u{2014} Votre équipe, dans le même scénario, sur n'importe quelle surface plane.",
|
||||||
|
"Unit training \u{B7} 1 to 16 \u{2014} The full training centre, in your facility."
|
||||||
|
=> "Entraînement d'unité \u{B7} 1 à 16 \u{2014} Le centre de formation complet, dans vos locaux.",
|
||||||
|
"Customization & options \u{2014} When the catalog doesn't fit your mission."
|
||||||
|
=> "Personnalisation & options \u{2014} Quand le catalogue ne correspond pas à votre mission.",
|
||||||
|
|
||||||
|
/* ====================== MY PROSERVE ====================== */
|
||||||
|
"French regulation requires three live-fire sessions and 90 cartridges per officer per year. Reality: 10 to 15. MY PROSERVE closes that gap, every shift, in 9 square meters."
|
||||||
|
=> "La réglementation française exige trois séances de tir réel et 90 cartouches par agent et par an. La réalité : 10 à 15. MY PROSERVE comble cet écart, à chaque service, en 9 mètres carrés.",
|
||||||
|
|
||||||
|
'Why solo training matters now' => "Pourquoi l'entraînement solo compte maintenant",
|
||||||
|
'The training deficit is operational risk.'
|
||||||
|
=> "Le déficit d'entraînement est un risque opérationnel.",
|
||||||
|
"Two facts shape this product. First, regulation sets a floor that almost no force consistently meets. Second, every officer issued a firearm — including municipal police, gendarmerie reservists, and protective security agents — needs more repetition than range schedules can deliver. MY PROSERVE answers both: it puts a personal range in 3 to 9 m\u{B2} of free floor space, available 24/7, with the same physics as the live one."
|
||||||
|
=> "Deux faits structurent ce produit. D'abord, la réglementation fixe un plancher que presque aucune force ne respecte vraiment. Ensuite, chaque agent porteur d'arme — y compris les polices municipales, les réservistes de la gendarmerie et les agents de sécurité — a besoin de plus de répétition que le planning des stands ne peut offrir. MY PROSERVE répond aux deux : un stand personnel dans 3 à 9 m\u{B2} de surface libre, disponible 24h/24, avec la même physique que le tir réel.",
|
||||||
|
|
||||||
|
'Key benefits' => 'Bénéfices clés',
|
||||||
|
'What you get with one mini-case.' => 'Ce que vous obtenez avec un seul mini-coffre.',
|
||||||
|
|
||||||
|
'Ultra-portable' => 'Ultra-portable',
|
||||||
|
'Fits in a single mini-case sized for routine transport. Weighs less than 20 kg. Sets up in under five minutes.'
|
||||||
|
=> "Tient dans un seul mini-coffre dimensionné pour le transport quotidien. Pèse moins de 20 kg. Installation en moins de cinq minutes.",
|
||||||
|
'Available around the clock' => 'Disponible 24h/24',
|
||||||
|
'No range scheduling, no firearms instructor required for routine drills. Train at the start of a shift, between two patrols, or before deployment.'
|
||||||
|
=> "Pas de réservation de stand, pas d'instructeur de tir requis pour les exercices courants. Entraînez-vous au début d'un service, entre deux patrouilles ou avant une mission.",
|
||||||
|
'Multi-site' => 'Multi-sites',
|
||||||
|
'Move it from station to station. Equip a region with a small fleet — your officers train where they are stationed.'
|
||||||
|
=> "Déplacez-le d'un poste à l'autre. Équipez une région avec une petite flotte — vos agents s'entraînent là où ils sont affectés.",
|
||||||
|
'Economical' => 'Économique',
|
||||||
|
"Ammunition costs collapse. A CO2 refill costs about \u{20AC}0.25 and powers 30 to 40 shots. A gas refill costs about \u{20AC}8 and powers 600 to 800 shots. Break-even versus live ammunition arrives in months, not years."
|
||||||
|
=> "Les coûts de munitions s'effondrent. Une recharge CO2 coûte environ 0,25 \u{20AC} et permet 30 à 40 tirs. Une recharge gaz coûte environ 8 \u{20AC} et permet 600 à 800 tirs. Le retour sur investissement face aux munitions réelles se compte en mois, pas en années.",
|
||||||
|
'Real-recoil firearm' => 'Arme à recul réel',
|
||||||
|
'A gas-blowback or CO2 replica fitted with our Universal Weapon Digitizer. Recoil is genuine. Shot detection is dual (sound + recoil) to avoid false positives.'
|
||||||
|
=> "Une réplique gas-blowback ou CO2 équipée de notre Universal Weapon Digitizer. Le recul est authentique. La détection du tir est double (son + recul) pour éviter les faux positifs.",
|
||||||
|
'Personal AAR' => 'AAR personnel',
|
||||||
|
'Every officer has a personal account and dashboard. Track precision, reaction time, missed shots, target identification accuracy over time. Shareable with the supervising instructor.'
|
||||||
|
=> "Chaque agent dispose d'un compte personnel et d'un tableau de bord. Suivez précision, temps de réaction, tirs manqués et identification des cibles dans le temps. Partageable avec l'instructeur référent.",
|
||||||
|
|
||||||
|
"What's in the mini-case" => 'Le contenu du mini-coffre',
|
||||||
|
'Inside the case.' => 'Dans le coffre.',
|
||||||
|
'VR rendering laptop (Intel Core i9-13900HX · NVIDIA RTX 4080 · 16 GB DDR5 · Wi-Fi 6E)'
|
||||||
|
=> 'Portable de rendu VR (Intel Core i9-13900HX · NVIDIA RTX 4080 · 16 Go DDR5 · Wi-Fi 6E)',
|
||||||
|
"5K wireless headset (per-eye 2448\u{D7}2448 \u{B7} 120\u{B0} FOV \u{B7} Snapdragon XR2)"
|
||||||
|
=> "Casque sans fil 5K (2448\u{D7}2448 par \u{153}il \u{B7} 120\u{B0} FOV \u{B7} Snapdragon XR2)",
|
||||||
|
'Spare battery + charging dock' => 'Batterie de rechange + station de charge',
|
||||||
|
'Wi-Fi 6E router (tri-band, AXE11000)' => 'Routeur Wi-Fi 6E (tri-bande, AXE11000)',
|
||||||
|
'One real-recoil replica (handgun OR rifle, your choice from the catalog)'
|
||||||
|
=> 'Une réplique à recul réel (pistolet OU fusil, au choix dans le catalogue)',
|
||||||
|
'Universal Weapon Digitizer (UWeD)' => 'Universal Weapon Digitizer (UWeD)',
|
||||||
|
'Gas / CO2 starter pack' => 'Pack de démarrage gaz / CO2',
|
||||||
|
"Custom-foam waterproof transport case (~20 kg fully loaded)"
|
||||||
|
=> "Coffre de transport étanche avec mousse sur mesure (~20 kg en charge)",
|
||||||
|
|
||||||
|
'Suitable for' => 'Adapté à',
|
||||||
|
'Designed for the officer, not the academy.' => "Conçu pour l'agent, pas l'académie.",
|
||||||
|
'Police municipale and protective security agents — comply with continuous training obligations without depending on a national range network.'
|
||||||
|
=> "Police municipale et agents de sécurité — respectez les obligations de formation continue sans dépendre d'un réseau national de stands.",
|
||||||
|
'Reservists and gendarmes — train at home base between mobilizations.'
|
||||||
|
=> "Réservistes et gendarmes — entraînez-vous à votre base entre deux mobilisations.",
|
||||||
|
'Firearm-licensed officers in transport, energy, or critical infrastructure security.'
|
||||||
|
=> "Agents porteurs d'arme dans la sécurité des transports, de l'énergie ou des infrastructures critiques.",
|
||||||
|
'Trainee officers preparing for live-fire qualifications.'
|
||||||
|
=> 'Élèves préparant leurs qualifications de tir réel.',
|
||||||
|
|
||||||
|
'Volume pricing' => 'Tarif volume',
|
||||||
|
'Volume pricing for regional rollouts.' => 'Tarif volume pour les déploiements régionaux.',
|
||||||
|
"Equip a department, a region, or a national programme. Indicative tiers: from \u{20AC}9,000 per unit at single-piece purchase, dropping to \u{20AC}6,000 per unit at 50+ units. Quote on request."
|
||||||
|
=> "Équipez un département, une région ou un programme national. Tarifs indicatifs : à partir de 9 000 \u{20AC} l'unité à l'achat unitaire, descendant à 6 000 \u{20AC} l'unité au-delà de 50 unités. Devis sur demande.",
|
||||||
|
|
||||||
|
'When to size up.' => "Quand passer à la taille supérieure.",
|
||||||
|
'MY PROSERVE is the right choice for solo, repetitive marksmanship and decision drills. If you need cooperative team training (multiple trainees in the same scenario, real-time tactical coordination), step up to PROSERVE FLEX. If you run a full curriculum for 8 to 16 trainees at once, PROSERVE ACADEMY is the right fit. The software is the same — only the format scales.'
|
||||||
|
=> "MY PROSERVE est le bon choix pour le tir et les drills décisionnels solo et répétés. Si vous avez besoin d'entraînement coopératif (plusieurs stagiaires dans le même scénario, coordination tactique en temps réel), montez à PROSERVE FLEX. Si vous opérez un cursus complet pour 8 à 16 stagiaires en simultané, PROSERVE ACADEMY est adapté. Le logiciel est le même — seul le format évolue.",
|
||||||
|
'Compare with PROSERVE FLEX' => 'Comparer avec PROSERVE FLEX',
|
||||||
|
'Compare with PROSERVE ACADEMY' => 'Comparer avec PROSERVE ACADEMY',
|
||||||
|
|
||||||
|
/* ====================== PROSERVE FLEX ====================== */
|
||||||
|
'The portable team kit. One case per operator, one instructor PC, full multi-user cooperation. Deploy on a station floor, in a school gym, in a tent. The whole unit trains together — and reviews together.'
|
||||||
|
=> "Le kit équipe portable. Un coffre par opérateur, un PC instructeur, coopération multi-utilisateurs complète. Déployez-le sur le sol d'un poste, dans un gymnase, sous une tente. Toute l'unité s'entraîne ensemble — et débriefe ensemble.",
|
||||||
|
|
||||||
|
'Why team training in VR' => "Pourquoi l'entraînement en équipe en VR",
|
||||||
|
'Tactical performance is collective. Train it that way.'
|
||||||
|
=> "La performance tactique est collective. Entraînez-la comme telle.",
|
||||||
|
'Solo drills build muscle memory. They do not build cohesion. PROSERVE FLEX puts up to five operators in the same scenario, seeing each other as 3D avatars in roles defined by the mission — point, breacher, security, K9 handler, sniper. Communication, coordination, command-of-fire decisions: everything an instructor can rate, the system records.'
|
||||||
|
=> "Les exercices solo construisent la mémoire musculaire. Pas la cohésion. PROSERVE FLEX réunit jusqu'à cinq opérateurs dans le même scénario, se voyant comme des avatars 3D dans des rôles définis par la mission — pointe, breacher, sécurité, maître-chien, tireur d'élite. Communication, coordination, décisions de tir : tout ce qu'un instructeur peut noter, le système l'enregistre.",
|
||||||
|
|
||||||
|
'Why FLEX wins on mobility.' => 'Pourquoi FLEX gagne sur la mobilité.',
|
||||||
|
'Versatile' => 'Polyvalent',
|
||||||
|
'Solo, two-by-two, or in teams of up to five. Same hardware, different scenario. The kit adapts to the schedule, not the other way around.'
|
||||||
|
=> "Solo, par binôme ou en équipe jusqu'à cinq. Même matériel, scénarios différents. Le kit s'adapte au planning, pas l'inverse.",
|
||||||
|
'Mobile' => 'Mobile',
|
||||||
|
"One case per operator — fully wireless. No fixed install, no cable management, no specialized facility. Set up on any flat indoor surface from 9 m\u{B2} (solo) to 100 m\u{B2} (full team)."
|
||||||
|
=> "Un coffre par opérateur — entièrement sans fil. Pas d'installation fixe, pas de câbles, pas de local spécifique. Déployez-vous sur toute surface plane intérieure, de 9 m\u{B2} (solo) à 100 m\u{B2} (équipe complète).",
|
||||||
|
'Fast' => 'Rapide',
|
||||||
|
'Solo configuration ready in under five minutes. Full team configuration ready in under twenty. The instructor PC handles calibration on the fly.'
|
||||||
|
=> "Configuration solo prête en moins de cinq minutes. Configuration équipe complète prête en moins de vingt. Le PC instructeur gère la calibration à la volée.",
|
||||||
|
'Immersive' => 'Immersif',
|
||||||
|
'Real-recoil replicas, ballistic physics powered by Unreal Engine 5, autonomous AI civilians and adversaries with vision, hearing, and personality (cautious, courageous, aggressive, panicked). No two scenarios play out the same way.'
|
||||||
|
=> "Répliques à recul réel, physique balistique par Unreal Engine 5, civils et adversaires IA autonomes dotés de vision, ouïe et personnalité (prudent, courageux, agressif, paniqué). Aucun scénario ne se joue deux fois de la même façon.",
|
||||||
|
'Instructor-led' => 'Piloté par l\'instructeur',
|
||||||
|
"A dedicated instructor PC follows the action from any angle, manipulates the scenario in real time, displays each trainee's field of view and shot trace. Statistics roll up automatically."
|
||||||
|
=> "Un PC instructeur dédié suit l'action sous tous les angles, manipule le scénario en temps réel, affiche le champ de vision et la trace de tir de chaque stagiaire. Les statistiques remontent automatiquement.",
|
||||||
|
'AAR-ready' => 'AAR-ready',
|
||||||
|
"Replay every second of the session in top-down tactical view, free camera, or any trainee's first-person view. Pause, slow down, compare. End every drill with the data, not just the impression."
|
||||||
|
=> "Rejouez chaque seconde de la session en vue tactique zénithale, caméra libre ou première personne d'un stagiaire. Pause, ralenti, comparaison. Terminez chaque drill avec les données, pas seulement l'impression.",
|
||||||
|
|
||||||
|
'Inside the kit' => 'Le contenu du kit',
|
||||||
|
"What's in the FLEX deployment." => 'Le contenu du déploiement FLEX.',
|
||||||
|
'One trainee case per operator (laptop, headset, batteries, weapon, UWeD)'
|
||||||
|
=> 'Un coffre stagiaire par opérateur (laptop, casque, batteries, arme, UWeD)',
|
||||||
|
'One instructor PC + display' => 'Un PC instructeur + écran',
|
||||||
|
'Charging station + spare batteries' => 'Station de charge + batteries de rechange',
|
||||||
|
'Optional: haptic vest, wrist tracker, holsters, additional weapon profiles (PROSERVE+)'
|
||||||
|
=> 'Optionnel : gilet haptique, tracker de poignet, étuis, profils d\'armes additionnels (PROSERVE+)',
|
||||||
|
|
||||||
|
'Designed for tactical teams.' => 'Conçu pour les équipes tactiques.',
|
||||||
|
'Special weapons and tactics units (RAID, BRI, GIGN equivalents)'
|
||||||
|
=> 'Unités d\'intervention spéciales (RAID, BRI, équivalents GIGN)',
|
||||||
|
'Mobile gendarmerie squadrons' => 'Escadrons de gendarmerie mobile',
|
||||||
|
'National police rapid-response teams' => 'Équipes d\'intervention rapide de la police nationale',
|
||||||
|
'K9 handler training pairs' => 'Binômes maître-chien',
|
||||||
|
'Embarked teams (counter-piracy, executive protection)'
|
||||||
|
=> 'Équipes embarquées (contre-piraterie, protection rapprochée)',
|
||||||
|
|
||||||
|
'Sample scenarios' => 'Scénarios types',
|
||||||
|
'Sample team scenarios.' => 'Scénarios types en équipe.',
|
||||||
|
'Tactical progression in a residential apartment' => 'Progression tactique dans un appartement',
|
||||||
|
'Hostage rescue on an office floor' => 'Libération d\'otages dans un étage de bureaux',
|
||||||
|
'Vehicle stop with armed driver' => 'Contrôle d\'un véhicule avec conducteur armé',
|
||||||
|
'Active shooter in a supermarket' => 'Tireur actif dans un supermarché',
|
||||||
|
'Crowd containment with isolated aggressor' => 'Maintien de foule avec agresseur isolé',
|
||||||
|
'Tactical entry on a parking deck' => 'Entrée tactique sur un parking en élévation',
|
||||||
|
|
||||||
|
/* ====================== PROSERVE ACADEMY ====================== */
|
||||||
|
'For training centres, national academies, and large operational units. Up to sixteen trainees in the same free-roam scenario. Up to 900 m² of indoor space. Permanent, calibrated, supervised by a dedicated instructor cockpit.'
|
||||||
|
=> "Pour les centres de formation, les écoles nationales et les grandes unités opérationnelles. Jusqu'à seize stagiaires dans le même scénario en libre déambulation. Jusqu'à 900 m\u{B2} en intérieur. Installation permanente, calibrée, supervisée par un cockpit instructeur dédié.",
|
||||||
|
|
||||||
|
'When ACADEMY is the right fit' => 'Quand ACADEMY est adapté',
|
||||||
|
'When you need scale, not portability.' => 'Quand vous avez besoin d\'échelle, pas de portabilité.',
|
||||||
|
"ACADEMY is the right configuration when you train cohorts of 8 to 16 trainees in the same scenario, when training is the unit's daily activity, and when you have a dedicated facility you can configure once. The hardware lives in a rack. The instructor sits in a control booth. The trainees walk in, calibrate, and start."
|
||||||
|
=> "ACADEMY est la bonne configuration quand vous formez des cohortes de 8 à 16 stagiaires dans le même scénario, quand l'entraînement est l'activité quotidienne de l'unité et que vous disposez d'un local dédié configurable une fois pour toutes. Le matériel vit en baie. L'instructeur est en cabine de contrôle. Les stagiaires entrent, se calibrent et commencent.",
|
||||||
|
|
||||||
|
'What ACADEMY brings to a curriculum.' => "Ce qu'ACADEMY apporte à un cursus.",
|
||||||
|
'Up to 16 trainees, same scenario' => "Jusqu'à 16 stagiaires, même scénario",
|
||||||
|
'All trainees see each other as full-body 3D avatars whose appearance reflects their role — assault, security, breach, civilian. Communication is voice-natural and proximity-based.'
|
||||||
|
=> "Tous les stagiaires se voient en avatars 3D corps entier dont l'apparence reflète leur rôle — assaut, sécurité, brèche, civil. La communication est vocale naturelle et basée sur la proximité.",
|
||||||
|
'Modular ergonomics' => 'Ergonomie modulaire',
|
||||||
|
'Configure for one trainee or sixteen. Self-configuring software. Less than five minutes from session-end to next-session-start.'
|
||||||
|
=> "Configuration pour un ou seize stagiaires. Logiciel auto-configurable. Moins de cinq minutes entre la fin d'une session et le début de la suivante.",
|
||||||
|
'Multi-mode training' => 'Entraînement multi-modes',
|
||||||
|
'Procedural learning. Motor-skill acquisition. Repetitive drill (skill-and-drill). Deliberate fatigue training. Whatever your pedagogical model, the platform supports it.'
|
||||||
|
=> "Apprentissage procédural. Acquisition motrice. Drill répétitif (skill-and-drill). Entraînement délibéré en fatigue. Quel que soit votre modèle pédagogique, la plateforme le supporte.",
|
||||||
|
'Instructor cockpit, dedicated control room' => 'Cockpit instructeur, salle de contrôle dédiée',
|
||||||
|
"Real-time 360\u{B0} video monitoring, multi-angle tactical view, shot-trace overlay, individual statistics, scenario manipulation on the fly."
|
||||||
|
=> "Supervision vidéo 360\u{B0} en temps réel, vue tactique multi-angles, overlay des traces de tir, statistiques individuelles, manipulation du scénario à la volée.",
|
||||||
|
'Customizable scenarios' => 'Scénarios personnalisables',
|
||||||
|
"Add bespoke scenarios produced by our French studio. Replicate your local training environments, your unit's uniforms, your specific weapons profiles. Production lead time: 1 to 3 months per environment."
|
||||||
|
=> "Ajoutez des scénarios sur mesure produits dans notre studio français. Reproduisez vos environnements locaux, les uniformes de votre unité, vos profils d'armes spécifiques. Délai de production : 1 à 3 mois par environnement.",
|
||||||
|
'Permanent compute infrastructure' => 'Infrastructure de calcul permanente',
|
||||||
|
'Server rack with VR rendering machines, instructor workstation, dedicated Wi-Fi 6E network. Engineered for daily operation, redundancy, and remote support.'
|
||||||
|
=> "Baie serveur avec machines de rendu VR, poste instructeur, réseau Wi-Fi 6E dédié. Conçu pour l'opération quotidienne, la redondance et le support à distance.",
|
||||||
|
|
||||||
|
'Facility requirements' => 'Exigences du local',
|
||||||
|
'What you need on site.' => 'Ce qu\'il vous faut sur site.',
|
||||||
|
"Indoor floor footprint: from 100 m\u{B2} to 900 m\u{B2} depending on the scenarios you train (we recommend 10\u{D7}10 m for full-team dynamic CQB)"
|
||||||
|
=> "Emprise au sol : de 100 m\u{B2} à 900 m\u{B2} selon les scénarios entraînés (nous recommandons 10\u{D7}10 m pour le CQB dynamique en équipe complète)",
|
||||||
|
'Even, non-flickering lighting (no direct sunlight)' => 'Éclairage homogène et stable (pas de lumière directe du soleil)',
|
||||||
|
'Decorated walls (avoid uniformly colored surfaces — posters or markings improve tracking)'
|
||||||
|
=> "Murs habillés (éviter les surfaces uniformes — affiches ou marquages améliorent le tracking)",
|
||||||
|
"Dedicated control room (minimum 6 m\u{B2}) for instructor cockpit"
|
||||||
|
=> "Salle de contrôle dédiée (minimum 6 m\u{B2}) pour le cockpit instructeur",
|
||||||
|
"No specific HVAC requirements (unlike live-fire ranges \u{2014} no smoke, no lead, no decontamination)"
|
||||||
|
=> "Aucune exigence CVC spécifique (contrairement aux stands de tir réel \u{2014} pas de fumée, pas de plomb, pas de décontamination)",
|
||||||
|
|
||||||
|
'Designed for academies.' => 'Conçu pour les académies.',
|
||||||
|
'Police academies (national and regional)' => 'Écoles de police (nationales et régionales)',
|
||||||
|
'Gendarmerie schools' => 'Écoles de gendarmerie',
|
||||||
|
'Military training centres' => 'Centres de formation militaire',
|
||||||
|
'Firefighter training schools' => 'Écoles de sapeurs-pompiers',
|
||||||
|
'Joint inter-services training centres' => 'Centres interarmées de formation',
|
||||||
|
'Distributors and integrators looking to operate a turnkey training-as-a-service offer'
|
||||||
|
=> "Distributeurs et intégrateurs souhaitant opérer une offre training-as-a-service clé en main",
|
||||||
|
|
||||||
|
/* ====================== PROSERVE+ ====================== */
|
||||||
|
'Your environments. Your weapons. Your uniforms. Your doctrine. Our engineers, your specifications. PROSERVE+ extends every PROSERVE configuration with bespoke hardware, environments, and software, designed and produced in our French studio.'
|
||||||
|
=> "Vos environnements. Vos armes. Vos uniformes. Votre doctrine. Nos ingénieurs, vos spécifications. PROSERVE+ étend chaque configuration PROSERVE avec du matériel, des environnements et des logiciels sur mesure, conçus et produits dans notre studio français.",
|
||||||
|
|
||||||
|
'Hardware add-ons' => 'Extensions matérielles',
|
||||||
|
'Hardware extensions.' => 'Extensions matérielles.',
|
||||||
|
'Haptic vest' => 'Gilet haptique',
|
||||||
|
'Full-torso wireless haptic vest. 32 ERM motors. Renders the impact location of incoming fire and explosive proximity. Bluetooth BLE, 13.5 hours autonomy.'
|
||||||
|
=> "Gilet haptique sans fil torso complet. 32 moteurs ERM. Restitue la localisation des impacts entrants et la proximité d'explosion. Bluetooth BLE, 13,5 heures d'autonomie.",
|
||||||
|
'Wrist trackers' => 'Trackers de poignet',
|
||||||
|
'Six degree-of-freedom wrist tracking for precise hand placement, weapon retention, and grappling drills.'
|
||||||
|
=> "Tracking de poignet à six degrés de liberté pour le placement précis des mains, la rétention de l'arme et les exercices de close-combat.",
|
||||||
|
'Custom replicas' => 'Répliques sur mesure',
|
||||||
|
'Sourcing and tracker integration for replicas that go beyond the standard catalog: country-specific service weapons, less-lethal options (electroshock, OC spray, beanbag launchers), shoulder-fired support weapons, and field-specific equipment.'
|
||||||
|
=> "Sourcing et intégration de trackers pour des répliques qui dépassent le catalogue standard : armes de service spécifiques à un pays, options non-létales (électrochoc, gaz OC, lanceurs de sacs lestés), armes d'appui à l'épaule et équipements terrain spécifiques.",
|
||||||
|
|
||||||
|
'Environments and scenarios' => 'Environnements et scénarios',
|
||||||
|
'Bespoke environments and scenarios.' => 'Environnements et scénarios sur mesure.',
|
||||||
|
'All environment types are possible. We model and animate in our French studio, with our team of 3D designers and senior engineers. Lead time: from one to three months per environment, depending on complexity. One environment typically generates five to ten distinct scenarios.'
|
||||||
|
=> "Tous les types d'environnements sont possibles. Modélisation et animation dans notre studio français, avec notre équipe de designers 3D et d'ingénieurs seniors. Délai : de un à trois mois par environnement, selon la complexité. Un environnement génère typiquement cinq à dix scénarios distincts.",
|
||||||
|
'Specific city blocks (urban CQB on actual local geometry)' => 'Quartiers urbains spécifiques (CQB urbain sur géométrie locale réelle)',
|
||||||
|
'Public transit infrastructure (specific airports, stations, metros)'
|
||||||
|
=> 'Infrastructures de transport public (aéroports, gares, métros spécifiques)',
|
||||||
|
'Industrial sites (oil & gas, chemical, energy)' => 'Sites industriels (oil & gas, chimie, énergie)',
|
||||||
|
'Critical infrastructure (data centres, power plants, water treatment)'
|
||||||
|
=> 'Infrastructures critiques (data centres, centrales, traitement des eaux)',
|
||||||
|
'Government and embassy buildings' => 'Bâtiments gouvernementaux et ambassades',
|
||||||
|
'Maritime (boats, naval vessels, oil rigs)' => 'Maritime (navires, bâtiments militaires, plateformes pétrolières)',
|
||||||
|
'Aviation (commercial airliners, airfields)' => 'Aviation (avions de ligne, aérodromes)',
|
||||||
|
|
||||||
|
'Custom characters' => 'Personnages sur mesure',
|
||||||
|
'Characters in your colors.' => 'Des personnages aux couleurs de votre unité.',
|
||||||
|
'Virtual characters dressed in the uniforms of your forces, equipped with the gear of your unit. Hostile actors based on threat profiles relevant to your area of operations. Civilian behaviors localized to the cultural context of the scenario.'
|
||||||
|
=> "Personnages virtuels en uniforme de vos forces, équipés du matériel de votre unité. Acteurs hostiles fondés sur des profils de menace pertinents pour votre zone d'opération. Comportements civils localisés au contexte culturel du scénario.",
|
||||||
|
|
||||||
|
'Process' => 'Processus',
|
||||||
|
'How a PROSERVE+ project runs.' => 'Comment se déroule un projet PROSERVE+.',
|
||||||
|
'1. Discovery' => '1. Cadrage',
|
||||||
|
'We interview your operators and instructors to capture the operational reality and the pedagogical goal.'
|
||||||
|
=> "Nous interviewons vos opérateurs et instructeurs pour capter la réalité opérationnelle et l'objectif pédagogique.",
|
||||||
|
'2. Specification' => '2. Spécification',
|
||||||
|
'Joint document defining the environment, scenarios, characters, weapons, and instructor controls.'
|
||||||
|
=> "Document conjoint définissant l'environnement, les scénarios, les personnages, les armes et les contrôles instructeur.",
|
||||||
|
'3. Production' => '3. Production',
|
||||||
|
'Modeling, animation, AI behavior tuning in our French studio. Weekly review milestones.'
|
||||||
|
=> "Modélisation, animation, réglage des comportements IA dans notre studio français. Jalons de revue hebdomadaires.",
|
||||||
|
'4. Integration' => '4. Intégration',
|
||||||
|
'Software update deploys the new content to your existing PROSERVE installation.'
|
||||||
|
=> "Une mise à jour logicielle déploie le nouveau contenu sur votre installation PROSERVE existante.",
|
||||||
|
'5. Validation' => '5. Validation',
|
||||||
|
'On-site instructor training and acceptance review.'
|
||||||
|
=> "Formation des instructeurs sur site et revue de réception.",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
URL trigger — fires on template_redirect (NOT init) so Bricks's
|
||||||
|
element controls are loaded (happens on 'wp' hook). Without loaded
|
||||||
|
controls, Bricks's wpml_page_builder_register_strings handler skips
|
||||||
|
every element silently and NO strings reach icl_strings → ATE empty.
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
add_action('template_redirect', function () {
|
||||||
|
if (empty($_GET['av_generate_solutions'])) return;
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
$data_file = 'C:\\ASTERION\\GIT\\WP2026\\content-source\\solutions-data.php';
|
||||||
|
if (! file_exists($data_file)) wp_die("data file not found: $data_file");
|
||||||
|
|
||||||
|
$solutions = include $data_file;
|
||||||
|
if (! is_array($solutions)) wp_die('solutions data not array');
|
||||||
|
|
||||||
|
$parent_id = av_ensure_hub_page('solutions', 'Solutions');
|
||||||
|
$report = ["Rebuilding /solutions/ hub + child detail pages..."];
|
||||||
|
|
||||||
|
// Hub
|
||||||
|
$hub_elements = av_build_solutions_hub_elements($solutions);
|
||||||
|
$hub_write = av_write_bricks_page_elements($parent_id, $hub_elements);
|
||||||
|
$report[] = " HUB → page #$parent_id (rebuilt) — " . count($hub_elements)
|
||||||
|
. " elements (json={$hub_write['json_len']}, stored={$hub_write['stored_len']})";
|
||||||
|
|
||||||
|
// Detail pages
|
||||||
|
foreach ($solutions as $slug => $d) {
|
||||||
|
$page = av_upsert_detail_page($slug, 'solutions', $d);
|
||||||
|
$report[] = " $slug → page #{$page['id']} ({$page['action']}) — {$page['element_count']} elements (json={$page['json_len']}, stored={$page['stored_len']})";
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo implode("\n", $report);
|
||||||
|
exit;
|
||||||
|
});
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
Solutions hub : hero + 4-card grid (tier comparison) + closer CTA
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
function av_build_solutions_hub_elements($solutions) {
|
||||||
|
$els = [];
|
||||||
|
|
||||||
|
// HERO
|
||||||
|
$els = array_merge($els, av_build_hero('shub', [
|
||||||
|
'eyebrow' => 'PROSERVE platform',
|
||||||
|
'tagline' => "Four tiers. One platform. Train where you are.",
|
||||||
|
'hero_lead' => "From the solo officer in a 9 m\u{B2} corner to a 16-trainee free-roam academy : PROSERVE\u{2122} scales without changing software. Same physics. Same AI. Same instructor logic. Pick the configuration that fits your context — upgrade or extend later.",
|
||||||
|
'ctas' => [
|
||||||
|
['label' => 'Book a demo', 'href' => '/request-demo/', 'variant' => 'primary'],
|
||||||
|
['label' => 'Request a T&E Kit', 'href' => '/request-te-kit/', 'variant' => 'secondary'],
|
||||||
|
],
|
||||||
|
]));
|
||||||
|
|
||||||
|
// 4-card grid — one card per tier
|
||||||
|
$cards_section = [
|
||||||
|
'eyebrow' => '',
|
||||||
|
'title' => 'Pick the right configuration for your unit.',
|
||||||
|
'items' => [],
|
||||||
|
];
|
||||||
|
foreach ($solutions as $slug => $d) {
|
||||||
|
$cards_section['items'][] = [
|
||||||
|
'title' => $d['title'],
|
||||||
|
'body' => ($d['eyebrow'] ?? '') . " \u{2014} " . ($d['tagline'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$els = array_merge($els, av_build_cards_section('shubgrid', 0, $cards_section, 'var(--color-bg-subtle)'));
|
||||||
|
|
||||||
|
// Append "Learn more →" link in each card (post-build patch)
|
||||||
|
$i = 0;
|
||||||
|
foreach ($solutions as $slug => $d) {
|
||||||
|
$kid = "kshubgridc0x{$i}";
|
||||||
|
$lid = "lnshubgridc0x{$i}";
|
||||||
|
$els[] = av_text_link($lid, $kid, "Learn more \u{2192}", "/solutions/$slug/");
|
||||||
|
foreach ($els as &$el) {
|
||||||
|
if (($el['id'] ?? '') === $kid) {
|
||||||
|
$el['children'][] = $lid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($el);
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLOSER CTA
|
||||||
|
$els = array_merge($els, av_build_closer_cta('shub'));
|
||||||
|
|
||||||
|
return $els;
|
||||||
|
}
|
||||||
61
tools/av-translate-industries-fr.php
Normal file
61
tools/av-translate-industries-fr.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Seed FR translations for Industries (one-shot)
|
||||||
|
* Description: /?av_translate_industries_fr=1 creates the FR linked posts for
|
||||||
|
* the Industries hub + 4 detail pages. After that, run av_generate_industries=1
|
||||||
|
* again to push the translated structure via the auto-sync pipeline.
|
||||||
|
*
|
||||||
|
* Order matters : run /?av_generate_industries=1 FIRST (creates EN pages),
|
||||||
|
* then this script (creates linked FR posts), then /?av_generate_industries=1
|
||||||
|
* AGAIN (auto-sync fills FR Bricks content via the shared dictionary).
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/_av-bricks-shared.php';
|
||||||
|
|
||||||
|
add_action('template_redirect', function () {
|
||||||
|
if (empty($_GET['av_translate_industries_fr'])) return;
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
$hub = get_page_by_path('industries');
|
||||||
|
if (! $hub) {
|
||||||
|
echo "FAIL : /industries/ hub not found. Run ?av_generate_industries=1 first.\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fr_titles = [
|
||||||
|
'industries' => 'Secteurs',
|
||||||
|
'police' => 'Police',
|
||||||
|
'special-forces' => 'Forces spéciales',
|
||||||
|
'military' => 'Armée',
|
||||||
|
'firefighters' => 'Sapeurs-pompiers',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 1) Hub FR
|
||||||
|
$fr_id = av_translate_page_fr($hub->ID, 'industries', $fr_titles['industries']);
|
||||||
|
if ($fr_id) {
|
||||||
|
echo "HUB EN #{$hub->ID} → FR #$fr_id (slug: industries)\n";
|
||||||
|
} else {
|
||||||
|
echo "HUB EN #{$hub->ID} : FR creation FAILED\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Detail pages FR
|
||||||
|
foreach (['police', 'special-forces', 'military', 'firefighters'] as $slug) {
|
||||||
|
$en = get_page_by_path("industries/$slug");
|
||||||
|
if (! $en) {
|
||||||
|
echo " $slug : EN page not found — skipping\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$fr_id = av_translate_page_fr($en->ID, $slug, $fr_titles[$slug]);
|
||||||
|
if ($fr_id) {
|
||||||
|
echo " $slug : EN #{$en->ID} → FR #$fr_id\n";
|
||||||
|
} else {
|
||||||
|
echo " $slug : FR creation FAILED\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nDONE. Now run /?av_generate_industries=1 again to push translated structure to FR via auto-sync.\n";
|
||||||
|
exit;
|
||||||
|
});
|
||||||
61
tools/av-translate-solutions-fr.php
Normal file
61
tools/av-translate-solutions-fr.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Seed FR translations for Solutions (one-shot)
|
||||||
|
* Description: /?av_translate_solutions_fr=1 creates the FR linked posts for
|
||||||
|
* the Solutions hub + 4 detail pages. After that, run av_generate_solutions=1
|
||||||
|
* again to push the translated structure via the auto-sync pipeline.
|
||||||
|
*
|
||||||
|
* Order matters : run /?av_generate_solutions=1 FIRST (creates EN pages),
|
||||||
|
* then this script (creates linked FR posts), then /?av_generate_solutions=1
|
||||||
|
* AGAIN (auto-sync fills the FR Bricks content via the shared dictionary).
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/_av-bricks-shared.php';
|
||||||
|
|
||||||
|
add_action('template_redirect', function () {
|
||||||
|
if (empty($_GET['av_translate_solutions_fr'])) return;
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
$hub = get_page_by_path('solutions');
|
||||||
|
if (! $hub) {
|
||||||
|
echo "FAIL : /solutions/ hub not found. Run ?av_generate_solutions=1 first.\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fr_titles = [
|
||||||
|
'solutions' => 'Solutions',
|
||||||
|
'my-proserve' => 'MY PROSERVE',
|
||||||
|
'proserve-flex' => 'PROSERVE FLEX',
|
||||||
|
'proserve-academy' => 'PROSERVE ACADEMY',
|
||||||
|
'customization' => 'PROSERVE+',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 1) Hub FR
|
||||||
|
$fr_id = av_translate_page_fr($hub->ID, 'solutions', $fr_titles['solutions']);
|
||||||
|
if ($fr_id) {
|
||||||
|
echo "HUB EN #{$hub->ID} → FR #$fr_id (slug: solutions)\n";
|
||||||
|
} else {
|
||||||
|
echo "HUB EN #{$hub->ID} : FR creation FAILED\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Detail pages FR
|
||||||
|
foreach (['my-proserve', 'proserve-flex', 'proserve-academy', 'customization'] as $slug) {
|
||||||
|
$en = get_page_by_path("solutions/$slug");
|
||||||
|
if (! $en) {
|
||||||
|
echo " $slug : EN page not found — skipping\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$fr_id = av_translate_page_fr($en->ID, $slug, $fr_titles[$slug]);
|
||||||
|
if ($fr_id) {
|
||||||
|
echo " $slug : EN #{$en->ID} → FR #$fr_id\n";
|
||||||
|
} else {
|
||||||
|
echo " $slug : FR creation FAILED\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nDONE. Now run /?av_generate_solutions=1 again to push translated structure to FR via auto-sync.\n";
|
||||||
|
exit;
|
||||||
|
});
|
||||||
185
tools/av-wpml-create-jobs.php
Normal file
185
tools/av-wpml-create-jobs.php
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Create ATE-flagged translate_job rows for our FR pages
|
||||||
|
* Description:
|
||||||
|
* /?av_wpml_create_jobs=1 → for each FR linked page, insert a row in
|
||||||
|
* wp_icl_translate_job with editor='ate' + translated=1, and adjust the
|
||||||
|
* translation_status row to mirror Bricks #14's pattern (status=2 IN_PROGRESS
|
||||||
|
* with uuid + batch_id). This should switch the page-list icon from pencil
|
||||||
|
* ✏️ to gear ⚙️ and route the click to ATE instead of WP editor.
|
||||||
|
*
|
||||||
|
* /?av_wpml_remove_jobs=1 → revert if it breaks something (delete jobs, set
|
||||||
|
* status=10 again).
|
||||||
|
*
|
||||||
|
* This is exploratory : we mimic the structure WPML's internal send_jobs()
|
||||||
|
* creates, without going through TranslationProxy (cloud).
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('template_redirect', function () {
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) return;
|
||||||
|
if (! empty($_GET['av_wpml_create_jobs'])) av_wpml_create_jobs();
|
||||||
|
if (! empty($_GET['av_wpml_remove_jobs'])) av_wpml_remove_jobs();
|
||||||
|
});
|
||||||
|
|
||||||
|
function av_target_fr_translations() {
|
||||||
|
global $wpdb;
|
||||||
|
$slugs = ['industries', 'police', 'special-forces', 'military', 'firefighters',
|
||||||
|
'solutions', 'my-proserve', 'proserve-flex', 'proserve-academy', 'customization'];
|
||||||
|
$placeholders = implode(',', array_fill(0, count($slugs), '%s'));
|
||||||
|
return $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT p.ID AS post_id, p.post_title AS title, t.trid, t.translation_id, ts.rid, ts.status
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
JOIN {$wpdb->prefix}icl_translations t ON t.element_id = p.ID AND t.element_type='post_page' AND t.language_code='fr'
|
||||||
|
LEFT JOIN {$wpdb->prefix}icl_translation_status ts ON ts.translation_id = t.translation_id
|
||||||
|
WHERE p.post_name IN ($placeholders)",
|
||||||
|
...$slugs
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
function av_wpml_create_jobs() {
|
||||||
|
global $wpdb;
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
$rows = av_target_fr_translations();
|
||||||
|
if (! $rows) { echo "No FR translations found\n"; exit; }
|
||||||
|
|
||||||
|
$batch_id = 1; // reuse Bricks #14's batch (or create new one — using 1 for simplicity)
|
||||||
|
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
if (! $r->rid) {
|
||||||
|
echo "FR #{$r->post_id} '{$r->title}' : no translation_status row, skipping\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Set translation_status to status=2 (ICL_TM_IN_PROGRESS) to match
|
||||||
|
// Bricks #14's pattern. This is what makes the FR column icon = gear ⚙️
|
||||||
|
// and routes the click to the translation editor (CTE).
|
||||||
|
// Note: editing the EN page directly will show a "Translation in progress,
|
||||||
|
// wait before editing" warning dialog — informative only, click "I understand"
|
||||||
|
// to dismiss.
|
||||||
|
$uuid = wp_generate_uuid4();
|
||||||
|
$wpdb->update(
|
||||||
|
$wpdb->prefix . 'icl_translation_status',
|
||||||
|
[
|
||||||
|
'status' => 2, // ICL_TM_IN_PROGRESS (= gear icon)
|
||||||
|
'needs_update' => 0,
|
||||||
|
'translator_id' => 1,
|
||||||
|
'translation_service' => 'local',
|
||||||
|
'batch_id' => $batch_id,
|
||||||
|
'uuid' => $uuid,
|
||||||
|
'tp_revision' => 1,
|
||||||
|
'timestamp' => current_time('mysql'),
|
||||||
|
'links_fixed' => 1,
|
||||||
|
],
|
||||||
|
['rid' => $r->rid]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Insert/update translate_job row with editor='ate'
|
||||||
|
$existing_job = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT job_id FROM {$wpdb->prefix}icl_translate_job WHERE rid=%d ORDER BY job_id DESC LIMIT 1",
|
||||||
|
$r->rid
|
||||||
|
));
|
||||||
|
|
||||||
|
// editor='wpml' = CTE (Classic Translation Editor), works locally without
|
||||||
|
// cloud sync. translated=1 + completed_date set = the FR is "ready",
|
||||||
|
// matches Bricks #14's pattern (which shows gear icon and opens CTE).
|
||||||
|
// CRITICAL : revision MUST be NULL. WPML's filter_status_css_class() in
|
||||||
|
// class-wpml-tm-translation-status-display.php joins icl_translate_job
|
||||||
|
// with the explicit condition `translate_job.revision IS NULL`. Jobs
|
||||||
|
// with non-null revision are silently ignored → pencil icon, no CTE.
|
||||||
|
$job_data = [
|
||||||
|
'rid' => $r->rid,
|
||||||
|
'translator_id' => 1,
|
||||||
|
'translated' => 1, // FR considered done (we pre-filled via dict)
|
||||||
|
'manager_id' => 1,
|
||||||
|
// 'revision' deliberately omitted → DB default = NULL
|
||||||
|
'title' => $r->title,
|
||||||
|
'deadline_date' => null,
|
||||||
|
'completed_date' => current_time('mysql'),
|
||||||
|
'editor' => 'wpml', // CTE (matches doc_translation_method=1)
|
||||||
|
'editor_job_id' => null,
|
||||||
|
'automatic' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($existing_job) {
|
||||||
|
$wpdb->update($wpdb->prefix . 'icl_translate_job', $job_data, ['job_id' => $existing_job]);
|
||||||
|
// $wpdb->update can't write SQL NULL (it coerces to empty string).
|
||||||
|
// Force revision NULL via raw query for the icon's JOIN to match.
|
||||||
|
$wpdb->query($wpdb->prepare(
|
||||||
|
"UPDATE {$wpdb->prefix}icl_translate_job SET revision = NULL WHERE job_id = %d",
|
||||||
|
$existing_job
|
||||||
|
));
|
||||||
|
echo "FR #{$r->post_id} '{$r->title}' (rid={$r->rid}) : job UPDATED #{$existing_job} (editor={$job_data['editor']}, revision=NULL)\n";
|
||||||
|
} else {
|
||||||
|
$wpdb->insert($wpdb->prefix . 'icl_translate_job', $job_data);
|
||||||
|
$new_job_id = $wpdb->insert_id;
|
||||||
|
// Belt-and-suspenders : ensure revision is NULL even on fresh insert.
|
||||||
|
$wpdb->query($wpdb->prepare(
|
||||||
|
"UPDATE {$wpdb->prefix}icl_translate_job SET revision = NULL WHERE job_id = %d",
|
||||||
|
$new_job_id
|
||||||
|
));
|
||||||
|
echo "FR #{$r->post_id} '{$r->title}' (rid={$r->rid}) : job CREATED #{$new_job_id} (editor={$job_data['editor']}, revision=NULL)\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nDONE. Refresh WP Admin → Pages. The FR column icon should switch from pencil ✏️ to gear ⚙️.\n";
|
||||||
|
echo "Click the gear to open ATE for that page.\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function av_wpml_remove_jobs() {
|
||||||
|
global $wpdb;
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
$rows = av_target_fr_translations();
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
if (! $r->rid) continue;
|
||||||
|
|
||||||
|
// 1. Remove translate_job rows
|
||||||
|
$wpdb->query($wpdb->prepare(
|
||||||
|
"DELETE FROM {$wpdb->prefix}icl_translate_job WHERE rid=%d",
|
||||||
|
$r->rid
|
||||||
|
));
|
||||||
|
|
||||||
|
// 2. Reset status row : ALL the cloud-sync artifacts cleared
|
||||||
|
$wpdb->update(
|
||||||
|
$wpdb->prefix . 'icl_translation_status',
|
||||||
|
[
|
||||||
|
'status' => 10,
|
||||||
|
'needs_update' => 0,
|
||||||
|
'batch_id' => 0,
|
||||||
|
'uuid' => '',
|
||||||
|
'tp_id' => '',
|
||||||
|
'tp_revision' => 0,
|
||||||
|
'ts_status' => null,
|
||||||
|
'review_status' => null,
|
||||||
|
'ate_comm_retry_count' => 0,
|
||||||
|
'_prevstate' => '',
|
||||||
|
'translation_service' => 'local',
|
||||||
|
'translator_id' => 0,
|
||||||
|
],
|
||||||
|
['rid' => $r->rid]
|
||||||
|
);
|
||||||
|
|
||||||
|
echo "FR #{$r->post_id} : job removed, status fully reset\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Cleanup orphan translation_batches rows that may trigger cloud-fetch
|
||||||
|
$orphans = $wpdb->query(
|
||||||
|
"DELETE FROM {$wpdb->prefix}icl_translation_batches
|
||||||
|
WHERE batch_name LIKE 'asterion%' OR id NOT IN
|
||||||
|
(SELECT DISTINCT batch_id FROM {$wpdb->prefix}icl_translation_status WHERE batch_id > 0)"
|
||||||
|
);
|
||||||
|
echo "\nOrphan translation_batches rows cleaned : $orphans\n";
|
||||||
|
|
||||||
|
// 4. Cleanup orphan translation_downloads rows (background ATE sync queue)
|
||||||
|
$tbl_downloads = $wpdb->prefix . 'icl_translation_downloads';
|
||||||
|
if ($wpdb->get_var("SHOW TABLES LIKE '$tbl_downloads'")) {
|
||||||
|
$cleared = $wpdb->query("DELETE FROM $tbl_downloads");
|
||||||
|
echo "Cleared $cleared rows from icl_translation_downloads (pending cloud sync queue)\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nReverted to clean state. WPML queue + page list should be back to normal.\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
59
tools/av-wpml-debug.php
Normal file
59
tools/av-wpml-debug.php
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — WPML debug helpers
|
||||||
|
* Description: Read-only URL handlers to inspect WPML state. Localhost only.
|
||||||
|
* - /?av_wpml_status=1 → active languages, default language
|
||||||
|
* - /?av_wpml_translations=ID → translations for a given post id
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) return;
|
||||||
|
|
||||||
|
if (! empty($_GET['av_wpml_status'])) {
|
||||||
|
av_wpml_status();
|
||||||
|
} elseif (! empty($_GET['av_wpml_translations'])) {
|
||||||
|
av_wpml_translations((int) $_GET['av_wpml_translations']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function av_wpml_status() {
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
$active = apply_filters('wpml_active_languages', null, []);
|
||||||
|
$default = apply_filters('wpml_default_language', null);
|
||||||
|
$current = apply_filters('wpml_current_language', null);
|
||||||
|
$setup = get_option('icl_sitepress_settings');
|
||||||
|
|
||||||
|
echo "WPML active : " . (function_exists('wpml_loaded') ? 'YES' : '?') . "\n";
|
||||||
|
echo "Default lang: $default\n";
|
||||||
|
echo "Current lang: $current\n";
|
||||||
|
echo "\nActive languages:\n";
|
||||||
|
foreach ((array) $active as $lc => $l) {
|
||||||
|
echo " $lc " . ($l['display_name'] ?? '') . " url=" . ($l['url'] ?? '') . "\n";
|
||||||
|
}
|
||||||
|
echo "\nURL strategy: " . ($setup['language_negotiation_type'] ?? '?') . " (1=lang in dir, 2=domain, 3=lang param)\n";
|
||||||
|
echo "Hidden langs: " . implode(',', $setup['hidden_languages'] ?? []) . "\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function av_wpml_translations($post_id) {
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
$type = get_post_type($post_id);
|
||||||
|
$title = get_the_title($post_id);
|
||||||
|
$element = 'post_' . $type;
|
||||||
|
$trid = apply_filters('wpml_element_trid', null, $post_id, $element);
|
||||||
|
$translations = apply_filters('wpml_get_element_translations', null, $trid, $element);
|
||||||
|
|
||||||
|
echo "Post #$post_id (type=$type) : \"$title\"\n";
|
||||||
|
echo "trid: " . ($trid ?? 'null') . "\n\n";
|
||||||
|
echo "Translations:\n";
|
||||||
|
foreach ((array) $translations as $lc => $t) {
|
||||||
|
$tid = $t->element_id ?? '?';
|
||||||
|
$name = $t->post_title ?? '?';
|
||||||
|
$slug = get_post($tid)->post_name ?? '?';
|
||||||
|
echo " [$lc] id=$tid slug=$slug title=\"$name\"\n";
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
270
tools/av-wpml-fix-status.php
Normal file
270
tools/av-wpml-fix-status.php
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — WPML translation_status diagnose + fix
|
||||||
|
* Description:
|
||||||
|
* /?av_wpml_status_dump=1 → dump state of every linked FR page (icl_translations
|
||||||
|
* + icl_translation_status + Bricks meta + ATE editor pref)
|
||||||
|
* /?av_wpml_status_fix=1 → insert/update icl_translation_status rows so WPML
|
||||||
|
* marks our programmatically-translated pages as
|
||||||
|
* "completed via ATE", clears the orange "needs update"
|
||||||
|
* icon, and lets the pencil → ATE button work.
|
||||||
|
*
|
||||||
|
* Why : our pipeline writes _bricks_page_content_2 directly via $wpdb. That bypasses
|
||||||
|
* WPML's normal "translation completed" hook. WPML sees the linked posts in
|
||||||
|
* icl_translations but icl_translation_status is empty → the WPML UI thinks the
|
||||||
|
* translation is missing/incomplete and routes the pencil button to the WP editor
|
||||||
|
* fallback instead of the Advanced Translation Editor.
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) return;
|
||||||
|
|
||||||
|
if (! empty($_GET['av_wpml_status_dump'])) {
|
||||||
|
av_wpml_status_dump();
|
||||||
|
}
|
||||||
|
if (! empty($_GET['av_wpml_status_fix'])) {
|
||||||
|
av_wpml_status_fix();
|
||||||
|
}
|
||||||
|
if (! empty($_GET['av_wpml_repair'])) {
|
||||||
|
av_wpml_repair(); // full pipeline : relink-trid → cleanup orphans → status_fix
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function av_wpml_target_trids() {
|
||||||
|
global $wpdb;
|
||||||
|
// All trids that have a /fr/ post WHERE EN parent is /industries/, /solutions/
|
||||||
|
// or the hub pages themselves.
|
||||||
|
return $wpdb->get_col(
|
||||||
|
"SELECT DISTINCT t.trid
|
||||||
|
FROM {$wpdb->prefix}icl_translations t
|
||||||
|
JOIN {$wpdb->posts} p ON p.ID = t.element_id
|
||||||
|
WHERE t.element_type='post_page'
|
||||||
|
AND t.element_id > 0
|
||||||
|
AND (p.post_name IN ('industries','solutions','police','special-forces','military','firefighters','my-proserve','proserve-flex','proserve-academy','customization'))"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function av_wpml_status_dump() {
|
||||||
|
global $wpdb;
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
// 1) Sitepress editor preference
|
||||||
|
$opts = get_option('icl_sitepress_settings');
|
||||||
|
$editor_default = $opts['translation-management']['doc_translation_method'] ?? '(unset)';
|
||||||
|
$editor_alt = $opts['translation_method'] ?? '(unset)';
|
||||||
|
$ate_enabled = $opts['translation-management']['ate']['ate_enabled'] ?? '(unset)';
|
||||||
|
echo "WPML editor preference :\n";
|
||||||
|
echo " doc_translation_method = $editor_default\n";
|
||||||
|
echo " translation_method = $editor_alt\n";
|
||||||
|
echo " ate_enabled = $ate_enabled\n";
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// 2) For each target trid, dump rows
|
||||||
|
$trids = av_wpml_target_trids();
|
||||||
|
if (! $trids) { echo "(no target trids found)\n"; exit; }
|
||||||
|
echo "Found " . count($trids) . " target trid(s) : " . implode(', ', $trids) . "\n\n";
|
||||||
|
|
||||||
|
foreach ($trids as $trid) {
|
||||||
|
echo "── trid $trid ──\n";
|
||||||
|
$tr_rows = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT translation_id, element_id, language_code, source_language_code
|
||||||
|
FROM {$wpdb->prefix}icl_translations
|
||||||
|
WHERE trid=%d ORDER BY language_code",
|
||||||
|
$trid
|
||||||
|
));
|
||||||
|
foreach ($tr_rows as $r) {
|
||||||
|
$title = get_the_title($r->element_id);
|
||||||
|
echo " [{$r->language_code}] post #{$r->element_id} '{$title}' (translation_id={$r->translation_id}, src={$r->source_language_code})\n";
|
||||||
|
|
||||||
|
// status row
|
||||||
|
$st = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT status, needs_update, translation_service, translator_id, md5
|
||||||
|
FROM {$wpdb->prefix}icl_translation_status
|
||||||
|
WHERE translation_id=%d",
|
||||||
|
$r->translation_id
|
||||||
|
));
|
||||||
|
if ($st) {
|
||||||
|
echo " status_row : status=$st->status needs_update=$st->needs_update service=$st->translation_service translator=$st->translator_id md5=" . substr($st->md5 ?? '', 0, 8) . "\n";
|
||||||
|
} else {
|
||||||
|
echo " status_row : MISSING (no row in icl_translation_status)\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full repair pipeline :
|
||||||
|
* 1. For each known EN slug, find EN's trid.
|
||||||
|
* 2. Find any FR post with the same slug. If it's linked to a DIFFERENT trid,
|
||||||
|
* relink it to EN's trid (this fixes session-3 re-creation cases where
|
||||||
|
* set_element_language_details fell back to a fresh trid because an orphan
|
||||||
|
* empty FR row blocked the canonical (trid, fr, ...) slot).
|
||||||
|
* 3. Delete orphan FR rows (element_id NULL/0/empty) in the canonical trid.
|
||||||
|
* 4. Reset icl_translation_status (status=10, needs_update=0, md5 from EN content).
|
||||||
|
*/
|
||||||
|
function av_wpml_repair() {
|
||||||
|
global $wpdb;
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
$slugs = [
|
||||||
|
// Industries
|
||||||
|
'industries', 'police', 'special-forces', 'military', 'firefighters',
|
||||||
|
// Solutions
|
||||||
|
'solutions', 'my-proserve', 'proserve-flex', 'proserve-academy', 'customization',
|
||||||
|
];
|
||||||
|
|
||||||
|
echo "── STEP 1+2 : align FR posts to EN's trid ──\n\n";
|
||||||
|
foreach ($slugs as $slug) {
|
||||||
|
// EN post for slug
|
||||||
|
$en = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT p.ID, t.trid
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
JOIN {$wpdb->prefix}icl_translations t ON t.element_id = p.ID AND t.element_type='post_page'
|
||||||
|
WHERE p.post_name=%s AND t.language_code='en'
|
||||||
|
LIMIT 1",
|
||||||
|
$slug
|
||||||
|
));
|
||||||
|
if (! $en) { echo " $slug : no EN page found — skip\n"; continue; }
|
||||||
|
|
||||||
|
// FR post for slug (regardless of current trid)
|
||||||
|
$fr = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT p.ID, t.trid, t.translation_id
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
JOIN {$wpdb->prefix}icl_translations t ON t.element_id = p.ID AND t.element_type='post_page'
|
||||||
|
WHERE p.post_name=%s AND t.language_code='fr'
|
||||||
|
LIMIT 1",
|
||||||
|
$slug
|
||||||
|
));
|
||||||
|
if (! $fr) { echo " $slug : EN #{$en->ID} (trid={$en->trid}) — no FR yet\n"; continue; }
|
||||||
|
|
||||||
|
if ((int) $fr->trid === (int) $en->trid) {
|
||||||
|
echo " $slug : EN #{$en->ID} ↔ FR #{$fr->ID} aligned on trid={$en->trid} ✓\n";
|
||||||
|
} else {
|
||||||
|
// 1) Remove any orphan FR row in EN's canonical trid
|
||||||
|
$orphans = $wpdb->query($wpdb->prepare(
|
||||||
|
"DELETE FROM {$wpdb->prefix}icl_translations
|
||||||
|
WHERE trid=%d AND language_code='fr' AND element_type='post_page'
|
||||||
|
AND (element_id IS NULL OR element_id = 0 OR element_id = '')",
|
||||||
|
$en->trid
|
||||||
|
));
|
||||||
|
// 2) Move FR translation_id to EN's trid
|
||||||
|
$wpdb->update(
|
||||||
|
$wpdb->prefix . 'icl_translations',
|
||||||
|
['trid' => $en->trid, 'source_language_code' => 'en'],
|
||||||
|
['translation_id' => $fr->translation_id],
|
||||||
|
['%d', '%s'],
|
||||||
|
['%d']
|
||||||
|
);
|
||||||
|
// 3) Delete the now-empty old trid row(s) of FR's previous trid (in case anything else was attached there)
|
||||||
|
// Skip — keep it conservative. The old trid row for FR is now moved, not deleted.
|
||||||
|
|
||||||
|
echo " $slug : FR #{$fr->ID} moved from trid={$fr->trid} → {$en->trid} (deleted $orphans orphan(s))\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n── STEP 3 : remove ALL orphan FR rows across known trids ──\n\n";
|
||||||
|
$trids = av_wpml_target_trids();
|
||||||
|
foreach ($trids as $trid) {
|
||||||
|
$deleted = $wpdb->query($wpdb->prepare(
|
||||||
|
"DELETE FROM {$wpdb->prefix}icl_translations
|
||||||
|
WHERE trid=%d AND (element_id IS NULL OR element_id = 0 OR element_id = '')",
|
||||||
|
$trid
|
||||||
|
));
|
||||||
|
if ($deleted > 0) echo " trid $trid : deleted $deleted orphan row(s)\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n── STEP 4 : write icl_translation_status (status=10 completed) ──\n\n";
|
||||||
|
av_wpml_status_fix(false);
|
||||||
|
echo "\nDONE.\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function av_wpml_status_fix($exit_after = true) {
|
||||||
|
global $wpdb;
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
$trids = av_wpml_target_trids();
|
||||||
|
$patched = 0; $inserted = 0;
|
||||||
|
|
||||||
|
foreach ($trids as $trid) {
|
||||||
|
// Get EN and FR translation_id + post_id
|
||||||
|
$en = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT translation_id, element_id FROM {$wpdb->prefix}icl_translations
|
||||||
|
WHERE trid=%d AND language_code='en' AND element_id > 0 LIMIT 1",
|
||||||
|
$trid
|
||||||
|
));
|
||||||
|
$fr = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT translation_id, element_id FROM {$wpdb->prefix}icl_translations
|
||||||
|
WHERE trid=%d AND language_code='fr' AND element_id > 0 LIMIT 1",
|
||||||
|
$trid
|
||||||
|
));
|
||||||
|
if (! $en || ! $fr) {
|
||||||
|
echo "trid $trid : EN or FR missing — skip\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute md5 of EN _bricks_page_content_2 (what WPML's translation engine would
|
||||||
|
// hash on a normal flow). When EN changes, md5 differs → WPML shows "needs update".
|
||||||
|
$en_content = get_post_meta($en->element_id, '_bricks_page_content_2', true);
|
||||||
|
if (is_array($en_content)) $en_content = wp_json_encode($en_content);
|
||||||
|
$md5 = md5((string) $en_content);
|
||||||
|
|
||||||
|
// Status codes (WPML constants, hardcoded for portability) :
|
||||||
|
// ICL_TM_NOT_TRANSLATED = 0
|
||||||
|
// ICL_TM_WAITING = 1
|
||||||
|
// ICL_TM_NEEDS_UPDATE = 3
|
||||||
|
// ICL_TM_DUPLICATE = 9
|
||||||
|
// ICL_TM_COMPLETE = 10
|
||||||
|
$data = [
|
||||||
|
'translation_id' => $fr->translation_id,
|
||||||
|
'status' => 10, // ICL_TM_COMPLETE
|
||||||
|
'needs_update' => 0,
|
||||||
|
'md5' => $md5,
|
||||||
|
'translation_service' => 'local', // not delegated to a paid service
|
||||||
|
'translator_id' => 0, // local human (us, programmatic)
|
||||||
|
'translated_by_id' => 0,
|
||||||
|
'links_fixed' => 1,
|
||||||
|
];
|
||||||
|
|
||||||
|
$existing = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT rid FROM {$wpdb->prefix}icl_translation_status WHERE translation_id=%d",
|
||||||
|
$fr->translation_id
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
// Only update fields that exist (drop optional ones to be safe)
|
||||||
|
$update = $data;
|
||||||
|
unset($update['translation_id']); // not changing PK
|
||||||
|
$wpdb->update($wpdb->prefix . 'icl_translation_status', $update, ['rid' => $existing]);
|
||||||
|
echo "trid $trid : FR #{$fr->element_id} → status row UPDATED (rid=$existing, status=10, md5=" . substr($md5, 0, 8) . ")\n";
|
||||||
|
$patched++;
|
||||||
|
} else {
|
||||||
|
// Be careful : the table may not have all columns. Try with full first, fallback.
|
||||||
|
$ok = $wpdb->insert($wpdb->prefix . 'icl_translation_status', $data);
|
||||||
|
if (! $ok) {
|
||||||
|
// Retry minimal
|
||||||
|
$minimal = [
|
||||||
|
'translation_id' => $fr->translation_id,
|
||||||
|
'status' => 10,
|
||||||
|
'needs_update' => 0,
|
||||||
|
'md5' => $md5,
|
||||||
|
];
|
||||||
|
$ok = $wpdb->insert($wpdb->prefix . 'icl_translation_status', $minimal);
|
||||||
|
}
|
||||||
|
if ($ok) {
|
||||||
|
echo "trid $trid : FR #{$fr->element_id} → status row INSERTED (status=10, md5=" . substr($md5, 0, 8) . ")\n";
|
||||||
|
$inserted++;
|
||||||
|
} else {
|
||||||
|
echo "trid $trid : FR #{$fr->element_id} → INSERT FAILED : " . $wpdb->last_error . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nDONE. patched=$patched, inserted=$inserted\n";
|
||||||
|
echo "Refresh WP Admin → Pages — orange refresh icons should clear, pencils should\n";
|
||||||
|
echo "route to WPML's editor (ATE if ate_enabled=1, classic editor otherwise).\n";
|
||||||
|
if ($exit_after) exit;
|
||||||
|
}
|
||||||
50
tools/av-wpml-plugins-check.php
Normal file
50
tools/av-wpml-plugins-check.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Inspect WPML installation state
|
||||||
|
* /?av_wpml_check=1 → liste les plugins WPML installés/actifs + clés de config pertinentes.
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) return;
|
||||||
|
if (empty($_GET['av_wpml_check'])) return;
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
// Active plugins
|
||||||
|
$active = get_option('active_plugins', []);
|
||||||
|
$all_plugins = get_plugins();
|
||||||
|
echo "WPML-related plugins :\n";
|
||||||
|
foreach ($all_plugins as $file => $data) {
|
||||||
|
if (stripos($file, 'wpml') === false && stripos($file, 'sitepress') === false) continue;
|
||||||
|
$is_active = in_array($file, $active, true);
|
||||||
|
echo sprintf(" %-50s %s (v%s)\n", $file, $is_active ? '[ACTIVE]' : '[inactive]', $data['Version']);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nSitepress settings (extracted) :\n";
|
||||||
|
$opts = get_option('icl_sitepress_settings');
|
||||||
|
$keys = [
|
||||||
|
['translation-management', 'doc_translation_method'],
|
||||||
|
['translation-management', 'ate', 'ate_enabled'],
|
||||||
|
['existing_content_language_verified'],
|
||||||
|
['setup_complete'],
|
||||||
|
['language_negotiation_type'],
|
||||||
|
];
|
||||||
|
foreach ($keys as $path) {
|
||||||
|
$val = $opts;
|
||||||
|
foreach ($path as $k) {
|
||||||
|
$val = is_array($val) && isset($val[$k]) ? $val[$k] : null;
|
||||||
|
if ($val === null) break;
|
||||||
|
}
|
||||||
|
echo " " . implode('.', $path) . " = " . (is_scalar($val) ? var_export($val, true) : '(' . gettype($val) . ')') . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has Translation Management features ?
|
||||||
|
echo "\nFeature detection :\n";
|
||||||
|
echo " class TranslationManagement exists : " . (class_exists('TranslationManagement') ? 'yes' : 'no') . "\n";
|
||||||
|
echo " class WPML_TM_Loader exists : " . (class_exists('WPML_TM_Loader') ? 'yes' : 'no') . "\n";
|
||||||
|
echo " class WPML_String_Translation exists : " . (class_exists('WPML_String_Translation') ? 'yes' : 'no') . "\n";
|
||||||
|
echo " WPML_TM_VERSION defined : " . (defined('WPML_TM_VERSION') ? WPML_TM_VERSION : 'no') . "\n";
|
||||||
|
|
||||||
|
exit;
|
||||||
|
});
|
||||||
122
tools/av-wpml-push-translations.php
Normal file
122
tools/av-wpml-push-translations.php
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Push dict translations into WPML's icl_strings (so ATE shows them)
|
||||||
|
* Description:
|
||||||
|
* /?av_wpml_strings_dump=1 → dump registered Bricks strings + their FR translation state
|
||||||
|
* /?av_wpml_strings_push=1 → for each EN string in icl_strings (Bricks context), if our
|
||||||
|
* av_fr_translation_dictionary() has a match, write the FR
|
||||||
|
* translation via icl_add_string_translation() with status=10.
|
||||||
|
*
|
||||||
|
* Goal : ATE shows our programmatic translations pre-filled when the linguist opens a page.
|
||||||
|
* Editing in ATE then updates icl_string_translations → WPML rebuilds the FR Bricks JSON.
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/_av-bricks-shared.php';
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) return;
|
||||||
|
|
||||||
|
if (! empty($_GET['av_wpml_strings_dump'])) av_wpml_strings_dump();
|
||||||
|
if (! empty($_GET['av_wpml_strings_push'])) av_wpml_strings_push();
|
||||||
|
});
|
||||||
|
|
||||||
|
function av_wpml_strings_dump() {
|
||||||
|
global $wpdb;
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
// 1) Count strings per context
|
||||||
|
$rows = $wpdb->get_results(
|
||||||
|
"SELECT context, COUNT(*) AS n FROM {$wpdb->prefix}icl_strings GROUP BY context ORDER BY n DESC"
|
||||||
|
);
|
||||||
|
echo "Strings registered (icl_strings) by context :\n";
|
||||||
|
foreach ($rows as $r) echo sprintf(" %-40s %d strings\n", $r->context, $r->n);
|
||||||
|
|
||||||
|
// 1b) Sample strings whose VALUE matches our content (find which context Bricks uses for page content)
|
||||||
|
echo "\nSearching for our content strings :\n";
|
||||||
|
foreach (['PROSERVE', 'Police training', 'Built for those', 'mini-case', 'Industries', 'Solutions'] as $needle) {
|
||||||
|
$found = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT context, name, LEFT(value, 70) AS preview FROM {$wpdb->prefix}icl_strings WHERE value LIKE %s LIMIT 3",
|
||||||
|
'%' . $wpdb->esc_like($needle) . '%'
|
||||||
|
));
|
||||||
|
echo " needle '$needle' : " . count($found) . " match(es)\n";
|
||||||
|
foreach ($found as $f) echo " context='{$f->context}' name='{$f->name}' value='{$f->preview}'\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Sample a few Bricks strings + their FR (context is just 'bricks', not per-page)
|
||||||
|
echo "\nSample Bricks strings (10 of 156) :\n";
|
||||||
|
$strings = $wpdb->get_results(
|
||||||
|
"SELECT id, name, LEFT(value, 80) AS preview, status FROM {$wpdb->prefix}icl_strings WHERE context='bricks' LIMIT 10"
|
||||||
|
);
|
||||||
|
foreach ($strings as $s) {
|
||||||
|
echo " id={$s->id} name={$s->name} status={$s->status}\n EN: {$s->preview}\n";
|
||||||
|
$fr = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT LEFT(value, 80) FROM {$wpdb->prefix}icl_string_translations WHERE string_id=%d AND language='fr' LIMIT 1",
|
||||||
|
$s->id
|
||||||
|
));
|
||||||
|
echo " FR: " . ($fr !== null ? $fr : '(no translation)') . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Count translations by language and status
|
||||||
|
$tr_rows = $wpdb->get_results(
|
||||||
|
"SELECT language, status, COUNT(*) AS n
|
||||||
|
FROM {$wpdb->prefix}icl_string_translations GROUP BY language, status"
|
||||||
|
);
|
||||||
|
echo "\nString translations summary :\n";
|
||||||
|
foreach ($tr_rows as $r) echo " lang={$r->language} status={$r->status} : {$r->n}\n";
|
||||||
|
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function av_wpml_strings_push() {
|
||||||
|
global $wpdb;
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
if (! function_exists('icl_add_string_translation')) {
|
||||||
|
echo "FAIL : icl_add_string_translation() not available\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger the dict to load (with all filter contributions from generators)
|
||||||
|
$dict = av_fr_translation_dictionary();
|
||||||
|
echo "Loaded FR dictionary : " . count($dict) . " EN→FR mappings\n\n";
|
||||||
|
|
||||||
|
// All Bricks page-content strings (per-page contexts : bricks-{POST_ID})
|
||||||
|
$strings = $wpdb->get_results(
|
||||||
|
"SELECT id, value, context FROM {$wpdb->prefix}icl_strings WHERE context LIKE 'bricks-%'"
|
||||||
|
);
|
||||||
|
echo "Found " . count($strings) . " EN strings registered for Bricks pages\n\n";
|
||||||
|
|
||||||
|
$matched = 0;
|
||||||
|
$unmatched = 0;
|
||||||
|
$written = 0;
|
||||||
|
foreach ($strings as $s) {
|
||||||
|
if (! isset($dict[$s->value])) {
|
||||||
|
$unmatched++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$matched++;
|
||||||
|
$fr = $dict[$s->value];
|
||||||
|
// ICL_TM_COMPLETE = 10
|
||||||
|
$result = icl_add_string_translation((int) $s->id, 'fr', $fr, 10);
|
||||||
|
if ($result) $written++;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Matched in dict : $matched\n";
|
||||||
|
echo "Written to icl_string_translations : $written\n";
|
||||||
|
echo "Unmatched (no FR in dict, will stay EN in ATE) : $unmatched\n";
|
||||||
|
|
||||||
|
if ($unmatched > 0 && $unmatched < 100) {
|
||||||
|
echo "\nSample unmatched EN strings (extend the dict to translate these) :\n";
|
||||||
|
$shown = 0;
|
||||||
|
foreach ($strings as $s) {
|
||||||
|
if (! isset($dict[$s->value]) && $shown < 15) {
|
||||||
|
echo " - " . substr($s->value, 0, 120) . (strlen($s->value) > 120 ? '...' : '') . "\n";
|
||||||
|
$shown++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nDONE. Refresh ATE — FR strings should be pre-filled.\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
61
tools/av-wpml-rescan.php
Normal file
61
tools/av-wpml-rescan.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Force WPML to rescan Bricks page strings
|
||||||
|
* Description: Pages created via $wpdb direct write (av-generate-industries.php) bypass
|
||||||
|
* Bricks's save_post hooks, so WPML never gets notified to extract translatable strings
|
||||||
|
* from _bricks_page_content_2. This mu-plugin re-fires the 'wpml_page_builder_register_strings'
|
||||||
|
* action manually so WPML's String Translation registers them.
|
||||||
|
*
|
||||||
|
* Usage :
|
||||||
|
* /?av_wpml_rescan=ID → rescan single post
|
||||||
|
* /?av_wpml_rescan_all=industries → rescan hub + 4 detail pages
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) return;
|
||||||
|
|
||||||
|
if (! empty($_GET['av_wpml_rescan'])) {
|
||||||
|
$id = (int) $_GET['av_wpml_rescan'];
|
||||||
|
av_wpml_rescan_one($id);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($_GET['av_wpml_rescan_all']) && $_GET['av_wpml_rescan_all'] === 'industries') {
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
$hub = get_page_by_path('industries');
|
||||||
|
if ($hub) av_wpml_rescan_one($hub->ID, false);
|
||||||
|
foreach (['police','special-forces','military','firefighters'] as $slug) {
|
||||||
|
$p = get_page_by_path("industries/$slug");
|
||||||
|
if ($p) av_wpml_rescan_one($p->ID, false);
|
||||||
|
}
|
||||||
|
echo "Done.";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function av_wpml_rescan_one($post_id, $output = true) {
|
||||||
|
$post = get_post($post_id);
|
||||||
|
if (! $post) {
|
||||||
|
if ($output) wp_die("post $post_id not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$package_data = [
|
||||||
|
'kind' => 'Bricks',
|
||||||
|
'name' => (string) $post->ID,
|
||||||
|
'post_id' => $post->ID,
|
||||||
|
'title' => "Bricks (ID {$post->ID})",
|
||||||
|
];
|
||||||
|
|
||||||
|
do_action('wpml_page_builder_register_strings', $post, $package_data);
|
||||||
|
|
||||||
|
if ($output) {
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
echo "Rescan triggered : post #$post_id (\"{$post->post_title}\")\n";
|
||||||
|
echo "WPML String Translation should now have the Bricks strings registered.\n";
|
||||||
|
echo "Return to ATE and reopen the translation — strings should appear.\n";
|
||||||
|
} else {
|
||||||
|
echo " rescanned #$post_id ({$post->post_title})\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
44
tools/av-wpml-set-cte.php
Normal file
44
tools/av-wpml-set-cte.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion — Switch WPML translation editor to Classic (CTE)
|
||||||
|
* /?av_wpml_set_cte=1 → updates icl_sitepress_settings.translation-management.doc_translation_method
|
||||||
|
* from 'ATE' to 1 (ICL_TM_TMETHOD_EDITOR = Classic Translation Editor).
|
||||||
|
*
|
||||||
|
* Idempotent. Reverse with ?av_wpml_set_ate=1.
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
add_action('template_redirect', function () {
|
||||||
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) return;
|
||||||
|
|
||||||
|
if (! empty($_GET['av_wpml_set_cte'])) av_wpml_set_editor(1); // ICL_TM_TMETHOD_EDITOR
|
||||||
|
if (! empty($_GET['av_wpml_set_ate'])) av_wpml_set_editor('ATE'); // ICL_TM_TMETHOD_ATE
|
||||||
|
});
|
||||||
|
|
||||||
|
function av_wpml_set_editor($method) {
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
$opts = get_option('icl_sitepress_settings');
|
||||||
|
if (! is_array($opts)) $opts = [];
|
||||||
|
if (! isset($opts['translation-management'])) $opts['translation-management'] = [];
|
||||||
|
|
||||||
|
$old = $opts['translation-management']['doc_translation_method'] ?? '(unset)';
|
||||||
|
$opts['translation-management']['doc_translation_method'] = $method;
|
||||||
|
|
||||||
|
// Also clear ate_enabled if switching away from ATE — keeps state coherent
|
||||||
|
if ($method !== 'ATE') {
|
||||||
|
if (isset($opts['translation-management']['ate'])) {
|
||||||
|
$opts['translation-management']['ate']['ate_enabled'] = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isset($opts['translation-management']['ate'])) {
|
||||||
|
$opts['translation-management']['ate']['ate_enabled'] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update_option('icl_sitepress_settings', $opts);
|
||||||
|
|
||||||
|
echo "WPML doc_translation_method : '" . var_export($old, true) . "' → " . var_export($method, true) . "\n";
|
||||||
|
echo "Refresh WPML → Settings → Translation Editor to verify the new selection.\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
125
tools/fresh-setup-mu-plugin.php
Normal file
125
tools/fresh-setup-mu-plugin.php
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Asterion Fresh Setup (one-shot)
|
||||||
|
* Description: Configures a brand-new LocalWP install for the Asterion VR project — sets siteurl/home, permalinks, activates Bricks child theme + WPML, configures Bricks postTypes. Self-deletes after success.
|
||||||
|
* Version: 0.1.0
|
||||||
|
*
|
||||||
|
* USAGE
|
||||||
|
* -----
|
||||||
|
* After deleting the old LocalWP site and creating a new "Asterion 2026":
|
||||||
|
* 1. Note the new site's HTTP port from LocalWP (likely 10004 again).
|
||||||
|
* 2. Re-create the Junction so the child theme lives in the LocalWP install:
|
||||||
|
* (run as admin from PowerShell)
|
||||||
|
* New-Item -ItemType Junction `
|
||||||
|
* -Path "<NEW_SITE_PATH>\app\public\wp-content\themes\asterion-bricks" `
|
||||||
|
* -Target "C:\ASTERION\GIT\WP2026\wp-content\themes\asterion-bricks"
|
||||||
|
* 3. Extract ThirdParty/bricks.X.Y.Z.zip into the LocalWP themes/ folder.
|
||||||
|
* 4. Extract ThirdParty/sitepress-multilingual-cms.X.Y.Z.zip into plugins/.
|
||||||
|
* 5. If the new port is NOT 10004, edit the AV_TARGET_HOME constant below.
|
||||||
|
* 6. Copy this file to <NEW_SITE_PATH>\app\public\wp-content\mu-plugins\.
|
||||||
|
* 7. Visit http://localhost:<port>/ once — the plugin runs and self-deletes.
|
||||||
|
*
|
||||||
|
* What it sets up
|
||||||
|
* ---------------
|
||||||
|
* - WP_HOME / WP_SITEURL options pointing at localhost:<port>
|
||||||
|
* - Permalinks /%postname%/
|
||||||
|
* - Activates Bricks parent + Asterion Bricks child theme (bricks-bricks-child)
|
||||||
|
* - Activates WPML core
|
||||||
|
* - Configures Bricks postTypes = ['page', 'post', 'case_study', 'scenario']
|
||||||
|
* - Sets the WP timezone to Europe/Paris and locale to en_US (default)
|
||||||
|
*
|
||||||
|
* What it does NOT do (do these manually in WP admin)
|
||||||
|
* ---------------------------------------------------
|
||||||
|
* - Bricks license activation (Bricks → License → paste your key)
|
||||||
|
* - WPML license + setup wizard (WPML → Setup → EN default + FR + directories)
|
||||||
|
* - Bricks Theme Styles (Bricks → Theme Styles → align with tokens.css)
|
||||||
|
* - Build of the Header / Footer / archive Bricks Templates
|
||||||
|
*
|
||||||
|
* @package AsterionBricks
|
||||||
|
* @since 0.2.0
|
||||||
|
*/
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
// Hardcode the target site URL. Adjust if LocalWP allocated a different port.
|
||||||
|
define('AV_TARGET_HOME', 'http://localhost:10004');
|
||||||
|
|
||||||
|
add_action('init', function () {
|
||||||
|
|
||||||
|
if (get_option('av_fresh_setup_done') === '1') {
|
||||||
|
$self = __FILE__;
|
||||||
|
add_action('shutdown', function () use ($self) {
|
||||||
|
if (file_exists($self)) {
|
||||||
|
@unlink($self);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$log = [];
|
||||||
|
|
||||||
|
// 1. Site URL + home (avoids the localhost:port → / 301 cache trap)
|
||||||
|
update_option('siteurl', AV_TARGET_HOME);
|
||||||
|
update_option('home', AV_TARGET_HOME);
|
||||||
|
$log[] = "Set siteurl/home to " . AV_TARGET_HOME;
|
||||||
|
|
||||||
|
// 2. Pretty permalinks (required for /customers/ /scenarios/ slugs and WPML)
|
||||||
|
if (get_option('permalink_structure') !== '/%postname%/') {
|
||||||
|
update_option('permalink_structure', '/%postname%/');
|
||||||
|
$log[] = "Set permalink_structure to /%postname%/";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Timezone + WP locale defaults
|
||||||
|
update_option('timezone_string', 'Europe/Paris');
|
||||||
|
if (get_locale() !== 'en_US') {
|
||||||
|
update_option('WPLANG', 'en_US');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Activate Bricks child theme (its parent Bricks must be present)
|
||||||
|
$themes = wp_get_themes();
|
||||||
|
if (isset($themes['bricks']) && isset($themes['asterion-bricks'])) {
|
||||||
|
if (get_option('stylesheet') !== 'asterion-bricks') {
|
||||||
|
switch_theme('asterion-bricks');
|
||||||
|
$log[] = "Switched theme to asterion-bricks (parent: bricks)";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$missing = [];
|
||||||
|
if (! isset($themes['bricks'])) $missing[] = 'bricks (parent)';
|
||||||
|
if (! isset($themes['asterion-bricks'])) $missing[] = 'asterion-bricks (child junction)';
|
||||||
|
$log[] = "Theme NOT switched. Missing: " . implode(', ', $missing);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Activate WPML core
|
||||||
|
if (! function_exists('is_plugin_active')) {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||||
|
}
|
||||||
|
$wpml_main = 'sitepress-multilingual-cms/sitepress.php';
|
||||||
|
if (file_exists(WP_PLUGIN_DIR . '/' . $wpml_main)) {
|
||||||
|
if (! is_plugin_active($wpml_main)) {
|
||||||
|
$result = activate_plugin($wpml_main, '', false, true);
|
||||||
|
$log[] = is_wp_error($result)
|
||||||
|
? 'WPML activation FAILED: ' . $result->get_error_message()
|
||||||
|
: 'WPML activated';
|
||||||
|
} else {
|
||||||
|
$log[] = 'WPML already active';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$log[] = "WPML not found at plugins/{$wpml_main}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Bricks: enable Page/Post/CPT in Builder access > postTypes
|
||||||
|
if (defined('BRICKS_DB_GLOBAL_SETTINGS')) {
|
||||||
|
$settings = get_option(BRICKS_DB_GLOBAL_SETTINGS, []);
|
||||||
|
$settings['postTypes'] = ['page', 'post', 'case_study', 'scenario'];
|
||||||
|
update_option(BRICKS_DB_GLOBAL_SETTINGS, $settings);
|
||||||
|
$log[] = "Bricks postTypes set to page/post/case_study/scenario";
|
||||||
|
} else {
|
||||||
|
$log[] = "Bricks not loaded yet — re-run by visiting the site once Bricks is active";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Flush rewrite rules so the new permalink structure takes effect
|
||||||
|
flush_rewrite_rules();
|
||||||
|
|
||||||
|
update_option('av_fresh_setup_done', '1', false);
|
||||||
|
update_option('av_fresh_setup_log', $log, false);
|
||||||
|
|
||||||
|
}, 50);
|
||||||
@@ -1,53 +1,84 @@
|
|||||||
# Asterion Bricks — Child theme
|
# Asterion Bricks — Child theme (minimalist, Bricks-first)
|
||||||
|
|
||||||
Custom WordPress child theme for **asterionvr.com**, built on top of [Bricks Builder](https://bricksbuilder.io). Tactical-cinematic design system for the PROSERVE XR training platform.
|
Custom WordPress child theme for **asterionvr.com**, built on top of [Bricks Builder](https://bricksbuilder.io). The whole site is rendered by Bricks Templates and Bricks-edited pages — this theme exists only to give Bricks a design system, GDPR-compliant fonts, and the project's Custom Post Types.
|
||||||
|
|
||||||
## Structure
|
## What this theme does
|
||||||
|
|
||||||
|
- 🎨 Loads `assets/css/tokens.css` on the public frontend AND inside the Bricks builder iframe — so every `--color-*`, `--space-*`, `--font-*` variable defined there is available to Bricks elements via the **CSS Class** field or **Custom CSS** panel.
|
||||||
|
- 🔤 Self-hosts Inter Variable (latin subset) for RGPD compliance, with a `<link rel="preload">` injected on every page.
|
||||||
|
- 📰 Registers two Custom Post Types — `case_study` (slug `/customers/`) and `scenario` (slug `/scenarios/`) — plus the shared `industry` taxonomy.
|
||||||
|
- 📐 Adds three image sizes : `av-hero` (2400×1350), `av-card` (800×600), `av-thumb` (400×300).
|
||||||
|
- 🌐 Ships an `asterion-bricks` text domain for future i18n.
|
||||||
|
|
||||||
|
## What this theme does NOT do (intentionally)
|
||||||
|
|
||||||
|
- ❌ No `header.php` / `footer.php` / `page.php` / `front-page.php` / `index.php`. Bricks Templates (Header / Footer / Single — Page) drive every screen.
|
||||||
|
- ❌ No Custom Page Templates (`templates/page-*.php`). Pages render through their Bricks data (`_bricks_page_content_2` post meta).
|
||||||
|
- ❌ No PHP renderers, Gutenberg block helpers, shortcodes. Content lives in Bricks data and WPML translations.
|
||||||
|
|
||||||
|
If you find yourself adding any of the above back, stop and revisit `RESTART-PLAN.md` at the repo root — there are usually better Bricks-native alternatives.
|
||||||
|
|
||||||
|
## File map
|
||||||
|
|
||||||
```
|
```
|
||||||
asterion-bricks/
|
asterion-bricks/
|
||||||
├── style.css # WP theme metadata only (CSS lives in assets/css/)
|
├── style.css # WP theme metadata only
|
||||||
├── functions.php # enqueues, theme support, includes
|
├── functions.php # enqueue tokens, preload font, theme support, CPT include
|
||||||
├── theme.json # Gutenberg color/typo palette
|
├── theme.json # Gutenberg color/typo palette aligned with tokens.css
|
||||||
├── assets/
|
├── README.md # this file
|
||||||
│ ├── css/
|
├── inc/
|
||||||
│ │ ├── tokens.css # design tokens — source of truth
|
│ └── cpt.php # case_study, scenario, industry taxonomy
|
||||||
│ │ ├── utilities.css # CSS reset + utility classes (.av-container, etc.)
|
└── assets/
|
||||||
│ │ └── components.css # component styles (buttons, cards, header, footer…)
|
├── css/
|
||||||
│ ├── js/
|
│ ├── tokens.css # design system source of truth (single file, big)
|
||||||
│ │ └── main.js # vanilla JS, defer-loaded
|
│ └── utilities.css # skip-link, sr-only, focus-visible, reduced motion
|
||||||
│ ├── fonts/ # self-hosted Inter (RGPD)
|
├── fonts/
|
||||||
│ └── img/ # static visuals
|
│ ├── Inter-Variable.woff2
|
||||||
├── templates/ # Bricks template JSON exports (versioned)
|
│ └── Inter-Variable-Italic.woff2
|
||||||
└── inc/
|
└── img/
|
||||||
├── cpt.php # custom post types: case_study, scenario
|
|
||||||
└── seo.php # Schema.org JSON-LD overrides
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Conventions
|
Total : ~10 files, < 700 lines of code (with tokens.css being the lion's share).
|
||||||
|
|
||||||
- **CSS prefix** : `av-` (asterion-vr) on every custom selector to avoid collision with Bricks classes.
|
## Where the content copy lives
|
||||||
- **BEM-light** naming : `.av-card`, `.av-card--feature`, `.av-card__icon`, `.av-card.is-loading`.
|
|
||||||
- **Zero hardcoded values** — every color, spacing, radius and duration goes through `tokens.css`.
|
The textual copy from the Strategy & Content PDF (page-by-page EN copy, deliverable D.1 to D.29) lives at the repo root in `/content-source/`. Those are PHP arrays keyed by page slug — they are not loaded at runtime. Their job is twofold:
|
||||||
- **JS** : vanilla, defer-loaded, prefer `IntersectionObserver` over scroll listeners.
|
- **Reference** when building a Bricks page : copy/paste the appropriate strings into the Bricks elements as you build.
|
||||||
- **A11y** : keyboard reachable, `:focus-visible` outline, ARIA where needed, contrast ≥ 4.5:1, `prefers-reduced-motion` respected.
|
- **Source for programmatic generation** when we replicate a Bricks-built archetype across its variants (e.g. the 4 industry detail pages, the 6 technology pillars).
|
||||||
|
|
||||||
## Activation
|
## Activation
|
||||||
|
|
||||||
1. Ensure Bricks parent is installed at `wp-content/themes/bricks/`.
|
1. Install Bricks parent theme (`wp-content/themes/bricks/`) — the ZIP lives in `ThirdParty/`.
|
||||||
2. WP Admin → Appearance → Themes → activate **Asterion Bricks**.
|
2. Install + activate WPML core (also in `ThirdParty/`).
|
||||||
|
3. WP Admin → Appearance → Themes → activate **Asterion Bricks**.
|
||||||
|
4. WP Admin → Bricks → Settings → check **Page** in « Builder access · Post types ».
|
||||||
|
|
||||||
## Performance budget (per BRIEF section 9)
|
## Tokens to know
|
||||||
|
|
||||||
| Asset | Budget |
|
The full design system lives in `assets/css/tokens.css`. The most-referenced tokens:
|
||||||
|-----------------|-------------------------------|
|
|
||||||
| HTML initial | < 100 KB gzipped |
|
| Variable | Value |
|
||||||
| CSS total | < 50 KB gzipped |
|
|---|---|
|
||||||
| JS initial | < 200 KB gzipped |
|
| `--color-brand-navy` | `#0B1F3A` |
|
||||||
| Hero video | < 2 MB |
|
| `--color-brand-gold` | `#C9A45A` |
|
||||||
| Hero poster | < 150 KB |
|
| `--color-text-primary` | `#0B1F3A` |
|
||||||
| LCP / CLS / TBT | < 2.5s / < 0.1 / < 200ms |
|
| `--font-display` | Inter Display fallback to Inter |
|
||||||
|
| `--font-body` | Inter |
|
||||||
|
| `--space-1..32` | 8-pt grid (4px → 128px) |
|
||||||
|
| `--container-max` | 1280px |
|
||||||
|
| `--ease-default` | `cubic-bezier(0.16, 1, 0.3, 1)` |
|
||||||
|
| `--shadow-sm`/`--shadow-md`/`--shadow-lg` | navy-tinted elevation |
|
||||||
|
|
||||||
|
Reference them inside Bricks via Custom CSS:
|
||||||
|
|
||||||
|
```css
|
||||||
|
%root% {
|
||||||
|
background: var(--color-brand-navy);
|
||||||
|
color: var(--color-text-on-dark);
|
||||||
|
padding-block: var(--space-20);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
GPL v2 or later. Same as WordPress core.
|
GPL v2 or later, matching WordPress core.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,110 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* Asterion Bricks — Utilities
|
* Asterion Bricks — Utilities (minimalist)
|
||||||
*
|
*
|
||||||
* Modern CSS reset + reusable utility classes.
|
* Loaded after tokens.css. Holds the small set of helper classes Bricks
|
||||||
* Loaded after tokens.css, before components.css.
|
* Builder cannot easily provide on its own (skip link, screen-reader-only
|
||||||
*
|
* text, focus ring). Everything else — section padding, container widths,
|
||||||
* Naming : prefix `av-` on every custom class.
|
* button design, card styles — is set inside Bricks via tokens.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* ============================================================================
|
/* === A11y skip link — first focusable element on every page === */
|
||||||
Modern CSS reset (inspired by Andy Bell's modern-css-reset, Josh Comeau)
|
|
||||||
============================================================================ */
|
|
||||||
|
|
||||||
*, *::before, *::after { box-sizing: border-box; }
|
|
||||||
|
|
||||||
* { margin: 0; padding: 0; }
|
|
||||||
|
|
||||||
html {
|
|
||||||
font-size: 100%;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: var(--font-body);
|
|
||||||
font-size: var(--text-body-md);
|
|
||||||
line-height: var(--leading-body-md);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
background-color: var(--color-bg-default);
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
img, picture, video, canvas, svg {
|
|
||||||
display: block;
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
input, button, textarea, select { font: inherit; color: inherit; }
|
|
||||||
|
|
||||||
p, h1, h2, h3, h4, h5, h6 { overflow-wrap: break-word; }
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul, ol { list-style: none; }
|
|
||||||
|
|
||||||
table { border-collapse: collapse; border-spacing: 0; }
|
|
||||||
|
|
||||||
/* ============================================================================
|
|
||||||
Heading hierarchy — opinionated defaults consumed when an element
|
|
||||||
carries no Bricks-specific class. Bricks may override, that's expected.
|
|
||||||
============================================================================ */
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: var(--text-h1);
|
|
||||||
line-height: var(--leading-h1);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
letter-spacing: var(--tracking-snug);
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: var(--text-h2);
|
|
||||||
line-height: var(--leading-h2);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
letter-spacing: var(--tracking-snug);
|
|
||||||
}
|
|
||||||
h3 {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: var(--text-h3);
|
|
||||||
line-height: var(--leading-h3);
|
|
||||||
font-weight: var(--font-weight-semibold);
|
|
||||||
letter-spacing: var(--tracking-snug);
|
|
||||||
}
|
|
||||||
h4 {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: var(--text-h4);
|
|
||||||
line-height: var(--leading-h4);
|
|
||||||
font-weight: var(--font-weight-semibold);
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
max-width: 72ch; /* prose readability per Design Handoff section 2.2 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================================
|
|
||||||
Focus visible — keyboard accessibility
|
|
||||||
Removes outline only when focus came from the mouse; preserves it for keyboard.
|
|
||||||
============================================================================ */
|
|
||||||
|
|
||||||
:focus { outline: none; }
|
|
||||||
|
|
||||||
:focus-visible {
|
|
||||||
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
||||||
outline-offset: var(--focus-ring-offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================================
|
|
||||||
Skip link — first focusable element on every page (a11y)
|
|
||||||
============================================================================ */
|
|
||||||
|
|
||||||
.av-skip-link {
|
.av-skip-link {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -40px;
|
top: -40px;
|
||||||
@@ -114,6 +17,7 @@ p {
|
|||||||
color: var(--color-text-on-dark);
|
color: var(--color-text-on-dark);
|
||||||
font-size: var(--text-body-sm);
|
font-size: var(--text-body-sm);
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: var(--font-weight-semibold);
|
||||||
|
text-decoration: none;
|
||||||
z-index: var(--z-toast);
|
z-index: var(--z-toast);
|
||||||
border-bottom-right-radius: var(--radius-md);
|
border-bottom-right-radius: var(--radius-md);
|
||||||
}
|
}
|
||||||
@@ -122,10 +26,7 @@ p {
|
|||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================================
|
/* === Visually hidden — readable by screen readers, hidden visually === */
|
||||||
Visually hidden — content readable by screen readers, hidden visually
|
|
||||||
============================================================================ */
|
|
||||||
|
|
||||||
.av-sr-only {
|
.av-sr-only {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
@@ -138,77 +39,32 @@ p {
|
|||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================================
|
/* === Focus visible — keyboard accessibility on every focusable element === */
|
||||||
Container — centred max-width with responsive padding
|
:focus { outline: none; }
|
||||||
============================================================================ */
|
:focus-visible {
|
||||||
|
outline: var(--focus-ring-width, 2px) solid var(--focus-ring-color, var(--color-info));
|
||||||
.av-container {
|
outline-offset: var(--focus-ring-offset, 2px);
|
||||||
max-width: var(--container-max);
|
|
||||||
margin-inline: auto;
|
|
||||||
padding-inline: var(--container-padding-mobile);
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
/* === Touch target minimum (WCAG AA / Apple HIG) === */
|
||||||
.av-container { padding-inline: var(--container-padding-tablet); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
|
||||||
.av-container { padding-inline: var(--container-padding-desktop); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.av-container--narrow {
|
|
||||||
max-width: 800px; /* article body */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================================
|
|
||||||
Tap target minimum — WCAG AA / Apple HIG (44×44px)
|
|
||||||
============================================================================ */
|
|
||||||
|
|
||||||
.av-tap-target {
|
.av-tap-target {
|
||||||
min-width: var(--tap-target-min);
|
min-width: var(--tap-target-min, 44px);
|
||||||
min-height: var(--tap-target-min);
|
min-height: var(--tap-target-min, 44px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================================
|
/* === Reduced motion — respect user preference === */
|
||||||
Stack helpers — vertical rhythm via owl-selector
|
@media (prefers-reduced-motion: reduce) {
|
||||||
============================================================================ */
|
*, *::before, *::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
.av-stack > * + * { margin-top: var(--av-stack-gap, var(--space-4)); }
|
animation-iteration-count: 1 !important;
|
||||||
.av-stack--sm > * + * { margin-top: var(--space-2); }
|
transition-duration: 0.01ms !important;
|
||||||
.av-stack--md > * + * { margin-top: var(--space-4); }
|
scroll-behavior: auto !important;
|
||||||
.av-stack--lg > * + * { margin-top: var(--space-8); }
|
|
||||||
.av-stack--xl > * + * { margin-top: var(--space-12); }
|
|
||||||
|
|
||||||
/* ============================================================================
|
|
||||||
Section padding helpers (vertical rhythm between page sections)
|
|
||||||
============================================================================ */
|
|
||||||
|
|
||||||
.av-section {
|
|
||||||
padding-block: var(--space-16);
|
|
||||||
}
|
}
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.av-section { padding-block: var(--space-20); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.av-section--hero {
|
/*
|
||||||
padding-block: var(--space-24);
|
* Section padding & flex-basis responsive overrides are now handled natively
|
||||||
}
|
* by Bricks via per-breakpoint settings on each element (suffix `:tablet_portrait`,
|
||||||
@media (min-width: 1024px) {
|
* `:mobile_landscape`, `:mobile_portrait` on the setting key). The page generator
|
||||||
.av-section--hero { padding-block: var(--space-32); }
|
* emits those automatically — no custom CSS needed here.
|
||||||
}
|
*/
|
||||||
|
|
||||||
.av-section--dark {
|
|
||||||
background-color: var(--color-bg-dark);
|
|
||||||
color: var(--color-text-on-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================================
|
|
||||||
Print
|
|
||||||
============================================================================ */
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
.av-skip-link, .av-no-print { display: none !important; }
|
|
||||||
body { color: #000; background: #fff; }
|
|
||||||
a::after { content: " (" attr(href) ")"; font-size: 0.8em; }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
/**
|
|
||||||
* Asterion Bricks — main JS
|
|
||||||
*
|
|
||||||
* Vanilla JS, defer-loaded. Wraps everything in DOMContentLoaded.
|
|
||||||
* Performance budget : initial bundle < 200 KB gzipped.
|
|
||||||
*
|
|
||||||
* Modules planned :
|
|
||||||
* - Header scroll behavior (background swap after 80px)
|
|
||||||
* - Mega menu trigger (hover desktop, click touch, ESC closes)
|
|
||||||
* - Mobile drawer (full-screen overlay, accordion sections)
|
|
||||||
* - Scroll-triggered fade-in (IntersectionObserver, threshold 0.15)
|
|
||||||
* - Stat callouts count-up (1000ms ease-out)
|
|
||||||
* - Hero video lazy-load + reduced-motion fallback to poster
|
|
||||||
*/
|
|
||||||
|
|
||||||
(function () {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect reduced motion once and expose a flag for downstream modules.
|
|
||||||
* Updated reactively if the user toggles the setting mid-session.
|
|
||||||
*/
|
|
||||||
const reducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
||||||
let prefersReducedMotion = reducedMotionQuery.matches;
|
|
||||||
reducedMotionQuery.addEventListener('change', (e) => {
|
|
||||||
prefersReducedMotion = e.matches;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Header scroll behavior.
|
|
||||||
* After 80px scroll, header gets a `is-scrolled` class
|
|
||||||
* (consumed by .av-header.is-scrolled rule in components.css).
|
|
||||||
*/
|
|
||||||
function initHeaderScroll() {
|
|
||||||
const header = document.querySelector('.av-header');
|
|
||||||
if (!header) return;
|
|
||||||
|
|
||||||
let ticking = false;
|
|
||||||
const onScroll = () => {
|
|
||||||
if (!ticking) {
|
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
header.classList.toggle('is-scrolled', window.scrollY > 80);
|
|
||||||
ticking = false;
|
|
||||||
});
|
|
||||||
ticking = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
onScroll();
|
|
||||||
window.addEventListener('scroll', onScroll, { passive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scroll-triggered fade-in (Design Handoff section 5.3).
|
|
||||||
* Sections fade-in + slide-up 16px on entry, threshold 0.15, trigger once.
|
|
||||||
* Skipped entirely if user prefers reduced motion.
|
|
||||||
*/
|
|
||||||
function initScrollReveal() {
|
|
||||||
if (prefersReducedMotion) return;
|
|
||||||
|
|
||||||
const targets = document.querySelectorAll('[data-av-reveal]');
|
|
||||||
if (!targets.length) return;
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
entry.target.classList.add('is-revealed');
|
|
||||||
observer.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, { threshold: 0.15, rootMargin: '0px 0px -10% 0px' });
|
|
||||||
|
|
||||||
targets.forEach((target) => observer.observe(target));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stat count-up (Design Handoff section 5.3).
|
|
||||||
* Element : <span data-av-countup="0|target|duration">target</span>
|
|
||||||
* Animates from 0 to target value over duration ms, ease-out.
|
|
||||||
*/
|
|
||||||
function initStatCountUp() {
|
|
||||||
if (prefersReducedMotion) return;
|
|
||||||
|
|
||||||
const stats = document.querySelectorAll('[data-av-countup]');
|
|
||||||
if (!stats.length) return;
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (!entry.isIntersecting) return;
|
|
||||||
const el = entry.target;
|
|
||||||
const target = parseFloat(el.dataset.avCountup) || 0;
|
|
||||||
const duration = parseInt(el.dataset.avCountupDuration || '1000', 10);
|
|
||||||
const start = performance.now();
|
|
||||||
|
|
||||||
const tick = (now) => {
|
|
||||||
const elapsed = Math.min((now - start) / duration, 1);
|
|
||||||
// ease-out cubic
|
|
||||||
const eased = 1 - Math.pow(1 - elapsed, 3);
|
|
||||||
el.textContent = Math.round(target * eased).toLocaleString();
|
|
||||||
if (elapsed < 1) requestAnimationFrame(tick);
|
|
||||||
};
|
|
||||||
requestAnimationFrame(tick);
|
|
||||||
observer.unobserve(el);
|
|
||||||
});
|
|
||||||
}, { threshold: 0.5 });
|
|
||||||
|
|
||||||
stats.forEach((s) => observer.observe(s));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mobile drawer toggle.
|
|
||||||
* Trigger : <button class="av-drawer-toggle" aria-controls="av-drawer">
|
|
||||||
* Drawer : <div id="av-drawer" class="av-drawer">
|
|
||||||
*/
|
|
||||||
function initMobileDrawer() {
|
|
||||||
const toggle = document.querySelector('.av-drawer-toggle');
|
|
||||||
const drawer = document.getElementById('av-drawer');
|
|
||||||
if (!toggle || !drawer) return;
|
|
||||||
|
|
||||||
const open = () => {
|
|
||||||
drawer.classList.add('is-open');
|
|
||||||
toggle.setAttribute('aria-expanded', 'true');
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
};
|
|
||||||
const close = () => {
|
|
||||||
drawer.classList.remove('is-open');
|
|
||||||
toggle.setAttribute('aria-expanded', 'false');
|
|
||||||
document.body.style.overflow = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
toggle.addEventListener('click', () => {
|
|
||||||
drawer.classList.contains('is-open') ? close() : open();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Escape' && drawer.classList.contains('is-open')) close();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Boot.
|
|
||||||
*/
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
initHeaderScroll();
|
|
||||||
initScrollReveal();
|
|
||||||
initStatCountUp();
|
|
||||||
initMobileDrawer();
|
|
||||||
});
|
|
||||||
|
|
||||||
})();
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — Global footer
|
|
||||||
*
|
|
||||||
* Per Design Handoff section 3.5 :
|
|
||||||
* - 5 columns desktop : Solutions / Industries / Why Asterion / Resources / Newsletter
|
|
||||||
* - Stack on mobile (could become accordions later)
|
|
||||||
* - Bottom strip : copyright · privacy · terms · cookies · social · language switcher
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
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 -->
|
|
||||||
|
|
||||||
<footer class="av-footer" role="contentinfo">
|
|
||||||
<div class="av-container">
|
|
||||||
|
|
||||||
<div class="av-footer__cols">
|
|
||||||
|
|
||||||
<div class="av-footer__brand">
|
|
||||||
<a href="<?php echo esc_url(home_url('/')); ?>" class="av-header__logo" aria-label="Asterion VR — home">
|
|
||||||
ASTERION VR
|
|
||||||
</a>
|
|
||||||
<p>
|
|
||||||
<?php echo esc_html(av_t('The modular, sovereign XR platform for European security forces. Train like the threat is now.', 'asterion-bricks')); ?>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="av-footer__col">
|
|
||||||
<h2 class="av-footer__col-title"><?php echo esc_html(av_t('Solutions', 'asterion-bricks')); ?></h2>
|
|
||||||
<ul class="av-footer__list">
|
|
||||||
<li><a href="<?php echo esc_url(home_url('/solutions/my-proserve/')); ?>">MY PROSERVE</a></li>
|
|
||||||
<li><a href="<?php echo esc_url(home_url('/solutions/proserve-flex/')); ?>">PROSERVE FLEX</a></li>
|
|
||||||
<li><a href="<?php echo esc_url(home_url('/solutions/proserve-academy/')); ?>">PROSERVE ACADEMY</a></li>
|
|
||||||
<li><a href="<?php echo esc_url(home_url('/solutions/customization/')); ?>">PROSERVE+</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="av-footer__col">
|
|
||||||
<h2 class="av-footer__col-title"><?php echo esc_html(av_t('Industries', 'asterion-bricks')); ?></h2>
|
|
||||||
<ul class="av-footer__list">
|
|
||||||
<li><a href="<?php echo esc_url(home_url('/industries/police/')); ?>"><?php echo esc_html(av_t('Police', 'asterion-bricks')); ?></a></li>
|
|
||||||
<li><a href="<?php echo esc_url(home_url('/industries/special-forces/')); ?>"><?php echo esc_html(av_t('Special Forces', 'asterion-bricks')); ?></a></li>
|
|
||||||
<li><a href="<?php echo esc_url(home_url('/industries/military/')); ?>"><?php echo esc_html(av_t('Military', 'asterion-bricks')); ?></a></li>
|
|
||||||
<li><a href="<?php echo esc_url(home_url('/industries/firefighters/')); ?>"><?php echo esc_html(av_t('Firefighters', 'asterion-bricks')); ?></a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="av-footer__col">
|
|
||||||
<h2 class="av-footer__col-title"><?php echo esc_html(av_t('Why Asterion', 'asterion-bricks')); ?></h2>
|
|
||||||
<ul class="av-footer__list">
|
|
||||||
<li><a href="<?php echo esc_url(home_url('/about/')); ?>"><?php echo esc_html(av_t('Our story', 'asterion-bricks')); ?></a></li>
|
|
||||||
<li><a href="<?php echo esc_url(home_url('/customers/')); ?>"><?php echo esc_html(av_t('Customers', 'asterion-bricks')); ?></a></li>
|
|
||||||
<li><a href="<?php echo esc_url(home_url('/trust/')); ?>"><?php echo esc_html(av_t('Trust & Compliance', 'asterion-bricks')); ?></a></li>
|
|
||||||
<li><a href="<?php echo esc_url(home_url('/partners/')); ?>"><?php echo esc_html(av_t('Partners', 'asterion-bricks')); ?></a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="av-footer__newsletter">
|
|
||||||
<h2 class="av-footer__col-title"><?php echo esc_html(av_t('Newsletter', 'asterion-bricks')); ?></h2>
|
|
||||||
<p style="color:var(--color-text-on-dark-muted);font-size:var(--text-body-sm);">
|
|
||||||
<?php echo esc_html(av_t('Insights from the front of immersive training. One email per month. No spam.', 'asterion-bricks')); ?>
|
|
||||||
</p>
|
|
||||||
<form class="av-footer__newsletter-form" method="post" action="#" aria-label="<?php esc_attr_e('Newsletter signup', 'asterion-bricks'); ?>">
|
|
||||||
<label for="av-newsletter-email" class="av-sr-only"><?php echo esc_html(av_t('Your email', 'asterion-bricks')); ?></label>
|
|
||||||
<input type="email" id="av-newsletter-email" name="email" class="av-input" placeholder="<?php esc_attr_e('your@email.com', 'asterion-bricks'); ?>" required>
|
|
||||||
<button type="submit" class="av-btn av-btn--primary av-btn--md"><?php echo esc_html(av_t('Subscribe', 'asterion-bricks')); ?></button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="av-footer__bottom">
|
|
||||||
<div>© <?php echo esc_html($av_year); ?> Asterion VR. <?php echo esc_html(av_t('All rights reserved.', 'asterion-bricks')); ?></div>
|
|
||||||
<div class="av-footer__bottom-links">
|
|
||||||
<a href="<?php echo esc_url(home_url('/legal/privacy/')); ?>"><?php echo esc_html(av_t('Privacy', 'asterion-bricks')); ?></a>
|
|
||||||
<a href="<?php echo esc_url(home_url('/legal/terms/')); ?>"><?php echo esc_html(av_t('Terms', 'asterion-bricks')); ?></a>
|
|
||||||
<a href="<?php echo esc_url(home_url('/legal/cookies/')); ?>"><?php echo esc_html(av_t('Cookies', 'asterion-bricks')); ?></a>
|
|
||||||
<a href="<?php echo esc_url(home_url('/legal/notice/')); ?>"><?php echo esc_html(av_t('Legal notice', 'asterion-bricks')); ?></a>
|
|
||||||
</div>
|
|
||||||
<div class="av-footer__social">
|
|
||||||
<a href="https://www.linkedin.com/company/asterion-vr/" rel="noopener" aria-label="LinkedIn">LinkedIn</a>
|
|
||||||
·
|
|
||||||
<a href="https://www.youtube.com/" rel="noopener" aria-label="YouTube">YouTube</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<?php wp_footer(); ?>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,377 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — Front page (home)
|
|
||||||
*
|
|
||||||
* Implements the 9-section home wireframe from the Design Handoff
|
|
||||||
* (section 4.1) using the EN copy from Strategy & Content (D.1).
|
|
||||||
*
|
|
||||||
* 1. Hero — dark, full-bleed video, signature
|
|
||||||
* 2. Trust bar — sector marks + 4 inline metrics
|
|
||||||
* 3. Solutions — 3 cards (MY / FLEX / ACADEMY)
|
|
||||||
* 4. Industries — 4 cards 2x2 (Police / SF / Military / Firefighters)
|
|
||||||
* 5. Feature heroes — 3 alternating (UWeD / Instructor Cockpit / AAR)
|
|
||||||
* 6. Featured case — single highlighted case
|
|
||||||
* 7. Testimonials — 3 quotes (slider becomes JS-driven later)
|
|
||||||
* 8. Latest insights — 3 blog cards
|
|
||||||
* 9. Conversion banner — Demo / T&E / Quote
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
|
|
||||||
$home_url = home_url('/');
|
|
||||||
$theme = get_stylesheet_directory_uri();
|
|
||||||
?>
|
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<!-- 1. HERO -->
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<section class="av-hero" aria-labelledby="hero-headline">
|
|
||||||
<div class="av-hero__media" aria-hidden="true">
|
|
||||||
<?php /* Hero video placeholder. Replace with a 25-35s loop, MP4 H.264 + WebM, < 2 MB, per Design Handoff 5.4. */ ?>
|
|
||||||
<div style="width:100%;height:100%;background:
|
|
||||||
radial-gradient(ellipse at 20% 30%, rgba(201,164,90,0.15), transparent 60%),
|
|
||||||
radial-gradient(ellipse at 80% 70%, rgba(30,91,168,0.20), transparent 60%),
|
|
||||||
linear-gradient(135deg, #081427 0%, #0B1F3A 50%, #13294B 100%);"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-hero__inner">
|
|
||||||
<p class="av-hero__eyebrow"><?php esc_html_e('Modular XR training for security forces', 'asterion-bricks'); ?></p>
|
|
||||||
<h1 id="hero-headline" class="av-hero__headline">Your Last Mistake Should Be in VR.</h1>
|
|
||||||
<p class="av-hero__sub">
|
|
||||||
PROSERVE™ is the modular, fully portable XR platform built for police, special forces, military and firefighters. Deploy in five minutes. Train anywhere. Debrief everything.
|
|
||||||
</p>
|
|
||||||
<div class="av-hero__ctas">
|
|
||||||
<a href="<?php echo esc_url($home_url . 'request-demo/'); ?>" class="av-btn av-btn--primary av-btn--xl"><?php esc_html_e('Request a Demo', 'asterion-bricks'); ?></a>
|
|
||||||
<a href="#feature-heroes" class="av-btn av-btn--on-dark av-btn--secondary av-btn--xl"><?php esc_html_e('Watch the 90-second film', 'asterion-bricks'); ?></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<!-- 2. TRUST BAR -->
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<section class="av-logo-strip" aria-label="<?php esc_attr_e('Trusted by', 'asterion-bricks'); ?>">
|
|
||||||
<div class="av-container">
|
|
||||||
<p class="av-logo-strip__intro"><?php esc_html_e('Trusted by security forces across Europe', 'asterion-bricks'); ?></p>
|
|
||||||
<div class="av-logo-strip__row">
|
|
||||||
<?php /* Real client logos pending NDA confirmation — see MISSING-ASSETS.md. */ ?>
|
|
||||||
<?php for ($i = 1; $i <= 6; $i++) : ?>
|
|
||||||
<div class="av-logo-strip__logo">Logo <?php echo (int) $i; ?></div>
|
|
||||||
<?php endfor; ?>
|
|
||||||
</div>
|
|
||||||
<div class="av-logo-strip__metrics">
|
|
||||||
<div class="av-logo-strip__metric"><?php esc_html_e('10 years of XR research for security forces', 'asterion-bricks'); ?></div>
|
|
||||||
<div class="av-logo-strip__metric"><?php esc_html_e('11 environments · 50+ scenarios · monthly updates', 'asterion-bricks'); ?></div>
|
|
||||||
<div class="av-logo-strip__metric"><?php esc_html_e('< 5 minutes from case to active training', 'asterion-bricks'); ?></div>
|
|
||||||
<div class="av-logo-strip__metric"><?php esc_html_e('Made in France · GDPR-native · works fully offline', 'asterion-bricks'); ?></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<!-- 3. SOLUTIONS -->
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<section class="av-section av-section--dark" aria-labelledby="solutions-title" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-section__head">
|
|
||||||
<span class="av-eyebrow av-eyebrow--gold"><?php esc_html_e('Solutions', 'asterion-bricks'); ?></span>
|
|
||||||
<h2 id="solutions-title" class="av-section__title">Three solutions. One platform. Zero compromise.</h2>
|
|
||||||
<p class="av-section__lead">
|
|
||||||
From the lone officer rebuilding muscle memory to a 16-trainee unit running a coordinated assault — PROSERVE scales with your mission, not the other way around.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="av-grid-3">
|
|
||||||
|
|
||||||
<article class="av-card is-on-dark">
|
|
||||||
<div class="av-card__media" style="background:linear-gradient(135deg,#13294B,#0B1F3A);"></div>
|
|
||||||
<div class="av-card__body">
|
|
||||||
<span class="av-card__eyebrow"><?php esc_html_e('Solo training', 'asterion-bricks'); ?></span>
|
|
||||||
<h3 class="av-card__title">MY PROSERVE</h3>
|
|
||||||
<p class="av-card__excerpt">
|
|
||||||
Mini-case. Anywhere, anytime. Built for the officer who needs to train 30 minutes between two shifts. 9 m² of free space, a headset, a real-recoil replica. The legal minimum becomes the daily standard.
|
|
||||||
</p>
|
|
||||||
<div class="av-card__cta">
|
|
||||||
<a href="<?php echo esc_url($home_url . 'solutions/my-proserve/'); ?>" class="av-link av-link--on-dark av-link--with-arrow"><?php esc_html_e('Discover MY PROSERVE', 'asterion-bricks'); ?></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="av-card is-on-dark">
|
|
||||||
<div class="av-card__media" style="background:linear-gradient(135deg,#13294B,#0B1F3A);"></div>
|
|
||||||
<div class="av-card__body">
|
|
||||||
<span class="av-card__eyebrow"><?php esc_html_e('Team training · 1 to 5', 'asterion-bricks'); ?></span>
|
|
||||||
<h3 class="av-card__title">PROSERVE FLEX</h3>
|
|
||||||
<p class="av-card__excerpt">
|
|
||||||
One case per operator. Multi-user cooperative scenarios, instructor PC, full AAR. Deploy on any flat surface from 9 to 100 m². For tactical units that move with their training.
|
|
||||||
</p>
|
|
||||||
<div class="av-card__cta">
|
|
||||||
<a href="<?php echo esc_url($home_url . 'solutions/proserve-flex/'); ?>" class="av-link av-link--on-dark av-link--with-arrow"><?php esc_html_e('Discover PROSERVE FLEX', 'asterion-bricks'); ?></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="av-card is-on-dark">
|
|
||||||
<div class="av-card__media" style="background:linear-gradient(135deg,#13294B,#0B1F3A);"></div>
|
|
||||||
<div class="av-card__body">
|
|
||||||
<span class="av-card__eyebrow"><?php esc_html_e('Unit training · 1 to 16', 'asterion-bricks'); ?></span>
|
|
||||||
<h3 class="av-card__title">PROSERVE ACADEMY</h3>
|
|
||||||
<p class="av-card__excerpt">
|
|
||||||
The full training centre, in your facility. Up to 900 m² of free-roam space, instructor cockpit, racked compute, parallel sessions. Built for academies and continuous training programs.
|
|
||||||
</p>
|
|
||||||
<div class="av-card__cta">
|
|
||||||
<a href="<?php echo esc_url($home_url . 'solutions/proserve-academy/'); ?>" class="av-link av-link--on-dark av-link--with-arrow"><?php esc_html_e('Discover PROSERVE ACADEMY', 'asterion-bricks'); ?></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<!-- 4. INDUSTRIES -->
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<section class="av-section" aria-labelledby="industries-title" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-section__head">
|
|
||||||
<span class="av-eyebrow"><?php esc_html_e('Industries', 'asterion-bricks'); ?></span>
|
|
||||||
<h2 id="industries-title" class="av-section__title">Built with operators, for operators.</h2>
|
|
||||||
<p class="av-section__lead">
|
|
||||||
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.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="av-grid-4" style="grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));">
|
|
||||||
|
|
||||||
<article class="av-card">
|
|
||||||
<div class="av-card__media" style="background:linear-gradient(135deg,#1F3252,#0B1F3A);"></div>
|
|
||||||
<div class="av-card__body">
|
|
||||||
<span class="av-card__eyebrow"><?php esc_html_e('National & Municipal', 'asterion-bricks'); ?></span>
|
|
||||||
<h3 class="av-card__title"><?php esc_html_e('Police', 'asterion-bricks'); ?></h3>
|
|
||||||
<p class="av-card__excerpt">Vehicle stops, identity checks, urban CQB, de-escalation, riot containment. Built for the realities of contemporary policing.</p>
|
|
||||||
<div class="av-card__cta"><a href="<?php echo esc_url($home_url . 'industries/police/'); ?>" class="av-link av-link--with-arrow"><?php esc_html_e('Police training', 'asterion-bricks'); ?></a></div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="av-card">
|
|
||||||
<div class="av-card__media" style="background:linear-gradient(135deg,#1F3252,#0B1F3A);"></div>
|
|
||||||
<div class="av-card__body">
|
|
||||||
<span class="av-card__eyebrow"><?php esc_html_e('Tactical Units', 'asterion-bricks'); ?></span>
|
|
||||||
<h3 class="av-card__title"><?php esc_html_e('Special Forces', 'asterion-bricks'); ?></h3>
|
|
||||||
<p class="av-card__excerpt">Hostage rescue, dynamic entries, hostile crowd extraction, sniper-spotter coordination. Built for the units where every error has a name.</p>
|
|
||||||
<div class="av-card__cta"><a href="<?php echo esc_url($home_url . 'industries/special-forces/'); ?>" class="av-link av-link--with-arrow"><?php esc_html_e('Special forces training', 'asterion-bricks'); ?></a></div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="av-card">
|
|
||||||
<div class="av-card__media" style="background:linear-gradient(135deg,#1F3252,#0B1F3A);"></div>
|
|
||||||
<div class="av-card__body">
|
|
||||||
<span class="av-card__eyebrow"><?php esc_html_e('Armed forces', 'asterion-bricks'); ?></span>
|
|
||||||
<h3 class="av-card__title"><?php esc_html_e('Military', 'asterion-bricks'); ?></h3>
|
|
||||||
<p class="av-card__excerpt">MOUT, convoy escort, FOB defense, IED awareness, joint operation drills. Built for armed forces that deploy fast and adapt faster.</p>
|
|
||||||
<div class="av-card__cta"><a href="<?php echo esc_url($home_url . 'industries/military/'); ?>" class="av-link av-link--with-arrow"><?php esc_html_e('Military training', 'asterion-bricks'); ?></a></div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="av-card">
|
|
||||||
<div class="av-card__media" style="background:linear-gradient(135deg,#1F3252,#0B1F3A);"></div>
|
|
||||||
<div class="av-card__body">
|
|
||||||
<span class="av-card__eyebrow"><?php esc_html_e('Emergency services', 'asterion-bricks'); ?></span>
|
|
||||||
<h3 class="av-card__title"><?php esc_html_e('Firefighters', 'asterion-bricks'); ?></h3>
|
|
||||||
<p class="av-card__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.</p>
|
|
||||||
<div class="av-card__cta"><a href="<?php echo esc_url($home_url . 'industries/firefighters/'); ?>" class="av-link av-link--with-arrow"><?php esc_html_e('Firefighter training', 'asterion-bricks'); ?></a></div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<!-- 5. FEATURE HEROES (3 alternating) -->
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<section id="feature-heroes" class="av-section av-section--dark" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-section__head">
|
|
||||||
<span class="av-eyebrow av-eyebrow--gold"><?php esc_html_e('Technology', 'asterion-bricks'); ?></span>
|
|
||||||
<h2 class="av-section__title">Behavior, not scripts.</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
$features = [
|
|
||||||
[
|
|
||||||
'eyebrow' => 'Real-recoil weapon integration',
|
|
||||||
'title' => 'The weapon you train with is the weapon you carry.',
|
|
||||||
'body' => 'Our Universal Weapon Digitizer (UWeD™) attaches to any gas-blowback or CO2 replica — Glock 17, HK416, MP7, M9, and dozens more — or to your real service weapon firing blanks. Bullet trajectory is physically simulated: ricochet, penetration, material density. Recoil-induced tracking loss is corrected by an exclusive IMU compensation system.',
|
|
||||||
'link' => 'See how UWeD works',
|
|
||||||
'href' => '/technology/weapons-tracking/',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'eyebrow' => 'Instructor Cockpit',
|
|
||||||
'title' => 'You orchestrate. The trainees adapt.',
|
|
||||||
'body' => "From a dedicated instructor PC, you orchestrate every detail of the scenario in real time. Walk through walls. Switch to any trainee's first-person view. Toggle field of view, shot traces, and threat highlighting. Add or remove civilians, change a hostage's behavior, escalate or de-escalate a contact — without pausing the scenario.",
|
|
||||||
'link' => 'Inside the Instructor Cockpit',
|
|
||||||
'href' => '/technology/instructor-cockpit/',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'eyebrow' => 'After-Action Review',
|
|
||||||
'title' => 'The session ends; the learning starts.',
|
|
||||||
'body' => "Every event is logged: shots fired, shots hit, shots missed by target type, reaction time, field-of-view direction, weapon muzzling, position over time. Replay any session from any angle — top-down tactical, free-camera, or in-eye view of any trainee. Speed up, slow down, pause. Compare a recruit's reaction time to the unit's median.",
|
|
||||||
'link' => 'Explore After-Action Review',
|
|
||||||
'href' => '/technology/after-action-review/',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
foreach ($features as $i => $f) :
|
|
||||||
$reverse = ($i % 2 === 1);
|
|
||||||
?>
|
|
||||||
<div style="display:grid;grid-template-columns:1fr;gap:var(--space-8);align-items:center;margin-bottom:var(--space-16);<?php echo $reverse ? 'direction:rtl;' : ''; ?>" class="av-feature-row">
|
|
||||||
<div style="aspect-ratio:16/9;background:linear-gradient(135deg,#13294B,#081427);border-radius:var(--radius-md);direction:ltr;"></div>
|
|
||||||
<div style="direction:ltr;display:flex;flex-direction:column;gap:var(--space-3);">
|
|
||||||
<span class="av-eyebrow av-eyebrow--gold"><?php echo esc_html($f['eyebrow']); ?></span>
|
|
||||||
<h3 style="font-family:var(--font-display);font-size:var(--text-display-md);line-height:var(--leading-display-md);font-weight:var(--font-weight-bold);letter-spacing:var(--tracking-tight);color:var(--color-text-on-dark);margin:0;"><?php echo esc_html($f['title']); ?></h3>
|
|
||||||
<p style="color:var(--color-text-on-dark-muted);font-size:var(--text-body-lg);line-height:var(--leading-body-lg);max-width:60ch;"><?php echo wp_kses_post($f['body']); ?></p>
|
|
||||||
<div><a href="<?php echo esc_url($home_url . ltrim($f['href'], '/')); ?>" class="av-link av-link--on-dark av-link--with-arrow"><?php echo esc_html($f['link']); ?></a></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<style>
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.av-feature-row { grid-template-columns: 1fr 1fr !important; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<!-- 6. FEATURED CASE STUDY -->
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<section class="av-section" aria-labelledby="case-title" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div style="display:grid;grid-template-columns:1fr;gap:var(--space-10);align-items:center;" class="av-case-row">
|
|
||||||
<div style="aspect-ratio:4/3;background:linear-gradient(135deg,#3E4C5E,#0B1F3A);border-radius:var(--radius-md);"></div>
|
|
||||||
<div style="display:flex;flex-direction:column;gap:var(--space-4);">
|
|
||||||
<span class="av-eyebrow"><?php esc_html_e('Case study', 'asterion-bricks'); ?></span>
|
|
||||||
<h2 id="case-title" class="av-section__title">How a European tactical unit cut tactical errors by 35%.</h2>
|
|
||||||
<p class="av-section__lead">
|
|
||||||
In 2025, a European tactical unit deployed PROSERVE FLEX as part of its continuous training program. Six months in, instructor-rated tactical errors dropped 35% across the unit. Reaction time improved 22%. And weekly training volume tripled, without a single live round fired.
|
|
||||||
</p>
|
|
||||||
<div style="display:flex;gap:var(--space-8);margin-top:var(--space-2);">
|
|
||||||
<div class="av-stat av-stat--gold"><div class="av-stat__value">-35%</div><div class="av-stat__label"><?php esc_html_e('Tactical errors', 'asterion-bricks'); ?></div></div>
|
|
||||||
<div class="av-stat av-stat--gold"><div class="av-stat__value">+22%</div><div class="av-stat__label"><?php esc_html_e('Reaction time', 'asterion-bricks'); ?></div></div>
|
|
||||||
<div class="av-stat av-stat--gold"><div class="av-stat__value">×3</div><div class="av-stat__label"><?php esc_html_e('Training volume', 'asterion-bricks'); ?></div></div>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top:var(--space-2);"><a href="<?php echo esc_url($home_url . 'customers/'); ?>" class="av-btn av-btn--secondary av-btn--md"><?php esc_html_e('Read the case study', 'asterion-bricks'); ?></a></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<style>
|
|
||||||
@media (min-width: 1024px) { .av-case-row { grid-template-columns: 1fr 1fr !important; } }
|
|
||||||
</style>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<!-- 7. TESTIMONIALS -->
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<section class="av-section" style="background-color:var(--color-bg-subtle);" aria-labelledby="testimonials-title" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-section__head" style="text-align:center;align-items:center;margin-inline:auto;">
|
|
||||||
<span class="av-eyebrow"><?php esc_html_e('Voices from the field', 'asterion-bricks'); ?></span>
|
|
||||||
<h2 id="testimonials-title" class="av-section__title" style="text-align:center;margin-inline:auto;">Train how it really feels.</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="av-grid-3">
|
|
||||||
<article class="av-card av-card--quote">
|
|
||||||
<div class="av-card__quote">We've stopped asking how often we train. We've started measuring how well.</div>
|
|
||||||
<div class="av-card__attribution">— Tactical training officer, European federal police (anonymized)</div>
|
|
||||||
</article>
|
|
||||||
<article class="av-card av-card--quote">
|
|
||||||
<div class="av-card__quote">The first time my recruits saw their own AAR replay, three of them asked to redo the drill. That had never happened before.</div>
|
|
||||||
<div class="av-card__attribution">— Senior instructor, national police academy</div>
|
|
||||||
</article>
|
|
||||||
<article class="av-card av-card--quote">
|
|
||||||
<div class="av-card__quote">What used to take a half-day live exercise now takes 20 minutes between shifts.</div>
|
|
||||||
<div class="av-card__attribution">— Captain, urban response unit</div>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<!-- 8. LATEST INSIGHTS -->
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<section class="av-section" aria-labelledby="insights-title" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-section__head">
|
|
||||||
<span class="av-eyebrow"><?php esc_html_e('Insights', 'asterion-bricks'); ?></span>
|
|
||||||
<h2 id="insights-title" class="av-section__title">Insights from the front of immersive training.</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
$insights = get_posts(['numberposts' => 3, 'post_status' => 'publish']);
|
|
||||||
if (! empty($insights)) :
|
|
||||||
?>
|
|
||||||
<div class="av-grid-3">
|
|
||||||
<?php foreach ($insights as $post) : setup_postdata($post); ?>
|
|
||||||
<article class="av-card av-card--blog">
|
|
||||||
<a href="<?php the_permalink(); ?>" class="av-card__media" aria-hidden="true" tabindex="-1" style="background:linear-gradient(135deg,#CCD3DC,#E5E9EE);"></a>
|
|
||||||
<div class="av-card__body">
|
|
||||||
<span class="av-card__eyebrow"><?php echo esc_html(get_the_date('M j, Y')); ?></span>
|
|
||||||
<h3 class="av-card__title"><a href="<?php the_permalink(); ?>" style="color:inherit;text-decoration:none;"><?php the_title(); ?></a></h3>
|
|
||||||
<p class="av-card__excerpt"><?php echo esc_html(wp_trim_words(get_the_excerpt(), 22)); ?></p>
|
|
||||||
<div class="av-card__cta"><a href="<?php the_permalink(); ?>" class="av-link av-link--with-arrow"><?php esc_html_e('Read more', 'asterion-bricks'); ?></a></div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<?php endforeach; wp_reset_postdata(); ?>
|
|
||||||
</div>
|
|
||||||
<?php else : ?>
|
|
||||||
<p style="color:var(--color-text-muted);">
|
|
||||||
<?php esc_html_e('Editorial blog launches with the site go-live. The 12-month content calendar is in the Strategy & Content brief, section B.4.', 'asterion-bricks'); ?>
|
|
||||||
</p>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div style="margin-top:var(--space-8);">
|
|
||||||
<a href="<?php echo esc_url($home_url . 'insights/'); ?>" class="av-link av-link--with-arrow"><?php esc_html_e('See all articles', 'asterion-bricks'); ?></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<!-- 9. CONVERSION BANNER -->
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<section class="av-conversion-banner" aria-labelledby="conversion-title" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-conversion-banner__head">
|
|
||||||
<h2 id="conversion-title" class="av-conversion-banner__title">Train like the threat is now.</h2>
|
|
||||||
<p class="av-conversion-banner__lead"><?php esc_html_e('Three paths into the platform — pick the one that fits where you are.', 'asterion-bricks'); ?></p>
|
|
||||||
</div>
|
|
||||||
<div class="av-conversion-banner__grid">
|
|
||||||
|
|
||||||
<div class="av-conversion-banner__card">
|
|
||||||
<h3 class="av-conversion-banner__card-title"><?php esc_html_e('Request a Demo', 'asterion-bricks'); ?></h3>
|
|
||||||
<p class="av-conversion-banner__card-desc"><?php esc_html_e('Schedule a 30-minute tailored session with our team. Receive a video demo of the configuration that matches your needs.', 'asterion-bricks'); ?></p>
|
|
||||||
<a href="<?php echo esc_url($home_url . 'request-demo/'); ?>" class="av-btn av-btn--primary av-btn--md"><?php esc_html_e('Schedule a demo', 'asterion-bricks'); ?></a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="av-conversion-banner__card">
|
|
||||||
<h3 class="av-conversion-banner__card-title"><?php esc_html_e('Request a T&E Kit', 'asterion-bricks'); ?></h3>
|
|
||||||
<p class="av-conversion-banner__card-desc"><?php esc_html_e('Get a PROSERVE case on loan at your facility for 5 to 10 days, with on-site support from our team.', 'asterion-bricks'); ?></p>
|
|
||||||
<a href="<?php echo esc_url($home_url . 'request-te-kit/'); ?>" class="av-btn av-btn--on-dark av-btn--secondary av-btn--md"><?php esc_html_e('Request the kit', 'asterion-bricks'); ?></a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="av-conversion-banner__card">
|
|
||||||
<h3 class="av-conversion-banner__card-title"><?php esc_html_e('Request a Quote', 'asterion-bricks'); ?></h3>
|
|
||||||
<p class="av-conversion-banner__card-desc"><?php esc_html_e('Get a detailed quote and deployment plan within 5 working days.', 'asterion-bricks'); ?></p>
|
|
||||||
<a href="<?php echo esc_url($home_url . 'request-quote/'); ?>" class="av-btn av-btn--on-dark av-btn--secondary av-btn--md"><?php esc_html_e('Get a quote', 'asterion-bricks'); ?></a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<?php get_footer(); ?>
|
|
||||||
@@ -1,35 +1,38 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Asterion Bricks — child theme bootstrap
|
* Asterion Bricks — Child theme bootstrap (minimalist, Bricks-first)
|
||||||
*
|
*
|
||||||
* Loads design tokens, utilities, components, and main JS in a deterministic
|
* Goals
|
||||||
* order. Registers theme support, image sizes, and includes modular pieces
|
* -----
|
||||||
* (custom post types, SEO schemas).
|
* 1. Provide the design system to Bricks Builder via tokens.css.
|
||||||
|
* 2. Self-host Inter fonts so Bricks-rendered pages stay GDPR-compliant.
|
||||||
|
* 3. Register Custom Post Types (case_study, scenario) used across the site.
|
||||||
|
*
|
||||||
|
* Non-goals (intentionally NOT here, on purpose)
|
||||||
|
* ----------------------------------------------
|
||||||
|
* ✗ No header.php / footer.php / page.php / front-page.php / index.php.
|
||||||
|
* Bricks Templates (Header / Footer / Single — Page) drive the chrome.
|
||||||
|
* ✗ No Custom Page Templates. Pages render through Bricks data.
|
||||||
|
* ✗ No PHP renderers. Content lives in Bricks data + WPML translations.
|
||||||
|
* ✗ No Gutenberg block libraries dequeued. Bricks needs them in the builder.
|
||||||
*
|
*
|
||||||
* @package AsterionBricks
|
* @package AsterionBricks
|
||||||
* @since 0.1.0
|
* @since 0.2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
define('AV_THEME_VERSION', '0.1.0');
|
define('AV_THEME_VERSION', '0.2.0');
|
||||||
define('AV_THEME_DIR', get_stylesheet_directory());
|
define('AV_THEME_DIR', get_stylesheet_directory());
|
||||||
define('AV_THEME_URI', get_stylesheet_directory_uri());
|
define('AV_THEME_URI', get_stylesheet_directory_uri());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enqueue child theme styles and scripts.
|
* Enqueue tokens.css + utilities.css on the public frontend AND inside the
|
||||||
*
|
* Bricks Builder iframe — the builder loads the front-end stylesheet so we
|
||||||
* Bricks parent stylesheet is loaded by the parent theme itself — no need to
|
* see our `--color-*`, `--space-*`, `--font-*` variables as we work.
|
||||||
* re-enqueue. We layer our tokens → utilities → components on top.
|
|
||||||
*
|
|
||||||
* Cache busting via `filemtime()` so dev changes are picked up without manual
|
|
||||||
* version bumps. In production, swap to AV_THEME_VERSION + a build step.
|
|
||||||
*/
|
*/
|
||||||
add_action('wp_enqueue_scripts', function () {
|
add_action('wp_enqueue_scripts', function () {
|
||||||
$css_dir = AV_THEME_DIR . '/assets/css';
|
$css_dir = AV_THEME_DIR . '/assets/css';
|
||||||
$css_uri = AV_THEME_URI . '/assets/css';
|
$css_uri = AV_THEME_URI . '/assets/css';
|
||||||
$js_dir = AV_THEME_DIR . '/assets/js';
|
|
||||||
$js_uri = AV_THEME_URI . '/assets/js';
|
|
||||||
|
|
||||||
if (file_exists($css_dir . '/tokens.css')) {
|
if (file_exists($css_dir . '/tokens.css')) {
|
||||||
wp_enqueue_style(
|
wp_enqueue_style(
|
||||||
@@ -39,7 +42,6 @@ add_action('wp_enqueue_scripts', function () {
|
|||||||
filemtime($css_dir . '/tokens.css')
|
filemtime($css_dir . '/tokens.css')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file_exists($css_dir . '/utilities.css')) {
|
if (file_exists($css_dir . '/utilities.css')) {
|
||||||
wp_enqueue_style(
|
wp_enqueue_style(
|
||||||
'av-utilities',
|
'av-utilities',
|
||||||
@@ -48,82 +50,25 @@ add_action('wp_enqueue_scripts', function () {
|
|||||||
filemtime($css_dir . '/utilities.css')
|
filemtime($css_dir . '/utilities.css')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file_exists($css_dir . '/components.css')) {
|
|
||||||
wp_enqueue_style(
|
|
||||||
'av-components',
|
|
||||||
$css_uri . '/components.css',
|
|
||||||
['av-tokens', 'av-utilities'],
|
|
||||||
filemtime($css_dir . '/components.css')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file_exists($js_dir . '/main.js')) {
|
|
||||||
wp_enqueue_script(
|
|
||||||
'av-main',
|
|
||||||
$js_uri . '/main.js',
|
|
||||||
[],
|
|
||||||
filemtime($js_dir . '/main.js'),
|
|
||||||
['strategy' => 'defer', 'in_footer' => true]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, 20);
|
}, 20);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preload self-hosted Inter fonts to reduce LCP.
|
* Preload the Inter Variable WOFF2 to reduce LCP. Conditional on the file
|
||||||
*
|
* existing so partial deploys don't 404.
|
||||||
* Only emits <link rel="preload"> for fonts that actually exist on disk —
|
|
||||||
* silent during early dev when the WOFF2 files have not been dropped yet.
|
|
||||||
*/
|
*/
|
||||||
add_action('wp_head', function () {
|
add_action('wp_head', function () {
|
||||||
$fonts = [
|
$font_path = AV_THEME_DIR . '/assets/fonts/Inter-Variable.woff2';
|
||||||
'Inter-Variable.woff2',
|
if (file_exists($font_path)) {
|
||||||
// 'InterDisplay-Variable.woff2',
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($fonts as $font) {
|
|
||||||
$path = AV_THEME_DIR . '/assets/fonts/' . $font;
|
|
||||||
if (file_exists($path)) {
|
|
||||||
printf(
|
printf(
|
||||||
'<link rel="preload" href="%s/assets/fonts/%s" as="font" type="font/woff2" crossorigin>' . "\n",
|
'<link rel="preload" href="%s/assets/fonts/Inter-Variable.woff2" as="font" type="font/woff2" crossorigin>' . "\n",
|
||||||
esc_url(AV_THEME_URI),
|
esc_url(AV_THEME_URI)
|
||||||
esc_attr($font)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}, 1);
|
}, 1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modular includes.
|
* Theme support + custom image sizes (used by Bricks media controls and
|
||||||
*
|
* Schema.org/OpenGraph emitters down the line).
|
||||||
* Each file is opt-in via file_exists() so partial scaffolds don't fatal.
|
|
||||||
*/
|
|
||||||
$av_includes = [
|
|
||||||
'inc/i18n.php', // av_lang(), av_t() — load FIRST.
|
|
||||||
'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 → 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.
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($av_includes as $av_include) {
|
|
||||||
$path = AV_THEME_DIR . '/' . $av_include;
|
|
||||||
if (file_exists($path)) {
|
|
||||||
require_once $path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
unset($av_include, $path);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Theme support and custom image sizes.
|
|
||||||
*
|
|
||||||
* Image sizes per Design Handoff section 8.3:
|
|
||||||
* - hero 2400×1350 (16:9)
|
|
||||||
* - card 800×600 (4:3)
|
|
||||||
* - thumb 400×300 (4:3)
|
|
||||||
*/
|
*/
|
||||||
add_action('after_setup_theme', function () {
|
add_action('after_setup_theme', function () {
|
||||||
add_theme_support('post-thumbnails');
|
add_theme_support('post-thumbnails');
|
||||||
@@ -133,41 +78,25 @@ add_action('after_setup_theme', function () {
|
|||||||
]);
|
]);
|
||||||
add_theme_support('responsive-embeds');
|
add_theme_support('responsive-embeds');
|
||||||
add_theme_support('align-wide');
|
add_theme_support('align-wide');
|
||||||
add_theme_support('editor-styles');
|
|
||||||
|
|
||||||
add_image_size('av-hero', 2400, 1350, true);
|
add_image_size('av-hero', 2400, 1350, true); // 16:9 hero
|
||||||
add_image_size('av-card', 800, 600, true);
|
add_image_size('av-card', 800, 600, true); // 4:3 card
|
||||||
add_image_size('av-thumb', 400, 300, true);
|
add_image_size('av-thumb', 400, 300, true); // 4:3 thumb
|
||||||
|
|
||||||
load_child_theme_textdomain('asterion-bricks', AV_THEME_DIR . '/languages');
|
load_child_theme_textdomain('asterion-bricks', AV_THEME_DIR . '/languages');
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hide the WP admin bar on frontend for non-admins (cleaner preview screenshots).
|
* Modular includes — currently only CPT registration. SEO and other modules
|
||||||
|
* will be added as needed; the include loop is opt-in via file_exists().
|
||||||
*/
|
*/
|
||||||
add_action('after_setup_theme', function () {
|
$av_includes = [
|
||||||
if (! current_user_can('administrator')) {
|
'inc/cpt.php',
|
||||||
show_admin_bar(false);
|
];
|
||||||
|
foreach ($av_includes as $av_include) {
|
||||||
|
$path = AV_THEME_DIR . '/' . $av_include;
|
||||||
|
if (file_exists($path)) {
|
||||||
|
require_once $path;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
unset($av_include, $path);
|
||||||
/**
|
|
||||||
* 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');
|
|
||||||
wp_dequeue_style('classic-theme-styles');
|
|
||||||
}, 100);
|
|
||||||
|
|||||||
@@ -1,138 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — Global header
|
|
||||||
*
|
|
||||||
* Sticky header per Design Handoff section 3.5 :
|
|
||||||
* - 60px on mobile, 72px on desktop
|
|
||||||
* - Logo left, main nav center (desktop), CTA + lang switch right
|
|
||||||
* - Hamburger drawer on mobile
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html <?php language_attributes(); ?>>
|
|
||||||
<head>
|
|
||||||
<meta charset="<?php bloginfo('charset'); ?>">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
||||||
<meta name="theme-color" content="#0B1F3A">
|
|
||||||
<link rel="profile" href="https://gmpg.org/xfn/11">
|
|
||||||
<?php wp_head(); ?>
|
|
||||||
</head>
|
|
||||||
<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">
|
|
||||||
<div class="av-container av-header__inner">
|
|
||||||
|
|
||||||
<a href="<?php echo esc_url(home_url('/')); ?>" class="av-header__logo" aria-label="Asterion VR — home">
|
|
||||||
ASTERION VR
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<nav class="av-nav" aria-label="<?php esc_attr_e('Primary', 'asterion-bricks'); ?>">
|
|
||||||
<ul class="av-nav__list" style="display:flex;gap:var(--space-6);list-style:none;margin:0;padding:0;">
|
|
||||||
<li class="av-nav__item">
|
|
||||||
<a href="<?php echo esc_url(home_url('/solutions/')); ?>" class="av-nav__link" aria-haspopup="true" aria-expanded="false">
|
|
||||||
<?php echo esc_html(av_t('Solutions', 'asterion-bricks')); ?>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="av-nav__item">
|
|
||||||
<a href="<?php echo esc_url(home_url('/industries/')); ?>" class="av-nav__link" aria-haspopup="true" aria-expanded="false">
|
|
||||||
<?php echo esc_html(av_t('Industries', 'asterion-bricks')); ?>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="av-nav__item">
|
|
||||||
<a href="<?php echo esc_url(home_url('/technology/')); ?>" class="av-nav__link" aria-haspopup="true" aria-expanded="false">
|
|
||||||
<?php echo esc_html(av_t('Technology', 'asterion-bricks')); ?>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="av-nav__item">
|
|
||||||
<a href="<?php echo esc_url(home_url('/why-asterion/')); ?>" class="av-nav__link">
|
|
||||||
<?php echo esc_html(av_t('Why Asterion', 'asterion-bricks')); ?>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="av-nav__item">
|
|
||||||
<a href="<?php echo esc_url(home_url('/insights/')); ?>" class="av-nav__link">
|
|
||||||
<?php echo esc_html(av_t('Insights', 'asterion-bricks')); ?>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="av-header__right">
|
|
||||||
<?php
|
|
||||||
// Render WPML language switcher when WPML is active and exposes languages.
|
|
||||||
$av_languages = apply_filters('wpml_active_languages', null, ['skip_missing' => 0]);
|
|
||||||
if (! empty($av_languages) && is_array($av_languages) && count($av_languages) > 1) :
|
|
||||||
?>
|
|
||||||
<div class="av-lang-switch" aria-label="<?php echo esc_attr(av_t('Language')); ?>">
|
|
||||||
<?php $av_first = true; foreach ($av_languages as $av_lang_data) :
|
|
||||||
if (! $av_first) : ?><span aria-hidden="true">·</span><?php endif; $av_first = false;
|
|
||||||
$av_code = strtoupper($av_lang_data['language_code']);
|
|
||||||
$av_url = esc_url($av_lang_data['url']);
|
|
||||||
$av_label = esc_html($av_lang_data['translated_name'] ?? $av_lang_data['native_name'] ?? $av_code);
|
|
||||||
if (! empty($av_lang_data['active'])) : ?>
|
|
||||||
<span class="av-lang-switch__current" aria-current="true"><?php echo esc_html($av_code); ?></span>
|
|
||||||
<?php else : ?>
|
|
||||||
<a href="<?php echo $av_url; ?>" class="av-lang-switch__other" hreflang="<?php echo esc_attr($av_lang_data['language_code']); ?>" lang="<?php echo esc_attr($av_lang_data['language_code']); ?>" aria-label="<?php echo esc_attr($av_label); ?>"><?php echo esc_html($av_code); ?></a>
|
|
||||||
<?php endif;
|
|
||||||
endforeach; ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<a href="<?php echo esc_url(home_url('/request-demo/')); ?>" class="av-btn av-btn--primary av-btn--md">
|
|
||||||
<?php echo esc_html(av_t('Request a Demo', 'asterion-bricks')); ?>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<button type="button" class="av-drawer-toggle" aria-controls="av-drawer" aria-expanded="false" aria-label="<?php esc_attr_e('Open menu', 'asterion-bricks'); ?>">
|
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
||||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
|
||||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
|
||||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<aside id="av-drawer" class="av-drawer" aria-hidden="true">
|
|
||||||
<button type="button" class="av-drawer__close" aria-label="<?php esc_attr_e('Close menu', 'asterion-bricks'); ?>">×</button>
|
|
||||||
<nav class="av-drawer__nav" aria-label="<?php esc_attr_e('Mobile primary', 'asterion-bricks'); ?>">
|
|
||||||
<div class="av-drawer__group">
|
|
||||||
<a href="<?php echo esc_url(home_url('/solutions/')); ?>" class="av-drawer__group-title"><?php echo esc_html(av_t('Solutions', 'asterion-bricks')); ?></a>
|
|
||||||
</div>
|
|
||||||
<div class="av-drawer__group">
|
|
||||||
<a href="<?php echo esc_url(home_url('/industries/')); ?>" class="av-drawer__group-title"><?php echo esc_html(av_t('Industries', 'asterion-bricks')); ?></a>
|
|
||||||
</div>
|
|
||||||
<div class="av-drawer__group">
|
|
||||||
<a href="<?php echo esc_url(home_url('/technology/')); ?>" class="av-drawer__group-title"><?php echo esc_html(av_t('Technology', 'asterion-bricks')); ?></a>
|
|
||||||
</div>
|
|
||||||
<div class="av-drawer__group">
|
|
||||||
<a href="<?php echo esc_url(home_url('/why-asterion/')); ?>" class="av-drawer__group-title"><?php echo esc_html(av_t('Why Asterion', 'asterion-bricks')); ?></a>
|
|
||||||
</div>
|
|
||||||
<div class="av-drawer__group">
|
|
||||||
<a href="<?php echo esc_url(home_url('/insights/')); ?>" class="av-drawer__group-title"><?php echo esc_html(av_t('Insights', 'asterion-bricks')); ?></a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
<div class="av-drawer__cta-area">
|
|
||||||
<a href="<?php echo esc_url(home_url('/request-demo/')); ?>" class="av-btn av-btn--primary av-btn--lg" style="width:100%;">
|
|
||||||
<?php echo esc_html(av_t('Request a Demo', 'asterion-bricks')); ?>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<main id="main" class="av-main">
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — Conversion form handler
|
|
||||||
*
|
|
||||||
* Receives POSTs from /request-demo/, /request-te-kit/, /request-quote/,
|
|
||||||
* /contact/. Validates the WordPress nonce, screens the honeypot, sanitizes
|
|
||||||
* the payload, and emails the lead to the configured recipient.
|
|
||||||
*
|
|
||||||
* Hook : admin_post_av_form_submit (logged-out users use admin_post_nopriv).
|
|
||||||
*
|
|
||||||
* Lead destination defaults to AV_LEADS_RECIPIENT (override in wp-config.php),
|
|
||||||
* falling back to the WP admin email.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
if (! defined('AV_LEADS_RECIPIENT')) {
|
|
||||||
define('AV_LEADS_RECIPIENT', 'jerome.foucher@asterionvr.com');
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_handle_form_submit() {
|
|
||||||
|
|
||||||
// 1. Nonce
|
|
||||||
if (! isset($_POST['av_nonce']) || ! wp_verify_nonce(wp_unslash($_POST['av_nonce']), 'av_form_submit')) {
|
|
||||||
wp_safe_redirect(home_url('/?av_status=error'));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Honeypot — bots fill anything
|
|
||||||
if (! empty($_POST['av_hp'])) {
|
|
||||||
// Pretend success so bots don't retry.
|
|
||||||
$redirect = isset($_POST['redirect_to']) ? esc_url_raw(wp_unslash($_POST['redirect_to'])) : home_url('/');
|
|
||||||
wp_safe_redirect(add_query_arg('av_status', 'success', $redirect));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Form data
|
|
||||||
$slug = isset($_POST['form_slug']) ? sanitize_key(wp_unslash($_POST['form_slug'])) : '';
|
|
||||||
$forms_data = include AV_THEME_DIR . '/inc/forms-data.php';
|
|
||||||
if (! isset($forms_data[$slug])) {
|
|
||||||
wp_safe_redirect(home_url('/?av_status=error'));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
$form = $forms_data[$slug];
|
|
||||||
|
|
||||||
// 4. Sanitize fields
|
|
||||||
$payload = [];
|
|
||||||
foreach ($form['fields'] as $field) {
|
|
||||||
$name = $field['name'];
|
|
||||||
if (! isset($_POST[$name])) {
|
|
||||||
$payload[$name] = '';
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$raw = wp_unslash($_POST[$name]);
|
|
||||||
if ($field['type'] === 'email') {
|
|
||||||
$payload[$name] = sanitize_email($raw);
|
|
||||||
} elseif ($field['type'] === 'textarea') {
|
|
||||||
$payload[$name] = sanitize_textarea_field($raw);
|
|
||||||
} else {
|
|
||||||
$payload[$name] = sanitize_text_field($raw);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Required-field validation
|
|
||||||
foreach ($form['fields'] as $field) {
|
|
||||||
if (! empty($field['required']) && empty($payload[$field['name']])) {
|
|
||||||
$redirect = isset($_POST['redirect_to']) ? esc_url_raw(wp_unslash($_POST['redirect_to'])) : home_url('/');
|
|
||||||
wp_safe_redirect(add_query_arg('av_status', 'error', $redirect));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Compose mail
|
|
||||||
$subject_prefix = '[Asterion VR · ' . $form['title'] . ']';
|
|
||||||
$subject = $subject_prefix . ' ' . ($payload['organization'] ?? $payload['full_name'] ?? '(unidentified lead)');
|
|
||||||
|
|
||||||
$body = "New lead from " . get_bloginfo('name') . " — form: " . $form['title'] . "\n";
|
|
||||||
$body .= "URL : " . (isset($_POST['redirect_to']) ? esc_url_raw(wp_unslash($_POST['redirect_to'])) : home_url('/')) . "\n";
|
|
||||||
$body .= str_repeat('-', 60) . "\n";
|
|
||||||
foreach ($form['fields'] as $field) {
|
|
||||||
$value = $payload[$field['name']] ?? '';
|
|
||||||
if ($value === '') continue;
|
|
||||||
$body .= str_pad($field['label'], 28) . ': ' . $value . "\n";
|
|
||||||
}
|
|
||||||
$body .= str_repeat('-', 60) . "\n";
|
|
||||||
$body .= "Submitted : " . current_time('mysql') . "\n";
|
|
||||||
$body .= "IP : " . (isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field($_SERVER['REMOTE_ADDR']) : '?') . "\n";
|
|
||||||
$body .= "User Agent: " . (isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '?') . "\n";
|
|
||||||
|
|
||||||
$headers = ['Content-Type: text/plain; charset=UTF-8'];
|
|
||||||
if (! empty($payload['email'])) {
|
|
||||||
$headers[] = 'Reply-To: ' . $payload['email'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$recipient = apply_filters('av_form_recipient', AV_LEADS_RECIPIENT, $slug, $payload);
|
|
||||||
|
|
||||||
$sent = wp_mail($recipient, $subject, $body, $headers);
|
|
||||||
|
|
||||||
// 7. Redirect
|
|
||||||
$redirect = isset($_POST['redirect_to']) ? esc_url_raw(wp_unslash($_POST['redirect_to'])) : home_url('/');
|
|
||||||
$status = $sent ? 'success' : 'error';
|
|
||||||
wp_safe_redirect(add_query_arg('av_status', $status, $redirect));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
add_action('admin_post_av_form_submit', 'av_handle_form_submit');
|
|
||||||
add_action('admin_post_nopriv_av_form_submit', 'av_handle_form_submit');
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — i18n helpers
|
|
||||||
*
|
|
||||||
* Lightweight WPML-aware language helpers used by Custom Templates to pick
|
|
||||||
* the right data file (e.g. solutions-data.php vs solutions-data-fr.php) and
|
|
||||||
* by header.php / footer.php to translate UI strings.
|
|
||||||
*
|
|
||||||
* We don't use WordPress .po/.mo loaders — WPML String Translation handles
|
|
||||||
* those — but we keep a small in-PHP map for the strings that aren't tied
|
|
||||||
* to data files (nav labels, generic CTAs, footer columns).
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Active language code, normalised to 'en' or 'fr'.
|
|
||||||
* Falls back to 'en' when WPML isn't active.
|
|
||||||
*/
|
|
||||||
function av_lang() {
|
|
||||||
$lang = apply_filters('wpml_current_language', null);
|
|
||||||
if (! $lang) {
|
|
||||||
$lang = function_exists('get_locale') ? substr(get_locale(), 0, 2) : 'en';
|
|
||||||
}
|
|
||||||
return ($lang === 'fr') ? 'fr' : 'en';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build the path to a localised data file.
|
|
||||||
* Example : av_data_file('solutions-data') returns
|
|
||||||
* '<theme>/inc/solutions-data.php' when EN
|
|
||||||
* '<theme>/inc/solutions-data-fr.php' when FR (if it exists)
|
|
||||||
*/
|
|
||||||
function av_data_file($basename) {
|
|
||||||
$base = AV_THEME_DIR . '/inc/' . $basename;
|
|
||||||
$localised = $base . '-' . av_lang() . '.php';
|
|
||||||
return file_exists($localised) ? $localised : $base . '.php';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Translate a UI string. Falls back to the original on EN or when no entry
|
|
||||||
* matches in FR. Strings are kept in this file rather than .po/.mo so the
|
|
||||||
* theme stays self-contained.
|
|
||||||
*/
|
|
||||||
function av_t($string, $textdomain = 'asterion-bricks') {
|
|
||||||
if (av_lang() !== 'fr') {
|
|
||||||
return $string;
|
|
||||||
}
|
|
||||||
static $map = null;
|
|
||||||
if ($map === null) {
|
|
||||||
$map = [
|
|
||||||
// Nav + header
|
|
||||||
'Skip to main content' => 'Aller au contenu principal',
|
|
||||||
'Primary' => 'Principal',
|
|
||||||
'Mobile primary' => 'Principal mobile',
|
|
||||||
'Solutions' => 'Solutions',
|
|
||||||
'Industries' => 'Secteurs',
|
|
||||||
'Technology' => 'Technologie',
|
|
||||||
'Why Asterion' => 'Pourquoi Asterion',
|
|
||||||
'Insights' => 'Insights',
|
|
||||||
'Open menu' => 'Ouvrir le menu',
|
|
||||||
'Close menu' => 'Fermer le menu',
|
|
||||||
'Language' => 'Langue',
|
|
||||||
'Switch to French' => 'Passer en français',
|
|
||||||
'Switch to English' => 'Switch to English',
|
|
||||||
'Request a Demo' => 'Demander une démo',
|
|
||||||
|
|
||||||
// Footer
|
|
||||||
'Newsletter' => 'Newsletter',
|
|
||||||
'Our story' => 'Notre histoire',
|
|
||||||
'Customers' => 'Clients',
|
|
||||||
'Trust & Compliance' => 'Confiance et conformité',
|
|
||||||
'Partners' => 'Partenaires',
|
|
||||||
'Police' => 'Police',
|
|
||||||
'Special Forces' => 'Forces spéciales',
|
|
||||||
'Military' => 'Militaires',
|
|
||||||
'Firefighters' => 'Pompiers',
|
|
||||||
'Privacy' => 'Confidentialité',
|
|
||||||
'Terms' => 'Conditions',
|
|
||||||
'Cookies' => 'Cookies',
|
|
||||||
'Legal notice' => 'Mentions légales',
|
|
||||||
'All rights reserved.' => 'Tous droits réservés.',
|
|
||||||
'Insights from the front of immersive training. One email per month. No spam.'
|
|
||||||
=> 'Les enseignements du terrain de la formation immersive. Un e-mail par mois. Pas de spam.',
|
|
||||||
'Subscribe' => 'S’abonner',
|
|
||||||
'your@email.com' => 'vous@email.com',
|
|
||||||
'The modular, sovereign XR platform for European security forces. Train like the threat is now.'
|
|
||||||
=> 'La plateforme XR modulaire et souveraine pour les forces de sécurité européennes. Entraînez-vous comme si la menace était maintenant.',
|
|
||||||
|
|
||||||
// Generic CTAs / labels
|
|
||||||
'Read more' => 'Lire la suite',
|
|
||||||
'See all articles' => 'Voir tous les articles',
|
|
||||||
'Watch the 90-second film' => 'Voir le film de 90 secondes',
|
|
||||||
'Schedule a demo' => 'Planifier une démo',
|
|
||||||
'Request the kit' => 'Demander le kit',
|
|
||||||
'Get a quote' => 'Demander un devis',
|
|
||||||
'Send' => 'Envoyer',
|
|
||||||
'Send the message' => 'Envoyer le message',
|
|
||||||
'Apply for a T&E Kit' => 'Postuler pour un kit T&E',
|
|
||||||
'Schedule the demo' => 'Planifier la démo',
|
|
||||||
'Request the quote' => 'Demander le devis',
|
|
||||||
'Read the case study' => 'Lire l’étude de cas',
|
|
||||||
'Discuss a custom scenario' => 'Discuter d’un scénario sur mesure',
|
|
||||||
'Schedule a discovery call' => 'Planifier un appel de cadrage',
|
|
||||||
'Discuss your custom request' => 'Parler de votre besoin sur mesure',
|
|
||||||
'Become the next case study' => 'Devenez la prochaine étude de cas',
|
|
||||||
'Request a Trust Pack' => 'Demander un Trust Pack',
|
|
||||||
'Become a partner — apply' => 'Devenir partenaire — candidater',
|
|
||||||
'Compare with PROSERVE FLEX' => 'Comparer avec PROSERVE FLEX',
|
|
||||||
'Compare with PROSERVE ACADEMY' => 'Comparer avec PROSERVE ACADEMY',
|
|
||||||
'Comparison and upgrade' => 'Comparaison et évolution',
|
|
||||||
'Eligibility' => 'Éligibilité',
|
|
||||||
'Frequently asked' => 'Questions fréquentes',
|
|
||||||
'Quick answers.' => 'Réponses brèves.',
|
|
||||||
|
|
||||||
// Section eyebrows used on home / hubs / forms
|
|
||||||
'Modular XR training for security forces' => 'Formation XR modulaire pour les forces de sécurité',
|
|
||||||
'Trusted by' => 'Reconnu par',
|
|
||||||
'Trusted by security forces across Europe' => 'Adopté par les forces de sécurité en Europe',
|
|
||||||
'Voices from the front of immersive training.' => 'La parole des opérateurs.',
|
|
||||||
'Compare' => 'Comparer',
|
|
||||||
'The six pillars' => 'Les six piliers',
|
|
||||||
'Tell us about your unit.' => 'Parlez-nous de votre unité.',
|
|
||||||
'What you receive' => 'Ce que vous recevez',
|
|
||||||
'Other inquiries' => 'Autres demandes',
|
|
||||||
'How can we help?' => 'Comment pouvons-nous vous aider ?',
|
|
||||||
|
|
||||||
// Form widgets
|
|
||||||
'— select —' => '— choisir —',
|
|
||||||
'Your email' => 'Votre e-mail',
|
|
||||||
'Newsletter signup' => 'Inscription newsletter',
|
|
||||||
'Mobile drawer' => 'Tiroir mobile',
|
|
||||||
'Voices from the field' => 'La parole du terrain',
|
|
||||||
'Train how it really feels.' => 'Entraînez-vous au plus près du réel.',
|
|
||||||
'Train like the threat is now.' => 'Entraînez-vous comme si la menace était maintenant.',
|
|
||||||
'Three paths into the platform — pick the one that fits where you are.' => 'Trois portes d’entrée dans la plateforme — choisissez celle qui correspond à votre besoin.',
|
|
||||||
'A 30-minute tailored session with our team. Walk through the configuration that matches your needs.' => 'Une session sur mesure de 30 minutes avec notre équipe. Démonstration de la configuration adaptée à vos besoins.',
|
|
||||||
'A 30-minute tailored session with our team.' => 'Une session de 30 minutes avec notre équipe.',
|
|
||||||
'A PROSERVE case on loan at your facility for 5 to 10 days, with on-site support from our team.' => 'Un kit PROSERVE en prêt dans vos locaux pendant 5 à 10 jours, avec accompagnement sur site.',
|
|
||||||
'A PROSERVE case on loan at your facility for 5 to 10 days.' => 'Un kit PROSERVE en prêt dans vos locaux pendant 5 à 10 jours.',
|
|
||||||
'A detailed quote and deployment plan within 5 working days.' => 'Un devis détaillé et un plan de déploiement sous 5 jours ouvrés.',
|
|
||||||
'Ready to get hands-on?' => 'Prêt à passer à la pratique ?',
|
|
||||||
'Last updated:' => 'Dernière mise à jour :',
|
|
||||||
'Legal' => 'Légal',
|
|
||||||
'Placeholder.' => 'À compléter.',
|
|
||||||
'This page is to be drafted with legal counsel. The structure below outlines the sections expected.' =>
|
|
||||||
'Cette page sera rédigée avec votre conseil juridique. La structure ci-dessous liste les sections attendues.',
|
|
||||||
'Sections to include' => 'Sections à inclure',
|
|
||||||
'Police training' => 'Formation police',
|
|
||||||
'Special forces training' => 'Formation forces spéciales',
|
|
||||||
'Military training' => 'Formation militaire',
|
|
||||||
'Firefighter training' => 'Formation pompier',
|
|
||||||
'Insights from the front of immersive training.' => 'Les enseignements du terrain de la formation immersive.',
|
|
||||||
'See the full scenario library' => 'Voir la bibliothèque complète de scénarios',
|
|
||||||
'Inside the hardware' => 'Inside the hardware',
|
|
||||||
'Inside the software' => 'Inside the software',
|
|
||||||
'Inside weapons & tracking' => 'Inside weapons & tracking',
|
|
||||||
'Browse the scenario library' => 'Parcourir la bibliothèque',
|
|
||||||
'Inside the instructor cockpit' => 'Inside the instructor cockpit',
|
|
||||||
'Inside after-action review' => 'Inside after-action review',
|
|
||||||
'See how UWeD works' => 'Voir comment fonctionne UWeD',
|
|
||||||
'Explore After-Action Review' => 'Découvrir l’After-Action Review',
|
|
||||||
'Explore PROSERVE+ customization' => 'Découvrir la personnalisation PROSERVE+',
|
|
||||||
'Discover MY PROSERVE' => 'Découvrir MY PROSERVE',
|
|
||||||
'Discover PROSERVE FLEX' => 'Découvrir PROSERVE FLEX',
|
|
||||||
'Discover PROSERVE ACADEMY' => 'Découvrir PROSERVE ACADEMY',
|
|
||||||
'Request access' => 'Demander l’accès',
|
|
||||||
'Request a T&E Kit' => 'Demander un kit T&E',
|
|
||||||
'Request a Quote' => 'Demander un devis',
|
|
||||||
'Request a Demo for police use' => 'Demander une démo pour la police',
|
|
||||||
'Request a Demo for fire & emergency' => 'Demander une démo pour les pompiers',
|
|
||||||
'Thank you — your request was sent.' => 'Merci — votre demande a bien été envoyée.',
|
|
||||||
'We will reply within the timeframe stated above. Check your inbox (and spam) for confirmation.' =>
|
|
||||||
'Nous reviendrons vers vous dans les délais indiqués. Vérifiez votre boîte de réception (et vos spams) pour la confirmation.',
|
|
||||||
'Something went wrong.' => 'Une erreur est survenue.',
|
|
||||||
' Please try again, or email us directly at hello@asterionvr.com.' =>
|
|
||||||
' Veuillez réessayer, ou écrivez-nous directement à hello@asterionvr.com.',
|
|
||||||
'Editorial blog launches with the site go-live. The 12-month content calendar is in the Strategy & Content brief, section B.4.' =>
|
|
||||||
'Le blog éditorial démarrera avec la mise en ligne du site. Le calendrier sur 12 mois figure dans le livrable Stratégie & Contenu, partie B.4.',
|
|
||||||
'Editorial blog launches with the site go-live. The 12-month content calendar is in the Strategy & Content brief, section B.4. New articles will appear here as soon as they ship.' =>
|
|
||||||
'Le blog éditorial démarrera avec la mise en ligne du site. Le calendrier sur 12 mois figure dans le livrable Stratégie & Contenu, partie B.4. Les articles apparaîtront ici dès leur publication.',
|
|
||||||
'Subscribe — one article per month.' => 'Abonnez-vous — un article par mois.',
|
|
||||||
'Notes from the front of immersive training.' => 'Les enseignements du terrain de la formation immersive.',
|
|
||||||
'Doctrine. Training science. Instructor stories. AAR data. Comparisons of VR vs live training. Found something useful? It probably came from a customer conversation. We publish what we learn.' =>
|
|
||||||
'Doctrine. Science de la formation. Récits d’instructeurs. Données AAR. Comparaisons VR vs entraînement réel. Quelque chose d’utile ? Cela vient probablement d’un échange client. Nous publions ce que nous apprenons.',
|
|
||||||
'Everything to evaluate, justify, and procure.' => 'Tout pour évaluer, justifier, acheter.',
|
|
||||||
'Whether you are building an internal pitch, drafting a procurement specification, or briefing your team — these resources are designed to help.' =>
|
|
||||||
'Que vous prépariez un argumentaire interne, rédigiez un cahier des charges ou briefiez vos équipes — ces ressources sont là pour vous aider.',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return $map[$string] ?? $string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience: echo a translated string.
|
|
||||||
*/
|
|
||||||
function av_te($string) {
|
|
||||||
echo esc_html(av_t($string));
|
|
||||||
}
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
<?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]);
|
|
||||||
}
|
|
||||||
@@ -1,376 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — Data → Gutenberg blocks renderer
|
|
||||||
*
|
|
||||||
* Converts the structured PHP arrays in inc/<section>-data.php into a
|
|
||||||
* Gutenberg block payload (`post_content`) so that :
|
|
||||||
*
|
|
||||||
* - Pages are editable in the WP admin block editor
|
|
||||||
* - Bricks Builder can pick them up via « Edit with Bricks » (Bricks reads
|
|
||||||
* post_content and converts core blocks → Bricks elements on save)
|
|
||||||
* - WPML's Translation Editor exposes every block for translation
|
|
||||||
*
|
|
||||||
* Strategy
|
|
||||||
* --------
|
|
||||||
* We generate Gutenberg-valid HTML using a mix of native blocks
|
|
||||||
* (`core/heading`, `core/paragraph`, `core/list`, `core/buttons`,
|
|
||||||
* `core/columns`) wrapped in `core/group` containers carrying our
|
|
||||||
* `av-*` classes. Visually-heavy items (hero, conversion banner,
|
|
||||||
* complex cards) fall back to `core/html` blocks so the rendering
|
|
||||||
* stays pixel-identical to the Custom Template version, while still
|
|
||||||
* being individually translatable in WPML.
|
|
||||||
*
|
|
||||||
* Public API
|
|
||||||
* ----------
|
|
||||||
* av_render_solution_blocks($data) → string (post_content)
|
|
||||||
* av_render_industry_blocks($data) → string
|
|
||||||
* av_render_technology_blocks($data) → string (alias of industry)
|
|
||||||
* av_render_editorial_blocks($data) → string (alias of industry)
|
|
||||||
* av_render_form_blocks($data, $slug) → string
|
|
||||||
* av_render_overview_blocks($cards, $hero, $bg) → string (hubs)
|
|
||||||
* av_render_home_blocks() → string
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
/* =================================================================== */
|
|
||||||
/* Block helpers */
|
|
||||||
/* =================================================================== */
|
|
||||||
|
|
||||||
function av_block_html($html) {
|
|
||||||
$html = trim($html);
|
|
||||||
return "<!-- wp:html -->\n{$html}\n<!-- /wp:html -->\n\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_block_heading($text, $level = 2, $class = '') {
|
|
||||||
$tag = 'h' . max(1, min(6, (int) $level));
|
|
||||||
$class = $class ? ' ' . esc_attr($class) : '';
|
|
||||||
$args = json_encode(['level' => (int) $level, 'className' => trim($class)]);
|
|
||||||
return "<!-- wp:heading {$args} -->\n<{$tag} class=\"wp-block-heading{$class}\">" . esc_html($text) . "</{$tag}>\n<!-- /wp:heading -->\n\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_block_paragraph($text, $class = '') {
|
|
||||||
$args = $class ? json_encode(['className' => $class]) . ' ' : '';
|
|
||||||
$cls = $class ? ' class="' . esc_attr($class) . '"' : '';
|
|
||||||
return "<!-- wp:paragraph {$args}-->\n<p{$cls}>" . esc_html($text) . "</p>\n<!-- /wp:paragraph -->\n\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_block_list($items) {
|
|
||||||
$out = "<!-- wp:list -->\n<ul>\n";
|
|
||||||
foreach ($items as $item) {
|
|
||||||
$out .= "<!-- wp:list-item -->\n<li>" . esc_html($item) . "</li>\n<!-- /wp:list-item -->\n";
|
|
||||||
}
|
|
||||||
$out .= "</ul>\n<!-- /wp:list -->\n\n";
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_block_button($label, $href, $variant = 'primary', $on_dark = false) {
|
|
||||||
$href = esc_url($href);
|
|
||||||
$label = esc_html($label);
|
|
||||||
$cls = 'av-btn av-btn--' . $variant . ($on_dark ? ' av-btn--on-dark' : '') . ' av-btn--lg';
|
|
||||||
return "<!-- wp:html -->\n<a class=\"{$cls}\" href=\"{$href}\">{$label}</a>\n<!-- /wp:html -->\n\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_block_buttons_row($ctas, $on_dark = false, $home_url = '') {
|
|
||||||
if (empty($ctas)) return '';
|
|
||||||
$out = "<!-- wp:html -->\n<div style=\"display:flex;flex-wrap:wrap;gap:var(--space-4);\">\n";
|
|
||||||
foreach ($ctas as $cta) {
|
|
||||||
$href = $home_url . ltrim($cta['href'], '/');
|
|
||||||
$variant = ($cta['variant'] === 'primary') ? 'av-btn--primary' : ($on_dark ? 'av-btn--on-dark av-btn--secondary' : 'av-btn--secondary');
|
|
||||||
$out .= '<a class="av-btn ' . esc_attr($variant) . ' av-btn--lg" href="' . esc_url($href) . '">' . esc_html($cta['label']) . "</a>\n";
|
|
||||||
}
|
|
||||||
$out .= "</div>\n<!-- /wp:html -->\n\n";
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =================================================================== */
|
|
||||||
/* Hero blocks */
|
|
||||||
/* =================================================================== */
|
|
||||||
|
|
||||||
function av_render_solution_hero($data, $home_url) {
|
|
||||||
$eyebrow = esc_html($data['eyebrow']);
|
|
||||||
$title = esc_html($data['title']);
|
|
||||||
$tagline = esc_html($data['tagline']);
|
|
||||||
$lead = esc_html($data['hero_lead']);
|
|
||||||
$ctas = '';
|
|
||||||
foreach ($data['ctas'] as $cta) {
|
|
||||||
$href = $home_url . ltrim($cta['href'], '/');
|
|
||||||
$variant = ($cta['variant'] === 'primary') ? 'av-btn--primary' : 'av-btn--secondary av-btn--on-dark';
|
|
||||||
$ctas .= '<a class="av-btn ' . $variant . ' av-btn--lg" href="' . esc_url($href) . '">' . esc_html($cta['label']) . "</a>\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
$html = <<<HTML
|
|
||||||
<section class="av-hero">
|
|
||||||
<div class="av-hero__media" aria-hidden="true">
|
|
||||||
<div style="width:100%;height:100%;background:radial-gradient(ellipse at 70% 30%, rgba(201,164,90,0.15), transparent 60%),linear-gradient(135deg, #081427 0%, #0B1F3A 50%, #13294B 100%);"></div>
|
|
||||||
</div>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-hero__inner">
|
|
||||||
<p class="av-hero__eyebrow">{$eyebrow}</p>
|
|
||||||
<h1 class="av-hero__headline" style="font-size:clamp(2.25rem,5vw,var(--text-display-lg));line-height:var(--leading-display-lg);">{$title}™ — {$tagline}</h1>
|
|
||||||
<p class="av-hero__sub">{$lead}</p>
|
|
||||||
<div class="av-hero__ctas">{$ctas}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
HTML;
|
|
||||||
return av_block_html($html);
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_render_industry_hero($data, $home_url) {
|
|
||||||
$eyebrow = esc_html($data['eyebrow']);
|
|
||||||
$tagline = esc_html($data['tagline']);
|
|
||||||
$lead = esc_html($data['hero_lead']);
|
|
||||||
$ctas = '';
|
|
||||||
foreach ($data['ctas'] as $cta) {
|
|
||||||
$href = $home_url . ltrim($cta['href'], '/');
|
|
||||||
$variant = ($cta['variant'] === 'primary') ? 'av-btn--primary' : 'av-btn--secondary av-btn--on-dark';
|
|
||||||
$ctas .= '<a class="av-btn ' . $variant . ' av-btn--lg" href="' . esc_url($href) . '">' . esc_html($cta['label']) . "</a>\n";
|
|
||||||
}
|
|
||||||
$html = <<<HTML
|
|
||||||
<section class="av-hero">
|
|
||||||
<div class="av-hero__media" aria-hidden="true">
|
|
||||||
<div style="width:100%;height:100%;background:radial-gradient(ellipse at 50% 40%, rgba(11,31,58,0.4), transparent 65%),linear-gradient(135deg, #081427 0%, #0B1F3A 50%, #1F3252 100%);"></div>
|
|
||||||
</div>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-hero__inner">
|
|
||||||
<p class="av-hero__eyebrow">{$eyebrow}</p>
|
|
||||||
<h1 class="av-hero__headline">{$tagline}</h1>
|
|
||||||
<p class="av-hero__sub">{$lead}</p>
|
|
||||||
<div class="av-hero__ctas">{$ctas}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
HTML;
|
|
||||||
return av_block_html($html);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =================================================================== */
|
|
||||||
/* Section renderer (shared by solution / industry / technology / */
|
|
||||||
/* editorial detail pages) */
|
|
||||||
/* =================================================================== */
|
|
||||||
|
|
||||||
function av_render_data_section($section, $is_dark, $home_url) {
|
|
||||||
$section_class = 'av-section' . ($is_dark ? ' av-section--dark' : '');
|
|
||||||
$eye_class = 'av-eyebrow' . ($is_dark ? ' av-eyebrow--gold' : '');
|
|
||||||
$eyebrow = esc_html($section['eyebrow'] ?? '');
|
|
||||||
$title = esc_html($section['title'] ?? '');
|
|
||||||
$lead = ! empty($section['lead']) ? '<p class="av-section__lead">' . esc_html($section['lead']) . '</p>' : '';
|
|
||||||
|
|
||||||
$head_html = <<<HTML
|
|
||||||
<section class="{$section_class}">
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-section__head">
|
|
||||||
<span class="{$eye_class}">{$eyebrow}</span>
|
|
||||||
<h2 class="av-section__title">{$title}</h2>
|
|
||||||
{$lead}
|
|
||||||
</div>
|
|
||||||
HTML;
|
|
||||||
|
|
||||||
$body = '';
|
|
||||||
|
|
||||||
switch ($section['type']) {
|
|
||||||
case 'prose':
|
|
||||||
$body = '<p class="av-section__lead" style="max-width:72ch;font-size:var(--text-body-lg);">' . esc_html($section['body'] ?? '') . '</p>';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'cards':
|
|
||||||
$body = '<div class="av-grid-3">';
|
|
||||||
foreach ($section['items'] as $card) {
|
|
||||||
$card_class = 'av-card' . ($is_dark ? ' is-on-dark' : '') . ' av-card--feature';
|
|
||||||
$body .= '<article class="' . $card_class . '"><div class="av-card__body">';
|
|
||||||
$body .= '<h3 class="av-card__title" style="font-size:var(--text-h4);">' . esc_html($card['title']) . '</h3>';
|
|
||||||
$body .= '<p class="av-card__excerpt">' . esc_html($card['body']) . '</p>';
|
|
||||||
$body .= '</div></article>';
|
|
||||||
}
|
|
||||||
$body .= '</div>';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'list':
|
|
||||||
$color = $is_dark ? 'color:var(--color-text-on-dark-muted);' : 'color:var(--color-text-secondary);';
|
|
||||||
$body = '<ul style="list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:var(--space-3);max-width:72ch;font-size:var(--text-body-lg);line-height:var(--leading-body-lg);' . $color . '">';
|
|
||||||
foreach ($section['items'] as $item) {
|
|
||||||
$body .= '<li style="padding-left:var(--space-6);position:relative;"><span aria-hidden="true" style="position:absolute;left:0;top:0.6em;width:8px;height:8px;background:var(--color-brand-gold);border-radius:50%;"></span>' . esc_html($item) . '</li>';
|
|
||||||
}
|
|
||||||
$body .= '</ul>';
|
|
||||||
if (! empty($section['cta'])) {
|
|
||||||
$href = $home_url . ltrim($section['cta']['href'], '/');
|
|
||||||
$linkcls = 'av-link' . ($is_dark ? ' av-link--on-dark' : '') . ' av-link--with-arrow';
|
|
||||||
$body .= '<p style="margin-top:var(--space-8);"><a class="' . $linkcls . '" href="' . esc_url($href) . '">' . esc_html($section['cta']['label']) . '</a></p>';
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'cta':
|
|
||||||
$body = '<p class="av-section__lead" style="max-width:72ch;margin-bottom:var(--space-6);">' . esc_html($section['body'] ?? '') . '</p>';
|
|
||||||
if (! empty($section['ctas'])) {
|
|
||||||
$body .= '<div style="display:flex;flex-wrap:wrap;gap:var(--space-4);">';
|
|
||||||
foreach ($section['ctas'] as $cta) {
|
|
||||||
$href = $home_url . ltrim($cta['href'], '/');
|
|
||||||
$variant = ($cta['variant'] === 'primary') ? 'av-btn--primary' : ($is_dark ? 'av-btn--on-dark av-btn--secondary' : 'av-btn--secondary');
|
|
||||||
$body .= '<a class="av-btn ' . $variant . ' av-btn--lg" href="' . esc_url($href) . '">' . esc_html($cta['label']) . "</a>";
|
|
||||||
}
|
|
||||||
$body .= '</div>';
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
$foot_html = "</div></section>";
|
|
||||||
return av_block_html($head_html . $body . $foot_html);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =================================================================== */
|
|
||||||
/* Conversion banner (shared) */
|
|
||||||
/* =================================================================== */
|
|
||||||
|
|
||||||
function av_render_conversion_banner($home_url) {
|
|
||||||
$html = <<<HTML
|
|
||||||
<section class="av-conversion-banner">
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-conversion-banner__head">
|
|
||||||
<h2 class="av-conversion-banner__title">Train like the threat is now.</h2>
|
|
||||||
<p class="av-conversion-banner__lead">Three paths into the platform — pick the one that fits where you are.</p>
|
|
||||||
</div>
|
|
||||||
<div class="av-conversion-banner__grid">
|
|
||||||
<div class="av-conversion-banner__card">
|
|
||||||
<h3 class="av-conversion-banner__card-title">Request a Demo</h3>
|
|
||||||
<p class="av-conversion-banner__card-desc">A 30-minute tailored session with our team.</p>
|
|
||||||
<a href="{$home_url}request-demo/" class="av-btn av-btn--primary av-btn--md">Schedule a demo</a>
|
|
||||||
</div>
|
|
||||||
<div class="av-conversion-banner__card">
|
|
||||||
<h3 class="av-conversion-banner__card-title">Request a T&E Kit</h3>
|
|
||||||
<p class="av-conversion-banner__card-desc">A PROSERVE case on loan at your facility for 5 to 10 days.</p>
|
|
||||||
<a href="{$home_url}request-te-kit/" class="av-btn av-btn--on-dark av-btn--secondary av-btn--md">Request the kit</a>
|
|
||||||
</div>
|
|
||||||
<div class="av-conversion-banner__card">
|
|
||||||
<h3 class="av-conversion-banner__card-title">Request a Quote</h3>
|
|
||||||
<p class="av-conversion-banner__card-desc">A detailed quote and deployment plan within 5 working days.</p>
|
|
||||||
<a href="{$home_url}request-quote/" class="av-btn av-btn--on-dark av-btn--secondary av-btn--md">Get a quote</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
HTML;
|
|
||||||
return av_block_html($html);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =================================================================== */
|
|
||||||
/* Top-level renderers */
|
|
||||||
/* =================================================================== */
|
|
||||||
|
|
||||||
function av_render_solution_blocks($data) {
|
|
||||||
$home_url = home_url('/');
|
|
||||||
$out = av_render_solution_hero($data, $home_url);
|
|
||||||
foreach ($data['sections'] as $idx => $section) {
|
|
||||||
$out .= av_render_data_section($section, ($idx % 2 === 1), $home_url);
|
|
||||||
}
|
|
||||||
if (! empty($data['upsell'])) {
|
|
||||||
$upsell_html = '<section class="av-section" style="background-color:var(--color-bg-subtle);"><div class="av-container">';
|
|
||||||
$upsell_html .= '<div class="av-section__head">';
|
|
||||||
$upsell_html .= '<span class="av-eyebrow">Comparison and upgrade</span>';
|
|
||||||
$upsell_html .= '<h2 class="av-section__title">' . esc_html($data['upsell']['title']) . '</h2>';
|
|
||||||
$upsell_html .= '<p class="av-section__lead">' . esc_html($data['upsell']['body']) . '</p>';
|
|
||||||
$upsell_html .= '</div><div style="display:flex;flex-wrap:wrap;gap:var(--space-4);">';
|
|
||||||
foreach ($data['upsell']['links'] as $link) {
|
|
||||||
$upsell_html .= '<a class="av-btn av-btn--secondary av-btn--md" href="' . esc_url($home_url . ltrim($link['href'], '/')) . '">' . esc_html($link['label']) . "</a>";
|
|
||||||
}
|
|
||||||
$upsell_html .= '</div></div></section>';
|
|
||||||
$out .= av_block_html($upsell_html);
|
|
||||||
}
|
|
||||||
$out .= av_render_conversion_banner($home_url);
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_render_industry_blocks($data) {
|
|
||||||
$home_url = home_url('/');
|
|
||||||
$out = av_render_industry_hero($data, $home_url);
|
|
||||||
foreach ($data['sections'] as $idx => $section) {
|
|
||||||
$out .= av_render_data_section($section, ($idx % 2 === 1), $home_url);
|
|
||||||
}
|
|
||||||
$out .= av_render_conversion_banner($home_url);
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_render_technology_blocks($data) {
|
|
||||||
return av_render_industry_blocks($data); // identical schema
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_render_editorial_blocks($data) {
|
|
||||||
return av_render_industry_blocks($data);
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_render_form_blocks($data, $slug) {
|
|
||||||
$home_url = home_url('/');
|
|
||||||
|
|
||||||
// Hero
|
|
||||||
$eyebrow = esc_html($data['eyebrow']);
|
|
||||||
$tagline = esc_html($data['tagline']);
|
|
||||||
$lead = esc_html($data['hero_lead']);
|
|
||||||
$shortcuts_html = '';
|
|
||||||
if (! empty($data['shortcuts'])) {
|
|
||||||
$shortcuts_html .= '<div class="av-hero__ctas">';
|
|
||||||
foreach ($data['shortcuts'] as $sc) {
|
|
||||||
$variant = ($sc['variant'] === 'primary') ? 'av-btn--primary' : 'av-btn--on-dark av-btn--secondary';
|
|
||||||
$shortcuts_html .= '<a class="av-btn ' . $variant . ' av-btn--md" href="' . esc_url($home_url . ltrim($sc['href'], '/')) . '">' . esc_html($sc['label']) . '</a>';
|
|
||||||
}
|
|
||||||
$shortcuts_html .= '</div>';
|
|
||||||
}
|
|
||||||
$hero_html = <<<HTML
|
|
||||||
<section class="av-hero">
|
|
||||||
<div class="av-hero__media" aria-hidden="true">
|
|
||||||
<div style="width:100%;height:100%;background:radial-gradient(ellipse at 50% 30%, rgba(201,164,90,0.12), transparent 60%),linear-gradient(135deg, #081427 0%, #0B1F3A 50%, #13294B 100%);"></div>
|
|
||||||
</div>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-hero__inner">
|
|
||||||
<p class="av-hero__eyebrow">{$eyebrow}</p>
|
|
||||||
<h1 class="av-hero__headline" style="font-size:clamp(2.25rem,5vw,var(--text-display-lg));line-height:var(--leading-display-lg);">{$tagline}</h1>
|
|
||||||
<p class="av-hero__sub">{$lead}</p>
|
|
||||||
{$shortcuts_html}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
HTML;
|
|
||||||
$out = av_block_html($hero_html);
|
|
||||||
|
|
||||||
// The form itself stays a shortcode — added by template wrapper instead.
|
|
||||||
// To keep the page editable, we drop a placeholder shortcode that the
|
|
||||||
// child theme's `av_form` shortcode handler renders on the front-end.
|
|
||||||
$out .= av_block_html('<div class="av-form-placeholder">[av_form slug="' . esc_attr($slug) . '"]</div>');
|
|
||||||
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function av_render_overview_blocks($cards, $hero_eyebrow, $hero_title, $hero_sub) {
|
|
||||||
$home_url = home_url('/');
|
|
||||||
$hero_html = <<<HTML
|
|
||||||
<section class="av-hero">
|
|
||||||
<div class="av-hero__media" aria-hidden="true">
|
|
||||||
<div style="width:100%;height:100%;background:linear-gradient(135deg, #081427 0%, #0B1F3A 50%, #13294B 100%);"></div>
|
|
||||||
</div>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-hero__inner">
|
|
||||||
<p class="av-hero__eyebrow">{$hero_eyebrow}</p>
|
|
||||||
<h1 class="av-hero__headline">{$hero_title}</h1>
|
|
||||||
<p class="av-hero__sub">{$hero_sub}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
HTML;
|
|
||||||
$out = av_block_html($hero_html);
|
|
||||||
|
|
||||||
$cards_html = '<section class="av-section"><div class="av-container"><div class="av-grid-3">';
|
|
||||||
foreach ($cards as $c) {
|
|
||||||
$href = $home_url . ltrim($c['href'], '/');
|
|
||||||
$cards_html .= '<article class="av-card"><a href="' . esc_url($href) . '" class="av-card__media" tabindex="-1" style="background:linear-gradient(135deg,#1F3252,#0B1F3A);display:block;"></a>';
|
|
||||||
$cards_html .= '<div class="av-card__body">';
|
|
||||||
$cards_html .= '<span class="av-card__eyebrow">' . esc_html($c['eyebrow'] ?? '') . '</span>';
|
|
||||||
$cards_html .= '<h2 class="av-card__title"><a href="' . esc_url($href) . '" style="color:inherit;text-decoration:none;">' . esc_html($c['title']) . '</a></h2>';
|
|
||||||
$cards_html .= '<p class="av-card__excerpt">' . esc_html($c['excerpt']) . '</p>';
|
|
||||||
$cards_html .= '<div class="av-card__cta"><a href="' . esc_url($href) . '" class="av-link av-link--with-arrow">' . esc_html($c['cta']) . '</a></div>';
|
|
||||||
$cards_html .= '</div></article>';
|
|
||||||
}
|
|
||||||
$cards_html .= '</div></div></section>';
|
|
||||||
$out .= av_block_html($cards_html);
|
|
||||||
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
<?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;
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — SEO module
|
|
||||||
*
|
|
||||||
* Hosts custom Schema.org JSON-LD and any meta-tag overrides specific to
|
|
||||||
* Asterion VR. Most generic SEO (title tags, OG, sitemap) is delegated to
|
|
||||||
* Rank Math or Yoast (per BRIEF section 4).
|
|
||||||
*
|
|
||||||
* Planned :
|
|
||||||
* - Organization schema on home + footer-rendered pages
|
|
||||||
* - Product schema on /solutions/* pages (each tier = a Product)
|
|
||||||
* - Article schema on /insights/<slug>/
|
|
||||||
* - FAQPage schema on /trust/ and conversion templates
|
|
||||||
* - BreadcrumbList schema site-wide
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
* @since 0.1.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inject Organization JSON-LD on the home page.
|
|
||||||
* Safe minimum — extended once logos/SameAs URLs are confirmed.
|
|
||||||
*/
|
|
||||||
add_action('wp_head', function () {
|
|
||||||
if (! is_front_page()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$org = [
|
|
||||||
'@context' => 'https://schema.org',
|
|
||||||
'@type' => 'Organization',
|
|
||||||
'name' => 'Asterion VR',
|
|
||||||
'url' => home_url('/'),
|
|
||||||
'logo' => get_stylesheet_directory_uri() . '/assets/img/logo-asterion-vr.svg',
|
|
||||||
'foundingDate' => '2016',
|
|
||||||
'address' => [
|
|
||||||
'@type' => 'PostalAddress',
|
|
||||||
'addressLocality' => 'Montgermont',
|
|
||||||
'addressRegion' => 'Bretagne',
|
|
||||||
'addressCountry' => 'FR',
|
|
||||||
],
|
|
||||||
'sameAs' => [
|
|
||||||
// TODO : confirm public profiles with client.
|
|
||||||
// 'https://www.linkedin.com/company/asterion-vr/',
|
|
||||||
// 'https://www.youtube.com/@asterionvr',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
printf(
|
|
||||||
"<script type=\"application/ld+json\">%s</script>\n",
|
|
||||||
wp_json_encode($org, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
|
|
||||||
);
|
|
||||||
}, 5);
|
|
||||||
|
|
||||||
// TODO : Product schema for /solutions/<tier>/ pages.
|
|
||||||
// TODO : Article schema for /insights/<slug>/ pages.
|
|
||||||
// TODO : FAQPage schema for /trust/ and conversion pages.
|
|
||||||
// TODO : BreadcrumbList schema site-wide.
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — Shortcodes
|
|
||||||
*
|
|
||||||
* Lets the migrated post_content reference dynamic blocks (form, recent
|
|
||||||
* posts, etc.) without hardcoding them in the Gutenberg payload.
|
|
||||||
*
|
|
||||||
* [av_form slug="request-demo"] — renders one of the 4 conversion forms
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
add_shortcode('av_form', function ($atts) {
|
|
||||||
$atts = shortcode_atts([
|
|
||||||
'slug' => '',
|
|
||||||
], $atts, 'av_form');
|
|
||||||
|
|
||||||
$slug = sanitize_key($atts['slug']);
|
|
||||||
if (! $slug) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = include AV_THEME_DIR . '/inc/forms-data.php';
|
|
||||||
if (! isset($data[$slug])) {
|
|
||||||
return '<p style="color:var(--color-error);">[av_form] : unknown slug "' . esc_html($slug) . '"</p>';
|
|
||||||
}
|
|
||||||
|
|
||||||
set_query_var('av_form', $data[$slug]);
|
|
||||||
set_query_var('av_form_slug_override', $slug);
|
|
||||||
|
|
||||||
ob_start();
|
|
||||||
get_template_part('template-parts/conversion-form');
|
|
||||||
return ob_get_clean();
|
|
||||||
});
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — Default index template
|
|
||||||
*
|
|
||||||
* Fallback used by WordPress when no more specific template matches.
|
|
||||||
* Renders standard post lists for archives and the blog hub.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
?>
|
|
||||||
|
|
||||||
<section class="av-section">
|
|
||||||
<div class="av-container av-container--narrow">
|
|
||||||
|
|
||||||
<?php if (have_posts()) : ?>
|
|
||||||
<header class="av-section__head">
|
|
||||||
<?php if (is_archive()) : ?>
|
|
||||||
<h1 class="av-section__title"><?php the_archive_title(); ?></h1>
|
|
||||||
<?php $desc = get_the_archive_description(); if ($desc) : ?>
|
|
||||||
<div class="av-section__lead"><?php echo wp_kses_post($desc); ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php elseif (is_search()) : ?>
|
|
||||||
<h1 class="av-section__title"><?php printf(esc_html__('Search results for « %s »', 'asterion-bricks'), get_search_query()); ?></h1>
|
|
||||||
<?php elseif (is_home()) : ?>
|
|
||||||
<h1 class="av-section__title"><?php esc_html_e('Insights', 'asterion-bricks'); ?></h1>
|
|
||||||
<?php endif; ?>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="av-stack av-stack--xl">
|
|
||||||
<?php while (have_posts()) : the_post(); ?>
|
|
||||||
<article <?php post_class('av-card av-card--blog'); ?> style="display:block;background:transparent;box-shadow:none;border-bottom:1px solid var(--color-border-subtle);padding-bottom:var(--space-8);">
|
|
||||||
<header style="display:flex;flex-direction:column;gap:var(--space-2);margin-bottom:var(--space-3);">
|
|
||||||
<span class="av-eyebrow"><?php echo esc_html(get_the_date()); ?></span>
|
|
||||||
<h2 style="font-family:var(--font-display);font-size:var(--text-h2);line-height:var(--leading-h2);font-weight:var(--font-weight-bold);">
|
|
||||||
<a href="<?php the_permalink(); ?>" style="color:inherit;text-decoration:none;"><?php the_title(); ?></a>
|
|
||||||
</h2>
|
|
||||||
</header>
|
|
||||||
<div style="color:var(--color-text-secondary);font-size:var(--text-body-md);line-height:var(--leading-body-md);">
|
|
||||||
<?php the_excerpt(); ?>
|
|
||||||
</div>
|
|
||||||
<p style="margin-top:var(--space-3);">
|
|
||||||
<a href="<?php the_permalink(); ?>" class="av-link av-link--with-arrow"><?php esc_html_e('Read more', 'asterion-bricks'); ?></a>
|
|
||||||
</p>
|
|
||||||
</article>
|
|
||||||
<?php endwhile; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav class="av-pagination" style="margin-top:var(--space-12);">
|
|
||||||
<?php the_posts_pagination([
|
|
||||||
'mid_size' => 1,
|
|
||||||
'prev_text' => esc_html__('← Previous', 'asterion-bricks'),
|
|
||||||
'next_text' => esc_html__('Next →', 'asterion-bricks'),
|
|
||||||
]); ?>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<?php else : ?>
|
|
||||||
<header class="av-section__head">
|
|
||||||
<h1 class="av-section__title"><?php esc_html_e('Nothing here yet.', 'asterion-bricks'); ?></h1>
|
|
||||||
<p class="av-section__lead"><?php esc_html_e('No content matches your request. Try a different search, or head back home.', 'asterion-bricks'); ?></p>
|
|
||||||
</header>
|
|
||||||
<p><a class="av-btn av-btn--primary av-btn--md" href="<?php echo esc_url(home_url('/')); ?>"><?php esc_html_e('Back to home', 'asterion-bricks'); ?></a></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<?php get_footer(); ?>
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — Default page template
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
|
|
||||||
$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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get_footer();
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — Conversion form partial
|
|
||||||
*
|
|
||||||
* Renders one conversion page (Demo / T&E / Quote / Contact). Two-column
|
|
||||||
* hero (form left, promise right) + optional eligibility list + FAQ.
|
|
||||||
*
|
|
||||||
* Data passed via `set_query_var('av_form', $form_data)`.
|
|
||||||
*
|
|
||||||
* Form posts to admin-post.php with action=av_form_submit; the handler in
|
|
||||||
* inc/form-handler.php validates the nonce, sanitizes, and emails the lead
|
|
||||||
* to jerome.foucher@asterionvr.com.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
$form = get_query_var('av_form');
|
|
||||||
if (! is_array($form)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$home_url = home_url('/');
|
|
||||||
$slug_override = get_query_var('av_form_slug_override');
|
|
||||||
$slug = $slug_override ?: get_post_field('post_name', get_queried_object_id());
|
|
||||||
|
|
||||||
// Surface success/error from query string (set by the handler on redirect).
|
|
||||||
$av_status = isset($_GET['av_status']) ? sanitize_key($_GET['av_status']) : '';
|
|
||||||
?>
|
|
||||||
|
|
||||||
<!-- HERO -->
|
|
||||||
<section class="av-hero" aria-labelledby="form-headline">
|
|
||||||
<div class="av-hero__media" aria-hidden="true">
|
|
||||||
<div style="width:100%;height:100%;background:
|
|
||||||
radial-gradient(ellipse at 50% 30%, rgba(201,164,90,0.12), transparent 60%),
|
|
||||||
linear-gradient(135deg, #081427 0%, #0B1F3A 50%, #13294B 100%);"></div>
|
|
||||||
</div>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-hero__inner">
|
|
||||||
<p class="av-hero__eyebrow"><?php echo esc_html($form['eyebrow']); ?></p>
|
|
||||||
<h1 id="form-headline" class="av-hero__headline" style="font-size:clamp(2.25rem,5vw,var(--text-display-lg));line-height:var(--leading-display-lg);"><?php echo esc_html($form['tagline']); ?></h1>
|
|
||||||
<p class="av-hero__sub"><?php echo esc_html($form['hero_lead']); ?></p>
|
|
||||||
|
|
||||||
<?php if (! empty($form['shortcuts'])) : ?>
|
|
||||||
<div class="av-hero__ctas">
|
|
||||||
<?php foreach ($form['shortcuts'] as $sc) :
|
|
||||||
$variant = ($sc['variant'] === 'primary') ? 'av-btn--primary' : 'av-btn--on-dark av-btn--secondary';
|
|
||||||
?>
|
|
||||||
<a href="<?php echo esc_url($home_url . ltrim($sc['href'], '/')); ?>" class="av-btn <?php echo esc_attr($variant); ?> av-btn--md">
|
|
||||||
<?php echo esc_html($sc['label']); ?>
|
|
||||||
</a>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- TWO-COLUMN: form (left) + promise (right) -->
|
|
||||||
<section class="av-section" aria-labelledby="form-title">
|
|
||||||
<div class="av-container">
|
|
||||||
|
|
||||||
<?php if ($av_status === 'success') : ?>
|
|
||||||
<div role="status" aria-live="polite" style="padding:var(--space-6);background:rgba(31,139,76,0.08);border-left:3px solid var(--color-success);border-radius:var(--radius-sm);margin-bottom:var(--space-8);">
|
|
||||||
<strong><?php esc_html_e('Thank you — your request was sent.', 'asterion-bricks'); ?></strong><br>
|
|
||||||
<?php esc_html_e('We will reply within the timeframe stated above. Check your inbox (and spam) for confirmation.', 'asterion-bricks'); ?>
|
|
||||||
</div>
|
|
||||||
<?php elseif ($av_status === 'error') : ?>
|
|
||||||
<div role="alert" style="padding:var(--space-6);background:rgba(200,16,46,0.06);border-left:3px solid var(--color-error);border-radius:var(--radius-sm);margin-bottom:var(--space-8);">
|
|
||||||
<strong><?php esc_html_e('Something went wrong.', 'asterion-bricks'); ?></strong>
|
|
||||||
<?php esc_html_e(' Please try again, or email us directly at hello@asterionvr.com.', 'asterion-bricks'); ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div style="display:grid;grid-template-columns:1fr;gap:var(--space-12);" class="av-form-grid">
|
|
||||||
|
|
||||||
<!-- FORM -->
|
|
||||||
<div>
|
|
||||||
<h2 id="form-title" class="av-section__title" style="font-size:var(--text-h2);"><?php esc_html_e('Tell us about your unit.', 'asterion-bricks'); ?></h2>
|
|
||||||
<p class="av-section__lead" style="margin-bottom:var(--space-6);"><?php echo esc_html($form['reassurance']); ?></p>
|
|
||||||
|
|
||||||
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" class="av-form">
|
|
||||||
<?php wp_nonce_field('av_form_submit', 'av_nonce'); ?>
|
|
||||||
<input type="hidden" name="action" value="av_form_submit">
|
|
||||||
<input type="hidden" name="form_slug" value="<?php echo esc_attr($slug); ?>">
|
|
||||||
<input type="hidden" name="redirect_to" value="<?php echo esc_attr(get_permalink()); ?>">
|
|
||||||
<input type="text" name="av_hp" value="" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px;" aria-hidden="true">
|
|
||||||
|
|
||||||
<div class="av-stack av-stack--lg">
|
|
||||||
<?php foreach ($form['fields'] as $field) :
|
|
||||||
$field_id = 'av-' . esc_attr($field['name']);
|
|
||||||
$required = ! empty($field['required']);
|
|
||||||
?>
|
|
||||||
<div>
|
|
||||||
<label for="<?php echo esc_attr($field_id); ?>" class="av-label">
|
|
||||||
<?php echo esc_html($field['label']); ?>
|
|
||||||
<?php if ($required) : ?><span class="av-label__required" aria-hidden="true">*</span><?php endif; ?>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<?php if ($field['type'] === 'textarea') : ?>
|
|
||||||
<textarea
|
|
||||||
id="<?php echo esc_attr($field_id); ?>"
|
|
||||||
name="<?php echo esc_attr($field['name']); ?>"
|
|
||||||
class="av-textarea"
|
|
||||||
rows="5"
|
|
||||||
<?php echo $required ? 'required aria-required="true"' : ''; ?>></textarea>
|
|
||||||
|
|
||||||
<?php elseif ($field['type'] === 'select') : ?>
|
|
||||||
<select
|
|
||||||
id="<?php echo esc_attr($field_id); ?>"
|
|
||||||
name="<?php echo esc_attr($field['name']); ?>"
|
|
||||||
class="av-select"
|
|
||||||
<?php echo $required ? 'required aria-required="true"' : ''; ?>>
|
|
||||||
<?php foreach ($field['options'] as $opt) : ?>
|
|
||||||
<option value="<?php echo esc_attr($opt); ?>"<?php echo $opt === '' ? ' disabled selected' : ''; ?>>
|
|
||||||
<?php echo $opt === '' ? esc_html__('— select —', 'asterion-bricks') : esc_html($opt); ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<?php else : ?>
|
|
||||||
<input
|
|
||||||
type="<?php echo esc_attr($field['type']); ?>"
|
|
||||||
id="<?php echo esc_attr($field_id); ?>"
|
|
||||||
name="<?php echo esc_attr($field['name']); ?>"
|
|
||||||
class="av-input"
|
|
||||||
<?php echo $required ? 'required aria-required="true"' : ''; ?>
|
|
||||||
autocomplete="<?php echo $field['type'] === 'email' ? 'email' : 'on'; ?>">
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
|
|
||||||
<button type="submit" class="av-btn av-btn--primary av-btn--lg" style="width:auto;align-self:flex-start;">
|
|
||||||
<?php echo esc_html($form['submit_label'] ?? __('Send', 'asterion-bricks')); ?>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- PROMISE -->
|
|
||||||
<aside style="background:var(--color-bg-subtle);padding:var(--space-8);border-radius:var(--radius-md);align-self:start;">
|
|
||||||
<h2 style="font-family:var(--font-display);font-size:var(--text-h3);font-weight:var(--font-weight-semibold);margin-bottom:var(--space-4);">
|
|
||||||
<?php echo esc_html($form['promise_title']); ?>
|
|
||||||
</h2>
|
|
||||||
<ul style="list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:var(--space-3);font-size:var(--text-body-md);line-height:var(--leading-body-md);color:var(--color-text-secondary);">
|
|
||||||
<?php foreach ($form['promise_items'] as $item) : ?>
|
|
||||||
<li style="padding-left:var(--space-6);position:relative;">
|
|
||||||
<span aria-hidden="true" style="position:absolute;left:0;top:0.55em;width:8px;height:8px;background:var(--color-brand-gold);border-radius:50%;"></span>
|
|
||||||
<?php echo esc_html($item); ?>
|
|
||||||
</li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.av-form-grid { grid-template-columns: 1.4fr 1fr !important; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ELIGIBILITY (T&E only) -->
|
|
||||||
<?php if (! empty($form['eligibility'])) : ?>
|
|
||||||
<section class="av-section av-section--dark" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-section__head">
|
|
||||||
<span class="av-eyebrow av-eyebrow--gold"><?php esc_html_e('Eligibility', 'asterion-bricks'); ?></span>
|
|
||||||
<h2 class="av-section__title"><?php echo esc_html($form['eligibility']['title']); ?></h2>
|
|
||||||
<p class="av-section__lead"><?php echo esc_html($form['eligibility']['lead']); ?></p>
|
|
||||||
</div>
|
|
||||||
<ul style="list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:var(--space-3);max-width:72ch;color:var(--color-text-on-dark-muted);">
|
|
||||||
<?php foreach ($form['eligibility']['items'] as $item) : ?>
|
|
||||||
<li style="padding-left:var(--space-6);position:relative;">
|
|
||||||
<span aria-hidden="true" style="position:absolute;left:0;top:0.6em;width:8px;height:8px;background:var(--color-brand-gold);border-radius:50%;"></span>
|
|
||||||
<?php echo esc_html($item); ?>
|
|
||||||
</li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<!-- FAQ (Demo only) -->
|
|
||||||
<?php if (! empty($form['faq'])) : ?>
|
|
||||||
<section class="av-section" data-av-reveal>
|
|
||||||
<div class="av-container av-container--narrow">
|
|
||||||
<div class="av-section__head">
|
|
||||||
<span class="av-eyebrow"><?php esc_html_e('Frequently asked', 'asterion-bricks'); ?></span>
|
|
||||||
<h2 class="av-section__title"><?php esc_html_e('Quick answers.', 'asterion-bricks'); ?></h2>
|
|
||||||
</div>
|
|
||||||
<div class="av-stack av-stack--lg">
|
|
||||||
<?php foreach ($form['faq'] as $i => $qa) : ?>
|
|
||||||
<details style="border-bottom:1px solid var(--color-border-subtle);padding-block:var(--space-4);">
|
|
||||||
<summary style="font-family:var(--font-display);font-size:var(--text-h4);font-weight:var(--font-weight-semibold);cursor:pointer;list-style:none;display:flex;justify-content:space-between;align-items:center;">
|
|
||||||
<?php echo esc_html($qa['q']); ?>
|
|
||||||
<span aria-hidden="true">+</span>
|
|
||||||
</summary>
|
|
||||||
<p style="margin-top:var(--space-3);color:var(--color-text-secondary);font-size:var(--text-body-md);line-height:var(--leading-body-md);">
|
|
||||||
<?php echo esc_html($qa['a']); ?>
|
|
||||||
</p>
|
|
||||||
</details>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php endif; ?>
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — Industry detail partial
|
|
||||||
*
|
|
||||||
* Renders one industry page (Police / Special Forces / Military / Firefighters)
|
|
||||||
* from data passed via `set_query_var('av_industry', $industry_data)`.
|
|
||||||
*
|
|
||||||
* Section types : prose | cards | list | cta (same as solution-detail).
|
|
||||||
*
|
|
||||||
* Hero differs from solution-detail : eyebrow + H1 (tagline) only — no
|
|
||||||
* trademark concat, no upsell block.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
$industry = get_query_var('av_industry');
|
|
||||||
if (! is_array($industry)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$home_url = home_url('/');
|
|
||||||
?>
|
|
||||||
|
|
||||||
<!-- HERO -->
|
|
||||||
<section class="av-hero" aria-labelledby="industry-headline">
|
|
||||||
<div class="av-hero__media" aria-hidden="true">
|
|
||||||
<div style="width:100%;height:100%;background:
|
|
||||||
radial-gradient(ellipse at 50% 40%, rgba(11,31,58,0.4), transparent 65%),
|
|
||||||
linear-gradient(135deg, #081427 0%, #0B1F3A 50%, #1F3252 100%);"></div>
|
|
||||||
</div>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-hero__inner">
|
|
||||||
<p class="av-hero__eyebrow"><?php echo esc_html($industry['eyebrow']); ?></p>
|
|
||||||
<h1 id="industry-headline" class="av-hero__headline"><?php echo esc_html($industry['tagline']); ?></h1>
|
|
||||||
<p class="av-hero__sub"><?php echo esc_html($industry['hero_lead']); ?></p>
|
|
||||||
<div class="av-hero__ctas">
|
|
||||||
<?php foreach ($industry['ctas'] as $cta) :
|
|
||||||
$variant = ($cta['variant'] === 'primary') ? 'av-btn--primary' : 'av-btn--secondary av-btn--on-dark';
|
|
||||||
?>
|
|
||||||
<a href="<?php echo esc_url($home_url . ltrim($cta['href'], '/')); ?>" class="av-btn <?php echo esc_attr($variant); ?> av-btn--lg">
|
|
||||||
<?php echo esc_html($cta['label']); ?>
|
|
||||||
</a>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- SECTIONS -->
|
|
||||||
<?php
|
|
||||||
foreach ($industry['sections'] as $idx => $section) :
|
|
||||||
$is_dark = ($idx % 2 === 1);
|
|
||||||
$section_class = 'av-section' . ($is_dark ? ' av-section--dark' : '');
|
|
||||||
?>
|
|
||||||
<section class="<?php echo esc_attr($section_class); ?>" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-section__head">
|
|
||||||
<span class="av-eyebrow<?php echo $is_dark ? ' av-eyebrow--gold' : ''; ?>"><?php echo esc_html($section['eyebrow']); ?></span>
|
|
||||||
<h2 class="av-section__title"><?php echo esc_html($section['title']); ?></h2>
|
|
||||||
<?php if (! empty($section['lead'])) : ?>
|
|
||||||
<p class="av-section__lead"><?php echo esc_html($section['lead']); ?></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if ($section['type'] === 'prose' && ! empty($section['body'])) : ?>
|
|
||||||
<p class="av-section__lead" style="max-width:72ch;font-size:var(--text-body-lg);">
|
|
||||||
<?php echo esc_html($section['body']); ?>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<?php elseif ($section['type'] === 'cards' && ! empty($section['items'])) : ?>
|
|
||||||
<div class="av-grid-3">
|
|
||||||
<?php foreach ($section['items'] as $card) : ?>
|
|
||||||
<article class="av-card<?php echo $is_dark ? ' is-on-dark' : ''; ?> av-card--feature">
|
|
||||||
<div class="av-card__body">
|
|
||||||
<h3 class="av-card__title" style="font-size:var(--text-h4);"><?php echo esc_html($card['title']); ?></h3>
|
|
||||||
<p class="av-card__excerpt"><?php echo esc_html($card['body']); ?></p>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php elseif ($section['type'] === 'list' && ! empty($section['items'])) : ?>
|
|
||||||
<ul style="list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:var(--space-3);max-width:72ch;font-size:var(--text-body-lg);line-height:var(--leading-body-lg);<?php echo $is_dark ? 'color:var(--color-text-on-dark-muted);' : 'color:var(--color-text-secondary);'; ?>">
|
|
||||||
<?php foreach ($section['items'] as $item) : ?>
|
|
||||||
<li style="padding-left:var(--space-6);position:relative;">
|
|
||||||
<span aria-hidden="true" style="position:absolute;left:0;top:0.6em;width:8px;height:8px;background:var(--color-brand-gold);border-radius:50%;"></span>
|
|
||||||
<?php echo esc_html($item); ?>
|
|
||||||
</li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
<?php if (! empty($section['cta'])) : ?>
|
|
||||||
<p style="margin-top:var(--space-8);">
|
|
||||||
<a href="<?php echo esc_url($home_url . ltrim($section['cta']['href'], '/')); ?>" class="av-link<?php echo $is_dark ? ' av-link--on-dark' : ''; ?> av-link--with-arrow">
|
|
||||||
<?php echo esc_html($section['cta']['label']); ?>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
|
|
||||||
<!-- CONVERSION BANNER (shared) -->
|
|
||||||
<section class="av-conversion-banner">
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-conversion-banner__head">
|
|
||||||
<h2 class="av-conversion-banner__title">Train like the threat is now.</h2>
|
|
||||||
<p class="av-conversion-banner__lead">Three paths into the platform — pick the one that fits where you are.</p>
|
|
||||||
</div>
|
|
||||||
<div class="av-conversion-banner__grid">
|
|
||||||
<div class="av-conversion-banner__card">
|
|
||||||
<h3 class="av-conversion-banner__card-title">Request a Demo</h3>
|
|
||||||
<p class="av-conversion-banner__card-desc">A 30-minute tailored session with our team.</p>
|
|
||||||
<a href="<?php echo esc_url($home_url . 'request-demo/'); ?>" class="av-btn av-btn--primary av-btn--md">Schedule a demo</a>
|
|
||||||
</div>
|
|
||||||
<div class="av-conversion-banner__card">
|
|
||||||
<h3 class="av-conversion-banner__card-title">Request a T&E Kit</h3>
|
|
||||||
<p class="av-conversion-banner__card-desc">A PROSERVE case on loan at your facility for 5 to 10 days.</p>
|
|
||||||
<a href="<?php echo esc_url($home_url . 'request-te-kit/'); ?>" class="av-btn av-btn--on-dark av-btn--secondary av-btn--md">Request the kit</a>
|
|
||||||
</div>
|
|
||||||
<div class="av-conversion-banner__card">
|
|
||||||
<h3 class="av-conversion-banner__card-title">Request a Quote</h3>
|
|
||||||
<p class="av-conversion-banner__card-desc">A detailed quote and deployment plan within 5 working days.</p>
|
|
||||||
<a href="<?php echo esc_url($home_url . 'request-quote/'); ?>" class="av-btn av-btn--on-dark av-btn--secondary av-btn--md">Get a quote</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Asterion Bricks — Solution detail partial
|
|
||||||
*
|
|
||||||
* Renders one PROSERVE tier (MY / FLEX / ACADEMY / +) from data passed via
|
|
||||||
* `set_query_var('av_solution', $solution_data)` before include.
|
|
||||||
*
|
|
||||||
* Section types supported : prose | cards | list | cta
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
$solution = get_query_var('av_solution');
|
|
||||||
if (! is_array($solution)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$home_url = home_url('/');
|
|
||||||
?>
|
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<!-- HERO -->
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<section class="av-hero" aria-labelledby="solution-headline">
|
|
||||||
<div class="av-hero__media" aria-hidden="true">
|
|
||||||
<div style="width:100%;height:100%;background:
|
|
||||||
radial-gradient(ellipse at 70% 30%, rgba(201,164,90,0.15), transparent 60%),
|
|
||||||
linear-gradient(135deg, #081427 0%, #0B1F3A 50%, #13294B 100%);"></div>
|
|
||||||
</div>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-hero__inner">
|
|
||||||
<p class="av-hero__eyebrow"><?php echo esc_html($solution['eyebrow']); ?></p>
|
|
||||||
<h1 id="solution-headline" class="av-hero__headline" style="font-size:clamp(2.25rem,5vw,var(--text-display-lg));line-height:var(--leading-display-lg);">
|
|
||||||
<?php echo esc_html($solution['title']); ?>™ — <?php echo esc_html($solution['tagline']); ?>
|
|
||||||
</h1>
|
|
||||||
<p class="av-hero__sub"><?php echo esc_html($solution['hero_lead']); ?></p>
|
|
||||||
<div class="av-hero__ctas">
|
|
||||||
<?php foreach ($solution['ctas'] as $i => $cta) :
|
|
||||||
$on_dark = ($cta['variant'] === 'secondary') ? ' av-btn--on-dark' : '';
|
|
||||||
$variant = ($cta['variant'] === 'primary') ? 'av-btn--primary' : 'av-btn--secondary';
|
|
||||||
?>
|
|
||||||
<a href="<?php echo esc_url($home_url . ltrim($cta['href'], '/')); ?>" class="av-btn <?php echo esc_attr($variant); ?><?php echo esc_attr($on_dark); ?> av-btn--lg">
|
|
||||||
<?php echo esc_html($cta['label']); ?>
|
|
||||||
</a>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<!-- SECTIONS -->
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<?php
|
|
||||||
foreach ($solution['sections'] as $idx => $section) :
|
|
||||||
$is_dark = ($idx % 2 === 1);
|
|
||||||
$section_class = 'av-section' . ($is_dark ? ' av-section--dark' : '');
|
|
||||||
?>
|
|
||||||
<section class="<?php echo esc_attr($section_class); ?>" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-section__head">
|
|
||||||
<span class="av-eyebrow<?php echo $is_dark ? ' av-eyebrow--gold' : ''; ?>"><?php echo esc_html($section['eyebrow']); ?></span>
|
|
||||||
<h2 class="av-section__title"><?php echo esc_html($section['title']); ?></h2>
|
|
||||||
<?php if (! empty($section['lead'])) : ?>
|
|
||||||
<p class="av-section__lead"><?php echo esc_html($section['lead']); ?></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if ($section['type'] === 'prose' && ! empty($section['body'])) : ?>
|
|
||||||
<p class="av-section__lead" style="max-width:72ch;font-size:var(--text-body-lg);">
|
|
||||||
<?php echo esc_html($section['body']); ?>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<?php elseif ($section['type'] === 'cards' && ! empty($section['items'])) : ?>
|
|
||||||
<div class="av-grid-3">
|
|
||||||
<?php foreach ($section['items'] as $card) : ?>
|
|
||||||
<article class="av-card<?php echo $is_dark ? ' is-on-dark' : ''; ?> av-card--feature">
|
|
||||||
<div class="av-card__body">
|
|
||||||
<h3 class="av-card__title" style="font-size:var(--text-h4);"><?php echo esc_html($card['title']); ?></h3>
|
|
||||||
<p class="av-card__excerpt"><?php echo esc_html($card['body']); ?></p>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php elseif ($section['type'] === 'list' && ! empty($section['items'])) : ?>
|
|
||||||
<ul style="list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:var(--space-3);max-width:72ch;font-size:var(--text-body-lg);line-height:var(--leading-body-lg);<?php echo $is_dark ? 'color:var(--color-text-on-dark-muted);' : 'color:var(--color-text-secondary);'; ?>">
|
|
||||||
<?php foreach ($section['items'] as $item) : ?>
|
|
||||||
<li style="padding-left:var(--space-6);position:relative;">
|
|
||||||
<span aria-hidden="true" style="position:absolute;left:0;top:0.6em;width:8px;height:8px;background:var(--color-brand-gold);border-radius:50%;"></span>
|
|
||||||
<?php echo esc_html($item); ?>
|
|
||||||
</li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
<?php if (! empty($section['cta'])) : ?>
|
|
||||||
<p style="margin-top:var(--space-8);">
|
|
||||||
<a href="<?php echo esc_url($home_url . ltrim($section['cta']['href'], '/')); ?>" class="av-link<?php echo $is_dark ? ' av-link--on-dark' : ''; ?> av-link--with-arrow">
|
|
||||||
<?php echo esc_html($section['cta']['label']); ?>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php elseif ($section['type'] === 'cta' && ! empty($section['body'])) : ?>
|
|
||||||
<p class="av-section__lead" style="max-width:72ch;margin-bottom:var(--space-6);"><?php echo esc_html($section['body']); ?></p>
|
|
||||||
<?php if (! empty($section['ctas'])) : ?>
|
|
||||||
<div style="display:flex;flex-wrap:wrap;gap:var(--space-4);">
|
|
||||||
<?php foreach ($section['ctas'] as $cta) :
|
|
||||||
$on_dark = $is_dark ? ' av-btn--on-dark' : '';
|
|
||||||
$variant = ($cta['variant'] === 'primary') ? 'av-btn--primary' : 'av-btn--secondary';
|
|
||||||
?>
|
|
||||||
<a href="<?php echo esc_url($home_url . ltrim($cta['href'], '/')); ?>" class="av-btn <?php echo esc_attr($variant); ?><?php echo esc_attr($on_dark); ?> av-btn--lg">
|
|
||||||
<?php echo esc_html($cta['label']); ?>
|
|
||||||
</a>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<!-- UPSELL (when to step up to the next tier) -->
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<?php if (! empty($solution['upsell'])) : ?>
|
|
||||||
<section class="av-section" style="background-color:var(--color-bg-subtle);" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-section__head">
|
|
||||||
<span class="av-eyebrow"><?php esc_html_e('Comparison and upgrade', 'asterion-bricks'); ?></span>
|
|
||||||
<h2 class="av-section__title"><?php echo esc_html($solution['upsell']['title']); ?></h2>
|
|
||||||
<p class="av-section__lead"><?php echo esc_html($solution['upsell']['body']); ?></p>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex;flex-wrap:wrap;gap:var(--space-4);">
|
|
||||||
<?php foreach ($solution['upsell']['links'] as $link) : ?>
|
|
||||||
<a href="<?php echo esc_url($home_url . ltrim($link['href'], '/')); ?>" class="av-btn av-btn--secondary av-btn--md">
|
|
||||||
<?php echo esc_html($link['label']); ?>
|
|
||||||
</a>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<!-- CONVERSION BANNER (shared) -->
|
|
||||||
<!-- ============================================================== -->
|
|
||||||
<section class="av-conversion-banner">
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-conversion-banner__head">
|
|
||||||
<h2 class="av-conversion-banner__title"><?php esc_html_e('Ready to get hands-on?', 'asterion-bricks'); ?></h2>
|
|
||||||
<p class="av-conversion-banner__lead"><?php esc_html_e('Three paths into the platform — pick the one that fits where you are.', 'asterion-bricks'); ?></p>
|
|
||||||
</div>
|
|
||||||
<div class="av-conversion-banner__grid">
|
|
||||||
<div class="av-conversion-banner__card">
|
|
||||||
<h3 class="av-conversion-banner__card-title"><?php esc_html_e('Request a Demo', 'asterion-bricks'); ?></h3>
|
|
||||||
<p class="av-conversion-banner__card-desc"><?php esc_html_e('A 30-minute tailored session with our team. Walk through the configuration that matches your needs.', 'asterion-bricks'); ?></p>
|
|
||||||
<a href="<?php echo esc_url($home_url . 'request-demo/'); ?>" class="av-btn av-btn--primary av-btn--md"><?php esc_html_e('Schedule a demo', 'asterion-bricks'); ?></a>
|
|
||||||
</div>
|
|
||||||
<div class="av-conversion-banner__card">
|
|
||||||
<h3 class="av-conversion-banner__card-title"><?php esc_html_e('Request a T&E Kit', 'asterion-bricks'); ?></h3>
|
|
||||||
<p class="av-conversion-banner__card-desc"><?php esc_html_e('A PROSERVE case on loan at your facility for 5 to 10 days, with on-site support from our team.', 'asterion-bricks'); ?></p>
|
|
||||||
<a href="<?php echo esc_url($home_url . 'request-te-kit/'); ?>" class="av-btn av-btn--on-dark av-btn--secondary av-btn--md"><?php esc_html_e('Request the kit', 'asterion-bricks'); ?></a>
|
|
||||||
</div>
|
|
||||||
<div class="av-conversion-banner__card">
|
|
||||||
<h3 class="av-conversion-banner__card-title"><?php esc_html_e('Request a Quote', 'asterion-bricks'); ?></h3>
|
|
||||||
<p class="av-conversion-banner__card-desc"><?php esc_html_e('A detailed quote and deployment plan within 5 working days.', 'asterion-bricks'); ?></p>
|
|
||||||
<a href="<?php echo esc_url($home_url . 'request-quote/'); ?>" class="av-btn av-btn--on-dark av-btn--secondary av-btn--md"><?php esc_html_e('Get a quote', 'asterion-bricks'); ?></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Template Name: Conversion Form
|
|
||||||
*
|
|
||||||
* Custom page template for /request-demo/, /request-te-kit/, /request-quote/, /contact/.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
$slug = get_post_field('post_name', get_queried_object_id());
|
|
||||||
$data = include av_data_file('forms-data');
|
|
||||||
|
|
||||||
if (! isset($data[$slug])) {
|
|
||||||
get_header();
|
|
||||||
echo '<section class="av-section"><div class="av-container">';
|
|
||||||
echo '<h1>' . esc_html(get_the_title()) . '</h1>';
|
|
||||||
echo '<p>' . esc_html__('Form data not found for slug:', 'asterion-bricks') . ' ' . esc_html($slug) . '</p>';
|
|
||||||
echo '</div></section>';
|
|
||||||
get_footer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
set_query_var('av_form', $data[$slug]);
|
|
||||||
get_template_part('template-parts/conversion-form');
|
|
||||||
get_footer();
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Template Name: Editorial Detail
|
|
||||||
*
|
|
||||||
* Custom page template for editorial pages (Why Asterion, About, Customers
|
|
||||||
* hub, Trust, Partners). Reuses the industry-detail partial.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
$slug = get_post_field('post_name', get_queried_object_id());
|
|
||||||
$data = include av_data_file('editorial-data');
|
|
||||||
|
|
||||||
if (! isset($data[$slug])) {
|
|
||||||
get_header();
|
|
||||||
echo '<section class="av-section"><div class="av-container">';
|
|
||||||
echo '<h1>' . esc_html(get_the_title()) . '</h1>';
|
|
||||||
echo '<p>' . esc_html__('Editorial data not found for slug:', 'asterion-bricks') . ' ' . esc_html($slug) . '</p>';
|
|
||||||
echo '</div></section>';
|
|
||||||
get_footer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
set_query_var('av_industry', $data[$slug]);
|
|
||||||
get_template_part('template-parts/industry-detail');
|
|
||||||
get_footer();
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Template Name: Industries Overview
|
|
||||||
*
|
|
||||||
* Hub page /industries/ — lists the 4 industries as cards.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
$home_url = 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/',
|
|
||||||
'gradient' => 'linear-gradient(135deg,#1F3252,#0B1F3A)',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'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/',
|
|
||||||
'gradient' => 'linear-gradient(135deg,#1F3252,#0B1F3A)',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'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/',
|
|
||||||
'gradient' => 'linear-gradient(135deg,#1F3252,#0B1F3A)',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'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/',
|
|
||||||
'gradient' => 'linear-gradient(135deg,#1F3252,#0B1F3A)',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
?>
|
|
||||||
|
|
||||||
<section class="av-hero" aria-labelledby="industries-headline">
|
|
||||||
<div class="av-hero__media" aria-hidden="true">
|
|
||||||
<div style="width:100%;height:100%;background:
|
|
||||||
radial-gradient(ellipse at 50% 40%, rgba(11,31,58,0.4), transparent 65%),
|
|
||||||
linear-gradient(135deg, #081427 0%, #0B1F3A 50%, #1F3252 100%);"></div>
|
|
||||||
</div>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-hero__inner">
|
|
||||||
<p class="av-hero__eyebrow"><?php esc_html_e('Industries', 'asterion-bricks'); ?></p>
|
|
||||||
<h1 id="industries-headline" class="av-hero__headline">Built with operators, for operators.</h1>
|
|
||||||
<p class="av-hero__sub">
|
|
||||||
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.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="av-section" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-grid-4" style="grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));">
|
|
||||||
<?php foreach ($industries as $i) : ?>
|
|
||||||
<article class="av-card">
|
|
||||||
<a href="<?php echo esc_url($home_url . ltrim($i['href'], '/')); ?>" class="av-card__media" aria-hidden="true" tabindex="-1" style="background:<?php echo esc_attr($i['gradient']); ?>;display:block;"></a>
|
|
||||||
<div class="av-card__body">
|
|
||||||
<span class="av-card__eyebrow"><?php echo esc_html($i['eyebrow']); ?></span>
|
|
||||||
<h2 class="av-card__title"><a href="<?php echo esc_url($home_url . ltrim($i['href'], '/')); ?>" style="color:inherit;text-decoration:none;"><?php echo esc_html($i['title']); ?></a></h2>
|
|
||||||
<p class="av-card__excerpt"><?php echo esc_html($i['excerpt']); ?></p>
|
|
||||||
<div class="av-card__cta">
|
|
||||||
<a href="<?php echo esc_url($home_url . ltrim($i['href'], '/')); ?>" class="av-link av-link--with-arrow"><?php echo esc_html($i['title']); ?> training</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<?php get_footer(); ?>
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Template Name: Industry Detail
|
|
||||||
*
|
|
||||||
* Custom page template for /industries/police/, /industries/special-forces/,
|
|
||||||
* /industries/military/, /industries/firefighters/.
|
|
||||||
*
|
|
||||||
* Looks up the page slug in inc/industries-data.php and hands the matched
|
|
||||||
* tier off to template-parts/industry-detail.php.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
$slug = get_post_field('post_name', get_queried_object_id());
|
|
||||||
$data = include av_data_file('industries-data');
|
|
||||||
|
|
||||||
if (! isset($data[$slug])) {
|
|
||||||
get_header();
|
|
||||||
echo '<section class="av-section"><div class="av-container">';
|
|
||||||
echo '<h1>' . esc_html(get_the_title()) . '</h1>';
|
|
||||||
echo '<p>' . esc_html__('Industry data not found for slug:', 'asterion-bricks') . ' ' . esc_html($slug) . '</p>';
|
|
||||||
echo '</div></section>';
|
|
||||||
get_footer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
set_query_var('av_industry', $data[$slug]);
|
|
||||||
get_template_part('template-parts/industry-detail');
|
|
||||||
get_footer();
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Template Name: Insights Hub
|
|
||||||
*
|
|
||||||
* /insights/ — blog hub. Lists native `post` items as blog cards.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
$home_url = home_url('/');
|
|
||||||
?>
|
|
||||||
|
|
||||||
<section class="av-hero" aria-labelledby="insights-headline">
|
|
||||||
<div class="av-hero__media" aria-hidden="true">
|
|
||||||
<div style="width:100%;height:100%;background:
|
|
||||||
radial-gradient(ellipse at 60% 40%, rgba(201,164,90,0.12), transparent 60%),
|
|
||||||
linear-gradient(135deg, #081427 0%, #0B1F3A 70%, #13294B 100%);"></div>
|
|
||||||
</div>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-hero__inner">
|
|
||||||
<p class="av-hero__eyebrow"><?php esc_html_e('Insights', 'asterion-bricks'); ?></p>
|
|
||||||
<h1 id="insights-headline" class="av-hero__headline">Notes from the front of immersive training.</h1>
|
|
||||||
<p class="av-hero__sub">
|
|
||||||
Doctrine. Training science. Instructor stories. AAR data. Comparisons of VR vs live training. Found something useful? It probably came from a customer conversation. We publish what we learn.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="av-section" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<?php
|
|
||||||
// Categories filter chips (placeholders — wired to native `category` taxonomy if used).
|
|
||||||
$categories = ['Doctrine', 'Technology', 'Case Studies', 'Industry News', "Founder's Notes"];
|
|
||||||
?>
|
|
||||||
<div style="display:flex;flex-wrap:wrap;gap:var(--space-3);margin-bottom:var(--space-10);">
|
|
||||||
<?php foreach ($categories as $cat) : ?>
|
|
||||||
<span class="av-btn av-btn--ghost av-btn--sm" style="border:1px solid var(--color-border-default);"><?php echo esc_html($cat); ?></span>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php $posts = get_posts(['numberposts' => 12, 'post_status' => 'publish']); ?>
|
|
||||||
<?php if (! empty($posts)) : ?>
|
|
||||||
<div class="av-grid-3">
|
|
||||||
<?php foreach ($posts as $post) : setup_postdata($post); ?>
|
|
||||||
<article class="av-card av-card--blog">
|
|
||||||
<a href="<?php the_permalink(); ?>" class="av-card__media" aria-hidden="true" tabindex="-1" style="background:linear-gradient(135deg,#CCD3DC,#E5E9EE);"></a>
|
|
||||||
<div class="av-card__body">
|
|
||||||
<span class="av-card__eyebrow"><?php echo esc_html(get_the_date('M j, Y')); ?></span>
|
|
||||||
<h2 class="av-card__title"><a href="<?php the_permalink(); ?>" style="color:inherit;text-decoration:none;"><?php the_title(); ?></a></h2>
|
|
||||||
<p class="av-card__excerpt"><?php echo esc_html(wp_trim_words(get_the_excerpt(), 22)); ?></p>
|
|
||||||
<div class="av-card__cta"><a href="<?php the_permalink(); ?>" class="av-link av-link--with-arrow"><?php esc_html_e('Read more', 'asterion-bricks'); ?></a></div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<?php endforeach; wp_reset_postdata(); ?>
|
|
||||||
</div>
|
|
||||||
<?php else : ?>
|
|
||||||
<p style="color:var(--color-text-muted);max-width:64ch;">
|
|
||||||
<?php esc_html_e('Editorial blog launches with the site go-live. The 12-month content calendar is in the Strategy & Content brief, section B.4. New articles will appear here as soon as they ship.', 'asterion-bricks'); ?>
|
|
||||||
</p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="av-section av-section--dark" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div style="display:grid;grid-template-columns:1fr;gap:var(--space-8);align-items:center;" class="av-newsletter-row">
|
|
||||||
<div>
|
|
||||||
<span class="av-eyebrow av-eyebrow--gold"><?php esc_html_e('Newsletter', 'asterion-bricks'); ?></span>
|
|
||||||
<h2 style="font-family:var(--font-display);font-size:var(--text-h1);line-height:var(--leading-h1);color:var(--color-text-on-dark);margin:var(--space-3) 0;">Subscribe — one article per month.</h2>
|
|
||||||
<p style="color:var(--color-text-on-dark-muted);max-width:60ch;">
|
|
||||||
Insights from the front of immersive training. One email per month. No spam. Unsubscribe in one click.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<form method="post" action="#" style="display:flex;gap:var(--space-3);flex-wrap:wrap;align-items:flex-start;" aria-label="<?php esc_attr_e('Newsletter signup', 'asterion-bricks'); ?>">
|
|
||||||
<label for="av-insights-newsletter" class="av-sr-only"><?php esc_html_e('Your email', 'asterion-bricks'); ?></label>
|
|
||||||
<input type="email" id="av-insights-newsletter" name="email" class="av-input" placeholder="<?php esc_attr_e('your@email.com', 'asterion-bricks'); ?>" required style="flex:1;min-width:200px;">
|
|
||||||
<button type="submit" class="av-btn av-btn--primary av-btn--md"><?php esc_html_e('Subscribe', 'asterion-bricks'); ?></button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<style>@media (min-width: 768px) { .av-newsletter-row { grid-template-columns: 2fr 1.5fr !important; } }</style>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<?php get_footer(); ?>
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Template Name: Legal
|
|
||||||
*
|
|
||||||
* /legal/privacy/, /legal/terms/, /legal/cookies/, /legal/notice/.
|
|
||||||
* Renders the WP page content (filled in by counsel) inside the standard
|
|
||||||
* narrow container with a clean editorial hero.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
?>
|
|
||||||
|
|
||||||
<section class="av-section">
|
|
||||||
<div class="av-container av-container--narrow">
|
|
||||||
|
|
||||||
<header class="av-section__head">
|
|
||||||
<span class="av-eyebrow"><?php esc_html_e('Legal', 'asterion-bricks'); ?></span>
|
|
||||||
<h1 class="av-section__title"><?php the_title(); ?></h1>
|
|
||||||
<p class="av-section__lead">
|
|
||||||
<?php esc_html_e('Last updated:', 'asterion-bricks'); ?>
|
|
||||||
<?php echo esc_html(get_the_modified_date()); ?>
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="av-prose" style="font-size:var(--text-body-md);line-height:var(--leading-body-md);color:var(--color-text-secondary);">
|
|
||||||
<?php
|
|
||||||
if (have_posts()) :
|
|
||||||
while (have_posts()) : the_post();
|
|
||||||
the_content();
|
|
||||||
endwhile;
|
|
||||||
endif;
|
|
||||||
|
|
||||||
// Default skeleton when the page content is empty — populated by counsel later.
|
|
||||||
$content = get_post_field('post_content', get_queried_object_id());
|
|
||||||
if (trim($content) === '') :
|
|
||||||
?>
|
|
||||||
<p style="padding:var(--space-6);background:var(--color-bg-subtle);border-left:3px solid var(--color-warning);border-radius:var(--radius-sm);">
|
|
||||||
<strong><?php esc_html_e('Placeholder.', 'asterion-bricks'); ?></strong>
|
|
||||||
<?php esc_html_e('This page is to be drafted with legal counsel. The structure below outlines the sections expected.', 'asterion-bricks'); ?>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
// Slug-driven placeholder sections.
|
|
||||||
$slug = get_post_field('post_name', get_queried_object_id());
|
|
||||||
$skeletons = [
|
|
||||||
'privacy' => [
|
|
||||||
'Data controller (Asterion VR), contact, DPO if relevant',
|
|
||||||
'Data collected (form submissions, cookie identifiers, IP, etc.)',
|
|
||||||
'Purpose of processing (sales follow-up, newsletter, analytics)',
|
|
||||||
'Legal basis (consent, contract, legitimate interest)',
|
|
||||||
'Retention periods',
|
|
||||||
'Data sharing (subcontractors, hosts)',
|
|
||||||
'Data subject rights (access, rectification, deletion, portability, objection)',
|
|
||||||
'Complaint procedure (CNIL link)',
|
|
||||||
],
|
|
||||||
'terms' => [
|
|
||||||
'Site usage conditions',
|
|
||||||
'Intellectual property',
|
|
||||||
'Liability limitations',
|
|
||||||
'Applicable law and jurisdiction',
|
|
||||||
],
|
|
||||||
'cookies' => [
|
|
||||||
'List of cookies (functional, analytics, marketing)',
|
|
||||||
'Cookie purpose, retention, third parties',
|
|
||||||
'Consent management procedure',
|
|
||||||
],
|
|
||||||
'notice' => [
|
|
||||||
'Asterion VR — RCS 821 520 970 Rennes — NAF 6201Z — SIRET 821 520 970 00012',
|
|
||||||
'Adresse, téléphone, e-mail, directeur de la publication',
|
|
||||||
'Hébergeur',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
if (isset($skeletons[$slug])) :
|
|
||||||
?>
|
|
||||||
<h2 style="font-family:var(--font-display);font-size:var(--text-h3);margin-top:var(--space-10);margin-bottom:var(--space-4);"><?php esc_html_e('Sections to include', 'asterion-bricks'); ?></h2>
|
|
||||||
<ul style="display:flex;flex-direction:column;gap:var(--space-2);padding-left:var(--space-6);">
|
|
||||||
<?php foreach ($skeletons[$slug] as $line) : ?>
|
|
||||||
<li style="list-style:disc;"><?php echo esc_html($line); ?></li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<?php get_footer(); ?>
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Template Name: Resources
|
|
||||||
*
|
|
||||||
* /resources/ — datasheets, whitepapers, videos, brochures.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
$home_url = home_url('/');
|
|
||||||
|
|
||||||
$groups = [
|
|
||||||
[
|
|
||||||
'eyebrow' => 'Datasheets',
|
|
||||||
'title' => 'Technical specifications.',
|
|
||||||
'items' => [
|
|
||||||
['title' => 'PROSERVE — full datasheet', 'note' => 'Hardware specifications, software capabilities, scenarios. Gated, light form.'],
|
|
||||||
['title' => 'MY PROSERVE — quick datasheet', 'note' => '4-page summary.'],
|
|
||||||
['title' => 'PROSERVE FLEX — quick datasheet', 'note' => '4-page summary.'],
|
|
||||||
['title' => 'PROSERVE ACADEMY — quick datasheet', 'note' => '4-page summary.'],
|
|
||||||
['title' => 'PROSERVE+ — customization brochure', 'note' => 'Hardware add-ons, custom scenarios, process.'],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'eyebrow' => 'Whitepapers',
|
|
||||||
'title' => 'Long-form analyses.',
|
|
||||||
'items' => [
|
|
||||||
['title' => 'VR vs Live Fire: the 2026 cost-benefit comparison', 'note' => 'Quantified comparison across cost, fidelity, scalability, environmental.'],
|
|
||||||
['title' => "The Instructor's Guide to VR Training Programs", 'note' => "Methodology, drill design, AAR debriefing, assessment frameworks."],
|
|
||||||
['title' => 'Use of Force Training: how immersive simulation reduces tactical errors', 'note' => 'Reviews academic literature and Asterion deployment data.'],
|
|
||||||
['title' => 'Sovereignty in security simulation: why your training data must never leave your network', 'note' => 'For procurement and IT teams.'],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'eyebrow' => 'Videos',
|
|
||||||
'title' => 'Show, not tell.',
|
|
||||||
'items' => [
|
|
||||||
['title' => 'PROSERVE — 90-second product film', 'note' => 'Cinematic overview.'],
|
|
||||||
['title' => 'Instructor Cockpit walkthrough (3 minutes)', 'note' => 'Inside the instructor PC.'],
|
|
||||||
['title' => 'AAR replay — annotated example (4 minutes)', 'note' => 'A drill replayed with overlays.'],
|
|
||||||
['title' => 'MY PROSERVE — unboxing & first deployment (6 min)', 'note' => 'From mini-case to first scenario in real time.'],
|
|
||||||
['title' => 'Customer story — [name redacted unit] (8 min)', 'note' => 'Operational deployment, instructor interview, AAR excerpts.'],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'eyebrow' => 'Brochures',
|
|
||||||
'title' => 'For procurement and end-user briefs.',
|
|
||||||
'items' => [
|
|
||||||
['title' => 'PROSERVE general brochure', 'note' => '8 pages, EN/FR.'],
|
|
||||||
['title' => 'Industry-specific brochures', 'note' => 'Police, Special Forces, Military, Firefighters — one PDF each.'],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
?>
|
|
||||||
|
|
||||||
<section class="av-hero" aria-labelledby="resources-headline">
|
|
||||||
<div class="av-hero__media" aria-hidden="true">
|
|
||||||
<div style="width:100%;height:100%;background:linear-gradient(135deg, #081427 0%, #0B1F3A 50%, #13294B 100%);"></div>
|
|
||||||
</div>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-hero__inner">
|
|
||||||
<p class="av-hero__eyebrow"><?php esc_html_e('Resources', 'asterion-bricks'); ?></p>
|
|
||||||
<h1 id="resources-headline" class="av-hero__headline">Everything to evaluate, justify, and procure.</h1>
|
|
||||||
<p class="av-hero__sub">
|
|
||||||
Whether you are building an internal pitch, drafting a procurement specification, or briefing your team — these resources are designed to help.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<?php foreach ($groups as $idx => $group) :
|
|
||||||
$is_dark = ($idx % 2 === 1);
|
|
||||||
$section_class = 'av-section' . ($is_dark ? ' av-section--dark' : '');
|
|
||||||
?>
|
|
||||||
<section class="<?php echo esc_attr($section_class); ?>" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-section__head">
|
|
||||||
<span class="av-eyebrow<?php echo $is_dark ? ' av-eyebrow--gold' : ''; ?>"><?php echo esc_html($group['eyebrow']); ?></span>
|
|
||||||
<h2 class="av-section__title"><?php echo esc_html($group['title']); ?></h2>
|
|
||||||
</div>
|
|
||||||
<div class="av-grid-3">
|
|
||||||
<?php foreach ($group['items'] as $item) : ?>
|
|
||||||
<article class="av-card<?php echo $is_dark ? ' is-on-dark' : ''; ?>">
|
|
||||||
<div class="av-card__body">
|
|
||||||
<h3 class="av-card__title" style="font-size:var(--text-h4);"><?php echo esc_html($item['title']); ?></h3>
|
|
||||||
<p class="av-card__excerpt"><?php echo esc_html($item['note']); ?></p>
|
|
||||||
<div class="av-card__cta">
|
|
||||||
<a href="<?php echo esc_url($home_url . 'contact/'); ?>" class="av-link<?php echo $is_dark ? ' av-link--on-dark' : ''; ?> av-link--with-arrow"><?php esc_html_e('Request access', 'asterion-bricks'); ?></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
|
|
||||||
<?php get_footer(); ?>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Template Name: Solution Detail
|
|
||||||
*
|
|
||||||
* Custom page template assigned to /solutions/my-proserve/, /solutions/proserve-flex/,
|
|
||||||
* /solutions/proserve-academy/ and /solutions/customization/.
|
|
||||||
*
|
|
||||||
* Looks up the page slug in inc/solutions-data.php, hands the matched array off
|
|
||||||
* to template-parts/solution-detail.php, and renders the result wrapped in the
|
|
||||||
* standard header + footer.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
$slug = get_post_field('post_name', get_queried_object_id());
|
|
||||||
$data = include av_data_file('solutions-data');
|
|
||||||
|
|
||||||
if (! isset($data[$slug])) {
|
|
||||||
// Fallback : fall back to the basic page rendering if the slug isn't mapped.
|
|
||||||
get_header();
|
|
||||||
echo '<section class="av-section"><div class="av-container">';
|
|
||||||
echo '<h1>' . esc_html(get_the_title()) . '</h1>';
|
|
||||||
echo '<p>' . esc_html__('Solution data not found for slug:', 'asterion-bricks') . ' ' . esc_html($slug) . '</p>';
|
|
||||||
echo '</div></section>';
|
|
||||||
get_footer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
set_query_var('av_solution', $data[$slug]);
|
|
||||||
get_template_part('template-parts/solution-detail');
|
|
||||||
get_footer();
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Template Name: Solutions Overview
|
|
||||||
*
|
|
||||||
* Custom page template assigned to /solutions/. Renders the 4-tier comparison
|
|
||||||
* and PROSERVE+ teaser per PDF Strategie & Contenu D.2.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
$home_url = home_url('/');
|
|
||||||
?>
|
|
||||||
|
|
||||||
<!-- HERO -->
|
|
||||||
<section class="av-hero" aria-labelledby="solutions-headline">
|
|
||||||
<div class="av-hero__media" aria-hidden="true">
|
|
||||||
<div style="width:100%;height:100%;background:
|
|
||||||
radial-gradient(ellipse at 30% 30%, rgba(201,164,90,0.15), transparent 60%),
|
|
||||||
linear-gradient(135deg, #081427 0%, #0B1F3A 50%, #13294B 100%);"></div>
|
|
||||||
</div>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-hero__inner">
|
|
||||||
<p class="av-hero__eyebrow"><?php esc_html_e('Solutions', 'asterion-bricks'); ?></p>
|
|
||||||
<h1 id="solutions-headline" class="av-hero__headline" style="font-size:clamp(2.25rem,5vw,var(--text-display-lg));line-height:var(--leading-display-lg);">
|
|
||||||
One platform. Three formats. Zero rip-and-replace.
|
|
||||||
</h1>
|
|
||||||
<p class="av-hero__sub">
|
|
||||||
Whether you train one officer or sixteen, on a single mission or a full curriculum, PROSERVE™ delivers the same software, the same scenarios, the same instructor tools — at the right scale for your operation. Start solo. Add operators. Scale to a full academy. Your investment never resets.
|
|
||||||
</p>
|
|
||||||
<div class="av-hero__ctas">
|
|
||||||
<a href="<?php echo esc_url($home_url . 'request-demo/'); ?>" class="av-btn av-btn--primary av-btn--lg"><?php esc_html_e('Request a Demo', 'asterion-bricks'); ?></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- COMPARISON TABLE -->
|
|
||||||
<section class="av-section" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-section__head">
|
|
||||||
<span class="av-eyebrow"><?php esc_html_e('Compare', 'asterion-bricks'); ?></span>
|
|
||||||
<h2 class="av-section__title">Compare the three solutions.</h2>
|
|
||||||
<p class="av-section__lead">
|
|
||||||
Same software. Same scenarios. Same instructor tools. Different format, different scale.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="overflow-x:auto;border:1px solid var(--color-border-subtle);border-radius:var(--radius-md);">
|
|
||||||
<table style="width:100%;border-collapse:collapse;font-size:var(--text-body-md);min-width:720px;">
|
|
||||||
<thead>
|
|
||||||
<tr style="background:var(--color-brand-navy);color:var(--color-text-on-dark);">
|
|
||||||
<th scope="col" style="padding:var(--space-4);text-align:left;font-weight:var(--font-weight-semibold);"></th>
|
|
||||||
<th scope="col" style="padding:var(--space-4);text-align:left;font-weight:var(--font-weight-semibold);">MY PROSERVE</th>
|
|
||||||
<th scope="col" style="padding:var(--space-4);text-align:left;font-weight:var(--font-weight-semibold);">PROSERVE FLEX</th>
|
|
||||||
<th scope="col" style="padding:var(--space-4);text-align:left;font-weight:var(--font-weight-semibold);">PROSERVE ACADEMY</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php
|
|
||||||
$rows = [
|
|
||||||
['Trainees', '1 (solo)', '1–5 (team)', '1–16 (unit)'],
|
|
||||||
['Format', 'Mini-case', 'One case per operator + instructor PC', 'Dedicated room + rack + instructor station'],
|
|
||||||
['Floor space', '3 m² minimum', '9 m² to 100 m²', 'Up to 900 m²'],
|
|
||||||
['Setup time', '< 5 min', '< 5 min (solo) / 20 min (team)', 'Pre-installed (rapid recalibration)'],
|
|
||||||
['Multi-user cooperative', 'No', 'Yes (up to 5)', 'Yes (up to 16)'],
|
|
||||||
['Mobility', 'Ultra-portable', 'Highly portable', 'Fixed'],
|
|
||||||
['Best for', 'Individual continuous training (police municipale, on-call officers)', 'Tactical teams, mobile units, special operations', 'Academies, central training centres, large units'],
|
|
||||||
['Indicative price', 'From €9,000 / unit', 'On request', 'On request'],
|
|
||||||
];
|
|
||||||
foreach ($rows as $i => $row) :
|
|
||||||
$bg = ($i % 2 === 0) ? 'transparent' : 'var(--color-bg-subtle)';
|
|
||||||
?>
|
|
||||||
<tr style="background:<?php echo esc_attr($bg); ?>;">
|
|
||||||
<th scope="row" style="padding:var(--space-4);text-align:left;font-weight:var(--font-weight-semibold);color:var(--color-text-primary);border-bottom:1px solid var(--color-border-subtle);">
|
|
||||||
<?php echo esc_html($row[0]); ?>
|
|
||||||
</th>
|
|
||||||
<?php for ($c = 1; $c <= 3; $c++) : ?>
|
|
||||||
<td style="padding:var(--space-4);color:var(--color-text-secondary);border-bottom:1px solid var(--color-border-subtle);">
|
|
||||||
<?php echo esc_html($row[$c]); ?>
|
|
||||||
</td>
|
|
||||||
<?php endfor; ?>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:flex;flex-wrap:wrap;gap:var(--space-4);margin-top:var(--space-8);">
|
|
||||||
<a href="<?php echo esc_url($home_url . 'solutions/my-proserve/'); ?>" class="av-btn av-btn--secondary av-btn--md">MY PROSERVE</a>
|
|
||||||
<a href="<?php echo esc_url($home_url . 'solutions/proserve-flex/'); ?>" class="av-btn av-btn--secondary av-btn--md">PROSERVE FLEX</a>
|
|
||||||
<a href="<?php echo esc_url($home_url . 'solutions/proserve-academy/'); ?>" class="av-btn av-btn--secondary av-btn--md">PROSERVE ACADEMY</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- PROSERVE+ TEASER -->
|
|
||||||
<section class="av-section av-section--dark" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div style="display:grid;grid-template-columns:1fr;gap:var(--space-10);align-items:center;" class="av-plus-row">
|
|
||||||
<div style="aspect-ratio:4/3;background:linear-gradient(135deg,#13294B,#081427);border-radius:var(--radius-md);"></div>
|
|
||||||
<div style="display:flex;flex-direction:column;gap:var(--space-4);">
|
|
||||||
<span class="av-eyebrow av-eyebrow--gold"><?php esc_html_e('PROSERVE+', 'asterion-bricks'); ?></span>
|
|
||||||
<h2 class="av-section__title">Need something the catalog doesn’t list?</h2>
|
|
||||||
<p class="av-section__lead">
|
|
||||||
PROSERVE+™ is the customization track. Wrist trackers, haptic vests, custom weapon replicas, bespoke environments, scenarios in your unit’s exact uniforms and doctrine. Designed and produced in our French studio.
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<a href="<?php echo esc_url($home_url . 'solutions/customization/'); ?>" class="av-btn av-btn--primary av-btn--md">
|
|
||||||
<?php esc_html_e('Explore PROSERVE+ customization', 'asterion-bricks'); ?>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<style>@media (min-width: 1024px) { .av-plus-row { grid-template-columns: 1fr 1.2fr !important; } }</style>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<?php get_footer(); ?>
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Template Name: Technology Detail
|
|
||||||
*
|
|
||||||
* Custom page template for the 6 /technology/* sub-pages.
|
|
||||||
* Same data schema as industries — reuses template-parts/industry-detail.php.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
$slug = get_post_field('post_name', get_queried_object_id());
|
|
||||||
$data = include av_data_file('technology-data');
|
|
||||||
|
|
||||||
if (! isset($data[$slug])) {
|
|
||||||
get_header();
|
|
||||||
echo '<section class="av-section"><div class="av-container">';
|
|
||||||
echo '<h1>' . esc_html(get_the_title()) . '</h1>';
|
|
||||||
echo '<p>' . esc_html__('Technology data not found for slug:', 'asterion-bricks') . ' ' . esc_html($slug) . '</p>';
|
|
||||||
echo '</div></section>';
|
|
||||||
get_footer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
// industry-detail partial expects the data under `av_industry`.
|
|
||||||
set_query_var('av_industry', $data[$slug]);
|
|
||||||
get_template_part('template-parts/industry-detail');
|
|
||||||
get_footer();
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Template Name: Technology Overview
|
|
||||||
*
|
|
||||||
* Hub /technology/ — six pillars per PDF Strategie & Contenu D.11.
|
|
||||||
*
|
|
||||||
* @package AsterionBricks
|
|
||||||
*/
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
get_header();
|
|
||||||
$home_url = home_url('/');
|
|
||||||
|
|
||||||
$pillars = [
|
|
||||||
[
|
|
||||||
'eyebrow' => '01',
|
|
||||||
'title' => 'VR Hardware',
|
|
||||||
'body' => 'Fully wireless 5K headset. Wi-Fi 6E streaming from a dedicated rendering laptop. 90 Hz refresh, 120° field of view, sub-millimeter inside-out tracking. Designed for daily operational use.',
|
|
||||||
'cta' => 'Inside the hardware',
|
|
||||||
'href' => '/technology/vr-hardware/',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'eyebrow' => '02',
|
|
||||||
'title' => 'Software & AI',
|
|
||||||
'body' => 'Powered by Unreal Engine 5. Autonomous AI agents with vision, hearing, and personality. Real-time ballistic simulation. Locally hosted, no cloud dependency.',
|
|
||||||
'cta' => 'Inside the software',
|
|
||||||
'href' => '/technology/software-ai/',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'eyebrow' => '03',
|
|
||||||
'title' => 'Weapons & Tracking',
|
|
||||||
'body' => 'Universal Weapon Digitizer (UWeD) attaches to gas-blowback replicas, CO2 replicas, or real service weapons firing blanks. Recoil-induced tracking loss is corrected by an exclusive IMU compensation system.',
|
|
||||||
'cta' => 'Inside weapons & tracking',
|
|
||||||
'href' => '/technology/weapons-tracking/',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'eyebrow' => '04',
|
|
||||||
'title' => 'Scenarios Library',
|
|
||||||
'body' => 'Eleven environments. More than fifty validated scenarios. Updates four times per year. From fire range and IPSC to multi-room CQB, supermarket, residential apartment, and industrial sites.',
|
|
||||||
'cta' => 'Browse the scenario library',
|
|
||||||
'href' => '/technology/scenarios/',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'eyebrow' => '05',
|
|
||||||
'title' => 'Instructor Cockpit',
|
|
||||||
'body' => "Real-time supervision from any angle. Walk through walls. Switch into any trainee's view. Toggle shot trace, field of view, threat highlighting. Manipulate the scenario in real time.",
|
|
||||||
'cta' => 'Inside the instructor cockpit',
|
|
||||||
'href' => '/technology/instructor-cockpit/',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'eyebrow' => '06',
|
|
||||||
'title' => 'After-Action Review',
|
|
||||||
'body' => 'Frame-level replay from any camera angle. Per-trainee statistics. Per-unit dashboards. Comparable across sessions. Exportable for command review.',
|
|
||||||
'cta' => 'Inside after-action review',
|
|
||||||
'href' => '/technology/after-action-review/',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
?>
|
|
||||||
|
|
||||||
<section class="av-hero" aria-labelledby="technology-headline">
|
|
||||||
<div class="av-hero__media" aria-hidden="true">
|
|
||||||
<div style="width:100%;height:100%;background:
|
|
||||||
radial-gradient(ellipse at 50% 30%, rgba(201,164,90,0.18), transparent 60%),
|
|
||||||
linear-gradient(135deg, #081427 0%, #0B1F3A 50%, #13294B 100%);"></div>
|
|
||||||
</div>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-hero__inner">
|
|
||||||
<p class="av-hero__eyebrow"><?php esc_html_e('Technology', 'asterion-bricks'); ?></p>
|
|
||||||
<h1 id="technology-headline" class="av-hero__headline">Engineered for the unforgiving.</h1>
|
|
||||||
<p class="av-hero__sub">
|
|
||||||
Six layers of technology, each engineered against a real failure mode of conventional simulators. Wireless 5K visuals. Real-recoil weapons. Autonomous AI agents. Ballistic physics. An instructor cockpit that sees through walls. And an after-action review that ends every session with data.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="av-section" data-av-reveal>
|
|
||||||
<div class="av-container">
|
|
||||||
<div class="av-section__head">
|
|
||||||
<span class="av-eyebrow"><?php esc_html_e('The six pillars', 'asterion-bricks'); ?></span>
|
|
||||||
<h2 class="av-section__title">The six pillars of PROSERVE™.</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="av-grid-3">
|
|
||||||
<?php foreach ($pillars as $p) : ?>
|
|
||||||
<article class="av-card av-card--feature">
|
|
||||||
<div class="av-card__body">
|
|
||||||
<span class="av-card__eyebrow" style="color:var(--color-brand-gold);"><?php echo esc_html($p['eyebrow']); ?></span>
|
|
||||||
<h3 class="av-card__title"><?php echo esc_html($p['title']); ?></h3>
|
|
||||||
<p class="av-card__excerpt"><?php echo esc_html($p['body']); ?></p>
|
|
||||||
<div class="av-card__cta">
|
|
||||||
<a href="<?php echo esc_url($home_url . ltrim($p['href'], '/')); ?>" class="av-link av-link--with-arrow"><?php echo esc_html($p['cta']); ?></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<?php get_footer(); ?>
|
|
||||||
Reference in New Issue
Block a user