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

+178
View File
@@ -846,9 +846,16 @@ img.nav-svg-icon {
overflow: hidden; overflow: hidden;
border-radius: var(--radius-md); border-radius: var(--radius-md);
margin-bottom: var(--space-md); margin-bottom: var(--space-md);
/* Reserve space at a fixed ratio so the cover doesn't shift layout while it
loads (Cumulative Layout Shift). */
aspect-ratio: 16 / 9;
} }
.cover-image-wrapper .card-img-top { .cover-image-wrapper .card-img-top {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 0.4s ease; transition: transform 0.4s ease;
} }
@@ -1904,10 +1911,181 @@ button:focus:not(:focus-visible) {
pre, blockquote, table, figure, img { page-break-inside: avoid; } pre, blockquote, table, figure, img { page-break-inside: avoid; }
} }
/* ========================================
HEADING ANCHOR LINKS
======================================== */
.content h2,
.content h3,
.content h4 { position: relative; }
.heading-anchor {
margin-left: 0.4rem;
color: var(--text-muted);
font-weight: 400;
text-decoration: none;
opacity: 0;
transition: opacity 0.15s ease, color 0.15s ease;
}
.content h2:hover .heading-anchor,
.content h3:hover .heading-anchor,
.content h4:hover .heading-anchor,
.heading-anchor:focus { opacity: 0.55; }
.heading-anchor:hover { color: var(--accent); opacity: 1; text-decoration: none; }
@media (hover: none) { .heading-anchor { opacity: 0.4; } }
/* ========================================
SHARE BUTTONS
======================================== */
.post-share {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.4rem;
margin-top: var(--space-lg);
padding-top: var(--space-md);
border-top: 1px solid var(--border-subtle);
}
.post-share-label {
font-size: var(--font-size-xs);
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
margin-right: 0.3rem;
}
.post-share-btn {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.3rem 0.8rem;
border: 1px solid var(--border-color);
border-radius: 999px;
background: transparent;
color: var(--text-secondary);
font-size: var(--font-size-xs);
font-weight: 600;
font-family: var(--font-sans);
cursor: pointer;
transition: all 0.2s ease;
}
.post-share-btn:hover,
.post-share-btn.is-copied {
background: var(--accent);
color: var(--accent-contrast);
border-color: var(--accent);
text-decoration: none;
}
.post-share-btn .bd-icon { margin-right: 0; }
/* ========================================
PREV / NEXT ARTICLE NAV
======================================== */
.post-nav {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-sm);
margin: var(--space-lg) 0;
}
.post-nav-link {
display: flex;
align-items: center;
gap: 0.5rem;
min-width: 0;
padding: var(--space-sm) var(--space-md);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
background: var(--bg-surface);
color: var(--text-primary);
transition: all 0.2s ease;
}
.post-nav-link:hover {
border-color: var(--accent);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
text-decoration: none;
}
.post-nav-next { justify-content: flex-end; text-align: right; }
.post-nav-text { display: flex; flex-direction: column; min-width: 0; }
.post-nav-meta {
font-size: var(--font-size-xs);
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
}
.post-nav-title {
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.post-nav .bd-icon { flex: 0 0 auto; color: var(--text-muted); margin: 0; }
/* ========================================
RELATED POSTS
======================================== */
.related-posts { margin: var(--space-xl) 0 var(--space-lg); }
.related-posts-title { font-size: 1.15rem; margin-bottom: var(--space-md); }
.related-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: var(--space-sm);
}
.related-card {
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
background: var(--bg-surface);
color: var(--text-primary);
transition: all 0.2s ease;
}
.related-card:hover {
border-color: var(--accent);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
text-decoration: none;
}
.related-cover {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
background-size: cover;
background-position: center;
background-color: var(--bg-subtle);
}
.related-card-title {
padding: 0.7rem 0.85rem;
font-weight: 600;
font-size: 0.95rem;
line-height: 1.3;
}
/* ======================================== /* ========================================
RESPONSIVE RESPONSIVE
======================================== */ ======================================== */
@media (max-width: 767.98px) { @media (max-width: 767.98px) {
.post-nav { grid-template-columns: 1fr; }
h1.title { font-size: 1.9rem; } h1.title { font-size: 1.9rem; }
h2.title { font-size: 1.5rem; } h2.title { font-size: 1.5rem; }
+1
View File
@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Email</title><path d="M1.5 4.5h21A1.5 1.5 0 0 1 24 6v12a1.5 1.5 0 0 1-1.5 1.5h-21A1.5 1.5 0 0 1 0 18V6a1.5 1.5 0 0 1 1.5-1.5zm.9 2.1L12 13.2l9.6-6.6H2.4zM21.9 8.4l-9.3 6.4a1 1 0 0 1-1.2 0L2.1 8.4V17.4h19.8V8.4z" style="fill: rgba(255, 255, 255, 0.5);"/></svg>

After

Width:  |  Height:  |  Size: 338 B

+12
View File
@@ -3,6 +3,18 @@
// first so blowdit_icon() is available to head.php and every body template. // first so blowdit_icon() is available to head.php and every body template.
include(THEME_DIR_PHP.'icons.php'); include(THEME_DIR_PHP.'icons.php');
// ---- Theme contact / social config ----
// The mailto icon uses Bludit's "Sender email" (Settings -> Advanced ->
// Email account settings). Leave it to auto-detect, or hard-set $blowditEmail
// below to override. Empty -> the icon is hidden.
$blowditEmail = '';
if (isset($site) && method_exists($site, 'getField')) {
$blowditEmail = $site->getField('emailFromAddress'); // Bludit's "Sender email"
if (empty($blowditEmail)) { $blowditEmail = $site->getField('email'); }
}
// $blowditEmail = 'you@example.com'; // <- uncomment to set manually
$blowditTwitterHandle = '@spleenftw'; // twitter:site / twitter:creator; '' to omit
// Read the theme from a cookie so the root element can be rendered // Read the theme from a cookie so the root element can be rendered
// pre-coloured. This paints the page in the theme colour from the first // pre-coloured. This paints the page in the theme colour from the first
// byte of every response, eliminating the white flash between navigations. // byte of every response, eliminating the white flash between navigations.
+99
View File
@@ -25,6 +25,26 @@
imageViewer: I18N.imageViewer || 'Image viewer' imageViewer: I18N.imageViewer || 'Image viewer'
}; };
// Clipboard helper (Clipboard API with a legacy fallback).
function copyToClipboard(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(text);
}
return new Promise(function (resolve, reject) {
try {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
resolve();
} catch (e) { reject(e); }
});
}
/* -------------------------------------------------------- /* --------------------------------------------------------
Mobile navbar toggle (replaces Bootstrap's collapse JS) Mobile navbar toggle (replaces Bootstrap's collapse JS)
Toggles the .show class that Bootstrap's CSS already styles. Toggles the .show class that Bootstrap's CSS already styles.
@@ -870,4 +890,83 @@
}); });
})(); })();
/* --------------------------------------------------------
Heading anchor links — hover a heading to reveal a "#" that links to it.
IDs are assigned by the ToC script; assign any missing ones here too.
-------------------------------------------------------- */
(function () {
var content = document.querySelector('.content');
if (!content) return;
var headings = content.querySelectorAll('h2, h3, h4');
if (!headings.length) return;
var used = {};
Array.prototype.forEach.call(content.querySelectorAll('[id]'), function (el) {
used[el.id] = true;
});
Array.prototype.forEach.call(headings, function (h) {
if (!h.id) {
var base = h.textContent.trim().toLowerCase()
.replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-')
.replace(/^-+|-+$/g, '') || 'section';
var id = base, n = 2;
while (used[id]) { id = base + '-' + (n++); }
used[id] = true;
h.id = id;
}
if (h.querySelector('.heading-anchor')) return;
var a = document.createElement('a');
a.className = 'heading-anchor';
a.href = '#' + h.id;
a.setAttribute('aria-label', 'Link to this section');
a.textContent = '#';
h.appendChild(a);
});
})();
/* --------------------------------------------------------
Harden outbound links in article content: rel="noopener noreferrer"
(anti reverse-tabnabbing + no referrer leak) and open in a new tab.
-------------------------------------------------------- */
(function () {
var content = document.querySelector('.content');
if (!content) return;
Array.prototype.forEach.call(content.querySelectorAll('a[href]'), function (a) {
if (!/^https?:\/\//i.test(a.getAttribute('href') || '')) return;
if (a.host === window.location.host) return; // same-site: leave alone
var rel = (a.getAttribute('rel') || '').split(/\s+/).filter(Boolean);
['noopener', 'noreferrer'].forEach(function (r) {
if (rel.indexOf(r) === -1) rel.push(r);
});
a.setAttribute('rel', rel.join(' '));
if (!a.getAttribute('target')) a.setAttribute('target', '_blank');
});
})();
/* --------------------------------------------------------
"Copy link" share button.
-------------------------------------------------------- */
(function () {
var btns = document.querySelectorAll('.js-copy-link');
if (!btns.length) return;
Array.prototype.forEach.call(btns, function (btn) {
var label = btn.querySelector('.post-share-copy-label');
var resetTimer = null;
btn.addEventListener('click', function () {
var url = btn.getAttribute('data-url') || window.location.href;
copyToClipboard(url).then(function () {
btn.classList.add('is-copied');
var original = label ? label.textContent : '';
if (label) label.textContent = T.copied;
clearTimeout(resetTimer);
resetTimer = setTimeout(function () {
btn.classList.remove('is-copied');
if (label) label.textContent = original;
}, 1500);
}).catch(function () {});
});
});
})();
})(); })();
+10
View File
@@ -9,6 +9,16 @@
"Skip to content": "Skip to content", "Skip to content": "Skip to content",
"Pagination": "Pagination", "Pagination": "Pagination",
"RSS": "RSS", "RSS": "RSS",
"Email": "Email",
"Share": "Share",
"Share on X": "Share on X",
"Share on LinkedIn": "Share on LinkedIn",
"Copy link": "Copy link",
"Link copied": "Link copied",
"Related posts": "Related posts",
"Previous": "Previous",
"Next": "Next",
"Updated": "Updated",
"Copy": "Copy", "Copy": "Copy",
"Copied": "Copied", "Copied": "Copied",
"Copy code": "Copy code", "Copy code": "Copy code",
+18
View File
@@ -0,0 +1,18 @@
{
"name": "Blog",
"short_name": "Blog",
"description": "A Bludit blog using the Blowdit theme.",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#171717",
"theme_color": "#1f1f1f",
"icons": [
{
"src": "img/favicon.png",
"sizes": "any",
"type": "image/png",
"purpose": "any"
}
]
}
+2 -2
View File
@@ -2,9 +2,9 @@
"author": "spleenftw", "author": "spleenftw",
"email": "", "email": "",
"website": "https://github.com/Spleenftw/blowdit", "website": "https://github.com/Spleenftw/blowdit",
"version": "1.3.0", "version": "1.4.0",
"releaseDate": "2026-06-02", "releaseDate": "2026-06-02",
"license": "MIT", "license": "MIT",
"compatible": "3.22.0", "compatible": "3.22.0",
"notes": "A Bludit theme inspired by the Blowfish Hugo theme. Five-theme swatch picker (Light/Dark/Nord/Dracula/Catppuccin), sticky ToC sidebar on articles, image lightbox with zoom, image carousel (```carousel fence), tabbed code blocks (```tabs fence), smart sidebar per page type, and clean responsive typography. v1.3.0: SEO (Open Graph, Twitter Card, JSON-LD, theme-color, cover preload), per-theme syntax highlighting (self-hosted highlight.js), copy-to-clipboard on code blocks, numbered pagination, inline-SVG icons (dropped the Bootstrap Icons webfont), removed jQuery/Bootstrap JS, deferred cached JS bundle, reading progress bar, back-to-top, randomised sidebar selection, lazy-loaded images, accessibility (skip link, ARIA, :focus-visible) and a print stylesheet." "notes": "A Bludit theme inspired by the Blowfish Hugo theme. Five-theme swatch picker (Light/Dark/Nord/Dracula/Catppuccin), sticky ToC sidebar on articles, image lightbox with zoom, image carousel (```carousel fence), tabbed code blocks (```tabs fence), smart sidebar per page type, and clean responsive typography. v1.3.0: SEO (Open Graph, Twitter Card, JSON-LD, theme-color, cover preload), per-theme syntax highlighting (self-hosted highlight.js), copy-to-clipboard on code blocks, numbered pagination, inline-SVG icons (dropped the Bootstrap Icons webfont), removed jQuery/Bootstrap JS, deferred cached JS bundle, reading progress bar, back-to-top, randomised sidebar selection, lazy-loaded images, accessibility (skip link, ARIA, :focus-visible) and a print stylesheet. v1.4.0: canonical URL, WebSite + BreadcrumbList + author sameAs JSON-LD, og:image:alt / twitter:site+creator, cover-image aspect-ratio (CLS), heading anchor links, prev/next article nav, related posts, share buttons (X/LinkedIn/copy-link), mailto social icon, PWA web app manifest, 'updated on' date, and outbound-link hardening (noopener noreferrer)."
} }
+78 -9
View File
@@ -93,6 +93,9 @@
$ogType = $ogIsArticle ? 'article' : 'website'; $ogType = $ogIsArticle ? 'article' : 'website';
$ogSiteName = $site->title(); $ogSiteName = $site->title();
?> ?>
<!-- Canonical URL -->
<link rel="canonical" href="<?php echo htmlspecialchars($ogUrl, ENT_QUOTES, 'UTF-8'); ?>">
<!-- Open Graph --> <!-- Open Graph -->
<meta property="og:type" content="<?php echo htmlspecialchars($ogType, ENT_QUOTES, 'UTF-8'); ?>"> <meta property="og:type" content="<?php echo htmlspecialchars($ogType, ENT_QUOTES, 'UTF-8'); ?>">
<meta property="og:site_name" content="<?php echo blowdit_text($ogSiteName); ?>"> <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:description" content="<?php echo blowdit_text($ogDesc); ?>">
<meta property="og:url" content="<?php echo htmlspecialchars($ogUrl, ENT_QUOTES, 'UTF-8'); ?>"> <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" 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): ?> <?php if ($ogIsArticle): ?>
<meta property="article:published_time" content="<?php echo htmlspecialchars($ogPublishedIso, ENT_QUOTES, 'UTF-8'); ?>"> <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()); ?>"> <meta property="article:author" content="<?php echo blowdit_text($page->username()); ?>">
<?php endif ?> <?php endif ?>
<!-- Twitter Card --> <!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image"> <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:title" content="<?php echo blowdit_text($ogTitle); ?>">
<meta name="twitter:description" content="<?php echo blowdit_text($ogDesc); ?>"> <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" content="<?php echo htmlspecialchars($ogImage, ENT_QUOTES, 'UTF-8'); ?>">
<meta name="twitter:image:alt" content="<?php echo blowdit_text($ogTitle); ?>">
<?php <?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) { if ($ogIsArticle) {
$ldDescription = $page->description() ?: ($site->description() ?: $site->slogan()); $ldDescription = $page->description() ?: ($site->description() ?: $site->slogan());
$ldImage = $page->coverImage(true) ?: (DOMAIN_THEME . 'img/spleenftw.jpeg'); $ldImage = $page->coverImage(true) ?: (DOMAIN_THEME . 'img/spleenftw.jpeg');
@@ -127,6 +170,13 @@
$ldTimeRequired = 'PT' . $rtMatch[0] . 'M'; $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( $jsonLd = array(
'@context' => 'https://schema.org', '@context' => 'https://schema.org',
'@type' => 'BlogPosting', '@type' => 'BlogPosting',
@@ -135,11 +185,7 @@
'image' => $ldImage, 'image' => $ldImage,
'datePublished' => $ldPublished, 'datePublished' => $ldPublished,
'dateModified' => $ldModified, 'dateModified' => $ldModified,
'author' => array( 'author' => $author,
'@type' => 'Person',
'name' => blowdit_plain($page->username()),
'url' => Theme::siteUrl(),
),
'publisher' => array( 'publisher' => array(
'@type' => 'Organization', '@type' => 'Organization',
'name' => blowdit_plain($site->title()), 'name' => blowdit_plain($site->title()),
@@ -155,10 +201,29 @@
'wordCount' => str_word_count(strip_tags($page->content())), 'wordCount' => str_word_count(strip_tags($page->content())),
); );
if ($ldTimeRequired) { $jsonLd['timeRequired'] = $ldTimeRequired; } if ($ldTimeRequired) { $jsonLd['timeRequired'] = $ldTimeRequired; }
$blowditEmitLd($jsonLd);
echo '<script type="application/ld+json">' // Breadcrumb: Home > [Category] > Article
. json_encode($jsonLd, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) $crumbs = array(array(
. '</script>' . "\n"; '@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 --> <!-- Include Favicon -->
<?php echo Theme::favicon('img/favicon.png'); ?> <?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: <!-- Inter typeface (Blowfish-like typography) — loaded non-render-blocking:
fetched as print media, then flipped to all once it arrives. --> fetched as print media, then flipped to all once it arrives. -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <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'); ?>"> <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'); ?>" /> <img class="profile-social-icon" src="<?php echo blowdit_asset('img/rss.svg') ?>" alt="<?php echo $L->get('RSS'); ?>" />
</a> </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> </div>
</header> </header>
+2
View File
@@ -20,6 +20,8 @@ if (!function_exists('blowdit_icon')) {
'chevron-left' => '<path d="M15 18l-6-6 6-6"/>', 'chevron-left' => '<path d="M15 18l-6-6 6-6"/>',
'chevron-right' => '<path d="M9 18l6-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"/>', '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) --- // --- 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"/>', '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"/>', '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> <span class="d-inline d-md-none"><?php echo $L->get('RSS'); ?></span>
</a> </a>
</li> </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; ?> <?php endif; ?>
<!-- Theme picker --> <!-- Theme picker -->
+97 -1
View File
@@ -16,10 +16,23 @@
<h1 class="title"><?php echo blowdit_text($page->title()); ?></h1> <h1 class="title"><?php echo blowdit_text($page->title()); ?></h1>
<?php if (!$page->isStatic() && !$url->notFound()): ?> <?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"> <div class="metadata mb-4">
<span><?php echo blowdit_icon('calendar'); ?><?php echo $page->date(); ?></span> <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> <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> </div>
<?php endif ?> <?php endif ?>
@@ -52,9 +65,92 @@
</div> </div>
<?php endif ?> <?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> </div>
<!-- Load Bludit Plugins: Page End --> <!-- Load Bludit Plugins: Page End -->
<?php Theme::plugins('pageEnd'); ?> <?php Theme::plugins('pageEnd'); ?>
</article> </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 } ?>