@leadertechie/personal-site-kit 0.1.0-alpha.8 → 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/content-api.d.ts +0 -1
- package/dist/api/handlers/content-api.d.ts.map +1 -1
- package/dist/api.js +2 -2
- package/dist/chunks/{index-CYd_Pe2U.js → index-CnSEOZse.js} +81 -121
- package/dist/chunks/{template-D1uGvdWZ.js → template-DWcsZW22.js} +1 -1
- package/dist/chunks/{website-api-FLejlWxJ.js → website-api-BEYGOsT3.js} +90 -131
- package/dist/index.js +3 -3
- package/dist/shared.js +1 -1
- package/dist/ui/admin/index.d.ts +8 -0
- package/dist/ui/admin/index.d.ts.map +1 -1
- package/dist/ui.js +1 -1
- package/package.json +4 -4
- 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 -109
- package/src/api/handlers/auth-handler.ts +0 -204
- 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 -142
- package/src/index.ts +0 -4
- package/src/prerender/__tests__/page-content.test.ts +0 -44
- package/src/prerender/__tests__/template.test.ts +0 -54
- package/src/prerender/data-fetcher.ts +0 -93
- package/src/prerender/index.ts +0 -7
- package/src/prerender/page-content.ts +0 -266
- package/src/prerender/page-generators/about.ts +0 -38
- package/src/prerender/page-generators/base.ts +0 -77
- package/src/prerender/page-generators/blog-detail.ts +0 -35
- package/src/prerender/page-generators/blogs-list.ts +0 -43
- package/src/prerender/page-generators/home.ts +0 -54
- package/src/prerender/page-generators/index.ts +0 -8
- package/src/prerender/page-generators/not-found.ts +0 -36
- package/src/prerender/page-generators/stories-list.ts +0 -43
- package/src/prerender/page-generators/story-detail.ts +0 -35
- 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 -121
- package/src/ui/about-me/styles.ts +0 -85
- package/src/ui/admin/api.ts +0 -93
- package/src/ui/admin/components/AboutMeSection.ts +0 -47
- package/src/ui/admin/components/AdminSection.ts +0 -134
- package/src/ui/admin/components/BlogsSection.ts +0 -62
- package/src/ui/admin/components/HomeSection.ts +0 -47
- package/src/ui/admin/components/ImagesSection.ts +0 -54
- package/src/ui/admin/components/LoginForm.ts +0 -116
- package/src/ui/admin/components/LogoSection.ts +0 -51
- package/src/ui/admin/components/ProfileSection.ts +0 -47
- package/src/ui/admin/components/StaticSection.ts +0 -67
- package/src/ui/admin/components/StoriesSection.ts +0 -62
- package/src/ui/admin/components/index.ts +0 -10
- package/src/ui/admin/index.ts +0 -413
- package/src/ui/admin/styles.ts +0 -270
- package/src/ui/admin/types.ts +0 -26
- 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 -127
- 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 -13
- package/src/ui/story-viewer/__tests__/storyviewer.test.ts +0 -7
- package/src/ui/story-viewer/index.ts +0 -123
- 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
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { IRoute, IFooterLink, PageContent } from '../page-content';
|
|
2
|
-
|
|
3
|
-
export interface StaticDetails {
|
|
4
|
-
siteTitle?: string;
|
|
5
|
-
copyright?: string;
|
|
6
|
-
linkedin?: string;
|
|
7
|
-
github?: string;
|
|
8
|
-
email?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface BasePageData {
|
|
12
|
-
routes: IRoute[];
|
|
13
|
-
footerLinks: IFooterLink[];
|
|
14
|
-
staticDetails: StaticDetails;
|
|
15
|
-
apiUrl: string;
|
|
16
|
-
baseUrl: string;
|
|
17
|
-
pathname: string;
|
|
18
|
-
name?: string;
|
|
19
|
-
title?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export class BasePageGenerator {
|
|
23
|
-
protected generateBanner(routes: IRoute[], siteTitle: string, logo: string): string {
|
|
24
|
-
const navLinks = routes
|
|
25
|
-
.map(r => `<a href="${r.link}" class="nav-link" data-route="${r.link === "/" ? "home" : r.text.toLowerCase()}">${r.text}</a>`)
|
|
26
|
-
.join("");
|
|
27
|
-
|
|
28
|
-
return `
|
|
29
|
-
<my-banner header="${siteTitle}" logo="${logo}">
|
|
30
|
-
<theme-toggle slot="theme-switcher"></theme-toggle>
|
|
31
|
-
<nav slot="nav-links">
|
|
32
|
-
${navLinks}
|
|
33
|
-
</nav>
|
|
34
|
-
</my-banner>`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
protected generateFooter(footerLinks: IFooterLink[], copyright: string): string {
|
|
38
|
-
return `
|
|
39
|
-
<my-footer
|
|
40
|
-
copyright="${copyright}"
|
|
41
|
-
footerlinks='${JSON.stringify(footerLinks)}'>
|
|
42
|
-
</my-footer>`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
protected generateMeta(title: string, description: string, canonicalUrl: string): void {
|
|
46
|
-
// This would be handled in the main function
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
protected wrapContent(banner: string, mainContent: string, footer: string): string {
|
|
50
|
-
return `${banner}${mainContent}${footer}`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
public generatePage(
|
|
54
|
-
pathname: string,
|
|
55
|
-
routes: IRoute[],
|
|
56
|
-
footerLinks: IFooterLink[],
|
|
57
|
-
staticDetails: StaticDetails,
|
|
58
|
-
apiUrl: string,
|
|
59
|
-
baseUrl: string,
|
|
60
|
-
mainContent: string,
|
|
61
|
-
title: string,
|
|
62
|
-
description: string
|
|
63
|
-
): PageContent {
|
|
64
|
-
const logo = "/api/logo";
|
|
65
|
-
const banner = this.generateBanner(routes, staticDetails.siteTitle || "My Personal Website", logo);
|
|
66
|
-
const footer = this.generateFooter(footerLinks, staticDetails.copyright || "2026 My Personal Website");
|
|
67
|
-
const canonicalUrl = new URL(pathname, baseUrl).toString();
|
|
68
|
-
const content = this.wrapContent(banner, mainContent, footer);
|
|
69
|
-
|
|
70
|
-
return {
|
|
71
|
-
title,
|
|
72
|
-
description,
|
|
73
|
-
canonicalUrl,
|
|
74
|
-
content
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { BasePageGenerator, StaticDetails } from './base';
|
|
2
|
-
import { IRoute, IFooterLink, PageContent } from '../page-content';
|
|
3
|
-
|
|
4
|
-
export interface BlogDetailPageData {
|
|
5
|
-
routes: IRoute[];
|
|
6
|
-
footerLinks: IFooterLink[];
|
|
7
|
-
staticDetails: StaticDetails;
|
|
8
|
-
apiUrl: string;
|
|
9
|
-
baseUrl: string;
|
|
10
|
-
pathname: string;
|
|
11
|
-
slug: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export class BlogDetailPageGenerator extends BasePageGenerator {
|
|
15
|
-
public generate(data: BlogDetailPageData): PageContent {
|
|
16
|
-
const { slug, staticDetails, ...baseData } = data;
|
|
17
|
-
|
|
18
|
-
const mainContent = `
|
|
19
|
-
<main class="container container-narrow">
|
|
20
|
-
<my-blog-viewer slug="${slug}"></my-blog-viewer>
|
|
21
|
-
</main>`;
|
|
22
|
-
|
|
23
|
-
return this.generatePage(
|
|
24
|
-
baseData.pathname,
|
|
25
|
-
baseData.routes,
|
|
26
|
-
baseData.footerLinks,
|
|
27
|
-
staticDetails,
|
|
28
|
-
baseData.apiUrl,
|
|
29
|
-
baseData.baseUrl,
|
|
30
|
-
mainContent,
|
|
31
|
-
`Blog: ${slug}`,
|
|
32
|
-
"Blog post"
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { BasePageGenerator, StaticDetails } from './base';
|
|
2
|
-
import { IRoute, IFooterLink, PageContent } from '../page-content';
|
|
3
|
-
import { BlogMeta } from '../data-fetcher';
|
|
4
|
-
|
|
5
|
-
export interface BlogsListPageData {
|
|
6
|
-
routes: IRoute[];
|
|
7
|
-
footerLinks: IFooterLink[];
|
|
8
|
-
staticDetails: StaticDetails;
|
|
9
|
-
apiUrl: string;
|
|
10
|
-
baseUrl: string;
|
|
11
|
-
pathname: string;
|
|
12
|
-
latestBlogs: BlogMeta[];
|
|
13
|
-
name: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export class BlogsListPageGenerator extends BasePageGenerator {
|
|
17
|
-
public generate(data: BlogsListPageData): PageContent {
|
|
18
|
-
const { latestBlogs, name, staticDetails, ...baseData } = data;
|
|
19
|
-
|
|
20
|
-
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("");
|
|
21
|
-
|
|
22
|
-
const mainContent = `
|
|
23
|
-
<main class="container container-wide">
|
|
24
|
-
<h1>Blogs</h1>
|
|
25
|
-
<input type="text" placeholder="Search blogs..." class="search-input" />
|
|
26
|
-
<div class="blog-list">
|
|
27
|
-
${blogGists || "<p>No blogs yet.</p>"}
|
|
28
|
-
</div>
|
|
29
|
-
</main>`;
|
|
30
|
-
|
|
31
|
-
return this.generatePage(
|
|
32
|
-
baseData.pathname,
|
|
33
|
-
baseData.routes,
|
|
34
|
-
baseData.footerLinks,
|
|
35
|
-
staticDetails,
|
|
36
|
-
baseData.apiUrl,
|
|
37
|
-
baseData.baseUrl,
|
|
38
|
-
mainContent,
|
|
39
|
-
`Blogs – ${name}`,
|
|
40
|
-
"Read the latest blog posts."
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { BasePageGenerator, StaticDetails } from './base';
|
|
2
|
-
import { IRoute, IFooterLink, PageContent } from '../page-content';
|
|
3
|
-
import { Profile, BlogMeta } from '../data-fetcher';
|
|
4
|
-
|
|
5
|
-
export interface HomePageData {
|
|
6
|
-
routes: IRoute[];
|
|
7
|
-
footerLinks: IFooterLink[];
|
|
8
|
-
staticDetails: StaticDetails;
|
|
9
|
-
apiUrl: string;
|
|
10
|
-
baseUrl: string;
|
|
11
|
-
pathname: string;
|
|
12
|
-
profile: Profile | null;
|
|
13
|
-
homeContent: string;
|
|
14
|
-
latestBlogs: BlogMeta[];
|
|
15
|
-
latestStories: BlogMeta[];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export class HomePageGenerator extends BasePageGenerator {
|
|
19
|
-
public generate(data: HomePageData): PageContent {
|
|
20
|
-
const { profile, homeContent, latestBlogs, latestStories, staticDetails, ...baseData } = data;
|
|
21
|
-
|
|
22
|
-
const name = profile?.name || "User";
|
|
23
|
-
const title = profile?.title || "Professional";
|
|
24
|
-
|
|
25
|
-
const homeHtml = homeContent || `<h1>Welcome to ${name}</h1><p>Upload home.md to customize this page.</p>`;
|
|
26
|
-
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("");
|
|
27
|
-
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("");
|
|
28
|
-
|
|
29
|
-
const mainContent = `
|
|
30
|
-
<main class="container container-wide column-layout">
|
|
31
|
-
<div class="main-column">
|
|
32
|
-
${homeHtml}
|
|
33
|
-
</div>
|
|
34
|
-
<div class="sidebar-column">
|
|
35
|
-
<h3>Recent Blogs</h3>
|
|
36
|
-
${blogGists || "<p>No blogs yet.</p>"}
|
|
37
|
-
<h3 class="mt-2">Recent Stories</h3>
|
|
38
|
-
${storyGists || "<p>No stories yet.</p>"}
|
|
39
|
-
</div>
|
|
40
|
-
</main>`;
|
|
41
|
-
|
|
42
|
-
return this.generatePage(
|
|
43
|
-
baseData.pathname,
|
|
44
|
-
baseData.routes,
|
|
45
|
-
baseData.footerLinks,
|
|
46
|
-
staticDetails,
|
|
47
|
-
baseData.apiUrl,
|
|
48
|
-
baseData.baseUrl,
|
|
49
|
-
mainContent,
|
|
50
|
-
`${name} – ${title}`,
|
|
51
|
-
`Welcome to ${name}'s personal website. Professional portfolio and content.`
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { BasePageGenerator, StaticDetails } from './base';
|
|
2
|
-
import { IRoute, IFooterLink, PageContent } from '../page-content';
|
|
3
|
-
|
|
4
|
-
export interface NotFoundPageData {
|
|
5
|
-
routes: IRoute[];
|
|
6
|
-
footerLinks: IFooterLink[];
|
|
7
|
-
staticDetails: StaticDetails;
|
|
8
|
-
apiUrl: string;
|
|
9
|
-
baseUrl: string;
|
|
10
|
-
pathname: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export class NotFoundPageGenerator extends BasePageGenerator {
|
|
14
|
-
public generate(data: NotFoundPageData): PageContent {
|
|
15
|
-
const { staticDetails, ...baseData } = data;
|
|
16
|
-
|
|
17
|
-
const mainContent = `
|
|
18
|
-
<main class="container container-narrow text-center">
|
|
19
|
-
<h1>Page Not Found</h1>
|
|
20
|
-
<p>The page you're looking for doesn't exist.</p>
|
|
21
|
-
<p><a href="/">Return to home</a></p>
|
|
22
|
-
</main>`;
|
|
23
|
-
|
|
24
|
-
return this.generatePage(
|
|
25
|
-
baseData.pathname,
|
|
26
|
-
baseData.routes,
|
|
27
|
-
baseData.footerLinks,
|
|
28
|
-
staticDetails,
|
|
29
|
-
baseData.apiUrl,
|
|
30
|
-
baseData.baseUrl,
|
|
31
|
-
mainContent,
|
|
32
|
-
"404 Not Found",
|
|
33
|
-
"The page you requested could not be found."
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { BasePageGenerator, StaticDetails } from './base';
|
|
2
|
-
import { IRoute, IFooterLink, PageContent } from '../page-content';
|
|
3
|
-
import { BlogMeta } from '../data-fetcher';
|
|
4
|
-
|
|
5
|
-
export interface StoriesListPageData {
|
|
6
|
-
routes: IRoute[];
|
|
7
|
-
footerLinks: IFooterLink[];
|
|
8
|
-
staticDetails: StaticDetails;
|
|
9
|
-
apiUrl: string;
|
|
10
|
-
baseUrl: string;
|
|
11
|
-
pathname: string;
|
|
12
|
-
latestStories: BlogMeta[];
|
|
13
|
-
name: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export class StoriesListPageGenerator extends BasePageGenerator {
|
|
17
|
-
public generate(data: StoriesListPageData): PageContent {
|
|
18
|
-
const { latestStories, name, staticDetails, ...baseData } = data;
|
|
19
|
-
|
|
20
|
-
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("");
|
|
21
|
-
|
|
22
|
-
const mainContent = `
|
|
23
|
-
<main class="container container-wide">
|
|
24
|
-
<h1>Stories</h1>
|
|
25
|
-
<input type="text" placeholder="Search stories..." class="search-input" />
|
|
26
|
-
<div class="story-list">
|
|
27
|
-
${storyGists || "<p>No stories yet.</p>"}
|
|
28
|
-
</div>
|
|
29
|
-
</main>`;
|
|
30
|
-
|
|
31
|
-
return this.generatePage(
|
|
32
|
-
baseData.pathname,
|
|
33
|
-
baseData.routes,
|
|
34
|
-
baseData.footerLinks,
|
|
35
|
-
staticDetails,
|
|
36
|
-
baseData.apiUrl,
|
|
37
|
-
baseData.baseUrl,
|
|
38
|
-
mainContent,
|
|
39
|
-
`Stories – ${name}`,
|
|
40
|
-
"Read the latest stories."
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { BasePageGenerator, StaticDetails } from './base';
|
|
2
|
-
import { IRoute, IFooterLink, PageContent } from '../page-content';
|
|
3
|
-
|
|
4
|
-
export interface StoryDetailPageData {
|
|
5
|
-
routes: IRoute[];
|
|
6
|
-
footerLinks: IFooterLink[];
|
|
7
|
-
staticDetails: StaticDetails;
|
|
8
|
-
apiUrl: string;
|
|
9
|
-
baseUrl: string;
|
|
10
|
-
pathname: string;
|
|
11
|
-
slug: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export class StoryDetailPageGenerator extends BasePageGenerator {
|
|
15
|
-
public generate(data: StoryDetailPageData): PageContent {
|
|
16
|
-
const { slug, staticDetails, ...baseData } = data;
|
|
17
|
-
|
|
18
|
-
const mainContent = `
|
|
19
|
-
<main class="container container-narrow">
|
|
20
|
-
<my-story-viewer slug="${slug}"></my-story-viewer>
|
|
21
|
-
</main>`;
|
|
22
|
-
|
|
23
|
-
return this.generatePage(
|
|
24
|
-
baseData.pathname,
|
|
25
|
-
baseData.routes,
|
|
26
|
-
baseData.footerLinks,
|
|
27
|
-
staticDetails,
|
|
28
|
-
baseData.apiUrl,
|
|
29
|
-
baseData.baseUrl,
|
|
30
|
-
mainContent,
|
|
31
|
-
`Story: ${slug}`,
|
|
32
|
-
"Story post"
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
// Minimal prerender AST->HTML utility
|
|
2
|
-
export function renderFromAst(ast: any): string {
|
|
3
|
-
if (!ast) return '';
|
|
4
|
-
if (ast.type === 'document' && Array.isArray(ast.children)) {
|
|
5
|
-
return ast.children.map(renderFromAst).join('\n');
|
|
6
|
-
}
|
|
7
|
-
if (ast.type === 'heading') {
|
|
8
|
-
const level = Math.max(1, Math.min(6, ast.depth || 1));
|
|
9
|
-
const inner = Array.isArray(ast.children) ? ast.children.map(renderFromAst).join('') : '';
|
|
10
|
-
return `<h${level}>${inner}</h${level}>`;
|
|
11
|
-
}
|
|
12
|
-
if (ast.type === 'paragraph') {
|
|
13
|
-
const inner = Array.isArray(ast.children) ? ast.children.map(renderFromAst).join('') : '';
|
|
14
|
-
return `<p>${inner}</p>`;
|
|
15
|
-
}
|
|
16
|
-
if (ast.type === 'text') {
|
|
17
|
-
return ast.value || '';
|
|
18
|
-
}
|
|
19
|
-
if (ast.children) {
|
|
20
|
-
return ast.children.map(renderFromAst).join('');
|
|
21
|
-
}
|
|
22
|
-
return '';
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export default renderFromAst
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
export interface TemplateProps {
|
|
2
|
-
title: string;
|
|
3
|
-
description: string;
|
|
4
|
-
canonicalUrl: string;
|
|
5
|
-
content: string;
|
|
6
|
-
hydrationData?: string;
|
|
7
|
-
baseSiteUrl?: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
async function getAssetPaths(baseSiteUrl: string): Promise<{ js: string; css: string }> {
|
|
11
|
-
const assetsUrl = `${baseSiteUrl}/cdn-assets.json`;
|
|
12
|
-
try {
|
|
13
|
-
const res = await fetch(assetsUrl);
|
|
14
|
-
if (res.ok) {
|
|
15
|
-
const data = await res.json();
|
|
16
|
-
return { js: data.js, css: data.css };
|
|
17
|
-
}
|
|
18
|
-
} catch (e) {}
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
const res = await fetch(`${baseSiteUrl}/?t=${Date.now()}`);
|
|
22
|
-
const html = await res.text();
|
|
23
|
-
const jsMatch = html.match(/src="(\/assets\/index-[^"]+\.js)"/);
|
|
24
|
-
const cssMatch = html.match(/href="(\/assets\/index-[^"]+\.css)"/);
|
|
25
|
-
return {
|
|
26
|
-
js: jsMatch ? jsMatch[1] : "/assets/index.js",
|
|
27
|
-
css: cssMatch ? cssMatch[1] : "/assets/index.css"
|
|
28
|
-
};
|
|
29
|
-
} catch (e) {}
|
|
30
|
-
return { js: "/assets/index.js", css: "/assets/index.css" };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export const createHtmlTemplate = async ({
|
|
34
|
-
title,
|
|
35
|
-
description,
|
|
36
|
-
canonicalUrl,
|
|
37
|
-
content,
|
|
38
|
-
hydrationData = "",
|
|
39
|
-
baseSiteUrl = ""
|
|
40
|
-
}: TemplateProps): Promise<string> => {
|
|
41
|
-
const { js: jsAsset, css: cssAsset } = await getAssetPaths(baseSiteUrl);
|
|
42
|
-
|
|
43
|
-
return `<!doctype html>
|
|
44
|
-
<html lang="en" data-theme="light">
|
|
45
|
-
<head>
|
|
46
|
-
<meta charset="UTF-8" />
|
|
47
|
-
<link rel="icon" type="image/svg+xml" href="/api/logo" />
|
|
48
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
49
|
-
<title>${title}</title>
|
|
50
|
-
<meta name="description" content="${description}" />
|
|
51
|
-
<meta property="og:title" content="${title}" />
|
|
52
|
-
<meta property="og:description" content="${description}" />
|
|
53
|
-
<meta property="og:url" content="${canonicalUrl}" />
|
|
54
|
-
<link rel="canonical" href="${canonicalUrl}" />
|
|
55
|
-
<link rel="stylesheet" crossorigin href="${cssAsset}" />
|
|
56
|
-
<script type="module" crossorigin src="${jsAsset}"></script>
|
|
57
|
-
</head>
|
|
58
|
-
<body>
|
|
59
|
-
${hydrationData}
|
|
60
|
-
<div id="app">
|
|
61
|
-
${content}
|
|
62
|
-
</div>
|
|
63
|
-
</body>
|
|
64
|
-
</html>`;
|
|
65
|
-
};
|
|
@@ -1,152 +0,0 @@
|
|
|
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/config/api.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
// API Configuration
|
|
2
|
-
export const API_CONFIG = {
|
|
3
|
-
// Base URL for API calls
|
|
4
|
-
BASE_URL: (typeof window !== 'undefined' && (window as any).__VITE_API_URL__) ||
|
|
5
|
-
(typeof window !== 'undefined' ? `${window.location.protocol}//${window.location.host}` : 'http://localhost:8787'),
|
|
6
|
-
|
|
7
|
-
// API endpoints
|
|
8
|
-
ENDPOINTS: {
|
|
9
|
-
ABOUTME: '/aboutme'
|
|
10
|
-
},
|
|
11
|
-
|
|
12
|
-
// Build full URL for an endpoint
|
|
13
|
-
getUrl(endpoint: keyof typeof API_CONFIG.ENDPOINTS) {
|
|
14
|
-
return `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS[endpoint]}`;
|
|
15
|
-
}
|
|
16
|
-
};
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { InfrastructureConfig, StaticDetails, WebsiteConfig } from './types';
|
|
2
|
-
|
|
3
|
-
export * from './types';
|
|
4
|
-
|
|
5
|
-
const DEFAULT_INFRA: InfrastructureConfig = {
|
|
6
|
-
baseUrl: typeof window !== 'undefined' ? window.location.origin : 'http://localhost:5173',
|
|
7
|
-
apiUrl: (typeof window !== 'undefined' && ((window as any).__VITE_API_URL__ || (import.meta as any).env?.VITE_API_URL)) || 'http://localhost:8788'
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
const DEFAULT_STATIC: StaticDetails = {
|
|
11
|
-
siteTitle: 'My Personal Website',
|
|
12
|
-
siteDescription: 'My Personal Website',
|
|
13
|
-
copyright: '2026 My Personal Website',
|
|
14
|
-
linkedin: 'https://linkedin.com/in/yourname',
|
|
15
|
-
github: 'https://github.com/yourname',
|
|
16
|
-
email: 'yourname@domain.com'
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
let activeConfig: WebsiteConfig = { ...DEFAULT_INFRA, ...DEFAULT_STATIC };
|
|
20
|
-
|
|
21
|
-
export async function initializeConfig(infra?: Partial<InfrastructureConfig>): Promise<WebsiteConfig> {
|
|
22
|
-
if (infra) {
|
|
23
|
-
// Only merge defined values
|
|
24
|
-
if (infra.baseUrl) activeConfig.baseUrl = infra.baseUrl;
|
|
25
|
-
if (infra.apiUrl) activeConfig.apiUrl = infra.apiUrl;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
const res = await fetch(`${activeConfig.apiUrl}/api/static`);
|
|
30
|
-
if (res.ok) {
|
|
31
|
-
const remoteStatic = await res.json().catch(() => ({}));
|
|
32
|
-
activeConfig = { ...activeConfig, ...remoteStatic };
|
|
33
|
-
}
|
|
34
|
-
} catch (e) {
|
|
35
|
-
console.warn('Failed to load static details from R2, using defaults.');
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return activeConfig;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function getConfig(): WebsiteConfig {
|
|
42
|
-
return activeConfig;
|
|
43
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export interface InfrastructureConfig {
|
|
2
|
-
baseUrl: string;
|
|
3
|
-
apiUrl: string;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export interface StaticDetails {
|
|
7
|
-
siteTitle: string;
|
|
8
|
-
siteDescription: string;
|
|
9
|
-
copyright: string;
|
|
10
|
-
linkedin: string;
|
|
11
|
-
github: string;
|
|
12
|
-
email: string;
|
|
13
|
-
twitter?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface WebsiteConfig extends InfrastructureConfig, StaticDetails {}
|