@rmdes/indiekit-endpoint-homepage 1.0.9 → 1.0.11

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
@@ -262,6 +262,62 @@ export default class HomepageEndpoint {
262
262
  ];
263
263
  }
264
264
 
265
+ /**
266
+ * Built-in blog post sidebar widget types (post-specific + universal)
267
+ */
268
+ get blogPostWidgets() {
269
+ return [
270
+ {
271
+ id: "author-card-compact",
272
+ label: "Author Card (Compact)",
273
+ description: "Compact h-card with avatar and name",
274
+ icon: "user",
275
+ defaultConfig: {},
276
+ configSchema: {},
277
+ },
278
+ {
279
+ id: "toc",
280
+ label: "Table of Contents",
281
+ description: "Auto-generated from headings",
282
+ icon: "list",
283
+ defaultConfig: {},
284
+ configSchema: {},
285
+ },
286
+ {
287
+ id: "post-categories",
288
+ label: "Post Categories",
289
+ description: "Categories for the current post",
290
+ icon: "tag",
291
+ defaultConfig: {},
292
+ configSchema: {},
293
+ },
294
+ {
295
+ id: "webmentions",
296
+ label: "Webmentions",
297
+ description: "Likes, reposts, and replies",
298
+ icon: "message-circle",
299
+ defaultConfig: {},
300
+ configSchema: {},
301
+ },
302
+ {
303
+ id: "share",
304
+ label: "Share",
305
+ description: "Share on Bluesky and Mastodon",
306
+ icon: "share",
307
+ defaultConfig: {},
308
+ configSchema: {},
309
+ },
310
+ {
311
+ id: "subscribe",
312
+ label: "Subscribe",
313
+ description: "RSS and JSON feed links",
314
+ icon: "rss",
315
+ defaultConfig: {},
316
+ configSchema: {},
317
+ },
318
+ ];
319
+ }
320
+
265
321
  /**
266
322
  * Protected routes (require authentication)
267
323
  */
@@ -335,6 +391,9 @@ export default class HomepageEndpoint {
335
391
  const discoveredSections = [...this.homepageSections];
336
392
  const discoveredWidgets = [...this.homepageWidgets];
337
393
 
394
+ // Blog post widgets = post-specific widgets + universal widgets
395
+ const discoveredBlogPostWidgets = [...this.blogPostWidgets];
396
+
338
397
  // Scan all endpoints for homepageSections
339
398
  for (const endpoint of Indiekit.endpoints || []) {
340
399
  if (endpoint === this) continue; // Skip self
@@ -362,9 +421,14 @@ export default class HomepageEndpoint {
362
421
  // Store discovered sections/widgets for API access
363
422
  Indiekit.config.application.discoveredSections = discoveredSections;
364
423
  Indiekit.config.application.discoveredWidgets = discoveredWidgets;
424
+ // Blog post widgets: post-specific + all universal widgets
425
+ Indiekit.config.application.discoveredBlogPostWidgets = [
426
+ ...discoveredBlogPostWidgets,
427
+ ...discoveredWidgets,
428
+ ];
365
429
 
366
430
  console.log(
367
- `[Homepage] Discovered ${discoveredSections.length} sections, ${discoveredWidgets.length} widgets`
431
+ `[Homepage] Discovered ${discoveredSections.length} sections, ${discoveredWidgets.length} widgets, ${discoveredBlogPostWidgets.length} blog post widgets`
368
432
  );
369
433
  }
370
434
  }
@@ -90,6 +90,8 @@ export const apiController = {
90
90
  hero: config.hero,
91
91
  sections: config.sections,
92
92
  sidebar: config.sidebar,
93
+ blogListingSidebar: config.blogListingSidebar,
94
+ blogPostSidebar: config.blogPostSidebar,
93
95
  footer: config.footer,
94
96
  identity: config.identity,
95
97
  updatedAt: config.updatedAt,
@@ -42,6 +42,7 @@ export const dashboardController = {
42
42
  // Get discovered sections and widgets
43
43
  const sections = application.discoveredSections || [];
44
44
  const widgets = application.discoveredWidgets || [];
45
+ const blogPostWidgets = application.discoveredBlogPostWidgets || [];
45
46
  const presets = application.layoutPresets || [];
46
47
 
47
48
  // Group sections by source plugin
@@ -62,6 +63,7 @@ export const dashboardController = {
62
63
  config,
63
64
  sections,
64
65
  widgets,
66
+ blogPostWidgets,
65
67
  presets,
66
68
  activePresetId,
67
69
  sectionsByPlugin,
@@ -89,7 +91,11 @@ export const dashboardController = {
89
91
  const { application } = request.app.locals;
90
92
 
91
93
  try {
92
- const { layout, hero, sections, sidebar, footer, identity } = request.body;
94
+ const {
95
+ layout, hero, sections, sidebar,
96
+ blogListingSidebar, blogPostSidebar,
97
+ footer, identity,
98
+ } = request.body;
93
99
 
94
100
  // Parse JSON strings if needed
95
101
  const config = {
@@ -97,6 +103,8 @@ export const dashboardController = {
97
103
  hero: typeof hero === "string" ? JSON.parse(hero) : hero,
98
104
  sections: typeof sections === "string" ? JSON.parse(sections) : sections,
99
105
  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,
100
108
  footer: typeof footer === "string" ? JSON.parse(footer) : footer,
101
109
  identity: typeof identity === "string" ? JSON.parse(identity) : identity,
102
110
  };
@@ -42,6 +42,8 @@ export async function saveConfig(application, config) {
42
42
  hero: config.hero || { enabled: true, showSocial: true },
43
43
  sections: config.sections || [],
44
44
  sidebar: config.sidebar || [],
45
+ blogListingSidebar: config.blogListingSidebar || [],
46
+ blogPostSidebar: config.blogPostSidebar || [],
45
47
  footer: config.footer || [],
46
48
  identity: config.identity || null,
47
49
  updatedAt: now,
@@ -79,6 +81,8 @@ async function writeConfigFile(application, config) {
79
81
  hero: config.hero,
80
82
  sections: config.sections,
81
83
  sidebar: config.sidebar,
84
+ blogListingSidebar: config.blogListingSidebar,
85
+ blogPostSidebar: config.blogPostSidebar,
82
86
  footer: config.footer,
83
87
  identity: config.identity,
84
88
  updatedAt: config.updatedAt,
@@ -113,6 +117,8 @@ export function getDefaultConfig() {
113
117
  { type: "recent-posts", config: { maxItems: 5 } },
114
118
  { type: "categories", config: {} },
115
119
  ],
120
+ blogListingSidebar: [],
121
+ blogPostSidebar: [],
116
122
  footer: [],
117
123
  identity: null,
118
124
  };
package/locales/en.json CHANGED
@@ -35,6 +35,18 @@
35
35
  "add": "Add Widget",
36
36
  "empty": "No widgets configured. Add widgets from the picker below."
37
37
  },
38
+ "blogListingSidebar": {
39
+ "title": "Blog Listing Sidebar",
40
+ "description": "Configure widgets that appear in the sidebar on blog listing pages (/blog/, /notes/, /articles/...). Leave empty to use default widgets.",
41
+ "add": "Add Widget",
42
+ "empty": "No widgets configured — using default sidebar."
43
+ },
44
+ "blogPostSidebar": {
45
+ "title": "Blog Post Sidebar",
46
+ "description": "Configure widgets that appear in the sidebar on individual post pages. Leave empty to use default widgets.",
47
+ "add": "Add Widget",
48
+ "empty": "No widgets configured — using default sidebar."
49
+ },
38
50
  "footer": {
39
51
  "title": "Footer (3-column)",
40
52
  "description": "A responsive 3-column area below the main content — add up to 3 blocks (one per column). Ideal for webrings, links, or custom content.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-homepage",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "Homepage builder endpoint for Indiekit. Configure layout, sections, and sidebar widgets from the admin UI.",
5
5
  "keywords": [
6
6
  "indiekit",
@@ -443,6 +443,59 @@
443
443
  <input type="hidden" name="sidebar" id="sidebar-json" value='{{ config.sidebar | dump }}'>
444
444
  </section>
445
445
 
446
+ {# Blog Listing Sidebar #}
447
+ <section class="hp-section">
448
+ <h2>{{ __("homepage.blogListingSidebar.title") }}</h2>
449
+ <p class="hp-section__desc">{{ __("homepage.blogListingSidebar.description") }}</p>
450
+
451
+ <ul class="hp-sections-list" id="blog-listing-sidebar-list"></ul>
452
+
453
+ <div class="hp-section-picker">
454
+ <h3>{{ __("homepage.blogListingSidebar.add") }}</h3>
455
+ <div class="hp-section-picker__grid">
456
+ {% for widget in widgets %}
457
+ <div class="hp-section-picker__item" data-add-blog-listing-widget="{{ widget.id }}">
458
+ {{ widget.label }}
459
+ </div>
460
+ {% endfor %}
461
+ </div>
462
+ </div>
463
+
464
+ <input type="hidden" name="blogListingSidebar" id="blog-listing-sidebar-json" value='{{ config.blogListingSidebar | dump }}'>
465
+ </section>
466
+
467
+ {# Blog Post Sidebar #}
468
+ <section class="hp-section">
469
+ <h2>{{ __("homepage.blogPostSidebar.title") }}</h2>
470
+ <p class="hp-section__desc">{{ __("homepage.blogPostSidebar.description") }}</p>
471
+
472
+ <ul class="hp-sections-list" id="blog-post-sidebar-list"></ul>
473
+
474
+ <div class="hp-section-picker">
475
+ <h3>{{ __("homepage.blogPostSidebar.add") }}</h3>
476
+ <div class="hp-section-picker__grid">
477
+ <div class="hp-section-picker__heading">Post Widgets</div>
478
+ {% for widget in blogPostWidgets %}
479
+ {% if widget.id in ["author-card-compact", "post-navigation", "toc", "post-categories", "webmentions", "share", "subscribe"] %}
480
+ <div class="hp-section-picker__item" data-add-blog-post-widget="{{ widget.id }}">
481
+ {{ widget.label }}
482
+ </div>
483
+ {% endif %}
484
+ {% endfor %}
485
+ <div class="hp-section-picker__heading">Universal Widgets</div>
486
+ {% for widget in blogPostWidgets %}
487
+ {% if widget.id not in ["author-card-compact", "post-navigation", "toc", "post-categories", "webmentions", "share", "subscribe"] %}
488
+ <div class="hp-section-picker__item" data-add-blog-post-widget="{{ widget.id }}">
489
+ {{ widget.label }}
490
+ </div>
491
+ {% endif %}
492
+ {% endfor %}
493
+ </div>
494
+ </div>
495
+
496
+ <input type="hidden" name="blogPostSidebar" id="blog-post-sidebar-json" value='{{ config.blogPostSidebar | dump }}'>
497
+ </section>
498
+
446
499
  {# Footer #}
447
500
  <section class="hp-section">
448
501
  <h2>{{ __("homepage.footer.title") }}</h2>
@@ -483,8 +536,11 @@
483
536
  var widgetLabels = {
484
537
  {% for widget in widgets %}'{{ widget.id }}': '{{ widget.label }}'{% if not loop.last %}, {% endif %}{% endfor %}
485
538
  };
539
+ var blogPostWidgetLabels = {
540
+ {% for widget in blogPostWidgets %}'{{ widget.id }}': '{{ widget.label }}'{% if not loop.last %}, {% endif %}{% endfor %}
541
+ };
486
542
  // Merge so all label maps cover all types
487
- var allLabels = Object.assign({}, sectionLabels, widgetLabels);
543
+ var allLabels = Object.assign({}, sectionLabels, widgetLabels, blogPostWidgetLabels);
488
544
 
489
545
  // Unique key counter for tracking items
490
546
  var nextKey = 0;
@@ -492,10 +548,14 @@
492
548
  // Parse current data from hidden inputs and assign keys
493
549
  var sections = JSON.parse(document.getElementById('sections-json').value || '[]');
494
550
  var sidebar = JSON.parse(document.getElementById('sidebar-json').value || '[]');
551
+ var blogListingSidebar = JSON.parse(document.getElementById('blog-listing-sidebar-json').value || '[]');
552
+ var blogPostSidebar = JSON.parse(document.getElementById('blog-post-sidebar-json').value || '[]');
495
553
  var footer = JSON.parse(document.getElementById('footer-json').value || '[]');
496
554
 
497
555
  sections.forEach(function(s) { s._key = nextKey++; });
498
556
  sidebar.forEach(function(s) { s._key = nextKey++; });
557
+ blogListingSidebar.forEach(function(s) { s._key = nextKey++; });
558
+ blogPostSidebar.forEach(function(s) { s._key = nextKey++; });
499
559
  footer.forEach(function(s) { s._key = nextKey++; });
500
560
 
501
561
  // Strip _key before form submission
@@ -739,6 +799,72 @@
739
799
  initSortable();
740
800
  }
741
801
 
802
+ // --- Blog Listing Sidebar ---
803
+ function addBlogListingWidget(id) {
804
+ var item = { type: id, config: {}, _key: nextKey++ };
805
+ blogListingSidebar.push(item);
806
+ updateBlogListingSidebar();
807
+ if (id === 'custom-html') {
808
+ var list = document.getElementById('blog-listing-sidebar-list');
809
+ var lastPanel = list.querySelector('details.hp-edit-panel:last-of-type');
810
+ if (lastPanel) lastPanel.open = true;
811
+ }
812
+ }
813
+
814
+ function removeBlogListingWidget(key) {
815
+ blogListingSidebar = blogListingSidebar.filter(function(w) { return w._key !== key; });
816
+ updateBlogListingSidebar();
817
+ }
818
+
819
+ function editBlogListingWidget(key, config) {
820
+ var item = blogListingSidebar.find(function(s) { return s._key === key; });
821
+ if (item) item.config = config;
822
+ updateBlogListingSidebar();
823
+ }
824
+
825
+ function updateBlogListingSidebar() {
826
+ document.getElementById('blog-listing-sidebar-json').value = JSON.stringify(stripKeys(blogListingSidebar));
827
+ renderList(
828
+ document.getElementById('blog-listing-sidebar-list'),
829
+ blogListingSidebar, allLabels, removeBlogListingWidget, editBlogListingWidget,
830
+ '{{ __("homepage.blogListingSidebar.empty") }}'
831
+ );
832
+ initSortable();
833
+ }
834
+
835
+ // --- Blog Post Sidebar ---
836
+ function addBlogPostWidget(id) {
837
+ var item = { type: id, config: {}, _key: nextKey++ };
838
+ blogPostSidebar.push(item);
839
+ updateBlogPostSidebar();
840
+ if (id === 'custom-html') {
841
+ var list = document.getElementById('blog-post-sidebar-list');
842
+ var lastPanel = list.querySelector('details.hp-edit-panel:last-of-type');
843
+ if (lastPanel) lastPanel.open = true;
844
+ }
845
+ }
846
+
847
+ function removeBlogPostWidget(key) {
848
+ blogPostSidebar = blogPostSidebar.filter(function(w) { return w._key !== key; });
849
+ updateBlogPostSidebar();
850
+ }
851
+
852
+ function editBlogPostWidget(key, config) {
853
+ var item = blogPostSidebar.find(function(s) { return s._key === key; });
854
+ if (item) item.config = config;
855
+ updateBlogPostSidebar();
856
+ }
857
+
858
+ function updateBlogPostSidebar() {
859
+ document.getElementById('blog-post-sidebar-json').value = JSON.stringify(stripKeys(blogPostSidebar));
860
+ renderList(
861
+ document.getElementById('blog-post-sidebar-list'),
862
+ blogPostSidebar, allLabels, removeBlogPostWidget, editBlogPostWidget,
863
+ '{{ __("homepage.blogPostSidebar.empty") }}'
864
+ );
865
+ initSortable();
866
+ }
867
+
742
868
  // --- Footer (max 3 columns) ---
743
869
  var FOOTER_MAX = 3;
744
870
 
@@ -802,6 +928,16 @@
802
928
  document.getElementById('sidebar-json').value = JSON.stringify(stripKeys(sidebar));
803
929
  }
804
930
 
931
+ function syncBlogListingSidebarFromDom() {
932
+ blogListingSidebar = syncFromDom(document.getElementById('blog-listing-sidebar-list'), blogListingSidebar);
933
+ document.getElementById('blog-listing-sidebar-json').value = JSON.stringify(stripKeys(blogListingSidebar));
934
+ }
935
+
936
+ function syncBlogPostSidebarFromDom() {
937
+ blogPostSidebar = syncFromDom(document.getElementById('blog-post-sidebar-list'), blogPostSidebar);
938
+ document.getElementById('blog-post-sidebar-json').value = JSON.stringify(stripKeys(blogPostSidebar));
939
+ }
940
+
805
941
  function syncFooterFromDom() {
806
942
  footer = syncFromDom(document.getElementById('footer-list'), footer);
807
943
  document.getElementById('footer-json').value = JSON.stringify(stripKeys(footer));
@@ -822,6 +958,12 @@
822
958
  document.querySelectorAll('[data-add-widget]').forEach(function(el) {
823
959
  el.addEventListener('click', function() { addWidget(el.dataset.addWidget); });
824
960
  });
961
+ document.querySelectorAll('[data-add-blog-listing-widget]').forEach(function(el) {
962
+ el.addEventListener('click', function() { addBlogListingWidget(el.dataset.addBlogListingWidget); });
963
+ });
964
+ document.querySelectorAll('[data-add-blog-post-widget]').forEach(function(el) {
965
+ el.addEventListener('click', function() { addBlogPostWidget(el.dataset.addBlogPostWidget); });
966
+ });
825
967
  document.querySelectorAll('[data-add-footer]').forEach(function(el) {
826
968
  el.addEventListener('click', function() { addFooter(el.dataset.addFooter); });
827
969
  });
@@ -847,6 +989,8 @@
847
989
  var lists = [
848
990
  { el: 'sections-list', sync: syncSectionsFromDom },
849
991
  { el: 'widgets-list', sync: syncSidebarFromDom },
992
+ { el: 'blog-listing-sidebar-list', sync: syncBlogListingSidebarFromDom },
993
+ { el: 'blog-post-sidebar-list', sync: syncBlogPostSidebarFromDom },
850
994
  { el: 'footer-list', sync: syncFooterFromDom }
851
995
  ];
852
996
 
@@ -877,6 +1021,8 @@
877
1021
  // --- Initial render ---
878
1022
  updateSections();
879
1023
  updateWidgets();
1024
+ updateBlogListingSidebar();
1025
+ updateBlogPostSidebar();
880
1026
  updateFooter();
881
1027
  </script>
882
1028
  {% endblock %}