feat: add email sharing functionality, canonical URLs, and enhance metadata for SEO

This commit is contained in:
interlope committed 2026-06-02 23:10:32 +02:00
1 parent efc83ec9f3
commit d448574ff8
12 files changed
+513 -12

No files matched your search

+78 -9
View File
@@ -93,6 +93,9 @@
$ogType = $ogIsArticle ? 'article' : 'website';
$ogSiteName = $site->title();
?>
<!-- Canonical URL -->
<link rel="canonical" href="<?php echo htmlspecialchars($ogUrl, ENT_QUOTES, 'UTF-8'); ?>">
<!-- Open Graph -->
<meta property="og:type" content="<?php echo htmlspecialchars($ogType, ENT_QUOTES, 'UTF-8'); ?>">
<meta property="og:site_name" content="<?php echo blowdit_text($ogSiteName); ?>">
@@ -100,18 +103,58 @@
<meta property="og:description" content="<?php echo blowdit_text($ogDesc); ?>">
<meta property="og:url" content="<?php echo htmlspecialchars($ogUrl, ENT_QUOTES, 'UTF-8'); ?>">
<meta property="og:image" content="<?php echo htmlspecialchars($ogImage, ENT_QUOTES, 'UTF-8'); ?>">
<meta property="og:image:alt" content="<?php echo blowdit_text($ogTitle); ?>">
<meta property="og:locale" content="<?php echo htmlspecialchars(str_replace('-', '_', Theme::lang()), ENT_QUOTES, 'UTF-8'); ?>">
<?php if ($ogIsArticle): ?>
<meta property="article:published_time" content="<?php echo htmlspecialchars($ogPublishedIso, ENT_QUOTES, 'UTF-8'); ?>">
<meta property="article:author" content="<?php echo blowdit_text($page->username()); ?>">
<?php endif ?>
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<?php if (!empty($blowditTwitterHandle)): ?>
<meta name="twitter:site" content="<?php echo htmlspecialchars($blowditTwitterHandle, ENT_QUOTES, 'UTF-8'); ?>">
<meta name="twitter:creator" content="<?php echo htmlspecialchars($blowditTwitterHandle, ENT_QUOTES, 'UTF-8'); ?>">
<?php endif ?>
<meta name="twitter:title" content="<?php echo blowdit_text($ogTitle); ?>">
<meta name="twitter:description" content="<?php echo blowdit_text($ogDesc); ?>">
<meta name="twitter:image" content="<?php echo htmlspecialchars($ogImage, ENT_QUOTES, 'UTF-8'); ?>">
<meta name="twitter:image:alt" content="<?php echo blowdit_text($ogTitle); ?>">
<?php
// ---- JSON-LD structured data (BlogPosting) — articles only ----
// ---- JSON-LD structured data ----
$blowditEmitLd = function ($doc) {
echo '<script type="application/ld+json">'
. json_encode($doc, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
. '</script>' . "\n";
};
// Social profile URLs, used as schema.org "sameAs".
$blowditSameAs = array();
foreach (Theme::socialNetworks() as $snKey => $snLabel) {
$snUrl = $site->{$snKey}();
if (!empty($snUrl)) { $blowditSameAs[] = $snUrl; }
}
// WebSite (every page) — declares the site + a sitelinks search box.
$siteLd = array(
'@context' => 'https://schema.org',
'@type' => 'WebSite',
'name' => blowdit_plain($site->title()),
'url' => Theme::siteUrl(),
);
$siteDesc = $site->description() ?: $site->slogan();
if ($siteDesc) { $siteLd['description'] = blowdit_plain($siteDesc); }
$siteLd['potentialAction'] = array(
'@type' => 'SearchAction',
'target' => array(
'@type' => 'EntryPoint',
'urlTemplate' => rtrim(Theme::siteUrl(), '/') . '/search/{search_term_string}',
),
'query-input' => 'required name=search_term_string',
);
$blowditEmitLd($siteLd);
// BlogPosting + BreadcrumbList — articles only.
if ($ogIsArticle) {
$ldDescription = $page->description() ?: ($site->description() ?: $site->slogan());
$ldImage = $page->coverImage(true) ?: (DOMAIN_THEME . 'img/spleenftw.jpeg');
@@ -127,6 +170,13 @@
$ldTimeRequired = 'PT' . $rtMatch[0] . 'M';
}
$author = array(
'@type' => 'Person',
'name' => blowdit_plain($page->username()),
'url' => Theme::siteUrl(),
);
if (!empty($blowditSameAs)) { $author['sameAs'] = $blowditSameAs; }
$jsonLd = array(
'@context' => 'https://schema.org',
'@type' => 'BlogPosting',
@@ -135,11 +185,7 @@
'image' => $ldImage,
'datePublished' => $ldPublished,
'dateModified' => $ldModified,
'author' => array(
'@type' => 'Person',
'name' => blowdit_plain($page->username()),
'url' => Theme::siteUrl(),
),
'author' => $author,
'publisher' => array(
'@type' => 'Organization',
'name' => blowdit_plain($site->title()),
@@ -155,10 +201,29 @@
'wordCount' => str_word_count(strip_tags($page->content())),
);
if ($ldTimeRequired) { $jsonLd['timeRequired'] = $ldTimeRequired; }
$blowditEmitLd($jsonLd);
echo '<script type="application/ld+json">'
. json_encode($jsonLd, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
. '</script>' . "\n";
// Breadcrumb: Home > [Category] > Article
$crumbs = array(array(
'@type' => 'ListItem', 'position' => 1,
'name' => blowdit_plain($site->title()), 'item' => Theme::siteUrl(),
));
$pos = 2;
if ($page->categoryKey()) {
$crumbs[] = array(
'@type' => 'ListItem', 'position' => $pos++,
'name' => blowdit_plain($page->category()), 'item' => $page->categoryPermalink(),
);
}
$crumbs[] = array(
'@type' => 'ListItem', 'position' => $pos,
'name' => blowdit_plain($page->title()), 'item' => $page->permalink(true),
);
$blowditEmitLd(array(
'@context' => 'https://schema.org',
'@type' => 'BreadcrumbList',
'itemListElement' => $crumbs,
));
}
?>
@@ -168,6 +233,10 @@
<!-- Include Favicon -->
<?php echo Theme::favicon('img/favicon.png'); ?>
<!-- PWA web app manifest (installable; uses theme-color above) -->
<link rel="manifest" href="<?php echo DOMAIN_THEME . 'manifest.webmanifest'; ?>">
<link rel="apple-touch-icon" href="<?php echo DOMAIN_THEME . 'img/favicon.png'; ?>">
<!-- Inter typeface (Blowfish-like typography) — loaded non-render-blocking:
fetched as print media, then flipped to all once it arrives. -->
<link rel="preconnect" href="https://fonts.googleapis.com">
+6
View File
@@ -26,6 +26,12 @@
<a href="<?php echo rtrim(Theme::siteUrl(), '/') . '/rss.xml'; ?>" target="_blank" rel="noopener" title="<?php echo $L->get('RSS'); ?>">
<img class="profile-social-icon" src="<?php echo blowdit_asset('img/rss.svg') ?>" alt="<?php echo $L->get('RSS'); ?>" />
</a>
<!-- Email (mailto) -->
<?php if (!empty($blowditEmail)) : ?>
<a href="mailto:<?php echo htmlspecialchars($blowditEmail, ENT_QUOTES, 'UTF-8'); ?>" title="<?php echo $L->get('Email'); ?>">
<img class="profile-social-icon" src="<?php echo blowdit_asset('img/mail.svg') ?>" alt="<?php echo $L->get('Email'); ?>" />
</a>
<?php endif ?>
</div>
</header>
+2
View File
@@ -20,6 +20,8 @@ if (!function_exists('blowdit_icon')) {
'chevron-left' => '<path d="M15 18l-6-6 6-6"/>',
'chevron-right' => '<path d="M9 18l6-6-6-6"/>',
'house' => '<path d="M3 10.5 12 3l9 7.5"/><path d="M5 9.5V21h14V9.5"/>',
'link' => '<path d="M9 17H7A5 5 0 0 1 7 7h2"/><path d="M15 7h2a5 5 0 0 1 0 10h-2"/><path d="M8 12h8"/>',
'share' => '<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="M8.6 13.5l6.8 4M15.4 6.5l-6.8 4"/>',
// --- Theme picker (mirror in blowdit.js) ---
'sun' => '<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/>',
'moon' => '<path d="M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8z"/>',
+10
View File
@@ -47,6 +47,16 @@
<span class="d-inline d-md-none"><?php echo $L->get('RSS'); ?></span>
</a>
</li>
<!-- Email (mailto) -->
<?php if (!empty($blowditEmail)) : ?>
<li class="nav-item">
<a class="nav-link" href="mailto:<?php echo htmlspecialchars($blowditEmail, ENT_QUOTES, 'UTF-8'); ?>" title="<?php echo $L->get('Email'); ?>">
<img class="d-none d-md-block nav-svg-icon" src="<?php echo blowdit_asset('img/mail.svg') ?>" alt="<?php echo $L->get('Email'); ?>" />
<span class="d-inline d-md-none"><?php echo $L->get('Email'); ?></span>
</a>
</li>
<?php endif; ?>
<?php endif; ?>
<!-- Theme picker -->
+97 -1
View File
@@ -16,10 +16,23 @@
<h1 class="title"><?php echo blowdit_text($page->title()); ?></h1>
<?php if (!$page->isStatic() && !$url->notFound()): ?>
<!-- Creation date, reading time and author -->
<!-- Creation date, reading time and (if edited) updated date -->
<?php
$bdPubRaw = $page->getValue('date');
$bdModRaw = $page->getValue('dateModified');
$bdUpdated = '';
if ($bdModRaw && class_exists('Date')
&& substr($bdModRaw, 0, 10) !== substr($bdPubRaw, 0, 10)
&& $bdModRaw > $bdPubRaw) {
$bdUpdated = Date::format($bdModRaw, DB_DATE_FORMAT, 'M j, Y');
}
?>
<div class="metadata mb-4">
<span><?php echo blowdit_icon('calendar'); ?><?php echo $page->date(); ?></span>
<span><?php echo blowdit_icon('clock'); ?><?php echo $L->get('Reading time') . ': ' . $page->readingTime() ?></span>
<?php if ($bdUpdated): ?>
<span><?php echo blowdit_icon('calendar'); ?><?php echo $L->get('Updated') . ': ' . htmlspecialchars($bdUpdated, ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif ?>
</div>
<?php endif ?>
@@ -52,9 +65,92 @@
</div>
<?php endif ?>
<!-- Share -->
<?php if (!$page->isStatic() && !$url->notFound()) : ?>
<?php $bdShareUrl = $page->permalink(true); $bdShareTitle = blowdit_plain($page->title()); ?>
<div class="post-share mt-4">
<span class="post-share-label"><?php echo $L->get('Share'); ?></span>
<a class="post-share-btn" href="https://twitter.com/intent/tweet?url=<?php echo rawurlencode($bdShareUrl); ?>&amp;text=<?php echo rawurlencode($bdShareTitle); ?>" target="_blank" rel="noopener" aria-label="<?php echo $L->get('Share on X'); ?>">X</a>
<a class="post-share-btn" href="https://www.linkedin.com/sharing/share-offsite/?url=<?php echo rawurlencode($bdShareUrl); ?>" target="_blank" rel="noopener" aria-label="<?php echo $L->get('Share on LinkedIn'); ?>">LinkedIn</a>
<button type="button" class="post-share-btn js-copy-link" data-url="<?php echo htmlspecialchars($bdShareUrl, ENT_QUOTES, 'UTF-8'); ?>" aria-label="<?php echo $L->get('Copy link'); ?>"><?php echo blowdit_icon('link'); ?><span class="post-share-copy-label"><?php echo $L->get('Copy link'); ?></span></button>
</div>
<?php endif ?>
</div>
<!-- Load Bludit Plugins: Page End -->
<?php Theme::plugins('pageEnd'); ?>
</article>
<?php
// ---- Prev/next + related posts ----
// Best-effort via Bludit's Pages API; wrapped so any API difference degrades
// to nothing instead of erroring the page.
if (!$page->isStatic() && !$url->notFound()) {
$bdOlder = null; $bdNewer = null; $bdRelated = array();
try {
global $pages;
if (isset($pages) && method_exists($pages, 'getList')) {
$bdKeys = $pages->getList(1, 10000, true); // published, newest-first
if (is_array($bdKeys)) {
$bdKeys = array_values($bdKeys);
$bdCur = $page->key();
$bdI = array_search($bdCur, $bdKeys, true);
if ($bdI !== false) {
if (isset($bdKeys[$bdI + 1])) { $bdOlder = new Page($bdKeys[$bdI + 1]); }
if ($bdI > 0 && isset($bdKeys[$bdI - 1])) { $bdNewer = new Page($bdKeys[$bdI - 1]); }
}
$bdCat = $page->categoryKey();
$bdTags = array_keys($page->tags(true));
foreach ($bdKeys as $bdK) {
if ($bdK === $bdCur) { continue; }
if (count($bdRelated) >= 3) { break; }
$bdCand = new Page($bdK);
$bdMatch = ($bdCat && $bdCand->categoryKey() === $bdCat);
if (!$bdMatch && $bdTags) {
$bdMatch = (bool) array_intersect($bdTags, array_keys($bdCand->tags(true)));
}
if ($bdMatch) { $bdRelated[] = $bdCand; }
}
}
}
} catch (\Throwable $e) {
$bdOlder = null; $bdNewer = null; $bdRelated = array();
}
?>
<?php if ($bdOlder || $bdNewer) : ?>
<nav class="post-nav" aria-label="<?php echo $L->get('Pagination'); ?>">
<?php if ($bdOlder) : ?>
<a class="post-nav-link post-nav-prev" href="<?php echo $bdOlder->permalink(); ?>">
<?php echo blowdit_icon('chevron-left'); ?>
<span class="post-nav-text"><span class="post-nav-meta"><?php echo $L->get('Previous'); ?></span><span class="post-nav-title"><?php echo blowdit_text($bdOlder->title()); ?></span></span>
</a>
<?php else : ?><span></span><?php endif ?>
<?php if ($bdNewer) : ?>
<a class="post-nav-link post-nav-next" href="<?php echo $bdNewer->permalink(); ?>">
<span class="post-nav-text"><span class="post-nav-meta"><?php echo $L->get('Next'); ?></span><span class="post-nav-title"><?php echo blowdit_text($bdNewer->title()); ?></span></span>
<?php echo blowdit_icon('chevron-right'); ?>
</a>
<?php endif ?>
</nav>
<?php endif ?>
<?php if (!empty($bdRelated)) : ?>
<section class="related-posts">
<h2 class="related-posts-title"><?php echo $L->get('Related posts'); ?></h2>
<div class="related-grid">
<?php foreach ($bdRelated as $bdRp) : ?>
<a class="related-card" href="<?php echo $bdRp->permalink(); ?>">
<?php if ($bdRp->coverImage()) : ?>
<span class="related-cover" style="background-image:url('<?php echo htmlspecialchars($bdRp->coverImage(), ENT_QUOTES, 'UTF-8'); ?>')"></span>
<?php endif ?>
<span class="related-card-title"><?php echo blowdit_text($bdRp->title()); ?></span>
</a>
<?php endforeach ?>
</div>
</section>
<?php endif ?>
<?php } ?>