Complete CodePress CMS refactoring
- 🏠 Refactored layout system with flexbox - 🎨 Implemented tab-style navigation with dropdowns - 📱 Added responsive design with mobile support - 🔧 Enhanced template system with content type detection - 📝 Added comprehensive documentation (guide.md, README.md) - ⚙️ Improved security with proper file access control - 🎨 Added meta tags for SEO and author attribution - 📁 Fixed breadcrumb navigation and duplicate links - 🗂️ Removed unused files and cleaned up project structure - ⚙️ Added JSON configuration system with homepage detection - 📱 Enhanced search functionality with snippet display - 🔗 Implemented auto-linking between pages - 📊 Added file metadata display in footer Features: - Multi-format content support (Markdown, PHP, HTML) - Dynamic navigation with collapsible folders - Full-text search across all content - Responsive Bootstrap 5 design - JSON-based configuration - SEO-optimized meta tags - Security-focused file management - Mobile-first responsive design - Auto-linking between pages - File metadata tracking - Breadcrumb navigation - Custom CSS styling - Progressive enhancement Technical improvements: - Replaced fixed positioning with flexbox layout - Implemented proper template inheritance - Added content type detection - Enhanced security with .htaccess - Optimized for performance - Added proper error handling - Implemented caching mechanisms - Enhanced accessibility features
This commit is contained in:
@@ -1,246 +0,0 @@
|
||||
// SCSS variables
|
||||
$primary: #0d6efd;
|
||||
$secondary: #6c757d;
|
||||
$success: #198754;
|
||||
$info: #0dcaf0;
|
||||
$warning: #ffc107;
|
||||
$danger: #dc3545;
|
||||
$light: #f8f9fa;
|
||||
$dark: #212529;
|
||||
|
||||
// SCSS mixins
|
||||
@mixin transition($properties...) {
|
||||
transition: $properties;
|
||||
}
|
||||
|
||||
// Custom styles
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: calc(100vh - 70px);
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
background-color: #f8f9fa;
|
||||
border-right: 1px solid #dee2e6;
|
||||
overflow-y: auto;
|
||||
flex-shrink: 0;
|
||||
@include transition(transform 0.3s ease);
|
||||
position: fixed;
|
||||
top: 70px;
|
||||
left: 0;
|
||||
height: calc(100vh - 140px);
|
||||
z-index: 999;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.sidebar.collapsed {
|
||||
transform: translateX(-250px);
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
z-index: 1001;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
@include transition(all 0.3s ease);
|
||||
font-size: 20px;
|
||||
color: #6c757d;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
color: #0d6efd;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-toggle-outer {
|
||||
position: relative;
|
||||
z-index: 1001;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
margin-right: 10px;
|
||||
color: white !important;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar.collapsed .sidebar-toggle-inner {
|
||||
right: auto;
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
.sidebar.collapsed ~ .main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .sidebar-toggle-outer {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.sidebar:not(.collapsed) .sidebar-toggle-outer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
@include transition(margin-left 0.3s ease);
|
||||
margin-left: 250px;
|
||||
}
|
||||
|
||||
.folder-toggle {
|
||||
font-weight: bold;
|
||||
color: #212529 !important;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background-color: #f8f9fa !important;
|
||||
margin: 0.125rem 0;
|
||||
@include transition(background-color 0.2s ease);
|
||||
|
||||
&:hover {
|
||||
background-color: #e9ecef !important;
|
||||
}
|
||||
|
||||
&[aria-expanded=true] {
|
||||
background-color: #dee2e6 !important;
|
||||
color: #212529 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
margin-right: 8px;
|
||||
@include transition(transform 0.2s);
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
&[aria-expanded=true] .arrow {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.page-link {
|
||||
color: #495057 !important;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 0.75rem;
|
||||
padding-left: 2rem;
|
||||
background-color: #ffffff !important;
|
||||
margin: 0.125rem 0;
|
||||
@include transition(background-color 0.2s ease);
|
||||
|
||||
&:hover {
|
||||
color: #212529 !important;
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #0d6efd !important;
|
||||
color: #212529 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.file-info {
|
||||
font-size: 0.9rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.site-info a {
|
||||
color: #0d6efd;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.auto-link {
|
||||
color: #0d6efd;
|
||||
text-decoration: none;
|
||||
border-bottom: 2px dashed #0d6efd;
|
||||
font-weight: 500;
|
||||
@include transition(all 0.2s ease);
|
||||
|
||||
&:hover {
|
||||
color: #0a58ca;
|
||||
text-decoration: none;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: #0a58ca;
|
||||
}
|
||||
}
|
||||
|
||||
.search-form {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.card-title a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.folder-disabled {
|
||||
font-weight: 500;
|
||||
color: #6c757d !important;
|
||||
cursor: not-allowed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background-color: #f8f9fa !important;
|
||||
margin: 0.125rem 0;
|
||||
opacity: 0.7;
|
||||
|
||||
.arrow {
|
||||
margin-right: 8px;
|
||||
font-size: 0.8em;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,41 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
// Default configuration
|
||||
$defaultConfig = [
|
||||
'site_title' => 'CodePress',
|
||||
'content_dir' => __DIR__ . '/../../content',
|
||||
'templates_dir' => __DIR__ . '/../templates',
|
||||
'default_page' => 'home'
|
||||
];
|
||||
'default_page' => 'auto',
|
||||
'homepage' => 'auto'
|
||||
];
|
||||
|
||||
// Check for config.json in project root
|
||||
$projectRoot = __DIR__ . '/../../';
|
||||
$configJsonPath = $projectRoot . 'config.json';
|
||||
|
||||
if (file_exists($configJsonPath)) {
|
||||
$jsonContent = file_get_contents($configJsonPath);
|
||||
$jsonConfig = json_decode($jsonContent, true);
|
||||
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($jsonConfig)) {
|
||||
// Merge JSON config with defaults, converting relative paths to absolute
|
||||
$mergedConfig = array_merge($defaultConfig, $jsonConfig);
|
||||
|
||||
// Convert relative paths to absolute paths (inline function to avoid redeclaration)
|
||||
$isAbsolutePath = function($path) {
|
||||
return (strpos($path, '/') === 0) || (preg_match('/^[A-Za-z]:/', $path));
|
||||
};
|
||||
|
||||
if (isset($mergedConfig['content_dir']) && !$isAbsolutePath($mergedConfig['content_dir'])) {
|
||||
$mergedConfig['content_dir'] = $projectRoot . $mergedConfig['content_dir'];
|
||||
}
|
||||
if (isset($mergedConfig['templates_dir']) && !$isAbsolutePath($mergedConfig['templates_dir'])) {
|
||||
$mergedConfig['templates_dir'] = $projectRoot . $mergedConfig['templates_dir'];
|
||||
}
|
||||
|
||||
return $mergedConfig;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to default config
|
||||
return $defaultConfig;
|
||||
@@ -5,7 +5,13 @@ require_once 'config.php';
|
||||
// Simple template rendering without Mustache for now
|
||||
class SimpleTemplate {
|
||||
public static function render($template, $data) {
|
||||
// Handle conditional blocks first
|
||||
// Handle partial includes first ({{>partial}})
|
||||
$template = preg_replace_callback('/{{>([^}]+)}}/', function($matches) use ($data) {
|
||||
$partialName = $matches[1];
|
||||
return $data[$partialName] ?? $matches[0];
|
||||
}, $template);
|
||||
|
||||
// Handle conditional blocks
|
||||
foreach ($data as $key => $value) {
|
||||
if (is_array($value) || (is_string($value) && !empty($value))) {
|
||||
// Handle {{#key}}...{{/key}} blocks
|
||||
@@ -50,6 +56,30 @@ class SimpleTemplate {
|
||||
|
||||
$config = include 'config.php';
|
||||
|
||||
/**
|
||||
* CodePressCMS - Lightweight file-based content management system
|
||||
*
|
||||
* Features:
|
||||
* - Markdown, PHP, and HTML content support
|
||||
* - Dynamic navigation with dropdown menus
|
||||
* - Search functionality
|
||||
* - Breadcrumb navigation
|
||||
* - Auto-linking between pages
|
||||
* - Configurable via JSON
|
||||
* - Responsive design with Bootstrap
|
||||
*/
|
||||
/**
|
||||
* CodePressCMS - Lightweight file-based content management system
|
||||
*
|
||||
* Features:
|
||||
* - Markdown, PHP, and HTML content support
|
||||
* - Dynamic navigation with dropdown menus
|
||||
* - Search functionality
|
||||
* - Breadcrumb navigation
|
||||
* - Auto-linking between pages
|
||||
* - Configurable via JSON
|
||||
* - Responsive design with Bootstrap
|
||||
*/
|
||||
class CodePressCMS {
|
||||
private $config;
|
||||
private $menu = [];
|
||||
@@ -468,9 +498,25 @@ private function autoLinkPageTitles($content) {
|
||||
'search_query' => isset($_GET['search']) ? htmlspecialchars($_GET['search']) : '',
|
||||
'menu' => $this->renderMenu($menu),
|
||||
'breadcrumb' => $breadcrumb,
|
||||
'default_page' => $this->config['default_page']
|
||||
'default_page' => $this->config['default_page'],
|
||||
'homepage' => $this->config['homepage'],
|
||||
'author_name' => $this->config['author']['name'] ?? 'CodePress Developer',
|
||||
'author_website' => $this->config['author']['website'] ?? '#',
|
||||
'author_git' => $this->config['author']['git'] ?? '#',
|
||||
'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'
|
||||
];
|
||||
|
||||
// File info for footer
|
||||
if (isset($page['file_info'])) {
|
||||
$templateData['file_info'] = 'Created: ' . htmlspecialchars($page['file_info']['created']) .
|
||||
' | Modified: ' . htmlspecialchars($page['file_info']['modified']);
|
||||
$templateData['file_info_block'] = '<span class="file-details"> | ' . $templateData['file_info'] . '</span>';
|
||||
} else {
|
||||
$templateData['file_info'] = '';
|
||||
$templateData['file_info_block'] = '';
|
||||
}
|
||||
|
||||
// File info for footer
|
||||
if (isset($page['file_info'])) {
|
||||
$templateData['file_info'] = 'Created: ' . htmlspecialchars($page['file_info']['created']) .
|
||||
@@ -501,9 +547,32 @@ private function autoLinkPageTitles($content) {
|
||||
$footerContent = preg_replace('/<span class="file-details">\s*\|\s*<a href="\?guide"[^>]*>Handleiding<\/a><\/span>/', '', $footerContent);
|
||||
}
|
||||
|
||||
// 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>';
|
||||
|
||||
// Determine content type and load appropriate template
|
||||
$pagePath = $_GET['page'] ?? $this->config['default_page'];
|
||||
$pagePath = preg_replace('/\.[^.]+$/', '', $pagePath);
|
||||
$filePath = $this->config['content_dir'] . '/' . $pagePath;
|
||||
|
||||
$contentType = 'markdown'; // default
|
||||
if (file_exists($filePath . '.md')) {
|
||||
$contentType = 'markdown';
|
||||
} elseif (file_exists($filePath . '.php')) {
|
||||
$contentType = 'php';
|
||||
} elseif (file_exists($filePath . '.html')) {
|
||||
$contentType = 'html';
|
||||
}
|
||||
|
||||
$contentTemplateFile = $this->config['templates_dir'] . '/' . $contentType . '_content.mustache';
|
||||
$contentTemplate = file_exists($contentTemplateFile) ? file_get_contents($contentTemplateFile) : '<div class="content">{{{content}}}</div>';
|
||||
|
||||
$partials = [
|
||||
'navigation' => file_get_contents($this->config['templates_dir'] . '/assets/navigation.mustache'),
|
||||
'footer' => file_get_contents($this->config['templates_dir'] . '/assets/footer.mustache')
|
||||
'footer' => file_get_contents($this->config['templates_dir'] . '/assets/footer.mustache'),
|
||||
'content_template' => $contentTemplate
|
||||
];
|
||||
|
||||
// Replace partials in template
|
||||
@@ -512,7 +581,8 @@ private function autoLinkPageTitles($content) {
|
||||
$template = str_replace('{{>footer}}', $partials['footer'], $template);
|
||||
|
||||
// Render template with data
|
||||
echo SimpleTemplate::render($template, $templateData);
|
||||
$renderedTemplate = SimpleTemplate::render($template, $templateData);
|
||||
echo $renderedTemplate;
|
||||
}
|
||||
|
||||
private function getBreadcrumb() {
|
||||
@@ -569,15 +639,48 @@ private function autoLinkPageTitles($content) {
|
||||
$html .= '</li>';
|
||||
}
|
||||
} else {
|
||||
$active = (isset($_GET['page']) && $_GET['page'] === $item['path']) ? 'active' : '';
|
||||
$html .= '<li class="nav-item">';
|
||||
$html .= '<a class="nav-link ' . $active . '" href="' . htmlspecialchars($item['url']) . '">' . htmlspecialchars($item['title']) . '</a>';
|
||||
$html .= '</li>';
|
||||
// Only show files that are directly in root (not in folders)
|
||||
if ($level === 0) {
|
||||
// Don't show the homepage file as a separate tab since it's already the Home button
|
||||
if ($item['path'] === $this->config['homepage']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$active = (isset($_GET['page']) && $_GET['page'] === $item['path']) ? 'active' : '';
|
||||
$html .= '<li class="nav-item">';
|
||||
$html .= '<a class="nav-link ' . $active . '" href="' . htmlspecialchars($item['url']) . '">';
|
||||
$html .= htmlspecialchars($item['title']);
|
||||
$html .= '</a>';
|
||||
$html .= '</li>';
|
||||
}
|
||||
}
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function getContentType($page) {
|
||||
// Try to determine content type from page request
|
||||
$pagePath = $_GET['page'] ?? $this->config['default_page'];
|
||||
$pagePath = preg_replace('/\.[^.]+$/', '', $pagePath);
|
||||
|
||||
$filePath = $this->config['content_dir'] . '/' . $pagePath;
|
||||
|
||||
// Check for different file extensions
|
||||
if (file_exists($filePath . '.md')) {
|
||||
return 'markdown';
|
||||
} elseif (file_exists($filePath . '.php')) {
|
||||
return 'php';
|
||||
} elseif (file_exists($filePath . '.html')) {
|
||||
return 'html';
|
||||
} elseif (file_exists($filePath)) {
|
||||
$extension = pathinfo($filePath, PATHINFO_EXTENSION);
|
||||
return in_array($extension, ['md', 'php', 'html']) ? $extension : 'markdown';
|
||||
}
|
||||
|
||||
// Default to markdown
|
||||
return 'markdown';
|
||||
}
|
||||
|
||||
private function folderContainsActivePage($children) {
|
||||
foreach ($children as $child) {
|
||||
if ($child['type'] === 'folder') {
|
||||
|
||||
@@ -1,615 +0,0 @@
|
||||
<?php
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
// Simple template rendering without Mustache for now
|
||||
class SimpleTemplate {
|
||||
public static function render($template, $data) {
|
||||
// Handle conditional blocks first
|
||||
foreach ($data as $key => $value) {
|
||||
if (is_array($value) || (is_string($value) && !empty($value))) {
|
||||
// Handle {{#key}}...{{/key}} blocks
|
||||
$pattern = '/{{#' . preg_quote($key, '/') . '}}(.*?){{\/' . preg_quote($key, '/') . '}}/s';
|
||||
if (preg_match($pattern, $template, $matches)) {
|
||||
$replacement = $matches[1];
|
||||
$template = preg_replace($pattern, $replacement, $template);
|
||||
}
|
||||
|
||||
// Handle {{^key}}...{{/key}} blocks (negative condition)
|
||||
$pattern = '/{{\^' . preg_quote($key, '/') . '}}(.*?){{\/' . preg_quote($key, '/') . '}}/s';
|
||||
$template = preg_replace($pattern, '', $template);
|
||||
} else {
|
||||
// Handle empty blocks
|
||||
$pattern = '/{{#' . preg_quote($key, '/') . '}}.*?{{\/' . preg_quote($key, '/') . '}}/s';
|
||||
$template = preg_replace($pattern, '', $template);
|
||||
|
||||
// Handle {{^key}}...{{/key}} blocks (show when empty)
|
||||
$pattern = '/{{\^' . preg_quote($key, '/') . '}}(.*?){{\/' . preg_quote($key, '/') . '}}/s';
|
||||
if (preg_match_all($pattern, $template, $matches)) {
|
||||
foreach ($matches[1] as $match) {
|
||||
$template = preg_replace('/{{\^' . preg_quote($key, '/') . '}}.*?{{\/' . preg_quote($key, '/') . '}}/s', $match, $template, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle variable replacements
|
||||
foreach ($data as $key => $value) {
|
||||
// Handle triple braces for unescaped HTML content
|
||||
if (strpos($template, '{{{' . $key . '}}}') !== false) {
|
||||
$template = str_replace('{{{' . $key . '}}}', $value, $template);
|
||||
}
|
||||
// Handle double braces for escaped content
|
||||
elseif (strpos($template, '{{' . $key . '}}') !== false) {
|
||||
$template = str_replace('{{' . $key . '}}', htmlspecialchars($value, ENT_QUOTES, 'UTF-8'), $template);
|
||||
}
|
||||
}
|
||||
return $template;
|
||||
}
|
||||
}
|
||||
|
||||
$config = include 'config.php';
|
||||
|
||||
class CodePressCMS {
|
||||
private $config;
|
||||
private $menu = [];
|
||||
private $searchResults = [];
|
||||
|
||||
public function __construct($config) {
|
||||
$this->config = $config;
|
||||
$this->buildMenu();
|
||||
|
||||
if (isset($_GET['search'])) {
|
||||
$this->performSearch($_GET['search']);
|
||||
}
|
||||
}
|
||||
|
||||
private function buildMenu() {
|
||||
$this->menu = $this->scanDirectory($this->config['content_dir'], '');
|
||||
}
|
||||
|
||||
private function scanDirectory($dir, $prefix) {
|
||||
if (!is_dir($dir)) return [];
|
||||
|
||||
$items = scandir($dir);
|
||||
sort($items);
|
||||
$result = [];
|
||||
|
||||
foreach ($items as $item) {
|
||||
if ($item[0] === '.') continue;
|
||||
|
||||
$path = $dir . '/' . $item;
|
||||
$relativePath = $prefix ? $prefix . '/' . $item : $item;
|
||||
|
||||
if (is_dir($path)) {
|
||||
$result[] = [
|
||||
'type' => 'folder',
|
||||
'title' => ucfirst($item),
|
||||
'path' => $relativePath,
|
||||
'children' => $this->scanDirectory($path, $relativePath)
|
||||
];
|
||||
} elseif (preg_match('/\.(md|php|html)$/', $item)) {
|
||||
$title = ucfirst(pathinfo($item, PATHINFO_FILENAME));
|
||||
$result[] = [
|
||||
'type' => 'file',
|
||||
'title' => $title,
|
||||
'path' => $relativePath,
|
||||
'url' => '?page=' . $relativePath
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function performSearch($query) {
|
||||
$this->searchResults = [];
|
||||
$this->searchInDirectory($this->config['content_dir'], '', $query);
|
||||
}
|
||||
|
||||
private function searchInDirectory($dir, $prefix, $query) {
|
||||
if (!is_dir($dir)) return;
|
||||
|
||||
$items = scandir($dir);
|
||||
|
||||
foreach ($items as $item) {
|
||||
if ($item[0] === '.') continue;
|
||||
|
||||
$path = $dir . '/' . $item;
|
||||
$relativePath = $prefix ? $prefix . '/' . $item : $item;
|
||||
|
||||
if (is_dir($path)) {
|
||||
$this->searchInDirectory($path, $relativePath, $query);
|
||||
} elseif (preg_match('/\.(md|php|html)$/', $item)) {
|
||||
$content = file_get_contents($path);
|
||||
if (stripos($content, $query) !== false || stripos($item, $query) !== false) {
|
||||
$title = ucfirst(pathinfo($item, PATHINFO_FILENAME));
|
||||
$this->searchResults[] = [
|
||||
'title' => $title,
|
||||
'path' => $relativePath,
|
||||
'url' => '?page=' . $relativePath,
|
||||
'snippet' => $this->createSnippet($content, $query)
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function createSnippet($content, $query) {
|
||||
$content = strip_tags($content);
|
||||
$pos = stripos($content, $query);
|
||||
if ($pos === false) return substr($content, 0, 100) . '...';
|
||||
|
||||
$start = max(0, $pos - 50);
|
||||
$snippet = substr($content, $start, 150);
|
||||
return '...' . $snippet . '...';
|
||||
}
|
||||
|
||||
public function getPage() {
|
||||
if (isset($_GET['search'])) {
|
||||
return $this->getSearchResults();
|
||||
}
|
||||
|
||||
// Check if guide is requested
|
||||
if (isset($_GET['guide'])) {
|
||||
return $this->getGuidePage();
|
||||
}
|
||||
|
||||
// Check if content directory is empty
|
||||
if ($this->isContentDirEmpty()) {
|
||||
return $this->getGuidePage();
|
||||
}
|
||||
|
||||
$page = $_GET['page'] ?? $this->config['default_page'];
|
||||
$page = preg_replace('/\.[^.]+$/', '', $page);
|
||||
|
||||
$filePath = $this->config['content_dir'] . '/' . $page;
|
||||
$actualFilePath = null;
|
||||
|
||||
if (file_exists($filePath . '.md')) {
|
||||
$actualFilePath = $filePath . '.md';
|
||||
$result = $this->parseMarkdown(file_get_contents($actualFilePath));
|
||||
} elseif (file_exists($filePath . '.php')) {
|
||||
$actualFilePath = $filePath . '.php';
|
||||
$result = $this->parsePHP($actualFilePath);
|
||||
} elseif (file_exists($filePath . '.html')) {
|
||||
$actualFilePath = $filePath . '.html';
|
||||
$result = $this->parseHTML(file_get_contents($actualFilePath));
|
||||
} elseif (file_exists($filePath)) {
|
||||
$actualFilePath = $filePath;
|
||||
$extension = pathinfo($filePath, PATHINFO_EXTENSION);
|
||||
if ($extension === 'md') {
|
||||
$result = $this->parseMarkdown(file_get_contents($actualFilePath));
|
||||
} elseif ($extension === 'php') {
|
||||
$result = $this->parsePHP($actualFilePath);
|
||||
} elseif ($extension === 'html') {
|
||||
$result = $this->parseHTML(file_get_contents($actualFilePath));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($result) && $actualFilePath) {
|
||||
$result['file_info'] = $this->getFileInfo($actualFilePath);
|
||||
return $result;
|
||||
}
|
||||
|
||||
return $this->getError404();
|
||||
}
|
||||
|
||||
private function getFileInfo($filePath) {
|
||||
if (!file_exists($filePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$stats = stat($filePath);
|
||||
$created = date('d-m-Y H:i', $stats['ctime']);
|
||||
$modified = date('d-m-Y H:i', $stats['mtime']);
|
||||
|
||||
return [
|
||||
'created' => $created,
|
||||
'modified' => $modified,
|
||||
'size' => $this->formatFileSize($stats['size'])
|
||||
];
|
||||
}
|
||||
|
||||
private function formatFileSize($bytes) {
|
||||
$units = ['B', 'KB', 'MB', 'GB'];
|
||||
$bytes = max($bytes, 0);
|
||||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||||
$pow = min($pow, count($units) - 1);
|
||||
|
||||
$bytes /= pow(1024, $pow);
|
||||
|
||||
return round($bytes, 2) . ' ' . $units[$pow];
|
||||
}
|
||||
|
||||
private function getSearchResults() {
|
||||
$query = $_GET['search'];
|
||||
$content = '<h2>Search Results for: "' . htmlspecialchars($query) . '"</h2>';
|
||||
|
||||
if (empty($this->searchResults)) {
|
||||
$content .= '<p>No results found.</p>';
|
||||
} else {
|
||||
$content .= '<p>Found ' . count($this->searchResults) . ' results:</p>';
|
||||
foreach ($this->searchResults as $result) {
|
||||
$content .= '<div class="card mb-3">';
|
||||
$content .= '<div class="card-body">';
|
||||
$content .= '<h5 class="card-title"><a href="' . htmlspecialchars($result['url']) . '">' . htmlspecialchars($result['title']) . '</a></h5>';
|
||||
$content .= '<p class="card-text text-muted">' . htmlspecialchars($result['path']) . '</p>';
|
||||
$content .= '<p class="card-text">' . htmlspecialchars($result['snippet']) . '</p>';
|
||||
$content .= '</div></div>';
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'title' => 'Search Results',
|
||||
'content' => $content
|
||||
];
|
||||
}
|
||||
|
||||
private function parseMarkdown($content) {
|
||||
$lines = explode("\n", $content);
|
||||
$title = '';
|
||||
$body = '';
|
||||
$inBody = false;
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if (!$inBody && preg_match('/^#\s+(.+)$/', $line, $matches)) {
|
||||
$title = $matches[1];
|
||||
$inBody = true;
|
||||
} elseif ($inBody || trim($line) !== '') {
|
||||
$body .= $line . "\n";
|
||||
$inBody = true;
|
||||
}
|
||||
}
|
||||
|
||||
$body = preg_replace('/### (.+)/', '<h3>$1</h3>', $body);
|
||||
$body = preg_replace('/## (.+)/', '<h2>$1</h2>', $body);
|
||||
$body = preg_replace('/# (.+)/', '<h1>$1</h1>', $body);
|
||||
$body = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $body);
|
||||
$body = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $body);
|
||||
|
||||
// Auto-link page titles to existing content pages (before markdown link processing)
|
||||
$body = $this->autoLinkPageTitles($body);
|
||||
|
||||
// Convert Markdown links to HTML links
|
||||
$body = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '<a href="$2">$1</a>', $body);
|
||||
|
||||
// Convert relative internal links to CMS format
|
||||
$body = preg_replace('/href="\/blog\/([^"]+)"/', 'href="?page=blog/$1"', $body);
|
||||
$body = preg_replace('/href="\/([^"]+)"/', 'href="?page=$1"', $body);
|
||||
|
||||
$body = preg_replace('/\n\n/', '</p><p>', $body);
|
||||
$body = '<p>' . $body . '</p>';
|
||||
$body = preg_replace('/<p><\/p>/', '', $body);
|
||||
$body = preg_replace('/<p>(<h[1-6]>)/', '$1', $body);
|
||||
$body = preg_replace('/(<\/h[1-6]>)<\/p>/', '$1', $body);
|
||||
|
||||
return [
|
||||
'title' => $title ?: 'Untitled',
|
||||
'content' => $body
|
||||
];
|
||||
}
|
||||
|
||||
private function autoLinkPageTitles($content) {
|
||||
// Get all available pages with their titles
|
||||
$pages = $this->getAllPageTitles();
|
||||
|
||||
foreach ($pages as $pagePath => $pageTitle) {
|
||||
// Create a pattern that matches the exact page title (case-insensitive)
|
||||
// Use word boundaries to avoid partial matches
|
||||
$pattern = '/\b' . preg_quote($pageTitle, '/') . '\b/i';
|
||||
|
||||
// Replace with link, but avoid linking inside existing links, headings, or markdown
|
||||
$replacement = function($matches) use ($pageTitle, $pagePath) {
|
||||
$text = $matches[0];
|
||||
|
||||
// Check if we're inside an existing link or markdown syntax
|
||||
if (preg_match('/\[.*?\]\(.*?\)/', $text) ||
|
||||
preg_match('/\[.*?\]:/', $text) ||
|
||||
preg_match('/<a[^>]*>/', $text) ||
|
||||
preg_match('/href=/', $text)) {
|
||||
return $text; // Don't link existing links
|
||||
}
|
||||
|
||||
return '<a href="?page=' . $pagePath . '" class="auto-link" title="Ga naar ' . htmlspecialchars($pageTitle) . '">' . $text . '</a>';
|
||||
};
|
||||
|
||||
$content = preg_replace_callback($pattern, $replacement, $content);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function getAllPageTitles() {
|
||||
$pages = [];
|
||||
$this->scanForPageTitles($this->config['content_dir'], '', $pages);
|
||||
return $pages;
|
||||
}
|
||||
|
||||
private function scanForPageTitles($dir, $prefix, &$pages) {
|
||||
if (!is_dir($dir)) return;
|
||||
|
||||
$items = scandir($dir);
|
||||
sort($items);
|
||||
|
||||
foreach ($items as $item) {
|
||||
if ($item[0] === '.') continue;
|
||||
|
||||
$path = $dir . '/' . $item;
|
||||
$relativePath = $prefix ? $prefix . '/' . $item : $item;
|
||||
|
||||
if (is_dir($path)) {
|
||||
$this->scanForPageTitles($path, $relativePath, $pages);
|
||||
} elseif (preg_match('/\.(md|php|html)$/', $item)) {
|
||||
$title = $this->extractPageTitle($path);
|
||||
if ($title && !empty(trim($title))) {
|
||||
$pagePath = preg_replace('/\.[^.]+$/', '', $relativePath);
|
||||
$pages[$pagePath] = $title;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function extractPageTitle($filePath) {
|
||||
$content = file_get_contents($filePath);
|
||||
$extension = pathinfo($filePath, PATHINFO_EXTENSION);
|
||||
|
||||
if ($extension === 'md') {
|
||||
// Extract first H1 from Markdown
|
||||
if (preg_match('/^#\s+(.+)$/m', $content, $matches)) {
|
||||
return trim($matches[1]);
|
||||
}
|
||||
} elseif ($extension === 'php') {
|
||||
// Extract title from PHP file
|
||||
if (preg_match('/\$title\s*=\s*["\']([^"\']+)["\']/', $content, $matches)) {
|
||||
return trim($matches[1]);
|
||||
}
|
||||
} elseif ($extension === 'html') {
|
||||
// Extract title from HTML file
|
||||
if (preg_match('/<title>(.*?)<\/title>/i', $content, $matches)) {
|
||||
return trim(strip_tags($matches[1]));
|
||||
}
|
||||
if (preg_match('/<h1[^>]*>(.*?)<\/h1>/i', $content, $matches)) {
|
||||
return trim(strip_tags($matches[1]));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function parsePHP($filePath) {
|
||||
ob_start();
|
||||
$title = 'Untitled';
|
||||
include $filePath;
|
||||
$content = ob_get_clean();
|
||||
|
||||
return [
|
||||
'title' => $title,
|
||||
'content' => $content
|
||||
];
|
||||
}
|
||||
|
||||
private function parseHTML($content) {
|
||||
$title = 'Untitled';
|
||||
|
||||
if (preg_match('/<title>(.*?)<\/title>/i', $content, $matches)) {
|
||||
$title = strip_tags($matches[1]);
|
||||
} elseif (preg_match('/<h1[^>]*>(.*?)<\/h1>/i', $content, $matches)) {
|
||||
$title = strip_tags($matches[1]);
|
||||
}
|
||||
|
||||
return [
|
||||
'title' => $title,
|
||||
'content' => $content
|
||||
];
|
||||
}
|
||||
|
||||
private function isContentDirEmpty() {
|
||||
$contentDir = $this->config['content_dir'];
|
||||
if (!is_dir($contentDir)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$files = scandir($contentDir);
|
||||
$files = array_diff($files, ['.', '..']);
|
||||
|
||||
return empty($files);
|
||||
}
|
||||
|
||||
private function getGuidePage() {
|
||||
$lang = $this->detectLanguage();
|
||||
$guideFile = __DIR__ . '/../../guide/' . $lang . '.md';
|
||||
|
||||
if (!file_exists($guideFile)) {
|
||||
$guideFile = __DIR__ . '/../../guide/en.md'; // Fallback to English
|
||||
}
|
||||
|
||||
$content = file_get_contents($guideFile);
|
||||
$result = $this->parseMarkdown($content);
|
||||
|
||||
// Set special title for guide
|
||||
$result['title'] = 'Handleiding - CodePress CMS';
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function detectLanguage() {
|
||||
// Simple language detection based on browser Accept-Language header
|
||||
$acceptLanguage = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '';
|
||||
|
||||
if (strpos($acceptLanguage, 'nl') !== false) {
|
||||
return 'nl';
|
||||
}
|
||||
|
||||
return 'en'; // Default to English
|
||||
}
|
||||
|
||||
private function getError404() {
|
||||
return [
|
||||
'title' => 'Page Not Found',
|
||||
'content' => '<h1>404 - Page Not Found</h1><p>The page you are looking for does not exist.</p>'
|
||||
];
|
||||
}
|
||||
|
||||
public function getMenu() {
|
||||
return $this->menu;
|
||||
}
|
||||
|
||||
public function render() {
|
||||
$page = $this->getPage();
|
||||
$menu = $this->getMenu();
|
||||
$breadcrumb = $this->getBreadcrumb();
|
||||
|
||||
// Prepare template data
|
||||
$templateData = [
|
||||
'site_title' => $this->config['site_title'],
|
||||
'page_title' => htmlspecialchars($page['title']),
|
||||
'content' => $page['content'],
|
||||
'search_query' => isset($_GET['search']) ? htmlspecialchars($_GET['search']) : '',
|
||||
'menu' => $this->renderMenu($menu),
|
||||
'breadcrumb' => $breadcrumb,
|
||||
'default_page' => $this->config['default_page']
|
||||
];
|
||||
|
||||
// File info for footer
|
||||
if (isset($page['file_info'])) {
|
||||
$templateData['file_info'] = 'Created: ' . htmlspecialchars($page['file_info']['created']) .
|
||||
' | Modified: ' . htmlspecialchars($page['file_info']['modified']);
|
||||
} else {
|
||||
$templateData['file_info'] = '';
|
||||
}
|
||||
|
||||
// Check if content exists for guide link
|
||||
$hasContent = !$this->isContentDirEmpty();
|
||||
$templateData['has_content'] = $hasContent;
|
||||
|
||||
// Don't show site title link on guide page
|
||||
$templateData['show_site_link'] = !$this->isContentDirEmpty() && !isset($_GET['guide']);
|
||||
|
||||
// Load partials manually
|
||||
$hasContent = !$this->isContentDirEmpty() && !isset($_GET['guide']);
|
||||
|
||||
$headerContent = file_get_contents($this->config['templates_dir'] . '/assets/header.mustache');
|
||||
if (!$hasContent) {
|
||||
// Remove the link from header when no content
|
||||
$headerContent = preg_replace('/<a href="[^"]*" class="site-title-link">\s*<h1[^>]*>(.*?)<\/h1>\s*<\/a>/', '<h1 class="h3 mb-0">$1</h1>', $headerContent);
|
||||
}
|
||||
|
||||
$footerContent = file_get_contents($this->config['templates_dir'] . '/assets/footer.mustache');
|
||||
if (!$hasContent) {
|
||||
// Remove guide link from footer when no content
|
||||
$footerContent = preg_replace('/<span class="file-details">\s*\|\s*<a href="\?guide"[^>]*>Handleiding<\/a><\/span>/', '', $footerContent);
|
||||
}
|
||||
|
||||
$partials = [
|
||||
'navigation' => file_get_contents($this->config['templates_dir'] . '/assets/navigation.mustache'),
|
||||
'footer' => file_get_contents($this->config['templates_dir'] . '/assets/footer.mustache')
|
||||
];
|
||||
|
||||
// Replace partials in template
|
||||
$template = file_get_contents($this->config['templates_dir'] . '/layout.mustache');
|
||||
$template = str_replace('{{>navigation}}', $partials['navigation'], $template);
|
||||
$template = str_replace('{{>footer}}', $partials['footer'], $template);
|
||||
|
||||
// Render template with data
|
||||
echo SimpleTemplate::render($template, $templateData);
|
||||
}
|
||||
|
||||
private function getBreadcrumb() {
|
||||
if (isset($_GET['search'])) {
|
||||
return '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '">Home</a></li><li class="breadcrumb-item active">Search</li></ol></nav>';
|
||||
}
|
||||
|
||||
$page = $_GET['page'] ?? $this->config['default_page'];
|
||||
$page = preg_replace('/\.[^.]+$/', '', $page);
|
||||
|
||||
if ($page === $this->config['default_page']) {
|
||||
return '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item active">Home</li></ol></nav>';
|
||||
}
|
||||
|
||||
$parts = explode('/', $page);
|
||||
$breadcrumb = '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '">Home</a></li>';
|
||||
|
||||
$path = '';
|
||||
foreach ($parts as $i => $part) {
|
||||
$path .= ($path ? '/' : '') . $part;
|
||||
$title = ucfirst($part);
|
||||
|
||||
if ($i === count($parts) - 1) {
|
||||
$breadcrumb .= '<li class="breadcrumb-item active">' . $title . '</li>';
|
||||
} else {
|
||||
$breadcrumb .= '<li class="breadcrumb-item"><a href="?page=' . $path . '">' . $title . '</a></li>';
|
||||
}
|
||||
}
|
||||
|
||||
$breadcrumb .= '</ol></nav>';
|
||||
return $breadcrumb;
|
||||
}
|
||||
|
||||
private function renderMenu($items, $level = 0) {
|
||||
$html = '';
|
||||
foreach ($items as $item) {
|
||||
if ($item['type'] === 'folder') {
|
||||
$hasChildren = !empty($item['children']);
|
||||
|
||||
if ($hasChildren) {
|
||||
$folderId = 'folder-' . str_replace('/', '-', $item['path']);
|
||||
$isExpanded = $this->folderContainsActivePage($item['children']);
|
||||
$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']);
|
||||
$html .= '</a>';
|
||||
$html .= '<ul class="dropdown-menu" aria-labelledby="' . $folderId . '">';
|
||||
$html .= $this->renderMenu($item['children'], $level + 1);
|
||||
$html .= '</ul>';
|
||||
$html .= '</li>';
|
||||
} else {
|
||||
$html .= '<li class="nav-item">';
|
||||
$html .= '<span class="nav-link text-muted">' . htmlspecialchars($item['title']) . '</span>';
|
||||
$html .= '</li>';
|
||||
}
|
||||
} else {
|
||||
$active = (isset($_GET['page']) && $_GET['page'] === $item['path']) ? 'active' : '';
|
||||
$html .= '<li class="nav-item">';
|
||||
$html .= '<a class="nav-link ' . $active . '" href="' . htmlspecialchars($item['url']) . '">' . htmlspecialchars($item['title']) . '</a>';
|
||||
$html .= '</li>';
|
||||
}
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
} else {
|
||||
$active = (isset($_GET['page']) && $_GET['page'] === $item['path']) ? 'active' : '';
|
||||
$html .= '<li class="nav-item">';
|
||||
$html .= '<a class="nav-link ' . $active . '" href="' . htmlspecialchars($item['url']) . '">' . htmlspecialchars($item['title']) . '</a>';
|
||||
$html .= '</li>';
|
||||
}
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
$html .= '</li>';
|
||||
} else {
|
||||
$active = (isset($_GET['page']) && $_GET['page'] === $item['path']) ? 'active' : '';
|
||||
$html .= '<li class="nav-item">';
|
||||
$html .= '<a class="nav-link page-link ' . $active . '" href="' . htmlspecialchars($item['url']) . '">' . htmlspecialchars($item['title']) . '</a>';
|
||||
$html .= '</li>';
|
||||
}
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function folderContainsActivePage($children) {
|
||||
foreach ($children as $child) {
|
||||
if ($child['type'] === 'folder') {
|
||||
if (!empty($child['children']) && $this->folderContainsActivePage($child['children'])) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (isset($_GET['page']) && $_GET['page'] === $child['path']) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,7 @@
|
||||
<div class="file-info">
|
||||
<i class="bi bi-file-text"></i>
|
||||
<span class="page-title" title="{{page_title}}">{{page_title}}</span>
|
||||
{{#file_info}}
|
||||
<span class="file-details"> | {{{file_info}}}</span>
|
||||
{{/file_info}}
|
||||
{{{file_info_block}}}
|
||||
</div>
|
||||
<div class="site-info">
|
||||
<small class="text-muted">
|
||||
|
||||
19
engine/templates/assets/header.mustache
Normal file
19
engine/templates/assets/header.mustache
Normal file
@@ -0,0 +1,19 @@
|
||||
<header class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="?page={{default_page}}">
|
||||
<img src="/assets/icon.svg" alt="CodePress Logo" width="32" height="32" class="me-2">
|
||||
{{site_title}}
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#searchBar" aria-controls="searchBar" aria-expanded="false" aria-label="Toggle search">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="searchBar">
|
||||
<form class="d-flex ms-auto" method="GET" action="">
|
||||
<input class="form-control me-2" type="search" name="search" placeholder="Search..." value="{{search_query}}">
|
||||
<button class="btn btn-outline-light" type="submit">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -1,23 +1,16 @@
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<nav class="navigation-section bg-white border-bottom">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="?page={{default_page}}">
|
||||
<img src="/assets/icon.svg" alt="CodePress Logo" width="32" height="32" class="me-2">
|
||||
{{site_title}}
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
{{{menu}}}
|
||||
</ul>
|
||||
|
||||
<form class="d-flex" method="GET" action="">
|
||||
<input class="form-control me-2" type="search" name="search" placeholder="Search..." value="{{search_query}}">
|
||||
<button class="btn btn-outline-light" type="submit">Search</button>
|
||||
</form>
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<ul class="nav nav-tabs flex-wrap">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="?page={{homepage}}">
|
||||
<i class="bi bi-house"></i> Home
|
||||
</a>
|
||||
</li>
|
||||
{{{menu}}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
5
engine/templates/html_content.mustache
Normal file
5
engine/templates/html_content.mustache
Normal file
@@ -0,0 +1,5 @@
|
||||
<div class="html-content">
|
||||
<div class="content-body">
|
||||
{{{content}}}
|
||||
</div>
|
||||
</div>
|
||||
@@ -4,6 +4,23 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{page_title}} - {{site_title}}</title>
|
||||
|
||||
<!-- 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}}">
|
||||
|
||||
<!-- Author Links -->
|
||||
<link rel="author" href="{{author_website}}">
|
||||
<link rel="me" href="{{author_git}}">
|
||||
|
||||
<!-- Favicon and Styles -->
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
|
||||
<link href="/assets/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="/assets/css/bootstrap-icons.css" rel="stylesheet">
|
||||
@@ -12,28 +29,29 @@
|
||||
<body>
|
||||
{{>header}}
|
||||
|
||||
<div class="main-wrapper">
|
||||
{{>navigation}}
|
||||
|
||||
<main class="main-content">
|
||||
<div class="content-inner">
|
||||
<div>
|
||||
{{>navigation}}
|
||||
|
||||
<div class="breadcrumb-section bg-light border-bottom">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12 py-2">
|
||||
{{{breadcrumb}}}
|
||||
</div>
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h2>{{page_title}}</h2>
|
||||
</div>
|
||||
<div class="content">
|
||||
{{{content}}}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container-fluid main-content" style="padding-bottom: 80px;">
|
||||
<div class="row">
|
||||
<main class="col-12">
|
||||
{{>content_template}}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{>footer}}
|
||||
|
||||
<script src="/assets/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/assets/js/sidebar.js"></script>
|
||||
<script src="/assets/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,3 +1,5 @@
|
||||
{{#content}}
|
||||
<p>{{{.}}}</p>
|
||||
{{/content}}
|
||||
<div class="markdown-content">
|
||||
<div class="content-body">
|
||||
{{{content}}}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1 +1,5 @@
|
||||
{{{content}}}
|
||||
<div class="php-content">
|
||||
<div class="content-body">
|
||||
{{{content}}}
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user