feat: implement dynamic Eleventy tiling blog, modular CSS, and agent customizations
- Add project foundation README.md and root AGENTS.md for pair programming - Configure .agents/ rules and skills for Eleventy tiling and Obsidian publishing - Decompose monolithic styles into modular tokens, reset, layout, and component CSS - Implement Nunjucks layout inheritance (base.njk, post.njk, page.njk) and reusable partials - Configure Eleventy 3 with markdown-it-anchor, syntax highlighting, and dynamic collections - Migrate sample prototypes into dynamic dispatches lander, gateway, and Markdown posts - Add multi-stage Dockerfile and clean-URL nginx.conf for production serving
This commit is contained in:
@@ -0,0 +1,56 @@
|
|||||||
|
# Technical Blog System Rules & Architecture Directives
|
||||||
|
|
||||||
|
## 1. Pedagogical Explanations (Interactive Learning)
|
||||||
|
1. **Explain the Code:** Whenever creating or refactoring code (Eleventy configuration, Nunjucks templates, CSS modules, build scripts), provide a clear, educational breakdown.
|
||||||
|
2. **Break Down Key Concepts:** Explain *why* a particular Eleventy filter, Nunjucks tag, or CSS variable structure was used so the user learns the foundational concepts.
|
||||||
|
3. **No Magic:** Avoid opaque or overly clever abstractions; prioritize readable, idiomatic code with commentary explaining tricky points.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. CSS Architecture & Isolation
|
||||||
|
1. **Zero Monolithic Inlines:** Do not inject raw `<style>` blocks into HTML templates.
|
||||||
|
2. **Modular File Structure:**
|
||||||
|
- Put global tokens in `src/assets/css/tokens.css`.
|
||||||
|
- Put reset and base typography in `src/assets/css/reset.css`.
|
||||||
|
- Put layout structures (tiling layout, split grids) in `src/assets/css/layout.css`.
|
||||||
|
- Put scoped component styles in `src/assets/css/components/<component-name>.css`.
|
||||||
|
3. **Design System Token Integrity:** Always use CSS custom properties defined in `tokens.css`:
|
||||||
|
- Canvas: `var(--bg-canvas)` (`#0d0f12`)
|
||||||
|
- Surface: `var(--bg-surface)` (`#121417`)
|
||||||
|
- Elevated: `var(--bg-elevated)` (`#1c1f26`)
|
||||||
|
- Borders: `var(--border-color)` (`#2b303b`)
|
||||||
|
- Text: `var(--text-primary)` (`#e0e6f0`), `var(--text-muted)` (`#8a95a5`)
|
||||||
|
- Accents: `var(--accent-cyan)` (`#00f0ff`), `var(--accent-green)` (`#00e676`), `var(--accent-amber)` (`#ffb700`)
|
||||||
|
4. **Hard Aesthetic Constraints:**
|
||||||
|
- No glassmorphism (`backdrop-filter`).
|
||||||
|
- No box-shadows or glows.
|
||||||
|
- No linear/radial gradients.
|
||||||
|
- Border radius strictly bounded between `0px` and `4px`.
|
||||||
|
- Transition durations set to `none` or `0s` for instant snap responses on hover.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Templating Guidelines (Eleventy + Nunjucks)
|
||||||
|
1. **Layout Hierarchy:**
|
||||||
|
- Root template: `src/_includes/layouts/base.njk` (contains HTML structure, `<head>`, metadata, scripts, stylesheets).
|
||||||
|
- Content layouts: `src/_includes/layouts/post.njk` (for articles), `src/_includes/layouts/page.njk` (for landing/catalog).
|
||||||
|
2. **Component Partials:**
|
||||||
|
- Isolated partials in `src/_includes/partials/` (`global-bar.njk`, `hero-tile.njk`, `post-card.njk`, `stats-sidebar.njk`, `code-box.njk`, `footer.njk`).
|
||||||
|
- Pass dynamic data via Nunjucks macros or contextual variables.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Obsidian Content Pipeline
|
||||||
|
1. **Frontmatter Specification:** Every post ingested from Obsidian must adhere to:
|
||||||
|
```yaml
|
||||||
|
title: "Article Title"
|
||||||
|
date: YYYY-MM-DD
|
||||||
|
tags: ["#tag1", "#tag2"]
|
||||||
|
excerpt: "Single paragraph summary"
|
||||||
|
readingTime: "X MIN READ"
|
||||||
|
draft: false
|
||||||
|
```
|
||||||
|
2. **Obsidian Normalization:**
|
||||||
|
- Wikilinks `[[slug]]` or `[[slug|Label]]` must be transformed into standard web URLs (`/posts/slug/`).
|
||||||
|
- Callout blocks (`> [!NOTE]`, `> [!WARNING]`, etc.) must render as terminal-style alert callouts.
|
||||||
|
- Images must be processed into `src/assets/images/posts/` and linked via standard `<img>` or Markdown image syntax.
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
---
|
||||||
|
name: eleventy-tiling-blog
|
||||||
|
description: >-
|
||||||
|
Use this skill when developing, refactoring, or building the River Rooks Eleventy blog, modularizing CSS into tokens and components, creating Nunjucks layouts and partials from sample designs, or configuring Eleventy build pipelines.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Eleventy Tiling Blog Development Skill
|
||||||
|
|
||||||
|
This skill guides the agent in modularizing templates and CSS from `sample-designs/` into a clean, DRY Eleventy (11ty) static publication site.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. CSS Modularization Workflow
|
||||||
|
|
||||||
|
When extracting CSS from `sample-designs/*.html` into `src/assets/css/`:
|
||||||
|
|
||||||
|
1. **Tokens (`src/assets/css/tokens.css`):**
|
||||||
|
Extract all `:root` custom properties:
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--bg-canvas: #0d0f12;
|
||||||
|
--bg-surface: #121417;
|
||||||
|
--bg-elevated: #1c1f26;
|
||||||
|
--border-color: #2b303b;
|
||||||
|
--text-primary: #e0e6f0;
|
||||||
|
--text-muted: #8a95a5;
|
||||||
|
--accent-cyan: #00f0ff;
|
||||||
|
--accent-green: #00e676;
|
||||||
|
--accent-amber: #ffb700;
|
||||||
|
--accent-red: #ff5252;
|
||||||
|
--font-mono: 'IBM Plex Mono', monospace;
|
||||||
|
--font-code: 'JetBrains Mono', monospace;
|
||||||
|
--font-prose: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Reset & Base (`src/assets/css/reset.css`):**
|
||||||
|
- Universal box-sizing reset (`box-sizing: border-box; margin: 0; padding: 0;`).
|
||||||
|
- Body font defaults, base line-height (`1.6`), instant hover transitions (`transition: none;`).
|
||||||
|
- Explicit 1px/2px focus ring styling for keyboard accessibility.
|
||||||
|
|
||||||
|
3. **Layout Grid (`src/assets/css/layout.css`):**
|
||||||
|
- `.tiling-layout`: Centered flex column, `max-width: 1120px` to `1160px`.
|
||||||
|
- `.split-grid`: 2-column or multi-tile layout with rigid gaps (`16px`).
|
||||||
|
- `.article-workspace-grid`: Left sticky sidebar (`290px`) + main article reading view (`1fr`).
|
||||||
|
|
||||||
|
4. **Component Modules (`src/assets/css/components/`):**
|
||||||
|
- `global-bar.css`: `.global-bar`, `.brand-group`, `.status-indicator`, `.bar-nav`, `.bar-link`.
|
||||||
|
- `tiles.css`: `.tile`, `.tile-header`, `.tile-title`, `.tile-meta`, `.hero-tile`.
|
||||||
|
- `code-box.css`: `.terminal-code-box`, `.code-titlebar`, `.code-content`.
|
||||||
|
- `article.css`: Prose typography capped at `65ch`, inline `<kbd>`, tables, callouts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Nunjucks (`.njk`) Component Architecture
|
||||||
|
|
||||||
|
1. **Layout Inheritance:**
|
||||||
|
- `src/_includes/layouts/base.njk`:
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>{{ title }} | River Rooks</title>
|
||||||
|
<link rel="stylesheet" href="/assets/css/main.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="tiling-layout">
|
||||||
|
{% include "partials/global-bar.njk" %}
|
||||||
|
{{ content | safe }}
|
||||||
|
{% include "partials/footer.njk" %}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
- `src/_includes/layouts/post.njk`:
|
||||||
|
Extends `layouts/base.njk`, injecting the `.article-workspace-grid` with the table of contents sidebar and article body.
|
||||||
|
|
||||||
|
2. **Reusable Partials:**
|
||||||
|
- `src/_includes/partials/global-bar.njk`
|
||||||
|
- `src/_includes/partials/hero-tile.njk`
|
||||||
|
- `src/_includes/partials/post-card.njk`
|
||||||
|
- `src/_includes/partials/stats-sidebar.njk`
|
||||||
|
- `src/_includes/partials/code-box.njk`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Explaining Changes to the User
|
||||||
|
|
||||||
|
When writing or modifying Eleventy files:
|
||||||
|
- Explain **how Nunjucks blocks and layout inheritance prevent template duplication**.
|
||||||
|
- Explain **how CSS custom properties cascade and ensure dark-mode consistency**.
|
||||||
|
- Point out how the resulting build outputs raw static HTML in `_site/` with zero runtime JavaScript overhead.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Validation & Verification
|
||||||
|
|
||||||
|
Run these commands to test Eleventy builds:
|
||||||
|
```bash
|
||||||
|
# Validate Eleventy configuration and build static output
|
||||||
|
npx @11ty/eleventy --dryrun
|
||||||
|
|
||||||
|
# Build to _site directory
|
||||||
|
npx @11ty/eleventy
|
||||||
|
|
||||||
|
# Launch local dev server with auto-refresh
|
||||||
|
npx @11ty/eleventy --serve
|
||||||
|
```
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
---
|
||||||
|
name: obsidian-sync
|
||||||
|
description: >-
|
||||||
|
Use this skill when designing, building, or troubleshooting the Obsidian vault publishing integration, the server upload API, note normalization (wikilinks, callouts, frontmatter), or the custom Obsidian plugin.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Obsidian Vault Publishing & Sync Skill
|
||||||
|
|
||||||
|
This skill guides the agent in implementing the custom Obsidian plugin and server-side ingestion workflow for publishing blog posts directly from an Obsidian vault.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. System Architecture
|
||||||
|
|
||||||
|
```text
|
||||||
|
+-------------------+ +-----------------------+
|
||||||
|
| Obsidian Vault | | Blog Server |
|
||||||
|
| | | |
|
||||||
|
| [Active Markdown] | POST /api/posts | /api/posts Endpoint |
|
||||||
|
| + | -------------------> | + |
|
||||||
|
| Custom Plugin | (Payload: JSON/MD) | Frontmatter Validator |
|
||||||
|
+-------------------+ | + |
|
||||||
|
| Wikilink Normalizer |
|
||||||
|
| + |
|
||||||
|
+-------------------+ | Write to `src/posts/` |
|
||||||
|
| Self-Hosted | CouchDB Sync | + |
|
||||||
|
| LiveSync Server | ===================> | 11ty Build Trigger |
|
||||||
|
+-------------------+ +-----------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Ingestion API Specification
|
||||||
|
|
||||||
|
### Endpoint: `POST /api/posts`
|
||||||
|
- **Headers:**
|
||||||
|
- `Content-Type: application/json`
|
||||||
|
- `Authorization: Bearer <API_SECRET>`
|
||||||
|
- **Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"slug": "reverse-engineering-iot-firmware",
|
||||||
|
"content": "---\ntitle: \"Reverse Engineering IoT Firmware\"\ndate: 2026-09-12\ntags: [\"#firmware\"]\n---\n\n## Content...",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"filename": "diagram.png",
|
||||||
|
"data": "<base64-encoded-image>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Response:**
|
||||||
|
- `200 OK`: `{"status": "published", "path": "/posts/reverse-engineering-iot-firmware/"}`
|
||||||
|
- `400 Bad Request`: `{"error": "Missing required frontmatter: title, date"}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Obsidian Markdown Normalization Rules
|
||||||
|
|
||||||
|
1. **Frontmatter Invariants:**
|
||||||
|
- Must contain `title`, `date`, `tags` (array), and optional `excerpt`, `readingTime`, `draft`.
|
||||||
|
2. **Wikilinks Translation:**
|
||||||
|
- `[[article-slug]]` $\rightarrow$ `[article-slug](/posts/article-slug/)`
|
||||||
|
- `[[article-slug|Custom Label]]` $\rightarrow$ `[Custom Label](/posts/article-slug/)`
|
||||||
|
3. **Callout Translation:**
|
||||||
|
- Obsidian callouts:
|
||||||
|
```markdown
|
||||||
|
> [!NOTE]
|
||||||
|
> Informational notice
|
||||||
|
```
|
||||||
|
Must be rendered with corresponding terminal alert styling.
|
||||||
|
4. **Internal Attachments:**
|
||||||
|
- `![[screenshot.png]]` $\rightarrow$ translated to `` and written to `src/assets/images/posts/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Custom Obsidian Plugin Development
|
||||||
|
|
||||||
|
The Obsidian plugin runs inside Obsidian desktop/mobile and interacts with the Obsidian API:
|
||||||
|
1. Registers a ribbon icon (e.g. `send` or `terminal`) and a command: `Publish Current Note to River Rooks Blog`.
|
||||||
|
2. Reads the active file:
|
||||||
|
```typescript
|
||||||
|
const activeFile = this.app.workspace.getActiveFile();
|
||||||
|
const content = await this.app.vault.read(activeFile);
|
||||||
|
```
|
||||||
|
3. Parses frontmatter using Obsidian's metadata cache:
|
||||||
|
```typescript
|
||||||
|
const cache = this.app.metadataCache.getFileCache(activeFile);
|
||||||
|
```
|
||||||
|
4. Sends the HTTP payload to the configured blog server URL with the Bearer token.
|
||||||
|
5. Displays an Obsidian `Notice('Published successfully to River Rooks Blog!')`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Self-Hosted LiveSync Compatibility
|
||||||
|
|
||||||
|
If using CouchDB-based self-hosted LiveSync:
|
||||||
|
- A local watcher or CouchDB change listener daemon can be configured to detect updates to notes marked with `published: true` or in a dedicated `Blog/` folder.
|
||||||
|
- When an update event fires, the daemon pulls the latest revision from the CouchDB database, runs the normalization pipeline, and saves it to `src/posts/`.
|
||||||
+5
-3
@@ -1,5 +1,7 @@
|
|||||||
.git
|
.git
|
||||||
.gitignore
|
.gitignore
|
||||||
Dockerfile
|
.agents
|
||||||
docker-compose.yml
|
node_modules
|
||||||
*.md
|
_site
|
||||||
|
sample-designs
|
||||||
|
*.log
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
_site/
|
||||||
|
.cache/
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# AGENTS.md: River Rooks Technical Blog Agent Guidelines
|
||||||
|
|
||||||
|
You are the dedicated Senior Pair Architect, Lead Developer, and Pedagogical Coding Mentor for the River Rooks Technical Blog.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Operating Persona & Responsibilities
|
||||||
|
|
||||||
|
### 1.1 Senior Pair Architect & Code Author
|
||||||
|
- Write robust, modular, production-grade code adhering to clean architecture.
|
||||||
|
- Structure files logically with clear component boundaries, minimal dependencies, and zero code duplication.
|
||||||
|
- Build static rendering workflows with **Eleventy (11ty)** and **Nunjucks (`.njk`)**.
|
||||||
|
|
||||||
|
### 1.2 Interactive Explainer & Teacher (Crucial)
|
||||||
|
- The user is building this blog to learn web architecture, templating, and CSS engineering.
|
||||||
|
- **Always explain the code you write**: Whenever creating or editing templates, scripts, or stylesheets, provide concise, high-signal explanations covering:
|
||||||
|
1. **How it works:** The mechanical flow, syntax, and internal lifecycle (e.g., how Eleventy processes collections, how CSS custom properties cascade).
|
||||||
|
2. **Why this approach was chosen:** The architectural trade-offs, advantages over alternatives, and best practices.
|
||||||
|
3. **Core concepts to understand:** Highlight foundational concepts so the user builds deep, transferable mental models.
|
||||||
|
|
||||||
|
### 1.3 Strict Design Guardian
|
||||||
|
Every component, style, and layout change must strictly adhere to [`sample-designs/design-specification.md`](./sample-designs/design-specification.md):
|
||||||
|
- **Dark Mode Native:** Canvas `#0d0f12`, Surface `#121417`, Elevated `#1c1f26`, Border `#2b303b`, Text `#e0e6f0`, Accents `#00f0ff` (Cyan), `#00e676` (Green), `#ffb700` (Amber).
|
||||||
|
- **Hard Anti-Patterns:**
|
||||||
|
- **NO** glassmorphism (`backdrop-filter: blur()`).
|
||||||
|
- **NO** drop shadows or glows (`box-shadow`, `text-shadow`).
|
||||||
|
- **NO** gradients (linear or radial).
|
||||||
|
- **NO** spring animations or floaty delays.
|
||||||
|
- **NO** rounded bubbles (border-radius capped at 0px to 4px).
|
||||||
|
- **Interactivity:** Instant binary hover state transitions (no 300ms easing); high-contrast 1px/2px focus outlines for keyboard navigation.
|
||||||
|
- **Typography:** IBM Plex Mono / JetBrains Mono for UI elements and code blocks; Inter for body prose.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Code Modularity & Isolation Guidelines
|
||||||
|
|
||||||
|
### 2.1 CSS Isolation
|
||||||
|
- **Forbidden:** Monolithic inline `<style>` blocks in HTML or templates.
|
||||||
|
- **Required Architecture:** All styling must be decomposed into modular files under `src/assets/css/`:
|
||||||
|
- `tokens.css`: Color variables, typography tokens, border variables, spacing scale.
|
||||||
|
- `reset.css`: CSS reset, typography baseline, instant transition overrides.
|
||||||
|
- `layout.css`: `.tiling-layout`, `.split-grid`, `.article-workspace-grid`.
|
||||||
|
- `components/*.css`: Isolated per-component stylesheets (`global-bar.css`, `tiles.css`, `code-box.css`, `article.css`).
|
||||||
|
|
||||||
|
### 2.2 Template Isolation (Nunjucks)
|
||||||
|
- Use layout inheritance: `base.njk` defines the outer shell and head assets; specialized layouts (`post.njk`, `page.njk`) extend `base.njk`.
|
||||||
|
- Reusable UI chunks must live in `src/_includes/partials/` (`global-bar.njk`, `hero-tile.njk`, `post-card.njk`, `code-box.njk`, `stats-sidebar.njk`).
|
||||||
|
- Use Nunjucks macros or parameters for parameterized UI elements rather than copy-pasting HTML.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Obsidian Content & API Integration
|
||||||
|
|
||||||
|
- Treat Markdown posts authored in Obsidian as the single source of truth for blog articles.
|
||||||
|
- Ingestion pipeline must validate frontmatter schemas:
|
||||||
|
```yaml
|
||||||
|
title: "Title of Post"
|
||||||
|
date: 2026-09-12
|
||||||
|
tags: ["#firmware", "#homelab"]
|
||||||
|
excerpt: "Short synopsis of article."
|
||||||
|
readingTime: "8 MIN READ"
|
||||||
|
draft: false
|
||||||
|
```
|
||||||
|
- Support Obsidian syntax conversion:
|
||||||
|
- Wikilinks `[[my-note|Custom Text]]` $\rightarrow$ `/posts/my-note/`.
|
||||||
|
- Obsidian callouts `> [!NOTE]` $\rightarrow$ semantic HTML alerts.
|
||||||
|
- Media/attachments $\rightarrow$ routed to static asset directories.
|
||||||
+18
-4
@@ -1,15 +1,29 @@
|
|||||||
|
# Stage 1: Build static site with Node
|
||||||
|
FROM node:22-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy dependency definitions and install
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copy source files and compile static output via Eleventy
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Stage 2: Serve static output via lightweight Alpine Nginx
|
||||||
FROM nginx:1.25-alpine
|
FROM nginx:1.25-alpine
|
||||||
|
|
||||||
# Remove default Nginx configuration
|
# Remove default Nginx configuration
|
||||||
RUN rm /etc/nginx/conf.d/default.conf
|
RUN rm /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
# Copy custom Nginx security & performance config
|
# Copy custom Nginx security & clean-URL configuration
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
# Copy static website files into Nginx server root
|
# Copy compiled static website files from builder stage
|
||||||
COPY . /usr/share/nginx/html
|
COPY --from=builder /app/_site /usr/share/nginx/html
|
||||||
|
|
||||||
# Expose standard HTTP port
|
# Expose HTTP port
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# River Rooks | Technical Publication & Systems Architecture Blog
|
||||||
|
|
||||||
|
> Developer-centric, high-signal, low-noise personal technical blog and homelab documentation site.
|
||||||
|
|
||||||
|
Inspired by native terminal environments, modal text editors (Zed, Ghostty), and tiling window managers, this site prioritizes raw performance, keyboard navigability, information density, and structured craftsmanship over generic marketing aesthetics.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Architectural Vision & Pillars
|
||||||
|
|
||||||
|
### 1.1 Static Generation via Eleventy (11ty)
|
||||||
|
- **Engine:** Fast, pre-rendered static HTML and minimal client-side runtime using **Eleventy** and **Nunjucks (`.njk`)** templates.
|
||||||
|
- **Performance:** Instant time-to-first-byte (TTFB), zero client-side framework bloat, and pure static asset delivery via Alpine Nginx.
|
||||||
|
- **Syntax Highlighting & Code Blocks:** Terminal-styled code blocks with title bars displaying filenames, language tags, and high-contrast monospace code.
|
||||||
|
|
||||||
|
### 1.2 Dynamic Obsidian Publishing Pipeline
|
||||||
|
- **Seamless Authoring:** Author and edit technical dispatches, CTF write-ups, and homelab notes directly inside an **Obsidian** vault.
|
||||||
|
- **Custom Ingestion API:** A dedicated server-side endpoint (`POST /api/posts`) accepting markdown payloads and assets, paired with a custom Obsidian plugin for one-click publishing.
|
||||||
|
- **LiveSync Integration:** Architectural support for syncing notes from a self-hosted Obsidian LiveSync (CouchDB) instance.
|
||||||
|
- **Content Normalization:** Translates Obsidian wikilinks (`[[slug|Title]]`), callouts (`> [!NOTE]`), and asset references into standard web formats during ingestion and build time.
|
||||||
|
|
||||||
|
### 1.3 Strict CSS Isolation & Reusable Templating
|
||||||
|
- **No Monolithic Inlines:** Replaces repetitive inline `<style>` tags with a modular CSS token and component architecture.
|
||||||
|
- **Design Tokens:** Centralized CSS custom properties (`--bg-canvas`, `--bg-surface`, `--border-color`, `--accent-cyan`, etc.) for consistent dark-mode native styling.
|
||||||
|
- **DRY Layouts & Partials:** Reusable Nunjucks components for global navigation (`global-bar`), hero tiles (`hero-tile`), split cards, and article workspace layouts (`article-workspace-grid`).
|
||||||
|
|
||||||
|
### 1.4 AI Pair Architect & Pedagogical Explainer
|
||||||
|
- **Interactive Learning:** The workspace includes customized Antigravity agent configurations (`.agents/` and `AGENTS.md`) that pair-program with the author, architecting solutions while clearly explaining *how* and *why* code is built.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Strict Design Specification
|
||||||
|
|
||||||
|
Adhering to the principles in [`sample-designs/design-specification.md`](./sample-designs/design-specification.md):
|
||||||
|
|
||||||
|
| Rule | Constraint |
|
||||||
|
| :--- | :--- |
|
||||||
|
| **Color Scheme** | Dark-mode native only. Canvas: `#0d0f12`, Surface: `#121417`, Elevated: `#1c1f26`, Borders: `#2b303b`. |
|
||||||
|
| **Accents** | High-signal terminal accents used sparingly: Cyan (`#00f0ff`), Green (`#00e676`), Amber (`#ffb700`). |
|
||||||
|
| **Borders & Radius** | Crisp 1px solid borders. Zero to 4px maximum border-radius (sharp corners preferred). |
|
||||||
|
| **Strict Anti-Patterns** | **NO** glassmorphism (`backdrop-filter`), **NO** drop shadows or glows, **NO** gradients, **NO** spring animations. |
|
||||||
|
| **Typography** | Monospace for UI & code (`IBM Plex Mono`, `JetBrains Mono`); clean sans-serif for body prose (`Inter`). |
|
||||||
|
| **Interactivity** | Instant binary hover snaps (no slow fade transitions); high-contrast focus outlines for keyboard navigation. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Project Directory Map
|
||||||
|
|
||||||
|
```text
|
||||||
|
.
|
||||||
|
├── .agents/ # Antigravity agent configurations & customizations
|
||||||
|
│ ├── rules/
|
||||||
|
│ │ └── blog-system.md # Architectural constraints, CSS/template isolation & explainer mode
|
||||||
|
│ └── skills/
|
||||||
|
│ ├── eleventy-tiling-blog/ # Runbook & recipes for Eleventy builds and component extraction
|
||||||
|
│ └── obsidian-sync/ # Runbook & API contracts for Obsidian publishing & plugin sync
|
||||||
|
├── sample-designs/ # Canonical HTML/CSS prototypes & design specification
|
||||||
|
│ ├── article-tiling-firmware.html
|
||||||
|
│ ├── blog-tiling-lead.html
|
||||||
|
│ ├── design-specification.md
|
||||||
|
│ └── service-gateway-tiling.html
|
||||||
|
├── src/ # Eleventy source directory (scaffolded in Phase 2)
|
||||||
|
│ ├── _data/ # Global site metadata, navigation, telemetry data
|
||||||
|
│ ├── _includes/
|
||||||
|
│ │ ├── layouts/ # Base, post, and page templates
|
||||||
|
│ │ └── partials/ # Global bar, hero tile, card, code box, sidebar
|
||||||
|
│ ├── assets/
|
||||||
|
│ │ ├── css/ # Modular CSS (tokens, reset, layout, components)
|
||||||
|
│ │ └── fonts/ # Self-hosted monospace & prose webfonts
|
||||||
|
│ └── posts/ # Markdown content ingested from Obsidian
|
||||||
|
├── .eleventy.js # Eleventy configuration, markdown-it plugins, and shortcodes
|
||||||
|
├── package.json # Project dependencies & build scripts
|
||||||
|
├── Dockerfile # Alpine Nginx container definition
|
||||||
|
├── docker-compose.yml # Multi-container orchestration (web + ingestion API)
|
||||||
|
└── AGENTS.md # Project-level agent instructions & pair programming guide
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Development & Build Roadmap
|
||||||
|
|
||||||
|
- [x] **Phase 1: Foundation & Agent Customizations**
|
||||||
|
- Establish `README.md` architectural blueprint.
|
||||||
|
- Setup `.agents/` workspace rules, skills, and root `AGENTS.md`.
|
||||||
|
- [ ] **Phase 2: Eleventy Scaffolding & Modularization**
|
||||||
|
- Initialize Node/npm environment and configure `.eleventy.js`.
|
||||||
|
- Extract design tokens, layout styles, and components into `src/assets/css/`.
|
||||||
|
- Build reusable Nunjucks layouts and partials modeled on `sample-designs/`.
|
||||||
|
- [ ] **Phase 3: Dynamic Obsidian Ingestion & Plugin**
|
||||||
|
- Implement the server-side note ingestion endpoint (`POST /api/posts`).
|
||||||
|
- Develop the custom Obsidian plugin for publishing notes and frontmatter.
|
||||||
|
- Integrate wikilink and callout transformation filters.
|
||||||
|
- [ ] **Phase 4: Containerization & Deployment**
|
||||||
|
- Verify static build outputs in `_site/`.
|
||||||
|
- Update Dockerfile and `docker-compose.yml` for unified production serving.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Development Setup
|
||||||
|
|
||||||
|
*(To be initialized during Phase 2)*
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Start Eleventy local development server with hot-reload
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Compile static assets for production
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Run via Docker Compose
|
||||||
|
docker compose up --build
|
||||||
|
```
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight";
|
||||||
|
import markdownIt from "markdown-it";
|
||||||
|
import markdownItAnchor from "markdown-it-anchor";
|
||||||
|
import { DateTime } from "luxon";
|
||||||
|
|
||||||
|
export default function (eleventyConfig) {
|
||||||
|
// 1. Static Asset Passthrough Copy
|
||||||
|
eleventyConfig.addPassthroughCopy({ "src/assets": "assets" });
|
||||||
|
|
||||||
|
// 2. Syntax Highlighting Plugin
|
||||||
|
eleventyConfig.addPlugin(syntaxHighlight);
|
||||||
|
|
||||||
|
// 3. Markdown-It Library Customization
|
||||||
|
const markdownOptions = {
|
||||||
|
html: true,
|
||||||
|
breaks: false,
|
||||||
|
linkify: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const markdownLib = markdownIt(markdownOptions).use(markdownItAnchor, {
|
||||||
|
permalink: markdownItAnchor.permalink.headerLink({
|
||||||
|
safariReaderFix: true
|
||||||
|
}),
|
||||||
|
slugify: (s) =>
|
||||||
|
s
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9 -]/g, "")
|
||||||
|
.replace(/\s+/g, "-")
|
||||||
|
.replace(/-+/g, "-")
|
||||||
|
});
|
||||||
|
|
||||||
|
eleventyConfig.setLibrary("md", markdownLib);
|
||||||
|
|
||||||
|
// 4. Custom Filters
|
||||||
|
// Format readable dates: SEP 12, 2026
|
||||||
|
eleventyConfig.addFilter("postDate", (dateObj) => {
|
||||||
|
if (!dateObj) return "";
|
||||||
|
return DateTime.fromJSDate(new Date(dateObj), { zone: "utc" })
|
||||||
|
.toFormat("LLL dd, yyyy")
|
||||||
|
.toUpperCase();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ISO date format for <time datetime="...">
|
||||||
|
eleventyConfig.addFilter("isoDate", (dateObj) => {
|
||||||
|
if (!dateObj) return "";
|
||||||
|
return DateTime.fromJSDate(new Date(dateObj), { zone: "utc" }).toISODate();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Limit array items
|
||||||
|
eleventyConfig.addFilter("limit", (arr, limit) => {
|
||||||
|
if (!Array.isArray(arr)) return [];
|
||||||
|
return arr.slice(0, limit);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter out internal system tags
|
||||||
|
eleventyConfig.addFilter("filterTagList", (tags) => {
|
||||||
|
const internalTags = ["all", "nav", "post", "posts"];
|
||||||
|
return (tags || []).filter((tag) => !internalTags.includes(tag.toLowerCase()));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reading time estimator (defaults to 200 words/min if frontmatter lacks readingTime)
|
||||||
|
eleventyConfig.addFilter("readingTime", (text, frontmatterTime) => {
|
||||||
|
if (frontmatterTime) return frontmatterTime;
|
||||||
|
if (!text) return "1 MIN READ";
|
||||||
|
const wordCount = text.trim().split(/\s+/).length;
|
||||||
|
const minutes = Math.ceil(wordCount / 200);
|
||||||
|
return `${minutes} MIN READ`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract H2 and H3 headings with IDs for dynamic Table of Contents
|
||||||
|
eleventyConfig.addFilter("extractHeadings", (htmlContent) => {
|
||||||
|
if (!htmlContent) return [];
|
||||||
|
const headingRegex = /<h([23])\s+id="([^"]+)"[^>]*>(.*?)<\/h\1>/gis;
|
||||||
|
const headings = [];
|
||||||
|
let match;
|
||||||
|
while ((match = headingRegex.exec(htmlContent)) !== null) {
|
||||||
|
headings.push({
|
||||||
|
level: parseInt(match[1], 10),
|
||||||
|
id: match[2],
|
||||||
|
title: match[3].replace(/<[^>]+>/g, "").trim()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return headings;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. Custom Collections
|
||||||
|
// Chronological blog posts collection
|
||||||
|
eleventyConfig.addCollection("posts", (collectionApi) => {
|
||||||
|
return collectionApi
|
||||||
|
.getFilteredByGlob("src/posts/*.md")
|
||||||
|
.filter((post) => !post.data.draft)
|
||||||
|
.sort((a, b) => b.date - a.date);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dynamic Tag List Collection across all posts
|
||||||
|
eleventyConfig.addCollection("tagList", (collectionApi) => {
|
||||||
|
const tagSet = new Set();
|
||||||
|
collectionApi.getFilteredByGlob("src/posts/*.md").forEach((item) => {
|
||||||
|
if ("tags" in item.data) {
|
||||||
|
let tags = item.data.tags;
|
||||||
|
if (typeof tags === "string") tags = [tags];
|
||||||
|
for (const tag of tags) {
|
||||||
|
const cleanTag = tag.replace(/^#/, "");
|
||||||
|
tagSet.add(cleanTag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return [...tagSet].sort();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
dir: {
|
||||||
|
input: "src",
|
||||||
|
includes: "_includes",
|
||||||
|
data: "_data",
|
||||||
|
output: "_site"
|
||||||
|
},
|
||||||
|
templateFormats: ["njk", "md", "html"],
|
||||||
|
htmlTemplateEngine: "njk",
|
||||||
|
markdownTemplateEngine: "njk"
|
||||||
|
};
|
||||||
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Gzip Compression
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml image/svg+xml;
|
||||||
|
|
||||||
|
# Security Headers
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-Frame-Options "DENY" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
|
||||||
|
# Clean URL Routing
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ $uri.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Static Assets Caching
|
||||||
|
location /assets/ {
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, max-age=2592000, immutable";
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+1754
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "river-rooks-blog",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Developer-centric personal technical publication and systems architecture blog",
|
||||||
|
"type": "module",
|
||||||
|
"main": "eleventy.config.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "eleventy --serve --watch",
|
||||||
|
"build": "eleventy",
|
||||||
|
"clean": "rm -rf _site",
|
||||||
|
"start": "npm run dev"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"eleventy",
|
||||||
|
"blog",
|
||||||
|
"tiling",
|
||||||
|
"nunjucks",
|
||||||
|
"obsidian"
|
||||||
|
],
|
||||||
|
"author": "River Rooks",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@11ty/eleventy": "^3.1.6",
|
||||||
|
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2",
|
||||||
|
"luxon": "^3.5.0",
|
||||||
|
"markdown-it": "^14.1.0",
|
||||||
|
"markdown-it-anchor": "^9.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "River Rooks",
|
||||||
|
"title": "River Rooks | Technical Publication",
|
||||||
|
"description": "Engineering Notes, CTF Write-Ups & Systems Architecture",
|
||||||
|
"status": "SYSTEMS NOMINAL",
|
||||||
|
"author": "River Rooks",
|
||||||
|
"systemSpec": "IEEE STD 1003.1",
|
||||||
|
"nav": [
|
||||||
|
{
|
||||||
|
"label": "Dispatches",
|
||||||
|
"url": "/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Gateway 3",
|
||||||
|
"url": "/gateway/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Git Forge",
|
||||||
|
"url": "https://git.rooks.dev",
|
||||||
|
"external": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "RSS Feed",
|
||||||
|
"url": "/feed.xml"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"topics": [
|
||||||
|
{
|
||||||
|
"title": "Hardware & Firmware",
|
||||||
|
"slug": "firmware",
|
||||||
|
"desc": "SPI dumping, UART interfacing & flash memory extraction",
|
||||||
|
"badge": "TRACE: HARDWARE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Systems Architecture",
|
||||||
|
"slug": "homelab",
|
||||||
|
"desc": "Traefik routing, WireGuard mesh & containerized microkernels",
|
||||||
|
"badge": "SYSTEM: INFRA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Offensive Security",
|
||||||
|
"slug": "security",
|
||||||
|
"desc": "Kernel exploitation, binary reverse-engineering & CTF writeups",
|
||||||
|
"badge": "CTF: OFFENSIVE"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ title | default(site.title) }}</title>
|
||||||
|
<meta name="description" content="{{ excerpt | default(site.description) }}">
|
||||||
|
|
||||||
|
<!-- Webfonts: IBM Plex Mono, JetBrains Mono, Inter -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,700;1,400&family=Inter:ital,wght@0,400;0,500;0,700;1,400&family=JetBrains+Mono:ital,wght@0,400;0,500;1,400&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Modular Bundled Stylesheet -->
|
||||||
|
<link rel="stylesheet" href="/assets/css/main.css">
|
||||||
|
|
||||||
|
{% block head %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="tiling-layout">
|
||||||
|
{% include "partials/global-bar.njk" %}
|
||||||
|
|
||||||
|
<main id="main-content">
|
||||||
|
{% block content %}
|
||||||
|
{{ content | safe }}
|
||||||
|
{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{% include "partials/footer.njk" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{% extends "layouts/base.njk" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="page-container">
|
||||||
|
{{ content | safe }}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
{% extends "layouts/base.njk" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="article-workspace-grid">
|
||||||
|
<!-- Left Sticky Sidebar: Document Outline & Telemetry -->
|
||||||
|
<aside class="article-sidebar" aria-label="Article Sidebar">
|
||||||
|
<!-- Dynamic Table of Contents -->
|
||||||
|
{% set headings = content | extractHeadings %}
|
||||||
|
{% if headings.length > 0 %}
|
||||||
|
<div class="sidebar-tile">
|
||||||
|
<div class="sidebar-heading">
|
||||||
|
<span>DOCUMENT OUTLINE</span>
|
||||||
|
<span style="color: var(--accent-cyan);">[{{ headings.length }}]</span>
|
||||||
|
</div>
|
||||||
|
<nav class="toc-nav" aria-label="Table of Contents">
|
||||||
|
{% for h in headings %}
|
||||||
|
<a href="#{{ h.id }}" class="toc-link {% if h.level == 3 %}toc-sub{% endif %}">
|
||||||
|
{{ h.title }}
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Article Telemetry Tile -->
|
||||||
|
<div class="sidebar-tile">
|
||||||
|
<div class="sidebar-heading">DISPATCH TELEMETRY</div>
|
||||||
|
<div class="sidebar-stats-box" style="padding: 0; background: transparent; border: none;">
|
||||||
|
<div class="stat-row">
|
||||||
|
<span>PUBLISHED:</span>
|
||||||
|
<span class="stat-val">{{ date | postDate }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span>DURATION:</span>
|
||||||
|
<span class="stat-val">{{ content | readingTime(readingTime) }}</span>
|
||||||
|
</div>
|
||||||
|
{% if difficulty %}
|
||||||
|
<div class="stat-row">
|
||||||
|
<span>DIFFICULTY:</span>
|
||||||
|
<span class="stat-val" style="color: var(--accent-amber);">{{ difficulty }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="padding-top: 10px; border-top: var(--border-width) solid var(--border-color);">
|
||||||
|
<div style="font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-muted); margin-bottom: 6px;">TOPIC TAGS:</div>
|
||||||
|
<div class="tag-cluster">
|
||||||
|
{% for tag in tags | filterTagList %}
|
||||||
|
<span class="tag-chip">#{{ tag }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="padding-top: 10px; border-top: var(--border-width) solid var(--border-color);">
|
||||||
|
<a href="/" class="filter-tag-btn" style="width: 100%; justify-content: center;">
|
||||||
|
← Return to Dispatches
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Right Main Article Workspace -->
|
||||||
|
<article class="article-tile" aria-labelledby="article-title">
|
||||||
|
<header class="article-header-block">
|
||||||
|
<nav class="article-breadcrumbs" aria-label="Breadcrumb">
|
||||||
|
<a href="/">~/NOTES</a>
|
||||||
|
<span>/</span>
|
||||||
|
<span>{{ (tags | filterTagList)[0] | default('DISPATCHES') | upper }}</span>
|
||||||
|
<span>/</span>
|
||||||
|
<span style="color: var(--accent-cyan);">{{ page.fileSlug | upper }}.MD</span>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<h1 id="article-title" class="article-main-title">{{ title }}</h1>
|
||||||
|
|
||||||
|
<div class="article-meta-row">
|
||||||
|
<span class="meta-item">
|
||||||
|
<time datetime="{{ date | isoDate }}">{{ date | postDate }}</time>
|
||||||
|
</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span class="meta-item">{{ content | readingTime(readingTime) }}</span>
|
||||||
|
{% if difficulty %}
|
||||||
|
<span>•</span>
|
||||||
|
<span class="meta-item" style="color: var(--accent-amber);">DIFFICULTY: {{ difficulty }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="markdown-prose">
|
||||||
|
{{ content | safe }}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{#
|
||||||
|
Reusable Tile Container Macro (macros/tile.njk)
|
||||||
|
Renders a terminal pane with header, title, meta pill, and custom body slot.
|
||||||
|
Usage:
|
||||||
|
{% import "macros/tile.njk" as ui %}
|
||||||
|
{% call ui.tile(title="STATISTICS", meta="STATIC HTML") %}
|
||||||
|
<p>Inside tile</p>
|
||||||
|
{% endcall %}
|
||||||
|
#}
|
||||||
|
|
||||||
|
{% macro tile(title="", meta="", class="", id="") %}
|
||||||
|
<section class="tile {{ class }}" {% if id %}id="{{ id }}"{% endif %}>
|
||||||
|
{% if title or meta %}
|
||||||
|
<header class="tile-header">
|
||||||
|
{% if title %}
|
||||||
|
<div class="tile-title">
|
||||||
|
<span class="status-indicator"></span>
|
||||||
|
{{ title }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if meta %}
|
||||||
|
<span class="tile-meta">{{ meta }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</header>
|
||||||
|
{% endif %}
|
||||||
|
<div class="tile-content">
|
||||||
|
{{ caller() }}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endmacro %}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
{% set featured = null %}
|
||||||
|
{% for p in collections.posts %}
|
||||||
|
{% if p.data.featured and not featured %}
|
||||||
|
{% set featured = p %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if not featured and collections.posts.length > 0 %}
|
||||||
|
{% set featured = collections.posts[0] %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if featured %}
|
||||||
|
<section class="tile" aria-label="Featured Technical Dispatch">
|
||||||
|
<header class="tile-header">
|
||||||
|
<div class="tile-title">
|
||||||
|
<span class="status-indicator"></span>
|
||||||
|
FEATURED DISPATCH
|
||||||
|
</div>
|
||||||
|
<span class="tile-meta">{{ (featured.data.tags | filterTagList)[0] | default('SYSTEMS') | upper }} • {{ featured.templateContent | readingTime(featured.data.readingTime) }}</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="featured-card-body">
|
||||||
|
<div class="featured-badge-row">
|
||||||
|
<span class="badge-pinned">[FEATURED DEEP DIVE]</span>
|
||||||
|
<time datetime="{{ featured.date | isoDate }}">{{ featured.date | postDate }}</time>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 style="margin: 0; font-size: inherit;">
|
||||||
|
<a href="{{ featured.url }}" class="featured-article-title">{{ featured.data.title }}</a>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="featured-prose">
|
||||||
|
{{ featured.data.excerpt }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Terminal Code Preview Snippet -->
|
||||||
|
<div class="terminal-code-box">
|
||||||
|
<div class="code-titlebar">
|
||||||
|
<span class="code-title">
|
||||||
|
<span style="color: var(--accent-cyan);">></span>
|
||||||
|
<span>flashrom_read.sh</span>
|
||||||
|
</span>
|
||||||
|
<span>POSIX sh</span>
|
||||||
|
</div>
|
||||||
|
<div class="code-content">
|
||||||
|
<pre><code class="language-bash"># Probe SPI flash chip and dump 16MB image
|
||||||
|
flashrom -p ch341a_spi -r firmware_dump.bin -V
|
||||||
|
# Verify checksums across consecutive reads
|
||||||
|
sha256sum dump1.bin dump2.bin</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="featured-cta-row">
|
||||||
|
<div class="tag-cluster">
|
||||||
|
{% for tag in featured.data.tags | filterTagList %}
|
||||||
|
<span class="tag-chip">#{{ tag }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<a href="{{ featured.url }}" class="cta-button">READ FULL WRITEUP →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<footer class="footer-tile" aria-label="Global Footer">
|
||||||
|
<div class="footer-left">
|
||||||
|
<span class="status-indicator"></span>
|
||||||
|
<span>RUNTIME: ELEVENTY 3.x STATIC</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>THEME: NATIVE TERMINAL DARK</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-right">
|
||||||
|
<span>© {{ currentYear or "2026" }} {{ site.author }}</span>
|
||||||
|
<a href="https://git.rooks.dev" target="_blank" rel="noopener noreferrer" class="footer-link">Source Forge ↗</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<header class="global-bar">
|
||||||
|
<a href="/" class="brand-group" aria-label="{{ site.name }} Home">
|
||||||
|
<span class="status-indicator"></span>
|
||||||
|
<span>{{ site.name }}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<nav class="bar-nav" aria-label="Quick Navigation">
|
||||||
|
{% for item in site.nav %}
|
||||||
|
<a href="{{ item.url }}"
|
||||||
|
class="bar-link {% if item.external %}external{% endif %} {% if page.url == item.url or (item.url != '/' and page.url.startsWith(item.url)) %}active{% endif %}"
|
||||||
|
{% if item.external %}target="_blank" rel="noopener noreferrer"{% endif %}>
|
||||||
|
{% if page.url == item.url %}> {% endif %}{{ item.label }}
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<section class="hero-tile" aria-label="Blog Mission & Overview">
|
||||||
|
<div class="hero-pretitle">TECHNICAL PUBLICATION REPOSITORY • ~/NOTES/INDEX.MD</div>
|
||||||
|
<h1 class="hero-main-title">{{ heroTitle or site.description }}</h1>
|
||||||
|
<p class="hero-landing-text">
|
||||||
|
{{ heroText or "A fast, zero-bloat digital workshop detailing hardware reverse engineering, low-level POSIX environments, self-hosted infrastructure, and kernel security research. Designed for dense information retrieval." }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="filter-pills-row" aria-label="Topic Filters">
|
||||||
|
<a href="/" class="filter-tag-btn {% if not currentTag %}active{% endif %}">
|
||||||
|
[All Dispatches ({{ collections.posts.length }})]
|
||||||
|
</a>
|
||||||
|
{% for tag in collections.tagList %}
|
||||||
|
{% set tagCount = 0 %}
|
||||||
|
{% for post in collections.posts %}
|
||||||
|
{% if tag in (post.data.tags or []) %}
|
||||||
|
{% set tagCount = tagCount + 1 %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<span class="filter-tag-btn {% if currentTag == tag %}active{% endif %}">
|
||||||
|
[#{{ tag }}{% if tagCount > 0 %} ({{ tagCount }}){% endif %}]
|
||||||
|
</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<article class="feed-card" aria-labelledby="post-title-{{ post.fileSlug }}">
|
||||||
|
<div class="feed-meta-row">
|
||||||
|
<time class="feed-date" datetime="{{ post.date | isoDate }}">
|
||||||
|
{{ post.date | postDate }}
|
||||||
|
</time>
|
||||||
|
<span>{{ post.templateContent | readingTime(post.data.readingTime) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 id="post-title-{{ post.fileSlug }}">
|
||||||
|
<a href="{{ post.url }}" class="feed-heading">{{ post.data.title }}</a>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{% if post.data.excerpt %}
|
||||||
|
<p class="feed-excerpt">{{ post.data.excerpt }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="feed-tags-row">
|
||||||
|
<div class="tag-cluster">
|
||||||
|
{% for tag in post.data.tags | filterTagList %}
|
||||||
|
<span class="tag-chip">#{{ tag }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% if post.data.difficulty %}
|
||||||
|
<span>DIFFICULTY: {{ post.data.difficulty }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<aside aria-label="Archive Statistics & Topics" style="display: flex; flex-direction: column; gap: var(--tile-gap);">
|
||||||
|
<!-- Archive Statistics Tile -->
|
||||||
|
<section class="tile" aria-label="Archive Telemetry">
|
||||||
|
<header class="tile-header">
|
||||||
|
<div class="tile-title">
|
||||||
|
<span class="status-indicator"></span>
|
||||||
|
ARCHIVE TELEMETRY
|
||||||
|
</div>
|
||||||
|
<span class="tile-meta">STATIC HTML</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="sidebar-stats-box">
|
||||||
|
<div class="stat-row">
|
||||||
|
<span>TOTAL DISPATCHES:</span>
|
||||||
|
<span class="stat-val">{{ collections.posts.length }} PUBLISHED</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span>TOPIC DOMAINS:</span>
|
||||||
|
<span class="stat-val">{{ collections.tagList.length }} CATEGORIES</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span>CORE SPECIFICATION:</span>
|
||||||
|
<span class="stat-val">{{ site.systemSpec }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span>OPERATIONAL STATUS:</span>
|
||||||
|
<span class="stat-val" style="color: var(--accent-green);">{{ site.status }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Curated Topic Tracks Tile -->
|
||||||
|
<section class="tile" aria-label="Curated Topic Tracks">
|
||||||
|
<header class="tile-header">
|
||||||
|
<div class="tile-title">
|
||||||
|
<span class="status-indicator"></span>
|
||||||
|
TOPIC TRACKS
|
||||||
|
</div>
|
||||||
|
<span class="tile-meta">CURATED PATHS</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="tile-content">
|
||||||
|
{% for topic in site.topics %}
|
||||||
|
<div class="topic-track-item">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: baseline;">
|
||||||
|
<span class="topic-track-title">{{ topic.title }}</span>
|
||||||
|
<span style="font-family: var(--font-mono); font-size: 0.68rem; color: var(--accent-cyan);">{{ topic.badge }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="topic-track-desc">{{ topic.desc }}</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
Article Workspace & Markdown Prose (components/article.css)
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/* Left Sticky Sidebar */
|
||||||
|
.article-sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--tile-gap);
|
||||||
|
position: sticky;
|
||||||
|
top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-tile {
|
||||||
|
background-color: var(--bg-surface);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 18px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-heading {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
border-bottom: var(--border-width) solid var(--border-color);
|
||||||
|
padding-bottom: 8px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Document Table of Contents */
|
||||||
|
.toc-nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-link {
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 4px 6px;
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
line-height: 1.4;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-link:hover {
|
||||||
|
background-color: var(--bg-elevated);
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-link.active {
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
background-color: var(--bg-canvas);
|
||||||
|
border-left: 2px solid var(--accent-cyan);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-sub {
|
||||||
|
padding-left: 16px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.74rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Article Reading Container */
|
||||||
|
.article-tile {
|
||||||
|
background-color: var(--bg-surface);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 36px 40px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.article-tile {
|
||||||
|
padding: 24px 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Article Header */
|
||||||
|
.article-header-block {
|
||||||
|
border-bottom: var(--border-width) solid var(--border-color);
|
||||||
|
padding-bottom: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-breadcrumbs {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-breadcrumbs a {
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-breadcrumbs a:hover {
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-main-title {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 2.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.article-main-title {
|
||||||
|
font-size: 1.7rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-meta-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Markdown Prose Hierarchy */
|
||||||
|
.markdown-prose {
|
||||||
|
max-width: var(--prose-max-width);
|
||||||
|
font-size: 1.02rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-prose p {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-prose h2 {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 1.45rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 36px 0 16px 0;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: var(--border-width) solid var(--border-color);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-prose h3 {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 28px 0 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-prose a {
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-prose a:hover {
|
||||||
|
background-color: var(--accent-cyan);
|
||||||
|
color: var(--bg-canvas);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline Code */
|
||||||
|
.markdown-prose code:not(pre code) {
|
||||||
|
font-family: var(--font-code);
|
||||||
|
font-size: 0.88rem;
|
||||||
|
background-color: var(--bg-canvas);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keyboard Shortcut Hints */
|
||||||
|
kbd {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
background-color: var(--bg-surface);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
color: var(--text-primary);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Markdown Callouts */
|
||||||
|
.markdown-callout {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 14px 18px;
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
background-color: var(--bg-canvas);
|
||||||
|
font-size: 0.94rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout-note {
|
||||||
|
border-left: 4px solid var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout-warning {
|
||||||
|
border-left: 4px solid var(--accent-amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout-title {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout-note .callout-title {
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout-warning .callout-title {
|
||||||
|
color: var(--accent-amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Monospace Tables */
|
||||||
|
.table-container {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 22px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-prose table,
|
||||||
|
.markdown-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.86rem;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
text-align: left;
|
||||||
|
margin: 20px 0;
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-prose th,
|
||||||
|
.markdown-table th {
|
||||||
|
background-color: var(--bg-canvas);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
padding: 8px 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-prose td,
|
||||||
|
.markdown-table td {
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
padding: 8px 12px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-prose tr:hover,
|
||||||
|
.markdown-table tr:hover {
|
||||||
|
background-color: var(--bg-elevated);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lists */
|
||||||
|
.markdown-prose ul, .markdown-prose ol {
|
||||||
|
margin: 16px 0 18px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-prose li {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,366 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
Cards & Feeds (components/cards.css)
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/* Featured Lead Article Card (Inside Tile) */
|
||||||
|
.featured-card-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-badge-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.76rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-pinned {
|
||||||
|
color: var(--accent-amber);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-article-title {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 1.35rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-article-title:hover {
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-prose {
|
||||||
|
font-size: 0.92rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-cta-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: var(--border-width) solid var(--border-color);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-button {
|
||||||
|
background-color: var(--accent-cyan);
|
||||||
|
color: var(--bg-canvas);
|
||||||
|
padding: 6px 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
border: var(--border-width) solid var(--accent-cyan);
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-button:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Stats / Telemetry Box */
|
||||||
|
.sidebar-stats-box {
|
||||||
|
background-color: var(--bg-canvas);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
padding: 12px 14px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-val {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Curated Topic Track Item */
|
||||||
|
.topic-track-item {
|
||||||
|
background-color: var(--bg-canvas);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
padding: 10px 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-track-item:hover {
|
||||||
|
border-color: var(--accent-cyan);
|
||||||
|
background-color: var(--bg-elevated);
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-track-title {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.84rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-track-item:hover .topic-track-title {
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-track-desc {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Full-Width Chronological Article Feed */
|
||||||
|
.feed-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-card {
|
||||||
|
background-color: var(--bg-canvas);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
padding: 16px 18px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-card:hover {
|
||||||
|
border-color: var(--accent-cyan);
|
||||||
|
background-color: var(--bg-elevated);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-meta-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.76rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-date {
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-heading {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-card:hover .feed-heading {
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-excerpt {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-tags-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 6px;
|
||||||
|
border-top: var(--border-width) solid var(--border-color);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.74rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-cluster {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-chip {
|
||||||
|
background-color: var(--bg-surface);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.74rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-chip:hover {
|
||||||
|
border-color: var(--accent-cyan);
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Featured Service Card (Homelab Gateway) */
|
||||||
|
.blog-service-feature {
|
||||||
|
background-color: var(--bg-canvas);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-feature-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-core {
|
||||||
|
background-color: var(--accent-cyan);
|
||||||
|
color: var(--bg-canvas);
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-feature-title {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-feature-title:hover {
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-feature-desc {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-sublinks {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 4px;
|
||||||
|
border-top: var(--border-width) solid var(--border-color);
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-sublink-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 4px 6px;
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-sublink-item:hover {
|
||||||
|
background-color: var(--bg-elevated);
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Homelab Services Mini-Grid */
|
||||||
|
.services-mini-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 540px) {
|
||||||
|
.services-mini-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-tile-item {
|
||||||
|
background-color: var(--bg-canvas);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
padding: 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-tile-item:hover {
|
||||||
|
border-color: var(--accent-cyan);
|
||||||
|
background-color: var(--bg-elevated);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-item-title {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-tile-item:hover .service-item-title {
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-item-tag {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-item-desc {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Infrastructure Node Telemetry */
|
||||||
|
.node-stat-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background-color: var(--bg-canvas);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-name {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-load {
|
||||||
|
color: var(--accent-green);
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
Terminal Code Blocks & Syntax Highlighting (components/code-box.css)
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/* Terminal Code Box Component */
|
||||||
|
.terminal-code-box,
|
||||||
|
.code-block-container {
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
background-color: var(--bg-canvas);
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: var(--font-code);
|
||||||
|
margin: 18px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-titlebar,
|
||||||
|
.code-block-header {
|
||||||
|
background-color: var(--bg-surface);
|
||||||
|
border-bottom: var(--border-width) solid var(--border-color);
|
||||||
|
padding: 6px 14px;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
color: var(--text-muted);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-code-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-code-btn:hover {
|
||||||
|
border-color: var(--accent-cyan);
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-content,
|
||||||
|
.code-block-container pre,
|
||||||
|
.markdown-prose pre {
|
||||||
|
padding: 14px 16px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.55;
|
||||||
|
margin: 18px 0;
|
||||||
|
background-color: var(--bg-canvas);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
font-family: var(--font-code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Syntax Highlighting Tokens (Prism / 11ty Syntax Highlight) */
|
||||||
|
.token.comment,
|
||||||
|
.token.prolog,
|
||||||
|
.token.doctype,
|
||||||
|
.token.cdata {
|
||||||
|
color: #5c6370;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.punctuation {
|
||||||
|
color: #abb2bf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.property,
|
||||||
|
.token.tag,
|
||||||
|
.token.boolean,
|
||||||
|
.token.number,
|
||||||
|
.token.constant,
|
||||||
|
.token.symbol,
|
||||||
|
.token.deleted {
|
||||||
|
color: var(--accent-amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.selector,
|
||||||
|
.token.attr-name,
|
||||||
|
.token.string,
|
||||||
|
.token.char,
|
||||||
|
.token.builtin,
|
||||||
|
.token.inserted {
|
||||||
|
color: var(--accent-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.operator,
|
||||||
|
.token.entity,
|
||||||
|
.token.url,
|
||||||
|
.language-css .token.string,
|
||||||
|
.style .token.string {
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.atrule,
|
||||||
|
.token.attr-value,
|
||||||
|
.token.keyword {
|
||||||
|
color: #c678dd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.function,
|
||||||
|
.token.class-name {
|
||||||
|
color: #61afef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.regex,
|
||||||
|
.token.important,
|
||||||
|
.token.variable {
|
||||||
|
color: var(--accent-amber);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
Footer Tile (components/footer.css)
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.footer-tile {
|
||||||
|
background-color: var(--bg-surface);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 14px 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.74rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-right {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link {
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link:hover {
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
Global Status Header Bar (components/global-bar.css)
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.global-bar {
|
||||||
|
background-color: var(--bg-surface);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 10px 18px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 700;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-group:hover {
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--accent-green);
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-link {
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-link:hover {
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-link.active {
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-bottom: 1px solid var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-link.external::after {
|
||||||
|
content: " ↗";
|
||||||
|
font-size: 0.75em;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
Tile Containers & Hero Tile (components/tiles.css)
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/* Standard Tile Container */
|
||||||
|
.tile {
|
||||||
|
background-color: var(--bg-surface);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
border-bottom: var(--border-width) solid var(--border-color);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-title {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-meta {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.74rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero Lander Tile */
|
||||||
|
.hero-tile {
|
||||||
|
background-color: var(--bg-surface);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-left: 3px solid var(--accent-cyan);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 24px 28px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-pretitle {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-main-title {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 2.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.hero-main-title {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-landing-text {
|
||||||
|
font-size: 0.98rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.65;
|
||||||
|
max-width: 82ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filter Pills Row in Hero Tile */
|
||||||
|
.filter-pills-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tag-btn {
|
||||||
|
background-color: var(--bg-canvas);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tag-btn:hover {
|
||||||
|
border-color: var(--accent-cyan);
|
||||||
|
color: var(--text-primary);
|
||||||
|
background-color: var(--bg-elevated);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tag-btn.active {
|
||||||
|
background-color: var(--accent-cyan);
|
||||||
|
color: var(--bg-canvas);
|
||||||
|
border-color: var(--accent-cyan);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Directive Callout Box inside Hero */
|
||||||
|
.hero-directive-box {
|
||||||
|
background-color: var(--bg-canvas);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-directive-box strong {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
Tiling Window Manager Grids & Layout (layout.css)
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/* Outer Shell Container */
|
||||||
|
.tiling-layout {
|
||||||
|
width: 100%;
|
||||||
|
max-width: var(--site-max-width);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--tile-gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Asymmetric 2-Column Split Grid (Lander: Lead Article + Telemetry Sidebar) */
|
||||||
|
.split-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.25fr 0.75fr;
|
||||||
|
gap: var(--tile-gap);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 880px) {
|
||||||
|
.split-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Article Workspace Grid (Left Sidebar TOC + Main Reading Workspace) */
|
||||||
|
.article-workspace-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 290px 1fr;
|
||||||
|
gap: var(--tile-gap);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.article-workspace-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Generic Symmetrical Grids */
|
||||||
|
.grid-2col {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: var(--tile-gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.grid-2col {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
Main Stylesheet (main.css)
|
||||||
|
River Rooks Blog - Modular CSS Architecture
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/* 1. Design Tokens & CSS Custom Properties */
|
||||||
|
@import "./tokens.css";
|
||||||
|
|
||||||
|
/* 2. Global Reset & Baseline Typography */
|
||||||
|
@import "./reset.css";
|
||||||
|
|
||||||
|
/* 3. Tiling Window Manager Grids */
|
||||||
|
@import "./layout.css";
|
||||||
|
|
||||||
|
/* 4. Component Styles */
|
||||||
|
@import "./components/global-bar.css";
|
||||||
|
@import "./components/tiles.css";
|
||||||
|
@import "./components/cards.css";
|
||||||
|
@import "./components/code-box.css";
|
||||||
|
@import "./components/article.css";
|
||||||
|
@import "./components/footer.css";
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
CSS Reset & Baseline (reset.css)
|
||||||
|
Strict adherence to developer-centric minimalism & instant transitions
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
*, *::before, *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 16px;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--bg-canvas);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: var(--font-prose);
|
||||||
|
line-height: 1.6;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24px 16px 48px 16px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hard anti-pattern enforcement: instant transitions across UI elements */
|
||||||
|
a, button, input, select, textarea {
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* High-contrast keyboard accessibility outline */
|
||||||
|
:focus-visible {
|
||||||
|
outline: 2px solid var(--accent-cyan);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img, svg, video {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Monospace font assignment for code & structural elements */
|
||||||
|
code, pre, kbd, samp {
|
||||||
|
font-family: var(--font-code);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
Design Tokens (tokens.css)
|
||||||
|
River Rooks Blog - Native Dark Mode Architecture
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Surface & Canvas Colors */
|
||||||
|
--bg-canvas: #0d0f12; /* Deep slate/charcoal canvas */
|
||||||
|
--bg-surface: #121417; /* Tile & card surface */
|
||||||
|
--bg-elevated: #1c1f26; /* Hovered surface */
|
||||||
|
|
||||||
|
/* Borders & Dividers */
|
||||||
|
--border-color: #2b303b; /* Crisp 1px border */
|
||||||
|
--border-width: 1px;
|
||||||
|
--border-radius: 4px; /* Strictly capped at 4px */
|
||||||
|
--border-radius-sm: 2px;
|
||||||
|
--border-radius-xs: 1px;
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
--font-mono: 'IBM Plex Mono', 'JetBrains Mono', monospace;
|
||||||
|
--font-code: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
|
--font-prose: 'Inter', system-ui, -apple-system, sans-serif;
|
||||||
|
|
||||||
|
/* Text Colors */
|
||||||
|
--text-primary: #e0e6f0; /* Light off-white prose */
|
||||||
|
--text-muted: #8a95a5; /* Monospace metadata */
|
||||||
|
|
||||||
|
/* High-Signal Terminal Accents */
|
||||||
|
--accent-cyan: #00f0ff; /* Primary active accent */
|
||||||
|
--accent-green: #00e676; /* Success / online indicator */
|
||||||
|
--accent-amber: #ffb700; /* Warning / highlight accent */
|
||||||
|
--accent-red: #ff5252; /* Danger / caution accent */
|
||||||
|
|
||||||
|
/* Structural Dimensions */
|
||||||
|
--site-max-width: 1140px;
|
||||||
|
--tile-gap: 16px;
|
||||||
|
--prose-max-width: 68ch;
|
||||||
|
}
|
||||||
+137
@@ -0,0 +1,137 @@
|
|||||||
|
---
|
||||||
|
layout: layouts/base.njk
|
||||||
|
title: "River Rooks | Homelab Gateway"
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Main Hero Landing Tile -->
|
||||||
|
<section class="hero-tile" aria-label="Gateway Declaration">
|
||||||
|
<div class="hero-pretitle">SYSTEM DECLARATION • RACK-01 NODE GATEWAY</div>
|
||||||
|
<h1 class="hero-main-title">Engineering Sovereign Infrastructure & Technical Notes</h1>
|
||||||
|
|
||||||
|
<div class="hero-landing-text">
|
||||||
|
<p>
|
||||||
|
My work centers on network forensics, self-hosted system architecture, low-level firmware exploitation, and zero-allocation Unix utilities.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This gateway functions as the frontend for my physical homelab cluster. Rather than hosting projects on opaque third-party clouds, everything here runs on bare metal in my personal server rack—orchestrated through Proxmox VE, partitioned across isolated VLANs via OPNsense, and protected by point-to-point WireGuard cryptographic meshes.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hero-directive-box">
|
||||||
|
<strong>PRIMARY DESTINATIONS:</strong>
|
||||||
|
<span style="color: var(--accent-cyan); margin-left: 8px;">[1] Technical Blog ({{ collections.posts.length }} Articles)</span> •
|
||||||
|
<span>[2] Forgejo Git Repos</span> •
|
||||||
|
<span>[3] Public Tools</span> •
|
||||||
|
<span>[4] Cluster Telemetry</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Split Tiling Columns -->
|
||||||
|
<div class="split-grid">
|
||||||
|
<!-- Left Column: Featured Service (Blog) + Dynamic Recent Articles -->
|
||||||
|
<div style="display: flex; flex-direction: column; gap: var(--tile-gap);">
|
||||||
|
<section class="tile" aria-label="Featured Service: Technical Blog">
|
||||||
|
<header class="tile-header">
|
||||||
|
<div class="tile-title">
|
||||||
|
<span class="status-indicator"></span>
|
||||||
|
// PRIMARY SERVICE: THE TECHNICAL BLOG
|
||||||
|
</div>
|
||||||
|
<span class="tile-meta">~/notes/ • {{ collections.posts.length }} ARTICLES</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="blog-service-feature">
|
||||||
|
<div class="blog-feature-top">
|
||||||
|
<span class="badge-core">[CORE PUBLICATION SERVICE]</span>
|
||||||
|
<span style="color: var(--text-muted);">{{ collections.posts[0].date | postDate }} LAST SYNC</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="/" class="blog-feature-title">> River Rooks Technical Notes & Research</a>
|
||||||
|
<p class="blog-feature-desc">
|
||||||
|
In-depth security post-mortems, CTF write-ups, shell portability benchmarks, and bare-metal homelab architecture guides.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="blog-sublinks">
|
||||||
|
{% for post in collections.posts | limit(4) %}
|
||||||
|
<a href="{{ post.url }}" class="blog-sublink-item">
|
||||||
|
<span>{{ loop.index }}. {{ post.data.title }}</span>
|
||||||
|
<span style="color: var(--accent-cyan);">[{{ post.templateContent | readingTime(post.data.readingTime) }}] →</span>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Column: Homelab Hosted Services & Node Telemetry -->
|
||||||
|
<div style="display: flex; flex-direction: column; gap: var(--tile-gap);">
|
||||||
|
<section class="tile" aria-label="Homelab Services Directory">
|
||||||
|
<header class="tile-header">
|
||||||
|
<div class="tile-title">
|
||||||
|
<span class="status-indicator"></span>
|
||||||
|
// HOMELAB SERVICES ROUTER
|
||||||
|
</div>
|
||||||
|
<span class="tile-meta">4 PUBLIC TOOLS</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="services-mini-grid">
|
||||||
|
<a href="https://git.rooks.dev" target="_blank" rel="noopener noreferrer" class="service-tile-item">
|
||||||
|
<div>
|
||||||
|
<div class="service-item-title">Forgejo Git</div>
|
||||||
|
<div class="service-item-tag">git.rooks.dev:2222</div>
|
||||||
|
</div>
|
||||||
|
<p class="service-item-desc">Self-hosted code repositories & dotfiles.</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://chef.rooks.dev" target="_blank" rel="noopener noreferrer" class="service-tile-item">
|
||||||
|
<div>
|
||||||
|
<div class="service-item-title">CyberChef</div>
|
||||||
|
<div class="service-item-tag">chef.rooks.dev:443</div>
|
||||||
|
</div>
|
||||||
|
<p class="service-item-desc">Hex decoding, cryptography & forensics.</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://pdf.rooks.dev" target="_blank" rel="noopener noreferrer" class="service-tile-item">
|
||||||
|
<div>
|
||||||
|
<div class="service-item-title">Stirling PDF</div>
|
||||||
|
<div class="service-item-tag">pdf.rooks.dev:443</div>
|
||||||
|
</div>
|
||||||
|
<p class="service-item-desc">Private document OCR and manipulation.</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://status.rooks.dev" target="_blank" rel="noopener noreferrer" class="service-tile-item">
|
||||||
|
<div>
|
||||||
|
<div class="service-item-title">Uptime Kuma</div>
|
||||||
|
<div class="service-item-tag">status.rooks.dev:443</div>
|
||||||
|
</div>
|
||||||
|
<p class="service-item-desc">High-frequency ping & latency monitors.</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Infrastructure Telemetry Tile -->
|
||||||
|
<section class="tile" aria-label="Infrastructure Nodes Telemetry">
|
||||||
|
<header class="tile-header">
|
||||||
|
<div class="tile-title">
|
||||||
|
<span class="status-indicator"></span>
|
||||||
|
// CLUSTER TELEMETRY
|
||||||
|
</div>
|
||||||
|
<span class="tile-meta">3 NODES ONLINE</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 8px;">
|
||||||
|
<div class="node-stat-row">
|
||||||
|
<span class="node-name">RACK-01 (Proxmox Primary)</span>
|
||||||
|
<span class="node-load">ONLINE • LOAD: 0.24</span>
|
||||||
|
</div>
|
||||||
|
<div class="node-stat-row">
|
||||||
|
<span class="node-name">NODE-02 (Storage TrueNAS)</span>
|
||||||
|
<span class="node-load">ONLINE • POOL: 98% HEALTH</span>
|
||||||
|
</div>
|
||||||
|
<div class="node-stat-row">
|
||||||
|
<span class="node-name">OPN-01 (Routing Gateway)</span>
|
||||||
|
<span class="node-load">ONLINE • WIREGUARD ACTIVE</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
layout: layouts/base.njk
|
||||||
|
title: "River Rooks | Engineering Notes & Systems Architecture"
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- 1. Lander Hero Tile with Dynamic Filter Tag Buttons -->
|
||||||
|
{% include "partials/hero-tile.njk" %}
|
||||||
|
|
||||||
|
<!-- 2. Asymmetric Split Grid: Featured Lead Post (Left) & Telemetry Sidebar (Right) -->
|
||||||
|
<div class="split-grid">
|
||||||
|
<div class="split-lead-col">
|
||||||
|
{% include "partials/featured-card.njk" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="split-sidebar-col">
|
||||||
|
{% include "partials/stats-sidebar.njk" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. Full-Width Chronological Dispatches Feed Tile -->
|
||||||
|
<section class="tile" aria-label="Dispatches Feed Catalog">
|
||||||
|
<header class="tile-header">
|
||||||
|
<div class="tile-title">
|
||||||
|
<span class="status-indicator"></span>
|
||||||
|
RECENT DISPATCHES
|
||||||
|
</div>
|
||||||
|
<span class="tile-meta">ORDER: CHRONOLOGICAL DESC • TOTAL: {{ collections.posts.length }}</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="feed-list">
|
||||||
|
{% for post in collections.posts %}
|
||||||
|
{% include "partials/post-card.njk" %}
|
||||||
|
{% else %}
|
||||||
|
<p style="color: var(--text-muted); font-family: var(--font-mono); font-size: 0.85rem; padding: 16px;">
|
||||||
|
No published dispatches found in repository.
|
||||||
|
</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
layout: layouts/post.njk
|
||||||
|
title: "High-Availability Traefik Router with Docker Compose"
|
||||||
|
date: 2026-07-15
|
||||||
|
tags:
|
||||||
|
- homelab
|
||||||
|
- networking
|
||||||
|
- docker
|
||||||
|
excerpt: "Architecting a redundant reverse-proxy setup with automated Let's Encrypt wildcard DNS challenge routing and zero-downtime container swaps."
|
||||||
|
readingTime: "12 MIN READ"
|
||||||
|
featured: false
|
||||||
|
difficulty: "ADVANCED"
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1.0 Topology Overview
|
||||||
|
|
||||||
|
Operating production services on bare-metal home clusters demands strict ingress routing, SSL termination, and health checking. Rather than managing manual Nginx upstream definitions, Traefik dynamically listens to Docker socket events and generates routing graphs automatically.
|
||||||
|
|
||||||
|
```text
|
||||||
|
[Internet / WireGuard Mesh]
|
||||||
|
|
|
||||||
|
[Edge Router]
|
||||||
|
|
|
||||||
|
+--------+--------+
|
||||||
|
| |
|
||||||
|
[Traefik 01] [Traefik 02] (Keepalived VIP)
|
||||||
|
| |
|
||||||
|
+--------+--------+
|
||||||
|
|
|
||||||
|
[Internal Overlay]
|
||||||
|
|
|
||||||
|
+-------+-------+
|
||||||
|
| | |
|
||||||
|
[App1] [App2] [App3]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2.0 Docker Compose Infrastructure
|
||||||
|
|
||||||
|
The core proxy layer is configured with strict network isolation, healthchecks, and non-root execution:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: traefik:v3.1
|
||||||
|
container_name: edge_router
|
||||||
|
restart: unless-stopped
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
environment:
|
||||||
|
- CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
|
||||||
|
volumes:
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- ./traefik.yml:/traefik.yml:ro
|
||||||
|
- ./acme.json:/acme.json
|
||||||
|
networks:
|
||||||
|
- proxy_net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy_net:
|
||||||
|
external: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3.0 Verification & Failover Testing
|
||||||
|
|
||||||
|
Run the health check endpoint using `curl` to verify response headers:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -Iv https://router.rooks.internal/ping
|
||||||
|
```
|
||||||
|
|
||||||
|
All requests route through mutual TLS with zero dropped connections during container redeploys.
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
---
|
||||||
|
layout: layouts/post.njk
|
||||||
|
title: "Reverse Engineering IoT Firmware: SPI Flash Extraction"
|
||||||
|
date: 2026-07-20
|
||||||
|
tags:
|
||||||
|
- firmware
|
||||||
|
- hardware
|
||||||
|
- security
|
||||||
|
- bus-pirate
|
||||||
|
excerpt: "Physical hardware exploitation and firmware dump walkthrough via direct SPI flash memory extraction on an IoT gateway using Bus Pirate and flashrom."
|
||||||
|
readingTime: "8 MIN READ"
|
||||||
|
featured: true
|
||||||
|
difficulty: "INTERMEDIATE"
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1.0 Executive Summary
|
||||||
|
|
||||||
|
In security audits of embedded systems, retrieving the firmware image is the critical foundational step. When manufacturers encrypt over-the-air (OTA) updates and blow electronic fuses to permanently disable silicon debug ports (such as `UART` serial consoles and `JTAG/SWD` test pads), physical memory extraction directly off the circuit board becomes mandatory.
|
||||||
|
|
||||||
|
This writeup details the complete process of extracting a binary image from an 8-pin SOP SPI flash chip (Winbond W25Q64JV, 8MB) using a **Dangerous Prototypes Bus Pirate v4** hardware bridge and the open-source `flashrom` utility.
|
||||||
|
|
||||||
|
<div class="markdown-callout callout-note">
|
||||||
|
<div class="callout-title">[!NOTE] Target Board Isolation</div>
|
||||||
|
<p>Always de-solder the SPI flash chip or clamp using an SOP8 test clip with the host microcontroller's reset line (<code>/RESET</code>) driven low to prevent the main SoC from contending on the SPI bus.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 2.0 Hardware Pinout & Bus Pirate Setup
|
||||||
|
|
||||||
|
The target is an 8-pin Small Outline Package (SOP-8). Pin 1 is easily identified by the circular depression in the epoxy mold. Connect the Bus Pirate flying leads directly according to the standard SPI protocol mapping:
|
||||||
|
|
||||||
|
### 2.1 SOP8 Pinout Matrix
|
||||||
|
|
||||||
|
| SOP8 Pin # | Signal Name | Bus Pirate Lead | Electrical Purpose |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| **1** | `/CS` | CS (White) | Chip Select (Active Low) |
|
||||||
|
| **2** | `DO (MISO)` | MISO (Black) | Serial Data Output from Flash |
|
||||||
|
| **3** | `/WP` | +3.3V (VPU) | Write Protect (Tied High to Disable) |
|
||||||
|
| **4** | `GND` | GND (Brown) | System Ground Reference |
|
||||||
|
| **5** | `DI (MOSI)` | MOSI (Grey) | Serial Data Input to Flash |
|
||||||
|
| **6** | `CLK` | CLK (Purple) | Serial Clock Driven by Master |
|
||||||
|
| **7** | `/HOLD` | +3.3V (VPU) | Hold / Suspend Input (Tied High) |
|
||||||
|
| **8** | `VCC` | +3.3V (Red) | Supply Voltage (Nominal 3.3V DC) |
|
||||||
|
|
||||||
|
<div class="markdown-callout callout-warning">
|
||||||
|
<div class="callout-title">[!WARNING] Electrical Voltage Rating</div>
|
||||||
|
<p>Verify whether your target chip is a <strong>3.3V</strong> part (e.g., <code>W25Q64FV</code>) or a <strong>1.8V</strong> part (e.g., <code>W25Q64FW</code>). Supplying 3.3V logic to a 1.8V chip will permanently destroy the silicon junction gates.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 3.0 SPI Extraction via flashrom
|
||||||
|
|
||||||
|
Plug the Bus Pirate into your analysis workstation via USB. It registers as `/dev/ttyUSB0`. Query the chip ID register using the `0x9F` JEDEC Read ID command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
#!/bin/sh
|
||||||
|
# Query the SPI flash details using flashrom via Bus Pirate
|
||||||
|
flashrom -p buspirate_spi:dev=/dev/ttyUSB0,spispeed=1M -r firmware_dump1.bin
|
||||||
|
|
||||||
|
# Repeat extraction to create a secondary dump for comparison
|
||||||
|
flashrom -p buspirate_spi:dev=/dev/ttyUSB0,spispeed=1M -r firmware_dump2.bin
|
||||||
|
|
||||||
|
# Verify the integrity of the binary dumps by diffing
|
||||||
|
cmp firmware_dump1.bin firmware_dump2.bin
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "[+] SPI Dumps match identically! Proceeding to hashing..."
|
||||||
|
md5sum firmware_dump1.bin
|
||||||
|
sha256sum firmware_dump1.bin
|
||||||
|
else
|
||||||
|
echo "[-] Bit flip or voltage sag detected! Re-run extraction." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
Press <kbd>Ctrl</kbd> + <kbd>C</kbd> if the bus fails to synchronize. Once complete, inspect the initial bytes with `hexdump`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
0000000: 2705 1956 7062 8251 60a4 2130 0041 5262 '..Vpb.Q`.!0.ARb
|
||||||
|
0000010: 6873 7173 0000 0004 0000 0000 0000 1000 hsqs............
|
||||||
|
0000020: 0002 0001 0000 0000 636f 6e66 6967 2e64 ........config.d
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice the magic bytes `6873 7173` (ASCII representation: **`hsqs`**) at offset `0x000010`. This confirms the presence of a SquashFS compressed root filesystem!
|
||||||
|
|
||||||
|
## 4.0 Binary Verification & Hashes
|
||||||
|
|
||||||
|
Before proceeding with automated deconstruction using `binwalk -Me`, record cryptographic hashes in your lab notebook:
|
||||||
|
|
||||||
|
- **Dump Size:** `8,388,608 bytes` (8192 KiB)
|
||||||
|
- **MD5:** `d41d8cd98f00b204e9800998ecf8427e`
|
||||||
|
- **SHA-256:** `a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e`
|
||||||
|
|
||||||
|
## 5.0 Audit Checklist
|
||||||
|
|
||||||
|
Ensure all forensics hygiene steps have been recorded:
|
||||||
|
|
||||||
|
- [x] Visual optical inspection and pinout diagram documented
|
||||||
|
- [x] Clean dual flashrom dumps matching via bitwise `cmp`
|
||||||
|
- [x] SquashFS magic header identified at offset `0x10`
|
||||||
|
- [ ] Filesystem extraction and hardcoded credentials analysis (Part 2)
|
||||||
Reference in New Issue
Block a user