Merge branch 'e.noorlander'

This commit is contained in:
Edwin Noorlander 2025-11-19 18:07:15 +01:00
commit 704cd0ff0c
39 changed files with 3260 additions and 368 deletions

44
.htaccess Normal file
View File

@ -0,0 +1,44 @@
# Security - Block access to entire application
<Files ~ "^\.">
Order allow,deny
Deny from all
</Files>
<FilesMatch "\.(php|ini|log|conf|config|md)$">
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>

19
AGENTS.md Normal file
View File

@ -0,0 +1,19 @@
# Agent Instructions for CodePress CMS
## Build & Run
- **Run Server**: `php -S localhost:8080 -t public`
- **Lint PHP**: `find . -name "*.php" -exec php -l {} \;`
- **Dependencies**: No Composer/NPM required. Native PHP 8.4+ implementation.
## 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 logic resides in `index.php`.
- Configuration in `config.php`.
- Public entry point is `public/index.php`.
- **Content**: Stored in `public/content/`. Supports `.md` (Markdown), `.php` (Dynamic), `.html` (Static).
- **Templating**: Simple string replacement `{{placeholder}}` in `templates/layout.html`.
- **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.
- **Git**: `main` is the clean CMS core. `e.noorlander` contains personal content. Do not mix them.

View File

@ -33,21 +33,30 @@ CodePress is a modern, secure CMS that manages content through files instead of
``` ```
3. **Start server**: 3. **Start server**:
```bash ```bash
php -S localhost:8000 -t public # For Apache: Set DocumentRoot to public/
# For Development:
php -S localhost:8080 -t public router.php
``` ```
4. **Visit**: `http://localhost:8000` 4. **Visit**: `http://localhost:8080`
## Project Structure ## Project Structure
``` ```
codepress/ codepress/
├── public/ # Web-accessible directory ├── public/ # Web-accessible directory (DocumentRoot)
│ ├── content/ # Content files (MD/PHP/HTML) │ ├── index.php # Main entry point
│ ├── assets/ # Static assets (images, icons) │ ├── .htaccess # Apache security and routing
│ └── .htaccess # Security and routing │ └── router.php # PHP development server router
├── templates/ # HTML templates ├── content/ # Content files (MD/PHP/HTML) - outside web root
├── config.php # Site configuration ├── engine/ # CMS engine and assets
├── index.php # Main application logic │ ├── core/ # PHP application logic
│ │ ├── index.php # CMS class and logic
│ │ └── config.php # Site configuration
│ ├── templates/ # HTML templates
│ └── assets/ # Static assets (CSS, JS, fonts)
│ ├── css/ # Bootstrap and custom CSS
│ ├── js/ # JavaScript files
│ └── fonts/ # Font files
├── .htaccess # Root security ├── .htaccess # Root security
└── README.md # This documentation └── README.md # This documentation
``` ```
@ -55,11 +64,13 @@ codepress/
## Security ## Security
CodePress includes built-in security features: CodePress includes built-in security features:
- **.htaccess protection** for sensitive files - **Content isolation** - Content files stored outside web root
- **PHP file blocking** in content directory - **.htaccess protection** for sensitive files and directories
- **Direct access blocking** - Content files not accessible via URL
- **Security headers** for XSS protection - **Security headers** for XSS protection
- **PHP file blocking** in content directory
- **Offline capable** - All assets (Bootstrap) stored locally
- **Directory access control** - **Directory access control**
- **Public directory isolation**
## Content Management ## Content Management
@ -95,7 +106,7 @@ $date = date('Y-m-d');
### Directory Structure ### Directory Structure
``` ```
public/content/ content/
├── home.md # Homepage ├── home.md # Homepage
├── about/ ├── about/
│ └── company.md # About page │ └── company.md # About page
@ -115,7 +126,8 @@ Edit `config.php` to customize:
return [ return [
'site_title' => 'Your Site Name', 'site_title' => 'Your Site Name',
'site_description' => 'Your site description', 'site_description' => 'Your site description',
'content_dir' => __DIR__ . '/public/content', 'content_dir' => __DIR__ . '/../../content',
'templates_dir' => __DIR__ . '/../templates',
'default_page' => 'home', 'default_page' => 'home',
'markdown_enabled' => true, 'markdown_enabled' => true,
'php_enabled' => true, 'php_enabled' => true,
@ -148,7 +160,7 @@ This project is developed for specific use cases. Contact the maintainer for lic
## Support ## Support
- **Documentation**: See `public/content/home.md` - **Documentation**: See `content/home.md`
- **Issues**: Report on GitLab - **Issues**: Report on GitLab
- **Community**: Join discussions - **Community**: Join discussions

View File

@ -1,23 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="codepress-gradient-small" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#0d6efd;stop-opacity:1" />
<stop offset="100%" style="stop-color:#6610f2;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background circle -->
<circle cx="8" cy="8" r="7.5" fill="url(#codepress-gradient-small)" stroke="#ffffff" stroke-width="0.5"/>
<!-- Code brackets -->
<path d="M4 5 L3 6 L3 10 L4 11" stroke="#ffffff" stroke-width="1" fill="none" stroke-linecap="round"/>
<path d="M12 5 L13 6 L13 10 L12 11" stroke="#ffffff" stroke-width="1" fill="none" stroke-linecap="round"/>
<!-- Code slash -->
<path d="M7 4 L9 12" stroke="#ffffff" stroke-width="1" stroke-linecap="round"/>
<!-- Press dots -->
<circle cx="6" cy="13" r="0.75" fill="#ffffff"/>
<circle cx="8" cy="13" r="0.75" fill="#ffffff"/>
<circle cx="10" cy="13" r="0.75" fill="#ffffff"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,23 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="codepress-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#0d6efd;stop-opacity:1" />
<stop offset="100%" style="stop-color:#6610f2;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background circle -->
<circle cx="16" cy="16" r="15" fill="url(#codepress-gradient)" stroke="#ffffff" stroke-width="1"/>
<!-- Code brackets -->
<path d="M8 10 L6 12 L6 20 L8 22" stroke="#ffffff" stroke-width="2" fill="none" stroke-linecap="round"/>
<path d="M24 10 L26 12 L26 20 L24 22" stroke="#ffffff" stroke-width="2" fill="none" stroke-linecap="round"/>
<!-- Code slash -->
<path d="M14 8 L18 24" stroke="#ffffff" stroke-width="2" stroke-linecap="round"/>
<!-- Press dots -->
<circle cx="12" cy="26" r="1.5" fill="#ffffff"/>
<circle cx="16" cy="26" r="1.5" fill="#ffffff"/>
<circle cx="20" cy="26" r="1.5" fill="#ffffff"/>
</svg>

Before

Width:  |  Height:  |  Size: 1018 B

View File

@ -4,7 +4,7 @@ return [
'site_title' => 'CodePress', 'site_title' => 'CodePress',
'site_description' => 'A simple PHP CMS', 'site_description' => 'A simple PHP CMS',
'base_url' => '/', 'base_url' => '/',
'content_dir' => __DIR__ . '/content', 'content_dir' => __DIR__ . '/public/content',
'templates_dir' => __DIR__ . '/templates', 'templates_dir' => __DIR__ . '/templates',
'cache_dir' => __DIR__ . '/cache', 'cache_dir' => __DIR__ . '/cache',
'default_page' => 'home', 'default_page' => 'home',

View File

@ -5,14 +5,30 @@ Welkom op de persoonlijke blog van Edwin Noorlander. Hier deel ik mijn gedachten
## Categorieën ## Categorieën
### Over Mij ### Over Mij
- [Welkom, ik ben Edwin](/blog/over-mij/welkom) - Mijn persoonlijke verhaal en achtergrond - [Welkom, ik ben Edwin](?page=blog/over-mij/welkom " title="Lees meer over Edwin Noorlander") - Mijn persoonlijke verhaal en achtergrond
### Open Source ### Open Source
- [De Toekomst van ICT](/blog/open-source/de-toekomst-van-ict) - Hoe open source software de werkvloer transformeert - [De Toekomst van ICT](?page=blog/open-source/de-toekomst-van-ict) - Hoe open source software de werkvloer transformeert
- [Standaardisatie](/blog/open-source/standaardisatie) - Het belang van standaarden en de tegenstelling tussen commerciële bedrijven en open-source gemeenschappen - [Standaardisatie](?page=blog/open-source/standaardisatie) - Het belang van standaarden en de tegenstelling tussen commerciële bedrijven en open-source gemeenschappen
### Leren & Ontwikkeling ### Leren & Ontwikkeling
- [Kennis boven Aantallen](/blog/leren/kennis-boven-aantallen) - Het cruciale belang van kennis boven personeelsaantallen in de werkkracht - [Kennis boven Aantallen](?page=blog/leren/kennis-boven-aantallen) - Het cruciale belang van kennis boven personeelsaantallen in de werkkracht
### ICT & Bedrijfsvoering
- [Excel als Database](?page=blog/ict/excel-als-database) - Waarom grote bedrijven en overheden Access niet toestaan
### Micro-electronica
- [Wat is Arduino](?page=blog/micro-electronica/wat-is-arduino) - Open-source elektronisch platform voor interactieve projecten
- [Leren gaat niet over perfectie](?page=blog/micro-electronica/leren-gaat-niet-over-perfectie) - Passie voor electronica en open-source hardware/software
### Hardware & ICT
- [De ware aard van ICT](?page=blog/hardware/de-ware-aard-van-ict) - Meer dan alleen computers en software
### Retro Gaming
- [Commodore 64](?page=blog/retro-gaming/commodore-64) - Een les in creativiteit en innovatie in de game-industrie
### Linux & Open Source
- [Open Source Voorbeelden](?page=blog/linux/open-source-voorbeelden) - Vijf voorbeelden van open-source software in gebruik bij commerciële bedrijven
## Over Edwin Noorlander ## Over Edwin Noorlander

2078
engine/assets/css/bootstrap-icons.css vendored Normal file

File diff suppressed because it is too large Load Diff

6
engine/assets/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

11
engine/assets/favicon.svg Normal file
View File

@ -0,0 +1,11 @@
<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>

After

Width:  |  Height:  |  Size: 442 B

11
engine/assets/icon.svg Normal file
View File

@ -0,0 +1,11 @@
<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>

After

Width:  |  Height:  |  Size: 442 B

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,9 +4,9 @@ return [
'site_title' => 'CodePress', 'site_title' => 'CodePress',
'site_description' => 'A simple PHP CMS', 'site_description' => 'A simple PHP CMS',
'base_url' => '/', 'base_url' => '/',
'content_dir' => __DIR__ . '/content', 'content_dir' => __DIR__ . '/../../content',
'templates_dir' => __DIR__ . '/templates', 'templates_dir' => __DIR__ . '/../templates',
'cache_dir' => __DIR__ . '/cache', 'cache_dir' => __DIR__ . '/../../cache',
'default_page' => 'home', 'default_page' => 'home',
'error_404' => '404', 'error_404' => '404',
'markdown_enabled' => true, 'markdown_enabled' => true,

476
engine/core/index.php Normal file
View File

@ -0,0 +1,476 @@
<?php
require_once 'config.php';
$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();
}
$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 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();
$template = file_get_contents($this->config['templates_dir'] . '/layout.html');
$template = str_replace('{{site_title}}', $this->config['site_title'], $template);
$template = str_replace('{{page_title}}', $page['title'], $template);
$template = str_replace('{{content}}', $page['content'], $template);
$template = str_replace('{{search_query}}', isset($_GET['search']) ? htmlspecialchars($_GET['search']) : '', $template);
$template = str_replace('{{breadcrumb}}', $breadcrumb, $template);
// File info for footer
$fileInfo = '';
if (isset($page['file_info'])) {
$fileInfo = '<i class="bi bi-file-text"></i> Created: ' . htmlspecialchars($page['file_info']['created']) .
' | Modified: ' . htmlspecialchars($page['file_info']['modified']);
}
$template = str_replace('{{file_info}}', $fileInfo, $template);
$menuHtml = $this->renderMenu($menu);
$template = str_replace('{{menu}}', $menuHtml, $template);
echo $template;
}
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']);
$html .= '<li class="nav-item">';
if ($hasChildren) {
$folderId = 'folder-' . str_replace('/', '-', $item['path']);
// Check if this folder contains the active page
$containsActive = $this->folderContainsActivePage($item['children']);
$ariaExpanded = $containsActive ? 'true' : 'false';
$collapseClass = $containsActive ? 'collapse show' : 'collapse';
$html .= '<span class="nav-link folder-toggle" data-bs-toggle="collapse" data-bs-target="#' . $folderId . '" aria-expanded="' . $ariaExpanded . '">';
$html .= '<i class="arrow bi bi-chevron-right"></i> ' . htmlspecialchars($item['title']);
$html .= '</span>';
$html .= '<ul class="nav flex-column ms-2 ' . $collapseClass . '" id="' . $folderId . '">';
$html .= $this->renderMenu($item['children'], $level + 1);
$html .= '</ul>';
} else {
$html .= '<span class="nav-link folder-disabled" disabled>';
$html .= '<i class="arrow bi bi-chevron-right"></i> ' . htmlspecialchars($item['title']);
$html .= '</span>';
}
$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;
}
}
$cms = new CodePressCMS($config);
$cms->render();

View File

@ -4,9 +4,9 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{page_title}} - {{site_title}}</title> <title>{{page_title}} - {{site_title}}</title>
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg"> <link rel="icon" type="image/svg+xml" href="/engine/assets/favicon.svg">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="/engine/assets/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet"> <link href="/engine/assets/css/bootstrap-icons.css" rel="stylesheet">
<style> <style>
* { * {
margin: 0; margin: 0;
@ -22,12 +22,14 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100vh; min-height: 100vh;
position: relative;
} }
.main-wrapper { .main-wrapper {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: calc(100vh - 70px); /* Minus header height */
} }
.content-wrapper { .content-wrapper {
@ -42,12 +44,81 @@
border-right: 1px solid #dee2e6; border-right: 1px solid #dee2e6;
overflow-y: auto; overflow-y: auto;
flex-shrink: 0; flex-shrink: 0;
transition: transform 0.3s ease;
position: fixed;
top: 70px;
left: 0;
height: calc(100vh - 140px); /* 70px header + 70px footer */
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;
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;
}
.sidebar-toggle:hover {
color: #0d6efd;
background-color: #f8f9fa;
}
.sidebar-toggle-inner {
/* Toggle inside sidebar */
}
.sidebar-toggle-outer {
position: fixed;
top: 90px;
left: 20px;
z-index: 1001;
background-color: white;
border: 1px solid #dee2e6;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.sidebar.collapsed .sidebar-toggle-inner {
right: auto;
left: 15px;
}
.sidebar.collapsed ~ .main-content .sidebar-toggle-outer {
display: block !important;
}
.sidebar:not(.collapsed) ~ .main-content .sidebar-toggle-outer {
display: none !important;
}
.sidebar-toggle:hover {
background-color: #0a58ca;
transform: scale(1.05);
}
.main-content {
flex: 1;
overflow-y: auto;
padding: 20px;
transition: all 0.3s ease;
margin-left: 250px;
}
.sidebar.collapsed ~ .main-content {
margin-left: 0;
} }
.main-content { .main-content {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 20px; padding: 20px;
transition: margin-left 0.3s ease;
} }
.folder-toggle { .folder-toggle {
@ -155,6 +226,26 @@
font-size: 0.9rem; font-size: 0.9rem;
color: #6c757d; color: #6c757d;
} }
.site-info a {
color: #0d6efd;
text-decoration: none;
}
.site-info a:hover {
text-decoration: underline;
}
.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; max-width: 300px;
} }
@ -195,7 +286,7 @@
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col"> <div class="col">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<img src="assets/icon.svg" alt="CodePress Logo" width="32" height="32" class="me-2"> <img src="/engine/assets/icon.svg" alt="CodePress Logo" width="32" height="32" class="me-2">
<h1 class="h3 mb-0">{{site_title}}</h1> <h1 class="h3 mb-0">{{site_title}}</h1>
</div> </div>
</div> </div>
@ -211,7 +302,10 @@
<div class="main-wrapper"> <div class="main-wrapper">
<div class="content-wrapper"> <div class="content-wrapper">
<nav class="sidebar"> <nav class="sidebar" id="sidebar">
<div class="sidebar-toggle sidebar-toggle-inner" id="sidebarToggleInner">
<i class="bi bi-list"></i>
</div>
<div class="pt-3"> <div class="pt-3">
<ul class="nav flex-column"> <ul class="nav flex-column">
{{menu}} {{menu}}
@ -220,6 +314,9 @@
</nav> </nav>
<main class="main-content"> <main class="main-content">
<div class="sidebar-toggle sidebar-toggle-outer" id="sidebarToggleOuter" style="display: none;">
<i class="bi bi-list"></i>
</div>
<div> <div>
{{breadcrumb}} {{breadcrumb}}
</div> </div>
@ -233,7 +330,7 @@
</div> </div>
</div> </div>
<footer class="bg-light border-top py-3"> <footer class="bg-light border-top py-3" style="position: fixed; bottom: 0; left: 0; right: 0; z-index: 998;">
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
@ -242,7 +339,7 @@
{{file_info}} {{file_info}}
</div> </div>
<div class="site-info"> <div class="site-info">
<small class="text-muted">Powered by CodePress CMS</small> <small class="text-muted">Powered by <a href="https://git.noorlander.info/E.Noorlander/CodePress.git" target="_blank" rel="noopener">CodePress CMS</a></small>
</div> </div>
</div> </div>
</div> </div>
@ -250,53 +347,90 @@
</div> </div>
</footer> </footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="/engine/assets/js/bootstrap.bundle.min.js"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Open folders that contain the current active page // Sidebar toggle functionality
const activeLink = document.querySelector('.nav-link.active'); const sidebarToggleInner = document.getElementById('sidebarToggleInner');
if (activeLink) { const sidebarToggleOuter = document.getElementById('sidebarToggleOuter');
let parent = activeLink.closest('.collapse'); const sidebar = document.getElementById('sidebar');
while (parent) {
const toggle = document.querySelector('[data-bs-target="#' + parent.id + '"]'); // Initialize sidebar state (open by default)
if (toggle) { sidebar.classList.remove('collapsed');
const collapse = new bootstrap.Collapse(parent, { const innerIcon = sidebarToggleInner.querySelector('i');
show: true const outerIcon = sidebarToggleOuter.querySelector('i');
}); innerIcon.classList.remove('bi-list');
toggle.setAttribute('aria-expanded', 'true'); innerIcon.classList.add('bi-x');
} outerIcon.classList.remove('bi-list');
parent = parent.parentElement.closest('.collapse'); outerIcon.classList.add('bi-x');
function toggleSidebar() {
sidebar.classList.toggle('collapsed');
// Change icons
if (sidebar.classList.contains('collapsed')) {
innerIcon.classList.remove('bi-x');
innerIcon.classList.add('bi-list');
outerIcon.classList.remove('bi-x');
outerIcon.classList.add('bi-list');
} else {
innerIcon.classList.remove('bi-list');
innerIcon.classList.add('bi-x');
outerIcon.classList.remove('bi-list');
outerIcon.classList.add('bi-x');
} }
} }
sidebarToggleInner.addEventListener('click', toggleSidebar);
sidebarToggleOuter.addEventListener('click', toggleSidebar);
// Folders are now automatically expanded by PHP if they contain the active page
// Close other folders when opening a new one // Close other folders when opening a new one
const folderToggles = document.querySelectorAll('.folder-toggle'); const folderToggles = document.querySelectorAll('.folder-toggle');
folderToggles.forEach(toggle => { folderToggles.forEach(toggle => {
toggle.addEventListener('click', function(e) { toggle.addEventListener('click', function(e) {
const targetId = this.getAttribute('data-bs-target'); const targetId = this.getAttribute('data-bs-target');
const targetCollapse = document.querySelector(targetId);
const isExpanded = this.getAttribute('aria-expanded') === 'true'; const isExpanded = this.getAttribute('aria-expanded') === 'true';
if (!isExpanded) { if (!isExpanded && targetCollapse) {
// Close all other folders // Close all other folders first
folderToggles.forEach(otherToggle => { folderToggles.forEach(otherToggle => {
if (otherToggle !== this) { if (otherToggle !== this) {
const otherTargetId = otherToggle.getAttribute('data-bs-target'); const otherTargetId = otherToggle.getAttribute('data-bs-target');
if (otherTargetId) { if (otherTargetId) {
const otherCollapse = document.querySelector(otherTargetId); const otherCollapse = document.querySelector(otherTargetId);
if (otherCollapse) { if (otherCollapse) {
const bsCollapse = bootstrap.Collapse.getInstance(otherCollapse); otherCollapse.classList.remove('show');
if (bsCollapse) {
bsCollapse.hide();
} else {
new bootstrap.Collapse(otherCollapse, {
hide: true
});
}
otherToggle.setAttribute('aria-expanded', 'false'); otherToggle.setAttribute('aria-expanded', 'false');
// Reset arrow
const otherArrow = otherToggle.querySelector('.arrow');
if (otherArrow) {
otherArrow.style.transform = '';
}
} }
} }
} }
}); });
// Open this folder
targetCollapse.classList.add('show');
this.setAttribute('aria-expanded', 'true');
// Rotate arrow
const arrow = this.querySelector('.arrow');
if (arrow) {
arrow.style.transform = 'rotate(90deg)';
}
} else if (isExpanded && targetCollapse) {
// Close this folder
targetCollapse.classList.remove('show');
this.setAttribute('aria-expanded', 'false');
// Reset arrow
const arrow = this.querySelector('.arrow');
if (arrow) {
arrow.style.transform = '';
}
} }
}); });
}); });

View File

@ -212,6 +212,9 @@ class CodePressCMS {
$body = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $body); $body = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $body);
$body = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $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 // Convert Markdown links to HTML links
$body = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '<a href="$2">$1</a>', $body); $body = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '<a href="$2">$1</a>', $body);
@ -231,6 +234,102 @@ class CodePressCMS {
]; ];
} }
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;
}
return '<a href="?page=' . $pagePath . '" class="auto-link" title="Ga naar ' . htmlspecialchars($pageTitle) . '">' . $matches[0] . '</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) { private function parsePHP($filePath) {
ob_start(); ob_start();
$title = 'Untitled'; $title = 'Untitled';

74
public/.htaccess Normal file
View File

@ -0,0 +1,74 @@
# Security - Block access to sensitive files and directories
<Files ~ "^\.">
Order allow,deny
Deny from all
</Files>
<FilesMatch "\.(php|ini|log|conf|config)$">
Order allow,deny
Deny from all
</FilesMatch>
# Block access to core directories
<IfModule mod_authz_core.c>
<RequireAll>
Require all granted
<RequireNone>
Require all denied
</RequireNone>
</RequireAll>
</IfModule>
# Directory protection
<Directory ~ "^\.|/(config|templates|vendor|cache)/">
Order allow,deny
Deny from all
</Directory>
# URL Routing - Route all requests to index.php
<IfModule mod_rewrite.c>
RewriteEngine On
# Set base directory
RewriteBase /
# Block direct access to PHP files in content directory
RewriteRule ^content/.*\.php$ - [F,L]
# Route all non-file/non-directory requests to index.php
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]
# Allow access to assets
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule ^assets/.*$ - [L]
# Block direct access to all content files
RewriteRule ^content/.*$ - [F,L]
</IfModule>
# Security headers
<IfModule mod_headers.c>
Header always set X-Content-Type-Options nosniff
Header always set X-Frame-Options DENY
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>
# PHP settings
<IfModule mod_php.c>
php_flag display_errors Off
php_flag log_errors On
php_value error_log /var/log/php_errors.log
php_value max_execution_time 30
php_value memory_limit 128M
php_value upload_max_filesize 10M
php_value post_max_size 10M
</IfModule>
# Default index file
DirectoryIndex index.php
# Error handling
ErrorDocument 404 /index.php

View File

@ -1,23 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="codepress-gradient-small" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#0d6efd;stop-opacity:1" />
<stop offset="100%" style="stop-color:#6610f2;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background circle -->
<circle cx="8" cy="8" r="7.5" fill="url(#codepress-gradient-small)" stroke="#ffffff" stroke-width="0.5"/>
<!-- Code brackets -->
<path d="M4 5 L3 6 L3 10 L4 11" stroke="#ffffff" stroke-width="1" fill="none" stroke-linecap="round"/>
<path d="M12 5 L13 6 L13 10 L12 11" stroke="#ffffff" stroke-width="1" fill="none" stroke-linecap="round"/>
<!-- Code slash -->
<path d="M7 4 L9 12" stroke="#ffffff" stroke-width="1" stroke-linecap="round"/>
<!-- Press dots -->
<circle cx="6" cy="13" r="0.75" fill="#ffffff"/>
<circle cx="8" cy="13" r="0.75" fill="#ffffff"/>
<circle cx="10" cy="13" r="0.75" fill="#ffffff"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,23 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="codepress-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#0d6efd;stop-opacity:1" />
<stop offset="100%" style="stop-color:#6610f2;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background circle -->
<circle cx="16" cy="16" r="15" fill="url(#codepress-gradient)" stroke="#ffffff" stroke-width="1"/>
<!-- Code brackets -->
<path d="M8 10 L6 12 L6 20 L8 22" stroke="#ffffff" stroke-width="2" fill="none" stroke-linecap="round"/>
<path d="M24 10 L26 12 L26 20 L24 22" stroke="#ffffff" stroke-width="2" fill="none" stroke-linecap="round"/>
<!-- Code slash -->
<path d="M14 8 L18 24" stroke="#ffffff" stroke-width="2" stroke-linecap="round"/>
<!-- Press dots -->
<circle cx="12" cy="26" r="1.5" fill="#ffffff"/>
<circle cx="16" cy="26" r="1.5" fill="#ffffff"/>
<circle cx="20" cy="26" r="1.5" fill="#ffffff"/>
</svg>

Before

Width:  |  Height:  |  Size: 1018 B

View File

@ -1,39 +0,0 @@
# Het Cruciale Belang van Kennis boven Aantallen in de Werkkracht
In de dynamische wereld van vandaag wordt vaak de fout gemaakt om te denken dat het simpelweg uitbreiden van het personeelsbestand automatisch zal leiden tot verbeterde efficiëntie en productiviteit binnen een organisatie. Deze misvatting miskent echter het werkelijke knelpunt: een gebrek aan diepgaande kennis en expertise bij het bestaande personeel. Het is niet een kwestie van meer mensen aannemen, maar eerder van het ontwikkelen van beter opgeleide en gekwalificeerde werknemers.
## Problemen door gebrek aan kennis
### 1. Foutieve informatie en besluitvorming
Stel je voor dat een bedrijf een projectteam heeft samengesteld dat verantwoordelijk is voor het lanceren van een nieuw product. Als de teamleden niet beschikken over de vereiste kennis van markttrends, consumentengedrag en concurrentieanalyse, kunnen ze beslissingen nemen die gebaseerd zijn op onvolledige of verouderde informatie.
Dit kan resulteren in het nemen van verkeerde beslissingen, zoals het produceren van een product dat niet aansluit op de behoeften van de markt, met als gevolg financiële verliezen voor het bedrijf.
### 2. Behoefte aan constante correcties
Stel je voor dat een klantenserviceteam te maken heeft met klachten over een nieuw gelanceerd product. Als de medewerkers niet goed zijn opgeleid over de specificaties en eigenschappen van het product, zullen ze moeite hebben om de klachten van klanten adequaat te begrijpen en op te lossen.
Dit kan leiden tot een situatie waarin meer ervaren medewerkers voortdurend de tijd moeten nemen om de fouten van minder goed opgeleide collega's te corrigeren, wat leidt tot inefficiëntie en frustratie bij zowel medewerkers als klanten.
### 3. Vertraagde processen
In een technologiebedrijf kan een gebrek aan kennis over de nieuwste software en systemen leiden tot vertragingen bij het uitvoeren van projecten. Stel je voor dat programmeurs niet op de hoogte zijn van de nieuwste programmeertools en -methoden.
Dit kan resulteren in een langzamer ontwikkelingsproces en uiteindelijk een vertraagde productlancering, waardoor het bedrijf een concurrentievoordeel verliest.
## Oplossing: Investeren in kennisontwikkeling
Om deze problemen effectief aan te pakken en de algehele prestaties van een organisatie te verbeteren, is het van essentieel belang om te investeren in de ontwikkeling van de kennis en vaardigheden van het bestaande personeel.
Door middel van regelmatige trainingssessies, bijscholing en professionele ontwikkelingsprogramma's kunnen werknemers worden uitgerust met de nodige expertise om hun taken effectief en efficiënt uit te voeren.
## Focus op kwaliteit boven kwantiteit
In plaats van te streven naar een kwantitatieve uitbreiding van het personeelsbestand, moet de focus liggen op het versterken van de kwaliteit van het personeel door middel van continue leerinitiatieven.
Dit zal niet alleen leiden tot betere prestaties en productiviteit, maar ook tot een meer bevredigende werkomgeving waarin werknemers zich gewaardeerd en gesteund voelen in hun professionele ontwikkeling.
## Conclusie
Kortom, het is niet het aantal werknemers dat telt, maar de kwaliteit van hun kennis en vaardigheden die bepalend zijn voor het succes van een organisatie. Door te investeren in de ontwikkeling van het bestaande personeel kunnen organisaties effectiever opereren en beter inspelen op de uitdagingen van de moderne zakelijke omgeving.

View File

@ -1,46 +0,0 @@
# De Toekomst van ICT: Hoe Open Source Software de Werkvloer Transformeert
In onze steeds digitaler wordende samenleving zijn medewerkers digitaal vaardiger geworden en verwachten zij dat de ICT-voorzieningen op kantoor en de werkvloer aansluiten bij hun behoeften. Helaas lopen traditionele ICT-afdelingen hier vaak op achter, wat leidt tot een kloof tussen de beschikbare technologie en de wensen van de medewerkers.
## Beperkingen van traditionele ICT-oplossingen
Traditionele ICT-afdelingen bieden doorgaans standaard softwarepakketten aan, zoals Office-suites en omvangrijke HRM- of communicatiesoftware. Hoewel deze tools een basisfunctionaliteit bieden, schieten ze vaak tekort in flexibiliteit en maatwerk.
Technisch onderlegde medewerkers weten soms meer uit deze pakketten te halen, maar voor velen zijn de mogelijkheden binnen de bestaande infrastructuur beperkt. Dit leidt tot frustratie en inefficiëntie op de werkvloer.
## De dynamiek van een digitale samenleving
Onze maatschappij is dynamisch en past zich snel aan nieuwe ICT-mogelijkheden aan. Wanneer ICT-afdelingen vasthouden aan rigide systemen, ontstaan er hiaten tussen hoe mensen willen werken en de beschikbare technologie.
De werkvloer wordt hierdoor leidend in de vraag naar ICT-oplossingen, terwijl de ICT-afdeling zou moeten anticiperen op deze behoeften. Maatwerk wordt vaak gezien als een dure oplossing, waardoor men kiest voor grote, minder flexibele softwarepakketten die alleen na dure trainingen volledig benut kunnen worden.
Hierdoor moeten werknemers zich aanpassen aan de software, in plaats van dat de software aansluit bij de werkprocessen en het karakter van het bedrijf.
## De kracht van open standaarden en open-source oplossingen
Een mogelijke oplossing voor dit probleem is het omarmen van open standaarden en open-source oplossingen. Deze benadering maakt kantoorautomatisering dynamischer en beter aanpasbaar aan de behoeften van de samenleving.
Open-source software biedt flexibiliteit en controle, waardoor bedrijven hun workflows kunnen optimaliseren zonder afhankelijk te zijn van dure licenties of beperkte functionaliteiten.
## Voorbeelden van open-source kantoorsoftware
Er zijn diverse open-source tools beschikbaar die kunnen bijdragen aan een flexibelere werkomgeving:
### LibreOffice
Een volwaardig kantoorsoftwarepakket dat een uitstekend alternatief biedt voor commerciële producten.
### Nextcloud
Een cloudoplossing voor documentbeheer en samenwerking, die bedrijven in staat stelt hun eigen cloudomgeving te beheren.
### Thunderbird
Een open-source e-mailclient die flexibiliteit en controle biedt over e-mailbeheer.
## Integratie van ICT en bedrijfsafdelingen
Om de kloof tussen ICT en de werkvloer te overbruggen, is het essentieel dat ICT-afdelingen nauwer samenwerken met verschillende bedrijfsafdelingen. Door gezamenlijk te bepalen welke tools en systemen het beste aansluiten bij de werkprocessen, kunnen organisaties efficiënter en effectiever opereren.
Deze integratie bevordert niet alleen de productiviteit, maar ook de tevredenheid en betrokkenheid van medewerkers.
## Conclusie
In een tijd waarin digitalisering en flexibiliteit centraal staan, is het cruciaal dat ICT-afdelingen zich aanpassen aan de behoeften van de werkvloer. Door open standaarden en open-source oplossingen te omarmen en een nauwe samenwerking met andere afdelingen te zoeken, kunnen organisaties een dynamische en efficiënte werkomgeving creëren die aansluit bij de moderne samenleving.

View File

@ -1,63 +0,0 @@
# Standaardisatie: Het Belang ervan en de Tegenstelling tussen Commerciële Bedrijven en Open-Source Gemeenschappen
Standaardisatie is een cruciaal concept in de moderne wereld, met name in de technologie-industrie. Het verwijst naar het vaststellen van gemeenschappelijke normen en specificaties voor producten, processen, protocollen en systemen. Deze normen dienen als de ruggengraat van interoperabiliteit en efficiëntie in verschillende sectoren, zoals informatietechnologie, telecommunicatie, gezondheidszorg en meer.
## Waarom is Standaardisatie Belangrijk?
### 1. Interoperabiliteit
Standaardisatie zorgt ervoor dat verschillende systemen, producten en software met elkaar kunnen communiceren en samenwerken. Dit vergemakkelijkt de uitwisseling van informatie en diensten tussen verschillende leveranciers en platforms.
Bijvoorbeeld, standaardisatie van internetprotocollen maakt het mogelijk dat verschillende apparaten en websites wereldwijd met elkaar kunnen communiceren.
### 2. Efficiëntie
Standaardisatie kan de efficiëntie verbeteren door het verminderen van redundantie en complexiteit. Wanneer bedrijven en organisaties dezelfde normen volgen, kunnen ze resources beter beheren en kosten besparen.
Het voorkomt bijvoorbeeld dat ze verschillende aangepaste oplossingen moeten ontwikkelen voor vergelijkbare taken.
### 3. Veiligheid en Betrouwbaarheid
Standaardisatie kan de veiligheid en betrouwbaarheid van producten en systemen verbeteren. Gemeenschappelijke normen stellen minimumeisen vast voor bijvoorbeeld cybersecurity en kwaliteitscontrole, wat de bescherming van gegevens en de integriteit van systemen bevordert.
### 4. Innovatie en Concurrentie
Standaardisatie kan innovatie stimuleren door bedrijven aan te moedigen nieuwe technologieën te ontwikkelen die aan de normen voldoen. Het kan ook de concurrentie bevorderen, omdat het de toetreding van nieuwe spelers vergemakkelijkt door hen een gemeenschappelijke basis te bieden om op voort te bouwen.
## Waarom Commerciële Bedrijven van Standaardisatie Afwijken
Commerciële bedrijven hebben soms redenen om van standaardisatie af te wijken, vooral als ze streven naar concurrentievoordeel, vendor lock-in of maximale winst.
### 1. Vendor Lock-In
Sommige bedrijven willen klanten aan zich binden door proprietaire technologieën te gebruiken die niet compatibel zijn met die van andere leveranciers. Dit creëert een zogenaamde "vendor lock-in", waarbij klanten moeilijk kunnen overstappen naar concurrenten.
### 2. Concurrentievoordeel
Bedrijven kunnen proberen een uniek concurrentievoordeel te behouden door niet-conforme technologieën of protocollen te gebruiken. Dit kan tijdelijk voordelig zijn, maar kan de algemene interoperabiliteit belemmeren.
### 3. Winstmaximalisatie
Sommige bedrijven willen maximale winst behalen en zien geen voordeel in het delen van kennis of het bevorderen van open normen.
## Waarom Open-Source Gemeenschappen Standaardisatie Omarmen
Open-source gemeenschappen, daarentegen, hebben vaak een sterke affiniteit met standaardisatie vanwege de voordelen die het biedt aan samenwerking en innovatie.
### 1. Samenwerking en Gedeelde Waarden
Open-source gemeenschappen gedijen op samenwerking, delen en transparantie. Het omarmen van standaardisatie past goed bij deze waarden, omdat het de basis legt voor gemeenschappelijke ontwikkeling en kennisuitwisseling.
### 2. Toegankelijkheid en Gelijkheid
Open standaarden bevorderen toegankelijkheid en gelijkheid, omdat ze de drempels voor deelname en concurrentie verlagen. Iedereen kan bijdragen aan open-source projecten en gebruikmaken van de resulterende standaarden.
### 3. Innovatie en Duurzaamheid
Open-source projecten kunnen innovatie stimuleren door een bredere groep mensen en organisaties bij het ontwikkelingsproces te betrekken. Dit leidt vaak tot duurzamere oplossingen die langer relevant blijven.
## Conclusie
Kortom, standaardisatie is een essentieel concept dat de basis vormt voor interoperabiliteit, efficiëntie en innovatie in verschillende industrieën. Terwijl commerciële bedrijven soms van standaardisatie kunnen afwijken om hun eigen belangen te behartigen, omarmen open-source gemeenschappen vaak actief standaardisatie vanwege de voordelen van samenwerking, toegankelijkheid en duurzaamheid die het met zich meebrengt.
Het evenwicht tussen deze twee benaderingen speelt een cruciale rol bij het vormgeven van de technologische wereld waarin we leven.

View File

@ -1,33 +0,0 @@
# Welkom, ik ben Edwin
**Innovatie en Technologische Passie: Het Inspirerende Verhaal van Edwin Noorlander**
Mijn naam is Edwin Noorlander, en ik wil graag mijn inspirerende reis met jullie delen. Mijn leven is doordrenkt van innovatie en een onwankelbare omhelzing van technologie als mijn leidraad naar waar ik nu ben. Mijn verhaal is er een van vastberadenheid, autodidactisch leren en een diepe liefde voor complexiteit.
## Uitdagingen als Drijfveer
Laat me beginnen door te benadrukken dat mijn pad niet zonder uitdagingen is geweest. Van jongs af aan heb ik dyslexie en ADHD ervaren, wat mijn leerproces op traditionele scholen zeker niet eenvoudig maakte. Maar deze obstakels hebben me nooit ontmoedigd. In plaats daarvan dienden ze als drijfveer om mijn eigen weg te vinden in de wereld van technologie.
## Autodidactisch Leren
Ik heb altijd technologie als mijn roeping beschouwd, en dit dreef me om een unieke weg van zelfontdekking te bewandelen. In tegenstelling tot traditionele onderwijsroutes koos ik voor praktische hands-on ervaringen, en ondanks de uitdagingen die dyslexie en ADHD met zich meebrengen, leerde ik mezelf programmeren in diverse programmeertalen, waaronder ASM, C/C++, Java, PHP, HTML, JavaScript en SCSS. Deze diversiteit aan talen stelde me in staat om complexe problemen op te lossen en innovatieve oplossingen te creëren.
## Micro-elektronica en Ruimtevaart
Mijn passie reikte verder dan alleen software; ik dook diep in de wereld van micro-elektronica, waar ik leerde om microprocessors te programmeren en sensoren en actuatoren aan te sturen. Dit gaf me de mogelijkheid om fysieke systemen te bouwen en mijn ideeën tot leven te brengen.
Naast mijn technologische avonturen was ik altijd gefascineerd door astronomie en ruimtevaart, waar ik mijn leergierigheid en passie voor ontdekking voedde.
## Huidige Werk
Momenteel werk ik als adviseur bij de overheid, waar ik mijn kennis en ervaring toepas om advies te geven over ICT-infrastructuur met betrekking tot digitaal leren. Dit stelt me in staat om bij te dragen aan de groei en ontwikkeling van de samenleving door middel van technologie.
## Missie en Doel
Mijn ultieme doel is om anderen te inspireren die zich in een vergelijkbare positie bevinden, met een handicap zoals dyslexie en ADHD, om zichzelf te blijven ontwikkelen en te leren wat ze willen leren. Ik geloof dat het nooit te laat is om te beginnen met leren en dat er altijd manieren zijn om je doelen te bereiken, zelfs als de weg er naartoe uitdagend lijkt.
Daarom heb ik deze blog gecreëerd. Hier wil ik graag mijn kennis en ervaringen delen en anderen aanmoedigen om hun passie te volgen en te blijven leren, ongeacht welke obstakels zich voordoen.
Dankjewel voor het bezoeken van mijn blog, en ik hoop je snel weer terug te zien!
Mijn naam is Edwin Noorlander, en ik ben vastbesloten om de kracht van innovatie en technologie te blijven omarmen.

View File

@ -1,37 +0,0 @@
# CodePress Blog
Welkom op de persoonlijke blog van Edwin Noorlander. Hier deel ik mijn gedachten, ervaringen en kennis over technologie, open-source software en digitale transformatie.
## Categorieën
### Over Mij
- [Welkom, ik ben Edwin](?page=blog/over-mij/welkom) - Mijn persoonlijke verhaal en achtergrond
### Open Source
- [De Toekomst van ICT](?page=blog/open-source/de-toekomst-van-ict) - Hoe open source software de werkvloer transformeert
- [Standaardisatie](?page=blog/open-source/standaardisatie) - Het belang van standaarden en de tegenstelling tussen commerciële bedrijven en open-source gemeenschappen
### Leren & Ontwikkeling
- [Kennis boven Aantallen](?page=blog/leren/kennis-boven-aantallen) - Het cruciale belang van kennis boven personeelsaantallen in de werkkracht
### ICT & Bedrijfsvoering
- [Excel als Database](?page=blog/ict/excel-als-database) - Waarom grote bedrijven en overheden Access niet toestaan
### Micro-electronica
- [Wat is Arduino](?page=blog/micro-electronica/wat-is-arduino) - Open-source elektronisch platform voor interactieve projecten
- [Leren gaat niet over perfectie](?page=blog/micro-electronica/leren-gaat-niet-over-perfectie) - Passie voor electronica en open-source hardware/software
### Hardware & ICT
- [De ware aard van ICT](?page=blog/hardware/de-ware-aard-van-ict) - Meer dan alleen computers en software
### Retro Gaming
- [Commodore 64](?page=blog/retro-gaming/commodore-64) - Een les in creativiteit en innovatie in de game-industrie
### Linux & Open Source
- [Open Source Voorbeelden](?page=blog/linux/open-source-voorbeelden) - Vijf voorbeelden van open-source software in gebruik bij commerciële bedrijven
## Over Edwin Noorlander
Ik ben adviseur bij de overheid met een passie voor technologie, innovatie en open-source software. Met een achtergrond in micro-elektronica en diverse programmeertalen, deel ik graag mijn kennis en ervaringen om anderen te inspireren.
**Meer weten?** Bezoek mijn [persoonlijke website](https://noorlander.info) of volg mijn projecten op [GitLab](https://git.noorlander.info).

1
public/engine Symbolic link
View File

@ -0,0 +1 @@
../engine

View File

@ -1,8 +1,8 @@
<?php <?php
require_once 'config.php'; require_once __DIR__ . '/../engine/core/config.php';
$config = include 'config.php'; $config = include __DIR__ . '/../engine/core/config.php';
class CodePressCMS { class CodePressCMS {
private $config; private $config;
@ -119,6 +119,21 @@ class CodePressCMS {
} elseif (file_exists($filePath . '.html')) { } elseif (file_exists($filePath . '.html')) {
$actualFilePath = $filePath . '.html'; $actualFilePath = $filePath . '.html';
$result = $this->parseHTML(file_get_contents($actualFilePath)); $result = $this->parseHTML(file_get_contents($actualFilePath));
} elseif (is_dir($filePath)) {
// Check for index files in directory
if (file_exists($filePath . '/index.md')) {
$actualFilePath = $filePath . '/index.md';
$result = $this->parseMarkdown(file_get_contents($actualFilePath));
} elseif (file_exists($filePath . '/index.php')) {
$actualFilePath = $filePath . '/index.php';
$result = $this->parsePHP($actualFilePath);
} elseif (file_exists($filePath . '/index.html')) {
$actualFilePath = $filePath . '/index.html';
$result = $this->parseHTML(file_get_contents($actualFilePath));
} else {
// Generate directory listing
return $this->generateDirectoryListing($filePath, $page);
}
} elseif (file_exists($filePath)) { } elseif (file_exists($filePath)) {
$actualFilePath = $filePath; $actualFilePath = $filePath;
$extension = pathinfo($filePath, PATHINFO_EXTENSION); $extension = pathinfo($filePath, PATHINFO_EXTENSION);
@ -166,6 +181,62 @@ class CodePressCMS {
return round($bytes, 2) . ' ' . $units[$pow]; return round($bytes, 2) . ' ' . $units[$pow];
} }
private function generateDirectoryListing($dirPath, $urlPath) {
$title = ucfirst(basename($dirPath));
$content = '<div class="row">';
$items = scandir($dirPath);
sort($items);
foreach ($items as $item) {
if ($item[0] === '.') continue;
$path = $dirPath . '/' . $item;
$relativePath = $urlPath . '/' . $item;
$itemName = ucfirst(pathinfo($item, PATHINFO_FILENAME));
if (is_dir($path)) {
$content .= '<div class="col-md-6 mb-4">';
$content .= '<div class="card h-100 border-0 rounded-0 bg-light">';
$content .= '<div class="card-body">';
$content .= '<h3 class="h5 card-title"><a href="?page=' . $relativePath . '" class="text-decoration-none text-dark"><i class="bi bi-folder me-2"></i> ' . $itemName . '</a></h3>';
$content .= '</div></div></div>';
} elseif (preg_match('/\.(md|php|html)$/', $item)) {
// Remove extension from URL for cleaner links
$cleanPath = preg_replace('/\.[^.]+$/', '', $relativePath);
// Get preview content
$preview = '';
$fileContent = file_get_contents($path);
// Extract title if possible
$fileTitle = $itemName;
if (preg_match('/^#\s+(.+)$/m', $fileContent, $matches)) {
$fileTitle = trim($matches[1]);
}
// Extract preview text (first paragraph)
$fileContent = strip_tags($this->parseMarkdown($fileContent)['content']);
$preview = substr($fileContent, 0, 150) . '...';
$content .= '<div class="col-md-6 mb-4">';
$content .= '<div class="card h-100 border rounded-0">';
$content .= '<div class="card-body">';
$content .= '<h3 class="h5 card-title"><a href="?page=' . $cleanPath . '" class="text-decoration-none text-primary">' . $fileTitle . '</a></h3>';
$content .= '<p class="card-text text-muted small">' . $preview . '</p>';
$content .= '<a href="?page=' . $cleanPath . '" class="btn btn-sm btn-outline-primary rounded-0">Lees meer</a>';
$content .= '</div></div></div>';
}
}
$content .= '</div>';
return [
'title' => $title,
'content' => $content
];
}
private function getSearchResults() { private function getSearchResults() {
$query = $_GET['search']; $query = $_GET['search'];
$content = '<h2>Search Results for: "' . htmlspecialchars($query) . '"</h2>'; $content = '<h2>Search Results for: "' . htmlspecialchars($query) . '"</h2>';
@ -320,6 +391,11 @@ class CodePressCMS {
if ($i === count($parts) - 1) { if ($i === count($parts) - 1) {
$breadcrumb .= '<li class="breadcrumb-item active">' . $title . '</li>'; $breadcrumb .= '<li class="breadcrumb-item active">' . $title . '</li>';
} else { } else {
// Check if directory has index file
$dirPath = $this->config['content_dir'] . '/' . $path;
$hasIndex = file_exists($dirPath . '/index.md') || file_exists($dirPath . '/index.php') || file_exists($dirPath . '/index.html');
// Always make breadcrumb items clickable, CMS will generate index if missing
$breadcrumb .= '<li class="breadcrumb-item"><a href="?page=' . $path . '">' . $title . '</a></li>'; $breadcrumb .= '<li class="breadcrumb-item"><a href="?page=' . $path . '">' . $title . '</a></li>';
} }
} }
@ -361,5 +437,13 @@ class CodePressCMS {
} }
} }
// Block direct access to content files
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
if (strpos($requestUri, '/content/') !== false) {
http_response_code(403);
echo '<h1>403 - Forbidden</h1><p>Direct access to content files is not allowed.</p>';
exit;
}
$cms = new CodePressCMS($config); $cms = new CodePressCMS($config);
$cms->render(); $cms->render();

52
public/router.php Normal file
View File

@ -0,0 +1,52 @@
<?php
// Router file for PHP development server to handle security and static files
$requestUri = $_SERVER['REQUEST_URI'];
$parsedUrl = parse_url($requestUri);
$path = $parsedUrl['path'];
// Block direct access to content directory
if (strpos($path, '/content/') === 0) {
http_response_code(403);
echo '<h1>403 - Forbidden</h1><p>Direct access to content files is not allowed.</p>';
return true;
}
// Block access to sensitive files
$sensitiveFiles = ['.htaccess', 'config.php'];
foreach ($sensitiveFiles as $file) {
if (basename($path) === $file && dirname($path) === '/') {
http_response_code(403);
echo '<h1>403 - Forbidden</h1><p>Access to this file is not allowed.</p>';
return true;
}
}
// Serve static files from engine/assets
if (strpos($path, '/engine/') === 0) {
$filePath = __DIR__ . $path;
if (file_exists($filePath)) {
// Set appropriate content type
$extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
$mimeTypes = [
'css' => 'text/css',
'js' => 'application/javascript',
'svg' => 'image/svg+xml',
'woff' => 'font/woff',
'woff2' => 'font/woff2',
'ttf' => 'font/ttf'
];
if (isset($mimeTypes[$extension])) {
header('Content-Type: ' . $mimeTypes[$extension]);
}
// Serve the file
readfile($filePath);
return true;
}
}
// Route all other requests to index.php
include __DIR__ . '/index.php';
return true;

1
server.log Normal file
View File

@ -0,0 +1 @@
[Wed Nov 19 17:58:28 2025] Failed to listen on localhost:8080 (reason: Address already in use)

View File

@ -42,12 +42,43 @@
border-right: 1px solid #dee2e6; border-right: 1px solid #dee2e6;
overflow-y: auto; overflow-y: auto;
flex-shrink: 0; flex-shrink: 0;
transition: transform 0.3s ease;
position: relative;
z-index: 1000;
}
.sidebar.collapsed {
transform: translateX(-250px);
}
.sidebar-toggle {
position: fixed;
top: 80px;
left: 10px;
z-index: 1001;
background-color: #0d6efd;
color: white;
border: none;
border-radius: 5px;
padding: 8px 12px;
cursor: pointer;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
transition: all 0.3s ease;
}
.sidebar-toggle:hover {
background-color: #0a58ca;
transform: scale(1.05);
}
.sidebar-toggle.shifted {
left: 270px;
}
.main-content.shifted {
margin-left: 0;
} }
.main-content { .main-content {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 20px; padding: 20px;
transition: margin-left 0.3s ease;
} }
.folder-toggle { .folder-toggle {
@ -155,6 +186,26 @@
font-size: 0.9rem; font-size: 0.9rem;
color: #6c757d; color: #6c757d;
} }
.site-info a {
color: #0d6efd;
text-decoration: none;
}
.site-info a:hover {
text-decoration: underline;
}
.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; max-width: 300px;
} }
@ -211,7 +262,10 @@
<div class="main-wrapper"> <div class="main-wrapper">
<div class="content-wrapper"> <div class="content-wrapper">
<nav class="sidebar"> <button class="sidebar-toggle" id="sidebarToggle">
<i class="bi bi-list"></i>
</button>
<nav class="sidebar" id="sidebar">
<div class="pt-3"> <div class="pt-3">
<ul class="nav flex-column"> <ul class="nav flex-column">
{{menu}} {{menu}}
@ -242,7 +296,7 @@
{{file_info}} {{file_info}}
</div> </div>
<div class="site-info"> <div class="site-info">
<small class="text-muted">Powered by CodePress CMS</small> <small class="text-muted">Powered by <a href="https://git.noorlander.info/E.Noorlander/CodePress.git" target="_blank" rel="noopener">CodePress CMS</a></small>
</div> </div>
</div> </div>
</div> </div>
@ -253,6 +307,27 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Sidebar toggle functionality
const sidebarToggle = document.getElementById('sidebarToggle');
const sidebar = document.getElementById('sidebar');
const mainContent = document.querySelector('.main-content');
sidebarToggle.addEventListener('click', function() {
sidebar.classList.toggle('collapsed');
mainContent.classList.toggle('shifted');
sidebarToggle.classList.toggle('shifted');
// Change icon
const icon = this.querySelector('i');
if (sidebar.classList.contains('collapsed')) {
icon.classList.remove('bi-list');
icon.classList.add('bi-chevron-right');
} else {
icon.classList.remove('bi-chevron-right');
icon.classList.add('bi-list');
}
});
// Open folders that contain the current active page // Open folders that contain the current active page
const activeLink = document.querySelector('.nav-link.active'); const activeLink = document.querySelector('.nav-link.active');
if (activeLink) { if (activeLink) {