CMS 2.0 - Theme engine, logging, admin improvements
Major changes: - New ThemeManager with Twig templating and SCSS compilation - Dynamic themes system (themes/default, themes/demo) - LogManager with SQLite storage and syslog forwarding - RequestLogger with static helper methods - Admin UI overhaul (Bootstrap 5, dark mode) - Admin config page with logging and theme settings - Admin logs page with filters and search - Removed legacy Mustache templates - Removed test plugin and theme - Composer dependencies: Twig, scssphp, CommonMark, MaxMind GeoIP
This commit is contained in:
@@ -591,7 +591,7 @@ class CodePressCMS {
|
||||
}
|
||||
}
|
||||
|
||||
$authorWebsite = $this->config['author']['website'] ?? '';
|
||||
$authorWebsite = $this->normalizeUrl($this->config['author']['website'] ?? '');
|
||||
if ($authorWebsite !== '') {
|
||||
$authorHost = parse_url($authorWebsite, PHP_URL_HOST);
|
||||
if ($authorHost) {
|
||||
@@ -602,6 +602,25 @@ class CodePressCMS {
|
||||
return array_values(array_unique(array_filter($hosts)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a URL: if no scheme is present, prepend https://.
|
||||
* Handles hostnames stored without protocol (e.g. "noorlander.info").
|
||||
*
|
||||
* @param string $url Raw URL or hostname
|
||||
* @return string Normalized absolute URL, or '' if empty
|
||||
*/
|
||||
private function normalizeUrl(string $url): string
|
||||
{
|
||||
$url = trim($url);
|
||||
if ($url === '') {
|
||||
return '';
|
||||
}
|
||||
if (!preg_match('#^[a-zA-Z][a-zA-Z0-9+.\-]*://#', $url)) {
|
||||
return 'https://' . $url;
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Markdown content to HTML using League CommonMark
|
||||
*
|
||||
@@ -1140,7 +1159,7 @@ class CodePressCMS {
|
||||
'is_guide_page' => isset($_GET['guide']),
|
||||
'lang_switch_url' => '',
|
||||
'author_name' => $this->config['author']['name'] ?? 'CodePress Developer',
|
||||
'author_website' => $this->config['author']['website'] ?? '#',
|
||||
'author_website' => $this->normalizeUrl($this->config['author']['website'] ?? '') ?: '#',
|
||||
'author_git' => 'https://git.noorlander.info/E.Noorlander',
|
||||
'seo_description' => $this->config['seo']['description'] ?? 'CodePress CMS - Lightweight file-based content management system',
|
||||
'seo_keywords' => $this->config['seo']['keywords'] ?? 'cms, php, content management, file-based',
|
||||
@@ -1156,8 +1175,6 @@ class CodePressCMS {
|
||||
'nav_height' => $this->config['theme']['nav_height'] ?? '42',
|
||||
'sidebar_background' => $this->config['theme']['sidebar_background'] ?? '#f8f9fa',
|
||||
'sidebar_border' => $this->config['theme']['sidebar_border'] ?? '#dee2e6',
|
||||
'background_image_css' => $this->getBackgroundImageCss(),
|
||||
'background_image_opacity' => $this->getBackgroundImageOpacity(),
|
||||
// Language
|
||||
'current_lang' => $this->currentLanguage,
|
||||
'current_lang_upper' => strtoupper($this->currentLanguage),
|
||||
@@ -1216,36 +1233,21 @@ class CodePressCMS {
|
||||
// Don't show site title link on guide page
|
||||
$templateData['show_site_link'] = !$this->isContentDirEmpty() && !isset($_GET['guide']);
|
||||
|
||||
// Load and render all templates with data
|
||||
$layoutTemplate = file_get_contents($this->config['templates_dir'] . '/layout.mustache');
|
||||
$headerTemplate = file_get_contents($this->config['templates_dir'] . '/assets/header.mustache');
|
||||
$navigationTemplate = file_get_contents($this->config['templates_dir'] . '/assets/navigation.mustache');
|
||||
$footerTemplate = file_get_contents($this->config['templates_dir'] . '/assets/footer.mustache');
|
||||
|
||||
// Determine content type and load appropriate template
|
||||
$contentType = $this->getContentType($page);
|
||||
$contentTemplateFile = $this->config['templates_dir'] . '/' . $contentType . '_content.mustache';
|
||||
$contentTemplate = file_exists($contentTemplateFile) ? file_get_contents($contentTemplateFile) : '<div class="content">{{{content}}}</div>';
|
||||
|
||||
// Map legacy frontmatter layout values to theme template keys
|
||||
$layoutKey = $this->mapLayoutToThemeKey($layout);
|
||||
|
||||
// Add theme asset URLs to template data
|
||||
$themeManager = new ThemeManager($this->config);
|
||||
$templateData['theme_title'] = $themeManager->getTitle();
|
||||
$templateData['theme_css_url'] = $themeManager->getCssUrl();
|
||||
$templateData['theme_js_url'] = $themeManager->getJsUrl();
|
||||
$templateData['theme_config'] = $themeManager->getConfig();
|
||||
|
||||
// Render the page through the active theme
|
||||
$renderedLayout = $themeManager->render($layoutKey, $templateData);
|
||||
|
||||
|
||||
// Render all templates with data
|
||||
$renderedHeader = SimpleTemplate::render($headerTemplate, $templateData);
|
||||
$renderedNavigation = SimpleTemplate::render($navigationTemplate, $templateData);
|
||||
$renderedFooter = SimpleTemplate::render($footerTemplate, $templateData);
|
||||
$renderedContent = SimpleTemplate::render($contentTemplate, $templateData);
|
||||
|
||||
// Replace partials in layout
|
||||
$finalTemplate = str_replace('{{>header}}', $renderedHeader, $layoutTemplate);
|
||||
$finalTemplate = str_replace('{{>navigation}}', $renderedNavigation, $finalTemplate);
|
||||
$finalTemplate = str_replace('{{>footer}}', $renderedFooter, $finalTemplate);
|
||||
$finalTemplate = str_replace('{{>content_template}}', $renderedContent, $finalTemplate);
|
||||
|
||||
// Render the final layout with all template data
|
||||
$renderedLayout = SimpleTemplate::render($finalTemplate, $templateData);
|
||||
|
||||
echo $renderedLayout;
|
||||
|
||||
|
||||
$this->pluginManager->doAction('onAfterRender', $renderedLayout);
|
||||
}
|
||||
|
||||
@@ -1371,6 +1373,25 @@ class CodePressCMS {
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a frontmatter layout value to a theme template key.
|
||||
*
|
||||
* Legacy values are translated to the new theme keys. Unknown values
|
||||
* are passed through so ThemeManager can fall back to default_layout.
|
||||
*
|
||||
* @param string $layout Layout value from page metadata
|
||||
* @return string Theme template key
|
||||
*/
|
||||
private function mapLayoutToThemeKey(string $layout): string {
|
||||
return match ($layout) {
|
||||
'content' => 'full_content',
|
||||
'sidebar-content', 'content-sidebar' => 'left_sidebar',
|
||||
'content-sidebar-reverse' => 'right_sidebar',
|
||||
'sidebar' => 'custom1',
|
||||
default => $layout,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine content type for current page
|
||||
*
|
||||
@@ -1540,24 +1561,6 @@ class CodePressCMS {
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getBackgroundImageCss(): string
|
||||
{
|
||||
$bg = $this->config['theme']['background_image'] ?? '';
|
||||
if (empty($bg)) {
|
||||
return 'none';
|
||||
}
|
||||
if (str_starts_with($bg, 'http')) {
|
||||
return 'url(' . $bg . ')';
|
||||
}
|
||||
return 'url(/themes/' . $bg . ')';
|
||||
}
|
||||
|
||||
private function getBackgroundImageOpacity(): int
|
||||
{
|
||||
$opacity = intval($this->config['theme']['background_image_opacity'] ?? 100);
|
||||
return max(0, min(100, $opacity));
|
||||
}
|
||||
|
||||
private function processContent(string $content): string
|
||||
{
|
||||
return str_replace('-/assets/', '/-assets/', $content);
|
||||
|
||||
Reference in New Issue
Block a user