A lightweight, elegant blogging platform built with FastHTML
Project description
title: Bloggy - A FastHTML Blogging Platform
Bloggy
A lightweight, elegant blogging platform built with FastHTML that renders Markdown files into beautiful web pages with advanced features.
Simple Table
| Feature | Description |
|---|---|
| FastHTML Integration | Built on FastHTML for high performance and ease of use |
| Advanced Markdown Support | Footnotes as sidenotes, YouTube embeds, task lists, Mermaid diagrams, math notation, tabbed content, and more |
| Modern UI | Responsive design, dark mode, three-panel layout, HTMX navigation |
| Interactive Diagrams | Zoomable, pannable Mermaid diagrams with fullscreen support |
Architecture Overview
---
width: 80vw
---
graph TB
subgraph "User Interface"
Browser[Web Browser]
Theme[Light/Dark Theme Toggle]
end
subgraph "FastHTML Application"
App[FastHTML App<br/>core.py]
Router[URL Router]
Layout[Layout Engine]
subgraph "Route Handlers"
Index[Index Route<br/>/]
PostDetail[Post Detail Route<br/>/posts/path]
end
end
subgraph "Markdown Processing"
MDParser[Mistletoe Parser]
Renderer[ContentRenderer]
subgraph "Custom Renderers"
Footnotes[Footnote Renderer<br/>Sidenotes]
Mermaid[Mermaid Diagram Renderer<br/>Zoom/Pan Controls]
Links[Link Renderer<br/>HTMX Integration]
end
end
subgraph "File System"
MDFiles[Markdown Files<br/>.md]
Tree[Folder Tree Builder<br/>build_post_tree]
end
subgraph "Frontend Assets"
Static[Static Files]
JS[scripts.js<br/>Mermaid + Interactions]
CSS[Styles<br/>TailwindCSS + MonsterUI]
end
Browser -->|HTTP Request| Router
Theme -->|Toggle Dark Mode| JS
Router --> Index
Router --> PostDetail
Index --> Tree
Tree --> MDFiles
Index --> Layout
PostDetail --> MDFiles
PostDetail --> MDParser
MDParser --> Renderer
Renderer --> Footnotes
Renderer --> Mermaid
Renderer --> Links
Footnotes -->|Marginal Notes| Layout
Mermaid -->|Interactive Diagrams| Layout
Links -->|HTMX Navigation| Layout
Layout --> Browser
JS -->|Theme Change| Mermaid
JS -->|Zoom/Pan/Reset| Mermaid
Static --> CSS
Static --> JS
App -.->|Serves| Static
style Browser fill:#e1f5ff
style App fill:#fff3cd
style MDParser fill:#d4edda
style Static fill:#f8d7da
style Mermaid fill:#cce5ff
style Footnotes fill:#cce5ff
How Bloggy Works
1. Request Flow
---
width: 80vw
---
sequenceDiagram
participant User
participant Browser
participant FastHTML
participant Router
participant FileSystem
participant Renderer
participant HTMX
User->>Browser: Visit /
Browser->>FastHTML: GET /
FastHTML->>Router: Route to index()
Router->>FileSystem: Scan for .md files
FileSystem-->>Router: Return file tree
Router->>Browser: Render post list + layout
User->>Browser: Click post link
Browser->>HTMX: hx-get="/posts/demo"
HTMX->>FastHTML: GET /posts/demo
FastHTML->>Router: Route to post_detail()
Router->>FileSystem: Read demo.md
FileSystem-->>Router: Markdown content
Router->>Renderer: Parse & render markdown
rect rgb(200, 220, 250)
Note over Renderer: Process custom syntax:<br/>- Footnotes [^1]<br/>- Mermaid diagrams<br/>- Internal links
end
Renderer-->>Router: Rendered HTML
Router->>HTMX: Return HTML fragment
HTMX->>Browser: Swap content (#main-content)
Browser->>User: Display post
2. Markdown Processing Pipeline
---
width: 80vw
---
flowchart LR
A[Raw Markdown] --> B{Extract Footnotes}
B -->|Content| C[Preprocess Super/Sub]
B -->|Footnote Defs| D[Store in Dict]
C --> E[Preprocess Tabs]
E -->|Content + Placeholders| F[Mistletoe Parser]
E -->|Tab Data| G[Tab Store]
F --> H[Token Stream]
H --> I{ContentRenderer}
I -->|BlockCode + 'mermaid'| J[Mermaid Renderer]
I -->|Link| K[Link Renderer]
I -->|FootnoteRef| L[Footnote Renderer]
I -->|Other| M[Standard HTML]
J --> N[Diagram with Controls]
K --> O{Relative/Internal?}
O -->|Relative| P[Resolve Path]
P --> Q[Add HTMX attrs]
O -->|Internal| Q
O -->|External| R[Add target=_blank]
L --> S[Sidenote Component]
D --> S
N --> T[Initial HTML]
Q --> T
R --> T
S --> T
M --> T
T --> U[Postprocess Tabs]
G --> U
U -->|Render Each Tab| V[ContentRenderer]
V --> U
U --> W[Apply CSS Classes]
W --> X[Final HTML]
style J fill:#ffe6cc
style L fill:#d1ecf1
style O fill:#fff3cd
style U fill:#e7d4ff
3. Mermaid Diagram Lifecycle
---
width: 60vw
---
stateDiagram-v2
[*] --> Rendered: Page Load
state Rendered {
[*] --> Initialize
Initialize --> AddControls: Create buttons
AddControls --> StoreCode: Save original code
StoreCode --> EnableInteraction: Mouse events
}
state EnableInteraction {
[*] --> Idle
Idle --> Panning: Mouse drag
Idle --> Zooming: Mouse wheel
Idle --> ButtonZoom: +/- buttons
ButtonZoom --> Idle
Zooming --> Idle
Panning --> Idle
state "Reset Button" as Reset
Idle --> Reset: Click reset
Reset --> Idle: Restore defaults
}
Rendered --> ThemeChange: Dark/Light toggle
state ThemeChange {
[*] --> DetectTheme
DetectTheme --> GetOriginalCode: Read data attribute
GetOriginalCode --> ClearWrapper
ClearWrapper --> ReinitMermaid: New theme
ReinitMermaid --> ReRender: mermaid.run()
}
ThemeChange --> Rendered: Re-rendered
note right of ThemeChange
MutationObserver watches
HTML class changes
end note
note right of EnableInteraction
Transform state stored
per diagram ID
end note
Key Features
✨ Advanced Markdown Features
- Footnotes as Sidenotes:
[^1]references become elegant margin notes on desktop, expandable on mobile with smooth animations - YouTube Embeds: Use
[yt:VIDEO_ID]or[yt:VIDEO_ID|Caption]for responsive iframe cards with aspect-ratio containers - Task Lists:
- [ ]/- [x]render as custom styled checkboxes (green for checked, gray for unchecked) with SVG checkmarks - Mermaid Diagrams: Full support for flowcharts, sequence diagrams, state diagrams, Gantt charts, etc.
- Interactive Diagrams:
- Zoom with mouse wheel (zooms towards cursor position)
- Pan by dragging with mouse
- Built-in controls: fullscreen, reset, zoom in/out buttons
- Auto-scaling based on diagram aspect ratio (wide diagrams like Gantt charts get special handling)
- Fullscreen modal viewer with dark mode support
- Theme-aware Rendering: Diagrams automatically re-render when switching light/dark mode via MutationObserver
- Mermaid Frontmatter: Configure diagram size with YAML frontmatter (width, height, min-height)
- Tabbed Content: Create multi-tab sections using
:::tabsand::tab{title="..."}syntax with smooth transitions - Relative Links: Full support for relative markdown links (
./file.md,../other.md) with automatic path resolution - Math Notation: KaTeX support for inline
$E=mc^2$and block$$math equations, auto-renders after HTMX swaps - Superscript & Subscript: Use
^text^for superscript and~text~for subscript (preprocessed before rendering) - Strikethrough: Use
~~text~~for strikethrough formatting - Pandoc-style Attributes: Add classes to inline text with
`text`{.class #id}syntax for semantic markup (renders as<span>tags, not<code>) - Cascading Custom CSS: Add
custom.cssorstyle.cssfiles at multiple levels (root, folders) with automatic scoping
🎨 Modern UI
- Responsive Design: Works beautifully on all screen sizes with mobile-first approach
- Three-Panel Layout: Posts sidebar, main content, and table of contents for easy navigation
- Dark Mode: Automatic theme switching with localStorage persistence and instant visual feedback
- HTMX Navigation: Fast, SPA-like navigation without full page reloads using
hx-get,hx-target, andhx-push-url - Collapsible Folders: Organize posts in nested directories with chevron indicators and smooth expand/collapse
- Auto-Generated TOC: Table of contents automatically extracted from headings with scroll-based active highlighting
- Mobile Menus: Slide-in panels for posts and TOC on mobile devices with smooth transitions
- Sticky Navigation: Navbar stays at top while scrolling, with mobile menu toggles
- Active Link Highlighting: Current post and TOC section highlighted with blue accents
- Auto-Reveal in Sidebar: Active post automatically expanded and scrolled into view when opening sidebar
- Ultra-Thin Scrollbars: Custom styled 3px scrollbars that adapt to light/dark theme
- Frosted Glass Sidebars: Backdrop blur and transparency effects on sidebar components
🚀 Technical Highlights
- Built on FastHTML for modern Python web development with integrated HTMX
- Uses Mistletoe for extensible Markdown parsing with custom token renderers
- TailwindCSS + MonsterUI for utility-first styling and UI components
- Hyperscript for declarative interactive behaviors (theme toggle, sidenote interactions)
- Mermaid.js v11 for diagram rendering with custom zoom/pan/fullscreen controls via ES modules
- KaTeX for mathematical notation rendering with auto-render on content swaps
- Smart Link Resolution: Automatically converts relative links to proper routes with HTMX attributes
- Frontmatter Caching: LRU cache for parsed frontmatter based on file modification time
- Lazy Sidebar Loading: Posts sidebar loaded progressively via HTMX endpoint for faster initial load
- Performance Logging: Debug-level logging tracks render times and bottlenecks to
/tmp/bloggy_core.log - Custom 404 Page: Elegant error page with navigation options and helpful tips
- Static File Serving: Serves images and assets from blog directories via
/posts/{path}.{ext}routes - Optional Authentication: Session-based auth with Beforeware when username/password configured
Content Writing Features
Footnotes as Sidenotes
[^1] references compile into margin sidenotes on desktop (xl breakpoint: 1280px+) and touch-friendly expandables on smaller screens, powered by the sidenote.css stylesheet. On desktop, clicking a footnote reference highlights the corresponding sidenote with a temporary border animation. On mobile, clicking toggles the visibility with smooth fade-in/out. Continue to define footnotes with the standard [^label]: ... syntax—the renderer keeps the footnote text close to the reference and gracefully handles missing definitions with helpful placeholders. Sidenotes have amber/blue borders (light/dark) and appear in the right margin with proper spacing.
Task Lists & YouTube Embeds
Checklists begin with - [ ] (open) or - [x] (done) and render as custom styled items with inline flex layout. Checked items display a green background with SVG checkmark, while unchecked items show a gray background. No bullet points are shown (list-style: none), and the checkbox is aligned to the start of the text. Embeds are just as easy: [yt:VIDEO_ID] or [yt:VIDEO_ID|Caption] drops in a responsive YouTube iframe with aspect-video ratio (16:9), rounded corners, and border. Optional captions appear below in smaller gray text. No extra HTML wrappers required—just use the simple bracket syntax.
Tabbed Content
Group related snippets with the :::tabs container and ::tab{title="Label"} blocks. Each tab is rendered into a fully interactive panel using switchTab() JavaScript function. Tabs feature:
- Clean header with active state (bold border-bottom, darker text)
- Smooth fade-in animation when switching tabs (0.2s ease-in)
- Height stabilization: all panels measured, container set to max height to prevent layout shifts
- Absolute positioning for inactive panels (hidden but measured for height calculation)
- Full markdown rendering support within each tab panel
- Frosted glass aesthetic matching the sidebar design
The client script stabilizes the height on DOMContentLoaded and after HTMX swaps, ensuring smooth transitions without content jumps.
:::tabs
::tab{title="Rust"}
```rust
// tab content
```
::tab{title="Python"}
```python
# tab content
```
:::
Relative Links & Asset Helpers
Relative references like [Next chapter](../chapter-02.md) automatically resolve to /posts/..., strip the .md extension, and gain hx-get, hx-target="#main-content", hx-push-url="true", and hx-swap="innerHTML show:window:top" attributes for seamless HTMX navigation. The renderer uses the current post's path to resolve relative links correctly, handling both ./ and ../ navigation. External links (starting with http://, https://, mailto:, etc.) automatically get target="_blank" and rel="noopener noreferrer" for security.
Images and other assets can live under the blog tree and are served through /posts/{path}.{ext} via the serve_post_static route. The FrankenRenderer.render_image() method rewrites relative image URLs based on the current post path (using img_dir calculated from current_path), so folder-specific assets stay tidy and work correctly. Images are styled with max-w-full h-auto rounded-lg mb-6 classes for responsive display.
Inline Formatting & Math
Use ^sup^, ~sub~, ~~strike~~, and Pandoc-style `code`{.class #id lang=python} tokens for fine-grained styling. The preprocessing stage converts ^text^ to <sup>text</sup> and ~text~ to <sub>text</sub> before markdown parsing (but preserves ~~strikethrough~~). The InlineCodeAttr token (precedence 8) parses backtick code with curly-brace attributes, supporting:
- Classes:
.variable,.emphasis,.keyword - IDs:
#unique-id - Key-value pairs:
lang=python
These render as <span> tags (not <code>) when attributes are present, making them perfect for semantic styling without monospace fonts.
KaTeX handles $inline$ or $$block$$ math with renderMathInElement() configured for both display styles. The bundled script at bloggy/static/scripts.js runs the auto-renderer on both DOMContentLoaded and htmx:afterSwap events, so math keeps rendering correctly even when HTMX swaps fragments or the theme flips. The configuration uses throwOnError: false for graceful degradation.
Interactive Diagrams & Visualizations
Mermaid Frontmatter & Controls
Mermaid blocks can opt into frontmatter to tweak the container size with YAML-style key-value pairs:
---
width: 85vw
height: 60vh
---
graph LR
A --> B
B --> C
Supported frontmatter keys:
- width: Container width (default:
65vw). Can use viewport units (vw), pixels (px), or percentages - height: Container height (default:
auto) - min-height: Minimum container height (default:
400px)
These hints set the wrapper dimensions; viewport-based widths (vw) trigger a CSS breakout that centers the diagram using position: relative; left: 50%; transform: translateX(-50%).
Every diagram includes a control bar (top-right corner with frosted glass background):
- ⛶ Fullscreen: Opens diagram in modal overlay with dark backdrop
- Reset: Restores original scale and pan position
- + / −: Zoom in/out buttons (10% increments, clamped 0.1x to 15x)
The controls are wired to JavaScript functions in bloggy/static/scripts.js:
openMermaidFullscreen(id): Creates modal with close button and ESC key handlerresetMermaidZoom(id): Resets transform totranslate(0px, 0px) scale(1)zoomMermaidIn(id)/zoomMermaidOut(id): Adjusts scale by 1.1x / 0.9x factors
Scripted Interaction & Resilience
bloggy/static/scripts.js drives the interactive layer with sophisticated features:
Mermaid Zoom & Pan
- Mouse wheel zoom: Zooms toward cursor position with 1% intensity per wheel event (prevents jarring jumps)
- Mouse drag panning: Click and drag to pan, cursor changes to
grabbingduring drag - Smart initial scaling:
- Wide diagrams (aspect ratio > 3): Scale to fit width, allow vertical scroll
- Normal diagrams: Fit to smaller dimension (width or height), max 3x upscaling
- Accounts for padding: 32px total (16px each side from
p-4class)
- State management: Per-diagram state stored in
mermaidStatesobject with scale, translateX, translateY, isPanning, startX, startY - Transform origin: Center-center for natural zoom behavior
Theme-Aware Re-rendering
- MutationObserver watches
<html>class changes for dark mode toggle reinitializeMermaid()function:- Detects theme via
getCurrentTheme()(checks for.darkclass) - Preserves wrapper height before clearing to prevent layout shifts
- Deletes old state and re-creates fresh diagram
- Decodes HTML entities from
data-mermaid-codeattribute - Re-runs
mermaid.run()andinitMermaidInteraction()after 100ms delay - Skips reinit on initial load (uses
isInitialLoadflag)
- Detects theme via
HTMX Integration
htmx:afterSwapevent listener: Re-runs Mermaid, updates active links, reinitializes mobile menus- Sidebar auto-reveal: Expands parent
<details>elements and scrolls active post into view - Active link highlighting: Adds blue ring and background to current post in sidebar
- TOC scroll tracking: Updates active TOC link based on scroll position with
requestAnimationFramethrottling
Mobile Menu Handling
- Slide-in panels for posts and TOC with
transformtransitions (-translate-x-full/translate-x-full) - Auto-close panels when link clicked (100ms delay for smooth transition)
- Toggle buttons in navbar show/hide panels (only one open at a time)
Additional Features
- Fullscreen modal: Dark backdrop with blur, ESC key and background click to close
- Tab height stabilization: Measures all tab panels, sets container to max height on load and after swaps
- Math rendering: Auto-runs KaTeX after swaps and on initial load
Navigation & Layout Details
The layout() helper builds the complete page structure with intelligent HTMX optimization:
Layout Components
- Navbar: Sticky header with blog title, theme toggle, and mobile menu buttons (posts/TOC toggles)
- Three-panel layout:
- Left sidebar (72 width): Posts file tree with lazy HTMX loading
- Main content (flex-1): Swappable content area with section-specific CSS classes
- Right sidebar (72 width): Auto-generated TOC from headings
- Mobile panels: Fullscreen overlays for posts and TOC with smooth slide transitions
- Footer: "Powered by Bloggy" right-aligned in max-width container
HTMX Optimization
When htmx.request is detected, layout() returns only swappable fragments:
- Main content container with
id="main-content" - TOC sidebar with
hx_swap_oob="true"for out-of-band swap - CSS container with
hx_swap_oob="true"for scoped styles Titleelement for browser tab title- Skips navbar, posts sidebar, footer, mobile panels (already in DOM)
Sidebar Features
- Left sidebar (
build_post_tree):- Recursive folder tree with chevron indicators
- Folders: Blue folder icon, clickable summary, nested
<ul>with border - Files: Gray file-text icon, HTMX-enhanced links with
data-pathattribute - Cached via
@lru_cachebased on max modification time fingerprint - Lazy loaded via
/_sidebar/postsendpoint with loading spinner placeholder
- Right sidebar (
extract_toc):- Parses headings from markdown (excludes code blocks)
- Generates anchor slugs matching heading IDs
- Indentation based on heading level (
ml-{(level-1)*3}) - Active tracking based on scroll position
CSS Scoping
get_custom_css_links() discovers and loads custom CSS:
- Root CSS: Applies globally (
/posts/custom.cssor/posts/style.css) - Folder CSS: Automatically scoped to section via wrapper class (e.g.,
#main-content.section-demo-flat-land) - Content wrapped in
<Style>tag with scoped selector to prevent cross-section leakage - All CSS changes swapped via
#scoped-css-containerfor HTMX compatibility
Performance Logging
- Debug logs track timing for each phase (section class, TOC build, CSS resolution, container build)
- Logs written to
/tmp/bloggy_core.logwith rotation (10 MB, 10 days retention) - Request start/complete markers for easy debugging
Authentication (optional)
Set username and password in your .bloggy file or via BLOGGY_USER / BLOGGY_PASSWORD environment variables to enable session-based authentication. When enabled:
- Beforeware middleware: Intercepts all requests (except login page, static files, and CSS/JS)
- Login flow:
- Unauthenticated users redirected to
/loginwithnextURL saved in session - Login form uses MonsterUI styling (UK input classes, styled buttons)
- Successful login stores username in
request.session["auth"] - Redirects to original
nextURL after authentication
- Unauthenticated users redirected to
- Error handling: Invalid credentials show red error message below form
- Route exclusions: RegEx patterns skip auth for
^/login$,^/_sidebar/.*,^/static/.*,.*\.css,.*\.js
Authentication is completely optional—if no credentials configured, Beforeware is set to None and all routes are publicly accessible.
Project Structure
graph LR
subgraph bloggy/
A[__init__.py]
B[core.py<br/>Main App Logic]
C[main.py<br/>Entry Point]
subgraph static/
D[scripts.js<br/>Mermaid + Interactions]
E[sidenote.css<br/>Footnote Styles]
F[favicon.png]
end
end
subgraph demo/
G[*.md Files<br/>Your Blog Posts]
subgraph guides/
H[*.md Files<br/>Nested Content]
end
end
B --> D
B --> E
B --> F
B -.reads.-> G
B -.reads.-> H
style B fill:#ffe6cc
style D fill:#d1ecf1
style G fill:#d4edda
Installation
From PyPI (recommended)
pip install bloggy
From source
git clone https://github.com/yeshwanth/bloggy.git
cd bloggy
pip install -e .
Quick Start
-
Create a directory with your markdown files:
mkdir my-blog cd my-blog echo "# Hello World" > hello.md
-
Run Bloggy:
bloggy . -
Open your browser at
http://127.0.0.1:5001
Configuration
Bloggy supports three ways to configure your blog (in priority order):
.bloggyconfiguration file (TOML format) - Highest priority- Environment variables - Fallback
- Default values - Final fallback
Using a .bloggy Configuration File
Create a .bloggy file in your blog directory or current directory:
# Blog title (default: derives from root folder name via slug_to_title)
title = "My Awesome Blog"
# Root folder containing markdown files (default: current directory)
root = "."
# Server host (default: 127.0.0.1)
# Use "0.0.0.0" to make the server accessible from network
host = "127.0.0.1"
# Server port (default: 5001)
port = 5001
# Optional authentication credentials (enables Beforeware middleware)
username = "admin"
password = "hunter2"
All settings in the .bloggy file are optional. The configuration is managed by the Config class in bloggy/config.py.
Custom Sidebar Ordering
Place a .bloggy file in any folder to control the sidebar order for that folder. .bloggy uses TOML format. Use order to pin items first, then sort and folders_first for the remainder.
# Items listed in order are shown first. Use exact names (include extensions).
order = ["todo.md", "static-build.md", "docs"]
# Sorting for items not listed in order
sort = "name_asc" # name_asc, name_desc, mtime_asc, mtime_desc
folders_first = true
folders_always_first = false
Notes:
folders_firstonly affects the items not listed inorder.folders_always_firstmoves all folders to the top after ordering/sorting, while preserving their relative order.
Environment Variables
You can also use environment variables as a fallback:
BLOGGY_ROOT: Path to your markdown files (default: current directory)BLOGGY_TITLE: Your blog's title (default: folder name converted viaslug_to_title())BLOGGY_HOST: Server host (default: 127.0.0.1)BLOGGY_PORT: Server port (default: 5001)BLOGGY_USER: Optional username to enable session-based authenticationBLOGGY_PASSWORD: Optional password paired withBLOGGY_USER
Examples
Using a .bloggy file:
# Create a .bloggy file in your blog directory
title = "My Tech Blog"
port = 8000
host = "0.0.0.0"
Using environment variables:
export BLOGGY_ROOT=/path/to/your/markdown/files
export BLOGGY_TITLE="My Awesome Blog"
export BLOGGY_PORT=8000
export BLOGGY_HOST="0.0.0.0"
bloggy
Passing directory as argument:
bloggy /path/to/your/markdown/files
Enabling authentication:
# Via .bloggy file
title = "Private Blog"
username = "admin"
password = "secret123"
# Or via environment variables
export BLOGGY_USER="admin"
export BLOGGY_PASSWORD="secret123"
Configuration priority example:
If you have both a .bloggy file with port = 8000 and an environment variable BLOGGY_PORT=9000, the .bloggy file takes precedence and port 8000 will be used.
Custom Styling with Cascading CSS
Bloggy supports cascading custom CSS at multiple levels, allowing you to style your entire blog globally or customize specific sections:
CSS Loading Order
- Framework CSS (
bloggy/static/custom.css) - Core styling for Bloggy itself - Blog-wide CSS (
/your-blog-root/custom.css) - Applies to all posts - Folder-specific CSS (
/your-blog-root/section/custom.css) - Applies only to posts in that folder
Each level can override styles from previous levels, following standard CSS cascade rules.
Pandoc-style Inline Attributes
Use backticks with attributes to add semantic classes to inline text:
:::tabs
::tab{title="Markdown Example"}
The variables x{.abcd}, y{.abcd}, and z{.abcd} represent coordinates.
Use important{.emphasis} for highlighted terms.
::tab{title="CSS"}
span.abcd {
font-family: monospace;
background-color: #f0f0f0;
padding: 2px 4px;
border-radius: 4px;
}
:::
Attributes support:
- Classes:
.variable,.emphasis,.keyword - IDs:
#unique-id - Key-value pairs:
lang=python
Classes like .variable, .emphasis, and .keyword render as <span> tags (not <code>), making them perfect for semantic styling without monospace fonts.
Example: Multi-level Custom CSS
Root level (/blog/custom.css) - Global styles:
/* Base variable styling for all posts */
span.variable {
color: #e06c75;
font-weight: 500;
}
span.emphasis {
background: linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%);
padding: 2px 6px;
border-radius: 3px;
}
Section level (/blog/tutorials/custom.css) - Tutorial-specific (auto-scoped to #main-content.section-tutorials):
/* Override for tutorial section - use blue variables */
span.variable {
color: #61afef;
position: relative;
}
/* Add overline to variables in tutorials */
span.variable::before {
content: '';
position: absolute;
top: -3px;
left: 50%;
transform: translateX(-50%);
width: 80%;
height: 2px;
background-color: currentColor;
}
span.emphasis {
background: #ffd93d;
color: #333;
font-weight: bold;
}
Chapter level (/blog/tutorials/advanced/custom.css) - Advanced chapter styling (auto-scoped to #main-content.section-tutorials-advanced):
/* Different style for advanced tutorials */
span.variable {
color: #c678dd;
font-style: italic;
}
/* Add special marker for keywords */
span.keyword {
color: #e5c07b;
text-transform: uppercase;
font-size: 0.85em;
letter-spacing: 1px;
}
Note: Folder-specific CSS is automatically wrapped by Bloggy with the appropriate section scope, so you don't need to write the #main-content.section-* wrapper yourself.
Real Example from Demo
See the demo/flat-land/ folder for a working example:
Markdown (demo/flat-land/chapter-02.md):
The two Northern sides `RO`{.variable}, `OF`{.variable}, constitute the roof.
CSS (demo/flat-land/custom.css):
span.variable {
color: #e06c75;
position: relative;
}
span.variable::before {
content: '';
position: absolute;
top: -3px;
left: 50%;
transform: translateX(-50%);
width: 80%;
height: 2px;
background-color: currentColor;
}
This renders RO and OF in red with a line above them, perfect for mathematical or geometric notation!
Technical Implementation Details
Markdown Processing Pipeline
- Frontmatter extraction:
parse_frontmatter()with file mtime-based caching - Footnote extraction:
extract_footnotes()using regex to find[^label]:definitions - Superscript/subscript preprocessing:
preprocess_super_sub()converts^text^and~text~to HTML - Tab preprocessing:
preprocess_tabs()replaces:::tabsblocks with placeholders, stores tab data - Mistletoe parsing: Custom
ContentRendererwith registered tokens:YoutubeEmbed(precedence 6):[yt:VIDEO_ID|caption]syntaxFootnoteRef:[^label]referencesInlineCodeAttr(precedence 8):`code`{.class}syntaxSuperscript(precedence 7):^text^(if not preprocessed)Subscript(precedence 7):~text~(if not preprocessed)Strikethrough(precedence 7):~~text~~
- Token rendering: Each token has custom
render_*method inContentRenderer - Tab postprocessing:
postprocess_tabs()replaces placeholders with rendered tab HTML - CSS class application:
apply_classes()adds Tailwind classes to HTML elements
Custom Renderers
render_list_item: Detects[ ]/[x]patterns, renders custom checkboxesrender_youtube_embed: Creates responsive iframe with aspect-video containerrender_footnote_ref: Generates sidenote with hyperscript toggle behaviorrender_heading: Adds anchor ID usingtext_to_anchor()functionrender_block_code: Special handling formermaidlanguage, parses frontmatterrender_link: Resolves relative paths, adds HTMX attributes ortarget="_blank"render_inline_code_attr: Parses Pandoc attributes, renders as<span>with classesrender_image: Resolves relative image paths usingimg_dir
Caching Strategy
- Frontmatter cache:
_frontmatter_cachedict with(mtime, data)tuples - Posts tree cache:
@lru_cache(maxsize=1)on_cached_build_post_tree(fingerprint) - Sidebar HTML cache:
@lru_cache(maxsize=1)on_cached_posts_sidebar_html(fingerprint) - Fingerprint: Max mtime of all
.mdfiles viaroot.rglob("*.md") - Cache invalidation: Automatic when fingerprint changes (file modified)
HTMX Integration
- Main content swap:
hx-get="/posts/path" hx-target="#main-content" hx-swap="innerHTML show:window:top" - Out-of-band swaps: TOC sidebar and CSS container use
hx_swap_oob="true" - Push URL:
hx-push-url="true"updates browser history - Lazy loading: Posts sidebar uses
hx-get="/_sidebar/posts" hx-trigger="load" - Event handling: JavaScript listens to
htmx:afterSwapfor re-initialization
Logging & Debugging
- Loguru: Two handlers - stdout (INFO+) and file (DEBUG+)
- Log file:
/tmp/bloggy_core.logwith 10 MB rotation, 10 days retention - Performance tracking:
time.time()checkpoints throughout request handling - Debug groups:
console.group()in JavaScript for Mermaid operations - Request markers:
########## REQUEST START/COMPLETE ##########for easy grepping
Security Features
- HTML escaping: Code blocks automatically escaped via
html.escape() - External link protection:
rel="noopener noreferrer"on external links - Path validation: Relative path resolution checks if resolved path is within root
- Session-based auth: Uses Starlette sessions, not exposed in URLs
- CSRF protection: Forms use POST with
enctype="multipart/form-data"
Responsive Breakpoints
- Mobile: < 768px (md breakpoint) - Shows mobile menu buttons, hides sidebars
- Tablet: 768px - 1279px - Shows sidebars but no sidenote margins
- Desktop: 1280px+ (xl breakpoint) - Full three-panel layout with sidenotes in margin
Font Stack
- Body text: IBM Plex Sans (weights: 400, 500, 600, 700)
- Code: IBM Plex Mono (monospace)
- Fallback: System font stack via TailwindCSS defaults
- Loading: Preconnect to Google Fonts with
crossoriginfor speed
Advanced Features
Index/README Files
Place an index.md or README.md (case-insensitive) in your blog root directory to customize the home page. If neither exists, Bloggy shows a default welcome message. The index file:
- Takes precedence over README if both exist
- Renders with full sidebar and TOC support
- Uses the file's frontmatter
titleor blog title as page title - Supports all markdown features (tabs, diagrams, footnotes, etc.)
Smart 404 Page
When a route doesn't exist, Bloggy shows a custom 404 page with:
- Large "404" heading in gray
- Helpful error message explaining the situation
- Action buttons: "Go to Home" and "Go Back" with icons
- Tip section suggesting to check the sidebar
- Full sidebar included for easy navigation to correct page
Code Highlighting
Code blocks are styled with proper language classes (class="language-{lang}") for syntax highlighting. HTML/XML code is automatically escaped for display, while markdown code blocks preserve raw source. IBM Plex Mono font provides clear, readable monospace text.
Heading Anchors
All headings (h1 through h6) automatically get id attributes based on their text content using the text_to_anchor() function:
- Removes special characters
- Converts to lowercase
- Replaces spaces with hyphens
- Enables direct linking via
#anchor-slug - Powers TOC navigation with smooth scrolling (scroll margin 7rem for navbar offset)
Image Handling
The FrankenRenderer class provides smart image handling:
- Responsive styling:
max-w-full h-auto rounded-lg mb-6 - Relative path resolution: prepends
img_dirbased on post location - Protocol detection: skips prepending for absolute URLs (
http://,https://,attachment:,blob:,data:) - Title attribute support: renders
titleif present in markdown - Alt text: extracted from markdown image syntax
Frontmatter Support
All markdown files support YAML frontmatter for metadata:
title: Override default title (derived from filename)- Parsed with
python-frontmatterlibrary - Cached based on file modification time
- Missing frontmatter gracefully handled (empty dict + raw content)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file bloggy-0.1.52.tar.gz.
File metadata
- Download URL: bloggy-0.1.52.tar.gz
- Upload date:
- Size: 81.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c9b6527d81dad6666d459b271da47e55d185df397e94fa40ac215830e7f529dc
|
|
| MD5 |
d0c5bdf733e9ed3a129c7041c5bf54cf
|
|
| BLAKE2b-256 |
5b33c15cc5b53d0b1d785bd88dd61370476a1d30f52a1ec50f21125c9f7517e4
|
File details
Details for the file bloggy-0.1.52-py3-none-any.whl.
File metadata
- Download URL: bloggy-0.1.52-py3-none-any.whl
- Upload date:
- Size: 56.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0a5df37860f95e2d96cb614088a98b0bcbd35b81189be3a4debb751e978b87ea
|
|
| MD5 |
796839c560e3388706c88a5b265825dd
|
|
| BLAKE2b-256 |
ec98f3cc85868e5a5e111d018b384fbae3d03455eeb4648bba5cf5e9c38ab86b
|