ghost-github-portfolio 0.3.4 → 0.4.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 +116 -118
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +91 -63
- package/dist/config.js.map +1 -1
- package/dist/generator.d.ts +36 -2
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js +181 -175
- package/dist/generator.js.map +1 -1
- package/dist/ghost.d.ts +8 -0
- package/dist/ghost.d.ts.map +1 -1
- package/dist/ghost.js +35 -19
- package/dist/ghost.js.map +1 -1
- package/dist/github.d.ts +26 -7
- package/dist/github.d.ts.map +1 -1
- package/dist/github.js +91 -63
- package/dist/github.js.map +1 -1
- package/dist/http.d.ts +9 -0
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +47 -1
- package/dist/http.js.map +1 -1
- package/dist/index.js +55 -56
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +15 -25
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -8,202 +8,200 @@
|
|
|
8
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
9
|
<a href="LICENSE"><img src="https://img.shields.io/github/license/GeiserX/ghost-github-portfolio?style=flat-square" alt="License"></a>
|
|
10
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
|
+
<a href="https://codecov.io/gh/GeiserX/ghost-github-portfolio"><img src="https://codecov.io/gh/GeiserX/ghost-github-portfolio/graph/badge.svg" alt="codecov"></a>
|
|
11
12
|
</p>
|
|
12
13
|
|
|
13
|
-
<p align="center"><strong>
|
|
14
|
-
|
|
15
|
-
---
|
|
14
|
+
<p align="center"><strong>Keep a Ghost portfolio page in step with your GitHub repositories.</strong></p>
|
|
16
15
|
|
|
17
16
|
## What it does
|
|
18
17
|
|
|
19
|
-
`ghost-github-portfolio`
|
|
18
|
+
`ghost-github-portfolio` reads your public GitHub repositories, keeps the most starred ones, and writes them to a Ghost page as a grid of cards through the Ghost Admin API. Run it on a schedule and the page follows your star counts.
|
|
19
|
+
|
|
20
|
+
The page is one Ghost html card with its own scoped style. It holds a short intro, then a card per repository, then one line that points at your GitHub profile. Each card has:
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
- the repository banner, or a tile with the repository name when there is no usable banner
|
|
23
|
+
- the name, linked to GitHub, and the star count as plain text, such as "215 stars"
|
|
24
|
+
- one line on what the repository does
|
|
25
|
+
- a "Site" link when the repository has a homepage outside github.com
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
The grid has three columns on desktop, two on tablets and one on phones. On themes built on Ghost's content grid, such as Alto, Source and Casper, the cards take the wide column and the text stays at reading width. The colours suit a dark theme.
|
|
24
28
|
|
|
25
|
-
-
|
|
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
|
|
29
|
+
[docs/preview-desktop.png](docs/preview-desktop.png) shows a real run at 1280 px wide, and [docs/preview-mobile.png](docs/preview-mobile.png) the same page on a phone. [docs/preview-config.yml](docs/preview-config.yml) is the config behind them.
|
|
32
30
|
|
|
33
31
|
## Quick start
|
|
34
32
|
|
|
35
|
-
|
|
33
|
+
Install it, or run it with `npx`:
|
|
36
34
|
|
|
37
35
|
```bash
|
|
38
|
-
# npm (global)
|
|
39
36
|
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.3.0
|
|
46
37
|
```
|
|
47
38
|
|
|
48
|
-
|
|
39
|
+
Write an example config and edit it:
|
|
49
40
|
|
|
50
41
|
```bash
|
|
51
42
|
ghost-github-portfolio init
|
|
52
43
|
```
|
|
53
44
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
### 3. Get your Ghost Admin API key
|
|
45
|
+
Get an Admin API key in Ghost under Settings, Integrations, "Add custom integration". It looks like `KEY_ID:SECRET`.
|
|
57
46
|
|
|
58
|
-
|
|
59
|
-
2. Create a new **Custom Integration**
|
|
60
|
-
3. Copy the **Admin API Key** (format: `KEY_ID:SECRET`)
|
|
61
|
-
|
|
62
|
-
### 4. Run
|
|
47
|
+
Preview the page, then sync it:
|
|
63
48
|
|
|
64
49
|
```bash
|
|
65
|
-
#
|
|
66
|
-
ghost-github-portfolio sync --config config.yml --
|
|
50
|
+
# Builds the page and writes a standalone preview. Ghost is not touched
|
|
51
|
+
ghost-github-portfolio sync --config config.yml --html-out preview.html
|
|
67
52
|
|
|
68
|
-
#
|
|
69
|
-
ghost-github-portfolio sync --config config.yml --verbose
|
|
53
|
+
# Writes the page to Ghost
|
|
54
|
+
GHOST_ADMIN_API_KEY=KEY_ID:SECRET ghost-github-portfolio sync --config config.yml --verbose
|
|
70
55
|
```
|
|
71
56
|
|
|
57
|
+
`--dry-run` builds the page and lists each card without writing anything. `--json` prints the Ghost document instead. Both write the html card to `/tmp/ghost-portfolio-preview.html`. `--html-out <file>` also wraps it in a dark page that approximates Ghost's Alto theme, so you can open it in a browser.
|
|
58
|
+
|
|
72
59
|
## Configuration
|
|
73
60
|
|
|
74
61
|
```yaml
|
|
75
62
|
github:
|
|
76
63
|
username: YOUR_GITHUB_USERNAME
|
|
77
|
-
# token: ghp_xxx
|
|
64
|
+
# token: ghp_xxx # Optional, for higher rate limits (env: GHOST_GITHUB_TOKEN)
|
|
65
|
+
excludePatterns: # Regular expressions matched against repo names
|
|
66
|
+
- "^homebrew-"
|
|
78
67
|
|
|
79
68
|
ghost:
|
|
80
69
|
url: https://your-ghost-blog.com
|
|
81
|
-
adminApiKey: "KEY_ID:SECRET_HEX"
|
|
82
|
-
pageSlug: portfolio
|
|
70
|
+
adminApiKey: "KEY_ID:SECRET_HEX" # env GHOST_ADMIN_API_KEY wins over this
|
|
71
|
+
pageSlug: portfolio # Or pageId with the page's hex ID
|
|
72
|
+
# originUrl: http://ghost:2368 # See "Writing through the Ghost origin"
|
|
73
|
+
# hostHeader: your-ghost-blog.com
|
|
83
74
|
|
|
84
75
|
portfolio:
|
|
85
|
-
minStars: 2
|
|
86
|
-
maxRepos:
|
|
76
|
+
minStars: 2 # Only repos with at least this many stars
|
|
77
|
+
maxRepos: 20 # How many cards, counted after every filter
|
|
78
|
+
columns: 3 # Cards per row on desktop, from 1 to 6
|
|
87
79
|
includeForked: false
|
|
88
|
-
|
|
80
|
+
includeArchived: false
|
|
81
|
+
excludeAwesomeLists: true # Skip repos named "awesome*" or tagged "awesome-list"
|
|
89
82
|
showBanner: true
|
|
90
|
-
centerContent: true
|
|
91
83
|
defaultBannerPath: docs/images/banner.svg
|
|
84
|
+
intro: "I build things nobody asked for, one repo at a time. These are the ones that got the most stars on GitHub, with a line on what each one does."
|
|
92
85
|
|
|
93
|
-
bannerPaths:
|
|
94
|
-
|
|
86
|
+
bannerPaths: # Banner path per repo, tried before defaultBannerPath
|
|
87
|
+
my-project: media/banner.svg
|
|
95
88
|
|
|
96
89
|
excludeRepos:
|
|
97
90
|
- .github
|
|
98
91
|
|
|
99
|
-
repos:
|
|
92
|
+
repos: # Per-repo overrides
|
|
100
93
|
my-project:
|
|
101
|
-
description: "
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
94
|
+
description: "One line that replaces the GitHub description"
|
|
95
|
+
homepage: https://example.com/guide/ # Replaces the GitHub homepage field
|
|
96
|
+
siteLabel: Guide # Link text, "Site" by default
|
|
97
|
+
bannerPath: assets/banner.png
|
|
98
|
+
exclude: false
|
|
121
99
|
```
|
|
122
100
|
|
|
123
|
-
|
|
101
|
+
Filters run in this order: minimum stars, forks, archived, `excludeRepos`, per-repo `exclude`, `excludePatterns`, awesome lists. The tool sorts what is left by stars and only then cuts it to `maxRepos`, so an excluded repository never takes a slot.
|
|
124
102
|
|
|
125
|
-
|
|
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) |
|
|
103
|
+
A repository can also carry its own `.ghost-portfolio.yml` at the root of its default branch with `description`, `homepage`, `siteLabel` and `bannerPath`. Values in your `config.yml` win over it.
|
|
129
104
|
|
|
130
|
-
|
|
105
|
+
| Variable | Meaning |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `GHOST_ADMIN_API_KEY` | Ghost Admin API key. Overrides `ghost.adminApiKey`, which can then be left out. |
|
|
108
|
+
| `GHOST_GITHUB_TOKEN` | GitHub token for higher rate limits. Used when `github.token` is not set. The generic `GITHUB_TOKEN` is ignored on purpose. |
|
|
131
109
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
110
|
+
## Descriptions and site links
|
|
111
|
+
|
|
112
|
+
A `description` in `config.yml` is used exactly as written, so that is the place for one-liners you want to keep by hand. Otherwise the tool takes the GitHub description, or the one in `.ghost-portfolio.yml`, and cleans it up:
|
|
113
|
+
|
|
114
|
+
- it keeps the first sentence only
|
|
115
|
+
- em and en dashes become commas
|
|
116
|
+
- emoji and `:shortcodes:` are removed
|
|
117
|
+
- it is cut to 140 characters at a word boundary
|
|
118
|
+
|
|
119
|
+
The site link comes from `homepage` in the overrides, else from the repository's homepage field. A homepage on github.com is skipped, since the name already links there.
|
|
120
|
+
|
|
121
|
+
## Banners
|
|
122
|
+
|
|
123
|
+
For each repository the tool fetches the per-repo banner path if you set one, then `defaultBannerPath`, from `raw.githubusercontent.com` on the default branch. It uses the first one that passes both checks:
|
|
124
|
+
|
|
125
|
+
1. It answers HTTP 200.
|
|
126
|
+
2. If it is an SVG, it is self-contained. Every `href`, `xlink:href`, `url(...)` and `@import` must point at a `#id` inside the file or at a `data:` URI. Browsers never load other files referenced by an SVG that is shown through `<img>`, so a banner that pulls in `logo.png` or a web font comes out blank or broken on the page.
|
|
127
|
+
|
|
128
|
+
When no path passes, the card gets a tile with the repository name, and the run prints a warning that names the reason so you can fix the source:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
Warning: my-project gets a name tile: docs/images/banner.svg: not self-contained, it loads logo.png
|
|
132
|
+
Warning: other-project gets a name tile: docs/images/banner.svg: missing (HTTP 404)
|
|
136
133
|
```
|
|
137
134
|
|
|
138
|
-
|
|
135
|
+
To make an SVG self-contained, inline the images it references as base64 `data:` URIs, or redraw them as SVG shapes. The card shows the banner in a 9:2 box and crops it around the centre, so a 900x200 banner fits exactly.
|
|
136
|
+
|
|
137
|
+
## Writing through the Ghost origin
|
|
138
|
+
|
|
139
|
+
Some setups put a web application firewall in front of the public Ghost URL that rejects request bodies containing `<img`, which every portfolio update does. Set `ghost.originUrl` to reach Ghost directly instead, for example on the local network:
|
|
139
140
|
|
|
140
141
|
```yaml
|
|
141
|
-
|
|
142
|
-
ghost-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
142
|
+
ghost:
|
|
143
|
+
url: https://your-ghost-blog.com
|
|
144
|
+
originUrl: http://ghost-host:2368
|
|
145
|
+
hostHeader: your-ghost-blog.com # Defaults to the host of url
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Admin API requests then go to `originUrl` with `Host: <hostHeader>` and `X-Forwarded-Proto: https`, so Ghost answers as it does for the public site. The token audience stays `/admin/`. The tool does not follow redirects, because a redirect would lead back through the public URL. Without `originUrl` the requests go to `url` as before.
|
|
149
|
+
|
|
150
|
+
## Running on a schedule
|
|
151
|
+
|
|
152
|
+
The Docker image runs one sync and exits. To keep it running, loop inside the container and let Docker restart it after a reboot:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
docker run -d --name ghost-portfolio --restart unless-stopped \
|
|
156
|
+
-v /path/to/config.yml:/config/config.yml:ro \
|
|
157
|
+
--env-file /path/to/portfolio.env \
|
|
158
|
+
--entrypoint sh \
|
|
159
|
+
drumsergio/ghost-github-portfolio:0.4.0 \
|
|
160
|
+
-c 'while true; do node dist/index.js sync --config /config/config.yml; sleep 86400; done'
|
|
147
161
|
```
|
|
148
162
|
|
|
149
|
-
|
|
163
|
+
`portfolio.env` holds `GHOST_ADMIN_API_KEY=KEY_ID:SECRET` and, if you want it, `GHOST_GITHUB_TOKEN`.
|
|
164
|
+
|
|
165
|
+
Or run it from the host's cron, once a day at 06:00:
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
0 6 * * * docker run --rm -v /path/to/config.yml:/config/config.yml:ro --env-file /path/to/portfolio.env drumsergio/ghost-github-portfolio:0.4.0
|
|
169
|
+
```
|
|
150
170
|
|
|
151
|
-
|
|
171
|
+
A GitHub Actions schedule works too, as long as the runner can reach your Ghost URL. A GitHub-hosted runner cannot reach an `originUrl` on a private network.
|
|
152
172
|
|
|
153
173
|
```yaml
|
|
154
|
-
name: Update
|
|
174
|
+
name: Update portfolio
|
|
155
175
|
|
|
156
176
|
on:
|
|
157
177
|
schedule:
|
|
158
|
-
- cron: "0 6 * * *"
|
|
159
|
-
workflow_dispatch:
|
|
178
|
+
- cron: "0 6 * * *"
|
|
179
|
+
workflow_dispatch:
|
|
160
180
|
|
|
161
181
|
jobs:
|
|
162
182
|
sync:
|
|
163
183
|
runs-on: ubuntu-latest
|
|
164
184
|
steps:
|
|
165
185
|
- uses: actions/checkout@v4
|
|
166
|
-
|
|
167
186
|
- uses: actions/setup-node@v4
|
|
168
187
|
with:
|
|
169
188
|
node-version: 22
|
|
170
|
-
|
|
171
|
-
- run: npx ghost-github-portfolio sync --config config.yml --verbose
|
|
189
|
+
- run: npx ghost-github-portfolio@0.4.0 sync --config config.yml --verbose
|
|
172
190
|
env:
|
|
173
|
-
|
|
191
|
+
GHOST_ADMIN_API_KEY: ${{ secrets.GHOST_ADMIN_API_KEY }}
|
|
192
|
+
GHOST_GITHUB_TOKEN: ${{ github.token }}
|
|
174
193
|
```
|
|
175
194
|
|
|
176
|
-
|
|
195
|
+
Keep `config.yml` in the repository without the key, and store the key as a secret.
|
|
177
196
|
|
|
178
197
|
## How it works
|
|
179
198
|
|
|
180
|
-
1.
|
|
181
|
-
2.
|
|
182
|
-
3.
|
|
183
|
-
4.
|
|
184
|
-
5.
|
|
185
|
-
6.
|
|
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.
|
|
199
|
+
1. Fetches every public repository the user owns from the GitHub REST API. The API cannot sort by stars, so it reads all pages and sorts locally.
|
|
200
|
+
2. Applies the filters, sorts by stars and keeps `maxRepos`.
|
|
201
|
+
3. Reads each repository's `.ghost-portfolio.yml`, if any.
|
|
202
|
+
4. Checks each banner as described above.
|
|
203
|
+
5. Builds one html card and wraps it in the Ghost lexical document format.
|
|
204
|
+
6. Fetches the page to get its `updated_at`, then writes the new content through the Admin API with a short-lived JWT.
|
|
207
205
|
|
|
208
206
|
## License
|
|
209
207
|
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Config } from "./types.js";
|
|
2
|
+
export declare const DEFAULT_INTRO = "I build things nobody asked for, one repo at a time. These are the ones that got the most stars on GitHub, with a line on what each one does.";
|
|
3
|
+
export declare const DEFAULT_EXCLUDE_PATTERNS: string[];
|
|
2
4
|
export declare function loadConfig(path: string): Config;
|
|
3
5
|
export declare function generateExampleConfig(): string;
|
|
4
6
|
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAmB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAmB,MAAM,YAAY,CAAC;AAE1D,eAAO,MAAM,aAAa,kJACuH,CAAC;AAElJ,eAAO,MAAM,wBAAwB,UAAiB,CAAC;AAqBvD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA8E/C;AAgBD,wBAAgB,qBAAqB,IAAI,MAAM,CA2C9C"}
|
package/dist/config.js
CHANGED
|
@@ -1,118 +1,146 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { parse } from "yaml";
|
|
3
|
+
export const DEFAULT_INTRO = "I build things nobody asked for, one repo at a time. These are the ones that got the most stars on GitHub, with a line on what each one does.";
|
|
4
|
+
export const DEFAULT_EXCLUDE_PATTERNS = ["^homebrew-"];
|
|
3
5
|
const DEFAULT_PORTFOLIO = {
|
|
4
6
|
minStars: 2,
|
|
5
|
-
maxRepos:
|
|
7
|
+
maxRepos: 20,
|
|
8
|
+
columns: 3,
|
|
6
9
|
excludeRepos: [],
|
|
7
10
|
includeForked: false,
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
includeArchived: false,
|
|
12
|
+
excludeAwesomeLists: true,
|
|
10
13
|
showBanner: true,
|
|
11
|
-
centerContent: true,
|
|
12
14
|
defaultBannerPath: "docs/images/banner.svg",
|
|
13
15
|
bannerPaths: {},
|
|
16
|
+
intro: DEFAULT_INTRO,
|
|
14
17
|
repos: {},
|
|
15
|
-
footer: { showStats: true, showViewAll: true },
|
|
16
18
|
};
|
|
19
|
+
function trimSlash(url) {
|
|
20
|
+
return url.replace(/\/+$/, "");
|
|
21
|
+
}
|
|
17
22
|
export function loadConfig(path) {
|
|
18
23
|
const raw = readFileSync(path, "utf-8");
|
|
19
|
-
const parsed = parse(raw);
|
|
20
|
-
|
|
24
|
+
const parsed = (parse(raw) ?? {});
|
|
25
|
+
const github = (parsed.github ?? {});
|
|
26
|
+
const ghost = (parsed.ghost ?? {});
|
|
27
|
+
if (!github.username) {
|
|
21
28
|
throw new Error("Config: github.username is required");
|
|
22
29
|
}
|
|
23
|
-
if (!
|
|
30
|
+
if (!ghost.url) {
|
|
24
31
|
throw new Error("Config: ghost.url is required");
|
|
25
32
|
}
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
// The environment wins over the config file, so the key can stay out of it.
|
|
34
|
+
const ghostKey = process.env.GHOST_ADMIN_API_KEY || ghost.adminApiKey;
|
|
35
|
+
if (!ghostKey) {
|
|
36
|
+
throw new Error("Config: ghost.adminApiKey is required (or set GHOST_ADMIN_API_KEY)");
|
|
28
37
|
}
|
|
29
|
-
const ghost = parsed.ghost;
|
|
30
38
|
if (!ghost.pageId && !ghost.pageSlug) {
|
|
31
39
|
throw new Error("Config: ghost.pageId or ghost.pageSlug is required");
|
|
32
40
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// Env var overrides (GHOST_GITHUB_TOKEN only — GITHUB_TOKEN is not used to
|
|
36
|
-
// avoid picking up unrelated tokens from gh CLI or CI environments)
|
|
41
|
+
// GHOST_GITHUB_TOKEN only. GITHUB_TOKEN is not used, to avoid picking up
|
|
42
|
+
// unrelated tokens from the gh CLI or CI environments.
|
|
37
43
|
const ghToken = github.token ?? process.env.GHOST_GITHUB_TOKEN;
|
|
38
|
-
const
|
|
44
|
+
const excludePatterns = (github.excludePatterns ??
|
|
45
|
+
DEFAULT_EXCLUDE_PATTERNS);
|
|
46
|
+
if (!Array.isArray(excludePatterns)) {
|
|
47
|
+
throw new Error("Config: github.excludePatterns must be a list");
|
|
48
|
+
}
|
|
49
|
+
for (const pattern of excludePatterns) {
|
|
50
|
+
try {
|
|
51
|
+
new RegExp(String(pattern), "i");
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new Error(`Config: github.excludePatterns has an invalid pattern: ${pattern}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const portfolio = (parsed.portfolio ?? {});
|
|
58
|
+
const merged = {
|
|
59
|
+
...DEFAULT_PORTFOLIO,
|
|
60
|
+
...portfolio,
|
|
61
|
+
bannerPaths: { ...portfolio.bannerPaths },
|
|
62
|
+
repos: { ...portfolio.repos },
|
|
63
|
+
excludeRepos: portfolio.excludeRepos ?? [],
|
|
64
|
+
};
|
|
65
|
+
if (!Number.isInteger(merged.columns) || merged.columns < 1 || merged.columns > 6) {
|
|
66
|
+
throw new Error("Config: portfolio.columns must be a whole number from 1 to 6");
|
|
67
|
+
}
|
|
68
|
+
if (!Number.isInteger(merged.maxRepos) || merged.maxRepos < 1) {
|
|
69
|
+
throw new Error("Config: portfolio.maxRepos must be a whole number above 0");
|
|
70
|
+
}
|
|
39
71
|
return {
|
|
40
72
|
github: {
|
|
41
73
|
username: github.username,
|
|
42
74
|
token: ghToken,
|
|
75
|
+
excludePatterns: excludePatterns.map(String),
|
|
43
76
|
},
|
|
44
77
|
ghost: {
|
|
45
|
-
url: ghost.url
|
|
78
|
+
url: trimSlash(ghost.url),
|
|
46
79
|
adminApiKey: ghostKey,
|
|
47
80
|
pageId: ghost.pageId,
|
|
48
81
|
pageSlug: ghost.pageSlug,
|
|
82
|
+
originUrl: ghost.originUrl ? validateOriginUrl(ghost.originUrl) : undefined,
|
|
83
|
+
hostHeader: ghost.hostHeader,
|
|
49
84
|
},
|
|
50
|
-
portfolio:
|
|
51
|
-
...DEFAULT_PORTFOLIO,
|
|
52
|
-
...portfolio,
|
|
53
|
-
footer: { ...DEFAULT_PORTFOLIO.footer, ...portfolio.footer },
|
|
54
|
-
bannerPaths: {
|
|
55
|
-
...DEFAULT_PORTFOLIO.bannerPaths,
|
|
56
|
-
...portfolio.bannerPaths,
|
|
57
|
-
},
|
|
58
|
-
repos: { ...DEFAULT_PORTFOLIO.repos, ...portfolio.repos },
|
|
59
|
-
},
|
|
85
|
+
portfolio: merged,
|
|
60
86
|
};
|
|
61
87
|
}
|
|
88
|
+
/** The origin route carries the Admin key in a JWT, so the scheme must be plain http: or https:. */
|
|
89
|
+
function validateOriginUrl(value) {
|
|
90
|
+
let parsed;
|
|
91
|
+
try {
|
|
92
|
+
parsed = new URL(value);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
throw new Error("Config: ghost.originUrl must be a full URL such as http://ghost:2368");
|
|
96
|
+
}
|
|
97
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
98
|
+
throw new Error("Config: ghost.originUrl must start with http:// or https://");
|
|
99
|
+
}
|
|
100
|
+
return trimSlash(value);
|
|
101
|
+
}
|
|
62
102
|
export function generateExampleConfig() {
|
|
63
103
|
return `# ghost-github-portfolio configuration
|
|
64
104
|
github:
|
|
65
105
|
username: YOUR_GITHUB_USERNAME
|
|
66
|
-
# token: ghp_xxx
|
|
106
|
+
# token: ghp_xxx # Optional, for higher rate limits (env: GHOST_GITHUB_TOKEN)
|
|
107
|
+
excludePatterns: # Regular expressions matched against repo names
|
|
108
|
+
- "^homebrew-"
|
|
67
109
|
|
|
68
110
|
ghost:
|
|
69
111
|
url: https://your-ghost-blog.com
|
|
70
|
-
adminApiKey: "KEY_ID:SECRET_HEX" #
|
|
71
|
-
pageSlug: portfolio #
|
|
112
|
+
adminApiKey: "KEY_ID:SECRET_HEX" # Ghost Admin > Integrations (env GHOST_ADMIN_API_KEY wins)
|
|
113
|
+
pageSlug: portfolio # Or pageId with the page's hex ID
|
|
114
|
+
# originUrl: http://ghost:2368 # Send Admin API requests straight to the Ghost origin
|
|
115
|
+
# hostHeader: your-ghost-blog.com # Host header for originUrl (defaults to the host of url)
|
|
72
116
|
|
|
73
117
|
portfolio:
|
|
74
|
-
minStars: 2
|
|
75
|
-
maxRepos:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
118
|
+
minStars: 2 # Only repos with at least this many stars
|
|
119
|
+
maxRepos: 20 # How many cards to show, counted after every filter
|
|
120
|
+
columns: 3 # Cards per row on desktop (two on tablets, one on phones)
|
|
121
|
+
includeForked: false
|
|
122
|
+
includeArchived: false
|
|
123
|
+
excludeAwesomeLists: true # Skip repos named "awesome*" or tagged "awesome-list"
|
|
124
|
+
showBanner: true # Use the repo banner when it passes the checks, else a name tile
|
|
125
|
+
defaultBannerPath: docs/images/banner.svg
|
|
81
126
|
|
|
82
|
-
|
|
127
|
+
intro: "${DEFAULT_INTRO}"
|
|
83
128
|
|
|
84
|
-
#
|
|
85
|
-
bannerPaths:
|
|
86
|
-
#
|
|
129
|
+
# Banner path for specific repos, tried before defaultBannerPath
|
|
130
|
+
bannerPaths: {}
|
|
131
|
+
# my-project: media/banner.svg
|
|
87
132
|
|
|
88
133
|
excludeRepos:
|
|
89
|
-
- .github
|
|
90
|
-
# - my-private-project
|
|
134
|
+
- .github
|
|
91
135
|
|
|
92
136
|
# Per-repo overrides
|
|
93
137
|
repos: {}
|
|
94
138
|
# my-project:
|
|
95
|
-
# description: "
|
|
96
|
-
#
|
|
97
|
-
#
|
|
139
|
+
# description: "One line that replaces the GitHub description"
|
|
140
|
+
# homepage: https://example.com/guide/ # Replaces the GitHub homepage field
|
|
141
|
+
# siteLabel: Guide # Link text, defaults to "Site"
|
|
98
142
|
# bannerPath: assets/banner.png
|
|
99
|
-
#
|
|
100
|
-
# - type: website
|
|
101
|
-
# url: https://my-project.com
|
|
102
|
-
# - type: awesome-list
|
|
103
|
-
# - type: platform
|
|
104
|
-
# label: macOS
|
|
105
|
-
# logo: apple
|
|
106
|
-
# - type: docs
|
|
107
|
-
# url: https://docs.my-project.com
|
|
108
|
-
# - type: custom
|
|
109
|
-
# label: MCP
|
|
110
|
-
# value: Official Registry
|
|
111
|
-
# color: E6522C
|
|
112
|
-
|
|
113
|
-
footer:
|
|
114
|
-
showStats: true # Show total stars and repo count
|
|
115
|
-
showViewAll: true # Show "View All Repositories" link
|
|
143
|
+
# exclude: false
|
|
116
144
|
`;
|
|
117
145
|
}
|
|
118
146
|
//# sourceMappingURL=config.js.map
|
package/dist/config.js.map
CHANGED
|
@@ -1 +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,
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/generator.d.ts
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
import type { Config, GitHubRepo, LexicalDocument } from "./types.js";
|
|
2
|
+
export declare const MAX_DESCRIPTION_LENGTH = 140;
|
|
3
|
+
/**
|
|
4
|
+
* Turn a GitHub description into one short line: emoji and :shortcodes:
|
|
5
|
+
* removed, em and en dashes turned into commas, first sentence only,
|
|
6
|
+
* at most 140 characters.
|
|
7
|
+
*/
|
|
8
|
+
export declare function cleanDescription(text: string, max?: number): string;
|
|
9
|
+
export declare function formatStars(count: number): string;
|
|
10
|
+
export interface SiteLink {
|
|
11
|
+
url: string;
|
|
12
|
+
label: string;
|
|
13
|
+
}
|
|
14
|
+
/** The card's site link: the override, else the GitHub homepage unless it points at github.com. */
|
|
15
|
+
export declare function siteLink(repo: GitHubRepo, config: Config): SiteLink | null;
|
|
16
|
+
export declare function repoDescription(repo: GitHubRepo, config: Config): string;
|
|
2
17
|
export declare function generateCard(repo: GitHubRepo, bannerUrl: string | null, config: Config): string;
|
|
3
|
-
export
|
|
4
|
-
|
|
18
|
+
export interface PortfolioInput {
|
|
19
|
+
/** Card HTML from generateCard, in display order. */
|
|
20
|
+
cards: string[];
|
|
21
|
+
/** Number of public repositories the user owns, used for the closing line. */
|
|
22
|
+
totalRepos: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The whole page as the HTML of one Ghost html card. The `kg-width-wide`
|
|
26
|
+
* class lets themes built on Ghost's content grid (Alto, Source, Casper)
|
|
27
|
+
* give the cards the wide column; the text stays at reading width.
|
|
28
|
+
*/
|
|
29
|
+
export declare function generatePortfolioHtml(input: PortfolioInput, config: Config): string;
|
|
30
|
+
export declare function buildLexical(html: string): LexicalDocument;
|
|
31
|
+
/** The html card as Ghost renders it, with its begin and end markers. */
|
|
32
|
+
export declare function wrapHtmlCard(html: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* A standalone dark page that approximates Ghost's Alto theme in dark mode
|
|
35
|
+
* (content grid, widths, the .gh-content rules that reach into an html
|
|
36
|
+
* card), for checking the output in a browser before syncing.
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildPreviewPage(html: string, title?: string): string;
|
|
5
39
|
//# sourceMappingURL=generator.d.ts.map
|
package/dist/generator.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../src/generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../src/generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEtE,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAK1C;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,GAAG,SAAyB,GAC3B,MAAM,CA4BR;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED,mGAAmG;AACnG,wBAAgB,QAAQ,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAgB1E;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAIxE;AAED,wBAAgB,YAAY,CAC1B,IAAI,EAAE,UAAU,EAChB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,MAAM,EAAE,MAAM,GACb,MAAM,CA0BR;AAsCD,MAAM,WAAW,cAAc;IAC7B,qDAAqD;IACrD,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,8EAA8E;IAC9E,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,cAAc,EACrB,MAAM,EAAE,MAAM,GACb,MAAM,CAiBR;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,CAW1D;AAED,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,SAAc,GAAG,MAAM,CAwC1E"}
|