- new blog cms
This commit is contained in:
+332
@@ -0,0 +1,332 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use NeonBlog\ContentRepository;
|
||||
|
||||
$app = require dirname(__DIR__) . '/app/bootstrap.php';
|
||||
$repo = $app->repository();
|
||||
$comments = $app->comments();
|
||||
$config = $app->config();
|
||||
$stats = $app->stats();
|
||||
|
||||
$argv = $_SERVER['argv'] ?? [];
|
||||
array_shift($argv);
|
||||
$command = array_shift($argv) ?? 'help';
|
||||
|
||||
try {
|
||||
match ($command) {
|
||||
'help', '--help', '-h' => help(),
|
||||
'init' => init($repo),
|
||||
'rebuild' => rebuild($repo),
|
||||
'list' => listContent($repo, options($argv)),
|
||||
'create' => createContent($repo, $argv),
|
||||
'update' => updateContent($repo, $argv),
|
||||
'delete' => deleteContent($repo, $argv),
|
||||
'comments:list' => listComments($comments, options($argv)),
|
||||
'comments:approve' => setCommentStatus($comments, $argv, 'approved'),
|
||||
'comments:pending' => setCommentStatus($comments, $argv, 'pending'),
|
||||
'comments:spam' => setCommentStatus($comments, $argv, 'spam'),
|
||||
'comments:delete' => deleteComment($comments, $argv),
|
||||
'site:set' => setConfig($config, $argv),
|
||||
'social:list' => listSocial($config),
|
||||
'social:add' => addSocial($config, $argv),
|
||||
'stats:summary' => statsSummary($stats),
|
||||
default => fail('Unknown command "' . $command . '". Run php cli/blog.php help.'),
|
||||
};
|
||||
} catch (Throwable $error) {
|
||||
fail($error->getMessage());
|
||||
}
|
||||
|
||||
function help(): void
|
||||
{
|
||||
echo <<<TEXT
|
||||
Neon Blog CLI
|
||||
|
||||
Content:
|
||||
php cli/blog.php init
|
||||
php cli/blog.php list [--type=post|page] [--all]
|
||||
php cli/blog.php create "Title" [--slug=custom] [--type=post|page] [--status=published|draft] [--category=Name] [--tags=a,b] [--summary=text] [--body-file=/path/file.md] [--no-comments]
|
||||
php cli/blog.php update slug [--title=New] [--status=published|draft] [--category=Name] [--tags=a,b] [--summary=text] [--body-file=/path/file.md]
|
||||
php cli/blog.php delete slug
|
||||
php cli/blog.php rebuild
|
||||
|
||||
Comments:
|
||||
php cli/blog.php comments:list [--status=pending|approved|spam] [--slug=post-slug]
|
||||
php cli/blog.php comments:approve 12
|
||||
php cli/blog.php comments:pending 12
|
||||
php cli/blog.php comments:spam 12
|
||||
php cli/blog.php comments:delete 12
|
||||
|
||||
Site:
|
||||
php cli/blog.php site:set site.title "My Blog"
|
||||
php cli/blog.php social:list
|
||||
php cli/blog.php social:add github "GitHub" https://github.com/example
|
||||
php cli/blog.php stats:summary
|
||||
|
||||
TEXT;
|
||||
}
|
||||
|
||||
function init(ContentRepository $repo): void
|
||||
{
|
||||
$repo->rebuildIndex();
|
||||
line('Content index rebuilt.');
|
||||
}
|
||||
|
||||
function rebuild(ContentRepository $repo): void
|
||||
{
|
||||
$repo->rebuildIndex();
|
||||
line('Content index rebuilt.');
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $opts */
|
||||
function listContent(ContentRepository $repo, array $opts): void
|
||||
{
|
||||
$includeDrafts = isset($opts['all']);
|
||||
$type = (string) ($opts['type'] ?? 'post');
|
||||
$items = $type === 'page' ? $repo->pages($includeDrafts) : $repo->posts($includeDrafts);
|
||||
|
||||
if ($items === []) {
|
||||
line('No content found.');
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($items as $item) {
|
||||
printf(
|
||||
"%-22s %-10s %-10s %s\n",
|
||||
$item['slug'],
|
||||
$item['type'],
|
||||
$item['status'],
|
||||
$item['title']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<int, string> $args */
|
||||
function createContent(ContentRepository $repo, array $args): void
|
||||
{
|
||||
$title = array_shift($args);
|
||||
if ($title === null || str_starts_with($title, '--')) {
|
||||
fail('A title is required.');
|
||||
}
|
||||
$opts = options($args);
|
||||
$slug = (string) ($opts['slug'] ?? ContentRepository::slugify($title));
|
||||
$body = bodyFromOptions($opts, "# {$title}\n\nStart writing here.");
|
||||
$metadata = metadataFromOptions($opts, [
|
||||
'title' => $title,
|
||||
'type' => $opts['type'] ?? 'post',
|
||||
'status' => $opts['status'] ?? 'draft',
|
||||
]);
|
||||
|
||||
$item = $repo->save($slug, $metadata, $body);
|
||||
line('Saved ' . $item['type'] . ' "' . $item['title'] . '" at bl-content/pages/' . $item['slug'] . '/index.txt');
|
||||
}
|
||||
|
||||
/** @param array<int, string> $args */
|
||||
function updateContent(ContentRepository $repo, array $args): void
|
||||
{
|
||||
$slug = array_shift($args);
|
||||
if ($slug === null || str_starts_with($slug, '--')) {
|
||||
fail('A slug is required.');
|
||||
}
|
||||
|
||||
$item = $repo->find($slug, true);
|
||||
if ($item === null) {
|
||||
fail('No content exists for slug "' . $slug . '".');
|
||||
}
|
||||
|
||||
$opts = options($args);
|
||||
$body = bodyFromOptions($opts, (string) $item['markdown']);
|
||||
$metadata = metadataFromOptions($opts, $item);
|
||||
$newSlug = (string) ($opts['slug'] ?? $slug);
|
||||
$saved = $repo->save($newSlug, $metadata, $body);
|
||||
if ($newSlug !== $slug) {
|
||||
$repo->delete($slug);
|
||||
}
|
||||
|
||||
line('Updated ' . $saved['slug'] . '.');
|
||||
}
|
||||
|
||||
/** @param array<int, string> $args */
|
||||
function deleteContent(ContentRepository $repo, array $args): void
|
||||
{
|
||||
$slug = $args[0] ?? '';
|
||||
if ($slug === '') {
|
||||
fail('A slug is required.');
|
||||
}
|
||||
|
||||
line($repo->delete($slug) ? 'Deleted ' . $slug . '.' : 'No content exists for slug "' . $slug . '".');
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $opts */
|
||||
function listComments($comments, array $opts): void
|
||||
{
|
||||
$rows = $comments->list($opts['status'] ?? null, $opts['slug'] ?? null);
|
||||
if ($rows === []) {
|
||||
line('No comments found.');
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($rows as $row) {
|
||||
printf(
|
||||
"#%-4d %-9s %-20s %-18s %s\n",
|
||||
$row['id'],
|
||||
$row['status'],
|
||||
$row['slug'],
|
||||
$row['author'],
|
||||
mb_substr(str_replace(["\r", "\n"], ' ', $row['body']), 0, 64)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<int, string> $args */
|
||||
function setCommentStatus($comments, array $args, string $status): void
|
||||
{
|
||||
$id = (int) ($args[0] ?? 0);
|
||||
if ($id < 1) {
|
||||
fail('A numeric comment id is required.');
|
||||
}
|
||||
|
||||
line($comments->setStatus($id, $status) ? 'Comment updated.' : 'Comment not found.');
|
||||
}
|
||||
|
||||
/** @param array<int, string> $args */
|
||||
function deleteComment($comments, array $args): void
|
||||
{
|
||||
$id = (int) ($args[0] ?? 0);
|
||||
if ($id < 1) {
|
||||
fail('A numeric comment id is required.');
|
||||
}
|
||||
|
||||
line($comments->delete($id) ? 'Comment deleted.' : 'Comment not found.');
|
||||
}
|
||||
|
||||
/** @param array<int, string> $args */
|
||||
function setConfig($config, array $args): void
|
||||
{
|
||||
$key = $args[0] ?? '';
|
||||
$value = $args[1] ?? null;
|
||||
if ($key === '' || $value === null) {
|
||||
fail('Usage: php cli/blog.php site:set site.title "My Blog"');
|
||||
}
|
||||
$config->set($key, parseCliValue($value));
|
||||
$config->save();
|
||||
line('Updated config key ' . $key . '.');
|
||||
}
|
||||
|
||||
function listSocial($config): void
|
||||
{
|
||||
$social = $config->get('social', []);
|
||||
if (!is_array($social) || $social === []) {
|
||||
line('No social links configured.');
|
||||
return;
|
||||
}
|
||||
foreach ($social as $network => $item) {
|
||||
printf("%-14s %-18s %s\n", $network, $item['label'] ?? $network, $item['url'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<int, string> $args */
|
||||
function addSocial($config, array $args): void
|
||||
{
|
||||
[$network, $label, $url] = [$args[0] ?? '', $args[1] ?? '', $args[2] ?? ''];
|
||||
if ($network === '' || $label === '' || $url === '') {
|
||||
fail('Usage: php cli/blog.php social:add github "GitHub" https://github.com/example');
|
||||
}
|
||||
|
||||
$social = $config->get('social', []);
|
||||
$social[$network] = ['label' => $label, 'url' => $url];
|
||||
$config->set('social', $social);
|
||||
$config->save();
|
||||
line('Added social link ' . $network . '.');
|
||||
}
|
||||
|
||||
function statsSummary($stats): void
|
||||
{
|
||||
$summary = $stats->summary();
|
||||
line('Visits: ' . $summary['visits']);
|
||||
line('Routes:');
|
||||
foreach ($summary['routes'] as $route => $count) {
|
||||
line(' ' . $route . ': ' . $count);
|
||||
}
|
||||
line('Top paths:');
|
||||
foreach ($summary['top_paths'] as $path => $count) {
|
||||
line(' ' . $path . ': ' . $count);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<int, string> $args @return array<string, mixed> */
|
||||
function options(array $args): array
|
||||
{
|
||||
$options = [];
|
||||
foreach ($args as $arg) {
|
||||
if (!str_starts_with($arg, '--')) {
|
||||
continue;
|
||||
}
|
||||
$arg = substr($arg, 2);
|
||||
if (str_contains($arg, '=')) {
|
||||
[$key, $value] = explode('=', $arg, 2);
|
||||
$options[$key] = parseCliValue($value);
|
||||
} else {
|
||||
$options[$arg] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $opts @param array<string, mixed> $base @return array<string, mixed> */
|
||||
function metadataFromOptions(array $opts, array $base): array
|
||||
{
|
||||
foreach (['title', 'slug', 'type', 'status', 'date', 'category', 'summary', 'author', 'cover', 'menu_order'] as $key) {
|
||||
if (array_key_exists($key, $opts)) {
|
||||
$base[$key] = $opts[$key];
|
||||
}
|
||||
}
|
||||
if (array_key_exists('tags', $opts)) {
|
||||
$base['tags'] = ContentRepository::normalizeList($opts['tags']);
|
||||
}
|
||||
if (isset($opts['no-comments'])) {
|
||||
$base['allow_comments'] = false;
|
||||
}
|
||||
|
||||
return $base;
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $opts */
|
||||
function bodyFromOptions(array $opts, string $fallback): string
|
||||
{
|
||||
if (!isset($opts['body-file'])) {
|
||||
return $fallback;
|
||||
}
|
||||
$path = (string) $opts['body-file'];
|
||||
if (!is_file($path)) {
|
||||
fail('Body file not found: ' . $path);
|
||||
}
|
||||
|
||||
return (string) file_get_contents($path);
|
||||
}
|
||||
|
||||
function parseCliValue(string $value): mixed
|
||||
{
|
||||
$lower = mb_strtolower($value);
|
||||
if (in_array($lower, ['true', 'false'], true)) {
|
||||
return $lower === 'true';
|
||||
}
|
||||
if (is_numeric($value) && preg_match('/^\d+$/', $value)) {
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
function line(string $message): void
|
||||
{
|
||||
echo $message . PHP_EOL;
|
||||
}
|
||||
|
||||
function fail(string $message): never
|
||||
{
|
||||
fwrite(STDERR, $message . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user