@rmdes/indiekit-endpoint-homepage 1.0.16 → 1.0.18

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/index.js CHANGED
@@ -269,6 +269,14 @@ export default class HomepageEndpoint {
269
269
  defaultConfig: {},
270
270
  configSchema: {},
271
271
  },
272
+ {
273
+ id: "fediverse-follow",
274
+ label: "Fediverse Follow",
275
+ description: "Follow button for fediverse instances",
276
+ icon: "globe",
277
+ defaultConfig: {},
278
+ configSchema: {},
279
+ },
272
280
  {
273
281
  id: "custom-html",
274
282
  label: "Custom Content",
@@ -364,6 +372,14 @@ export default class HomepageEndpoint {
364
372
  // Get current config
365
373
  protectedRouter.get("/api/config", apiController.getConfig);
366
374
 
375
+ // Blog sidebar tab
376
+ protectedRouter.get("/blog-sidebar", dashboardController.getBlogSidebar);
377
+ protectedRouter.post("/save-blog-sidebar", dashboardController.saveBlogSidebar);
378
+
379
+ // Identity tab
380
+ protectedRouter.get("/identity", dashboardController.getIdentity);
381
+ protectedRouter.post("/save-identity", dashboardController.saveIdentity);
382
+
367
383
  return protectedRouter;
368
384
  }
369
385
 
@@ -5,6 +5,26 @@
5
5
 
6
6
  import { getConfig, saveConfig, getDefaultConfig } from "../storage/config.js";
7
7
 
8
+ /**
9
+ * Parse social links from form body.
10
+ * Express parses social[0][name], social[0][url] etc. into nested objects.
11
+ */
12
+ function parseSocialLinks(body) {
13
+ const social = [];
14
+ if (!body.social) return social;
15
+ const entries = Array.isArray(body.social) ? body.social : Object.values(body.social);
16
+ for (const entry of entries) {
17
+ if (!entry || (!entry.name && !entry.url)) continue;
18
+ social.push({
19
+ name: entry.name || "",
20
+ url: entry.url || "",
21
+ rel: entry.rel || "me",
22
+ icon: entry.icon || "",
23
+ });
24
+ }
25
+ return social;
26
+ }
27
+
8
28
  /**
9
29
  * Detect which preset matches the current config (if any)
10
30
  */
@@ -60,6 +80,7 @@ export const dashboardController = {
60
80
 
61
81
  response.render("homepage-dashboard", {
62
82
  title: "Homepage Builder",
83
+ activeTab: "builder",
63
84
  config,
64
85
  sections,
65
86
  widgets,
@@ -91,11 +112,10 @@ export const dashboardController = {
91
112
  const { application } = request.app.locals;
92
113
 
93
114
  try {
94
- const {
95
- layout, hero, sections, sidebar,
96
- blogListingSidebar, blogPostSidebar,
97
- footer, identity,
98
- } = request.body;
115
+ const { layout, hero, sections, sidebar, footer } = request.body;
116
+
117
+ // Get current config to preserve fields from other tabs
118
+ const currentConfig = await getConfig(application);
99
119
 
100
120
  // Parse JSON strings if needed
101
121
  const config = {
@@ -103,10 +123,10 @@ export const dashboardController = {
103
123
  hero: typeof hero === "string" ? JSON.parse(hero) : hero,
104
124
  sections: typeof sections === "string" ? JSON.parse(sections) : sections,
105
125
  sidebar: typeof sidebar === "string" ? JSON.parse(sidebar) : sidebar,
106
- blogListingSidebar: typeof blogListingSidebar === "string" ? JSON.parse(blogListingSidebar) : blogListingSidebar,
107
- blogPostSidebar: typeof blogPostSidebar === "string" ? JSON.parse(blogPostSidebar) : blogPostSidebar,
126
+ blogListingSidebar: currentConfig?.blogListingSidebar || [],
127
+ blogPostSidebar: currentConfig?.blogPostSidebar || [],
108
128
  footer: typeof footer === "string" ? JSON.parse(footer) : footer,
109
- identity: typeof identity === "string" ? JSON.parse(identity) : identity,
129
+ identity: currentConfig?.identity || null,
110
130
  };
111
131
 
112
132
  await saveConfig(application, config);
@@ -174,4 +194,179 @@ export const dashboardController = {
174
194
  });
175
195
  }
176
196
  },
197
+
198
+ /**
199
+ * GET /blog-sidebar - Blog sidebar tab
200
+ */
201
+ async getBlogSidebar(request, response) {
202
+ const { application } = request.app.locals;
203
+
204
+ try {
205
+ let config = await getConfig(application);
206
+ if (!config) {
207
+ config = getDefaultConfig();
208
+ }
209
+
210
+ const widgets = application.discoveredWidgets || [];
211
+ const blogPostWidgets = application.discoveredBlogPostWidgets || [];
212
+
213
+ response.render("homepage-blog-sidebar", {
214
+ title: "Homepage Builder",
215
+ activeTab: "blog-sidebar",
216
+ config,
217
+ widgets,
218
+ blogPostWidgets,
219
+ homepageEndpoint: application.homepageEndpoint,
220
+ });
221
+ } catch (error) {
222
+ console.error("[Homepage] Blog sidebar error:", error);
223
+ response.status(500).render("error", {
224
+ title: "Error",
225
+ message: "Failed to load blog sidebar configuration",
226
+ error: error.message,
227
+ });
228
+ }
229
+ },
230
+
231
+ /**
232
+ * POST /save-blog-sidebar - Save blog sidebar configuration
233
+ */
234
+ async saveBlogSidebar(request, response) {
235
+ const { application } = request.app.locals;
236
+
237
+ try {
238
+ const { blogListingSidebar, blogPostSidebar } = request.body;
239
+
240
+ // Get current config to preserve fields from other tabs
241
+ const currentConfig = await getConfig(application);
242
+
243
+ const config = {
244
+ layout: currentConfig?.layout || "single-column",
245
+ hero: currentConfig?.hero || { enabled: true, showSocial: true },
246
+ sections: currentConfig?.sections || [],
247
+ sidebar: currentConfig?.sidebar || [],
248
+ blogListingSidebar: typeof blogListingSidebar === "string" ? JSON.parse(blogListingSidebar) : (blogListingSidebar || []),
249
+ blogPostSidebar: typeof blogPostSidebar === "string" ? JSON.parse(blogPostSidebar) : (blogPostSidebar || []),
250
+ footer: currentConfig?.footer || [],
251
+ identity: currentConfig?.identity || null,
252
+ };
253
+
254
+ await saveConfig(application, config);
255
+
256
+ if (request.headers.accept?.includes("application/json")) {
257
+ response.json({ success: true, message: "Blog sidebar saved" });
258
+ } else {
259
+ response.redirect(application.homepageEndpoint + "/blog-sidebar?saved=1");
260
+ }
261
+ } catch (error) {
262
+ console.error("[Homepage] Save blog sidebar error:", error);
263
+
264
+ if (request.headers.accept?.includes("application/json")) {
265
+ response.status(500).json({ success: false, error: error.message });
266
+ } else {
267
+ response.status(500).render("error", {
268
+ title: "Error",
269
+ message: "Failed to save blog sidebar configuration",
270
+ error: error.message,
271
+ });
272
+ }
273
+ }
274
+ },
275
+
276
+ /**
277
+ * GET /identity - Identity editor tab
278
+ */
279
+ async getIdentity(request, response) {
280
+ const { application } = request.app.locals;
281
+
282
+ try {
283
+ let config = await getConfig(application);
284
+ if (!config) {
285
+ config = getDefaultConfig();
286
+ }
287
+
288
+ const identity = config.identity || {};
289
+
290
+ response.render("homepage-identity", {
291
+ title: "Homepage Builder",
292
+ activeTab: "identity",
293
+ identity,
294
+ homepageEndpoint: application.homepageEndpoint,
295
+ });
296
+ } catch (error) {
297
+ console.error("[Homepage] Identity error:", error);
298
+ response.status(500).render("error", {
299
+ title: "Error",
300
+ message: "Failed to load identity configuration",
301
+ error: error.message,
302
+ });
303
+ }
304
+ },
305
+
306
+ /**
307
+ * POST /save-identity - Save identity configuration
308
+ */
309
+ async saveIdentity(request, response) {
310
+ const { application } = request.app.locals;
311
+
312
+ try {
313
+ const body = request.body;
314
+
315
+ // Build identity object from form fields
316
+ const identity = {
317
+ name: body["identity-name"] || "",
318
+ avatar: body["identity-avatar"] || "",
319
+ title: body["identity-title"] || "",
320
+ pronoun: body["identity-pronoun"] || "",
321
+ bio: body["identity-bio"] || "",
322
+ description: body["identity-description"] || "",
323
+ locality: body["identity-locality"] || "",
324
+ country: body["identity-country"] || "",
325
+ org: body["identity-org"] || "",
326
+ url: body["identity-url"] || "",
327
+ email: body["identity-email"] || "",
328
+ keyUrl: body["identity-keyUrl"] || "",
329
+ categories: body["identity-categories"]
330
+ ? (typeof body["identity-categories"] === "string"
331
+ ? body["identity-categories"].split(",").map(s => s.trim()).filter(Boolean)
332
+ : body["identity-categories"])
333
+ : [],
334
+ social: parseSocialLinks(body),
335
+ };
336
+
337
+ // Get current config to preserve fields from other tabs
338
+ const currentConfig = await getConfig(application);
339
+
340
+ const config = {
341
+ layout: currentConfig?.layout || "single-column",
342
+ hero: currentConfig?.hero || { enabled: true, showSocial: true },
343
+ sections: currentConfig?.sections || [],
344
+ sidebar: currentConfig?.sidebar || [],
345
+ blogListingSidebar: currentConfig?.blogListingSidebar || [],
346
+ blogPostSidebar: currentConfig?.blogPostSidebar || [],
347
+ footer: currentConfig?.footer || [],
348
+ identity,
349
+ };
350
+
351
+ await saveConfig(application, config);
352
+
353
+ if (request.headers.accept?.includes("application/json")) {
354
+ response.json({ success: true, message: "Identity saved" });
355
+ } else {
356
+ response.redirect(application.homepageEndpoint + "/identity?saved=1");
357
+ }
358
+ } catch (error) {
359
+ console.error("[Homepage] Save identity error:", error);
360
+
361
+ if (request.headers.accept?.includes("application/json")) {
362
+ response.status(500).json({ success: false, error: error.message });
363
+ } else {
364
+ response.status(500).render("error", {
365
+ title: "Error",
366
+ message: "Failed to save identity configuration",
367
+ error: error.message,
368
+ });
369
+ }
370
+ }
371
+ },
177
372
  };
package/locales/en.json CHANGED
@@ -2,6 +2,11 @@
2
2
  "homepageBuilder": {
3
3
  "title": "Homepage Builder",
4
4
  "description": "Configure your homepage layout, sections, sidebar widgets, and footer.",
5
+ "tabs": {
6
+ "builder": "Homepage",
7
+ "blogSidebar": "Blog Sidebar",
8
+ "identity": "Identity"
9
+ },
5
10
  "presets": {
6
11
  "title": "Quick Start",
7
12
  "description": "Choose a preset to quickly configure your homepage. You can customize it further below.",
@@ -60,6 +65,44 @@
60
65
  "contentLabel": "Content (HTML or text)",
61
66
  "save": "Apply",
62
67
  "cancel": "Cancel"
68
+ },
69
+ "identity": {
70
+ "title": "Identity",
71
+ "description": "Configure your author profile, contact details, and social links. These override environment variable defaults.",
72
+ "saved": "Identity saved successfully. Refresh your site to see changes.",
73
+ "profile": {
74
+ "legend": "Profile",
75
+ "name": { "label": "Name", "hint": "Your display name" },
76
+ "avatar": { "label": "Avatar URL", "hint": "URL to your avatar image" },
77
+ "title": { "label": "Title", "hint": "Job title or subtitle" },
78
+ "pronoun": { "label": "Pronoun", "hint": "e.g. he/him, she/her, they/them" },
79
+ "bio": { "label": "Bio", "hint": "Short biography" },
80
+ "description": { "label": "Site Description", "hint": "Description shown in the hero section" }
81
+ },
82
+ "location": {
83
+ "legend": "Location",
84
+ "locality": { "label": "City", "hint": "City or locality" },
85
+ "country": { "label": "Country" },
86
+ "org": { "label": "Organization", "hint": "Company or organization" }
87
+ },
88
+ "contact": {
89
+ "legend": "Contact",
90
+ "url": { "label": "URL", "hint": "Your personal website URL" },
91
+ "email": { "label": "Email" },
92
+ "keyUrl": { "label": "PGP Key URL", "hint": "URL to your public PGP key" }
93
+ },
94
+ "skills": {
95
+ "legend": "Skills & Interests",
96
+ "categories": { "label": "Categories", "hint": "Comma-separated skills, interests, or tags" }
97
+ },
98
+ "social": {
99
+ "legend": "Social Links",
100
+ "description": "Add links to your social profiles. These appear in the hero section and h-card.",
101
+ "name": { "label": "Name" },
102
+ "url": { "label": "URL" },
103
+ "rel": { "label": "Rel" },
104
+ "icon": { "label": "Icon" }
105
+ }
63
106
  }
64
107
  }
65
108
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-homepage",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "Homepage builder endpoint for Indiekit. Configure layout, sections, and sidebar widgets from the admin UI.",
5
5
  "keywords": [
6
6
  "indiekit",