@leadertechie/personal-site-kit 0.0.0 → 0.1.0-alpha.2
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/__tests__/info.test.d.ts +2 -0
- package/dist/api/__tests__/info.test.d.ts.map +1 -0
- package/dist/api/__tests__/utils.test.d.ts +2 -0
- package/dist/api/__tests__/utils.test.d.ts.map +1 -0
- package/dist/api/handlers/{aboutme.d.ts → about-me.d.ts} +1 -1
- package/dist/api/handlers/about-me.d.ts.map +1 -0
- package/dist/api/handlers/{staticdetails.d.ts → static-details.d.ts} +1 -1
- package/dist/api/handlers/static-details.d.ts.map +1 -0
- package/dist/api/index.d.ts +5 -8
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/website-api.d.ts +10 -0
- package/dist/api/website-api.d.ts.map +1 -0
- package/dist/api.d.ts +2 -0
- package/dist/api.js +4 -589
- package/dist/chunks/index-BqixlS-2.js +1157 -0
- package/dist/chunks/template-gGTkeOcA.js +622 -0
- package/dist/chunks/website-api-CVsi-OLc.js +596 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -352
- package/dist/prerender/__tests__/page-content.test.d.ts +2 -0
- package/dist/prerender/__tests__/page-content.test.d.ts.map +1 -0
- package/dist/prerender/__tests__/template.test.d.ts +2 -0
- package/dist/prerender/__tests__/template.test.d.ts.map +1 -0
- package/dist/prerender/index.d.ts +5 -4
- package/dist/prerender/index.d.ts.map +1 -1
- package/dist/prerender/{pageContent.d.ts → page-content.d.ts} +1 -1
- package/dist/prerender/page-content.d.ts.map +1 -0
- package/dist/prerender/website-prerender.d.ts +22 -0
- package/dist/prerender/website-prerender.d.ts.map +1 -0
- package/dist/prerender.d.ts +2 -0
- package/dist/prerender.js +56 -51
- package/dist/shared/core/__tests__/theme-toggle.test.d.ts +2 -0
- package/dist/shared/core/__tests__/theme-toggle.test.d.ts.map +1 -0
- package/dist/shared/index.d.ts +5 -3
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/interfaces/{iFooterLink.d.ts → ifooter-link.d.ts} +1 -1
- package/dist/shared/interfaces/ifooter-link.d.ts.map +1 -0
- package/dist/shared/interfaces/{iRoute.d.ts → iroute.d.ts} +1 -1
- package/dist/shared/interfaces/iroute.d.ts.map +1 -0
- package/dist/shared/{pageContent.d.ts → page-content.d.ts} +4 -3
- package/dist/shared/page-content.d.ts.map +1 -0
- package/dist/shared/router.d.ts +22 -0
- package/dist/shared/router.d.ts.map +1 -0
- package/dist/shared/runtime.d.ts +6 -6
- package/dist/shared/runtime.d.ts.map +1 -1
- package/dist/shared/website-ui.d.ts +31 -0
- package/dist/shared/website-ui.d.ts.map +1 -0
- package/dist/shared.js +11 -8
- package/dist/ui/about-me/api.d.ts.map +1 -0
- package/dist/ui/about-me/index.d.ts.map +1 -0
- package/dist/ui/about-me/renderer.d.ts.map +1 -0
- package/dist/ui/about-me/styles.d.ts.map +1 -0
- package/dist/ui/footer/index.d.ts +1 -1
- package/dist/ui/footer/index.d.ts.map +1 -1
- package/dist/ui/index.d.ts +5 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +5 -818
- package/package.json +8 -1
- package/src/api/index.ts +6 -124
- package/src/api/website-api.ts +124 -0
- package/src/index.ts +4 -0
- package/src/prerender/__tests__/{pageContent.test.ts → page-content.test.ts} +1 -1
- package/src/prerender/index.ts +6 -137
- package/src/prerender/website-prerender.ts +152 -0
- package/src/shared/index.ts +5 -3
- package/src/shared/{pageContent.ts → page-content.ts} +5 -4
- package/src/shared/router.ts +241 -0
- package/src/shared/runtime.ts +6 -6
- package/src/shared/website-ui.ts +92 -0
- package/src/ui/footer/index.ts +1 -1
- package/src/ui/index.ts +4 -0
- package/dist/api/handlers/aboutme.d.ts.map +0 -1
- package/dist/api/handlers/staticdetails.d.ts.map +0 -1
- package/dist/prerender/pageContent.d.ts.map +0 -1
- package/dist/shared/interfaces/iFooterLink.d.ts.map +0 -1
- package/dist/shared/interfaces/iRoute.d.ts.map +0 -1
- package/dist/shared/pageContent.d.ts.map +0 -1
- package/dist/ui/aboutme/api.d.ts.map +0 -1
- package/dist/ui/aboutme/index.d.ts.map +0 -1
- package/dist/ui/aboutme/renderer.d.ts.map +0 -1
- package/dist/ui/aboutme/styles.d.ts.map +0 -1
- /package/dist/ui/{aboutme → about-me}/api.d.ts +0 -0
- /package/dist/ui/{aboutme → about-me}/index.d.ts +0 -0
- /package/dist/ui/{aboutme → about-me}/renderer.d.ts +0 -0
- /package/dist/ui/{aboutme → about-me}/styles.d.ts +0 -0
- /package/src/api/handlers/{aboutme.ts → about-me.ts} +0 -0
- /package/src/api/handlers/{staticdetails.ts → static-details.ts} +0 -0
- /package/src/prerender/{pageContent.ts → page-content.ts} +0 -0
- /package/src/shared/interfaces/{iFooterLink.ts → ifooter-link.ts} +0 -0
- /package/src/shared/interfaces/{iRoute.ts → iroute.ts} +0 -0
- /package/src/ui/{aboutme → about-me}/api.ts +0 -0
- /package/src/ui/{aboutme → about-me}/index.ts +0 -0
- /package/src/ui/{aboutme → about-me}/renderer.ts +0 -0
- /package/src/ui/{aboutme → about-me}/styles.ts +0 -0
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leadertechie/personal-site-kit",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.1.0-alpha.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A high-performance personal website engine for Cloudflare Workers and R2",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/leadertechie/personal-site-kit"
|
|
9
|
+
},
|
|
6
10
|
"author": "Techie Leader",
|
|
7
11
|
"license": "MIT",
|
|
8
12
|
"files": [
|
|
@@ -37,5 +41,8 @@
|
|
|
37
41
|
},
|
|
38
42
|
"publishConfig": {
|
|
39
43
|
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"overrides": {
|
|
46
|
+
"lodash": "^4.18.1"
|
|
40
47
|
}
|
|
41
48
|
}
|
package/src/api/index.ts
CHANGED
|
@@ -1,125 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { WebsiteAPI } from './website-api';
|
|
2
|
+
export { WebsiteAPI };
|
|
3
|
+
export type { APIHandler } from './website-api';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
// Extract route from pathname, removing leading/trailing slashes and /api prefix
|
|
11
|
-
const pathname = url.pathname;
|
|
12
|
-
const route = pathname
|
|
13
|
-
.replace(/^\/api\//, '') // Remove /api/ prefix if present
|
|
14
|
-
.replace(/^\//, '') // Remove leading slash
|
|
15
|
-
.replace(/\/+$/, ''); // Remove trailing slashes
|
|
16
|
-
|
|
17
|
-
return handleAPIRoute(route, request, env);
|
|
18
|
-
|
|
19
|
-
},
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
function handleCORS(): Response {
|
|
23
|
-
return new Response(null, {
|
|
24
|
-
status: 200,
|
|
25
|
-
headers: {
|
|
26
|
-
'Access-Control-Allow-Origin': '*',
|
|
27
|
-
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
|
28
|
-
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
29
|
-
'Access-Control-Max-Age': '86400',
|
|
30
|
-
},
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function addCORSHeaders(response: Response): Response {
|
|
35
|
-
response.headers.set('Access-Control-Allow-Origin', '*');
|
|
36
|
-
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
37
|
-
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
38
|
-
return response;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
import { createErrorResponse } from './utils';
|
|
42
|
-
|
|
43
|
-
import { handleAboutMe, clearContentCache } from './handlers/aboutme';
|
|
44
|
-
import { handleHome } from './handlers/home';
|
|
45
|
-
import { handleInfo } from './handlers/info';
|
|
46
|
-
import { handleContent } from './handlers/content';
|
|
47
|
-
import { handleBlogs, handleStories, handleSearch } from './handlers/content-api';
|
|
48
|
-
import { handleLogo } from './handlers/logo';
|
|
49
|
-
import { handleStaticDetails } from './handlers/staticdetails';
|
|
50
|
-
|
|
51
|
-
function requireAuth(request: Request, env?: Env): Response | null {
|
|
52
|
-
const authHeader = request.headers.get('Authorization');
|
|
53
|
-
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
54
|
-
return createErrorResponse('Unauthorized', 401);
|
|
55
|
-
}
|
|
56
|
-
const token = authHeader.slice(7);
|
|
57
|
-
if (token !== env?.ADMIN_API_KEY) {
|
|
58
|
-
return createErrorResponse('Unauthorized', 401);
|
|
59
|
-
}
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function handleAPIRoute(route: string, request: Request, env?: Env): Promise<Response> {
|
|
64
|
-
try {
|
|
65
|
-
// Check for content route first (content/*)
|
|
66
|
-
if (route === 'content' || route.startsWith('content/')) {
|
|
67
|
-
const subpath = route.replace(/^content\/?/, '');
|
|
68
|
-
return addCORSHeaders(await handleContent(request, env, subpath));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
switch (route) {
|
|
72
|
-
case 'info':
|
|
73
|
-
return addCORSHeaders(await handleInfo());
|
|
74
|
-
case 'home':
|
|
75
|
-
return addCORSHeaders(await handleHome(env));
|
|
76
|
-
case 'cache-clear':
|
|
77
|
-
const authError = requireAuth(request, env);
|
|
78
|
-
if (authError) return addCORSHeaders(authError);
|
|
79
|
-
clearContentCache();
|
|
80
|
-
return addCORSHeaders(new Response(JSON.stringify({ success: true, message: 'Cache cleared' }), { status: 200 }));
|
|
81
|
-
case 'aboutme':
|
|
82
|
-
return addCORSHeaders(await handleAboutMe(env));
|
|
83
|
-
case 'logo':
|
|
84
|
-
return addCORSHeaders(await handleLogo(env));
|
|
85
|
-
case 'static':
|
|
86
|
-
return addCORSHeaders(await handleStaticDetails(env));
|
|
87
|
-
case 'blogs':
|
|
88
|
-
return addCORSHeaders(await handleBlogs(env));
|
|
89
|
-
case 'blogs/latest':
|
|
90
|
-
const url = new URL(request.url);
|
|
91
|
-
const latestCount = url.searchParams.get('count');
|
|
92
|
-
return addCORSHeaders(await handleBlogs(env, undefined, latestCount ? parseInt(latestCount) : 5));
|
|
93
|
-
default:
|
|
94
|
-
if (route.startsWith('blogs/')) {
|
|
95
|
-
const slug = route.replace('blogs/', '');
|
|
96
|
-
return addCORSHeaders(await handleBlogs(env, slug));
|
|
97
|
-
}
|
|
98
|
-
if (route.startsWith('stories')) {
|
|
99
|
-
if (route === 'stories') {
|
|
100
|
-
return addCORSHeaders(await handleStories(env));
|
|
101
|
-
}
|
|
102
|
-
if (route === 'stories/latest') {
|
|
103
|
-
const url = new URL(request.url);
|
|
104
|
-
const latestCount = url.searchParams.get('count');
|
|
105
|
-
return addCORSHeaders(await handleStories(env, undefined, latestCount ? parseInt(latestCount) : 5));
|
|
106
|
-
}
|
|
107
|
-
const slug = route.replace('stories/', '');
|
|
108
|
-
return addCORSHeaders(await handleStories(env, slug));
|
|
109
|
-
}
|
|
110
|
-
if (route === 'search') {
|
|
111
|
-
const url = new URL(request.url);
|
|
112
|
-
const query = url.searchParams.get('q');
|
|
113
|
-
return addCORSHeaders(await handleSearch(env, query || undefined));
|
|
114
|
-
}
|
|
115
|
-
return addCORSHeaders(createErrorResponse('Route not found', 404));
|
|
116
|
-
}
|
|
117
|
-
} catch (error) {
|
|
118
|
-
return addCORSHeaders(createErrorResponse('Internal server error', 500));
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
interface Env {
|
|
123
|
-
CONTENT_BUCKET?: any; // R2Bucket type not strictly enforced here to avoid large diff
|
|
124
|
-
ADMIN_API_KEY?: string;
|
|
125
|
-
}
|
|
5
|
+
// Default worker export using WebsiteAPI
|
|
6
|
+
const defaultAPI = new WebsiteAPI();
|
|
7
|
+
export default defaultAPI;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { createErrorResponse } from './utils';
|
|
2
|
+
import { handleAboutMe, clearContentCache } from './handlers/about-me';
|
|
3
|
+
import { handleHome } from './handlers/home';
|
|
4
|
+
import { handleInfo } from './handlers/info';
|
|
5
|
+
import { handleContent } from './handlers/content';
|
|
6
|
+
import { handleBlogs, handleStories, handleSearch } from './handlers/content-api';
|
|
7
|
+
import { handleLogo } from './handlers/logo';
|
|
8
|
+
import { handleStaticDetails } from './handlers/static-details';
|
|
9
|
+
|
|
10
|
+
export type APIHandler = (request: Request, env: any) => Promise<Response>;
|
|
11
|
+
|
|
12
|
+
export class WebsiteAPI {
|
|
13
|
+
private customHandlers = new Map<string, APIHandler>();
|
|
14
|
+
|
|
15
|
+
public registerHandler(route: string, handler: APIHandler) {
|
|
16
|
+
this.customHandlers.set(route, handler);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private addCORSHeaders(response: Response): Response {
|
|
20
|
+
response.headers.set('Access-Control-Allow-Origin', '*' );
|
|
21
|
+
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS' );
|
|
22
|
+
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
23
|
+
return response;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private handleCORS(): Response {
|
|
27
|
+
return new Response(null, {
|
|
28
|
+
status: 200,
|
|
29
|
+
headers: {
|
|
30
|
+
'Access-Control-Allow-Origin': '*' ,
|
|
31
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS' ,
|
|
32
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
33
|
+
'Access-Control-Max-Age': '86400',
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private requireAuth(request: Request, env?: any): Response | null {
|
|
39
|
+
const authHeader = request.headers.get('Authorization');
|
|
40
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
41
|
+
return createErrorResponse('Unauthorized', 401);
|
|
42
|
+
}
|
|
43
|
+
const token = authHeader.slice(7);
|
|
44
|
+
if (token !== env?.ADMIN_API_KEY) {
|
|
45
|
+
return createErrorResponse('Unauthorized', 401);
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public async fetch(request: Request, env: any): Promise<Response> {
|
|
51
|
+
const url = new URL(request.url);
|
|
52
|
+
|
|
53
|
+
if (request.method === 'OPTIONS') {
|
|
54
|
+
return this.handleCORS();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const pathname = url.pathname;
|
|
58
|
+
const route = pathname
|
|
59
|
+
.replace(/^\/api\//, '')
|
|
60
|
+
.replace(/^\//, '')
|
|
61
|
+
.replace(/\/+$/, '');
|
|
62
|
+
|
|
63
|
+
// Check custom handlers first
|
|
64
|
+
if (this.customHandlers.has(route)) {
|
|
65
|
+
const handler = this.customHandlers.get(route)!;
|
|
66
|
+
return this.addCORSHeaders(await handler(request, env));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
// Check for content route first (content/*)
|
|
71
|
+
if (route === 'content' || route.startsWith('content/')) {
|
|
72
|
+
const subpath = route.replace(/^content\/?/, '');
|
|
73
|
+
return this.addCORSHeaders(await handleContent(request, env, subpath));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
switch (route) {
|
|
77
|
+
case 'info':
|
|
78
|
+
return this.addCORSHeaders(await handleInfo());
|
|
79
|
+
case 'home':
|
|
80
|
+
return this.addCORSHeaders(await handleHome(env));
|
|
81
|
+
case 'cache-clear':
|
|
82
|
+
const authError = this.requireAuth(request, env);
|
|
83
|
+
if (authError) return this.addCORSHeaders(authError);
|
|
84
|
+
clearContentCache();
|
|
85
|
+
return this.addCORSHeaders(new Response(JSON.stringify({ success: true, message: 'Cache cleared' }), { status: 200 }));
|
|
86
|
+
case 'aboutme':
|
|
87
|
+
return this.addCORSHeaders(await handleAboutMe(env));
|
|
88
|
+
case 'logo':
|
|
89
|
+
return this.addCORSHeaders(await handleLogo(env));
|
|
90
|
+
case 'static':
|
|
91
|
+
return this.addCORSHeaders(await handleStaticDetails(env));
|
|
92
|
+
case 'blogs':
|
|
93
|
+
return this.addCORSHeaders(await handleBlogs(env));
|
|
94
|
+
case 'blogs/latest':
|
|
95
|
+
const latestCount = url.searchParams.get('count');
|
|
96
|
+
return this.addCORSHeaders(await handleBlogs(env, undefined, latestCount ? parseInt(latestCount) : 5));
|
|
97
|
+
default:
|
|
98
|
+
if (route.startsWith('blogs/')) {
|
|
99
|
+
const slug = route.replace('blogs/', '');
|
|
100
|
+
return this.addCORSHeaders(await handleBlogs(env, slug));
|
|
101
|
+
}
|
|
102
|
+
if (route.startsWith('stories')) {
|
|
103
|
+
if (route === 'stories') {
|
|
104
|
+
return this.addCORSHeaders(await handleStories(env));
|
|
105
|
+
}
|
|
106
|
+
if (route === 'stories/latest') {
|
|
107
|
+
const latestCount = url.searchParams.get('count');
|
|
108
|
+
return this.addCORSHeaders(await handleStories(env, undefined, latestCount ? parseInt(latestCount) : 5));
|
|
109
|
+
}
|
|
110
|
+
const slug = route.replace('stories/', '');
|
|
111
|
+
return this.addCORSHeaders(await handleStories(env, slug));
|
|
112
|
+
}
|
|
113
|
+
if (route === 'search') {
|
|
114
|
+
const query = url.searchParams.get('q');
|
|
115
|
+
return this.addCORSHeaders(await handleSearch(env, query || undefined));
|
|
116
|
+
}
|
|
117
|
+
return this.addCORSHeaders(createErrorResponse('Route not found', 404));
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error('API Error:', error);
|
|
121
|
+
return this.addCORSHeaders(createErrorResponse('Internal server error', 500));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
package/src/index.ts
ADDED
package/src/prerender/index.ts
CHANGED
|
@@ -1,138 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { WebsitePrerender } from './website-prerender';
|
|
2
|
+
export { WebsitePrerender };
|
|
3
|
+
export type { PrerenderOptions } from './website-prerender';
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
{ link: "/", text: "Home" },
|
|
7
|
-
{ link: "/blogs", text: "Blogs" },
|
|
8
|
-
{ link: "/stories", text: "Stories" },
|
|
9
|
-
{ link: "/about-me", text: "About Me" },
|
|
10
|
-
];
|
|
11
|
-
|
|
12
|
-
// Default footer links (fallback)
|
|
13
|
-
const defaultFooterLinks: IFooterLink[] = [
|
|
14
|
-
{ text: "LinkedIn", link: "https://linkedin.com/in/yourname" },
|
|
15
|
-
{ text: "GitHub", link: "https://github.com/yourname" },
|
|
16
|
-
{ text: "Email", link: "mailto:yourname@domain.com" },
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
let footerLinks: IFooterLink[] = defaultFooterLinks;
|
|
20
|
-
let siteTitle = "My Personal Website";
|
|
21
|
-
let copyright = "2026 My Personal Website";
|
|
22
|
-
|
|
23
|
-
async function fetchStaticDetails(apiUrl: string) {
|
|
24
|
-
try {
|
|
25
|
-
const res = await fetch(`${apiUrl}/api/static`);
|
|
26
|
-
if (res.ok) {
|
|
27
|
-
const data = await res.json();
|
|
28
|
-
siteTitle = data.siteTitle || siteTitle;
|
|
29
|
-
copyright = data.copyright || copyright;
|
|
30
|
-
|
|
31
|
-
const normalizeUrl = (url?: string) => {
|
|
32
|
-
if (!url) return "";
|
|
33
|
-
if (url.startsWith("http://") || url.startsWith("https://")) return url;
|
|
34
|
-
if (url.startsWith("www.")) return `https://${url}`;
|
|
35
|
-
return url;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
footerLinks = [
|
|
39
|
-
{ text: "LinkedIn", link: normalizeUrl(data.linkedin) || defaultFooterLinks[0].link },
|
|
40
|
-
{ text: "GitHub", link: normalizeUrl(data.github) || defaultFooterLinks[1].link },
|
|
41
|
-
{ text: "Email", link: data.email ? `mailto:${data.email}` : defaultFooterLinks[2].link },
|
|
42
|
-
];
|
|
43
|
-
}
|
|
44
|
-
} catch (e) {}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function fetchAboutMeData(apiUrl: string): Promise<any> {
|
|
48
|
-
try {
|
|
49
|
-
const res = await fetch(`${apiUrl}/api/aboutme`);
|
|
50
|
-
if (res.ok) return await res.json();
|
|
51
|
-
} catch (e) {}
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export default {
|
|
56
|
-
async fetch(request: Request, env: any, ctx: any) {
|
|
57
|
-
const apiUrl = env?.API_URL || "https://api.example.com";
|
|
58
|
-
const baseSiteUrl = env?.BASE_SITE_URL || "https://site.example.com";
|
|
59
|
-
|
|
60
|
-
await fetchStaticDetails(apiUrl);
|
|
61
|
-
|
|
62
|
-
const url = new URL(request.url);
|
|
63
|
-
|
|
64
|
-
if (url.pathname.startsWith("/api/")) {
|
|
65
|
-
return fetch(`${apiUrl}${url.pathname}${url.search}`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (url.pathname.startsWith("/images/")) {
|
|
69
|
-
const imageKey = url.pathname.slice(1);
|
|
70
|
-
try {
|
|
71
|
-
const image = await env.CONTENT_BUCKET.get(imageKey);
|
|
72
|
-
if (image) {
|
|
73
|
-
return new Response(image.body, {
|
|
74
|
-
headers: {
|
|
75
|
-
"content-type": image.httpMetadata?.contentType || "image/jpeg",
|
|
76
|
-
"cache-control": "public, max-age=86400",
|
|
77
|
-
},
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
} catch (e) {}
|
|
81
|
-
return new Response("Not found", { status: 404 });
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (url.pathname.startsWith("/assets/") || url.pathname === "/logo.png" || url.pathname === "/favicon.ico") {
|
|
85
|
-
const path = url.pathname;
|
|
86
|
-
const ext = path.split(".").pop()?.toLowerCase();
|
|
87
|
-
const contentTypes: Record<string, string> = {
|
|
88
|
-
js: "application/javascript",
|
|
89
|
-
css: "text/css",
|
|
90
|
-
png: "image/png",
|
|
91
|
-
jpg: "image/jpeg",
|
|
92
|
-
jpeg: "image/jpeg",
|
|
93
|
-
gif: "image/gif",
|
|
94
|
-
svg: "image/svg+xml",
|
|
95
|
-
webp: "image/webp",
|
|
96
|
-
ico: "image/x-icon",
|
|
97
|
-
};
|
|
98
|
-
const contentType = contentTypes[ext || ""] || "application/octet-stream";
|
|
99
|
-
|
|
100
|
-
const response = await fetch(`${baseSiteUrl}${path}`);
|
|
101
|
-
if (response.ok) {
|
|
102
|
-
return new Response(response.body, {
|
|
103
|
-
headers: {
|
|
104
|
-
"content-type": contentType,
|
|
105
|
-
"cache-control": "public, max-age=31536000",
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
return new Response("Not found", { status: 404 });
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const PRERENDERED_DOMAINS = [
|
|
113
|
-
url.hostname
|
|
114
|
-
];
|
|
115
|
-
|
|
116
|
-
if (!PRERENDERED_DOMAINS.includes(url.hostname) && !url.hostname.includes("localhost")) {
|
|
117
|
-
return fetch(request);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
let hydrationScript = "";
|
|
121
|
-
if (url.pathname === "/about-me" || url.pathname === "/about-me/") {
|
|
122
|
-
const aboutMeData = await fetchAboutMeData(apiUrl);
|
|
123
|
-
if (aboutMeData) {
|
|
124
|
-
hydrationScript = `<script>window.__HYDRATION_DATA__ = ${JSON.stringify(aboutMeData)};</script>`;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const pageContent = await generatePageContent(url.pathname, routes, footerLinks, { ...env, apiUrl });
|
|
129
|
-
const html = await createHtmlTemplate({ ...pageContent, hydrationData: hydrationScript });
|
|
130
|
-
|
|
131
|
-
return new Response(html, {
|
|
132
|
-
headers: {
|
|
133
|
-
"content-type": "text/html",
|
|
134
|
-
"cache-control": "public, max-age=60",
|
|
135
|
-
},
|
|
136
|
-
});
|
|
137
|
-
},
|
|
138
|
-
};
|
|
5
|
+
// Default worker export using WebsitePrerender
|
|
6
|
+
const defaultPrerender = new WebsitePrerender();
|
|
7
|
+
export default defaultPrerender;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { createHtmlTemplate, TemplateProps } from './template';
|
|
2
|
+
import { IFooterLink, IRoute, generatePageContent } from './page-content';
|
|
3
|
+
|
|
4
|
+
export interface PrerenderOptions {
|
|
5
|
+
routes?: IRoute[];
|
|
6
|
+
defaultFooterLinks?: IFooterLink[];
|
|
7
|
+
siteTitle?: string;
|
|
8
|
+
copyright?: string;
|
|
9
|
+
templateRenderer?: (props: TemplateProps) => Promise<string> | string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class WebsitePrerender {
|
|
13
|
+
private routes: IRoute[];
|
|
14
|
+
private defaultFooterLinks: IFooterLink[];
|
|
15
|
+
private footerLinks: IFooterLink[];
|
|
16
|
+
private siteTitle: string;
|
|
17
|
+
private copyright: string;
|
|
18
|
+
private templateRenderer: (props: TemplateProps) => Promise<string> | string;
|
|
19
|
+
|
|
20
|
+
constructor(options: PrerenderOptions = {}) {
|
|
21
|
+
this.routes = options.routes || [
|
|
22
|
+
{ link: '/', text: 'Home' },
|
|
23
|
+
{ link: '/blogs', text: 'Blogs' },
|
|
24
|
+
{ link: '/stories', text: 'Stories' },
|
|
25
|
+
{ link: '/about-me', text: 'About Me' },
|
|
26
|
+
];
|
|
27
|
+
this.defaultFooterLinks = options.defaultFooterLinks || [
|
|
28
|
+
{ text: 'LinkedIn', link: 'https://linkedin.com/in/yourname' },
|
|
29
|
+
{ text: 'GitHub', link: 'https://github.com/yourname' },
|
|
30
|
+
{ text: 'Email', link: 'mailto:yourname@domain.com' },
|
|
31
|
+
];
|
|
32
|
+
this.footerLinks = [...this.defaultFooterLinks];
|
|
33
|
+
this.siteTitle = options.siteTitle || 'My Personal Website';
|
|
34
|
+
this.copyright = options.copyright || '2026 My Personal Website';
|
|
35
|
+
this.templateRenderer = options.templateRenderer || createHtmlTemplate;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private async fetchStaticDetails(apiUrl: string) {
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch(`${apiUrl}/api/static`);
|
|
41
|
+
if (res.ok) {
|
|
42
|
+
const data = await res.json();
|
|
43
|
+
this.siteTitle = data.siteTitle || this.siteTitle;
|
|
44
|
+
this.copyright = data.copyright || this.copyright;
|
|
45
|
+
|
|
46
|
+
const normalizeUrl = (url?: string) => {
|
|
47
|
+
if (!url) return '';
|
|
48
|
+
if (url.startsWith('http://') || url.startsWith('https://')) return url;
|
|
49
|
+
if (url.startsWith('www.')) return `https://${url}`;
|
|
50
|
+
return url;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
this.footerLinks = [
|
|
54
|
+
{ text: 'LinkedIn', link: normalizeUrl(data.linkedin) || this.defaultFooterLinks[0].link },
|
|
55
|
+
{ text: 'GitHub', link: normalizeUrl(data.github) || this.defaultFooterLinks[1].link },
|
|
56
|
+
{ text: 'Email', link: data.email ? `mailto:${data.email}` : this.defaultFooterLinks[2].link },
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
} catch (e) {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private async fetchAboutMeData(apiUrl: string): Promise<any> {
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch(`${apiUrl}/api/about-me`);
|
|
65
|
+
if (res.ok) return await res.json();
|
|
66
|
+
} catch (e) {}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public async fetch(request: Request, env: any, ctx: any): Promise<Response> {
|
|
71
|
+
const apiUrl = env?.API_URL || 'https://api.example.com';
|
|
72
|
+
const baseSiteUrl = env?.BASE_SITE_URL || 'https://site.example.com';
|
|
73
|
+
|
|
74
|
+
await this.fetchStaticDetails(apiUrl);
|
|
75
|
+
|
|
76
|
+
const url = new URL(request.url);
|
|
77
|
+
|
|
78
|
+
if (url.pathname.startsWith('/api/')) {
|
|
79
|
+
return fetch(`${apiUrl}${url.pathname}${url.search}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (url.pathname.startsWith('/images/')) {
|
|
83
|
+
const imageKey = url.pathname.slice(1);
|
|
84
|
+
try {
|
|
85
|
+
const image = await env.CONTENT_BUCKET.get(imageKey);
|
|
86
|
+
if (image) {
|
|
87
|
+
return new Response(image.body, {
|
|
88
|
+
headers: {
|
|
89
|
+
'content-type': image.httpMetadata?.contentType || 'image/jpeg',
|
|
90
|
+
'cache-control': 'public, max-age=86400',
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
} catch (e) {}
|
|
95
|
+
return new Response('Not found', { status: 404 });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (url.pathname.startsWith('/assets/') || url.pathname === '/logo.png' || url.pathname === '/favicon.ico') {
|
|
99
|
+
const path = url.pathname;
|
|
100
|
+
const ext = path.split('.').pop()?.toLowerCase();
|
|
101
|
+
const contentTypes: Record<string, string> = {
|
|
102
|
+
js: 'application/javascript',
|
|
103
|
+
css: 'text/css',
|
|
104
|
+
png: 'image/png',
|
|
105
|
+
jpg: 'image/jpeg',
|
|
106
|
+
jpeg: 'image/jpeg',
|
|
107
|
+
gif: 'image/gif',
|
|
108
|
+
svg: 'image/svg+xml',
|
|
109
|
+
webp: 'image/webp',
|
|
110
|
+
ico: 'image/x-icon',
|
|
111
|
+
};
|
|
112
|
+
const contentType = contentTypes[ext || ''] || 'application/octet-stream';
|
|
113
|
+
|
|
114
|
+
const response = await fetch(`${baseSiteUrl}${path}`);
|
|
115
|
+
if (response.ok) {
|
|
116
|
+
return new Response(response.body, {
|
|
117
|
+
headers: {
|
|
118
|
+
'content-type': contentType,
|
|
119
|
+
'cache-control': 'public, max-age=31536000',
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return new Response('Not found', { status: 404 });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const PRERENDERED_DOMAINS = [
|
|
127
|
+
url.hostname
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
if (!PRERENDERED_DOMAINS.includes(url.hostname) && !url.hostname.includes('localhost')) {
|
|
131
|
+
return fetch(request);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let hydrationScript = '';
|
|
135
|
+
if (url.pathname === '/about-me' || url.pathname === '/about-me/') {
|
|
136
|
+
const aboutMeData = await this.fetchAboutMeData(apiUrl);
|
|
137
|
+
if (aboutMeData) {
|
|
138
|
+
hydrationScript = `<script>window.__HYDRATION_DATA__ = ${JSON.stringify(aboutMeData)};</script>`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const pageContent = await generatePageContent(url.pathname, this.routes, this.footerLinks, { ...env, apiUrl, siteTitle: this.siteTitle, copyright: this.copyright });
|
|
143
|
+
const html = await this.templateRenderer({ ...pageContent, hydrationData: hydrationScript });
|
|
144
|
+
|
|
145
|
+
return new Response(html, {
|
|
146
|
+
headers: {
|
|
147
|
+
'content-type': 'text/html',
|
|
148
|
+
'cache-control': 'public, max-age=60',
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
package/src/shared/index.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
// Shared Config and Store
|
|
2
2
|
export * from './config';
|
|
3
3
|
export * from './core/site-store';
|
|
4
|
+
export * from './website-ui';
|
|
5
|
+
export * from './router';
|
|
4
6
|
|
|
5
7
|
// Interfaces
|
|
6
|
-
export * from './interfaces/
|
|
7
|
-
export * from './interfaces/
|
|
8
|
+
export * from './interfaces/iroute';
|
|
9
|
+
export * from './interfaces/ifooter-link';
|
|
8
10
|
|
|
9
11
|
// Core Logic
|
|
10
12
|
export * from './core/theme-toggle';
|
|
11
13
|
export * from './template';
|
|
12
|
-
export * from './
|
|
14
|
+
export * from './page-content';
|
|
13
15
|
|
|
14
16
|
// Runtime
|
|
15
17
|
export * from './runtime';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { IFooterLink } from './interfaces/
|
|
2
|
-
import { IRoute } from './interfaces/
|
|
1
|
+
import { IFooterLink } from './interfaces/ifooter-link';
|
|
2
|
+
import { IRoute } from './interfaces/iroute';
|
|
3
|
+
export type { IFooterLink, IRoute };
|
|
3
4
|
import { MarkdownPipeline } from '@leadertechie/md2html';
|
|
4
5
|
|
|
5
6
|
export interface PageContent {
|
|
@@ -78,7 +79,7 @@ export const generatePageContent = (
|
|
|
78
79
|
const footerTemplate = `
|
|
79
80
|
<my-footer
|
|
80
81
|
copyright="${copyright}"
|
|
81
|
-
footerlinks
|
|
82
|
+
footerlinks='\${JSON.stringify(footerLinks)}'>
|
|
82
83
|
</my-footer>`;
|
|
83
84
|
|
|
84
85
|
const renderContentGists = (items: ContentMetadata[] = [], title: string, type: 'blogs' | 'stories') => {
|
|
@@ -194,7 +195,7 @@ export const generatePageContent = (
|
|
|
194
195
|
${bannerTemplate}
|
|
195
196
|
<main class="container container-narrow text-center page-content">
|
|
196
197
|
<h1 class="page-title">Page Not Found</h1>
|
|
197
|
-
<p>The page you
|
|
198
|
+
<p>The page you're looking for doesn't exist.</p>
|
|
198
199
|
<p><a href="/">Return to home</a></p>
|
|
199
200
|
</main>
|
|
200
201
|
${footerTemplate}`;
|