@leadertechie/personal-site-kit 0.1.0-alpha.7 → 0.1.0-alpha.9
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/content-utils.d.ts +27 -0
- package/dist/api/content-utils.d.ts.map +1 -0
- package/dist/api/handlers/about-me.d.ts.map +1 -1
- package/dist/api/handlers/content-api.d.ts +0 -1
- package/dist/api/handlers/content-api.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-CnSEOZse.js} +1285 -477
- package/dist/chunks/{template-Boh_MKY5.js → template-DWcsZW22.js} +8 -7
- package/dist/chunks/{website-api-XoeLwo_N.js → website-api-BEYGOsT3.js} +137 -156
- 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 +18 -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 +4 -4
- package/dist/ui/about-me/renderer.d.ts +0 -6
- package/dist/ui/about-me/renderer.d.ts.map +0 -1
- package/src/api/__tests__/info.test.ts +0 -44
- package/src/api/__tests__/utils.test.ts +0 -78
- package/src/api/handlers/about-me.ts +0 -99
- package/src/api/handlers/auth-handler.ts +0 -194
- package/src/api/handlers/auth.ts +0 -157
- package/src/api/handlers/content-api.ts +0 -268
- package/src/api/handlers/content.ts +0 -139
- package/src/api/handlers/home.ts +0 -79
- package/src/api/handlers/info.ts +0 -12
- package/src/api/handlers/logo.ts +0 -55
- package/src/api/handlers/static-details.ts +0 -48
- package/src/api/index.ts +0 -9
- package/src/api/utils.ts +0 -16
- package/src/api/website-api.ts +0 -138
- package/src/index.ts +0 -4
- package/src/prerender/__tests__/page-content.test.ts +0 -54
- package/src/prerender/__tests__/template.test.ts +0 -54
- package/src/prerender/index.ts +0 -7
- package/src/prerender/page-content.ts +0 -263
- package/src/prerender/prerender.ts +0 -25
- package/src/prerender/template.ts +0 -65
- package/src/prerender/website-prerender.ts +0 -152
- package/src/shared/config/api.ts +0 -16
- package/src/shared/config/index.ts +0 -43
- package/src/shared/config/types.ts +0 -16
- package/src/shared/core/__tests__/theme-toggle.test.ts +0 -204
- package/src/shared/core/site-store.ts +0 -38
- package/src/shared/core/theme-toggle.ts +0 -118
- package/src/shared/index.ts +0 -17
- package/src/shared/interfaces/ifooter-link.ts +0 -4
- package/src/shared/interfaces/iroute.ts +0 -4
- package/src/shared/models/theme-variables.css +0 -25
- package/src/shared/page-content.ts +0 -210
- package/src/shared/router.ts +0 -250
- package/src/shared/runtime.ts +0 -11
- package/src/shared/template.ts +0 -35
- package/src/shared/website-ui.ts +0 -92
- package/src/styles/markdown.css +0 -129
- package/src/ui/about-me/api.ts +0 -12
- package/src/ui/about-me/index.ts +0 -164
- package/src/ui/about-me/renderer.ts +0 -23
- package/src/ui/about-me/styles.ts +0 -85
- package/src/ui/admin/index.ts +0 -655
- package/src/ui/admin/styles.ts +0 -270
- package/src/ui/banner/index.ts +0 -38
- package/src/ui/banner/styles.ts +0 -95
- package/src/ui/blog-viewer/__tests__/blogviewer.test.ts +0 -7
- package/src/ui/blog-viewer/index.ts +0 -124
- package/src/ui/blog-viewer/styles.ts +0 -23
- package/src/ui/footer/index.ts +0 -37
- package/src/ui/footer/styles.ts +0 -50
- package/src/ui/index.ts +0 -6
- package/src/ui/story-viewer/__tests__/storyviewer.test.ts +0 -7
- package/src/ui/story-viewer/index.ts +0 -120
- package/src/ui/story-viewer/styles.ts +0 -54
- /package/{src/shared → dist}/styles/markdown.css +0 -0
- /package/{src → dist}/styles/theme.css +0 -0
package/src/api/handlers/logo.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
const PLACEHOLDER_LOGO = `<svg width="600" height="320" viewBox="0 0 600 320" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
-
<style>
|
|
3
|
-
.text-main {
|
|
4
|
-
fill: light-dark(#4A5568, #E2E8F0);
|
|
5
|
-
font-family: 'Segoe UI', 'Arial Black', sans-serif;
|
|
6
|
-
font-weight: 900;
|
|
7
|
-
font-size: 46px;
|
|
8
|
-
letter-spacing: -0.01em;
|
|
9
|
-
text-anchor: middle;
|
|
10
|
-
}
|
|
11
|
-
</style>
|
|
12
|
-
|
|
13
|
-
<!-- Big circle placeholder -->
|
|
14
|
-
<circle cx="300" cy="160" r="100" fill="none" stroke="light-dark(#A0AEC0, #718096)" stroke-width="2" stroke-dasharray="8 4" opacity="0.5" />
|
|
15
|
-
|
|
16
|
-
<!-- Plus sign in circle -->
|
|
17
|
-
<line x1="300" y1="100" x2="300" y2="220" stroke="light-dark(#A0AEC0, #718096)" stroke-width="2" opacity="0.3" />
|
|
18
|
-
<line x1="240" y1="160" x2="360" y2="160" stroke="light-dark(#A0AEC0, #718096)" stroke-width="2" opacity="0.3" />
|
|
19
|
-
|
|
20
|
-
<text x="300" y="290" class="text-main">YOUR LOGO</text>
|
|
21
|
-
</svg>`;
|
|
22
|
-
|
|
23
|
-
export async function handleLogo(env?: any): Promise<Response> {
|
|
24
|
-
try {
|
|
25
|
-
// Try to get logo from R2
|
|
26
|
-
if (env?.CONTENT_BUCKET) {
|
|
27
|
-
const logo = await env.CONTENT_BUCKET.get('logo.svg');
|
|
28
|
-
if (logo) {
|
|
29
|
-
const headers = new Headers();
|
|
30
|
-
logo.writeHttpMetadata(headers);
|
|
31
|
-
headers.set('Content-Type', 'image/svg+xml');
|
|
32
|
-
headers.set('Cache-Control', 'public, max-age=3600');
|
|
33
|
-
headers.set('Access-Control-Allow-Origin', '*');
|
|
34
|
-
return new Response(logo.body, { headers });
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Return placeholder logo
|
|
39
|
-
return new Response(PLACEHOLDER_LOGO, {
|
|
40
|
-
headers: {
|
|
41
|
-
'Content-Type': 'image/svg+xml',
|
|
42
|
-
'Cache-Control': 'public, max-age=3600',
|
|
43
|
-
'Access-Control-Allow-Origin': '*',
|
|
44
|
-
},
|
|
45
|
-
});
|
|
46
|
-
} catch (error) {
|
|
47
|
-
console.error('Error serving logo:', error);
|
|
48
|
-
return new Response(PLACEHOLDER_LOGO, {
|
|
49
|
-
headers: {
|
|
50
|
-
'Content-Type': 'image/svg+xml',
|
|
51
|
-
'Access-Control-Allow-Origin': '*',
|
|
52
|
-
},
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
const DEFAULT_STATIC_DETAILS = {
|
|
2
|
-
siteTitle: "My Personal Website",
|
|
3
|
-
copyright: "2026 My Personal Website",
|
|
4
|
-
linkedin: "https://linkedin.com/in/yourname",
|
|
5
|
-
github: "https://github.com/yourname",
|
|
6
|
-
email: "yourname@domain.com"
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export async function handleStaticDetails(env?: any, method?: string, body?: any): Promise<Response> {
|
|
10
|
-
try {
|
|
11
|
-
if (!env?.CONTENT_BUCKET) {
|
|
12
|
-
return new Response(JSON.stringify(DEFAULT_STATIC_DETAILS), {
|
|
13
|
-
headers: { 'Content-Type': 'application/json' },
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// GET - return static details
|
|
18
|
-
if (!method || method === 'GET') {
|
|
19
|
-
const staticDetails = await env.CONTENT_BUCKET.get('staticdetails.json');
|
|
20
|
-
if (staticDetails) {
|
|
21
|
-
const data = await staticDetails.json();
|
|
22
|
-
return new Response(JSON.stringify({ ...DEFAULT_STATIC_DETAILS, ...data }), {
|
|
23
|
-
headers: {
|
|
24
|
-
'Content-Type': 'application/json',
|
|
25
|
-
'Access-Control-Allow-Origin': '*'
|
|
26
|
-
},
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
return new Response(JSON.stringify(DEFAULT_STATIC_DETAILS), {
|
|
30
|
-
headers: {
|
|
31
|
-
'Content-Type': 'application/json',
|
|
32
|
-
'Access-Control-Allow-Origin': '*'
|
|
33
|
-
},
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// PUT - save static details (handled in content handler)
|
|
38
|
-
return new Response(JSON.stringify({ error: 'Use content endpoint for PUT' }), {
|
|
39
|
-
status: 400,
|
|
40
|
-
headers: { 'Content-Type': 'application/json' },
|
|
41
|
-
});
|
|
42
|
-
} catch (error) {
|
|
43
|
-
console.error('Error serving static details:', error);
|
|
44
|
-
return new Response(JSON.stringify(DEFAULT_STATIC_DETAILS), {
|
|
45
|
-
headers: { 'Content-Type': 'application/json' },
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
}
|
package/src/api/index.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { WebsiteAPI } from './website-api';
|
|
2
|
-
export { WebsiteAPI };
|
|
3
|
-
export type { APIHandler } from './website-api';
|
|
4
|
-
export * from './handlers/auth';
|
|
5
|
-
export * from './handlers/auth-handler';
|
|
6
|
-
|
|
7
|
-
// Default worker export using WebsiteAPI
|
|
8
|
-
const defaultAPI = new WebsiteAPI();
|
|
9
|
-
export default defaultAPI;
|
package/src/api/utils.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export interface APIResponse {
|
|
2
|
-
data?: any;
|
|
3
|
-
error?: string;
|
|
4
|
-
status: number;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function createJSONResponse(data: any, status: number = 200): Response {
|
|
8
|
-
return new Response(JSON.stringify(data), {
|
|
9
|
-
status,
|
|
10
|
-
headers: { 'Content-Type': 'application/json' },
|
|
11
|
-
});
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function createErrorResponse(message: string, status: number = 500): Response {
|
|
15
|
-
return createJSONResponse({ error: message }, status);
|
|
16
|
-
}
|
package/src/api/website-api.ts
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
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 { handleAuth } from './handlers/auth-handler';
|
|
7
|
-
import { handleBlogs, handleStories, handleSearch } from './handlers/content-api';
|
|
8
|
-
import { handleLogo } from './handlers/logo';
|
|
9
|
-
import { handleStaticDetails } from './handlers/static-details';
|
|
10
|
-
import { getAuthStore } from './handlers/auth';
|
|
11
|
-
|
|
12
|
-
export type APIHandler = (request: Request, env: any) => Promise<Response>;
|
|
13
|
-
|
|
14
|
-
export class WebsiteAPI {
|
|
15
|
-
private customHandlers = new Map<string, APIHandler>();
|
|
16
|
-
|
|
17
|
-
public registerHandler(route: string, handler: APIHandler) {
|
|
18
|
-
this.customHandlers.set(route, handler);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
private addCORSHeaders(response: Response): Response {
|
|
22
|
-
response.headers.set('Access-Control-Allow-Origin', '*' );
|
|
23
|
-
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS' );
|
|
24
|
-
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
25
|
-
return response;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
private addAdminCORSHeaders(response: Response, origin: string): Response {
|
|
29
|
-
const allowOrigin = origin.includes('localhost') || origin.includes('127.0.0.1')
|
|
30
|
-
? origin
|
|
31
|
-
: 'same-origin';
|
|
32
|
-
response.headers.set('Access-Control-Allow-Origin', allowOrigin);
|
|
33
|
-
response.headers.set('Access-Control-Allow-Credentials', 'true');
|
|
34
|
-
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
35
|
-
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
36
|
-
return response;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
private handleCORS(origin: string): Response {
|
|
40
|
-
const allowOrigin = origin.includes('localhost') || origin.includes('127.0.0.1')
|
|
41
|
-
? origin
|
|
42
|
-
: '*';
|
|
43
|
-
return new Response(null, {
|
|
44
|
-
status: 200,
|
|
45
|
-
headers: {
|
|
46
|
-
'Access-Control-Allow-Origin': allowOrigin ,
|
|
47
|
-
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS' ,
|
|
48
|
-
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
49
|
-
'Access-Control-Allow-Credentials': 'true',
|
|
50
|
-
'Access-Control-Max-Age': '86400',
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
public async fetch(request: Request, env: any): Promise<Response> {
|
|
56
|
-
const url = new URL(request.url);
|
|
57
|
-
const origin = request.headers.get('Origin') || url.origin;
|
|
58
|
-
|
|
59
|
-
if (request.method === 'OPTIONS') {
|
|
60
|
-
return this.handleCORS(origin);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const pathname = url.pathname;
|
|
64
|
-
const route = pathname
|
|
65
|
-
.replace(/^\/api\//, '')
|
|
66
|
-
.replace(/^\//, '')
|
|
67
|
-
.replace(/\/+$/, '');
|
|
68
|
-
|
|
69
|
-
// Check custom handlers first
|
|
70
|
-
if (this.customHandlers.has(route)) {
|
|
71
|
-
const handler = this.customHandlers.get(route)!;
|
|
72
|
-
return this.addCORSHeaders(await handler(request, env));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
// Check for content route first (content/*)
|
|
77
|
-
if (route === 'content' || route.startsWith('content/')) {
|
|
78
|
-
const subpath = route.replace(/^content\/?/, '');
|
|
79
|
-
return this.addAdminCORSHeaders(await handleContent(request, env, subpath), origin);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
switch (route) {
|
|
83
|
-
case 'info':
|
|
84
|
-
return this.addCORSHeaders(await handleInfo());
|
|
85
|
-
case 'home':
|
|
86
|
-
return this.addCORSHeaders(await handleHome(env));
|
|
87
|
-
case 'cache-clear':
|
|
88
|
-
const cookieHeader = request.headers.get('Cookie');
|
|
89
|
-
const sessionToken = cookieHeader?.split(';')
|
|
90
|
-
.find(c => c.trim().startsWith('session='))
|
|
91
|
-
?.split('=')[1];
|
|
92
|
-
const session = sessionToken ? await env.KV.get(`session:${sessionToken}`, 'json') : null;
|
|
93
|
-
if (!session || session.expiresAt < Date.now()) {
|
|
94
|
-
return this.addAdminCORSHeaders(createErrorResponse('Unauthorized', 401), origin);
|
|
95
|
-
}
|
|
96
|
-
clearContentCache();
|
|
97
|
-
return this.addAdminCORSHeaders(new Response(JSON.stringify({ success: true, message: 'Cache cleared' }), { status: 200 }), origin);
|
|
98
|
-
case 'aboutme':
|
|
99
|
-
return this.addCORSHeaders(await handleAboutMe(env));
|
|
100
|
-
case 'logo':
|
|
101
|
-
return this.addCORSHeaders(await handleLogo(env));
|
|
102
|
-
case 'static':
|
|
103
|
-
return this.addCORSHeaders(await handleStaticDetails(env));
|
|
104
|
-
case 'auth':
|
|
105
|
-
return this.addAdminCORSHeaders(await handleAuth(request, env, '/auth'), origin);
|
|
106
|
-
case 'blogs':
|
|
107
|
-
return this.addCORSHeaders(await handleBlogs(env));
|
|
108
|
-
case 'blogs/latest':
|
|
109
|
-
const latestCount = url.searchParams.get('count');
|
|
110
|
-
return this.addCORSHeaders(await handleBlogs(env, undefined, latestCount ? parseInt(latestCount) : 5));
|
|
111
|
-
default:
|
|
112
|
-
if (route.startsWith('blogs/')) {
|
|
113
|
-
const slug = route.replace('blogs/', '');
|
|
114
|
-
return this.addCORSHeaders(await handleBlogs(env, slug));
|
|
115
|
-
}
|
|
116
|
-
if (route.startsWith('stories')) {
|
|
117
|
-
if (route === 'stories') {
|
|
118
|
-
return this.addCORSHeaders(await handleStories(env));
|
|
119
|
-
}
|
|
120
|
-
if (route === 'stories/latest') {
|
|
121
|
-
const latestCount = url.searchParams.get('count');
|
|
122
|
-
return this.addCORSHeaders(await handleStories(env, undefined, latestCount ? parseInt(latestCount) : 5));
|
|
123
|
-
}
|
|
124
|
-
const slug = route.replace('stories/', '');
|
|
125
|
-
return this.addCORSHeaders(await handleStories(env, slug));
|
|
126
|
-
}
|
|
127
|
-
if (route === 'search') {
|
|
128
|
-
const query = url.searchParams.get('q');
|
|
129
|
-
return this.addCORSHeaders(await handleSearch(env, query || undefined));
|
|
130
|
-
}
|
|
131
|
-
return this.addCORSHeaders(createErrorResponse('Route not found', 404));
|
|
132
|
-
}
|
|
133
|
-
} catch (error) {
|
|
134
|
-
console.error('API Error:', error);
|
|
135
|
-
return this.addCORSHeaders(createErrorResponse('Internal server error', 500));
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { generatePageContent } from '../page-content';
|
|
3
|
-
|
|
4
|
-
describe('generatePageContent', () => {
|
|
5
|
-
const mockRoutes = [
|
|
6
|
-
{ link: '/', text: 'Home' },
|
|
7
|
-
{ link: '/about-me', text: 'About' }
|
|
8
|
-
];
|
|
9
|
-
const mockFooterLinks = [
|
|
10
|
-
{ text: 'Link', href: '#' }
|
|
11
|
-
];
|
|
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
|
-
apiUrl: 'https://api.example.com',
|
|
20
|
-
baseUrl: 'https://www.example.com'
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
beforeEach(() => {
|
|
24
|
-
vi.clearAllMocks();
|
|
25
|
-
global.fetch = vi.fn().mockResolvedValue({
|
|
26
|
-
ok: true,
|
|
27
|
-
json: () => Promise.resolve({ siteTitle: 'My Site', copyright: '2026' })
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('should generate home page content', async () => {
|
|
32
|
-
const result = await generatePageContent('/', mockRoutes, mockFooterLinks, mockEnv);
|
|
33
|
-
|
|
34
|
-
expect(result.title).toContain('User');
|
|
35
|
-
expect(result.canonicalUrl).toBe('https://www.example.com/');
|
|
36
|
-
expect(result.content).toContain('<my-banner');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('should generate about-me page content', async () => {
|
|
40
|
-
const result = await generatePageContent('/about-me', mockRoutes, mockFooterLinks, mockEnv);
|
|
41
|
-
|
|
42
|
-
expect(result.title).toContain('About');
|
|
43
|
-
expect(result.canonicalUrl).toBe('https://www.example.com/about-me');
|
|
44
|
-
expect(result.content).toContain('<my-aboutme');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('should generate 404 page content', async () => {
|
|
48
|
-
const result = await generatePageContent('/non-existent', mockRoutes, mockFooterLinks, mockEnv);
|
|
49
|
-
|
|
50
|
-
expect(result.title).toContain('Not Found');
|
|
51
|
-
expect(result.canonicalUrl).toBe('https://www.example.com/non-existent');
|
|
52
|
-
expect(result.content).toContain('Page Not Found');
|
|
53
|
-
});
|
|
54
|
-
});
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { createHtmlTemplate } from '../template';
|
|
3
|
-
|
|
4
|
-
describe('createHtmlTemplate', () => {
|
|
5
|
-
it('should generate complete HTML template', async () => {
|
|
6
|
-
const props = {
|
|
7
|
-
title: 'Test Title',
|
|
8
|
-
description: 'Test Description',
|
|
9
|
-
canonicalUrl: 'https://example.com/test',
|
|
10
|
-
content: '<main>Test Content</main>'
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const html = await createHtmlTemplate(props);
|
|
14
|
-
|
|
15
|
-
expect(html).toContain('<!doctype html>');
|
|
16
|
-
expect(html).toContain('<title>Test Title</title>');
|
|
17
|
-
expect(html).toContain('<meta name="description" content="Test Description" />');
|
|
18
|
-
expect(html).toContain('<meta property="og:title" content="Test Title" />');
|
|
19
|
-
expect(html).toContain('<link rel="canonical" href="https://example.com/test" />');
|
|
20
|
-
expect(html).toContain('<main>Test Content</main>');
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('should escape HTML entities properly', async () => {
|
|
24
|
-
const props = {
|
|
25
|
-
title: 'Test & Title',
|
|
26
|
-
description: 'Test "Description"',
|
|
27
|
-
canonicalUrl: 'https://example.com/test',
|
|
28
|
-
content: '<main>Test Content</main>'
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const html = await createHtmlTemplate(props);
|
|
32
|
-
|
|
33
|
-
expect(html).toContain('<title>Test & Title</title>');
|
|
34
|
-
expect(html).toContain('<meta name="description" content="Test "Description"" />');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('should have proper HTML structure', async () => {
|
|
38
|
-
const props = {
|
|
39
|
-
title: 'Test',
|
|
40
|
-
description: 'Test',
|
|
41
|
-
canonicalUrl: 'https://example.com',
|
|
42
|
-
content: '<div>Content</div>'
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const html = await createHtmlTemplate(props);
|
|
46
|
-
|
|
47
|
-
expect(html).toMatch(/^<!doctype html>/);
|
|
48
|
-
expect(html).toContain('<html lang="en" data-theme="light">');
|
|
49
|
-
expect(html).toContain('<head>');
|
|
50
|
-
expect(html).toContain('<body>');
|
|
51
|
-
expect(html).toContain('<div id="app">');
|
|
52
|
-
expect(html).toContain('</html>');
|
|
53
|
-
});
|
|
54
|
-
});
|
package/src/prerender/index.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { WebsitePrerender } from './website-prerender';
|
|
2
|
-
export { WebsitePrerender };
|
|
3
|
-
export type { PrerenderOptions } from './website-prerender';
|
|
4
|
-
|
|
5
|
-
// Default worker export using WebsitePrerender
|
|
6
|
-
const defaultPrerender = new WebsitePrerender();
|
|
7
|
-
export default defaultPrerender;
|
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
import { R2ContentLoader } from "@leadertechie/r2tohtml";
|
|
2
|
-
|
|
3
|
-
interface Profile {
|
|
4
|
-
name: string;
|
|
5
|
-
title: string;
|
|
6
|
-
experience: string;
|
|
7
|
-
profileImageUrl: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface IRoute {
|
|
11
|
-
text: string;
|
|
12
|
-
link: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface IFooterLink {
|
|
16
|
-
text: string;
|
|
17
|
-
link: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface PageContent {
|
|
21
|
-
title: string;
|
|
22
|
-
description: string;
|
|
23
|
-
canonicalUrl: string;
|
|
24
|
-
content: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface BlogMeta {
|
|
28
|
-
slug: string;
|
|
29
|
-
title: string;
|
|
30
|
-
summary: string;
|
|
31
|
-
tags: string[];
|
|
32
|
-
date: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
let loader: R2ContentLoader | null = null;
|
|
36
|
-
|
|
37
|
-
function getLoader(env: any): R2ContentLoader | null {
|
|
38
|
-
if (!loader) {
|
|
39
|
-
if (!env?.CONTENT_BUCKET) return null;
|
|
40
|
-
loader = new R2ContentLoader(
|
|
41
|
-
{ bucket: env.CONTENT_BUCKET, cacheTTL: 5 * 60 * 1000 },
|
|
42
|
-
{ md2html: { imagePathPrefix: "images/", styleOptions: { classPrefix: "md-", addHeadingIds: true } } }
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
return loader;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function fetchProfile(env: any): Promise<Profile | null> {
|
|
49
|
-
try {
|
|
50
|
-
const r2 = getLoader(env);
|
|
51
|
-
if (!r2) return null;
|
|
52
|
-
const obj = await r2.getObject("profile.json");
|
|
53
|
-
if (!obj) return null;
|
|
54
|
-
return await obj.json() as Profile;
|
|
55
|
-
} catch { return null; }
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function fetchAboutMe(env: any): Promise<string> {
|
|
59
|
-
try {
|
|
60
|
-
const r2 = getLoader(env);
|
|
61
|
-
if (!r2) return "";
|
|
62
|
-
const result = await r2.getRendered("about-me.md");
|
|
63
|
-
return result?.content || "";
|
|
64
|
-
} catch { return ""; }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function fetchHome(env: any): Promise<string> {
|
|
68
|
-
try {
|
|
69
|
-
const r2 = getLoader(env);
|
|
70
|
-
if (!r2) return "";
|
|
71
|
-
const result = await r2.getRendered("home.md");
|
|
72
|
-
return result?.content || "";
|
|
73
|
-
} catch { return ""; }
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function fetchLatestBlogSummaries(env: any, count: number = 3): Promise<BlogMeta[]> {
|
|
77
|
-
try {
|
|
78
|
-
const r2 = getLoader(env);
|
|
79
|
-
if (!r2) return [];
|
|
80
|
-
const list = await r2.list("blogs/");
|
|
81
|
-
const metas: BlogMeta[] = [];
|
|
82
|
-
for (const obj of list.objects) {
|
|
83
|
-
if (obj.key.endsWith(".json")) {
|
|
84
|
-
try {
|
|
85
|
-
const metaObj = await r2.getObject(obj.key);
|
|
86
|
-
if (metaObj) metas.push(await metaObj.json() as BlogMeta);
|
|
87
|
-
} catch {}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return metas.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()).slice(0, count);
|
|
91
|
-
} catch { return []; }
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function fetchLatestStorySummaries(env: any, count: number = 3): Promise<BlogMeta[]> {
|
|
95
|
-
try {
|
|
96
|
-
const r2 = getLoader(env);
|
|
97
|
-
if (!r2) return [];
|
|
98
|
-
const list = await r2.list("stories/");
|
|
99
|
-
const metas: BlogMeta[] = [];
|
|
100
|
-
for (const obj of list.objects) {
|
|
101
|
-
if (obj.key.endsWith(".json")) {
|
|
102
|
-
try {
|
|
103
|
-
const metaObj = await r2.getObject(obj.key);
|
|
104
|
-
if (metaObj) metas.push(await metaObj.json() as BlogMeta);
|
|
105
|
-
} catch {}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return metas.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()).slice(0, count);
|
|
109
|
-
} catch { return []; }
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export const generatePageContent = async (
|
|
113
|
-
pathname: string,
|
|
114
|
-
routes: IRoute[],
|
|
115
|
-
footerLinks: IFooterLink[],
|
|
116
|
-
env?: any
|
|
117
|
-
): Promise<PageContent> => {
|
|
118
|
-
const apiUrl = env?.apiUrl || "https://api.techieleader.com";
|
|
119
|
-
const baseUrl = env?.baseUrl || "https://www.techieleader.com";
|
|
120
|
-
|
|
121
|
-
let staticDetails = {
|
|
122
|
-
siteTitle: "My Personal Website",
|
|
123
|
-
copyright: "2026 My Personal Website",
|
|
124
|
-
linkedin: "https://linkedin.com/in/yourname",
|
|
125
|
-
github: "https://github.com/yourname",
|
|
126
|
-
email: "yourname@domain.com"
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
const res = await fetch(`${apiUrl}/api/static`);
|
|
131
|
-
if (res.ok) staticDetails = await res.json();
|
|
132
|
-
} catch (e) {}
|
|
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
|
-
let profile: Profile | null = null;
|
|
152
|
-
let aboutMeContent = "";
|
|
153
|
-
let homeContent = "";
|
|
154
|
-
let latestBlogs: BlogMeta[] = [];
|
|
155
|
-
let latestStories: BlogMeta[] = [];
|
|
156
|
-
|
|
157
|
-
if (env?.CONTENT_BUCKET) {
|
|
158
|
-
[profile, aboutMeContent, homeContent, latestBlogs, latestStories] = await Promise.all([
|
|
159
|
-
fetchProfile(env), fetchAboutMe(env), fetchHome(env), fetchLatestBlogSummaries(env, 3), fetchLatestStorySummaries(env, 3)
|
|
160
|
-
]);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const name = profile?.name || "User";
|
|
164
|
-
const title = profile?.title || "Professional";
|
|
165
|
-
const experience = profile?.experience || "some";
|
|
166
|
-
const canonicalUrl = new URL(pathname, baseUrl).toString();
|
|
167
|
-
|
|
168
|
-
if (pathname === "/" || pathname === "") {
|
|
169
|
-
const homeHtml = homeContent || `<h1>Welcome to ${name}</h1><p>Upload home.md to customize this page.</p>`;
|
|
170
|
-
const blogGists = latestBlogs.map(b => `<div class="gist-card"><a href="/blogs/${b.slug}"><h4>${b.title}</h4></a><p>${b.summary}</p><small>${b.date}</small></div>`).join("");
|
|
171
|
-
const storyGists = latestStories.map(s => `<div class="gist-card"><a href="/stories/${s.slug}"><h4>${s.title}</h4></a><p>${s.summary}</p><small>${s.date}</small></div>`).join("");
|
|
172
|
-
|
|
173
|
-
const mainContent = `
|
|
174
|
-
${bannerTemplate}
|
|
175
|
-
<main class="container container-wide column-layout">
|
|
176
|
-
<div class="main-column">
|
|
177
|
-
${homeHtml}
|
|
178
|
-
</div>
|
|
179
|
-
<div class="sidebar-column">
|
|
180
|
-
<h3>Recent Blogs</h3>
|
|
181
|
-
${blogGists || "<p>No blogs yet.</p>"}
|
|
182
|
-
<h3 class="mt-2">Recent Stories</h3>
|
|
183
|
-
${storyGists || "<p>No stories yet.</p>"}
|
|
184
|
-
</div>
|
|
185
|
-
</main>
|
|
186
|
-
${footerTemplate}`;
|
|
187
|
-
|
|
188
|
-
return {
|
|
189
|
-
title: `${name} – ${title}`,
|
|
190
|
-
description: `Welcome to ${name}'s personal website. Professional portfolio and content.`,
|
|
191
|
-
canonicalUrl,
|
|
192
|
-
content: mainContent
|
|
193
|
-
};
|
|
194
|
-
} else if (pathname === "/about-me") {
|
|
195
|
-
const mainContent = `
|
|
196
|
-
${bannerTemplate}
|
|
197
|
-
<main class="container container-narrow">
|
|
198
|
-
<my-aboutme base-url="${apiUrl}"></my-aboutme>
|
|
199
|
-
</main>
|
|
200
|
-
${footerTemplate}`;
|
|
201
|
-
|
|
202
|
-
return {
|
|
203
|
-
title: `About - ${name}`,
|
|
204
|
-
description: `Learn more about ${name}'s experience and skills.`,
|
|
205
|
-
canonicalUrl,
|
|
206
|
-
content: mainContent
|
|
207
|
-
};
|
|
208
|
-
} else if (pathname === "/blogs" || pathname === "/blogs/") {
|
|
209
|
-
const blogGists = latestBlogs.map(b => `<div class="gist-card"><a href="/blogs/${b.slug}"><h4>${b.title}</h4></a><p>${b.summary}</p><small>${b.date}</small></div>`).join("");
|
|
210
|
-
const mainContent = `
|
|
211
|
-
${bannerTemplate}
|
|
212
|
-
<main class="container container-wide">
|
|
213
|
-
<h1>Blogs</h1>
|
|
214
|
-
<input type="text" placeholder="Search blogs..." class="search-input" />
|
|
215
|
-
<div class="blog-list">
|
|
216
|
-
${blogGists || "<p>No blogs yet.</p>"}
|
|
217
|
-
</div>
|
|
218
|
-
</main>
|
|
219
|
-
${footerTemplate}`;
|
|
220
|
-
return { title: `Blogs – ${name}`, description: "Read the latest blog posts.", canonicalUrl, content: mainContent };
|
|
221
|
-
} else if (pathname === "/stories" || pathname === "/stories/") {
|
|
222
|
-
const storyGists = latestStories.map(s => `<div class="gist-card"><a href="/stories/${s.slug}"><h4>${s.title}</h4></a><p>${s.summary}</p><small>${s.date}</small></div>`).join("");
|
|
223
|
-
const mainContent = `
|
|
224
|
-
${bannerTemplate}
|
|
225
|
-
<main class="container container-wide">
|
|
226
|
-
<h1>Stories</h1>
|
|
227
|
-
<input type="text" placeholder="Search stories..." class="search-input" />
|
|
228
|
-
<div class="story-list">
|
|
229
|
-
${storyGists || "<p>No stories yet.</p>"}
|
|
230
|
-
</div>
|
|
231
|
-
</main>
|
|
232
|
-
${footerTemplate}`;
|
|
233
|
-
return { title: `Stories – ${name}`, description: "Read the latest stories.", canonicalUrl, content: mainContent };
|
|
234
|
-
} else if (pathname.startsWith("/blogs/")) {
|
|
235
|
-
const slug = pathname.replace("/blogs/", "").replace("/", "");
|
|
236
|
-
const mainContent = `
|
|
237
|
-
${bannerTemplate}
|
|
238
|
-
<main class="container container-narrow">
|
|
239
|
-
<my-blog-viewer slug="${slug}"></my-blog-viewer>
|
|
240
|
-
</main>
|
|
241
|
-
${footerTemplate}`;
|
|
242
|
-
return { title: `Blog: ${slug}`, description: "Blog post", canonicalUrl, content: mainContent };
|
|
243
|
-
} else if (pathname.startsWith("/stories/")) {
|
|
244
|
-
const slug = pathname.replace("/stories/", "").replace("/", "");
|
|
245
|
-
const mainContent = `
|
|
246
|
-
${bannerTemplate}
|
|
247
|
-
<main class="container container-narrow">
|
|
248
|
-
<my-story-viewer slug="${slug}"></my-story-viewer>
|
|
249
|
-
</main>
|
|
250
|
-
${footerTemplate}`;
|
|
251
|
-
return { title: `Story: ${slug}`, description: "Story post", canonicalUrl, content: mainContent };
|
|
252
|
-
} else {
|
|
253
|
-
const mainContent = `
|
|
254
|
-
${bannerTemplate}
|
|
255
|
-
<main class="container container-narrow text-center">
|
|
256
|
-
<h1>Page Not Found</h1>
|
|
257
|
-
<p>The page you're looking for doesn't exist.</p>
|
|
258
|
-
<p><a href="/">Return to home</a></p>
|
|
259
|
-
</main>
|
|
260
|
-
${footerTemplate}`;
|
|
261
|
-
return { title: "404 Not Found", description: "The page you requested could not be found.", canonicalUrl, content: mainContent };
|
|
262
|
-
}
|
|
263
|
-
};
|