bloggy 0.1.29__tar.gz → 0.1.57__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
bloggy-0.1.57/PKG-INFO ADDED
@@ -0,0 +1,943 @@
1
+ Metadata-Version: 2.4
2
+ Name: bloggy
3
+ Version: 0.1.57
4
+ Summary: A lightweight, elegant blogging platform built with FastHTML
5
+ Home-page: https://github.com/yeshwanth/bloggy
6
+ Author: Yeshwanth
7
+ Author-email:
8
+ License: Apache-2.0
9
+ Project-URL: Homepage, https://github.com/yeshwanth/bloggy
10
+ Project-URL: Repository, https://github.com/yeshwanth/bloggy
11
+ Project-URL: Issues, https://github.com/yeshwanth/bloggy/issues
12
+ Keywords: fasthtml,blog,markdown,htmx,mermaid,sidenotes
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Framework :: FastAPI
22
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: python-fasthtml>=0.6.9
28
+ Requires-Dist: mistletoe>=1.4.0
29
+ Requires-Dist: python-frontmatter>=1.1.0
30
+ Requires-Dist: uvicorn>=0.30.0
31
+ Requires-Dist: monsterui>=0.0.37
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
34
+ Requires-Dist: black>=24.0.0; extra == "dev"
35
+ Requires-Dist: ruff>=0.5.0; extra == "dev"
36
+ Dynamic: home-page
37
+ Dynamic: license-file
38
+ Dynamic: requires-python
39
+
40
+ ---
41
+ title: Bloggy - A FastHTML Blogging Platform
42
+ ---
43
+ # Bloggy
44
+
45
+ A lightweight, elegant blogging platform built with FastHTML that renders Markdown files into beautiful web pages with advanced features.
46
+
47
+ Simple Table
48
+
49
+ | Feature | Description |
50
+ |-----------------------------|--------------------------------------------------|
51
+ | FastHTML Integration | Built on FastHTML for high performance and ease of use |
52
+ | Advanced Markdown Support | Footnotes as sidenotes, YouTube embeds, task lists, Mermaid diagrams, math notation, tabbed content, and more |
53
+ | Modern UI | Responsive design, dark mode, three-panel layout, HTMX navigation |
54
+ | Interactive Diagrams | Zoomable, pannable Mermaid diagrams with fullscreen support |
55
+
56
+ ## Architecture Overview
57
+
58
+ ```mermaid
59
+ ---
60
+ width: 80vw
61
+ ---
62
+ graph TB
63
+ subgraph "User Interface"
64
+ Browser[Web Browser]
65
+ Theme[Light/Dark Theme Toggle]
66
+ end
67
+
68
+ subgraph "FastHTML Application"
69
+ App[FastHTML App<br/>core.py]
70
+ Router[URL Router]
71
+ Layout[Layout Engine]
72
+
73
+ subgraph "Route Handlers"
74
+ Index[Index Route<br/>/]
75
+ PostDetail[Post Detail Route<br/>/posts/path]
76
+ end
77
+ end
78
+
79
+ subgraph "Markdown Processing"
80
+ MDParser[Mistletoe Parser]
81
+ Renderer[ContentRenderer]
82
+
83
+ subgraph "Custom Renderers"
84
+ Footnotes[Footnote Renderer<br/>Sidenotes]
85
+ Mermaid[Mermaid Diagram Renderer<br/>Zoom/Pan Controls]
86
+ Links[Link Renderer<br/>HTMX Integration]
87
+ end
88
+ end
89
+
90
+ subgraph "File System"
91
+ MDFiles[Markdown Files<br/>.md]
92
+ Tree[Folder Tree Builder<br/>build_post_tree]
93
+ end
94
+
95
+ subgraph "Frontend Assets"
96
+ Static[Static Files]
97
+ JS[scripts.js<br/>Mermaid + Interactions]
98
+ CSS[Styles<br/>TailwindCSS + MonsterUI]
99
+ end
100
+
101
+ Browser -->|HTTP Request| Router
102
+ Theme -->|Toggle Dark Mode| JS
103
+
104
+ Router --> Index
105
+ Router --> PostDetail
106
+
107
+ Index --> Tree
108
+ Tree --> MDFiles
109
+ Index --> Layout
110
+
111
+ PostDetail --> MDFiles
112
+ PostDetail --> MDParser
113
+
114
+ MDParser --> Renderer
115
+ Renderer --> Footnotes
116
+ Renderer --> Mermaid
117
+ Renderer --> Links
118
+
119
+ Footnotes -->|Marginal Notes| Layout
120
+ Mermaid -->|Interactive Diagrams| Layout
121
+ Links -->|HTMX Navigation| Layout
122
+
123
+ Layout --> Browser
124
+
125
+ JS -->|Theme Change| Mermaid
126
+ JS -->|Zoom/Pan/Reset| Mermaid
127
+
128
+ Static --> CSS
129
+ Static --> JS
130
+
131
+ App -.->|Serves| Static
132
+
133
+ style Browser fill:#e1f5ff
134
+ style App fill:#fff3cd
135
+ style MDParser fill:#d4edda
136
+ style Static fill:#f8d7da
137
+ style Mermaid fill:#cce5ff
138
+ style Footnotes fill:#cce5ff
139
+ ```
140
+
141
+ ## How Bloggy Works
142
+
143
+ ### 1. Request Flow
144
+
145
+ ```mermaid
146
+ ---
147
+ width: 80vw
148
+ ---
149
+ sequenceDiagram
150
+ participant User
151
+ participant Browser
152
+ participant FastHTML
153
+ participant Router
154
+ participant FileSystem
155
+ participant Renderer
156
+ participant HTMX
157
+
158
+ User->>Browser: Visit /
159
+ Browser->>FastHTML: GET /
160
+ FastHTML->>Router: Route to index()
161
+ Router->>FileSystem: Scan for .md files
162
+ FileSystem-->>Router: Return file tree
163
+ Router->>Browser: Render post list + layout
164
+
165
+ User->>Browser: Click post link
166
+ Browser->>HTMX: hx-get="/posts/demo"
167
+ HTMX->>FastHTML: GET /posts/demo
168
+ FastHTML->>Router: Route to post_detail()
169
+ Router->>FileSystem: Read demo.md
170
+ FileSystem-->>Router: Markdown content
171
+ Router->>Renderer: Parse & render markdown
172
+
173
+ rect rgb(200, 220, 250)
174
+ Note over Renderer: Process custom syntax:<br/>- Footnotes [^1]<br/>- Mermaid diagrams<br/>- Internal links
175
+ end
176
+
177
+ Renderer-->>Router: Rendered HTML
178
+ Router->>HTMX: Return HTML fragment
179
+ HTMX->>Browser: Swap content (#main-content)
180
+ Browser->>User: Display post
181
+ ```
182
+
183
+ ### 2. Markdown Processing Pipeline
184
+
185
+ ```mermaid
186
+ ---
187
+ width: 80vw
188
+ ---
189
+ flowchart LR
190
+ A[Raw Markdown] --> B{Extract Footnotes}
191
+ B -->|Content| C[Preprocess Super/Sub]
192
+ B -->|Footnote Defs| D[Store in Dict]
193
+
194
+ C --> E[Preprocess Tabs]
195
+ E -->|Content + Placeholders| F[Mistletoe Parser]
196
+ E -->|Tab Data| G[Tab Store]
197
+
198
+ F --> H[Token Stream]
199
+
200
+ H --> I{ContentRenderer}
201
+
202
+ I -->|BlockCode + 'mermaid'| J[Mermaid Renderer]
203
+ I -->|Link| K[Link Renderer]
204
+ I -->|FootnoteRef| L[Footnote Renderer]
205
+ I -->|Other| M[Standard HTML]
206
+
207
+ J --> N[Diagram with Controls]
208
+ K --> O{Relative/Internal?}
209
+ O -->|Relative| P[Resolve Path]
210
+ P --> Q[Add HTMX attrs]
211
+ O -->|Internal| Q
212
+ O -->|External| R[Add target=_blank]
213
+
214
+ L --> S[Sidenote Component]
215
+ D --> S
216
+
217
+ N --> T[Initial HTML]
218
+ Q --> T
219
+ R --> T
220
+ S --> T
221
+ M --> T
222
+
223
+ T --> U[Postprocess Tabs]
224
+ G --> U
225
+ U -->|Render Each Tab| V[ContentRenderer]
226
+ V --> U
227
+
228
+ U --> W[Apply CSS Classes]
229
+ W --> X[Final HTML]
230
+
231
+ style J fill:#ffe6cc
232
+ style L fill:#d1ecf1
233
+ style O fill:#fff3cd
234
+ style U fill:#e7d4ff
235
+ ```
236
+
237
+ ### 3. Mermaid Diagram Lifecycle
238
+
239
+ ```mermaid
240
+ ---
241
+ width: 60vw
242
+ ---
243
+ stateDiagram-v2
244
+ [*] --> Rendered: Page Load
245
+
246
+ state Rendered {
247
+ [*] --> Initialize
248
+ Initialize --> AddControls: Create buttons
249
+ AddControls --> StoreCode: Save original code
250
+ StoreCode --> EnableInteraction: Mouse events
251
+ }
252
+
253
+ state EnableInteraction {
254
+ [*] --> Idle
255
+ Idle --> Panning: Mouse drag
256
+ Idle --> Zooming: Mouse wheel
257
+ Idle --> ButtonZoom: +/- buttons
258
+ ButtonZoom --> Idle
259
+ Zooming --> Idle
260
+ Panning --> Idle
261
+
262
+ state "Reset Button" as Reset
263
+ Idle --> Reset: Click reset
264
+ Reset --> Idle: Restore defaults
265
+ }
266
+
267
+ Rendered --> ThemeChange: Dark/Light toggle
268
+
269
+ state ThemeChange {
270
+ [*] --> DetectTheme
271
+ DetectTheme --> GetOriginalCode: Read data attribute
272
+ GetOriginalCode --> ClearWrapper
273
+ ClearWrapper --> ReinitMermaid: New theme
274
+ ReinitMermaid --> ReRender: mermaid.run()
275
+ }
276
+
277
+ ThemeChange --> Rendered: Re-rendered
278
+
279
+ note right of ThemeChange
280
+ MutationObserver watches
281
+ HTML class changes
282
+ end note
283
+
284
+ note right of EnableInteraction
285
+ Transform state stored
286
+ per diagram ID
287
+ end note
288
+ ```
289
+
290
+ ## Key Features
291
+
292
+ ### ✨ Advanced Markdown Features
293
+ - **Footnotes as Sidenotes**: `[^1]` references become elegant margin notes on desktop, expandable on mobile with smooth animations
294
+ - **YouTube Embeds**: Use `[yt:VIDEO_ID]` or `[yt:VIDEO_ID|Caption]` for responsive iframe cards with aspect-ratio containers
295
+ - **Task Lists**: `- [ ]` / `- [x]` render as custom styled checkboxes (green for checked, gray for unchecked) with SVG checkmarks
296
+ - **Mermaid Diagrams**: Full support for flowcharts, sequence diagrams, state diagrams, Gantt charts, etc.
297
+ - **Interactive Diagrams**:
298
+ - Zoom with mouse wheel (zooms towards cursor position)
299
+ - Pan by dragging with mouse
300
+ - Built-in controls: fullscreen, reset, zoom in/out buttons
301
+ - Auto-scaling based on diagram aspect ratio (wide diagrams like Gantt charts get special handling)
302
+ - Fullscreen modal viewer with dark mode support
303
+ - **Theme-aware Rendering**: Diagrams automatically re-render when switching light/dark mode via MutationObserver
304
+ - **Mermaid Frontmatter**: Configure diagram size with YAML frontmatter (width, height, min-height)
305
+ - **Tabbed Content**: Create multi-tab sections using `:::tabs` and `::tab{title="..."}` syntax with smooth transitions
306
+ - **Relative Links**: Full support for relative markdown links (`./file.md`, `../other.md`) with automatic path resolution
307
+ - **Plain-Text Headings**: Inline markdown in headings is stripped for clean display and consistent anchor slugs
308
+ - **Math Notation**: KaTeX support for inline `$E=mc^2$` and block `$$` math equations, auto-renders after HTMX swaps
309
+ - **Superscript & Subscript**: Use `^text^` for superscript and `~text~` for subscript (preprocessed before rendering)
310
+ - **Strikethrough**: Use `~~text~~` for strikethrough formatting
311
+ - **Pandoc-style Attributes**: Add classes to inline text with `` `text`{.class #id} `` syntax for semantic markup (renders as `<span>` tags, not `<code>`)
312
+ - **Cascading Custom CSS**: Add `custom.css` or `style.css` files at multiple levels (root, folders) with automatic scoping
313
+
314
+ ### 🎨 Modern UI
315
+ - **Responsive Design**: Works beautifully on all screen sizes with mobile-first approach
316
+ - **Three-Panel Layout**: Posts sidebar, main content, and table of contents for easy navigation
317
+ - **Dark Mode**: Automatic theme switching with localStorage persistence and instant visual feedback
318
+ - **HTMX Navigation**: Fast, SPA-like navigation without full page reloads using `hx-get`, `hx-target`, and `hx-push-url`
319
+ - **Collapsible Folders**: Organize posts in nested directories with chevron indicators and smooth expand/collapse
320
+ - **Sidebar Search**: HTMX-powered filename search with results shown below the search bar (tree stays intact)
321
+ - **Auto-Generated TOC**: Table of contents automatically extracted from headings with scroll-based active highlighting
322
+ - **TOC Autoscroll + Accurate Highlights**: Active TOC item stays in view and highlight logic handles duplicate headings
323
+ - **Mobile Menus**: Slide-in panels for posts and TOC on mobile devices with smooth transitions
324
+ - **Sticky Navigation**: Navbar stays at top while scrolling, with mobile menu toggles
325
+ - **Active Link Highlighting**: Current post and TOC section highlighted with blue accents
326
+ - **Auto-Reveal in Sidebar**: Active post automatically expanded and scrolled into view when opening sidebar
327
+ - **Ultra-Thin Scrollbars**: Custom styled 3px scrollbars that adapt to light/dark theme
328
+ - **Frosted Glass Sidebars**: Backdrop blur and transparency effects on sidebar components
329
+
330
+ ### 🚀 Technical Highlights
331
+ - Built on **FastHTML** for modern Python web development with integrated HTMX
332
+ - Uses **Mistletoe** for extensible Markdown parsing with custom token renderers
333
+ - **TailwindCSS** + **MonsterUI** for utility-first styling and UI components
334
+ - **Hyperscript** for declarative interactive behaviors (theme toggle, sidenote interactions)
335
+ - **Mermaid.js v11** for diagram rendering with custom zoom/pan/fullscreen controls via ES modules
336
+ - **KaTeX** for mathematical notation rendering with auto-render on content swaps
337
+ - **Smart Link Resolution**: Automatically converts relative links to proper routes with HTMX attributes
338
+ - **Frontmatter Caching**: LRU cache for parsed frontmatter based on file modification time
339
+ - **Lazy Sidebar Loading**: Posts sidebar loaded progressively via HTMX endpoint for faster initial load
340
+ - **Performance Logging**: Debug-level logging tracks render times and bottlenecks to `/tmp/bloggy_core.log`
341
+ - **Custom 404 Page**: Elegant error page with navigation options and helpful tips
342
+ - **Static File Serving**: Serves images and assets from blog directories via `/posts/{path}.{ext}` routes
343
+ - **Raw Markdown Access**: Append `.md` to any post URL (e.g. `/posts/demo.md`) to fetch source content
344
+ - **Optional Authentication**: Session-based auth with Beforeware when username/password configured
345
+
346
+ ### Quick Usage Examples
347
+ - Sidebar search: type a filename fragment like `write up` or `write-up` to filter results without collapsing the tree
348
+ - Raw markdown: fetch a post's source via `/posts/demo.md`
349
+
350
+ ## Content Writing Features
351
+
352
+ ### Footnotes as Sidenotes
353
+ `[^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.
354
+
355
+ ### Task Lists & YouTube Embeds
356
+ 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.
357
+
358
+ ### Tabbed Content
359
+ 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:
360
+ - Clean header with active state (bold border-bottom, darker text)
361
+ - Smooth fade-in animation when switching tabs (0.2s ease-in)
362
+ - Height stabilization: all panels measured, container set to max height to prevent layout shifts
363
+ - Absolute positioning for inactive panels (hidden but measured for height calculation)
364
+ - Full markdown rendering support within each tab panel
365
+ - Frosted glass aesthetic matching the sidebar design
366
+
367
+ The client script stabilizes the height on DOMContentLoaded and after HTMX swaps, ensuring smooth transitions without content jumps.
368
+
369
+ ````markdown
370
+ :::tabs
371
+ ::tab{title="Rust"}
372
+ ```rust
373
+ // tab content
374
+ ```
375
+ ::tab{title="Python"}
376
+ ```python
377
+ # tab content
378
+ ```
379
+ :::
380
+ ````
381
+
382
+ ### Relative Links & Asset Helpers
383
+ 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.
384
+
385
+ 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.
386
+
387
+ ### Inline Formatting & Math
388
+ 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:
389
+ - Classes: `.variable`, `.emphasis`, `.keyword`
390
+ - IDs: `#unique-id`
391
+ - Key-value pairs: `lang=python`
392
+
393
+ These render as `<span>` tags (not `<code>`) when attributes are present, making them perfect for semantic styling without monospace fonts.
394
+
395
+ 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.
396
+
397
+ ## Interactive Diagrams & Visualizations
398
+
399
+ ### Mermaid Frontmatter & Controls
400
+ Mermaid blocks can opt into frontmatter to tweak the container size with YAML-style key-value pairs:
401
+
402
+ ```mermaid
403
+ ---
404
+ width: 85vw
405
+ height: 60vh
406
+ ---
407
+ graph LR
408
+ A --> B
409
+ B --> C
410
+ ```
411
+
412
+ Supported frontmatter keys:
413
+ - **width**: Container width (default: `65vw`). Can use viewport units (`vw`), pixels (`px`), or percentages
414
+ - **height**: Container height (default: `auto`)
415
+ - **min-height**: Minimum container height (default: `400px`)
416
+
417
+ 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%)`.
418
+
419
+ Every diagram includes a control bar (top-right corner with frosted glass background):
420
+ - **⛶ Fullscreen**: Opens diagram in modal overlay with dark backdrop
421
+ - **Reset**: Restores original scale and pan position
422
+ - **+ / −**: Zoom in/out buttons (10% increments, clamped 0.1x to 15x)
423
+
424
+ The controls are wired to JavaScript functions in `bloggy/static/scripts.js`:
425
+ - `openMermaidFullscreen(id)`: Creates modal with close button and ESC key handler
426
+ - `resetMermaidZoom(id)`: Resets transform to `translate(0px, 0px) scale(1)`
427
+ - `zoomMermaidIn(id)` / `zoomMermaidOut(id)`: Adjusts scale by 1.1x / 0.9x factors
428
+
429
+ ### Scripted Interaction & Resilience
430
+ `bloggy/static/scripts.js` drives the interactive layer with sophisticated features:
431
+
432
+ #### Mermaid Zoom & Pan
433
+ - **Mouse wheel zoom**: Zooms toward cursor position with 1% intensity per wheel event (prevents jarring jumps)
434
+ - **Mouse drag panning**: Click and drag to pan, cursor changes to `grabbing` during drag
435
+ - **Smart initial scaling**:
436
+ - Wide diagrams (aspect ratio > 3): Scale to fit width, allow vertical scroll
437
+ - Normal diagrams: Fit to smaller dimension (width or height), max 3x upscaling
438
+ - Accounts for padding: 32px total (16px each side from `p-4` class)
439
+ - **State management**: Per-diagram state stored in `mermaidStates` object with scale, translateX, translateY, isPanning, startX, startY
440
+ - **Transform origin**: Center-center for natural zoom behavior
441
+
442
+ #### Theme-Aware Re-rendering
443
+ - **MutationObserver** watches `<html>` class changes for dark mode toggle
444
+ - `reinitializeMermaid()` function:
445
+ - Detects theme via `getCurrentTheme()` (checks for `.dark` class)
446
+ - Preserves wrapper height before clearing to prevent layout shifts
447
+ - Deletes old state and re-creates fresh diagram
448
+ - Decodes HTML entities from `data-mermaid-code` attribute
449
+ - Re-runs `mermaid.run()` and `initMermaidInteraction()` after 100ms delay
450
+ - Skips reinit on initial load (uses `isInitialLoad` flag)
451
+
452
+ #### HTMX Integration
453
+ - **`htmx:afterSwap` event listener**: Re-runs Mermaid, updates active links, reinitializes mobile menus
454
+ - **Sidebar auto-reveal**: Expands parent `<details>` elements and scrolls active post into view
455
+ - **Active link highlighting**: Adds blue ring and background to current post in sidebar
456
+ - **TOC scroll tracking**: Updates active TOC link based on scroll position with `requestAnimationFrame` throttling
457
+
458
+ #### Mobile Menu Handling
459
+ - Slide-in panels for posts and TOC with `transform` transitions (`-translate-x-full` / `translate-x-full`)
460
+ - Auto-close panels when link clicked (100ms delay for smooth transition)
461
+ - Toggle buttons in navbar show/hide panels (only one open at a time)
462
+
463
+ #### Additional Features
464
+ - **Fullscreen modal**: Dark backdrop with blur, ESC key and background click to close
465
+ - **Tab height stabilization**: Measures all tab panels, sets container to max height on load and after swaps
466
+ - **Math rendering**: Auto-runs KaTeX after swaps and on initial load
467
+
468
+ ## Navigation & Layout Details
469
+
470
+ The `layout()` helper builds the complete page structure with intelligent HTMX optimization:
471
+
472
+ ### Layout Components
473
+ - **Navbar**: Sticky header with blog title, theme toggle, and mobile menu buttons (posts/TOC toggles)
474
+ - **Three-panel layout**:
475
+ - Left sidebar (72 width): Posts file tree with lazy HTMX loading
476
+ - Main content (flex-1): Swappable content area with section-specific CSS classes
477
+ - Right sidebar (72 width): Auto-generated TOC from headings
478
+ - **Mobile panels**: Fullscreen overlays for posts and TOC with smooth slide transitions
479
+ - **Footer**: "Powered by Bloggy" right-aligned in max-width container
480
+
481
+ ### HTMX Optimization
482
+ When `htmx.request` is detected, `layout()` returns only swappable fragments:
483
+ - Main content container with `id="main-content"`
484
+ - TOC sidebar with `hx_swap_oob="true"` for out-of-band swap
485
+ - CSS container with `hx_swap_oob="true"` for scoped styles
486
+ - `Title` element for browser tab title
487
+ - Skips navbar, posts sidebar, footer, mobile panels (already in DOM)
488
+
489
+ ### Sidebar Features
490
+ - **Left sidebar** (`build_post_tree`):
491
+ - Recursive folder tree with chevron indicators
492
+ - Folders: Blue folder icon, clickable summary, nested `<ul>` with border
493
+ - Files: Gray file-text icon, HTMX-enhanced links with `data-path` attribute
494
+ - Cached via `@lru_cache` based on max modification time fingerprint
495
+ - Lazy loaded via `/_sidebar/posts` endpoint with loading spinner placeholder
496
+ - **Right sidebar** (`extract_toc`):
497
+ - Parses headings from markdown (excludes code blocks)
498
+ - Generates anchor slugs matching heading IDs
499
+ - Indentation based on heading level (`ml-{(level-1)*3}`)
500
+ - Active tracking based on scroll position
501
+
502
+ ### CSS Scoping
503
+ `get_custom_css_links()` discovers and loads custom CSS:
504
+ 1. **Root CSS**: Applies globally (`/posts/custom.css` or `/posts/style.css`)
505
+ 2. **Folder CSS**: Automatically scoped to section via wrapper class (e.g., `#main-content.section-demo-flat-land`)
506
+ 3. Content wrapped in `<Style>` tag with scoped selector to prevent cross-section leakage
507
+ 4. All CSS changes swapped via `#scoped-css-container` for HTMX compatibility
508
+
509
+ ### Performance Logging
510
+ - Debug logs track timing for each phase (section class, TOC build, CSS resolution, container build)
511
+ - Logs written to `/tmp/bloggy_core.log` with rotation (10 MB, 10 days retention)
512
+ - Request start/complete markers for easy debugging
513
+
514
+ ## Authentication (optional)
515
+
516
+ Set `username` and `password` in your `.bloggy` file or via `BLOGGY_USER` / `BLOGGY_PASSWORD` environment variables to enable session-based authentication. When enabled:
517
+
518
+ - **Beforeware middleware**: Intercepts all requests (except login page, static files, and CSS/JS)
519
+ - **Login flow**:
520
+ - Unauthenticated users redirected to `/login` with `next` URL saved in session
521
+ - Login form uses MonsterUI styling (UK input classes, styled buttons)
522
+ - Successful login stores username in `request.session["auth"]`
523
+ - Redirects to original `next` URL after authentication
524
+ - **Error handling**: Invalid credentials show red error message below form
525
+ - **Route exclusions**: RegEx patterns skip auth for `^/login$`, `^/_sidebar/.*`, `^/static/.*`, `.*\.css`, `.*\.js`
526
+
527
+ Authentication is completely optional—if no credentials configured, Beforeware is set to `None` and all routes are publicly accessible.
528
+
529
+ ## Project Structure
530
+
531
+ ```mermaid
532
+ graph LR
533
+ subgraph bloggy/
534
+ A[__init__.py]
535
+ B[core.py<br/>Main App Logic]
536
+ C[main.py<br/>Entry Point]
537
+
538
+ subgraph static/
539
+ D[scripts.js<br/>Mermaid + Interactions]
540
+ E[sidenote.css<br/>Footnote Styles]
541
+ F[favicon.png]
542
+ end
543
+ end
544
+
545
+ subgraph demo/
546
+ G[*.md Files<br/>Your Blog Posts]
547
+
548
+ subgraph guides/
549
+ H[*.md Files<br/>Nested Content]
550
+ end
551
+ end
552
+
553
+ B --> D
554
+ B --> E
555
+ B --> F
556
+ B -.reads.-> G
557
+ B -.reads.-> H
558
+
559
+ style B fill:#ffe6cc
560
+ style D fill:#d1ecf1
561
+ style G fill:#d4edda
562
+ ```
563
+
564
+ ## Installation
565
+
566
+ ### From PyPI (recommended)
567
+
568
+ ```bash
569
+ pip install bloggy
570
+ ```
571
+
572
+ ### From source
573
+
574
+ ```bash
575
+ git clone https://github.com/yeshwanth/bloggy.git
576
+ cd bloggy
577
+ pip install -e .
578
+ ```
579
+
580
+ ## Quick Start
581
+
582
+ 1. Create a directory with your markdown files:
583
+ ```bash
584
+ mkdir my-blog
585
+ cd my-blog
586
+ echo "# Hello World" > hello.md
587
+ ```
588
+
589
+ 2. Run Bloggy:
590
+ ```bash
591
+ bloggy .
592
+ ```
593
+
594
+ 3. Open your browser at `http://127.0.0.1:5001`
595
+
596
+ ## Configuration
597
+
598
+ Bloggy supports three ways to configure your blog (in priority order):
599
+
600
+ 1. **`.bloggy` configuration file** (TOML format) - Highest priority
601
+ 2. **Environment variables** - Fallback
602
+ 3. **Default values** - Final fallback
603
+
604
+ ### Using a `.bloggy` Configuration File
605
+
606
+ Create a `.bloggy` file in your blog directory or current directory:
607
+
608
+ ```toml
609
+ # Blog title (default: derives from root folder name via slug_to_title)
610
+ title = "My Awesome Blog"
611
+
612
+ # Root folder containing markdown files (default: current directory)
613
+ root = "."
614
+
615
+ # Server host (default: 127.0.0.1)
616
+ # Use "0.0.0.0" to make the server accessible from network
617
+ host = "127.0.0.1"
618
+
619
+ # Server port (default: 5001)
620
+ port = 5001
621
+
622
+ # Optional authentication credentials (enables Beforeware middleware)
623
+ username = "admin"
624
+ password = "hunter2"
625
+ ```
626
+
627
+ All settings in the `.bloggy` file are optional. The configuration is managed by the `Config` class in `bloggy/config.py`.
628
+
629
+ ### Custom Sidebar Ordering
630
+
631
+ 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.
632
+
633
+ ```toml
634
+ # Items listed in order are shown first. Use exact names (include extensions).
635
+ order = ["todo.md", "static-build.md", "docs"]
636
+
637
+ # Sorting for items not listed in order
638
+ sort = "name_asc" # name_asc, name_desc, mtime_asc, mtime_desc
639
+ folders_first = true
640
+ folders_always_first = false
641
+ ```
642
+
643
+ Notes:
644
+ - `folders_first` only affects the items not listed in `order`.
645
+ - `folders_always_first` moves all folders to the top after ordering/sorting, while preserving their relative order.
646
+
647
+ ### Environment Variables
648
+
649
+ You can also use environment variables as a fallback:
650
+
651
+ - `BLOGGY_ROOT`: Path to your markdown files (default: current directory)
652
+ - `BLOGGY_TITLE`: Your blog's title (default: folder name converted via `slug_to_title()`)
653
+ - `BLOGGY_HOST`: Server host (default: 127.0.0.1)
654
+ - `BLOGGY_PORT`: Server port (default: 5001)
655
+ - `BLOGGY_USER`: Optional username to enable session-based authentication
656
+ - `BLOGGY_PASSWORD`: Optional password paired with `BLOGGY_USER`
657
+
658
+ ### Examples
659
+
660
+ **Using a `.bloggy` file:**
661
+
662
+ ```bash
663
+ # Create a .bloggy file in your blog directory
664
+ title = "My Tech Blog"
665
+ port = 8000
666
+ host = "0.0.0.0"
667
+ ```
668
+
669
+ **Using environment variables:**
670
+
671
+ ```bash
672
+ export BLOGGY_ROOT=/path/to/your/markdown/files
673
+ export BLOGGY_TITLE="My Awesome Blog"
674
+ export BLOGGY_PORT=8000
675
+ export BLOGGY_HOST="0.0.0.0"
676
+ bloggy
677
+ ```
678
+
679
+ **Passing directory as argument:**
680
+
681
+ ```bash
682
+ bloggy /path/to/your/markdown/files
683
+ ```
684
+
685
+ **Enabling authentication:**
686
+
687
+ ```.env
688
+ # Via .bloggy file
689
+ title = "Private Blog"
690
+ username = "admin"
691
+ password = "secret123"
692
+ ```
693
+
694
+ ```bash
695
+ # Or via environment variables
696
+ export BLOGGY_USER="admin"
697
+ export BLOGGY_PASSWORD="secret123"
698
+ ```
699
+
700
+ **Configuration priority example:**
701
+
702
+ 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.
703
+
704
+ ## Custom Styling with Cascading CSS
705
+
706
+ Bloggy supports **cascading custom CSS** at multiple levels, allowing you to style your entire blog globally or customize specific sections:
707
+
708
+ ### CSS Loading Order
709
+
710
+ 1. **Framework CSS** (`bloggy/static/custom.css`) - Core styling for Bloggy itself
711
+ 2. **Blog-wide CSS** (`/your-blog-root/custom.css`) - Applies to all posts
712
+ 3. **Folder-specific CSS** (`/your-blog-root/section/custom.css`) - Applies only to posts in that folder
713
+
714
+ Each level can override styles from previous levels, following standard CSS cascade rules.
715
+
716
+ ### Pandoc-style Inline Attributes
717
+
718
+ Use backticks with attributes to add semantic classes to inline text:
719
+
720
+ :::tabs
721
+ ::tab{title="Markdown Example"}
722
+ The variables `x`{.abcd}, `y`{.abcd}, and `z`{.abcd} represent coordinates.
723
+ Use `important`{.emphasis} for highlighted terms.
724
+ ::tab{title="CSS"}
725
+ ```css
726
+ span.abcd {
727
+ font-family: monospace;
728
+ background-color: #f0f0f0;
729
+ padding: 2px 4px;
730
+ border-radius: 4px;
731
+ }
732
+ ```
733
+ :::
734
+
735
+ Attributes support:
736
+ - **Classes**: `.variable`, `.emphasis`, `.keyword`
737
+ - **IDs**: `#unique-id`
738
+ - **Key-value pairs**: `lang=python`
739
+
740
+ Classes like `.variable`, `.emphasis`, and `.keyword` render as `<span>` tags (not `<code>`), making them perfect for semantic styling without monospace fonts.
741
+
742
+ ### Example: Multi-level Custom CSS
743
+
744
+ **Root level** (`/blog/custom.css`) - Global styles:
745
+ ```css
746
+ /* Base variable styling for all posts */
747
+ span.variable {
748
+ color: #e06c75;
749
+ font-weight: 500;
750
+ }
751
+
752
+ span.emphasis {
753
+ background: linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%);
754
+ padding: 2px 6px;
755
+ border-radius: 3px;
756
+ }
757
+ ```
758
+
759
+ **Section level** (`/blog/tutorials/custom.css`) - Tutorial-specific (auto-scoped to `#main-content.section-tutorials`):
760
+ ```css
761
+ /* Override for tutorial section - use blue variables */
762
+ span.variable {
763
+ color: #61afef;
764
+ position: relative;
765
+ }
766
+
767
+ /* Add overline to variables in tutorials */
768
+ span.variable::before {
769
+ content: '';
770
+ position: absolute;
771
+ top: -3px;
772
+ left: 50%;
773
+ transform: translateX(-50%);
774
+ width: 80%;
775
+ height: 2px;
776
+ background-color: currentColor;
777
+ }
778
+
779
+ span.emphasis {
780
+ background: #ffd93d;
781
+ color: #333;
782
+ font-weight: bold;
783
+ }
784
+ ```
785
+
786
+ **Chapter level** (`/blog/tutorials/advanced/custom.css`) - Advanced chapter styling (auto-scoped to `#main-content.section-tutorials-advanced`):
787
+ ```css
788
+ /* Different style for advanced tutorials */
789
+ span.variable {
790
+ color: #c678dd;
791
+ font-style: italic;
792
+ }
793
+
794
+ /* Add special marker for keywords */
795
+ span.keyword {
796
+ color: #e5c07b;
797
+ text-transform: uppercase;
798
+ font-size: 0.85em;
799
+ letter-spacing: 1px;
800
+ }
801
+ ```
802
+
803
+ 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.
804
+
805
+ ### Real Example from Demo
806
+
807
+ See the `demo/flat-land/` folder for a working example:
808
+
809
+ **Markdown** (`demo/flat-land/chapter-02.md`):
810
+ ```markdown
811
+ The two Northern sides `RO`{.variable}, `OF`{.variable}, constitute the roof.
812
+ ```
813
+
814
+ **CSS** (`demo/flat-land/custom.css`):
815
+ ```css
816
+ span.variable {
817
+ color: #e06c75;
818
+ position: relative;
819
+ }
820
+
821
+ span.variable::before {
822
+ content: '';
823
+ position: absolute;
824
+ top: -3px;
825
+ left: 50%;
826
+ transform: translateX(-50%);
827
+ width: 80%;
828
+ height: 2px;
829
+ background-color: currentColor;
830
+ }
831
+ ```
832
+
833
+ This renders `RO` and `OF` in red with a line above them, perfect for mathematical or geometric notation!
834
+
835
+ ## Technical Implementation Details
836
+
837
+ ### Markdown Processing Pipeline
838
+ 1. **Frontmatter extraction**: `parse_frontmatter()` with file mtime-based caching
839
+ 2. **Footnote extraction**: `extract_footnotes()` using regex to find `[^label]:` definitions
840
+ 3. **Superscript/subscript preprocessing**: `preprocess_super_sub()` converts `^text^` and `~text~` to HTML
841
+ 4. **Tab preprocessing**: `preprocess_tabs()` replaces `:::tabs` blocks with placeholders, stores tab data
842
+ 5. **Mistletoe parsing**: Custom `ContentRenderer` with registered tokens:
843
+ - `YoutubeEmbed` (precedence 6): `[yt:VIDEO_ID|caption]` syntax
844
+ - `FootnoteRef`: `[^label]` references
845
+ - `InlineCodeAttr` (precedence 8): `` `code`{.class} `` syntax
846
+ - `Superscript` (precedence 7): `^text^` (if not preprocessed)
847
+ - `Subscript` (precedence 7): `~text~` (if not preprocessed)
848
+ - `Strikethrough` (precedence 7): `~~text~~`
849
+ 6. **Token rendering**: Each token has custom `render_*` method in `ContentRenderer`
850
+ 7. **Tab postprocessing**: `postprocess_tabs()` replaces placeholders with rendered tab HTML
851
+ 8. **CSS class application**: `apply_classes()` adds Tailwind classes to HTML elements
852
+
853
+ ### Custom Renderers
854
+ - **`render_list_item`**: Detects `[ ]` / `[x]` patterns, renders custom checkboxes
855
+ - **`render_youtube_embed`**: Creates responsive iframe with aspect-video container
856
+ - **`render_footnote_ref`**: Generates sidenote with hyperscript toggle behavior
857
+ - **`render_heading`**: Adds anchor ID using `text_to_anchor()` function
858
+ - **`render_block_code`**: Special handling for `mermaid` language, parses frontmatter
859
+ - **`render_link`**: Resolves relative paths, adds HTMX attributes or `target="_blank"`
860
+ - **`render_inline_code_attr`**: Parses Pandoc attributes, renders as `<span>` with classes
861
+ - **`render_image`**: Resolves relative image paths using `img_dir`
862
+
863
+ ### Caching Strategy
864
+ - **Frontmatter cache**: `_frontmatter_cache` dict with `(mtime, data)` tuples
865
+ - **Posts tree cache**: `@lru_cache(maxsize=1)` on `_cached_build_post_tree(fingerprint)`
866
+ - **Sidebar HTML cache**: `@lru_cache(maxsize=1)` on `_cached_posts_sidebar_html(fingerprint)`
867
+ - **Fingerprint**: Max mtime of all `.md` files via `root.rglob("*.md")`
868
+ - Cache invalidation: Automatic when fingerprint changes (file modified)
869
+
870
+ ### HTMX Integration
871
+ - **Main content swap**: `hx-get="/posts/path" hx-target="#main-content" hx-swap="innerHTML show:window:top"`
872
+ - **Out-of-band swaps**: TOC sidebar and CSS container use `hx_swap_oob="true"`
873
+ - **Push URL**: `hx-push-url="true"` updates browser history
874
+ - **Lazy loading**: Posts sidebar uses `hx-get="/_sidebar/posts" hx-trigger="load"`
875
+ - **Event handling**: JavaScript listens to `htmx:afterSwap` for re-initialization
876
+
877
+ ### Logging & Debugging
878
+ - **Loguru**: Two handlers - stdout (INFO+) and file (DEBUG+)
879
+ - **Log file**: `/tmp/bloggy_core.log` with 10 MB rotation, 10 days retention
880
+ - **Performance tracking**: `time.time()` checkpoints throughout request handling
881
+ - **Debug groups**: `console.group()` in JavaScript for Mermaid operations
882
+ - **Request markers**: `########## REQUEST START/COMPLETE ##########` for easy grepping
883
+
884
+ ### Security Features
885
+ - **HTML escaping**: Code blocks automatically escaped via `html.escape()`
886
+ - **External link protection**: `rel="noopener noreferrer"` on external links
887
+ - **Path validation**: Relative path resolution checks if resolved path is within root
888
+ - **Session-based auth**: Uses Starlette sessions, not exposed in URLs
889
+ - **CSRF protection**: Forms use POST with `enctype="multipart/form-data"`
890
+
891
+ ### Responsive Breakpoints
892
+ - **Mobile**: < 768px (md breakpoint) - Shows mobile menu buttons, hides sidebars
893
+ - **Tablet**: 768px - 1279px - Shows sidebars but no sidenote margins
894
+ - **Desktop**: 1280px+ (xl breakpoint) - Full three-panel layout with sidenotes in margin
895
+
896
+ ### Font Stack
897
+ - **Body text**: IBM Plex Sans (weights: 400, 500, 600, 700)
898
+ - **Code**: IBM Plex Mono (monospace)
899
+ - **Fallback**: System font stack via TailwindCSS defaults
900
+ - **Loading**: Preconnect to Google Fonts with `crossorigin` for speed
901
+
902
+ ## Advanced Features
903
+
904
+ ### Index/README Files
905
+ 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:
906
+ - Takes precedence over README if both exist
907
+ - Renders with full sidebar and TOC support
908
+ - Uses the file's frontmatter `title` or blog title as page title
909
+ - Supports all markdown features (tabs, diagrams, footnotes, etc.)
910
+
911
+ ### Smart 404 Page
912
+ When a route doesn't exist, Bloggy shows a custom 404 page with:
913
+ - Large "404" heading in gray
914
+ - Helpful error message explaining the situation
915
+ - Action buttons: "Go to Home" and "Go Back" with icons
916
+ - Tip section suggesting to check the sidebar
917
+ - Full sidebar included for easy navigation to correct page
918
+
919
+ ### Code Highlighting
920
+ 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.
921
+
922
+ ### Heading Anchors
923
+ All headings (`h1` through `h6`) automatically get `id` attributes based on their text content using the `text_to_anchor()` function:
924
+ - Removes special characters
925
+ - Converts to lowercase
926
+ - Replaces spaces with hyphens
927
+ - Enables direct linking via `#anchor-slug`
928
+ - Powers TOC navigation with smooth scrolling (scroll margin 7rem for navbar offset)
929
+
930
+ ### Image Handling
931
+ The `FrankenRenderer` class provides smart image handling:
932
+ - Responsive styling: `max-w-full h-auto rounded-lg mb-6`
933
+ - Relative path resolution: prepends `img_dir` based on post location
934
+ - Protocol detection: skips prepending for absolute URLs (`http://`, `https://`, `attachment:`, `blob:`, `data:`)
935
+ - Title attribute support: renders `title` if present in markdown
936
+ - Alt text: extracted from markdown image syntax
937
+
938
+ ### Frontmatter Support
939
+ All markdown files support YAML frontmatter for metadata:
940
+ - `title`: Override default title (derived from filename)
941
+ - Parsed with `python-frontmatter` library
942
+ - Cached based on file modification time
943
+ - Missing frontmatter gracefully handled (empty dict + raw content)