@leadertechie/personal-site-kit 0.1.0-alpha.7 → 0.1.0-alpha.8

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 (107) hide show
  1. package/dist/api/handlers/about-me.d.ts.map +1 -1
  2. package/dist/api/website-api.d.ts.map +1 -1
  3. package/dist/api.js +2 -2
  4. package/dist/chunks/index-CGvOrVf8.js +213 -0
  5. package/dist/chunks/{index-Bq8WDk9L.js → index-CYd_Pe2U.js} +1321 -473
  6. package/dist/chunks/{template-Boh_MKY5.js → template-D1uGvdWZ.js} +8 -7
  7. package/dist/chunks/{website-api-XoeLwo_N.js → website-api-FLejlWxJ.js} +47 -25
  8. package/dist/index.js +19 -9
  9. package/dist/prerender/data-fetcher.d.ts +19 -0
  10. package/dist/prerender/data-fetcher.d.ts.map +1 -0
  11. package/dist/prerender/page-content.d.ts.map +1 -1
  12. package/dist/prerender/page-generators/about.d.ts +16 -0
  13. package/dist/prerender/page-generators/about.d.ts.map +1 -0
  14. package/dist/prerender/page-generators/base.d.ts +26 -0
  15. package/dist/prerender/page-generators/base.d.ts.map +1 -0
  16. package/dist/prerender/page-generators/blog-detail.d.ts +15 -0
  17. package/dist/prerender/page-generators/blog-detail.d.ts.map +1 -0
  18. package/dist/prerender/page-generators/blogs-list.d.ts +17 -0
  19. package/dist/prerender/page-generators/blogs-list.d.ts.map +1 -0
  20. package/dist/prerender/page-generators/home.d.ts +19 -0
  21. package/dist/prerender/page-generators/home.d.ts.map +1 -0
  22. package/dist/prerender/page-generators/index.d.ts +9 -0
  23. package/dist/prerender/page-generators/index.d.ts.map +1 -0
  24. package/dist/prerender/page-generators/not-found.d.ts +14 -0
  25. package/dist/prerender/page-generators/not-found.d.ts.map +1 -0
  26. package/dist/prerender/page-generators/stories-list.d.ts +17 -0
  27. package/dist/prerender/page-generators/stories-list.d.ts.map +1 -0
  28. package/dist/prerender/page-generators/story-detail.d.ts +15 -0
  29. package/dist/prerender/page-generators/story-detail.d.ts.map +1 -0
  30. package/dist/prerender.js +109 -102
  31. package/dist/shared.js +1 -1
  32. package/dist/ui/about-me/index.d.ts +2 -10
  33. package/dist/ui/about-me/index.d.ts.map +1 -1
  34. package/dist/ui/admin/api.d.ts +16 -0
  35. package/dist/ui/admin/api.d.ts.map +1 -0
  36. package/dist/ui/admin/components/AboutMeSection.d.ts +7 -0
  37. package/dist/ui/admin/components/AboutMeSection.d.ts.map +1 -0
  38. package/dist/ui/admin/components/AdminSection.d.ts +13 -0
  39. package/dist/ui/admin/components/AdminSection.d.ts.map +1 -0
  40. package/dist/ui/admin/components/BlogsSection.d.ts +7 -0
  41. package/dist/ui/admin/components/BlogsSection.d.ts.map +1 -0
  42. package/dist/ui/admin/components/HomeSection.d.ts +7 -0
  43. package/dist/ui/admin/components/HomeSection.d.ts.map +1 -0
  44. package/dist/ui/admin/components/ImagesSection.d.ts +7 -0
  45. package/dist/ui/admin/components/ImagesSection.d.ts.map +1 -0
  46. package/dist/ui/admin/components/LoginForm.d.ts +9 -0
  47. package/dist/ui/admin/components/LoginForm.d.ts.map +1 -0
  48. package/dist/ui/admin/components/LogoSection.d.ts +7 -0
  49. package/dist/ui/admin/components/LogoSection.d.ts.map +1 -0
  50. package/dist/ui/admin/components/ProfileSection.d.ts +7 -0
  51. package/dist/ui/admin/components/ProfileSection.d.ts.map +1 -0
  52. package/dist/ui/admin/components/StaticSection.d.ts +13 -0
  53. package/dist/ui/admin/components/StaticSection.d.ts.map +1 -0
  54. package/dist/ui/admin/components/StoriesSection.d.ts +7 -0
  55. package/dist/ui/admin/components/StoriesSection.d.ts.map +1 -0
  56. package/dist/ui/admin/components/index.d.ts +11 -0
  57. package/dist/ui/admin/components/index.d.ts.map +1 -0
  58. package/dist/ui/admin/index.d.ts +10 -26
  59. package/dist/ui/admin/index.d.ts.map +1 -1
  60. package/dist/ui/admin/types.d.ts +24 -0
  61. package/dist/ui/admin/types.d.ts.map +1 -0
  62. package/dist/ui/blog-viewer/index.d.ts.map +1 -1
  63. package/dist/ui/index.d.ts.map +1 -1
  64. package/dist/ui/story-viewer/index.d.ts.map +1 -1
  65. package/dist/ui.js +14 -4
  66. package/package.json +1 -1
  67. package/src/api/handlers/about-me.ts +19 -9
  68. package/src/api/handlers/auth-handler.ts +18 -8
  69. package/src/api/handlers/content.ts +1 -1
  70. package/src/api/handlers/home.ts +2 -2
  71. package/src/api/website-api.ts +11 -7
  72. package/src/prerender/__tests__/page-content.test.ts +1 -11
  73. package/src/prerender/data-fetcher.ts +93 -0
  74. package/src/prerender/page-content.ts +109 -106
  75. package/src/prerender/page-generators/about.ts +38 -0
  76. package/src/prerender/page-generators/base.ts +77 -0
  77. package/src/prerender/page-generators/blog-detail.ts +35 -0
  78. package/src/prerender/page-generators/blogs-list.ts +43 -0
  79. package/src/prerender/page-generators/home.ts +54 -0
  80. package/src/prerender/page-generators/index.ts +8 -0
  81. package/src/prerender/page-generators/not-found.ts +36 -0
  82. package/src/prerender/page-generators/stories-list.ts +43 -0
  83. package/src/prerender/page-generators/story-detail.ts +35 -0
  84. package/src/shared/config/index.ts +1 -1
  85. package/src/shared/page-content.ts +1 -1
  86. package/src/shared/router.ts +5 -5
  87. package/src/ui/about-me/index.ts +14 -57
  88. package/src/ui/admin/api.ts +93 -0
  89. package/src/ui/admin/components/AboutMeSection.ts +47 -0
  90. package/src/ui/admin/components/AdminSection.ts +134 -0
  91. package/src/ui/admin/components/BlogsSection.ts +62 -0
  92. package/src/ui/admin/components/HomeSection.ts +47 -0
  93. package/src/ui/admin/components/ImagesSection.ts +54 -0
  94. package/src/ui/admin/components/LoginForm.ts +116 -0
  95. package/src/ui/admin/components/LogoSection.ts +51 -0
  96. package/src/ui/admin/components/ProfileSection.ts +47 -0
  97. package/src/ui/admin/components/StaticSection.ts +67 -0
  98. package/src/ui/admin/components/StoriesSection.ts +62 -0
  99. package/src/ui/admin/components/index.ts +10 -0
  100. package/src/ui/admin/index.ts +192 -434
  101. package/src/ui/admin/types.ts +26 -0
  102. package/src/ui/blog-viewer/index.ts +4 -1
  103. package/src/ui/index.ts +7 -0
  104. package/src/ui/story-viewer/index.ts +4 -1
  105. package/dist/ui/about-me/renderer.d.ts +0 -6
  106. package/dist/ui/about-me/renderer.d.ts.map +0 -1
  107. package/src/ui/about-me/renderer.ts +0 -23
@@ -66,24 +66,34 @@ export async function handleAboutMe(env?: any): Promise<Response> {
66
66
  }
67
67
  console.log('handleAboutMe: r2 created, fetching data');
68
68
 
69
- const [profileObj, astResult] = await Promise.all([
69
+ const [profileObj, rendered] = await Promise.all([
70
70
  r2.getObject('profile.json'),
71
- r2.getWithAST('about-me.md')
71
+ r2.getRendered('about-me.md')
72
72
  ]);
73
73
 
74
- console.log('handleAboutMe: profileObj =', !!profileObj, 'astResult =', !!astResult);
75
-
76
- if (!profileObj || !astResult) {
77
- throw new Error('Content not found in R2');
74
+ if (!rendered) {
75
+ return new Response(JSON.stringify({ error: 'About-me content not found. Please run seed.' }), {
76
+ status: 404,
77
+ headers: { 'Content-Type': 'application/json' },
78
+ });
78
79
  }
79
80
 
80
- const profile = await profileObj.json() as Profile;
81
+ let profile: Profile = {
82
+ name: 'Your Name',
83
+ title: 'Professional',
84
+ experience: 'Experienced',
85
+ profileImageUrl: ''
86
+ };
87
+
88
+ if (profileObj) {
89
+ profile = await profileObj.json() as Profile;
90
+ }
81
91
  console.log('handleAboutMe: profile loaded:', profile.name);
82
92
 
83
93
  const responseData: AboutMeApiResponse = {
84
94
  profile,
85
- contentNodes: astResult.contentNodes,
86
- processedMarkdown: ''
95
+ contentNodes: [],
96
+ processedMarkdown: rendered.content
87
97
  };
88
98
 
89
99
  return new Response(JSON.stringify(responseData), {
@@ -60,18 +60,21 @@ export async function handleAuth(request: Request, env: any, subpath: string): P
60
60
  }
61
61
 
62
62
  async function handleSetup(request: Request, env: any, clientIP: string, origin: string): Promise<Response> {
63
+ console.log('handleSetup: starting setup, env.KV exists:', !!env.KV);
63
64
  if (request.method !== 'POST') {
64
65
  return createErrorResponse('Method not allowed', 405);
65
66
  }
66
67
 
67
- const existing = await getAuthStore(env);
68
- if (existing) {
69
- return createErrorResponse('Admin already configured. Use /auth/login to login.', 400);
70
- }
71
-
72
68
  try {
69
+ const existing = await getAuthStore(env);
70
+ console.log('handleSetup: existing store check:', !!existing);
71
+ if (existing) {
72
+ return createErrorResponse('Admin already configured. Use /auth/login to login.', 400);
73
+ }
74
+
73
75
  const body = await request.json();
74
76
  const { username, password } = body;
77
+ console.log('handleSetup: body parsed, username:', username);
75
78
 
76
79
  if (!username || !password) {
77
80
  return createErrorResponse('Username and password required', 400);
@@ -81,18 +84,23 @@ async function handleSetup(request: Request, env: any, clientIP: string, origin:
81
84
  return createErrorResponse('Username must be 3+ chars, password must be 8+ chars', 400);
82
85
  }
83
86
 
87
+ console.log('handleSetup: calling setupAuth');
84
88
  await setupAuth(env, username, password);
89
+ console.log('handleSetup: setupAuth successful');
85
90
  await clearRateLimit(env, clientIP);
86
91
 
87
92
  const token = crypto.randomUUID();
93
+ console.log('handleSetup: session token generated');
88
94
  await env.KV.put(`session:${token}`, JSON.stringify({
89
95
  createdAt: Date.now(),
90
96
  expiresAt: Date.now() + (7 * 24 * 60 * 60 * 1000)
91
97
  }), { expirationTtl: 7 * 24 * 60 * 60 });
98
+ console.log('handleSetup: session stored in KV');
92
99
 
93
100
  const headers: Record<string, string> = {
94
101
  'Content-Type': 'application/json',
95
- 'Set-Cookie': createSessionCookie(token, origin)
102
+ 'Set-Cookie': createSessionCookie(token, origin),
103
+ 'X-Session-Token': token
96
104
  };
97
105
 
98
106
  return new Response(JSON.stringify({
@@ -103,7 +111,8 @@ async function handleSetup(request: Request, env: any, clientIP: string, origin:
103
111
  headers
104
112
  });
105
113
  } catch (e) {
106
- return createErrorResponse('Invalid request body', 400);
114
+ console.error('handleSetup: error occurred:', e);
115
+ return createErrorResponse('Internal server error: ' + (e as Error).message, 500);
107
116
  }
108
117
  }
109
118
 
@@ -145,7 +154,8 @@ async function handleLogin(request: Request, env: any, clientIP: string, origin:
145
154
 
146
155
  const headers: Record<string, string> = {
147
156
  'Content-Type': 'application/json',
148
- 'Set-Cookie': createSessionCookie(token, origin)
157
+ 'Set-Cookie': createSessionCookie(token, origin),
158
+ 'X-Session-Token': token
149
159
  };
150
160
 
151
161
  return new Response(JSON.stringify({
@@ -48,7 +48,7 @@ export async function handleContent(request: Request, env: any, subpath: string)
48
48
  return createErrorResponse('Admin not configured. Use POST /auth/setup to configure.', 401);
49
49
  }
50
50
 
51
- const sessionToken = getSessionToken(request);
51
+ const sessionToken = getSessionToken(request) || request.headers.get('X-Session-Token');
52
52
  let isAuthenticated = false;
53
53
 
54
54
  if (sessionToken) {
@@ -46,8 +46,8 @@ export async function handleHome(env?: any): Promise<Response> {
46
46
 
47
47
  const r2 = getLoader(env);
48
48
  const [astResult, renderedResult] = await Promise.all([
49
- r2.getWithAST('home.md'),
50
- r2.getRendered('home.md')
49
+ r2.getWithAST('pages/home.md'),
50
+ r2.getRendered('pages/home.md')
51
51
  ]);
52
52
 
53
53
  if (!astResult || !renderedResult) {
@@ -21,23 +21,23 @@ export class WebsiteAPI {
21
21
  private addCORSHeaders(response: Response): Response {
22
22
  response.headers.set('Access-Control-Allow-Origin', '*' );
23
23
  response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS' );
24
- response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
24
+ response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Session-Token');
25
25
  return response;
26
26
  }
27
27
 
28
28
  private addAdminCORSHeaders(response: Response, origin: string): Response {
29
- const allowOrigin = origin.includes('localhost') || origin.includes('127.0.0.1')
29
+ const allowOrigin = origin && (origin.includes('localhost') || origin.includes('127.0.0.1'))
30
30
  ? origin
31
31
  : 'same-origin';
32
32
  response.headers.set('Access-Control-Allow-Origin', allowOrigin);
33
33
  response.headers.set('Access-Control-Allow-Credentials', 'true');
34
34
  response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
35
- response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
35
+ response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Session-Token');
36
36
  return response;
37
37
  }
38
38
 
39
39
  private handleCORS(origin: string): Response {
40
- const allowOrigin = origin.includes('localhost') || origin.includes('127.0.0.1')
40
+ const allowOrigin = origin && (origin.includes('localhost') || origin.includes('127.0.0.1'))
41
41
  ? origin
42
42
  : '*';
43
43
  return new Response(null, {
@@ -45,7 +45,7 @@ export class WebsiteAPI {
45
45
  headers: {
46
46
  'Access-Control-Allow-Origin': allowOrigin ,
47
47
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS' ,
48
- 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
48
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Session-Token',
49
49
  'Access-Control-Allow-Credentials': 'true',
50
50
  'Access-Control-Max-Age': '86400',
51
51
  },
@@ -79,6 +79,12 @@ export class WebsiteAPI {
79
79
  return this.addAdminCORSHeaders(await handleContent(request, env, subpath), origin);
80
80
  }
81
81
 
82
+ // Check for auth route (auth/*)
83
+ if (route === 'auth' || route.startsWith('auth/')) {
84
+ const subpath = route.replace(/^auth\/?/, '');
85
+ return this.addAdminCORSHeaders(await handleAuth(request, env, subpath || '/'), origin);
86
+ }
87
+
82
88
  switch (route) {
83
89
  case 'info':
84
90
  return this.addCORSHeaders(await handleInfo());
@@ -101,8 +107,6 @@ export class WebsiteAPI {
101
107
  return this.addCORSHeaders(await handleLogo(env));
102
108
  case 'static':
103
109
  return this.addCORSHeaders(await handleStaticDetails(env));
104
- case 'auth':
105
- return this.addAdminCORSHeaders(await handleAuth(request, env, '/auth'), origin);
106
110
  case 'blogs':
107
111
  return this.addCORSHeaders(await handleBlogs(env));
108
112
  case 'blogs/latest':
@@ -7,25 +7,15 @@ describe('generatePageContent', () => {
7
7
  { link: '/about-me', text: 'About' }
8
8
  ];
9
9
  const mockFooterLinks = [
10
- { text: 'Link', href: '#' }
10
+ { text: 'Link', link: '#' }
11
11
  ];
12
12
  const mockEnv = {
13
- CONTENT_BUCKET: {
14
- get: vi.fn().mockResolvedValue({
15
- json: () => Promise.resolve({ name: 'User', title: 'Professional', experience: 'some' })
16
- }),
17
- list: vi.fn().mockResolvedValue({ objects: [] })
18
- },
19
13
  apiUrl: 'https://api.example.com',
20
14
  baseUrl: 'https://www.example.com'
21
15
  };
22
16
 
23
17
  beforeEach(() => {
24
18
  vi.clearAllMocks();
25
- global.fetch = vi.fn().mockResolvedValue({
26
- ok: true,
27
- json: () => Promise.resolve({ siteTitle: 'My Site', copyright: '2026' })
28
- });
29
19
  });
30
20
 
31
21
  it('should generate home page content', async () => {
@@ -0,0 +1,93 @@
1
+ import { R2ContentLoader } from "@leadertechie/r2tohtml";
2
+
3
+ export interface Profile {
4
+ name: string;
5
+ title: string;
6
+ experience: string;
7
+ profileImageUrl: string;
8
+ }
9
+
10
+ export interface BlogMeta {
11
+ slug: string;
12
+ title: string;
13
+ summary: string;
14
+ tags: string[];
15
+ date: string;
16
+ }
17
+
18
+ let loader: R2ContentLoader | null = null;
19
+
20
+ function getLoader(env: any): R2ContentLoader | null {
21
+ if (!loader) {
22
+ if (!env?.CONTENT_BUCKET) return null;
23
+ loader = new R2ContentLoader(
24
+ { bucket: env.CONTENT_BUCKET, cacheTTL: 5 * 60 * 1000 },
25
+ { md2html: { imagePathPrefix: "images/", styleOptions: { classPrefix: "md-", addHeadingIds: true } } }
26
+ );
27
+ }
28
+ return loader;
29
+ }
30
+
31
+ export async function fetchProfile(env: any): Promise<Profile | null> {
32
+ try {
33
+ const r2 = getLoader(env);
34
+ if (!r2) return null;
35
+ const obj = await r2.getObject("profile.json");
36
+ if (!obj) return null;
37
+ return await obj.json() as Profile;
38
+ } catch { return null; }
39
+ }
40
+
41
+ export async function fetchAboutMe(env: any): Promise<string> {
42
+ try {
43
+ const r2 = getLoader(env);
44
+ if (!r2) return "";
45
+ const result = await r2.getRendered("about-me.md");
46
+ return result?.content || "";
47
+ } catch { return ""; }
48
+ }
49
+
50
+ export async function fetchHome(env: any): Promise<string> {
51
+ try {
52
+ const r2 = getLoader(env);
53
+ if (!r2) return "";
54
+ const result = await r2.getRendered("pages/home.md");
55
+ return result?.content || "";
56
+ } catch { return ""; }
57
+ }
58
+
59
+ export async function fetchLatestBlogSummaries(env: any, count: number = 3): Promise<BlogMeta[]> {
60
+ try {
61
+ const r2 = getLoader(env);
62
+ if (!r2) return [];
63
+ const list = await r2.list("blogs/");
64
+ const metas: BlogMeta[] = [];
65
+ for (const obj of list.objects) {
66
+ if (obj.key.endsWith(".json")) {
67
+ try {
68
+ const metaObj = await r2.getObject(obj.key);
69
+ if (metaObj) metas.push(await metaObj.json() as BlogMeta);
70
+ } catch {}
71
+ }
72
+ }
73
+ return metas.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()).slice(0, count);
74
+ } catch { return []; }
75
+ }
76
+
77
+ export async function fetchLatestStorySummaries(env: any, count: number = 3): Promise<BlogMeta[]> {
78
+ try {
79
+ const r2 = getLoader(env);
80
+ if (!r2) return [];
81
+ const list = await r2.list("stories/");
82
+ const metas: BlogMeta[] = [];
83
+ for (const obj of list.objects) {
84
+ if (obj.key.endsWith(".json")) {
85
+ try {
86
+ const metaObj = await r2.getObject(obj.key);
87
+ if (metaObj) metas.push(await metaObj.json() as BlogMeta);
88
+ } catch {}
89
+ }
90
+ }
91
+ return metas.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()).slice(0, count);
92
+ } catch { return []; }
93
+ }
@@ -68,7 +68,7 @@ async function fetchHome(env: any): Promise<string> {
68
68
  try {
69
69
  const r2 = getLoader(env);
70
70
  if (!r2) return "";
71
- const result = await r2.getRendered("home.md");
71
+ const result = await r2.getRendered("pages/home.md");
72
72
  return result?.content || "";
73
73
  } catch { return ""; }
74
74
  }
@@ -115,8 +115,8 @@ export const generatePageContent = async (
115
115
  footerLinks: IFooterLink[],
116
116
  env?: any
117
117
  ): Promise<PageContent> => {
118
- const apiUrl = env?.apiUrl || "https://api.techieleader.com";
119
- const baseUrl = env?.baseUrl || "https://www.techieleader.com";
118
+ const apiUrl = env?.API_URL || env?.apiUrl || "https://api.example.com";
119
+ const baseUrl = env?.BASE_URL || env?.baseUrl || "https://www.example.com";
120
120
 
121
121
  let staticDetails = {
122
122
  siteTitle: "My Personal Website",
@@ -131,23 +131,6 @@ export const generatePageContent = async (
131
131
  if (res.ok) staticDetails = await res.json();
132
132
  } catch (e) {}
133
133
 
134
- const logo = "/api/logo";
135
- const navLinks = routes.map(r => `<a href="${r.link}" class="nav-link" data-route="${r.link === "/" ? "home" : r.text.toLowerCase()}">${r.text}</a>`).join("");
136
-
137
- const bannerTemplate = `
138
- <my-banner header="${staticDetails.siteTitle}" logo="${logo}">
139
- <theme-toggle slot="theme-switcher"></theme-toggle>
140
- <nav slot="nav-links">
141
- ${navLinks}
142
- </nav>
143
- </my-banner>`;
144
-
145
- const footerTemplate = `
146
- <my-footer
147
- copyright="${staticDetails.copyright}"
148
- footerlinks='${JSON.stringify(footerLinks)}'>
149
- </my-footer>`;
150
-
151
134
  let profile: Profile | null = null;
152
135
  let aboutMeContent = "";
153
136
  let homeContent = "";
@@ -162,102 +145,122 @@ export const generatePageContent = async (
162
145
 
163
146
  const name = profile?.name || "User";
164
147
  const title = profile?.title || "Professional";
165
- const experience = profile?.experience || "some";
166
148
  const canonicalUrl = new URL(pathname, baseUrl).toString();
167
149
 
168
- if (pathname === "/" || pathname === "") {
169
- const homeHtml = homeContent || `<h1>Welcome to ${name}</h1><p>Upload home.md to customize this page.</p>`;
170
- const blogGists = latestBlogs.map(b => `<div class="gist-card"><a href="/blogs/${b.slug}"><h4>${b.title}</h4></a><p>${b.summary}</p><small>${b.date}</small></div>`).join("");
171
- const storyGists = latestStories.map(s => `<div class="gist-card"><a href="/stories/${s.slug}"><h4>${s.title}</h4></a><p>${s.summary}</p><small>${s.date}</small></div>`).join("");
172
-
173
- const mainContent = `
174
- ${bannerTemplate}
175
- <main class="container container-wide column-layout">
176
- <div class="main-column">
177
- ${homeHtml}
178
- </div>
179
- <div class="sidebar-column">
180
- <h3>Recent Blogs</h3>
181
- ${blogGists || "<p>No blogs yet.</p>"}
182
- <h3 class="mt-2">Recent Stories</h3>
183
- ${storyGists || "<p>No stories yet.</p>"}
184
- </div>
185
- </main>
186
- ${footerTemplate}`;
150
+ // Strategy pattern: map pathname patterns to generators
151
+ const strategies = {
152
+ home: async () => {
153
+ const { HomePageGenerator } = await import('./page-generators');
154
+ const generator = new HomePageGenerator();
155
+ return generator.generate({
156
+ routes,
157
+ footerLinks,
158
+ staticDetails,
159
+ apiUrl,
160
+ baseUrl,
161
+ pathname,
162
+ profile,
163
+ homeContent,
164
+ latestBlogs,
165
+ latestStories
166
+ });
167
+ },
168
+ about: async () => {
169
+ const { AboutPageGenerator } = await import('./page-generators');
170
+ const generator = new AboutPageGenerator();
171
+ return generator.generate({
172
+ routes,
173
+ footerLinks,
174
+ staticDetails,
175
+ apiUrl,
176
+ baseUrl,
177
+ pathname,
178
+ profile
179
+ });
180
+ },
181
+ blogsList: async () => {
182
+ const { BlogsListPageGenerator } = await import('./page-generators');
183
+ const generator = new BlogsListPageGenerator();
184
+ return generator.generate({
185
+ routes,
186
+ footerLinks,
187
+ staticDetails,
188
+ apiUrl,
189
+ baseUrl,
190
+ pathname,
191
+ latestBlogs,
192
+ name
193
+ });
194
+ },
195
+ storiesList: async () => {
196
+ const { StoriesListPageGenerator } = await import('./page-generators');
197
+ const generator = new StoriesListPageGenerator();
198
+ return generator.generate({
199
+ routes,
200
+ footerLinks,
201
+ staticDetails,
202
+ apiUrl,
203
+ baseUrl,
204
+ pathname,
205
+ latestStories,
206
+ name
207
+ });
208
+ },
209
+ blogDetail: async (slug: string) => {
210
+ const { BlogDetailPageGenerator } = await import('./page-generators');
211
+ const generator = new BlogDetailPageGenerator();
212
+ return generator.generate({
213
+ routes,
214
+ footerLinks,
215
+ staticDetails,
216
+ apiUrl,
217
+ baseUrl,
218
+ pathname,
219
+ slug
220
+ });
221
+ },
222
+ storyDetail: async (slug: string) => {
223
+ const { StoryDetailPageGenerator } = await import('./page-generators');
224
+ const generator = new StoryDetailPageGenerator();
225
+ return generator.generate({
226
+ routes,
227
+ footerLinks,
228
+ staticDetails,
229
+ apiUrl,
230
+ baseUrl,
231
+ pathname,
232
+ slug
233
+ });
234
+ },
235
+ notFound: async () => {
236
+ const { NotFoundPageGenerator } = await import('./page-generators');
237
+ const generator = new NotFoundPageGenerator();
238
+ return generator.generate({
239
+ routes,
240
+ footerLinks,
241
+ staticDetails,
242
+ apiUrl,
243
+ baseUrl,
244
+ pathname
245
+ });
246
+ }
247
+ };
187
248
 
188
- return {
189
- title: `${name} – ${title}`,
190
- description: `Welcome to ${name}'s personal website. Professional portfolio and content.`,
191
- canonicalUrl,
192
- content: mainContent
193
- };
249
+ if (pathname === "/" || pathname === "") {
250
+ return strategies.home();
194
251
  } else if (pathname === "/about-me") {
195
- const mainContent = `
196
- ${bannerTemplate}
197
- <main class="container container-narrow">
198
- <my-aboutme base-url="${apiUrl}"></my-aboutme>
199
- </main>
200
- ${footerTemplate}`;
201
-
202
- return {
203
- title: `About - ${name}`,
204
- description: `Learn more about ${name}'s experience and skills.`,
205
- canonicalUrl,
206
- content: mainContent
207
- };
252
+ return strategies.about();
208
253
  } else if (pathname === "/blogs" || pathname === "/blogs/") {
209
- const blogGists = latestBlogs.map(b => `<div class="gist-card"><a href="/blogs/${b.slug}"><h4>${b.title}</h4></a><p>${b.summary}</p><small>${b.date}</small></div>`).join("");
210
- const mainContent = `
211
- ${bannerTemplate}
212
- <main class="container container-wide">
213
- <h1>Blogs</h1>
214
- <input type="text" placeholder="Search blogs..." class="search-input" />
215
- <div class="blog-list">
216
- ${blogGists || "<p>No blogs yet.</p>"}
217
- </div>
218
- </main>
219
- ${footerTemplate}`;
220
- return { title: `Blogs – ${name}`, description: "Read the latest blog posts.", canonicalUrl, content: mainContent };
254
+ return strategies.blogsList();
221
255
  } else if (pathname === "/stories" || pathname === "/stories/") {
222
- const storyGists = latestStories.map(s => `<div class="gist-card"><a href="/stories/${s.slug}"><h4>${s.title}</h4></a><p>${s.summary}</p><small>${s.date}</small></div>`).join("");
223
- const mainContent = `
224
- ${bannerTemplate}
225
- <main class="container container-wide">
226
- <h1>Stories</h1>
227
- <input type="text" placeholder="Search stories..." class="search-input" />
228
- <div class="story-list">
229
- ${storyGists || "<p>No stories yet.</p>"}
230
- </div>
231
- </main>
232
- ${footerTemplate}`;
233
- return { title: `Stories – ${name}`, description: "Read the latest stories.", canonicalUrl, content: mainContent };
256
+ return strategies.storiesList();
234
257
  } else if (pathname.startsWith("/blogs/")) {
235
258
  const slug = pathname.replace("/blogs/", "").replace("/", "");
236
- const mainContent = `
237
- ${bannerTemplate}
238
- <main class="container container-narrow">
239
- <my-blog-viewer slug="${slug}"></my-blog-viewer>
240
- </main>
241
- ${footerTemplate}`;
242
- return { title: `Blog: ${slug}`, description: "Blog post", canonicalUrl, content: mainContent };
259
+ return strategies.blogDetail(slug);
243
260
  } else if (pathname.startsWith("/stories/")) {
244
261
  const slug = pathname.replace("/stories/", "").replace("/", "");
245
- const mainContent = `
246
- ${bannerTemplate}
247
- <main class="container container-narrow">
248
- <my-story-viewer slug="${slug}"></my-story-viewer>
249
- </main>
250
- ${footerTemplate}`;
251
- return { title: `Story: ${slug}`, description: "Story post", canonicalUrl, content: mainContent };
262
+ return strategies.storyDetail(slug);
252
263
  } else {
253
- const mainContent = `
254
- ${bannerTemplate}
255
- <main class="container container-narrow text-center">
256
- <h1>Page Not Found</h1>
257
- <p>The page you're looking for doesn't exist.</p>
258
- <p><a href="/">Return to home</a></p>
259
- </main>
260
- ${footerTemplate}`;
261
- return { title: "404 Not Found", description: "The page you requested could not be found.", canonicalUrl, content: mainContent };
264
+ return strategies.notFound();
262
265
  }
263
266
  };
@@ -0,0 +1,38 @@
1
+ import { BasePageGenerator, StaticDetails } from './base';
2
+ import { IRoute, IFooterLink, PageContent } from '../page-content';
3
+ import { Profile } from '../data-fetcher';
4
+
5
+ export interface AboutPageData {
6
+ routes: IRoute[];
7
+ footerLinks: IFooterLink[];
8
+ staticDetails: StaticDetails;
9
+ apiUrl: string;
10
+ baseUrl: string;
11
+ pathname: string;
12
+ profile: Profile | null;
13
+ }
14
+
15
+ export class AboutPageGenerator extends BasePageGenerator {
16
+ public generate(data: AboutPageData): PageContent {
17
+ const { profile, staticDetails, ...baseData } = data;
18
+
19
+ const name = profile?.name || "User";
20
+
21
+ const mainContent = `
22
+ <main class="container container-narrow">
23
+ <my-aboutme base-url="${baseData.apiUrl}"></my-aboutme>
24
+ </main>`;
25
+
26
+ return this.generatePage(
27
+ baseData.pathname,
28
+ baseData.routes,
29
+ baseData.footerLinks,
30
+ staticDetails,
31
+ baseData.apiUrl,
32
+ baseData.baseUrl,
33
+ mainContent,
34
+ `About - ${name}`,
35
+ `Learn more about ${name}'s experience and skills.`
36
+ );
37
+ }
38
+ }