ghost-github-portfolio 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,210 @@
1
+ <p align="center">
2
+ <img src="docs/images/banner.svg" alt="Ghost GitHub Portfolio banner" width="900"/>
3
+ </p>
4
+
5
+ <p align="center">
6
+ <a href="https://www.npmjs.com/package/ghost-github-portfolio"><img src="https://img.shields.io/npm/v/ghost-github-portfolio?style=flat-square&logo=npm" alt="npm"></a>
7
+ <a href="https://hub.docker.com/r/drumsergio/ghost-github-portfolio"><img src="https://img.shields.io/docker/pulls/drumsergio/ghost-github-portfolio?style=flat-square&logo=docker" alt="Docker Pulls"></a>
8
+ <a href="https://github.com/GeiserX/ghost-github-portfolio/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/GeiserX/ghost-github-portfolio/ci.yml?style=flat-square&logo=github&label=CI" alt="CI"></a>
9
+ <a href="LICENSE"><img src="https://img.shields.io/github/license/GeiserX/ghost-github-portfolio?style=flat-square" alt="License"></a>
10
+ <a href="https://github.com/GeiserX/ghost-github-portfolio/stargazers"><img src="https://img.shields.io/github/stars/GeiserX/ghost-github-portfolio?style=flat-square&logo=github" alt="Stars"></a>
11
+ </p>
12
+
13
+ <p align="center"><strong>Auto-sync your GitHub repositories to a Ghost CMS portfolio page.</strong></p>
14
+
15
+ ---
16
+
17
+ ## What it does
18
+
19
+ `ghost-github-portfolio` fetches your public GitHub repositories, sorts them by stars, generates beautiful portfolio cards with banners and badges, and pushes the result to a Ghost page via the Admin API.
20
+
21
+ Run it daily via cron, GitHub Actions, or Docker to keep your portfolio always up to date.
22
+
23
+ **Features:**
24
+
25
+ - Fetches repos sorted by stars, filtered by configurable minimum
26
+ - Auto-detects banner images (`docs/images/banner.svg`, `media/banner.svg`, etc.)
27
+ - Dynamic shields.io badges (stars, forks, license, Docker pulls, website, awesome-list)
28
+ - Per-repo overrides (custom description, badges, Docker image, tech stack)
29
+ - Ghost lexical editor format (the current Ghost editor)
30
+ - Dry-run mode for previewing changes
31
+ - Runs as CLI, Docker container, or GitHub Action
32
+
33
+ ## Quick start
34
+
35
+ ### 1. Install
36
+
37
+ ```bash
38
+ # npm (global)
39
+ npm install -g ghost-github-portfolio
40
+
41
+ # npx (no install)
42
+ npx ghost-github-portfolio
43
+
44
+ # Docker
45
+ docker pull drumsergio/ghost-github-portfolio:0.2.0
46
+ ```
47
+
48
+ ### 2. Create a config
49
+
50
+ ```bash
51
+ ghost-github-portfolio init
52
+ ```
53
+
54
+ This generates a `config.yml` with all available options. Edit it with your GitHub username and Ghost API credentials.
55
+
56
+ ### 3. Get your Ghost Admin API key
57
+
58
+ 1. Go to your Ghost Admin panel > **Settings** > **Integrations**
59
+ 2. Create a new **Custom Integration**
60
+ 3. Copy the **Admin API Key** (format: `KEY_ID:SECRET`)
61
+
62
+ ### 4. Run
63
+
64
+ ```bash
65
+ # Preview (no changes to Ghost)
66
+ ghost-github-portfolio sync --config config.yml --dry-run --verbose
67
+
68
+ # Sync to Ghost
69
+ ghost-github-portfolio sync --config config.yml --verbose
70
+ ```
71
+
72
+ ## Configuration
73
+
74
+ ```yaml
75
+ github:
76
+ username: YOUR_GITHUB_USERNAME
77
+ # token: ghp_xxx # Optional: higher rate limits (env: GHOST_GITHUB_TOKEN)
78
+
79
+ ghost:
80
+ url: https://your-ghost-blog.com
81
+ adminApiKey: "KEY_ID:SECRET_HEX"
82
+ pageSlug: portfolio # or pageId: "hex_id"
83
+
84
+ portfolio:
85
+ minStars: 2
86
+ maxRepos: 50
87
+ includeForked: false
88
+ badgeStyle: for-the-badge # flat, flat-square, for-the-badge, plastic, social
89
+ showBanner: true
90
+ centerContent: true
91
+ defaultBannerPath: docs/images/banner.svg
92
+
93
+ bannerPaths:
94
+ awesome-spain: media/banner.svg # Override per repo
95
+
96
+ excludeRepos:
97
+ - .github
98
+
99
+ repos:
100
+ my-project:
101
+ description: "Custom description"
102
+ dockerImage: myuser/my-project # Adds Docker pulls badge
103
+ techStack: "Python, Docker, Redis"
104
+ badges:
105
+ - type: website
106
+ url: https://my-project.com
107
+ - type: awesome-list
108
+ - type: platform
109
+ label: macOS
110
+ logo: apple
111
+ - type: docs
112
+ url: https://docs.my-project.com
113
+ - type: custom
114
+ label: MCP
115
+ value: Official Registry
116
+ color: E6522C
117
+
118
+ footer:
119
+ showStats: true
120
+ showViewAll: true
121
+ ```
122
+
123
+ ### Environment variables
124
+
125
+ | Variable | Description |
126
+ |----------|-------------|
127
+ | `GHOST_GITHUB_TOKEN` | GitHub token for private repos / higher rate limits |
128
+ | `GHOST_ADMIN_API_KEY` | Ghost Admin API key (overrides config file) |
129
+
130
+ ## Docker
131
+
132
+ ```bash
133
+ docker run --rm \
134
+ -v /path/to/config.yml:/config/config.yml \
135
+ drumsergio/ghost-github-portfolio:0.2.0
136
+ ```
137
+
138
+ ### Docker Compose (daily cron)
139
+
140
+ ```yaml
141
+ services:
142
+ ghost-portfolio:
143
+ image: drumsergio/ghost-github-portfolio:0.2.0
144
+ volumes:
145
+ - ./config.yml:/config/config.yml:ro
146
+ # Run daily at 6 AM via external cron or restart policy
147
+ ```
148
+
149
+ ## GitHub Action
150
+
151
+ Create `.github/workflows/portfolio.yml` in any repo:
152
+
153
+ ```yaml
154
+ name: Update Portfolio
155
+
156
+ on:
157
+ schedule:
158
+ - cron: "0 6 * * *" # Daily at 6 AM UTC
159
+ workflow_dispatch: # Manual trigger
160
+
161
+ jobs:
162
+ sync:
163
+ runs-on: ubuntu-latest
164
+ steps:
165
+ - uses: actions/checkout@v4
166
+
167
+ - uses: actions/setup-node@v4
168
+ with:
169
+ node-version: 22
170
+
171
+ - run: npx ghost-github-portfolio sync --config config.yml --verbose
172
+ env:
173
+ GHOST_GITHUB_TOKEN: ${{ secrets.GHOST_GITHUB_TOKEN }}
174
+ ```
175
+
176
+ Store `config.yml` in the repo (without secrets) and use environment variables for the API keys.
177
+
178
+ ## How it works
179
+
180
+ 1. **Fetches** all public repos for the configured GitHub user via the REST API
181
+ 2. **Filters** by minimum stars, excludes forks and blocklisted repos
182
+ 3. **Sorts** by star count (descending)
183
+ 4. **Detects** banner images by checking common paths (`docs/images/banner.svg`, `media/banner.svg`, etc.)
184
+ 5. **Generates** HTML cards with: banner, name, dynamic badges, description, tech stack
185
+ 6. **Composes** a Ghost lexical JSON document (the format Ghost's editor uses internally)
186
+ 7. **Updates** the target Ghost page via the Admin API with JWT authentication
187
+
188
+ Badges are dynamic (served by shields.io) — stars, forks, and Docker pulls update automatically on every page view without re-running the tool.
189
+
190
+ ## Badge types
191
+
192
+ | Type | Auto-detected | Description |
193
+ |------|--------------|-------------|
194
+ | Stars | Yes | Always shown |
195
+ | Forks | Yes | Always shown |
196
+ | License | Yes | From GitHub repo metadata |
197
+ | Docker pulls | Config | Set `dockerImage` in repo overrides |
198
+ | Website | Yes | From GitHub homepage field |
199
+ | Awesome list | Yes | If repo has `awesome-list` topic |
200
+ | Docs | Yes | If homepage is a GitHub Pages URL |
201
+ | Platform | Config | Custom platform badge (macOS, Linux, etc.) |
202
+ | Custom | Config | Any shields.io-compatible badge |
203
+
204
+ ## Roadmap
205
+
206
+ See [docs/ROADMAP.md](docs/ROADMAP.md) for the full roadmap — themes, multi-CMS support, AI features, analytics, team portfolios, and more.
207
+
208
+ ## License
209
+
210
+ [GPL-3.0](LICENSE)
@@ -0,0 +1,4 @@
1
+ import type { Config } from "./types.js";
2
+ export declare function loadConfig(path: string): Config;
3
+ export declare function generateExampleConfig(): string;
4
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAmB,MAAM,YAAY,CAAC;AAgB1D,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAkD/C;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAsD9C"}
package/dist/config.js ADDED
@@ -0,0 +1,116 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { parse } from "yaml";
3
+ const DEFAULT_PORTFOLIO = {
4
+ minStars: 2,
5
+ maxRepos: 50,
6
+ excludeRepos: [],
7
+ includeForked: false,
8
+ badgeStyle: "for-the-badge",
9
+ showBanner: true,
10
+ centerContent: true,
11
+ defaultBannerPath: "docs/images/banner.svg",
12
+ bannerPaths: {},
13
+ repos: {},
14
+ footer: { showStats: true, showViewAll: true },
15
+ };
16
+ export function loadConfig(path) {
17
+ const raw = readFileSync(path, "utf-8");
18
+ const parsed = parse(raw);
19
+ if (!parsed.github || !parsed.github.username) {
20
+ throw new Error("Config: github.username is required");
21
+ }
22
+ if (!parsed.ghost || !parsed.ghost.url) {
23
+ throw new Error("Config: ghost.url is required");
24
+ }
25
+ if (!parsed.ghost.adminApiKey) {
26
+ throw new Error("Config: ghost.adminApiKey is required");
27
+ }
28
+ const ghost = parsed.ghost;
29
+ if (!ghost.pageId && !ghost.pageSlug) {
30
+ throw new Error("Config: ghost.pageId or ghost.pageSlug is required");
31
+ }
32
+ const github = parsed.github;
33
+ const portfolio = (parsed.portfolio ?? {});
34
+ // Env var overrides (GHOST_GITHUB_TOKEN only — GITHUB_TOKEN is not used to
35
+ // avoid picking up unrelated tokens from gh CLI or CI environments)
36
+ const ghToken = github.token ?? process.env.GHOST_GITHUB_TOKEN;
37
+ const ghostKey = ghost.adminApiKey ?? process.env.GHOST_ADMIN_API_KEY;
38
+ return {
39
+ github: {
40
+ username: github.username,
41
+ token: ghToken,
42
+ },
43
+ ghost: {
44
+ url: ghost.url.replace(/\/$/, ""),
45
+ adminApiKey: ghostKey,
46
+ pageId: ghost.pageId,
47
+ pageSlug: ghost.pageSlug,
48
+ },
49
+ portfolio: {
50
+ ...DEFAULT_PORTFOLIO,
51
+ ...portfolio,
52
+ footer: { ...DEFAULT_PORTFOLIO.footer, ...portfolio.footer },
53
+ bannerPaths: {
54
+ ...DEFAULT_PORTFOLIO.bannerPaths,
55
+ ...portfolio.bannerPaths,
56
+ },
57
+ repos: { ...DEFAULT_PORTFOLIO.repos, ...portfolio.repos },
58
+ },
59
+ };
60
+ }
61
+ export function generateExampleConfig() {
62
+ return `# ghost-github-portfolio configuration
63
+ github:
64
+ username: YOUR_GITHUB_USERNAME
65
+ # token: ghp_xxx # Optional: for private repos or higher rate limits (env: GHOST_GITHUB_TOKEN)
66
+
67
+ ghost:
68
+ url: https://your-ghost-blog.com
69
+ adminApiKey: "KEY_ID:SECRET_HEX" # From Ghost Admin > Integrations (env: GHOST_ADMIN_API_KEY)
70
+ pageSlug: portfolio # Update this page (or use pageId for the hex ID)
71
+
72
+ portfolio:
73
+ minStars: 2 # Only show repos with at least this many stars
74
+ maxRepos: 50 # Maximum number of repos to display
75
+ includeForked: false # Skip forked repos
76
+ badgeStyle: for-the-badge # shields.io style: flat, flat-square, for-the-badge, plastic, social
77
+ showBanner: true # Show repo banner images (SVG/PNG from the repo)
78
+ centerContent: true # Center project names and badges
79
+
80
+ defaultBannerPath: docs/images/banner.svg # Default location to look for banners
81
+
82
+ # Override banner path for specific repos
83
+ bannerPaths:
84
+ # awesome-spain: media/banner.svg
85
+
86
+ excludeRepos:
87
+ - .github # Profile config repo
88
+ # - my-private-project
89
+
90
+ # Per-repo overrides
91
+ repos: {}
92
+ # my-project:
93
+ # description: "Custom description (overrides GitHub)"
94
+ # dockerImage: drumsergio/my-project # Adds Docker pulls badge
95
+ # techStack: "Python, Docker, Redis"
96
+ # bannerPath: assets/banner.png
97
+ # badges:
98
+ # - type: website
99
+ # url: https://my-project.com
100
+ # - type: awesome-list
101
+ # - type: platform
102
+ # label: macOS
103
+ # logo: apple
104
+ # - type: docs
105
+ # url: https://docs.my-project.com
106
+ # - type: custom
107
+ # label: MCP
108
+ # value: Official Registry
109
+ # color: E6522C
110
+
111
+ footer:
112
+ showStats: true # Show total stars and repo count
113
+ showViewAll: true # Show "View All Repositories" link
114
+ `;
115
+ }
116
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,KAAK,EAAE,MAAM,MAAM,CAAC;AAG7B,MAAM,iBAAiB,GAAoB;IACzC,QAAQ,EAAE,CAAC;IACX,QAAQ,EAAE,EAAE;IACZ,YAAY,EAAE,EAAE;IAChB,aAAa,EAAE,KAAK;IACpB,UAAU,EAAE,eAAe;IAC3B,UAAU,EAAE,IAAI;IAChB,aAAa,EAAE,IAAI;IACnB,iBAAiB,EAAE,wBAAwB;IAC3C,WAAW,EAAE,EAAE;IACf,KAAK,EAAE,EAAE;IACT,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE;CAC/C,CAAC;AAEF,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACxC,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAA4B,CAAC;IAErD,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,CAAE,MAAM,CAAC,MAAkC,CAAC,QAAQ,EAAE,CAAC;QAC3E,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;IACzD,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,CAAE,MAAM,CAAC,KAAiC,CAAC,GAAG,EAAE,CAAC;QACpE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;IACnD,CAAC;IACD,IAAI,CAAE,MAAM,CAAC,KAAiC,CAAC,WAAW,EAAE,CAAC;QAC3D,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC3D,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,KAAgC,CAAC;IACtD,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,CAAC,MAAgC,CAAC;IACvD,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,CAA6B,CAAC;IAEvE,2EAA2E;IAC3E,oEAAoE;IACpE,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,IAAI,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;IAC/D,MAAM,QAAQ,GACX,KAAK,CAAC,WAAsB,IAAI,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC;IAEnE,OAAO;QACL,MAAM,EAAE;YACN,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,KAAK,EAAE,OAAO;SACf;QACD,KAAK,EAAE;YACL,GAAG,EAAG,KAAK,CAAC,GAAc,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;YAC7C,WAAW,EAAE,QAAQ;YACrB,MAAM,EAAE,KAAK,CAAC,MAA4B;YAC1C,QAAQ,EAAE,KAAK,CAAC,QAA8B;SAC/C;QACD,SAAS,EAAE;YACT,GAAG,iBAAiB;YACpB,GAAG,SAAS;YACZ,MAAM,EAAE,EAAE,GAAG,iBAAiB,CAAC,MAAM,EAAE,GAAG,SAAS,CAAC,MAAM,EAAE;YAC5D,WAAW,EAAE;gBACX,GAAG,iBAAiB,CAAC,WAAW;gBAChC,GAAG,SAAS,CAAC,WAAW;aACzB;YACD,KAAK,EAAE,EAAE,GAAG,iBAAiB,CAAC,KAAK,EAAE,GAAG,SAAS,CAAC,KAAK,EAAE;SAC1D;KACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,qBAAqB;IACnC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoDR,CAAC;AACF,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { Config, GitHubRepo, LexicalDocument } from "./types.js";
2
+ export declare function generateCard(repo: GitHubRepo, bannerUrl: string | null, config: Config): string;
3
+ export declare function generateFooter(repos: GitHubRepo[], config: Config): string | null;
4
+ export declare function buildLexical(cards: string[], footer: string | null): LexicalDocument;
5
+ //# sourceMappingURL=generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../src/generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,MAAM,EACN,UAAU,EACV,eAAe,EAEhB,MAAM,YAAY,CAAC;AAyGpB,wBAAgB,YAAY,CAC1B,IAAI,EAAE,UAAU,EAChB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,MAAM,EAAE,MAAM,GACb,MAAM,CA0DR;AAuCD,wBAAgB,cAAc,CAC5B,KAAK,EAAE,UAAU,EAAE,EACnB,MAAM,EAAE,MAAM,GACb,MAAM,GAAG,IAAI,CAoDf;AAED,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,EAAE,EACf,MAAM,EAAE,MAAM,GAAG,IAAI,GACpB,eAAe,CA0BjB"}
@@ -0,0 +1,204 @@
1
+ function shieldsUrl(label, value, color, style, logo) {
2
+ const encoded = `${encodeURIComponent(label)}-${encodeURIComponent(value)}-${color}`;
3
+ let url = `https://img.shields.io/badge/${encoded}?style=${style}`;
4
+ if (logo)
5
+ url += `&logo=${logo}&logoColor=white`;
6
+ return url;
7
+ }
8
+ function buildBadges(repo, config) {
9
+ const style = config.portfolio.badgeStyle;
10
+ const overrides = config.portfolio.repos[repo.name];
11
+ const badges = [];
12
+ // Stars
13
+ badges.push(`<img src="https://img.shields.io/github/stars/${repo.full_name}?style=${style}" alt="Stars">`);
14
+ // Forks
15
+ badges.push(`<img src="https://img.shields.io/github/forks/${repo.full_name}?style=${style}" alt="Forks">`);
16
+ // License
17
+ if (repo.license) {
18
+ badges.push(`<img src="https://img.shields.io/github/license/${repo.full_name}?style=${style}" alt="License">`);
19
+ }
20
+ // Docker pulls (from config override)
21
+ const dockerImage = overrides?.dockerImage;
22
+ if (dockerImage) {
23
+ badges.push(`<a href="https://hub.docker.com/r/${dockerImage}"><img src="https://img.shields.io/docker/pulls/${dockerImage}?style=${style}" alt="Docker Pulls"></a>`);
24
+ }
25
+ // Website badge (from GitHub homepage or config)
26
+ const homepage = repo.homepage;
27
+ if (homepage &&
28
+ !homepage.includes("hub.docker.com") &&
29
+ !homepage.includes("github.io")) {
30
+ const domain = new URL(homepage).hostname;
31
+ badges.push(`<a href="${homepage}"><img src="${shieldsUrl("website", domain, "blue", style)}" alt="Website"></a>`);
32
+ }
33
+ // Custom badges from config
34
+ if (overrides?.badges) {
35
+ for (const badge of overrides.badges) {
36
+ badges.push(renderCustomBadge(badge, style));
37
+ }
38
+ }
39
+ // Auto-detect awesome-list from topics
40
+ if (repo.topics.includes("awesome-list") &&
41
+ !overrides?.badges?.some((b) => b.type === "awesome-list")) {
42
+ badges.push(`<img src="${shieldsUrl("awesome", "list", "fc60a8", style, "awesomelists")}" alt="Awesome List">`);
43
+ }
44
+ // Docs badge for GitHub Pages
45
+ if (homepage?.includes("github.io") &&
46
+ !overrides?.badges?.some((b) => b.type === "docs")) {
47
+ badges.push(`<a href="${homepage}"><img src="${shieldsUrl("docs", "online", "blue", style)}" alt="Docs"></a>`);
48
+ }
49
+ return badges;
50
+ }
51
+ function renderCustomBadge(badge, style) {
52
+ switch (badge.type) {
53
+ case "website":
54
+ return `<a href="${badge.url}"><img src="${shieldsUrl("website", badge.label ?? new URL(badge.url).hostname, badge.color ?? "blue", style)}" alt="Website"></a>`;
55
+ case "docker":
56
+ return `<a href="https://hub.docker.com/r/${badge.label}"><img src="https://img.shields.io/docker/pulls/${badge.label}?style=${style}" alt="Docker Pulls"></a>`;
57
+ case "awesome-list":
58
+ return `<img src="${shieldsUrl("awesome", "list", badge.color ?? "fc60a8", style, "awesomelists")}" alt="Awesome List">`;
59
+ case "platform":
60
+ return `<img src="${shieldsUrl("platform", badge.label ?? "unknown", badge.color ?? "000", style, badge.logo)}" alt="Platform">`;
61
+ case "docs":
62
+ return `<a href="${badge.url}"><img src="${shieldsUrl("docs", badge.label ?? "online", badge.color ?? "blue", style)}" alt="Docs"></a>`;
63
+ case "custom":
64
+ return `<img src="${shieldsUrl(badge.label ?? "", badge.value ?? "", badge.color ?? "grey", style, badge.logo)}" alt="${badge.label}">`;
65
+ }
66
+ }
67
+ export function generateCard(repo, bannerUrl, config) {
68
+ const overrides = config.portfolio.repos[repo.name];
69
+ const center = config.portfolio.centerContent;
70
+ const align = center ? ' style="text-align:center;"' : "";
71
+ const flexJustify = center ? " justify-content:center;" : "";
72
+ const parts = [];
73
+ // Banner
74
+ if (bannerUrl) {
75
+ parts.push(`<a href="${repo.html_url}" rel="noreferrer"${center ? ' style="display:block; text-align:center;"' : ""}>` +
76
+ `<img src="${bannerUrl}" alt="${repo.name} banner" style="width:100%; max-width:900px; border-radius:8px; margin-bottom:12px; display:block;${center ? " margin-left:auto; margin-right:auto;" : ""}">` +
77
+ `</a>`);
78
+ }
79
+ // Title
80
+ parts.push(`<h3${align}><a href="${repo.html_url}" rel="noreferrer">${repo.name}</a></h3>`);
81
+ // Badges
82
+ const badges = buildBadges(repo, config);
83
+ parts.push(`<div style="display:flex; gap:4px; align-items:center; flex-wrap:wrap;${flexJustify}">`, ` ${badges.join("\n ")}`, `</div>`);
84
+ // Description
85
+ const description = overrides?.description ?? repo.description;
86
+ if (description) {
87
+ parts.push(`<p><em>${escapeHtml(description)}</em></p>`);
88
+ }
89
+ // Key features (from config only)
90
+ if (overrides?.keyFeatures?.length) {
91
+ parts.push(`<p><strong>Key Features:</strong></p>`);
92
+ parts.push(`<ul>`);
93
+ for (const feature of overrides.keyFeatures) {
94
+ parts.push(`<li>${escapeHtml(feature)}</li>`);
95
+ }
96
+ parts.push(`</ul>`);
97
+ }
98
+ // Tech stack
99
+ const techStack = overrides?.techStack ?? inferTechStack(repo);
100
+ if (techStack) {
101
+ parts.push(`<p><strong>Tech Stack:</strong>&nbsp;${escapeHtml(techStack)}</p>`);
102
+ }
103
+ // Separator
104
+ parts.push(`<hr>`);
105
+ return parts.join("\n");
106
+ }
107
+ function inferTechStack(repo) {
108
+ const languages = [];
109
+ if (repo.language)
110
+ languages.push(repo.language);
111
+ // Add relevant topics as tech
112
+ const techTopics = repo.topics.filter((t) => [
113
+ "docker",
114
+ "kubernetes",
115
+ "helm",
116
+ "nextjs",
117
+ "react",
118
+ "vue",
119
+ "svelte",
120
+ "fastapi",
121
+ "flask",
122
+ "express",
123
+ "nestjs",
124
+ "terraform",
125
+ "ansible",
126
+ "mcp",
127
+ "jellyfin",
128
+ "telegram",
129
+ "swift",
130
+ "macos",
131
+ "nodejs",
132
+ ].includes(t.toLowerCase()));
133
+ const topicLabels = techTopics.map((t) => t.charAt(0).toUpperCase() + t.slice(1));
134
+ const all = [...new Set([...languages, ...topicLabels])];
135
+ return all.length > 0 ? all.join(", ") : null;
136
+ }
137
+ export function generateFooter(repos, config) {
138
+ if (!config.portfolio.footer.showStats && !config.portfolio.footer.showViewAll)
139
+ return null;
140
+ const center = config.portfolio.centerContent;
141
+ const align = center ? ' style="text-align:center;"' : "";
142
+ const listStyle = center
143
+ ? ' style="list-style:none; padding:0; text-align:center;"'
144
+ : "";
145
+ const parts = [];
146
+ parts.push(`<h2${align}>GitHub Stats</h2>`);
147
+ if (config.portfolio.footer.showStats) {
148
+ const totalStars = repos.reduce((s, r) => s + r.stargazers_count, 0);
149
+ const totalRepos = repos.length;
150
+ const languages = [
151
+ ...new Set(repos.map((r) => r.language).filter(Boolean)),
152
+ ];
153
+ const focusAreas = [
154
+ ...new Set(repos.flatMap((r) => r.topics).filter(Boolean)),
155
+ ]
156
+ .slice(0, 8)
157
+ .map((t) => t.charAt(0).toUpperCase() + t.slice(1));
158
+ parts.push(`<ul${listStyle}>`);
159
+ parts.push(`<li><strong>Total Stars (featured):</strong>&nbsp;${totalStars}+</li>`);
160
+ parts.push(`<li><strong>Featured Repositories:</strong>&nbsp;${totalRepos}</li>`);
161
+ if (languages.length > 0) {
162
+ parts.push(`<li><strong>Primary Languages:</strong>&nbsp;${languages.join(", ")}</li>`);
163
+ }
164
+ if (focusAreas.length > 0) {
165
+ parts.push(`<li><strong>Focus Areas:</strong>&nbsp;${focusAreas.join(", ")}</li>`);
166
+ }
167
+ parts.push(`</ul>`);
168
+ }
169
+ if (config.portfolio.footer.showViewAll) {
170
+ parts.push(`<p${align}><a href="https://github.com/${config.github.username}?tab=repositories"><strong>View All Repositories &rarr;</strong></a></p>`);
171
+ }
172
+ return parts.join("\n");
173
+ }
174
+ export function buildLexical(cards, footer) {
175
+ const children = [];
176
+ // Opening separator
177
+ children.push({ type: "horizontalrule", version: 1 });
178
+ // Project cards
179
+ for (const card of cards) {
180
+ children.push({ type: "html", version: 1, html: card });
181
+ }
182
+ // Footer
183
+ if (footer) {
184
+ children.push({ type: "html", version: 1, html: footer });
185
+ }
186
+ return {
187
+ root: {
188
+ children,
189
+ direction: "ltr",
190
+ format: "",
191
+ indent: 0,
192
+ type: "root",
193
+ version: 1,
194
+ },
195
+ };
196
+ }
197
+ function escapeHtml(str) {
198
+ return str
199
+ .replace(/&/g, "&amp;")
200
+ .replace(/</g, "&lt;")
201
+ .replace(/>/g, "&gt;")
202
+ .replace(/"/g, "&quot;");
203
+ }
204
+ //# sourceMappingURL=generator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generator.js","sourceRoot":"","sources":["../src/generator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { Config, LexicalDocument } from "./types.js";
2
+ export declare function generateJwt(apiKey: string): string;
3
+ interface GhostPage {
4
+ id: string;
5
+ updated_at: string;
6
+ title: string;
7
+ lexical: string;
8
+ }
9
+ export declare function fetchPage(config: Config): Promise<GhostPage>;
10
+ export declare function updatePage(config: Config, pageId: string, updatedAt: string, lexical: LexicalDocument): Promise<GhostPage>;
11
+ export {};
12
+ //# sourceMappingURL=ghost.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ghost.d.ts","sourceRoot":"","sources":["../src/ghost.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAG1D,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CA6BlD;AAUD,UAAU,SAAS;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAqBlE;AAED,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,SAAS,CAAC,CA0BpB"}
package/dist/ghost.js ADDED
@@ -0,0 +1,71 @@
1
+ import { createHmac } from "node:crypto";
2
+ import { fetchWithRetry } from "./http.js";
3
+ export function generateJwt(apiKey) {
4
+ const [keyId, secretHex] = apiKey.split(":");
5
+ if (!keyId || !secretHex) {
6
+ throw new Error('Invalid Ghost Admin API key format. Expected "KEY_ID:SECRET_HEX"');
7
+ }
8
+ const secret = Buffer.from(secretHex, "hex");
9
+ const now = Math.floor(Date.now() / 1000);
10
+ const header = Buffer.from(JSON.stringify({ alg: "HS256", kid: keyId, typ: "JWT" }))
11
+ .toString("base64url")
12
+ .replace(/=+$/, "");
13
+ const payload = Buffer.from(JSON.stringify({ iat: now, exp: now + 300, aud: "/admin/" }))
14
+ .toString("base64url")
15
+ .replace(/=+$/, "");
16
+ const signature = createHmac("sha256", secret)
17
+ .update(`${header}.${payload}`)
18
+ .digest("base64url")
19
+ .replace(/=+$/, "");
20
+ return `${header}.${payload}.${signature}`;
21
+ }
22
+ function ghostHeaders(apiKey) {
23
+ return {
24
+ Authorization: `Ghost ${generateJwt(apiKey)}`,
25
+ "Content-Type": "application/json",
26
+ "User-Agent": "Mozilla/5.0 ghost-github-portfolio",
27
+ };
28
+ }
29
+ export async function fetchPage(config) {
30
+ const { url, adminApiKey, pageId, pageSlug } = config.ghost;
31
+ let endpoint;
32
+ if (pageId) {
33
+ endpoint = `${url}/ghost/api/admin/pages/${pageId}/`;
34
+ }
35
+ else {
36
+ endpoint = `${url}/ghost/api/admin/pages/slug/${pageSlug}/`;
37
+ }
38
+ const res = await fetchWithRetry(endpoint, {
39
+ headers: ghostHeaders(adminApiKey),
40
+ });
41
+ if (!res.ok) {
42
+ const body = await res.text();
43
+ throw new Error(`Ghost API error ${res.status}: ${body}`);
44
+ }
45
+ const data = (await res.json());
46
+ return data.pages[0];
47
+ }
48
+ export async function updatePage(config, pageId, updatedAt, lexical) {
49
+ const { url, adminApiKey } = config.ghost;
50
+ const endpoint = `${url}/ghost/api/admin/pages/${pageId}/`;
51
+ const body = {
52
+ pages: [
53
+ {
54
+ lexical: JSON.stringify(lexical),
55
+ updated_at: updatedAt,
56
+ },
57
+ ],
58
+ };
59
+ const res = await fetchWithRetry(endpoint, {
60
+ method: "PUT",
61
+ headers: ghostHeaders(adminApiKey),
62
+ body: JSON.stringify(body),
63
+ });
64
+ if (!res.ok) {
65
+ const text = await res.text();
66
+ throw new Error(`Ghost update error ${res.status}: ${text}`);
67
+ }
68
+ const data = (await res.json());
69
+ return data.pages[0];
70
+ }
71
+ //# sourceMappingURL=ghost.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ghost.js","sourceRoot":"","sources":["../src/ghost.ts"],"names":[],"mappings":"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"}