simple-blog-engine
Version:
Современный легковесный генератор статического блога с поддержкой Markdown
480 lines (400 loc) • 16.1 kB
JavaScript
#!/usr/bin/env node
/**
* Blog Engine CLI
* Command-line interface for the Markdown Blog Engine
*/
const { program } = require('commander');
const path = require('path');
const fs = require('fs');
const { buildSite } = require('../lib/index');
const readline = require('readline');
const { createPost } = require('../lib/postGenerator');
const { log } = require('../lib/siteBuilder');
const { readFile, writeFile } = require('../lib/fileHandler');
// Get package version
const enginePackageJson = require('../package.json');
program
.name('simple-blog-engine')
.description('Markdown blog engine CLI')
.version(enginePackageJson.version);
program
.command('build')
.description('Build static site from markdown content')
.option('-c, --config <path>', 'Path to config file', './blog/config.json')
.option('-o, --output <directory>', 'Output directory', './dist')
.option('-v, --verbose', 'Verbose output')
.option('-d, --debug', 'Show debug information')
.action(async (options) => {
try {
console.log('Building site...');
if (options.debug) {
console.log('Debug info:');
console.log(`- Current working directory: ${process.cwd()}`);
console.log(`- Config path: ${path.resolve(options.config)}`);
console.log(`- Output directory: ${path.resolve(options.output)}`);
// Check if config file exists
try {
fs.accessSync(options.config, fs.constants.R_OK);
console.log(`- Config file exists and is readable`);
} catch (err) {
console.log(`- Config file does not exist or is not readable: ${err.message}`);
}
}
await buildSite({
configPath: options.config,
outputDir: options.output,
verbose: options.verbose,
debug: options.debug
});
console.log('Build completed successfully!');
} catch (error) {
console.error('Build failed:', error);
process.exit(1);
}
});
program
.command('serve')
.description('Build and serve the site locally')
.option('-p, --port <number>', 'Port to serve on', '3000')
.option('-c, --config <path>', 'Path to config file', './blog/config.json')
.option('-o, --output <directory>', 'Output directory', './dist')
.action(async (options) => {
try {
// First build the site
await buildSite({
configPath: options.config,
outputDir: options.output
});
// Then serve it using npx serve
const { spawn } = require('child_process');
const serve = spawn('npx', ['serve', options.output, '-p', options.port], {
stdio: 'inherit'
});
console.log(`Serving site on http://localhost:${options.port}`);
// Handle process termination
process.on('SIGINT', () => {
serve.kill();
process.exit(0);
});
} catch (error) {
console.error('Failed to serve site:', error);
process.exit(1);
}
});
program
.command('init')
.description('Initialize a new blog project')
.option('-d, --directory <path>', 'Project directory', '.')
.option('-f, --force', 'Force overwrite existing files except blog/content and blog/images', false)
.action(async (options) => {
const targetDir = path.resolve(options.directory);
// Check if directory exists
if (!fs.existsSync(targetDir)) {
console.log(`Creating directory ${targetDir}`);
fs.mkdirSync(targetDir, { recursive: true });
}
// Initialize project structure
const dirs = [
'blog/content/posts',
'blog/content/about',
'blog/templates',
'blog/css',
'blog/images'
];
dirs.forEach(dir => {
const dirPath = path.join(targetDir, dir);
if (!fs.existsSync(dirPath)) {
console.log(`Creating ${dir} directory`);
fs.mkdirSync(dirPath, { recursive: true });
}
});
// Copy default files from engine/defaults
console.log('Copying default files to blog directory...');
// Handle config.json with intelligent merging
const configPath = path.join(targetDir, 'blog/config.json');
const defaultConfigPath = path.join(__dirname, '../defaults/config.json');
if (!fs.existsSync(configPath)) {
// For new installations, just copy the default config
fs.copyFileSync(defaultConfigPath, configPath);
console.log('Created new config.json');
} else if (options.force) {
// For force updates, merge existing config with defaults
try {
const existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
const defaultConfig = JSON.parse(fs.readFileSync(defaultConfigPath, 'utf8'));
const { mergeConfigs } = require('../lib/configManager');
// Merge configs preserving user settings
const newConfig = mergeConfigs(existingConfig, defaultConfig);
// Write merged config
fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2));
console.log('Updated config.json (preserved user settings)');
} catch (error) {
console.error('Error updating config.json:', error);
console.log('Keeping existing config.json');
}
}
// Copy default templates
const templatesDir = path.join(targetDir, 'blog/templates');
const defaultTemplatesDir = path.join(__dirname, '../defaults/templates');
fs.readdirSync(defaultTemplatesDir).forEach(file => {
const targetPath = path.join(templatesDir, file);
if (!fs.existsSync(targetPath) || options.force) {
fs.copyFileSync(
path.join(defaultTemplatesDir, file),
targetPath
);
console.log(options.force ? `Overwritten template: ${file}` : `Copied template: ${file}`);
}
});
// Copy default CSS files
const cssDir = path.join(targetDir, 'blog/css');
const defaultCssDir = path.join(__dirname, '../defaults/css');
// Create CSS directory if it doesn't exist
if (!fs.existsSync(cssDir)) {
fs.mkdirSync(cssDir, { recursive: true });
}
// Copy CSS files from defaults
fs.readdirSync(defaultCssDir).forEach(file => {
// Only copy .css files
if (file.endsWith('.css')) {
const targetPath = path.join(cssDir, file);
if (!fs.existsSync(targetPath) || options.force) {
fs.copyFileSync(
path.join(defaultCssDir, file),
targetPath
);
console.log(options.force ? `Overwritten CSS: ${file}` : `Copied CSS: ${file}`);
}
}
});
// Copy default images - skip if exists, even with --force
const imagesDir = path.join(targetDir, 'blog/images');
const defaultImagesDir = path.join(__dirname, '../defaults/images');
fs.readdirSync(defaultImagesDir).forEach(file => {
const targetPath = path.join(imagesDir, file);
if (!fs.existsSync(targetPath)) {
fs.copyFileSync(
path.join(defaultImagesDir, file),
targetPath
);
console.log(`Copied image: ${file}`);
}
});
// Create GitHub Actions workflow for GitHub Pages
const githubWorkflowsDir = path.join(targetDir, '.github/workflows');
if (!fs.existsSync(githubWorkflowsDir)) {
fs.mkdirSync(githubWorkflowsDir, { recursive: true });
console.log('Created .github/workflows directory');
}
const githubPagesWorkflowPath = path.join(githubWorkflowsDir, 'github-pages.yml');
if (!fs.existsSync(githubPagesWorkflowPath) || options.force) {
fs.copyFileSync(
path.join(__dirname, '../defaults/github/workflows/github-pages.yml'),
githubPagesWorkflowPath
);
console.log(options.force ? 'Overwritten GitHub Pages workflow file' : 'Created GitHub Pages workflow file');
}
// Copy .gitignore to project root
const gitignorePath = path.join(__dirname, '../defaults/gitignore.template');
const targetGitignorePath = path.join(targetDir, '.gitignore');
if (!fs.existsSync(targetGitignorePath) || options.force) {
fs.copyFileSync(gitignorePath, targetGitignorePath);
console.log(options.force ? 'Overwritten .gitignore file' : 'Created .gitignore file');
}
// Copy favicon.ico to blog directory
const engineFavicon = path.join(__dirname, '../favicon.ico');
const blogFavicon = path.join(targetDir, 'blog/favicon.ico');
if (fs.existsSync(engineFavicon) && (!fs.existsSync(blogFavicon) || options.force)) {
fs.copyFileSync(engineFavicon, blogFavicon);
console.log(options.force ? 'Overwritten favicon.ico in blog directory' : 'Copied favicon.ico to blog directory');
}
// Create Telegram IV template
const telegramTemplatePath = path.join(targetDir, 'blog/telegram-iv-template.txt');
if (!fs.existsSync(telegramTemplatePath)) {
const telegramTemplate = `# Telegram Instant View Template
# This template allows Telegram to create Instant View pages for your blog
# More info: https://instantview.telegram.org/
~version: "2.1"
# Article detection
?path: /posts/.+
body: //main
title: //h1
# Content
cover: //figure[has-class("post-cover")]//img
author: $author
published_date: $date
kicker: //p[has-class("post-summary")]
channel: $site_title
# Cleanup
@remove: //aside
@remove: //footer
@remove: //header
# Image handling
image_url: $srcset_largest_image
`;
fs.writeFileSync(telegramTemplatePath, telegramTemplate);
console.log('Created Telegram Instant View template');
}
// Create sample post
const samplePostPath = path.join(targetDir, 'blog/content/posts/hello-world.md');
if (!fs.existsSync(samplePostPath)) {
const samplePost = `---
title: Hello World
date: ${new Date().toISOString().split('T')[0]}
tags: [hello, blog]
---
# Hello World!
This is your first blog post. Edit it or create a new one in the \`blog/content/posts\` directory.
## Markdown Support
This blog engine supports all standard Markdown features.
- Lists
- **Bold text**
- *Italic text*
- [Links](https://example.com)
- And more!
`;
fs.writeFileSync(samplePostPath, samplePost);
console.log('Created sample blog post');
}
// Create sample about page
const aboutDirPath = path.join(targetDir, 'blog/content/about');
const aboutPagePath = path.join(aboutDirPath, 'index.md');
if (!fs.existsSync(aboutPagePath)) {
const aboutPage = `---
title: About This Blog
---
# About This Blog
This is an about page. Edit it to tell your readers about yourself and your blog.
## About Me
Write something about yourself here.
## Contact
- Email: your.email@example.com
- Twitter: @yourhandle
`;
fs.writeFileSync(aboutPagePath, aboutPage);
console.log('Created sample about page');
}
// Update package.json or create if it doesn't exist
const packageJsonPath = path.join(targetDir, 'package.json');
let packageJson = {};
if (fs.existsSync(packageJsonPath)) {
try {
packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
} catch (error) {
console.warn('Could not parse existing package.json, creating new one');
}
}
// Add/update necessary fields
packageJson.name = packageJson.name || 'my-markdown-blog';
packageJson.version = packageJson.version || '1.0.0';
packageJson.description = packageJson.description || 'My blog built with simple-blog-engine';
// Add scripts if they don't exist
packageJson.scripts = packageJson.scripts || {};
packageJson.scripts.start = packageJson.scripts.start || 'npx serve dist';
packageJson.scripts.dev = 'simple-blog-engine serve';
packageJson.scripts.build = 'simple-blog-engine build';
packageJson.scripts.init = packageJson.scripts.init || 'simple-blog-engine init';
packageJson.scripts.post = packageJson.scripts.post || 'simple-blog-engine post';
packageJson.scripts.deploy = packageJson.scripts.deploy || 'npm run build && echo "Deploy command goes here"';
// For backward compatibility, keep the 'new' script if it already exists
if (packageJson.scripts.new && packageJson.scripts.new.includes('new')) {
packageJson.scripts.new = 'simple-blog-engine post';
}
// Ensure dependencies exist
packageJson.dependencies = packageJson.dependencies || {};
// Remove old package name if it exists
if (packageJson.dependencies['markdown-blog-engine']) {
delete packageJson.dependencies['markdown-blog-engine'];
}
// Add simple-blog-engine dependency with current major version
// Extract major version number from the package version
const currentVersion = enginePackageJson.version;
const majorVersion = currentVersion.split('.')[0];
packageJson.dependencies['simple-blog-engine'] = `^${majorVersion}.0.0`;
fs.writeFileSync(
packageJsonPath,
JSON.stringify(packageJson, null, 2)
);
console.log('Updated package.json');
// Copy root files
const rootFiles = ['.htaccess', '_redirects', '.nojekyll'];
await Promise.all(rootFiles.map(async (file) => {
const sourcePath = path.join(__dirname, '../', file);
const destPath = path.join(targetDir, file);
if (fs.existsSync(sourcePath)) {
log(`Copying ${file}...`, options);
const content = await readFile(sourcePath);
await writeFile(destPath, content);
}
}));
console.log('\nBlog initialized successfully!');
console.log('\nTo build your blog, run:');
console.log(' npm run build');
console.log('\nTo serve your blog locally, run:');
console.log(' npm run dev');
});
program
.command('post')
.description('Create a new blog post template')
.option('-d, --directory <path>', 'Blog directory', './blog')
.action((options) => {
// Create readline interface when needed
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// Prompt for post title
rl.question('Enter the title for your new post: ', (title) => {
if (!title.trim()) {
console.error('Error: Post title cannot be empty');
rl.close();
return;
}
console.log(`Creating new post: "${title}"...`);
// Create the post
const result = createPost(title, options.directory);
if (result.success) {
console.log(`Post created successfully!`);
console.log(`Path: ${result.path}`);
console.log(`Slug: ${result.slug}`);
} else {
console.error(`Error creating post: ${result.error}`);
}
rl.close();
});
});
// Backward compatibility command
program
.command('new')
.description('[DEPRECATED] Use "post" command instead')
.option('-d, --directory <path>', 'Blog directory', './blog')
.action((options) => {
console.log('⚠️ The "new" command has been renamed to "post"');
console.log('Please use "simple-blog-engine post" instead');
console.log('Running "post" command for backward compatibility...\n');
// Create readline interface when needed
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// Prompt for post title
rl.question('Enter the title for your new post: ', (title) => {
if (!title.trim()) {
console.error('Error: Post title cannot be empty');
rl.close();
return;
}
console.log(`Creating new post: "${title}"...`);
// Create the post
const result = createPost(title, options.directory);
if (result.success) {
console.log(`Post created successfully!`);
console.log(`Path: ${result.path}`);
console.log(`Slug: ${result.slug}`);
} else {
console.error(`Error creating post: ${result.error}`);
}
rl.close();
});
});
program.parse(process.argv);