feat: add email sharing functionality, canonical URLs, and enhance metadata for SEO
This commit is contained in:
1 parent
efc83ec9f3
commit
d448574ff8
12 files changed
+513
-12
No files matched your search
+178
@@ -846,9 +846,16 @@ img.nav-svg-icon {
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-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 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
|
||||
@@ -1904,10 +1911,181 @@ button:focus:not(:focus-visible) {
|
||||
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
|
||||
======================================== */
|
||||
@media (max-width: 767.98px) {
|
||||
.post-nav { grid-template-columns: 1fr; }
|
||||
h1.title { font-size: 1.9rem; }
|
||||
h2.title { font-size: 1.5rem; }
|
||||
|
||||
|
||||
@@ -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 |
@@ -3,6 +3,18 @@
|
||||
// first so blowdit_icon() is available to head.php and every body template.
|
||||
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
|
||||
// pre-coloured. This paints the page in the theme colour from the first
|
||||
// byte of every response, eliminating the white flash between navigations.
|
||||
|
||||
@@ -25,6 +25,26 @@
|
||||
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)
|
||||
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 () {});
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
})();
|
||||
@@ -9,6 +9,16 @@
|
||||
"Skip to content": "Skip to content",
|
||||
"Pagination": "Pagination",
|
||||
"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",
|
||||
"Copied": "Copied",
|
||||
"Copy code": "Copy code",
|
||||
|
||||
@@ -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
@@ -2,9 +2,9 @@
|
||||
"author": "spleenftw",
|
||||
"email": "",
|
||||
"website": "https://github.com/Spleenftw/blowdit",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.0",
|
||||
"releaseDate": "2026-06-02",
|
||||
"license": "MIT",
|
||||
"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
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"/>',
|
||||
|
||||
@@ -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
@@ -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); ?>&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 } ?>
|
||||
Reference in new issue
Block a user