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