feat: add blog system with static site generator
- Add blog posts in Markdown (_posts/) - Build script converts MD to HTML at container build time - First posts: building with AI lessons, quick start guide - AGENTS.md documents blog writing style (unixsheikh-inspired)
This commit is contained in:
200
www/build-blog.js
Normal file
200
www/build-blog.js
Normal file
@@ -0,0 +1,200 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const matter = require('gray-matter');
|
||||
|
||||
const postsDir = path.join(__dirname, '_posts');
|
||||
const outputDir = path.join(__dirname, 'blog');
|
||||
|
||||
// Ensure output directory exists
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Get all markdown files
|
||||
const files = fs.readdirSync(postsDir).filter(f => f.endsWith('.md'));
|
||||
|
||||
// Parse and generate HTML for each post
|
||||
const posts = files.map(file => {
|
||||
const content = fs.readFileSync(path.join(postsDir, file), 'utf-8');
|
||||
const { data: frontmatter, content: markdown } = matter(content);
|
||||
|
||||
// Simple markdown to HTML conversion
|
||||
let html = markdown
|
||||
// Headers
|
||||
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
||||
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
||||
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
||||
// Bold
|
||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
// Italic
|
||||
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||||
// Code blocks
|
||||
.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code class="$1">$2</code></pre>')
|
||||
// Inline code
|
||||
.replace(/`(.*?)`/g, '<code>$1</code>')
|
||||
// Links
|
||||
.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2">$1</a>')
|
||||
// Images
|
||||
.replace(/!\[(.*?)\]\((.*?)\)/g, '<img src="$2" alt="$1"/>')
|
||||
// Line breaks
|
||||
.replace(/\n\n/g, '</p><p>')
|
||||
// Lists
|
||||
.replace(/^\- (.*$)/gim, '<li>$1</li>')
|
||||
.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>');
|
||||
|
||||
// Wrap in paragraph
|
||||
html = `<p>${html}</p>`;
|
||||
|
||||
// Clean up empty paragraphs
|
||||
html = html.replace(/<p><\/p>/g, '');
|
||||
|
||||
const slug = file.replace('.md', '');
|
||||
const url = `/blog/${slug}/`;
|
||||
|
||||
return {
|
||||
...frontmatter,
|
||||
slug,
|
||||
url,
|
||||
html,
|
||||
date: frontmatter.date ? frontmatter.date.toISOString().split('T')[0] : ''
|
||||
};
|
||||
});
|
||||
|
||||
// Sort posts by date (newest first)
|
||||
posts.sort((a, b) => new Date(b.date) - new Date(a.date));
|
||||
|
||||
const relativePath = process.env.GIT_URL ? '' : '../';
|
||||
|
||||
// Generate index page
|
||||
const indexHtml = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Blog - DearDiary</title>
|
||||
<link rel="stylesheet" href="${relativePath}css/styles.css">
|
||||
<style>
|
||||
.blog-container { max-width: 800px; margin: 0 auto; padding: 2rem; }
|
||||
.blog-header { margin-bottom: 2rem; }
|
||||
.blog-header h1 { font-size: 2rem; margin-bottom: 0.5rem; }
|
||||
.blog-list { list-style: none; padding: 0; }
|
||||
.blog-list li { margin-bottom: 1.5rem; padding-bottom: 1.5rem; border-bottom: 1px solid #334155; }
|
||||
.blog-list h2 { margin: 0 0 0.5rem; font-size: 1.25rem; }
|
||||
.blog-list h2 a { color: inherit; text-decoration: none; }
|
||||
.blog-list h2 a:hover { color: #a855f7; }
|
||||
.blog-meta { color: #64748b; font-size: 0.875rem; margin-bottom: 0.5rem; }
|
||||
.blog-excerpt { color: #94a3b8; }
|
||||
.blog-back { display: inline-block; margin-bottom: 1rem; color: #a855f7; text-decoration: none; }
|
||||
.blog-back:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<div class="nav-container">
|
||||
<a href="${relativePath}index.html" class="logo">
|
||||
<svg width="32" height="32" viewBox="0 0 100 100"><defs><linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#6d28d9"/><stop offset="100%" style="stop-color:#4c1d95"/></linearGradient></defs><rect width="100" height="100" rx="20" fill="url(#g)"/><path d="M25 25 L75 25 L75 80 L25 80 Z" fill="none" stroke="white" stroke-width="3"/><path d="M35 40 L65 40 M35 50 L65 50 M35 60 L55 60" stroke="white" stroke-width="2"/></svg>
|
||||
DearDiary
|
||||
</a>
|
||||
<div class="nav-links">
|
||||
<a href="${relativePath}index.html">Home</a>
|
||||
<a href="${relativePath}docs/">Docs</a>
|
||||
<a href="${relativePath}blog/">Blog</a>
|
||||
<a href="${relativePath}index.html" class="btn btn-primary">Join Free Alpha</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="blog-container">
|
||||
<div class="blog-header">
|
||||
<h1>Blog</h1>
|
||||
<p style="color: #94a3b8;">Updates, tutorials, and thoughts on AI-powered journaling.</p>
|
||||
</div>
|
||||
|
||||
<ul class="blog-list">
|
||||
${posts.map(post => `
|
||||
<li>
|
||||
<h2><a href="${post.url}">${post.title}</a></h2>
|
||||
<div class="blog-meta">${post.date}${post.author ? ` · ${post.author}` : ''}</div>
|
||||
${post.excerpt ? `<p class="blog-excerpt">${post.excerpt}</p>` : ''}
|
||||
</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
fs.writeFileSync(path.join(outputDir, 'index.html'), indexHtml);
|
||||
|
||||
// Generate individual post pages
|
||||
posts.forEach(post => {
|
||||
const postDir = path.join(outputDir, post.slug);
|
||||
if (!fs.existsSync(postDir)) {
|
||||
fs.mkdirSync(postDir, { recursive: true });
|
||||
}
|
||||
|
||||
const postRelativePath = process.env.GIT_URL ? '' : '../../';
|
||||
|
||||
const postHtml = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${post.title} - DearDiary Blog</title>
|
||||
<link rel="stylesheet" href="${postRelativePath}css/styles.css">
|
||||
<style>
|
||||
.blog-container { max-width: 700px; margin: 0 auto; padding: 2rem; }
|
||||
.blog-back { display: inline-block; margin-bottom: 1rem; color: #a855f7; text-decoration: none; }
|
||||
.blog-back:hover { text-decoration: underline; }
|
||||
.blog-header { margin-bottom: 2rem; padding-bottom: 1rem; border-bottom: 1px solid #334155; }
|
||||
.blog-header h1 { margin: 0 0 0.5rem; }
|
||||
.blog-meta { color: #64748b; font-size: 0.875rem; }
|
||||
.blog-content h1, .blog-content h2, .blog-content h3 { margin-top: 1.5em; margin-bottom: 0.5em; color: #e2e8f0; }
|
||||
.blog-content p { margin-bottom: 1em; line-height: 1.7; color: #cbd5e1; }
|
||||
.blog-content a { color: #a855f7; }
|
||||
.blog-content a:hover { text-decoration: underline; }
|
||||
.blog-content code { background: #1e293b; padding: 0.2em 0.4em; border-radius: 4px; font-size: 0.9em; }
|
||||
.blog-content pre { background: #1e293b; padding: 1em; border-radius: 8px; overflow-x: auto; margin: 1em 0; }
|
||||
.blog-content pre code { background: none; padding: 0; }
|
||||
.blog-content ul, .blog-content ol { margin: 1em 0; padding-left: 1.5em; color: #cbd5e1; }
|
||||
.blog-content li { margin-bottom: 0.5em; line-height: 1.6; }
|
||||
.blog-content img { max-width: 100%; height: auto; border-radius: 8px; margin: 1em 0; }
|
||||
.blog-content blockquote { border-left: 3px solid #a855f7; padding-left: 1em; margin: 1em 0; color: #94a3b8; font-style: italic; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<div class="nav-container">
|
||||
<a href="${postRelativePath}index.html" class="logo">
|
||||
<svg width="32" height="32" viewBox="0 0 100 100"><defs><linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#6d28d9"/><stop offset="100%" style="stop-color:#4c1d95"/></linearGradient></defs><rect width="100" height="100" rx="20" fill="url(#g)"/><path d="M25 25 L75 25 L75 80 L25 80 Z" fill="none" stroke="white" stroke-width="3"/><path d="M35 40 L65 40 M35 50 L65 50 M35 60 L55 60" stroke="white" stroke-width="2"/></svg>
|
||||
DearDiary
|
||||
</a>
|
||||
<div class="nav-links">
|
||||
<a href="${postRelativePath}index.html">Home</a>
|
||||
<a href="${postRelativePath}docs/">Docs</a>
|
||||
<a href="${postRelativePath}blog/">Blog</a>
|
||||
<a href="${postRelativePath}index.html" class="btn btn-primary">Join Free Alpha</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="blog-container">
|
||||
<a href="${postRelativePath}blog/" class="blog-back">← Back to Blog</a>
|
||||
|
||||
<article>
|
||||
<header class="blog-header">
|
||||
<h1>${post.title}</h1>
|
||||
<div class="blog-meta">${post.date}${post.author ? ` · ${post.author}` : ''}</div>
|
||||
</header>
|
||||
|
||||
<div class="blog-content">
|
||||
${post.html}
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
fs.writeFileSync(path.join(postDir, 'index.html'), postHtml);
|
||||
});
|
||||
|
||||
console.log(`Generated ${posts.length} blog posts.`);
|
||||
Reference in New Issue
Block a user