>
editableListItemAttrs($page, $listKey, $index)
// Returns data-list-page + data-list-key + data-list-index attributes.
// Add to each item:
>
editableListGroupItemAttrs($page, $listKey, $index, $schema = [], $defaults = [], $label = 'Item')
// Returns list-item attributes plus data-editable-group when the schema/defaults
// describe a structured card-like item. If $schema is empty, Raiju infers one
// from $defaults only for multi-field or special-field items. Single-field text
// lists remain inline-only.
editableListItems($page, $listKey)
// Returns array of items from JSON. Filters hidden items for visitors.
editableTextList($page, $listKey, $defaults = ['content' => ''])
// Renders a list of editable HTML paragraphs with add/remove/reorder controls.
// Each item is a single editableHtml() field. Admins get inline editing with
// a floating toolbar (Bold, Italic, Link, Clean). Visitors see clean HTML.
// JSON stores: {"0": {"content": "..."}, "1": {"content": "..."}}
```
**List usage pattern:**
```php
'New', 'desc' => '']); ?>>
$item): ?>
'Feature',
'fields' => [
['key' => 'title', 'type' => 'text', 'label' => 'Title'],
['key' => 'desc', 'type' => 'textarea', 'label' => 'Description'],
],
]); ?>>
```
For a simple one-field list, keep the item inline-only:
```php
```
**Paragraph list usage pattern:**
```php
```
```json
{
"about": {
"paragraphs": {
"0": { "content": "First paragraph text..." },
"1": { "content": "Second paragraph with
formatting..." }
}
}
}
```
### Render Components
```php
renderAllSections($page, $staggerCards = false) // All sections from JSON
renderFeatureGrid($page) // features.items — icon + title + desc cards
renderPricingTable($page) // pricing.plans — pricing cards with CTA
renderFaqAccordion($page) // faq.entries — collapsible Q&A
renderTeamGrid($page) // team.members — avatar + name + role + bio
renderGallery($page) // gallery.images — image grid with lightbox
renderTimeline($page) // timeline.entries — changelog/roadmap
renderStats($page) // stats.items — key figures
renderTestimonials($page) // testimonials.items — quote cards
renderComparisonTable($page) // comparison.rows — feature comparison table
renderNewsList($limit, $lang) // News posts from content/news/
renderBreadcrumb($page, $basePath) // Breadcrumb trail from JSON "breadcrumb" field
```
### Content Helpers
```php
loadContent($page) // Returns full JSON array for a page
getContentValue($page, $key) // Returns a single top-level value
loadNewsPosts($limit, $lang) // Returns sorted news post array
```
## Theming
Edit CSS custom properties in `css/style.css`:
```css
:root {
/* Colors — neutral blue primary */
--color-primary: #2563eb;
--color-primary-dark: #1d4ed8;
--color-primary-dark-muted: #1e40af;
--color-primary-light: #60a5fa;
--color-primary-really-dark: #172554;
--color-primary-btn: radial-gradient(...);
--color-primary-btn-hover: radial-gradient(...);
--color-secondary: #60a5fa;
--color-text: #171717;
--color-text-secondary: #525252;
--color-background: #ffffff;
--color-background-dark: #eff6ff;
--color-background-darker: #dbeafe;
--color-background-section: #f5f5f4;
--color-background-card: #ffffff;
--color-border: rgba(0, 0, 0, 0.08);
/* Dark theme */
--color-bg-dark: #0c0c0c;
--color-bg-dark-elevated: #141414;
--color-bg-dark-card: #1c1c1c;
--color-text-on-dark: #e5e5e5;
--color-text-on-dark-muted: #a3a3a3;
/* Gradients & glass */
--gradient-brand: linear-gradient(135deg, ...);
--gradient-brand-text: linear-gradient(90deg, ...);
--glass-bg: rgba(255, 255, 255, 0.03);
/* Typography */
--font-display: system-ui, -apple-system, sans-serif;
--font-body: system-ui, -apple-system, sans-serif;
--font-mono: 'SF Mono', 'Fira Code', Menlo, Consolas, monospace;
/* Spacing */
--spacing-xs: 0.5rem; --spacing-sm: 1rem;
--spacing-md: 2rem; --spacing-lg: 4rem; --spacing-xl: 6rem;
/* Transitions */
--transition-fast: 0.2s ease;
--transition-normal: 0.3s ease;
/* Layout */
--header-height: 80px;
--container-max: 1400px;
--container-narrow: 800px;
}
```
Dark theme is toggled via `data-theme="dark"` on ``. Preference stored in `localStorage` key `site-theme`.
Custom fonts: place files in `assets/fonts/`, create `css/fonts.css` — `includes/header.php` loads it automatically if the file exists.
**Best practice — overrides go in `css/website.css`, not `css/style.css`.** `style.css` is Raiju-Core and gets overwritten on upgrade. Override theme variables in a `:root` block inside `css/website.css` (loaded after Core, so site values win on conflict). A pre-filled template with every overridable property is in `examples/css/website.css.template` — copy it to `css/website.css` and uncomment what you want to change.
## Site Settings (`content/settings.json`)
```json
{
"favicon": "/assets/images/favicon.svg",
"favicon_png": "/assets/images/favicon.png",
"branding": {
"logo": "",
"name": "My Site",
"showBranding": true
},
"theme": {
"adminTheme": "light",
"primaryColor": "#2563eb",
"accentColor": "#60a5fa"
}
}
```
- `favicon` — path to primary favicon (SVG or PNG); `type` attribute is set automatically
- `favicon_png` — optional PNG fallback for browsers that don't support SVG favicons; also used as `apple-touch-icon`
- `branding.logo` — path to logo image (empty = text-only)
- `branding.name` — site name shown in header and admin
- `theme.adminTheme` — `"light"` or `"dark"` for admin panel
- `theme.primaryColor` / `accentColor` — used by admin dashboard color picker
## Page Template Variables
Templates can set these variables before `header.php` / `footer.php` to control page behavior:
| Variable | Where | Purpose |
|---|---|---|
| `$pageExternalStyles` | header.php | Array of external CSS URLs (e.g. Google Fonts CDN) |
| `$pageExternalScripts` | footer.php | Array of external JS URLs (e.g. map libraries) |
| `$pageStylesheet` | header.php | Path to a page-specific CSS file |
| `$pageClass` | header.php | Extra CSS class on `` |
## Site Page Hook
For standard pages (served via `includes/page.php` + `renderAllSections()`), create `includes/site-page-hook.php` to add per-page logic without modifying core files. The hook runs after `$slug`, `$lang`, `$data`, and `$basePath` are set but before `header.php`:
```php
**`"nav": []` only hides the page from menus — the URL remains publicly reachable.**
> The front controller serves any existing page slug regardless of `nav`. If you want the page truly gone, delete its JSON file (or move it out of `content/pages/`). For migrations where the content was merged into another page, prefer either deleting the JSON and adding a 301 redirect in `.htaccess`, or returning `410 Gone` if there is no replacement (see `410.php`).
### Menu Registry (`content/menus.json`)
Defines available menus with multilingual labels and sort order:
```json
{
"menus": {
"header": { "label": { "en": "Header", "de": "Kopfzeile" }, "type": "standard", "weight": 0 },
"footer-pages": { "label": { "en": "Pages", "de": "Seiten" }, "weight": 10 },
"footer-legal": { "label": { "en": "Info", "de": "Rechtliches" }, "weight": 20 }
}
}
```
Footer columns are rendered automatically from the registry (all menus except `header`). Adding a new footer column = add an entry to `menus.json` + tag pages with the new menu ID. Column headings come from the `label` field. The dashboard's Page Settings checkboxes are generated dynamically from the registry.
### Nested Menus (Dropdowns)
Group nav items into dropdowns via `children` in `$NAV_ITEMS` (`nav-config.php`):
```php
$NAV_ITEMS = [
'en' => [
['href' => '.', 'label' => 'Home', 'page' => 'home'],
['href' => 'docs', 'label' => 'Resources', 'page' => 'docs', 'children' => [
['href' => 'docs', 'label' => 'Documentation', 'page' => 'docs'],
['href' => 'showcase', 'label' => 'Showcase', 'page' => 'showcase'],
]],
['href' => 'pricing', 'label' => 'Pricing', 'page' => 'pricing', 'nav' => ['header']], // header only, not in footer
],
];
```
- Desktop: hover/focus opens a dropdown below the parent
- Mobile: children are rendered inline, indented
- If any child is the active page, the parent also gets the `.active` class
- Manual items support an optional `'nav'` key (defaults to `['header', 'footer']`)
### One-Page Menus
Set a menu registry entry to `"type": "one-page"` when header links point to
sections on the homepage:
```json
{ "menus": { "header": { "label": { "en": "Header" }, "type": "one-page", "weight": 0 } } }
```
Manual `$NAV_ITEMS` may then use hash-only links such as `'#services'`. On the
homepage Raiju keeps the link as `#services`; on subpages it normalizes the link
to the current language homepage plus hash, e.g. `./#services` or `de/#services`.
Hash items do not receive server-side `.active`. The frontend marks them active
only while the target section is visible.
### Local Dev Login
Raiju supports a guarded development login for local work:
```php
define('RAIJU_DEV_LOGIN', true);
```
When enabled, existing admin users can log in with password `dev` only if both
the request host and remote address are loopback (`localhost`, `127.0.0.1`, or
`::1`). The bypass is ignored on non-local hosts even if the flag is accidentally
left enabled. Set `RAIJU_DEV_LOGIN` to `false` to disable it locally too. Older
local configs without the constant use the same loopback-only behavior.
### Breadcrumbs
Optional `"breadcrumb"` array in page JSON. Each entry has `label` (required) and `href` (optional):
```json
{
"title": "API Reference",
"breadcrumb": [
{ "label": "Docs", "href": "docs" },
{ "label": "API Reference" }
]
}
```
Standard pages render breadcrumbs automatically above `renderAllSections()`. Custom layouts call `renderBreadcrumb($contentPage, $basePath)` manually. If no `"breadcrumb"` field is present, nothing is rendered.
`nav-config.php` is still used for: explicit navigation ordering, custom labels, dropdown grouping, the language switcher's `$PAGE_MAPPING`, and pages that need different slugs per language.
## Migration Redirects (`content/redirects.json`)
For SEO-preserving migrations from another CMS, drop URL → URL mappings into `content/redirects.json`. The router applies the first matching rule before any other routing — both in production (via `route.php` after `.htaccess` rewrites) and in development (via `router.php`). One file works for both.
```json
{
"redirects": [
{ "from": "^/wp/about/?$", "to": "/about", "status": 301 },
{ "from": "^/wp/page/(.+)$", "to": "/$1", "status": 301 },
{ "from": "^/services/coaching/?$", "to": "/services#coaching", "status": 301 },
{ "from": "^/temp/(.+)$", "to": "/hub", "status": 302 },
{ "from": "^/retired/lifecoach$", "status": 410 }
]
}
```
- `from` is a PCRE pattern matched against the request path (with leading slash). Backreferences (`$1`, `$2`, …) in `to` are resolved.
- `to` is the target URL. Anchors (`#section`) are preserved as written.
- `status`: `301` (default), `302`, or `410` (Gone — `to` is ignored, the branded `/410.php` is rendered).
- Rules are evaluated top-to-bottom; the first match wins.
- Bad regex in a single rule is silently skipped, so a typo doesn't take the whole site down.
A starter file lives at `content/redirects.example.json`. Copy it to `content/redirects.json` to activate; the live file is gitignored so site owners can edit it without polluting the repo.
## HTML-to-Raiju Converter
Automatically convert a static HTML page into an editable Raiju template + JSON + CSS:
```bash
php cli/convert.php my-page.html --slug=about --lang=en --dry-run
```
The converter parses the HTML, identifies sections, headings, text, images, links, and repeating patterns (cards, testimonials), and generates:
- A PHP template with `editableText()`, `editableImage()`, `editableLink()`, and editable list calls
- A JSON content file with all extracted content
- A CSS file (`css/page-{slug}.css`) with all styles extracted from `