Gestion des pieces jointes dans edition + SEO par article
This commit is contained in:
+20
-2
@@ -31,6 +31,9 @@ switch ($action) {
|
|||||||
$postSlug = $_POST['slug'] ?? '';
|
$postSlug = $_POST['slug'] ?? '';
|
||||||
$published = isset($_POST['published']);
|
$published = isset($_POST['published']);
|
||||||
$published_at = str_replace('T', ' ', $_POST['published_at'] ?? date('Y-m-d H:i:s'));
|
$published_at = str_replace('T', ' ', $_POST['published_at'] ?? date('Y-m-d H:i:s'));
|
||||||
|
$seoTitle = $_POST['seo_title'] ?? '';
|
||||||
|
$seoDescription = $_POST['seo_description'] ?? '';
|
||||||
|
$ogImage = $_POST['og_image'] ?? '';
|
||||||
$errors = [];
|
$errors = [];
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
@@ -38,7 +41,7 @@ switch ($action) {
|
|||||||
$errors[] = 'Le titre est obligatoire.';
|
$errors[] = 'Le titre est obligatoire.';
|
||||||
}
|
}
|
||||||
if (empty($errors)) {
|
if (empty($errors)) {
|
||||||
$newUuid = $articles->create($title, $content, $published, $postSlug, $published_at, currentUserEmail() ?? '');
|
$newUuid = $articles->create($title, $content, $published, $postSlug, $published_at, currentUserEmail() ?? '', $seoTitle, $seoDescription, $ogImage);
|
||||||
|
|
||||||
foreach ($_FILES['files']['tmp_name'] ?? [] as $i => $tmpName) {
|
foreach ($_FILES['files']['tmp_name'] ?? [] as $i => $tmpName) {
|
||||||
if ($_FILES['files']['error'][$i] === UPLOAD_ERR_OK) {
|
if ($_FILES['files']['error'][$i] === UPLOAD_ERR_OK) {
|
||||||
@@ -111,6 +114,9 @@ switch ($action) {
|
|||||||
$published = isset($_POST['published']) ? true : $article['published'];
|
$published = isset($_POST['published']) ? true : $article['published'];
|
||||||
$published_at = $_POST['published_at']
|
$published_at = $_POST['published_at']
|
||||||
?? date('Y-m-d\TH:i', strtotime((string)($article['published_at'] ?? 'now')));
|
?? date('Y-m-d\TH:i', strtotime((string)($article['published_at'] ?? 'now')));
|
||||||
|
$seoTitle = $_POST['seo_title'] ?? ($article['seo_title'] ?? '');
|
||||||
|
$seoDescription = $_POST['seo_description'] ?? ($article['seo_description'] ?? '');
|
||||||
|
$ogImage = $_POST['og_image'] ?? ($article['og_image'] ?? '');
|
||||||
$errors = [];
|
$errors = [];
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
@@ -125,7 +131,10 @@ switch ($action) {
|
|||||||
$published,
|
$published,
|
||||||
$_POST['slug'] ?? '',
|
$_POST['slug'] ?? '',
|
||||||
str_replace('T', ' ', $_POST['published_at'] ?? ''),
|
str_replace('T', ' ', $_POST['published_at'] ?? ''),
|
||||||
$_POST['revision_comment'] ?? ''
|
$_POST['revision_comment'] ?? '',
|
||||||
|
$_POST['seo_title'] ?? '',
|
||||||
|
$_POST['seo_description'] ?? '',
|
||||||
|
$_POST['og_image'] ?? ''
|
||||||
);
|
);
|
||||||
|
|
||||||
foreach ($_FILES['files']['tmp_name'] ?? [] as $i => $tmpName) {
|
foreach ($_FILES['files']['tmp_name'] ?? [] as $i => $tmpName) {
|
||||||
@@ -150,6 +159,15 @@ switch ($action) {
|
|||||||
include BASE_PATH . '/templates/post_form.php';
|
include BASE_PATH . '/templates/post_form.php';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'delete_file':
|
||||||
|
requireAuth();
|
||||||
|
$fileName = basename($_POST['name'] ?? '');
|
||||||
|
if ($uuid !== '' && $fileName !== '' && $fileName[0] !== '.') {
|
||||||
|
$articles->deleteFile($uuid, $fileName);
|
||||||
|
}
|
||||||
|
header('Location: /?action=edit&uuid=' . rawurlencode($uuid));
|
||||||
|
exit;
|
||||||
|
|
||||||
case 'delete':
|
case 'delete':
|
||||||
requireAuth();
|
requireAuth();
|
||||||
if ($uuid !== '') {
|
if ($uuid !== '') {
|
||||||
|
|||||||
+24
-2
@@ -69,7 +69,7 @@ class ArticleManager
|
|||||||
// Écriture
|
// Écriture
|
||||||
// ------------------------------------------------------------------ //
|
// ------------------------------------------------------------------ //
|
||||||
|
|
||||||
public function create(string $title, string $content, bool $published, string $slug = '', string $publishedAt = '', string $author = ''): string
|
public function create(string $title, string $content, bool $published, string $slug = '', string $publishedAt = '', string $author = '', string $seoTitle = '', string $seoDescription = '', string $ogImage = ''): string
|
||||||
{
|
{
|
||||||
$uuid = $this->generateUuid();
|
$uuid = $this->generateUuid();
|
||||||
$slug = $slug !== '' ? $this->sanitizeSlug($slug) : $this->generateSlug($title);
|
$slug = $slug !== '' ? $this->sanitizeSlug($slug) : $this->generateSlug($title);
|
||||||
@@ -91,6 +91,9 @@ class ArticleManager
|
|||||||
'created_at' => $now,
|
'created_at' => $now,
|
||||||
'updated_at' => $now,
|
'updated_at' => $now,
|
||||||
'revisions' => [],
|
'revisions' => [],
|
||||||
|
'seo_title' => $seoTitle,
|
||||||
|
'seo_description' => $seoDescription,
|
||||||
|
'og_image' => $ogImage,
|
||||||
];
|
];
|
||||||
$this->writeMeta($dir, $meta);
|
$this->writeMeta($dir, $meta);
|
||||||
file_put_contents($dir . '/index.md', ltrim($content));
|
file_put_contents($dir . '/index.md', ltrim($content));
|
||||||
@@ -98,7 +101,7 @@ class ArticleManager
|
|||||||
return $uuid;
|
return $uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(string $uuid, string $title, string $content, bool $published, string $slug, string $publishedAt, string $revisionComment = ''): void
|
public function update(string $uuid, string $title, string $content, bool $published, string $slug, string $publishedAt, string $revisionComment = '', string $seoTitle = '', string $seoDescription = '', string $ogImage = ''): void
|
||||||
{
|
{
|
||||||
$article = $this->getByUuid($uuid);
|
$article = $this->getByUuid($uuid);
|
||||||
if (!$article) {
|
if (!$article) {
|
||||||
@@ -122,6 +125,9 @@ class ArticleManager
|
|||||||
'created_at' => $article['created_at'] ?? date('Y-m-d H:i:s'),
|
'created_at' => $article['created_at'] ?? date('Y-m-d H:i:s'),
|
||||||
'updated_at' => date('Y-m-d H:i:s'),
|
'updated_at' => date('Y-m-d H:i:s'),
|
||||||
'revisions' => $revisions,
|
'revisions' => $revisions,
|
||||||
|
'seo_title' => $seoTitle,
|
||||||
|
'seo_description' => $seoDescription,
|
||||||
|
'og_image' => $ogImage,
|
||||||
];
|
];
|
||||||
$dir = $this->dataDir . '/' . $uuid;
|
$dir = $this->dataDir . '/' . $uuid;
|
||||||
$this->writeMeta($dir, $meta);
|
$this->writeMeta($dir, $meta);
|
||||||
@@ -173,6 +179,22 @@ class ArticleManager
|
|||||||
return $files;
|
return $files;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function deleteFile(string $uuid, string $name): bool
|
||||||
|
{
|
||||||
|
if (!$this->isValidUuid($uuid)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$name = basename($name);
|
||||||
|
if ($name === '' || $name[0] === '.') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$path = $this->dataDir . '/' . $uuid . '/files/' . $name;
|
||||||
|
if (!is_file($path)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return unlink($path);
|
||||||
|
}
|
||||||
|
|
||||||
public function addFile(string $uuid, array $uploadedFile): ?string
|
public function addFile(string $uuid, array $uploadedFile): ?string
|
||||||
{
|
{
|
||||||
if (!$this->isValidUuid($uuid)) {
|
if (!$this->isValidUuid($uuid)) {
|
||||||
|
|||||||
@@ -2,25 +2,28 @@
|
|||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title><?= htmlspecialchars($title ?? 'varlog') ?></title>
|
<title><?= htmlspecialchars(($seoTitle ?? '') ?: ($title ?? 'varlog')) ?></title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
<!-- SEO -->
|
<!-- SEO -->
|
||||||
<meta name="description" content="Varlog est un journal personnel en ligne de Cédrix développé par ces soins. Informatique, hack et loisirs techniques.">
|
<meta name="description" content="<?= htmlspecialchars(($seoDescription ?? '') ?: 'Varlog est un journal personnel en ligne de Cédrix. Informatique, hack et loisirs techniques.') ?>">
|
||||||
<meta name="robots" content="index, follow">
|
<meta name="robots" content="index, follow">
|
||||||
|
|
||||||
<!-- Open Graph -->
|
<!-- Open Graph -->
|
||||||
<meta property="og:title" content="<?= htmlspecialchars($title ?? 'varlog') ?>">
|
<meta property="og:title" content="<?= htmlspecialchars(($seoTitle ?? '') ?: ($title ?? 'varlog')) ?>">
|
||||||
<meta property="og:description" content="Découvrez les derniers articles publiés sur le journal personnel varlog.">
|
<meta property="og:description" content="<?= htmlspecialchars(($seoDescription ?? '') ?: 'Découvrez les derniers articles publiés sur le journal personnel varlog.') ?>">
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="<?= htmlspecialchars($ogType ?? 'website') ?>">
|
||||||
<meta property="og:locale" content="fr_FR">
|
<meta property="og:locale" content="fr_FR">
|
||||||
<meta property="og:url" content="https://varlog.a5l.fr/">
|
<meta property="og:url" content="<?= htmlspecialchars($ogUrl ?? APP_URL) ?>">
|
||||||
<meta property="og:site_name" content="varlog">
|
<meta property="og:site_name" content="varlog">
|
||||||
<?php if (!empty($ogImage ?? '')): ?>
|
<?php if (!empty($ogImage ?? '')): ?>
|
||||||
<meta property="og:image" content="<?= htmlspecialchars($ogImage) ?>">
|
<meta property="og:image" content="<?= htmlspecialchars($ogImage) ?>">
|
||||||
<meta property="og:image:width" content="1200">
|
<meta property="og:image:width" content="1200">
|
||||||
<meta property="og:image:height" content="630">
|
<meta property="og:image:height" content="630">
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($articlePublishedAt ?? '')): ?>
|
||||||
|
<meta property="article:published_time" content="<?= htmlspecialchars(date('c', strtotime((string)$articlePublishedAt))) ?>">
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- RSS autodiscovery -->
|
<!-- RSS autodiscovery -->
|
||||||
<link rel="alternate" type="application/rss+xml" title="varlog" href="/feed">
|
<link rel="alternate" type="application/rss+xml" title="varlog" href="/feed">
|
||||||
|
|||||||
+112
-7
@@ -73,14 +73,50 @@ $dateValue = isset($published_at)
|
|||||||
<?php if ($action === 'edit' && !empty($existingFiles)): ?>
|
<?php if ($action === 'edit' && !empty($existingFiles)): ?>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<p class="form-label">Fichiers existants</p>
|
<p class="form-label">Fichiers existants</p>
|
||||||
<ul class="list-unstyled">
|
<div class="list-group">
|
||||||
<?php foreach ($existingFiles as $f): ?>
|
<?php foreach ($existingFiles as $i => $f): ?>
|
||||||
<li>
|
<?php $fileUrl = '/file?uuid=' . rawurlencode($uuid) . '&name=' . rawurlencode($f['name']); ?>
|
||||||
<code><?= htmlspecialchars($f['name']) ?></code>
|
<div class="list-group-item d-flex align-items-center gap-3 py-2">
|
||||||
<small class="text-muted ms-2"><?= htmlspecialchars(number_format($f['size'] / 1024, 1)) ?> Ko</small>
|
<?php if ($f['is_image']): ?>
|
||||||
</li>
|
<a href="<?= htmlspecialchars($fileUrl) ?>" target="_blank" rel="noopener">
|
||||||
|
<img src="<?= htmlspecialchars($fileUrl) ?>" alt=""
|
||||||
|
style="width:48px;height:48px;object-fit:cover;border-radius:4px;flex-shrink:0">
|
||||||
|
</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<a href="<?= htmlspecialchars($fileUrl) ?>" target="_blank" rel="noopener"
|
||||||
|
style="width:48px;text-align:center;font-size:1.5rem;flex-shrink:0;text-decoration:none">
|
||||||
|
<?php
|
||||||
|
$icon = match(true) {
|
||||||
|
str_starts_with($f['mime'], 'video/') => '🎬',
|
||||||
|
str_starts_with($f['mime'], 'audio/') => '🎵',
|
||||||
|
$f['mime'] === 'application/pdf' => '📑',
|
||||||
|
default => '📄',
|
||||||
|
};
|
||||||
|
echo $icon;
|
||||||
|
?>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="flex-grow-1 overflow-hidden">
|
||||||
|
<code class="d-block text-truncate"><?= htmlspecialchars($f['name']) ?></code>
|
||||||
|
<small class="text-muted">
|
||||||
|
<?= htmlspecialchars(number_format($f['size'] / 1024, 1)) ?> Ko
|
||||||
|
— <?= htmlspecialchars($f['mime']) ?>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2 flex-shrink-0">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||||
|
onclick="copyMdRef(<?= htmlspecialchars(json_encode($f['name'])) ?>, <?= $f['is_image'] ? 'true' : 'false' ?>, this)">
|
||||||
|
Référence MD
|
||||||
|
</button>
|
||||||
|
<button type="submit" form="del-file-<?= $i ?>"
|
||||||
|
class="btn btn-sm btn-outline-danger"
|
||||||
|
onclick="return confirm('Supprimer « <?= htmlspecialchars(addslashes($f['name'])) ?> » définitivement ?')">
|
||||||
|
Supprimer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
@@ -92,10 +128,70 @@ $dateValue = isset($published_at)
|
|||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="card mb-3 border-secondary">
|
||||||
|
<div class="card-header bg-transparent py-2">
|
||||||
|
<button class="btn btn-sm btn-link text-secondary text-decoration-none p-0 fw-semibold"
|
||||||
|
type="button" data-bs-toggle="collapse" data-bs-target="#seoPanel" aria-expanded="false">
|
||||||
|
▸ SEO — titre, description, image
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="collapse" id="seoPanel">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="seo_title" class="form-label">
|
||||||
|
Titre SEO
|
||||||
|
<small class="text-muted">(balise <title> et og:title)</small>
|
||||||
|
</label>
|
||||||
|
<input type="text" class="form-control" id="seo_title" name="seo_title"
|
||||||
|
maxlength="70"
|
||||||
|
value="<?= htmlspecialchars($seoTitle ?? '') ?>"
|
||||||
|
placeholder="Généré automatiquement depuis le titre">
|
||||||
|
<div class="d-flex justify-content-between mt-1">
|
||||||
|
<small class="text-muted">Idéal : 30–60 caractères</small>
|
||||||
|
<small id="seo_title_counter" class="text-muted">0 / 60</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="seo_description" class="form-label">
|
||||||
|
Description SEO
|
||||||
|
<small class="text-muted">(meta description et og:description)</small>
|
||||||
|
</label>
|
||||||
|
<textarea class="form-control" id="seo_description" name="seo_description"
|
||||||
|
rows="3" maxlength="200"
|
||||||
|
placeholder="Générée automatiquement depuis le début du contenu"><?= htmlspecialchars($seoDescription ?? '') ?></textarea>
|
||||||
|
<div class="d-flex justify-content-between mt-1">
|
||||||
|
<small class="text-muted">Idéal : 120–155 caractères</small>
|
||||||
|
<small id="seo_desc_counter" class="text-muted">0 / 155</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="og_image" class="form-label">
|
||||||
|
Image Open Graph
|
||||||
|
<small class="text-muted">(URL absolue, optionnel)</small>
|
||||||
|
</label>
|
||||||
|
<input type="url" class="form-control font-monospace" id="og_image" name="og_image"
|
||||||
|
value="<?= htmlspecialchars($ogImage ?? '') ?>"
|
||||||
|
placeholder="https://varlog.a5l.fr/…">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-success">Enregistrer</button>
|
<button type="submit" class="btn btn-success">Enregistrer</button>
|
||||||
<a href="/" class="btn btn-secondary">Annuler</a>
|
<a href="/" class="btn btn-secondary">Annuler</a>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<?php if ($action === 'edit' && !empty($existingFiles)): ?>
|
||||||
|
<?php foreach ($existingFiles as $i => $f): ?>
|
||||||
|
<form id="del-file-<?= $i ?>" method="POST"
|
||||||
|
action="/?action=delete_file&uuid=<?= rawurlencode($uuid) ?>">
|
||||||
|
<input type="hidden" name="name" value="<?= htmlspecialchars($f['name']) ?>">
|
||||||
|
</form>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function slugify(s) {
|
function slugify(s) {
|
||||||
const map = {'à':'a','â':'a','ä':'a','é':'e','è':'e','ê':'e','ë':'e','î':'i','ï':'i','ô':'o','ö':'o','ù':'u','û':'u','ü':'u','ç':'c','æ':'ae','œ':'oe'};
|
const map = {'à':'a','â':'a','ä':'a','é':'e','è':'e','ê':'e','ë':'e','î':'i','ï':'i','ô':'o','ö':'o','ù':'u','û':'u','ü':'u','ç':'c','æ':'ae','œ':'oe'};
|
||||||
@@ -121,6 +217,15 @@ document.getElementById('slug').addEventListener('input', function() {
|
|||||||
const s = document.getElementById('slug');
|
const s = document.getElementById('slug');
|
||||||
if (s.value !== '') s._auto = false;
|
if (s.value !== '') s._auto = false;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
function copyMdRef(name, isImage, btn) {
|
||||||
|
const ref = isImage ? `` : `[${name}](${name})`;
|
||||||
|
navigator.clipboard.writeText(ref).then(() => {
|
||||||
|
const orig = btn.textContent;
|
||||||
|
btn.textContent = 'Copié !';
|
||||||
|
setTimeout(() => { btn.textContent = orig; }, 1500);
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
|
|||||||
@@ -89,4 +89,10 @@ $ogImage = $coverFile !== ''
|
|||||||
<?php
|
<?php
|
||||||
$content = ob_get_clean();
|
$content = ob_get_clean();
|
||||||
$title = htmlspecialchars($article['title']);
|
$title = htmlspecialchars($article['title']);
|
||||||
|
$seoTitle = ($article['seo_title'] ?? '') ?: $article['title'];
|
||||||
|
$seoDescription = $article['seo_description'] ?? '';
|
||||||
|
$ogImage = $article['og_image'] ?? '';
|
||||||
|
$ogType = 'article';
|
||||||
|
$ogUrl = url('post/' . rawurlencode($article['slug'] ?? ''));
|
||||||
|
$articlePublishedAt = $article['published_at'] ?? '';
|
||||||
include __DIR__ . '/layout.php';
|
include __DIR__ . '/layout.php';
|
||||||
|
|||||||
Reference in New Issue
Block a user