Kettlebell - статический генератор блога, для лога коммерческой рефлексии https://log.lex.sale
| Флаг | Агументы | Описание |
|---|---|---|
| --force | Перезаписать существующие посты | |
| --lang | {lang} | Сборка только для языка {lang} |
| --clean | Удалить ./build перед сборкой | |
| --new | {slug} | Создать файлы md для заметки, в существующих языках |
| --post | {lang}/{slug} | Собрать конкретный пост на конкретном языке |
Полный набор команд для правильного использования
# Полная сборка всех новых статей
> ./kettlebell
# Пересобрать все статьи
> ./kettlebell --force
# Пересобрать все статьи, предварительно удалив папку ./build
> ./kettlebell --clean --force
# Собрать статьи только для 1 языка, только новые
> ./kettlebell --lang ru
# Пересобрать все статьи для 1 языка
> ./kettlebell --lang ru --force
# Генерация только 1 поста для 1 языка
> ./kettlebell --post ru/new-idea
# Перезаписать существующий пост или элемент в RSS
> ./kettlebell --post ru/llm-as-lvr --force
# Создать блан для поста во всех языках
> ./kettlebell --new last-bastionНабор команд для контроллируемой ошибки:
# Пост для генерации отсутствует
> ./kettlebell --post ru/not-found
Error: source file not found: src/ru/posts/not-found.md
# Ошибка в формате
> ./kettlebell --post en/llm-as-lvr
Error: malformed source file: src/en/posts/llm-as-lvr
# Ошибка в формате команды post
> ./kettlebell --post enllm-as-lvr
Error: invalid --post format. Expected {lang}/{slug}
# Пост уже есть (запуск без --force)
> ./kettlebell --post de/llm-as-lvr
Error: post 'src/de/posts/llm-as-lvr' already exists
# Пересборка с нуля -- только принудительно
> ./kettlebell --clean
Error: --clean requires --forceФайловая структура на входе:
src/
├── ru/
│ ├── index.tmpl ← шаблон главной
│ ├── post.tmpl ← шаблон поста
│ ├── feed.xml.tmpl ← шаблон RSS
│ └── posts/
│ ├── 2026-06-09-llm-as-lvr.md
│ └── 2026-06-08-pain-is-gypsy-talk.md
├── en/
│ ├── index.tmpl
│ ├── post.tmpl
│ ├── feed.xml.tmpl
│ └── posts/
│ ├── 2026-06-09-llm-as-dm.md
│ └── ...
└── de/
├── index.tmpl
├── post.tmpl
├── feed.xml.tmpl
└── posts/
└── ...
Файловая структура после сборки:
build/
├── ru/
│ ├── index.html
│ ├── feed.xml
│ └── posts/
│ ├── llm-as-lvr.html
│ └── pain-is-gypsy-talk.html
├── en/
│ ├── index.html
│ ├── feed.xml
│ └── posts/
│ └── llm-as-dm.html
├── de/
│ ├── index.html
│ ├── feed.xml
│ └── posts/
│ └── llm-as-dm.html
└── assets/
├── log-preview-ru.png
├── log-preview-en.png
└── log-preview-de.png
Общая архитектура:
-
Чтение конфига (config.inc)
-
Парсинг аргументов командной строки (--force, --lang, --post, --new)
3.1 Если --post
3.2 Если --new
3.3 --foce, --lang или без аргумента
3.3.1 Обход src/{lang}/posts/*.md
3.3.2 Для каждого поста:
- Проверка: существует ли build/{lang}/posts/{slug}.html
- Если существует и нет флага --force → пропустить
- Если флаг --force → перезаписать
3.3.3 Генерация HTML (с мета-тегами, OG, Twitter)3.3.4 Генерация RSS для каждого языка
3.3.5 Генерация index.html для каждого языка
3.3.6 Копирование ассетов (assets/* → build/assets/)
Формат поста (frontmatter + тело):
---
title: ЛПР умер. Да здравствует LLM-as-a-Decision-Maker
description: Руководитель советуется с нейросетью или делегирует ей решение. Как это меняет архитектуру B2B-продаж.
lang: ru
slug: llm-as-lvr
related: { en: llm-as-dm, de: llm-as-dm }
---
Текст поста...
Поля:
- title — заголовок (в
<h1>и<title>) - description — мета-описание
- lang — язык (ru/en/de)
- slug — идентификатор (для URL и связей)
- related — slug той же статьи на других языках
Если related не указан — kettlebell пытается найти файл с тем же slug в другой языковой папке.
┌─────────────────────────────────────────────────────────────────┐
│ MAIN │
├─────────────────────────────────────────────────────────────────┤
│ 1. Парсинг аргументов командной строки │
│ 2. Валидация комбинаций флагов │
│ 3. Диспетчеризация в соответствующий режим │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌─────────┐
│ --new │ │ --post │ │ --lang │
│ создать │ │ ген. │ │ или │
│ шаблон │ │ 1 пост │ │ полная │
└─────────┘ └──────────┘ └─────────┘
│ │ │
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌─────────┐
│ Генера- │ │ Проверка │ │ Обход │
│ ция md │ │ сущест- │ │ языков │
│ файлов │ │ вования │ │ │
└─────────┘ └──────────┘ └─────────┘
│ │
▼ ▼
┌──────────┐ ┌─────────┐
│ Парсинг │ │ Обход │
│ фронт- │ │ постов │
│ материи │ │ в языке │
└──────────┘ └─────────┘
│ │
└──────────┬──────────┘
▼
┌─────────────────────┐
│ Генерация HTML: │
│ - подстановка мета │
│ - подстановка config│
│ - кросс-ссылки │
└─────────────────────┘
│
▼
┌──────────────────────┐
│ Обновление RSS: │
│ - пересборка feed.xml│
│ - сортировка по дате │
└──────────────────────┘
│
▼
┌──────────────────────┐
│ Обновление index.html│
│ (список постов) │
└──────────────────────┘
Режим 1: --new <slug>
--new last-bastion
Действие:
1. Проверить, что slug корректен (буквы, цифры, дефис)
2. Для каждого языка (ru, en, de):
- Создать файл src/{lang}/posts/current-date-{slug}.md
- Заполнить шаблоном с frontmatter
3. Вывести список созданных файлов
4. Exit(0)
Шаблон нового поста:
---
title: TITLE_PLACEHOLDER
description: DESCRIPTION_PLACEHOLDER
lang: {lang}
slug: {slug}
related: { ru: {slug}, en: {slug}, de: {slug} }
---
Напишите текст здесь.Режим 2: --post {lang}/{slug}
--post ru/new-idea
Действие:
1. Проверить формат (должен быть "/" внутри)
2. Извлечь lang и slug
3. Сформировать путь src/{lang}/posts/{slug}.md
4. Если файла нет → Error: source file not found
5. Если файл есть → вызвать generate_one_post(lang, slug, force_flag)
6. Если force_flag == 0 и build/{lang}/posts/{slug}.html существует → Error: already exists
7. Если force_flag == 1 → перезаписать
8. После генерации поста → обновить RSS для этого языка
9. После RSS → обновить index.html для этого языка
Режим 3: --lang {lang} (с опциональным --force)
--lang ru --force
Действие:
1. Валидировать lang (должен быть ru/en/de)
2. Если --clean указан → удалить build/{lang}/
3. Обойти директорию src/{lang}/posts/*.md
4. Для каждого поста:
- Вызвать generate_one_post(lang, slug, force_flag)
5. После всех постов → сгенерировать RSS для языка
6. Сгенерировать index.html для языка
Режим 4: Полная сборка (без флагов)
./kettlebell
Действие:
1. Для каждого языка (ru, en, de):
- Обойти src/{lang}/posts/*.md
- Для каждого поста:
- если force_flag == 0 и файл существует в build → пропустить
- иначе → сгенерировать
2. После всех постов → сгенерировать RSS для всех языков
3. Сгенерировать index.html для всех языков
4. Скопировать assets/ → build/assets/
post:
.title string
.description string
.lang string (ru/en/de)
.slug string
.date string (YYYY-MM-DD)
.related_ru string
.related_en string
.related_de string
.content string (HTML после парсинга)
config:
.site_url string
.bot_name string
.threema_id string
.support_email stringint main(int argc, char **argv) {
parse_args(argc, argv);
validate_flags();
load_config();
if (mode_new) {
create_post_template(slug);
exit(0);
}
if (mode_clean && !mode_force) {
error("--clean requires --force");
exit(1);
}
if (mode_post) {
if (!file_exists(source_path)) error("source not found");
if (!mode_force && file_exists(build_path)) error("already exists");
generate_one_post(lang, slug, mode_force);
update_rss_for_lang(lang);
update_index_for_lang(lang);
exit(0);
}
if (mode_lang) {
if (mode_clean) remove_build_dir(lang);
process_language(lang, mode_force);
exit(0);
}
// full build
if (mode_clean) remove_build_dir("all");
process_all_languages(mode_force);
copy_assets();
exit(0);
}Просто пересобираем весь feed -- на ассемблере это доли секунд даже для 100 постов.
Системные вызовы Solaris для kettlebell
| № | Сискол | Название | Назначение |
|---|---|---|---|
| 3 | SYS_read | sys_read | Чтение .md файлов, шаблонов |
| 4 | SYS_write | sys_write | Запись HTML, RSS, index.html |
| 68 | SYS_openat | sys_open | Открытие файлов (O_RDONLY, O_WRONLY O_CREAT, O_RDWR) |
| 6 | SYS_close | sys_close | Закрытие дескрипторов |
| 35 | SYS_statatfs | sys_stat | Проверка существования файла (--force check) |
| 115 | SYS_mmap | sys_mmap | Маппинг файлов в память (быстрый парсинг) |
| 117 | SYS_munmap | sys_munmap | Освобождение памяти |
| 17 | SYS_brk | sys_brk | Управление кучей (если не используем mmap) |
| 81 | SYS_getdents | sys_getdents | Чтение директории src/{lang}/posts/*.md |
| 102 | SYS_mkdirat | sys_mkdir | Создание build/ и поддиректорий |
| 1 | SYS_exit | sys_exit | Выход с кодом ошибки |
| 156 | SYS_gettimeofday | sys_gettimeofday | Время для RSS lastBuildDate |
Дополнительные вызовы для работы с директориями Для opendir/readdir используется SYS_getdents:
mov $78, %rax # SYS_getdents
mov $fd, %rdi # дескриптор директории
mov $buffer, %rsi # буфер для dirent
mov $1024, %rdx # размер буфера
syscall| Номера сисколов Solaris 11.4 | /usr/include/sys/syscall.h (на Solaris) |