# 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 |
