581 lines
25 KiB
Markdown
581 lines
25 KiB
Markdown
# Raiju CMS — AI Agent Skills
|
||
|
||
Machine-readable instructions for common tasks. Each skill describes what to do, which files to touch, and how to verify the result.
|
||
|
||
**What Raiju does:** Raiju turns any HTML or PHP page into an editable website — no database required. You take an existing page (hand-crafted, designed in Figma, or vibe-coded with AI), replace hardcoded text, images, and links with PHP helper functions (`editableText()`, `editableImage()`, `editableLink()`), and get a fully functional CMS with inline editing. The helpers inject `data-*` attributes that the inline editor JavaScript discovers. Admins click to edit directly on the page. Content is stored as JSON files. Visitors see clean HTML. See `AI-AGENT-GUIDE.md` → "How Inline Editing Works" for the full attribute reference.
|
||
|
||
**Existing-site rule:** When converting an existing website, preserve the visual system first and only add editability around it. Keep the original CSS, JS, assets, fonts, class names, markup hierarchy, animation hooks, dimensions, spacing, colors, and responsive behavior unless the user explicitly requests a redesign. A visually different converted site is a migration bug, even when all fields are technically editable.
|
||
|
||
---
|
||
|
||
## Skill: make-page-editable
|
||
|
||
### Description
|
||
This is Raiju's primary use case. Take any existing HTML or PHP page — hand-built, designed in Figma and exported, or vibe-coded with AI — and turn it into a fully editable CMS page. Replace hardcoded text, images, and links with Raiju's PHP helpers. The result: admins can edit everything directly on the page, content is stored in JSON, and visitors see clean HTML. No database, no complex migration.
|
||
|
||
Before changing code, capture or inspect the source page section by section. After conversion, compare the Raiju page against the original at desktop and mobile widths. Header transparency, scroll states, gradients, accent lines, image masks, type weights, paddings, backgrounds, and animations must match unless intentionally changed.
|
||
|
||
### Automated Option
|
||
Run the converter tool to do this automatically:
|
||
```bash
|
||
php cli/convert.php my-page.html --slug=my-page --lang=en --dry-run
|
||
```
|
||
This parses the HTML, detects sections/headings/images/links/repeating patterns, and generates the PHP template + JSON file. Review the output with `--dry-run` first, then run without it to write files. See `cli/README.md` for all options.
|
||
|
||
The converter is a starter pass, not a license to rebuild the design. If the output drifts visually, repair the template and styles toward the source page instead of normalizing it to generic Raiju markup.
|
||
|
||
### Manual Steps (Prerequisites)
|
||
- Read `AI-AGENT-GUIDE.md` → "How Inline Editing Works" for the data-attribute system
|
||
- Read `AI-AGENT-GUIDE.md` → "Template API" for available editable functions
|
||
- Have the static HTML page ready
|
||
|
||
### Concept
|
||
Every piece of content that should be editable needs to be wrapped in an editable PHP function. The function reads from a JSON file and outputs `data-*` attributes that the inline editor recognizes:
|
||
|
||
| Static HTML | Editable replacement |
|
||
|---|---|
|
||
| `<h1>Welcome</h1>` | `<h1><?php echo editableText($_p, 'hero.title', 'Welcome'); ?></h1>` |
|
||
| `<p>Some text</p>` | `<p><?php echo editableText($_p, 'intro.text', 'Some text'); ?></p>` |
|
||
| `<div>Rich HTML</div>` | `<div><?php echo editableHtml($_p, 'about.content', '<p>Rich HTML</p>'); ?></div>` |
|
||
| `<a href="/x">Click</a>` | `<?php echo editableLink($_p, 'hero.cta', 'Click', '/x', 'btn'); ?>` |
|
||
| `<img src="x.jpg" alt="Y">` | `<?php echo editableImage($_p, 'hero.image', 'x.jpg', 'Y', 'img-class'); ?>` |
|
||
|
||
### Steps
|
||
|
||
1. **Add the PHP boilerplate** at the top of the page template:
|
||
```php
|
||
<?php
|
||
$pageTitle = 'Page Title';
|
||
$pageDescription = 'SEO description.';
|
||
$currentLang = 'en';
|
||
$currentPage = 'my-page';
|
||
$contentPage = 'en_my-page';
|
||
$basePath = '../';
|
||
|
||
include '../includes/header.php';
|
||
include '../includes/content-loader.php';
|
||
$_p = $contentPage;
|
||
?>
|
||
```
|
||
And the footer at the bottom:
|
||
```php
|
||
<?php include '../includes/footer.php'; ?>
|
||
```
|
||
|
||
2. **Replace every hardcoded text** with `editableText()` or `editableHtml()`:
|
||
- Use `editableText()` for single-line text (headings, labels, short paragraphs)
|
||
- Use `editableHtml()` for rich content with formatting (admin gets a floating toolbar with Bold, Italic, Link, Clean)
|
||
- Choose meaningful dot-notation keys: `section.field` (e.g. `hero.title`, `about.content`, `pricing.heading`)
|
||
|
||
3. **Replace every hardcoded link** with `editableLink()`:
|
||
```php
|
||
// Before: <a href="/contact" class="btn btn-primary">Contact Us</a>
|
||
// After:
|
||
<?php echo editableLink($_p, 'hero.cta', 'Contact Us', '/contact', 'btn btn-primary'); ?>
|
||
```
|
||
|
||
4. **Replace every content image** with `editableImage()`:
|
||
```php
|
||
// Before: <img src="assets/images/hero.jpg" alt="Hero image" class="hero__img">
|
||
// After:
|
||
<?php echo editableImage($_p, 'hero.image', 'assets/images/hero.jpg', 'Hero image', 'hero__img'); ?>
|
||
```
|
||
|
||
5. **For repeating content** (cards, features, team members), use the editable list system. For card-like items with multiple fields or image/link/icon fields, use `editableListGroupItemAttrs()` so editors get one structured modal for the whole item. For a single text-only item, keep `editableListItemAttrs()` and inline editing only:
|
||
```php
|
||
<?php $featureDefaults = ['title' => 'New', 'desc' => '', 'link' => ['text' => 'Learn more', 'href' => '#']]; ?>
|
||
<div class="cards" <?php echo editableListAttrs($_p, 'features.items', $featureDefaults); ?>>
|
||
<?php foreach (editableListItems($_p, 'features.items') as $i => $item): ?>
|
||
<div class="card" <?php echo editableListGroupItemAttrs($_p, 'features.items', $i, [], $featureDefaults, 'Feature'); ?>>
|
||
<h3><?php echo editableText($_p, "features.items.$i.title", 'Feature'); ?></h3>
|
||
<p><?php echo editableText($_p, "features.items.$i.desc", 'Description'); ?></p>
|
||
<?php echo editableLink($_p, "features.items.$i.link", 'Learn more', '#', 'card__link'); ?>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
```
|
||
|
||
6. **For editable paragraph blocks** (multiple paragraphs that admins can add, delete, and reorder), use `editableTextList()`:
|
||
```php
|
||
<div class="about-text">
|
||
<?php echo editableTextList($_p, 'about.paragraphs'); ?>
|
||
</div>
|
||
```
|
||
Each paragraph gets inline editing with the floating toolbar. JSON structure:
|
||
```json
|
||
{ "about": { "paragraphs": { "0": { "content": "First paragraph..." }, "1": { "content": "Second..." } } } }
|
||
```
|
||
|
||
7. **Create the matching JSON file** `content/pages/{lang}_{slug}.json` with all the keys used in the template:
|
||
```json
|
||
{
|
||
"page": "en_my-page",
|
||
"lang": "en",
|
||
"title": "Page Title",
|
||
"hero": {
|
||
"title": "Welcome",
|
||
"subtitle": "Tagline",
|
||
"cta": { "text": "Contact Us", "href": "/contact" },
|
||
"image": { "src": "assets/images/hero.jpg", "alt": "Hero image" }
|
||
},
|
||
"features": {
|
||
"heading": "Features",
|
||
"items": {
|
||
"0": { "title": "Feature 1", "desc": "Description 1" },
|
||
"1": { "title": "Feature 2", "desc": "Description 2" }
|
||
}
|
||
}
|
||
}
|
||
```
|
||
Note: List items use numbered-object format (`"0": {...}`) not arrays.
|
||
|
||
8. **Register the page** in `includes/nav-config.php` (`$PAGE_MAPPING` and optionally `$NAV_ITEMS`).
|
||
|
||
### What NOT to make editable
|
||
- Navigation structure (handled by `nav-config.php`)
|
||
- CSS classes, layout structure, HTML skeleton
|
||
- Decorative images that are part of the design (use CSS `background-image` instead)
|
||
- JavaScript behavior or interactive elements
|
||
|
||
### Validation
|
||
- PHP template renders without errors
|
||
- When logged in: elements show `data-page` and `data-field` attributes in DOM inspector
|
||
- When logged in + edit mode active: clicking text makes it editable inline
|
||
- When logged in: clicking images opens image picker, clicking links opens link editor
|
||
- When logged out: page shows clean HTML with no `data-*` attributes
|
||
- Changes made in edit mode persist after "Speichern" (save) and page reload
|
||
|
||
---
|
||
|
||
## Skill: create-page
|
||
|
||
### Description
|
||
Create a new standard page with content editable via the inline editor. No PHP template needed.
|
||
|
||
### Prerequisites
|
||
- Read `AI-AGENT-GUIDE.md` for conventions
|
||
- Read `includes/nav-config.php` for existing pages and language config
|
||
|
||
### Steps
|
||
1. Create `content/pages/{lang}_{slug}.json`:
|
||
```json
|
||
{
|
||
"page": "{lang}_{slug}",
|
||
"lang": "{lang}",
|
||
"title": "Page Title",
|
||
"description": "SEO description.",
|
||
"sections": [
|
||
{ "id": "s1", "type": "heading", "text": "Page Title", "level": "h1" },
|
||
{ "id": "s2", "type": "text", "title": "", "content": "<p>Content here.</p>" }
|
||
]
|
||
}
|
||
```
|
||
2. Add entry to `$PAGE_MAPPING` in `includes/nav-config.php`:
|
||
```php
|
||
'{slug}' => '{lang}_{slug}',
|
||
```
|
||
3. (Optional) Add to `$NAV_ITEMS` if the page should appear in navigation.
|
||
|
||
### Validation
|
||
- `content/pages/{lang}_{slug}.json` exists and is valid JSON
|
||
- Page is accessible at `/{slug}` (primary lang) or `/{lang}/{slug}`
|
||
- Sections render correctly via `renderAllSections()`
|
||
|
||
---
|
||
|
||
## Skill: add-section
|
||
|
||
### Description
|
||
Add a new content section to an existing page.
|
||
|
||
### Prerequisites
|
||
- Read `AI-AGENT-GUIDE.md` → Block Types table for available types and fields
|
||
- Read the target page's JSON file
|
||
|
||
### Steps
|
||
1. Open `content/pages/{lang}_{slug}.json`
|
||
2. Append a new object to the `sections` array:
|
||
```json
|
||
{
|
||
"id": "s{next_number}",
|
||
"type": "{block_type}",
|
||
...fields per block type...
|
||
}
|
||
```
|
||
3. Ensure `id` is unique within the page
|
||
|
||
### Available Block Types
|
||
| Type | Required Fields |
|
||
|---|---|
|
||
| `text` | `content` (HTML) |
|
||
| `heading` | `text`, `level` (h1-h6) |
|
||
| `quote` | `text` |
|
||
| `list` | `title`, `style` (bullet/numbered), `content` (HTML) |
|
||
| `image` | `src`, `alt` |
|
||
| `card` | `title`, `content` |
|
||
| `youtube` | `videoId` |
|
||
| `soundcloud` | `trackId` |
|
||
| `audio` | `src` |
|
||
| `divider` | *(none)* |
|
||
| `spacer` | `height` (sm/md/lg/xl) |
|
||
|
||
### Validation
|
||
- JSON is valid after edit
|
||
- New section has a unique `id`
|
||
- Page renders without errors
|
||
|
||
---
|
||
|
||
## Skill: create-json-form
|
||
|
||
### Description
|
||
Create or adjust a simple public form using Raiju's JSON-backed form system.
|
||
Use this when a generated/vibe-coded site needs a form that editors can later
|
||
rename, re-label, or slightly adjust in the dashboard without editing PHP.
|
||
|
||
### Prerequisites
|
||
- Read `AI-AGENT-GUIDE.md` -> "JSON-Backed Public Forms"
|
||
- Check existing definitions in `content/forms/*.json`
|
||
- Decide whether the requested form is simple enough for JSON. If it needs
|
||
advanced conditional logic, custom widgets, uploads, or page-specific
|
||
calculations, keep that behaviour in a site-owned template/script and use the
|
||
JSON form only for the editable field definition when practical.
|
||
|
||
### Steps
|
||
1. Create `content/forms/{id}.json`:
|
||
```json
|
||
{
|
||
"id": "contact",
|
||
"label": "Contact form",
|
||
"description": "",
|
||
"enabled": true,
|
||
"submit": {
|
||
"store": true,
|
||
"email": true,
|
||
"subject": "{form}: {name}",
|
||
"successText": "Thank you for your message."
|
||
},
|
||
"fields": [
|
||
{ "key": "name", "type": "text", "label": "Name", "required": true, "width": 6 },
|
||
{ "key": "email", "type": "email", "label": "Email", "required": true, "width": 6 },
|
||
{ "key": "message", "type": "textarea", "label": "Message", "required": true, "width": 12 }
|
||
]
|
||
}
|
||
```
|
||
2. Use supported field types only: `text`, `email`, `tel`, `textarea`,
|
||
`select`, `radio`, `checkbox`, `date`, `time`, `hidden`, `heading`, `note`.
|
||
3. Use supported widths only: `3`, `4`, `6`, `8`, `12`.
|
||
4. For `select` and `radio`, add `options`:
|
||
```json
|
||
"options": [
|
||
{ "value": "private", "label": "Private" },
|
||
{ "value": "business", "label": "Business" }
|
||
]
|
||
```
|
||
5. Render it in a template:
|
||
```php
|
||
<?php
|
||
require_once __DIR__ . '/../includes/forms.php';
|
||
echo raijuForm('contact', ['basePath' => $basePath, 'lang' => $currentLang]);
|
||
?>
|
||
```
|
||
Or use `raijuLazyFormPlaceholder('contact', ...)` when the form should load
|
||
lazily.
|
||
|
||
### Validation
|
||
- JSON is valid.
|
||
- `php -l includes/forms.php api/form-submit.php api/form.php` passes after core changes.
|
||
- `api/form.php?form={id}` renders the form with `form_id`, `form_token`,
|
||
`.raiju-form-grid`, `.btn-text`, `.btn-loading`, and `.form-feedback`.
|
||
- Submitting stores a row in `content/mails.json` with `formId`, `formLabel`,
|
||
and `fields[]`.
|
||
- The dashboard messages view can filter by the form.
|
||
- The form appears in **Dashboard -> Settings -> Forms** and reloads with all
|
||
fields intact.
|
||
|
||
---
|
||
|
||
## Skill: create-news-post
|
||
|
||
### Description
|
||
Create a news article that appears in the news listing.
|
||
|
||
### Prerequisites
|
||
- Read `AI-AGENT-GUIDE.md` for conventions
|
||
- Check `content/news/` for existing slug patterns
|
||
|
||
### Steps
|
||
1. Create `content/news/{slug}.json`:
|
||
```json
|
||
{
|
||
"slug": "{slug}",
|
||
"lang": "{lang}",
|
||
"title": "Article Title",
|
||
"description": "Short summary for listing.",
|
||
"date": "YYYY-MM-DD",
|
||
"image": "assets/images/news_{slug}.webp",
|
||
"sections": [
|
||
{ "id": "s1", "type": "heading", "text": "Article Title", "level": "h1" },
|
||
{ "id": "s2", "type": "text", "title": "", "content": "<p>Article body.</p>" }
|
||
]
|
||
}
|
||
```
|
||
2. (Optional) Add an image to `assets/images/`
|
||
|
||
### Validation
|
||
- `content/news/{slug}.json` exists and is valid JSON
|
||
- Post appears in news listing rendered by `renderNewsList()`
|
||
- Post is accessible at `/{lang}/news/{slug}`
|
||
|
||
---
|
||
|
||
## Skill: add-component
|
||
|
||
### Description
|
||
Add a render component (FAQ, Pricing, Team, Gallery, etc.) to a custom layout page.
|
||
|
||
### Prerequisites
|
||
- Read `AI-AGENT-GUIDE.md` → Render Components for available components
|
||
- Read the target page's JSON and PHP template
|
||
|
||
### Steps
|
||
1. Add the data structure to the page's JSON file. Each component reads from a specific key:
|
||
|
||
| Component | JSON Key | Item Structure |
|
||
|---|---|---|
|
||
| `renderFeatureGrid()` | `features.items` | `{icon, title, desc}` |
|
||
| `renderPricingTable()` | `pricing.plans` | `{name, price, period, desc, features, cta}` |
|
||
| `renderFaqAccordion()` | `faq.entries` | `{question, answer}` |
|
||
| `renderTeamGrid()` | `team.members` | `{name, role, bio, image}` |
|
||
| `renderGallery()` | `gallery.images` | `{src, alt, caption}` |
|
||
| `renderTimeline()` | `timeline.entries` | `{date, title, content}` |
|
||
| `renderStats()` | `stats.items` | `{value, label}` |
|
||
| `renderTestimonials()` | `testimonials.items` | `{text, author, role}` |
|
||
| `renderComparisonTable()` | `comparison.rows` | `{feature, values[]}` |
|
||
|
||
2. Add the PHP render call in the page template:
|
||
```php
|
||
<?php echo renderFaqAccordion($_p); ?>
|
||
```
|
||
|
||
3. Component CSS classes are in `css/components.css` (auto-loaded).
|
||
|
||
### Validation
|
||
- JSON data matches expected structure for the component
|
||
- Component renders on the page without errors
|
||
- Styling matches other components (uses BEM classes from `components.css`)
|
||
|
||
---
|
||
|
||
## Skill: change-theme
|
||
|
||
### Description
|
||
Customize the site's colors, fonts, and spacing via CSS Custom Properties.
|
||
|
||
### Prerequisites
|
||
- Read `css/style.css` → `:root` block for current token values
|
||
|
||
### Steps
|
||
1. Edit CSS custom properties in `css/style.css`:
|
||
```css
|
||
:root {
|
||
--color-primary: #your-color;
|
||
--color-primary-dark: #darker-shade;
|
||
--color-primary-light: #lighter-shade;
|
||
--color-secondary: #accent-color;
|
||
--font-display: 'Your Font', sans-serif;
|
||
--font-body: 'Your Font', sans-serif;
|
||
}
|
||
```
|
||
2. For custom fonts: place font files in `assets/fonts/`, create `css/fonts.css` with `@font-face` declarations — `header.php` loads it automatically if the file exists.
|
||
3. Update gradient tokens to match new primary color:
|
||
```css
|
||
--color-primary-btn: radial-gradient(ellipse at 50% 0%, #lighter 0%, #primary 70%);
|
||
--gradient-brand: linear-gradient(135deg, #lighter 0%, #darker 100%);
|
||
```
|
||
|
||
### Validation
|
||
- All color values in `:root` are consistent (dark/light/gradient variants match primary)
|
||
- No hardcoded color values outside of `:root`
|
||
- Text contrast meets WCAG AA (4.5:1 for body text)
|
||
|
||
---
|
||
|
||
## Skill: ai-schema-override
|
||
|
||
### Description
|
||
Restrict what the AI agent (the Copilot's agent mode) may change on a page you built. The agent learns a page's fields by rendering its template and reading the `data-*` attributes (the "capability map"), so a correctly built Raiju page needs no extra file. Add an override only when a site needs tighter rules than the template shows: a headline that must stay short, a legally relevant paragraph the agent must not touch, a card list with a maximum, or a hint such as "write in the formal tone". Overrides can only **restrict**; they can never make a field editable or lift a limit.
|
||
|
||
### Prerequisites
|
||
- Read `AI-AGENT-GUIDE.md` → "AI Agent (Frontend Copilot)"
|
||
- The page is already editable (see `make-page-editable`); override the paths its `editableText()`, `editableHtml()`, `editableImage()` and list helpers produce.
|
||
|
||
### Steps
|
||
1. Find the paths the agent sees: open the page as editor, ask the agent in the chat to "list what can be edited on this page", or read the `data-field` values of the rendered page.
|
||
2. Create `content/ai-schema/{target}.json`. The file name is the target with `:` replaced by `_`:
|
||
- page `en_services__test` (the content key; `/` becomes `__`) → `page_en_services__test.json`
|
||
- news post `2026-10-10-launch` → `news_2026-10-10-launch.json`
|
||
- shared footer / sidebar → `page_footer.json` / `page_sidebar.json`
|
||
3. Describe the restrictions:
|
||
```json
|
||
{
|
||
"fields": {
|
||
"hero.title": { "maxLength": 60, "required": true },
|
||
"legal.notice": { "readOnly": true },
|
||
"team.image": { "imageAspect": "1:1", "creditPath": "team.imageCredit" }
|
||
},
|
||
"lists": {
|
||
"team.items": { "minItems": 1, "maxItems": 8 }
|
||
},
|
||
"readOnly": ["pricing.*", "legal.*"],
|
||
"hints": ["Address visitors formally.", "Never mention competitors."],
|
||
"news": { "requireImage": true }
|
||
}
|
||
```
|
||
- `fields.{path}`: `maxLength` (lowers the limit), `required`, `readOnly`, `imageAspect` (e.g. `16:9`), `creditPath` (field that receives photo credits; by default the agent uses the caption of image sections).
|
||
- `lists.{listKey}`: `minItems` and `maxItems` (0–1000).
|
||
- `readOnly`: path patterns in shell style (`*` matches any characters, e.g. `pricing.*` covers every field and list below `pricing`; letters, digits, `_`, `.`, `-`, `*` are allowed); matching fields and lists are not editable by the agent.
|
||
- `hints`: up to 10 short sentences (200 characters each) that go into the agent's view of the page. Hints are guidance, not security: the agent's text is still validated.
|
||
- `news.requireImage`: new news posts need a cover image (only used for news targets).
|
||
4. Optional site-wide file `content/ai-schema/_site.json` may use only `readOnly`, `hints` and `news`; `fields` and `lists` are ignored there because they are page specific.
|
||
5. Clear the page scan cache after you changed a **template** (Dashboard → Settings → AI Settings → "AI agent" → "Clear page scan cache"). Changing an override file takes effect immediately.
|
||
|
||
### Validation
|
||
- `php -r 'json_decode(file_get_contents("content/ai-schema/page_en_home.json"), true) ?: exit(1);'` (valid JSON; an invalid file is ignored and the agent mentions it)
|
||
- Ask the agent to change a read-only field: it must answer that the field cannot be edited, and a staged operation must fail validation.
|
||
- `python3 tests/run-smoke.py` stays green (`agent-capabilities-smoke.php` covers the restriction rules).
|
||
- `content/ai-schema/` is under `content/` and therefore not public.
|
||
|
||
---
|
||
|
||
## Skill: register-dashboard-location
|
||
|
||
### Description
|
||
Teach the AI assistants where something is in the dashboard. The Raiju map (`includes/ai/raiju-map.php`) is the one table that knows the dashboard areas, the settings tabs and the single settings people ask about ("where do I change the logo?"). The assistants answer with the exact place (Settings › Branding › Logo) and a link that opens in a new tab. Do this whenever you add a dashboard area, a settings tab or a setting.
|
||
|
||
### Prerequisites
|
||
- Read `AI-AGENT-GUIDE.md` → "Raiju map"
|
||
- The area, tab or setting exists in `admin/dashboard.php` and has its label in `admin/lang/*.json` (all nine languages).
|
||
|
||
### Steps
|
||
1. New area (`data-tab`) or settings tab (`data-settings-tab`): add it to `raijuMapSections()` or `raijuMapSettingsTabs()` (label key, role `admin` or `editor`, module key or `needs AI`), and add its `area_…` or `tab_…` entry to `raijuMapEntries()`. Also add the new tab to `VALID_DASHBOARD_TABS`/the settings tab handling of the dashboard so `#settings/{tab}` opens it.
|
||
2. New single setting: add one entry to `raijuMapEntries()`: `[area, tab, label key, [synonyms], 'English hint']`. Use the label that the dashboard shows (so the name exists in every language); synonyms are the words people use instead (any language). A synonym may belong to one entry only.
|
||
3. If the assistant may change the setting after a confirm, see "Dashboard actions" in `AI-AGENT-GUIDE.md` (`RAIJU_DASHBOARD_SETTINGS`, `raijuMapActionEntries()`, the runner in `admin/dashboard/scripts/settings.php`).
|
||
|
||
### Validation
|
||
- `php tests/raiju-map-smoke.php` (the map matches the dashboard, every name exists in nine languages, no duplicated synonyms, links stay on the site).
|
||
- Ask the dashboard assistant "Where do I find …?": it names the place and offers a card with an "Open in a new tab" link.
|
||
|
||
---
|
||
|
||
## Skill: add-language
|
||
|
||
### Description
|
||
Add a new language to the site.
|
||
|
||
### Prerequisites
|
||
- Read `includes/nav-config.php` for `$SITE_LANGUAGES` and `$NAV_ITEMS`
|
||
- Read `admin/config.php` for `SITE_LANG_DEFAULT`
|
||
|
||
### Steps
|
||
1. Create directory `{lang}/` (e.g. `fr/`)
|
||
2. Create `{lang}/index.php` (homepage template):
|
||
```php
|
||
<?php
|
||
$pageTitle = 'Accueil';
|
||
$pageDescription = 'Description.';
|
||
$currentLang = 'fr';
|
||
$currentPage = 'home';
|
||
$contentPage = 'fr_home';
|
||
$basePath = '../';
|
||
include '../includes/header.php';
|
||
include '../includes/content-loader.php';
|
||
$_p = $contentPage;
|
||
?>
|
||
<main class="main-content">
|
||
<div class="content-inner">
|
||
<?php echo renderAllSections($_p); ?>
|
||
</div>
|
||
</main>
|
||
<?php include '../includes/footer.php'; ?>
|
||
```
|
||
3. Copy content files: for each `content/pages/{source_lang}_*.json`, create `content/pages/{lang}_*.json` with translated text and updated `"page"` and `"lang"` fields.
|
||
4. Add the language to `$SITE_LANGUAGES` in `includes/nav-config.php`:
|
||
```php
|
||
$SITE_LANGUAGES = ['en' => 'English', 'de' => 'Deutsch', 'fr' => 'Français'];
|
||
```
|
||
5. Add `$NAV_ITEMS['{lang}']` array mirroring the structure of existing languages.
|
||
|
||
### Validation
|
||
- `{lang}/index.php` exists
|
||
- Content files exist for all pages in the new language
|
||
- Language appears in the language switcher
|
||
- Navigation works for the new language
|
||
|
||
---
|
||
|
||
## Skill: create-custom-layout
|
||
|
||
### Description
|
||
Create a page with a custom PHP template (hero sections, grids, components) instead of the standard `renderAllSections()` approach.
|
||
|
||
### Prerequisites
|
||
- Read `AI-AGENT-GUIDE.md` → "How to Create a Custom Layout Page"
|
||
- Read `examples/templates/page-custom.php` for reference
|
||
- Read `examples/content/custom-layout.json` for JSON structure
|
||
|
||
### Steps
|
||
1. Create `{lang}/{slug}.php`:
|
||
```php
|
||
<?php
|
||
$pageTitle = 'Page Title';
|
||
$pageDescription = 'Description.';
|
||
$currentLang = '{lang}';
|
||
$currentPage = '{slug}';
|
||
$contentPage = '{lang}_{slug}';
|
||
$basePath = '../';
|
||
|
||
include '../includes/header.php';
|
||
include '../includes/content-loader.php';
|
||
$_p = $contentPage;
|
||
?>
|
||
<main class="main-content">
|
||
<section class="hero">
|
||
<h1><?php echo editableText($_p, 'hero.title', 'Default Title'); ?></h1>
|
||
<p><?php echo editableText($_p, 'hero.subtitle', 'Default subtitle'); ?></p>
|
||
<?php echo editableImage($_p, 'hero.image', 'https://placehold.co/800x400', 'Hero image'); ?>
|
||
</section>
|
||
<?php echo renderFeatureGrid($_p); ?>
|
||
<?php echo renderFaqAccordion($_p); ?>
|
||
</main>
|
||
<?php include '../includes/footer.php'; ?>
|
||
```
|
||
2. Create `content/pages/{lang}_{slug}.json` with matching keys:
|
||
```json
|
||
{
|
||
"page": "{lang}_{slug}",
|
||
"lang": "{lang}",
|
||
"title": "Page Title",
|
||
"hero": {
|
||
"title": "Welcome",
|
||
"subtitle": "Tagline",
|
||
"image": { "src": "https://placehold.co/800x400", "alt": "Hero" }
|
||
},
|
||
"features": {
|
||
"heading": "Features",
|
||
"items": { "0": { "icon": "zap", "title": "Fast", "desc": "No database." } }
|
||
},
|
||
"faq": {
|
||
"heading": "FAQ",
|
||
"entries": { "0": { "question": "How?", "answer": "Like this." } }
|
||
}
|
||
}
|
||
```
|
||
3. Add to `$PAGE_MAPPING` and optionally `$NAV_ITEMS` in `includes/nav-config.php`.
|
||
|
||
### Validation
|
||
- PHP template renders without errors
|
||
- All `editableText()`/`editableImage()`/`editableLink()` fields display content from JSON
|
||
- Render components (features, FAQ, etc.) display correctly
|
||
- Inline editor works when logged in (fields are editable, changes save)
|