25 KiB
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:
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
-
Add the PHP boilerplate at the top of the page template:
<?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 include '../includes/footer.php'; ?> -
Replace every hardcoded text with
editableText()oreditableHtml():- 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)
- Use
-
Replace every hardcoded link with
editableLink():// Before: <a href="/contact" class="btn btn-primary">Contact Us</a> // After: <?php echo editableLink($_p, 'hero.cta', 'Contact Us', '/contact', 'btn btn-primary'); ?> -
Replace every content image with
editableImage():// 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'); ?> -
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, keepeditableListItemAttrs()and inline editing only:<?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> -
For editable paragraph blocks (multiple paragraphs that admins can add, delete, and reorder), use
editableTextList():<div class="about-text"> <?php echo editableTextList($_p, 'about.paragraphs'); ?> </div>Each paragraph gets inline editing with the floating toolbar. JSON structure:
{ "about": { "paragraphs": { "0": { "content": "First paragraph..." }, "1": { "content": "Second..." } } } } -
Create the matching JSON file
content/pages/{lang}_{slug}.jsonwith all the keys used in the template:{ "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. -
Register the page in
includes/nav-config.php($PAGE_MAPPINGand 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-imageinstead) - JavaScript behavior or interactive elements
Validation
- PHP template renders without errors
- When logged in: elements show
data-pageanddata-fieldattributes 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.mdfor conventions - Read
includes/nav-config.phpfor existing pages and language config
Steps
- Create
content/pages/{lang}_{slug}.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>" } ] } - Add entry to
$PAGE_MAPPINGinincludes/nav-config.php:'{slug}' => '{lang}_{slug}', - (Optional) Add to
$NAV_ITEMSif the page should appear in navigation.
Validation
content/pages/{lang}_{slug}.jsonexists 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
- Open
content/pages/{lang}_{slug}.json - Append a new object to the
sectionsarray:{ "id": "s{next_number}", "type": "{block_type}", ...fields per block type... } - Ensure
idis 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
- Create
content/forms/{id}.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 } ] } - Use supported field types only:
text,email,tel,textarea,select,radio,checkbox,date,time,hidden,heading,note. - Use supported widths only:
3,4,6,8,12. - For
selectandradio, addoptions:"options": [ { "value": "private", "label": "Private" }, { "value": "business", "label": "Business" } ] - Render it in a template:
Or use<?php require_once __DIR__ . '/../includes/forms.php'; echo raijuForm('contact', ['basePath' => $basePath, 'lang' => $currentLang]); ?>raijuLazyFormPlaceholder('contact', ...)when the form should load lazily.
Validation
- JSON is valid.
php -l includes/forms.php api/form-submit.php api/form.phppasses after core changes.api/form.php?form={id}renders the form withform_id,form_token,.raiju-form-grid,.btn-text,.btn-loading, and.form-feedback.- Submitting stores a row in
content/mails.jsonwithformId,formLabel, andfields[]. - 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.mdfor conventions - Check
content/news/for existing slug patterns
Steps
- Create
content/news/{slug}.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>" } ] } - (Optional) Add an image to
assets/images/
Validation
content/news/{slug}.jsonexists 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
-
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[]} -
Add the PHP render call in the page template:
<?php echo renderFaqAccordion($_p); ?> -
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→:rootblock for current token values
Steps
- Edit CSS custom properties in
css/style.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; } - For custom fonts: place font files in
assets/fonts/, createcss/fonts.csswith@font-facedeclarations —header.phploads it automatically if the file exists. - Update gradient tokens to match new primary color:
--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
:rootare 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 itseditableText(),editableHtml(),editableImage()and list helpers produce.
Steps
- 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-fieldvalues of the rendered page. - 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
- page
- Describe the restrictions:
{ "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}:minItemsandmaxItems(0–1000).readOnly: path patterns in shell style (*matches any characters, e.g.pricing.*covers every field and list belowpricing; 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).
- Optional site-wide file
content/ai-schema/_site.jsonmay use onlyreadOnly,hintsandnews;fieldsandlistsare ignored there because they are page specific. - 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.pystays green (agent-capabilities-smoke.phpcovers the restriction rules).content/ai-schema/is undercontent/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.phpand has its label inadmin/lang/*.json(all nine languages).
Steps
- New area (
data-tab) or settings tab (data-settings-tab): add it toraijuMapSections()orraijuMapSettingsTabs()(label key, roleadminoreditor, module key orneeds AI), and add itsarea_…ortab_…entry toraijuMapEntries(). Also add the new tab toVALID_DASHBOARD_TABS/the settings tab handling of the dashboard so#settings/{tab}opens it. - 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. - If the assistant may change the setting after a confirm, see "Dashboard actions" in
AI-AGENT-GUIDE.md(RAIJU_DASHBOARD_SETTINGS,raijuMapActionEntries(), the runner inadmin/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.phpfor$SITE_LANGUAGESand$NAV_ITEMS - Read
admin/config.phpforSITE_LANG_DEFAULT
Steps
- Create directory
{lang}/(e.g.fr/) - Create
{lang}/index.php(homepage template):<?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'; ?> - Copy content files: for each
content/pages/{source_lang}_*.json, createcontent/pages/{lang}_*.jsonwith translated text and updated"page"and"lang"fields. - Add the language to
$SITE_LANGUAGESinincludes/nav-config.php:$SITE_LANGUAGES = ['en' => 'English', 'de' => 'Deutsch', 'fr' => 'Français']; - Add
$NAV_ITEMS['{lang}']array mirroring the structure of existing languages.
Validation
{lang}/index.phpexists- 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.phpfor reference - Read
examples/content/custom-layout.jsonfor JSON structure
Steps
- Create
{lang}/{slug}.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'; ?> - Create
content/pages/{lang}_{slug}.jsonwith matching keys:{ "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." } } } } - Add to
$PAGE_MAPPINGand optionally$NAV_ITEMSinincludes/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)