22 Commits
Author SHA1 Message Date
E.Noorlander e0e6e28dcc Fix: Dashboard altijd zichtbaar in sidebar (onafhankelijk van plugin status)
Dashboard was niet zichtbaar in de sidebar als de Dashboard plugin niet
in enabled_plugins stond. Nu is Dashboard hardcoded bovenaan de Algemeen
sectie, en wordt het uit de plugin menu items gefilterd om dubbele links
te voorkomen.
2026-08-15 19:56:34 +02:00
E.Noorlander 616b23ce77 Merge development v2.6.0 into main
Resolved conflicts by taking development (v2.6.0) version for all files.
Removed statistics.twig (replaced by Statistics plugin).
2026-08-15 19:32:47 +02:00
E.Noorlander b38be8366c Update all guides (NL + EN) for CodePress 2.5.2 features
- Admin guide: RBAC roles, role-based dashboard, plugin types (content/system)
- CodePress developer guide: plugin types, admin plugin API, SCSS compilation, asset serving
- Theme developer guide: SCSS sole CSS source, css_compiled read-only, guide layout
- Content manager guide: layout selection from theme.json, plugin order in frontmatter
- Both NL and EN updated with identical structure
- 35 files updated
2026-08-12 12:05:53 +02:00
E.Noorlander c2bcd7be22 System plugin support: admin menu items, admin routes, API integration
- PluginManager: getAdminMenuItems(), handleAdminRoute(), getPluginType()
- admin.php: load PluginManager, pass plugin_admin_menu to Twig
- admin.php: route to plugin admin pages via handleAdminRoute()
- admin.twig: show 'Plugins' sidebar section for system plugins
- plugins-new.twig: choose content or system plugin type
- handlePluginsNew: generate proper template based on type (content/system)
- plugin-admin.twig: renders plugin output in admin layout
- GeoIPInfo: example system plugin (admin page with GeoIP info)
- System plugins: getAdminMenu(), getAdminRoutes(), handleAdminRoute()
- Content plugins: getSidebarContent() (unchanged)
2026-08-11 18:06:17 +02:00
E.Noorlander 6daa0a6f49 Fix plugin arrows (grey/disable not hide) + live frontmatter update on plugin change
- Arrow buttons: opacity 0.4 + pointer-events none at first/last (not hidden)
- updateFrontmatter(): updates both layout AND plugins in editor live
- Plugin order change -> frontmatter updated immediately in CodeMirror
- Plugin checkbox toggle -> frontmatter updated immediately
- Label is 'Plugins' (not 'Zichtbare plugins')
- Update AGENTS.md with plugin types (content vs system)
2026-08-11 17:29:54 +02:00
E.Noorlander 73450a17c1 Plugin system: content vs system types, sidebar-aware UI, arrow visibility
- plugin.json type field: 'content' (sidebar) or 'system' (API only)
- Content-edit: label 'Plugins', hide section if layout has no sidebar
- Content-edit: disable checkboxes when no sidebar layout selected
- Content-edit: hide up arrow on first item, down arrow on last item
- PluginManager: isPluginViewable() checks type=system -> not viewable
- Admin plugins page: show Content/Systeem type badge
- HTMLBlock: add plugin.json with type=content
- Navigation: add type=content to config
2026-08-11 17:22:41 +02:00
E.Noorlander 8ebfcb6061 Fix: show all plugins in content-edit, not just those with plugin.json
HTMLBlock has no plugin.json, only HTMLBlock.php.
Accept plugin if it has plugin.json OR <PluginName>.php.
2026-08-11 17:12:04 +02:00
E.Noorlander b6896c60e5 Fix Twig syntax error in content-edit.twig: if not in inside for loop
Twig does not support 'for x in y if x not in z' syntax.
Use separate {% if %} block inside {% for %} loop instead.
2026-08-11 17:09:02 +02:00
E.Noorlander d733e26f91 Plugin sidebar order + directory layout from index.md
- PluginManager: iterate allowedPlugins in user-defined order (frontmatter order)
- content-edit.twig: plugin list with up/down arrows to set order
- Directory listing: read layout/plugins from index.md if present
- Directory default layout: full_content (no sidebar) unless index.md says otherwise
2026-08-11 17:04:27 +02:00
E.Noorlander 4e369d887b Fix submenu arrow: flexbox centering, proper spacing
- Use display: flex + align-items: center on dropdown-toggle
- gap: 0.75rem between text and arrow
- Remove float: right and margin-top hack
- chevron-right: flex-shrink: 0, font-size: 0.7rem
2026-08-11 16:57:50 +02:00
E.Noorlander e8e3c97ccd Remove focus outline/border on nav-link, add focus-visible override
- Remove .nav-link:focus from accessibility outline rules
- Add outline: none and box-shadow: none on nav-tabs .nav-link states
- Override :focus-visible with border: none, outline: none, box-shadow: none
2026-08-11 16:55:31 +02:00
E.Noorlander 8ebf11d7e4 Add CRITICAL sections to AGENTS.md to prevent repeated mistakes
- SCSS is sole CSS source, never edit theme.css manually
- Bootstrap 5: override all CSS variables AND properties
- Path references from public/admin.php: use ../ not ../../
- Twig: no dirname filter, use default() not ?? on filter expressions
- CodeMirror mode load order dependencies
- Plugin filename convention: <Name>.php not plugin.php
- Essential plugins protection
- Live server asset serving via asset.php
2026-08-11 16:51:05 +02:00
E.Noorlander 3d84e61ed1 Use SCSS as sole CSS source, remove manual theme.css
- Remove manual themes/default/assets/css/theme.css
- SCSS is compiled by scssphp to css_compiled/theme.css
- All nav-tabs, dropdown-menu, dropdown-item overrides in SCSS only
- Override ALL Bootstrap nav-tabs CSS variables and properties
2026-08-11 16:48:21 +02:00
E.Noorlander 0137877439 Fix dropdown width: width: max-content on menu, width: 100% on items
- dropdown-menu: width: max-content (menu adapts to longest item)
- dropdown-item: width: 100% (items fill menu width)
- Same for submenu dropdown-menu
2026-08-11 16:43:13 +02:00
E.Noorlander 46c653eb21 Remove --bs-nav-tabs CSS variables from theme, use direct !important overrides
- Remove --bs-nav-tabs-border-radius/width/color from theme.css
- Use border: none !important and border-radius: 0 !important directly
- Override all Bootstrap --bs-dropdown-* variables completely
- Add gap: 0 on .nav-tabs
2026-08-11 16:41:02 +02:00
E.Noorlander 2d61f9bab6 Override Bootstrap dropdown CSS variables completely: no borders, no radius, no shadow, auto-width
- Set --bs-dropdown-min-width: 0 (width adapts to content)
- Set --bs-dropdown-border-width: 0 (no borders)
- Set --bs-dropdown-border-radius: 0 (no rounded corners)
- Set --bs-dropdown-box-shadow: none (no shadow)
- Set --bs-dropdown-padding-x/y: 0 (no padding)
- white-space: nowrap and width: auto on dropdown-item
- Same overrides on submenu dropdown-menu
2026-08-11 16:30:26 +02:00
E.Noorlander e9d2ec64bb Remove dropdown borders, auto-width to content, nowrap items
- border: none on all dropdown-menu (no borders)
- min-width: 0 (width adapts to longest item title)
- white-space: nowrap on dropdown-item
- Remove border-left on mobile submenus
2026-08-11 11:36:48 +02:00
E.Noorlander 8a0637eddc Override Bootstrap nav-tabs CSS variables: border-radius, border-width, border-color
- Set --bs-nav-tabs-border-radius: 0
- Set --bs-nav-tabs-border-width: 0
- Set --bs-nav-tabs-border-color: transparent
- Add border-radius: 0 !important on .nav-tabs
2026-08-10 16:17:17 +02:00
E.Noorlander d84a2989d4 Bump version to 2.5.2 2026-08-10 16:15:27 +02:00
E.Noorlander 6bdd5faa7a Fix dropdown menu styling: no rounded corners, no gaps, consistent hover
- border-radius: 0 on all dropdown-menu and dropdown-item
- padding: 0 and margin: 0 on dropdown-menu
- margin-left: 0 on submenu (no gap between parent and child)
- margin-top: -1px on submenu (seamless border overlap)
- CSS hover opens submenu on desktop, click on mobile
- white-space: nowrap on dropdown items
- Fix statistics getFullStats -> getStats
- Fix Twig ?? operator -> default filter on dashboard/statistics
- Asset server (public/asset.php) for production static file serving
- .htaccess rewrite rules for themes/admin/plugins assets
2026-08-10 16:14:12 +02:00
E.Noorlander b495fc9ab0 live bug fixed 2026-08-10 15:51:00 +02:00
E.Noorlander 2c0e62bbae AGENTS.md and TODO.md change 2026-08-10 15:45:09 +02:00
45 changed files with 1042 additions and 1102 deletions
+3
View File
@@ -21,6 +21,9 @@ admin/storage/geoip/
admin/storage/stats.json admin/storage/stats.json
var/ var/
# Runtime-compiled theme assets (generated by scssphp, read-only)
themes/*/assets/css_compiled/
# Runtime-compiled theme assets # Runtime-compiled theme assets
public/themes/ public/themes/
Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 41 KiB

+7 -4
View File
@@ -40,10 +40,14 @@
<i class="bi bi-gear-fill"></i> {{ ta.admin_title|default('CodePress Admin') }} <i class="bi bi-gear-fill"></i> {{ ta.admin_title|default('CodePress Admin') }}
</div> </div>
<ul class="nav flex-column mt-2"> <ul class="nav flex-column mt-2">
{# Algemene sectie: Dashboard (plugin) + plugin items met section=general #} {# Algemene sectie: Dashboard (altijd zichtbaar) + plugin items met section=general #}
{% set general_plugins = plugin_admin_menu|filter(item => (item.section|default('general')) == 'general') %} {% set general_plugins = plugin_admin_menu|filter(item => (item.section|default('general')) == 'general' and item.route != 'dashboard') %}
{% if general_plugins is not empty or has_permission('dashboard') or true %}
<li class="nav-section">{{ ta.section_general|default('Algemeen') }}</li> <li class="nav-section">{{ ta.section_general|default('Algemeen') }}</li>
<li class="nav-item">
<a class="nav-link {{ route == 'dashboard' or route == '' ? 'active' : '' }}" href="/admin/dashboard">
<i class="bi bi-speedometer2"></i> {{ ta.dashboard|default('Dashboard') }}
</a>
</li>
{% for item in general_plugins %} {% for item in general_plugins %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {{ route == item.route or route starts with item.route ~ '/' ? 'active' : '' }}" href="/admin/{{ item.route }}"> <a class="nav-link {{ route == item.route or route starts with item.route ~ '/' ? 'active' : '' }}" href="/admin/{{ item.route }}">
@@ -51,7 +55,6 @@
</a> </a>
</li> </li>
{% endfor %} {% endfor %}
{% endif %}
{% if has_permission('content') %} {% if has_permission('content') %}
<li class="nav-section">{{ ta.section_content|default('Content') }}</li> <li class="nav-section">{{ ta.section_content|default('Content') }}</li>
@@ -0,0 +1,7 @@
{% extends "layouts/admin.twig" %}
{% block title %}{{ route|capitalize }} - CodePress Admin{% endblock %}
{% block content %}
{{ plugin_content|raw }}
{% endblock %}
@@ -13,6 +13,21 @@
<input type="text" class="form-control" id="name" name="name" required autofocus> <input type="text" class="form-control" id="name" name="name" required autofocus>
<small class="form-text text-muted">{{ ta.plugin_name_help|default('Alleen letters, cijfers, underscores en streepjes.') }}</small> <small class="form-text text-muted">{{ ta.plugin_name_help|default('Alleen letters, cijfers, underscores en streepjes.') }}</small>
</div> </div>
<div class="mb-3">
<label class="form-label">Plugin type</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="type" id="type-content" value="content" checked>
<label class="form-check-label" for="type-content">
<strong>Content plugin</strong> — Verschijnt in de sidebar op content pagina's
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="type" id="type-system" value="system">
<label class="form-check-label" for="type-system">
<strong>Systeem plugin</strong> — Voegt functionaliteit toe aan het CMS (admin menu, API)
</label>
</div>
</div>
</div> </div>
<div class="card-footer bg-white"> <div class="card-footer bg-white">
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
+20 -7
View File
@@ -1203,9 +1203,22 @@ class CodePressCMS {
$content .= '<p>' . $this->t('directory_empty') . '.</p>'; $content .= '<p>' . $this->t('directory_empty') . '.</p>';
} }
// Check for index.md in this directory for layout/plugins metadata
$indexFile = $dirPath . '/index.md';
$layout = 'full_content';
$metadata = [];
if (file_exists($indexFile)) {
$indexContent = file_get_contents($indexFile);
$parsed = $this->parseMetadata($indexContent);
$metadata = $parsed['metadata'];
$layout = $metadata['layout'] ?? 'full_content';
}
return [ return [
'title' => $title, 'title' => $title,
'content' => $content 'content' => $content,
'layout' => $layout,
'metadata' => $metadata,
]; ];
} }
@@ -1489,7 +1502,7 @@ class CodePressCMS {
$isExpanded = $this->folderContainsActivePage($item['children']); $isExpanded = $this->folderContainsActivePage($item['children']);
if ($level === 0) { if ($level === 0) {
// Root level folders // Root level folders - Bootstrap dropdown
$html .= '<li class="nav-item dropdown">'; $html .= '<li class="nav-item dropdown">';
$html .= '<a class="nav-link dropdown-toggle" href="#" id="' . $folderId . '" role="button" data-bs-toggle="dropdown" aria-expanded="' . ($isExpanded ? 'true' : 'false') . '">'; $html .= '<a class="nav-link dropdown-toggle" href="#" id="' . $folderId . '" role="button" data-bs-toggle="dropdown" aria-expanded="' . ($isExpanded ? 'true' : 'false') . '">';
$html .= htmlspecialchars($item['title']) . ' <i class="bi bi-chevron-down"></i>'; $html .= htmlspecialchars($item['title']) . ' <i class="bi bi-chevron-down"></i>';
@@ -1499,12 +1512,12 @@ class CodePressCMS {
$html .= '</ul>'; $html .= '</ul>';
$html .= '</li>'; $html .= '</li>';
} else { } else {
// Nested folders in dropdown // Nested folders - CSS hover submenu (unlimited depth)
$html .= '<li class="dropdown-submenu">'; $html .= '<li class="dropdown-submenu' . ($isExpanded ? ' show' : '') . '">';
$html .= '<a class="dropdown-item dropdown-toggle" href="#" id="' . $folderId . '">'; $html .= '<a class="dropdown-item dropdown-toggle" href="#" id="' . $folderId . '" role="button" aria-expanded="' . ($isExpanded ? 'true' : 'false') . '">';
$html .= htmlspecialchars($item['title']) . ' <i class="bi bi-chevron-down"></i>'; $html .= htmlspecialchars($item['title']) . ' <i class="bi bi-chevron-right"></i>';
$html .= '</a>'; $html .= '</a>';
$html .= '<ul class="dropdown-menu" aria-labelledby="' . $folderId . '">'; $html .= '<ul class="dropdown-menu' . ($isExpanded ? ' show' : '') . '" aria-labelledby="' . $folderId . '">';
$html .= $this->renderMenu($item['children'], $level + 1); $html .= $this->renderMenu($item['children'], $level + 1);
$html .= '</ul>'; $html .= '</ul>';
$html .= '</li>'; $html .= '</li>';
+15 -6
View File
@@ -169,8 +169,22 @@ class ThemeManager {
$compiler = new Compiler(); $compiler = new Compiler();
$compiler->setImportPaths($this->themeDir . '/assets/scss'); $compiler->setImportPaths($this->themeDir . '/assets/scss');
$css = $compiler->compileString(file_get_contents($scssFile))->getCss(); $css = $compiler->compileString(file_get_contents($scssFile))->getCss();
// Remove read-only before writing (file may be locked from previous compile)
if (is_file($outFile)) {
@chmod($outFile, 0644);
}
if (is_file($cacheFile)) {
@chmod($cacheFile, 0644);
}
file_put_contents($outFile, $css); file_put_contents($outFile, $css);
file_put_contents($cacheFile, (string)$mtime); file_put_contents($cacheFile, (string)$mtime);
// Set read-only to prevent manual edits
@chmod($outFile, 0444);
@chmod($cacheFile, 0444);
return $outFile; return $outFile;
} catch (\Throwable $e) { } catch (\Throwable $e) {
error_log('ThemeManager SCSS compile error: ' . $e->getMessage()); error_log('ThemeManager SCSS compile error: ' . $e->getMessage());
@@ -180,16 +194,11 @@ class ThemeManager {
/** /**
* Get the public URL for the theme CSS. * Get the public URL for the theme CSS.
* Priority: 1) assets/css/theme.css (manual), 2) assets/css_compiled/theme.css (compiled), 3) null * Uses assets/css_compiled/theme.css (compiled from SCSS by scssphp).
*/ */
public function getCssUrl(): ?string { public function getCssUrl(): ?string {
$manualCss = $this->themeDir . '/assets/css/theme.css';
$compiledCss = $this->themeDir . '/assets/css_compiled/theme.css'; $compiledCss = $this->themeDir . '/assets/css_compiled/theme.css';
if (is_file($manualCss)) {
return $this->getThemeAssetUrl('css/theme.css');
}
if (is_file($compiledCss)) { if (is_file($compiledCss)) {
return $this->getThemeAssetUrl('css_compiled/theme.css'); return $this->getThemeAssetUrl('css_compiled/theme.css');
} }
+8 -8
View File
@@ -2,14 +2,14 @@
The admin manager guide contains the following topics: The admin manager guide contains the following topics:
- **Dashboard** - Website overview - **Dashboard** - Role-based overview of the website (Admin, Content Manager, BI Manager, Site Admin)
- **Content management** - Managing files and pages - **Content management** - Managing files and pages with layout selection and plugins
- **Configuration** - Site settings - **Configuration** - Site settings (title, language, author, analytics, logging)
- **Theme management** - Managing and creating themes - **Theme management** - Managing, activating and creating themes (SCSS compilation)
- **Security** - Bot protection and sessions - **Security** - Bot protection, rate limiting and session settings
- **Plugins** - Managing and creating plugins - **Plugins** - Managing plugins: content (sidebar) and system (admin menu/API)
- **Users** - Adding and removing users - **Users** - User management with roles (Admin, Content Manager, BI Manager, Site Admin)
- **Statistics** - Viewing visitor statistics - **Statistics** - Viewing and exporting visitor statistics
- **Logs** - Viewing and filtering log files - **Logs** - Viewing and filtering log files
- **Media** - Managing media files - **Media** - Managing media files
- **Update** - Check for updates - **Update** - Check for updates
+40 -11
View File
@@ -2,16 +2,45 @@
## Managing files ## Managing files
- **Upload** - Upload media files - **New folder** — Create folder structure
- **New folder** - Create folder structure - **New file** Create a page (`.md`, `.php`, `.html`)
- **New file** - Create a page - **Edit** — Modify existing content in the CodeMirror editor
- **Edit** - Modify existing content - **Rename** — Change file or folder names
- **Rename** - Change file names - **Move** — Move content to another folder
- **Move** - Move content - **Delete** — Remove content
- **Delete** - Remove content - **Rename folder** — Change a folder name
## Editor ## Editor (content-edit)
- CodeMirror with syntax highlighting - **CodeMirror** with syntax highlighting (Markdown, PHP, HTML)
- Toolbar for Markdown formatting - **Toolbar** for quickly inserting Markdown
- Shortcuts: Ctrl+S (save), Ctrl+N (new) - **Shortcuts**: Ctrl+S (save), Ctrl+N (new)
## Layout selection
On the content-edit page you can choose the layout from the layouts defined in `theme.json` (template mapping). The selected layout is stored in the frontmatter `layout:` key.
## Plugins on pages
- Content plugins (from `plugin.json` with `type: "content"`) appear in the plugin selection
- Choose which plugins appear in the sidebar
- **Order is adjustable** with up/down buttons
- The plugin order is stored in the frontmatter `plugins:` key
- Plugins are **hidden** when the chosen layout has no sidebar (e.g. `full_content`)
## Frontmatter
The editor updates the frontmatter live when layout or plugin selection changes:
```markdown
---
layout: left_sidebar
plugins:
- HTMLBlock
- Navigation
---
# Page title
Content...
```
+26 -4
View File
@@ -1,10 +1,32 @@
# Dashboard # Dashboard
The dashboard shows an overview of: The dashboard shows a **role-based overview** of the website. Which widgets are visible depends on the role of the logged-in user.
- Views (30 days) ## Roles and widgets
- Unique visitors
### Admin
- Views (30 days) and unique visitors
- Number of pages and folders - Number of pages and folders
- Active plugins - Active plugins
- Recent activity - Recent activity
- Quick actions - System information
- Quick actions (new page, plugin, theme)
### Content Manager
- Number of pages and folders
- Recent content changes
- Quick actions (new page, folder)
### BI Manager
- Views (30 days) and unique visitors
- Top pages
- Recent visits
### Site Admin
- Active plugins and themes
- System information
- Quick actions (theme, plugin, update)
## Access
The dashboard is the default page after login (`/admin/dashboard`). All roles have access to the dashboard.
+32 -5
View File
@@ -1,13 +1,40 @@
# Users # Users
## Add user Users are stored in `admin/config/admin.json` (file-based, no database). Each user has a **role** that determines which admin routes and sidebar items are visible.
## Roles
CodePress has four roles, defined in `AdminAuth::ROLE_PERMISSIONS`:
| Role | Label | Permissions |
|------|-------|-------------|
| `admin` | Admin | Everything (`*`) |
| `content-manager` | Content Manager | Content management, guide |
| `bi-manager` | BI Manager | Statistics, logs, guide |
| `site-admin` | Site Admin | Theme, plugins, statistics, logs, update, guide |
Roles are displayed with their label via `AdminAuth::ROLE_LABELS`.
## Adding a user
1. Go to **Users** 1. Go to **Users**
2. Enter username 2. Enter username
3. Choose password 3. Choose password (stored as bcrypt hash)
4. Click **Add** 4. Select a role
5. Click **Add**
## Delete user ## Editing a user
- Cannot delete own account - Change password (new bcrypt hash)
- Change role (immediately affects visible routes and sidebar items)
## Deleting a user
- Not possible for own account
- Confirm with password - Confirm with password
## Access control
- Route access is checked in `public/admin.php` via `AdminAuth::hasPermission()`
- Unauthorized routes return a **403 error**
- Sidebar items are conditionally shown via the `has_permission()` Twig function in `admin.twig`
+56 -11
View File
@@ -2,20 +2,65 @@
## Managing themes ## Managing themes
1. Go to **Theme** in admin menu 1. Go to **Theme** in the admin menu
2. **Activate** - Choose active theme 2. **Activate** Choose the active theme (stored in `config.json`)
3. **Compile SCSS** - Process SCSS to CSS 3. **Compile SCSS** Process SCSS to CSS (forced)
4. **New theme** - Create custom theme 4. **New theme** Create a custom theme via admin or manually
## Theme structure ## Theme structure
``` ```
themes/default/ themes/default/
├── theme.json # Theme configuration ├── theme.json # { title, config.default_template, template: layout→.twig }
├── base.twig # Main layout ├── base.twig # Main layout (head, header, nav, breadcrumb, footer)
├── full_content.twig # Layouts ├── full_content.twig # Layout: full width
├── left_sidebar.twig ├── left_sidebar.twig # Layout: sidebar on the left
├── right_sidebar.twig ├── right_sidebar.twig # Layout: sidebar on the right
├── partials/ # Header, nav, footer ├── custom1.twig # Layout: custom
── assets/ # CSS, JS, images ── guide.twig # Layout: guide with sidebar (Navigation plugin)
├── partials/ # header.twig, navigation.twig, footer.twig
└── assets/
├── scss/theme.scss # SCSS source (only CSS source — manual css/theme.css must not exist)
├── css_compiled/ # Generated by scssphp (read-only, do not edit manually)
├── css/ # External CSS (bootstrap.min.css, bootstrap-icons.css, mobile.css)
├── js/ # app.js, bootstrap.bundle.min.js
├── fonts/ # bootstrap-icons.woff, woff2
└── img/ # favicon, icon, world-map
``` ```
## theme.json
```json
{
"title": "default",
"config": {
"default_template": "full_content"
},
"template": {
"full_content": "full_content.twig",
"left_sidebar": "left_sidebar.twig",
"right_sidebar": "right_sidebar.twig",
"custom1": "custom1.twig",
"guide": "guide.twig"
}
}
```
- `title` — Display name in admin
- `config.default_template` — Default layout for pages without a `layout:` frontmatter
- `template` — Mapping from layout name to `.twig` file
## SCSS compilation
- `ThemeManager` compiles `assets/scss/theme.scss` at runtime to `assets/css_compiled/theme.css` via scssphp
- `css_compiled/` is **read-only** — do not edit manually
- `assets/css/theme.css` must **not** exist; otherwise `ThemeManager::getCssUrl()` ignores the SCSS
- After SCSS changes: remove `assets/css_compiled/theme.css` and `.mtime` to force recompilation
## Layouts
Layouts are chosen via the frontmatter `layout:` key in content files. Unknown layouts fall back to `config.default_template` from `theme.json`.
## guide.twig
Special layout for guides. Injects the Navigation plugin into the sidebar for sidebar navigation. Automatically used for pages in the `guide/` folder.
+15 -8
View File
@@ -2,13 +2,20 @@
The CodePress developer guide contains the following topics: The CodePress developer guide contains the following topics:
- **Architecture** - CMS architecture and folder structure - **Architecture** CMS architecture and folder structure
- **Core classes** - CodePressCMS, ThemeManager, PluginManager - **Core classes** CodePressCMS, ThemeManager, PluginManager, AdminAuth
- **Plugin development** - Developing plugins - **Plugin development** — Plugin types (content/system), structure, API
- **Routing** - Frontend and admin routing - **Routing** Frontend, admin and plugin admin routing
- **Security** - XSS, CSRF, path traversal - **Security** XSS, CSRF, path traversal, RBAC
- **Testing** - Penetration, accessibility and functional tests - **Testing** Penetration, accessibility and functional tests
- **Debugging** - Logging and cache - **Debugging** Logging and cache
- **Performance** - OPcache and SCSS caching - **Performance** OPcache and SCSS caching
Important concepts in CodePress 2.5.2:
- **Plugin types** — Content plugins (sidebar) and system plugins (admin menu, routes, API)
- **Admin plugin API** — System plugins can register admin menu items and routes
- **SCSS compilation** — `ThemeManager` compiles SCSS to `css_compiled/` via scssphp (read-only)
- **Asset serving** — `public/asset.php` serves themes/, admin/assets/ and plugins/ on Apache (instead of PHP dev router)
Select a topic from the navigation on the left. Select a topic from the navigation on the left.
+49 -7
View File
@@ -2,18 +2,60 @@
## Frontend routing ## Frontend routing
Via `cms/router.php` for PHP dev server: Frontend routing goes through `cms/router.php` (PHP dev server) or `.htaccess` (Apache). Both provide clean URLs.
```php ### PHP dev server
// Clean URLs: /nl/page
// Query: ?page=page&lang=nl Start the server with:
```bash
php -S localhost:8080 cms/router.php
``` ```
`cms/router.php` serves:
- Clean URLs: `/nl/page``public/index.php?page=page&lang=nl`
- `themes/` assets
- `admin/assets/` assets
- `plugins/` assets
### Apache (live server)
`public/.htaccess` rewrites URLs to `public/index.php`. Asset URLs (`/themes/`, `/admin/assets/`, `/plugins/`) are forwarded to `public/asset.php`.
`public/asset.php` serves files from the correct folders with the correct MIME type:
- `/themes/<name>/assets/...``themes/<name>/assets/...`
- `/admin/assets/...``admin/theme/default/assets/...`
- `/plugins/<name>/assets/...``plugins/<name>/assets/...`
## Admin routing ## Admin routing
Via `public/admin.php`: Admin routing goes through `public/admin.php` with clean URLs:
```
/admin/dashboard → ?route=dashboard
/admin/content → ?route=content
/admin/plugins → ?route=plugins
```
`cms/router.php` or `.htaccess` converts `/admin/<route>` to `?route=<route>`.
### Route access control (RBAC)
Each route is checked via `AdminAuth::hasPermission($route)`:
- The `admin` role has wildcard `*` access
- Other roles have an explicit list of allowed routes in `ROLE_PERMISSIONS`
- Unauthorized routes return a **403 error**
## Plugin admin routing
System plugins can register admin routes. These are handled by `PluginManager::handleAdminRoute($route)`:
1. The system plugin registers routes via `getAdminRoutes()`
2. `PluginManager::getAdminMenuItems()` collects admin menu items via `getAdminMenu()`
3. On an admin request `PluginManager::handleAdminRoute($route)` finds a plugin that handles the route
4. The plugin method `handleAdminRoute($route)` renders the page content
```php ```php
// Routes: /admin/dashboard, /admin/content, etc. // PluginManager calls this for /admin/my-system
// Query parameter: ?route=dashboard $plugin->handleAdminRoute('my-system');
``` ```
@@ -4,28 +4,48 @@ Content is stored in the `content/` folder without a database.
## Supported file formats ## Supported file formats
- `.md` - Markdown (recommended) - `.md` Markdown (recommended)
- `.php` - Dynamic PHP pages - `.php` Dynamic PHP pages
- `.html` - Static HTML pages - `.html` Static HTML pages
## File name conventions ## File name conventions
``` ```
en.page-name.md # English page
nl.pagina-naam.md # Dutch page nl.pagina-naam.md # Dutch page
en.page-name.md # English page
nl.folder/ # Dutch folder (requires an index file to be clickable)
``` ```
The language prefix is dynamically removed based on available languages via `getAvailableLanguages()`.
## Frontmatter ## Frontmatter
Markdown files can contain frontmatter metadata: Markdown files can contain frontmatter metadata:
```markdown ```markdown
--- ---
layout: full_content layout: left_sidebar
plugins: HTMLBlock plugins:
- HTMLBlock
- Navigation
--- ---
# Page title # Page title
Content... Content...
``` ```
## Frontmatter fields
- `layout` — Layout name (must exist in the `theme.json` template mapping; fallback to `config.default_template`)
- `plugins` — List of content plugins that appear in the sidebar
### plugins field
The `plugins` field determines:
- **Which plugins** appear in the sidebar (only content plugins, not system plugins)
- **The order** of the plugins in the sidebar (top to bottom)
The order in the frontmatter is the order in which the plugins are shown. In the admin content-edit page you can adjust the order with up/down buttons; the frontmatter is updated live.
Plugins are not shown when the chosen layout has no sidebar (e.g. `full_content`).
+35 -3
View File
@@ -5,12 +5,44 @@
1. Login at `/admin` 1. Login at `/admin`
2. Go to **Content** 2. Go to **Content**
3. Choose a folder or create a new page 3. Choose a folder or create a new page
4. Edit content in the editor 4. Edit content in the CodeMirror editor
5. Save with Ctrl+S 5. Save with Ctrl+S
## Editor features ## Editor features
- **CodeMirror** editor with syntax highlighting - **CodeMirror** editor with syntax highlighting (Markdown, PHP, HTML)
- **Toolbar** for quick Markdown insertion - **Toolbar** for quick Markdown insertion
- **Live preview** via button - **Shortcuts**: Ctrl+S (save), Ctrl+N (new)
- **Auto-save** backups in `.bak/` folder - **Auto-save** backups in `.bak/` folder
## Layout selection
On the content-edit page you can choose the layout from the layouts defined in `theme.json` (template mapping). The selected layout is stored in the frontmatter `layout:` key.
```markdown
---
layout: left_sidebar
---
```
Unknown layouts fall back to `config.default_template` from `theme.json`.
## Plugin selection and order
On the content-edit page you can choose content plugins for the sidebar:
- **Selection** — Only content plugins (from `plugin.json` with `type: "content"`) appear in the plugin selection
- **Order** — Adjust the order with up/down buttons
- **Frontmatter update** — The frontmatter `plugins:` key is updated live on changes
- **Layout without sidebar** — Plugins are hidden when the chosen layout has no sidebar (e.g. `full_content`)
```markdown
---
layout: left_sidebar
plugins:
- HTMLBlock
- Navigation
---
```
The order in the frontmatter determines the order of the plugins in the sidebar (top to bottom).
+9
View File
@@ -1,3 +1,12 @@
# Manual # Manual
Welcome to the CodePress CMS manual (version 2.5.2). CodePress is a file-based CMS without a database — content, configuration and users are stored in files.
The manual is divided into four sections:
- **Admin Manager** — Managing the website: dashboard, content, configuration, themes, security, plugins (content and system), users with roles, statistics and logs
- **Content Manager** — Managing content: structure, managing pages (layout selection, plugin order), media and the Content API
- **Theme Developer** — Developing themes: structure, `theme.json`, Twig templates, SCSS styling (only CSS source), layouts and new themes
- **CodePress Developer** — Core development: architecture, core classes, plugin development (content/system), routing, security (RBAC), testing, debugging and performance
Select a topic from the navigation on the left. Select a topic from the navigation on the left.
+1 -1
View File
@@ -3,7 +3,7 @@
The theme developer guide contains the following topics: The theme developer guide contains the following topics:
- **Theme structure** - Folder structure of a theme - **Theme structure** - Folder structure of a theme
- **theme.json** - Theme configuration - **theme.json** - Configuration of a theme
- **Twig templates** - Twig syntax, variables and blocks - **Twig templates** - Twig syntax, variables and blocks
- **SCSS styling** - Compiling and writing SCSS - **SCSS styling** - Compiling and writing SCSS
- **Layouts** - Defining and using layouts - **Layouts** - Defining and using layouts
+40 -11
View File
@@ -2,20 +2,49 @@
``` ```
themes/my-theme/ themes/my-theme/
├── theme.json # Theme configuration ├── theme.json # { title, config.default_template, template: layout→.twig }
├── base.twig # Main layout ├── base.twig # Main layout (head, header, nav, breadcrumb, footer)
├── full_content.twig # Layout: full-width ├── full_content.twig # Layout: full width
├── left_sidebar.twig # Layout: left sidebar ├── left_sidebar.twig # Layout: sidebar on the left
├── right_sidebar.twig # Layout: right sidebar ├── right_sidebar.twig # Layout: sidebar on the right
├── custom.twig # Layout: custom ├── custom1.twig # Layout: custom
├── guide.twig # Layout: guide with sidebar (Navigation plugin)
├── partials/ ├── partials/
│ ├── header.twig │ ├── header.twig
│ ├── navigation.twig │ ├── navigation.twig
│ └── footer.twig │ └── footer.twig
└── assets/ └── assets/
├── scss/theme.scss # SCSS source ├── scss/theme.scss # SCSS source (the only CSS source)
├── css/ # CSS files ├── css_compiled/ # Generated by scssphp (read-only, do not edit manually)
├── js/theme.js # JavaScript ├── css/ # External CSS (bootstrap.min.css, bootstrap-icons.css, mobile.css)
├── fonts/ # Fonts ├── js/ # app.js, bootstrap.bundle.min.js
── img/ # Images ── fonts/ # bootstrap-icons.woff, woff2
└── img/ # favicon, icon, world-map
```
## SCSS is the only CSS source
- **ALWAYS** edit `assets/scss/theme.scss` — this is the only CSS source
- `ThemeManager` compiles SCSS at runtime to `assets/css_compiled/theme.css` via scssphp
- `assets/css_compiled/` is **read-only** — do not edit manually
- **NEVER** create or edit `assets/css/theme.css` manually; this file must not exist
- `ThemeManager::getCssUrl()` priority: 1) `assets/css/theme.css` (manual), 2) `assets/css_compiled/theme.css` (compiled). If `theme.css` exists, the SCSS is ignored.
- After SCSS changes: remove `assets/css_compiled/theme.css` and `.mtime` to force recompilation
```bash
rm themes/my-theme/assets/css_compiled/theme.css themes/my-theme/assets/css_compiled/.mtime
```
## guide.twig
Special layout for guides. Injects the Navigation plugin into the sidebar for sidebar navigation. Automatically used for pages in the `guide/` folder.
## Plugin CSS loading
Plugin CSS is automatically loaded after theme CSS in `base.twig`, so themes can override plugin styling:
```twig
{% for cssUrl in plugin_css_urls|default([]) %}
<link href="{{ cssUrl }}" rel="stylesheet">
{% endfor %}
``` ```
+8 -8
View File
@@ -2,14 +2,14 @@
De admin beheerder handleiding bevat de volgende onderwerpen: De admin beheerder handleiding bevat de volgende onderwerpen:
- **Dashboard** - Overzicht van de website - **Dashboard** - Role-based overzicht van de website (Admin, Content Beheerder, BI Beheerder, Site Admin)
- **Content beheer** - Bestanden en pagina's beheren - **Content beheer** - Bestanden en pagina's beheren met layout selectie en plugins
- **Configuratie** - Site instellingen - **Configuratie** - Site instellingen (titel, taal, auteur, analytics, logging)
- **Thema beheer** - Thema's beheren en aanmaken - **Thema beheer** - Thema's beheren, activeren en aanmaken (SCSS compilatie)
- **Beveiliging** - Bot bescherming en sessies - **Beveiliging** - Bot bescherming, rate limiting en sessie instellingen
- **Plugins** - Plugins beheren en aanmaken - **Plugins** - Plugins beheren: content (sidebar) en systeem (admin menu/API)
- **Gebruikers** - Gebruikers toevoegen en verwijderen - **Gebruikers** - Gebruikersbeheer met rollen (Admin, Content Beheerder, BI Beheerder, Site Admin)
- **Statistieken** - Bezoekersstatistieken bekijken - **Statistieken** - Bezoekersstatistieken bekijken en exporteren
- **Logs** - Logbestanden bekijken en filteren - **Logs** - Logbestanden bekijken en filteren
- **Media** - Media bestanden beheren - **Media** - Media bestanden beheren
- **Update** - Controleren op updates - **Update** - Controleren op updates
+40 -11
View File
@@ -2,16 +2,45 @@
## Bestanden beheren ## Bestanden beheren
- **Uploaden** - Media bestanden uploaden - **Nieuwe map** Mappen structuur aanmaken
- **Nieuwe map** - Mappen structuur aanmaken - **Nieuw bestand** — Pagina aanmaken (`.md`, `.php`, `.html`)
- **Nieuw bestand** - Pagina aanmaken - **Bewerken** — Bestaande content wijzigen in CodeMirror editor
- **Bewerken** - Bestaande content wijzigen - **Hernoemen** Bestands- of mapnamen aanpassen
- **Hernoemen** - Bestandsnamen aanpassen - **Verplaatsen** — Content verplaatsen naar andere map
- **Verplaatsen** - Content verplaatsen - **Verwijderen** Content verwijderen
- **Verwijderen** - Content verwijderen - **Map hernoemen** — Map naam wijzigen
## Editor ## Editor (content-edit)
- CodeMirror met syntax highlighting - **CodeMirror** met syntax highlighting (Markdown, PHP, HTML)
- Toolbar voor Markdown formatting - **Toolbar** voor snel Markdown invoeren
- Sneltoetsen: Ctrl+S (opslaan), Ctrl+N (nieuw) - **Sneltoetsen**: Ctrl+S (opslaan), Ctrl+N (nieuw)
## Layout selectie
Op de content-edit pagina kun je de layout kiezen uit de layouts gedefinieerd in `theme.json` (template mapping). De geselecteerde layout wordt opgeslagen in de frontmatter `layout:` key.
## Plugins op pagina's
- Content plugins (uit `plugin.json` met `type: "content"`) verschijnen in de plugin selectie
- Kies welke plugins in de sidebar verschijnen
- **Volgorde aanpasbaar** met up/down knoppen
- De plugin volgorde wordt opgeslagen in de frontmatter `plugins:` key
- Plugins worden **verbergen** als de gekozen layout geen sidebar heeft (bijv. `full_content`)
## Frontmatter
De editor werkt de frontmatter live bij bij wijzigingen van layout of plugin selectie:
```markdown
---
layout: left_sidebar
plugins:
- HTMLBlock
- Navigation
---
# Pagina titel
Content...
```
+26 -4
View File
@@ -1,10 +1,32 @@
# Dashboard # Dashboard
Het dashboard toont een overzicht van: Het dashboard toont een **role-based overzicht** van de website. Welke widgets zichtbaar zijn, hangt af van de rol van de ingelogde gebruiker.
- Weergaven (30 dagen) ## Rollen en widgets
- Unieke bezoekers
### Admin
- Weergaven (30 dagen) en unieke bezoekers
- Aantal pagina's en mappen - Aantal pagina's en mappen
- Actieve plugins - Actieve plugins
- Recente activiteit - Recente activiteit
- Snelle acties - Systeem informatie
- Snelle acties (nieuwe pagina, plugin, thema)
### Content Beheerder
- Aantal pagina's en mappen
- Recente content wijzigingen
- Snelle acties (nieuwe pagina, map)
### BI Beheerder
- Weergaven (30 dagen) en unieke bezoekers
- Top pagina's
- Recente bezoeken
### Site Admin
- Actieve plugins en thema's
- Systeem informatie
- Snelle acties (thema, plugin, update)
## Toegang
Het dashboard is de standaard pagina na login (`/admin/dashboard`). Alle rollen hebben toegang tot het dashboard.
+29 -2
View File
@@ -1,13 +1,40 @@
# Gebruikers # Gebruikers
Gebruikers worden opgeslagen in `admin/config/admin.json` (file-based, geen database). Elke gebruiker heeft een **rol** die bepaalt welke admin routes en sidebar items zichtbaar zijn.
## Rollen
CodePress kent vier rollen, gedefinieerd in `AdminAuth::ROLE_PERMISSIONS`:
| Rol | Label | Permissies |
|-----|-------|-----------|
| `admin` | Admin | Alles (`*`) |
| `content-manager` | Content Beheerder | Content beheer, handleiding |
| `bi-manager` | BI Beheerder | Statistieken, logs, handleiding |
| `site-admin` | Site Admin | Thema, plugins, statistieken, logs, update, handleiding |
Rollen worden getoond met hun label via `AdminAuth::ROLE_LABELS`.
## Gebruiker toevoegen ## Gebruiker toevoegen
1. Ga naar **Gebruikers** 1. Ga naar **Gebruikers**
2. Vul gebruikersnaam in 2. Vul gebruikersnaam in
3. Kies wachtwoord 3. Kies wachtwoord (opgeslagen als bcrypt hash)
4. Klik **Toevoegen** 4. Selecteer een rol
5. Klik **Toevoegen**
## Gebruiker bewerken
- Wachtwoord wijzigen (nieuwe bcrypt hash)
- Rol wijzigen (beïnvloedt direct zichtbare routes en sidebar items)
## Gebruiker verwijderen ## Gebruiker verwijderen
- Kan niet voor eigen account - Kan niet voor eigen account
- Bevestig met wachtwoord - Bevestig met wachtwoord
## Toegangscontrole
- Route access wordt gecontroleerd in `public/admin.php` via `AdminAuth::hasPermission()`
- Onbevoegde routes geven een **403 error**
- Sidebar items worden conditioneel getoond via de `has_permission()` Twig function in `admin.twig`
+55 -8
View File
@@ -3,17 +3,64 @@
## Thema's beheren ## Thema's beheren
1. Ga naar **Thema** in admin menu 1. Ga naar **Thema** in admin menu
2. **Activeren** - Kies actief thema 2. **Activeren** Kies actief thema (wordt opgeslagen in `config.json`)
3. **SCSS compileren** - Verwerk SCSS naar CSS 3. **SCSS compileren** Verwerk SCSS naar CSS (geforceerd)
4. **Nieuw thema** - Eigen thema aanmaken 4. **Nieuw thema** Eigen thema aanmaken via admin of handmatig
## Thema structuur ## Thema structuur
``` ```
themes/default/ themes/default/
├── theme.json # Thema configuratie ├── theme.json # { title, config.default_template, template: layout→.twig }
├── base.twig # Hoofd layout ├── base.twig # Hoofd layout (head, header, nav, breadcrumb, footer)
├── full_content.twig # Layouts ├── full_content.twig # Layout: volledige breedte
├── partials/ # Header, nav, footer ├── left_sidebar.twig # Layout: sidebar links
── assets/ # CSS, JS, afbeeldingen ── right_sidebar.twig # Layout: sidebar rechts
├── custom1.twig # Layout: custom
├── guide.twig # Layout: handleiding met sidebar (Navigation plugin)
├── partials/ # header.twig, navigation.twig, footer.twig
└── assets/
├── scss/theme.scss # SCSS bron (enige CSS bron — handmatige css/theme.css mag niet bestaan)
├── css_compiled/ # Gegenereerd door scssphp (read-only, niet handmatig aanpassen)
├── css/ # Externe CSS (bootstrap.min.css, bootstrap-icons.css, mobile.css)
├── js/ # app.js, bootstrap.bundle.min.js
├── fonts/ # bootstrap-icons.woff, woff2
└── img/ # favicon, icon, world-map
``` ```
## theme.json
```json
{
"title": "default",
"config": {
"default_template": "full_content"
},
"template": {
"full_content": "full_content.twig",
"left_sidebar": "left_sidebar.twig",
"right_sidebar": "right_sidebar.twig",
"custom1": "custom1.twig",
"guide": "guide.twig"
}
}
```
- `title` — Weergavenaam in admin
- `config.default_template` — Standaard layout voor pagina's zonder `layout:` frontmatter
- `template` — Mapping van layout naam naar `.twig` bestand
## SCSS compilatie
- `ThemeManager` compileert `assets/scss/theme.scss` runtime naar `assets/css_compiled/theme.css` via scssphp
- `css_compiled/` is **read-only** — niet handmatig aanpassen
- `assets/css/theme.css` mag **niet** bestaan; anders negeert `ThemeManager::getCssUrl()` de SCSS
- Na SCSS wijzigingen: verwijder `assets/css_compiled/theme.css` en `.mtime` om te forceren
## Layouts
Layouts worden gekozen via frontmatter `layout:` key in content bestanden. Onbekende layouts vallen terug op `config.default_template` uit `theme.json`.
## guide.twig
Speciale layout voor handleidingen. Injecteert de Navigation plugin in de sidebar voor zijbalk navigatie. Wordt automatisch gebruikt voor pagina's in de `guide/` map.
+15 -8
View File
@@ -2,13 +2,20 @@
De CodePress developer handleiding bevat de volgende onderwerpen: De CodePress developer handleiding bevat de volgende onderwerpen:
- **Architectuur** - CMS architectuur en mappenstructuur - **Architectuur** CMS architectuur en mappenstructuur
- **Core classes** - CodePressCMS, ThemeManager, PluginManager - **Core classes** CodePressCMS, ThemeManager, PluginManager, AdminAuth
- **Plugin development** - Plugins ontwikkelen - **Plugin development** Plugin types (content/systeem), structuur, API
- **Routing** - Frontend en admin routing - **Routing** Frontend, admin en plugin admin routing
- **Beveiliging** - XSS, CSRF, path traversal - **Beveiliging** XSS, CSRF, path traversal, RBAC
- **Testing** - Penetratie, accessibility en functionele tests - **Testing** Penetratie, accessibility en functionele tests
- **Debugging** - Logging en cache - **Debugging** Logging en cache
- **Performance** - OPcache en SCSS caching - **Performance** OPcache en SCSS caching
Belangrijke concepten in CodePress 2.5.2:
- **Plugin types** — Content plugins (sidebar) en systeem plugins (admin menu, routes, API)
- **Admin plugin API** — Systeem plugins kunnen admin menu items en routes registreren
- **SCSS compilatie** — `ThemeManager` compileert SCSS naar `css_compiled/` via scssphp (read-only)
- **Asset serving** — `public/asset.php` serveert themes/, admin/assets/ en plugins/ op Apache (i.p.v. PHP dev router)
Selecteer een onderwerp uit de navigatie aan de linkerkant. Selecteer een onderwerp uit de navigatie aan de linkerkant.
+49 -7
View File
@@ -2,18 +2,60 @@
## Frontend routing ## Frontend routing
Via `cms/router.php` voor PHP dev server: Frontend routing verloopt via `cms/router.php` (PHP dev server) of `.htaccess` (Apache). Beide zorgen voor clean URLs.
```php ### PHP dev server
// Schone URLs: /nl/pagina
// Query: ?page=pagina&lang=nl Start de server met:
```bash
php -S localhost:8080 cms/router.php
``` ```
`cms/router.php` serveert:
- Schone URLs: `/nl/pagina``public/index.php?page=pagina&lang=nl`
- `themes/` assets
- `admin/assets/` assets
- `plugins/` assets
### Apache (live server)
`public/.htaccess` herschrijft URLs naar `public/index.php`. Asset URLs (`/themes/`, `/admin/assets/`, `/plugins/`) worden doorgestuurd naar `public/asset.php`.
`public/asset.php` serveert bestanden vanuit de juiste mappen met het juiste MIME-type:
- `/themes/<name>/assets/...``themes/<name>/assets/...`
- `/admin/assets/...``admin/theme/default/assets/...`
- `/plugins/<name>/assets/...``plugins/<name>/assets/...`
## Admin routing ## Admin routing
Via `public/admin.php`: Admin routing verloopt via `public/admin.php` met clean URLs:
```
/admin/dashboard → ?route=dashboard
/admin/content → ?route=content
/admin/plugins → ?route=plugins
```
`cms/router.php` of `.htaccess` zet `/admin/<route>` om naar `?route=<route>`.
### Route access control (RBAC)
Elke route wordt gecontroleerd via `AdminAuth::hasPermission($route)`:
- `admin` rol heeft wildcard `*` toegang
- Andere rollen hebben een expliciete lijst van toegestane routes in `ROLE_PERMISSIONS`
- Onbevoegde routes geven een **403 error**
## Plugin admin routing
Systeem plugins kunnen admin routes registreren. Deze worden afgehandeld door `PluginManager::handleAdminRoute($route)`:
1. Systeem plugin registreert routes via `getAdminRoutes()`
2. `PluginManager::getAdminMenuItems()` verzamelt admin menu items via `getAdminMenu()`
3. Bij een admin request zoekt `PluginManager::handleAdminRoute($route)` een plugin die de route afhandelt
4. De plugin methode `handleAdminRoute($route)` rendert de pagina inhoud
```php ```php
// Routes: /admin/dashboard, /admin/content, etc. // PluginManager roept deze aan voor /admin/mijn-systeem
// Query parameter: ?route=dashboard $plugin->handleAdminRoute('mijn-systeem');
``` ```
@@ -4,28 +4,48 @@ Content wordt opgeslagen in de `content/` map zonder database.
## Ondersteunde bestandsformaten ## Ondersteunde bestandsformaten
- `.md` - Markdown (aanbevolen) - `.md` Markdown (aanbevolen)
- `.php` - Dynamische PHP pagina's - `.php` Dynamische PHP pagina's
- `.html` - Statische HTML pagina's - `.html` Statische HTML pagina's
## Bestandsnaam conventies ## Bestandsnaam conventies
``` ```
nl.pagina-naam.md # Nederlandse pagina nl.pagina-naam.md # Nederlandse pagina
en.page-name.md # Engelse pagina en.page-name.md # Engelse pagina
nl.map/ # Nederlandse map (vereist index bestand om klikbaar te zijn)
``` ```
Taalprefix wordt dynamisch verwijderd op basis van beschikbare talen via `getAvailableLanguages()`.
## Frontmatter ## Frontmatter
Markdown bestanden kunnen frontmatter metadata bevatten: Markdown bestanden kunnen frontmatter metadata bevatten:
```markdown ```markdown
--- ---
layout: full_content layout: left_sidebar
plugins: HTMLBlock plugins:
- HTMLBlock
- Navigation
--- ---
# Pagina titel # Pagina titel
Content... Content...
``` ```
## Frontmatter velden
- `layout` — Layout naam (moet voorkomen in `theme.json` template mapping; fallback op `config.default_template`)
- `plugins` — Lijst van content plugins die in de sidebar verschijnen
### plugins veld
Het `plugins` veld bepaalt:
- **Welke plugins** in de sidebar verschijnen (alleen content plugins, niet systeem plugins)
- **De volgorde** van de plugins in de sidebar (top tot onder)
De volgorde in de frontmatter is de volgorde waarin de plugins worden getoond. In de admin content-edit pagina kun je de volgorde aanpassen met up/down knoppen; de frontmatter wordt live bijgewerkt.
Plugins worden niet getoond als de gekozen layout geen sidebar heeft (bijv. `full_content`).
+35 -3
View File
@@ -5,12 +5,44 @@
1. Login op `/admin` 1. Login op `/admin`
2. Ga naar **Content** 2. Ga naar **Content**
3. Kies een map of maak een nieuwe pagina 3. Kies een map of maak een nieuwe pagina
4. Bewerk content in de editor 4. Bewerk content in de CodeMirror editor
5. Sla op met Ctrl+S 5. Sla op met Ctrl+S
## Editor functies ## Editor functies
- **CodeMirror** editor met syntax highlighting - **CodeMirror** editor met syntax highlighting (Markdown, PHP, HTML)
- **Toolbar** voor snel Markdown invoegen - **Toolbar** voor snel Markdown invoegen
- **Live preview** via knop - **Sneltoetsen**: Ctrl+S (opslaan), Ctrl+N (nieuw)
- **Auto-save** backups in `.bak/` map - **Auto-save** backups in `.bak/` map
## Layout selectie
Op de content-edit pagina kun je de layout kiezen uit de layouts gedefinieerd in `theme.json` (template mapping). De geselecteerde layout wordt opgeslagen in de frontmatter `layout:` key.
```markdown
---
layout: left_sidebar
---
```
Onbekende layouts vallen terug op `config.default_template` uit `theme.json`.
## Plugin selectie en volgorde
Op de content-edit pagina kun je content plugins kiezen voor de sidebar:
- **Selectie** — Alleen content plugins (uit `plugin.json` met `type: "content"`) verschijnen in de plugin selectie
- **Volgorde** — Pas de volgorde aan met up/down knoppen
- **Frontmatter update** — De frontmatter `plugins:` key wordt live bijgewerkt bij wijzigingen
- **Layout zonder sidebar** — Plugins worden verborgen als de gekozen layout geen sidebar heeft (bijv. `full_content`)
```markdown
---
layout: left_sidebar
plugins:
- HTMLBlock
- Navigation
---
```
De volgorde in de frontmatter bepaalt de volgorde van de plugins in de sidebar (top tot onder).
+9
View File
@@ -1,3 +1,12 @@
# Handleiding # Handleiding
Welkom bij de CodePress CMS handleiding (versie 2.5.2). CodePress is een file-based CMS zonder database — content, configuratie en gebruikers worden opgeslagen in bestanden.
De handleiding is verdeeld in vier secties:
- **Admin Beheerder** — Beheer van de website: dashboard, content, configuratie, thema's, beveiliging, plugins (content en systeem), gebruikers met rollen, statistieken en logs
- **Content Beheerder** — Content beheren: structuur, pagina's beheren (layout selectie, plugin volgorde), media en de Content API
- **Theme Developer** — Thema's ontwikkelen: structuur, `theme.json`, Twig templates, SCSS styling (enige CSS bron), layouts en nieuwe thema's
- **CodePress Developer** — Core ontwikkeling: architectuur, core classes, plugin development (content/systeem), routing, beveiliging (RBAC), testing, debugging en performance
Selecteer een onderwerp uit de navigatie aan de linkerkant. Selecteer een onderwerp uit de navigatie aan de linkerkant.
+38 -9
View File
@@ -2,20 +2,49 @@
``` ```
themes/mijn-thema/ themes/mijn-thema/
├── theme.json # Thema configuratie ├── theme.json # { title, config.default_template, template: layout→.twig }
├── base.twig # Hoofd layout ├── base.twig # Hoofd layout (head, header, nav, breadcrumb, footer)
├── full_content.twig # Layout: full-width ├── full_content.twig # Layout: volledige breedte
├── left_sidebar.twig # Layout: sidebar links ├── left_sidebar.twig # Layout: sidebar links
├── right_sidebar.twig # Layout: sidebar rechts ├── right_sidebar.twig # Layout: sidebar rechts
├── custom.twig # Layout: custom ├── custom1.twig # Layout: custom
├── guide.twig # Layout: handleiding met sidebar (Navigation plugin)
├── partials/ ├── partials/
│ ├── header.twig │ ├── header.twig
│ ├── navigation.twig │ ├── navigation.twig
│ └── footer.twig │ └── footer.twig
└── assets/ └── assets/
├── scss/theme.scss # SCSS bron ├── scss/theme.scss # SCSS bron (de enige CSS bron)
├── css/ # CSS bestanden ├── css_compiled/ # Gegenereerd door scssphp (read-only, niet handmatig aanpassen)
├── js/theme.js # JavaScript ├── css/ # Externe CSS (bootstrap.min.css, bootstrap-icons.css, mobile.css)
├── fonts/ # Lettertypes ├── js/ # app.js, bootstrap.bundle.min.js
── img/ # Afbeeldingen ── fonts/ # bootstrap-icons.woff, woff2
└── img/ # favicon, icon, world-map
```
## SCSS is de enige CSS bron
- **ALTIJD** `assets/scss/theme.scss` aanpassen — dit is de enige CSS bron
- `ThemeManager` compileert SCSS runtime naar `assets/css_compiled/theme.css` via scssphp
- `assets/css_compiled/` is **read-only** — niet handmatig aanpassen
- **NOOIT** `assets/css/theme.css` handmatig aanmaken of aanpassen; dit bestand mag niet bestaan
- `ThemeManager::getCssUrl()` prioriteit: 1) `assets/css/theme.css` (handmatig), 2) `assets/css_compiled/theme.css` (gecompileerd). Als `theme.css` bestaat, wordt de SCSS negeren.
- Na SCSS wijzigingen: verwijder `assets/css_compiled/theme.css` en `.mtime` om te forceren
```bash
rm themes/mijn-thema/assets/css_compiled/theme.css themes/mijn-thema/assets/css_compiled/.mtime
```
## guide.twig
Speciale layout voor handleidingen. Injecteert de Navigation plugin in de sidebar voor zijbalk navigatie. Wordt automatisch gebruikt voor pagina's in de `guide/` map.
## Plugin CSS laden
Plugin CSS wordt automatisch geladen ná theme CSS in `base.twig`, zodat thema's plugin styling kunnen overschrijven:
```twig
{% for cssUrl in plugin_css_urls|default([]) %}
<link href="{{ cssUrl }}" rel="stylesheet">
{% endfor %}
``` ```
+57
View File
@@ -0,0 +1,57 @@
<?php
class GeoIPInfo
{
private ?CMSAPI $api = null;
public function setAPI(CMSAPI $api): void
{
$this->api = $api;
}
public function getConfig(): array
{
return [
'title' => 'GeoIP Info',
'type' => 'system',
];
}
/**
* Register admin menu items.
*/
public function getAdminMenu(): array
{
return [
['label' => 'GeoIP Info', 'route' => 'geoip-info', 'icon' => 'bi-globe2'],
];
}
/**
* Register admin routes this plugin handles.
*/
public function getAdminRoutes(): array
{
return ['geoip-info'];
}
/**
* Handle admin route — render the admin page.
*/
public function handleAdminRoute(string $route): void
{
$ip = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
$geoip = new GeoIP();
$countryCode = $geoip->lookupCountry($ip);
$country = GeoIP::getCountryName($countryCode);
$flag = GeoIP::getCountryFlagEmoji($countryCode);
echo '<h2 class="mb-4"><i class="bi bi-globe2"></i> GeoIP Info</h2>';
echo '<div class="card shadow-sm"><div class="card-body">';
echo '<table class="table table-sm">';
echo '<tr><td>IP adres</td><td><code>' . htmlspecialchars($ip) . '</code></td></tr>';
echo '<tr><td>Land</td><td>' . $flag . ' ' . htmlspecialchars($country) . '</td></tr>';
echo '</table>';
echo '</div></div>';
}
}
+7
View File
@@ -0,0 +1,7 @@
{
"name": "GeoIPInfo",
"version": "1.0.0",
"author": "CodePress",
"description": "Systeem plugin voor GeoIP informatie in admin",
"type": "system"
}
+22 -3
View File
@@ -23,6 +23,8 @@ class HTMLBlock
$currentPage = $this->api ? $this->api->getCurrentPageTitle() : 'Onbekend'; $currentPage = $this->api ? $this->api->getCurrentPageTitle() : 'Onbekend';
$isHomepage = $this->api ? $this->api->isHomepage() : false; $isHomepage = $this->api ? $this->api->isHomepage() : false;
$currentLang = $this->api ? $this->api->getCurrentLanguage() : 'nl'; $currentLang = $this->api ? $this->api->getCurrentLanguage() : 'nl';
$currentPath = $_GET['page'] ?? '';
$currentPath = preg_replace('/\.(md|php|html)$/', '', $currentPath);
$content = ' $content = '
<p class="mb-2"><strong>Huidige pagina:</strong> ' . htmlspecialchars($currentPage) . '</p> <p class="mb-2"><strong>Huidige pagina:</strong> ' . htmlspecialchars($currentPage) . '</p>
@@ -43,7 +45,7 @@ class HTMLBlock
</div>'; </div>';
} }
// Add quick navigation // Add dynamic quick navigation
$menu = $this->api->getMenu(); $menu = $this->api->getMenu();
if (!empty($menu)) { if (!empty($menu)) {
$content .= ' $content .= '
@@ -52,8 +54,25 @@ class HTMLBlock
foreach ($menu as $item) { foreach ($menu as $item) {
if ($item['type'] === 'file') { if ($item['type'] === 'file') {
$url = $this->api->createUrl($item['path']); $url = '/' . $currentLang . '/' . $item['path'];
$content .= '<li><a href="' . htmlspecialchars($url) . '" class="text-decoration-none">📄 ' . htmlspecialchars($item['title']) . '</a></li>'; $isActive = ($item['path'] === $currentPath) ? ' fw-bold' : '';
$content .= '<li><a href="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '" class="text-decoration-none' . $isActive . '"><i class="bi bi-file-earmark"></i> ' . htmlspecialchars($item['title'], ENT_QUOTES, 'UTF-8') . '</a></li>';
} elseif ($item['type'] === 'folder' && !empty($item['children'])) {
$url = '/' . $currentLang . '/' . $item['path'];
$isActive = strpos($currentPath, $item['path']) === 0 ? ' fw-bold' : '';
$content .= '<li><a href="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '" class="text-decoration-none' . $isActive . '"><i class="bi bi-folder"></i> ' . htmlspecialchars($item['title'], ENT_QUOTES, 'UTF-8') . '</a></li>';
// Show children if current page is in this folder
if (strpos($currentPath, $item['path']) === 0) {
$content .= '<ul class="list-unstyled ms-3 mb-1">';
foreach ($item['children'] as $child) {
if ($child['type'] === 'file') {
$childUrl = '/' . $currentLang . '/' . $child['path'];
$childActive = ($child['path'] === $currentPath) ? ' fw-bold' : '';
$content .= '<li><a href="' . htmlspecialchars($childUrl, ENT_QUOTES, 'UTF-8') . '" class="text-decoration-none' . $childActive . '"><i class="bi bi-file-earmark"></i> ' . htmlspecialchars($child['title'], ENT_QUOTES, 'UTF-8') . '</a></li>';
}
}
$content .= '</ul>';
}
} }
} }
+10 -1
View File
@@ -18,7 +18,16 @@ Options -Indexes
RewriteEngine On RewriteEngine On
RewriteBase / RewriteBase /
# Admin routes: /admin/login → admin.php?route=login # Serve theme assets via asset.php (themes are outside webroot)
RewriteRule ^themes/([^/]+)/assets/(.+)$ asset.php [L,QSA]
# Serve admin theme assets via asset.php
RewriteRule ^admin/assets/(.+)$ asset.php [L,QSA]
# Serve plugin assets via asset.php
RewriteRule ^plugins/([^/]+)/assets/(.+)$ asset.php [L,QSA]
# Admin routes: /admin/login -> admin.php?route=login
RewriteRule ^admin$ admin.php?route=dashboard [L,QSA] RewriteRule ^admin$ admin.php?route=dashboard [L,QSA]
RewriteRule ^admin/(.+)$ admin.php?route=$1 [L,QSA] RewriteRule ^admin/(.+)$ admin.php?route=$1 [L,QSA]
+64
View File
@@ -0,0 +1,64 @@
<?php
/**
* Asset server - serves static files from themes/, admin/theme/, and plugins/
* This file is used on production servers where the PHP router is not available.
* The .htaccess rewrites /themes/, /admin/assets/, and /plugins/ URLs to this file.
*/
$basePath = dirname(__DIR__);
$requestUri = $_SERVER['REQUEST_URI'] ?? '/';
$path = parse_url($requestUri, PHP_URL_PATH) ?? '/';
$mimeTypes = [
'css' => 'text/css',
'js' => 'application/javascript',
'svg' => 'image/svg+xml',
'png' => 'image/png',
'jpg' => 'image/jpeg',
'ico' => 'image/x-icon',
'woff' => 'font/woff',
'woff2' => 'font/woff2',
'json' => 'application/json',
'map' => 'application/json',
];
// Determine which asset directory to serve from
$assetFile = null;
// /themes/<name>/assets/...
if (preg_match('#^/themes/([^/]+)/assets/(.+)$#', $path, $m)) {
$themeName = $m[1];
$assetRel = $m[2];
$assetFile = $basePath . '/themes/' . $themeName . '/assets/' . $assetRel;
$realBase = realpath($basePath . '/themes/' . $themeName . '/assets');
}
// /admin/assets/...
elseif (preg_match('#^/admin/assets/(.+)$#', $path, $m)) {
$assetRel = $m[1];
$assetFile = $basePath . '/admin/theme/default/assets/' . $assetRel;
$realBase = realpath($basePath . '/admin/theme/default/assets');
}
// /plugins/<name>/assets/...
elseif (preg_match('#^/plugins/([^/]+)/assets/(.+)$#', $path, $m)) {
$pluginName = $m[1];
$assetRel = $m[2];
$assetFile = $basePath . '/plugins/' . $pluginName . '/assets/' . $assetRel;
$realBase = realpath($basePath . '/plugins/' . $pluginName . '/assets');
}
if ($assetFile && $realBase) {
$realFile = realpath($assetFile);
if ($realFile && strpos($realFile, $realBase) === 0 && is_file($realFile)) {
$ext = strtolower(pathinfo($realFile, PATHINFO_EXTENSION));
if (isset($mimeTypes[$ext])) {
header('Content-Type: ' . $mimeTypes[$ext]);
}
header('Cache-Control: public, max-age=86400');
readfile($realFile);
exit;
}
}
http_response_code(404);
header('Content-Type: text/plain');
echo '404 Not Found';
-323
View File
@@ -1,323 +0,0 @@
#site-header .search-input {
background-color: #ffffff !important;
color: #212529 !important;
border: 2px solid #0a369d !important;
}
#site-header .search-input::placeholder {
color: #6c757d !important;
}
#site-header .search-input:focus {
background-color: #ffffff !important;
color: #212529 !important;
border-color: #2754b4 !important;
box-shadow: 0 0 0 0.2rem rgba(10, 54, 157, 0.25) !important;
}
:root {
--header-bg: #0a369d;
--header-font: #ffffff;
--header-height: 56px;
--nav-bg: #2754b4;
--nav-font: #ffffff;
--nav-height: 42px;
--sidebar-bg: #f8f9fa;
--sidebar-border: #dee2e6;
}
#site-header {
background-image: none;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
position: relative;
}
.navbar {
background-color: var(--header-bg) !important;
min-height: var(--header-height);
}
.navbar .navbar-brand,
.navbar .navbar-text,
.navbar .btn {
color: var(--header-font) !important;
}
.navbar .form-control {
color: var(--header-font) !important;
}
.navbar .form-control::placeholder {
color: rgba(255, 255, 255, 0.7) !important;
}
.navbar .search-input {
background-color: #ffffff !important;
color: #212529 !important;
border: 2px solid #0a369d !important;
}
.navbar .search-input::placeholder {
color: #6c757d !important;
opacity: 1 !important;
}
.navbar .search-input:focus {
background-color: #ffffff !important;
color: #212529 !important;
border-color: #2754b4 !important;
box-shadow: 0 0 0 0.2rem rgba(10, 54, 157, 0.25) !important;
}
.navbar .btn-outline-light {
border-color: var(--header-font) !important;
}
.dropdown-menu {
background-color: var(--header-bg) !important;
border: 1px solid var(--header-font) !important;
}
.dropdown-item {
color: var(--header-font) !important;
}
.dropdown-item:hover {
background-color: rgba(255, 255, 255, 0.1) !important;
color: var(--header-font) !important;
}
.dropdown-toggle::after {
display: none !important;
}
.btn-outline-light {
color: var(--header-font) !important;
border-color: var(--header-font) !important;
}
.btn-outline-light:hover {
background-color: rgba(255, 255, 255, 0.1) !important;
color: var(--header-font) !important;
}
.btn-outline-light:focus, .btn-outline-light:active, .show > .btn-outline-light.dropdown-toggle {
background-color: rgba(255, 255, 255, 0.1) !important;
color: var(--header-font) !important;
border-color: var(--header-font) !important;
box-shadow: none !important;
}
.bi-chevron-down {
font-size: 0.75em;
margin-left: 0.25rem;
}
.breadcrumb-item + .breadcrumb-item::before {
content: "" !important;
padding: 0 !important;
}
.breadcrumb {
--bs-breadcrumb-divider: "";
--bs-breadcrumb-margin-bottom: 0;
margin-bottom: 0;
}
.breadcrumb .breadcrumb-item {
color: var(--header-bg) !important;
}
.breadcrumb .breadcrumb-item a {
color: var(--header-bg) !important;
text-decoration: none;
}
.breadcrumb .breadcrumb-item a:hover {
text-decoration: underline;
}
.sidebar-toggle-item {
display: flex;
align-items: center;
margin-right: 0.5rem;
}
.sidebar-toggle-btn {
padding: 0;
line-height: 1;
font-size: 1.1rem;
color: var(--header-bg) !important;
border: none !important;
background: transparent !important;
box-shadow: none !important;
cursor: pointer;
}
.sidebar-toggle-btn:hover {
opacity: 0.7;
}
.sidebar-column {
transition: all 0.3s ease;
}
.sidebar-hidden {
display: none !important;
}
.navigation-section {
background-color: var(--nav-bg) !important;
color: var(--nav-font) !important;
min-height: var(--nav-height);
}
.focus-visible:focus,
.btn:focus,
.form-control:focus,
.nav-link:focus {
outline: 3px solid #0056b3 !important;
outline-offset: 2px !important;
box-shadow: 0 0 0 1px #ffffff, 0 0 0 4px #0056b3 !important;
}
@media (prefers-contrast: high) {
:root {
--text-color: #000000;
--bg-color: #ffffff;
--border-color: #000000;
--focus-color: #000000;
}
.btn-primary {
background-color: #000000 !important;
border-color: #000000 !important;
color: #ffffff !important;
}
.btn-outline-light {
color: #000000 !important;
border-color: #000000 !important;
}
.text-muted {
color: #000000 !important;
}
.navbar {
background-color: #ffffff !important;
border-bottom: 1px solid #000000 !important;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
.nav-tabs {
background-color: transparent !important;
border: none !important;
}
.nav-tabs .nav-link {
background-color: transparent !important;
border: none !important;
color: var(--nav-font) !important;
}
.nav-tabs .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1) !important;
}
.nav-tabs .nav-link.active {
background-color: rgba(255, 255, 255, 0.2) !important;
border-bottom: 2px solid var(--nav-font) !important;
}
.sidebar-column {
background-color: var(--sidebar-bg) !important;
border-right: 1px solid var(--sidebar-border) !important;
position: sticky;
top: 0;
min-height: calc(100vh - var(--header-height) - var(--nav-height) - 42px);
}
.sidebar {
padding: 1.5rem;
height: 100%;
overflow-y: auto;
}
.content-column {
background-color: #ffffff;
}
.content-wrapper {
padding: 2rem;
padding-bottom: 80px !important;
}
.main-content {
flex: 1;
min-height: auto;
}
@media (max-width: 767.98px) {
.sidebar-column {
border-right: none !important;
border-top: 1px solid var(--sidebar-border) !important;
min-height: auto;
margin-top: 1rem;
}
.content-column {
background-color: #ffffff;
}
.content-wrapper {
min-height: auto;
padding-bottom: 2rem !important;
}
}
@media (max-width: 991.98px) {
.sidebar-column {
order: 2 !important;
}
.content-column {
order: 1 !important;
}
}
pre {
background: #f8f9fa;
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
border: 1px solid #dee2e6;
margin-bottom: 1rem;
}
pre code {
background: none;
padding: 0;
color: #333;
font-size: 0.85rem;
line-height: 1.5;
}
code {
background: #e8e8e8;
padding: 0.15rem 0.4rem;
border-radius: 3px;
font-size: 0.9em;
color: #d63384;
}
.footer-icon {
color: #6c757d;
text-decoration: none;
transition: all 0.2s ease-in-out;
display: inline-block;
padding: 2px;
}
.footer-icon:hover {
color: #0d6efd;
transform: translateY(-1px);
}
.footer-icon:active {
transform: translateY(0);
}
.footer-icon.guide:hover {
color: #198754;
}
.footer-icon.cms:hover {
color: #dc3545;
}
.footer-icon.git:hover {
color: #6f42c1;
}
.footer-icon.website:hover {
color: #fd7e14;
}
@@ -1 +0,0 @@
1786279979
@@ -1,323 +0,0 @@
#site-header .search-input {
background-color: #ffffff !important;
color: #212529 !important;
border: 2px solid #0a369d !important;
}
#site-header .search-input::placeholder {
color: #6c757d !important;
}
#site-header .search-input:focus {
background-color: #ffffff !important;
color: #212529 !important;
border-color: #2754b4 !important;
box-shadow: 0 0 0 0.2rem rgba(10, 54, 157, 0.25) !important;
}
:root {
--header-bg: #0a369d;
--header-font: #ffffff;
--header-height: 56px;
--nav-bg: #2754b4;
--nav-font: #ffffff;
--nav-height: 42px;
--sidebar-bg: #f8f9fa;
--sidebar-border: #dee2e6;
}
#site-header {
background-image: none;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
position: relative;
}
.navbar {
background-color: var(--header-bg) !important;
min-height: var(--header-height);
}
.navbar .navbar-brand,
.navbar .navbar-text,
.navbar .btn {
color: var(--header-font) !important;
}
.navbar .form-control {
color: var(--header-font) !important;
}
.navbar .form-control::placeholder {
color: rgba(255, 255, 255, 0.7) !important;
}
.navbar .search-input {
background-color: #ffffff !important;
color: #212529 !important;
border: 2px solid #0a369d !important;
}
.navbar .search-input::placeholder {
color: #6c757d !important;
opacity: 1 !important;
}
.navbar .search-input:focus {
background-color: #ffffff !important;
color: #212529 !important;
border-color: #2754b4 !important;
box-shadow: 0 0 0 0.2rem rgba(10, 54, 157, 0.25) !important;
}
.navbar .btn-outline-light {
border-color: var(--header-font) !important;
}
.dropdown-menu {
background-color: var(--header-bg) !important;
border: 1px solid var(--header-font) !important;
}
.dropdown-item {
color: var(--header-font) !important;
}
.dropdown-item:hover {
background-color: rgba(255, 255, 255, 0.1) !important;
color: var(--header-font) !important;
}
.dropdown-toggle::after {
display: none !important;
}
.btn-outline-light {
color: var(--header-font) !important;
border-color: var(--header-font) !important;
}
.btn-outline-light:hover {
background-color: rgba(255, 255, 255, 0.1) !important;
color: var(--header-font) !important;
}
.btn-outline-light:focus, .btn-outline-light:active, .show > .btn-outline-light.dropdown-toggle {
background-color: rgba(255, 255, 255, 0.1) !important;
color: var(--header-font) !important;
border-color: var(--header-font) !important;
box-shadow: none !important;
}
.bi-chevron-down {
font-size: 0.75em;
margin-left: 0.25rem;
}
.breadcrumb-item + .breadcrumb-item::before {
content: "" !important;
padding: 0 !important;
}
.breadcrumb {
--bs-breadcrumb-divider: "";
--bs-breadcrumb-margin-bottom: 0;
margin-bottom: 0;
}
.breadcrumb .breadcrumb-item {
color: var(--header-bg) !important;
}
.breadcrumb .breadcrumb-item a {
color: var(--header-bg) !important;
text-decoration: none;
}
.breadcrumb .breadcrumb-item a:hover {
text-decoration: underline;
}
.sidebar-toggle-item {
display: flex;
align-items: center;
margin-right: 0.5rem;
}
.sidebar-toggle-btn {
padding: 0;
line-height: 1;
font-size: 1.1rem;
color: var(--header-bg) !important;
border: none !important;
background: transparent !important;
box-shadow: none !important;
cursor: pointer;
}
.sidebar-toggle-btn:hover {
opacity: 0.7;
}
.sidebar-column {
transition: all 0.3s ease;
}
.sidebar-hidden {
display: none !important;
}
.navigation-section {
background-color: var(--nav-bg) !important;
color: var(--nav-font) !important;
min-height: var(--nav-height);
}
.focus-visible:focus,
.btn:focus,
.form-control:focus,
.nav-link:focus {
outline: 3px solid #0056b3 !important;
outline-offset: 2px !important;
box-shadow: 0 0 0 1px #ffffff, 0 0 0 4px #0056b3 !important;
}
@media (prefers-contrast: high) {
:root {
--text-color: #000000;
--bg-color: #ffffff;
--border-color: #000000;
--focus-color: #000000;
}
.btn-primary {
background-color: #000000 !important;
border-color: #000000 !important;
color: #ffffff !important;
}
.btn-outline-light {
color: #000000 !important;
border-color: #000000 !important;
}
.text-muted {
color: #000000 !important;
}
.navbar {
background-color: #ffffff !important;
border-bottom: 1px solid #000000 !important;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
.nav-tabs {
background-color: transparent !important;
border: none !important;
}
.nav-tabs .nav-link {
background-color: transparent !important;
border: none !important;
color: var(--nav-font) !important;
}
.nav-tabs .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1) !important;
}
.nav-tabs .nav-link.active {
background-color: rgba(255, 255, 255, 0.2) !important;
border-bottom: 2px solid var(--nav-font) !important;
}
.sidebar-column {
background-color: var(--sidebar-bg) !important;
border-right: 1px solid var(--sidebar-border) !important;
position: sticky;
top: 0;
min-height: calc(100vh - var(--header-height) - var(--nav-height) - 42px);
}
.sidebar {
padding: 1.5rem;
height: 100%;
overflow-y: auto;
}
.content-column {
background-color: #ffffff;
}
.content-wrapper {
padding: 2rem;
padding-bottom: 80px !important;
}
.main-content {
flex: 1;
min-height: auto;
}
@media (max-width: 767.98px) {
.sidebar-column {
border-right: none !important;
border-top: 1px solid var(--sidebar-border) !important;
min-height: auto;
margin-top: 1rem;
}
.content-column {
background-color: #ffffff;
}
.content-wrapper {
min-height: auto;
padding-bottom: 2rem !important;
}
}
@media (max-width: 991.98px) {
.sidebar-column {
order: 2 !important;
}
.content-column {
order: 1 !important;
}
}
pre {
background: #f8f9fa;
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
border: 1px solid #dee2e6;
margin-bottom: 1rem;
}
pre code {
background: none;
padding: 0;
color: #333;
font-size: 0.85rem;
line-height: 1.5;
}
code {
background: #e8e8e8;
padding: 0.15rem 0.4rem;
border-radius: 3px;
font-size: 0.9em;
color: #d63384;
}
.footer-icon {
color: #6c757d;
text-decoration: none;
transition: all 0.2s ease-in-out;
display: inline-block;
padding: 2px;
}
.footer-icon:hover {
color: #0d6efd;
transform: translateY(-1px);
}
.footer-icon:active {
transform: translateY(0);
}
.footer-icon.guide:hover {
color: #198754;
}
.footer-icon.cms:hover {
color: #dc3545;
}
.footer-icon.git:hover {
color: #6f42c1;
}
.footer-icon.website:hover {
color: #fd7e14;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

+15 -8
View File
@@ -93,34 +93,41 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
}); });
// Handle nested dropdowns for touch devices using event delegation // Handle nested dropdowns — toggle submenu on click (for mobile/touch)
// On desktop, CSS hover handles submenu display
document.addEventListener('click', function(e) { document.addEventListener('click', function(e) {
const toggle = e.target.closest('.dropdown-submenu .dropdown-toggle'); const toggle = e.target.closest('.dropdown-submenu > .dropdown-toggle');
if (toggle) { if (toggle) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const submenu = toggle.closest('.dropdown-submenu'); const submenu = toggle.parentElement;
const dropdown = submenu.querySelector('.dropdown-menu'); const dropdown = submenu.querySelector('.dropdown-menu');
// Close other submenus at the same level // Close other open submenus at the same level
const parent = submenu.parentElement; const parent = submenu.parentElement;
parent.querySelectorAll('.dropdown-submenu').forEach(function(sibling) { parent.querySelectorAll(':scope > .dropdown-submenu').forEach(function(sibling) {
if (sibling !== submenu) { if (sibling !== submenu) {
sibling.classList.remove('show');
var siblingMenu = sibling.querySelector('.dropdown-menu'); var siblingMenu = sibling.querySelector('.dropdown-menu');
if (siblingMenu) siblingMenu.classList.remove('show'); if (siblingMenu) siblingMenu.classList.remove('show');
} }
}); });
// Toggle current submenu // Toggle current submenu
if (dropdown) dropdown.classList.toggle('show'); if (dropdown) {
dropdown.classList.toggle('show');
submenu.classList.toggle('show');
}
return; return;
} }
// Close all open submenus when clicking outside // Close all open submenus when clicking outside
document.querySelectorAll('.dropdown-submenu .dropdown-menu.show').forEach(function(menu) { document.querySelectorAll('.dropdown-submenu.show').forEach(function(sm) {
menu.classList.remove('show'); sm.classList.remove('show');
var menu = sm.querySelector('.dropdown-menu');
if (menu) menu.classList.remove('show');
}); });
}); });
}); });
+122 -7
View File
@@ -106,21 +106,43 @@ $header-bg-opacity: 1;
} }
} }
// Language dropdown styling // Dropdown menu styling — override Bootstrap variables completely
.dropdown-menu { .dropdown-menu {
--bs-dropdown-min-width: 0;
--bs-dropdown-padding-x: 0;
--bs-dropdown-padding-y: 0;
--bs-dropdown-border-width: 0;
--bs-dropdown-border-radius: 0;
--bs-dropdown-box-shadow: none;
background-color: var(--header-bg) !important; background-color: var(--header-bg) !important;
border: 1px solid var(--header-font) !important; border: none !important;
border-radius: 0 !important;
padding: 0 !important;
margin: 0 !important;
min-width: 0 !important;
box-shadow: none !important;
width: max-content;
} }
.dropdown-item { .dropdown-item {
color: var(--header-font) !important; color: var(--header-font) !important;
border-radius: 0 !important;
padding: 0.4rem 1rem;
white-space: nowrap;
display: block;
width: 100%;
&:hover { &:hover, &:focus {
background-color: rgba(255, 255, 255, 0.1) !important; background-color: rgba(255, 255, 255, 0.1) !important;
color: var(--header-font) !important; color: var(--header-font) !important;
} }
} }
// Remove Bootstrap dropdown border/spacing on nav-tabs dropdown
.nav-tabs .dropdown-menu {
margin-top: 0 !important;
}
// Hide Bootstrap dropdown arrow and use custom icon // Hide Bootstrap dropdown arrow and use custom icon
.dropdown-toggle::after { .dropdown-toggle::after {
display: none !important; display: none !important;
@@ -217,8 +239,7 @@ $header-bg-opacity: 1;
// Enhanced accessibility styles // Enhanced accessibility styles
.focus-visible:focus, .focus-visible:focus,
.btn:focus, .btn:focus,
.form-control:focus, .form-control:focus {
.nav-link:focus {
outline: 3px solid #0056b3 !important; outline: 3px solid #0056b3 !important;
outline-offset: 2px !important; outline-offset: 2px !important;
box-shadow: 0 0 0 1px #ffffff, 0 0 0 4px #0056b3 !important; box-shadow: 0 0 0 1px #ffffff, 0 0 0 4px #0056b3 !important;
@@ -266,25 +287,119 @@ $header-bg-opacity: 1;
} }
} }
// Remove nav-tabs background so it inherits from parent // Navigation tabs — completely override Bootstrap nav-tabs styling
.nav-tabs { .nav-tabs {
--bs-nav-tabs-border-width: 0 !important;
--bs-nav-tabs-border-color: transparent !important;
--bs-nav-tabs-border-radius: 0 !important;
--bs-nav-tabs-link-hover-border-color: transparent !important;
--bs-nav-tabs-link-active-color: var(--nav-font) !important;
--bs-nav-tabs-link-active-bg: transparent !important;
--bs-nav-tabs-link-active-border-color: transparent !important;
background-color: transparent !important; background-color: transparent !important;
border: none !important; border: none !important;
border-bottom: none !important;
border-radius: 0 !important;
gap: 0 !important;
.nav-link { .nav-link {
margin-bottom: 0 !important;
background-color: transparent !important; background-color: transparent !important;
border: none !important; border: none !important;
border-top-left-radius: 0 !important;
border-top-right-radius: 0 !important;
border-bottom-left-radius: 0 !important;
border-bottom-right-radius: 0 !important;
color: var(--nav-font) !important; color: var(--nav-font) !important;
outline: none !important;
box-shadow: none !important;
&:hover { &:hover, &:focus, &:focus-visible {
isolation: auto;
background-color: rgba(255, 255, 255, 0.1) !important; background-color: rgba(255, 255, 255, 0.1) !important;
border: none !important;
border-color: transparent !important;
outline: none !important;
box-shadow: none !important;
} }
&.active { &.active {
background-color: rgba(255, 255, 255, 0.2) !important; background-color: rgba(255, 255, 255, 0.2) !important;
border: none !important;
border-color: transparent !important;
border-bottom: 2px solid var(--nav-font) !important; border-bottom: 2px solid var(--nav-font) !important;
outline: none !important;
box-shadow: none !important;
} }
} }
.dropdown-menu {
margin-top: 0 !important;
border-top-left-radius: 0 !important;
border-top-right-radius: 0 !important;
}
}
// Bootstrap 5 nested dropdown support (dropdown-submenu) — unlimited depth, CSS hover
.dropdown-submenu {
position: relative;
> .dropdown-menu {
top: 0;
left: 100%;
margin-top: 0;
margin-left: 0;
display: none;
--bs-dropdown-border-width: 0;
--bs-dropdown-border-radius: 0;
--bs-dropdown-box-shadow: none;
border: none !important;
border-radius: 0 !important;
box-shadow: none !important;
width: max-content;
}
// Open submenu on hover (desktop)
&:hover > .dropdown-menu {
display: block;
}
// Keep parent highlighted when hovering submenu
&:hover > .dropdown-toggle {
background-color: rgba(255, 255, 255, 0.15) !important;
}
// Arrow pointing right — vertically centered with text
> .dropdown-toggle {
display: flex !important;
justify-content: space-between;
align-items: center;
gap: 0.75rem;
.bi-chevron-right {
flex-shrink: 0;
font-size: 0.7rem;
}
}
}
// Mobile: submenu appears below instead of beside, open on tap
@media (max-width: 767.98px) {
.dropdown-submenu > .dropdown-menu {
left: 0;
top: 100%;
margin-top: 0;
margin-left: 1rem;
border: none !important;
border-radius: 0 !important;
box-shadow: none;
}
.dropdown-submenu:hover > .dropdown-menu {
display: none;
}
.dropdown-submenu.show > .dropdown-menu {
display: block;
}
} }
// Sidebar styling // Sidebar styling
+1 -1
View File
@@ -31,7 +31,7 @@
<link rel="me" href="{{ author_git }}"> <link rel="me" href="{{ author_git }}">
<!-- Favicon --> <!-- Favicon -->
<link rel="icon" href="{{ theme_base_url }}/assets/img/favicon.ico" sizes="any"> <link rel="icon" type="image/x-icon" href="{{ theme_base_url }}/assets/img/favicon.ico">
<link rel="icon" type="image/svg+xml" href="{{ theme_base_url }}/assets/img/favicon.svg"> <link rel="icon" type="image/svg+xml" href="{{ theme_base_url }}/assets/img/favicon.svg">
<link rel="apple-touch-icon" href="{{ theme_base_url }}/assets/img/favicon.svg"> <link rel="apple-touch-icon" href="{{ theme_base_url }}/assets/img/favicon.svg">
<link rel="manifest" href="/manifest.json"> <link rel="manifest" href="/manifest.json">
-1
View File
@@ -1 +0,0 @@
1786274744
-266
View File
@@ -1,266 +0,0 @@
:root {
--header-bg: #613583;
--header-font: #ffffff;
--header-height: 120px;
--nav-bg: #813d9c;
--nav-font: #ffffff;
--nav-height: 50px;
--sidebar-bg: #f3e8f7;
--sidebar-border: #d8b4e0;
}
#site-header {
background-image: none;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
position: relative;
}
.navbar {
background-color: var(--header-bg) !important;
min-height: var(--header-height);
border-bottom: 4px solid var(--nav-bg) !important;
}
.navbar .navbar-brand,
.navbar .navbar-text,
.navbar .form-control,
.navbar .btn {
color: var(--header-font) !important;
}
.navbar .form-control::placeholder {
color: rgba(255, 255, 255, 0.7) !important;
}
.navbar .btn-outline-light {
border-color: var(--header-font) !important;
}
.dropdown-menu {
background-color: var(--header-bg) !important;
border: 1px solid var(--header-font) !important;
}
.dropdown-item {
color: var(--header-font) !important;
}
.dropdown-item:hover {
background-color: rgba(255, 255, 255, 0.1) !important;
color: var(--header-font) !important;
}
.dropdown-toggle::after {
display: none !important;
}
.btn-outline-light {
color: var(--header-font) !important;
border-color: var(--header-font) !important;
}
.btn-outline-light:hover {
background-color: rgba(255, 255, 255, 0.1) !important;
color: var(--header-font) !important;
}
.btn-outline-light:focus, .btn-outline-light:active, .show > .btn-outline-light.dropdown-toggle {
background-color: rgba(255, 255, 255, 0.1) !important;
color: var(--header-font) !important;
border-color: var(--header-font) !important;
box-shadow: none !important;
}
.bi-chevron-down {
font-size: 0.75em;
margin-left: 0.25rem;
}
.breadcrumb-item + .breadcrumb-item::before {
content: "" !important;
padding: 0 !important;
}
.breadcrumb {
--bs-breadcrumb-divider: "";
--bs-breadcrumb-margin-bottom: 0;
margin-bottom: 0;
}
.breadcrumb .breadcrumb-item {
color: var(--header-bg) !important;
}
.breadcrumb .breadcrumb-item a {
color: var(--header-bg) !important;
text-decoration: none;
}
.breadcrumb .breadcrumb-item a:hover {
text-decoration: underline;
}
.sidebar-toggle-item {
display: flex;
align-items: center;
margin-right: 0.5rem;
}
.sidebar-toggle-btn {
padding: 0;
line-height: 1;
font-size: 1.1rem;
color: var(--header-bg) !important;
border: none !important;
background: transparent !important;
box-shadow: none !important;
cursor: pointer;
}
.sidebar-toggle-btn:hover {
opacity: 0.7;
}
.sidebar-column {
transition: all 0.3s ease;
}
.sidebar-hidden {
display: none !important;
}
.navigation-section {
background-color: var(--nav-bg) !important;
color: var(--nav-font) !important;
min-height: var(--nav-height);
}
.focus-visible:focus,
.btn:focus,
.form-control:focus,
.nav-link:focus {
outline: 3px solid #0056b3 !important;
outline-offset: 2px !important;
box-shadow: 0 0 0 1px #ffffff, 0 0 0 4px #0056b3 !important;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
.nav-tabs {
background-color: transparent !important;
border: none !important;
}
.nav-tabs .nav-link {
background-color: transparent !important;
border: none !important;
color: var(--nav-font) !important;
}
.nav-tabs .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1) !important;
}
.nav-tabs .nav-link.active {
background-color: rgba(255, 255, 255, 0.2) !important;
border-bottom: 2px solid var(--nav-font) !important;
}
.sidebar-column {
background-color: var(--sidebar-bg) !important;
border-right: 1px solid var(--sidebar-border) !important;
position: sticky;
top: 0;
min-height: calc(100vh - var(--header-height) - var(--nav-height) - 42px);
}
.sidebar {
padding: 1.5rem;
height: 100%;
overflow-y: auto;
}
.sidebar .card {
border-radius: 1rem;
border: 1px solid var(--sidebar-border);
}
.content-column {
background-color: #ffffff;
}
.content-wrapper {
padding: 2rem;
padding-bottom: 80px !important;
}
.main-content {
flex: 1;
}
@media (max-width: 767.98px) {
.sidebar-column {
border-right: none !important;
border-top: 1px solid var(--sidebar-border) !important;
min-height: auto;
margin-top: 1rem;
}
.content-wrapper {
min-height: auto;
padding-bottom: 2rem !important;
}
}
@media (max-width: 991.98px) {
.sidebar-column {
order: 2 !important;
}
.content-column {
order: 1 !important;
}
}
pre {
background: #f8f9fa;
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
border: 1px solid #dee2e6;
margin-bottom: 1rem;
}
pre code {
background: none;
padding: 0;
color: #333;
font-size: 0.85rem;
line-height: 1.5;
}
code {
background: #e8e8e8;
padding: 0.15rem 0.4rem;
border-radius: 3px;
font-size: 0.9em;
color: #d63384;
}
.footer-icon {
color: #6c757d;
text-decoration: none;
transition: all 0.2s ease-in-out;
display: inline-block;
padding: 2px;
}
.footer-icon:hover {
color: #0d6efd;
transform: translateY(-1px);
}
.footer-icon:active {
transform: translateY(0);
}
.footer-icon.guide:hover {
color: #198754;
}
.footer-icon.cms:hover {
color: #dc3545;
}
.footer-icon.git:hover {
color: #6f42c1;
}
.footer-icon.website:hover {
color: #fd7e14;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB