SEO: canonical, sitemap.xml, robots.txt, JSON-LD, noindex admin

This commit is contained in:
Cedric Abonnel
2026-05-12 01:19:38 +02:00
parent ca6fcefe51
commit 3a5ae2631d
7 changed files with 139 additions and 3 deletions
+3
View File
@@ -16,6 +16,9 @@ RewriteRule ^feed/?$ /feed.php [L,QSA]
RewriteRule ^rss/?$ /feed.php [L,QSA] RewriteRule ^rss/?$ /feed.php [L,QSA]
RewriteRule ^rss\.xml$ /feed.php [L,QSA] RewriteRule ^rss\.xml$ /feed.php [L,QSA]
# Sitemap
RewriteRule ^sitemap\.xml$ /sitemap.php [L]
# Ajoute .php si le fichier correspondant existe # Ajoute .php si le fichier correspondant existe
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI}.php -f RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI}.php -f
RewriteRule ^(.+?)/?$ /$1.php [L,QSA] RewriteRule ^(.+?)/?$ /$1.php [L,QSA]
+4
View File
@@ -21,6 +21,10 @@ $action = $_GET['action'] ?? 'list';
$uuid = $_GET['uuid'] ?? ''; $uuid = $_GET['uuid'] ?? '';
$slug = $_GET['slug'] ?? ''; $slug = $_GET['slug'] ?? '';
$_noindexActions = ['create', 'edit', 'admin', 'categories', 'diff', 'add_files', 'import_image', 'import_image_step2', 'sources', 'profile', 'delete_file', 'delete_external_link', 'rename_category', 'delete_category', 'toggle_private_category'];
$metaRobots = in_array($action, $_noindexActions, true) ? 'noindex, nofollow' : null;
unset($_noindexActions);
// ─── Extraction de métadonnées depuis une URL ──────────────────────────────── // ─── Extraction de métadonnées depuis une URL ────────────────────────────────
function fetchUrlMeta(string $url): array function fetchUrlMeta(string $url): array
{ {
+16
View File
@@ -0,0 +1,16 @@
User-agent: *
Disallow: /?action=edit
Disallow: /?action=create
Disallow: /?action=admin
Disallow: /?action=delete
Disallow: /?action=diff
Disallow: /?action=categories
Disallow: /?action=add_files
Disallow: /?action=import_image
Disallow: /?action=sources
Disallow: /?action=profile
Disallow: /login
Disallow: /logout.php
Disallow: /oidc/
Sitemap: https://varlog.a5l.fr/sitemap.xml
+49
View File
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
define('BASE_PATH', realpath(__DIR__ . '/../'));
require_once BASE_PATH . '/src/helpers.php';
require_once BASE_PATH . '/config/config.php';
require_once BASE_PATH . '/src/ArticleManager.php';
$articles = new ArticleManager(BASE_PATH . '/data');
$privateCats = $articles->getPrivateCategories();
$published = array_filter($articles->getAll(true), static function (array $a) use ($privateCats): bool {
$cat = trim($a['category'] ?? '');
if ($cat !== '' && in_array($cat, $privateCats, true)) {
return false;
}
if (strtotime((string)($a['published_at'] ?? '')) > time()) {
return false;
}
return true;
});
header('Content-Type: application/xml; charset=UTF-8');
header('X-Robots-Tag: noindex');
echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
// Homepage
echo ' <url>' . "\n";
echo ' <loc>' . htmlspecialchars(rtrim(APP_URL, '/') . '/') . '</loc>' . "\n";
echo ' <changefreq>daily</changefreq>' . "\n";
echo ' <priority>1.0</priority>' . "\n";
echo ' </url>' . "\n";
foreach ($published as $article) {
$loc = htmlspecialchars(rtrim(APP_URL, '/') . '/post/' . rawurlencode($article['slug'] ?? ''));
$lastmod = date('Y-m-d', strtotime((string)($article['updated_at'] ?? $article['published_at'] ?? 'now')));
echo ' <url>' . "\n";
echo ' <loc>' . $loc . '</loc>' . "\n";
echo ' <lastmod>' . $lastmod . '</lastmod>' . "\n";
echo ' <changefreq>monthly</changefreq>' . "\n";
echo ' <priority>0.8</priority>' . "\n";
echo ' </url>' . "\n";
}
echo '</urlset>' . "\n";
+6 -1
View File
@@ -7,7 +7,8 @@
<!-- SEO --> <!-- SEO -->
<meta name="description" content="<?= htmlspecialchars(($seoDescription ?? '') ?: 'Varlog est un journal personnel en ligne de Cédrix. 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="<?= htmlspecialchars($metaRobots ?? 'index, follow') ?>">
<link rel="canonical" href="<?= htmlspecialchars($canonical ?? $ogUrl ?? rtrim(APP_URL, '/') . '/') ?>">
<!-- Open Graph --> <!-- Open Graph -->
<meta property="og:title" content="<?= htmlspecialchars(($seoTitle ?? '') ?: ($title ?? 'varlog')) ?>"> <meta property="og:title" content="<?= htmlspecialchars(($seoTitle ?? '') ?: ($title ?? 'varlog')) ?>">
@@ -25,6 +26,10 @@
<meta property="article:published_time" content="<?= htmlspecialchars(date('c', strtotime((string)$articlePublishedAt))) ?>"> <meta property="article:published_time" content="<?= htmlspecialchars(date('c', strtotime((string)$articlePublishedAt))) ?>">
<?php endif; ?> <?php endif; ?>
<?php if (!empty($jsonLd ?? '')): ?>
<script type="application/ld+json"><?= $jsonLd ?></script>
<?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">
+24
View File
@@ -90,4 +90,28 @@ ob_start();
<?php <?php
$content = ob_get_clean(); $content = ob_get_clean();
$title = 'varlog'; $title = 'varlog';
// Pages avec curseur = contenu non-canonique → noindex
if (!empty($cursor)) {
$metaRobots = 'noindex, follow';
$canonical = rtrim(APP_URL, '/') . '/';
} elseif ($filterCat !== '') {
$canonical = rtrim(APP_URL, '/') . '/?' . http_build_query(['cat' => $filterCat]);
} else {
$canonical = rtrim(APP_URL, '/') . '/';
}
// JSON-LD WebSite sur la page d'accueil sans filtre
if (empty($cursor) && $filterCat === '') {
$jsonLd = json_encode([
'@context' => 'https://schema.org',
'@type' => 'WebSite',
'name' => 'varlog',
'url' => rtrim(APP_URL, '/') . '/',
'description' => 'Journal personnel de Cédrix. Informatique, hack et loisirs techniques.',
'inLanguage' => 'fr-FR',
'author' => ['@type' => 'Person', 'name' => 'Cédrix'],
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
include __DIR__ . '/layout.php'; include __DIR__ . '/layout.php';
+37 -2
View File
@@ -255,10 +255,45 @@ $hasSources = (!empty($externalLinks) || !empty($files))
$content = ob_get_clean(); $content = ob_get_clean();
$title = htmlspecialchars($article['title']); $title = htmlspecialchars($article['title']);
$seoTitle = ($article['seo_title'] ?? '') ?: $article['title']; $seoTitle = ($article['seo_title'] ?? '') ?: $article['title'];
$seoDescription = $article['seo_description'] ?? '';
$ogImage = $article['og_image'] ?? '';
$ogType = 'article'; $ogType = 'article';
$ogUrl = url('post/' . rawurlencode($article['slug'] ?? '')); $ogUrl = url('post/' . rawurlencode($article['slug'] ?? ''));
$canonical = $ogUrl;
$articlePublishedAt = $article['published_at'] ?? ''; $articlePublishedAt = $article['published_at'] ?? '';
$mainClass = 'container-fluid'; $mainClass = 'container-fluid';
// Auto-description depuis le contenu si le champ SEO est vide
$seoDescription = $article['seo_description'] ?? '';
if ($seoDescription === '') {
$plain = strip_tags((new Parsedown())->text($article['content']));
$plain = preg_replace('/\s+/', ' ', $plain);
$seoDescription = mb_strimwidth(trim((string)$plain), 0, 155, '…');
}
// og:image : cover puis fallback og_image du meta
if ($ogImage === null || $ogImage === '') {
$ogImage = $article['og_image'] ?? '';
}
// JSON-LD Article
$jsonLdData = [
'@context' => 'https://schema.org',
'@type' => 'BlogPosting',
'headline' => $seoTitle,
'description' => $seoDescription,
'url' => $canonical,
'datePublished' => date('c', strtotime((string)$articlePublishedAt)),
'dateModified' => date('c', strtotime((string)($article['updated_at'] ?? $articlePublishedAt))),
'author' => ['@type' => 'Person', 'name' => 'Cédrix'],
'publisher' => [
'@type' => 'Person',
'name' => 'Cédrix',
'url' => rtrim(APP_URL, '/'),
],
'inLanguage' => 'fr-FR',
];
if (!empty($ogImage)) {
$jsonLdData['image'] = $ogImage;
}
$jsonLd = json_encode($jsonLdData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
include __DIR__ . '/layout.php'; include __DIR__ . '/layout.php';