23 Commits
Author SHA1 Message Date
root 2d9ffaa942 v2.6.1 (Lyra): Welcome page, 404 handling, installatie docs, opschoning
Nieuwe features:
- Welkomstpagina bij lege content-map (nieuwe installatie detectie)
- 404-afhandeling binnen actieve theme via admin/static/404.html
- HTTP 404 status bij onbekende pagina's en missende taalprefix

Opschoning:
- Verwijderd: package.json, src/scss/, root .htaccess, themes/demo/
- Verwijderde vendor packages: php-mqtt/client, mustache/mustache
- AGENTS.md samengevoegd naar root, development/AGENTS.md verwijderd
- .gitignore opgeschoond (NPM/node_modules/.sass-cache verwijderd)

Documentatie:
- Installatie instructies toegevoegd aan README (Apache2/Nginx/PHP/composer)
- README en guide versie referenties bijgewerkt naar 2.6.1
- Release notes: docs/release-notes/v2.6.1.md

Tests:
- Pentest: 30/30 geslaagd, 0 vulnerabilities
- WCAG 2.1 AA: 25/25 geslaagd, 100% compliance
- Test scripts gebruiken Apache-URL i.p.v. localhost:8080
2026-08-17 14:48:46 +00:00
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
92 changed files with 1614 additions and 5062 deletions
+3 -5
View File
@@ -1,7 +1,3 @@
# Dependencies
node_modules/
package-lock.json
# Build outputs
*.log
.DS_Store
@@ -15,12 +11,14 @@ Thumbs.db
# Cache & Storage
.cache/
.sass-cache/
admin/storage/cache/
admin/storage/geoip/
admin/storage/stats.json
var/
# Runtime-compiled theme assets (generated by scssphp, read-only)
themes/*/assets/css_compiled/
# Runtime-compiled theme assets
public/themes/
-53
View File
@@ -1,53 +0,0 @@
# Disable directory listing globally
Options -Indexes
# Security - Block access to entire application
<Files ~ "^\.">
Order allow,deny
Deny from all
</Files>
<FilesMatch "\.(php|ini|log|conf|config|md|map)$">
Order allow,deny
Deny from all
</FilesMatch>
# Block access to backup files
<FilesMatch "\.(backup|bak|old|orig|swp|save)$">
Order allow,deny
Deny from all
</FilesMatch>
# Block access to all application files
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
# Directory protection - Block all access
<Directory />
Order allow,deny
Deny from all
</Directory>
# Only allow access to public directory
<Directory "public">
Order allow,deny
Allow from all
Require all granted
</Directory>
# Set default directory to public
DirectoryIndex public/index.php
# Redirect root to public directory
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
# Redirect root to public
RewriteRule ^$ public/ [L]
# Redirect all other requests to public
RewriteCond %{REQUEST_URI} !^/public/
RewriteRule ^(.*)$ public/$1 [L]
</IfModule>
-115
View File
@@ -1,115 +0,0 @@
# Agent Instructions for CodePress CMS
## AI Model
- **Huidig model**: `claude-opus-4-6` (OpenCode / `opencode/claude-opus-4-6`)
- Sessie gestart: 16 feb 2026
## Build & Run
- **Run Server**: `php -S localhost:8080 cms/router.php` (router nodig voor clean URLs)
- **Lint PHP**: `find . -name "*.php" -not -path "./vendor/*" -exec php -l {} \;`
- **Dependencies**: Composer vereist voor CommonMark, Twig en scssphp. Geen NPM.
- **Admin Console**: Toegankelijk op `/admin.php` (standaard login: `admin` / `admin`)
## Project Structuur
```
codepress/
├── cms/ # Core CMS engine
│ ├── core/
│ │ ├── class/
│ │ │ ├── CodePressCMS.php # Hoofd CMS class
│ │ │ ├── ThemeManager.php # Thema-resolver + Twig render + SCSS compile
│ │ │ └── Logger.php # Logging systeem
│ │ ├── plugin/
│ │ │ ├── PluginManager.php # Plugin loader
│ │ │ └── CMSAPI.php # API voor plugins
│ │ ├── config.php # Config loader (leest config.json)
│ │ └── index.php # Bootstrap (autoloader, requires)
│ └── router.php # PHP dev server router (serveert ook /themes/)
├── language/ # Taalbestanden (nl/, en/, de/ — elk met site.php + admin.php)
├── themes/ # Dynamische thema's (volledig zelfstandig)
│ ├── default/ # Standaard thema
│ │ ├── theme.json # { title, default_layout, layout→.twig mapping, kleuren }
│ │ ├── base.twig # Hoofd layout (head, header, nav, footer)
│ │ ├── full_content.twig # Layout: volledige breedte
│ │ ├── left_sidebar.twig # Layout: sidebar links
│ │ ├── right_sidebar.twig # Layout: sidebar rechts
│ │ ├── custom1.twig # Layout: custom
│ │ ├── guide.twig # Layout: handleiding
│ │ ├── partials/ # header.twig, navigation.twig, footer.twig
│ │ ├── assets/scss/theme.scss # SCSS bron (runtime gecompileerd)
│ │ └── assets/js/ # app.js, bootstrap.bundle.min.js
│ ├── demo/ # Demo thema (zelfde structuur, andere look)
├── admin/ # Admin paneel
│ ├── config/
│ │ ├── app.php # Admin app configuratie
│ │ ├── admin.json # Gebruikers & security (file-based, gitignored)
│ │ └── admin.json.example # Voorbeeld met placeholder-wachtwoord
│ ├── src/
│ │ └── AdminAuth.php # Authenticatie (sessies, bcrypt, CSRF, lockout)
│ ├── theme/default/views/ # Twig templates
│ │ ├── login.twig # Login pagina
│ │ ├── layouts/admin.twig # Admin layout met sidebar
│ │ └── pages/ # dashboard, content, content-edit, config, plugins, theme, users, statistics, logs, security, update, guide, media, etc.
│ └── storage/logs/ # Admin logs (gedeeld met front-end)
├── cli/ # CLI scripts & tests
│ └── test/
│ ├── accessibility.sh # WCAG 2.1 AA test suite
│ ├── enhanced-suite.sh # Enhanced test suite
│ ├── functional/ # Functionele testen
│ └── pentest/ # Penetratietesten
├── plugins/ # CMS plugins
│ ├── HTMLBlock/
│ └── Navigation/
├── public/ # Web root
│ ├── assets/css/js/
│ ├── index.php # Website entry point
│ └── admin.php # Admin entry point + router
├── content/ # Content bestanden
├── guide/ # Handleidingen (nl/en)
├── docs/ # Documentatie
├── config.json # Site configuratie
└── AGENTS.md # Dit bestand
```
## Code Style & Conventions
- **PHP Standards**: Follow PSR-12. Use 4 spaces for indentation.
- **Naming**: Classes `PascalCase` (e.g., `CodePressCMS`), methods `camelCase` (e.g., `renderMenu`), variables `camelCase`, config keys `snake_case`.
- **Architecture**:
- Core CMS logic in `cms/core/class/CodePressCMS.php`
- Bootstrap/requires in `cms/core/index.php`
- Configuration loaded from `config.json` via `cms/core/config.php`
- Public website entry point: `public/index.php`
- Admin entry point + routing: `public/admin.php`
- Admin authenticatie: `admin/src/AdminAuth.php`
- **Content**: Stored in `content/`. Supports `.md` (Markdown), `.php` (Dynamic), `.html` (Static).
- **Templating**: Twig templates in `themes/<naam>/`. `ThemeManager` rendert via Twig en compileert `assets/scss/theme.scss` runtime naar `public/themes/<naam>/theme.css`. Layout gekozen via frontmatter `layout:` key; onbekende layouts vallen terug op `default_layout` in `theme.json`.
- **Navigation**: Auto-generated from directory structure. Folders require an index file to be clickable in breadcrumbs.
- **Security**:
- Always use `htmlspecialchars()` for outputting user/content data
- Use `realpath()` + prefix-check for path traversal prevention
- Admin forms require CSRF tokens via `AdminAuth::verifyCsrf()`
- Passwords stored as bcrypt hashes in `admin.json`
- **Git**: `main` is the clean CMS core. `development` is de actieve development branch. `e.noorlander` bevat persoonlijke content. Niet mixen.
## Admin Console
- **File-based**: Geen database. Gebruikers opgeslagen in `admin/config/admin.json`
- **Routing**: Via `?route=` parameter in `public/admin.php`
- **Routes**: `login`, `logout`, `dashboard`, `content`, `content-edit`, `content-new`, `content-delete`, `content-dir-create`, `content-dir-rename`, `content-dir-delete`, `content-move`, `config`, `plugins`, `plugins-new`, `plugins-edit`, `plugins-config`, `plugins-toggle`, `plugins-delete`, `theme`, `theme-new`, `users`, `security`, `statistics`, `logs`, `guide`, `media`, `update`
- **Auth**: Session-based. `AdminAuth` class handelt login, logout, CSRF, brute-force lockout af
- **Templates**: Twig templates in `admin/theme/default/views/`. Layout in `layouts/admin.twig`
## Important: Title vs File/Directory Name Logic
- **CRITICAL**: When user asks for "title" corrections, they usually mean **FILE/DIRECTORY NAME WITHOUT LANGUAGE PREFIX AND EXTENSIONS**, not the HTML title from content!
- **Examples**:
- `nl.test.md` → display as "Test" (not content title)
- `nl.test/` directory → display as "Test" (not H1 content)
- `en.php-testen` → display as "Php Testen" (not "ICT")
- **Method**: Use `formatDisplayName()` to process file/directory names correctly
- **Priority**: Directory names take precedence over file names when both exist
- **Language prefixes**: Dynamisch verwijderd op basis van beschikbare talen via `getAvailableLanguages()`
## Bekende aandachtspunten
- LSP errors over "Undefined function" in PHP files zijn vals-positief (standaard PHP functies worden niet herkend door de LSP). Negeer deze.
- Zie `TODO.md` voor alle openstaande verbeteringen en nieuwe features.
- `vendor/` map bevat Composer dependencies (CommonMark, Twig, scssphp, GeoIP2). Niet handmatig wijzigen.
- `admin/config/admin.json` bevat wachtwoord-hashes. Niet committen met echte productie-wachtwoorden.
+132 -4
View File
@@ -4,13 +4,13 @@
A lightweight, file-based content management system built with PHP (≥8.0).
**Version:** 2.5.1 | **License:** AGPL v3 / Commercial
**Version:** 2.6.1 | **License:** AGPL v3 / Commercial
## ✨ Features
- 📝 **Multi-format Content** - Markdown, PHP and HTML files
- 🧭 **Dynamic Navigation** - Automatic menu generation
- 🌍 **Multi-language** - NL/EN/DE/FR support
- 🌍 **Multi-language** - NL/EN/DE support
- 🔍 **Search** - Full-text search
- 📱 **Responsive** - Bootstrap 5 themes
- 🔒 **Security** - 100/100 pentest score
@@ -27,13 +27,141 @@ A lightweight, file-based content management system built with PHP (≥8.0).
# Install dependencies
composer install
# Start server with router for clean URLs
# Start server with router for clean URLs (local only)
php -S localhost:8080 cms/router.php
```
**Website:** `http://localhost:8080`
**Admin:** `http://localhost:8080/admin` (login: `admin` / `admin`)
## 📦 Installation
### Requirements
- **PHP** ≥ 8.0 with extensions: `json`, `mbstring`
- **Composer** (PHP dependency manager)
- Web server: **Apache 2.4+** with `mod_rewrite` or **Nginx** with PHP-FPM
- Optional: `opcache` (recommended for performance), `git` (for content versioning), `zip` extension (for ZIP backup/restore)
### Step 1 — Code and dependencies
```bash
git clone <repository-url> codepress
cd codepress
composer install
```
### Step 2 — Configuration
```bash
cp config.json.example config.json
cp admin/config/admin.json.example admin/config/admin.json
```
Edit `config.json` with your site title, language and plugins. Change the admin password in `admin/config/admin.json` (default `admin`/`admin`).
### Step 3a — Apache 2.4+
The webroot is the `public/` directory. Example vhost (`/etc/apache2/sites-available/codepress.conf`):
```apache
<VirtualHost *:80>
ServerName example.com
DocumentRoot /var/www/codepress/public
<Directory /var/www/codepress/public>
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/codepress_error.log
CustomLog ${APACHE_LOG_DIR}/codepress_access.log combined
</VirtualHost>
```
Required Apache modules:
```bash
sudo a2enmod rewrite headers
sudo systemctl restart apache2
```
- `mod_rewrite` — for clean URLs (`/nl/page`) and asset-serving
- `mod_headers` — for security headers
- `AllowOverride All` — so the `.htaccess` in `public/` is applied
### Step 3b — Nginx
Example server block (`/etc/nginx/sites-available/codepress`):
```nginx
server {
listen 80;
server_name example.com;
root /var/www/codepress/public;
index index.php;
# Clean URLs: language-prefixed pages
location ~ ^/(nl|en|de)(/(.+))?$ {
try_files $uri /index.php?lang=$1&page=$2;
}
# Admin routes
location /admin {
try_files $uri /admin.php?$args;
}
# Asset-serving via asset.php (themes/plugins/admin outside webroot)
location ~ ^/(themes|plugins)/([^/]+)/assets/(.+)$ {
try_files $uri /asset.php;
}
location ~ ^/admin/assets/(.+)$ {
try_files $uri /asset.php;
}
# PHP via FPM
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.0-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# Security: block access to sensitive directories
location ~ ^/(content|cms|admin/src|admin/config|admin/storage|var|vendor)/ {
deny all;
return 403;
}
location ~ /\.(git|htaccess) {
deny all;
}
}
```
**Note:** Nginx does not use `.htaccess`. Security headers must be set in the Nginx config:
```nginx
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options SAMEORIGIN;
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy strict-origin-when-cross-origin;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';";
```
### Step 4 — Directory permissions
Make sure the web server has write access to the runtime directories:
```bash
chown -R www-data:www-data var/ admin/storage/ content/
chmod -R 755 .
```
### Step 5 — Test
Open the website in your browser. With an empty content directory you'll see a welcome page. The admin console is available at `/admin` (login `admin`/`admin`).
## 📚 Documentation
See **[guide/](guide/)** for extensive documentation per role:
@@ -68,6 +196,7 @@ codepress/
├── admin/ # Admin console
│ ├── config/ # Admin configuration (admin.json)
│ ├── src/AdminAuth.php # Authentication, roles, permissions
│ ├── static/ # Static files (404.html)
│ ├── storage/ # Logs, cache, geoip
│ └── theme/default/ # Admin theme
│ ├── assets/ # CSS, JS, fonts, codemirror
@@ -80,7 +209,6 @@ codepress/
│ │ ├── *.twig # Layout templates
│ │ ├── partials/ # Header, navigation, footer
│ │ └── assets/ # SCSS, CSS, JS, img
│ └── demo/ # Demo theme
├── plugins/ # Plugins
│ ├── HTMLBlock/ # Example sidebar plugin
│ └── Navigation/ # Essential navigation plugin (protected)
+132 -4
View File
@@ -4,13 +4,13 @@
Een lichtgewicht, file-based content management systeem gebouwd met PHP (≥8.0).
**Versie:** 2.5.0 | **Licentie:** AGPL v3 / Commercial
**Versie:** 2.6.1 | **Licentie:** AGPL v3 / Commercial
## ✨ Features
- 📝 **Multi-format Content** - Markdown, PHP en HTML bestanden
- 🧭 **Dynamic Navigation** - Automatische menu generatie
- 🌍 **Multi-language** - NL/EN/DE/FR ondersteuning
- 🌍 **Multi-language** - NL/EN/DE ondersteuning
- 🔍 **Search** - Volledige tekst zoekfunctie
- 📱 **Responsive** - Bootstrap 5 thema's
- 🔒 **Security** - 100/100 pentest score
@@ -27,13 +27,141 @@ Een lichtgewicht, file-based content management systeem gebouwd met PHP (≥8.0)
# Installeer dependencies
composer install
# Start server met router voor schone URLs
# Start server met router voor schone URLs (alleen lokaal)
php -S localhost:8080 cms/router.php
```
**Website:** `http://localhost:8080`
**Admin:** `http://localhost:8080/admin` (login: `admin` / `admin`)
## 📦 Installatie
### Vereisten
- **PHP** ≥ 8.0 met extensies: `json`, `mbstring`
- **Composer** (PHP dependency manager)
- Webserver: **Apache 2.4+** met `mod_rewrite` of **Nginx** met PHP-FPM
- Optioneel: `opcache` (aanbevolen voor performance), `git` (voor content versioning), `zip` extensie (voor ZIP backup/restore)
### Stap 1 — Code en dependencies
```bash
git clone <repository-url> codepress
cd codepress
composer install
```
### Stap 2 — Configuratie
```bash
cp config.json.example config.json
cp admin/config/admin.json.example admin/config/admin.json
```
Pas `config.json` aan met je site titel, taal en plugins. Wijzig het admin wachtwoord in `admin/config/admin.json` (standaard `admin`/`admin`).
### Stap 3a — Apache 2.4+
De webroot is de `public/` map. Voorbeeld vhost (`/etc/apache2/sites-available/codepress.conf`):
```apache
<VirtualHost *:80>
ServerName example.com
DocumentRoot /var/www/codepress/public
<Directory /var/www/codepress/public>
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/codepress_error.log
CustomLog ${APACHE_LOG_DIR}/codepress_access.log combined
</VirtualHost>
```
Benodigde Apache modules:
```bash
sudo a2enmod rewrite headers
sudo systemctl restart apache2
```
- `mod_rewrite` — voor clean URLs (`/nl/pagina`) en asset-serving
- `mod_headers` — voor security headers
- `AllowOverride All` — zodat `.htaccess` in `public/` wordt toegepast
### Stap 3b — Nginx
Voorbeeld server block (`/etc/nginx/sites-available/codepress`):
```nginx
server {
listen 80;
server_name example.com;
root /var/www/codepress/public;
index index.php;
# Clean URLs: taal-prefixed pagina's
location ~ ^/(nl|en|de)(/(.+))?$ {
try_files $uri /index.php?lang=$1&page=$2;
}
# Admin routes
location /admin {
try_files $uri /admin.php?$args;
}
# Asset-serving via asset.php (themes/plugins/admin buiten webroot)
location ~ ^/(themes|plugins)/([^/]+)/assets/(.+)$ {
try_files $uri /asset.php;
}
location ~ ^/admin/assets/(.+)$ {
try_files $uri /asset.php;
}
# PHP via FPM
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.0-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# Beveiliging: blokkeer toegang tot gevoelige mappen
location ~ ^/(content|cms|admin/src|admin/config|admin/storage|var|vendor)/ {
deny all;
return 403;
}
location ~ /\.(git|htaccess) {
deny all;
}
}
```
**Let op:** Nginx gebruikt geen `.htaccess`. De security headers moeten in de Nginx config worden gezet:
```nginx
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options SAMEORIGIN;
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy strict-origin-when-cross-origin;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';";
```
### Stap 4 — Mappen rechten
Zorg dat de webserver schrijfrechten heeft op de runtime mappen:
```bash
chown -R www-data:www-data var/ admin/storage/ content/
chmod -R 755 .
```
### Stap 5 — Test
Open de website in je browser. Bij een lege content-map zie je een welkomstpagina. De admin console is bereikbaar via `/admin` (login `admin`/`admin`).
## 📚 Handleidingen
Zie **[guide/](guide/)** voor uitgebreide documentatie per rol:
@@ -68,6 +196,7 @@ codepress/
├── admin/ # Admin console
│ ├── config/ # Admin configuratie (admin.json)
│ ├── src/AdminAuth.php # Authenticatie, rollen, permissies
│ ├── static/ # Statische bestanden (404.html)
│ ├── storage/ # Logs, cache, geoip
│ └── theme/default/ # Admin thema
│ ├── assets/ # CSS, JS, fonts, codemirror
@@ -80,7 +209,6 @@ codepress/
│ │ ├── *.twig # Layout templates
│ │ ├── partials/ # Header, navigation, footer
│ │ └── assets/ # SCSS, CSS, JS, img
│ └── demo/ # Demo thema
├── plugins/ # Plugins
│ ├── HTMLBlock/ # Voorbeeld sidebar plugin
│ └── Navigation/ # Essentiële navigatie plugin (beschermd)
+9
View File
@@ -49,6 +49,15 @@
- [x] Pentest controles uitgevoerd (30/30 tests geslaagd — 0 vulnerabilities)
- [x] WCAG 2.1 AA accessibility tests (25/25 tests geslaagd — 100% compliance, test-script verbeterd met min/max checks en grep -E)
## v2.6.1 (2026-08-17) ✅
- [x] Pentest controles uitgevoerd (30/30)
- [x] WCAG 2.1 AA accessibility tests (25/25)
- [x] README.md en handleidingen in guide/ doorgeloken en bijgewerkt
- [x] Installatie instructies toegevoegd aan README (Apache2/Nginx/PHP/composer)
- [x] Verwijderde vendor packages: php-mqtt/client, mustache/mustache (uit composer + autoloader)
- [x] Verslag gemaakt (docs/release-notes/v2.6.1.md)
- [x] Versie verhoogd naar 2.6.1
## v2.6.0 (2026-08-15) ✅
- [x] Pentest controles uitgevoerd (30/30)
- [x] WCAG 2.1 AA accessibility tests (25/25)
+9
View File
@@ -0,0 +1,9 @@
<div class="error-page text-center py-5">
<div class="error-code display-1 fw-bold text-primary mb-3">404</div>
<h1 class="h2 mb-3">Pagina niet gevonden</h1>
<p class="text-muted mb-4">De pagina die u zoekt bestaat niet of is verplaatst.</p>
<a href="/" class="btn btn-primary">
<i class="bi bi-house me-1" aria-hidden="true"></i>
Terug naar de hoofdpagina
</a>
</div>
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') }}
</div>
<ul class="nav flex-column mt-2">
{# Algemene sectie: Dashboard (plugin) + plugin items met section=general #}
{% set general_plugins = plugin_admin_menu|filter(item => (item.section|default('general')) == 'general') %}
{% if general_plugins is not empty or has_permission('dashboard') or true %}
{# Algemene sectie: Dashboard (altijd zichtbaar) + plugin items met section=general #}
{% set general_plugins = plugin_admin_menu|filter(item => (item.section|default('general')) == 'general' and item.route != 'dashboard') %}
<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 %}
<li class="nav-item">
<a class="nav-link {{ route == item.route or route starts with item.route ~ '/' ? 'active' : '' }}" href="/admin/{{ item.route }}">
@@ -51,7 +55,6 @@
</a>
</li>
{% endfor %}
{% endif %}
{% if has_permission('content') %}
<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>
<small class="form-text text-muted">{{ ta.plugin_name_help|default('Alleen letters, cijfers, underscores en streepjes.') }}</small>
</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 class="card-footer bg-white">
<button type="submit" class="btn btn-primary">
+1 -1
View File
@@ -3,7 +3,7 @@
# WCAG 2.1 AA Accessibility Test Suite for CodePress CMS
# Tests for web accessibility compliance
BASE_URL="http://localhost:8080"
BASE_URL="http://development.codepress.noorlander.info"
TOTAL_TESTS=0
PASSED_TESTS=0
FAILED_TESTS=0
+6 -10
View File
@@ -3,7 +3,7 @@
# CodePress CMS Functional Test Suite v1.5.0
# Tests core functionality, new features, and regressions
BASE_URL="http://localhost:8080"
BASE_URL="http://development.codepress.noorlander.info"
TEST_DATE=$(date '+%Y-%m-%d %H:%M:%S')
TOTAL_TESTS=0
PASSED_TESTS=0
@@ -80,14 +80,11 @@ echo ""
echo "2. CONTENT RENDERING TESTS"
echo "--------------------------"
# Test Markdown content
run_test "Markdown rendering" "curl -s '$BASE_URL/?page=demo/content-only' | grep -c '<h1>'" "1"
# Test Markdown content (guide page uses Markdown)
run_test "Markdown rendering" "curl -s '$BASE_URL/?guide' | grep -c '<h1>'" "1"
# Test HTML content
run_test "HTML content" "curl -s '$BASE_URL/?page=demo/html-demo' | grep -c '<h1>'" "1"
# Test PHP content
run_test "PHP content" "curl -s '$BASE_URL/?page=demo/php-demo' | grep -c 'PHP Version'" "1"
# Test 404 handling for non-existent pages
run_test "404 content handling" "curl -s '$BASE_URL/?page=nonexistent' | grep -c '404'" "1"
echo ""
echo "3. NAVIGATION TESTS"
@@ -97,7 +94,7 @@ echo "-------------------"
run_test "Menu generation" "curl -s '$BASE_URL/' | grep -c 'nav-item'" "2"
# Test breadcrumb navigation
run_test "Breadcrumb navigation" "curl -s '$BASE_URL/?page=demo/content-only' | grep -c 'breadcrumb'" "1"
run_test "Breadcrumb navigation" "curl -s '$BASE_URL/?guide' | grep -c 'breadcrumb'" "1"
echo ""
echo "4. TEMPLATE SYSTEM TESTS"
@@ -243,7 +240,6 @@ Functional testing performed on CodePress CMS v1.5.0 covering core functionality
### Plugin System
- **HTMLBlock Plugin**: Custom HTML blocks in sidebar
- **MQTTTracker Plugin**: Real-time analytics and tracking
- **Plugin Manager**: Centralized plugin loading system
### Enhanced Documentation
+1 -1
View File
@@ -2,7 +2,7 @@
# CodePress CMS Penetration Test Script
# WARNING: Only run this on systems you have permission to test!
TARGET="http://localhost:8080"
TARGET="http://development.codepress.noorlander.info"
RESULTS_FILE="pentest_results.txt"
echo "🔒 CodePress CMS Penetration Test" > $RESULTS_FILE
+106 -10
View File
@@ -449,10 +449,29 @@ class CodePressCMS {
if (isset($_GET['guide']) || $pageCheck === 'guide') {
return $this->getGuidePage();
}
// Determine if the request has a valid language prefix
$availableLangs = array_keys($this->getAvailableLanguages());
$requestPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?? '/';
$requestPath = ltrim($requestPath, '/');
$langFromUrl = '';
if ($requestPath !== '' && in_array(explode('/', $requestPath)[0], $availableLangs, true)) {
$langFromUrl = explode('/', $requestPath)[0];
}
// No language prefix and not the root → 404
if ($langFromUrl === '' && $requestPath !== '' && $requestPath !== 'favicon.ico') {
return $this->getError404();
}
// Check if content directory is empty
// Check if content directory is empty — show welcome page for new installations
if ($this->isContentDirEmpty()) {
return $this->getGuidePage();
$requestedPage = $_GET['page'] ?? '';
// Only show welcome page on the home URL; everything else is a 404
if (empty($requestedPage) || $requestedPage === $this->getEffectiveDefaultPage()) {
return $this->getWelcomePage();
}
return $this->getError404();
}
$page = $_GET['page'] ?? $this->getEffectiveDefaultPage();
@@ -1048,9 +1067,60 @@ class CodePressCMS {
$files = scandir($contentDir);
$files = array_diff($files, ['.', '..']);
// Filter out hidden files (e.g. .gitkeep) — only count visible content
$files = array_filter($files, function($f) {
return $f[0] !== '.';
});
return empty($files);
}
/**
* Get welcome page content for new installations (empty content directory)
*
* Shows a clear "new installation" message with next steps when the
* content directory has no content files yet.
*
* @return array Welcome page data
*/
private function getWelcomePage() {
$adminUrl = $this->buildAdminUrl();
$guideUrl = '/' . $this->currentLanguage . '/guide';
$content = '<div class="welcome-page">';
$content .= '<h1>' . htmlspecialchars($this->t('welcome_title')) . '</h1>';
$content .= '<p class="alert alert-info" role="alert">';
$content .= '<i class="bi bi-info-circle me-2" aria-hidden="true"></i>';
$content .= htmlspecialchars($this->t('welcome_intro'));
$content .= '</p>';
$content .= '<h2>' . htmlspecialchars($this->t('welcome_next_steps')) . '</h2>';
$content .= '<ol class="list-group list-group-numbered mb-4">';
$content .= '<li class="list-group-item">' . htmlspecialchars($this->t('welcome_step_1')) . '</li>';
$content .= '<li class="list-group-item">' . htmlspecialchars($this->t('welcome_step_2')) . '</li>';
$content .= '<li class="list-group-item">' . htmlspecialchars($this->t('welcome_step_3')) . '</li>';
$content .= '</ol>';
$content .= '<div class="d-flex flex-column flex-md-row gap-2 mb-4">';
$content .= '<a href="' . htmlspecialchars($adminUrl) . '" class="btn btn-primary">';
$content .= '<i class="bi bi-gear me-1" aria-hidden="true"></i> ';
$content .= htmlspecialchars($this->t('welcome_admin_link'));
$content .= '</a>';
$content .= '<a href="' . htmlspecialchars($guideUrl) . '" class="btn btn-outline-secondary">';
$content .= '<i class="bi bi-book me-1" aria-hidden="true"></i> ';
$content .= htmlspecialchars($this->t('welcome_guide_link'));
$content .= '</a>';
$content .= '</div>';
$content .= '</div>';
return [
'title' => $this->t('welcome_title'),
'content' => $content,
'layout' => 'full_content',
'metadata' => [],
];
}
/**
* Get guide page content based on user language
*
@@ -1203,9 +1273,22 @@ class CodePressCMS {
$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 [
'title' => $title,
'content' => $content
'content' => $content,
'layout' => $layout,
'metadata' => $metadata,
];
}
@@ -1215,9 +1298,17 @@ class CodePressCMS {
* @return array 404 page data
*/
private function getError404() {
$staticFile = __DIR__ . '/../../../admin/static/404.html';
$body = file_exists($staticFile)
? file_get_contents($staticFile)
: '<h1>404 - ' . $this->t('page_not_found') . '</h1><p>' . $this->t('page_not_found_text') . '</p>';
return [
'title' => $this->t('page_not_found'),
'content' => '<h1>404 - ' . $this->t('page_not_found') . '</h1><p>' . $this->t('page_not_found_text') . '</p>'
'content' => $body,
'layout' => 'full_content',
'metadata' => [],
'is_404' => true,
];
}
@@ -1239,6 +1330,11 @@ class CodePressCMS {
$page = $this->getPage();
$this->pluginManager->doAction('onPageLoad', $page);
$this->pluginManager->doAction('onBeforeRender');
// Set 404 status for not-found pages (rendered through the theme)
if (!empty($page['is_404'])) {
http_response_code(404);
}
$menu = $this->getMenu();
@@ -1489,7 +1585,7 @@ class CodePressCMS {
$isExpanded = $this->folderContainsActivePage($item['children']);
if ($level === 0) {
// Root level folders
// Root level folders - Bootstrap 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 .= htmlspecialchars($item['title']) . ' <i class="bi bi-chevron-down"></i>';
@@ -1499,12 +1595,12 @@ class CodePressCMS {
$html .= '</ul>';
$html .= '</li>';
} else {
// Nested folders in dropdown
$html .= '<li class="dropdown-submenu">';
$html .= '<a class="dropdown-item dropdown-toggle" href="#" id="' . $folderId . '">';
$html .= htmlspecialchars($item['title']) . ' <i class="bi bi-chevron-down"></i>';
// Nested folders - CSS hover submenu (unlimited depth)
$html .= '<li class="dropdown-submenu' . ($isExpanded ? ' show' : '') . '">';
$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-right"></i>';
$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 .= '</ul>';
$html .= '</li>';
+17 -8
View File
@@ -169,8 +169,22 @@ class ThemeManager {
$compiler = new Compiler();
$compiler->setImportPaths($this->themeDir . '/assets/scss');
$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($cacheFile, (string)$mtime);
// Set read-only to prevent manual edits
@chmod($outFile, 0444);
@chmod($cacheFile, 0444);
return $outFile;
} catch (\Throwable $e) {
error_log('ThemeManager SCSS compile error: ' . $e->getMessage());
@@ -180,20 +194,15 @@ class ThemeManager {
/**
* 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 {
$manualCss = $this->themeDir . '/assets/css/theme.css';
$compiledCss = $this->themeDir . '/assets/css_compiled/theme.css';
if (is_file($manualCss)) {
return $this->getThemeAssetUrl('css/theme.css');
}
if (is_file($compiledCss)) {
return $this->getThemeAssetUrl('css_compiled/theme.css');
}
$compiled = $this->compileCss();
if ($compiled !== null) {
return $this->getThemeAssetUrl('css_compiled/theme.css');
Generated
+3 -113
View File
@@ -697,59 +697,6 @@
},
"time": "2026-01-13T17:56:03+00:00"
},
{
"name": "mustache/mustache",
"version": "v3.0.0",
"source": {
"type": "git",
"url": "https://github.com/bobthecow/mustache.php.git",
"reference": "176b6b21d68516dd5107a63ab71b0050e518b7a4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bobthecow/mustache.php/zipball/176b6b21d68516dd5107a63ab71b0050e518b7a4",
"reference": "176b6b21d68516dd5107a63ab71b0050e518b7a4",
"shasum": ""
},
"require": {
"php": ">=5.6"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.19.3",
"yoast/phpunit-polyfills": "^2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Mustache\\": "src/"
},
"classmap": [
"src/compat.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Justin Hileman",
"email": "justin@justinhileman.info",
"homepage": "http://justinhileman.com"
}
],
"description": "A Mustache implementation in PHP.",
"homepage": "https://github.com/bobthecow/mustache.php",
"keywords": [
"mustache",
"templating"
],
"support": {
"issues": "https://github.com/bobthecow/mustache.php/issues",
"source": "https://github.com/bobthecow/mustache.php/tree/v3.0.0"
},
"time": "2025-06-28T18:28:20+00:00"
},
{
"name": "myclabs/php-enum",
"version": "1.8.5",
@@ -967,63 +914,6 @@
},
"time": "2025-10-31T00:45:47+00:00"
},
{
"name": "php-mqtt/client",
"version": "v2.3.0",
"source": {
"type": "git",
"url": "https://github.com/php-mqtt/client.git",
"reference": "3d141846753a0adee265680ae073cfb9030f2390"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-mqtt/client/zipball/3d141846753a0adee265680ae073cfb9030f2390",
"reference": "3d141846753a0adee265680ae073cfb9030f2390",
"shasum": ""
},
"require": {
"myclabs/php-enum": "^1.7",
"php": "^8.0",
"psr/log": "^1.1|^2.0|^3.0"
},
"require-dev": {
"phpunit/php-invoker": "^3.0",
"phpunit/phpunit": "^9.0",
"squizlabs/php_codesniffer": "^3.5"
},
"suggest": {
"ext-redis": "Required for the RedisRepository"
},
"type": "library",
"autoload": {
"psr-4": {
"PhpMqtt\\Client\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Marvin Mall",
"email": "marvin-mall@msn.com",
"role": "developer"
}
],
"description": "An MQTT client written in and for PHP.",
"keywords": [
"client",
"mqtt",
"publish",
"subscribe"
],
"support": {
"issues": "https://github.com/php-mqtt/client/issues",
"source": "https://github.com/php-mqtt/client/tree/v2.3.0"
},
"time": "2025-09-30T17:53:34+00:00"
},
{
"name": "psr/event-dispatcher",
"version": "1.0.0",
@@ -1837,10 +1727,10 @@
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {},
"platform-dev": {},
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.9.0"
}
+114
View File
@@ -0,0 +1,114 @@
# CodePress CMS v2.6.1 — Release Notes
**Release datum:** 2026-08-17
**Codename:** Lyra
**Status:** stable
---
## Nieuwe features
### Welkomstpagina bij lege content-map (nieuwe installatie)
- Wanneer de `content/` map leeg is (nieuwe installatie), toont de CMS nu een duidelijke welkomstpagina
- De pagina bevat een introductietekst, een lijst met volgende stappen en knoppen naar de admin console en handleiding
- Vertaald in NL, EN en DE (`welcome_*` keys in `language/*/site.php`)
- `isContentDirEmpty()` filtert nu verborgen bestanden (zoals `.gitkeep`) zodat een map met alleen een `.gitkeep` als leeg wordt herkend
### 404-afhandeling binnen het actieve theme
- Nieuwe statische 404 pagina in `admin/static/404.html` met een link naar de home pagina
- De 404 inhoud wordt binnen het actieve theme gerenderd (met header, navigatie, footer en Bootstrap styling)
- `getError404()` laadt de inhoud uit `admin/static/404.html` en geeft deze als page content terug (layout `full_content`)
- `render()` zet de juiste `http_response_code(404)` status bij niet-gevonden pagina's
### HTTP 404 status bij onbekende pagina's en missende taalprefix
- `getPage()` geeft een 404 als de URL geen geldige taalprefix bevat (bijv. `/random-page` in plaats van `/nl/random-page`)
- Niet-bestaande pagina's binnen een taal (`/nl/nonexistent`) geven nu ook een 404 in plaats van HTTP 200
- Eerder werd bij een lege content-map altijd de welkomstpagina getoond, ook voor niet-bestaande pagina's — dit is nu beperkt tot de home URL
---
## Verbeteringen
### Test scripts
- Test scripts (`pentest.sh`, `accessibility.sh`, `run-tests.sh`) gebruiken nu de Apache-URL (`http://development.codepress.noorlander.info`) in plaats van `localhost:8080`
- Functionele tests opgeschoond: tests die naar niet-bestaande `demo/` content pagina's verwezen vervangen door werkende tests
### Documentatie
- `README.md` en `README.en.md` bijgewerkt: versie naar 2.6.1, multi-language talen gecorrigeerd (NL/EN/DE), `admin/static/` map toegevoegd aan project structuur
- Guide index bestanden (`guide/nl/index.md`, `guide/en/index.md`) bijgewerkt naar versie 2.6.1
- `guide/*/codepress-developer.md` versie referenties bijgewerkt naar 2.6.1
- `AGENTS.md` samengevoegd naar de root map (`./AGENTS.md`); `./development/AGENTS.md` is verwijderd
- AGENTS.md verduidelijkt welke URL bij welke map hoort en dat tests via Apache draaien (niet via PHP dev server)
---
## Opschoning
### Verwijderde ongebruikte bestanden
- `package.json` — was voor NPM build (`npm run build:css`), runtime gebruikt scssphp (PHP) voor SCSS compilatie
- `src/scss/` map — was voor NPM sass build, overbodig na verwijderen van `package.json`
- `.htaccess` (root) — Apache docroot is `public/`, deze root `.htaccess` werd niet geserveerd
- `themes/demo/` — niet actief in `config.json` (gebruikt `default`), alleen genoemd als voorbeeld
### Opschoning referenties
- `README.md` en `README.en.md` — demo theme referenties verwijderd uit project structuur
- `guide/*/codepress-developer/architectuur.md` — demo verwijdering uit mappenstructuur boom
- `themes/gen-preview.sh` — hardcoded `default demo test` loop vervangen door dynamische `*/` loop
- `.gitignore``node_modules/`, `package-lock.json` en `.sass-cache/` regels verwijderd (NPM niet meer gebruikt)
### Verwijderde vendor packages
- `mustache/mustache` — niet meer gebruikt (Twig is de template engine)
- `php-mqtt/client` — niet meer gebruikt
- Beide packages verwijderd uit `composer.lock`, `vendor/composer/installed.json`, `installed.php`, en de autoloader bestanden
- `MQTTTracker` referentie verwijderd uit functionele test script
### Installatie documentatie
- `README.md` en `README.en.md` bevatten nu een volledige installatie sectie met:
- Vereisten (PHP ≥8.0, composer, extensies)
- Apache 2.4+ vhost voorbeeld met `mod_rewrite`, `mod_headers`, `AllowOverride All`
- Nginx server block voorbeeld met PHP-FPM, clean URLs, asset-serving en security headers
- Mappen rechten en test instructies
---
## Beveiliging
- Pentest suite: 30/30 tests geslaagd (XSS, path traversal, PHP injection, null byte, command injection, template injection, HTTP header injection, information disclosure, security headers, DoS)
- Security headers aanwezig: X-Frame-Options, Content-Security-Policy, X-Content-Type-Options
- Geen vulnerabilities gedetecteerd
---
## Accessibility
- WCAG 2.1 AA: 25/25 tests geslaagd (100%)
- ARIA landmarks aanwezig (24 per pagina)
- Semantic HTML structuur (header, nav, main, footer)
- Skip-to-content links, focus indicators, screen reader support
- Mobile viewport en touch targets aanwezig
---
## Systeem vereisten
- PHP >= 8.0
- Extensies: json, mbstring
- Optioneel: opcache (aanbevolen voor performance)
- Git (optioneel, voor content versioning feature)
- ZipArchive (optioneel, voor ZIP backup/restore feature)
- Apache met `mod_rewrite` en `AllowOverride All` (voor clean URLs en asset-serving)
---
## Upgraden
1. Maak een backup van de huidige content map
2. Pull de nieuwe code
3. Run `composer install` om dependencies bij te werken
4. Test de website
5. Optioneel: initialiseer git in content/ via Admin → Backup & Restore → Git init
---
## Test resultaten samenvatting
| Test | Resultaat |
|------|----------|
| Pentest | 30/30 geslaagd, 0 vulnerabilities |
| WCAG 2.1 AA Accessibility | 25/25 geslaagd, 100% compliance |
+8 -8
View File
@@ -2,14 +2,14 @@
The admin manager guide contains the following topics:
- **Dashboard** - Website overview
- **Content management** - Managing files and pages
- **Configuration** - Site settings
- **Theme management** - Managing and creating themes
- **Security** - Bot protection and sessions
- **Plugins** - Managing and creating plugins
- **Users** - Adding and removing users
- **Statistics** - Viewing visitor statistics
- **Dashboard** - Role-based overview of the website (Admin, Content Manager, BI Manager, Site Admin)
- **Content management** - Managing files and pages with layout selection and plugins
- **Configuration** - Site settings (title, language, author, analytics, logging)
- **Theme management** - Managing, activating and creating themes (SCSS compilation)
- **Security** - Bot protection, rate limiting and session settings
- **Plugins** - Managing plugins: content (sidebar) and system (admin menu/API)
- **Users** - User management with roles (Admin, Content Manager, BI Manager, Site Admin)
- **Statistics** - Viewing and exporting visitor statistics
- **Logs** - Viewing and filtering log files
- **Media** - Managing media files
- **Update** - Check for updates
+40 -11
View File
@@ -2,16 +2,45 @@
## Managing files
- **Upload** - Upload media files
- **New folder** - Create folder structure
- **New file** - Create a page
- **Edit** - Modify existing content
- **Rename** - Change file names
- **Move** - Move content
- **Delete** - Remove content
- **New folder** — Create folder structure
- **New file** Create a page (`.md`, `.php`, `.html`)
- **Edit** — Modify existing content in the CodeMirror editor
- **Rename** — Change file or folder names
- **Move** — Move content to another folder
- **Delete** — Remove content
- **Rename folder** — Change a folder name
## Editor
## Editor (content-edit)
- CodeMirror with syntax highlighting
- Toolbar for Markdown formatting
- Shortcuts: Ctrl+S (save), Ctrl+N (new)
- **CodeMirror** with syntax highlighting (Markdown, PHP, HTML)
- **Toolbar** for quickly inserting Markdown
- **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
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)
- Unique visitors
## Roles and widgets
### Admin
- Views (30 days) and unique visitors
- Number of pages and folders
- Active plugins
- 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.
+33 -6
View File
@@ -1,13 +1,40 @@
# 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**
2. Enter username
3. Choose password
4. Click **Add**
3. Choose password (stored as bcrypt hash)
4. Select a role
5. Click **Add**
## Delete user
## Editing a user
- Cannot delete own account
- Confirm with password
- 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
## 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`
+57 -12
View File
@@ -2,20 +2,65 @@
## Managing themes
1. Go to **Theme** in admin menu
2. **Activate** - Choose active theme
3. **Compile SCSS** - Process SCSS to CSS
4. **New theme** - Create custom theme
1. Go to **Theme** in the admin menu
2. **Activate** Choose the active theme (stored in `config.json`)
3. **Compile SCSS** Process SCSS to CSS (forced)
4. **New theme** Create a custom theme via admin or manually
## Theme structure
```
themes/default/
├── theme.json # Theme configuration
├── base.twig # Main layout
├── full_content.twig # Layouts
├── left_sidebar.twig
├── right_sidebar.twig
├── partials/ # Header, nav, footer
── assets/ # CSS, JS, images
```
├── theme.json # { title, config.default_template, template: layout→.twig }
├── base.twig # Main layout (head, header, nav, breadcrumb, footer)
├── full_content.twig # Layout: full width
├── left_sidebar.twig # Layout: sidebar on the left
├── right_sidebar.twig # Layout: sidebar on the right
├── custom1.twig # Layout: custom
── 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:
- **Architecture** - CMS architecture and folder structure
- **Core classes** - CodePressCMS, ThemeManager, PluginManager
- **Plugin development** - Developing plugins
- **Routing** - Frontend and admin routing
- **Security** - XSS, CSRF, path traversal
- **Testing** - Penetration, accessibility and functional tests
- **Debugging** - Logging and cache
- **Performance** - OPcache and SCSS caching
- **Architecture** CMS architecture and folder structure
- **Core classes** CodePressCMS, ThemeManager, PluginManager, AdminAuth
- **Plugin development** — Plugin types (content/system), structure, API
- **Routing** Frontend, admin and plugin admin routing
- **Security** XSS, CSRF, path traversal, RBAC
- **Testing** Penetration, accessibility and functional tests
- **Debugging** Logging and cache
- **Performance** OPcache and SCSS caching
Important concepts in CodePress 2.6.1:
- **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.
+1 -1
View File
@@ -11,7 +11,7 @@ codepress/
│ ├── config/ # app.php, admin.json
│ ├── src/ # AdminAuth
│ └── theme/default/views/ # Twig templates
├── themes/ # Themes (default, demo, ...)
├── themes/ # Themes (default, ...)
├── plugins/ # Plugins (Dashboard, HTMLBlock, Navigation, ...)
├── content/ # Content files (.md, .php, .html)
├── language/ # Language files (nl/, en/, de/)
+49 -7
View File
@@ -2,18 +2,60 @@
## 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
// Clean URLs: /nl/page
// Query: ?page=page&lang=nl
### PHP dev server
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
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
// Routes: /admin/dashboard, /admin/content, etc.
// Query parameter: ?route=dashboard
// PluginManager calls this for /admin/my-system
$plugin->handleAdminRoute('my-system');
```
@@ -4,28 +4,48 @@ Content is stored in the `content/` folder without a database.
## Supported file formats
- `.md` - Markdown (recommended)
- `.php` - Dynamic PHP pages
- `.html` - Static HTML pages
- `.md` Markdown (recommended)
- `.php` Dynamic PHP pages
- `.html` Static HTML pages
## 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
Markdown files can contain frontmatter metadata:
```markdown
---
layout: full_content
plugins: HTMLBlock
layout: left_sidebar
plugins:
- HTMLBlock
- Navigation
---
# Page title
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`).
+36 -4
View File
@@ -5,12 +5,44 @@
1. Login at `/admin`
2. Go to **Content**
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
## Editor features
- **CodeMirror** editor with syntax highlighting
- **CodeMirror** editor with syntax highlighting (Markdown, PHP, HTML)
- **Toolbar** for quick Markdown insertion
- **Live preview** via button
- **Auto-save** backups in `.bak/` folder
- **Shortcuts**: Ctrl+S (save), Ctrl+N (new)
- **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
Welcome to the CodePress CMS manual (version 2.6.1). 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.
+1 -1
View File
@@ -3,7 +3,7 @@
The theme developer guide contains the following topics:
- **Theme structure** - Folder structure of a theme
- **theme.json** - Theme configuration
- **theme.json** - Configuration of a theme
- **Twig templates** - Twig syntax, variables and blocks
- **SCSS styling** - Compiling and writing SCSS
- **Layouts** - Defining and using layouts
+40 -11
View File
@@ -2,20 +2,49 @@
```
themes/my-theme/
├── theme.json # Theme configuration
├── base.twig # Main layout
├── full_content.twig # Layout: full-width
├── left_sidebar.twig # Layout: left sidebar
├── right_sidebar.twig # Layout: right sidebar
├── custom.twig # Layout: custom
├── theme.json # { title, config.default_template, template: layout→.twig }
├── base.twig # Main layout (head, header, nav, breadcrumb, footer)
├── full_content.twig # Layout: full width
├── left_sidebar.twig # Layout: sidebar on the left
├── right_sidebar.twig # Layout: sidebar on the right
├── custom1.twig # Layout: custom
├── guide.twig # Layout: guide with sidebar (Navigation plugin)
├── partials/
│ ├── header.twig
│ ├── navigation.twig
│ └── footer.twig
└── assets/
├── scss/theme.scss # SCSS source
├── css/ # CSS files
├── js/theme.js # JavaScript
├── fonts/ # Fonts
── img/ # Images
├── scss/theme.scss # SCSS source (the only CSS source)
├── 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
```
## 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:
- **Dashboard** - Overzicht van de website
- **Content beheer** - Bestanden en pagina's beheren
- **Configuratie** - Site instellingen
- **Thema beheer** - Thema's beheren en aanmaken
- **Beveiliging** - Bot bescherming en sessies
- **Plugins** - Plugins beheren en aanmaken
- **Gebruikers** - Gebruikers toevoegen en verwijderen
- **Statistieken** - Bezoekersstatistieken bekijken
- **Dashboard** - Role-based overzicht van de website (Admin, Content Beheerder, BI Beheerder, Site Admin)
- **Content beheer** - Bestanden en pagina's beheren met layout selectie en plugins
- **Configuratie** - Site instellingen (titel, taal, auteur, analytics, logging)
- **Thema beheer** - Thema's beheren, activeren en aanmaken (SCSS compilatie)
- **Beveiliging** - Bot bescherming, rate limiting en sessie instellingen
- **Plugins** - Plugins beheren: content (sidebar) en systeem (admin menu/API)
- **Gebruikers** - Gebruikersbeheer met rollen (Admin, Content Beheerder, BI Beheerder, Site Admin)
- **Statistieken** - Bezoekersstatistieken bekijken en exporteren
- **Logs** - Logbestanden bekijken en filteren
- **Media** - Media bestanden beheren
- **Update** - Controleren op updates
+40 -11
View File
@@ -2,16 +2,45 @@
## Bestanden beheren
- **Uploaden** - Media bestanden uploaden
- **Nieuwe map** - Mappen structuur aanmaken
- **Nieuw bestand** - Pagina aanmaken
- **Bewerken** - Bestaande content wijzigen
- **Hernoemen** - Bestandsnamen aanpassen
- **Verplaatsen** - Content verplaatsen
- **Verwijderen** - Content verwijderen
- **Nieuwe map** Mappen structuur aanmaken
- **Nieuw bestand** — Pagina aanmaken (`.md`, `.php`, `.html`)
- **Bewerken** — Bestaande content wijzigen in CodeMirror editor
- **Hernoemen** Bestands- of mapnamen aanpassen
- **Verplaatsen** — Content verplaatsen naar andere map
- **Verwijderen** Content verwijderen
- **Map hernoemen** — Map naam wijzigen
## Editor
## Editor (content-edit)
- CodeMirror met syntax highlighting
- Toolbar voor Markdown formatting
- Sneltoetsen: Ctrl+S (opslaan), Ctrl+N (nieuw)
- **CodeMirror** met syntax highlighting (Markdown, PHP, HTML)
- **Toolbar** voor snel Markdown invoeren
- **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
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)
- Unieke bezoekers
## Rollen en widgets
### Admin
- Weergaven (30 dagen) en unieke bezoekers
- Aantal pagina's en mappen
- Actieve plugins
- 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.
+30 -3
View File
@@ -1,13 +1,40 @@
# 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
1. Ga naar **Gebruikers**
2. Vul gebruikersnaam in
3. Kies wachtwoord
4. Klik **Toevoegen**
3. Kies wachtwoord (opgeslagen als bcrypt hash)
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
- 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`
+56 -9
View File
@@ -3,17 +3,64 @@
## Thema's beheren
1. Ga naar **Thema** in admin menu
2. **Activeren** - Kies actief thema
3. **SCSS compileren** - Verwerk SCSS naar CSS
4. **Nieuw thema** - Eigen thema aanmaken
2. **Activeren** Kies actief thema (wordt opgeslagen in `config.json`)
3. **SCSS compileren** Verwerk SCSS naar CSS (geforceerd)
4. **Nieuw thema** Eigen thema aanmaken via admin of handmatig
## Thema structuur
```
themes/default/
├── theme.json # Thema configuratie
├── base.twig # Hoofd layout
├── full_content.twig # Layouts
├── partials/ # Header, nav, footer
── assets/ # CSS, JS, afbeeldingen
```
├── theme.json # { title, config.default_template, template: layout→.twig }
├── base.twig # Hoofd layout (head, header, nav, breadcrumb, footer)
├── full_content.twig # Layout: volledige breedte
├── left_sidebar.twig # Layout: sidebar links
── 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:
- **Architectuur** - CMS architectuur en mappenstructuur
- **Core classes** - CodePressCMS, ThemeManager, PluginManager
- **Plugin development** - Plugins ontwikkelen
- **Routing** - Frontend en admin routing
- **Beveiliging** - XSS, CSRF, path traversal
- **Testing** - Penetratie, accessibility en functionele tests
- **Debugging** - Logging en cache
- **Performance** - OPcache en SCSS caching
- **Architectuur** CMS architectuur en mappenstructuur
- **Core classes** CodePressCMS, ThemeManager, PluginManager, AdminAuth
- **Plugin development** Plugin types (content/systeem), structuur, API
- **Routing** Frontend, admin en plugin admin routing
- **Beveiliging** XSS, CSRF, path traversal, RBAC
- **Testing** Penetratie, accessibility en functionele tests
- **Debugging** Logging en cache
- **Performance** OPcache en SCSS caching
Belangrijke concepten in CodePress 2.6.1:
- **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.
+1 -1
View File
@@ -11,7 +11,7 @@ codepress/
│ ├── config/ # app.php, admin.json
│ ├── src/ # AdminAuth
│ └── theme/default/views/ # Twig templates
├── themes/ # Thema's (default, demo, ...)
├── themes/ # Thema's (default, ...)
├── plugins/ # Plugins (Dashboard, HTMLBlock, Navigation, ...)
├── content/ # Content bestanden (.md, .php, .html)
├── language/ # Taalbestanden (nl/, en/, de/)
+49 -7
View File
@@ -2,18 +2,60 @@
## 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
// Schone URLs: /nl/pagina
// Query: ?page=pagina&lang=nl
### PHP dev server
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
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
// Routes: /admin/dashboard, /admin/content, etc.
// Query parameter: ?route=dashboard
// PluginManager roept deze aan voor /admin/mijn-systeem
$plugin->handleAdminRoute('mijn-systeem');
```
@@ -4,28 +4,48 @@ Content wordt opgeslagen in de `content/` map zonder database.
## Ondersteunde bestandsformaten
- `.md` - Markdown (aanbevolen)
- `.php` - Dynamische PHP pagina's
- `.html` - Statische HTML pagina's
- `.md` Markdown (aanbevolen)
- `.php` Dynamische PHP pagina's
- `.html` Statische HTML pagina's
## Bestandsnaam conventies
```
nl.pagina-naam.md # Nederlandse 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
Markdown bestanden kunnen frontmatter metadata bevatten:
```markdown
---
layout: full_content
plugins: HTMLBlock
layout: left_sidebar
plugins:
- HTMLBlock
- Navigation
---
# Pagina titel
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`).
+36 -4
View File
@@ -5,12 +5,44 @@
1. Login op `/admin`
2. Ga naar **Content**
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
## Editor functies
- **CodeMirror** editor met syntax highlighting
- **CodeMirror** editor met syntax highlighting (Markdown, PHP, HTML)
- **Toolbar** voor snel Markdown invoegen
- **Live preview** via knop
- **Auto-save** backups in `.bak/` map
- **Sneltoetsen**: Ctrl+S (opslaan), Ctrl+N (nieuw)
- **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
Welkom bij de CodePress CMS handleiding (versie 2.6.1). 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.
+38 -9
View File
@@ -2,20 +2,49 @@
```
themes/mijn-thema/
├── theme.json # Thema configuratie
├── base.twig # Hoofd layout
├── full_content.twig # Layout: full-width
├── theme.json # { title, config.default_template, template: layout→.twig }
├── base.twig # Hoofd layout (head, header, nav, breadcrumb, footer)
├── full_content.twig # Layout: volledige breedte
├── left_sidebar.twig # Layout: sidebar links
├── right_sidebar.twig # Layout: sidebar rechts
├── custom.twig # Layout: custom
├── 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
├── css/ # CSS bestanden
├── js/theme.js # JavaScript
├── fonts/ # Lettertypes
── img/ # Afbeeldingen
├── scss/theme.scss # SCSS bron (de enige CSS bron)
├── 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
```
## 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 %}
```
+10
View File
@@ -35,4 +35,14 @@ return [
'plugin_development' => 'Plugin Entwicklung',
'template_system' => 'Template System',
'go_to' => 'Gehe zu',
'welcome_title' => 'Willkommen bei CodePress CMS',
'welcome_new_install' => 'Neue Installation',
'welcome_intro' => 'Sie sehen diese Seite, weil das Inhaltsverzeichnis noch leer ist. Dies ist eine neue Installation von CodePress CMS.',
'welcome_next_steps' => 'Nächste Schritte',
'welcome_step_1' => 'Melden Sie sich über die Admin-Konsole an, um Inhalte zu verwalten',
'welcome_step_2' => 'Fügen Sie Markdown-, PHP- oder HTML-Dateien zum Inhaltsverzeichnis hinzu',
'welcome_step_3' => 'Lesen Sie das Handbuch für die vollständige Dokumentation',
'welcome_admin_link' => 'Zur Admin-Konsole',
'welcome_guide_link' => 'Handbuch ansehen',
'welcome_content_empty' => 'Das Inhaltsverzeichnis ist leer',
];
+10
View File
@@ -35,4 +35,14 @@ return [
'plugin_development' => 'Plugin Development',
'template_system' => 'Template System',
'go_to' => 'Go to',
'welcome_title' => 'Welcome to CodePress CMS',
'welcome_new_install' => 'New installation',
'welcome_intro' => 'You are seeing this page because the content directory is still empty. This is a new installation of CodePress CMS.',
'welcome_next_steps' => 'Next steps',
'welcome_step_1' => 'Log in via the admin console to manage content',
'welcome_step_2' => 'Add Markdown, PHP or HTML files to the content directory',
'welcome_step_3' => 'Read the manual for full documentation',
'welcome_admin_link' => 'Go to admin console',
'welcome_guide_link' => 'View manual',
'welcome_content_empty' => 'The content directory is empty',
];
+10
View File
@@ -35,4 +35,14 @@ return [
'plugin_development' => 'Plugin Ontwikkeling',
'template_system' => 'Template Systeem',
'go_to' => 'Ga naar',
'welcome_title' => 'Welkom bij CodePress CMS',
'welcome_new_install' => 'Nieuwe installatie',
'welcome_intro' => 'U ziet deze pagina omdat de content-map nog leeg is. Dit is een nieuwe installatie van CodePress CMS.',
'welcome_next_steps' => 'Volgende stappen',
'welcome_step_1' => 'Log in via de admin console om content te beheren',
'welcome_step_2' => 'Voeg Markdown-, PHP- of HTML-bestanden toe in de content-map',
'welcome_step_3' => 'Bekijk de handleiding voor uitgebreide documentatie',
'welcome_admin_link' => 'Naar de admin console',
'welcome_guide_link' => 'Handleiding bekijken',
'welcome_content_empty' => 'De content-map is leeg',
];
-25
View File
@@ -1,25 +0,0 @@
{
"name": "codepress",
"version": "1.7.1",
"description": "A lightweight, file-based Content Management System built with PHP and Bootstrap.",
"main": "index.js",
"scripts": {
"build:css": "npx sass --load-path=node_modules src/scss/main.scss public/assets/css/style.css --style=compressed",
"watch:css": "npx sass --load-path=node_modules --watch src/scss/main.scss public/assets/css/style.css",
"build": "npm run build:css",
"clean": "rm -rf node_modules package-lock.json",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://git.noorlander.info/E.Noorlander/CodePress.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"bootstrap": "^5.3.8",
"sass": "^1.94.2"
}
}
+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';
$isHomepage = $this->api ? $this->api->isHomepage() : false;
$currentLang = $this->api ? $this->api->getCurrentLanguage() : 'nl';
$currentPath = $_GET['page'] ?? '';
$currentPath = preg_replace('/\.(md|php|html)$/', '', $currentPath);
$content = '
<p class="mb-2"><strong>Huidige pagina:</strong> ' . htmlspecialchars($currentPage) . '</p>
@@ -43,7 +45,7 @@ class HTMLBlock
</div>';
}
// Add quick navigation
// Add dynamic quick navigation
$menu = $this->api->getMenu();
if (!empty($menu)) {
$content .= '
@@ -52,8 +54,25 @@ class HTMLBlock
foreach ($menu as $item) {
if ($item['type'] === 'file') {
$url = $this->api->createUrl($item['path']);
$content .= '<li><a href="' . htmlspecialchars($url) . '" class="text-decoration-none">📄 ' . htmlspecialchars($item['title']) . '</a></li>';
$url = '/' . $currentLang . '/' . $item['path'];
$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
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=$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';
-361
View File
@@ -1,361 +0,0 @@
// Custom variables (must come before Bootstrap import)
$primary: #0d6efd;
$navigation-opacity: 0.5; // 50% opacity
// Import Bootstrap
@import "bootstrap/scss/bootstrap";
// Custom navigation background with 50% opacity of primary color
.navigation-50-opacity {
background-color: rgba($primary, $navigation-opacity) !important;
}
// Custom CodePress styles
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
margin: 0;
padding: 0;
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
}
/* Header */
header.navbar {
flex-shrink: 0;
height: 66px;
}
/* Navigation section */
.navigation-section {
background-color: #ffffff;
border-bottom: 1px solid #dee2e6;
padding: 0.5rem 0;
flex-shrink: 0;
}
/* Tab styling */
.nav-tabs {
border-bottom: none;
}
.nav-tabs .nav-link {
border: 1px solid transparent;
border-bottom: none;
border-radius: 0; // Geen ronde hoeken
color: #6c757d;
background-color: transparent; // Transparante achtergrond voor eerste laag
margin-right: 0.25rem;
transition: all 0.2s ease;
}
.nav-tabs .nav-link:hover {
color: #495057;
background-color: rgba(0, 0, 0, 0.05); // Subtiel hover effect
border-color: transparent;
}
.nav-tabs .nav-link.active {
color: #495057;
background-color: #ffffff; // Alleen active heeft witte achtergrond
border-color: #dee2e6 #dee2e6 transparent;
font-weight: 500;
}
/* Dropdown in tabs - eerste laag */
.nav-tabs .nav-item.dropdown .nav-link {
background-color: transparent; // Transparante achtergrond
border: 1px solid transparent;
border-radius: 0; // Geen ronde hoeken
}
.nav-tabs .nav-item.dropdown .nav-link:hover {
background-color: rgba(0, 0, 0, 0.05); // Subtiel hover effect
border-color: transparent;
}
/* Dropdown menu styling */
.nav-tabs .dropdown-menu {
margin-top: 0.25rem;
border-radius: 0.375rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
min-width: 200px;
max-width: 300px;
width: auto;
}
/* Dropdown items styling */
.nav-tabs .dropdown-menu .dropdown-item {
white-space: normal;
padding: 0.5rem 1rem;
line-height: 1.4;
}
/* Long text truncation for very long titles */
.nav-tabs .dropdown-menu .dropdown-item {
overflow: hidden;
text-overflow: ellipsis;
max-width: 280px;
}
/* Breadcrumb section */
.breadcrumb-section {
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
flex-shrink: 0;
}
/* Breadcrumb styling consistency */
.breadcrumb-section .breadcrumb {
margin-bottom: 0;
}
.breadcrumb-section .breadcrumb-item {
color: #6c757d !important;
}
.breadcrumb-section .breadcrumb-item a {
color: #0d6efd !important;
text-decoration: none;
}
.breadcrumb-section .breadcrumb-item a:hover {
color: #0a58ca !important;
text-decoration: underline;
}
.breadcrumb-section .breadcrumb-item.active {
color: #6c757d !important;
}
/* Main content area */
.main-content {
flex: 1;
overflow-y: auto;
padding: 1rem 0;
}
/* Content specific styling */
.markdown-content,
.php-content,
.html-content {
margin-bottom: 2rem;
}
.markdown-content .content-body,
.php-content .content-body,
.html-content .content-body {
padding: 2rem;
line-height: 1.6;
}
/* Footer */
footer {
flex-shrink: 0;
}
/* Navigation dropdown styling */
.navigation-section .btn {
font-weight: 500;
}
.navigation-section .collapse .nav-link {
color: #495057;
padding: 0.375rem 0.75rem;
border-radius: 0.25rem;
transition: all 0.2s ease;
}
.navigation-section .collapse .nav-link:hover {
background-color: #e9ecef;
color: #212529;
}
.navigation-section .collapse .nav-link.active {
background-color: #0d6efd;
color: white;
}
.navigation-section .collapse .nav-link i {
margin-right: 0.5rem;
}
/* File info styling */
.file-info {
font-size: 0.9rem;
color: #6c757d;
flex: 1;
min-width: 0;
}
.file-info i {
margin-right: 5px;
}
.page-title {
color: #6c757d;
max-width: 250px;
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
margin-right: 10px;
}
.page-title:hover {
color: #495057;
cursor: default;
}
.file-details {
color: #6c757d;
font-size: 0.85rem;
}
.site-info a {
color: #0d6efd;
text-decoration: none;
}
.site-info a:hover {
text-decoration: underline;
}
.guide-link {
color: #6c757d !important;
text-decoration: none;
font-size: 0.9rem;
}
.guide-link:hover {
color: #0d6efd !important;
text-decoration: none;
}
.guide-link i {
font-size: 1rem;
color: #6c757d !important;
}
.guide-link:hover i {
color: #0d6efd !important;
}
.auto-link {
color: #0d6efd;
text-decoration: none;
border-bottom: 2px dashed #0d6efd;
font-weight: 500;
transition: all 0.2s ease;
}
.auto-link:hover {
color: #0a58ca;
text-decoration: none;
border-bottom-style: solid;
border-bottom-color: #0a58ca;
}
/* Search form */
.search-form {
max-width: 300px;
}
/* Search input text color - override navbar form-control styling */
.navbar .form-control.search-input {
color: #000000 !important;
}
.navbar .form-control.search-input::placeholder {
color: #6c757d !important;
}
/* Card styling */
.card-title a {
text-decoration: none;
color: inherit;
}
.card-title a:hover {
text-decoration: underline;
}
/* Dropdown submenu styling */
.dropdown-menu .dropdown-menu {
top: 0;
left: 100%;
margin-top: -1px;
margin-left: -1px;
}
/* Show nested dropdowns on hover */
.dropdown-submenu {
position: relative;
}
.dropdown-submenu .dropdown-menu {
top: 0;
left: 100%;
margin-top: -1px;
}
.dropdown-submenu:hover > .dropdown-menu {
display: block;
}
/* For touch devices - click to open */
.dropdown-submenu > .dropdown-toggle:active::after {
transform: rotate(90deg);
}
/* Responsive adjustments */
@media (max-width: 768px) {
.main-content {
padding-top: 120px; /* More space for mobile */
}
.markdown-content .content-body,
.php-content .content-body,
.html-content .content-body {
padding: 1.5rem;
}
.nav-tabs .dropdown-menu {
min-width: 180px;
max-width: 250px;
}
.nav-tabs .dropdown-menu .dropdown-item {
max-width: 230px;
font-size: 0.9rem;
}
}
@media (max-width: 576px) {
.markdown-content .content-body,
.php-content .content-body,
.html-content .content-body {
padding: 1rem;
}
.nav-tabs .dropdown-menu {
min-width: 160px;
max-width: 200px;
}
.nav-tabs .dropdown-menu .dropdown-item {
max-width: 180px;
font-size: 0.85rem;
padding: 0.4rem 0.8rem;
}
}
-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) {
const toggle = e.target.closest('.dropdown-submenu .dropdown-toggle');
const toggle = e.target.closest('.dropdown-submenu > .dropdown-toggle');
if (toggle) {
e.preventDefault();
e.stopPropagation();
const submenu = toggle.closest('.dropdown-submenu');
const submenu = toggle.parentElement;
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;
parent.querySelectorAll('.dropdown-submenu').forEach(function(sibling) {
parent.querySelectorAll(':scope > .dropdown-submenu').forEach(function(sibling) {
if (sibling !== submenu) {
sibling.classList.remove('show');
var siblingMenu = sibling.querySelector('.dropdown-menu');
if (siblingMenu) siblingMenu.classList.remove('show');
}
});
// Toggle current submenu
if (dropdown) dropdown.classList.toggle('show');
if (dropdown) {
dropdown.classList.toggle('show');
submenu.classList.toggle('show');
}
return;
}
// Close all open submenus when clicking outside
document.querySelectorAll('.dropdown-submenu .dropdown-menu.show').forEach(function(menu) {
menu.classList.remove('show');
document.querySelectorAll('.dropdown-submenu.show').forEach(function(sm) {
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 {
--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;
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 {
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;
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
.dropdown-toggle::after {
display: none !important;
@@ -217,8 +239,7 @@ $header-bg-opacity: 1;
// Enhanced accessibility styles
.focus-visible:focus,
.btn:focus,
.form-control:focus,
.nav-link:focus {
.form-control:focus {
outline: 3px solid #0056b3 !important;
outline-offset: 2px !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 {
--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;
border: none !important;
border-bottom: none !important;
border-radius: 0 !important;
gap: 0 !important;
.nav-link {
margin-bottom: 0 !important;
background-color: transparent !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;
outline: none !important;
box-shadow: none !important;
&:hover {
&:hover, &:focus, &:focus-visible {
isolation: auto;
background-color: rgba(255, 255, 255, 0.1) !important;
border: none !important;
border-color: transparent !important;
outline: none !important;
box-shadow: none !important;
}
&.active {
background-color: rgba(255, 255, 255, 0.2) !important;
border: none !important;
border-color: transparent !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
+1 -1
View File
@@ -31,7 +31,7 @@
<link rel="me" href="{{ author_git }}">
<!-- 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="apple-touch-icon" href="{{ theme_base_url }}/assets/img/favicon.svg">
<link rel="manifest" href="/manifest.json">
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-54
View File
@@ -1,54 +0,0 @@
/* Mobile search improvements */
@media (max-width: 991.98px) {
.navbar {
position: relative;
}
/* Hide desktop elements on mobile */
.d-none.d-lg-flex {
display: none !important;
}
#mobileSearch {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--header-bg);
z-index: 1030;
}
#mobileSearch.show {
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
/* Ensure navigation doesn't overlap with expanded search */
.navigation-section {
margin-top: 0;
border: none !important;
}
}
/* Desktop improvements */
@media (min-width: 992px) {
/* Hide mobile elements on desktop */
.d-lg-none {
display: none !important;
}
/* Hide mobile search on desktop */
#mobileSearch {
display: none !important;
}
}
/* Search form improvements */
.form-control[type="search"] {
min-width: 200px;
}
@media (max-width: 576px) {
.form-control[type="search"] {
min-width: 150px;
}
}
-4
View File
@@ -1,4 +0,0 @@
/*!
* Bootstrap v5.3.8 (https://getbootstrap.com/)
* Copyright 2011-2025 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
-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.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

-11
View File
@@ -1,11 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<!-- < -->
<path d="M8 8 L3 16 L8 24" stroke="#ffffff" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<!-- / -->
<path d="M12 24 L18 8" stroke="#ffffff" stroke-width="3" stroke-linecap="round"/>
<!-- .. -->
<circle cx="22" cy="20" r="2" fill="#ffffff"/>
<circle cx="28" cy="20" r="2" fill="#ffffff"/>
</svg>

Before

Width:  |  Height:  |  Size: 442 B

-11
View File
@@ -1,11 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<!-- < -->
<path d="M8 8 L3 16 L8 24" stroke="#ffffff" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<!-- / -->
<path d="M12 24 L18 8" stroke="#ffffff" stroke-width="3" stroke-linecap="round"/>
<!-- .. -->
<circle cx="22" cy="20" r="2" fill="#ffffff"/>
<circle cx="28" cy="20" r="2" fill="#ffffff"/>
</svg>

Before

Width:  |  Height:  |  Size: 442 B

-94
View File
@@ -1,94 +0,0 @@
// Main application JavaScript
// This file contains general application functionality
/**
* Toggle sidebar visibility (open/close)
*/
function toggleSidebar() {
const sidebar = document.getElementById('site-sidebar');
const contentCol = sidebar ? sidebar.nextElementSibling || sidebar.parentElement.querySelector('.content-column') : null;
const btn = document.querySelector('.sidebar-toggle-btn');
const icon = btn ? btn.querySelector('i') : null;
if (!sidebar) return;
sidebar.classList.toggle('sidebar-hidden');
// Adjust content column width, toggle icon, and update aria-expanded
if (sidebar.classList.contains('sidebar-hidden')) {
if (contentCol) {
contentCol.classList.remove('col-lg-9', 'col-md-8');
contentCol.classList.add('col-12');
}
if (icon) {
icon.classList.remove('bi-layout-sidebar-inset');
icon.classList.add('bi-layout-sidebar');
}
if (btn) {
btn.setAttribute('aria-expanded', 'false');
}
sessionStorage.setItem('sidebarHidden', 'true');
} else {
if (contentCol) {
contentCol.classList.remove('col-12');
contentCol.classList.add('col-lg-9', 'col-md-8');
}
if (icon) {
icon.classList.remove('bi-layout-sidebar');
icon.classList.add('bi-layout-sidebar-inset');
}
if (btn) {
btn.setAttribute('aria-expanded', 'true');
}
sessionStorage.setItem('sidebarHidden', 'false');
}
}
/**
* Restore sidebar state from sessionStorage on page load
*/
function restoreSidebarState() {
if (sessionStorage.getItem('sidebarHidden') === 'true') {
const sidebar = document.getElementById('site-sidebar');
if (sidebar) {
toggleSidebar();
}
}
}
// Initialize application when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
// Restore sidebar state
restoreSidebarState();
// Handle nested dropdowns for touch devices using event delegation
document.addEventListener('click', function(e) {
const toggle = e.target.closest('.dropdown-submenu .dropdown-toggle');
if (toggle) {
e.preventDefault();
e.stopPropagation();
const submenu = toggle.closest('.dropdown-submenu');
const dropdown = submenu.querySelector('.dropdown-menu');
// Close other submenus at the same level
const parent = submenu.parentElement;
parent.querySelectorAll('.dropdown-submenu').forEach(function(sibling) {
if (sibling !== submenu) {
var siblingMenu = sibling.querySelector('.dropdown-menu');
if (siblingMenu) siblingMenu.classList.remove('show');
}
});
// Toggle current submenu
if (dropdown) dropdown.classList.toggle('show');
return;
}
// Close all open submenus when clicking outside
document.querySelectorAll('.dropdown-submenu .dropdown-menu.show').forEach(function(menu) {
menu.classList.remove('show');
});
});
});
File diff suppressed because one or more lines are too long
-333
View File
@@ -1,333 +0,0 @@
// CodePress demo theme styles
// A distinct look to demonstrate that themes are fully swappable.
// Theme color variables (defined per theme; used by the CSS custom properties below)
$header-bg: #613583;
$header-font: #ffffff;
$header-height: 120px;
$nav-bg: #813d9c;
$nav-font: #ffffff;
$nav-height: 50px;
$sidebar-bg: #f3e8f7;
$sidebar-border: #d8b4e0;
// Optional header background image and overlay opacity
$header-bg-image: none;
$header-bg-opacity: 1;
:root {
--header-bg: #{$header-bg};
--header-font: #{$header-font};
--header-height: #{$header-height};
--nav-bg: #{$nav-bg};
--nav-font: #{$nav-font};
--nav-height: #{$nav-height};
--sidebar-bg: #{$sidebar-bg};
--sidebar-border: #{$sidebar-border};
}
// Header background (optional background image via $header-bg-image)
#site-header {
background-image: $header-bg-image;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
position: relative;
}
@if $header-bg-image != 'none' {
#site-header::before {
content: '';
position: absolute;
inset: 0;
background-color: var(--header-bg);
opacity: $header-bg-opacity;
pointer-events: none;
z-index: 0;
}
}
// Rounded, softer header
.navbar {
background-color: var(--header-bg) !important;
min-height: var(--header-height);
border-bottom: 4px solid var(--nav-bg) !important;
.navbar-brand,
.navbar-text,
.form-control,
.btn {
color: var(--header-font) !important;
}
.form-control::placeholder {
color: rgba(255, 255, 255, 0.7) !important;
}
.btn-outline-light {
border-color: var(--header-font) !important;
}
}
// Language dropdown styling
.dropdown-menu {
background-color: var(--header-bg) !important;
border: 1px solid var(--header-font) !important;
}
.dropdown-item {
color: var(--header-font) !important;
&: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;
&:hover {
background-color: rgba(255, 255, 255, 0.1) !important;
color: var(--header-font) !important;
}
&:focus,
&:active,
.show > &.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
.breadcrumb-item + .breadcrumb-item::before {
content: "" !important;
padding: 0 !important;
}
.breadcrumb {
--bs-breadcrumb-divider: "";
--bs-breadcrumb-margin-bottom: 0;
margin-bottom: 0;
.breadcrumb-item {
color: var(--header-bg) !important;
a {
color: var(--header-bg) !important;
text-decoration: none;
&: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;
&:hover {
opacity: 0.7;
}
}
.sidebar-column {
transition: all 0.3s ease;
}
.sidebar-hidden {
display: none !important;
}
// Navigation section background
.navigation-section {
background-color: var(--nav-bg) !important;
color: var(--nav-font) !important;
min-height: var(--nav-height);
}
// Accessibility
.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-link {
background-color: transparent !important;
border: none !important;
color: var(--nav-font) !important;
&:hover {
background-color: rgba(255, 255, 255, 0.1) !important;
}
&.active {
background-color: rgba(255, 255, 255, 0.2) !important;
border-bottom: 2px solid var(--nav-font) !important;
}
}
}
// Sidebar styling - rounded cards look
.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;
.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;
}
// Mobile responsive
@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;
}
}
// Code block styling
pre {
background: #f8f9fa;
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
border: 1px solid #dee2e6;
margin-bottom: 1rem;
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 hover effects
.footer-icon {
color: #6c757d;
text-decoration: none;
transition: all 0.2s ease-in-out;
display: inline-block;
padding: 2px;
&:hover {
color: #0d6efd;
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
}
&.guide:hover {
color: #198754;
}
&.cms:hover {
color: #dc3545;
}
&.git:hover {
color: #6f42c1;
}
&.website:hover {
color: #fd7e14;
}
}
-143
View File
@@ -1,143 +0,0 @@
<!DOCTYPE html>
<html lang="{{ current_lang }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ page_title }} - {{ site_title }}</title>
<!-- Skip to content link for accessibility -->
<a href="#main-content" class="skip-link sr-only sr-only-focusable">Skip to main content</a>
<!-- CMS Meta Tags -->
<meta name="generator" content="{{ site_title }} CMS">
<meta name="application-name" content="{{ site_title }}">
<meta name="author" content="{{ author_name }}">
<meta name="creator" content="{{ author_name }}">
<meta name="publisher" content="{{ author_name }}">
<!-- SEO Meta Tags -->
<meta name="description" content="{{ seo_description }}">
<meta name="keywords" content="{{ seo_keywords }}">
{% if block_ai_bots %}
<meta name="robots" content="noai, noimageai">
<meta name="tdm-reservation" content="1">
{% endif %}
{% if block_search_engines %}
<meta name="robots" content="noindex, nofollow">
{% endif %}
<!-- Author Links -->
<link rel="author" href="{{ author_website }}">
<link rel="me" href="{{ author_git }}">
<!-- Favicon -->
<link rel="icon" href="{{ theme_base_url }}/assets/img/favicon.ico" sizes="any">
<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="manifest" href="/manifest.json">
<meta name="theme-color" content="{{ theme_config.header_color|default('#613583') }}">
<!-- Theme CSS -->
{% for cssFile in theme_css_files %}
<link href="{{ cssFile }}" rel="stylesheet">
{% endfor %}
<!-- Compiled theme CSS (if SCSS was used) -->
{% if theme_css_url %}
<link href="{{ theme_css_url }}" rel="stylesheet">
{% endif %}
<!-- Accessibility styles -->
<style>
.skip-link {
position: absolute;
top: -40px;
left: 6px;
background: #000;
color: #fff;
padding: 8px;
text-decoration: none;
z-index: 100;
}
.skip-link:focus {
top: 6px;
outline: 3px solid #0056b3;
outline-offset: 2px;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.sr-only-focusable:focus {
position: static;
width: auto;
height: auto;
padding: inherit;
margin: inherit;
overflow: visible;
clip: auto;
white-space: normal;
}
</style>
<!-- Dynamic theme styles -->
<style>
html, body {
height: 100%;
}
body {
display: flex;
flex-direction: column;
}
#site-header > * {
position: relative;
z-index: 1;
}
</style>
</head>
<body>
{% include 'partials/header.twig' %}
<nav role="navigation" aria-label="Main navigation" id="site-navigation">
{% include 'partials/navigation.twig' %}
</nav>
<nav id="site-breadcrumb" class="breadcrumb-section bg-light border-bottom" aria-label="Breadcrumb navigation">
<div class="container-fluid">
<div class="row">
<div class="col-12 py-2">
<h2 class="sr-only">Breadcrumb Navigation</h2>
{{ breadcrumb|raw }}
</div>
</div>
</div>
</nav>
<main role="main" id="main-content" class="main-content" style="padding: 0;">
{% block content %}{% endblock %}
</main>
<footer role="contentinfo" id="site-footer">
{% include 'partials/footer.twig' %}
</footer>
<!-- Theme JS -->
{% for jsFile in theme_js_files %}
<script src="{{ jsFile }}"></script>
{% endfor %}
<!-- Theme-specific JS -->
{% if theme_js_url %}
<script src="{{ theme_js_url }}"></script>
{% endif %}
</body>
</html>
-18
View File
@@ -1,18 +0,0 @@
{% extends 'base.twig' %}
{% block content %}
<div class="container-fluid">
<div class="row g-0">
<aside role="complementary" aria-label="Sidebar content" id="site-sidebar" class="col-12 sidebar-column">
<div class="sidebar">
{{ sidebar_content|raw }}
</div>
</aside>
<section id="site-content" class="col-12">
<div class="content-wrapper p-4">
{{ content|raw }}
</div>
</section>
</div>
</div>
{% endblock %}
-11
View File
@@ -1,11 +0,0 @@
{% extends 'base.twig' %}
{% block content %}
<div class="container">
<section id="site-content" class="col-12">
<div class="content-wrapper p-4">
{{ content|raw }}
</div>
</section>
</div>
{% endblock %}
-32
View File
@@ -1,32 +0,0 @@
{% extends 'base.twig' %}
{% block title %}{{ page.title }}{% endblock %}
{% block content %}
{% if sidebar_content %}
<div class="row g-0">
<aside role="complementary" aria-label="Handleiding navigatie" id="site-sidebar" class="col-lg-3 col-md-4 sidebar-column order-2 order-md-1">
<div class="sidebar h-100">
{{ sidebar_content|raw }}
</div>
</aside>
<section id="site-content" class="col-lg-9 col-md-8 content-column order-1 order-md-2">
<div class="content-wrapper p-4">
<div class="guide-content">
{{ content|raw }}
</div>
</div>
</section>
</div>
{% else %}
<div class="container">
<section id="site-content" class="col-12">
<div class="content-wrapper p-4">
<div class="guide-content">
{{ content|raw }}
</div>
</div>
</section>
</div>
{% endif %}
{% endblock %}
-26
View File
@@ -1,26 +0,0 @@
{% extends 'base.twig' %}
{% block content %}
{% if sidebar_content %}
<div class="row g-0">
<aside role="complementary" aria-label="Sidebar content" id="site-sidebar" class="col-lg-3 col-md-4 sidebar-column order-2 order-md-1">
<div class="sidebar h-100">
{{ sidebar_content|raw }}
</div>
</aside>
<section id="site-content" class="col-lg-9 col-md-8 content-column order-1 order-md-2">
<div class="content-wrapper p-4">
{{ content|raw }}
</div>
</section>
</div>
{% else %}
<div class="container">
<section id="site-content" class="col-12">
<div class="content-wrapper p-4">
{{ content|raw }}
</div>
</section>
</div>
{% endif %}
{% endblock %}
-48
View File
@@ -1,48 +0,0 @@
<footer class="bg-light border-top py-1">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center">
<div class="file-info mb-2 mb-md-0">
<small class="text-muted">
<i class="bi bi-file-text footer-icon" title="{{ t_file_details }}: {{ page_title }}"></i>
<span class="page-title d-none d-lg-inline" title="{{ page_title }}">{{ page_title }}</span>
{% if file_info_block %}
<span class="ms-2">
{% if show_created %}
<i class="bi bi-calendar-plus footer-icon" title="{{ t_created }}: {{ created }}"></i>
<span class="file-created me-1" title="{{ t_created }}: {{ created }}">{{ created }}</span>
{% endif %}
<i class="bi bi-calendar-check footer-icon" title="{{ t_modified }}: {{ modified }}"></i>
<span class="file-modified" title="{{ t_modified }}: {{ modified }}">{{ modified }}</span>
</span>
{% endif %}
</small>
</div>
<div class="site-info">
<small class="text-muted">
<a href="/{{ current_lang }}/guide" class="footer-icon guide" title="{{ t_guide }}">
<i class="bi bi-book"></i>
</a>
<span class="ms-1">|</span>
{% if cms_version %}
<span class="ms-1 cms-version text-muted">{{ cms_version }}</span>
{% endif %}
<a href="https://git.noorlander.info/E.Noorlander/CodePress.git" target="_blank" rel="noopener noreferrer" class="footer-icon cms ms-1" title="{{ t_powered_by }} CodePress CMS">
<i class="bi bi-cpu"></i>
</a>
<span class="ms-1">|</span>
<a href="{{ author_website }}" target="_blank" rel="noopener noreferrer" class="footer-icon website" title="{{ t_author_website }}">
<i class="bi bi-globe"></i>
</a>
<span class="ms-1">|</span>
<a href="{{ author_git }}" target="_blank" rel="noopener noreferrer" class="footer-icon git" title="{{ t_author_git }}">
<i class="bi bi-git"></i>
</a>
</small>
</div>
</div>
</div>
</div>
</div>
</footer>
-75
View File
@@ -1,75 +0,0 @@
<header id="site-header" class="navbar navbar-expand-lg navbar-dark" style="background-color: transparent;">
<div class="container-fluid">
<a class="navbar-brand" href="/{{ current_lang }}">
<img src="{{ theme_base_url }}/assets/img/icon.svg" alt="CodePress Logo" width="32" height="32" class="me-2">
{{ site_title }}
</a>
<!-- Desktop search and language -->
<div class="d-none d-lg-flex ms-auto align-items-center">
<form class="d-flex me-3" method="GET" action="" role="search" aria-label="Site search">
<div class="form-group">
<label for="desktop-search-input" class="sr-only">{{ t_search_placeholder }}</label>
<input class="form-control me-2 search-input" type="search" id="desktop-search-input" name="search" placeholder="{{ t_search_placeholder }}" value="{{ search_query }}" aria-describedby="search-help">
<div id="search-help" class="sr-only">Enter keywords to search through the documentation</div>
</div>
<button class="btn btn-outline-light" type="submit" aria-label="{{ t_search_button }}">
<i class="bi bi-search" aria-hidden="true"></i>
<span class="sr-only">{{ t_search_button }}</span>
</button>
</form>
<!-- Language switcher -->
<div class="dropdown">
<button class="btn btn-outline-light" type="button" data-bs-toggle="dropdown" aria-haspopup="menu" aria-expanded="false" aria-label="Select language - currently {{ current_lang_upper }}">
{{ current_lang_upper }} <i class="bi bi-chevron-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end" role="menu">
{% for lang in available_langs %}
<li role="none">
<a class="dropdown-item {{ lang.is_current ? 'active' : '' }}" href="{{ lang.url }}" role="menuitem" {% if lang.is_current %}aria-current="true"{% endif %} lang="{{ lang.code }}">
{{ lang.native_name }}
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
<!-- Mobile search and language toggle -->
<div class="d-lg-none">
<button class="btn btn-outline-light" type="button" data-bs-toggle="collapse" data-bs-target="#mobileSearch" aria-controls="mobileSearch" aria-expanded="false" aria-label="Toggle search">
<i class="bi bi-search"></i>
</button>
<button class="btn btn-outline-light" type="button" data-bs-toggle="dropdown" aria-haspopup="menu" aria-expanded="false" aria-label="Select language - currently {{ current_lang_upper }}">
{{ current_lang_upper }} <i class="bi bi-chevron-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end" role="menu">
{% for lang in available_langs %}
<li role="none">
<a class="dropdown-item {{ lang.is_current ? 'active' : '' }}" href="{{ lang.url }}" role="menuitem" {% if lang.is_current %}aria-current="true"{% endif %} lang="{{ lang.code }}">
{{ lang.native_name }}
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
<!-- Mobile search bar -->
<div class="collapse navbar-collapse d-lg-none" id="mobileSearch">
<div class="container-fluid px-0">
<form class="d-flex px-3 pb-3" method="GET" action="" role="search" aria-label="Site search">
<div class="form-group w-100">
<label for="mobile-search-input" class="sr-only">{{ t_search_placeholder }}</label>
<input class="form-control me-2 search-input" type="search" id="mobile-search-input" name="search" placeholder="{{ t_search_placeholder }}" value="{{ search_query }}" aria-describedby="mobile-search-help">
<div id="mobile-search-help" class="sr-only">Enter keywords to search through the documentation</div>
</div>
<button class="btn btn-outline-light" type="submit" aria-label="{{ t_search_button }}">
<i class="bi bi-search" aria-hidden="true"></i>
<span class="sr-only">{{ t_search_button }}</span>
</button>
</form>
</div>
</div>
</header>
-17
View File
@@ -1,17 +0,0 @@
<nav class="navigation-section" role="navigation" aria-label="Main navigation">
<h2 class="sr-only">Site Navigation</h2>
<div class="container-fluid">
<div class="row align-items-center">
<div class="col">
<ul class="nav nav-tabs flex-wrap" role="menubar">
<li class="nav-item" role="none">
<a class="nav-link {{ home_active_class }}" href="/{{ current_lang }}" role="menuitem" {% if is_homepage %}aria-current="page"{% endif %}>
<i class="bi bi-house" aria-hidden="true"></i> {{ homepage_title }}
</a>
</li>
{{ menu|raw }}
</ul>
</div>
</div>
</div>
</nav>
-26
View File
@@ -1,26 +0,0 @@
{% extends 'base.twig' %}
{% block content %}
{% if sidebar_content %}
<div class="row g-0">
<section id="site-content" class="col-lg-9 col-md-8 content-column order-1">
<div class="content-wrapper p-4">
{{ content|raw }}
</div>
</section>
<aside role="complementary" aria-label="Sidebar content" id="site-sidebar" class="col-lg-3 col-md-4 sidebar-column order-2">
<div class="sidebar h-100">
{{ sidebar_content|raw }}
</div>
</aside>
</div>
{% else %}
<div class="container">
<section id="site-content" class="col-12">
<div class="content-wrapper p-4">
{{ content|raw }}
</div>
</section>
</div>
{% endif %}
{% endblock %}
-13
View File
@@ -1,13 +0,0 @@
{
"title": "demo",
"config": {
"default_template": "left_sidebar"
},
"template": {
"full_content": "full_content.twig",
"left_sidebar": "left_sidebar.twig",
"right_sidebar": "right_sidebar.twig",
"custom1": "custom1.twig",
"guide": "guide.twig"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

+3 -4
View File
@@ -40,8 +40,7 @@ gen_preview() {
echo "Generated $dir/theme.png"
}
for d in default demo test; do
if [[ -d "$d" ]]; then
gen_preview "$d"
fi
for d in */; do
d="${d%/}"
gen_preview "$d"
done
-33
View File
@@ -8,39 +8,6 @@ $baseDir = dirname($vendorDir);
return array(
'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'Mustache_Cache' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Cache_AbstractCache' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Cache_FilesystemCache' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Cache_NoopCache' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Compiler' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Context' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Engine' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Exception' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Exception_InvalidArgumentException' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Exception_LogicException' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Exception_RuntimeException' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Exception_SyntaxException' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Exception_UnknownFilterException' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Exception_UnknownHelperException' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Exception_UnknownTemplateException' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_HelperCollection' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_LambdaHelper' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Loader' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Loader_ArrayLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Loader_CascadingLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Loader_FilesystemLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Loader_InlineLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Loader_MutableLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Loader_ProductionFilesystemLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Loader_StringLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Logger' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Logger_AbstractLogger' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Logger_StreamLogger' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Parser' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Source' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Source_FilesystemSource' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Template' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Tokenizer' => $vendorDir . '/mustache/mustache/src/compat.php',
'Nette\\ArgumentOutOfRangeException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\DeprecatedException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\DirectoryNotFoundException' => $vendorDir . '/nette/utils/src/exceptions.php',
+30 -72
View File
@@ -18,11 +18,11 @@ class ComposerStaticInit071586d19f5409de22b3235d85d8476c
);
public static $prefixLengthsPsr4 = array (
'T' =>
'T' =>
array (
'Twig\\' => 5,
),
'S' =>
'S' =>
array (
'Symfony\\Polyfill\\Php80\\' => 23,
'Symfony\\Polyfill\\Mbstring\\' => 26,
@@ -31,18 +31,17 @@ class ComposerStaticInit071586d19f5409de22b3235d85d8476c
'SourceSpan\\' => 11,
'ScssPhp\\ScssPhp\\' => 16,
),
'P' =>
'P' =>
array (
'Psr\\Log\\' => 8,
'Psr\\Http\\Message\\' => 17,
'Psr\\EventDispatcher\\' => 20,
'PhpMqtt\\Client\\' => 15,
),
'N' =>
'N' =>
array (
'Nette\\' => 6,
),
'M' =>
'M' =>
array (
'MyCLabs\\Enum\\' => 13,
'Mustache\\' => 9,
@@ -50,119 +49,111 @@ class ComposerStaticInit071586d19f5409de22b3235d85d8476c
'MaxMind\\Exception\\' => 18,
'MaxMind\\Db\\' => 11,
),
'L' =>
'L' =>
array (
'League\\Uri\\' => 11,
'League\\Config\\' => 14,
'League\\CommonMark\\' => 18,
),
'G' =>
'G' =>
array (
'GeoIp2\\' => 7,
),
'D' =>
'D' =>
array (
'Dflydev\\DotAccessData\\' => 22,
),
'C' =>
'C' =>
array (
'Composer\\CaBundle\\' => 18,
),
);
public static $prefixDirsPsr4 = array (
'Twig\\' =>
'Twig\\' =>
array (
0 => __DIR__ . '/..' . '/twig/twig/src',
),
'Symfony\\Polyfill\\Php80\\' =>
'Symfony\\Polyfill\\Php80\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
),
'Symfony\\Polyfill\\Mbstring\\' =>
'Symfony\\Polyfill\\Mbstring\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
),
'Symfony\\Polyfill\\Ctype\\' =>
'Symfony\\Polyfill\\Ctype\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-ctype',
),
'Symfony\\Component\\Filesystem\\' =>
'Symfony\\Component\\Filesystem\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/filesystem',
),
'SourceSpan\\' =>
'SourceSpan\\' =>
array (
0 => __DIR__ . '/..' . '/scssphp/source-span/src',
),
'ScssPhp\\ScssPhp\\' =>
'ScssPhp\\ScssPhp\\' =>
array (
0 => __DIR__ . '/..' . '/scssphp/scssphp/src',
),
'Psr\\Log\\' =>
'Psr\\Log\\' =>
array (
0 => __DIR__ . '/..' . '/psr/log/src',
),
'Psr\\Http\\Message\\' =>
'Psr\\Http\\Message\\' =>
array (
0 => __DIR__ . '/..' . '/psr/http-factory/src',
1 => __DIR__ . '/..' . '/psr/http-message/src',
),
'Psr\\EventDispatcher\\' =>
'Psr\\EventDispatcher\\' =>
array (
0 => __DIR__ . '/..' . '/psr/event-dispatcher/src',
),
'PhpMqtt\\Client\\' =>
array (
0 => __DIR__ . '/..' . '/php-mqtt/client/src',
),
'Nette\\' =>
'Nette\\' =>
array (
0 => __DIR__ . '/..' . '/nette/schema/src',
1 => __DIR__ . '/..' . '/nette/utils/src',
),
'MyCLabs\\Enum\\' =>
'MyCLabs\\Enum\\' =>
array (
0 => __DIR__ . '/..' . '/myclabs/php-enum/src',
),
'Mustache\\' =>
array (
0 => __DIR__ . '/..' . '/mustache/mustache/src',
),
'MaxMind\\WebService\\' =>
'MaxMind\\WebService\\' =>
array (
0 => __DIR__ . '/..' . '/maxmind/web-service-common/src/WebService',
),
'MaxMind\\Exception\\' =>
'MaxMind\\Exception\\' =>
array (
0 => __DIR__ . '/..' . '/maxmind/web-service-common/src/Exception',
),
'MaxMind\\Db\\' =>
'MaxMind\\Db\\' =>
array (
0 => __DIR__ . '/..' . '/maxmind-db/reader/src/MaxMind/Db',
),
'League\\Uri\\' =>
'League\\Uri\\' =>
array (
0 => __DIR__ . '/..' . '/league/uri',
1 => __DIR__ . '/..' . '/league/uri-interfaces',
),
'League\\Config\\' =>
'League\\Config\\' =>
array (
0 => __DIR__ . '/..' . '/league/config/src',
),
'League\\CommonMark\\' =>
'League\\CommonMark\\' =>
array (
0 => __DIR__ . '/..' . '/league/commonmark/src',
),
'GeoIp2\\' =>
'GeoIp2\\' =>
array (
0 => __DIR__ . '/..' . '/geoip2/geoip2/src',
),
'Dflydev\\DotAccessData\\' =>
'Dflydev\\DotAccessData\\' =>
array (
0 => __DIR__ . '/..' . '/dflydev/dot-access-data/src',
),
'Composer\\CaBundle\\' =>
'Composer\\CaBundle\\' =>
array (
0 => __DIR__ . '/..' . '/composer/ca-bundle/src',
),
@@ -171,39 +162,6 @@ class ComposerStaticInit071586d19f5409de22b3235d85d8476c
public static $classMap = array (
'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'Mustache_Cache' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Cache_AbstractCache' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Cache_FilesystemCache' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Cache_NoopCache' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Compiler' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Context' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Engine' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Exception' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Exception_InvalidArgumentException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Exception_LogicException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Exception_RuntimeException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Exception_SyntaxException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Exception_UnknownFilterException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Exception_UnknownHelperException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Exception_UnknownTemplateException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_HelperCollection' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_LambdaHelper' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Loader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Loader_ArrayLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Loader_CascadingLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Loader_FilesystemLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Loader_InlineLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Loader_MutableLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Loader_ProductionFilesystemLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Loader_StringLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Logger' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Logger_AbstractLogger' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Logger_StreamLogger' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Parser' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Source' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Source_FilesystemSource' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Template' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Tokenizer' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Nette\\ArgumentOutOfRangeException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\DeprecatedException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\DirectoryNotFoundException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
+5 -121
View File
@@ -1,5 +1,5 @@
{
"packages": [
[
[
{
"name": "composer/ca-bundle",
"version": "1.5.13",
@@ -718,62 +718,6 @@
},
"install-path": "../maxmind/web-service-common"
},
{
"name": "mustache/mustache",
"version": "v3.0.0",
"version_normalized": "3.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/bobthecow/mustache.php.git",
"reference": "176b6b21d68516dd5107a63ab71b0050e518b7a4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bobthecow/mustache.php/zipball/176b6b21d68516dd5107a63ab71b0050e518b7a4",
"reference": "176b6b21d68516dd5107a63ab71b0050e518b7a4",
"shasum": ""
},
"require": {
"php": ">=5.6"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.19.3",
"yoast/phpunit-polyfills": "^2.0"
},
"time": "2025-06-28T18:28:20+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Mustache\\": "src/"
},
"classmap": [
"src/compat.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Justin Hileman",
"email": "justin@justinhileman.info",
"homepage": "http://justinhileman.com"
}
],
"description": "A Mustache implementation in PHP.",
"homepage": "https://github.com/bobthecow/mustache.php",
"keywords": [
"mustache",
"templating"
],
"support": {
"issues": "https://github.com/bobthecow/mustache.php/issues",
"source": "https://github.com/bobthecow/mustache.php/tree/v3.0.0"
},
"install-path": "../mustache/mustache"
},
{
"name": "myclabs/php-enum",
"version": "1.8.5",
@@ -1000,66 +944,6 @@
},
"install-path": "../nette/utils"
},
{
"name": "php-mqtt/client",
"version": "v2.3.0",
"version_normalized": "2.3.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-mqtt/client.git",
"reference": "3d141846753a0adee265680ae073cfb9030f2390"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-mqtt/client/zipball/3d141846753a0adee265680ae073cfb9030f2390",
"reference": "3d141846753a0adee265680ae073cfb9030f2390",
"shasum": ""
},
"require": {
"myclabs/php-enum": "^1.7",
"php": "^8.0",
"psr/log": "^1.1|^2.0|^3.0"
},
"require-dev": {
"phpunit/php-invoker": "^3.0",
"phpunit/phpunit": "^9.0",
"squizlabs/php_codesniffer": "^3.5"
},
"suggest": {
"ext-redis": "Required for the RedisRepository"
},
"time": "2025-09-30T17:53:34+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"PhpMqtt\\Client\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Marvin Mall",
"email": "marvin-mall@msn.com",
"role": "developer"
}
],
"description": "An MQTT client written in and for PHP.",
"keywords": [
"client",
"mqtt",
"publish",
"subscribe"
],
"support": {
"issues": "https://github.com/php-mqtt/client/issues",
"source": "https://github.com/php-mqtt/client/tree/v2.3.0"
},
"install-path": "../php-mqtt/client"
},
{
"name": "psr/event-dispatcher",
"version": "1.0.0",
@@ -1906,6 +1790,6 @@
"install-path": "../twig/twig"
}
],
"dev": true,
"dev-package-names": []
}
true,
[]
]
+6 -24
View File
@@ -1,9 +1,9 @@
<?php return array(
'root' => array(
'name' => '__root__',
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => 'd453b8073f07afc24f2a03f94a982977263aa107',
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'reference' => null,
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -11,9 +11,9 @@
),
'versions' => array(
'__root__' => array(
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => 'd453b8073f07afc24f2a03f94a982977263aa107',
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'reference' => null,
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -100,15 +100,6 @@
'aliases' => array(),
'dev_requirement' => false,
),
'mustache/mustache' => array(
'pretty_version' => 'v3.0.0',
'version' => '3.0.0.0',
'reference' => '176b6b21d68516dd5107a63ab71b0050e518b7a4',
'type' => 'library',
'install_path' => __DIR__ . '/../mustache/mustache',
'aliases' => array(),
'dev_requirement' => false,
),
'myclabs/php-enum' => array(
'pretty_version' => '1.8.5',
'version' => '1.8.5.0',
@@ -136,15 +127,6 @@
'aliases' => array(),
'dev_requirement' => false,
),
'php-mqtt/client' => array(
'pretty_version' => 'v2.3.0',
'version' => '2.3.0.0',
'reference' => '3d141846753a0adee265680ae073cfb9030f2390',
'type' => 'library',
'install_path' => __DIR__ . '/../php-mqtt/client',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/event-dispatcher' => array(
'pretty_version' => '1.0.0',
'version' => '1.0.0.0',
+3 -3
View File
@@ -4,9 +4,9 @@
*/
return [
'version' => '2.6.0',
'release_date' => '2026-08-15',
'codename' => 'Atlas',
'version' => '2.6.1',
'release_date' => '2026-08-17',
'codename' => 'Lyra',
'status' => 'stable',
'system_requirements' => [