@rmdes/indiekit-endpoint-github 1.0.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,107 @@
1
+ # @rmdes/indiekit-endpoint-github
2
+
3
+ A GitHub activity endpoint plugin for [Indiekit](https://getindiekit.com). Display your GitHub commits, stars, contributions, and featured repositories in your Indiekit admin dashboard, with a public JSON API for use in your static site.
4
+
5
+ ## Features
6
+
7
+ - **Featured Projects**: Showcase specific repositories with recent commits
8
+ - **Recent Commits**: Display your latest commits across repositories
9
+ - **Starred Repositories**: Show repos you've recently starred
10
+ - **PRs & Issues**: Track your open source contributions
11
+ - **Public API**: JSON endpoints for integrating with static site generators
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @rmdes/indiekit-endpoint-github
17
+ ```
18
+
19
+ ## Configuration
20
+
21
+ Add the plugin to your Indiekit configuration:
22
+
23
+ ```javascript
24
+ export default {
25
+ plugins: [
26
+ "@rmdes/indiekit-endpoint-github",
27
+ // ... other plugins
28
+ ],
29
+
30
+ "@rmdes/indiekit-endpoint-github": {
31
+ // Required: Your GitHub username
32
+ username: "your-username",
33
+
34
+ // Optional: GitHub personal access token for higher rate limits
35
+ // and access to private repos (set via GITHUB_TOKEN env var)
36
+ token: process.env.GITHUB_TOKEN,
37
+
38
+ // Optional: Mount path for the endpoint (default: "/github")
39
+ mountPath: "/github-api",
40
+
41
+ // Optional: Cache duration in milliseconds (default: 15 minutes)
42
+ cacheTtl: 900_000,
43
+
44
+ // Optional: Limits for each section
45
+ limits: {
46
+ commits: 10,
47
+ stars: 20,
48
+ contributions: 10,
49
+ repos: 10,
50
+ },
51
+
52
+ // Optional: Featured repositories to showcase with commits
53
+ featuredRepos: [
54
+ "owner/repo-name",
55
+ "owner/another-repo",
56
+ ],
57
+ },
58
+ };
59
+ ```
60
+
61
+ ## Endpoints
62
+
63
+ ### Protected (require authentication)
64
+
65
+ - `GET /github-api/` - Dashboard view with all sections
66
+ - `GET /github-api/commits` - Recent commits page
67
+ - `GET /github-api/stars` - Starred repositories page
68
+ - `GET /github-api/contributions` - PRs & Issues page
69
+ - `GET /github-api/activity` - Activity feed page
70
+
71
+ ### Public API (no authentication)
72
+
73
+ - `GET /github-api/api/commits` - JSON: Recent commits
74
+ - `GET /github-api/api/stars` - JSON: Starred repositories
75
+ - `GET /github-api/api/activity` - JSON: Activity feed
76
+ - `GET /github-api/api/featured` - JSON: Featured repositories with commits
77
+
78
+ ## Using the Public API
79
+
80
+ The public API endpoints return JSON and can be used by your static site generator (Eleventy, Hugo, etc.) to display GitHub activity on your public site.
81
+
82
+ Example Eleventy data file:
83
+
84
+ ```javascript
85
+ import EleventyFetch from "@11ty/eleventy-fetch";
86
+
87
+ export default async function() {
88
+ const url = "https://your-site.com/github-api/api/featured";
89
+
90
+ const data = await EleventyFetch(url, {
91
+ duration: "15m",
92
+ type: "json",
93
+ });
94
+
95
+ return data.featured || [];
96
+ }
97
+ ```
98
+
99
+ ## Environment Variables
100
+
101
+ - `GITHUB_TOKEN` - Optional GitHub personal access token for:
102
+ - Higher API rate limits (5000/hour vs 60/hour)
103
+ - Access to private repository information
104
+
105
+ ## License
106
+
107
+ MIT
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
2
+ <path fill="#24292f" d="M0 0h96v96H0z"/>
3
+ <path fill="#fff" d="M48 16c-17.7 0-32 14.3-32 32 0 14.1 9.2 26.1 21.9 30.4 1.6.3 2.2-.7 2.2-1.5v-5.3c-8.9 1.9-10.8-4.3-10.8-4.3-1.5-3.7-3.6-4.7-3.6-4.7-2.9-2 .2-2 .2-2 3.2.2 4.9 3.3 4.9 3.3 2.9 4.9 7.5 3.5 9.3 2.7.3-2.1 1.1-3.5 2-4.3-7.1-.8-14.6-3.6-14.6-15.8 0-3.5 1.2-6.3 3.3-8.6-.3-.8-1.4-4.1.3-8.5 0 0 2.7-.9 8.8 3.3 2.6-.7 5.3-1.1 8-1.1s5.5.4 8 1.1c6.1-4.1 8.8-3.3 8.8-3.3 1.7 4.4.7 7.7.3 8.5 2 2.3 3.3 5.1 3.3 8.6 0 12.3-7.5 15-14.6 15.8 1.1 1 2.2 2.9 2.2 5.9v8.7c0 .8.6 1.8 2.2 1.5C70.8 74.1 80 62.1 80 48c0-17.7-14.3-32-32-32z"/>
4
+ </svg>
@@ -0,0 +1,245 @@
1
+ /* GitHub endpoint styles */
2
+
3
+ /* Section spacing */
4
+ .github-section {
5
+ margin-block-end: var(--space-xl);
6
+ }
7
+
8
+ .github-section h2 {
9
+ margin-block-end: var(--space-m);
10
+ }
11
+
12
+ .github-profile {
13
+ align-items: center;
14
+ display: flex;
15
+ gap: var(--space-m);
16
+ margin-block-end: var(--space-l);
17
+ }
18
+
19
+ .github-profile__avatar {
20
+ border-radius: 50%;
21
+ flex-shrink: 0;
22
+ }
23
+
24
+ .github-profile__info {
25
+ display: flex;
26
+ flex-direction: column;
27
+ gap: var(--space-2xs);
28
+ }
29
+
30
+ .github-profile__info p {
31
+ color: var(--color-text-secondary);
32
+ margin: 0;
33
+ }
34
+
35
+ .github-profile__link {
36
+ font-size: var(--step--1);
37
+ }
38
+
39
+ /* GitHub list */
40
+ .github-list {
41
+ display: flex;
42
+ flex-direction: column;
43
+ gap: var(--space-s);
44
+ list-style: none;
45
+ margin: 0;
46
+ padding: 0;
47
+ }
48
+
49
+ .github-list--compact {
50
+ gap: var(--space-2xs);
51
+ }
52
+
53
+ .github-list--grid {
54
+ display: grid;
55
+ gap: var(--space-m);
56
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
57
+ }
58
+
59
+ .github-list__item {
60
+ align-items: baseline;
61
+ display: flex;
62
+ flex-wrap: wrap;
63
+ gap: var(--space-xs);
64
+ }
65
+
66
+ /* Activity styles */
67
+ .github-activity {
68
+ align-items: center;
69
+ }
70
+
71
+ .github-activity__avatar {
72
+ border-radius: 50%;
73
+ flex-shrink: 0;
74
+ }
75
+
76
+ .github-activity__content {
77
+ display: flex;
78
+ flex-wrap: wrap;
79
+ gap: var(--space-xs);
80
+ }
81
+
82
+ .github-activity__detail {
83
+ color: var(--color-text-secondary);
84
+ }
85
+
86
+ /* Commit styles (in main list) */
87
+ .github-commit__message {
88
+ flex: 1;
89
+ min-inline-size: 200px;
90
+ }
91
+
92
+ /* Commit list styles (inside featured repos) */
93
+ .github-commit-list {
94
+ display: flex;
95
+ flex-direction: column;
96
+ font-size: var(--step--1);
97
+ gap: var(--space-xs);
98
+ list-style: none;
99
+ margin: var(--space-s) 0 0;
100
+ padding: 0;
101
+ }
102
+
103
+ .github-commit-list code {
104
+ background: var(--color-background);
105
+ border-radius: var(--radius-s);
106
+ padding: var(--space-3xs) var(--space-2xs);
107
+ }
108
+
109
+ .github-commit-list li {
110
+ display: flex;
111
+ flex-wrap: wrap;
112
+ gap: var(--space-xs);
113
+ }
114
+
115
+ .github-commit-list small {
116
+ color: var(--color-text-secondary);
117
+ }
118
+
119
+ /* Featured repos styles */
120
+ .github-featured {
121
+ display: grid;
122
+ gap: var(--space-l);
123
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
124
+ margin-block-end: var(--space-xl);
125
+ }
126
+
127
+ .github-featured__commits {
128
+ border-block-start: 1px solid var(--color-border);
129
+ margin-block-start: var(--space-s);
130
+ padding-block-start: var(--space-s);
131
+ }
132
+
133
+ .github-featured__commits summary {
134
+ color: var(--color-text-secondary);
135
+ cursor: pointer;
136
+ font-size: var(--step--1);
137
+ }
138
+
139
+ .github-featured__desc {
140
+ color: var(--color-text-secondary);
141
+ font-size: var(--step--1);
142
+ margin: 0 0 var(--space-s);
143
+ }
144
+
145
+ .github-featured__meta {
146
+ color: var(--color-text-secondary);
147
+ display: flex;
148
+ font-size: var(--step--1);
149
+ gap: var(--space-s);
150
+ margin-block-end: var(--space-s);
151
+ }
152
+
153
+ .github-featured__name {
154
+ font-size: var(--step-0);
155
+ margin: 0 0 var(--space-xs);
156
+ }
157
+
158
+ .github-featured__repo {
159
+ background: var(--color-offset);
160
+ border-radius: var(--radius-m);
161
+ padding: var(--space-m);
162
+ }
163
+
164
+ /* Language indicator */
165
+ .github-lang {
166
+ margin-inline-end: var(--space-xs);
167
+ }
168
+
169
+ /* Metadata */
170
+ .github-meta {
171
+ color: var(--color-text-secondary);
172
+ font-size: var(--step--1);
173
+ }
174
+
175
+ /* Repo card styles */
176
+ .github-repo {
177
+ flex-direction: column;
178
+ }
179
+
180
+ .github-repo__desc {
181
+ color: var(--color-text-secondary);
182
+ font-size: var(--step--1);
183
+ margin: 0;
184
+ }
185
+
186
+ .github-repo__forks,
187
+ .github-repo__stars {
188
+ margin-inline-start: var(--space-xs);
189
+ }
190
+
191
+ .github-repo__name {
192
+ font-weight: var(--font-weight-semibold);
193
+ }
194
+
195
+ /* SHA badge styles */
196
+ .github-sha {
197
+ background: var(--color-offset);
198
+ border-radius: var(--radius-s);
199
+ font-size: var(--step--1);
200
+ padding-block: var(--space-3xs);
201
+ padding-inline: var(--space-2xs);
202
+ }
203
+
204
+ /* Star styles */
205
+ .github-star {
206
+ flex-direction: column;
207
+ }
208
+
209
+ .github-star__count {
210
+ margin-inline-start: var(--space-xs);
211
+ }
212
+
213
+ .github-star__desc {
214
+ color: var(--color-text-secondary);
215
+ font-size: var(--step--1);
216
+ margin: 0;
217
+ }
218
+
219
+ .github-star__name {
220
+ font-weight: var(--font-weight-semibold);
221
+ }
222
+
223
+ /*
224
+ * Link styles - grouped by specificity (ascending)
225
+ * Non-pseudo selectors first, then pseudo selectors
226
+ */
227
+
228
+ /* Specificity: 0,1,1 (one class, one element) */
229
+ .github-featured__name a {
230
+ text-decoration: none;
231
+ }
232
+
233
+ .github-sha a {
234
+ text-decoration: none;
235
+ }
236
+
237
+ /* Specificity: 0,1,2 (one class, two elements) */
238
+ .github-commit-list code a {
239
+ text-decoration: none;
240
+ }
241
+
242
+ /* Pseudo-class selectors */
243
+ .github-featured__name a:hover {
244
+ text-decoration: underline;
245
+ }
@@ -0,0 +1,13 @@
1
+ {# Commits partial for embedding in other templates #}
2
+ {% if commits and commits.length > 0 %}
3
+ <ul class="github-list github-list--compact">
4
+ {% for commit in commits %}
5
+ {% if loop.index <= 5 %}
6
+ <li class="github-list__item">
7
+ <code class="github-sha"><a href="{{ commit.url }}" target="_blank" rel="noopener">{{ commit.sha }}</a></code>
8
+ <span class="github-commit__message">{{ commit.message }}</span>
9
+ </li>
10
+ {% endif %}
11
+ {% endfor %}
12
+ </ul>
13
+ {% endif %}
@@ -0,0 +1,13 @@
1
+ {# Stars partial for embedding in other templates #}
2
+ {% if stars and stars.length > 0 %}
3
+ <ul class="github-list github-list--compact">
4
+ {% for star in stars %}
5
+ {% if loop.index <= 5 %}
6
+ <li class="github-list__item">
7
+ <a href="{{ star.url }}" target="_blank" rel="noopener">{{ star.name }}</a>
8
+ <small class="github-meta">{{ star.stars }} stars</small>
9
+ </li>
10
+ {% endif %}
11
+ {% endfor %}
12
+ </ul>
13
+ {% endif %}
@@ -0,0 +1,12 @@
1
+ {% call widget({
2
+ title: __("github.title")
3
+ }) %}
4
+ <p class="prose">{{ __("github.widget.description") }}</p>
5
+ <div class="button-grid">
6
+ {{ button({
7
+ classes: "button--secondary-on-offset",
8
+ href: application.githubEndpoint or "/github-api",
9
+ text: __("github.widget.view")
10
+ }) }}
11
+ </div>
12
+ {% endcall %}
package/index.js ADDED
@@ -0,0 +1,98 @@
1
+ import express from "express";
2
+
3
+ import { activityController } from "./lib/controllers/activity.js";
4
+ import { commitsController } from "./lib/controllers/commits.js";
5
+ import { contributionsController } from "./lib/controllers/contributions.js";
6
+ import { dashboardController } from "./lib/controllers/dashboard.js";
7
+ import { featuredController } from "./lib/controllers/featured.js";
8
+ import { starsController } from "./lib/controllers/stars.js";
9
+
10
+ // Module-level routers (matching Indiekit's endpoint pattern)
11
+ const protectedRouter = express.Router();
12
+ const publicRouter = express.Router();
13
+
14
+ const defaults = {
15
+ mountPath: "/github",
16
+ username: "",
17
+ token: process.env.GITHUB_TOKEN,
18
+ cacheTtl: 900_000, // 15 minutes in ms
19
+ limits: {
20
+ commits: 10,
21
+ stars: 20,
22
+ contributions: 10,
23
+ activity: 20,
24
+ repos: 10,
25
+ },
26
+ repos: [], // Empty = all repos, or specify ['owner/repo', ...] for filtering activity
27
+ featuredRepos: [], // Repos to showcase with commits, e.g. ['owner/repo', ...]
28
+ };
29
+
30
+ export default class GitHubEndpoint {
31
+ name = "GitHub activity endpoint";
32
+
33
+ constructor(options = {}) {
34
+ this.options = { ...defaults, ...options };
35
+ this.mountPath = this.options.mountPath;
36
+ }
37
+
38
+ get environment() {
39
+ return ["GITHUB_TOKEN"];
40
+ }
41
+
42
+ get navigationItems() {
43
+ return {
44
+ href: this.options.mountPath,
45
+ text: "github.title",
46
+ requiresDatabase: false,
47
+ };
48
+ }
49
+
50
+ get shortcutItems() {
51
+ return {
52
+ url: this.options.mountPath,
53
+ name: "github.activity",
54
+ iconName: "syndicate",
55
+ requiresDatabase: false,
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Protected routes (require authentication)
61
+ * HTML pages for admin dashboard
62
+ */
63
+ get routes() {
64
+ // Dashboard
65
+ protectedRouter.get("/", dashboardController.get);
66
+
67
+ // Individual sections (HTML pages)
68
+ protectedRouter.get("/commits", commitsController.get);
69
+ protectedRouter.get("/stars", starsController.get);
70
+ protectedRouter.get("/contributions", contributionsController.get);
71
+ protectedRouter.get("/activity", activityController.get);
72
+ protectedRouter.get("/featured", featuredController.get);
73
+
74
+ return protectedRouter;
75
+ }
76
+
77
+ /**
78
+ * Public routes (no authentication required)
79
+ * JSON API endpoints for Eleventy widgets
80
+ */
81
+ get routesPublic() {
82
+ // JSON API for widgets - publicly accessible
83
+ publicRouter.get("/api/commits", commitsController.api);
84
+ publicRouter.get("/api/stars", starsController.api);
85
+ publicRouter.get("/api/activity", activityController.api);
86
+ publicRouter.get("/api/featured", featuredController.api);
87
+
88
+ return publicRouter;
89
+ }
90
+
91
+ init(Indiekit) {
92
+ Indiekit.addEndpoint(this);
93
+
94
+ // Store GitHub config in application for controller access
95
+ Indiekit.config.application.githubConfig = this.options;
96
+ Indiekit.config.application.githubEndpoint = this.mountPath;
97
+ }
98
+ }
@@ -0,0 +1,124 @@
1
+ import { GitHubClient } from "../github-client.js";
2
+ import * as utils from "../utils.js";
3
+
4
+ /**
5
+ * Display activity on user's repositories from others
6
+ * @type {import("express").RequestHandler}
7
+ */
8
+ export const activityController = {
9
+ async get(request, response, next) {
10
+ try {
11
+ const { username, token, cacheTtl, limits, repos } =
12
+ request.app.locals.application.githubConfig;
13
+
14
+ if (!username) {
15
+ return response.render("activity", {
16
+ title: response.locals.__("github.activity.title"),
17
+ error: { message: response.locals.__("github.error.noUsername") },
18
+ });
19
+ }
20
+
21
+ const client = new GitHubClient({ token, cacheTtl });
22
+
23
+ let activity = [];
24
+
25
+ try {
26
+ if (repos.length > 0) {
27
+ // Fetch events for specific repos
28
+ const repoEventPromises = repos.map(async (repoPath) => {
29
+ const [owner, repo] = repoPath.split("/");
30
+ try {
31
+ return await client.getRepoEvents(owner, repo, 30);
32
+ } catch {
33
+ return [];
34
+ }
35
+ });
36
+
37
+ const repoEvents = await Promise.all(repoEventPromises);
38
+ const allEvents = repoEvents.flat();
39
+ activity = utils.extractRepoActivity(allEvents, username);
40
+ } else {
41
+ // Use received events (events on user's repos)
42
+ const events = await client.fetch(
43
+ `/users/${username}/received_events?per_page=${limits.activity * 2}`,
44
+ );
45
+ activity = utils.extractRepoActivity(events, username);
46
+ }
47
+ } catch (apiError) {
48
+ console.error("GitHub API error:", apiError);
49
+ return response.render("activity", {
50
+ title: response.locals.__("github.activity.title"),
51
+ actions: [],
52
+ parent: {
53
+ href: request.baseUrl,
54
+ text: response.locals.__("github.title"),
55
+ },
56
+ error: { message: apiError.message || "Failed to fetch activity" },
57
+ });
58
+ }
59
+
60
+ activity = activity.slice(0, limits.activity);
61
+
62
+ response.render("activity", {
63
+ title: response.locals.__("github.activity.title"),
64
+ actions: [],
65
+ parent: {
66
+ href: request.baseUrl,
67
+ text: response.locals.__("github.title"),
68
+ },
69
+ activity,
70
+ username,
71
+ mountPath: request.baseUrl,
72
+ });
73
+ } catch (error) {
74
+ next(error);
75
+ }
76
+ },
77
+
78
+ async api(request, response, next) {
79
+ try {
80
+ const { username, token, cacheTtl, limits, repos } =
81
+ request.app.locals.application.githubConfig;
82
+
83
+ if (!username) {
84
+ return response.status(400).json({ error: "No username configured" });
85
+ }
86
+
87
+ const client = new GitHubClient({ token, cacheTtl });
88
+
89
+ let activity = [];
90
+
91
+ try {
92
+ if (repos.length > 0) {
93
+ const repoEventPromises = repos.map(async (repoPath) => {
94
+ const [owner, repo] = repoPath.split("/");
95
+ try {
96
+ return await client.getRepoEvents(owner, repo, 20);
97
+ } catch {
98
+ return [];
99
+ }
100
+ });
101
+
102
+ const repoEvents = await Promise.all(repoEventPromises);
103
+ const allEvents = repoEvents.flat();
104
+ activity = utils.extractRepoActivity(allEvents, username);
105
+ } else {
106
+ const events = await client.fetch(
107
+ `/users/${username}/received_events?per_page=${limits.activity}`,
108
+ );
109
+ activity = utils.extractRepoActivity(events, username);
110
+ }
111
+ } catch (apiError) {
112
+ return response
113
+ .status(apiError.status || 500)
114
+ .json({ error: apiError.message });
115
+ }
116
+
117
+ activity = activity.slice(0, limits.activity);
118
+
119
+ response.json({ activity });
120
+ } catch (error) {
121
+ next(error);
122
+ }
123
+ },
124
+ };
@@ -0,0 +1,84 @@
1
+ import { GitHubClient } from "../github-client.js";
2
+ import * as utils from "../utils.js";
3
+
4
+ /**
5
+ * Display commits list
6
+ * @type {import("express").RequestHandler}
7
+ */
8
+ export const commitsController = {
9
+ async get(request, response, next) {
10
+ try {
11
+ const { username, token, cacheTtl, limits } =
12
+ request.app.locals.application.githubConfig;
13
+
14
+ if (!username) {
15
+ return response.render("commits", {
16
+ title: response.locals.__("github.commits.title"),
17
+ error: { message: response.locals.__("github.error.noUsername") },
18
+ });
19
+ }
20
+
21
+ const client = new GitHubClient({ token, cacheTtl });
22
+
23
+ let events = [];
24
+ try {
25
+ events = await client.getUserEvents(username, 100);
26
+ } catch (apiError) {
27
+ console.error("GitHub API error:", apiError);
28
+ return response.render("commits", {
29
+ title: response.locals.__("github.commits.title"),
30
+ actions: [],
31
+ parent: {
32
+ href: request.baseUrl,
33
+ text: response.locals.__("github.title"),
34
+ },
35
+ error: { message: apiError.message || "Failed to fetch commits" },
36
+ });
37
+ }
38
+
39
+ const commits = utils.extractCommits(events).slice(0, limits.commits * 3);
40
+
41
+ response.render("commits", {
42
+ title: response.locals.__("github.commits.title"),
43
+ actions: [],
44
+ parent: {
45
+ href: request.baseUrl,
46
+ text: response.locals.__("github.title"),
47
+ },
48
+ commits,
49
+ username,
50
+ mountPath: request.baseUrl,
51
+ });
52
+ } catch (error) {
53
+ next(error);
54
+ }
55
+ },
56
+
57
+ async api(request, response, next) {
58
+ try {
59
+ const { username, token, cacheTtl, limits } =
60
+ request.app.locals.application.githubConfig;
61
+
62
+ if (!username) {
63
+ return response.status(400).json({ error: "No username configured" });
64
+ }
65
+
66
+ const client = new GitHubClient({ token, cacheTtl });
67
+
68
+ let events = [];
69
+ try {
70
+ events = await client.getUserEvents(username, 50);
71
+ } catch (apiError) {
72
+ return response
73
+ .status(apiError.status || 500)
74
+ .json({ error: apiError.message });
75
+ }
76
+
77
+ const commits = utils.extractCommits(events).slice(0, limits.commits);
78
+
79
+ response.json({ commits });
80
+ } catch (error) {
81
+ next(error);
82
+ }
83
+ },
84
+ };