vending-mocha 0.1.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 (42) hide show
  1. package/.github/workflows/npm-publish.yml +45 -0
  2. package/.github/workflows/regenerate-dist.yml +38 -0
  3. package/LICENSE +201 -0
  4. package/README.md +85 -0
  5. package/SKILL.md +82 -0
  6. package/bin/cli.js +441 -0
  7. package/eslint.config.js +23 -0
  8. package/index.html +16 -0
  9. package/package.json +57 -0
  10. package/posts/customization-guide.md +45 -0
  11. package/posts/deploy-to-github-pages.md +109 -0
  12. package/posts/hello-world.md +20 -0
  13. package/posts/markdown-features.md +57 -0
  14. package/prerender.js +221 -0
  15. package/projects/legacy-api.md +7 -0
  16. package/projects/task-master.md +7 -0
  17. package/projects/vending-mocha.md +7 -0
  18. package/scripts/generate-posts-data.js +41 -0
  19. package/scripts/generate-projects-data.js +40 -0
  20. package/scripts/generate-rss.js +75 -0
  21. package/src/App.css +566 -0
  22. package/src/App.tsx +33 -0
  23. package/src/components/Footer.tsx +11 -0
  24. package/src/components/MarkdownImage.tsx +40 -0
  25. package/src/components/Profile.tsx +45 -0
  26. package/src/components/SiteHeader.tsx +44 -0
  27. package/src/context/ThemeContext.tsx +75 -0
  28. package/src/entry-client.tsx +32 -0
  29. package/src/entry-server.tsx +26 -0
  30. package/src/pages/BlogPost.tsx +93 -0
  31. package/src/pages/HomePage.tsx +85 -0
  32. package/src/pages/Projects.tsx +50 -0
  33. package/src/site.config.ts +38 -0
  34. package/src/utils/basePath.ts +21 -0
  35. package/src/utils/date.ts +17 -0
  36. package/src/utils/frontmatter.ts +32 -0
  37. package/static/favicon.ico +0 -0
  38. package/static/images/profile.png +0 -0
  39. package/tsconfig.app.json +36 -0
  40. package/tsconfig.json +7 -0
  41. package/tsconfig.node.json +26 -0
  42. package/vite.config.ts +61 -0
@@ -0,0 +1,20 @@
1
+ ---
2
+ title: "Welcome to Vending Mocha ☕"
3
+ date: "2026-02-17 00:00:00"
4
+ summary: "Welcome to your new blogging framework! This is a sample post to get you started."
5
+ ---
6
+
7
+ ## Hello World!
8
+
9
+ Welcome to **Vending Mocha**! This is a sample post. You can edit this file or add new Markdown files to the `/posts` directory to start blogging.
10
+
11
+ ### Features
12
+
13
+ - **Simple and Clean UI**: Minimalist design that focuses on readability.
14
+ - **Markdown Support**: write your posts in Markdown.
15
+ - **Code Highlighting**: Syntax highlighting for code blocks.
16
+ - **SSG**: Static Site Generation for optimal performance.
17
+ - **Theme Support**: Customize your blog with different themes.
18
+ - **Deployment**: Ready for GitHub Pages deployment.
19
+
20
+ Enjoy building your new site! 🚀
@@ -0,0 +1,57 @@
1
+ ---
2
+ title: "Markdown Features Demo 📝"
3
+ date: "2026-02-17 02:00:00"
4
+ summary: "A demonstration of the Markdown features supported by Vending Mocha, including code blocks, lists, and formatting."
5
+ ---
6
+
7
+ ## Markdown Features
8
+
9
+ **Vending Mocha** isn't just about text; it supports a wide range of Markdown features to help you write rich content.
10
+
11
+ ### Typography
12
+
13
+ You can use **bold**, *italics*, and ~~strikethrough~~ text.
14
+
15
+ > "Blockquotes are great for emphasizing important information or quoting sources."
16
+
17
+ ### Lists
18
+
19
+ #### Unordered List
20
+ - Item 1
21
+ - Item 2
22
+ - Subitem 2.1
23
+ - Subitem 2.2
24
+
25
+ #### Ordered List
26
+ 1. First step
27
+ 2. Second step
28
+ 3. Third step
29
+
30
+ ### Code Blocks
31
+
32
+ You can write code blocks with syntax highlighting!
33
+
34
+ ```typescript
35
+ interface User {
36
+ id: number;
37
+ name: string;
38
+ role: 'admin' | 'user';
39
+ }
40
+
41
+ const getUser = (id: number): User => {
42
+ return {
43
+ id,
44
+ name: 'Jane Doe',
45
+ role: 'admin'
46
+ };
47
+ };
48
+ ```
49
+
50
+ ### Links and Images
51
+
52
+ You can link to [Google](https://google.com) or internal pages.
53
+
54
+ ![Vending Mocha](/images/profile.png?vmw=300&vmh=300)
55
+
56
+
57
+ Happy writing! ✍️
package/prerender.js ADDED
@@ -0,0 +1,221 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import prettier from 'prettier';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const toAbsolute = (p) => path.resolve(__dirname, p);
8
+
9
+
10
+
11
+ async function prerender() {
12
+ // 1. Read the template (client build output)
13
+ const templatePath = toAbsolute('docs/index.html');
14
+ if (!fs.existsSync(templatePath)) {
15
+ console.error('docs/index.html not found. Run client build first.');
16
+ process.exit(1);
17
+ }
18
+ const template = fs.readFileSync(templatePath, 'utf-8');
19
+
20
+ // 2. Import the server entry (SSR build output)
21
+ const serverEntryPath = toAbsolute('dist/server/entry-server.js');
22
+ if (!fs.existsSync(serverEntryPath)) {
23
+ console.error('dist/server/entry-server.js not found. Run server build first.');
24
+ process.exit(1);
25
+ }
26
+ const { render, siteConfig } = await import(serverEntryPath);
27
+
28
+ let basePath = '/';
29
+ try {
30
+ if (siteConfig.url) {
31
+ const url = new URL(siteConfig.url);
32
+ basePath = url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`;
33
+ }
34
+ } catch (e) {
35
+ console.warn('Invalid siteConfig.url, defaulting to /');
36
+ }
37
+ const normalizedBasePath = basePath;
38
+
39
+ // 3. Determine routes to prerender
40
+ const routesToPrerender = ['/', '/projects/'];
41
+
42
+ // Add blog post routes and pagination
43
+ const postsPath = toAbsolute('src/posts.json');
44
+ if (fs.existsSync(postsPath)) {
45
+ const posts = JSON.parse(fs.readFileSync(postsPath, 'utf-8'));
46
+
47
+ // Add post routes
48
+ posts.forEach(post => {
49
+ routesToPrerender.push(`/post/${post.slug}/`);
50
+ });
51
+
52
+ // Add paginated routes
53
+ const POSTS_PER_PAGE = 50;
54
+ const totalPages = Math.ceil(posts.length / POSTS_PER_PAGE);
55
+ for (let i = 2; i <= totalPages; i++) {
56
+ routesToPrerender.push(`/page/${i}/`);
57
+ }
58
+ }
59
+
60
+ // Generate theme.js content once
61
+ const themeHash = Math.random().toString(36).substring(2, 10);
62
+ const themeFilename = `theme.${themeHash}.js`;
63
+ const themeJsContent = `
64
+ (function() {
65
+ const themeConfig = ${JSON.stringify(siteConfig.theme)};
66
+ const STORAGE_KEY = 'theme';
67
+ const PREFERS_DARK = window.matchMedia('(prefers-color-scheme: dark)');
68
+
69
+ function getTheme() {
70
+ const savedTheme = localStorage.getItem(STORAGE_KEY);
71
+ if (savedTheme) return savedTheme;
72
+ return PREFERS_DARK.matches ? 'dark' : 'light';
73
+ }
74
+
75
+ function setTheme(theme) {
76
+ const root = document.documentElement;
77
+ const config = themeConfig[theme];
78
+
79
+ for (const [key, value] of Object.entries(config)) {
80
+ // Check if key maps to CSS vars we use.
81
+ // The config keys (primary, secondary, etc) map to --color-[key]
82
+ // Exception: linkColor -> --color-link, cardBackground -> --color-card-bg
83
+
84
+ let cssVar = \`--color-\${key}\`;
85
+ if (key === 'linkColor') cssVar = '--color-link';
86
+ if (key === 'cardBackground') cssVar = '--color-card-bg';
87
+
88
+ root.style.setProperty(cssVar, value);
89
+ }
90
+ root.style.setProperty('--font-family', themeConfig.fontFamily);
91
+
92
+ localStorage.setItem(STORAGE_KEY, theme);
93
+
94
+ // Update toggle button icon visibility if needed (could be CSS based)
95
+ document.documentElement.setAttribute('data-theme', theme);
96
+ }
97
+
98
+ // Initial setup
99
+ setTheme(getTheme());
100
+
101
+ // Listen for system changes
102
+ PREFERS_DARK.addEventListener('change', (e) => {
103
+ if (!localStorage.getItem(STORAGE_KEY)) {
104
+ setTheme(e.matches ? 'dark' : 'light');
105
+ }
106
+ });
107
+
108
+ // Setup toggle button listener when DOM is ready
109
+ document.addEventListener('DOMContentLoaded', () => {
110
+ const toggleBtn = document.querySelector('.theme-toggle');
111
+ if (toggleBtn) {
112
+ toggleBtn.addEventListener('click', () => {
113
+ const current = getTheme();
114
+ const next = current === 'light' ? 'dark' : 'light';
115
+ setTheme(next);
116
+ });
117
+ }
118
+
119
+ const hamburgerBtn = document.querySelector('.hamburger-menu');
120
+ const topNav = document.querySelector('.top-nav');
121
+ if (hamburgerBtn && topNav) {
122
+ hamburgerBtn.addEventListener('click', () => {
123
+ topNav.classList.toggle('active');
124
+ });
125
+
126
+ // Close menu when clicking links
127
+ topNav.querySelectorAll('a').forEach(link => {
128
+ link.addEventListener('click', () => {
129
+ topNav.classList.remove('active');
130
+ });
131
+ });
132
+ }
133
+ });
134
+ })();
135
+ `;
136
+ // Write theme.js
137
+ const themeJsPath = toAbsolute(`docs/assets/${themeFilename}`);
138
+ if (!fs.existsSync(path.dirname(themeJsPath))) {
139
+ fs.mkdirSync(path.dirname(themeJsPath), { recursive: true });
140
+ }
141
+ fs.writeFileSync(themeJsPath, themeJsContent);
142
+ console.log(`Generated ${themeFilename}`);
143
+
144
+ // 4. Render and save each route
145
+ for (const url of routesToPrerender) {
146
+ try {
147
+ const renderUrl = url === '/' ? normalizedBasePath : `${normalizedBasePath}${url.startsWith('/') ? url.slice(1) : url}`;
148
+ const { html: renderedHtml, helmet } = render(renderUrl);
149
+
150
+ let html = template.replace('<!--app-html-->', renderedHtml);
151
+
152
+
153
+
154
+ const helmetHead = `
155
+ ${helmet.title.toString()}
156
+ ${helmet.meta.toString()}
157
+ ${helmet.link.toString()}
158
+ ${helmet.script.toString()}
159
+ <link rel="alternate" type="application/rss+xml" title="RSS Feed for Krishna Thota" href="${normalizedBasePath}rss.xml" />
160
+ <script src="${normalizedBasePath}assets/${themeFilename}"></script>
161
+ `;
162
+
163
+ html = html.replace('<!--app-head-->', helmetHead);
164
+
165
+ // Strip out the client-side React bundle scripts and preloads
166
+ const escapedBase = normalizedBasePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
167
+ const scriptRegex = new RegExp(`<script type="module" crossorigin src="${escapedBase}assets\\/index-[^"]+\\.js"><\\/script>`, 'g');
168
+ const preloadRegex = new RegExp(`<link rel="modulepreload" crossorigin href="${escapedBase}assets\\/[^"]+">`, 'g');
169
+
170
+ html = html.replace(scriptRegex, '');
171
+ html = html.replace(preloadRegex, '');
172
+
173
+ const formattedHtml = await prettier.format(html, { parser: 'html' });
174
+
175
+ // Determine output file path
176
+ // For '/', it is docs/index.html
177
+ // For others, it is docs/subpath/index.html (to support clean URLs)
178
+ let filePath = `docs${url === '/' ? '/index.html' : `${url}/index.html`}`;
179
+ const absoluteFilePath = toAbsolute(filePath);
180
+ const dir = path.dirname(absoluteFilePath);
181
+
182
+ if (!fs.existsSync(dir)) {
183
+ fs.mkdirSync(dir, { recursive: true });
184
+ }
185
+
186
+ fs.writeFileSync(absoluteFilePath, formattedHtml);
187
+ console.log(`Pre-rendered: ${url} -> ${filePath}`);
188
+ } catch (e) {
189
+ console.error(`Failed to render ${url}:`, e);
190
+ }
191
+ }
192
+
193
+ // 5. Generate Sitemap
194
+ // siteConfig is already imported above
195
+ const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
196
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
197
+ ${routesToPrerender.map(url => `
198
+ <url>
199
+ <loc>${siteConfig.url.replace(/\/$/, '')}${url}</loc>
200
+ <lastmod>${new Date().toISOString().split('T')[0]}</lastmod>
201
+ </url>
202
+ `).join('')}
203
+ </urlset>`;
204
+
205
+ fs.writeFileSync(toAbsolute('docs/sitemap.xml'), sitemap);
206
+ console.log('Generated sitemap.xml');
207
+
208
+ // 6. Cleanup unused assets
209
+ const assetsDir = toAbsolute('docs/assets');
210
+ if (fs.existsSync(assetsDir)) {
211
+ const files = fs.readdirSync(assetsDir);
212
+ for (const file of files) {
213
+ if (file.endsWith('.js') && file !== themeFilename) {
214
+ fs.unlinkSync(path.join(assetsDir, file));
215
+ console.log(`Deleted unused asset: ${file}`);
216
+ }
217
+ }
218
+ }
219
+ }
220
+
221
+ prerender();
@@ -0,0 +1,7 @@
1
+ ---
2
+ title: "Legacy API Wrapper"
3
+ description: "An old library I wrote back in 2018. No longer maintained but good for reference."
4
+ link: "https://example.com/legacy-api"
5
+ status: "dead"
6
+ weight: 1
7
+ ---
@@ -0,0 +1,7 @@
1
+ ---
2
+ title: "Task Master 3000"
3
+ description: "A productivity application for managing daily tasks and goals efficiently."
4
+ link: "https://example.com/task-master"
5
+ status: "inactive"
6
+ weight: 5
7
+ ---
@@ -0,0 +1,7 @@
1
+ ---
2
+ title: "Vending Mocha"
3
+ description: "A lightweight, blazing fast, personal blogging framework built with React."
4
+ link: "https://github.com/kcthota/vending-mocha"
5
+ status: "active"
6
+ weight: 10
7
+ ---
@@ -0,0 +1,41 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import matter from 'gray-matter';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ const postsDir = path.join(__dirname, '../posts');
10
+ const outputFile = path.join(__dirname, '../src/posts.json');
11
+
12
+ function generatePostsData() {
13
+ if (!fs.existsSync(postsDir)) {
14
+ console.log('No posts directory found.');
15
+ return;
16
+ }
17
+
18
+ const files = fs.readdirSync(postsDir).filter(file => file.endsWith('.md'));
19
+ const posts = files.map(file => {
20
+ const filePath = path.join(postsDir, file);
21
+ const content = fs.readFileSync(filePath, 'utf-8');
22
+ const { data } = matter(content);
23
+ const slug = file.replace('.md', '');
24
+
25
+ return {
26
+ slug,
27
+ title: data.title || 'Untitled',
28
+ date: data.date || 'Unknown Date',
29
+ summary: data.summary || '',
30
+ weight: data.weight || 0
31
+ };
32
+ });
33
+
34
+ // Sort by date descending
35
+ posts.sort((a, b) => new Date(b.date) - new Date(a.date));
36
+
37
+ fs.writeFileSync(outputFile, JSON.stringify(posts, null, 2));
38
+ console.log(`Generated ${posts.length} posts in ${outputFile}`);
39
+ }
40
+
41
+ generatePostsData();
@@ -0,0 +1,40 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import matter from 'gray-matter';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ const projectsDir = path.join(__dirname, '../projects');
10
+ const outputFile = path.join(__dirname, '../src/projects.json');
11
+
12
+ function generateProjectsData() {
13
+ if (!fs.existsSync(projectsDir)) {
14
+ console.log('No projects directory found.');
15
+ return;
16
+ }
17
+
18
+ const files = fs.readdirSync(projectsDir).filter(file => file.endsWith('.md'));
19
+ const projects = files.map(file => {
20
+ const filePath = path.join(projectsDir, file);
21
+ const content = fs.readFileSync(filePath, 'utf-8');
22
+ const { data } = matter(content);
23
+
24
+ return {
25
+ name: data.title || 'Untitled Project',
26
+ description: data.description || '',
27
+ link: data.link || '#',
28
+ status: data.status || 'inactive',
29
+ weight: data.weight || 0
30
+ };
31
+ });
32
+
33
+ // Sort by weight descending
34
+ projects.sort((a, b) => b.weight - a.weight);
35
+
36
+ fs.writeFileSync(outputFile, JSON.stringify(projects, null, 2));
37
+ console.log(`Generated ${projects.length} projects in ${outputFile}`);
38
+ }
39
+
40
+ generateProjectsData();
@@ -0,0 +1,75 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+
8
+ const postsPath = path.join(__dirname, '../src/posts.json');
9
+ const docsDir = path.join(__dirname, '../docs');
10
+ const outputFile = path.join(docsDir, 'rss.xml');
11
+
12
+ // Import site config
13
+ const configPath = path.join(__dirname, '../src/site.config.ts');
14
+ // Simple extraction of values from site.config.ts since it's TS and we are in JS environment
15
+ const configContent = fs.readFileSync(configPath, 'utf-8');
16
+ const siteTitle = configContent.match(/title:\s*"(.*?)"/)?.[1] || 'My Blog';
17
+ const siteUrl = configContent.match(/url:\s*"(.*?)"/)?.[1] || 'http://localhost:3000';
18
+ const siteDescription = configContent.match(/description:\s*`(.*?)`/s)?.[1].replace(/\*\*/g, '').replace(/👋/g, '').trim() || '';
19
+ // Derive base path from URL
20
+ let derivedBasePath = '/';
21
+ try {
22
+ if (siteUrl) {
23
+ const urlObj = new URL(siteUrl);
24
+ derivedBasePath = urlObj.pathname.endsWith('/') ? urlObj.pathname : `${urlObj.pathname}/`;
25
+ }
26
+ } catch (e) {
27
+ console.warn('Invalid siteUrl, defaulting to /');
28
+ }
29
+
30
+ const normalizedBasePath = derivedBasePath;
31
+
32
+ function generateRSS() {
33
+ if (!fs.existsSync(postsPath)) {
34
+ console.error('posts.json not found. Run generate-posts-data.js first.');
35
+ return;
36
+ }
37
+
38
+ if (!fs.existsSync(docsDir)) {
39
+ fs.mkdirSync(docsDir, { recursive: true });
40
+ }
41
+
42
+ const posts = JSON.parse(fs.readFileSync(postsPath, 'utf-8')).slice(0, 25);
43
+
44
+ const items = posts.map(post => {
45
+ const url = `${siteUrl.replace(/\/$/, '')}/post/${post.slug}`;
46
+ const pubDate = new Date(post.date).toUTCString();
47
+
48
+ return `
49
+ <item>
50
+ <title><![CDATA[${post.title}]]></title>
51
+ <link>${url}</link>
52
+ <guid isPermaLink="true">${url}</guid>
53
+ <pubDate>${pubDate}</pubDate>
54
+ <description><![CDATA[${post.summary}]]></description>
55
+ </item>`;
56
+ }).join('');
57
+
58
+ const rss = `<?xml version="1.0" encoding="UTF-8" ?>
59
+ <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
60
+ <channel>
61
+ <title>${siteTitle}</title>
62
+ <link>${siteUrl}</link>
63
+ <description>${siteDescription}</description>
64
+ <language>en-us</language>
65
+ <lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
66
+ <atom:link href="${siteUrl.replace(/\/$/, '')}/rss.xml" rel="self" type="application/rss+xml" />
67
+ ${items}
68
+ </channel>
69
+ </rss>`;
70
+
71
+ fs.writeFileSync(outputFile, rss);
72
+ console.log(`Generated RSS feed with ${posts.length} posts at ${outputFile}`);
73
+ }
74
+
75
+ generateRSS();