@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.
- package/dist/api/handlers/about-me.d.ts.map +1 -1
- package/dist/api/website-api.d.ts.map +1 -1
- package/dist/api.js +2 -2
- package/dist/chunks/index-CGvOrVf8.js +213 -0
- package/dist/chunks/{index-Bq8WDk9L.js → index-CYd_Pe2U.js} +1321 -473
- package/dist/chunks/{template-Boh_MKY5.js → template-D1uGvdWZ.js} +8 -7
- package/dist/chunks/{website-api-XoeLwo_N.js → website-api-FLejlWxJ.js} +47 -25
- package/dist/index.js +19 -9
- package/dist/prerender/data-fetcher.d.ts +19 -0
- package/dist/prerender/data-fetcher.d.ts.map +1 -0
- package/dist/prerender/page-content.d.ts.map +1 -1
- package/dist/prerender/page-generators/about.d.ts +16 -0
- package/dist/prerender/page-generators/about.d.ts.map +1 -0
- package/dist/prerender/page-generators/base.d.ts +26 -0
- package/dist/prerender/page-generators/base.d.ts.map +1 -0
- package/dist/prerender/page-generators/blog-detail.d.ts +15 -0
- package/dist/prerender/page-generators/blog-detail.d.ts.map +1 -0
- package/dist/prerender/page-generators/blogs-list.d.ts +17 -0
- package/dist/prerender/page-generators/blogs-list.d.ts.map +1 -0
- package/dist/prerender/page-generators/home.d.ts +19 -0
- package/dist/prerender/page-generators/home.d.ts.map +1 -0
- package/dist/prerender/page-generators/index.d.ts +9 -0
- package/dist/prerender/page-generators/index.d.ts.map +1 -0
- package/dist/prerender/page-generators/not-found.d.ts +14 -0
- package/dist/prerender/page-generators/not-found.d.ts.map +1 -0
- package/dist/prerender/page-generators/stories-list.d.ts +17 -0
- package/dist/prerender/page-generators/stories-list.d.ts.map +1 -0
- package/dist/prerender/page-generators/story-detail.d.ts +15 -0
- package/dist/prerender/page-generators/story-detail.d.ts.map +1 -0
- package/dist/prerender.js +109 -102
- package/dist/shared.js +1 -1
- package/dist/ui/about-me/index.d.ts +2 -10
- package/dist/ui/about-me/index.d.ts.map +1 -1
- package/dist/ui/admin/api.d.ts +16 -0
- package/dist/ui/admin/api.d.ts.map +1 -0
- package/dist/ui/admin/components/AboutMeSection.d.ts +7 -0
- package/dist/ui/admin/components/AboutMeSection.d.ts.map +1 -0
- package/dist/ui/admin/components/AdminSection.d.ts +13 -0
- package/dist/ui/admin/components/AdminSection.d.ts.map +1 -0
- package/dist/ui/admin/components/BlogsSection.d.ts +7 -0
- package/dist/ui/admin/components/BlogsSection.d.ts.map +1 -0
- package/dist/ui/admin/components/HomeSection.d.ts +7 -0
- package/dist/ui/admin/components/HomeSection.d.ts.map +1 -0
- package/dist/ui/admin/components/ImagesSection.d.ts +7 -0
- package/dist/ui/admin/components/ImagesSection.d.ts.map +1 -0
- package/dist/ui/admin/components/LoginForm.d.ts +9 -0
- package/dist/ui/admin/components/LoginForm.d.ts.map +1 -0
- package/dist/ui/admin/components/LogoSection.d.ts +7 -0
- package/dist/ui/admin/components/LogoSection.d.ts.map +1 -0
- package/dist/ui/admin/components/ProfileSection.d.ts +7 -0
- package/dist/ui/admin/components/ProfileSection.d.ts.map +1 -0
- package/dist/ui/admin/components/StaticSection.d.ts +13 -0
- package/dist/ui/admin/components/StaticSection.d.ts.map +1 -0
- package/dist/ui/admin/components/StoriesSection.d.ts +7 -0
- package/dist/ui/admin/components/StoriesSection.d.ts.map +1 -0
- package/dist/ui/admin/components/index.d.ts +11 -0
- package/dist/ui/admin/components/index.d.ts.map +1 -0
- package/dist/ui/admin/index.d.ts +10 -26
- package/dist/ui/admin/index.d.ts.map +1 -1
- package/dist/ui/admin/types.d.ts +24 -0
- package/dist/ui/admin/types.d.ts.map +1 -0
- package/dist/ui/blog-viewer/index.d.ts.map +1 -1
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/story-viewer/index.d.ts.map +1 -1
- package/dist/ui.js +14 -4
- package/package.json +1 -1
- package/src/api/handlers/about-me.ts +19 -9
- package/src/api/handlers/auth-handler.ts +18 -8
- package/src/api/handlers/content.ts +1 -1
- package/src/api/handlers/home.ts +2 -2
- package/src/api/website-api.ts +11 -7
- package/src/prerender/__tests__/page-content.test.ts +1 -11
- package/src/prerender/data-fetcher.ts +93 -0
- package/src/prerender/page-content.ts +109 -106
- package/src/prerender/page-generators/about.ts +38 -0
- package/src/prerender/page-generators/base.ts +77 -0
- package/src/prerender/page-generators/blog-detail.ts +35 -0
- package/src/prerender/page-generators/blogs-list.ts +43 -0
- package/src/prerender/page-generators/home.ts +54 -0
- package/src/prerender/page-generators/index.ts +8 -0
- package/src/prerender/page-generators/not-found.ts +36 -0
- package/src/prerender/page-generators/stories-list.ts +43 -0
- package/src/prerender/page-generators/story-detail.ts +35 -0
- package/src/shared/config/index.ts +1 -1
- package/src/shared/page-content.ts +1 -1
- package/src/shared/router.ts +5 -5
- package/src/ui/about-me/index.ts +14 -57
- package/src/ui/admin/api.ts +93 -0
- package/src/ui/admin/components/AboutMeSection.ts +47 -0
- package/src/ui/admin/components/AdminSection.ts +134 -0
- package/src/ui/admin/components/BlogsSection.ts +62 -0
- package/src/ui/admin/components/HomeSection.ts +47 -0
- package/src/ui/admin/components/ImagesSection.ts +54 -0
- package/src/ui/admin/components/LoginForm.ts +116 -0
- package/src/ui/admin/components/LogoSection.ts +51 -0
- package/src/ui/admin/components/ProfileSection.ts +47 -0
- package/src/ui/admin/components/StaticSection.ts +67 -0
- package/src/ui/admin/components/StoriesSection.ts +62 -0
- package/src/ui/admin/components/index.ts +10 -0
- package/src/ui/admin/index.ts +192 -434
- package/src/ui/admin/types.ts +26 -0
- package/src/ui/blog-viewer/index.ts +4 -1
- package/src/ui/index.ts +7 -0
- package/src/ui/story-viewer/index.ts +4 -1
- package/dist/ui/about-me/renderer.d.ts +0 -6
- package/dist/ui/about-me/renderer.d.ts.map +0 -1
- 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,
|
|
69
|
+
const [profileObj, rendered] = await Promise.all([
|
|
70
70
|
r2.getObject('profile.json'),
|
|
71
|
-
r2.
|
|
71
|
+
r2.getRendered('about-me.md')
|
|
72
72
|
]);
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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) {
|
package/src/api/handlers/home.ts
CHANGED
|
@@ -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) {
|
package/src/api/website-api.ts
CHANGED
|
@@ -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',
|
|
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.
|
|
119
|
-
const baseUrl = env?.baseUrl || "https://www.
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|