Raiju/SKILLS.md
2026-10-25 06:05:42 +03:00

25 KiB
Raw Permalink Blame History

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

  1. 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'; ?>
    
  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():

    // 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():

    // 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 $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():

    <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..." } } } }
    
  7. Create the matching JSON file content/pages/{lang}_{slug}.json with 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.

  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:
    {
      "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:
    '{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:
    {
      "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:
    {
      "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:
    "options": [
      { "value": "private", "label": "Private" },
      { "value": "business", "label": "Business" }
    ]
    
  5. Render it in a template:
    <?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:
    {
      "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 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:
    :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:
    --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:
    {
      "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
    $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:
    $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
    $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:
    {
      "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)