@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.
Files changed (96) hide show
  1. package/dist/api/__tests__/info.test.d.ts +2 -0
  2. package/dist/api/__tests__/info.test.d.ts.map +1 -0
  3. package/dist/api/__tests__/utils.test.d.ts +2 -0
  4. package/dist/api/__tests__/utils.test.d.ts.map +1 -0
  5. package/dist/api/handlers/{aboutme.d.ts → about-me.d.ts} +1 -1
  6. package/dist/api/handlers/about-me.d.ts.map +1 -0
  7. package/dist/api/handlers/{staticdetails.d.ts → static-details.d.ts} +1 -1
  8. package/dist/api/handlers/static-details.d.ts.map +1 -0
  9. package/dist/api/index.d.ts +5 -8
  10. package/dist/api/index.d.ts.map +1 -1
  11. package/dist/api/website-api.d.ts +10 -0
  12. package/dist/api/website-api.d.ts.map +1 -0
  13. package/dist/api.d.ts +2 -0
  14. package/dist/api.js +4 -589
  15. package/dist/chunks/index-BqixlS-2.js +1157 -0
  16. package/dist/chunks/template-gGTkeOcA.js +622 -0
  17. package/dist/chunks/website-api-CVsi-OLc.js +596 -0
  18. package/dist/index.d.ts +5 -2
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +20 -352
  21. package/dist/prerender/__tests__/page-content.test.d.ts +2 -0
  22. package/dist/prerender/__tests__/page-content.test.d.ts.map +1 -0
  23. package/dist/prerender/__tests__/template.test.d.ts +2 -0
  24. package/dist/prerender/__tests__/template.test.d.ts.map +1 -0
  25. package/dist/prerender/index.d.ts +5 -4
  26. package/dist/prerender/index.d.ts.map +1 -1
  27. package/dist/prerender/{pageContent.d.ts → page-content.d.ts} +1 -1
  28. package/dist/prerender/page-content.d.ts.map +1 -0
  29. package/dist/prerender/website-prerender.d.ts +22 -0
  30. package/dist/prerender/website-prerender.d.ts.map +1 -0
  31. package/dist/prerender.d.ts +2 -0
  32. package/dist/prerender.js +56 -51
  33. package/dist/shared/core/__tests__/theme-toggle.test.d.ts +2 -0
  34. package/dist/shared/core/__tests__/theme-toggle.test.d.ts.map +1 -0
  35. package/dist/shared/index.d.ts +5 -3
  36. package/dist/shared/index.d.ts.map +1 -1
  37. package/dist/shared/interfaces/{iFooterLink.d.ts → ifooter-link.d.ts} +1 -1
  38. package/dist/shared/interfaces/ifooter-link.d.ts.map +1 -0
  39. package/dist/shared/interfaces/{iRoute.d.ts → iroute.d.ts} +1 -1
  40. package/dist/shared/interfaces/iroute.d.ts.map +1 -0
  41. package/dist/shared/{pageContent.d.ts → page-content.d.ts} +4 -3
  42. package/dist/shared/page-content.d.ts.map +1 -0
  43. package/dist/shared/router.d.ts +22 -0
  44. package/dist/shared/router.d.ts.map +1 -0
  45. package/dist/shared/runtime.d.ts +6 -6
  46. package/dist/shared/runtime.d.ts.map +1 -1
  47. package/dist/shared/website-ui.d.ts +31 -0
  48. package/dist/shared/website-ui.d.ts.map +1 -0
  49. package/dist/shared.js +11 -8
  50. package/dist/ui/about-me/api.d.ts.map +1 -0
  51. package/dist/ui/about-me/index.d.ts.map +1 -0
  52. package/dist/ui/about-me/renderer.d.ts.map +1 -0
  53. package/dist/ui/about-me/styles.d.ts.map +1 -0
  54. package/dist/ui/footer/index.d.ts +1 -1
  55. package/dist/ui/footer/index.d.ts.map +1 -1
  56. package/dist/ui/index.d.ts +5 -0
  57. package/dist/ui/index.d.ts.map +1 -0
  58. package/dist/ui.d.ts +1 -1
  59. package/dist/ui.js +5 -818
  60. package/package.json +8 -1
  61. package/src/api/index.ts +6 -124
  62. package/src/api/website-api.ts +124 -0
  63. package/src/index.ts +4 -0
  64. package/src/prerender/__tests__/{pageContent.test.ts → page-content.test.ts} +1 -1
  65. package/src/prerender/index.ts +6 -137
  66. package/src/prerender/website-prerender.ts +152 -0
  67. package/src/shared/index.ts +5 -3
  68. package/src/shared/{pageContent.ts → page-content.ts} +5 -4
  69. package/src/shared/router.ts +241 -0
  70. package/src/shared/runtime.ts +6 -6
  71. package/src/shared/website-ui.ts +92 -0
  72. package/src/ui/footer/index.ts +1 -1
  73. package/src/ui/index.ts +4 -0
  74. package/dist/api/handlers/aboutme.d.ts.map +0 -1
  75. package/dist/api/handlers/staticdetails.d.ts.map +0 -1
  76. package/dist/prerender/pageContent.d.ts.map +0 -1
  77. package/dist/shared/interfaces/iFooterLink.d.ts.map +0 -1
  78. package/dist/shared/interfaces/iRoute.d.ts.map +0 -1
  79. package/dist/shared/pageContent.d.ts.map +0 -1
  80. package/dist/ui/aboutme/api.d.ts.map +0 -1
  81. package/dist/ui/aboutme/index.d.ts.map +0 -1
  82. package/dist/ui/aboutme/renderer.d.ts.map +0 -1
  83. package/dist/ui/aboutme/styles.d.ts.map +0 -1
  84. /package/dist/ui/{aboutme → about-me}/api.d.ts +0 -0
  85. /package/dist/ui/{aboutme → about-me}/index.d.ts +0 -0
  86. /package/dist/ui/{aboutme → about-me}/renderer.d.ts +0 -0
  87. /package/dist/ui/{aboutme → about-me}/styles.d.ts +0 -0
  88. /package/src/api/handlers/{aboutme.ts → about-me.ts} +0 -0
  89. /package/src/api/handlers/{staticdetails.ts → static-details.ts} +0 -0
  90. /package/src/prerender/{pageContent.ts → page-content.ts} +0 -0
  91. /package/src/shared/interfaces/{iFooterLink.ts → ifooter-link.ts} +0 -0
  92. /package/src/shared/interfaces/{iRoute.ts → iroute.ts} +0 -0
  93. /package/src/ui/{aboutme → about-me}/api.ts +0 -0
  94. /package/src/ui/{aboutme → about-me}/index.ts +0 -0
  95. /package/src/ui/{aboutme → about-me}/renderer.ts +0 -0
  96. /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.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
- export default {
2
- async fetch(request: Request, env?: Env): Promise<Response> {
3
- const url = new URL(request.url);
1
+ import { WebsiteAPI } from './website-api';
2
+ export { WebsiteAPI };
3
+ export type { APIHandler } from './website-api';
4
4
 
5
- // Handle CORS preflight requests
6
- if (request.method === 'OPTIONS') {
7
- return handleCORS();
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
@@ -0,0 +1,4 @@
1
+ export * from './api';
2
+ export * from './prerender';
3
+ export * from './ui';
4
+ export * from './shared';
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { generatePageContent } from '../pageContent';
2
+ import { generatePageContent } from '../page-content';
3
3
 
4
4
  describe('generatePageContent', () => {
5
5
  const mockRoutes = [
@@ -1,138 +1,7 @@
1
- import { createHtmlTemplate, TemplateProps } from "./template";
1
+ import { WebsitePrerender } from './website-prerender';
2
+ export { WebsitePrerender };
3
+ export type { PrerenderOptions } from './website-prerender';
2
4
 
3
- import { IFooterLink, IRoute, generatePageContent } from "./pageContent";
4
-
5
- const routes: IRoute[] = [
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
+ }
@@ -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/iRoute';
7
- export * from './interfaces/iFooterLink';
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 './pageContent';
14
+ export * from './page-content';
13
15
 
14
16
  // Runtime
15
17
  export * from './runtime';
@@ -1,5 +1,6 @@
1
- import { IFooterLink } from './interfaces/iFooterLink';
2
- import { IRoute } from './interfaces/iRoute';
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=\\'\${JSON.stringify(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\\'re looking for doesn\\'t exist.</p>
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}`;