## Major CodePress CMS Enhancement

### 🚀 New Features
- **League CommonMark Integration**: Replaced basic Markdown parser with full CommonMark 2.7 support
- **Bootstrap Sass Architecture**: Modern SCSS build system with Bootstrap 5.3.8
- **Enhanced Navigation**: Uses filenames instead of H1 titles for consistency
- **Improved Styling**: Transparent navigation backgrounds, no rounded corners

### 🎨 UI/UX Improvements
- Navigation items now use formatted filenames (e.g., "kennis-boven-aantallen" → "Kennis Boven Aantallen")
- Transparent navigation backgrounds with subtle hover effects
- Removed rounded corners from first-level navigation
- 50% opacity navigation background using Bootstrap variables

### 🔧 Technical Improvements
- **Class Organization**: Extracted CodePressCMS and SimpleTemplate to separate files
- **Full PHPDoc Documentation**: Complete documentation for all methods
- **Modern Build Process**: npm scripts for SCSS compilation
- **Enhanced Markdown Support**: Tables, strikethrough, task lists, autolinks
- **Security**: Proper HTML sanitization with CommonMark

### 📦 Dependencies
- Added `league/commonmark` for professional Markdown parsing
- Added `bootstrap` for SCSS-based styling
- Updated `sass` build process

### 🐛 Bug Fixes
- Fixed content directory path configuration
- Resolved navigation title inconsistencies
- Improved Markdown bold/italic formatting
- Fixed homepage 404 issues

### 🔄 Migration Notes
- Content directory moved from `content/` to `public/content/`
- Navigation now displays filenames instead of content H1 titles
- CSS now compiled from SCSS source files

The CMS now provides a professional, modern experience with robust Markdown support and clean, maintainable code architecture.
This commit is contained in:
2025-11-21 20:23:20 +01:00
parent dfe2df141b
commit 863661612a
434 changed files with 36256 additions and 1027 deletions
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\ExternalLink;
use League\CommonMark\Environment\EnvironmentBuilderInterface;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\ConfigurableExtensionInterface;
use League\Config\ConfigurationBuilderInterface;
use Nette\Schema\Expect;
final class ExternalLinkExtension implements ConfigurableExtensionInterface
{
public function configureSchema(ConfigurationBuilderInterface $builder): void
{
$applyOptions = [
ExternalLinkProcessor::APPLY_NONE,
ExternalLinkProcessor::APPLY_ALL,
ExternalLinkProcessor::APPLY_INTERNAL,
ExternalLinkProcessor::APPLY_EXTERNAL,
];
$builder->addSchema('external_link', Expect::structure([
'internal_hosts' => Expect::type('string|string[]'),
'open_in_new_window' => Expect::bool(false),
'html_class' => Expect::string()->default(''),
'nofollow' => Expect::anyOf(...$applyOptions)->default(ExternalLinkProcessor::APPLY_NONE),
'noopener' => Expect::anyOf(...$applyOptions)->default(ExternalLinkProcessor::APPLY_EXTERNAL),
'noreferrer' => Expect::anyOf(...$applyOptions)->default(ExternalLinkProcessor::APPLY_EXTERNAL),
]));
}
public function register(EnvironmentBuilderInterface $environment): void
{
$environment->addEventListener(DocumentParsedEvent::class, new ExternalLinkProcessor($environment->getConfiguration()), -50);
}
}
@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\ExternalLink;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
use League\Config\ConfigurationInterface;
final class ExternalLinkProcessor
{
public const APPLY_NONE = '';
public const APPLY_ALL = 'all';
public const APPLY_EXTERNAL = 'external';
public const APPLY_INTERNAL = 'internal';
/** @psalm-readonly */
private ConfigurationInterface $config;
public function __construct(ConfigurationInterface $config)
{
$this->config = $config;
}
public function __invoke(DocumentParsedEvent $e): void
{
$internalHosts = $this->config->get('external_link/internal_hosts');
$openInNewWindow = $this->config->get('external_link/open_in_new_window');
$classes = $this->config->get('external_link/html_class');
foreach ($e->getDocument()->iterator() as $link) {
if (! ($link instanceof Link)) {
continue;
}
$host = \parse_url($link->getUrl(), PHP_URL_HOST);
if (! \is_string($host)) {
// Something is terribly wrong with this URL
continue;
}
if (self::hostMatches($host, $internalHosts)) {
$link->data->set('external', false);
$this->applyRelAttribute($link, false);
continue;
}
// Host does not match our list
$this->markLinkAsExternal($link, $openInNewWindow, $classes);
}
}
private function markLinkAsExternal(Link $link, bool $openInNewWindow, string $classes): void
{
$link->data->set('external', true);
$this->applyRelAttribute($link, true);
if ($openInNewWindow) {
$link->data->set('attributes/target', '_blank');
}
if ($classes !== '') {
$link->data->append('attributes/class', $classes);
}
}
private function applyRelAttribute(Link $link, bool $isExternal): void
{
$options = [
'nofollow' => $this->config->get('external_link/nofollow'),
'noopener' => $this->config->get('external_link/noopener'),
'noreferrer' => $this->config->get('external_link/noreferrer'),
];
foreach ($options as $type => $option) {
switch (true) {
case $option === self::APPLY_ALL:
case $isExternal && $option === self::APPLY_EXTERNAL:
case ! $isExternal && $option === self::APPLY_INTERNAL:
$link->data->append('attributes/rel', $type);
}
}
// No rel attributes? Mark the attribute as 'false' so LinkRenderer doesn't add defaults
if (! $link->data->has('attributes/rel')) {
$link->data->set('attributes/rel', false);
}
}
/**
* @internal This method is only public so we can easily test it. DO NOT USE THIS OUTSIDE OF THIS EXTENSION!
*
* @param non-empty-string|list<non-empty-string> $compareTo
*/
public static function hostMatches(string $host, $compareTo): bool
{
foreach ((array) $compareTo as $c) {
if (\strpos($c, '/') === 0) {
if (\preg_match($c, $host)) {
return true;
}
} elseif ($c === $host) {
return true;
}
}
return false;
}
}