# 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 | |---|---| | `
Some text
` | `` | | `
` | `` |
### Steps
1. **Add the PHP boilerplate** at the top of the page template:
```php
```
And the footer at the bottom:
```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: Contact Us
// After:
```
4. **Replace every content image** with `editableImage()`:
```php
// Before:
// After:
```
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
'New', 'desc' => '', 'link' => ['text' => 'Learn more', 'href' => '#']]; ?>
Content here.
" } ] } ``` 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 $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": "Article body.
" } ] } ``` 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 ``` 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