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.
- package/.github/workflows/npm-publish.yml +45 -0
- package/.github/workflows/regenerate-dist.yml +38 -0
- package/LICENSE +201 -0
- package/README.md +85 -0
- package/SKILL.md +82 -0
- package/bin/cli.js +441 -0
- package/eslint.config.js +23 -0
- package/index.html +16 -0
- package/package.json +57 -0
- package/posts/customization-guide.md +45 -0
- package/posts/deploy-to-github-pages.md +109 -0
- package/posts/hello-world.md +20 -0
- package/posts/markdown-features.md +57 -0
- package/prerender.js +221 -0
- package/projects/legacy-api.md +7 -0
- package/projects/task-master.md +7 -0
- package/projects/vending-mocha.md +7 -0
- package/scripts/generate-posts-data.js +41 -0
- package/scripts/generate-projects-data.js +40 -0
- package/scripts/generate-rss.js +75 -0
- package/src/App.css +566 -0
- package/src/App.tsx +33 -0
- package/src/components/Footer.tsx +11 -0
- package/src/components/MarkdownImage.tsx +40 -0
- package/src/components/Profile.tsx +45 -0
- package/src/components/SiteHeader.tsx +44 -0
- package/src/context/ThemeContext.tsx +75 -0
- package/src/entry-client.tsx +32 -0
- package/src/entry-server.tsx +26 -0
- package/src/pages/BlogPost.tsx +93 -0
- package/src/pages/HomePage.tsx +85 -0
- package/src/pages/Projects.tsx +50 -0
- package/src/site.config.ts +38 -0
- package/src/utils/basePath.ts +21 -0
- package/src/utils/date.ts +17 -0
- package/src/utils/frontmatter.ts +32 -0
- package/static/favicon.ico +0 -0
- package/static/images/profile.png +0 -0
- package/tsconfig.app.json +36 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- 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
|
+

|
|
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,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();
|