Fix: /admin/content is nu boom-editor + image-grootte knop + custom syntax

- /admin/content is nu de boom-editor (plugin-file-tree structuur),
  consistent met plugin- en thema-editors. Oude tabelweergave verhuisd
  naar /admin/content-list, bereikbaar via 'Boom weergave'/'Lijst
  weergave' knoppen. /admin/content-files redirect naar /admin/content.
- Image-grootte knop in markdown editor-toolbar: selecteer ![alt](url),
  klik knop, breedte/hoogte dialog, {:width=... height=...} syntax
  toevoegen/vervangen. Bestaande waarden worden in prompts getoond.
- Custom-grootte syntax {:width=...} nu ook correct herschreven naar
  /-media/ endpoint (was alleen gewone ![alt](url) herschreven).
- Handleidingen bijgewerkt (content-beheer.md NL+EN: boom/lijst
  weergave uitleg + image-grootte + images in content sectie)
- Release notes v2.6.2 bijgewerkt
- Pentest 30/30, WCAG 25/25
This commit is contained in:
2026-08-19 12:15:56 +00:00
parent e926a3a40d
commit 97c4d52c78
17 changed files with 171 additions and 81 deletions
+4 -2
View File
@@ -104,12 +104,14 @@
- [x] Pentest controles uitgevoerd (30/30)
- [x] WCAG 2.1 AA accessibility tests (25/25)
- [x] Thema editor (thema's net zo bewerkbaar maken als plugins): bestandsbrowser zijbalk, nieuw bestand, uploaden, verwijderen, verplaatsen, SCSS compileren vanuit editor, thema activeren/verwijderen, nieuw thema met basis-kopie, uniforme thema-structuur
- [x] Content editor (content consistent met plugins en thema's): bestandsbrowser zijbalk naast bestaande lijst weergave, nieuw bestand/map, uploaden, verwijderen, verplaatsen, mappen beheer, layout/plugins selectie, git/backup integratie in editor zijbalk
- [x] Content editor (content consistent met plugins en thema's): bestandsbrowser zijbalk nu op /admin/content (vervangt tabelweergave), nieuw bestand/map, uploaden, verwijderen, verplaatsen, mappen beheer, layout/plugins selectie, git/backup integratie in editor zijbalk; oude tabelweergave verhuisd naar /admin/content-list
- [x] Uniformiteit: scanEditorFiles() unified scanner (scanPluginFiles/scanThemeFiles/scanContentFiles als dunne wrappers)
- [x] Media invoegen in editor: ?theme= scope voor media-list endpoint + _media-modal.twig
- [x] Bug: thema-naam niet overgenomen bij kopiëren — title in theme.json wordt overschreven met nieuwe themanaam; README.md krijgt nieuwe header
- [x] Bug: mappen met `_` prefix zichtbaar in frontend navigatie — scanDirectory/searchInDirectory/scanForPageTitles skippen nu `_` prefix (consistente filtering met `.` en `-`)
- [x] Bug: images in markdown niet weergegeven — processContent() herschrijft lokale image/link URLs naar /-media/ endpoint (relatief, /content/, subdir); externe URLs ongewijzigd gelaten
- [x] Bug: images in markdown niet weergegeven — processContent() herschrijft lokale image/link URLs naar /-media/ endpoint (relatief, /content/, subdir, custom-grootte syntax); externe URLs ongewijzigd gelaten
- [x] UX: /admin/content is nu de boom-editor (plugin-file-tree structuur); oude tabelweergave op /admin/content-list met "Boom weergave"/"Lijst weergave" knoppen
- [x] UX: image-grootte knop in editor-toolbar (markdown) — selecteer ![alt](url), klik knop, breedte/hoogte dialog, {:width=... height=...} syntax toevoegen/vervangen
- [x] Handleidingen in guide/ bijgewerkt (thema-beheer.md NL+EN volledig herschreven met thema-editor uitleg; content-beheer.md NL+EN volledig herschreven met content-editor uitleg)
- [x] Verslag gemaakt (docs/release-notes/v2.6.2.md)
- [x] Versie verhoogd naar 2.6.2
+1 -1
View File
@@ -16,7 +16,7 @@ class AdminAuth
*/
public const ROLE_PERMISSIONS = [
'admin' => ['*'],
'content-manager' => ['dashboard', 'content', 'content-files', 'content-edit', 'content-new', 'content-delete', 'content-file-upload', 'content-file-delete', 'content-file-move', 'content-dir-create', 'content-dir-create-in', 'content-dir-rename', 'content-dir-rename-in', 'content-dir-delete', 'content-dir-delete-in', 'content-move', 'content-backup', 'content-restore', 'content-git-init', 'content-git-commit', 'content-git-restore', 'guide', 'logout'],
'content-manager' => ['dashboard', 'content', 'content-list', 'content-files', 'content-edit', 'content-new', 'content-delete', 'content-file-upload', 'content-file-delete', 'content-file-move', 'content-dir-create', 'content-dir-create-in', 'content-dir-rename', 'content-dir-rename-in', 'content-dir-delete', 'content-dir-delete-in', 'content-move', 'content-backup', 'content-restore', 'content-git-init', 'content-git-commit', 'content-git-restore', 'guide', 'logout'],
'bi-manager' => ['dashboard', 'statistics', 'logs', 'guide', 'logout'],
'site-admin' => ['dashboard', 'theme', 'theme-new', 'theme-edit', 'theme-file-upload', 'theme-file-delete', 'theme-file-move', 'theme-activate', 'theme-delete', 'theme-scss', 'plugins', 'plugins-new', 'plugins-edit', 'plugins-config', 'plugins-toggle', 'plugins-delete', 'statistics', 'logs', 'update', 'guide', 'logout'],
];
@@ -42,6 +42,7 @@
{ cmd: 'quote', icon: 'bi-chat-quote', title: 'Citaat' },
null,
{ cmd: 'hr', icon: 'bi-hr', title: 'Horizontale lijn' },
{ cmd: 'image-size', icon: 'bi-arrows-angle-expand', title: 'Afbeelding grootte' },
null,
{ cmd: 'media', icon: 'bi-images', title: 'Media invoegen' }
],
@@ -152,6 +153,43 @@
modal.show();
}
break;
case 'image-size':
// Set width/height on a markdown image: ![alt](url){:width="W" height="H"}
var selImg = editor.getSelection();
if (!selImg) {
// No selection — ask the user to select an image first
alert('Selecteer eerst een afbeelding in de editor (![alt](url)) om de grootte in te stellen.');
editor.focus();
break;
}
// Match ![alt](url) optionally followed by {:...}
var imgMatch = selImg.match(/^!\[([^\]]*)\]\(([^)]+)\)(\s*\{:([^}]*)\})?/);
if (!imgMatch) {
alert('Selectie is geen afbeelding. Selecteer een afbeelding in markdown formaat: ![alt](url)');
editor.focus();
break;
}
var alt = imgMatch[1];
var url = imgMatch[2];
// Parse existing width/height from the {:...} block
var existingAttrs = imgMatch[4] || '';
var existingW = (existingAttrs.match(/width\s*=\s*"([^"]*)"/) || [])[1] || '';
var existingH = (existingAttrs.match(/height\s*=\s*"([^"]*)"/) || [])[1] || '';
var width = prompt('Breedte (px of %, leeg = niet instellen):', existingW);
if (width === null) { editor.focus(); break; }
var height = prompt('Hoogte (px of %, leeg = niet instellen):', existingH);
if (height === null) { editor.focus(); break; }
// Build the {:width=... height=...} suffix
var attrs = [];
if (width.trim() !== '') attrs.push('width="' + width.replace(/"/g, '') + '"');
if (height.trim() !== '') attrs.push('height="' + height.replace(/"/g, '') + '"');
var replacement = '![' + alt + '](' + url + ')';
if (attrs.length > 0) {
replacement += '{:' + attrs.join(' ') + '}';
}
editor.replaceSelection(replacement);
editor.focus();
break;
}
}
@@ -17,7 +17,7 @@
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> {{ ta.rename|default('Hernoemen') }}
</button>
<a href="/admin/content?dir={{ dir|url_encode }}" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
<a href="/admin/content-list?dir={{ dir|url_encode }}" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
</div>
</form>
{% endblock %}
@@ -5,7 +5,7 @@
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
<h2 class="mb-0"><i class="bi bi-pencil"></i> {{ ta.rename_folder|default('Map hernoemen') }}</h2>
<a href="/admin/content-files" class="btn btn-outline-secondary btn-sm">
<a href="/admin/content" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left"></i> {{ ta.back|default('Terug') }}
</a>
</div>
@@ -28,7 +28,7 @@
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> {{ ta.rename|default('Hernoemen') }}
</button>
<a href="/admin/content-files" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
<a href="/admin/content" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
</div>
</form>
{% endblock %}
@@ -71,7 +71,7 @@
<i class="bi bi-eye"></i> {{ ta.preview|default('Preview') }}
</a>
{% endif %}
<a href="/admin/content?dir={{ fileDir|url_encode }}" class="btn btn-outline-secondary" id="back-btn">{{ ta.back|default('Terug') }}</a>
<a href="/admin/content-list?dir={{ fileDir|url_encode }}" class="btn btn-outline-secondary" id="back-btn">{{ ta.back|default('Terug') }}</a>
</div>
</form>
</div>
@@ -49,7 +49,7 @@
{% endif %}
</div>
{% else %}
<a href="/admin/content-files?file={{ n.path|url_encode }}"
<a href="/admin/content?file={{ n.path|url_encode }}"
class="tree-link {{ isActive ? 'is-active' : '' }}"
title="{{ n.path }}">
<span class="tree-chevron-spacer"></span>
@@ -159,7 +159,7 @@
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#newDirModal" data-in-dir="">
<i class="bi bi-folder-plus"></i> {{ ta.new_folder|default('Nieuwe map') }}
</button>
<a href="/admin/content" class="btn btn-outline-secondary btn-sm">
<a href="/admin/content-list" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-list"></i> {{ ta.list_view|default('Lijst weergave') }}
</a>
</div>
@@ -228,7 +228,7 @@
{% if loop.last %}
<li class="breadcrumb-item active" aria-current="page">{{ part }}</li>
{% else %}
<li class="breadcrumb-item"><a href="/admin/content-files?file={{ crumbPath|url_encode }}">{{ part }}</a></li>
<li class="breadcrumb-item"><a href="/admin/content?file={{ crumbPath|url_encode }}">{{ part }}</a></li>
{% endif %}
{% endfor %}
</ol>
@@ -236,7 +236,7 @@
{% endif %}
{% if isEditable %}
<form method="POST" action="/admin/content-files?file={{ relFile|url_encode }}" id="editor-form">
<form method="POST" action="/admin/content?file={{ relFile|url_encode }}" id="editor-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="card shadow-sm">
<div class="card-body">
@@ -297,7 +297,7 @@
<a href="/{{ currentLang }}{% if fileDir %}/{{ fileDir }}{% endif %}/{{ fileName|replace({'.md': '', '.php': '', '.html': ''}) }}" target="_blank" class="btn btn-outline-info" title="{{ ta.open_new_tab|default('Open in nieuw tabblad') }}">
<i class="bi bi-eye"></i> {{ ta.preview|default('Preview') }}
</a>
<a href="/admin/content?dir={{ fileDir|url_encode }}" class="btn btn-outline-secondary">{{ ta.list_view|default('Lijst weergave') }}</a>
<a href="/admin/content-list?dir={{ fileDir|url_encode }}" class="btn btn-outline-secondary">{{ ta.list_view|default('Lijst weergave') }}</a>
</div>
</form>
{% else %}
@@ -319,7 +319,7 @@
<div class="modal fade" id="newFileModal" tabindex="-1" aria-labelledby="newFileModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="POST" action="/admin/content-files">
<form method="POST" action="/admin/content">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="action" value="new_file">
<input type="hidden" name="in_dir" id="newFileDir" value="">
@@ -5,7 +5,7 @@
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
<h2 class="mb-0"><i class="bi bi-arrows-move"></i> {{ ta.file|default('Bestand') }} {{ ta.move_suffix|default('verplaatsen') }}</h2>
<a href="/admin/content-files?file={{ relFile|url_encode }}" class="btn btn-outline-secondary btn-sm">
<a href="/admin/content?file={{ relFile|url_encode }}" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left"></i> {{ ta.back|default('Terug') }}
</a>
</div>
@@ -32,7 +32,7 @@
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> {{ ta.move|default('Verplaatsen') }}
</button>
<a href="/admin/content-files?file={{ relFile|url_encode }}" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
<a href="/admin/content?file={{ relFile|url_encode }}" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
</div>
</form>
{% endblock %}
@@ -34,7 +34,7 @@
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> {{ ta.create|default('Aanmaken') }}
</button>
<a href="/admin/content?dir={{ dir|url_encode }}" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
<a href="/admin/content-list?dir={{ dir|url_encode }}" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
</div>
</form>
</div>
+7 -4
View File
@@ -6,6 +6,9 @@
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-file-earmark-text"></i> {{ ta.content|default('Content') }}</h2>
<div>
<a href="/admin/content" class="btn btn-outline-primary btn-sm me-1" title="{{ ta.tree_view|default('Boom weergave') }}">
<i class="bi bi-diagram-3"></i> {{ ta.tree_view|default('Boom weergave') }}
</a>
<button type="button" class="btn btn-outline-success btn-sm me-1" data-bs-toggle="collapse" data-bs-target="#uploadForm">
<i class="bi bi-cloud-upload"></i> {{ ta.upload|default('Upload') }}
</button>
@@ -24,7 +27,7 @@
<div class="collapse mb-4" id="uploadForm">
<div class="card shadow-sm">
<div class="card-body">
<form method="POST" action="/admin/content?dir={{ subdir|url_encode }}" enctype="multipart/form-data">
<form method="POST" action="/admin/content-list?dir={{ subdir|url_encode }}" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="mb-3">
<label for="file" class="form-label">{{ ta.select_files|default('Bestanden selecteren') }}</label>
@@ -43,7 +46,7 @@
{% set parentDir = subdir|split('/')|slice(0, -1)|join('/') %}
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/admin/content"><i class="bi bi-house"></i></a></li>
<li class="breadcrumb-item"><a href="/admin/content-list"><i class="bi bi-house"></i></a></li>
{% set crumbPath = '' %}
{% for crumb in subdir|split('/') %}
{% set crumbPath = crumbPath ? crumbPath ~ '/' ~ crumb : crumb %}
@@ -51,7 +54,7 @@
{% if crumbPath == subdir %}
{{ crumb }}
{% else %}
<a href="/admin/content?dir={{ crumbPath|url_encode }}">{{ crumb }}</a>
<a href="/admin/content-list?dir={{ crumbPath|url_encode }}">{{ crumb }}</a>
{% endif %}
</li>
{% endfor %}
@@ -86,7 +89,7 @@
<tr data-name="{{ item.name|lower }}">
<td>
{% if item.is_dir %}
<a href="/admin/content?dir={{ item.path|url_encode }}">
<a href="/admin/content-list?dir={{ item.path|url_encode }}">
<i class="bi bi-folder-fill text-warning"></i> {{ item.name }}
</a>
{% else %}
+7 -2
View File
@@ -117,8 +117,13 @@ De file-upload/delete/move handler-families zijn per-scope behouden (plugin: pro
## Bug fixes
- **Thema-naam niet overgenomen bij kopiëren** — bij het aanmaken van een nieuw thema met een basisthema werd de `title` uit het basisthema's `theme.json` gekopieerd; nu wordt de `title` overschreven met de nieuwe themanaam en krijgt `README.md` een nieuwe header
- **Mappen met `_` prefix zichtbaar in frontend navigatie** — mappen zoals `_drafts`, `_data` werden in het menu getoond omdat `scanDirectory()` alleen `.` en `-` prefixen oversloeg. Nu worden ook `_`-prefix mappen overgeslagen in `scanDirectory()`, `searchInDirectory()` en `scanForPageTitles()` (consistente filtering)
- **Images in markdown niet weergegeven** — `![alt](test.svg)` werd gerenderd als `<img src="test.svg">` (relatieve URL → 404) of `![alt](/content/test.svg)` als `<img src="/content/test.svg">` (content/ staat buiten webroot → 404). `processContent()` herschrijft nu lokale image/link URLs naar de `/-media/` endpoint die bestanden uit `content/` serveert met juiste MIME-type. Externe URLs (`http(s)://`), `/-media/`, `/-assets/`, `/themes/`, `/plugins/`, `/admin/`, `data:` en `mailto:` worden ongewijzigd gelaten.
- **Mappen met `_` prefix zichtbaar in frontend navigatie** — mappen zoals `_drafts`, `_data`, `_images` werden in het menu getoond omdat `scanDirectory()` alleen `.` en `-` prefixen oversloeg. Nu worden ook `_`-prefix mappen overgeslagen in `scanDirectory()`, `searchInDirectory()` en `scanForPageTitles()` (consistente filtering)
- **Images in markdown niet weergegeven** — `![alt](test.svg)` werd gerenderd als `<img src="test.svg">` (relatieve URL → 404) of `![alt](/content/test.svg)` als `<img src="/content/test.svg">` (content/ staat buiten webroot → 404). `processContent()` herschrijft nu lokale image/link URLs naar de `/-media/` endpoint die bestanden uit `content/` serveert met juiste MIME-type. Externe URLs (`http(s)://`), `/-media/`, `/-assets/`, `/themes/`, `/plugins/`, `/admin/`, `data:` en `mailto:` worden ongewijzigd gelaten. Ook de custom-grootte syntax `![alt](url){:width=... height=...}` wordt correct herschreven.
## UX verbeteringen
- **`/admin/content` is nu de boom-editor** — de hoofdroute `/admin/content` toont nu de bestandsbrowser zijbalk structuur (zoals `plugin-file-tree`), consistent met de plugin- en thema-editors. De oude tabelweergave is verhuisd naar `/admin/content-list` en is bereikbaar via de "Lijst weergave" knop. Een "Boom weergave" knop in de tabelweergave keert terug naar de boom-editor. De `content-files` route redirect naar `/admin/content` (backward compat).
- **Image-grootte knop in editor-toolbar** — nieuwe knop "Afbeelding grootte" (expand-icoon) in de markdown editor-toolbar. Selecteer een `![alt](url)` image in de editor en klik op de knop om een breedte/hoogte-dialog te krijgen. De `{:width=... height=...}` syntax wordt aan de image toegevoegd of vervangen. Bestaande waarden worden getoond in de prompts.
---
+21 -4
View File
@@ -2,12 +2,12 @@
CodePress offers two views for content management:
1. **List view** (`/admin/content`) — classic table with filter, upload, new folder/file per folder
2. **Content editor** (`/admin/content-files`) — file browser sidebar + CodeMirror editor (uniform with plugin/theme editors)
1. **Tree view** (`/admin/content`) — default, file browser sidebar + CodeMirror editor (uniform with plugin/theme editors)
2. **List view** (`/admin/content-list`) — classic table with filter, upload, new folder/file per folder
Both work side-by-side and use the same content directory. The content editor offers the same functionality as the list view, but with a nested file-tree sidebar like the plugin and theme editors — for a consistent editor experience.
Both work side-by-side and use the same content directory. Switch between views via the "List view"/"Tree view" buttons at the top right. The tree view offers the same functionality as the list view, but with a nested file-tree sidebar like the plugin and theme editors — for a consistent editor experience.
## Content editor (content-files)
## Tree view (content)
### File browser sidebar
- Shows the nested file tree of `content/`
@@ -19,6 +19,23 @@ Both work side-by-side and use the same content directory. The content editor of
### Editable file types
`.md` (Markdown), `.php` (PHP), `.html` (HTML) — consistent with the existing content-edit page.
### Set image size (Markdown)
The markdown editor toolbar has an "Image size" button (expand icon):
1. Select an image in the editor in markdown format `![alt](url)`
2. Click the "Image size" button
3. Enter width and/or height (px or %, empty = don't set)
4. The `{:width=... height=...}` syntax is added to or replaced on the image
Example:
```markdown
![logo](/content/_images/logo.png){:width="200" height="100"}
```
Existing `{:width=...}` values are shown in the prompts. The size attributes are rendered as `width`/`height` HTML attributes on the `<img>` tag in the frontend.
### Images in content
Images in `content/` are served via the `/-media/` endpoint (content/ lives outside the webroot). Markdown syntax `![alt](url)` with local URLs (relative like `image.jpg`, or absolute like `/content/_images/image.jpg`) is automatically rewritten to `/-media/...`. External URLs (`https://...`) are left untouched.
### Create a new file
- Click **New file** (at the top or per folder in the sidebar)
- Enter a path within content (e.g. `en.page` or `blog/en.post`)
+21 -4
View File
@@ -2,12 +2,12 @@
CodePress biedt twee weergaven voor content-beheer:
1. **Lijst weergave** (`/admin/content`) — klassieke tabel met filter, upload, nieuwe map/bestand per map
2. **Content editor** (`/admin/content-files`) — bestandsbrowser zijbalk + CodeMirror editor (uniform met plugin/theme-editors)
1. **Boom weergave** (`/admin/content`) — standaard, bestandsbrowser zijbalk + CodeMirror editor (uniform met plugin/theme-editors)
2. **Lijst weergave** (`/admin/content-list`) — klassieke tabel met filter, upload, nieuwe map/bestand per map
Beide werken naast elkaarlijk en gebruiken dezelfde content-map. De content-editor biedt dezelfde functionaliteit als de lijst weergave, maar dan met een geneste bestandsboom zijbalk zoals de plugin- en thema-editors — voor consistente editor-ervaring.
Beide werken naast elkaar en gebruiken dezelfde content-map. Wissel tussen de weergaven via de "Lijst weergave"/"Boom weergave" knoppen rechtsboven. De boom weergave biedt dezelfde functionaliteit als de lijst weergave, maar dan met een geneste bestandsboom zijbalk zoals de plugin- en thema-editors — voor consistente editor-ervaring.
## Content editor (content-files)
## Boom weergave (content)
### Bestandsbrowser zijbalk
- Toont de geneste bestandsboom van `content/`
@@ -19,6 +19,23 @@ Beide werken naast elkaarlijk en gebruiken dezelfde content-map. De content-edit
### Bewerkbare bestandstypen
`.md` (Markdown), `.php` (PHP), `.html` (HTML) — consistent met de bestaande content-edit pagina.
### Image-grootte instellen (Markdown)
In de markdown editor-toolbar staat een "Afbeelding grootte" knop (expand-icoon):
1. Selecteer een afbeelding in de editor in markdown formaat `![alt](url)`
2. Klik op de "Afbeelding grootte" knop
3. Vul breedte en/of hoogte in (px of %, leeg = niet instellen)
4. De `{:width=... height=...}` syntax wordt aan de afbeelding toegevoegd of vervangen
Voorbeeld:
```markdown
![logo](/content/_images/logo.png){:width="200" height="100"}
```
Bestaande `{:width=...}` waarden worden in de prompts getoond. De grootte-attributen worden in de frontend als `width`/`height` HTML-attributen op de `<img>` tag gezet.
### Images in content
Images in `content/` worden via de `/-media/` endpoint geserveerd (content/ staat buiten de webroot). De markdown syntax `![alt](url)` met lokale URLs (relatief zoals `image.jpg`, of absoluut zoals `/content/_images/image.jpg`) wordt automatisch herschreven naar `/-media/...`. Externe URLs (`https://...`) worden ongewijzigd gelaten.
### Nieuw bestand aanmaken
- Klik op **Nieuw bestand** (bovenaan of per map in de zijbalk)
- Geef een pad op binnen content (bijv. `nl.pagina` of `blog/nl.post`)
+1
View File
@@ -100,6 +100,7 @@ return [
'content_upload_help' => 'Dateien werden in content/ hochgeladen. Erlaubt: Bilder, Video, Audio, PDF, ZIP, Office-Dokumente, CSS, SCSS, JS, JSON, HTML, MD.',
'file_type' => 'Dateityp',
'list_view' => 'Listenansicht',
'tree_view' => 'Baumansicht',
'rename_folder' => 'Ordner umbenennen',
'rename_prefix' => 'Umbenennen',
'new_name' => 'Neuer Name',
+1
View File
@@ -100,6 +100,7 @@ return [
'content_upload_help' => 'Files are uploaded to content/. Allowed: images, video, audio, PDF, ZIP, office docs, CSS, SCSS, JS, JSON, HTML, MD.',
'file_type' => 'File type',
'list_view' => 'List view',
'tree_view' => 'Tree view',
'rename_folder' => 'Rename folder',
'rename_prefix' => 'Rename',
'new_name' => 'New name',
+1
View File
@@ -102,6 +102,7 @@ return [
'content_upload_help' => 'Bestanden worden geüpload naar content/. Toegestaan: afbeeldingen, video, audio, PDF, ZIP, office docs, CSS, SCSS, JS, JSON, HTML, MD.',
'file_type' => 'Bestandstype',
'list_view' => 'Lijst weergave',
'tree_view' => 'Boom weergave',
'rename_folder' => 'Map hernoemen',
'rename_prefix' => 'Hernoem',
'new_name' => 'Nieuwe naam',
+56 -51
View File
@@ -270,12 +270,17 @@ switch ($route) {
break;
case 'content':
handleContentFiles($auth, $appConfig, $twig, $user, $csrf, $siteConfig);
break;
case 'content-list':
handleContent($auth, $appConfig, $twig, $user, $csrf, $siteConfig);
break;
case 'content-files':
handleContentFiles($auth, $appConfig, $twig, $user, $csrf, $siteConfig);
break;
// Backward compat: redirect to the unified content editor
header('Location: /admin/content');
exit;
case 'content-file-upload':
handleContentFileUpload($auth, $appConfig, $user);
@@ -1015,18 +1020,18 @@ function handleContentFiles($auth, $config, $twig, $user, $csrf, $siteConfig): v
function handleContentFileUpload($auth, $config, $user): void
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
$contentDir = $config['content_dir'];
$realContentDir = realpath($contentDir);
if (!$realContentDir || !is_dir($realContentDir)) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
@@ -1040,13 +1045,13 @@ function handleContentFileUpload($auth, $config, $user): void
$realTarget = realpath($targetDir);
if ($realTarget === false) {
if (!@mkdir($targetDir, 0755, true)) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
$realTarget = realpath($targetDir);
}
if (!$realTarget || strpos($realTarget, $realContentDir) !== 0 || !is_dir($realTarget)) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
@@ -1090,7 +1095,7 @@ function handleContentFileUpload($auth, $config, $user): void
$type = $type === 'success' ? 'success' : 'danger';
}
$redir = '/admin/content-files';
$redir = '/admin/content';
if (!empty($msg)) {
$redir .= '?msg=' . urlencode(implode(' ', $msg)) . '&msgtype=' . urlencode($type);
}
@@ -1105,18 +1110,18 @@ function handleContentFileUpload($auth, $config, $user): void
function handleContentFileDelete($auth, $config, $user): void
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
$contentDir = $config['content_dir'];
$realContentDir = realpath($contentDir);
if (!$realContentDir || !is_dir($realContentDir)) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
@@ -1124,20 +1129,20 @@ function handleContentFileDelete($auth, $config, $user): void
$relFile = ltrim($relFile, '/');
foreach (explode('/', $relFile) as $seg) {
if ($seg === '..' || $seg === '.') {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
}
$base = basename($relFile);
if ($base === '' || $base[0] === '.') {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
$fullPath = $realContentDir . '/' . $relFile;
$realPath = realpath($fullPath);
if ($realPath === false || strpos($realPath, $realContentDir) !== 0 || !is_file($realPath)) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
@@ -1152,7 +1157,7 @@ function handleContentFileDelete($auth, $config, $user): void
$type = 'danger';
}
header('Location: /admin/content-files?msg=' . urlencode($msg) . '&msgtype=' . urlencode($type));
header('Location: /admin/content?msg=' . urlencode($msg) . '&msgtype=' . urlencode($type));
exit;
}
@@ -1165,7 +1170,7 @@ function handleContentFileMove($auth, $config, $twig, $user, $csrf): void
$contentDir = $config['content_dir'];
$realContentDir = realpath($contentDir);
if (!$realContentDir || !is_dir($realContentDir)) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
@@ -1173,20 +1178,20 @@ function handleContentFileMove($auth, $config, $twig, $user, $csrf): void
$relFile = ltrim($relFile, '/');
foreach (explode('/', $relFile) as $seg) {
if ($seg === '..' || $seg === '.') {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
}
$base = basename($relFile);
if ($base === '' || $base[0] === '.') {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
$fullPath = $realContentDir . '/' . $relFile;
$realPath = realpath($fullPath);
if ($realPath === false || strpos($realPath, $realContentDir) !== 0 || !is_file($realPath)) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
@@ -1241,7 +1246,7 @@ function handleContentFileMove($auth, $config, $twig, $user, $csrf): void
} elseif (rename($realPath, $newPath)) {
adminLog($config, 'info', $user['username'] . ' verplaatste content/' . $relFile . ' naar content/' . ($dest ? $dest . '/' : '') . basename($relFile));
$newRel = $dest === '' ? basename($relFile) : $dest . '/' . basename($relFile);
header('Location: /admin/content-files?file=' . urlencode($newRel) . '&msg=' . urlencode('Bestand verplaatst.') . '&msgtype=success');
header('Location: /admin/content?file=' . urlencode($newRel) . '&msg=' . urlencode('Bestand verplaatst.') . '&msgtype=success');
exit;
} else {
$err = error_get_last();
@@ -1275,18 +1280,18 @@ function handleContentFileMove($auth, $config, $twig, $user, $csrf): void
function handleContentDirCreateIn($auth, $config, $user): void
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
$contentDir = $config['content_dir'];
$realContentDir = realpath($contentDir);
if (!$realContentDir || !is_dir($realContentDir)) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
@@ -1298,19 +1303,19 @@ function handleContentDirCreateIn($auth, $config, $user): void
$dirname = trim($_POST['dirname'] ?? '');
$dirname = preg_replace('/[^a-zA-Z0-9._-]/', '-', $dirname);
if ($dirname === '' || $dirname === '.' || $dirname === '..') {
header('Location: /admin/content-files?msg=' . urlencode('Ongeldige mapnaam.') . '&msgtype=danger');
header('Location: /admin/content?msg=' . urlencode('Ongeldige mapnaam.') . '&msgtype=danger');
exit;
}
$newPath = $realContentDir . '/' . ($inDir ? $inDir . '/' : '') . $dirname;
if (file_exists($newPath)) {
header('Location: /admin/content-files?msg=' . urlencode('Map bestaat al.') . '&msgtype=danger');
header('Location: /admin/content?msg=' . urlencode('Map bestaat al.') . '&msgtype=danger');
exit;
}
if (@mkdir($newPath, 0755, true)) {
adminLog($config, 'info', $user['username'] . ' creëerde content map ' . ($inDir ? $inDir . '/' : '') . $dirname);
header('Location: /admin/content-files?msg=' . urlencode('Map aangemaakt.') . '&msgtype=success');
header('Location: /admin/content?msg=' . urlencode('Map aangemaakt.') . '&msgtype=success');
} else {
header('Location: /admin/content-files?msg=' . urlencode('Map aanmaken mislukt.') . '&msgtype=danger');
header('Location: /admin/content?msg=' . urlencode('Map aanmaken mislukt.') . '&msgtype=danger');
}
exit;
}
@@ -1324,7 +1329,7 @@ function handleContentDirRenameIn($auth, $config, $twig, $user, $csrf): void
$contentDir = $config['content_dir'];
$realContentDir = realpath($contentDir);
if (!$realContentDir || !is_dir($realContentDir)) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
@@ -1366,7 +1371,7 @@ function handleContentDirRenameIn($auth, $config, $twig, $user, $csrf): void
$messageType = 'danger';
} elseif (rename($realDir, $newPath)) {
adminLog($config, 'info', $user['username'] . ' hernoemde content map ' . $dirRel . ' naar ' . $newName);
header('Location: /admin/content-files?msg=' . urlencode('Map hernoemd.') . '&msgtype=success');
header('Location: /admin/content?msg=' . urlencode('Map hernoemd.') . '&msgtype=success');
exit;
} else {
$message = 'Hernoemen mislukt.';
@@ -1378,7 +1383,7 @@ function handleContentDirRenameIn($auth, $config, $twig, $user, $csrf): void
}
if ($dirRel === '') {
header('Location: /admin/content-files?msg=' . urlencode('Map niet gevonden.') . '&msgtype=danger');
header('Location: /admin/content?msg=' . urlencode('Map niet gevonden.') . '&msgtype=danger');
exit;
}
@@ -1403,18 +1408,18 @@ function handleContentDirRenameIn($auth, $config, $twig, $user, $csrf): void
function handleContentDirDeleteIn($auth, $config, $user): void
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
$contentDir = $config['content_dir'];
$realContentDir = realpath($contentDir);
if (!$realContentDir || !is_dir($realContentDir)) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
@@ -1422,19 +1427,19 @@ function handleContentDirDeleteIn($auth, $config, $user): void
$dirRel = ltrim($dirRel, '/');
foreach (explode('/', $dirRel) as $seg) {
if ($seg === '..' || $seg === '.') {
header('Location: /admin/content-files?msg=' . urlencode('Ongeldige map.') . '&msgtype=danger');
header('Location: /admin/content?msg=' . urlencode('Ongeldige map.') . '&msgtype=danger');
exit;
}
}
if ($dirRel === '') {
header('Location: /admin/content-files?msg=' . urlencode('Ongeldige map.') . '&msgtype=danger');
header('Location: /admin/content?msg=' . urlencode('Ongeldige map.') . '&msgtype=danger');
exit;
}
$fullPath = $realContentDir . '/' . $dirRel;
$realDir = realpath($fullPath);
if (!$realDir || !is_dir($realDir) || strpos($realDir, $realContentDir) !== 0) {
header('Location: /admin/content-files?msg=' . urlencode('Map niet gevonden.') . '&msgtype=danger');
header('Location: /admin/content?msg=' . urlencode('Map niet gevonden.') . '&msgtype=danger');
exit;
}
@@ -1445,14 +1450,14 @@ function handleContentDirDeleteIn($auth, $config, $user): void
if ($e[0] !== '.') { $isEmpty = false; break; }
}
if (!$isEmpty) {
header('Location: /admin/content-files?msg=' . urlencode('Map moet leeg zijn.') . '&msgtype=danger');
header('Location: /admin/content?msg=' . urlencode('Map moet leeg zijn.') . '&msgtype=danger');
exit;
}
if (@rmdir($realDir)) {
adminLog($config, 'info', $user['username'] . ' verwijderde content map ' . $dirRel);
header('Location: /admin/content-files?msg=' . urlencode('Map verwijderd.') . '&msgtype=success');
header('Location: /admin/content?msg=' . urlencode('Map verwijderd.') . '&msgtype=success');
} else {
header('Location: /admin/content-files?msg=' . urlencode('Map verwijderen mislukt.') . '&msgtype=danger');
header('Location: /admin/content?msg=' . urlencode('Map verwijderen mislukt.') . '&msgtype=danger');
}
exit;
}
@@ -1470,7 +1475,7 @@ function handleContentEdit($auth, $config, $twig, $user, $csrf, $siteConfig): vo
$realPath = realpath($filePath);
$realContentDir = realpath($contentDir);
if (!$realPath || !$realContentDir || strpos($realPath, $realContentDir) !== 0) {
header('Location: /admin/content');
header('Location: /admin/content-list');
exit;
}
@@ -1690,13 +1695,13 @@ function handleContentNew($auth, $config, $twig, $user, $csrf, $siteConfig): voi
function handleContentDelete($auth, $config): void
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /admin/content');
header('Location: /admin/content-list');
exit;
}
$user = $auth->getCurrentUser();
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
header('Location: /admin/content');
header('Location: /admin/content-list');
exit;
}
@@ -1710,20 +1715,20 @@ function handleContentDelete($auth, $config): void
adminLog($config, 'info', $user['username'] . ' verwijderde ' . $file);
}
header('Location: /admin/content?dir=' . urlencode(dirname($file)));
header('Location: /admin/content-list?dir=' . urlencode(dirname($file)));
exit;
}
function handleContentDirCreate($auth, $config): void
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /admin/content');
header('Location: /admin/content-list');
exit;
}
$user = $auth->getCurrentUser();
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
header('Location: /admin/content');
header('Location: /admin/content-list');
exit;
}
@@ -1741,7 +1746,7 @@ function handleContentDirCreate($auth, $config): void
}
}
header('Location: /admin/content?dir=' . urlencode($subdir));
header('Location: /admin/content-list?dir=' . urlencode($subdir));
exit;
}
@@ -1768,7 +1773,7 @@ function handleContentDirRename($auth, $config, $twig, $user, $csrf): void
if (!file_exists($newPath) && $newPath !== $fullPath) {
rename($fullPath, $newPath);
adminLog($config, 'info', $user['username'] . ' hernoemde map ' . basename($dir) . ' naar ' . $newName);
header('Location: /admin/content?dir=' . urlencode(dirname($dir) . '/' . $newName));
header('Location: /admin/content-list?dir=' . urlencode(dirname($dir) . '/' . $newName));
exit;
} else {
$message = 'Map bestaat al of ongeldige naam.';
@@ -1797,13 +1802,13 @@ function handleContentDirRename($auth, $config, $twig, $user, $csrf): void
function handleContentDirDelete($auth, $config): void
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /admin/content');
header('Location: /admin/content-list');
exit;
}
$user = $auth->getCurrentUser();
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
header('Location: /admin/content');
header('Location: /admin/content-list');
exit;
}
@@ -1821,7 +1826,7 @@ function handleContentDirDelete($auth, $config): void
}
}
header('Location: /admin/content?dir=' . urlencode(dirname($dir)));
header('Location: /admin/content-list?dir=' . urlencode(dirname($dir)));
exit;
}
@@ -1867,7 +1872,7 @@ function handleContentMove($auth, $config, $twig, $user, $csrf): void
if (!file_exists($newPath)) {
rename($fullPath, $newPath);
adminLog($config, 'info', $user['username'] . ' verplaatste ' . $item . ' naar ' . $dest);
header('Location: /admin/content?dir=' . urlencode($dest));
header('Location: /admin/content-list?dir=' . urlencode($dest));
exit;
} else {
$message = 'Bestand of map bestaat al op de bestemming.';