v2.5.1: Admin theme refactor, Navigation plugin, user roles, guide restructure

- Reorganize admin into admin/theme/default/ (views + assets)
- Rename GuideNav to Navigation plugin (essential, protected)
- Plugin assets support (SCSS/CSS) loaded after theme CSS
- User roles: Admin, Content Manager, BI Manager, Site Admin
- Role-based access control (RBAC) for admin routes and sidebar
- Guide restructure: sub-topics in separate folders with sidebar nav
- Dynamic breadcrumb for homepage and subdirectories
- Fix theme path traversal (../../ -> ../) in admin.php
- Fix CodeMirror mode load order (xml -> css -> js -> htmlmixed -> php)
- Fix editor-toolbar.js null checks for plugin edit pages
- Layout select from theme.json with live frontmatter update
- Footer sticky at bottom of viewport (min-height: 100vh)
- Breadcrumb color fix (var(--nav-font) -> var(--header-bg))
- Remove language switcher from guide pages
- Update README.md and README.en.md
- Bump version to 2.5.1
This commit is contained in:
2026-08-10 15:36:29 +02:00
parent 0961b23b8d
commit cd498c8c3a
209 changed files with 11033 additions and 9125 deletions
+110 -20
View File
@@ -658,14 +658,16 @@ class CodePressCMS {
'allow_unsafe_links' => false,
'max_nesting_level' => 100,
'heading_permalink' => [
'symbol' => '',
'aria_hidden' => true,
'html_class' => 'heading-permalink',
'id_prefix' => '',
'fragment_prefix' => '',
'apply_id_to_heading' => true,
'insert' => 'after',
'min_heading_level' => 1,
'max_heading_level' => 6,
'title' => 'Permalink',
'symbol' => '',
'aria_hidden' => true,
],
'external_link' => [
'internal_hosts' => $this->getInternalHosts(),
@@ -987,20 +989,64 @@ class CodePressCMS {
*/
private function getGuidePage() {
$lang = $this->currentLanguage;
$guideFile = __DIR__ . '/../../../guide/' . $lang . '.codepress.md';
$pagePath = $_GET['page'] ?? '';
// Special case: /nl/guide sets page='guide', but we want index
if ($pagePath === 'guide') {
$pagePath = '';
}
$rootDir = dirname(__DIR__, 3);
$guideDir = $rootDir . '/guide/' . $lang;
// Get guide file
if (empty($pagePath)) {
$guideFile = $guideDir . '/index.md';
} else {
$pagePath = trim($pagePath, '/');
$guideFile = $guideDir . '/' . $pagePath . '.md';
}
// Fallback to English
if (!file_exists($guideFile) && $lang !== 'en') {
$guideFile = $rootDir . '/guide/en/' . (empty($pagePath) ? 'index.md' : $pagePath . '.md');
}
// Handle 404
if (!file_exists($guideFile)) {
$guideFile = __DIR__ . '/../../../guide/en.codepress.md'; // Fallback to English
http_response_code(404);
return [
'title' => 'Handleiding niet gevonden',
'content' => '<p>De gevraagde handleiding is niet gevonden.</p>',
'layout' => 'content',
'guide_breadcrumbs' => [],
'guide_lang' => $lang,
'guide_page' => $pagePath,
];
}
$content = file_get_contents($guideFile);
// Reuse parseMarkdown to avoid duplicating CommonMark setup
$result = $this->parseMarkdown($content, $guideFile);
// Override title for guide
$result['title'] = $this->t('manual') . ' - CodePress CMS';
$result['layout'] = $result['metadata']['layout'] ?? 'content';
// Build breadcrumbs
$guideBase = '/' . $lang . '/guide';
$breadcrumbs = [['title' => $this->t('manual'), 'url' => $guideBase]];
if (!empty($pagePath)) {
$parts = explode('/', $pagePath);
$buildPath = '';
foreach ($parts as $part) {
$buildPath .= ($buildPath ? '/' : '') . $part;
$breadcrumbs[] = [
'title' => str_replace('-', ' ', ucfirst($part)),
'url' => $guideBase . '?page=' . $buildPath,
];
}
}
$result['title'] = ($result['metadata']['title'] ?? $this->t('manual')) . ' - CodePress CMS';
$result['layout'] = 'guide';
$result['metadata']['plugins'] = 'Navigation';
$result['guide_breadcrumbs'] = $breadcrumbs;
$result['guide_lang'] = $lang;
$result['guide_page'] = $pagePath;
return $result;
}
@@ -1148,8 +1194,9 @@ class CodePressCMS {
$layout = $page['layout'] ?? 'sidebar-content';
// Determine if sidebar toggle should be shown
$isGuidePage = isset($_GET['guide']) || ($page['layout'] ?? '') === 'guide';
$hasSidebar = $layout !== 'content' && !empty(trim($sidebarContent));
$breadcrumb = $this->generateBreadcrumb($hasSidebar);
$breadcrumb = $this->generateBreadcrumb($hasSidebar, $isGuidePage ? $page : null);
// Prepare template data
$templateData = [
@@ -1247,6 +1294,13 @@ class CodePressCMS {
// Don't show site title link on guide page
$templateData['show_site_link'] = !$this->isContentDirEmpty() && !isset($_GET['guide']);
// Pass guide-specific data to template
if (isset($page['guide_breadcrumbs'])) {
$templateData['guide_breadcrumbs'] = $page['guide_breadcrumbs'];
$templateData['guide_lang'] = $page['guide_lang'] ?? $this->currentLanguage;
$templateData['guide_page'] = $page['guide_page'] ?? '';
}
// Map legacy frontmatter layout values to theme template keys
$layoutKey = $this->mapLayoutToThemeKey($layout);
@@ -1256,6 +1310,13 @@ class CodePressCMS {
$templateData['theme_css_url'] = $themeManager->getCssUrl();
$templateData['theme_js_url'] = $themeManager->getJsUrl();
$templateData['theme_config'] = $themeManager->getConfig();
$templateData['theme_base_url'] = '/themes/' . basename($themeManager->getThemeDir());
$templateData['theme_css_files'] = $themeManager->getCssFiles();
$templateData['theme_js_files'] = $themeManager->getJsFiles();
$templateData['theme_favicon'] = $themeManager->getFaviconUrl();
// Plugin CSS (loaded after theme CSS so theme can override)
$templateData['plugin_css_urls'] = $this->pluginManager->getPluginCssUrls();
// Render the page through the active theme
$renderedLayout = $themeManager->render($layoutKey, $templateData);
@@ -1269,15 +1330,46 @@ class CodePressCMS {
* Generate breadcrumb navigation HTML
*
* @param bool $hasSidebar Whether sidebar content exists and should show toggle
* @param array|null $guidePage Guide page data (when on a guide page)
* @return string Breadcrumb HTML
*/
public function generateBreadcrumb($hasSidebar = true) {
public function generateBreadcrumb($hasSidebar = true, $guidePage = null) {
// Sidebar toggle button (shown before home icon in breadcrumb)
$sidebarToggle = '';
if ($hasSidebar) {
$sidebarToggle = '<li class="breadcrumb-item sidebar-toggle-item"><button type="button" class="sidebar-toggle-btn" onclick="toggleSidebar()" title="Toggle Sidebar" aria-label="Toggle Sidebar" aria-expanded="true"><i class="bi bi-layout-sidebar-inset"></i></button></li>';
}
// Guide page breadcrumb: Home > Handleiding > [page parts]
if ($guidePage !== null) {
$breadcrumb = '<nav aria-label="breadcrumb"><ol class="breadcrumb">';
$breadcrumb .= $sidebarToggle;
$breadcrumb .= '<li class="breadcrumb-item"><a href="/' . $this->currentLanguage . '"><i class="bi bi-house"></i></a></li>';
$breadcrumb .= '<li class="breadcrumb-item"> > </li>';
$guidePagePath = $guidePage['guide_page'] ?? '';
$guideLang = $guidePage['guide_lang'] ?? $this->currentLanguage;
$guideBase = '/' . $guideLang . '/guide';
if (empty($guidePagePath)) {
$breadcrumb .= '<li class="breadcrumb-item active">' . htmlspecialchars($this->t('manual'), ENT_QUOTES, 'UTF-8') . '</li>';
} else {
$breadcrumb .= '<li class="breadcrumb-item"><a href="' . htmlspecialchars($guideBase, ENT_QUOTES, 'UTF-8') . '">' . htmlspecialchars($this->t('manual'), ENT_QUOTES, 'UTF-8') . '</a></li>';
$parts = explode('/', $guidePagePath);
$buildPath = '';
foreach ($parts as $i => $part) {
$buildPath .= ($buildPath ? '/' : '') . $part;
$title = htmlspecialchars(str_replace('-', ' ', ucfirst($part)), ENT_QUOTES, 'UTF-8');
$url = htmlspecialchars($guideBase . '?page=' . $buildPath, ENT_QUOTES, 'UTF-8');
if ($i === count($parts) - 1) {
$breadcrumb .= '<li class="breadcrumb-item"> > </li><li class="breadcrumb-item active">' . $title . '</li>';
} else {
$breadcrumb .= '<li class="breadcrumb-item"> > </li><li class="breadcrumb-item"><a href="' . $url . '">' . $title . '</a></li>';
}
}
}
$breadcrumb .= '</ol></nav>';
return $breadcrumb;
}
if (isset($_GET['search'])) {
return '<nav aria-label="breadcrumb"><ol class="breadcrumb">' . $sidebarToggle . '<li class="breadcrumb-item"><a href="/' . $this->currentLanguage . '"><i class="bi bi-house"></i></a></li><li class="breadcrumb-item"> > </li><li class="breadcrumb-item active">' . $this->t('search') . '</li></ol></nav>';
}
@@ -1286,18 +1378,16 @@ class CodePressCMS {
$page = htmlspecialchars($page, ENT_QUOTES, 'UTF-8');
$page = preg_replace('/\.[^.]+$/', '', $page);
// Convert page to clean URL format for breadcrumb
if ($page === $this->getEffectiveDefaultPage()) {
return '<nav aria-label="breadcrumb"><ol class="breadcrumb">' . $sidebarToggle . '<li class="breadcrumb-item active"><i class="bi bi-house"></i></li></ol></nav>';
}
$isHomepage = ($page === $this->getEffectiveDefaultPage());
$homeUrl = '/' . $this->currentLanguage;
$breadcrumb = '<nav aria-label="breadcrumb"><ol class="breadcrumb">';
// Start with sidebar toggle, then home icon linking to default page (root)
$breadcrumb .= $sidebarToggle;
$breadcrumb .= '<li class="breadcrumb-item"><a href="/' . $this->currentLanguage . '"><i class="bi bi-house"></i></a></li>';
// Split page path and build breadcrumb items
// Home icon: clickable link to language root (always, unless we're on the root itself)
$breadcrumb .= '<li class="breadcrumb-item"><a href="' . $homeUrl . '"><i class="bi bi-house"></i></a></li>';
// Split page path and build breadcrumb items (handles subdirectories dynamically)
$parts = explode('/', $page);
$currentPath = '';
@@ -1579,4 +1669,4 @@ class CodePressCMS {
{
return str_replace('-/assets/', '/-assets/', $content);
}
}
}// Guide fix: 1786349431