# CLAUDE.md — Ghost GitHub Portfolio
## Overview
Writes a Ghost page with the owner's top GitHub repositories by stars: one html card with scoped CSS holding a responsive grid (3, 2, 1 columns) of banner tiles, name, one-line description, star count and optional Site link. Since 0.4.0 there are no badges, no footer and no stats block.
## Tech Stack
- TypeScript (strict mode, ES2022, NodeNext, ESM)
- Node.js 18+ (native `fetch`, `crypto` — zero external HTTP dependencies)
- Commander (CLI framework)
- YAML (config file parsing)
- Vitest (test framework)
- Docker (multi-stage Alpine container, image: `drumsergio/ghost-github-portfolio`)
- GitHub Actions CI (Node 18/20/22 matrix), Release (npm + Docker + GH Release)
- Ghost Admin API (lexical editor format, HS256 JWT auth)
## Development
```bash
npm install
npm run dev # run with tsx
npm run build # compile TypeScript
npm run test # vitest run
npm run test:watch # vitest watch mode
npm run lint # tsc --noEmit
```
## Architecture
```
src/
├── index.ts # CLI entry point (commander: sync + init commands)
├── config.ts # YAML config loader, defaults, env var overrides, validation
├── github.ts # GitHub REST API: fetch all repos (paginated), sort client-side, filter, validate banners (200 + self-contained SVG)
├── ghost.ts # Ghost Admin API: JWT generation (HS256), fetch page, update page (lexical format)
├── generator.ts # The single html card: intro, grid of tiles, closing line; lexical document builder
├── http.ts # rawRequest over node:http/https for the origin route (fetch drops a custom Host header)
└── types.ts # TypeScript interfaces: Config, GitHubRepo, LexicalDocument, CustomBadge
```
Other files:
- `dist/` — compiled output (CLI entrypoint)
- `docs/` — documentation and images
- `action.yml` — GitHub Action definition
- `Dockerfile` — multi-stage container build
- `vitest.config.ts` — test configuration
## Key Rules
- Never hardcode Ghost API credentials; use config.yml or environment variables
- Docker images published to Docker Hub with semver tags (never `:latest`)
- Ghost page content uses lexical format (not mobiledoc)
- Supports dry-run mode for previewing changes without writing to Ghost
- `GHOST_GITHUB_TOKEN` is the env var (NOT `GITHUB_TOKEN` — avoids accidental CI token pickup)
## Design Decisions
1. **Client-side star sorting**: GitHub REST API `/users/{user}/repos` does NOT support `sort=stars`. All pages are fetched, then sorted in memory. Do NOT add `sort=stars` to the API URL.
2. **Ghost lexical format**: The document is a JSON AST with `html` nodes and `horizontalrule` nodes. Do NOT invent new node types.
3. **JWT authentication**: Ghost Admin API uses HS256 JWT with key ID in `kid` header. Secret is hex-decoded. Tokens expire in 5 minutes. Implemented via `node:crypto` only.
4. **Banner validation**: the per-repo `bannerPaths` entry, else `defaultBannerPath`, fetched from `raw.githubusercontent.com`; used only if it answers 200 and, for SVG, has no relative or external `href`/`url()` reference (browsers never load those inside an `
`). Otherwise a typographic tile of the same 9:2 shape, and a warning naming the reason.
5. **No badges, no stats**: removed in 0.4.0 on purpose. Stars are plain text and refresh when the tool runs.
6. **One html card with a scoped `