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