@leadertechie/personal-site-kit 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/dist/api/handlers/aboutme.d.ts +3 -0
  4. package/dist/api/handlers/aboutme.d.ts.map +1 -0
  5. package/dist/api/handlers/content-api.d.ts +5 -0
  6. package/dist/api/handlers/content-api.d.ts.map +1 -0
  7. package/dist/api/handlers/content.d.ts +2 -0
  8. package/dist/api/handlers/content.d.ts.map +1 -0
  9. package/dist/api/handlers/home.d.ts +3 -0
  10. package/dist/api/handlers/home.d.ts.map +1 -0
  11. package/dist/api/handlers/info.d.ts +2 -0
  12. package/dist/api/handlers/info.d.ts.map +1 -0
  13. package/dist/api/handlers/logo.d.ts +2 -0
  14. package/dist/api/handlers/logo.d.ts.map +1 -0
  15. package/dist/api/handlers/staticdetails.d.ts +2 -0
  16. package/dist/api/handlers/staticdetails.d.ts.map +1 -0
  17. package/dist/api/index.d.ts +9 -0
  18. package/dist/api/index.d.ts.map +1 -0
  19. package/dist/api/utils.d.ts +8 -0
  20. package/dist/api/utils.d.ts.map +1 -0
  21. package/dist/api.d.ts +4 -0
  22. package/dist/api.js +591 -0
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +354 -0
  25. package/dist/prerender/index.d.ts +5 -0
  26. package/dist/prerender/index.d.ts.map +1 -0
  27. package/dist/prerender/pageContent.d.ts +16 -0
  28. package/dist/prerender/pageContent.d.ts.map +1 -0
  29. package/dist/prerender/prerender.d.ts +3 -0
  30. package/dist/prerender/prerender.d.ts.map +1 -0
  31. package/dist/prerender/template.d.ts +10 -0
  32. package/dist/prerender/template.d.ts.map +1 -0
  33. package/dist/prerender.d.ts +4 -0
  34. package/dist/prerender.js +399 -0
  35. package/dist/shared/config/api.d.ts +2 -0
  36. package/dist/shared/config/api.d.ts.map +1 -0
  37. package/dist/shared/config/index.d.ts +5 -0
  38. package/dist/shared/config/index.d.ts.map +1 -0
  39. package/dist/shared/config/types.d.ts +16 -0
  40. package/dist/shared/config/types.d.ts.map +1 -0
  41. package/dist/shared/core/site-store.d.ts +16 -0
  42. package/dist/shared/core/site-store.d.ts.map +1 -0
  43. package/dist/shared/core/theme-toggle.d.ts +13 -0
  44. package/dist/shared/core/theme-toggle.d.ts.map +1 -0
  45. package/dist/shared/index.d.ts +9 -0
  46. package/dist/shared/index.d.ts.map +1 -0
  47. package/dist/shared/interfaces/iFooterLink.d.ts +5 -0
  48. package/dist/shared/interfaces/iFooterLink.d.ts.map +1 -0
  49. package/dist/shared/interfaces/iRoute.d.ts +5 -0
  50. package/dist/shared/interfaces/iRoute.d.ts.map +1 -0
  51. package/dist/shared/pageContent.d.ts +35 -0
  52. package/dist/shared/pageContent.d.ts.map +1 -0
  53. package/dist/shared/runtime.d.ts +7 -0
  54. package/dist/shared/runtime.d.ts.map +1 -0
  55. package/dist/shared/template.d.ts +8 -0
  56. package/dist/shared/template.d.ts.map +1 -0
  57. package/dist/shared.d.ts +2 -0
  58. package/dist/shared.js +10 -0
  59. package/dist/ui/aboutme/api.d.ts +11 -0
  60. package/dist/ui/aboutme/api.d.ts.map +1 -0
  61. package/dist/ui/aboutme/index.d.ts +26 -0
  62. package/dist/ui/aboutme/index.d.ts.map +1 -0
  63. package/dist/ui/aboutme/renderer.d.ts +5 -0
  64. package/dist/ui/aboutme/renderer.d.ts.map +1 -0
  65. package/dist/ui/aboutme/styles.d.ts +2 -0
  66. package/dist/ui/aboutme/styles.d.ts.map +1 -0
  67. package/dist/ui/admin/index.d.ts +42 -0
  68. package/dist/ui/admin/index.d.ts.map +1 -0
  69. package/dist/ui/admin/styles.d.ts +2 -0
  70. package/dist/ui/admin/styles.d.ts.map +1 -0
  71. package/dist/ui/banner/index.d.ts +17 -0
  72. package/dist/ui/banner/index.d.ts.map +1 -0
  73. package/dist/ui/banner/styles.d.ts +2 -0
  74. package/dist/ui/banner/styles.d.ts.map +1 -0
  75. package/dist/ui/footer/index.d.ts +9 -0
  76. package/dist/ui/footer/index.d.ts.map +1 -0
  77. package/dist/ui/footer/styles.d.ts +2 -0
  78. package/dist/ui/footer/styles.d.ts.map +1 -0
  79. package/dist/ui.d.ts +2 -0
  80. package/dist/ui.js +820 -0
  81. package/package.json +41 -0
  82. package/src/api/__tests__/info.test.ts +44 -0
  83. package/src/api/__tests__/utils.test.ts +78 -0
  84. package/src/api/handlers/aboutme.ts +99 -0
  85. package/src/api/handlers/content-api.ts +268 -0
  86. package/src/api/handlers/content.ts +72 -0
  87. package/src/api/handlers/home.ts +79 -0
  88. package/src/api/handlers/info.ts +12 -0
  89. package/src/api/handlers/logo.ts +55 -0
  90. package/src/api/handlers/staticdetails.ts +48 -0
  91. package/src/api/index.ts +125 -0
  92. package/src/api/utils.ts +16 -0
  93. package/src/prerender/__tests__/pageContent.test.ts +54 -0
  94. package/src/prerender/__tests__/template.test.ts +54 -0
  95. package/src/prerender/index.ts +138 -0
  96. package/src/prerender/pageContent.ts +263 -0
  97. package/src/prerender/prerender.ts +25 -0
  98. package/src/prerender/template.ts +65 -0
  99. package/src/shared/config/api.ts +16 -0
  100. package/src/shared/config/index.ts +41 -0
  101. package/src/shared/config/types.ts +16 -0
  102. package/src/shared/core/__tests__/theme-toggle.test.ts +204 -0
  103. package/src/shared/core/site-store.ts +38 -0
  104. package/src/shared/core/theme-toggle.ts +118 -0
  105. package/src/shared/index.ts +15 -0
  106. package/src/shared/interfaces/iFooterLink.ts +4 -0
  107. package/src/shared/interfaces/iRoute.ts +4 -0
  108. package/src/shared/models/theme-variables.css +25 -0
  109. package/src/shared/pageContent.ts +209 -0
  110. package/src/shared/runtime.ts +11 -0
  111. package/src/shared/styles/markdown.css +129 -0
  112. package/src/shared/template.ts +35 -0
  113. package/src/styles/markdown.css +129 -0
  114. package/src/styles/theme.css +432 -0
  115. package/src/ui/aboutme/api.ts +12 -0
  116. package/src/ui/aboutme/index.ts +155 -0
  117. package/src/ui/aboutme/renderer.ts +7 -0
  118. package/src/ui/aboutme/styles.ts +10 -0
  119. package/src/ui/admin/index.ts +492 -0
  120. package/src/ui/admin/styles.ts +317 -0
  121. package/src/ui/banner/index.ts +38 -0
  122. package/src/ui/banner/styles.ts +10 -0
  123. package/src/ui/footer/index.ts +37 -0
  124. package/src/ui/footer/styles.ts +9 -0
@@ -0,0 +1,118 @@
1
+ export class ThemeToggle extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+ this.attachShadow({ mode: 'open' });
5
+ this.render();
6
+ this.applyThemeFromLocalStorage();
7
+ }
8
+
9
+ connectedCallback() {
10
+ this.shadowRoot?.querySelector('button')?.addEventListener('click', this.toggleTheme);
11
+ }
12
+
13
+ disconnectedCallback() {
14
+ this.shadowRoot?.querySelector('button')?.removeEventListener('click', this.toggleTheme);
15
+ }
16
+
17
+ render() {
18
+ if (this.shadowRoot) {
19
+ this.shadowRoot.innerHTML = `
20
+ <style>
21
+ :host {
22
+ display: inline-block;
23
+ }
24
+ button {
25
+ background: none;
26
+ border: none;
27
+ cursor: pointer;
28
+ font-size: 1.5rem;
29
+ padding: 0.5rem;
30
+ color: var(--text-color, #213547); /* Default for light mode */
31
+ transition: color 0.3s ease;
32
+ }
33
+ button:hover {
34
+ color: var(--primary-color, #747bff); /* Hover color */
35
+ }
36
+ /* Dark mode specific styles for the button */
37
+ html[data-theme='dark'] button {
38
+ color: var(--dark-mode-text-color, rgba(255, 255, 255, 0.87));
39
+ }
40
+ html[data-theme='dark'] button:hover {
41
+ color: var(--dark-mode-primary-color, #646cff);
42
+ }
43
+ </style>
44
+ <button id="theme-toggle" aria-label="Toggle theme">
45
+ <span class="icon">${this.getSunIcon()}</span> <!-- Default to sun for light mode -->
46
+ </button>
47
+ `;
48
+ }
49
+ }
50
+
51
+ applyThemeFromLocalStorage() {
52
+ const savedTheme = localStorage.getItem('theme');
53
+ if (savedTheme) {
54
+ document.documentElement.setAttribute('data-theme', savedTheme);
55
+ this.updateToggleButton(savedTheme);
56
+ } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
57
+ document.documentElement.setAttribute('data-theme', 'dark');
58
+ this.updateToggleButton('dark');
59
+ } else {
60
+ document.documentElement.setAttribute('data-theme', 'light');
61
+ this.updateToggleButton('light');
62
+ }
63
+ }
64
+
65
+ toggleTheme = () => {
66
+ const currentTheme = document.documentElement.getAttribute('data-theme');
67
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
68
+ document.documentElement.setAttribute('data-theme', newTheme);
69
+ localStorage.setItem('theme', newTheme);
70
+ this.updateToggleButton(newTheme);
71
+ this.dispatchThemeChangeEvent(newTheme);
72
+ };
73
+
74
+ updateToggleButton(theme: string) {
75
+ const button = this.shadowRoot?.querySelector('#theme-toggle');
76
+ if (button) {
77
+ const iconSpan = button.querySelector('.icon');
78
+ if (iconSpan) {
79
+ iconSpan.innerHTML = theme === 'dark' ? this.getMoonIcon() : this.getSunIcon();
80
+ }
81
+ }
82
+ }
83
+
84
+ getSunIcon(): string {
85
+ return `
86
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
87
+ <circle cx="12" cy="12" r="5"/>
88
+ <line x1="12" y1="1" x2="12" y2="3"/>
89
+ <line x1="12" y1="21" x2="12" y2="23"/>
90
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
91
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
92
+ <line x1="1" y1="12" x2="3" y2="12"/>
93
+ <line x1="21" y1="12" x2="23" y2="12"/>
94
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
95
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
96
+ </svg>
97
+ `;
98
+ }
99
+
100
+ getMoonIcon(): string {
101
+ return `
102
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
103
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
104
+ </svg>
105
+ `;
106
+ }
107
+
108
+ dispatchThemeChangeEvent(theme: string) {
109
+ const event = new CustomEvent('theme-changed', {
110
+ detail: { theme: theme },
111
+ bubbles: true,
112
+ composed: true,
113
+ });
114
+ this.dispatchEvent(event);
115
+ }
116
+ }
117
+
118
+ customElements.define('theme-toggle', ThemeToggle);
@@ -0,0 +1,15 @@
1
+ // Shared Config and Store
2
+ export * from './config';
3
+ export * from './core/site-store';
4
+
5
+ // Interfaces
6
+ export * from './interfaces/iRoute';
7
+ export * from './interfaces/iFooterLink';
8
+
9
+ // Core Logic
10
+ export * from './core/theme-toggle';
11
+ export * from './template';
12
+ export * from './pageContent';
13
+
14
+ // Runtime
15
+ export * from './runtime';
@@ -0,0 +1,4 @@
1
+ export interface IFooterLink {
2
+ text: string;
3
+ link: string;
4
+ }
@@ -0,0 +1,4 @@
1
+ export interface IRoute {
2
+ text: string;
3
+ link: string;
4
+ }
@@ -0,0 +1,25 @@
1
+ :root {
2
+ /* Light Theme Variables */
3
+ --text-color: #213547;
4
+ --background-color: #ffffff;
5
+ --link-color: #646cff;
6
+ --link-hover-color: #747bff;
7
+ --nav-link-color: #007bff;
8
+ --nav-link-hover-bg: #e2e6ea;
9
+ --footer-bg: #fff;
10
+ --footer-color: #222;
11
+ --footer-border: #eee;
12
+ }
13
+
14
+ /* Dark Theme Variables */
15
+ html[data-theme='dark'] {
16
+ --text-color: rgba(255, 255, 255, 0.87);
17
+ --background-color: #242424;
18
+ --link-color: #646cff;
19
+ --link-hover-color: #535bf2;
20
+ --nav-link-color: #007bff; /* Can be adjusted for better contrast in dark mode */
21
+ --nav-link-hover-bg: #4a4a4a;
22
+ --footer-bg: #222;
23
+ --footer-color: #fff;
24
+ --footer-border: #444;
25
+ }
@@ -0,0 +1,209 @@
1
+ import { IFooterLink } from './interfaces/iFooterLink';
2
+ import { IRoute } from './interfaces/iRoute';
3
+ import { MarkdownPipeline } from '@leadertechie/md2html';
4
+
5
+ export interface PageContent {
6
+ title: string;
7
+ description: string;
8
+ canonicalUrl: string;
9
+ content: string;
10
+ }
11
+
12
+ export interface ContentMetadata {
13
+ slug: string;
14
+ title: string;
15
+ description?: string;
16
+ summary?: string;
17
+ date: string;
18
+ imageUrl?: string;
19
+ tags?: string[];
20
+ author?: string;
21
+ }
22
+
23
+ const pipeline = new MarkdownPipeline({
24
+ imagePathPrefix: 'images/',
25
+ styleOptions: {
26
+ classPrefix: 'md-',
27
+ addHeadingIds: true
28
+ }
29
+ });
30
+
31
+ export const renderMarkdown = (content: string): string => {
32
+ if (!content) return '';
33
+ return pipeline.renderMarkdown(content);
34
+ };
35
+
36
+ export const generatePageContent = (
37
+ pathname: string,
38
+ routes: IRoute[],
39
+ footerLinks: IFooterLink[],
40
+ data?: {
41
+ blogs?: ContentMetadata[];
42
+ stories?: ContentMetadata[];
43
+ profile?: any;
44
+ content?: string;
45
+ title?: string;
46
+ description?: string;
47
+ date?: string;
48
+ slug?: string;
49
+ siteTitle?: string;
50
+ siteDescription?: string;
51
+ copyright?: string;
52
+ apiUrl?: string;
53
+ baseUrl?: string;
54
+ }
55
+ ): PageContent => {
56
+ const logo = "/api/logo";
57
+ const siteTitle = data?.siteTitle || "My Personal Website";
58
+ const siteDescription = data?.siteDescription || "Welcome to my professional portfolio and blog.";
59
+ const copyright = data?.copyright || "2026 All Rights Reserved";
60
+ const baseUrl = data?.baseUrl || "";
61
+ const canonicalUrl = baseUrl ? new URL(pathname, baseUrl).toString() : pathname;
62
+
63
+ const navLinks = routes
64
+ .map(
65
+ (route) =>
66
+ `<a href="${route.link}" class="nav-link" data-route="${route.link === "/" ? "home" : route.text.toLowerCase()}">${route.text}</a>`,
67
+ )
68
+ .join("");
69
+
70
+ const bannerTemplate = `
71
+ <my-banner header="${siteTitle}" logo="${logo}">
72
+ <theme-toggle slot="theme-switcher"></theme-toggle>
73
+ <nav slot="nav-links">
74
+ ${navLinks}
75
+ </nav>
76
+ </my-banner>`;
77
+
78
+ const footerTemplate = `
79
+ <my-footer
80
+ copyright="${copyright}"
81
+ footerlinks=\\'\${JSON.stringify(footerLinks)}\\'\'>
82
+ </my-footer>`;
83
+
84
+ const renderContentGists = (items: ContentMetadata[] = [], title: string, type: 'blogs' | 'stories') => {
85
+ const listHtml = items.length > 0
86
+ ? items.map(item => `
87
+ <div class="gist-card">
88
+ <a href="${type}/${item.slug}" data-route="${type}-${item.slug}"><h4>${item.title}</h4></a>
89
+ <p>${item.summary || item.description || ''}</p>
90
+ <small>${new Date(item.date).toLocaleDateString()}</small>
91
+ </div>
92
+ `).join('')
93
+ : `<p>No ${type} available yet.</p>`;
94
+
95
+ return `
96
+ <div class="recent-content-section ${title.includes('Stories') ? 'mt-2' : ''}">
97
+ <h3 class="border-bottom pb-05">${title}</h3>
98
+ ${listHtml}
99
+ </div>
100
+ `;
101
+ };
102
+
103
+ if (pathname === "/" || pathname === "") {
104
+ const homeHtml = data?.content || `<h1>Welcome to ${siteTitle}</h1><p>Start by configuring your content in the admin portal.</p>`;
105
+
106
+ const mainContent = `
107
+ ${bannerTemplate}
108
+ <main class="container container-wide column-layout row-layout">
109
+ <div class="home-main-content main-column text-left">
110
+ ${homeHtml}
111
+ </div>
112
+ <aside class="home-side-content sidebar-column">
113
+ ${renderContentGists(data?.blogs, 'Recent Blogs', 'blogs')}
114
+ ${renderContentGists(data?.stories, 'Recent Stories', 'stories')}
115
+ </aside>
116
+ </main>
117
+ ${footerTemplate}`;
118
+
119
+ return {
120
+ title: `${siteTitle} - Home`,
121
+ description: siteDescription,
122
+ canonicalUrl,
123
+ content: mainContent
124
+ };
125
+ } else if (pathname === "/blogs" || pathname === "/stories" || pathname.startsWith("/blogs/") || pathname.startsWith("/stories/")) {
126
+ const isBlog = pathname.includes("blogs");
127
+ const type = isBlog ? "blogs" : "stories";
128
+ const items = isBlog ? data?.blogs : data?.stories;
129
+ const title = type.charAt(0).toUpperCase() + type.slice(1);
130
+ const currentSlug = data?.slug;
131
+
132
+ const mainContent = `
133
+ ${bannerTemplate}
134
+ <main class="container container-wide">
135
+ <div class="mb-2">
136
+ <input type="text" id="content-search" placeholder="Search ${type}..." class="search-input search-input-lg">
137
+ </div>
138
+ <div class="column-layout">
139
+ <aside class="sidebar-nav sidebar-column">
140
+ <div id="content-list">
141
+ ${items?.map(item => {
142
+ const searchTerms = [
143
+ item.title,
144
+ ...(item.tags || []),
145
+ item.summary || item.description || '',
146
+ item.slug
147
+ ].join(' ').toLowerCase();
148
+
149
+ return `
150
+ <div class="list-item sidebar-item ${item.slug === currentSlug ? 'active' : ''}"
151
+ data-search="${searchTerms}">
152
+ <a href="/${type}/${item.slug}" data-route="${type}-${item.slug}" class="sidebar-item sidebar-item-link">
153
+ <h4>${item.title}</h4>
154
+ <small>${new Date(item.date).toLocaleDateString()}</small>
155
+ </a>
156
+ </div>
157
+ `}).join('') || `<p>No ${type} available yet.</p>`}
158
+ </div>
159
+ </aside>
160
+ <div class="wide-main-column text-left">
161
+ <div id="content-viewer">
162
+ ${currentSlug
163
+ ? (isBlog ? `<my-blog-viewer slug="${currentSlug}"></my-blog-viewer>` : `<my-story-viewer slug="${currentSlug}"></my-story-viewer>`)
164
+ : (items && items.length > 0 ? `<p>Select a ${type.slice(0, -1)} to read.</p>` : '')}
165
+ </div>
166
+ </div>
167
+ </div>
168
+ </main>
169
+ ${footerTemplate}`;
170
+
171
+ return {
172
+ title: `${title} - ${siteTitle}`,
173
+ description: `Read the latest ${type} from ${siteTitle}.`,
174
+ canonicalUrl,
175
+ content: mainContent
176
+ };
177
+ } else if (pathname === "/about-me") {
178
+ const apiBaseUrl = data?.apiUrl || "";
179
+ const mainContent = `
180
+ ${bannerTemplate}
181
+ <main class="container container-narrow">
182
+ <my-aboutme base-url="${apiBaseUrl}"></my-aboutme>
183
+ </main>
184
+ ${footerTemplate}`;
185
+
186
+ return {
187
+ title: `About - ${siteTitle}`,
188
+ description: `Learn more about ${siteTitle}.`,
189
+ canonicalUrl,
190
+ content: mainContent
191
+ };
192
+ } else {
193
+ const mainContent = `
194
+ ${bannerTemplate}
195
+ <main class="container container-narrow text-center page-content">
196
+ <h1 class="page-title">Page Not Found</h1>
197
+ <p>The page you\\'re looking for doesn\\'t exist.</p>
198
+ <p><a href="/">Return to home</a></p>
199
+ </main>
200
+ ${footerTemplate}`;
201
+
202
+ return {
203
+ title: `404 Not Found - ${siteTitle}`,
204
+ description: "The page you requested could not be found.",
205
+ canonicalUrl,
206
+ content: mainContent
207
+ };
208
+ }
209
+ };
@@ -0,0 +1,11 @@
1
+ // Runtime-only exports for Cloudflare Workers (no DOM/web components)
2
+ export * from './interfaces/iRoute';
3
+ export * from './interfaces/iFooterLink';
4
+ export * from './pageContent';
5
+
6
+ // Core utilities and types (DOM-free)
7
+ export type { IRoute } from './interfaces/iRoute';
8
+ export type { IFooterLink } from './interfaces/iFooterLink';
9
+
10
+ // Page content generation utilities
11
+ export { generatePageContent } from './pageContent';
@@ -0,0 +1,129 @@
1
+ /* Markdown Content Styles */
2
+
3
+ .md-heading {
4
+ margin: 1.5rem 0 0.75rem 0;
5
+ font-weight: 600;
6
+ line-height: 1.3;
7
+ color: var(--text-color, #213547);
8
+ }
9
+
10
+ .md-heading h1 { font-size: 2rem; }
11
+ .md-heading h2 { font-size: 1.5rem; }
12
+ .md-heading h3 { font-size: 1.25rem; }
13
+ .md-heading h4 { font-size: 1.1rem; }
14
+ .md-heading h5, .md-heading h6 { font-size: 1rem; }
15
+
16
+ .md-paragraph {
17
+ margin: 0 0 1rem 0;
18
+ line-height: 1.7;
19
+ color: var(--text-color, #213547);
20
+ }
21
+
22
+ .md-list {
23
+ margin: 0 0 1rem 0;
24
+ padding-left: 1.5rem;
25
+ }
26
+
27
+ .md-list-item {
28
+ margin-bottom: 0.5rem;
29
+ line-height: 1.6;
30
+ }
31
+
32
+ .md-blockquote {
33
+ margin: 1rem 0;
34
+ padding: 0.75rem 1rem;
35
+ border-left: 4px solid var(--link-color, #646cff);
36
+ background: var(--nav-link-hover-bg, #f8f9fa);
37
+ border-radius: 0 8px 8px 0;
38
+ color: var(--secondary-text, #666);
39
+ }
40
+
41
+ .md-blockquote .md-paragraph {
42
+ margin: 0;
43
+ }
44
+
45
+ .md-code {
46
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
47
+ font-size: 0.9em;
48
+ background: var(--nav-link-hover-bg, #f5f5f5);
49
+ padding: 0.2rem 0.4rem;
50
+ border-radius: 4px;
51
+ }
52
+
53
+ .md-image {
54
+ max-width: 100%;
55
+ height: auto;
56
+ border-radius: 8px;
57
+ margin: 1rem 0;
58
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
59
+ }
60
+
61
+ .md-container {
62
+ margin: 1rem 0;
63
+ }
64
+
65
+ .md-container hr {
66
+ border: none;
67
+ height: 2px;
68
+ background: var(--border-color, #eee);
69
+ margin: 2rem 0;
70
+ border-radius: 1px;
71
+ }
72
+
73
+ .md-container[tag="hr"] {
74
+ margin: 2rem 0;
75
+ }
76
+
77
+ /* Links within markdown content */
78
+ .md-paragraph a,
79
+ .md-list-item a {
80
+ color: var(--link-color, #646cff);
81
+ text-decoration: none;
82
+ transition: color 0.2s ease;
83
+ }
84
+
85
+ .md-paragraph a:hover,
86
+ .md-list-item a:hover {
87
+ color: var(--link-hover-color, #535bf2);
88
+ text-decoration: underline;
89
+ }
90
+
91
+ /* Strong and emphasis */
92
+ .md-paragraph strong,
93
+ .md-list-item strong {
94
+ font-weight: 600;
95
+ color: var(--text-color, #1a1a1a);
96
+ }
97
+
98
+ .md-paragraph em,
99
+ .md-list-item em {
100
+ font-style: italic;
101
+ }
102
+
103
+ /* Tables (if supported) */
104
+ .md-container table {
105
+ width: 100%;
106
+ border-collapse: collapse;
107
+ margin: 1rem 0;
108
+ }
109
+
110
+ .md-container th,
111
+ .md-container td {
112
+ padding: 0.75rem;
113
+ border: 1px solid var(--border-color, #ddd);
114
+ text-align: left;
115
+ }
116
+
117
+ .md-container th {
118
+ background: var(--nav-link-hover-bg, #f5f5f5);
119
+ font-weight: 600;
120
+ }
121
+
122
+ /* Dark theme adjustments */
123
+ html[data-theme="dark"] .md-code {
124
+ background: rgba(255, 255, 255, 0.1);
125
+ }
126
+
127
+ html[data-theme="dark"] .md-container th {
128
+ background: rgba(255, 255, 255, 0.05);
129
+ }
@@ -0,0 +1,35 @@
1
+ export interface TemplateProps {
2
+ title: string;
3
+ description: string;
4
+ canonicalUrl: string;
5
+ content: string;
6
+ }
7
+
8
+ export const createHtmlTemplate = ({
9
+ title,
10
+ description,
11
+ canonicalUrl,
12
+ content
13
+ }: TemplateProps): string => {
14
+ return `<!doctype html>
15
+ <html lang="en" data-theme="light">
16
+ <head>
17
+ <meta charset="UTF-8" />
18
+ <link rel="icon" type="image/svg+xml" href="/api/logo" />
19
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
20
+ <title>${title}</title>
21
+ <meta name="description" content="${description}" />
22
+ <meta property="og:title" content="${title}" />
23
+ <meta property="og:description" content="${description}" />
24
+ <meta property="og:url" content="${canonicalUrl}" />
25
+ <link rel="canonical" href="${canonicalUrl}" />
26
+ <link rel="stylesheet" href="/src/styles.css" />
27
+ <script type="module" src="/src/index.ts"></script>
28
+ </head>
29
+ <body>
30
+ <div id="app">
31
+ ${content}
32
+ </div>
33
+ </body>
34
+ </html>`;
35
+ };
@@ -0,0 +1,129 @@
1
+ /* Markdown Content Styles */
2
+
3
+ .md-heading {
4
+ margin: 1.5rem 0 0.75rem 0;
5
+ font-weight: 600;
6
+ line-height: 1.3;
7
+ color: var(--text-color, #213547);
8
+ }
9
+
10
+ .md-heading h1 { font-size: 2rem; }
11
+ .md-heading h2 { font-size: 1.5rem; }
12
+ .md-heading h3 { font-size: 1.25rem; }
13
+ .md-heading h4 { font-size: 1.1rem; }
14
+ .md-heading h5, .md-heading h6 { font-size: 1rem; }
15
+
16
+ .md-paragraph {
17
+ margin: 0 0 1rem 0;
18
+ line-height: 1.7;
19
+ color: var(--text-color, #213547);
20
+ }
21
+
22
+ .md-list {
23
+ margin: 0 0 1rem 0;
24
+ padding-left: 1.5rem;
25
+ }
26
+
27
+ .md-list-item {
28
+ margin-bottom: 0.5rem;
29
+ line-height: 1.6;
30
+ }
31
+
32
+ .md-blockquote {
33
+ margin: 1rem 0;
34
+ padding: 0.75rem 1rem;
35
+ border-left: 4px solid var(--link-color, #646cff);
36
+ background: var(--nav-link-hover-bg, #f8f9fa);
37
+ border-radius: 0 8px 8px 0;
38
+ color: var(--secondary-text, #666);
39
+ }
40
+
41
+ .md-blockquote .md-paragraph {
42
+ margin: 0;
43
+ }
44
+
45
+ .md-code {
46
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
47
+ font-size: 0.9em;
48
+ background: var(--nav-link-hover-bg, #f5f5f5);
49
+ padding: 0.2rem 0.4rem;
50
+ border-radius: 4px;
51
+ }
52
+
53
+ .md-image {
54
+ max-width: 100%;
55
+ height: auto;
56
+ border-radius: 8px;
57
+ margin: 1rem 0;
58
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
59
+ }
60
+
61
+ .md-container {
62
+ margin: 1rem 0;
63
+ }
64
+
65
+ .md-container hr {
66
+ border: none;
67
+ height: 2px;
68
+ background: var(--border-color, #eee);
69
+ margin: 2rem 0;
70
+ border-radius: 1px;
71
+ }
72
+
73
+ .md-container[tag="hr"] {
74
+ margin: 2rem 0;
75
+ }
76
+
77
+ /* Links within markdown content */
78
+ .md-paragraph a,
79
+ .md-list-item a {
80
+ color: var(--link-color, #646cff);
81
+ text-decoration: none;
82
+ transition: color 0.2s ease;
83
+ }
84
+
85
+ .md-paragraph a:hover,
86
+ .md-list-item a:hover {
87
+ color: var(--link-hover-color, #535bf2);
88
+ text-decoration: underline;
89
+ }
90
+
91
+ /* Strong and emphasis */
92
+ .md-paragraph strong,
93
+ .md-list-item strong {
94
+ font-weight: 600;
95
+ color: var(--text-color, #1a1a1a);
96
+ }
97
+
98
+ .md-paragraph em,
99
+ .md-list-item em {
100
+ font-style: italic;
101
+ }
102
+
103
+ /* Tables (if supported) */
104
+ .md-container table {
105
+ width: 100%;
106
+ border-collapse: collapse;
107
+ margin: 1rem 0;
108
+ }
109
+
110
+ .md-container th,
111
+ .md-container td {
112
+ padding: 0.75rem;
113
+ border: 1px solid var(--border-color, #ddd);
114
+ text-align: left;
115
+ }
116
+
117
+ .md-container th {
118
+ background: var(--nav-link-hover-bg, #f5f5f5);
119
+ font-weight: 600;
120
+ }
121
+
122
+ /* Dark theme adjustments */
123
+ html[data-theme="dark"] .md-code {
124
+ background: rgba(255, 255, 255, 0.1);
125
+ }
126
+
127
+ html[data-theme="dark"] .md-container th {
128
+ background: rgba(255, 255, 255, 0.05);
129
+ }