summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-05-11 10:17:53 +0200
committerDanilo M. <danix@danix.xyz>2026-05-11 10:17:53 +0200
commit884df671a1bd744d3bc004cd8a6f2b5838d24b7b (patch)
tree729a073e24f69e234aa514927c8a76d3af2c8df5
downloadpkgs-html-structure-884df671a1bd744d3bc004cd8a6f2b5838d24b7b.tar.gz
pkgs-html-structure-884df671a1bd744d3bc004cd8a6f2b5838d24b7b.zip
feat: initial commit — Apache autoindex theme with matrix rain
- gen_web_hook.sh: generates _header.html/_footer.html per repo/category/package dir - .assets/matrix-rain.js: danix2-engine matrix rain, header-scoped (65% width, right-aligned) - Category headers include gradient accent pill with category name - htaccess: autoindex config, MIME types, cache headers, IndexIgnore .assets - vhost.conf: Apache VirtualHost template (values masked for public repo) - CLAUDE.md: repo architecture docs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--.assets/matrix-rain.js136
-rw-r--r--CLAUDE.md49
-rw-r--r--docs/superpowers/plans/2026-05-11-matrix-rain.md373
-rw-r--r--docs/superpowers/specs/2026-05-11-matrix-rain-design.md71
-rw-r--r--gen_web_hook.sh342
-rw-r--r--htaccess43
-rw-r--r--vhost.conf33
7 files changed, 1047 insertions, 0 deletions
diff --git a/.assets/matrix-rain.js b/.assets/matrix-rain.js
new file mode 100644
index 0000000..e22ada2
--- /dev/null
+++ b/.assets/matrix-rain.js
@@ -0,0 +1,136 @@
+// Matrix rain background effect — header-scoped build for packages.danix.xyz
+// Derived from danix2-hugo-theme/assets/js/matrix-rain.js
+(function() {
+ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
+
+ var header = document.querySelector('.site-header');
+ if (!header) return;
+
+ // Canvas
+ var canvas = document.createElement('canvas');
+ canvas.id = 'matrix-rain';
+ canvas.style.cssText = 'position:absolute;top:0;right:0;height:100%;pointer-events:none;z-index:0;';
+ header.appendChild(canvas);
+
+ // Gradient fade div — covers left portion of canvas area, left→transparent
+ var fade = document.createElement('div');
+ fade.style.cssText = 'position:absolute;top:0;right:0;bottom:0;width:65%;background:linear-gradient(to right,var(--bg-card) 0%,transparent 60%);pointer-events:none;z-index:1;';
+ header.appendChild(fade);
+
+ var ctx = canvas.getContext('2d');
+
+ var columns = [];
+ var frameCount = 0;
+ var colors = { accent: '#5c9cf5', accent2: '#4ec97b', bg: '#161b25', head: '#ffffff' };
+
+ var ASCII = Array.from({ length: 94 }, function(_, i) { return String.fromCharCode(33 + i); });
+ var KATA = Array.from({ length: 96 }, function(_, i) { return String.fromCodePoint(0x30a0 + i); });
+ var CHARS = shuffle([].concat(ASCII, KATA, KATA, KATA));
+
+ function shuffle(arr) {
+ for (var i = arr.length - 1; i > 0; i--) {
+ var j = Math.floor(Math.random() * (i + 1));
+ var tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp;
+ }
+ return arr;
+ }
+
+ function hexToRgba(color, alpha) {
+ var rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
+ if (rgbMatch) return 'rgba(' + rgbMatch[1] + ',' + rgbMatch[2] + ',' + rgbMatch[3] + ',' + alpha + ')';
+ var hex = color.replace('#', '');
+ var r = parseInt(hex.substring(0, 2), 16);
+ var g = parseInt(hex.substring(2, 4), 16);
+ var b = parseInt(hex.substring(4, 6), 16);
+ return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')';
+ }
+
+ function sampleColors() {
+ var style = getComputedStyle(document.documentElement);
+ var isDark = !document.documentElement.classList.contains('theme-light');
+ colors.accent = style.getPropertyValue('--accent').trim() || '#5c9cf5';
+ colors.accent2 = style.getPropertyValue('--accent2').trim() || '#4ec97b';
+ colors.bg = style.getPropertyValue('--bg-card').trim() || '#161b25';
+ colors.head = isDark ? '#ffffff' : '#1a0533';
+ }
+
+ function resizeCanvas() {
+ canvas.width = Math.floor(header.offsetWidth * 0.65);
+ canvas.height = header.offsetHeight;
+ ctx.font = '14px "IBM Plex Mono", monospace';
+ ctx.textBaseline = 'top';
+ initColumns();
+ }
+
+ function initColumns() {
+ columns = [];
+ var columnWidth = 14;
+ var columnCount = Math.floor(canvas.width / columnWidth);
+ for (var i = 0; i < columnCount; i++) {
+ columns.push({
+ x: i * columnWidth,
+ y: -Math.floor(Math.random() * 40),
+ speed: 2 + Math.floor(Math.random() * 3),
+ color: Math.random() < 0.6 ? 'accent2' : 'accent',
+ charIndex: Math.floor(Math.random() * CHARS.length),
+ length: 8 + Math.floor(Math.random() * 13)
+ });
+ }
+ }
+
+ function setupThemeObserver() {
+ var observer = new MutationObserver(function(mutations) {
+ for (var i = 0; i < mutations.length; i++) {
+ if (mutations[i].attributeName === 'class') { sampleColors(); break; }
+ }
+ });
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
+ }
+
+ function drawFrame() {
+ frameCount++;
+ ctx.fillStyle = hexToRgba(colors.bg, 0.085);
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ for (var ci = 0; ci < columns.length; ci++) {
+ var col = columns[ci];
+ if (frameCount % col.speed !== 0) continue;
+
+ ctx.fillStyle = colors[col.color];
+ for (var i = 1; i <= col.length; i++) {
+ var trailY = (col.y - i) * 14;
+ if (trailY < 0) continue;
+ var trailCharIndex = (col.charIndex - i + CHARS.length) % CHARS.length;
+ ctx.fillText(CHARS[trailCharIndex], col.x, trailY);
+ }
+
+ ctx.fillStyle = colors.head;
+ ctx.fillText(CHARS[col.charIndex % CHARS.length], col.x, col.y * 14);
+
+ col.y++;
+ col.charIndex = (col.charIndex + 1) % CHARS.length;
+
+ if (col.y * 14 > canvas.height + col.length * 14) {
+ col.y = -Math.floor(Math.random() * 20);
+ col.charIndex = Math.floor(Math.random() * CHARS.length);
+ col.color = Math.random() < 0.6 ? 'accent2' : 'accent';
+ }
+ }
+
+ requestAnimationFrame(drawFrame);
+ }
+
+ sampleColors();
+ resizeCanvas();
+ setupThemeObserver();
+
+ var resizeTimer;
+ window.addEventListener('resize', function() {
+ clearTimeout(resizeTimer);
+ resizeTimer = setTimeout(resizeCanvas, 150);
+ });
+
+ document.fonts.ready.then(function() {
+ requestAnimationFrame(drawFrame);
+ });
+})();
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..2e61988
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,49 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## What this repo is
+
+Web frontend for `packages.danix.xyz` — an Apache autoindex-based Slackware package repository. Apache serves directory listings; this repo provides the HTML chrome injected around them.
+
+## How header/footer injection works
+
+Apache's `HeaderName` / `ReadmeName` directives (in `.htaccess`) point to `_header.html` and `_footer.html`. These static files are generated by `gen_web_hook.sh`, which walks `$PKGREPO/{category}/{package}/` and writes a pair at each level. Run standalone or as a slackrepo `HOOK_FINISH` hook.
+
+## Repository directory structure (on server)
+
+```
+/var/www/pkgs/ ← REPO_ROOT (depth 0)
+ category/ ← depth 1
+ pkgname/ ← depth 2
+ pkgname-ver.txz
+ pkgname-ver.txt ← parsed for title/description/Homepage
+ pkgname-ver.meta ← parsed for compressed/uncompressed sizes
+```
+
+## CSS design system
+
+CSS variables defined on `:root`:
+
+- Colors: `--bg`, `--bg-card`, `--bg-hover`, `--border`, `--accent`, `--accent-dim`, `--accent2`, `--green`, `--text`, `--text-dim`, `--text-head`
+- Fonts: `--mono` (IBM Plex Mono), `--sans` (IBM Plex Sans) — loaded from Google Fonts
+
+## Static assets
+
+`.assets/matrix-rain.js` — matrix rain canvas animation. Served from `$PKGREPO/.assets/`. Loaded via `<script defer>` in every `_header.html`. Hidden from Apache autoindex via `IndexIgnore .assets` in `.htaccess`. When updating, copy the adapted file to `$PKGREPO/.assets/matrix-rain.js` on the server — it is not regenerated by `gen_web_hook.sh`.
+
+## Apache configuration
+
+- `vhost.conf`: VirtualHost for `packages.danix.xyz:443`, DocumentRoot `/var/www/pkgs`, delegates all index config to `.htaccess`
+- `htaccess`: `IndexOptions HTMLTable SuppressHTMLPreamble ...` — `SuppressHTMLPreamble` is critical; it tells Apache not to emit its own `<html>` so our header file controls the full document
+
+## Running the hook
+
+```bash
+# Standalone against a local repo path
+bash gen_web_hook.sh /path/to/pkgrepo
+
+# Via slackrepo (picks up SR_PKGREPO automatically)
+HOOK_FINISH=/path/to/gen_web_hook.sh slackrepo build pkgname
+```
+
diff --git a/docs/superpowers/plans/2026-05-11-matrix-rain.md b/docs/superpowers/plans/2026-05-11-matrix-rain.md
new file mode 100644
index 0000000..e7b38e1
--- /dev/null
+++ b/docs/superpowers/plans/2026-05-11-matrix-rain.md
@@ -0,0 +1,373 @@
+# Matrix Rain Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Add matrix rain canvas animation to `packages.danix.xyz` headers, using the `danix2-hugo-theme` engine adapted for the header-scoped layout.
+
+**Architecture:** A single JS file at `.assets/matrix-rain.js` is served from the repo root and loaded via `<script defer>` in every generated `_header.html`. The script appends a canvas + gradient fade div to `.site-header` at runtime. CSS vars `--accent`, `--accent2`, `--bg` drive colors — no hardcoded values.
+
+**Tech Stack:** Vanilla JS (ES5-compatible IIFE), HTML Canvas API, Apache `.htaccess`, bash (`gen_web_hook.sh`)
+
+---
+
+### Task 1: Add `--accent2` CSS variable and `position:relative` to `.site-header`
+
+**Files:**
+- Modify: `gen_web_hook.sh` (CSS heredoc and `.site-header` rule)
+
+- [ ] **Step 1: Add `--accent2` to the `:root` block in the `CSS=` heredoc**
+
+In `gen_web_hook.sh`, find the `:root {` block inside the `CSS='<style>` heredoc (around line 37). Add `--accent2` after `--green`:
+
+```bash
+# Before:
+ --green: #4ec97b;
+
+# After:
+ --green: #4ec97b;
+ --accent2: #4ec97b;
+```
+
+- [ ] **Step 2: Add `position: relative` to `.site-header`**
+
+In the same `CSS=` heredoc, find the `.site-header {` rule (around line 56). Add `position: relative;`:
+
+```css
+.site-header { border-bottom: 1px solid var(--border); padding: 2rem 2.5rem 1.5rem; background: var(--bg-card); position: relative; overflow: hidden; }
+```
+
+(`overflow: hidden` clips the canvas to the header bounds.)
+
+- [ ] **Step 3: Verify the heredoc is syntactically intact**
+
+```bash
+bash -n gen_web_hook.sh
+```
+
+Expected: no output (no syntax errors).
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add gen_web_hook.sh
+git commit -m "style: add --accent2 CSS var and position:relative to .site-header"
+```
+
+---
+
+### Task 2: Create `.assets/matrix-rain.js`
+
+**Files:**
+- Create: `.assets/matrix-rain.js`
+
+This is the danix2 engine with canvas targeting changed from `#matrix-rain` (full-screen) to a dynamically created canvas appended to `.site-header` (65% width, right-aligned, with gradient fade div).
+
+- [ ] **Step 1: Create the `.assets/` directory and write the file**
+
+```bash
+mkdir -p .assets
+```
+
+Write `.assets/matrix-rain.js` with the following content (danix2 engine, header-scoped):
+
+```javascript
+// Matrix rain background effect — header-scoped build for packages.danix.xyz
+// Derived from danix2-hugo-theme/assets/js/matrix-rain.js
+(function() {
+ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
+
+ var header = document.querySelector('.site-header');
+ if (!header) return;
+
+ // Canvas
+ var canvas = document.createElement('canvas');
+ canvas.id = 'matrix-rain';
+ canvas.style.cssText = 'position:absolute;top:0;right:0;height:100%;pointer-events:none;z-index:0;';
+ header.appendChild(canvas);
+
+ // Gradient fade div — covers left 75% of canvas area, left→transparent
+ var fade = document.createElement('div');
+ fade.style.cssText = 'position:absolute;top:0;right:0;bottom:0;width:' + Math.floor(0.65 * 100) + '%;background:linear-gradient(to right,var(--bg-card) 0%,transparent 60%);pointer-events:none;z-index:1;';
+ header.appendChild(fade);
+
+ var ctx = canvas.getContext('2d');
+
+ var columns = [];
+ var frameCount = 0;
+ var colors = { accent: '#5c9cf5', accent2: '#4ec97b', bg: '#161b25', head: '#ffffff' };
+
+ var ASCII = Array.from({ length: 94 }, function(_, i) { return String.fromCharCode(33 + i); });
+ var KATA = Array.from({ length: 96 }, function(_, i) { return String.fromCodePoint(0x30a0 + i); });
+ var CHARS = shuffle([].concat(ASCII, KATA, KATA, KATA));
+
+ function shuffle(arr) {
+ for (var i = arr.length - 1; i > 0; i--) {
+ var j = Math.floor(Math.random() * (i + 1));
+ var tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp;
+ }
+ return arr;
+ }
+
+ function hexToRgba(color, alpha) {
+ var rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
+ if (rgbMatch) return 'rgba(' + rgbMatch[1] + ',' + rgbMatch[2] + ',' + rgbMatch[3] + ',' + alpha + ')';
+ var hex = color.replace('#', '');
+ var r = parseInt(hex.substring(0, 2), 16);
+ var g = parseInt(hex.substring(2, 4), 16);
+ var b = parseInt(hex.substring(4, 6), 16);
+ return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')';
+ }
+
+ function sampleColors() {
+ var style = getComputedStyle(document.documentElement);
+ var isDark = !document.documentElement.classList.contains('theme-light');
+ colors.accent = style.getPropertyValue('--accent').trim() || '#5c9cf5';
+ colors.accent2 = style.getPropertyValue('--accent2').trim() || '#4ec97b';
+ colors.bg = style.getPropertyValue('--bg-card').trim() || '#161b25';
+ colors.head = isDark ? '#ffffff' : '#1a0533';
+ }
+
+ function resizeCanvas() {
+ canvas.width = Math.floor(header.offsetWidth * 0.65);
+ canvas.height = header.offsetHeight;
+ ctx.font = '14px "IBM Plex Mono", monospace';
+ ctx.textBaseline = 'top';
+ initColumns();
+ }
+
+ function initColumns() {
+ columns = [];
+ var columnWidth = 14;
+ var columnCount = Math.floor(canvas.width / columnWidth);
+ for (var i = 0; i < columnCount; i++) {
+ columns.push({
+ x: i * columnWidth,
+ y: -Math.floor(Math.random() * 40),
+ speed: 2 + Math.floor(Math.random() * 3),
+ color: Math.random() < 0.6 ? 'accent2' : 'accent',
+ charIndex: Math.floor(Math.random() * CHARS.length),
+ length: 8 + Math.floor(Math.random() * 13)
+ });
+ }
+ }
+
+ function setupThemeObserver() {
+ var observer = new MutationObserver(function(mutations) {
+ for (var i = 0; i < mutations.length; i++) {
+ if (mutations[i].attributeName === 'class') { sampleColors(); break; }
+ }
+ });
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
+ }
+
+ function drawFrame() {
+ frameCount++;
+ ctx.fillStyle = hexToRgba(colors.bg, 0.085);
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ for (var ci = 0; ci < columns.length; ci++) {
+ var col = columns[ci];
+ if (frameCount % col.speed !== 0) continue;
+
+ ctx.fillStyle = colors[col.color];
+ for (var i = 1; i <= col.length; i++) {
+ var trailY = (col.y - i) * 14;
+ if (trailY < 0) continue;
+ var trailCharIndex = (col.charIndex - i + CHARS.length) % CHARS.length;
+ ctx.fillText(CHARS[trailCharIndex], col.x, trailY);
+ }
+
+ ctx.fillStyle = colors.head;
+ ctx.fillText(CHARS[col.charIndex % CHARS.length], col.x, col.y * 14);
+
+ col.y++;
+ col.charIndex = (col.charIndex + 1) % CHARS.length;
+
+ if (col.y * 14 > canvas.height + col.length * 14) {
+ col.y = -Math.floor(Math.random() * 20);
+ col.charIndex = Math.floor(Math.random() * CHARS.length);
+ col.color = Math.random() < 0.6 ? 'accent2' : 'accent';
+ }
+ }
+
+ requestAnimationFrame(drawFrame);
+ }
+
+ sampleColors();
+ resizeCanvas();
+ setupThemeObserver();
+
+ var resizeTimer;
+ window.addEventListener('resize', function() {
+ clearTimeout(resizeTimer);
+ resizeTimer = setTimeout(resizeCanvas, 150);
+ });
+
+ document.fonts.ready.then(function() {
+ requestAnimationFrame(drawFrame);
+ });
+})();
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add .assets/matrix-rain.js
+git commit -m "feat: add matrix-rain.js (danix2 engine, header-scoped)"
+```
+
+---
+
+### Task 3: Wire `<script>` tag into `write_header()` in `gen_web_hook.sh`
+
+**Files:**
+- Modify: `gen_web_hook.sh` (`write_header()` function)
+
+- [ ] **Step 1: Add the `<script>` tag to `write_header()`**
+
+In `gen_web_hook.sh`, find `write_header()` (around line 241). Inside the `cat > "$dir/_header.html" << EOF` block, add the script tag in `<head>`, after the Google Fonts `<link>` and before `${CSS}`:
+
+```bash
+ <script src="/.assets/matrix-rain.js" defer></script>
+ ${CSS}
+```
+
+Full `<head>` block after change:
+
+```bash
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>danix Slackware Repository</title>
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=IBM+Plex+Sans:wght@300;400;600&display=swap" rel="stylesheet">
+ <script src="/.assets/matrix-rain.js" defer></script>
+ ${CSS}
+</head>
+```
+
+- [ ] **Step 2: Verify syntax**
+
+```bash
+bash -n gen_web_hook.sh
+```
+
+Expected: no output.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add gen_web_hook.sh
+git commit -m "feat: load matrix-rain.js in generated _header.html"
+```
+
+---
+
+### Task 4: Hide `.assets/` from Apache directory listing
+
+**Files:**
+- Modify: `htaccess`
+
+- [ ] **Step 1: Add `IndexIgnore .assets` to `htaccess`**
+
+In `htaccess`, find the `IndexIgnore` line (around line 15). Append `.assets` to it:
+
+```apache
+IndexIgnore _header.html _footer.html .htaccess .htpasswd \
+ CHECKSUMS.md5 CHECKSUMS.md5.asc CHECKSUMS.md5.gz CHECKSUMS.md5.gz.asc \
+ FILELIST.TXT MANIFEST.bz2 \
+ PACKAGES.TXT PACKAGES.TXT.gz \
+ ChangeLog.txt.gz GPG-KEY \
+ .assets
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add htaccess
+git commit -m "chore: hide .assets dir from Apache autoindex"
+```
+
+---
+
+### Task 5: Update CLAUDE.md
+
+**Files:**
+- Modify: `CLAUDE.md`
+
+- [ ] **Step 1: Add `.assets/` and `--accent2` notes to CLAUDE.md**
+
+In the **CSS design system** section, update the CSS variables list to include `--accent2`:
+
+```markdown
+- Colors: `--bg`, `--bg-card`, `--bg-hover`, `--border`, `--accent`, `--accent-dim`, `--accent2`, `--green`, `--text`, `--text-dim`, `--text-head`
+```
+
+Add a new **Static assets** section after the CSS section:
+
+```markdown
+## Static assets
+
+`.assets/matrix-rain.js` — matrix rain canvas animation. Served from `$PKGREPO/.assets/`. Loaded via `<script defer>` in every `_header.html`. Hidden from Apache autoindex via `IndexIgnore .assets` in `.htaccess`. When updating, copy the adapted file to `$PKGREPO/.assets/matrix-rain.js` on the server — it is not regenerated by `gen_web_hook.sh`.
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add CLAUDE.md
+git commit -m "docs: document .assets/ dir and --accent2 var in CLAUDE.md"
+```
+
+---
+
+### Task 6: Smoke-test the generated output
+
+**Files:** none (verification only)
+
+- [ ] **Step 1: Run `gen_web_hook.sh` against a test directory**
+
+```bash
+mkdir -p /tmp/test-pkgrepo/audio/ffmpeg
+echo "ffmpeg: FFmpeg multimedia framework" > /tmp/test-pkgrepo/audio/ffmpeg/ffmpeg-6.1-x86_64-1.txt
+bash gen_web_hook.sh /tmp/test-pkgrepo
+```
+
+Expected output:
+```
+gen_web_hook: Generating static web files in /tmp/test-pkgrepo ...
+gen_web_hook: Written: /tmp/test-pkgrepo/_header.html
+gen_web_hook: Written: /tmp/test-pkgrepo/_footer.html
+gen_web_hook: Written: /tmp/test-pkgrepo/audio/_header.html
+gen_web_hook: Written: /tmp/test-pkgrepo/audio/_footer.html
+gen_web_hook: Written: /tmp/test-pkgrepo/audio/ffmpeg/_header.html
+gen_web_hook: Written: /tmp/test-pkgrepo/audio/ffmpeg/_footer.html
+gen_web_hook: Done.
+```
+
+- [ ] **Step 2: Verify `_header.html` contains expected elements**
+
+```bash
+grep -c 'matrix-rain.js' /tmp/test-pkgrepo/_header.html
+grep -c '\-\-accent2' /tmp/test-pkgrepo/_header.html
+grep -c 'position: relative' /tmp/test-pkgrepo/_header.html
+grep -c 'overflow: hidden' /tmp/test-pkgrepo/_header.html
+```
+
+Expected: each command prints `1`.
+
+- [ ] **Step 3: Validate HTML structure**
+
+```bash
+grep -A2 'matrix-rain' /tmp/test-pkgrepo/_header.html
+```
+
+Expected output contains:
+```html
+<script src="/.assets/matrix-rain.js" defer></script>
+```
+
+- [ ] **Step 4: Clean up**
+
+```bash
+rm -rf /tmp/test-pkgrepo
+```
diff --git a/docs/superpowers/specs/2026-05-11-matrix-rain-design.md b/docs/superpowers/specs/2026-05-11-matrix-rain-design.md
new file mode 100644
index 0000000..17d8401
--- /dev/null
+++ b/docs/superpowers/specs/2026-05-11-matrix-rain-design.md
@@ -0,0 +1,71 @@
+# Matrix Rain — Design Spec
+**Date:** 2026-05-11
+**Repo:** repo-html-structure (`packages.danix.xyz`)
+
+## Goal
+
+Add the matrix rain canvas animation to the `_header.html` of the Slackware package repository, matching the implementation in `danix2-hugo-theme` as closely as possible for brand consistency.
+
+## Architecture
+
+### New file: `.assets/matrix-rain.js`
+
+Served from `$PKGREPO/.assets/matrix-rain.js` (i.e. `https://packages.danix.xyz/.assets/matrix-rain.js`).
+
+Source: `danix2-hugo-theme/assets/js/matrix-rain.js` with minimal adaptations (see below). No logic changes — rendering engine, trail model, CSS var integration, font-ready wait, debounced resize, MutationObserver for theme switching all carried over verbatim.
+
+**Adaptations from danix2 version:**
+
+| danix2 | This repo |
+|--------|-----------|
+| Canvas = `#matrix-rain`, full window width/height | Canvas appended to `.site-header`, width = 65% of header width, height = header height |
+| No fade overlay | Gradient fade `<div>` over left 75% of canvas (matches cgit approach) |
+| `canvas.width = window.innerWidth` | `canvas.width = Math.floor(header.offsetWidth * 0.65)` |
+| `canvas.height = window.innerHeight` | `canvas.height = header.offsetHeight` |
+| Resize: `resizeCanvas()` uses `window.innerWidth/Height` | Resize: recalculate from `header.offsetWidth/Height` |
+| Canvas positioned full-screen fixed | Canvas `position:absolute; top:0; right:0; height:100%; pointer-events:none; z-index:0` |
+
+Everything else (character set, trail rendering, color sampling, `hexToRgba`, `sampleColors`, `MutationObserver`, `document.fonts.ready`, debounced resize) is identical.
+
+### CSS variable addition: `--accent2`
+
+Add to `:root` in the `CSS=` heredoc in `gen_web_hook.sh`:
+
+```css
+--accent2: #4ec97b;
+```
+
+This matches the existing `--green` value and aligns the var name with danix2-hugo-theme.
+
+The script references `--accent2` (green rain) and `--accent` (purple rain) — both already present in this repo's palette.
+
+### Header structure change (in `gen_web_hook.sh` → `write_header()`)
+
+Add `position: relative` to `.site-header` so the absolutely-positioned canvas is contained within it.
+
+Add `<script src="/.assets/matrix-rain.js" defer></script>` to the `<head>` of every generated `_header.html`.
+
+The canvas and fade `<div>` are injected by the JS itself (matching cgit pattern) — no HTML changes needed beyond the `<script>` tag.
+
+### Apache: hide `.assets/` from directory listing
+
+Add to `.htaccess`:
+
+```apache
+IndexIgnore .assets
+```
+
+## File changes summary
+
+| File | Change |
+|------|--------|
+| `gen_web_hook.sh` | Add `--accent2` CSS var; add `position:relative` to `.site-header`; add `<script>` tag in `write_header()` |
+| `.assets/matrix-rain.js` | New file — danix2 engine adapted for header-scoped canvas |
+| `.htaccess` | Add `IndexIgnore .assets` |
+| `CLAUDE.md` | Note `.assets/` directory and CSS var addition |
+
+## Out of scope
+
+- Light theme CSS (no light theme exists yet; MutationObserver in the script already handles it when added)
+- Any changes to `_footer.html` generation
+- Feather icons or any other JS (cgit-specific, not needed here)
diff --git a/gen_web_hook.sh b/gen_web_hook.sh
new file mode 100644
index 0000000..050b89d
--- /dev/null
+++ b/gen_web_hook.sh
@@ -0,0 +1,342 @@
+#!/bin/bash
+# gen_web_hook.sh — generates static HTML header/footer files for the
+# Apache autoindex listing of the danix Slackware package repository.
+#
+# Can be called standalone or from slackrepo's HOOK_FINISH.
+# Usage: gen_web_hook.sh [/path/to/pkgrepo]
+#
+# If no argument is given, uses $SR_PKGREPO if set, otherwise /repo.
+
+set -euo pipefail
+
+PKGREPO="${1:-${SR_PKGREPO:-/repo}}"
+
+log() { echo "gen_web_hook: $*"; }
+warn() { echo "gen_web_hook: WARNING: $*" >&2; }
+
+if [ ! -d "$PKGREPO" ]; then
+ warn "PKGREPO '$PKGREPO' not found."
+ exit 1
+fi
+
+log "Generating static web files in $PKGREPO ..."
+
+# ── HTML escape ───────────────────────────────────────────────────────────────
+html_escape() {
+ local s="$1"
+ s="${s//&/&amp;}"
+ s="${s//</&lt;}"
+ s="${s//>/&gt;}"
+ s="${s//\"/&quot;}"
+ printf '%s' "$s"
+}
+
+# ── Shared CSS ────────────────────────────────────────────────────────────────
+CSS='<style>
+:root {
+ --bg: #0e1117;
+ --bg-card: #161b25;
+ --bg-hover: #1e2535;
+ --border: #2a3147;
+ --accent: #5c9cf5;
+ --accent-dim:#3a5f99;
+ --green: #4ec97b;
+ --accent2: #4ec97b;
+ --text: #c9d1e0;
+ --text-dim: #6b7a99;
+ --text-head: #e8edf7;
+ --mono: "IBM Plex Mono", monospace;
+ --sans: "IBM Plex Sans", sans-serif;
+}
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+body {
+ background: var(--bg); color: var(--text); font-family: var(--sans);
+ font-size: 15px; line-height: 1.6; min-height: 100vh;
+ display: flex; flex-direction: column;
+}
+.site-header { border-bottom: 1px solid var(--border); padding: 2rem 2.5rem 1.5rem; background: var(--bg-card); position: relative; overflow: hidden; }
+.header-top { display: flex; align-items: baseline; gap: 1rem; flex-wrap: wrap; margin-bottom: 0.5rem; }
+.site-title { font-family: var(--mono); font-size: 1.35rem; font-weight: 600; color: var(--text-head); }
+.site-title span { color: var(--accent); }
+.site-subtitle { font-size: 0.8rem; color: var(--text-dim); font-family: var(--mono); }
+.header-desc { font-size: 0.9rem; color: var(--text-dim); max-width: 65ch; margin-bottom: 1rem; }
+.header-links { display: flex; gap: 1.25rem; align-items: center; flex-wrap: wrap; }
+.header-links a {
+ font-family: var(--mono); font-size: 0.8rem; color: var(--accent); text-decoration: none;
+ border: 1px solid var(--accent-dim); padding: 0.2rem 0.65rem; border-radius: 3px;
+}
+.header-links a:hover { background: var(--accent); color: var(--bg); }
+pre { font-family: var(--mono) !important; color: var(--text) !important; background: transparent !important; padding: 1.5rem 2.5rem !important; flex: 1; }
+table { width: calc(100% - 5rem); margin: 1.5rem 2.5rem; border-collapse: collapse; font-family: var(--mono); font-size: 0.85rem; }
+th { text-align: left; color: var(--text-dim); font-weight: 600; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.08em; padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--border); }
+td { padding: 0.4rem 0.75rem; border-bottom: 1px solid rgba(42,49,71,0.5); vertical-align: middle; }
+tr:hover td { background: var(--bg-hover); }
+td a, td a:visited { color: var(--text-head); text-decoration: none; }
+td a[href$="/"]:not([href="../"]) { color: var(--accent); font-weight: 600; }
+td a:hover { color: var(--accent); text-decoration: underline; }
+td:nth-child(3), td:nth-child(4) { color: var(--text-dim); font-size: 0.8rem; }
+td a[href$=".txz"] { color: var(--green); font-weight: 600; }
+.autoindex-wrapper { flex: 1; display: flex; flex-direction: column; }
+hr { display: none; }
+address { display: none; }
+.pkg-info { margin: 0 2.5rem 1.5rem; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; background: var(--bg-card); }
+.pkg-info-header { display: flex; align-items: baseline; gap: 0.75rem; padding: 0.6rem 1rem; background: rgba(92,156,245,0.06); border-bottom: 1px solid var(--border); }
+.pkg-info-label { font-family: var(--mono); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-dim); }
+.pkg-info-name { font-family: var(--mono); font-size: 0.85rem; font-weight: 600; color: var(--accent); }
+.pkg-info-body { padding: 0.75rem 1rem; font-size: 0.875rem; line-height: 1.65; color: var(--text); }
+.pkg-info-body p { margin-bottom: 0.2rem; }
+.pkg-title { font-weight: 600; color: var(--text-head); margin-bottom: 0.5rem !important; }
+.pkg-meta { display: flex; gap: 1.5rem; flex-wrap: wrap; padding: 0.5rem 1rem; border-top: 1px solid var(--border); font-family: var(--mono); font-size: 0.75rem; }
+.pkg-meta em { font-style: normal; margin-right: 0.3rem; color: var(--text-dim); }
+.pkg-meta span { color: var(--text); }
+.pkg-homepage { padding: 0.4rem 1rem 0.6rem; font-family: var(--mono); font-size: 0.78rem; border-top: 1px solid var(--border); }
+.pkg-homepage a { color: var(--accent); text-decoration: none; }
+.pkg-homepage a:hover { text-decoration: underline; }
+.site-footer { border-top: 1px solid var(--border); background: var(--bg-card); padding: 1rem 2.5rem; margin-top: auto; }
+.footer-inner { display: flex; flex-direction: column; gap: 0.35rem; }
+.footer-meta { display: flex; align-items: center; gap: 1rem; margin-bottom: 0.2rem; }
+.footer-updated { font-family: var(--mono); font-size: 0.8rem; color: var(--text-dim); }
+.footer-date { color: var(--green); font-weight: 600; }
+.footer-rss { display: inline-flex; align-items: center; gap: 0.3rem; font-family: var(--mono); font-size: 0.75rem; color: #e8923a; text-decoration: none; border: 1px solid rgba(232,146,58,0.3); padding: 0.15rem 0.5rem; border-radius: 3px; }
+.footer-rss:hover { background: rgba(232,146,58,0.1); }
+.footer-sig { font-family: var(--mono); font-size: 0.75rem; color: var(--text-dim); display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
+.footer-sig a { color: var(--accent); text-decoration: none; }
+.footer-sep { opacity: 0.3; }
+.category-pill { position: absolute; top: 50%; right: 2.5rem; transform: translateY(-50%); z-index: 2; background: var(--bg-card); border: 1px solid var(--border); border-radius: 999px; padding: 0.4rem 1.25rem; font-family: var(--mono); font-size: 1.4rem; font-weight: 700; letter-spacing: 0.02em; text-transform: capitalize; }
+.category-pill span { background: linear-gradient(135deg, var(--accent) 0%, var(--accent2) 100%); -webkit-background-clip: text; background-clip: text; color: transparent; }
+</style>'
+
+# ── Shared footer signature ───────────────────────────────────────────────────
+footer_sig() {
+ cat << 'EOF'
+<footer class="site-footer">
+ <div class="footer-inner">
+ <div class="footer-sig">
+ <span>danix packages &middot; Slackware64-current</span>
+ <span class="footer-sep">&middot;</span>
+ <a href="https://danix.xyz/is/here/">Contact danix</a>
+ <span class="footer-sep">&middot;</span>
+ <span>GPG signed &middot; use at your own risk</span>
+ </div>
+ </div>
+</footer>
+</body>
+</html>
+EOF
+}
+
+# ── Generate root _footer.html (last-updated + signature) ────────────────────
+generate_root_footer() {
+ local last_updated='unknown'
+ if [ -f "$PKGREPO/ChangeLog.txt" ]; then
+ last_updated=$(grep -m1 -E '^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)' "$PKGREPO/ChangeLog.txt" || echo 'unknown')
+ fi
+
+ {
+ cat << EOF
+</div><!-- .autoindex-wrapper -->
+<footer class="site-footer">
+ <div class="footer-inner">
+ <div class="footer-meta">
+ <span class="footer-updated">Last updated:
+ <span class="footer-date">$(html_escape "$last_updated")</span>
+ </span>
+ <a class="footer-rss" href="/ChangeLog.rss">
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
+ <circle cx="5" cy="19" r="3"/>
+ <path d="M4 4a16 16 0 0 1 16 16h-3A13 13 0 0 0 4 7z"/>
+ <path d="M4 11a9 9 0 0 1 9 9H10a6 6 0 0 0-6-6z"/>
+ </svg>
+ RSS feed
+ </a>
+ </div>
+ <div class="footer-sig">
+ <span>danix packages &middot; Slackware64-current</span>
+ <span class="footer-sep">&middot;</span>
+ <a href="https://danix.xyz/is/here/">Contact danix</a>
+ <span class="footer-sep">&middot;</span>
+ <span>GPG signed &middot; use at your own risk</span>
+ </div>
+ </div>
+</footer>
+</body>
+</html>
+EOF
+ } > "$PKGREPO/_footer.html"
+ log "Written: $PKGREPO/_footer.html"
+}
+
+# ── Generate per-package _footer.html ────────────────────────────────────────
+generate_package_footer() {
+ local pkg_dir="$1"
+ local pkg_name
+ pkg_name=$(basename "$pkg_dir")
+
+ local txt_file
+ txt_file=$(find "$pkg_dir" -maxdepth 1 -name '*.txt' | head -1)
+
+ if [ -z "$txt_file" ]; then
+ # No .txt file — minimal footer
+ { echo '</div><!-- .autoindex-wrapper -->'; footer_sig; } > "$pkg_dir/_footer.html"
+ return
+ fi
+
+ # Parse .txt
+ local title='' homepage='' body_lines=()
+ while IFS= read -r line; do
+ # Strip "pkgname:" or "pkgname: " prefix
+ local text
+ text=$(echo "$line" | sed 's/^[^:]*: \?//')
+ # Skip lines that were just "pkgname:" with nothing after
+ [[ "$line" == *: ]] && text=''
+ if [[ "$text" == Homepage:* ]]; then
+ homepage="${text#Homepage: }"
+ continue
+ fi
+ if [ -z "$title" ]; then
+ [ -n "$text" ] && title="$text"
+ else
+ [ -n "$text" ] && body_lines+=("$text")
+ fi
+ done < "$txt_file"
+
+ # Parse .meta for sizes
+ local meta_file size_c='' size_u=''
+ meta_file=$(find "$pkg_dir" -maxdepth 1 -name '*.meta' | head -1)
+ if [ -n "$meta_file" ]; then
+ size_c=$(grep 'PACKAGE SIZE (compressed):' "$meta_file" | sed 's/.*: *//')
+ size_u=$(grep 'PACKAGE SIZE (uncompressed):' "$meta_file" | sed 's/.*: *//')
+ fi
+
+ {
+ echo '</div><!-- .autoindex-wrapper -->'
+ echo '<section class="pkg-info">'
+ echo ' <div class="pkg-info-header">'
+ echo ' <span class="pkg-info-label">Package description</span>'
+ echo " <span class=\"pkg-info-name\">$(html_escape "$pkg_name")</span>"
+ echo ' </div>'
+ echo ' <div class="pkg-info-body">'
+ echo " <p class=\"pkg-title\">$(html_escape "$title")</p>"
+ for bline in "${body_lines[@]}"; do
+ echo " <p>$(html_escape "$bline")</p>"
+ done
+ echo ' </div>'
+
+ if [ -n "$size_c" ] || [ -n "$size_u" ]; then
+ echo ' <div class="pkg-meta">'
+ [ -n "$size_c" ] && echo " <span><em>Size (compressed):</em> $(html_escape "$size_c")</span>"
+ [ -n "$size_u" ] && echo " <span><em>Size (uncompressed):</em> $(html_escape "$size_u")</span>"
+ echo ' </div>'
+ fi
+
+ if [ -n "$homepage" ]; then
+ echo " <div class=\"pkg-homepage\"><a href=\"$(html_escape "$homepage")\" rel=\"noopener noreferrer\">$(html_escape "$homepage")</a></div>"
+ fi
+
+ echo '</section>'
+ footer_sig
+ } > "$pkg_dir/_footer.html"
+
+ log "Written: $pkg_dir/_footer.html"
+}
+
+# ── Write _header.html to a given directory ───────────────────────────────────
+write_header() {
+ local dir="$1"
+ cat > "$dir/_header.html" << EOF
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>danix Slackware Repository</title>
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=IBM+Plex+Sans:wght@300;400;600&display=swap" rel="stylesheet">
+ <script src="/.assets/matrix-rain.js" defer></script>
+ ${CSS}
+</head>
+<body>
+<header class="site-header">
+ <div class="header-top">
+ <div class="site-title"><span>//</span> <a href="https://packages.danix.xyz" style="color:var(--accent)">danix packages</a></div>
+ <div class="site-subtitle">Slackware64-current &middot; unofficial repository</div>
+ </div>
+ <p class="header-desc">
+ Third-party Slackware packages built with
+ <a href="https://github.com/aclemons/slackrepo" style="color:var(--accent)">slackrepo</a>
+ on Slackware64-current. Use at your own risk. All packages are signed with my GPG key.
+ </p>
+ <div class="header-links">
+ <a href="https://danix.xyz/is/here/">Contact me</a>
+ <a href="/ChangeLog.txt">ChangeLog</a>
+ <a href="/ChangeLog.rss">RSS</a>
+ </div>
+</header>
+<div class="autoindex-wrapper">
+EOF
+ log "Written: $dir/_header.html"
+}
+
+# ── Write _header.html for a category directory (includes pill) ───────────────
+write_category_header() {
+ local dir="$1"
+ local cat_name
+ cat_name=$(basename "$dir")
+ cat > "$dir/_header.html" << EOF
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>danix Slackware Repository</title>
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=IBM+Plex+Sans:wght@300;400;600&display=swap" rel="stylesheet">
+ <script src="/.assets/matrix-rain.js" defer></script>
+ ${CSS}
+</head>
+<body>
+<header class="site-header">
+ <div class="header-top">
+ <div class="site-title"><span>//</span> <a href="https://packages.danix.xyz" style="color:var(--accent)">danix packages</a></div>
+ <div class="site-subtitle">Slackware64-current &middot; unofficial repository</div>
+ </div>
+ <p class="header-desc">
+ Third-party Slackware packages built with
+ <a href="https://github.com/aclemons/slackrepo" style="color:var(--accent)">slackrepo</a>
+ on Slackware64-current. Use at your own risk. All packages are signed with my GPG key.
+ </p>
+ <div class="header-links">
+ <a href="https://danix.xyz/is/here/">Contact me</a>
+ <a href="/ChangeLog.txt">ChangeLog</a>
+ <a href="/ChangeLog.rss">RSS</a>
+ </div>
+ <div class="category-pill"><span>$(html_escape "$cat_name")</span></div>
+</header>
+<div class="autoindex-wrapper">
+EOF
+ log "Written: $dir/_header.html"
+}
+
+# ── Generate category _footer.html (just the signature) ──────────────────────
+generate_category_footer() {
+ local cat_dir="$1"
+ { echo '</div><!-- .autoindex-wrapper -->'; footer_sig; } > "$cat_dir/_footer.html"
+ log "Written: $cat_dir/_footer.html"
+}
+
+# ── Main ──────────────────────────────────────────────────────────────────────
+write_header "$PKGREPO"
+generate_root_footer
+
+for category in "$PKGREPO"/*/; do
+ [ -d "$category" ] || continue
+ write_category_header "$category"
+ generate_category_footer "$category"
+ for pkg in "$category"*/; do
+ [ -d "$pkg" ] || continue
+ write_header "$pkg"
+ generate_package_footer "$pkg"
+ done
+done
+
+log "Done."
diff --git a/htaccess b/htaccess
new file mode 100644
index 0000000..bb92f15
--- /dev/null
+++ b/htaccess
@@ -0,0 +1,43 @@
+# danix Slackware package repository
+
+Options +Indexes
+
+# ── Autoindex options ─────────────────────────────────────────────────────────
+IndexOptions FancyIndexing HTMLTable IgnoreCase SuppressDescription SuppressHTMLPreamble NameWidth=* FoldersFirst ScanHTMLTitles
+
+IndexOrderDefault Ascending Name
+
+# ── Header / Footer (static HTML, generated by gen_web_hook) ─────────────────
+HeaderName _header.html
+ReadmeName _footer.html
+
+# ── Hide internal files from the listing ─────────────────────────────────────
+IndexIgnore _header.html _footer.html .htaccess .htpasswd \
+ CHECKSUMS.md5 CHECKSUMS.md5.asc CHECKSUMS.md5.gz CHECKSUMS.md5.gz.asc \
+ FILELIST.TXT MANIFEST.bz2 \
+ PACKAGES.TXT PACKAGES.TXT.gz \
+ ChangeLog.txt.gz GPG-KEY \
+ .assets
+
+# ── Icons ─────────────────────────────────────────────────────────────────────
+AddIconByType (DIR,/icons-pkg/dir.svg) httpd/unix-directory
+AddIcon /icons-pkg/dir.svg ^^DIRECTORY^^
+AddIcon /icons-pkg/package.svg .txz
+AddIcon /icons-pkg/signature.svg .asc
+AddIcon /icons-pkg/checksum.svg .md5 .sha256
+AddIcon /icons-pkg/compressed.svg .gz .bz2
+AddIcon /icons-pkg/rss.svg .rss
+AddIcon /icons-pkg/file.svg .txt .lst .meta .dep
+DefaultIcon /icons-pkg/file.svg
+
+# ── MIME types for Slackware-specific extensions ─────────────────────────────
+AddType application/octet-stream .txz .dep
+AddType text/plain .txt .asc .lst .meta .md5 .sha256
+
+# ── Cache control ─────────────────────────────────────────────────────────────
+<FilesMatch "\.(txz|dep)$">
+ Header set Cache-Control "public, max-age=86400"
+</FilesMatch>
+<FilesMatch "\.(txt|asc|lst|meta|md5|sha256)$">
+ Header set Cache-Control "public, max-age=3600"
+</FilesMatch>
diff --git a/vhost.conf b/vhost.conf
new file mode 100644
index 0000000..03fef31
--- /dev/null
+++ b/vhost.conf
@@ -0,0 +1,33 @@
+<VirtualHost YOUR_SERVER_IP:443>
+ ServerName YOUR_DOMAIN
+ DocumentRoot /path/to/pkgrepo
+
+ SSLEngine On
+
+ ErrorLog "/var/log/apache2/packages_error_log"
+ CustomLog "/var/log/apache2/packages_access_log" common
+
+ <Directory /path/to/pkgrepo>
+ Require all granted
+ Options +Indexes
+ AllowOverride All
+ </Directory>
+
+ # All IndexOptions, HeaderName, ReadmeName are managed in .htaccess
+
+ Include /etc/letsencrypt/options-ssl-apache.conf
+ SSLCertificateFile /etc/letsencrypt/live/YOUR_DOMAIN/fullchain.pem
+ SSLCertificateKeyFile /etc/letsencrypt/live/YOUR_DOMAIN/privkey.pem
+</VirtualHost>
+
+# ── Required Apache modules ───────────────────────────────────────────────────
+# Check what's active: apache2ctl -M | grep -E 'autoindex|headers|php|proxy'
+#
+# a2enmod autoindex <- directory listings
+# a2enmod headers <- Cache-Control headers in .htaccess
+#
+# PHP -- pick one:
+# mod_php: a2enmod php8.4
+# php-fpm: a2enmod proxy_fcgi setenvif && a2enconf php8.4-fpm
+#
+# systemctl reload apache2 \ No newline at end of file