Add minimalist CMS for editable homepage and footer content #2

Closed
LordSchmackes wants to merge 1 commits from codex/implement-minimalist-cms-for-website into main
4 changed files with 185 additions and 34 deletions
+126 -2
View File
@@ -57,7 +57,9 @@ $editing = null;
$editIndex = null;
$editingEn = [];
$editingDe = [];
$view = (isset($_GET['view']) && $_GET['view'] === 'settings') ? 'settings' : 'recipes';
$allowedViews = ['recipes', 'settings', 'content'];
$view = $_GET['view'] ?? 'recipes';
$view = in_array($view, $allowedViews, true) ? $view : 'recipes';
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(16));
@@ -163,6 +165,59 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'save_
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'save_content') {
if (!hash_equals($_SESSION['csrf_token'] ?? '', $_POST['token'] ?? '')) {
$errors[] = 'Invalid form token, please retry.';
} else {
$cms = $siteSettings['cms'] ?? ['de' => [], 'en' => []];
$contentKeys = [
'landing_eyebrow',
'landing_line_1',
'landing_line_2',
'landing_line_3',
'landing_cta',
'coming_soon_badge',
'coming_soon_suffix',
'footer_designed_by',
'footer_contact_label',
'home_title',
'home_description',
'coming_feature_1',
'coming_feature_2',
'coming_feature_3',
];
foreach (['de', 'en'] as $langKey) {
foreach ($contentKeys as $field) {
$postKey = $field . '_' . $langKey;
$cms[$langKey][$field] = trim((string) ($_POST[$postKey] ?? ($cms[$langKey][$field] ?? '')));
}
}
$requiredCms = ['landing_line_1', 'landing_line_2', 'landing_line_3', 'landing_cta', 'home_title', 'home_description'];
foreach ($requiredCms as $field) {
if (($cms['de'][$field] ?? '') === '' && ($cms['en'][$field] ?? '') === '') {
$errors[] = "Content field \"$field\" needs DE or EN value.";
}
if (($cms['de'][$field] ?? '') === '') {
$cms['de'][$field] = $cms['en'][$field] ?? '';
}
if (($cms['en'][$field] ?? '') === '') {
$cms['en'][$field] = $cms['de'][$field] ?? '';
}
}
if (empty($errors)) {
$siteSettings['cms'] = $cms;
if (save_site_settings($siteSettings)) {
$message = 'Content saved.';
} else {
$errors[] = 'Could not save CMS content. Check permissions on data/site.json.';
}
}
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete') {
$slugDel = trim($_POST['slug'] ?? '');
$before = count($allRecipes);
@@ -350,6 +405,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
<div style="display:flex; gap:8px; align-items:center;">
<div class="admin-tabs">
<a href="/admin.php" class="<?php echo $view === 'recipes' ? 'active' : ''; ?>">Recipes</a>
<a href="/admin.php?view=content" class="<?php echo $view === 'content' ? 'active' : ''; ?>">Content</a>
<a href="/admin.php?view=settings" class="<?php echo $view === 'settings' ? 'active' : ''; ?>">Site settings</a>
</div>
<a class="button" href="/index.php" style="padding:10px 14px;">View site</a>
@@ -368,7 +424,75 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
</div>
<?php endif; ?>
<?php if ($view === 'settings'): ?>
<?php if ($view === 'content'): ?>
<?php $cms = $siteSettings['cms']; ?>
<form class="admin-form" method="post">
<input type="hidden" name="action" value="save_content">
<input type="hidden" name="token" value="<?php echo e($_SESSION['csrf_token']); ?>">
<h2>Homepage Content (DE)</h2>
<label>Landing eyebrow</label>
<input type="text" name="landing_eyebrow_de" value="<?php echo e($cms['de']['landing_eyebrow'] ?? ''); ?>">
<div class="row">
<div><label>Landing line 1 *</label><input type="text" name="landing_line_1_de" value="<?php echo e($cms['de']['landing_line_1'] ?? ''); ?>" required></div>
<div><label>Landing line 2 *</label><input type="text" name="landing_line_2_de" value="<?php echo e($cms['de']['landing_line_2'] ?? ''); ?>" required></div>
</div>
<div class="row">
<div><label>Landing line 3 *</label><input type="text" name="landing_line_3_de" value="<?php echo e($cms['de']['landing_line_3'] ?? ''); ?>" required></div>
<div><label>Landing CTA *</label><input type="text" name="landing_cta_de" value="<?php echo e($cms['de']['landing_cta'] ?? ''); ?>" required></div>
</div>
<div class="row">
<div><label>Coming soon badge</label><input type="text" name="coming_soon_badge_de" value="<?php echo e($cms['de']['coming_soon_badge'] ?? ''); ?>"></div>
<div><label>Coming soon suffix</label><input type="text" name="coming_soon_suffix_de" value="<?php echo e($cms['de']['coming_soon_suffix'] ?? ''); ?>"></div>
</div>
<label>Homepage title *</label>
<input type="text" name="home_title_de" value="<?php echo e($cms['de']['home_title'] ?? ''); ?>" required>
<label>Homepage meta description *</label>
<textarea name="home_description_de" required><?php echo e($cms['de']['home_description'] ?? ''); ?></textarea>
<h3>Coming soon feature bullets</h3>
<div class="row">
<div><input type="text" name="coming_feature_1_de" value="<?php echo e($cms['de']['coming_feature_1'] ?? ''); ?>"></div>
<div><input type="text" name="coming_feature_2_de" value="<?php echo e($cms['de']['coming_feature_2'] ?? ''); ?>"></div>
<div><input type="text" name="coming_feature_3_de" value="<?php echo e($cms['de']['coming_feature_3'] ?? ''); ?>"></div>
</div>
<div class="row">
<div><label>Footer line</label><input type="text" name="footer_designed_by_de" value="<?php echo e($cms['de']['footer_designed_by'] ?? ''); ?>"></div>
<div><label>Footer contact label</label><input type="text" name="footer_contact_label_de" value="<?php echo e($cms['de']['footer_contact_label'] ?? ''); ?>"></div>
</div>
<hr>
<h2>Homepage Content (EN)</h2>
<label>Landing eyebrow</label>
<input type="text" name="landing_eyebrow_en" value="<?php echo e($cms['en']['landing_eyebrow'] ?? ''); ?>">
<div class="row">
<div><label>Landing line 1 *</label><input type="text" name="landing_line_1_en" value="<?php echo e($cms['en']['landing_line_1'] ?? ''); ?>" required></div>
<div><label>Landing line 2 *</label><input type="text" name="landing_line_2_en" value="<?php echo e($cms['en']['landing_line_2'] ?? ''); ?>" required></div>
</div>
<div class="row">
<div><label>Landing line 3 *</label><input type="text" name="landing_line_3_en" value="<?php echo e($cms['en']['landing_line_3'] ?? ''); ?>" required></div>
<div><label>Landing CTA *</label><input type="text" name="landing_cta_en" value="<?php echo e($cms['en']['landing_cta'] ?? ''); ?>" required></div>
</div>
<div class="row">
<div><label>Coming soon badge</label><input type="text" name="coming_soon_badge_en" value="<?php echo e($cms['en']['coming_soon_badge'] ?? ''); ?>"></div>
<div><label>Coming soon suffix</label><input type="text" name="coming_soon_suffix_en" value="<?php echo e($cms['en']['coming_soon_suffix'] ?? ''); ?>"></div>
</div>
<label>Homepage title *</label>
<input type="text" name="home_title_en" value="<?php echo e($cms['en']['home_title'] ?? ''); ?>" required>
<label>Homepage meta description *</label>
<textarea name="home_description_en" required><?php echo e($cms['en']['home_description'] ?? ''); ?></textarea>
<h3>Coming soon feature bullets</h3>
<div class="row">
<div><input type="text" name="coming_feature_1_en" value="<?php echo e($cms['en']['coming_feature_1'] ?? ''); ?>"></div>
<div><input type="text" name="coming_feature_2_en" value="<?php echo e($cms['en']['coming_feature_2'] ?? ''); ?>"></div>
<div><input type="text" name="coming_feature_3_en" value="<?php echo e($cms['en']['coming_feature_3'] ?? ''); ?>"></div>
</div>
<div class="row">
<div><label>Footer line</label><input type="text" name="footer_designed_by_en" value="<?php echo e($cms['en']['footer_designed_by'] ?? ''); ?>"></div>
<div><label>Footer contact label</label><input type="text" name="footer_contact_label_en" value="<?php echo e($cms['en']['footer_contact_label'] ?? ''); ?>"></div>
</div>
<button type="submit" class="button" style="width: fit-content;">Save content</button>
</form>
<?php elseif ($view === 'settings'): ?>
<?php $im = $siteSettings['imprint']; $pv = $siteSettings['privacy']; ?>
<form class="admin-form" method="post">
<input type="hidden" name="action" value="save_settings">
+34
View File
@@ -34,6 +34,40 @@ function save_recipes(array $recipes): bool {
function default_site_settings(): array {
$today = date('Y-m-d');
return [
'cms' => [
'en' => [
'landing_eyebrow' => 'Seasonal · Unfussy · Ridiculously tasty',
'landing_line_1' => 'Cook better',
'landing_line_2' => 'weeknights.',
'landing_line_3' => 'Brighter weekends.',
'landing_cta' => 'Explore recipes',
'coming_soon_badge' => 'Coming soon',
'coming_soon_suffix' => 'in progress',
'footer_designed_by' => 'Website designed by Felix Brockers',
'footer_contact_label' => 'Contact me',
'home_title' => 'FlixCooks | Modern food blog for busy home cooks',
'home_description' => 'Fresh, fast recipes with clear steps, smart prep notes, and big flavor. Built for Hetzner web hosting in plain PHP.',
'coming_feature_1' => 'Nutrition info & recipe suggestions tailored to your goals',
'coming_feature_2' => 'Send your grocery list straight to your phone',
'coming_feature_3' => 'Step-by-step recipe assistant with videos',
],
'de' => [
'landing_eyebrow' => 'Saisonal · Unkompliziert · Richtig lecker',
'landing_line_1' => 'Besser kochen',
'landing_line_2' => 'unter der Woche.',
'landing_line_3' => 'Glänzen am Wochenende.',
'landing_cta' => 'Rezepte entdecken',
'coming_soon_badge' => 'Bald verfügbar',
'coming_soon_suffix' => 'in Arbeit',
'footer_designed_by' => 'Website designed by Felix Brockers',
'footer_contact_label' => 'Kontakt',
'home_title' => 'FlixCooks | Moderner Foodblog für schnelle Alltagsküche',
'home_description' => 'Frische, schnelle Rezepte mit klaren Schritten und großem Geschmack. Gebaut für Hetzner Webhosting in einfachem PHP.',
'coming_feature_1' => 'Nährwerte & Rezepte Vorschlag individuell angepasst auf Ernährungsziel',
'coming_feature_2' => 'Einkaufsliste ans Handy senden',
'coming_feature_3' => 'Step-by-Step Rezepte Assistent mit Videos',
],
],
'imprint' => [
'de' => [
'owner_name' => 'FlixCooks (bitte anpassen)',
+20 -30
View File
@@ -54,6 +54,8 @@ $copy = [
$t = $copy[$lang];
$allRecipes = load_recipes();
$siteSettings = load_site_settings();
$cms = $siteSettings['cms'][$lang] ?? [];
$localizedAll = localize_recipes($allRecipes, $lang);
$recipes = filter_recipes($allRecipes, $q ?: null, $tag ?: null, $lang);
$recipes = array_values(array_filter($recipes, fn($r) => empty($r['coming_soon']))); // hide coming soon from main list
@@ -84,24 +86,18 @@ foreach ($localizedAll as $recipe) {
}
$allTags = array_values(array_unique($allTags));
$comingFeatures = $lang === 'de'
? [
'Nährwerte & Rezepte Vorschlag individuell angepasst auf Ernährungsziel',
'Einkaufsliste ans Handy senden',
'Step-by-Step Rezepte Assistent mit Videos',
]
: [
'Nutrition info & recipe suggestions tailored to your goals',
'Send your grocery list straight to your phone',
'Step-by-step recipe assistant with videos',
];
$comingFeatures = array_values(array_filter([
trim((string) ($cms['coming_feature_1'] ?? '')),
trim((string) ($cms['coming_feature_2'] ?? '')),
trim((string) ($cms['coming_feature_3'] ?? '')),
], 'strlen'));
$pageTitle = $lang === 'de'
$pageTitle = trim((string) ($cms['home_title'] ?? '')) ?: ($lang === 'de'
? 'FlixCooks | Moderner Foodblog für schnelle Alltagsküche'
: 'FlixCooks | Modern food blog for busy home cooks';
$description = $lang === 'de'
: 'FlixCooks | Modern food blog for busy home cooks');
$description = trim((string) ($cms['home_description'] ?? '')) ?: ($lang === 'de'
? 'Frische, schnelle Rezepte mit klaren Schritten und großem Geschmack. Gebaut für Hetzner Webhosting in einfachem PHP.'
: 'Fresh, fast recipes with clear steps, smart prep notes, and big flavor. Built for Hetzner web hosting in plain PHP.';
: 'Fresh, fast recipes with clear steps, smart prep notes, and big flavor. Built for Hetzner web hosting in plain PHP.');
$langSwitchLabel = $lang === 'de' ? 'DE' : 'EN';
$langSwitchHref = lang_url($lang === 'de' ? 'en' : 'de');
$navHome = $t['home'];
@@ -183,24 +179,18 @@ include __DIR__ . '/partials/head.php';
</div>
<div class="landing__copy">
<p class="landing__eyebrow">
<?php echo $lang === 'de'
? 'Saisonal &nbsp;·&nbsp; Unkompliziert &nbsp;·&nbsp; Richtig lecker'
: 'Seasonal &nbsp;·&nbsp; Unfussy &nbsp;·&nbsp; Ridiculously tasty'; ?>
<?php echo e($cms['landing_eyebrow'] ?? ($lang === 'de'
? 'Saisonal · Unkompliziert · Richtig lecker'
: 'Seasonal · Unfussy · Ridiculously tasty')); ?>
</p>
<h1 class="landing__headline">
<?php if ($lang === 'de'): ?>
<span class="line line--1">Besser&nbsp;<em>kochen</em></span>
<span class="line line--2">unter der Woche.</span>
<span class="line line--3">Glänzen&nbsp;<em>am Wochenende.</em></span>
<?php else: ?>
<span class="line line--1">Cook&nbsp;<em>better</em></span>
<span class="line line--2">weeknights.</span>
<span class="line line--3">Brighter&nbsp;<em>weekends.</em></span>
<?php endif; ?>
<span class="line line--1"><?php echo e($cms['landing_line_1'] ?? ($lang === 'de' ? 'Besser kochen' : 'Cook better')); ?></span>
<span class="line line--2"><?php echo e($cms['landing_line_2'] ?? ($lang === 'de' ? 'unter der Woche.' : 'weeknights.')); ?></span>
<span class="line line--3"><?php echo e($cms['landing_line_3'] ?? ($lang === 'de' ? 'Glänzen am Wochenende.' : 'Brighter weekends.')); ?></span>
</h1>
<p class="landing__sub"><?php echo e($t['subhead']); ?></p>
<a href="#recipes-start" class="landing__cta" id="landingCta">
<span><?php echo $lang === 'de' ? 'Rezepte entdecken' : 'Explore recipes'; ?></span>
<span><?php echo e($cms['landing_cta'] ?? ($lang === 'de' ? 'Rezepte entdecken' : 'Explore recipes')); ?></span>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M12 5v14M5 12l7 7 7-7"/></svg>
</a>
</div>
@@ -221,8 +211,8 @@ document.getElementById('landingCta').addEventListener('click', function(e){
<?php if (!empty($comingSoon)): ?>
<div class="coming-strip" aria-label="Coming soon recipes">
<div class="coming-strip__head">
<p class="pill pill--ghost"><?php echo e($lang === 'de' ? 'Bald verfügbar' : 'Coming soon'); ?></p>
<small><?php echo count($comingSoon); ?> <?php echo $lang === 'de' ? 'in Arbeit' : 'in progress'; ?></small>
<p class="pill pill--ghost"><?php echo e($cms['coming_soon_badge'] ?? ($lang === 'de' ? 'Bald verfügbar' : 'Coming soon')); ?></p>
<small><?php echo count($comingSoon); ?> <?php echo e($cms['coming_soon_suffix'] ?? ($lang === 'de' ? 'in Arbeit' : 'in progress')); ?></small>
</div>
<div class="coming-strip__row">
<?php foreach (array_slice($comingSoonDisplay, 0, 5) as $cs): ?>
+5 -2
View File
@@ -3,11 +3,14 @@
$langSuffix = $langCode === 'de' ? '?lang=de' : '';
$settings = function_exists('load_site_settings') ? load_site_settings() : null;
$contactEmail = $settings['imprint'][$langCode]['email'] ?? 'contact@flixcooks.at';
$cms = $settings['cms'][$langCode] ?? [];
$footerByline = trim((string) ($cms['footer_designed_by'] ?? '')) ?: 'Website designed by Felix Brockers';
$footerContact = trim((string) ($cms['footer_contact_label'] ?? '')) ?: ($langCode === 'de' ? 'Kontakt' : 'Contact me');
?>
<footer class="site-footer">
<div class="footer-meta">
<p>Website designed by Felix Brockers</p>
<a class="contact-link" href="mailto:<?php echo e($contactEmail); ?>">Contact me</a>
<p><?php echo e($footerByline); ?></p>
<a class="contact-link" href="mailto:<?php echo e($contactEmail); ?>"><?php echo e($footerContact); ?></a>
<small>© <?php echo date('Y'); ?> FlixCooks</small>
</div>
<div class="footer-links">