feat: Implement sticky sidebar TOC with active section highlighting and add related template files

This commit is contained in:
2026-03-18 23:18:27 +03:00
parent 5dfab393b7
commit 36aa886d26
5 changed files with 202 additions and 15 deletions
+6 -14
View File
@@ -1,6 +1,4 @@
# Future Enhancements Backlog
Here is a curated list of suggestions to further refine the blog's typography, user experience, feature set, and performance. Check items off as they are implemented!
## Typography & Reading Experience
@@ -8,33 +6,27 @@ Here is a curated list of suggestions to further refine the blog's typography, u
- [ ] **Text Wrap Balancing** — Apply `text-wrap: balance` to headings and `text-wrap: pretty` to paragraphs to prevent awkward orphaned words.
- [ ] **Fluid Typography Scale** — Refactor font sizes to use CSS `clamp()` functions for perfectly smooth scaling across all devices without relying on breakpoints.
- [ ] **Line Highlighting in Code Blocks** — Implement a custom shortcode or modify the syntax highlighter to allow highlighting specific lines of code (vital for exploit tutorials).
- [ ] **Reading Progress Time Remaining** — Instead of just a progress bar, dynamically calculate and display "X mins remaining" based on current scroll depth.
- [ ] **Footnote Backlinks** — Add a smooth-scroll return link (e.g., `[↩]`) inside the expanded footnote tooltip or at the bottom of the article to jump back to the reading position.
## UI / UX Polish
- [ ] **Image Zoom / Lightbox** — Implement a Medium-style image zoom (e.g., using `medium-zoom` library) allowing users to enlarge detailed screenshots by clicking them.
- [ ] **Heading Anchor Links** — Add an interactive `#` link that appears when hovering over `<h2>` and `<h3>` tags to make linking to specific sections easier.
- [ ] **Custom Terminal Scrollbars** — Style the browser's default scrollbar to match the dark terminal aesthetic using `::-webkit-scrollbar` and `scrollbar-color`.
- [ ] **Sticky Sidebar Table of Contents** — On wide desktop layouts, detach the Table of Contents and make it sticky on the right or left side so it follows the user as they read.
- [ ] **Code Block Copy Confirmation** — Add a subtle visual state change (like a checkmark or a "Copied!" popover toast) when a user successfully copies a code block.
- [ ] **Custom 404 Page** — Design an interactive terminal-style 404 page ("Command not found") that fits the pwnforfunandprofit theme.
## Features & Content
- [ ] **Post Series Support** — Add a `series` front-matter field to automatically group related tutorials (e.g., "Buffer Overflows Part 1, 2, 3") and render a "Part N of X" navigation box.
- [ ] **Interactive Exploit Demos** — If applicable, embed small interactive WebAssembly (WASM) or xterm.js terminals demonstrating basic concepts directly in the browser.
- [ ] **"Recently Viewed" Posts** — Use `localStorage` to remember the last 3 posts a user visited and display them on the homepage.
- [ ] **Drafts & Unlisted Posts** — Configure Zola to ignore posts marked with `draft = true` or create an `unlisted` tag that hides posts from the main feed but keeps them accessible via direct link.
- [ ] **Exploit Scripts Download** — A dedicated button or shortcode block that bundles all code snippets from an article into a single downloadable `.py` or `.c` file.
- [ ] **Interactive CVE Timeline** — A shortcode to render animated timeline infographics for big exploit write-ups or breach post-mortems.
- [ ] **Configurable Comments** — Modify the Utterances/Giscus integration to respect a `comments = false` flag in the post front-matter, allowing you to disable discussion on specific pages.
## Performance & SEO
- [ ] **Automatic WebP Image Optimization** — Refactor image rendering to use Zola's `resize_image` function to automatically generate highly compressed `webp` versions of all uploaded images at build time.
- [ ] **Lazy Loading** — Ensure all `<img>` and `<iframe>` tags across the site use `loading="lazy"` to drastically improve initial page load times.
- [ ] **Asset Preloading** — Add `<link rel="preload">` tags in the `<head>` for critical assets like the primary font files to eliminate text rendering flashes (FOUT/FOIT).
- [ ] **PWA / Offline Support** — Add a simple Service Worker and a `manifest.json` so readers can "install" the blog to their home screen and read cached articles offline.
- [ ] **RSS Feed Thumbnail Images** — Modify the `rss.xml` template to include post cover images (or a default banner) so aggregators display your articles more attractively.
- [ ] **Minify HTML & CSS** — Enable and configure Zola's built-in minification settings (`minify_html = true`) to shave off extra bytes in production.
+104
View File
@@ -261,6 +261,110 @@
}
}
// ──────────────────────────────────────────
// Sidebar TOC Layout
// ──────────────────────────────────────────
.post-layout {
display: contents;
@media (min-width: 1200px) {
display: grid;
grid-template-columns: minmax(0, 800px) 240px;
gap: 3rem;
align-items: start;
max-width: 1100px;
margin: 0 auto;
}
}
@media (min-width: 1200px) {
.post-detail {
max-width: none;
margin: 0;
}
// Hide inline TOC — sidebar takes over
.post-toc {
display: none !important;
}
}
.post-toc-sidebar {
display: none;
@media (min-width: 1200px) {
display: block;
position: sticky;
top: 5.5rem;
max-height: calc(100vh - 7rem);
overflow-y: auto;
padding-top: 4rem; // align with article content start
scrollbar-width: thin;
scrollbar-color: var(--border-color) transparent;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 4px;
}
}
}
.toc-nav {
ul {
list-style: none;
padding: 0;
margin: 0;
// Nested level (h2 children)
ul {
padding-left: 1rem;
margin-top: 0.25rem;
}
}
li {
margin-bottom: 0.35rem;
}
a {
display: block;
color: var(--text-muted);
font-size: 0.85rem;
line-height: 1.4;
padding: 0.2rem 0;
border-left: 2px solid transparent;
padding-left: 0.75rem;
transition: color 0.2s ease, border-color 0.2s ease;
text-decoration: none;
&:hover {
color: var(--accent-color);
}
&.toc-active {
color: var(--accent-color);
border-left-color: var(--accent-color);
}
}
}
.toc-sidebar-title {
font-size: 0.75rem;
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text-muted);
margin: 0 0 1rem 0;
padding-left: 0.75rem;
}
// ──────────────────────────────────────────
// Media Wrappers (YouTube, etc)
// ──────────────────────────────────────────
+61
View File
@@ -0,0 +1,61 @@
(function () {
'use strict';
const sidebar = document.querySelector('.post-toc-sidebar');
if (!sidebar) return;
const links = Array.from(sidebar.querySelectorAll('a[data-toc-id]'));
if (!links.length) return;
const headingIds = links.map(a => a.dataset.tocId);
const headings = headingIds
.map(id => document.getElementById(id))
.filter(Boolean);
let activeId = null;
function setActive(id) {
if (id === activeId) return;
activeId = id;
links.forEach(a => {
if (a.dataset.tocId === id) {
a.classList.add('toc-active');
} else {
a.classList.remove('toc-active');
}
});
}
const observer = new IntersectionObserver(
(entries) => {
// Find the topmost visible heading
const visible = headings.filter(h => {
const rect = h.getBoundingClientRect();
return rect.top >= 0 && rect.top < window.innerHeight * 0.6;
});
if (visible.length > 0) {
setActive(visible[0].id);
} else {
// If scrolled past all headings, keep last active
const above = headings.filter(h => h.getBoundingClientRect().top < 0);
if (above.length > 0) {
setActive(above[above.length - 1].id);
}
}
},
{
rootMargin: '-10% 0px -60% 0px',
threshold: 0,
}
);
headings.forEach(h => observer.observe(h));
// Set initial active on load
const firstVisible = headings.find(h => {
const rect = h.getBoundingClientRect();
return rect.top >= 0;
});
if (firstVisible) setActive(firstVisible.id);
})();
+10 -1
View File
@@ -82,6 +82,11 @@
<!-- Reading Progress Bar -->
<script defer src="{{ get_url(path="js/progress.js") }}"></script>
<!-- Sticky TOC active-section highlighting -->
{% if page.toc %}
<script defer src="{{ get_url(path="js/toc.js") }}"></script>
{% endif %}
{% endblock head %}
@@ -90,6 +95,7 @@
<div class="reading-progress-bar" id="reading-progress-bar"></div>
</div>
<div class="post-layout">
<article class="post-detail">
<header class="post-header">
<h1 class="post-title">{{ page.title }}</h1>
@@ -137,9 +143,12 @@
</div>
{% include "partials/social-share.html" %}
</div>
<!-- Utterances comments (posts only) -->
{% include "partials/comments.html" %}
</footer>
</article>
{% include "partials/toc-sidebar.html" %}
</div>
{% endblock content %}
+21
View File
@@ -0,0 +1,21 @@
{% if page.toc %}
<aside class="post-toc-sidebar" aria-label="Table of contents">
<nav class="toc-nav">
<h2 class="toc-sidebar-title">Contents</h2>
<ul>
{% for h1 in page.toc %}
<li>
<a href="{{ h1.permalink | safe }}" data-toc-id="{{ h1.id }}">{{ h1.title }}</a>
{% if h1.children %}
<ul>
{% for h2 in h1.children %}
<li><a href="{{ h2.permalink | safe }}" data-toc-id="{{ h2.id }}">{{ h2.title }}</a></li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>
</nav>
</aside>
{% endif %}