@leadertechie/personal-site-kit 0.1.0-alpha.6 → 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 (109) hide show
  1. package/dist/api/handlers/about-me.d.ts.map +1 -1
  2. package/dist/api/handlers/auth-handler.d.ts.map +1 -1
  3. package/dist/api/website-api.d.ts.map +1 -1
  4. package/dist/api.js +2 -2
  5. package/dist/chunks/index-CGvOrVf8.js +213 -0
  6. package/dist/chunks/{index-C3wLSCKU.js → index-CYd_Pe2U.js} +1353 -482
  7. package/dist/chunks/{template-MawmknFQ.js → template-D1uGvdWZ.js} +10 -8
  8. package/dist/chunks/{website-api-DI3muo2s.js → website-api-FLejlWxJ.js} +78 -41
  9. package/dist/index.js +19 -9
  10. package/dist/prerender/data-fetcher.d.ts +19 -0
  11. package/dist/prerender/data-fetcher.d.ts.map +1 -0
  12. package/dist/prerender/page-content.d.ts.map +1 -1
  13. package/dist/prerender/page-generators/about.d.ts +16 -0
  14. package/dist/prerender/page-generators/about.d.ts.map +1 -0
  15. package/dist/prerender/page-generators/base.d.ts +26 -0
  16. package/dist/prerender/page-generators/base.d.ts.map +1 -0
  17. package/dist/prerender/page-generators/blog-detail.d.ts +15 -0
  18. package/dist/prerender/page-generators/blog-detail.d.ts.map +1 -0
  19. package/dist/prerender/page-generators/blogs-list.d.ts +17 -0
  20. package/dist/prerender/page-generators/blogs-list.d.ts.map +1 -0
  21. package/dist/prerender/page-generators/home.d.ts +19 -0
  22. package/dist/prerender/page-generators/home.d.ts.map +1 -0
  23. package/dist/prerender/page-generators/index.d.ts +9 -0
  24. package/dist/prerender/page-generators/index.d.ts.map +1 -0
  25. package/dist/prerender/page-generators/not-found.d.ts +14 -0
  26. package/dist/prerender/page-generators/not-found.d.ts.map +1 -0
  27. package/dist/prerender/page-generators/stories-list.d.ts +17 -0
  28. package/dist/prerender/page-generators/stories-list.d.ts.map +1 -0
  29. package/dist/prerender/page-generators/story-detail.d.ts +15 -0
  30. package/dist/prerender/page-generators/story-detail.d.ts.map +1 -0
  31. package/dist/prerender.js +109 -102
  32. package/dist/shared/config/index.d.ts.map +1 -1
  33. package/dist/shared.js +1 -1
  34. package/dist/ui/about-me/index.d.ts +2 -10
  35. package/dist/ui/about-me/index.d.ts.map +1 -1
  36. package/dist/ui/admin/api.d.ts +16 -0
  37. package/dist/ui/admin/api.d.ts.map +1 -0
  38. package/dist/ui/admin/components/AboutMeSection.d.ts +7 -0
  39. package/dist/ui/admin/components/AboutMeSection.d.ts.map +1 -0
  40. package/dist/ui/admin/components/AdminSection.d.ts +13 -0
  41. package/dist/ui/admin/components/AdminSection.d.ts.map +1 -0
  42. package/dist/ui/admin/components/BlogsSection.d.ts +7 -0
  43. package/dist/ui/admin/components/BlogsSection.d.ts.map +1 -0
  44. package/dist/ui/admin/components/HomeSection.d.ts +7 -0
  45. package/dist/ui/admin/components/HomeSection.d.ts.map +1 -0
  46. package/dist/ui/admin/components/ImagesSection.d.ts +7 -0
  47. package/dist/ui/admin/components/ImagesSection.d.ts.map +1 -0
  48. package/dist/ui/admin/components/LoginForm.d.ts +9 -0
  49. package/dist/ui/admin/components/LoginForm.d.ts.map +1 -0
  50. package/dist/ui/admin/components/LogoSection.d.ts +7 -0
  51. package/dist/ui/admin/components/LogoSection.d.ts.map +1 -0
  52. package/dist/ui/admin/components/ProfileSection.d.ts +7 -0
  53. package/dist/ui/admin/components/ProfileSection.d.ts.map +1 -0
  54. package/dist/ui/admin/components/StaticSection.d.ts +13 -0
  55. package/dist/ui/admin/components/StaticSection.d.ts.map +1 -0
  56. package/dist/ui/admin/components/StoriesSection.d.ts +7 -0
  57. package/dist/ui/admin/components/StoriesSection.d.ts.map +1 -0
  58. package/dist/ui/admin/components/index.d.ts +11 -0
  59. package/dist/ui/admin/components/index.d.ts.map +1 -0
  60. package/dist/ui/admin/index.d.ts +10 -26
  61. package/dist/ui/admin/index.d.ts.map +1 -1
  62. package/dist/ui/admin/types.d.ts +24 -0
  63. package/dist/ui/admin/types.d.ts.map +1 -0
  64. package/dist/ui/blog-viewer/index.d.ts.map +1 -1
  65. package/dist/ui/index.d.ts.map +1 -1
  66. package/dist/ui/story-viewer/index.d.ts.map +1 -1
  67. package/dist/ui.js +14 -4
  68. package/package.json +1 -1
  69. package/src/api/handlers/about-me.ts +19 -9
  70. package/src/api/handlers/auth-handler.ts +41 -18
  71. package/src/api/handlers/content.ts +1 -1
  72. package/src/api/handlers/home.ts +2 -2
  73. package/src/api/website-api.ts +25 -13
  74. package/src/prerender/__tests__/page-content.test.ts +1 -11
  75. package/src/prerender/data-fetcher.ts +93 -0
  76. package/src/prerender/page-content.ts +109 -106
  77. package/src/prerender/page-generators/about.ts +38 -0
  78. package/src/prerender/page-generators/base.ts +77 -0
  79. package/src/prerender/page-generators/blog-detail.ts +35 -0
  80. package/src/prerender/page-generators/blogs-list.ts +43 -0
  81. package/src/prerender/page-generators/home.ts +54 -0
  82. package/src/prerender/page-generators/index.ts +8 -0
  83. package/src/prerender/page-generators/not-found.ts +36 -0
  84. package/src/prerender/page-generators/stories-list.ts +43 -0
  85. package/src/prerender/page-generators/story-detail.ts +35 -0
  86. package/src/shared/config/index.ts +4 -2
  87. package/src/shared/page-content.ts +1 -1
  88. package/src/shared/router.ts +5 -5
  89. package/src/ui/about-me/index.ts +23 -57
  90. package/src/ui/admin/api.ts +93 -0
  91. package/src/ui/admin/components/AboutMeSection.ts +47 -0
  92. package/src/ui/admin/components/AdminSection.ts +134 -0
  93. package/src/ui/admin/components/BlogsSection.ts +62 -0
  94. package/src/ui/admin/components/HomeSection.ts +47 -0
  95. package/src/ui/admin/components/ImagesSection.ts +54 -0
  96. package/src/ui/admin/components/LoginForm.ts +116 -0
  97. package/src/ui/admin/components/LogoSection.ts +51 -0
  98. package/src/ui/admin/components/ProfileSection.ts +47 -0
  99. package/src/ui/admin/components/StaticSection.ts +67 -0
  100. package/src/ui/admin/components/StoriesSection.ts +62 -0
  101. package/src/ui/admin/components/index.ts +10 -0
  102. package/src/ui/admin/index.ts +192 -434
  103. package/src/ui/admin/types.ts +26 -0
  104. package/src/ui/blog-viewer/index.ts +4 -1
  105. package/src/ui/index.ts +7 -0
  106. package/src/ui/story-viewer/index.ts +4 -1
  107. package/dist/ui/about-me/renderer.d.ts +0 -5
  108. package/dist/ui/about-me/renderer.d.ts.map +0 -1
  109. package/src/ui/about-me/renderer.ts +0 -7
@@ -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), {
@@ -10,18 +10,25 @@ import {
10
10
  MAX_ATTEMPTS
11
11
  } from './auth';
12
12
 
13
- function createSessionCookie(token: string, secure: boolean): string {
13
+ function createSessionCookie(token: string, origin: string): string {
14
14
  const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toUTCString();
15
- const SameSite = secure ? 'Strict' : 'Lax';
16
- return `session=${token}; HttpOnly; Secure; SameSite=${SameSite}; Path=/; Expires=${expires}`;
15
+ const isSecure = origin.startsWith('https://');
16
+ const hostname = new URL(origin).hostname;
17
+ const SameSite = isSecure ? 'Strict' : 'Lax';
18
+
19
+ let cookie = `session=${token}; HttpOnly; SameSite=${SameSite}; Path=/; Expires=${expires}`;
20
+ if (isSecure) {
21
+ cookie += '; Secure';
22
+ }
23
+ cookie += `; Domain=${hostname}`;
24
+ return cookie;
17
25
  }
18
26
 
19
27
  export async function handleAuth(request: Request, env: any, subpath: string): Promise<Response> {
20
28
  const method = request.method;
21
29
  const clientIP = getClientIP(request);
22
30
  const path = subpath.replace(/^\//, '').split('/')[0];
23
- const url = new URL(request.url);
24
- const isSecure = url.protocol === 'https:';
31
+ const origin = request.headers.get('Origin') || new URL(request.url).origin;
25
32
 
26
33
  // Check rate limit for login attempts
27
34
  const rateCheck = await checkRateLimit(env, clientIP);
@@ -40,11 +47,11 @@ export async function handleAuth(request: Request, env: any, subpath: string): P
40
47
 
41
48
  switch (path) {
42
49
  case 'setup':
43
- return handleSetup(request, env, clientIP, isSecure);
50
+ return handleSetup(request, env, clientIP, origin);
44
51
  case 'status':
45
52
  return handleStatus(env);
46
53
  case 'login':
47
- return handleLogin(request, env, clientIP, isSecure);
54
+ return handleLogin(request, env, clientIP, origin);
48
55
  case 'logout':
49
56
  return handleLogout(request, env);
50
57
  default:
@@ -52,19 +59,22 @@ export async function handleAuth(request: Request, env: any, subpath: string): P
52
59
  }
53
60
  }
54
61
 
55
- async function handleSetup(request: Request, env: any, clientIP: string, isSecure: boolean): Promise<Response> {
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);
56
64
  if (request.method !== 'POST') {
57
65
  return createErrorResponse('Method not allowed', 405);
58
66
  }
59
67
 
60
- const existing = await getAuthStore(env);
61
- if (existing) {
62
- return createErrorResponse('Admin already configured. Use /auth/login to login.', 400);
63
- }
64
-
65
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
+
66
75
  const body = await request.json();
67
76
  const { username, password } = body;
77
+ console.log('handleSetup: body parsed, username:', username);
68
78
 
69
79
  if (!username || !password) {
70
80
  return createErrorResponse('Username and password required', 400);
@@ -74,18 +84,23 @@ async function handleSetup(request: Request, env: any, clientIP: string, isSecur
74
84
  return createErrorResponse('Username must be 3+ chars, password must be 8+ chars', 400);
75
85
  }
76
86
 
87
+ console.log('handleSetup: calling setupAuth');
77
88
  await setupAuth(env, username, password);
89
+ console.log('handleSetup: setupAuth successful');
78
90
  await clearRateLimit(env, clientIP);
79
91
 
80
92
  const token = crypto.randomUUID();
93
+ console.log('handleSetup: session token generated');
81
94
  await env.KV.put(`session:${token}`, JSON.stringify({
82
95
  createdAt: Date.now(),
83
96
  expiresAt: Date.now() + (7 * 24 * 60 * 60 * 1000)
84
97
  }), { expirationTtl: 7 * 24 * 60 * 60 });
98
+ console.log('handleSetup: session stored in KV');
85
99
 
86
100
  const headers: Record<string, string> = {
87
101
  'Content-Type': 'application/json',
88
- 'Set-Cookie': createSessionCookie(token, isSecure)
102
+ 'Set-Cookie': createSessionCookie(token, origin),
103
+ 'X-Session-Token': token
89
104
  };
90
105
 
91
106
  return new Response(JSON.stringify({
@@ -96,7 +111,8 @@ async function handleSetup(request: Request, env: any, clientIP: string, isSecur
96
111
  headers
97
112
  });
98
113
  } catch (e) {
99
- return createErrorResponse('Invalid request body', 400);
114
+ console.error('handleSetup: error occurred:', e);
115
+ return createErrorResponse('Internal server error: ' + (e as Error).message, 500);
100
116
  }
101
117
  }
102
118
 
@@ -109,7 +125,7 @@ async function handleStatus(env: any): Promise<Response> {
109
125
  });
110
126
  }
111
127
 
112
- async function handleLogin(request: Request, env: any, clientIP: string, isSecure: boolean): Promise<Response> {
128
+ async function handleLogin(request: Request, env: any, clientIP: string, origin: string): Promise<Response> {
113
129
  if (request.method !== 'POST') {
114
130
  return createErrorResponse('Method not allowed', 405);
115
131
  }
@@ -138,7 +154,8 @@ async function handleLogin(request: Request, env: any, clientIP: string, isSecur
138
154
 
139
155
  const headers: Record<string, string> = {
140
156
  'Content-Type': 'application/json',
141
- 'Set-Cookie': createSessionCookie(token, isSecure)
157
+ 'Set-Cookie': createSessionCookie(token, origin),
158
+ 'X-Session-Token': token
142
159
  };
143
160
 
144
161
  return new Response(JSON.stringify({
@@ -162,6 +179,7 @@ async function handleLogout(request: Request, env: any): Promise<Response> {
162
179
  return createErrorResponse('Method not allowed', 405);
163
180
  }
164
181
 
182
+ const origin = request.headers.get('Origin') || new URL(request.url).origin;
165
183
  const cookieHeader = request.headers.get('Cookie');
166
184
  const sessionToken = cookieHeader?.split(';')
167
185
  .find(c => c.trim().startsWith('session='))
@@ -171,11 +189,16 @@ async function handleLogout(request: Request, env: any): Promise<Response> {
171
189
  await env.KV.delete(`session:${sessionToken}`);
172
190
  }
173
191
 
192
+ const hostname = new URL(origin).hostname;
193
+ const isSecure = origin.startsWith('https://');
194
+ const SameSite = isSecure ? 'Strict' : 'Lax';
195
+ const logoutCookie = `session=; HttpOnly; SameSite=${SameSite}; Path=/; Max-Age=0; Domain=${hostname}${isSecure ? '; Secure' : ''}`;
196
+
174
197
  return new Response(JSON.stringify({ success: true, message: 'Logged out' }), {
175
198
  status: 200,
176
199
  headers: {
177
200
  'Content-Type': 'application/json',
178
- 'Set-Cookie': 'session=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0'
201
+ 'Set-Cookie': logoutCookie
179
202
  }
180
203
  });
181
204
  }
@@ -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,25 +21,32 @@ 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
- private addAdminCORSHeaders(response: Response): Response {
29
- response.headers.set('Access-Control-Allow-Origin', 'same-origin');
28
+ private addAdminCORSHeaders(response: Response, origin: string): Response {
29
+ const allowOrigin = origin && (origin.includes('localhost') || origin.includes('127.0.0.1'))
30
+ ? origin
31
+ : 'same-origin';
32
+ response.headers.set('Access-Control-Allow-Origin', allowOrigin);
30
33
  response.headers.set('Access-Control-Allow-Credentials', 'true');
31
34
  response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
32
- response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
35
+ response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Session-Token');
33
36
  return response;
34
37
  }
35
38
 
36
- private handleCORS(): Response {
39
+ private handleCORS(origin: string): Response {
40
+ const allowOrigin = origin && (origin.includes('localhost') || origin.includes('127.0.0.1'))
41
+ ? origin
42
+ : '*';
37
43
  return new Response(null, {
38
44
  status: 200,
39
45
  headers: {
40
- 'Access-Control-Allow-Origin': '*' ,
46
+ 'Access-Control-Allow-Origin': allowOrigin ,
41
47
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS' ,
42
- 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
48
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Session-Token',
49
+ 'Access-Control-Allow-Credentials': 'true',
43
50
  'Access-Control-Max-Age': '86400',
44
51
  },
45
52
  });
@@ -47,9 +54,10 @@ export class WebsiteAPI {
47
54
 
48
55
  public async fetch(request: Request, env: any): Promise<Response> {
49
56
  const url = new URL(request.url);
57
+ const origin = request.headers.get('Origin') || url.origin;
50
58
 
51
59
  if (request.method === 'OPTIONS') {
52
- return this.handleCORS();
60
+ return this.handleCORS(origin);
53
61
  }
54
62
 
55
63
  const pathname = url.pathname;
@@ -68,7 +76,13 @@ export class WebsiteAPI {
68
76
  // Check for content route first (content/*)
69
77
  if (route === 'content' || route.startsWith('content/')) {
70
78
  const subpath = route.replace(/^content\/?/, '');
71
- return this.addAdminCORSHeaders(await handleContent(request, env, subpath));
79
+ return this.addAdminCORSHeaders(await handleContent(request, env, subpath), origin);
80
+ }
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);
72
86
  }
73
87
 
74
88
  switch (route) {
@@ -83,18 +97,16 @@ export class WebsiteAPI {
83
97
  ?.split('=')[1];
84
98
  const session = sessionToken ? await env.KV.get(`session:${sessionToken}`, 'json') : null;
85
99
  if (!session || session.expiresAt < Date.now()) {
86
- return this.addAdminCORSHeaders(createErrorResponse('Unauthorized', 401));
100
+ return this.addAdminCORSHeaders(createErrorResponse('Unauthorized', 401), origin);
87
101
  }
88
102
  clearContentCache();
89
- return this.addAdminCORSHeaders(new Response(JSON.stringify({ success: true, message: 'Cache cleared' }), { status: 200 }));
103
+ return this.addAdminCORSHeaders(new Response(JSON.stringify({ success: true, message: 'Cache cleared' }), { status: 200 }), origin);
90
104
  case 'aboutme':
91
105
  return this.addCORSHeaders(await handleAboutMe(env));
92
106
  case 'logo':
93
107
  return this.addCORSHeaders(await handleLogo(env));
94
108
  case 'static':
95
109
  return this.addCORSHeaders(await handleStaticDetails(env));
96
- case 'auth':
97
- return this.addAdminCORSHeaders(await handleAuth(request, env, '/auth'));
98
110
  case 'blogs':
99
111
  return this.addCORSHeaders(await handleBlogs(env));
100
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
+ }