@leadertechie/personal-site-kit 0.1.0-alpha.2 → 0.1.0-alpha.21

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.
Files changed (169) hide show
  1. package/README.md +94 -17
  2. package/dist/api/content-utils.d.ts +27 -0
  3. package/dist/api/content-utils.d.ts.map +1 -0
  4. package/dist/api/handlers/about-me.d.ts.map +1 -1
  5. package/dist/api/handlers/auth-handler.d.ts +2 -0
  6. package/dist/api/handlers/auth-handler.d.ts.map +1 -0
  7. package/dist/api/handlers/auth.d.ts +23 -0
  8. package/dist/api/handlers/auth.d.ts.map +1 -0
  9. package/dist/api/handlers/content-api.d.ts +0 -1
  10. package/dist/api/handlers/content-api.d.ts.map +1 -1
  11. package/dist/api/handlers/content.d.ts.map +1 -1
  12. package/dist/api/handlers/home.d.ts.map +1 -1
  13. package/dist/api/handlers/static-details.d.ts +1 -1
  14. package/dist/api/handlers/static-details.d.ts.map +1 -1
  15. package/dist/api/index.d.ts +2 -0
  16. package/dist/api/index.d.ts.map +1 -1
  17. package/dist/api/website-api.d.ts +1 -1
  18. package/dist/api/website-api.d.ts.map +1 -1
  19. package/dist/api.js +17 -2
  20. package/dist/assets/logo-placeholder.svg +21 -0
  21. package/dist/chunks/index-C1krnvU3.js +211 -0
  22. package/dist/chunks/index-DsRjL9Uy.js +2727 -0
  23. package/dist/chunks/site-store-CGV9c2DI.js +89 -0
  24. package/dist/chunks/{template-gGTkeOcA.js → template-Lmx7Dxoc.js} +132 -91
  25. package/dist/chunks/website-api-CFRUPu0X.js +958 -0
  26. package/dist/index.js +42 -14
  27. package/dist/prerender/data-fetcher.d.ts +19 -0
  28. package/dist/prerender/data-fetcher.d.ts.map +1 -0
  29. package/dist/prerender/page-content.d.ts.map +1 -1
  30. package/dist/prerender/page-generators/about.d.ts +16 -0
  31. package/dist/prerender/page-generators/about.d.ts.map +1 -0
  32. package/dist/prerender/page-generators/base.d.ts +25 -0
  33. package/dist/prerender/page-generators/base.d.ts.map +1 -0
  34. package/dist/prerender/page-generators/blog-detail.d.ts +15 -0
  35. package/dist/prerender/page-generators/blog-detail.d.ts.map +1 -0
  36. package/dist/prerender/page-generators/blogs-list.d.ts +17 -0
  37. package/dist/prerender/page-generators/blogs-list.d.ts.map +1 -0
  38. package/dist/prerender/page-generators/home.d.ts +19 -0
  39. package/dist/prerender/page-generators/home.d.ts.map +1 -0
  40. package/dist/prerender/page-generators/index.d.ts +9 -0
  41. package/dist/prerender/page-generators/index.d.ts.map +1 -0
  42. package/dist/prerender/page-generators/not-found.d.ts +14 -0
  43. package/dist/prerender/page-generators/not-found.d.ts.map +1 -0
  44. package/dist/prerender/page-generators/stories-list.d.ts +17 -0
  45. package/dist/prerender/page-generators/stories-list.d.ts.map +1 -0
  46. package/dist/prerender/page-generators/story-detail.d.ts +15 -0
  47. package/dist/prerender/page-generators/story-detail.d.ts.map +1 -0
  48. package/dist/prerender/template.d.ts +3 -1
  49. package/dist/prerender/template.d.ts.map +1 -1
  50. package/dist/prerender/website-prerender.d.ts +6 -0
  51. package/dist/prerender/website-prerender.d.ts.map +1 -1
  52. package/dist/prerender.js +291 -145
  53. package/dist/shared/config/index.d.ts +1 -0
  54. package/dist/shared/config/index.d.ts.map +1 -1
  55. package/dist/shared/core/site-store.d.ts +1 -0
  56. package/dist/shared/core/site-store.d.ts.map +1 -1
  57. package/dist/shared/core/theme-toggle.d.ts.map +1 -1
  58. package/dist/shared/page-content.d.ts.map +1 -1
  59. package/dist/shared/router.d.ts +9 -3
  60. package/dist/shared/router.d.ts.map +1 -1
  61. package/dist/shared/website-ui.d.ts +23 -0
  62. package/dist/shared/website-ui.d.ts.map +1 -1
  63. package/dist/shared.js +6 -4
  64. package/dist/ui/about-me/index.d.ts +2 -10
  65. package/dist/ui/about-me/index.d.ts.map +1 -1
  66. package/dist/ui/about-me/styles.d.ts.map +1 -1
  67. package/dist/ui/admin/api.d.ts +16 -0
  68. package/dist/ui/admin/api.d.ts.map +1 -0
  69. package/dist/ui/admin/components/AboutMeSection.d.ts +7 -0
  70. package/dist/ui/admin/components/AboutMeSection.d.ts.map +1 -0
  71. package/dist/ui/admin/components/AdminSection.d.ts +13 -0
  72. package/dist/ui/admin/components/AdminSection.d.ts.map +1 -0
  73. package/dist/ui/admin/components/BlogsSection.d.ts +7 -0
  74. package/dist/ui/admin/components/BlogsSection.d.ts.map +1 -0
  75. package/dist/ui/admin/components/HomeSection.d.ts +7 -0
  76. package/dist/ui/admin/components/HomeSection.d.ts.map +1 -0
  77. package/dist/ui/admin/components/ImagesSection.d.ts +7 -0
  78. package/dist/ui/admin/components/ImagesSection.d.ts.map +1 -0
  79. package/dist/ui/admin/components/LoginForm.d.ts +9 -0
  80. package/dist/ui/admin/components/LoginForm.d.ts.map +1 -0
  81. package/dist/ui/admin/components/LogoSection.d.ts +7 -0
  82. package/dist/ui/admin/components/LogoSection.d.ts.map +1 -0
  83. package/dist/ui/admin/components/ProfileSection.d.ts +7 -0
  84. package/dist/ui/admin/components/ProfileSection.d.ts.map +1 -0
  85. package/dist/ui/admin/components/StaticSection.d.ts +9 -0
  86. package/dist/ui/admin/components/StaticSection.d.ts.map +1 -0
  87. package/dist/ui/admin/components/StoriesSection.d.ts +7 -0
  88. package/dist/ui/admin/components/StoriesSection.d.ts.map +1 -0
  89. package/dist/ui/admin/components/index.d.ts +11 -0
  90. package/dist/ui/admin/components/index.d.ts.map +1 -0
  91. package/dist/ui/admin/index.d.ts +27 -26
  92. package/dist/ui/admin/index.d.ts.map +1 -1
  93. package/dist/ui/admin/styles.d.ts.map +1 -1
  94. package/dist/ui/admin/types.d.ts +24 -0
  95. package/dist/ui/admin/types.d.ts.map +1 -0
  96. package/dist/ui/banner/index.d.ts.map +1 -1
  97. package/dist/ui/banner/styles.d.ts.map +1 -1
  98. package/dist/ui/blog-viewer/__tests__/blogviewer.test.d.ts +2 -0
  99. package/dist/ui/blog-viewer/__tests__/blogviewer.test.d.ts.map +1 -0
  100. package/dist/ui/blog-viewer/index.d.ts +25 -0
  101. package/dist/ui/blog-viewer/index.d.ts.map +1 -0
  102. package/dist/ui/blog-viewer/styles.d.ts +2 -0
  103. package/dist/ui/blog-viewer/styles.d.ts.map +1 -0
  104. package/dist/ui/footer/index.d.ts.map +1 -1
  105. package/dist/ui/footer/styles.d.ts.map +1 -1
  106. package/dist/ui/index.d.ts +2 -0
  107. package/dist/ui/index.d.ts.map +1 -1
  108. package/dist/ui/story-viewer/__tests__/storyviewer.test.d.ts +2 -0
  109. package/dist/ui/story-viewer/__tests__/storyviewer.test.d.ts.map +1 -0
  110. package/dist/ui/story-viewer/index.d.ts +25 -0
  111. package/dist/ui/story-viewer/index.d.ts.map +1 -0
  112. package/dist/ui/story-viewer/styles.d.ts +2 -0
  113. package/dist/ui/story-viewer/styles.d.ts.map +1 -0
  114. package/dist/ui.js +15 -3
  115. package/package.json +37 -13
  116. package/public/assets/logo-placeholder.svg +21 -0
  117. package/dist/chunks/index-BqixlS-2.js +0 -1157
  118. package/dist/chunks/website-api-CVsi-OLc.js +0 -596
  119. package/dist/ui/about-me/renderer.d.ts +0 -5
  120. package/dist/ui/about-me/renderer.d.ts.map +0 -1
  121. package/src/api/__tests__/info.test.ts +0 -44
  122. package/src/api/__tests__/utils.test.ts +0 -78
  123. package/src/api/handlers/about-me.ts +0 -99
  124. package/src/api/handlers/content-api.ts +0 -268
  125. package/src/api/handlers/content.ts +0 -72
  126. package/src/api/handlers/home.ts +0 -79
  127. package/src/api/handlers/info.ts +0 -12
  128. package/src/api/handlers/logo.ts +0 -55
  129. package/src/api/handlers/static-details.ts +0 -48
  130. package/src/api/index.ts +0 -7
  131. package/src/api/utils.ts +0 -16
  132. package/src/api/website-api.ts +0 -124
  133. package/src/index.ts +0 -4
  134. package/src/prerender/__tests__/page-content.test.ts +0 -54
  135. package/src/prerender/__tests__/template.test.ts +0 -54
  136. package/src/prerender/index.ts +0 -7
  137. package/src/prerender/page-content.ts +0 -263
  138. package/src/prerender/prerender.ts +0 -25
  139. package/src/prerender/template.ts +0 -65
  140. package/src/prerender/website-prerender.ts +0 -152
  141. package/src/shared/config/api.ts +0 -16
  142. package/src/shared/config/index.ts +0 -41
  143. package/src/shared/config/types.ts +0 -16
  144. package/src/shared/core/__tests__/theme-toggle.test.ts +0 -204
  145. package/src/shared/core/site-store.ts +0 -38
  146. package/src/shared/core/theme-toggle.ts +0 -118
  147. package/src/shared/index.ts +0 -17
  148. package/src/shared/interfaces/ifooter-link.ts +0 -4
  149. package/src/shared/interfaces/iroute.ts +0 -4
  150. package/src/shared/models/theme-variables.css +0 -25
  151. package/src/shared/page-content.ts +0 -210
  152. package/src/shared/router.ts +0 -241
  153. package/src/shared/runtime.ts +0 -11
  154. package/src/shared/template.ts +0 -35
  155. package/src/shared/website-ui.ts +0 -92
  156. package/src/styles/markdown.css +0 -129
  157. package/src/ui/about-me/api.ts +0 -12
  158. package/src/ui/about-me/index.ts +0 -155
  159. package/src/ui/about-me/renderer.ts +0 -7
  160. package/src/ui/about-me/styles.ts +0 -10
  161. package/src/ui/admin/index.ts +0 -492
  162. package/src/ui/admin/styles.ts +0 -317
  163. package/src/ui/banner/index.ts +0 -38
  164. package/src/ui/banner/styles.ts +0 -10
  165. package/src/ui/footer/index.ts +0 -37
  166. package/src/ui/footer/styles.ts +0 -9
  167. package/src/ui/index.ts +0 -4
  168. /package/{src/shared → dist}/styles/markdown.css +0 -0
  169. /package/{src → dist}/styles/theme.css +0 -0
@@ -0,0 +1,89 @@
1
+ const __vite_import_meta_env__ = {};
2
+ const DEFAULT_INFRA = {
3
+ baseUrl: typeof window !== "undefined" ? window.location.origin : "http://localhost:5173",
4
+ apiUrl: typeof window !== "undefined" && (window.__VITE_API_URL__ || __vite_import_meta_env__?.VITE_API_URL) || (typeof window !== "undefined" ? window.location.origin : "http://localhost:8787")
5
+ };
6
+ const DEFAULT_STATIC = {
7
+ siteTitle: "My Personal Website",
8
+ siteDescription: "My Personal Website",
9
+ copyright: "2026 My Personal Website",
10
+ linkedin: "https://linkedin.com/in/yourname",
11
+ github: "https://github.com/yourname",
12
+ email: "yourname@domain.com"
13
+ };
14
+ let activeConfig = { ...DEFAULT_INFRA, ...DEFAULT_STATIC };
15
+ async function initializeConfig(infra) {
16
+ if (infra) {
17
+ if (infra.baseUrl) activeConfig.baseUrl = infra.baseUrl;
18
+ if (infra.apiUrl) activeConfig.apiUrl = infra.apiUrl;
19
+ }
20
+ try {
21
+ const res = await fetch(`${activeConfig.apiUrl}/api/static`);
22
+ if (res.ok) {
23
+ const remoteStatic = await res.json().catch(() => ({}));
24
+ const filteredStatic = Object.fromEntries(
25
+ Object.entries(remoteStatic).filter(([_, v]) => v != null && v !== "")
26
+ );
27
+ activeConfig = { ...activeConfig, ...filteredStatic };
28
+ }
29
+ } catch (e) {
30
+ console.warn("Failed to load static details from R2, using defaults.");
31
+ }
32
+ return activeConfig;
33
+ }
34
+ async function refreshConfig() {
35
+ try {
36
+ const res = await fetch(`${activeConfig.apiUrl}/api/static`);
37
+ if (res.ok) {
38
+ const remoteStatic = await res.json().catch(() => ({}));
39
+ activeConfig = { ...activeConfig, ...remoteStatic };
40
+ }
41
+ } catch (e) {
42
+ console.warn("Failed to refresh static details.");
43
+ }
44
+ return activeConfig;
45
+ }
46
+ function getConfig() {
47
+ return activeConfig;
48
+ }
49
+ class SiteStore {
50
+ constructor() {
51
+ this.config = null;
52
+ this.listeners = /* @__PURE__ */ new Set();
53
+ }
54
+ static getInstance() {
55
+ if (!SiteStore.instance) {
56
+ SiteStore.instance = new SiteStore();
57
+ }
58
+ return SiteStore.instance;
59
+ }
60
+ async init(infra) {
61
+ this.config = await initializeConfig(infra);
62
+ this.notify();
63
+ return this.config;
64
+ }
65
+ subscribe(listener) {
66
+ this.listeners.add(listener);
67
+ if (this.config) listener(this.config);
68
+ return () => this.listeners.delete(listener);
69
+ }
70
+ notify() {
71
+ if (this.config) {
72
+ this.listeners.forEach((l) => l(this.config));
73
+ }
74
+ }
75
+ async refresh() {
76
+ await refreshConfig();
77
+ this.config = getConfig();
78
+ this.notify();
79
+ }
80
+ getConfig() {
81
+ return this.config || getConfig();
82
+ }
83
+ }
84
+ export {
85
+ SiteStore as S,
86
+ getConfig as g,
87
+ initializeConfig as i,
88
+ refreshConfig as r
89
+ };
@@ -1,70 +1,23 @@
1
- import { MarkdownPipeline } from "@leadertechie/md2html";
2
- const DEFAULT_INFRA = {
3
- baseUrl: typeof window !== "undefined" ? window.location.origin : "http://localhost:5173",
4
- apiUrl: typeof window !== "undefined" && window.__VITE_API_URL__ || "http://localhost:8787"
5
- };
6
- const DEFAULT_STATIC = {
7
- siteTitle: "My Personal Website",
8
- siteDescription: "My Personal Website",
9
- copyright: "2026 My Personal Website",
10
- linkedin: "https://linkedin.com/in/yourname",
11
- github: "https://github.com/yourname",
12
- email: "yourname@domain.com"
13
- };
14
- let activeConfig = { ...DEFAULT_INFRA, ...DEFAULT_STATIC };
15
- async function initializeConfig(infra) {
16
- if (infra) {
17
- activeConfig = { ...activeConfig, ...infra };
18
- }
19
- try {
20
- const res = await fetch(`${activeConfig.apiUrl}/api/static`);
21
- if (res.ok) {
22
- const remoteStatic = await res.json();
23
- activeConfig = { ...activeConfig, ...remoteStatic };
24
- }
25
- } catch (e) {
26
- console.warn("Failed to load static details from R2, using defaults.");
27
- }
28
- return activeConfig;
29
- }
30
- function getConfig() {
31
- return activeConfig;
32
- }
33
- class SiteStore {
34
- constructor() {
35
- this.config = null;
36
- this.listeners = /* @__PURE__ */ new Set();
37
- }
38
- static getInstance() {
39
- if (!SiteStore.instance) {
40
- SiteStore.instance = new SiteStore();
41
- }
42
- return SiteStore.instance;
43
- }
44
- async init(infra) {
45
- this.config = await initializeConfig(infra);
46
- this.notify();
47
- return this.config;
48
- }
49
- subscribe(listener) {
50
- this.listeners.add(listener);
51
- if (this.config) listener(this.config);
52
- return () => this.listeners.delete(listener);
53
- }
54
- notify() {
55
- if (this.config) {
56
- this.listeners.forEach((l) => l(this.config));
57
- }
58
- }
59
- getConfig() {
60
- return this.config || getConfig();
61
- }
62
- }
1
+ import { S as SiteStore } from "./site-store-CGV9c2DI.js";
2
+ import { MarkdownPipeline } from "@leadertechie/r2tohtml";
3
+ import { init, reinit } from "@leadertechie/md2interact";
63
4
  const pipeline = new MarkdownPipeline({
64
5
  imagePathPrefix: "images/",
6
+ preserveRawHTML: true,
7
+ errorRecovery: "warn",
8
+ maxRecursionDepth: 50,
65
9
  styleOptions: {
66
10
  classPrefix: "md-",
67
- addHeadingIds: true
11
+ addHeadingIds: true,
12
+ emitScopeAnchors: true
13
+ },
14
+ slotPattern: /\[\[(.*?)\]\]/g,
15
+ onSlot: (name) => {
16
+ const slotMap = {
17
+ "SITE_TITLE": document?.querySelector("title")?.textContent || "My Site",
18
+ "CURRENT_YEAR": (/* @__PURE__ */ new Date()).getFullYear().toString()
19
+ };
20
+ return slotMap[name] || `[[${name}]]`;
68
21
  }
69
22
  });
70
23
  const renderMarkdown = (content) => {
@@ -91,12 +44,12 @@ const generatePageContent = (pathname, routes, footerLinks, data) => {
91
44
  const footerTemplate = `
92
45
  <my-footer
93
46
  copyright="${copyright}"
94
- footerlinks='\${JSON.stringify(footerLinks)}'>
47
+ footer-links='${JSON.stringify(footerLinks)}'>
95
48
  </my-footer>`;
96
49
  const renderContentGists = (items = [], title, type) => {
97
50
  const listHtml = items.length > 0 ? items.map((item) => `
98
51
  <div class="gist-card">
99
- <a href="${type}/${item.slug}" data-route="${type}-${item.slug}"><h4>${item.title}</h4></a>
52
+ <a href="/${type}/${item.slug}" data-route="${type}-${item.slug}"><h4>${item.title}</h4></a>
100
53
  <p>${item.summary || item.description || ""}</p>
101
54
  <small>${new Date(item.date).toLocaleDateString()}</small>
102
55
  </div>
@@ -164,7 +117,7 @@ const generatePageContent = (pathname, routes, footerLinks, data) => {
164
117
  </aside>
165
118
  <div class="wide-main-column text-left">
166
119
  <div id="content-viewer">
167
- ${currentSlug ? isBlog ? `<my-blog-viewer slug="${currentSlug}"></my-blog-viewer>` : `<my-story-viewer slug="${currentSlug}"></my-story-viewer>` : items && items.length > 0 ? `<p>Select a ${type.slice(0, -1)} to read.</p>` : ""}
120
+ ${currentSlug ? isBlog ? `<my-blog-viewer slug="${currentSlug}" api-url="${data?.apiUrl || ""}"></my-blog-viewer>` : `<my-story-viewer slug="${currentSlug}" api-url="${data?.apiUrl || ""}"></my-story-viewer>` : items && items.length > 0 ? `<p>Select a ${type.slice(0, -1)} to read.</p>` : ""}
168
121
  </div>
169
122
  </div>
170
123
  </div>
@@ -220,20 +173,29 @@ class Router {
220
173
  { link: "/stories", text: "Stories" },
221
174
  { link: "/about-me", text: "About Me" }
222
175
  ];
223
- this.siteTitle = config.siteTitle;
224
- this.copyright = config.copyright;
225
176
  this.apiUrl = config.apiUrl;
177
+ }
178
+ get config() {
179
+ return this.ui.getStore().getConfig();
180
+ }
181
+ get siteTitle() {
182
+ return this.config.siteTitle;
183
+ }
184
+ get copyright() {
185
+ return this.config.copyright;
186
+ }
187
+ get footerLinks() {
226
188
  const normalizeUrl = (url) => {
227
189
  if (!url) return "";
228
190
  if (url.startsWith("http://") || url.startsWith("https://")) return url;
229
191
  if (url.startsWith("www.")) return `https://${url}`;
230
192
  return url;
231
193
  };
232
- this.footerLinks = [
233
- { text: "LinkedIn", link: normalizeUrl(config.linkedin) },
234
- { text: "GitHub", link: normalizeUrl(config.github) },
235
- { text: "Email", link: config.email ? `mailto:${config.email}` : "" }
236
- ].filter((link) => link.link !== "");
194
+ return [
195
+ { text: "LinkedIn", link: normalizeUrl(this.config.linkedin) || "https://linkedin.com" },
196
+ { text: "GitHub", link: normalizeUrl(this.config.github) || "https://github.com" },
197
+ { text: "Email", link: this.config.email ? `mailto:${this.config.email}` : "mailto:hello@example.com" }
198
+ ];
237
199
  }
238
200
  init(appElementId = "app") {
239
201
  this.appElement = document.getElementById(appElementId);
@@ -246,7 +208,10 @@ class Router {
246
208
  }
247
209
  setupEventListeners() {
248
210
  document.body.addEventListener("click", (event) => {
249
- const target = event.target.closest("a[data-route]");
211
+ const path = event.composedPath();
212
+ const target = path.find(
213
+ (el) => el instanceof HTMLElement && (el.matches("a[data-route]") || el.closest("a[data-route]"))
214
+ );
250
215
  if (target) {
251
216
  event.preventDefault();
252
217
  const route = target.getAttribute("href");
@@ -328,6 +293,13 @@ class Router {
328
293
  }
329
294
  canonicalLink.setAttribute("href", url);
330
295
  }
296
+ /**
297
+ * After rendering new content, reinitialize md2interact so it
298
+ * re-scans the DOM for interaction elements (poll, live-update, etc.)
299
+ */
300
+ afterRender() {
301
+ this.ui.reinitInteract();
302
+ }
331
303
  async renderHomePage() {
332
304
  let blogs = [];
333
305
  let stories = [];
@@ -338,9 +310,18 @@ class Router {
338
310
  fetch(`${this.apiUrl}/api/stories`),
339
311
  fetch(`${this.apiUrl}/api/home`)
340
312
  ]);
341
- if (blogsRes.ok) blogs = (await blogsRes.json()).slice(0, 3);
342
- if (storiesRes.ok) stories = (await storiesRes.json()).slice(0, 3);
343
- if (homeRes.ok) homeContent = (await homeRes.json()).content;
313
+ if (blogsRes.ok) {
314
+ const data = await blogsRes.json().catch(() => []);
315
+ blogs = Array.isArray(data) ? data.slice(0, 3) : [];
316
+ }
317
+ if (storiesRes.ok) {
318
+ const data = await storiesRes.json().catch(() => []);
319
+ stories = Array.isArray(data) ? data.slice(0, 3) : [];
320
+ }
321
+ if (homeRes.ok) {
322
+ const data = await homeRes.json().catch(() => ({}));
323
+ homeContent = data.content || "";
324
+ }
344
325
  } catch (e) {
345
326
  console.error("Failed to fetch home content", e);
346
327
  }
@@ -353,6 +334,7 @@ class Router {
353
334
  });
354
335
  if (this.appElement) this.appElement.innerHTML = pageContent.content;
355
336
  this.setPageMeta(pageContent.title, pageContent.description, pageContent.canonicalUrl);
337
+ this.afterRender();
356
338
  }
357
339
  renderAboutMePage() {
358
340
  const pageContent = generatePageContent("/about-me", this.routes, this.footerLinks, {
@@ -362,6 +344,7 @@ class Router {
362
344
  });
363
345
  if (this.appElement) this.appElement.innerHTML = pageContent.content;
364
346
  this.setPageMeta(pageContent.title, pageContent.description, pageContent.canonicalUrl);
347
+ this.afterRender();
365
348
  }
366
349
  async renderContentListPage(pathname) {
367
350
  const type = pathname === "/blogs" ? "blogs" : "stories";
@@ -372,7 +355,7 @@ class Router {
372
355
  } catch (e) {
373
356
  }
374
357
  const latestSlug = items.length > 0 ? items[0].slug : void 0;
375
- const data = type === "blogs" ? { blogs: items, slug: latestSlug } : { stories: items, slug: latestSlug };
358
+ const data = type === "blogs" ? { blogs: items, slug: latestSlug, apiUrl: this.apiUrl } : { stories: items, slug: latestSlug, apiUrl: this.apiUrl };
376
359
  const pageContent = generatePageContent(pathname, this.routes, this.footerLinks, {
377
360
  ...data,
378
361
  siteTitle: this.siteTitle,
@@ -380,6 +363,7 @@ class Router {
380
363
  });
381
364
  if (this.appElement) this.appElement.innerHTML = pageContent.content;
382
365
  this.setPageMeta(pageContent.title, pageContent.description, pageContent.canonicalUrl);
366
+ this.afterRender();
383
367
  }
384
368
  async renderContentDetailPage(pathname) {
385
369
  const isBlog = pathname.startsWith("/blogs/");
@@ -391,7 +375,7 @@ class Router {
391
375
  if (res.ok) items = await res.json();
392
376
  } catch (e) {
393
377
  }
394
- const data = type === "blogs" ? { blogs: items, slug } : { stories: items, slug };
378
+ const data = type === "blogs" ? { blogs: items, slug, apiUrl: this.apiUrl } : { stories: items, slug, apiUrl: this.apiUrl };
395
379
  const pageContent = generatePageContent(pathname, this.routes, this.footerLinks, {
396
380
  ...data,
397
381
  siteTitle: this.siteTitle,
@@ -399,12 +383,9 @@ class Router {
399
383
  });
400
384
  if (this.appElement) this.appElement.innerHTML = pageContent.content;
401
385
  this.setPageMeta(`${slug.replace(/-/g, " ")} - ${this.siteTitle}`, "Read more content", window.location.href);
386
+ this.afterRender();
402
387
  }
403
388
  async renderAdminPage() {
404
- generatePageContent("/admin", this.routes, this.footerLinks, {
405
- siteTitle: this.siteTitle,
406
- copyright: this.copyright
407
- });
408
389
  if (this.appElement) {
409
390
  this.appElement.innerHTML = `
410
391
  <my-banner header="${this.siteTitle}" logo="${this.logo}">
@@ -416,14 +397,16 @@ class Router {
416
397
  <main class="container container-medium">
417
398
  <admin-portal></admin-portal>
418
399
  </main>
419
- <my-footer copyright="${this.copyright}" footerlinks='[]'></my-footer>
400
+ <my-footer copyright="${this.copyright}" footerLinks='${JSON.stringify(this.footerLinks)}'></my-footer>
420
401
  `;
421
402
  }
403
+ this.afterRender();
422
404
  }
423
405
  }
424
406
  class WebsiteUI {
425
407
  constructor(config = {}) {
426
408
  this.router = null;
409
+ this.interactInitialized = false;
427
410
  this.store = SiteStore.getInstance();
428
411
  this.config = config;
429
412
  }
@@ -440,14 +423,64 @@ class WebsiteUI {
440
423
  apiUrl: this.config.apiUrl,
441
424
  baseUrl: this.config.baseUrl
442
425
  });
426
+ this.store.subscribe((config) => {
427
+ this.applyTheme();
428
+ this.updateFavicon();
429
+ this.updateBanner(config);
430
+ if (this.router && window.location.pathname !== "/admin") {
431
+ this.router.navigate(window.location.pathname);
432
+ }
433
+ });
443
434
  this.applyTheme();
435
+ this.updateFavicon();
436
+ this.updateBanner(this.store.getConfig());
444
437
  this.router = new Router(this);
445
438
  this.router.init(this.config.appElementId || "app");
439
+ this.initInteract();
446
440
  if (this.config.onBootstrap) {
447
441
  await this.config.onBootstrap(this);
448
442
  }
449
443
  console.log("WebsiteUI bootstrapped");
450
444
  }
445
+ /**
446
+ * Initialize md2interact for client-side DOM interactions,
447
+ * CSS hydration, and event bus communication.
448
+ */
449
+ initInteract() {
450
+ if (this.interactInitialized) return;
451
+ const interactCfg = this.config.interactConfig || {};
452
+ init({
453
+ interactions: interactCfg.interactions || {
454
+ "poll": { selector: '[data-interact="poll"]' },
455
+ "live-update": { selector: '[data-interact="live-update"]' },
456
+ "click-toggle": { selector: '[data-interact="click-toggle"]' },
457
+ "infinite-scroll": { selector: '[data-interact="infinite-scroll"]' },
458
+ "form-live": { selector: '[data-interact="form-live"]' }
459
+ },
460
+ cssHydration: interactCfg.cssHydration || {
461
+ inlineCritical: true,
462
+ layerInjection: true,
463
+ themeToggle: true
464
+ }
465
+ });
466
+ this.interactInitialized = true;
467
+ }
468
+ /**
469
+ * Reinitialize md2interact after dynamic content changes (e.g., SPA navigation).
470
+ * This re-scans the DOM for new interaction elements.
471
+ */
472
+ reinitInteract() {
473
+ if (this.interactInitialized) {
474
+ reinit();
475
+ }
476
+ }
477
+ updateBanner(config) {
478
+ const banner = document.querySelector("my-banner");
479
+ if (banner) {
480
+ banner.setAttribute("header", config.siteTitle || "My Site");
481
+ banner.setAttribute("logo", `${config.apiUrl}/api/logo?t=${Date.now()}`);
482
+ }
483
+ }
451
484
  applyTheme() {
452
485
  if (!this.config.theme) return;
453
486
  const { theme } = this.config;
@@ -461,6 +494,18 @@ class WebsiteUI {
461
494
  document.head.appendChild(style);
462
495
  }
463
496
  }
497
+ updateFavicon() {
498
+ const favicon = document.querySelector('link[rel="icon"]');
499
+ const logoUrl = `/api/logo?t=${Date.now()}`;
500
+ if (favicon) {
501
+ favicon.setAttribute("href", logoUrl);
502
+ } else {
503
+ const newFavicon = document.createElement("link");
504
+ newFavicon.rel = "icon";
505
+ newFavicon.href = logoUrl;
506
+ document.head.appendChild(newFavicon);
507
+ }
508
+ }
464
509
  getStore() {
465
510
  return this.store;
466
511
  }
@@ -506,13 +551,12 @@ class ThemeToggle extends HTMLElement {
506
551
  cursor: pointer;
507
552
  font-size: 1.5rem;
508
553
  padding: 0.5rem;
509
- color: var(--text-color, #213547); /* Default for light mode */
554
+ color: var(--text-color, #213547);
510
555
  transition: color 0.3s ease;
511
556
  }
512
557
  button:hover {
513
- color: var(--primary-color, #747bff); /* Hover color */
558
+ color: var(--primary-color, #747bff);
514
559
  }
515
- /* Dark mode specific styles for the button */
516
560
  html[data-theme='dark'] button {
517
561
  color: var(--dark-mode-text-color, rgba(255, 255, 255, 0.87));
518
562
  }
@@ -521,7 +565,7 @@ class ThemeToggle extends HTMLElement {
521
565
  }
522
566
  </style>
523
567
  <button id="theme-toggle" aria-label="Toggle theme">
524
- <span class="icon">${this.getSunIcon()}</span> <!-- Default to sun for light mode -->
568
+ <span class="icon">${this.getSunIcon()}</span>
525
569
  </button>
526
570
  `;
527
571
  }
@@ -610,13 +654,10 @@ const createHtmlTemplate = ({
610
654
  };
611
655
  export {
612
656
  Router as R,
613
- SiteStore as S,
614
657
  ThemeToggle as T,
615
658
  WebsiteUI as W,
616
- getConfig as a,
617
659
  bootstrap as b,
618
660
  createHtmlTemplate as c,
619
661
  generatePageContent as g,
620
- initializeConfig as i,
621
662
  renderMarkdown as r
622
663
  };