"There and Back Again: A DevOps Engineer's Journey Through AI and Infrastructure"
Transform your GitHub Gists into a beautiful, terminal-themed static blog with automatic RSS feeds, tag filtering, and modern responsive design.
- GitHub Gists Integration - Automatically fetches and converts your public gists to blog posts
- Markdown Processing - Full markdown support with GitHub-style syntax highlighting
- Dual Theme Support - Light/dark mode toggle with system preference detection
- Terminal Theme - Cyberpunk/DevOps aesthetic with green terminal prompts
- Static Generation - Builds fast, lightweight HTML files ready for deployment
- Theme Toggle - Smart light/dark mode with system preference detection and localStorage persistence
- Syntax Highlighting - GitHub-style code highlighting with language-specific colors for XML, JSON, JavaScript, Python, CSS, and more
- Multi-Tag System - Extract hashtags from gist descriptions for automatic categorization
- Advanced Filtering - Select multiple tags with AND logic for precise content discovery
- Reading Analytics - Terminal-themed word count and estimated reading time for each post
- Internal Gist Links - Automatically converts your gist URLs to internal blog post links for seamless navigation
- Table of Contents - Automatic ToC generation with sticky sidebar navigation and active section highlighting
- Permalink Navigation - Click-to-copy section links with smooth scrolling
- Global Tag Graph - Explore connections between tags across all posts; hover to highlight, click a tag to filter the homepage
- Pointer-centered zoom, pinch-zoom on touch, double-tap to zoom, and a reset view button
- RSS Feed - Auto-generated RSS 2.0 feed with proper metadata and categories
- Responsive Design - Mobile-optimized layouts with compact headers
- Cache Busting - Timestamp-driven cache busting for CSS and JS assets
- Modern Graph Styling - Terminal-themed graph visualizations with grid backgrounds, subtle glows, and improved accessibility
- Interactive Tag Graphs - Explore tag connections with hover highlighting, pointer-centered zoom, pinch gestures, and keyboard navigation
- Theme Switching - Seamless light/dark mode toggle in navigation bar
- Interactive Terminal Windows - Functional close, minimize, and maximize buttons with hover icons
- Multi-Tag Filtering - Select multiple tags with AND logic for precise content discovery
- Reading Metrics - Terminal commands show word count and reading time:
$ wc -w file.md
β1151 words
- Sticky Table of Contents - Desktop-only floating sidebar with active section highlighting
- Permalink Anchors - Hover-activated # links for easy section sharing
- Blinking Terminal Cursor - Authentic terminal feel in the header
- Pipeline Theme - Posts displayed as "deployments" with commit hashes
- Compact Post Headers - Mobile-friendly design that prioritizes content
- Advanced Tag Filtering UI - Multi-tag display:
$ grep --tag #ai #devops β 3 results
- Node.js 24+
- npm or yarn
-
Clone the repository
git clone <your-repo-url> cd gist-blog
-
Install dependencies
npm install
-
Configure your username
Prefer setting an environment variable:
export GIST_USERNAME=your-github-username
Or change the default in src/lib/config.js
:
DEFAULT_GIST_USERNAME: process.env.GIST_USERNAME || 'rbstp';
-
Optional: Configure RSS metadata
export SITE_URL=https://yourdomain.com export SITE_TITLE="Your Blog Title" export SITE_DESCRIPTION="Your blog description"
-
Build your blog
npm run build # or directly: node src/build.js
This repo uses ESLint (flat config) for JS and CSS.
- Run lints:
npm run lint
- Auto-fix what can be fixed:
npm run lint:fix
Notes:
- Generated output in
dist/
and the local cache.cache/
are ignored by ESLint. - CSS linting is scoped to
src/**/*.css
. Rules that flagged modern properties and important flags have been relaxed for this project.
To improve reliability and speed (especially in CI), you can provide a personal access token and benefit from conditional requests:
- Set
GITHUB_TOKEN
to raise rate limits for the GitHub API. - The build uses ETags (
If-None-Match
) for both the gist list and perβgist requests; when content is unchanged, cached data from.cache/
is reused after a304 Not Modified
response. - If an invalid or insufficientβscope token causes a
401 Unauthorized
, the build now automatically retries the request without the token so public gist data can still be fetched. A warning is emitted and the build continues (helpful if a secret was rotated or missing). - In CI, if zero posts are ultimately generated the build exits with code
2
to surface a likely auth or data issue early.
Example (macOS zsh):
export GITHUB_TOKEN=ghp_your_token_here
export GIST_USERNAME=your-github-username
npm run build
- Fetch concurrency: set
FETCH_CONCURRENCY
(default 5) to control parallel GitHub requests during build. - Local API caching: set
GIST_CACHE=true|false
(default true in local dev) to enable/disable on-disk caching under.cache/
.- Cache TTLs can be tuned via
GIST_CACHE_TTL_LIST_MS
(default 600000 = 10m) andGIST_CACHE_TTL_GIST_MS
(default 3600000 = 60m).
- Cache TTLs can be tuned via
- Post-build minification runs automatically. The
postbuild
script minifies HTML, inline JS/CSS, andstyles.css
.
In CI, you can disable caching to always fetch fresh data:
GIST_CACHE=false npm run build
If rate limits are hit in CI, set a GITHUB_TOKEN
secret and pass it to the build environment.
The build stores normalized gist list + perβgist JSON (plus ETags) in the local .cache/
directory. In CI you can persist this between runs using actions/cache
to drastically cut API calls and stay well below rate limits:
Benefits:
- Faster builds when nothing changed (many 304s avoided entirely β you never hit the network if still fresh in cache and TTL not expired).
- Reduced likelihood of secondary rate limiting on busy schedules.
- Allows safe increases to
FETCH_CONCURRENCY
for large gist sets.
Example snippet added before the build step:
- name: Restore gist API cache
uses: actions/cache@v4
with:
path: .cache
key: gist-cache-${{ env.GIST_USERNAME }}-${{ hashFiles('package-lock.json') }}-${{ github.run_id }}
restore-keys: |
gist-cache-${{ env.GIST_USERNAME }}-
Key strategy rationale:
- Includes username so forks donβt collide.
- Includes a hash of
package-lock.json
so changes to dependencies (potentially affecting parsing logic) can naturally bust the cache. - Appends
github.run_id
in the primary key so each run writes a fresh segment (avoids concurrent write races). The restore key prefix reuses the most recent cache from the same username if present.
TTL vs. cache persistence:
- Your internal TTLs (
GIST_CACHE_TTL_LIST_MS
,GIST_CACHE_TTL_GIST_MS
) still gate staleness; even if a file is restored from cache, the code re-validates TTL before reusing. - If you want to force a clean fetch while keeping the Action step, you can set
GIST_CACHE=false
for that run or tweak the key (e.g., add a manual suffix-bust1
).
When NOT to cache:
- Extremely small gist sets (benefit negligible).
- Highly dynamic private gists (not applicable here since only public gists are used).
Local dev: caching is always on by default; Action-level caching only affects CI persistence.
- Create a Gist on GitHub with a
.md
file - Add tags to your gist description using hashtags:
Fix for CLI tools running in monochrome mode #ai #cli #fix
- Run the build - your gist becomes a blog post automatically
Your gist should contain:
- At least one
.md
or.markdown
file - Optional: Title as first H1 heading, otherwise filename is used
- Optional: Multiple files (first markdown file becomes the post)
Add hashtags anywhere in your gist description:
#ai #devops #tutorial
β Creates clickable filter tags- Tags are extracted and removed from the display description
- Click tags to filter posts with a terminal-style interface
When you reference your own gists in markdown content, they automatically become internal links:
Before (in your markdown):
Check out my other post: https://gist.github.com/rbstp/abc123def456
After (in generated HTML):
Check out my other post: /posts/abc123def456.html
Features:
- Username-specific: Only your gist URLs are converted (preserves external links to other users' gists)
- All markdown formats: Works with inline links, reference links, and plain URLs
- Build-time transformation: No performance impact on site visitors
- Cross-post navigation: Create seamless content series and references
Automatic ToC Generation:
- Smart Detection - Automatically generates ToC for posts with heading levels 2-6 (
##
,###
, etc.) - Terminal Styling - ToC styled as terminal window with
$ grep -n "^##" filename.md
command - Desktop Only - ToC appears as floating sidebar on desktop, hidden on mobile for clean mobile experience
Interactive Features:
- Sticky Positioning - ToC follows along as you scroll, always accessible
- Active Section Highlighting - Current section highlighted in blue with bold text
- Smooth Scrolling - Clicking ToC links smoothly scrolls to target section
- Permalink Anchors - Hover over headings to reveal clickable # symbols for easy link sharing
Layout:
- Container-Aligned - On wide viewports, the ToC/graph sidebar aligns with the main content container
- Smart Sizing - Width uses a clamp (min ~240px, ideal ~22vw, max ~360px) for a stable ratio across screen sizes
- Fallback Docking - If there isnβt enough room next to content, it docks to the right edge and reserves space so content isnβt overlapped
- Responsive Behavior - Hidden only on narrower screens (β€1080px) or when there truly isnβt space
- Footer-Aware - Sidebar height shrinks as the footer enters view so they never overlap
Interactive Terminal Windows (Available on index page):
- Close Button (red) - Hides the entire terminal section
- Minimize Button (yellow) - Collapses terminal to header-only view
- Maximize Button (green) - Expands terminal to full width
- Hover Effects - Shows macOS-style icons (Γ, β, β±) when hovering over buttons
Post Page Terminals:
- Terminal Header - Shows
$ cat filename.md
with green prompt and blue command - Static Display - No interactive controls for cleaner reading experience
Multi-Tag Filtering System:
- Multiple Selection: Click multiple tags to combine filters using AND logic
- Toggle Behavior: Click active tags to remove them, inactive tags to add them
- Maximize Access: Maximize the pagination terminal to reveal all available tags
- Synchronized State: Tags in pagination terminal sync with main post area
- Smart Display: Filter status shows all active tags:
$ grep --tag #ai #devops β 3 results
- Persistent Terminal: Pagination terminal stays visible when tags are active
- Precise Filtering: Posts must contain ALL selected tags to appear in results
Dev Mode Easter Egg π―:
- Activation: Click the "main" branch button in the top navigation
- Chaos Mode: Instantly transforms the site into a DevOps disaster scenario
- Visual Changes: Build failures, error indicators, emergency rollback messages
- Branch Switch: Navigation and post cards change from "main" to "dev" branch
- Authentic Errors: Realistic terminal output with deployment failures and database issues
- Toggle Back: Click "dev" button to restore normal operation
- No Persistence: Easter egg resets on page reload for clean demo experience
Navigate to /graph.html
(also available in the header) to explore connections between tags across all posts.
- Uses the generated
dist/graph.json
(built from post tags duringnpm run build
) - Interactions:
- Pan: click/touch-drag to move the graph
- Zoom toward pointer: mouse wheel zoom is centered on the cursor
- Pinch-zoom (mobile/tablet): two-finger pinch to zoom with the midpoint anchored
- Double-tap to zoom toward the tap point
- Reset: a small βresetβ button in the top-right restores the default view
- Hover: highlight neighbors when hovering a tag (desktop)
- Click a tag to jump back to the homepage with that tag preselected (filter applied automatically)
- Node sizes scale with tag frequency; edge widths scale with co-occurrence weight
Post pages also include a compact topic graph (in the ToC sidebar on desktop) with the same pan/zoom/doubleβtap/reset behavior.
Notes:
- By default, the graph includes up to 20 most frequent tags. You can change this via
GRAPH_MAX_NODES
insrc/lib/config.js
(or env varGRAPH_MAX_NODES
).
- Dual Theme Support with CSS custom properties in
src/styles/main.css
- Dark Theme: GitHub dark color palette (
--bg-primary: #0d1117
) - Light Theme: Clean light palette (
--bg-primary: #ffffff
) - System Integration: Automatically detects and follows OS preference
- Manual Override: Theme toggle persists user choice in localStorage
- Fonts: JetBrains Mono (code) + Inter (text) Fonts: JetBrains Mono (code) + Inter (text) β selfβhosted (no Google Fonts request) Icons: Inline SVG symbols
This project now self-hosts its fonts for improved privacy, performance, and resilience.
Why:
- Removes external
fonts.googleapis.com
/fonts.gstatic.com
network dependency - Avoids layout shift and speeds up first render (
font-display: swap
) - Keeps consistent caching behavior with other static assets
Implementation:
- Variable font files placed in
src/fonts/
and copied todist/fonts
during build @font-face
declarations added near the top ofsrc/styles/main.css
- Preload hints added in
layout.html
for faster font availability
Expected filenames (place these manually β they are NOT committed):
src/fonts/InterVariable.woff2
src/fonts/JetBrainsMono-Variable.woff2
Obtain them from official releases:
- Inter: https://github.com/rsms/inter/releases
- JetBrains Mono: https://github.com/JetBrains/JetBrainsMono/releases
Licenses (SIL OFL 1.1) are included as OFL-INTER.txt
and OFL-JETBRAINS-MONO.txt
in the same directory.
If the font files are missing the site will gracefully fall back to the system sans/monospace stacks defined in the CSS custom properties.
Previously icons were provided by Font Awesome via a CDN stylesheet which triggered font downloads (fa-solid-900.woff2
, fa-brands-400.woff2
). These external requests have been removed. A tiny inline SVG sprite (sun, moon, branch, github, rss, graph) now serves icons:
- No layout shift waiting for icon font
- No crossβorigin font requests
- Easy to add more: drop another
<symbol>
into the sprite insidelayout.html
and reference with<use href="#icon-name"/>
.
Theme toggle now swaps the <use>
target between #icon-sun
and #icon-moon
instead of toggling Font Awesome classes.
The system includes built-in templates for:
layout.html
- Main page wrapper with navigation; loads/assets/main.js
with a build timestampindex.html
- Homepage with post gridpost.html
- Individual post pages with compact topic graph and ToC sidebar; no inline scripts or stylesgraph.html
- Global tag graph page; scripts are loaded dynamically bymain.js
Override by creating files in templates/
directory.
Most knobs live in src/lib/config.js
and can also be set via environment variables:
POSTS_PER_PAGE
(default 6)GRAPH_MAX_NODES
(default 20)GIST_USERNAME
(default fromDEFAULT_GIST_USERNAME
)GIST_CACHE
(true/false),GIST_CACHE_TTL_LIST_MS
,GIST_CACHE_TTL_GIST_MS
FETCH_CONCURRENCY
(default 5)
Configure RSS feed via environment variables:
export SITE_URL=https://yourdomain.com
export SITE_TITLE="Your Blog Title"
export SITE_DESCRIPTION="Your blog description"
Create .github/workflows/build-blog.yml
(simplified example with caching enabled):
name: Build and Deploy Gist Blog
on:
schedule:
- cron: '47 * */6 * *' # every 6 hours
workflow_dispatch:
push:
branches: [master]
paths:
- 'src/**'
- '.github/workflows/**'
jobs:
build:
runs-on: ubuntu-latest
env:
GIST_USERNAME: ${{ github.repository_owner }}
SITE_URL: https://example.com
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Test
run: npm test
- name: Restore gist API cache
uses: actions/cache@v4
with:
path: .cache
key: gist-cache-${{ env.GIST_USERNAME }}-${{ hashFiles('package-lock.json') }}-${{ github.run_id }}
restore-keys: |
gist-cache-${{ env.GIST_USERNAME }}-
- name: Build site
env:
GIST_CACHE: true
GITHUB_TOKEN: ${{ secrets.GIST_BLOG_TOKEN }} # optional PAT (gist scope)
run: npm run build
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: './dist'
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
The default GITHUB_TOKEN
that GitHub Actions provides does not allow calling the Gist API at elevated rate limits; for most publicβgist use cases anonymous access works, but heavy schedules (hourly + many gists) may hit secondary limits. To harden builds:
- Create a classic PAT with the minimal
gist
scope (no repo scope required). - Add it to your repository secrets as
GIST_BLOG_TOKEN
. - Pass it as
GITHUB_TOKEN: ${{ secrets.GIST_BLOG_TOKEN }}
in the build step env.
If the token becomes invalid the build will log a warning and fall back to unauthenticated requests; monitor for the zeroβposts exit code in CI.
Symptom | Cause | Fix |
---|---|---|
Build log shows GitHub API Error 401 then succeeds |
Invalid / missing PAT | Recreate PAT with gist scope and update secret |
β
Build complete! Generated 0 posts. with exit code 2 in CI |
No public gists fetched (auth failure or no gists) | Verify GIST_USERNAME , token validity, at least one public gist with markdown |
Frequent 403 then retry |
Rate limiting | Add PAT or reduce FETCH_CONCURRENCY |
Slow builds | Large gist set | Increase cache TTLs or reduce schedule frequency |
- Zero dependencies at runtime (pure HTML/CSS/JS)
- Modular build system with separated concerns:
BlogGenerator.js
- Build orchestrator: fetch β parse β shape β render β emitGistParser.js
- High-level parser delegating to focused modulesTagManager.js
- Extracts/cleans hashtags with cachingMarkdownProcessor.js
- marked + highlight.js wrapper with anchors/ToC and cachingLinkTransformer.js
- Converts own gist URLs into internal post linksGraphBuilder.js
- Builds tag co-occurrence graph dataDataShaper.js
- Shapes view-models for templates (single-pass reduce, DI for date utils)TemplateEngine.js
- Custom mustache-like template renderingTemplateLoader.js
- Cached template file loaderRSSGenerator.js
- RSS feed generation with configurable metadataconfig.js
- Central configuration (env + defaults)Cache.js
- JSON/ETag on-disk cachingGitHubClient.js
- Fetch with timeout, ETag handling, 304 reuse, 403 backoffAsyncPool.js
- Controlled concurrency helperDateUtils.js
- ISO date formatting utilities
- External templates in
src/templates/
for easy customization - Rate limit handling with automatic retries and 30s request timeouts
- Template caching for improved build performance
- Controlled concurrency for GitHub API requests
Internal Gist Link Transformation
- Regex-based URL transformation during markdown processing
- Username-specific filtering preserves external links to other users' gists
- Supports all markdown link formats (inline, reference, plain URLs)
- Build-time transformation for zero runtime performance impact
- Pattern matching:
https://gist.github.com/{username}/{gistId}
β/posts/{gistId}.html
Multi-Tag System
- Extracts
#tagname
from gist descriptions - Supports multiple tag selection with AND logic
- Interactive filter buttons with toggle behavior
- Terminal-style filter status display showing all active tags
- Synchronized state across post area and pagination terminal
- Maintains clean descriptions without hashtags
Reading Analytics
- Calculates word count by removing markdown syntax and code blocks
- Estimates reading time based on 225 words per minute average
- Displays as terminal commands with color-coded output
- Green
$
prompt and blue command text for authenticity - Two-line format: word count and reading time separately
Table of Contents System
- Extracts headings (levels 2-6) from markdown content during build
- Generates URL-friendly anchor IDs with proper slug formatting
- Custom marked.js renderer adds permalink anchors with hover effects
- JavaScript scroll tracking with throttled active section detection
- Fixed positioning with proper z-index management for overlay behavior
- CSS media queries ensure desktop-only display (hidden below 768px)
- Performance optimized with requestAnimationFrame for smooth scroll updates
RSS Feed
- Full RSS 2.0 compliance
- Post categories from tags
- Proper CDATA encoding
- Self-referencing atom:link
Performance
- Parallel gist processing with error resilience
- Template caching to reduce file I/O
- Pre-compiled regex patterns in template engine
- Request timeouts prevent hanging API calls
- Client-side pagination and filtering
- Minimal CSS/JS payload
- Static HTML generation
- Timestamp cache-busting for assets
- Lazy-loaded highlight.js only on pages containing code blocks
- rAF-throttled ToC layout adjustments on scroll/resize
- CSS
content-visibility
+contain-intrinsic-size
to speed initial render of heavy/offscreen sections - Respects
prefers-reduced-motion
to disable animations and smooth scrolling
- Client scripts live in
src/client/
and are emitted todist/assets/
via esbuildmain.js
is referenced once inlayout.html
; it lazy-loads page-specific modules (graph-page.js
,topic-graph-enhance.js
) when needed- Default build produces separate minified IIFEs without bundling for predictable filenames
- Optional: enable bundling with environment variable
BUNDLE_CLIENT=true
(keeps filenames stable)
- Post-build minification uses
html-minifier-terser
on HTML andstyles.css
- Timestamp is injected as
data-build-ts
on<body>
and appended as?v=...
to asset URLs for cache-busting
gist-blog/
βββ CLAUDE.md
βββ CNAME
βββ LICENSE
βββ README.md
βββ eslint.config.mjs
βββ package.json
βββ scripts/
β βββ minify.js
βββ src/
β βββ build.js
β βββ client/
β β βββ main.js
β β βββ graph-page.js
β β βββ topic-graph-enhance.js
β βββ lib/
β β βββ AsyncPool.js
β β βββ BlogGenerator.js
β β βββ Cache.js
β β βββ DataShaper.js
β β βββ DateUtils.js
β β βββ GistParser.js
β β βββ GitHubClient.js
β β βββ GraphBuilder.js
β β βββ LinkTransformer.js
β β βββ MarkdownProcessor.js
β β βββ RSSGenerator.js
β β βββ StringUtils.js
β β βββ TagManager.js
β β βββ config.js
β βββ styles/
β β βββ main.css
β βββ templates/
β βββ graph.html
β βββ index.html
β βββ layout.html
β βββ post.html
βββ test/
β βββ blog-generator.smoke.test.js
β βββ bloggenerator.data.test.js
β βββ cache.test.js
β βββ config.test.js
β βββ gist-parser.test.js
β βββ github-client.test.js
β βββ template-engine.test.js
βββ dist/
βββ assets/
β βββ main.js
β βββ graph-page.js
β βββ topic-graph-enhance.js
βββ posts/
β βββ {gist-id}.html
βββ feed.xml
βββ graph.html
βββ index.html
βββ styles.css
Pagination & Filtering
- All posts loaded once for instant filtering
- Terminal-themed pagination UI
- Smart tag filtering across all posts
- No server requests for navigation
This repo uses Nodeβs built-in test runner (no external frameworks).
- Run tests:
npm test
The tests isolate temp `dist/` and cache directories, and stub network calls where needed. The smoke test ensures the generator can build a minimal site with fake gist data.
## π€ Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Test the build process
5. Submit a pull request
## π License
MIT License - feel free to use for your own blog!
---
_Built with β€οΈ for developers who love terminals, gists, and clean code._