summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-05-14 20:22:24 +0200
committerDanilo M. <danix@danix.xyz>2026-05-14 20:22:24 +0200
commite83e1959e8a6040c4526a6a3c573710b80732cd1 (patch)
tree50954a5207ee6be847a32adaa0739e89a43ad140
parentbe0a6b4a7657264b3088b69802450b1382a52392 (diff)
downloaddanixxyz-e83e1959e8a6040c4526a6a3c573710b80732cd1.tar.gz
danixxyz-e83e1959e8a6040c4526a6a3c573710b80732cd1.zip
docs: add implementation plan for packages shortcodes
9-task plan covering both Alpine.js shortcodes, i18n keys, JS loading, CORS setup, and CSS rebuild workflow. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--docs/superpowers/plans/2026-05-14-packages-shortcodes.md683
1 files changed, 683 insertions, 0 deletions
diff --git a/docs/superpowers/plans/2026-05-14-packages-shortcodes.md b/docs/superpowers/plans/2026-05-14-packages-shortcodes.md
new file mode 100644
index 0000000..16277f4
--- /dev/null
+++ b/docs/superpowers/plans/2026-05-14-packages-shortcodes.md
@@ -0,0 +1,683 @@
+# Packages Shortcodes 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 two Alpine.js shortcodes (`pkg-list` and `pkg-changelog`) to the theme that fetch live data from `packages.danix.xyz` and render a filterable package table and a changelog timeline on the repository page.
+
+**Architecture:** Each shortcode is a self-contained Alpine.js component registered via `alpine:init` in its own JS file (same pattern as `contact-form.js`). The shortcode HTML template renders the mount point; the JS file contains all fetch/parse/render logic. Hugo renders i18n strings into `data-*` attributes so JS can read them without knowing about Hugo.
+
+**Tech Stack:** Hugo (shortcode templates + i18n), Alpine.js 3.x, vanilla `fetch()`, Tailwind CSS utility classes already present in `main.min.css`.
+
+---
+
+## File Map
+
+| File | Action | Responsibility |
+|------|--------|---------------|
+| `themes/danix-xyz-hacker/layouts/shortcodes/pkg-list.html` | Create | Mount point + i18n data attrs for package table |
+| `themes/danix-xyz-hacker/layouts/shortcodes/pkg-changelog.html` | Create | Mount point + i18n data attrs for changelog timeline |
+| `themes/danix-xyz-hacker/assets/js/pkg-list.js` | Create | Alpine component: fetch PACKAGES.TXT, parse, filter, render |
+| `themes/danix-xyz-hacker/assets/js/pkg-changelog.js` | Create | Alpine component: fetch ChangeLog.txt, parse N entries, render timeline |
+| `themes/danix-xyz-hacker/i18n/en.yaml` | Modify | Add 6 new i18n keys |
+| `themes/danix-xyz-hacker/i18n/it.yaml` | Modify | Add 6 new i18n keys (Italian) |
+| `themes/danix-xyz-hacker/layouts/partials/head.html` | Modify | Conditionally load pkg-list.js and pkg-changelog.js |
+| `content/en/repository/index.md` | Modify | Add shortcode calls |
+| `content/it/repository/index.md` | Modify | Add shortcode calls |
+
+---
+
+## Task 1: Add i18n keys
+
+**Files:**
+- Modify: `themes/danix-xyz-hacker/i18n/en.yaml`
+- Modify: `themes/danix-xyz-hacker/i18n/it.yaml`
+
+- [ ] **Step 1: Add keys to en.yaml**
+
+Open `themes/danix-xyz-hacker/i18n/en.yaml` and append at the end:
+
+```yaml
+# Package repository shortcodes
+pkg_list_loading: "Loading packages..."
+pkg_list_error: "Could not load packages. Visit the repository directly."
+pkg_list_filter: "Filter packages..."
+pkg_list_link_label: "View"
+pkg_changelog_loading: "Loading changelog..."
+pkg_changelog_error: "Could not load changelog. Visit the repository directly."
+```
+
+- [ ] **Step 2: Add keys to it.yaml**
+
+Open `themes/danix-xyz-hacker/i18n/it.yaml` and append at the end:
+
+```yaml
+# Shortcode repository pacchetti
+pkg_list_loading: "Caricamento pacchetti..."
+pkg_list_error: "Impossibile caricare i pacchetti. Visita il repository direttamente."
+pkg_list_filter: "Filtra pacchetti..."
+pkg_list_link_label: "Visualizza"
+pkg_changelog_loading: "Caricamento changelog..."
+pkg_changelog_error: "Impossibile caricare il changelog. Visita il repository direttamente."
+```
+
+- [ ] **Step 3: Verify Hugo renders them**
+
+Run from content repo root:
+```bash
+hugo server --buildDrafts 2>&1 | grep -i "error\|warn" | head -20
+```
+Expected: no i18n-related errors.
+
+- [ ] **Step 4: Commit**
+
+```bash
+cd themes/danix-xyz-hacker
+git add i18n/en.yaml i18n/it.yaml
+git commit -m "feat: add i18n keys for pkg-list and pkg-changelog shortcodes"
+```
+
+---
+
+## Task 2: Create `pkg-list.js` Alpine component
+
+**Files:**
+- Create: `themes/danix-xyz-hacker/assets/js/pkg-list.js`
+
+- [ ] **Step 1: Create the file**
+
+Create `themes/danix-xyz-hacker/assets/js/pkg-list.js` with this content:
+
+```javascript
+document.addEventListener('alpine:init', () => {
+ Alpine.data('pkgList', (i18n) => ({
+ state: 'loading', // 'loading' | 'loaded' | 'error'
+ packages: [],
+ filter: '',
+
+ get filtered() {
+ if (!this.filter) return this.packages;
+ const q = this.filter.toLowerCase();
+ return this.packages.filter(p => p.name.toLowerCase().includes(q));
+ },
+
+ async init() {
+ try {
+ const res = await fetch('https://packages.danix.xyz/PACKAGES.TXT');
+ if (!res.ok) throw new Error('HTTP ' + res.status);
+ const text = await res.text();
+ this.packages = parsePkgTxt(text);
+ this.state = 'loaded';
+ } catch (e) {
+ console.error('pkg-list fetch error:', e);
+ this.state = 'error';
+ }
+ }
+ }));
+});
+
+function parsePkgTxt(text) {
+ const packages = [];
+ const nameRe = /^PACKAGE NAME:\s+(.+\.txz)\s*$/m;
+ const locRe = /^PACKAGE LOCATION:\s+(.+)\s*$/m;
+
+ // Split on blank lines between blocks
+ const blocks = text.split(/\n{2,}/);
+
+ for (const block of blocks) {
+ const nameMatch = block.match(nameRe);
+ const locMatch = block.match(locRe);
+ if (!nameMatch || !locMatch) continue;
+
+ const filename = nameMatch[1].trim();
+ const location = locMatch[1].trim(); // e.g. ./desktop/waybar
+
+ // Parse name and version from filename: name-version-arch-build_tag.txz
+ // Strategy: version segment starts with a digit after a hyphen
+ const parts = filename.replace(/\.txz$/, '').split('-');
+ let nameEnd = 1;
+ for (let i = 1; i < parts.length; i++) {
+ if (/^\d/.test(parts[i])) { nameEnd = i; break; }
+ }
+ const name = parts.slice(0, nameEnd).join('-');
+ const version = parts[nameEnd] || '';
+
+ // Derive folder URL: ./desktop/waybar -> https://packages.danix.xyz/desktop/waybar/
+ const folder = location.replace(/^\.\//, '');
+ const url = 'https://packages.danix.xyz/' + folder + '/';
+ const label = folder; // e.g. desktop/waybar
+
+ packages.push({ name, version, url, label });
+ }
+
+ // Sort alphabetically by name
+ packages.sort((a, b) => a.name.localeCompare(b.name));
+ return packages;
+}
+```
+
+- [ ] **Step 2: Verify parse logic manually**
+
+In browser console or Node REPL, test with one block from PACKAGES.TXT:
+
+```javascript
+// Expected output for input block:
+// PACKAGE NAME: waybar-0.14.0-x86_64-2_danix.txz
+// PACKAGE LOCATION: ./desktop/waybar
+// → { name: 'waybar', version: '0.14.0', url: 'https://packages.danix.xyz/desktop/waybar/', label: 'desktop/waybar' }
+
+// Edge case: package starting with capital letter
+// PACKAGE NAME: Catch2-3.14.0-x86_64-1_danix.txz
+// → { name: 'Catch2', version: '3.14.0', ... }
+
+// Edge case: multi-hyphen name
+// PACKAGE NAME: slack-wallpapers-1.0-noarch-2_danix.txz
+// → { name: 'slack-wallpapers', version: '1.0', ... }
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+cd themes/danix-xyz-hacker
+git add assets/js/pkg-list.js
+git commit -m "feat: add pkg-list Alpine component for PACKAGES.TXT"
+```
+
+---
+
+## Task 3: Create `pkg-changelog.js` Alpine component
+
+**Files:**
+- Create: `themes/danix-xyz-hacker/assets/js/pkg-changelog.js`
+
+- [ ] **Step 1: Create the file**
+
+Create `themes/danix-xyz-hacker/assets/js/pkg-changelog.js` with this content:
+
+```javascript
+document.addEventListener('alpine:init', () => {
+ Alpine.data('pkgChangelog', (count) => ({
+ state: 'loading', // 'loading' | 'loaded' | 'error'
+ entries: [],
+
+ async init() {
+ try {
+ const res = await fetch('https://packages.danix.xyz/ChangeLog.txt');
+ if (!res.ok) throw new Error('HTTP ' + res.status);
+ const text = await res.text();
+ this.entries = parseChangelog(text, count);
+ this.state = 'loaded';
+ } catch (e) {
+ console.error('pkg-changelog fetch error:', e);
+ this.state = 'error';
+ }
+ }
+ }));
+});
+
+function parseChangelog(text, maxEntries) {
+ const SEPARATOR = '+--------------------------+';
+ const chunks = text.split(SEPARATOR);
+ const entries = [];
+
+ for (const chunk of chunks) {
+ if (entries.length >= maxEntries) break;
+ const lines = chunk.split('\n').map(l => l.trimEnd()).filter(l => l.trim() !== '');
+ if (lines.length < 2) continue;
+
+ const timestamp = lines[0].trim();
+ // Validate it looks like a timestamp (starts with a day name or date pattern)
+ if (!/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)/.test(timestamp)) continue;
+
+ const changes = lines.slice(1).join('\n').trim();
+ entries.push({ timestamp, changes });
+ }
+
+ return entries;
+}
+```
+
+- [ ] **Step 2: Verify parse logic manually**
+
+Test with sample ChangeLog.txt content:
+
+```javascript
+// Input (between two SEPARATOR lines):
+// Thu May 14 17:17:42 UTC 2026
+// personal/python3-platformdirs/python3-platformdirs-4.9.6-x86_64-2_danix.txz:
+// Updated for git 7836e58
+
+// Expected output:
+// { timestamp: 'Thu May 14 17:17:42 UTC 2026',
+// changes: 'personal/python3-platformdirs/...: Updated for git 7836e58' }
+
+// Edge case: multi-package entry (multiple lines in changes)
+// Should join all lines after timestamp into changes string
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+cd themes/danix-xyz-hacker
+git add assets/js/pkg-changelog.js
+git commit -m "feat: add pkg-changelog Alpine component for ChangeLog.txt"
+```
+
+---
+
+## Task 4: Create `pkg-list.html` shortcode template
+
+**Files:**
+- Create: `themes/danix-xyz-hacker/layouts/shortcodes/pkg-list.html`
+
+- [ ] **Step 1: Create the shortcode**
+
+Create `themes/danix-xyz-hacker/layouts/shortcodes/pkg-list.html`:
+
+```html
+{{- $loading := i18n "pkg_list_loading" | default "Loading packages..." -}}
+{{- $error := i18n "pkg_list_error" | default "Could not load packages." -}}
+{{- $filter := i18n "pkg_list_filter" | default "Filter packages..." -}}
+{{- $view := i18n "pkg_list_link_label" | default "View" -}}
+
+<div
+ x-data="pkgList({ loading: {{ $loading | jsonify }}, error: {{ $error | jsonify }}, filter: {{ $filter | jsonify }}, view: {{ $view | jsonify }} })"
+ x-init="init()"
+ class="not-prose my-6"
+>
+ {{/* Loading state */}}
+ <div x-show="state === 'loading'" class="flex items-center gap-2 text-text-dim py-4">
+ <svg class="animate-spin w-4 h-4 text-accent" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true">
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"></path>
+ </svg>
+ <span x-text="i18n.loading"></span>
+ </div>
+
+ {{/* Error state */}}
+ <div x-show="state === 'error'" class="px-4 py-3 rounded-lg bg-surface border border-border text-sm text-text">
+ <span x-text="i18n.error"></span>
+ <a href="https://packages.danix.xyz" target="_blank" rel="noopener" class="ml-2 text-accent underline">packages.danix.xyz</a>
+ </div>
+
+ {{/* Loaded state */}}
+ <div x-show="state === 'loaded'">
+ <div class="mb-3">
+ <input
+ type="text"
+ x-model="filter"
+ :placeholder="i18n.filter"
+ class="w-full sm:w-72 px-3 py-2 text-sm bg-bg border border-border/50 rounded-lg text-text placeholder-text-dim focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent transition-colors"
+ aria-label="Filter packages"
+ />
+ </div>
+ <div class="overflow-x-auto rounded-lg border border-border/50">
+ <table class="w-full text-sm text-left">
+ <thead class="bg-surface text-text-dim uppercase text-xs tracking-wide">
+ <tr>
+ <th class="px-4 py-3">Package</th>
+ <th class="px-4 py-3">Version</th>
+ <th class="px-4 py-3 text-right">Link</th>
+ </tr>
+ </thead>
+ <tbody class="divide-y divide-border/30">
+ <template x-for="pkg in filtered" :key="pkg.label">
+ <tr class="hover:bg-surface/50 transition-colors">
+ <td class="px-4 py-2 font-mono font-medium text-text" x-text="pkg.name"></td>
+ <td class="px-4 py-2 font-mono text-text-dim" x-text="pkg.version"></td>
+ <td class="px-4 py-2 text-right">
+ <a
+ :href="pkg.url"
+ target="_blank"
+ rel="noopener"
+ class="inline-flex items-center gap-1 text-accent hover:underline text-xs"
+ :aria-label="'View ' + pkg.name + ' in repository'"
+ >
+ <span x-text="pkg.label"></span>
+ <i data-feather="external-link" class="w-3 h-3" aria-hidden="true"></i>
+ </a>
+ </td>
+ </tr>
+ </template>
+ <tr x-show="filtered.length === 0">
+ <td colspan="3" class="px-4 py-4 text-center text-text-dim text-sm">No packages match your filter.</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <p class="mt-2 text-xs text-text-dim" x-text="filtered.length + ' / ' + packages.length + ' packages'"></p>
+ </div>
+</div>
+```
+
+- [ ] **Step 2: Update `pkg-list.js` to accept i18n object**
+
+The component definition in `pkg-list.js` must accept the `i18n` parameter and store it. Open `themes/danix-xyz-hacker/assets/js/pkg-list.js` and confirm the `Alpine.data` call matches:
+
+```javascript
+Alpine.data('pkgList', (i18n) => ({
+ state: 'loading',
+ i18n: i18n, // store it so template can use i18n.loading, i18n.error, etc.
+ packages: [],
+ filter: '',
+ // ... rest unchanged
+}));
+```
+
+If the `i18n:` property line is missing, add it after `state: 'loading',`.
+
+- [ ] **Step 3: Commit**
+
+```bash
+cd themes/danix-xyz-hacker
+git add layouts/shortcodes/pkg-list.html assets/js/pkg-list.js
+git commit -m "feat: add pkg-list shortcode template"
+```
+
+---
+
+## Task 5: Create `pkg-changelog.html` shortcode template
+
+**Files:**
+- Create: `themes/danix-xyz-hacker/layouts/shortcodes/pkg-changelog.html`
+
+- [ ] **Step 1: Create the shortcode**
+
+Create `themes/danix-xyz-hacker/layouts/shortcodes/pkg-changelog.html`:
+
+```html
+{{- $count := .Get "count" | default "10" -}}
+{{- $loading := i18n "pkg_changelog_loading" | default "Loading changelog..." -}}
+{{- $error := i18n "pkg_changelog_error" | default "Could not load changelog." -}}
+
+<div
+ x-data="pkgChangelog({{ $count }}, { loading: {{ $loading | jsonify }}, error: {{ $error | jsonify }} })"
+ x-init="init()"
+ class="not-prose my-6"
+>
+ {{/* Loading state */}}
+ <div x-show="state === 'loading'" class="flex items-center gap-2 text-text-dim py-4">
+ <svg class="animate-spin w-4 h-4 text-accent" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true">
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"></path>
+ </svg>
+ <span x-text="i18n.loading"></span>
+ </div>
+
+ {{/* Error state */}}
+ <div x-show="state === 'error'" class="px-4 py-3 rounded-lg bg-surface border border-border text-sm text-text">
+ <span x-text="i18n.error"></span>
+ <a href="https://packages.danix.xyz/ChangeLog.txt" target="_blank" rel="noopener" class="ml-2 text-accent underline">ChangeLog.txt</a>
+ </div>
+
+ {{/* Timeline */}}
+ <div x-show="state === 'loaded'" class="relative">
+ {{/* Vertical line */}}
+ <div class="absolute left-3 top-0 bottom-0 w-px bg-border/40" aria-hidden="true"></div>
+
+ <ul class="space-y-6 pl-10" role="list">
+ <template x-for="(entry, idx) in entries" :key="idx">
+ <li class="relative">
+ {{/* Timeline dot */}}
+ <span
+ class="absolute -left-7 top-1 w-3 h-3 rounded-full bg-accent ring-2 ring-bg"
+ aria-hidden="true"
+ ></span>
+ <time
+ class="block text-xs font-semibold text-accent mb-1"
+ x-text="entry.timestamp"
+ ></time>
+ <pre
+ class="text-xs text-text-dim font-mono whitespace-pre-wrap break-words bg-surface/50 rounded px-3 py-2 border border-border/30"
+ x-text="entry.changes"
+ ></pre>
+ </li>
+ </template>
+ </ul>
+ </div>
+</div>
+```
+
+- [ ] **Step 2: Update `pkg-changelog.js` to accept i18n object**
+
+Open `themes/danix-xyz-hacker/assets/js/pkg-changelog.js` and confirm the `Alpine.data` call signature and stores i18n:
+
+```javascript
+Alpine.data('pkgChangelog', (count, i18n) => ({
+ state: 'loading',
+ i18n: i18n,
+ entries: [],
+ // ... rest unchanged
+}));
+```
+
+If `i18n` parameter or `i18n:` property line is missing, add them.
+
+- [ ] **Step 3: Commit**
+
+```bash
+cd themes/danix-xyz-hacker
+git add layouts/shortcodes/pkg-changelog.html assets/js/pkg-changelog.js
+git commit -m "feat: add pkg-changelog shortcode template"
+```
+
+---
+
+## Task 6: Load JS files in baseof.html
+
+**Files:**
+- Modify: `themes/danix-xyz-hacker/layouts/_default/baseof.html`
+
+The theme loads all JS unconditionally via `resources.Get "js/..." | minify` in `baseof.html`. The contact form script is at lines 90-92 as a reference:
+
+```html
+<!-- Contact form script -->
+{{ $contactScript := resources.Get "js/contact-form.js" | minify }}
+<script src="{{ $contactScript.RelPermalink }}"></script>
+```
+
+- [ ] **Step 1: Add pkg-list.js and pkg-changelog.js after the contact form block**
+
+Open `themes/danix-xyz-hacker/layouts/_default/baseof.html`. After line 92 (`<script src="{{ $contactScript.RelPermalink }}"></script>`), add:
+
+```html
+<!-- Package repository shortcodes -->
+{{ $pkgListScript := resources.Get "js/pkg-list.js" | minify }}
+<script src="{{ $pkgListScript.RelPermalink }}"></script>
+{{ $pkgChangelogScript := resources.Get "js/pkg-changelog.js" | minify }}
+<script src="{{ $pkgChangelogScript.RelPermalink }}"></script>
+```
+
+- [ ] **Step 2: Verify Hugo builds without error**
+
+```bash
+cd ~/Programming/GIT/danix.xyz-hacker-theme
+hugo server --buildDrafts 2>&1 | grep -i "error\|warn" | grep -v "^W " | head -20
+```
+
+Expected: no errors about missing resources or template syntax.
+
+- [ ] **Step 3: Commit**
+
+```bash
+cd themes/danix-xyz-hacker
+git add layouts/_default/baseof.html
+git commit -m "feat: load pkg-list and pkg-changelog JS in baseof.html"
+```
+
+---
+
+## Task 7: Add shortcodes to repository content pages
+
+**Files:**
+- Modify: `content/en/repository/index.md`
+- Modify: `content/it/repository/index.md`
+
+- [ ] **Step 1: Add shortcodes to English repository page**
+
+Open `content/en/repository/index.md`. Replace the "Available Packages" section at the bottom:
+
+```markdown
+## Available Packages
+
+{{< pkg-list >}}
+
+## Recent Changes
+
+{{< pkg-changelog count="10" >}}
+```
+
+Remove the old placeholder text: `Check the repository for the latest available packages. See the GitHub SlackBuild repositories below for build information and source files.`
+
+- [ ] **Step 2: Add shortcodes to Italian repository page**
+
+Open `content/it/repository/index.md`. Replace the "Pacchetti disponibili" section:
+
+```markdown
+## Pacchetti disponibili
+
+{{< pkg-list >}}
+
+## Modifiche recenti
+
+{{< pkg-changelog count="10" >}}
+```
+
+Remove the old placeholder text: `Dai un'occhiata al repository per gli ultimi pacchetti disponibili. Vedi i repository SlackBuild su GitHub qui sotto per i dettagli sulle build e i file sorgente.`
+
+- [ ] **Step 3: Verify pages render in dev server**
+
+```bash
+cd ~/Programming/GIT/danix.xyz-hacker-theme
+hugo server --buildDrafts
+```
+
+Visit `http://localhost:1313/en/repository/` and `http://localhost:1313/it/repository/`. Expect to see loading spinners (fetch will fail without CORS header; that's expected in this step).
+
+Open browser devtools → Network tab. Confirm `PACKAGES.TXT` and `ChangeLog.txt` fetch requests are made. Confirm they fail with CORS error (expected until Apache is configured).
+
+- [ ] **Step 4: Commit content changes**
+
+```bash
+cd ~/Programming/GIT/danix.xyz-hacker-theme # NOT in submodule
+git add content/en/repository/index.md content/it/repository/index.md
+git commit -m "feat: add pkg-list and pkg-changelog shortcodes to repository pages"
+```
+
+---
+
+## Task 8: Configure CORS on packages.danix.xyz
+
+> **Note:** This is a server-side change outside the theme. Do it on the `packages.danix.xyz` Apache server.
+
+- [ ] **Step 1: Enable mod_headers if not already enabled**
+
+```bash
+# On the packages.danix.xyz server
+apachectl -M | grep headers
+```
+
+Expected output includes `headers_module`. If not present:
+```bash
+# Slackware: edit /etc/httpd/httpd.conf, uncomment:
+# LoadModule headers_module lib64/httpd/modules/mod_headers.so
+```
+
+- [ ] **Step 2: Add CORS header to Apache vhost config**
+
+In the VirtualHost block for `packages.danix.xyz`, add:
+
+```apache
+<IfModule mod_headers.c>
+ Header set Access-Control-Allow-Origin "https://danix.xyz"
+</IfModule>
+```
+
+- [ ] **Step 3: Reload Apache**
+
+```bash
+apachectl graceful
+```
+
+- [ ] **Step 4: Verify CORS header**
+
+```bash
+curl -sI https://packages.danix.xyz/PACKAGES.TXT | grep -i access-control
+```
+
+Expected:
+```
+Access-Control-Allow-Origin: https://danix.xyz
+```
+
+- [ ] **Step 5: Test in browser**
+
+Visit `http://localhost:1313/en/repository/` (dev server still running). Reload. Expect:
+- Loading spinner appears briefly
+- Package table renders with 132 packages
+- Filter input works
+- Changelog timeline renders with 10 entries
+- Timeline dots are purple, timestamps in accent color
+
+---
+
+## Task 9: Build CSS and final submodule bump
+
+- [ ] **Step 1: Build CSS from content repo root**
+
+```bash
+cd ~/Programming/GIT/danix.xyz-hacker-theme
+npm run build
+```
+
+Expected: `themes/danix-xyz-hacker/assets/css/main.min.css` updated.
+
+- [ ] **Step 2: Commit compiled CSS in submodule**
+
+```bash
+cd themes/danix-xyz-hacker
+git add assets/css/main.min.css
+git commit -m "build: recompile CSS for pkg-list and pkg-changelog shortcodes"
+git push origin master
+```
+
+- [ ] **Step 3: Bump submodule pointer in content repo**
+
+```bash
+cd ~/Programming/GIT/danix.xyz-hacker-theme
+git add themes/danix-xyz-hacker
+git commit -m "chore: bump theme submodule (pkg-list and pkg-changelog shortcodes)"
+git push origin master
+```
+
+- [ ] **Step 4: Final smoke test**
+
+```bash
+hugo server --buildDrafts
+```
+
+Visit both repository pages. Confirm:
+- Package table loads and is filterable
+- Changelog timeline shows 10 entries with purple dots
+- No console errors
+- Both EN and IT pages work
+- i18n strings correct in each language
+
+---
+
+## Spec Coverage Check
+
+| Spec requirement | Task |
+|-----------------|------|
+| `pkg-list` shortcode, fetches PACKAGES.TXT | Tasks 2, 4 |
+| Parse name + version from filename | Task 2 |
+| Table columns: name, version, link to folder | Tasks 2, 4 |
+| Client-side filter | Tasks 2, 4 |
+| `pkg-changelog` shortcode, fetches ChangeLog.txt | Tasks 3, 5 |
+| `count` param | Tasks 3, 5 |
+| Timeline style with accent dots | Task 5 |
+| Loading / error / loaded states | Tasks 4, 5 |
+| i18n keys for all UI strings | Task 1 |
+| CORS header on packages.danix.xyz | Task 8 |
+| Shortcodes added to EN and IT repository pages | Task 7 |
+| CSS rebuild | Task 9 |
+| Submodule bump | Task 9 |