ghost-github-portfolio 0.3.5 → 0.4.1

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 CHANGED
@@ -1,210 +1,51 @@
1
1
  <p align="center">
2
- <img src="docs/images/banner.svg" alt="Ghost GitHub Portfolio banner" width="900"/>
2
+ <img src="https://raw.githubusercontent.com/GeiserX/ghost-github-portfolio/main/docs/images/banner.svg" alt="Ghost GitHub Portfolio" width="900"/>
3
3
  </p>
4
4
 
5
5
  <p align="center">
6
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
7
  <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>
8
+ <a href="https://github.com/GeiserX/ghost-github-portfolio/blob/main/LICENSE"><img src="https://img.shields.io/github/license/GeiserX/ghost-github-portfolio?style=flat-square" alt="License"></a>
9
+ <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>
10
+ <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
11
  </p>
12
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
13
+ <p align="center"><strong>Keep a Ghost portfolio page in step with your GitHub repositories.</strong></p>
18
14
 
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.
15
+ `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 as a CLI, in Docker, or as a GitHub Action on a schedule, and the page follows your star counts.
20
16
 
21
- Run it daily via cron, GitHub Actions, or Docker to keep your portfolio always up to date.
17
+ <p align="center"><img src="https://raw.githubusercontent.com/GeiserX/ghost-github-portfolio/main/docs/images/screenshots/preview-desktop.png" alt="Portfolio page preview" width="900"/></p>
22
18
 
23
- **Features:**
19
+ ## Features
24
20
 
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
21
+ - One Ghost html card with its own scoped style: an intro, a card per repository, and a line pointing at your GitHub profile.
22
+ - Each card has the repository banner (or a name tile), the name and star count, one line on what it does, and a "Site" link.
23
+ - Three columns on desktop, two on tablets, one on phones; wide cards on Alto, Source and Casper.
24
+ - Filters by stars, forks, archived, name patterns and awesome lists, then cuts to `maxRepos`.
25
+ - Per-repo overrides in `config.yml` or in a `.ghost-portfolio.yml` inside each repository.
26
+ - Checks that each SVG banner is self-contained, and warns when it falls back to a name tile.
27
+ - Can write through the Ghost origin when a firewall blocks `<img` in request bodies.
28
+ - `--dry-run`, `--json` and `--html-out` previews that never touch Ghost.
32
29
 
33
30
  ## Quick start
34
31
 
35
- ### 1. Install
36
-
37
32
  ```bash
38
- # npm (global)
39
33
  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
- ```
47
-
48
- ### 2. Create a config
49
-
50
- ```bash
51
34
  ghost-github-portfolio init
35
+ ghost-github-portfolio sync --config config.yml --html-out preview.html
52
36
  ```
53
37
 
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.3.0
136
- ```
137
-
138
- ### Docker Compose (daily cron)
139
-
140
- ```yaml
141
- services:
142
- ghost-portfolio:
143
- image: drumsergio/ghost-github-portfolio:0.3.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
38
+ Open `preview.html` in a browser; Ghost is not touched. When the preview looks right, drop `--html-out` and set `GHOST_ADMIN_API_KEY` (Ghost, Settings, Integrations, "Add custom integration") to write the page. [Usage](https://github.com/GeiserX/ghost-github-portfolio/blob/main/docs/usage.md) has the other install paths.
191
39
 
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 |
40
+ ## Documentation
203
41
 
204
- ## Roadmap
42
+ - [Configuration](https://github.com/GeiserX/ghost-github-portfolio/blob/main/docs/configuration.md): `config.yml`, filters, environment variables, descriptions, banners, writing through the Ghost origin
43
+ - [Usage](https://github.com/GeiserX/ghost-github-portfolio/blob/main/docs/usage.md): install, init, preview and sync
44
+ - [How it works](https://github.com/GeiserX/ghost-github-portfolio/blob/main/docs/how-it-works.md): the page layout and the sync steps
45
+ - [Running on a schedule](https://github.com/GeiserX/ghost-github-portfolio/blob/main/docs/scheduling.md): the GitHub Action, cron and a Docker loop
205
46
 
206
- See [docs/ROADMAP.md](docs/ROADMAP.md) for the full roadmap — themes, multi-CMS support, AI features, analytics, team portfolios, and more.
47
+ The roadmap is in [docs/ROADMAP.md](https://github.com/GeiserX/ghost-github-portfolio/blob/main/docs/ROADMAP.md).
207
48
 
208
49
  ## License
209
50
 
210
- [GPL-3.0](LICENSE)
51
+ [GPL-3.0-or-later](https://github.com/GeiserX/ghost-github-portfolio/blob/main/LICENSE)
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
@@ -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;AAiB1D,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAkD/C;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAuD9C"}
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: 50,
7
+ maxRepos: 20,
8
+ columns: 3,
6
9
  excludeRepos: [],
7
10
  includeForked: false,
8
- excludeAwesomeLists: false,
9
- badgeStyle: "for-the-badge",
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
- if (!parsed.github || !parsed.github.username) {
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 (!parsed.ghost || !parsed.ghost.url) {
30
+ if (!ghost.url) {
24
31
  throw new Error("Config: ghost.url is required");
25
32
  }
26
- if (!parsed.ghost.adminApiKey) {
27
- throw new Error("Config: ghost.adminApiKey is required");
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
- const github = parsed.github;
34
- const portfolio = (parsed.portfolio ?? {});
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 ghostKey = ghost.adminApiKey ?? process.env.GHOST_ADMIN_API_KEY;
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.replace(/\/$/, ""),
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 # Optional: for private repos or higher rate limits (env: GHOST_GITHUB_TOKEN)
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" # From Ghost Admin > Integrations (env: GHOST_ADMIN_API_KEY)
71
- pageSlug: portfolio # Update this page (or use pageId for the hex ID)
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 # Only show repos with at least this many stars
75
- maxRepos: 50 # Maximum number of repos to display
76
- includeForked: false # Skip forked repos
77
- excludeAwesomeLists: false # Skip repos starting with "awesome" or tagged "awesome-list"
78
- badgeStyle: for-the-badge # shields.io style: flat, flat-square, for-the-badge, plastic, social
79
- showBanner: true # Show repo banner images (SVG/PNG from the repo)
80
- centerContent: true # Center project names and badges
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
- defaultBannerPath: docs/images/banner.svg # Default location to look for banners
127
+ intro: "${DEFAULT_INTRO}"
83
128
 
84
- # Override banner path for specific repos
85
- bannerPaths:
86
- # awesome-spain: media/banner.svg
129
+ # Banner path for specific repos, tried before defaultBannerPath
130
+ bannerPaths: {}
131
+ # my-project: media/banner.svg
87
132
 
88
133
  excludeRepos:
89
- - .github # Profile config repo
90
- # - my-private-project
134
+ - .github
91
135
 
92
136
  # Per-repo overrides
93
137
  repos: {}
94
138
  # my-project:
95
- # description: "Custom description (overrides GitHub)"
96
- # dockerImage: drumsergio/my-project # Adds Docker pulls badge
97
- # techStack: "Python, Docker, Redis"
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
- # badges:
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
@@ -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,mBAAmB,EAAE,KAAK;IAC1B,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqDR,CAAC;AACF,CAAC"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -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 declare function generateFooter(allRepos: GitHubRepo[], config: Config): string | null;
4
- export declare function buildLexical(cards: string[], footer: string | null): LexicalDocument;
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
@@ -1 +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,CA+DR;AAuCD,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,UAAU,EAAE,EACtB,MAAM,EAAE,MAAM,GACb,MAAM,GAAG,IAAI,CA2Cf;AAED,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,EAAE,EACf,MAAM,EAAE,MAAM,GAAG,IAAI,GACpB,eAAe,CA0BjB"}
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"}