Initiales CMS: Deutschsprachiges Blog-System mit Admin-Bereich

Vollständiges, schlankes PHP/SQLite-CMS für IT-, KI- und Gaming-Inhalte:

- Core: DB-Singleton, Auth mit Passwort-Hashing, Session-Cookies,
  CSRF-Schutz, Login-Rate-Limit, Bild-Upload mit serverseitiger Validierung
- Admin: Dashboard, Artikel/Seiten-Verwaltung mit Quill WYSIWYG-Editor,
  Kategorien, Navigation (Drag & Drop), Medienbibliothek, Profil
- Frontend: Responsive Dark-Theme, Artikel-Grid, Kategorie-Filter,
  Archiv, Paginierung, SEO-Meta-Tags
- Sicherheit: Prepared Statements, HTML-Sanitizer, .htaccess-Schutz
  für sensible Verzeichnisse, PHP-Ausführungsschutz im Upload-Ordner
- Installation: install.php erstellt DB-Schema und Admin-Account

https://claude.ai/code/session_01Xsg4j2t4S9goMuWVpF3ezG
This commit is contained in:
Claude
2026-04-05 20:59:52 +00:00
commit 3c97192386
45 changed files with 2839 additions and 0 deletions

86
admin/articles.php Normal file
View File

@@ -0,0 +1,86 @@
<?php
require_once __DIR__ . '/../core/auth.php';
auth_start_session();
auth_require_login();
$pdo = db();
// Artikel löschen
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_id'])) {
csrf_verify();
$stmt = $pdo->prepare('DELETE FROM articles WHERE id = ?');
$stmt->execute([(int) $_POST['delete_id']]);
flash('success', 'Artikel gelöscht.');
redirect('/admin/articles.php');
}
$page = max(1, (int) ($_GET['page'] ?? 1));
$total = (int) $pdo->query('SELECT COUNT(*) FROM articles')->fetchColumn();
$pag = paginate($total, $page, ITEMS_PER_PAGE);
$stmt = $pdo->prepare(
"SELECT a.id, a.title, a.slug, a.status, a.published_at, a.created_at, c.name as category_name
FROM articles a LEFT JOIN categories c ON a.category_id = c.id
ORDER BY a.created_at DESC LIMIT ? OFFSET ?"
);
$stmt->execute([$pag['limit'], $pag['offset']]);
$articles = $stmt->fetchAll();
$pageTitle = 'Artikel';
$currentPage = 'articles';
ob_start();
?>
<div class="card">
<div class="card-header">
<h3>Alle Artikel (<?= $total ?>)</h3>
<a href="/admin/article-edit.php" class="btn btn-primary btn-sm">Neuer Artikel</a>
</div>
<?php if (empty($articles)): ?>
<p class="empty-state">Noch keine Artikel vorhanden.</p>
<?php else: ?>
<table class="table">
<thead>
<tr>
<th>Titel</th>
<th>Kategorie</th>
<th>Status</th>
<th>Datum</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<?php foreach ($articles as $article): ?>
<tr>
<td><a href="/admin/article-edit.php?id=<?= $article['id'] ?>"><?= e($article['title']) ?></a></td>
<td><?= e($article['category_name'] ?? '') ?></td>
<td>
<span class="badge badge-<?= $article['status'] === 'published' ? 'success' : 'warning' ?>">
<?= $article['status'] === 'published' ? 'Veröffentlicht' : 'Entwurf' ?>
</span>
</td>
<td><?= format_date($article['published_at'] ?? $article['created_at']) ?></td>
<td class="actions">
<a href="/admin/article-edit.php?id=<?= $article['id'] ?>" class="btn btn-sm">Bearbeiten</a>
<form method="post" class="inline-form" onsubmit="return confirm('Artikel wirklich löschen?')">
<?= csrf_field() ?>
<input type="hidden" name="delete_id" value="<?= $article['id'] ?>">
<button type="submit" class="btn btn-sm btn-danger">Löschen</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if ($pag['total_pages'] > 1): ?>
<div class="pagination">
<?php for ($i = 1; $i <= $pag['total_pages']; $i++): ?>
<a href="?page=<?= $i ?>" class="<?= $i === $pag['current_page'] ? 'active' : '' ?>"><?= $i ?></a>
<?php endfor; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<?php
$content = ob_get_clean();
include __DIR__ . '/templates/layout.php';