The companion pelican-svbhack theme repo lives here.
We upgraded the pelican-svbhack theme to add a right sidebar table of contents. This enhancement improves navigation on longer articles by providing quick access to section headings. The theme now features responsive breakpoints, dark mode with theme-aware syntax highlighting, and progressive enhancement for JavaScript-dependent features.
the design
The implementation follows these principles:
- Desktop experience: Fixed right sidebar at 10% width displaying the table of contents
- Mobile experience: Table of contents remains inline within the article content
- Pure CSS: No JavaScript required for positioning or behavior
- Fixed header: Navigation stays visible while scrolling
- CSS variables: All colors use variables for easy theming
key features
Right Sidebar TOC
On desktop screens (1024px+), the table of contents automatically moves from inline to a fixed right sidebar. The sidebar scrolls independently from the main content, with the scrollbar hidden for a clean appearance.
Article-Only Display
The right sidebar only appears on article pages, not on the homepage or archive pages. This is accomplished using a body class system that targets article pages specifically.
Fixed Header
The main navigation header now uses position: fixed so it stays visible when scrolling. All padding and spacing was adjusted to accommodate this change.
Responsive Design
On tablets and mobile devices (screens ≤1023px), the table of contents reverts to its inline position within the article content, ensuring usability across all screen sizes. The media queries are structured to unfix positioned elements first, then reposition them, creating smooth visual transitions at breakpoints.
Article content uses fluid width (max-width: 660px; width: 100%) instead of fixed width, allowing it to shrink responsively when the TOC sidebar is present. This prevents overlap at screen widths between 1024px and approximately 1300px, where the fixed header, article content, and TOC sidebar would otherwise compete for space.
CSS Color Variables
All hardcoded colors were converted to CSS custom properties:
:root {
--color-white: #ffffff;
--color-black: #000000;
--color-text: #4d4d4d;
--color-link: #0084B4;
--color-border: #eeeeee;
/* ... and more */
}
This makes it trivial to create new color schemes or dark mode variants.
implementation details
The right sidebar uses CSS positioning to reposition the existing inline table of contents:
body.article main article div.article_text div.contents {
position: fixed;
top: 0;
right: 0;
width: 10%;
height: 100vh;
padding: 40px 15px 40px 15px;
overflow-y: auto;
/* Hide scrollbar but keep functionality */
scrollbar-width: none;
-ms-overflow-style: none;
}
When the TOC is present, the main content width adjusts using the :has() pseudo-class:
body.article main:has(article div.article_text div.contents) {
width: 65%;
}
homepage summary filtering
ReStructuredText automatically generates anchor links in section headings when a table of contents is present. These anchors work perfectly on article pages, but become broken links when article summaries appear on the homepage - the anchor targets don't exist in that context.
To solve this, we created a Pelican plugin that provides a strip_anchors Jinja2 filter. This filter removes TOC divs and toc-backref anchor links from article summaries before they're rendered on the homepage.
Update: We submitted Pull Request #3512 to add this functionality directly to Pelican core. Once merged, this will work automatically without needing the plugin or template filter.
The Plugin
The plugin (lib/strip_toc_summary.py) uses regex to strip problematic HTML:
def strip_anchors(text):
"""Jinja2 filter to strip TOC and anchor links from HTML text."""
if not text:
return text
# Remove the entire <div class="contents"> ... </div> block
text = re.sub(
r'<div\s+class="contents[^"]*"[^>]*>.*?</div>',
'',
text,
flags=re.DOTALL | re.IGNORECASE
)
# Remove anchor links from headings
# Converts <a class="toc-backref" href="#id1">text</a> to just text
text = re.sub(
r'<a[^>]*class="[^"]*toc-backref[^"]*"[^>]*>(.*?)</a>',
r'\1',
text,
flags=re.DOTALL | re.IGNORECASE
)
return text
Template Usage
The filter is applied in the theme's index.html template:
<div class="article_text">
{{ article.summary | strip_anchors }}
</div>
This approach ensures that:
- Article pages retain their TOC anchor links for proper navigation
- Homepage summaries display clean headings without broken links
- The filtering happens at template render time, not during content generation
Why This Matters
Without this filter, homepage excerpts would contain links like <a href="#the-design">the design</a> that point to anchors that don't exist on the homepage. This creates a poor user experience with broken navigation. The filter strips these anchors while preserving the heading text, resulting in clean, functional homepage previews.
dark mode implementation
After publishing this post, we immediately implemented dark mode using the CSS variable system! The dark theme is now the default, with a light mode toggle available in both desktop and mobile navigation.
Key Features
The dark mode implementation includes:
- Default dark theme with light mode as the alternative
- localStorage persistence - your theme preference is saved across sessions
- Flash-of-unstyled-content prevention - inline script in <head> applies theme before page renders
- Toggle buttons in both desktop header and mobile menu
- Inverted colors for third-party widgets like UncloseAI button
Theme Colors
The dark mode uses these color overrides:
:root.light-mode {
--color-white: #000000;
--color-black: #ffffff;
--color-text: #333333;
--color-link: #0084B4;
--color-link-visited: #551A8B;
--color-border: #dddddd;
--color-code-bg: #f5f5f5;
--color-background: #ffffff;
--color-header-bg: #ffffff;
}
By inverting the black and white variables and adjusting the other colors, the entire theme switches seamlessly between dark and light modes.
Flash-of-Unstyled-Content Prevention
To prevent the flash-of-unstyled-content when loading a saved theme preference, an inline script runs synchronously in the <head>:
(function() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'light') {
document.documentElement.classList.add('light-mode');
}
})();
This applies the theme class before the page renders, eliminating any visual flash.
Button Text Synchronization
The toggle buttons need to show the opposite of the current theme (when in dark mode, button says "Light"). This is handled by another inline script at the bottom of the page that executes after the buttons are in the DOM but before the page is visible:
(function() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'light') {
const desktopButton = document.getElementById('theme-toggle');
const mobileButton = document.getElementById('mobile-theme-toggle');
if (desktopButton) desktopButton.textContent = 'Dark';
if (mobileButton) mobileButton.textContent = 'Dark';
}
})();
Third-Party Widget Styling
The UncloseAI button required special handling with hardcoded colors to override its default styling:
/* Dark mode (default) - white background, black text */
.uncloseai-floating-button,
#floating-ai-button {
background-color: #ffffff;
color: #000000;
border: 2px solid #000000;
}
/* Light mode - black background, white text */
:root.light-mode .uncloseai-floating-button,
:root.light-mode #floating-ai-button {
background-color: #000000;
color: #ffffff;
border: 2px solid #ffffff;
}
The UncloseAI widget dynamically sets CSS custom properties for its button colors, but our CSS naturally overrides these defaults because our theme stylesheet loads after the widget's CSS. No !important flags are needed - standard CSS cascade rules are sufficient.
Progressive Enhancement - Hiding Buttons Without JavaScript
The theme toggle buttons only work when JavaScript is enabled, so they should be hidden when JavaScript is disabled. This is accomplished using progressive enhancement:
/* Hide theme toggle buttons by default */
#theme-toggle,
#mobile-theme-toggle {
display: none;
}
/* Show buttons only when JavaScript is enabled */
.js-enabled #theme-toggle,
.js-enabled #mobile-theme-toggle {
display: inline-block;
}
The js-enabled class is added to the document element in the same inline script that handles flash-of-unstyled-content prevention:
(function() {
// Add js-enabled class to show theme toggle buttons
document.documentElement.classList.add('js-enabled');
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'light') {
document.documentElement.classList.add('light-mode');
}
})();
This approach ensures the buttons are never visible if they can't function. Without JavaScript, users still get a perfectly usable site in the default dark theme - they just don't see non-functional toggle buttons cluttering the interface.
This technique follows the same principles we wrote about in the capability driven presentation post from 2016. For more on resilient web design philosophy, I highly recommend the free online book Resilient Web Design by Jeremy Keith.
Syntax Highlighting Theme Switching
Code syntax highlighting needed theme-aware styling since most color schemes are designed for either light or dark backgrounds. We implemented automatic switching between Pygments themes:
- Dark mode: Uses the monokai style with bright colors on dark backgrounds
- Light mode: Uses the default style with dark colors on light backgrounds
The implementation loads both stylesheets but disables the inactive one:
<!-- Dark mode syntax highlighting (default) -->
<link rel="stylesheet" href="/theme/css/pygments-dark.css" id="pygments-dark">
<!-- Light mode syntax highlighting -->
<link rel="stylesheet" href="/theme/css/pygments-light.css" id="pygments-light" disabled>
The theme toggle function switches between them by enabling/disabling the stylesheets:
function toggleTheme() {
const pygmentsDark = document.getElementById('pygments-dark');
const pygmentsLight = document.getElementById('pygments-light');
if (root.classList.contains('light-mode')) {
// Switching to dark mode
pygmentsDark.disabled = false;
pygmentsLight.disabled = true;
} else {
// Switching to light mode
pygmentsDark.disabled = true;
pygmentsLight.disabled = false;
}
}
The flash-of-unstyled-content prevention script also switches the syntax highlighting stylesheet before the page renders, ensuring code blocks always appear with the correct colors.
Preventing Scroll Jump with URL Anchors
When toggling themes on a page with a URL anchor (like #syntax-highlighting-theme-switching), the browser would jump to the anchor after theme changes caused layout reflow. This creates poor UX when users toggle themes mid-article.
The solution temporarily removes the hash from the URL during theme toggle, then restores it without triggering navigation:
function toggleTheme() {
// Save hash and temporarily remove to prevent jump
const hash = window.location.hash;
if (hash) {
history.replaceState(null, null, ' ');
}
// ... perform theme toggle ...
// Restore hash without jumping
if (hash) {
history.replaceState(null, null, hash);
}
}
By removing the hash before theme changes, the browser has no anchor to jump to during layout reflow. The hash is restored afterward, preserving the URL without triggering the browser's default anchor-jumping behavior.