@leadertechie/personal-site-kit 0.1.0-alpha.6 → 0.1.0-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/handlers/about-me.d.ts.map +1 -1
- package/dist/api/handlers/auth-handler.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-C3wLSCKU.js → index-CYd_Pe2U.js} +1353 -482
- package/dist/chunks/{template-MawmknFQ.js → template-D1uGvdWZ.js} +10 -8
- package/dist/chunks/{website-api-DI3muo2s.js → website-api-FLejlWxJ.js} +78 -41
- 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/config/index.d.ts.map +1 -1
- package/dist/shared.js +1 -1
- package/dist/ui/about-me/index.d.ts +2 -10
- package/dist/ui/about-me/index.d.ts.map +1 -1
- package/dist/ui/admin/api.d.ts +16 -0
- package/dist/ui/admin/api.d.ts.map +1 -0
- package/dist/ui/admin/components/AboutMeSection.d.ts +7 -0
- package/dist/ui/admin/components/AboutMeSection.d.ts.map +1 -0
- package/dist/ui/admin/components/AdminSection.d.ts +13 -0
- package/dist/ui/admin/components/AdminSection.d.ts.map +1 -0
- package/dist/ui/admin/components/BlogsSection.d.ts +7 -0
- package/dist/ui/admin/components/BlogsSection.d.ts.map +1 -0
- package/dist/ui/admin/components/HomeSection.d.ts +7 -0
- package/dist/ui/admin/components/HomeSection.d.ts.map +1 -0
- package/dist/ui/admin/components/ImagesSection.d.ts +7 -0
- package/dist/ui/admin/components/ImagesSection.d.ts.map +1 -0
- package/dist/ui/admin/components/LoginForm.d.ts +9 -0
- package/dist/ui/admin/components/LoginForm.d.ts.map +1 -0
- package/dist/ui/admin/components/LogoSection.d.ts +7 -0
- package/dist/ui/admin/components/LogoSection.d.ts.map +1 -0
- package/dist/ui/admin/components/ProfileSection.d.ts +7 -0
- package/dist/ui/admin/components/ProfileSection.d.ts.map +1 -0
- package/dist/ui/admin/components/StaticSection.d.ts +13 -0
- package/dist/ui/admin/components/StaticSection.d.ts.map +1 -0
- package/dist/ui/admin/components/StoriesSection.d.ts +7 -0
- package/dist/ui/admin/components/StoriesSection.d.ts.map +1 -0
- package/dist/ui/admin/components/index.d.ts +11 -0
- package/dist/ui/admin/components/index.d.ts.map +1 -0
- package/dist/ui/admin/index.d.ts +10 -26
- package/dist/ui/admin/index.d.ts.map +1 -1
- package/dist/ui/admin/types.d.ts +24 -0
- package/dist/ui/admin/types.d.ts.map +1 -0
- package/dist/ui/blog-viewer/index.d.ts.map +1 -1
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/story-viewer/index.d.ts.map +1 -1
- package/dist/ui.js +14 -4
- package/package.json +1 -1
- package/src/api/handlers/about-me.ts +19 -9
- package/src/api/handlers/auth-handler.ts +41 -18
- package/src/api/handlers/content.ts +1 -1
- package/src/api/handlers/home.ts +2 -2
- package/src/api/website-api.ts +25 -13
- package/src/prerender/__tests__/page-content.test.ts +1 -11
- package/src/prerender/data-fetcher.ts +93 -0
- package/src/prerender/page-content.ts +109 -106
- package/src/prerender/page-generators/about.ts +38 -0
- package/src/prerender/page-generators/base.ts +77 -0
- package/src/prerender/page-generators/blog-detail.ts +35 -0
- package/src/prerender/page-generators/blogs-list.ts +43 -0
- package/src/prerender/page-generators/home.ts +54 -0
- package/src/prerender/page-generators/index.ts +8 -0
- package/src/prerender/page-generators/not-found.ts +36 -0
- package/src/prerender/page-generators/stories-list.ts +43 -0
- package/src/prerender/page-generators/story-detail.ts +35 -0
- package/src/shared/config/index.ts +4 -2
- package/src/shared/page-content.ts +1 -1
- package/src/shared/router.ts +5 -5
- package/src/ui/about-me/index.ts +23 -57
- package/src/ui/admin/api.ts +93 -0
- package/src/ui/admin/components/AboutMeSection.ts +47 -0
- package/src/ui/admin/components/AdminSection.ts +134 -0
- package/src/ui/admin/components/BlogsSection.ts +62 -0
- package/src/ui/admin/components/HomeSection.ts +47 -0
- package/src/ui/admin/components/ImagesSection.ts +54 -0
- package/src/ui/admin/components/LoginForm.ts +116 -0
- package/src/ui/admin/components/LogoSection.ts +51 -0
- package/src/ui/admin/components/ProfileSection.ts +47 -0
- package/src/ui/admin/components/StaticSection.ts +67 -0
- package/src/ui/admin/components/StoriesSection.ts +62 -0
- package/src/ui/admin/components/index.ts +10 -0
- package/src/ui/admin/index.ts +192 -434
- package/src/ui/admin/types.ts +26 -0
- package/src/ui/blog-viewer/index.ts +4 -1
- package/src/ui/index.ts +7 -0
- package/src/ui/story-viewer/index.ts +4 -1
- package/dist/ui/about-me/renderer.d.ts +0 -5
- package/dist/ui/about-me/renderer.d.ts.map +0 -1
- package/src/ui/about-me/renderer.ts +0 -7
|
@@ -68,7 +68,7 @@ async function fetchHome(env: any): Promise<string> {
|
|
|
68
68
|
try {
|
|
69
69
|
const r2 = getLoader(env);
|
|
70
70
|
if (!r2) return "";
|
|
71
|
-
const result = await r2.getRendered("home.md");
|
|
71
|
+
const result = await r2.getRendered("pages/home.md");
|
|
72
72
|
return result?.content || "";
|
|
73
73
|
} catch { return ""; }
|
|
74
74
|
}
|
|
@@ -115,8 +115,8 @@ export const generatePageContent = async (
|
|
|
115
115
|
footerLinks: IFooterLink[],
|
|
116
116
|
env?: any
|
|
117
117
|
): Promise<PageContent> => {
|
|
118
|
-
const apiUrl = env?.apiUrl || "https://api.
|
|
119
|
-
const baseUrl = env?.baseUrl || "https://www.
|
|
118
|
+
const apiUrl = env?.API_URL || env?.apiUrl || "https://api.example.com";
|
|
119
|
+
const baseUrl = env?.BASE_URL || env?.baseUrl || "https://www.example.com";
|
|
120
120
|
|
|
121
121
|
let staticDetails = {
|
|
122
122
|
siteTitle: "My Personal Website",
|
|
@@ -131,23 +131,6 @@ export const generatePageContent = async (
|
|
|
131
131
|
if (res.ok) staticDetails = await res.json();
|
|
132
132
|
} catch (e) {}
|
|
133
133
|
|
|
134
|
-
const logo = "/api/logo";
|
|
135
|
-
const navLinks = routes.map(r => `<a href="${r.link}" class="nav-link" data-route="${r.link === "/" ? "home" : r.text.toLowerCase()}">${r.text}</a>`).join("");
|
|
136
|
-
|
|
137
|
-
const bannerTemplate = `
|
|
138
|
-
<my-banner header="${staticDetails.siteTitle}" logo="${logo}">
|
|
139
|
-
<theme-toggle slot="theme-switcher"></theme-toggle>
|
|
140
|
-
<nav slot="nav-links">
|
|
141
|
-
${navLinks}
|
|
142
|
-
</nav>
|
|
143
|
-
</my-banner>`;
|
|
144
|
-
|
|
145
|
-
const footerTemplate = `
|
|
146
|
-
<my-footer
|
|
147
|
-
copyright="${staticDetails.copyright}"
|
|
148
|
-
footerlinks='${JSON.stringify(footerLinks)}'>
|
|
149
|
-
</my-footer>`;
|
|
150
|
-
|
|
151
134
|
let profile: Profile | null = null;
|
|
152
135
|
let aboutMeContent = "";
|
|
153
136
|
let homeContent = "";
|
|
@@ -162,102 +145,122 @@ export const generatePageContent = async (
|
|
|
162
145
|
|
|
163
146
|
const name = profile?.name || "User";
|
|
164
147
|
const title = profile?.title || "Professional";
|
|
165
|
-
const experience = profile?.experience || "some";
|
|
166
148
|
const canonicalUrl = new URL(pathname, baseUrl).toString();
|
|
167
149
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
150
|
+
// Strategy pattern: map pathname patterns to generators
|
|
151
|
+
const strategies = {
|
|
152
|
+
home: async () => {
|
|
153
|
+
const { HomePageGenerator } = await import('./page-generators');
|
|
154
|
+
const generator = new HomePageGenerator();
|
|
155
|
+
return generator.generate({
|
|
156
|
+
routes,
|
|
157
|
+
footerLinks,
|
|
158
|
+
staticDetails,
|
|
159
|
+
apiUrl,
|
|
160
|
+
baseUrl,
|
|
161
|
+
pathname,
|
|
162
|
+
profile,
|
|
163
|
+
homeContent,
|
|
164
|
+
latestBlogs,
|
|
165
|
+
latestStories
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
about: async () => {
|
|
169
|
+
const { AboutPageGenerator } = await import('./page-generators');
|
|
170
|
+
const generator = new AboutPageGenerator();
|
|
171
|
+
return generator.generate({
|
|
172
|
+
routes,
|
|
173
|
+
footerLinks,
|
|
174
|
+
staticDetails,
|
|
175
|
+
apiUrl,
|
|
176
|
+
baseUrl,
|
|
177
|
+
pathname,
|
|
178
|
+
profile
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
blogsList: async () => {
|
|
182
|
+
const { BlogsListPageGenerator } = await import('./page-generators');
|
|
183
|
+
const generator = new BlogsListPageGenerator();
|
|
184
|
+
return generator.generate({
|
|
185
|
+
routes,
|
|
186
|
+
footerLinks,
|
|
187
|
+
staticDetails,
|
|
188
|
+
apiUrl,
|
|
189
|
+
baseUrl,
|
|
190
|
+
pathname,
|
|
191
|
+
latestBlogs,
|
|
192
|
+
name
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
storiesList: async () => {
|
|
196
|
+
const { StoriesListPageGenerator } = await import('./page-generators');
|
|
197
|
+
const generator = new StoriesListPageGenerator();
|
|
198
|
+
return generator.generate({
|
|
199
|
+
routes,
|
|
200
|
+
footerLinks,
|
|
201
|
+
staticDetails,
|
|
202
|
+
apiUrl,
|
|
203
|
+
baseUrl,
|
|
204
|
+
pathname,
|
|
205
|
+
latestStories,
|
|
206
|
+
name
|
|
207
|
+
});
|
|
208
|
+
},
|
|
209
|
+
blogDetail: async (slug: string) => {
|
|
210
|
+
const { BlogDetailPageGenerator } = await import('./page-generators');
|
|
211
|
+
const generator = new BlogDetailPageGenerator();
|
|
212
|
+
return generator.generate({
|
|
213
|
+
routes,
|
|
214
|
+
footerLinks,
|
|
215
|
+
staticDetails,
|
|
216
|
+
apiUrl,
|
|
217
|
+
baseUrl,
|
|
218
|
+
pathname,
|
|
219
|
+
slug
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
storyDetail: async (slug: string) => {
|
|
223
|
+
const { StoryDetailPageGenerator } = await import('./page-generators');
|
|
224
|
+
const generator = new StoryDetailPageGenerator();
|
|
225
|
+
return generator.generate({
|
|
226
|
+
routes,
|
|
227
|
+
footerLinks,
|
|
228
|
+
staticDetails,
|
|
229
|
+
apiUrl,
|
|
230
|
+
baseUrl,
|
|
231
|
+
pathname,
|
|
232
|
+
slug
|
|
233
|
+
});
|
|
234
|
+
},
|
|
235
|
+
notFound: async () => {
|
|
236
|
+
const { NotFoundPageGenerator } = await import('./page-generators');
|
|
237
|
+
const generator = new NotFoundPageGenerator();
|
|
238
|
+
return generator.generate({
|
|
239
|
+
routes,
|
|
240
|
+
footerLinks,
|
|
241
|
+
staticDetails,
|
|
242
|
+
apiUrl,
|
|
243
|
+
baseUrl,
|
|
244
|
+
pathname
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
};
|
|
187
248
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
description: `Welcome to ${name}'s personal website. Professional portfolio and content.`,
|
|
191
|
-
canonicalUrl,
|
|
192
|
-
content: mainContent
|
|
193
|
-
};
|
|
249
|
+
if (pathname === "/" || pathname === "") {
|
|
250
|
+
return strategies.home();
|
|
194
251
|
} else if (pathname === "/about-me") {
|
|
195
|
-
|
|
196
|
-
${bannerTemplate}
|
|
197
|
-
<main class="container container-narrow">
|
|
198
|
-
<my-aboutme base-url="${apiUrl}"></my-aboutme>
|
|
199
|
-
</main>
|
|
200
|
-
${footerTemplate}`;
|
|
201
|
-
|
|
202
|
-
return {
|
|
203
|
-
title: `About - ${name}`,
|
|
204
|
-
description: `Learn more about ${name}'s experience and skills.`,
|
|
205
|
-
canonicalUrl,
|
|
206
|
-
content: mainContent
|
|
207
|
-
};
|
|
252
|
+
return strategies.about();
|
|
208
253
|
} else if (pathname === "/blogs" || pathname === "/blogs/") {
|
|
209
|
-
|
|
210
|
-
const mainContent = `
|
|
211
|
-
${bannerTemplate}
|
|
212
|
-
<main class="container container-wide">
|
|
213
|
-
<h1>Blogs</h1>
|
|
214
|
-
<input type="text" placeholder="Search blogs..." class="search-input" />
|
|
215
|
-
<div class="blog-list">
|
|
216
|
-
${blogGists || "<p>No blogs yet.</p>"}
|
|
217
|
-
</div>
|
|
218
|
-
</main>
|
|
219
|
-
${footerTemplate}`;
|
|
220
|
-
return { title: `Blogs – ${name}`, description: "Read the latest blog posts.", canonicalUrl, content: mainContent };
|
|
254
|
+
return strategies.blogsList();
|
|
221
255
|
} else if (pathname === "/stories" || pathname === "/stories/") {
|
|
222
|
-
|
|
223
|
-
const mainContent = `
|
|
224
|
-
${bannerTemplate}
|
|
225
|
-
<main class="container container-wide">
|
|
226
|
-
<h1>Stories</h1>
|
|
227
|
-
<input type="text" placeholder="Search stories..." class="search-input" />
|
|
228
|
-
<div class="story-list">
|
|
229
|
-
${storyGists || "<p>No stories yet.</p>"}
|
|
230
|
-
</div>
|
|
231
|
-
</main>
|
|
232
|
-
${footerTemplate}`;
|
|
233
|
-
return { title: `Stories – ${name}`, description: "Read the latest stories.", canonicalUrl, content: mainContent };
|
|
256
|
+
return strategies.storiesList();
|
|
234
257
|
} else if (pathname.startsWith("/blogs/")) {
|
|
235
258
|
const slug = pathname.replace("/blogs/", "").replace("/", "");
|
|
236
|
-
|
|
237
|
-
${bannerTemplate}
|
|
238
|
-
<main class="container container-narrow">
|
|
239
|
-
<my-blog-viewer slug="${slug}"></my-blog-viewer>
|
|
240
|
-
</main>
|
|
241
|
-
${footerTemplate}`;
|
|
242
|
-
return { title: `Blog: ${slug}`, description: "Blog post", canonicalUrl, content: mainContent };
|
|
259
|
+
return strategies.blogDetail(slug);
|
|
243
260
|
} else if (pathname.startsWith("/stories/")) {
|
|
244
261
|
const slug = pathname.replace("/stories/", "").replace("/", "");
|
|
245
|
-
|
|
246
|
-
${bannerTemplate}
|
|
247
|
-
<main class="container container-narrow">
|
|
248
|
-
<my-story-viewer slug="${slug}"></my-story-viewer>
|
|
249
|
-
</main>
|
|
250
|
-
${footerTemplate}`;
|
|
251
|
-
return { title: `Story: ${slug}`, description: "Story post", canonicalUrl, content: mainContent };
|
|
262
|
+
return strategies.storyDetail(slug);
|
|
252
263
|
} else {
|
|
253
|
-
|
|
254
|
-
${bannerTemplate}
|
|
255
|
-
<main class="container container-narrow text-center">
|
|
256
|
-
<h1>Page Not Found</h1>
|
|
257
|
-
<p>The page you're looking for doesn't exist.</p>
|
|
258
|
-
<p><a href="/">Return to home</a></p>
|
|
259
|
-
</main>
|
|
260
|
-
${footerTemplate}`;
|
|
261
|
-
return { title: "404 Not Found", description: "The page you requested could not be found.", canonicalUrl, content: mainContent };
|
|
264
|
+
return strategies.notFound();
|
|
262
265
|
}
|
|
263
266
|
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { BasePageGenerator, StaticDetails } from './base';
|
|
2
|
+
import { IRoute, IFooterLink, PageContent } from '../page-content';
|
|
3
|
+
import { Profile } from '../data-fetcher';
|
|
4
|
+
|
|
5
|
+
export interface AboutPageData {
|
|
6
|
+
routes: IRoute[];
|
|
7
|
+
footerLinks: IFooterLink[];
|
|
8
|
+
staticDetails: StaticDetails;
|
|
9
|
+
apiUrl: string;
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
pathname: string;
|
|
12
|
+
profile: Profile | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class AboutPageGenerator extends BasePageGenerator {
|
|
16
|
+
public generate(data: AboutPageData): PageContent {
|
|
17
|
+
const { profile, staticDetails, ...baseData } = data;
|
|
18
|
+
|
|
19
|
+
const name = profile?.name || "User";
|
|
20
|
+
|
|
21
|
+
const mainContent = `
|
|
22
|
+
<main class="container container-narrow">
|
|
23
|
+
<my-aboutme base-url="${baseData.apiUrl}"></my-aboutme>
|
|
24
|
+
</main>`;
|
|
25
|
+
|
|
26
|
+
return this.generatePage(
|
|
27
|
+
baseData.pathname,
|
|
28
|
+
baseData.routes,
|
|
29
|
+
baseData.footerLinks,
|
|
30
|
+
staticDetails,
|
|
31
|
+
baseData.apiUrl,
|
|
32
|
+
baseData.baseUrl,
|
|
33
|
+
mainContent,
|
|
34
|
+
`About - ${name}`,
|
|
35
|
+
`Learn more about ${name}'s experience and skills.`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
}
|
|
@@ -4,7 +4,7 @@ export * from './types';
|
|
|
4
4
|
|
|
5
5
|
const DEFAULT_INFRA: InfrastructureConfig = {
|
|
6
6
|
baseUrl: typeof window !== 'undefined' ? window.location.origin : 'http://localhost:5173',
|
|
7
|
-
apiUrl: (typeof window !== 'undefined' && (window as any).__VITE_API_URL__) || 'http://localhost:
|
|
7
|
+
apiUrl: (typeof window !== 'undefined' && ((window as any).__VITE_API_URL__ || (import.meta as any).env?.VITE_API_URL)) || 'http://localhost:8788'
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
const DEFAULT_STATIC: StaticDetails = {
|
|
@@ -20,7 +20,9 @@ let activeConfig: WebsiteConfig = { ...DEFAULT_INFRA, ...DEFAULT_STATIC };
|
|
|
20
20
|
|
|
21
21
|
export async function initializeConfig(infra?: Partial<InfrastructureConfig>): Promise<WebsiteConfig> {
|
|
22
22
|
if (infra) {
|
|
23
|
-
|
|
23
|
+
// Only merge defined values
|
|
24
|
+
if (infra.baseUrl) activeConfig.baseUrl = infra.baseUrl;
|
|
25
|
+
if (infra.apiUrl) activeConfig.apiUrl = infra.apiUrl;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
try {
|
|
@@ -79,7 +79,7 @@ export const generatePageContent = (
|
|
|
79
79
|
const footerTemplate = `
|
|
80
80
|
<my-footer
|
|
81
81
|
copyright="${copyright}"
|
|
82
|
-
|
|
82
|
+
footerLinks='${JSON.stringify(footerLinks)}'>
|
|
83
83
|
</my-footer>`;
|
|
84
84
|
|
|
85
85
|
const renderContentGists = (items: ContentMetadata[] = [], title: string, type: 'blogs' | 'stories') => {
|