Как работи системата за сайтове, която не яде ресурси когато не се ползва
Система за правене на уебсайтове. Състои се от две части:
Админ панел, където редактираш съдържание. Написан на езика Go. Компилира се до един файл. Пази данните в SQLite — обикновен файл на диска, не е нужен отделен database сървър.
Генератор за сайта. Взема данните от Go и създава готови HTML файлове. Написан на JavaScript/Astro. Работи само при "Build" — после спира.
Публичният сайт са готови HTML файлове. Когато посетител отвори сайта, се чете файл от диска — никакъв код не се изпълнява. Не работи нито Go, нито Node.js, нито PHP, нито нищо друго. Само nginx подава файла.
| Програма | Какво прави | Кога работи | RAM |
|---|---|---|---|
| nginx | Подава HTML файлове на посетители. Пренасочва /admin и /api заявки към Go. |
Винаги | ~5 MB |
| systemd | Слуша порта на Go. Когато дойде заявка — буди Go. | Винаги (част от Linux) | ~0 MB (вече работи) |
| Go CMS | Админ панел, обработка на форми, API, стартиране на build. | Само при заявка. Спи след 2 мин. |
~20 MB (когато работи) |
| Node.js / Astro | Генерира HTML файлове от данните. | Само при Build. Секунди, после спира. |
~100 MB (само при build) |
| SQLite | Базата данни. Просто файл — site.db |
Не е програма. Файл на диска. |
0 MB |
Работи САМО nginx (~5 MB). Всичко останало спи. Ако имаш 20 сайта на сървъра — пак само nginx работи, защото всички Go процеси спят. Общо ~5 MB RAM за 20 сайта.
| Софтуер | За какво | Как се инсталира |
|---|---|---|
| nginx | Уеб сървър (подава файлове, proxy) | apt install nginx |
| Node.js | Нужен за Astro build (генериране на HTML) | apt install nodejs npm |
| rsync | Копира генерираните файлове в публичната папка | apt install rsync |
| systemd | Управлява Go процеса (буди/спира) | Вече е на всеки Linux |
Не задължително. Go binary-то може да се компилира на друг компютър и да се копира готово. Компилираш на лаптопа си с GOOS=linux go build и качваш 1 файл. Но ако имаш Go на сървъра — може и там.
НЕ. Node.js се стартира САМО когато админът натисне "Build". Работи 5-30 секунди (генерира HTML файловете) и спира. Не работи постоянно. Не яде RAM когато не се ползва.
НЕ. Няма Docker, няма контейнери. Всичко работи директно на операционната система.
НЕ. Базата данни е SQLite — един файл (site.db) вътре в папката на проекта. Няма MySQL, PostgreSQL, нищо друго. Файлът може да се копира на USB, по email — това е цялата база.
Някой пише https://example.com/about в браузъра.
Посетител попълва форма за контакт и натиска "Изпрати".
systemd автоматично буди Go при заявка. Посетителят не знае, че Go е спял. Вижда нормална бърза реакция.
Админът отваря https://example.com/admin/
Докато админът работи (кликва, навигира) — всяко действие нулира idle таймера. Go не спи докато някой го ползва.
npx astro build с указание кои страници да се генерират. Node.js стартира, Astro работи.
Ако имаш 50 страници и промениш само 2 — Astro генерира САМО тези 2. Не ги прави всичките 50. Затова отнема 5 секунди вместо 30+.
Full rebuild (бутон "Full rebuild") генерира ВСИЧКИ страници. Ползва се при промяна на дизайн или при начален deploy.
Това е най-важната функционалност. Ето я подробно:
Вместо Go да слуша порт 8080 постоянно (и да яде RAM), systemd (вградена програма в Linux) слуша вместо него. Когато дойде заявка — systemd стартира Go и му подава връзката.
Ако Go стартира Astro build (5-30 секунди), idle таймерът се паузира. Иначе може Go да спре по средата на build-а. След build-а — таймерът се възобновява.
Цялият проект живее в една папка. Ето какво има вътре:
| Ситуация | nginx | Go CMS | Node/Astro | SQLite |
|---|---|---|---|---|
| Посетител чете страница | ✅ сервира HTML | 💤 спи | 💤 спи | — файл |
| Посетител праща форма | ✅ proxy | ✅ буди се | 💤 спи | ✅ пише |
| Админ влиза | ✅ proxy | ✅ буди се | 💤 спи | ✅ чете |
| Админ запазва страница | ✅ proxy | ✅ работи | 💤 спи | ✅ пише |
| Админ натиска Build | ✅ proxy | ✅ стартира build | ✅ билдва HTML | ✅ Go чете |
| Покой (99% от времето) | ✅ слуша | ❌ не съществува | ❌ не съществува | — файл |
scp -r /var/www/example.com/ root@new-server:/var/www/apt install nginx nodejs npm rsync
cd deploy && ./setup.sh example.com 8080
Съдържанието на всяка страница се състои от блокове (секции). Всеки блок е визуален елемент:
| Блок | Описание | Файл |
|---|---|---|
| hero | Голям банер с заглавие, подзаглавие, фоново изображение | WfHero.astro |
| text | Текстов блок с HTML съдържание | WfText.astro |
| image | Картинка с описание | WfImage.astro |
| gallery | Мрежа от картинки (галерия) | WfGallery.astro |
| contact | Контактна форма (име, имейл, съобщение) | WfContact.astro |
В базата данни блоковете се пазят като JSON:
Добавяне на нов тип блок:
frontend/src/blocks/WfNewBlock.astroWfRender.astro{"type": "newblock", "data": {...}}Един Go binary + Astro за генерация на HTML + nginx за сервиране + systemd за on-demand стартиране. Нищо не работи когато не се ползва. Всичко е в една папка. Мести се с scp.
Нужен софтуер: nginx, Node.js, rsync, systemd
НЕ трябва: Docker, MySQL, Go compiler*, PHP, Redis, или нещо друго
* Go compiler трябва само за компилация на binary-то. Може да се направи на друг компютър.
Ако промениш една статия, тя може да се показва на 15 други места — homepage, listing страница, sidebar. Простият selective build ще ребилдне само статията, но не и тези 15 страници. Как се решава?
Всеки тип секция декларира от какво зависи в конфигурационен файл:
Когато Go запази статия:
| Ниво | Пример | Какво се случва |
|---|---|---|
| Section | Секция "latest_articles" зависи от колекция "article" | Промяна на статия → rebuild на всички страници с тази секция |
| Page | Страница A включва данни от страница B | Промяна на B → rebuild на A и B |
| Global | Промяна на настройки, навигация, layout | Пълен rebuild на всички страници |
Зависимостите се дефинират ВЕДНЪЖ в section-types.json. После Go автоматично знае кои страници да маркира при всяка промяна. Админът не трябва да мисли за това.
При много сайтове, ръчното управление на портове (8080, 8081, 8082...) е проблем. Unix socket-ите го решават изцяло.
Всеки сайт = уникален порт.
Ръчно трябва да помниш кой порт е за кого.
Потенциални конфликти.
ListenStream=127.0.0.1:8080
proxy_pass http://127.0.0.1:8080;
Всеки сайт = файл с име от домейна.
Автоматично уникално, нищо за помнене.
Невъзможни конфликти.
ListenStream=/run/cms/example-com.sock
proxy_pass http://unix:/run/cms/example-com.sock;
Абсолютно по същия начин. systemd слуша socket файла вместо порт. Когато дойде заявка → буди Go. Когато Go спре → socket файлът остава. Следващата заявка → Go пак се буди. Единствената разлика е файлов път вместо порт номер.
Целият проект е една папка. Преместването е 3 стъпки:
rsync -az /var/www/example.com/ root@new-server:/var/www/example.com/cd /var/www/example.com/deploy && ./setup.sh example.com| Действие | Ръчно? |
|---|---|
| Създава systemd socket файл | Автоматично |
| Създава systemd service файл | Автоматично |
| Генерира nginx конфигурация | Автоматично |
| Включва nginx сайта (symlink) | Автоматично |
| Пуска certbot за SSL | Автоматично |
| Тества nginx и reload-ва | Автоматично |
| DNS промяна | Ръчно (при регистратора) |
КОПИРА СЕ (от старата машина): Go binary, Astro код, SQLite база (1 файл), качени снимки, генериран HTML, deploy скриптове.
НЕ СЕ КОПИРА (генерира се на място): systemd файлове, nginx конфиг, SSL сертификат. Всичко това setup.sh създава автоматично.
Вместо да редактираш JSON на ръка, админът предоставя визуален editor — като ACF Pro в WordPress, но без WordPress.
Конфигурационен файл section-types.json дефинира какви секции и полета съществуват. Админ панелът чете този файл и генерира формите автоматично.
| Тип | Описание | Аналог в ACF |
|---|---|---|
text | Един ред текст | Text |
textarea | Много редове | Text Area |
richtext | WYSIWYG editor (визуално форматиране) | WYSIWYG Editor |
number | Число | Number |
select | Dropdown меню | Select |
checkbox | Да/Не | True/False |
image | Качване + media picker | Image |
gallery | Много снимки с drag/drop | Gallery |
link | URL + текст + target | Link |
color | Избор на цвят | Color Picker |
date | Дата | Date Picker |
repeater | Множество реда с подполета | Repeater |
group | Вложени полета | Group |
1. Добави го в section-types.json (полета и конфигурация)
2. Създай Astro компонент frontend/src/blocks/WfNewType.astro
3. Регистрирай го в WfRender.astro. Готово — без промяна на Go код!
Какво става ако двама админа натиснат Build едновременно? Или ако админ натисне Build докато друг build работи?
Вместо да стартира build директно, Go записва заявката в опашка:
Ако 3 build-а чакат за различни страници — Go ги обединява в 1 build. Вместо 3 пъти да стартира Node.js, стартира го веднъж с всички страници. По-бързо и по-ефективно.
SQLite по подразбиране заключва ЦЯЛАТА база при писане. С WAL mode:
| Операция | Без WAL | С WAL |
|---|---|---|
| Админ чете докато build пише | ❌ "database is locked" | ✅ Работи |
| Build чете докато админ пише | ❌ "database is locked" | ✅ Работи |
| Двама пишат едновременно | ❌ Грешка | ⏳ Чака 5 сек, после пише |
Снимките живеят на ЕДНО място. Няма копия.
rsync --delete ще ИЗТРИЕ uploads ако не ги изключиш!
Правилната команда:
rsync -a --delete --exclude='images/uploads/' frontend/dist/ public_html/
--exclude='images/uploads/' запазва качените снимки при пълен rebuild.
Всеки проект с Astro има node_modules/ папка с ~200MB зависимости. При 20 проекта = 4GB дублирани файлове. PNPM решава това.
Всеки проект: собствено копие
20 проекта × 200MB = 4,000 MB
Милиони файлове на диска
npm install всеки път сваля наново
Глобален store: 1 копие на всеки пакет
1 store (200MB) + 20 × symlinks = ~220 MB
Symlinks вместо копия
pnpm install е мигновено (пакетите вече са в store)
Go build.go проверява дали pnpm е инсталиран. Ако да — ползва го. Ако не — fallback към npm. Не трябва ръчна конфигурация.
Може ли публичният сайт да е на Cloudflare CDN, а бекендът на нашия сървър? Да — има три варианта:
Най-простият. Нищо не се променя по архитектурата.
ДА. Формата на example.com (CF Pages) праща POST към cms.example.com/api/form/contact (вашият сървър). Go добавя CORS headers, които казват на браузъра: "Приемам заявки от example.com". Cloudflare НЕ прихваща и НЕ блокира тези заявки.
systemd socket activation работи нормално — Go се буди при POST, обработва формата, после пак заспива.
За повечето случаи: Вариант А (Cloudflare proxy). Нищо не се променя, само DNS. Безплатен CDN + DDoS защита.
За maximum performance: Вариант Б (CF Pages). Статичният HTML се сервира от 300+ CF data центъра по света.
За maximum сигурност: Вариант В (CF Tunnel). Сървърът е напълно скрит.
Целта: един панел за ВСИЧКИ видове сайтове. По-мощен от WordPress + ACF Pro, но без PHP, без MySQL, без plugins, без security patches.
Неограничен брой типове съдържание. Всеки тип е конфигурируем — не е нужен Go код за нов тип.
Всеки тип се дефинира така:
| Вид | Пример | Структура |
|---|---|---|
| Hierarchical | Категории, Региони | Дърво: Parent → Child → Grandchild |
| Flat | Тагове, Цветове, Размери | Плосък списък, без нива |
Всяка таксономия се свързва с определени content types:
Relationships (връзки между типове): Article → Author (team), Product → Related Products, Event → Venue. Bidirectional — ако A сочи към B, B автоматично сочи обратно.
Всички типове полета, които ACF Pro поддържа — и повече:
| Тип | Описание | ACF еквивалент |
|---|---|---|
text | Един ред текст | Text |
textarea | Много редове | Text Area |
richtext | WYSIWYG визуален editor | WYSIWYG Editor |
number | Число (min/max/step) | Number |
email | Email с валидация | |
url | URL с валидация | URL |
select | Dropdown (единичен/множествен) | Select |
checkbox | Множествен избор | Checkbox |
radio | Единичен избор | Radio Button |
toggle | Да/Не switch | True/False |
image | Качване + media picker | Image |
file | Файл (PDF, ZIP) | File |
gallery | Много снимки + drag/drop | Gallery |
link | URL + текст + target | Link |
color | Color picker (hex, rgba) | Color Picker |
date | Date picker | Date Picker |
relation | Връзка към друг item | Relationship |
taxonomy | Избор на term | Taxonomy |
repeater | Множество еднакви реда | Repeater |
flexible | Различни layouts в свободен ред | Flexible Content |
group | Вложени полета | Group |
clone | Преизползване на field group | Clone |
code | Code editor (JSON, HTML) | — |
oembed | YouTube/Vimeo от URL | oEmbed |
map | Координати (lat/lng) | Google Map |
Всеки ред е еднакъв
За: списъци, таблици, спецификации
Всеки ред е различен (избираш layout)
За: page builder, свободни layouts
Показвай/скривай полета на база стойности на други полета:
Неограничен брой езици. Всеки с различен routing:
| Routing | URL | Пример |
|---|---|---|
| none | example.com/about | Default език, без prefix |
| prefix | example.com/en/about | Поддиректория |
| subdomain | en.example.com/about | Поддомейн |
| domain | example.co.uk/about | Различен домейн |
Translation groups: Преводите се свързват чрез translation_group ID. Страница #5 (BG) и страница #12 (EN) имат същия group → системата знае, че са превод на едно и също.
hreflang тагове се генерират автоматично от Go API (коректни URL-и per routing config):
Пълен SEO контрол per страница:
| Функция | Кой я прави | Описание |
|---|---|---|
| Sitemap | Go (динамичен) | Go сервира /sitemap.xml — винаги актуален, без rebuild. Per-language sitemaps. Respects noindex и exclude_sitemap. |
| robots.txt | Go (динамичен) | Конфигурируем от Settings. Включва Sitemap URL. |
| hreflang | Go API → Astro | Автоматични alternate tags за всички езикови версии. |
| JSON-LD | Go API → Astro | Structured data per content type: Article, Product, Event, FAQPage, Person... |
| Canonical | Go API → Astro | Автоматичен canonical URL (или custom). |
| Redirects | Go | 301/302 redirects. Автоматичен при промяна на slug. Ръчни за миграция. |
| RSS Feed | Go (динамичен) | /feed.xml за типове с has_feed=true. Per-language feeds. |
| Роля | Може | Не може |
|---|---|---|
| super_admin | Всичко | — |
| admin | Съдържание, медия, builds, settings | User management |
| editor | Редактира и публикува всяко съдържание | Settings, users |
| author | Създава и редактира САМО своето | Публикуване, чуждо съдържание |
| viewer | Вижда dashboard и съдържание | Всякакви промени |
Granular permissions per content type — editor може да трие статии, но не продукти:
| Защита | Описание |
|---|---|
| CSRF tokens | На всяка форма — предпазва от cross-site заявки |
| Rate limiting | Login: 5 опита за 15 мин, после lockout 30 мин |
| Password policy | Мин. 8 символа, bcrypt хеширане |
| JWT + Refresh | Access token 24ч, refresh token 7 дни |
| 2FA (TOTP) | Google Authenticator / Authy — optional per user |
| IP whitelist | Ограничи /admin до определени IP-та (optional) |
| CSP headers | Content-Security-Policy на admin pages |
| Audit log | Кой какво е направил кога — activity_log таблица |
| Session management | Виж активни сесии, прекрати отдалечено |
| Brute force | Progressive delay при грешни пароли |
Три начина за подаване на данни към Astro:
Astro и Go на същия сървър
fetch("http://localhost:8080/api/...")
Бързо, без auth
Astro на друг сървър или CF Pages
fetch("https://cms.example.com/api/...")
С Bearer token
Offline builds, CI/CD
Go exports .data/*.json
Без мрежа
| Функция | Описание |
|---|---|
| Menu Builder | Drag/drop, nested items, per-language. Менюта: main, footer, sidebar... |
| Global Settings | Site name, logo, contact info, social links, SMTP, analytics, cookie consent. Per-language. |
| Revisions | Последните 10 версии на всяка страница. Timeline, diff view, restore. |
| Autosave | Всеки 60 сек. При reload — възстановява ако е по-ново от последния save. |
| Preview | Бутон "Preview" — временен URL с token, показва как ще изглежда преди publish. |
| Scheduled publish | publish_at / expire_at — публикувай на дата, скрий след дата. |
| Activity log | Кой какво е правил — create, update, delete, publish, login, build, upload. |
| Redirects | 301/302 management. Автоматичен при промяна на slug. |
| Import/Export | Цялото съдържание като JSON + медия в ZIP. За миграция/backup. |
| Search index | JSON index при build за client-side search. Или Pagefind интеграция. |
| Code injection | Custom HTML в head/body — per page и global. За tracking, analytics, embeds. |
| Media Library | Папки, search, drag/drop upload, auto WebP + resize при upload, bulk actions. |
| Password pages | Защитени с парола страници — Go проверява при заявка. |
Една SQLite база с 14 таблици покрива ВСИЧКО:
Вместо отделна таблица за articles, products, team, events... — всичко е в content с колона type. Custom полета са в fields_json (JSON). Block editor данни са в sections_json. SEO е в seo_json.
Това означава: добавяне на нов content type = 0 промени по базата. Само конфигурация.
| Функция | WordPress + ACF Pro | Нашият CMS |
|---|---|---|
| RAM в покой | 50-200 MB (PHP+MySQL) | 0 MB (спи) |
| Page load | 200-500ms (PHP render) | < 5ms (static HTML) |
| Сигурност | #1 хакван CMS в света | Няма PHP, няма plugins |
| Deploy | LAMP stack + 20 plugins | 1 файл + SQLite |
| Updates | WP core + plugins + PHP | 1 Go binary |
| DB backup | mysqldump | cp site.db backup.db |
| 20 сайта RAM | 1-4 GB | ~5 MB (само nginx) |
| Selective rebuild | — (не е SSG) | Само променени pages |
| Image optim | Плъгин (Imagify) | Вградено |
| 2FA | Плъгин | Вградено |
| Activity log | Плъгин | Вградено |
| Redirects | Плъгин (Redirection) | Вградено |
| Multi-language | Плъгин (WPML €199/год) | Вградено, безплатно |
| SEO | Плъгин (Yoast €99/год) | Вградено, безплатно |
| CDN ready | Плъгин (cache) | Static HTML = CDN native |
Неща, които лесно се пропускат при проектиране на CMS, но са критични при реална употреба:
| Проблем | Решение |
|---|---|
| Slug колизии — две страници с еднакъв slug | UNIQUE constraint на (slug, locale, type). При конфликт Go добавя суфикс: about → about-2 |
| Slug транслитерация — "Добре дошли" → ? | Go транслитерира кирилица автоматично: → dobre-doshli. Поддържа и Unicode (китайски, арабски) |
| Soft delete — изтрито = загубено завинаги? | НЕ. status='trashed' → кошче. Възстановяване. Автоматично изтриване след 30 дни |
| Concurrent editing — двама редактират едно | Optimistic locking: при save проверява updated_at. Ако се е променил → предупреждение "Друг потребител е направил промени" |
| Йерархични URL-и — /services/web-design/ | parent_id колона в content таблицата. Slug = пълен path от parent chain |
| Duplicate — копиране на страница | Бутон "Duplicate" → нов запис с slug-copy, status=draft, всички полета копирани |
| Content Templates | Запази layout като шаблон. При "New" → избери шаблон → попълни данни |
| Празен нов проект | setup.sh създава demo pages (Home, About, Contact) с примерни секции. Не е празна база. |
| Проблем | Решение |
|---|---|
| 404 page per език | Astro генерира 404.html per locale. nginx: error_page 404 /404.html; |
| Trailing slashes — /about vs /about/ | Консистентност: Astro генерира /about/index.html. nginx redirect /about → /about/ |
| Pagination — blog с 100 статии | Генерирай /blog/, /blog/page/2/, /blog/page/3/ при build. Go API: ?page=2&per_page=10 |
| 410 Gone | За окончателно премахнато съдържание. По-добре от 404 за SEO. Status в redirects таблицата. |
| Проблем | Решение |
|---|---|
| RTL езици (арабски, иврит) | is_rtl флаг в languages. Astro добавя dir="rtl". CSS обръща layout |
| Частични преводи — 30 от 50 стр. преведени | Конфигурируемо: скрий непреведени от sitemap/навигация ИЛИ покажи default език като fallback |
| Locale формати — дати, числа | Go API подава raw данни. Astro/frontend форматира: "5 март" (BG) vs "March 5" (EN) |
| Settings fallback | Ако site_name има стойност само за BG → EN fallback-ва към BG, не към празно |
| Language detection | Опция: auto-redirect по Accept-Language. Или: banner "Available in English" + бисквитка |
| Image alt per locale | Една снимка, различен alt text per език. Media таблица поддържа per-locale alt |
| Заплаха | Защита |
|---|---|
| XSS в WYSIWYG | Go санитайзва HTML при save. Whitelist: само безопасни тагове (p, h1-h6, a, img, strong, em, ul, ol, li, blockquote, table). Strip <script>, on* атрибути |
| SVG с JavaScript | При upload на SVG — strip <script>, on* атрибути, data: URI. SVG може да изпълни JS ако не е санитайзиран |
| CORS злоупотреба | Whitelist на позволени Origins в settings. Не echo-вай произволен Origin |
| API scraping | Rate limiting на API: 100 заявки/мин per IP. Burst tolerance за build процеса |
| Забравена парола | 1) Email с reset link (SMTP). 2) CLI: ./cms reset-password admin newpass. И двата варианта |
| Fake file upload | Не доверявай extension. Проверявай MIME type server-side (magic bytes). Макс размер per тип |
| Оптимизация | Описание |
|---|---|
| Build cache | Astro --cache flag. Кешира artifacts между builds. 2-3x по-бързо |
| Image lazy loading | loading="lazy" на всички изображения освен hero (above-fold) |
| Critical CSS | Inline critical CSS в <head>, останалият CSS async. 100/100 Lighthouse |
| Функция | Описание |
|---|---|
| Form Builder | Админ създава custom форми. Всяка: име, полета, email получател. Не само contact |
| Email notification | При submission → Go изпраща email до конфигуриран адрес. SMTP settings |
| Anti-spam | Honeypot поле (скрито CSS — ботове попълват, хора не). Optional: reCAPTCHA v3 |
| File upload | Посетители качват файлове (CV, документи). Макс размер, типове — конфигурируемо |
| CSV export | Export submissions за период. За анализ/отчети |
| Функция | Описание |
|---|---|
| Build rollback | Go пази backup на public_html преди всеки build. Бутон "Rollback" → възстановява последната работеща версия |
| Build history | Лог: кой, кога, колко страници, колко време, success/fail, error log |
| Webhooks | При publish/build → Go вика external URL. За: Slack, cache purge, deploy trigger, CDN invalidation |
| Content workflow | За екипи: Draft → Review → Approved → Published. Optional — не за всеки проект |
| Timezone | Всичко в UTC в базата. Admin показва в конфигуриран timezone. Go конвертира при показване/запис |
| Функция | Описание |
|---|---|
| Bulk operations | Избери 20 items → Publish All / Delete All / Move to Category |
| Content compare | Side-by-side сравнение на две езикови версии. Полезно при превод |
| Dashboard widgets | Последна активност, pending builds, непрочетени форми, content stats |
| Mobile admin | Responsive — работи на телефон/таблет за спешни промени |
| Onboarding | Първо влизане → wizard: site name, език, първа страница |
| Social preview | Визуален preview как ще изглежда страницата при share във Facebook/Twitter |
| Breadcrumbs | BreadcrumbList JSON-LD. Изисква parent/child hierarchy. Go API предоставя chain |
| WP Import | Tool за импортиране от WordPress XML export. Posts→content, categories→terms, images→media |
След пълен преглед на всичко описано — ето ситуациите, в които по средата на работата бихме казали "ааа, това не сме го помислили":
publish_at = "2026-04-01 09:00" — но Go не работи в 09:00, защото никой не е влязъл в админа. Кой ще промени status от "scheduled" на "published"?
Решение: При ВСЯКА заявка (форма, посетител, админ) → Go проверява: "има ли scheduled posts, чийто publish_at е минал?" → ако да → publish + needs_rebuild=1. ПЛЮС: отделен lightweight cron процес (Go binary с --cron flag) — systemd timer на всеки 5 минути проверява и тригерва build ако има промени. Той е различен от CMS процеса — не заема порт, не слуша заявки, просто проверява базата и излиза.
Не е описано КАК админът създава превод. Ето flow-то:
Решение: Global sections. Секция може да е "global" — дефинирана веднъж, включена на много страници по reference.
В sections_json: {"type": "global_ref", "data": {"ref_id": "newsletter-cta"}}
Go подменя reference-а с реалните данни при API response. Промяна на global section → needs_rebuild=1 на ВСИЧКИ страници, които я включват.
Нова таблица: global_sections (id, name, sections_json, updated_at)
| Действие | Как работи |
|---|---|
| Скрий секция (без триене) | Всяка секция има "visible": true/false. Скритите не се рендерират, но остават в JSON-а. Toggle бутон 👁 |
| Дублирай секция | Бутон [⧉] — копира секцията и я вмъква отдолу. Полезно за вариации |
| Per-language видимост | "visible_locales": ["bg","en"] — показвай тази секция САМО на BG и EN, скрий на DE. За различно съдържание per език без separate pages |
| Copy между страници | Clipboard: копирай секция от страница A, paste в страница B |
Ако seo_json.meta_title е празен → Go прилага шаблона. Ако е попълнен → ползва го директно.
Max build time: 5 минути. Ако Astro виси по-дълго → Go убива процеса (exec.CommandContext с timeout). Предотвратява зомби build-ове, които блокират queue-то.
| Функция | Описание |
|---|---|
| Къде е използвана? | За всяка снимка — бутон "Usage": списък на ВСИЧКИ страници, които я реферират. Сканира sections_json и fields_json |
| Замяна на файл | Replace image: нов файл, СЪЩИЯТ URL. Всички references обновени автоматично (защото URL-ът не се мени) |
| Unused cleanup | Бутон "Find unused media": показва файлове, които не се реферират никъде. Bulk delete |
| Duplicate detection | При upload — проверка по SHA256 hash. Ако файлът вече съществува → покажи предупреждение |
| Image crop в админа | Crop/resize без re-upload. Focal point за responsive cropping (не само center crop) |
Когато страница мине от published → draft:
Go трябва: 1) изтрий файла от public_html, 2) маркирай зависимите pages, 3) тригерни selective rebuild, 4) обнови менюта
| Проверка | Кога | Какво |
|---|---|---|
| Required fields | При Publish | Title задължително. Slug задължително. Cover image за articles (ако е required в config) |
| Slug format | При Save | Само a-z, 0-9, dash. Без special chars, без spaces. Auto-fix |
| JSON validation | При Save | sections_json и fields_json трябва да са валиден JSON. Иначе → error |
| Image dimensions | При Publish | OG image трябва да е мин 1200×630 (Facebook изискване). Warning ако не е |
| Broken links | При Build | Провери дали вътрешните линкове сочат към съществуващи страници |
| Функция | Описание |
|---|---|
| Health check endpoint | GET /health → 200 OK + {"db":"ok","uptime":"5m"}. За мониторинг (UptimeRobot и др.) |
| Backup automation | systemd timer: дневен backup на site.db + uploads. Retention: 7 daily, 4 weekly. Към backup сървър |
| Environment config | .env файл за dev/staging/production. Go чете env vars с fallback към .env. JWT_SECRET, SMTP_* и др. |
| Zero-downtime update | Нов binary → replace → systemctl restart → socket activation state запазен. 0 downtime |
| Logging | Go logs → systemd journal (journalctl -u cms-example-com). Structured JSON logging за production |
| Admin UI i18n | Самият admin панел на различни езици. User preference: "Показвай ми админа на български/English". Отделно от site languages |
| User invitation | Super admin създава user → email с invite link → новият user задава парола. Не ръчно даване на пароли |
| Изискване | Решение |
|---|---|
| Form submissions retention | Auto-delete след N дни (конфигурируемо, default 90). GDPR compliant |
| Cookie consent данни | Settings за cookie banner текст + категории (necessary, analytics, marketing). Astro компонент за banner |
| Data export per user | Ако потребител поиска данните си → export на submissions с негов IP/email |
| Activity log retention | Auto-cleanup на activity_log записи по-стари от 1 година |
Всеки проект ще има нужда от тези — трябва да са в шаблона:
| Компонент | Какво прави |
|---|---|
SEO.astro | Meta tags, OG, Twitter cards, JSON-LD, hreflang, canonical — всичко от seo_json |
Image.astro | srcset, lazy loading, WebP, alt text. Wrapper за img с оптимизации |
LanguageSwitcher.astro | Линкове към другите езикови версии. Чете от translation data |
Breadcrumbs.astro | Breadcrumb навигация + BreadcrumbList JSON-LD. Чете parent chain |
Pagination.astro | Previous/Next + page numbers. За listing pages |
ContactForm.astro | Form с honeypot, validation, submit handler. Конфигурируеми полета |
CookieConsent.astro | GDPR cookie banner. Категории, preferences, localStorage persistence |
С тези 12 допълнения спецификацията покрива:
ЕДИН файл на диска.
ID 42, sunset-beach.jpg, 3000×2000
Има: оригинал + 5 размера × 3 формата
МНОГО употреби в различни страници.
Всяка с различен: alt text, caption, crop, size
Едно и също ID, различен контекст
media/originals/42_sunset-beach.jpg — НИКОГА не се пипа. Backup, reference, re-processing.
imaging.
Обработката е при upload (веднъж), не при build (всеки път). Не трябва sharp (native Node module). Не бави build-а. По-преносимо. Go ползва pure Go библиотека за resize + CLI инструменти (cwebp, avifenc) за конвертиране.
Зависимости на сървъра: apt install webp libavif-bin poppler-utils ffmpeg
Astro компонент Image.astro генерира <picture> с всички формати:
Един файл (ID 42), 5 страници, 5 различни alt текста:
Go извлича доминиращия цвят при upload. Astro го ползва като placeholder:
| Тип | Обработка | Resize | Резултат |
|---|---|---|---|
| JPEG / PNG / GIF | Resize + WebP + AVIF + strip EXIF | ✅ | 15 файла (5 размера × 3 формата) |
| WebP (upload) | Resize + AVIF | ✅ | 10 файла |
| SVG | Санитизация (strip JS) | ❌ | Само оригинал (вектор) |
| Thumbnail от стр.1 (pdftoppm) | ❌ | Оригинал + 1 thumbnail | |
| Video | Poster кадър (ffmpeg) | ❌ | Оригинал + 1 poster |
| Документи | Нищо | ❌ | Само оригинал |
| Функция | Как работи |
|---|---|
| Къде е използвана? | Бутон "Usage" → Go сканира sections_json и fields_json за media_id → списък на всички страници |
| Замяна на файл | Replace: нов файл, СЪЩИЯТ ID и URL. Go регенерира всички размери. Всички references работят (URL не се мени) |
| Unused cleanup | "Find unused" → файлове нереферирани никъде → bulk delete с preview |
| Duplicate detection | SHA256 хеш при upload. Ако съществува → предупреждение + линк |
| Crop в админа | Crop/resize без re-upload. + Focal point за responsive cropping |
| Папки | Виртуални (в DB, не физически). За организация: General, Team, Products, Blog... |
| Bulk upload | Drag/drop 20 файла наведнъж. Progress bar per файл |
| Disk quota | Per-project лимит: "Media: 2.1 GB / 5.0 GB (42%)". Warning 80%, block 100% |
Физическият filename НИКОГА не се сменя — ще счупи кеширани URL-и, CDN, shared links.
Display name (title) в DB може да се сменя свободно — показва се в admin, не влияе на URL.
Ако SEO изисква конкретно име в URL-а → Go може да сервира /media/красив-залез.webp като alias за /media/42_sunset-beach/large.webp
URL-ът е базиран на ID → стабилен завинаги, независимо от rename.
КРИТИЧНО: какво НЕ МОЖЕ static site и как го решаваме:
| Не може | Решение |
|---|---|
| Реално-времеви данни (цени, наличности) | Go API endpoint + JS fetch при зареждане. Socket activation буди Go |
| Login / member area | Go API за auth + JS cookie/localStorage. Или Go сервира защитени страници с парола |
| Динамично филтриране/сортиране | Client-side JS (Alpine.js/vanilla). Или генерирай отделна page per филтър |
| Кошница за пазаруване | Snipcart (JS overlay) или Stripe Checkout (redirect). Go обработва webhook |
| Персонализация per потребител | Astro islands + Go API. Static page + JS попълва персонализирани секции |
| Коментари | Giscus (GitHub-based), или Go API + socket activation за custom коментари |
| Real-time search | Pagefind (client-side, offline, бърз). Или Go API /api/search?q=term |
| Функция | Описание |
|---|---|
| TypeScript типове от конфиг | Go автоматично генерира TS interfaces от content_types: ./cms generate-types → frontend/src/types/*.ts. Type safety между CMS и Astro |
| Developer CLI | ./cms new-type "product", ./cms new-section "pricing", ./cms generate-types, ./cms import wordpress, ./cms backup |
| Admin sidebar групиране | При 50 content types → групиране: Content, Shop, System. + Favorites, Search. config: "admin_group": "Shop" |
| In-app notifications | 🔴 Badge при нова форма, toast при build complete/fail, ⚠ при concurrent edit, warning при quota 80% |
| Caching стратегия | HTML: 1h + must-revalidate. CSS/JS: 1y immutable (content hash). Media: 30d. /admin,/api: no-store |
| Accessibility (a11y) | Admin: keyboard nav, ARIA, focus management. Site: alt text enforcement, heading hierarchy, semantic HTML, skip-to-content |
| Content embedding | Вмъкни content от друга страница (transclusion). + oEmbed за YouTube/Twitter/Instagram автоматичен embed |
| Internal notes | Бележки между editors per page: "@John провери превода". Видими само в админа |
| E-commerce | Product variants (size/color repeater), price+currency, sale price, stock. Checkout: Snipcart/Stripe |
| Мащаб | Проблем | Решение |
|---|---|---|
| 10k pages | Full rebuild: 5-10 мин | Selective build. Full rebuild само при deploy |
| 10k pages | Admin dashboard бавен | Pagination + search + filters. Lazy load |
| 100k media | Папка с 100k файла | Поддиректории по ID: media/0-999/, media/1000-1999/ |
| 50 content types | Sidebar неизползваем | Групиране, favorites, search в sidebar |
| 100 builds/ден | Queue натоварен | Merge pending builds. Debounce 5 сек |
Max DB size: 281 TB. Max rows: 1 билион. За нашия use case — практически неограничено. 10k pages с 50 секции всяка = ~50MB database.
| Конфликт | Решение |
|---|---|
| Unix sockets vs CF Pages CF прави HTTP от internet → трябва TCP порт |
Два режима: local frontend → Unix socket. Remote frontend (CF Pages) → TCP порт + CORS. setup.sh пита |
| Go dynamic sitemap + Go спи Бот ходи на /sitemap.xml → буди Go всеки път |
nginx кешира /sitemap.xml, /robots.txt, /feed.xml за 5 мин. Go се буди само при cache miss |
| Cron + CMS = два процеса пишат в SQLite | WAL mode + busy_timeout=5000. Cron: INSERT в build_queue → EXIT. CMS процесва queue-то. Ако CMS спи → cron може сам да процесва |
| Build timeout 5 мин vs 10k pages Full rebuild > 5 мин |
Timeout е конфигурируем: build_timeout_minutes: 5 в settings. Per project |
| Alt text: per usage + per locale media.alt_text е 1 поле |
Сменяме с alt_text_json: {"bg":"Залез","en":"Sunset"}. Override: section alt > media alt per locale > media alt default |
| Global section change = rebuild 200 pages? | Да, но с warning: "This will rebuild 200 pages. Continue?" Global sections рядко се променят |
| parent_id + slug_prefix /blog/parent/child — объркващо |
parent_id е само за type "page". Types с slug_prefix НЕ поддържат parent_id |
| PNPM PATH в systemd systemd не source-ва .bashrc |
Service файл: Environment=PATH=/usr/local/bin:/usr/bin:/root/.local/share/pnpm |
| rsync --exclude: uploads/ vs media/ Astro може да генерира в media/ |
Uploads в public_html/uploads/. Astro ползва _astro/. Различни пътища = 0 конфликт |
В секция 24 казахме media е в public_html/media/. СМЕНЯМЕ на public_html/uploads/ за да няма конфликт с Astro assets.
rsync при full rebuild: rsync -a --delete --exclude='uploads/' frontend/dist/ public_html/
| Елемент | Детайл |
|---|---|
| Sidebar лого | WebFactor logo (бяло) горе ляво + "CMS" label |
| Login страница | WebFactor лого + "Content Management System" + тъмен фон |
| Favicon | WebFactor icon |
| Login footer | "Powered by WebFactor" |
| Скрито от клиента | Никъде "Go", "Astro", "SQLite" — клиентът не трябва да знае технологията |
Референции: Linear, Vercel dashboard, Payload CMS, Notion
| UI елемент | Описание |
|---|---|
| Toast notifications | Горе-дясно, slide-in, auto-dismiss 5 сек. Persist за errors. Green/red/orange |
| Skeleton loading | Сиви пулсиращи правоъгълници докато зарежда. НЕ spinner |
| Empty states | Илюстрация + текст: "No articles yet. Create your first one." + CTA бутон |
| Keyboard shortcuts | Cmd+K search, Cmd+S save, Cmd+N new, Escape close |
| Dark/Light toggle | Бутон в sidebar footer. Preference в localStorage |
| Responsive | Sidebar → hamburger на mobile. Touch-friendly бутони (44px min) |
| Ситуация | UI отговор |
|---|---|
| Save fail | Toast (red): "Failed to save. Check your connection." |
| Build fail | Toast (red) + error log в modal при клик |
| Upload fail | Toast (red): "Upload failed: file too large (max 10MB)" |
| Session expired | Redirect → login: "Session expired. Please log in again." |
| Concurrent edit | Modal: "John updated this 2 min ago. View changes / Overwrite / Cancel" |
| DB busy | Retry 3× (busy_timeout), после Toast: "Database busy, please try again" |
Ред на имплементация — от MVP до пълна система:
В DB таблицата section_types — source of truth. Admin може да ги създава/редактира в UI. При нов проект — seed от JSON файл. Astro НЕ знае за DB-то — има component map в WfRender.astro. Нов section type = 1) добави в DB, 2) създай .astro компонент, 3) добави в map.
Секция 21.3 изброи типовете полета. Тук описваме ПЪЛНАТА система — всяка настройка, всеки edge case, всеки UI елемент. Ако се имплементира по тази секция — нищо няма да липсва.
| Настройка | Тип | Описание |
|---|---|---|
key | string | Уникален идентификатор. Snake_case: hero_title |
label | string | Показвано име: "Hero Title" |
type | string | Тип: text, image, repeater, и т.н. |
instructions | string | Помощен текст под label-а. HTML позволен |
required | boolean | Задължително при Publish. Validation error ако е празно |
default_value | any | Стойност по подразбиране при нов запис |
placeholder | string | Сив текст в празен input |
conditional | object | Покажи/скрий по правила (виж 29.6) |
wrapper_width | number | Ширина в % (25, 33, 50, 75, 100). За side-by-side layout |
wrapper_class | string | Custom CSS класове |
| Настройка | Описание |
|---|---|
prepend | Текст ПРЕД input-а. Пример: "$" за цена, "https://" за URL |
append | Текст СЛЕД input-а. Пример: "лв.", "kg", "px" |
max_length | Макс символи. Показва брояч "45/60" |
| Настройка | Описание |
|---|---|
min | Минимална стойност |
max | Максимална стойност |
step | Стъпка (1, 0.01, 5, 100...) |
prepend / append | Като text |
| Настройка | Описание |
|---|---|
rows | Височина (брой редове). Default: 4 |
max_length | Макс символи с брояч |
| Настройка | Описание |
|---|---|
toolbar | full (всички бутони) или basic (bold, italic, link, lists) |
media_upload | Покажи бутон за вмъкване на медия (bool) |
max_length | Макс символи (strip HTML за броене) |
| Настройка | Описание |
|---|---|
choices | Масив от опции: [{"value":"sm","label":"Small"},{"value":"lg","label":"Large"}] |
allow_null | Празна опция "— Select —" (bool) |
multiple | Множествен избор (bool) |
searchable | Enhanced UI с търсене (като Select2). За 20+ опции |
| Настройка | Описание |
|---|---|
preview_size | Thumbnail size в админа: thumb, medium |
min_width / min_height | Минимални px. Validation при избор |
max_size | Макс MB. Пример: 5 |
allowed_types | MIME типове: jpg,png,webp |
| Настройка | Описание |
|---|---|
min / max | Мин/макс брой снимки. Validation |
min_width / min_height | Per image валидация |
allowed_types | MIME типове |
insert | beginning или end — къде отиват нови |
| Настройка | Описание |
|---|---|
related_type | Кой content type да се показва: article, product, team |
related_taxonomy | Филтър по таксономия |
filters | Какво може да търси: ["search","type","taxonomy"] |
show_thumbnail | Покажи cover image в dropdown (bool) |
min / max | Мин/макс избрани items |
bidirectional | Auto-update обратната връзка (bool) |
| Настройка | Описание |
|---|---|
display_format | Как се показва: d.m.Y, m/d/Y |
return_format | Как се запазва: Y-m-d (ISO) |
first_day | Начало на седмицата: 1=Понеделник, 0=Неделя |
| Настройка | Описание |
|---|---|
enable_opacity | rgba vs hex (bool). rgba дава прозрачност |
presets | Палитра от предефинирани цветове за бърз избор |
| Настройка | Описание |
|---|---|
min / max / step | Граници и стъпка |
prepend / append | Текст пред/след |
| Настройка | Описание |
|---|---|
sub_fields | Масив от полета вътре (ВСЕКИ тип, включително друг repeater) |
min / max | Мин/макс реда. Validation |
button_label | Текст на бутона: "+ Add Team Member" вместо "+ Add Row" |
layout | block (пълна ширина) или table (компактна таблица) или row (inline) |
collapsed | Кое sub_field да се показва като заглавие при collapsed ред (напр. "name") |
Repeater вътре в repeater вътре в repeater. Без лимит на дълбочината. Пример: Courses → Lessons → Chapters → Exercises. ACF Pro го поддържа. Ние също.
| Настройка | Описание |
|---|---|
layouts | Масив от layout дефиниции (всеки layout = name + label + icon + sub_fields) |
min / max | Общ мин/макс layouts на цялото поле |
button_label | "+ Add Section", "+ Add Block"... |
Per-layout min/max | Макс 1 Hero, макс 3 Gallery, без лимит на Text |
sections_json на страница е точно Flexible Content поле. Всяка "секция" е layout. Разликата: Flexible Content може да е ВЛОЖЕН field (вътре в repeater, group, или друг flexible). Секционният editor е top-level. И двете работят по същия начин.
Стойности: 25%, 33%, 50%, 66%, 75%, 100% (default). CSS flexbox row с flex-wrap.
Тези полета НЕ пазят данни — организират визуално другите полета:
В ACF Pro, Field Group = колекция от полета, прикрепена към определени типове. При нас — fields_json е в самия content type. НО можем и external field groups:
| Функция | ACF Pro | WebFactor CMS |
|---|---|---|
| Headless API | Плъгин (WPGraphQL) | Вградено (REST) |
| TypeScript типове | Ръчно | Auto-generated |
| Multi-language fields | WPML плъгин | Вградено |
| SEO полета | Yoast плъгин | Вградено |
| Media optimization | Плъгин | WebP/AVIF при upload |
| Focal point crop | Не | Вградено |
| Build/deploy | — (PHP dynamic) | Selective SSG build |
| 0 RAM в покой | 50-200MB 24/7 | 0 MB |
| DB ефективност | 2 meta записа per field | 1 JSON колона per item |
| Section видимост per locale | Не | visible_locales |
| Global reusable sections | Reusable blocks (limited) | global_ref с auto-rebuild |
| Build dependencies | — (не е SSG) | Декларативни depends_on |
| Repeater performance | Бавно при 10+ реда | JSON = бързо при 1000+ |
ACF Pro записва ВСЯКО подполе на ВСЕКИ ред като отделен запис в wp_postmeta. Product с 10 specs × 2 fields = 20 DB записа. При нас: 1 JSON колона fields_json = 1 запис. SELECT е 1 заявка вместо 20. При 100 продукта: 100 заявки вместо 2000.
Единственият недостатък: не можеш да правиш SQL WHERE по JSON подполета ефективно. Решение: за филтриране — ползвай таксономии (бързи index-и), не custom fields.
29 секции. Go + Astro + SQLite + nginx + systemd. 0 ресурси в покой. Selective build. Unix sockets. 28 типа полета с пълни настройки per тип (prepend/append, min/max, wrapper_width, conditional logic, nested repeaters, flexible content layouts). Build queue. PNPM. Cloudflare-ready. Неограничени езици. Пълно SEO. 5 роли + 2FA. Медия с focal point + WebP/AVIF. TypeScript типове. CLI. WebFactor брандинг. 6-фазов roadmap. Всичко документирано.
Нужен софтуер: nginx, Node.js (pnpm), rsync, systemd, webp, libavif-bin, poppler-utils, ffmpeg
НЕ трябва: Docker, MySQL, Go compiler*, PHP, Redis, sharp, WPML, Yoast, ACF Pro
* Go compiler трябва само за компилация. Може на друг компютър.