Go + Astro CMS

Как работи системата за сайтове, която не яде ресурси когато не се ползва

1 Какво е това накратко?

Система за правене на уебсайтове. Състои се от две части:

Go Backend (бекенд)

Админ панел, където редактираш съдържание. Написан на езика Go. Компилира се до един файл. Пази данните в SQLite — обикновен файл на диска, не е нужен отделен database сървър.

Astro Frontend (фронтенд)

Генератор за сайта. Взема данните от Go и създава готови HTML файлове. Написан на JavaScript/Astro. Работи само при "Build" — после спира.

💡 Ключовата идея

Публичният сайт са готови HTML файлове. Когато посетител отвори сайта, се чете файл от диска — никакъв код не се изпълнява. Не работи нито Go, нито Node.js, нито PHP, нито нищо друго. Само nginx подава файла.

2 Кои програми участват?

Програма Какво прави Кога работи 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
⏰ 99% от времето

Работи САМО nginx (~5 MB). Всичко останало спи. Ако имаш 20 сайта на сървъра — пак само nginx работи, защото всички Go процеси спят. Общо ~5 MB RAM за 20 сайта.

3 Какво трябва на сървъра?

Софтуер За какво Как се инсталира
nginx Уеб сървър (подава файлове, proxy) apt install nginx
Node.js Нужен за Astro build (генериране на HTML) apt install nodejs npm
rsync Копира генерираните файлове в публичната папка apt install rsync
systemd Управлява Go процеса (буди/спира) Вече е на всеки Linux
❓ Трябва ли Go compiler на сървъра?

Не задължително. Go binary-то може да се компилира на друг компютър и да се копира готово. Компилираш на лаптопа си с GOOS=linux go build и качваш 1 файл. Но ако имаш Go на сървъра — може и там.

❓ Трябва ли Node.js да работи постоянно?

НЕ. Node.js се стартира САМО когато админът натисне "Build". Работи 5-30 секунди (генерира HTML файловете) и спира. Не работи постоянно. Не яде RAM когато не се ползва.

❓ Трябва ли Docker?

НЕ. Няма Docker, няма контейнери. Всичко работи директно на операционната система.

❓ Трябва ли отделен database сървър?

НЕ. Базата данни е SQLite — един файл (site.db) вътре в папката на проекта. Няма MySQL, PostgreSQL, нищо друго. Файлът може да се копира на USB, по email — това е цялата база.

4 Какво се случва при различни ситуации

Ситуация А: Посетител отваря страница

Някой пише https://example.com/about в браузъра.

Браузърът праща заявка към nginx nginx получава: "Искам /about"
nginx проверява правилата си /about — не започва с /admin или /api → значи е статичен файл
nginx търси файла на диска Търси: public_html/about/index.html → Намерен!
nginx връща файла HTML файлът отива при браузъра. Готово.
Време: < 5ms Go: Не участва. Не се буди. Не знае. Node.js: Не участва. Не се буди. Не знае. RAM: 0 допълнителна (nginx вече работи)

Ситуация Б: Посетител праща контактна форма

Посетител попълва форма за контакт и натиска "Изпрати".

Браузърът праща POST заявка POST /api/form/contact с данни от формата (име, имейл, съобщение)
nginx получава заявката /api/* → не е статичен файл, трябва Go. Пренасочва към порт 8080.
systemd буди Go systemd слуша порт 8080. Вижда връзка → стартира Go binary-то. Отнема ~100ms (0.1 секунда). Посетителят не усеща забавяне.
Go обработва формата Проверява rate limit (макс 1 заявка на 30 сек от един IP). Записва данните в SQLite таблица "submissions".
Go връща отговор {"ok": "saved"} → Браузърът показва "Съобщението е изпратено!"
Go чака 120 секунди... Ако няма нови заявки за 2 минути → Go спира сам → 0 RAM, 0 CPU. Ако дойде нова заявка → таймерът се нулира.
✅ Формите работят дори когато Go спи!

systemd автоматично буди Go при заявка. Посетителят не знае, че Go е спял. Вижда нормална бърза реакция.

Ситуация В: Админът влиза в панела

Админът отваря https://example.com/admin/

nginx получава /admin/ заявка /admin/* → пренасочва към порт 8080
systemd буди Go (ако спи) Go стартира за ~100ms. Ако вече работи — стъпката се пропуска.
Go проверява дали е логнат Търси JWT cookie в браузъра. Няма cookie → показва login страница.
Админът въвежда потребител и парола Go проверява с bcrypt (защитен алгоритъм за пароли). Ако е правилно → създава JWT token и го записва в cookie.
Админът вижда Dashboard Списък с всички страници. Може да създава, редактира, трие.

Докато админът работи (кликва, навигира) — всяко действие нулира idle таймера. Go не спи докато някой го ползва.

Ситуация Г: Админът редактира и публикува страница

Админът отваря страница за редакция /admin/edit/5 → Go зарежда страница #5 от SQLite и показва формата.
Админът променя съдържанието и натиска Save Go записва промените в SQLite. Маркира страницата: needs_rebuild = 1 (трябва rebuild).
Админът натиска Build Go проверява кои страници имат needs_rebuild = 1.
Go стартира Astro build Изпълнява командата npx astro build с указание кои страници да се генерират. Node.js стартира, Astro работи.
Astro генерира HTML файлове Astro пита Go API: "Дай ми данните за всички страници" → Go връща JSON → Astro създава HTML файлове САМО за променените страници.
rsync копира файловете Новите HTML файлове се копират в public_html/ (без да се трият останалите).
Готово! Node.js спира. Go маркира страниците: needs_rebuild = 0. Посетителите вече виждат новото съдържание.
⚡ Selective Build — защо е бързо

Ако имаш 50 страници и промениш само 2 — Astro генерира САМО тези 2. Не ги прави всичките 50. Затова отнема 5 секунди вместо 30+.

Full rebuild (бутон "Full rebuild") генерира ВСИЧКИ страници. Ползва се при промяна на дизайн или при начален deploy.

5 Как точно работи "заспиването"

Това е най-важната функционалност. Ето я подробно:

Какво е systemd socket activation?

Вместо Go да слуша порт 8080 постоянно (и да яде RAM), systemd (вградена програма в Linux) слуша вместо него. Когато дойде заявка — systemd стартира Go и му подава връзката.

Конфигурация — 2 файла: cms.socket — казва на systemd: "Слушай порт 8080" ┌──────────────────────────────────────┐ │ [Socket] │ │ ListenStream=127.0.0.1:8080 │ │ Accept=no │ └──────────────────────────────────────┘ cms.service — казва на systemd: "Когато дойде връзка, стартирай това" ┌──────────────────────────────────────┐ │ [Service] │ │ ExecStart=/var/www/.../backend/cms │ │ Restart=on-failure │ └──────────────────────────────────────┘

Idle timeout — как Go решава кога да спре

Таймлайн: 0:00 Заявка пристига → systemd буди Go → таймер: 120 сек 0:05 Нова заявка → таймер НУЛИРАН: 120 сек 0:30 Нова заявка → таймер НУЛИРАН: 120 сек 0:45 Нова заявка → таймер НУЛИРАН: 120 сек ... 5:00 Последната заявка → таймер: 120 сек 5:30 ... тишина ... таймер: 90 сек 6:00 ... тишина ... таймер: 60 сек 6:30 ... тишина ... таймер: 30 сек 7:00 120 сек без заявки → Go СПИРА → 0 RAM, 0 CPU ... (часове/дни по-късно) Нова заявка → systemd буди Go → работи отново

Специален случай: Build

Ако Go стартира Astro build (5-30 секунди), idle таймерът се паузира. Иначе може Go да спре по средата на build-а. След build-а — таймерът се възобновява.

6 Файлова структура

Цялият проект живее в една папка. Ето какво има вътре:

/var/www/example.com/ ← цялата папка на проекта │ ├── backend/ ← Go код (CMS сървър) │ ├── main.go ← входна точка, конфигурация │ ├── server.go ← HTTP сървър, socket activation │ ├── db.go ← база данни, таблици │ ├── auth.go ← login, пароли, JWT │ ├── admin.go ← админ панел │ ├── api.go ← API (данни за Astro) │ ├── build.go ← стартиране на Astro build │ ├── media.go ← качване на файлове │ ├── public.go ← обработка на форми │ ├── cms ← компилиран binary (~15MB) │ └── embed/ ← вградени в binary-то │ ├── templates/ ← HTML за админ панела │ └── static/ ← CSS и JS за админа │ ├── frontend/ ← Astro код (генератор на сайта) │ ├── astro.config.mjs ← Astro конфигурация │ ├── package.json ← Node.js зависимости │ └── src/ │ ├── pages/ ← маршрути (URL-и) │ ├── blocks/ ← визуални блокове │ └── layouts/ ← HTML структура │ ├── data/ ← база данни │ └── site.db ← SQLite файл (ЦЯЛАТА база) │ ├── public_html/ ← публичният сайт │ ├── index.html ← начална страница │ ├── about/index.html ← /about │ ├── contacts/index.html ← /contacts │ └── images/uploads/ ← качени картинки │ └── deploy/ ← конфигурации за сървъра ├── setup.sh ← инсталира всичко с 1 команда ├── cms.socket ← systemd socket ├── cms.service ← systemd service └── nginx.conf ← nginx конфигурация

7 Кой процес кога работи

Ситуация nginx Go CMS Node/Astro SQLite
Посетител чете страница ✅ сервира HTML 💤 спи 💤 спи — файл
Посетител праща форма ✅ proxy ✅ буди се 💤 спи ✅ пише
Админ влиза ✅ proxy ✅ буди се 💤 спи ✅ чете
Админ запазва страница ✅ proxy ✅ работи 💤 спи ✅ пише
Админ натиска Build ✅ proxy ✅ стартира build ✅ билдва HTML ✅ Go чете
Покой (99% от времето) ✅ слуша ❌ не съществува ❌ не съществува — файл

8 Пълна схема

СЪРВЪР ┌─────────────────────────────────────────────────────────────────┐ │ │ │ ┌──────────┐ ┌─────────────────────────────────────────┐ │ │ │ │ │ nginx (винаги работи) │ │ │ │ Посетител├────►│ │ │ │ │ │ │ /* ──► public_html/ (статични HTML) │ │ │ └──────────┘ │ │ │ │ │ /admin/* ──┐ │ │ │ ┌──────────┐ │ /api/* ──┤ proxy към порт 8080 │ │ │ │ │ └─────────────┼───────────────────────────┘ │ │ │ Админ ├──────────────────►│ │ │ │ │ ▼ │ │ └──────────┘ ┌─────────────────────────────────────────┐ │ │ │ systemd (слуша порт 8080) │ │ │ │ │ │ │ │ Заявка? ──► Стартирай Go binary │ │ │ └──────────────────┬──────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────┐ │ │ │ Go CMS (on-demand) │ │ │ │ │ │ │ │ • Админ панел (login, edit, save) │ │ │ │ • API (данни за Astro) │ │ │ │ • Форми (contact, newsletter) │ │ │ │ • Media upload │ │ │ │ • Build trigger │ │ │ │ │ │ │ │ SQLite: data/site.db │ │ │ │ │ │ │ │ Idle 120 сек → СПИРА │ │ │ └──────────────────┬──────────────────────┘ │ │ │ │ │ (при Build) │ │ │ ▼ │ │ ┌─────────────────────────────────────────┐ │ │ │ Node.js / Astro (при build) │ │ │ │ │ │ │ │ 1. Fetch данни от Go API │ │ │ │ 2. Генерирай HTML файлове │ │ │ │ 3. rsync → public_html/ │ │ │ │ 4. СПРИ (готово) │ │ │ └─────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘

9 Инсталация от нулата

Стъпка 1: Подготви сървъра

$ apt update $ apt install nginx nodejs npm rsync # Проверка: $ nginx -v # nginx version: nginx/1.x $ node --version # v18.x или по-нова

Стъпка 2: Копирай проекта на сървъра

$ scp -r project/ root@server:/var/www/example.com/

Стъпка 3: Компилирай Go binary

# Вариант А: На сървъра (ако има Go) $ cd /var/www/example.com/backend $ go build -o cms . # Вариант Б: На лаптопа (cross-compile) $ GOOS=linux GOARCH=amd64 go build -o cms . $ scp cms root@server:/var/www/example.com/backend/

Стъпка 4: Инсталирай Node зависимости

$ cd /var/www/example.com/frontend $ npm install # Инсталира САМО astro (без native модули, бързо)

Стъпка 5: Пусни setup.sh

$ cd /var/www/example.com/deploy $ ./setup.sh example.com 8080 # Това прави автоматично: # 1. Създава systemd socket (слуша порт 8080) # 2. Създава systemd service (стартира Go при заявка) # 3. Конфигурира nginx (static + proxy) # 4. Генерира JWT secret

Стъпка 6: SSL сертификат

$ certbot --nginx -d example.com

Стъпка 7: Влез в админа

Отвори: https://example.com/admin/ Login: admin / admin123 # Създай страници → натисни Build → сайтът е жив!

10 Преместване на друг сървър

Копирай цялата папка scp -r /var/www/example.com/ root@new-server:/var/www/
Всичко е вътре: код, база данни, качени файлове, генериран сайт.
На новия сървър: инсталирай nginx, node, rsync apt install nginx nodejs npm rsync
Пусни setup.sh cd deploy && ./setup.sh example.com 8080
Готово Сайтът работи на новия сървър. Същите данни, същите потребители, всичко.

11 Множество сайтове на един сървър

Сайт Порт systemd socket ─────────────────────── ────── ───────────────────────── example.com 8080 example-com-cms.socket portfolio.bg 8081 portfolio-bg-cms.socket shop.store 8082 shop-store-cms.socket gallery.art 8083 gallery-art-cms.socket Всеки сайт има собствена: • Папка (/var/www/domain/) • Go binary • SQLite база • Astro frontend • nginx конфигурация • systemd socket + service В покой: ВСИЧКИ Go процеси спят. RAM = само nginx ≈ 5 MB за ВСИЧКИ сайтове.

12 Блок система (секции)

Съдържанието на всяка страница се състои от блокове (секции). Всеки блок е визуален елемент:

Блок Описание Файл
hero Голям банер с заглавие, подзаглавие, фоново изображение WfHero.astro
text Текстов блок с HTML съдържание WfText.astro
image Картинка с описание WfImage.astro
gallery Мрежа от картинки (галерия) WfGallery.astro
contact Контактна форма (име, имейл, съобщение) WfContact.astro

В базата данни блоковете се пазят като JSON:

[ { "type": "hero", "data": { "title": "Добре дошли", "image": "/images/uploads/hero.jpg" } }, { "type": "text", "data": { "content": "<p>Ние сме компания...</p>" } }, { "type": "gallery", "data": { "images": [ { "url": "/images/uploads/1.jpg" }, { "url": "/images/uploads/2.jpg" } ] } } ]

Добавяне на нов тип блок:

  1. Създай frontend/src/blocks/WfNewBlock.astro
  2. Импортирай го в WfRender.astro
  3. Ползвай го в страница: {"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-то. Може да се направи на друг компютър.

13 Selective Build с зависимости

Ако промениш една статия, тя може да се показва на 15 други места — homepage, listing страница, sidebar. Простият selective build ще ребилдне само статията, но не и тези 15 страници. Как се решава?

Декларативни зависимости

Всеки тип секция декларира от какво зависи в конфигурационен файл:

{ "type": "latest_articles", "label": "Последни статии", "depends_on": ["article"] ← зависи от колекция "article" }

Когато Go запази статия:

Маркира самата статия UPDATE pages SET needs_rebuild=1 WHERE id=5
Сканира ВСИЧКИ страници за зависимости Търси страници, които имат секция от тип "latest_articles" (защото тя зависи от "article"). Намира: homepage, blog listing, sidebar.
Маркира зависимите страници UPDATE pages SET needs_rebuild=1 WHERE id IN (1, 12, 33)
Build ги ребилдва всичките Вместо 1 страница — ребилдва 4 (статията + 3 зависими). Все пак е 4, не 50.

Три нива на зависимости

Ниво Пример Какво се случва
Section Секция "latest_articles" зависи от колекция "article" Промяна на статия → rebuild на всички страници с тази секция
Page Страница A включва данни от страница B Промяна на B → rebuild на A и B
Global Промяна на настройки, навигация, layout Пълен rebuild на всички страници
✅ Автоматично, без ръчна конфигурация

Зависимостите се дефинират ВЕДНЪЖ в section-types.json. После Go автоматично знае кои страници да маркира при всяка промяна. Админът не трябва да мисли за това.

14 Unix сокети вместо портове

При много сайтове, ръчното управление на портове (8080, 8081, 8082...) е проблем. Unix socket-ите го решават изцяло.

TCP портове (старо)

Всеки сайт = уникален порт.

Ръчно трябва да помниш кой порт е за кого.

Потенциални конфликти.

ListenStream=127.0.0.1:8080

proxy_pass http://127.0.0.1:8080;

Unix сокети (ново)

Всеки сайт = файл с име от домейна.

Автоматично уникално, нищо за помнене.

Невъзможни конфликти.

ListenStream=/run/cms/example-com.sock

proxy_pass http://unix:/run/cms/example-com.sock;

20 сайта — без нито един порт номер: /run/cms/example-com.sock /run/cms/portfolio-bg.sock /run/cms/shop-store.sock /run/cms/gallery-art.sock ... Всеки socket файл се създава автоматично от setup.sh Името = домейнът с тирета вместо точки По-бързо от TCP (няма мрежов overhead за localhost)
❓ Как systemd socket activation работи с Unix socket?

Абсолютно по същия начин. systemd слуша socket файла вместо порт. Когато дойде заявка → буди Go. Когато Go спре → socket файлът остава. Следващата заявка → Go пак се буди. Единствената разлика е файлов път вместо порт номер.

15 Преместване на сайт

Целият проект е една папка. Преместването е 3 стъпки:

Копирай папката на новия сървър rsync -az /var/www/example.com/ root@new-server:/var/www/example.com/
Копира: Go binary, Astro код, SQLite база, качени снимки, генериран HTML. Всичко.
Пусни setup.sh на новия сървър cd /var/www/example.com/deploy && ./setup.sh example.com
Автоматично създава: systemd socket/service, nginx config, SSL сертификат.
Насочи DNS-а Промени A record на домейна към IP-то на новия сървър. Готово.

Какво прави setup.sh автоматично:

ДействиеРъчно?
Създава systemd socket файлАвтоматично
Създава systemd service файлАвтоматично
Генерира nginx конфигурацияАвтоматично
Включва nginx сайта (symlink)Автоматично
Пуска certbot за SSLАвтоматично
Тества nginx и reload-ваАвтоматично
DNS промянаРъчно (при регистратора)
📦 Какво се копира, какво не

КОПИРА СЕ (от старата машина): Go binary, Astro код, SQLite база (1 файл), качени снимки, генериран HTML, deploy скриптове.

НЕ СЕ КОПИРА (генерира се на място): systemd файлове, nginx конфиг, SSL сертификат. Всичко това setup.sh създава автоматично.

16 Универсален админ панел (ACF Pro стил)

Вместо да редактираш JSON на ръка, админът предоставя визуален editor — като ACF Pro в WordPress, но без WordPress.

Как работи

Конфигурационен файл section-types.json дефинира какви секции и полета съществуват. Админ панелът чете този файл и генерира формите автоматично.

section-types.json (пример): { "sections": [ { "type": "hero", "label": "Hero банер", "fields": [ {"key": "title", "type": "text", "label": "Заглавие", "required": true}, {"key": "subtitle", "type": "textarea", "label": "Подзаглавие"}, {"key": "image", "type": "image", "label": "Фон"}, {"key": "cta", "type": "link", "label": "Бутон"}, {"key": "overlay", "type": "color", "label": "Overlay"} ] }, { "type": "features", "label": "Features Grid", "fields": [ {"key": "title", "type": "text", "label": "Заглавие"}, {"key": "columns", "type": "select", "label": "Колони", "options": [2,3,4]}, {"key": "items", "type": "repeater", "label": "Елементи", "fields": [ {"key": "icon", "type": "image", "label": "Икона"}, {"key": "title", "type": "text", "label": "Заглавие"}, {"key": "text", "type": "richtext", "label": "Описание"} ]} ] } ] }

Поддържани типове полета

ТипОписаниеАналог в ACF
textЕдин ред текстText
textareaМного редовеText Area
richtextWYSIWYG editor (визуално форматиране)WYSIWYG Editor
numberЧислоNumber
selectDropdown менюSelect
checkboxДа/НеTrue/False
imageКачване + media pickerImage
galleryМного снимки с drag/dropGallery
linkURL + текст + targetLink
colorИзбор на цвятColor Picker
dateДатаDate Picker
repeaterМножество реда с подполетаRepeater
groupВложени полетаGroup

Как изглежда в админа

Визуален section editor: ┌─ Hero банер ──────────────────────── [≡] [▾] [×] ─┐ │ │ │ Заглавие: [Добре дошли__________________] │ │ Подзаглавие: [На нашия сайт________________] │ │ Фон: [📷 hero.jpg ] [Избери/Качи] │ │ Бутон: [Научи повече] [/about ] │ │ Overlay: [■ rgba(0,0,0,0.4)] │ │ │ └─────────────────────────────────────────────────────┘ ┌─ Features Grid ──────────────────── [≡] [▾] [×] ─┐ │ │ │ Заглавие: [Нашите услуги] │ │ Колони: [3 ▼] │ │ Елементи: │ │ ┌─ #1 ───────────────────────── [≡] [×] ──┐ │ │ │ Икона: [📷 icon1.svg] │ │ │ │ Загл.: [Уеб дизайн] │ │ │ │ Текст: [Правим красиви сайтове...] │ │ │ └──────────────────────────────────────────┘ │ │ [+ Добави елемент] │ │ │ └─────────────────────────────────────────────────────┘ [+ Добави секция] ← dropdown с всички налични типове
🔧 Добавяне на нов тип секция

1. Добави го в section-types.json (полета и конфигурация)

2. Създай Astro компонент frontend/src/blocks/WfNewType.astro

3. Регистрирай го в WfRender.astro. Готово — без промяна на Go код!

17 Build Queue + SQLite WAL mode

Какво става ако двама админа натиснат Build едновременно? Или ако админ натисне Build докато друг build работи?

Build Queue (опашка)

Вместо да стартира build директно, Go записва заявката в опашка:

Админ 1 натиска Build INSERT INTO build_queue (status='pending', pages='about,contacts')
Go goroutine взема заявката и я изпълнява status → 'running'. Стартира Astro build.
Админ 2 натиска Build (докато първият работи) INSERT INTO build_queue (status='pending', pages='products,home'). Чака на опашка.
Първият build завършва → вторият стартира Или по-умно: двата pending build-а се ОБЕДИНЯВАТ в един за ['about','contacts','products','home'].
🔀 Merge логика

Ако 3 build-а чакат за различни страници — Go ги обединява в 1 build. Вместо 3 пъти да стартира Node.js, стартира го веднъж с всички страници. По-бързо и по-ефективно.

WAL mode (Write-Ahead Logging)

SQLite по подразбиране заключва ЦЯЛАТА база при писане. С WAL mode:

Операция Без WAL С WAL
Админ чете докато build пише ❌ "database is locked" ✅ Работи
Build чете докато админ пише ❌ "database is locked" ✅ Работи
Двама пишат едновременно ❌ Грешка ⏳ Чака 5 сек, после пише
WAL настройки (задължителни): PRAGMA journal_mode = WAL; -- позволява concurrent reads PRAGMA busy_timeout = 5000; -- чакай 5 сек преди грешка PRAGMA synchronous = NORMAL; -- баланс бързина/сигурност

18 Файлове и снимки (без дубликация)

Снимките живеят на ЕДНО място. Няма копия.

Къде са снимките: public_html/ images/ uploads/ ← ТУКА (единственото копие) hero.jpg photo1.jpg logo.svg index.html ← генериран от Astro about/index.html ← генериран от Astro Как се записват: Админ качва снимка → Go я записва в public_html/images/uploads/ HTML я реферира: <img src="/images/uploads/hero.jpg"> nginx я сервира: директно от public_html/ При Astro build: Astro генерира САМО HTML/CSS/JS → frontend/dist/ dist/ НЕ съдържа uploads — те си стоят в public_html/
⚠ Важно при full rebuild

rsync --delete ще ИЗТРИЕ uploads ако не ги изключиш!

Правилната команда:
rsync -a --delete --exclude='images/uploads/' frontend/dist/ public_html/
--exclude='images/uploads/' запазва качените снимки при пълен rebuild.

19 PNPM за споделени модули

Всеки проект с Astro има node_modules/ папка с ~200MB зависимости. При 20 проекта = 4GB дублирани файлове. PNPM решава това.

npm (стандартно)

Всеки проект: собствено копие

20 проекта × 200MB = 4,000 MB

Милиони файлове на диска

npm install всеки път сваля наново

pnpm (оптимизирано)

Глобален store: 1 копие на всеки пакет

1 store (200MB) + 20 × symlinks = ~220 MB

Symlinks вместо копия

pnpm install е мигновено (пакетите вече са в store)

Как работи PNPM: ~/.pnpm-store/ ← ГЛОБАЛЕН STORE (1 копие на всичко) └── astro@5.0.0/ └── vite@5.0.0/ └── ... /var/www/site-1/frontend/ └── node_modules/ → symlinks към store ~1MB /var/www/site-2/frontend/ └── node_modules/ → symlinks към store ~1MB /var/www/site-3/frontend/ └── node_modules/ → symlinks към store ~1MB Инсталация (веднъж на сървъра): $ npm install -g pnpm Вместо npm: $ cd frontend && pnpm install # вместо npm install $ pnpm exec astro build # вместо npx astro build
💾 Go автоматично открива pnpm

Go build.go проверява дали pnpm е инсталиран. Ако да — ползва го. Ако не — fallback към npm. Не трябва ръчна конфигурация.

20 Cloudflare интеграция

Може ли публичният сайт да е на Cloudflare CDN, а бекендът на нашия сървър? Да — има три варианта:

Вариант А: Cloudflare като proxy (препоръчан)

Най-простият. Нищо не се променя по архитектурата.

Как работи: DNS: example.com → Cloudflare (orange cloud) → вашият nginx Cloudflare кешира: /*.html, /*.css, /*.js, /*.jpg → CDN (бърз, глобален) Cloudflare НЕ кешира: /admin/* → пропуска до вашия nginx → Go /api/* → пропуска до вашия nginx → Go Какво получаваш безплатно: ✓ DDoS защита ✓ Глобален CDN кеш ✓ SSL сертификат ✓ HTTP/3, Brotli компресия ✓ Аналитика Не се променя НИЩО по системата. Само DNS.

Вариант Б: Cloudflare Pages (фронтенд на CF)

Как работи: Посетител → Cloudflare Pages (static HTML, глобален CDN) ↓ форма (POST) cms.example.com → вашият сървър → Go backend Настройка: example.com → CF Pages (статичен сайт) cms.example.com → вашият сървър (Go админ + API) Build flow: 1. Админ натиска Build в cms.example.com/admin/ 2. Go стартира Astro build локално 3. Go push-ва dist/ към Git → CF Pages deploy 4. Или: Go вика CF Pages API директно
❓ Ще работят ли формите от друг домейн?

ДА. Формата на example.com (CF Pages) праща POST към cms.example.com/api/form/contact (вашият сървър). Go добавя CORS headers, които казват на браузъра: "Приемам заявки от example.com". Cloudflare НЕ прихваща и НЕ блокира тези заявки.

systemd socket activation работи нормално — Go се буди при POST, обработва формата, после пак заспива.

Вариант В: Cloudflare Tunnel (максимална сигурност)

Как работи: Посетител → CloudflareCF Tunnel → вашият сървър Какво е различно: • Сървърът НЯМА публичен IP • НЯМА отворени портове • НЯМА nginx пред света • cloudflared процес се свързва КЪМ Cloudflare (outbound) • Cloudflare маршрутизира заявките обратно по тунела За кого: • Максимална сигурност • Сървъри зад NAT / firewall • Когато не искаш да излагаш нищо публично
📌 Коя опция да изберете?

За повечето случаи: Вариант А (Cloudflare proxy). Нищо не се променя, само DNS. Безплатен CDN + DDoS защита.

За maximum performance: Вариант Б (CF Pages). Статичният HTML се сервира от 300+ CF data центъра по света.

За maximum сигурност: Вариант В (CF Tunnel). Сървърът е напълно скрит.

21 Универсален CMS панел

Целта: един панел за ВСИЧКИ видове сайтове. По-мощен от WordPress + ACF Pro, но без PHP, без MySQL, без plugins, без security patches.

21.1 Content Types (пост типове)

Неограничен брой типове съдържание. Всеки тип е конфигурируем — не е нужен Go код за нов тип.

Примери за content types: page Страница (About, Contacts, Home) article Статия / Blog post (с дата, автор, категория) product Продукт (с цена, галерия, спецификации) team Член на екипа (с позиция, снимка, social links) testimonial Отзив (с име, рейтинг, текст) event Събитие (с дата, място, линк за регистрация) faq Въпрос и отговор portfolio Проект в портфолио villa Вила (за имотни сайтове) painting Картина (за галерии) ... Каквото поискаш

Всеки тип се дефинира така:

{ "id": "article", "label": "Статия", "label_plural": "Статии", "icon": "newspaper", "slug_prefix": "blog/", ← URL: /blog/my-article "has_content": true, ← има rich text body "has_sections": true, ← има block editor "has_seo": true, ← има SEO полета "has_feed": true, ← включи в RSS feed "fields_json": [ ← custom полета (ACF стил) {"key": "subtitle", "type": "text", "label": "Подзаглавие"}, {"key": "cover", "type": "image", "label": "Cover Image"}, {"key": "author", "type": "relation", "label": "Автор", "related_type": "team"} ] }

21.2 Таксономии

ВидПримерСтруктура
Hierarchical Категории, Региони Дърво: Parent → Child → Grandchild
Flat Тагове, Цветове, Размери Плосък списък, без нива

Всяка таксономия се свързва с определени content types:

{ "id": "category", "label": "Категория", "hierarchical": true, "content_types": ["article", "product"] ← кои типове я ползват } Connections: article ← category, tag product ← category, color, size event ← region villa ← property_type, region

Relationships (връзки между типове): Article → Author (team), Product → Related Products, Event → Venue. Bidirectional — ако A сочи към B, B автоматично сочи обратно.

21.3 Полета (ACF Pro ниво)

Всички типове полета, които ACF Pro поддържа — и повече:

ТипОписаниеACF еквивалент
textЕдин ред текстText
textareaМного редовеText Area
richtextWYSIWYG визуален editorWYSIWYG Editor
numberЧисло (min/max/step)Number
emailEmail с валидацияEmail
urlURL с валидацияURL
selectDropdown (единичен/множествен)Select
checkboxМножествен изборCheckbox
radioЕдиничен изборRadio Button
toggleДа/Не switchTrue/False
imageКачване + media pickerImage
fileФайл (PDF, ZIP)File
galleryМного снимки + drag/dropGallery
linkURL + текст + targetLink
colorColor picker (hex, rgba)Color Picker
dateDate pickerDate Picker
relationВръзка към друг itemRelationship
taxonomyИзбор на termTaxonomy
repeaterМножество еднакви редаRepeater
flexibleРазлични layouts в свободен редFlexible Content
groupВложени полетаGroup
cloneПреизползване на field groupClone
codeCode editor (JSON, HTML)
oembedYouTube/Vimeo от URLoEmbed
mapКоординати (lat/lng)Google Map

Repeater vs Flexible Content

Repeater

Всеки ред е еднакъв

Екип: Ред 1: [Име] [Позиция] [Снимка] Ред 2: [Име] [Позиция] [Снимка] Ред 3: [Име] [Позиция] [Снимка]

За: списъци, таблици, спецификации

Flexible Content

Всеки ред е различен (избираш layout)

Страница: Layout "Hero": [Заглавие] [Фон] Layout "Text": [Съдържание] Layout "Gallery": [Снимки...] Layout "CTA": [Текст] [Бутон]

За: page builder, свободни layouts

Conditional Logic

Показвай/скривай полета на база стойности на други полета:

Пример: покажи "Video URL" само ако "Has Video" е включено { "key": "video_url", "type": "url", "label": "Video URL", "conditional": { "rules": [{"field": "has_video", "operator": "==", "value": true}] } } Оператори: ==, !=, >, <, contains, not_empty, empty

Tabs за организация на полета

Групиране на полета в табове:[Основни] [Медия] [SEO] ─────────────────────┐ │ │ │ Заглавие: [________________________] │ │ Подзаглавие: [________________________] │ │ Съдържание: [WYSIWYG editor...........] │ │ │ └───────────────────────────────────────────────────────┘ ┌ [Основни] [Медия] [SEO] ─────────────────────┐ │ │ │ Cover: [📷 hero.jpg ] [Избери] │ │ Галерия: [📷📷📷📷] [+ Добави] │ │ │ └───────────────────────────────────────────────────────┘

21.4 Многоезичност

Неограничен брой езици. Всеки с различен routing:

RoutingURLПример
noneexample.com/aboutDefault език, без prefix
prefixexample.com/en/aboutПоддиректория
subdomainen.example.com/aboutПоддомейн
domainexample.co.uk/aboutРазличен домейн
Конфигурация: { "languages": [ {"code": "bg", "name": "Български", "routing": "none", "is_default": true}, {"code": "en", "name": "English", "routing": "prefix", "prefix": "en"}, {"code": "de", "name": "Deutsch", "routing": "subdomain", "subdomain": "de"}, {"code": "fr", "name": "Français", "routing": "domain", "domain": "example.fr"} ] } Резултат: BG: example.com/about (default, без prefix) EN: example.com/en/about (поддиректория) DE: de.example.com/about (поддомейн) FR: example.fr/about (отделен домейн)

Translation groups: Преводите се свързват чрез translation_group ID. Страница #5 (BG) и страница #12 (EN) имат същия group → системата знае, че са превод на едно и също.

hreflang тагове се генерират автоматично от Go API (коректни URL-и per routing config):

<link rel="alternate" hreflang="bg" href="https://example.com/about" /> <link rel="alternate" hreflang="en" href="https://example.com/en/about" /> <link rel="alternate" hreflang="de" href="https://de.example.com/about" /> <link rel="alternate" hreflang="fr" href="https://example.fr/about" /> <link rel="alternate" hreflang="x-default" href="https://example.com/about" />

21.5 SEO

Пълен SEO контрол per страница:

SEO полета за всяка страница (seo_json): meta_title: "За нас | Example" meta_description: "Научете повече за нашата компания..." og_title: (fallback → meta_title) og_description: (fallback → meta_description) og_image: "/images/og-about.jpg" twitter_card: "summary_large_image" canonical: (празно = автоматичен) noindex: false ← скрий от Google nofollow: false exclude_sitemap: false ← изключи от sitemap schema_type: "WebPage" ← JSON-LD schema

Автоматични SEO функции

ФункцияКой я правиОписание
SitemapGo (динамичен)Go сервира /sitemap.xml — винаги актуален, без rebuild. Per-language sitemaps. Respects noindex и exclude_sitemap.
robots.txtGo (динамичен)Конфигурируем от Settings. Включва Sitemap URL.
hreflangGo API → AstroАвтоматични alternate tags за всички езикови версии.
JSON-LDGo API → AstroStructured data per content type: Article, Product, Event, FAQPage, Person...
CanonicalGo API → AstroАвтоматичен canonical URL (или custom).
RedirectsGo301/302 redirects. Автоматичен при промяна на slug. Ръчни за миграция.
RSS FeedGo (динамичен)/feed.xml за типове с has_feed=true. Per-language feeds.

21.6 Потребители, роли и права

РоляМожеНе може
super_admin Всичко
admin Съдържание, медия, builds, settings User management
editor Редактира и публикува всяко съдържание Settings, users
author Създава и редактира САМО своето Публикуване, чуждо съдържание
viewer Вижда dashboard и съдържание Всякакви промени

Granular permissions per content type — editor може да трие статии, но не продукти:

{ "role": "editor", "permissions": { "article": {"create": true, "read": true, "update": true, "delete": true, "publish": true}, "product": {"create": true, "read": true, "update": true, "delete": false, "publish": true}, "settings": {"read": true, "write": false}, "media": {"upload": true, "delete": false}, "users": {"manage": false} } }

Сигурност

ЗащитаОписание
CSRF tokensНа всяка форма — предпазва от cross-site заявки
Rate limitingLogin: 5 опита за 15 мин, после lockout 30 мин
Password policyМин. 8 символа, bcrypt хеширане
JWT + RefreshAccess token 24ч, refresh token 7 дни
2FA (TOTP)Google Authenticator / Authy — optional per user
IP whitelistОграничи /admin до определени IP-та (optional)
CSP headersContent-Security-Policy на admin pages
Audit logКой какво е направил кога — activity_log таблица
Session managementВиж активни сесии, прекрати отдалечено
Brute forceProgressive delay при грешни пароли

21.7 Data delivery

Три начина за подаване на данни към Astro:

Local API

Astro и Go на същия сървър

fetch("http://localhost:8080/api/...")

Бързо, без auth

Remote API

Astro на друг сървър или CF Pages

fetch("https://cms.example.com/api/...")

С Bearer token

JSON Export

Offline builds, CI/CD

Go exports .data/*.json

Без мрежа

API Endpoints: GET /api/content?type=article&locale=bg&status=published GET /api/content/:id GET /api/content/by-slug/:type/:slug GET /api/taxonomy/:taxonomy?locale=bg GET /api/terms/:taxonomy GET /api/menus?locale=bg GET /api/settings?locale=bg GET /api/languages GET /api/redirects GET /api/sitemap-data Динамични (Go сервира директно, не са static): GET /sitemap.xml ← sitemap index GET /sitemap-bg.xml ← per-language sitemap GET /robots.txt GET /feed.xml ← RSS POST /api/form/:type ← form submissions

21.8 Допълнителни функции

ФункцияОписание
Menu BuilderDrag/drop, nested items, per-language. Менюта: main, footer, sidebar...
Global SettingsSite 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 publishpublish_at / expire_at — публикувай на дата, скрий след дата.
Activity logКой какво е правил — create, update, delete, publish, login, build, upload.
Redirects301/302 management. Автоматичен при промяна на slug.
Import/ExportЦялото съдържание като JSON + медия в ZIP. За миграция/backup.
Search indexJSON index при build за client-side search. Или Pagefind интеграция.
Code injectionCustom HTML в head/body — per page и global. За tracking, analytics, embeds.
Media LibraryПапки, search, drag/drop upload, auto WebP + resize при upload, bulk actions.
Password pagesЗащитени с парола страници — Go проверява при заявка.

21.9 Database schema

Една SQLite база с 14 таблици покрива ВСИЧКО:

14 таблици: content_types Дефиниции на типове (page, article, product...) content Универсална таблица за ВСЯКО съдържание taxonomies Дефиниции на таксономии (category, tag...) terms Стойности на таксономии (конкретни категории) content_terms Връзки content ↔ terms (many-to-many) languages Конфигурация на езици и routing menus Навигационни менюта (per-language) settings Key-value настройки (global + per-locale) section_types Дефиниции на section блокове media Качени файлове (metadata) users Потребители с роли и permissions redirects 301/302 пренасочвания activity_log Лог на действията revisions Версии на страниците submissions Форми от посетители build_queue Опашка за builds
💡 Ключова идея: ЕДНА таблица content за всичко

Вместо отделна таблица за articles, products, team, events... — всичко е в content с колона type. Custom полета са в fields_json (JSON). Block editor данни са в sections_json. SEO е в seo_json.

Това означава: добавяне на нов content type = 0 промени по базата. Само конфигурация.

21.10 Сравнение с WordPress + ACF Pro

ФункцияWordPress + ACF ProНашият CMS
RAM в покой50-200 MB (PHP+MySQL)0 MB (спи)
Page load200-500ms (PHP render)< 5ms (static HTML)
Сигурност#1 хакван CMS в светаНяма PHP, няма plugins
DeployLAMP stack + 20 plugins1 файл + SQLite
UpdatesWP core + plugins + PHP1 Go binary
DB backupmysqldumpcp site.db backup.db
20 сайта RAM1-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

22 Edge cases и пропуснати неща

Неща, които лесно се пропускат при проектиране на CMS, но са критични при реална употреба:

22.1 Данни и съдържание

ПроблемРешение
Slug колизии — две страници с еднакъв slug UNIQUE constraint на (slug, locale, type). При конфликт Go добавя суфикс: aboutabout-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) с примерни секции. Не е празна база.

22.2 URL-и и маршрутизиране

ПроблемРешение
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 таблицата.

22.3 Многоезичност — edge cases

ПроблемРешение
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

22.4 Сигурност — допълнителни защити

ЗаплахаЗащита
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 тип

22.5 Performance

Database indexes (задължителни при 1000+ записа): CREATE INDEX idx_content_type_locale ON content(type, locale, status); CREATE INDEX idx_content_slug ON content(slug, locale); CREATE INDEX idx_content_translation ON content(translation_group); CREATE INDEX idx_terms_taxonomy ON terms(taxonomy, locale); CREATE INDEX idx_activity_log_date ON activity_log(created_at);
ОптимизацияОписание
Build cacheAstro --cache flag. Кешира artifacts между builds. 2-3x по-бързо
Image lazy loadingloading="lazy" на всички изображения освен hero (above-fold)
Critical CSSInline critical CSS в <head>, останалият CSS async. 100/100 Lighthouse

22.6 Форми — пропуснати функции

ФункцияОписание
Form BuilderАдмин създава custom форми. Всяка: име, полета, email получател. Не само contact
Email notificationПри submission → Go изпраща email до конфигуриран адрес. SMTP settings
Anti-spamHoneypot поле (скрито CSS — ботове попълват, хора не). Optional: reCAPTCHA v3
File uploadПосетители качват файлове (CV, документи). Макс размер, типове — конфигурируемо
CSV exportExport submissions за период. За анализ/отчети

22.7 Build & Deploy

ФункцияОписание
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 конвертира при показване/запис

22.8 Admin UX

ФункцияОписание
Bulk operationsИзбери 20 items → Publish All / Delete All / Move to Category
Content compareSide-by-side сравнение на две езикови версии. Полезно при превод
Dashboard widgetsПоследна активност, pending builds, непрочетени форми, content stats
Mobile adminResponsive — работи на телефон/таблет за спешни промени
OnboardingПърво влизане → wizard: site name, език, първа страница
Social previewВизуален preview как ще изглежда страницата при share във Facebook/Twitter
BreadcrumbsBreadcrumbList JSON-LD. Изисква parent/child hierarchy. Go API предоставя chain
WP ImportTool за импортиране от WordPress XML export. Posts→content, categories→terms, images→media
⚠ Неща, които НИКОГА да не забравяме

23 Второ ревю — неща, които лесно се пропускат при разработка

След пълен преглед на всичко описано — ето ситуациите, в които по средата на работата бихме казали "ааа, това не сме го помислили":

23.1 Scheduled publishing — КОЙ тригерва?

⚠ Проблем: Go спи, а страницата трябва да се публикува в 09:00

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 процеса — не заема порт, не слуша заявки, просто проверява базата и излиза.

systemd timer за scheduled posts: cms-cron.timer [Timer] OnCalendar=*:0/5 ← на всеки 5 мин Persistent=true cms-cron.service [Service] Type=oneshot ExecStart=/var/www/example.com/backend/cms --cron ← проверява scheduled posts, тригерва build ако има, излиза

23.2 Translation creation workflow

Не е описано КАК админът създава превод. Ето flow-то:

Админ отваря BG страница "За нас" Вижда бутон: [+ EN] [+ DE] [+ FR] (за всеки език без превод)
Кликва [+ EN] Go създава нов content запис: locale="en", translation_group=same, status="draft". Копира sections_json от BG за reference.
Admin edit page показва side-by-side Лява колона: BG (read-only, за reference). Дясна: EN (editable). Превеждаш поле по поле.
Save + Publish EN версията е свързана с BG чрез translation_group. hreflang тагове се генерират автоматично.

23.3 Global/Shared секции (Reusable Blocks)

💡 Проблем: Newsletter CTA е на 20 страници. Промяна = 20 ръчни edit-а?

Решение: 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)

23.4 Section видимост и дублиране

ДействиеКак работи
Скрий секция (без триене) Всяка секция има "visible": true/false. Скритите не се рендерират, но остават в JSON-а. Toggle бутон 👁
Дублирай секция Бутон [⧉] — копира секцията и я вмъква отдолу. Полезно за вариации
Per-language видимост "visible_locales": ["bg","en"] — показвай тази секция САМО на BG и EN, скрий на DE. За различно съдържание per език без separate pages
Copy между страници Clipboard: копирай секция от страница A, paste в страница B

23.5 Meta title template

Проблем: Всяка страница трябва meta title като "За нас | Example". Ако няма зададен meta_title — Go трябва да генерира автоматичен. Решение — шаблон в Settings: meta_title_template = "{title} | {site_name}" Per content type: article: "{title} - {category} | {site_name}" product: "{title} — Купи онлайн | {site_name}" Separator конфигурация: | или - или — или ·

Ако seo_json.meta_title е празен → Go прилага шаблона. Ако е попълнен → ползва го директно.

23.6 Build error handling

Astro build ФЕЙЛВА npm error, синтактична грешка в template, memory overflow
Go хваща грешката cmd.CombinedOutput() връща error + output log
public_html НЕ СЕ ПИПА rsync НЕ се изпълнява. Старата работеща версия си стои. Сайтът продължава да работи.
Admin вижда error log Build queue: status='failed', error='...'. Admin може да коригира и ретрайне.
⚠ Build timeout

Max build time: 5 минути. Ако Astro виси по-дълго → Go убива процеса (exec.CommandContext с timeout). Предотвратява зомби build-ове, които блокират queue-то.

23.7 Media management

ФункцияОписание
Къде е използвана? За всяка снимка — бутон "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)

23.8 Unpublish = rebuild

❓ Какво става при unpublish?

Когато страница мине от published → draft:

Go трябва: 1) изтрий файла от public_html, 2) маркирай зависимите pages, 3) тригерни selective rebuild, 4) обнови менюта

23.9 Content validation

ПроверкаКогаКакво
Required fieldsПри PublishTitle задължително. Slug задължително. Cover image за articles (ако е required в config)
Slug formatПри SaveСамо a-z, 0-9, dash. Без special chars, без spaces. Auto-fix
JSON validationПри Savesections_json и fields_json трябва да са валиден JSON. Иначе → error
Image dimensionsПри PublishOG image трябва да е мин 1200×630 (Facebook изискване). Warning ако не е
Broken linksПри BuildПровери дали вътрешните линкове сочат към съществуващи страници

23.10 Operations & Infrastructure

ФункцияОписание
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 задава парола. Не ръчно даване на пароли

23.11 GDPR & Privacy

ИзискванеРешение
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 година

23.12 Standard Astro компоненти

Всеки проект ще има нужда от тези — трябва да са в шаблона:

КомпонентКакво прави
SEO.astroMeta tags, OG, Twitter cards, JSON-LD, hreflang, canonical — всичко от seo_json
Image.astrosrcset, lazy loading, WebP, alt text. Wrapper за img с оптимизации
LanguageSwitcher.astroЛинкове към другите езикови версии. Чете от translation data
Breadcrumbs.astroBreadcrumb навигация + BreadcrumbList JSON-LD. Чете parent chain
Pagination.astroPrevious/Next + page numbers. За listing pages
ContactForm.astroForm с honeypot, validation, submit handler. Конфигурируеми полета
CookieConsent.astroGDPR cookie banner. Категории, preferences, localStorage persistence
✅ Нищо не е пропуснато

С тези 12 допълнения спецификацията покрива:


24 Пълна медия система

24.1 Ключов принцип: Файл ≠ Употреба

Media File (физически)

ЕДИН файл на диска.

ID 42, sunset-beach.jpg, 3000×2000

Има: оригинал + 5 размера × 3 формата

Media Reference (употреба)

МНОГО употреби в различни страници.

Всяка с различен: alt text, caption, crop, size

Едно и също ID, различен контекст

24.2 Какво се случва при upload

Валидация Magic bytes → MIME type (не доверявай extension). Макс размер: images 10MB, docs 50MB. SVG → strip <script>, on* атрибути.
Дедупликация SHA256 хеш. "Този файл вече съществува" → покажи линк. Админ избира: ползвай или качи ново копие.
Запис на оригинал media/originals/42_sunset-beach.jpg — НИКОГА не се пипа. Backup, reference, re-processing.
Resize (5 размера) thumb 150×150 (crop по focal point), small 400px, medium 800px, large 1600px, xl 2400px (retina). Go pure library: imaging.
Конвертиране (3 формата) WebP (cwebp, 82% quality, 30% по-малко), AVIF (avifenc, 50% по-малко), оригинален формат (JPEG/PNG fallback). = 15 файла per image.
Metadata width, height, file_size, dominant_color (#hex за blur-up placeholder), EXIF strip от processed версии (privacy!).
Готово Връща media_id + URLs за всички размери/формати. Веднага готови за ползване.
💡 Go обработва, НЕ Astro

Обработката е при upload (веднъж), не при build (всеки път). Не трябва sharp (native Node module). Не бави build-а. По-преносимо. Go ползва pure Go библиотека за resize + CLI инструменти (cwebp, avifenc) за конвертиране.

Зависимости на сървъра: apt install webp libavif-bin poppler-utils ffmpeg

24.3 Файлова структура

На диска: public_html/media/ originals/ ← оригинали (НИКОГА не се пипат) 42_sunset-beach.jpg 43_team-photo.png 44_logo.svg 45_brochure.pdf 46_intro.mp4 42_sunset-beach/ ← обработени версии (1 папка per image) thumb.webp ← 150×150 small.webp ← 400w medium.webp ← 800w large.webp ← 1600w xl.webp ← 2400w (retina) thumb.avif small.avif medium.avif large.avif xl.avif medium.jpg large.jpg ← fallback в оригинален формат 43_team-photo/ thumb.webp small.webp medium.webp large.webp xl.webp ... 45_brochure_thumb.webp ← PDF thumbnail (стр.1, pdftoppm) 46_intro_poster.webp ← Video poster (ffmpeg кадър)

24.4 Responsive images в HTML

Astro компонент Image.astro генерира <picture> с всички формати:

<picture> <source srcset="/media/42_sunset-beach/small.avif 400w, /media/42_sunset-beach/medium.avif 800w, /media/42_sunset-beach/large.avif 1600w" type="image/avif"> <source srcset="/media/42_sunset-beach/small.webp 400w, /media/42_sunset-beach/medium.webp 800w, /media/42_sunset-beach/large.webp 1600w" type="image/webp"> <img src="/media/42_sunset-beach/medium.jpg" alt="Красив залез на плажа" width="800" height="533" loading="lazy" style="background:#3a5a8c"> ← dominant color placeholder </picture> Браузърът избира: AVIF (ако поддържа, най-малък) → WebP (по-широка поддръжка) → JPEG (fallback) + избира размер спрямо viewport (400w, 800w, 1600w)

24.5 Alt текст per употреба

Един файл (ID 42), 5 страници, 5 различни alt текста:

Page "Home", Hero секция: {"media_id": 42, "alt": "Красив залез над морето", "size": "xl"} Page "About", Image секция: {"media_id": 42, "alt": "Нашият офис с изглед към морето", "size": "large"} Page "Gallery", item #3: {"media_id": 42, "alt": "Залез", "caption": "Фото: Иван Иванов", "size": "medium"} alt в section data ЗАМЕСТВА default alt от media таблицата. Ако не е зададен → fallback към default alt.

24.6 Focal Point

Проблем: Center crop отрязва лицата при thumbnail. Решение: Админ кликва върху снимката → задава focal point. ┌───────────────────────────┐ │ │ │ │ ← focal point (60%, 30%) │ (лице) │ │ │ │ │ └───────────────────────────┘ DB: focal_x: 60, focal_y: 30 Thumbnail crop: центрира около focal point → лицето е видимо.

24.7 Dominant Color (blur-up)

Go извлича доминиращия цвят при upload. Astro го ползва като placeholder:

Преди зареждане: цветен правоъгълник (#3a5a8c) След зареждане: снимката се fade-ва отгоре По-добър UX от: бяло петно → снимка (layout shift)

24.8 Обработка per тип файл

ТипОбработкаResizeРезултат
JPEG / PNG / GIFResize + WebP + AVIF + strip EXIF15 файла (5 размера × 3 формата)
WebP (upload)Resize + AVIF10 файла
SVGСанитизация (strip JS)Само оригинал (вектор)
PDFThumbnail от стр.1 (pdftoppm)Оригинал + 1 thumbnail
VideoPoster кадър (ffmpeg)Оригинал + 1 poster
ДокументиНищоСамо оригинал

24.9 Media management функции

ФункцияКак работи
Къде е използвана? Бутон "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%

24.10 Преименуване

❓ Как работи преименуването?

Физическият 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.

24.11 Media таблица

Пълна media таблица: CREATE TABLE media ( id INTEGER PRIMARY KEY, filename TEXT, -- 42_sunset-beach.jpg (на диска) original_name TEXT, -- sunset-beach.jpg (каквото е качил) title TEXT, -- display name (преименуваем) mime_type TEXT, -- image/jpeg size INTEGER, -- bytes width INTEGER, -- px height INTEGER, -- px focal_x INTEGER, -- focal point X (%, default 50) focal_y INTEGER, -- focal point Y (%, default 50) dominant_color TEXT, -- #hex за blur-up placeholder hash TEXT, -- SHA256 за dedup alt_text TEXT, -- default alt (overridable per usage) caption TEXT, -- default caption folder TEXT, -- виртуална папка has_webp INTEGER, -- генерирани ли са WebP has_avif INTEGER, -- генерирани ли са AVIF sizes_json TEXT, -- {"thumb":"150x150","small":"400x267",...} created_at DATETIME, created_by INTEGER );

25 Трето ревю + конфликти

25.1 Ограничения на статичен сайт

КРИТИЧНО: какво НЕ МОЖЕ static site и как го решаваме:

Не можеРешение
Реално-времеви данни (цени, наличности)Go API endpoint + JS fetch при зареждане. Socket activation буди Go
Login / member areaGo 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 searchPagefind (client-side, offline, бърз). Или Go API /api/search?q=term

25.2 Нови пропуснати функции

ФункцияОписание
TypeScript типове от конфиг Go автоматично генерира TS interfaces от content_types: ./cms generate-typesfrontend/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

25.3 Мащабиране

МащабПроблемРешение
10k pagesFull rebuild: 5-10 минSelective build. Full rebuild само при deploy
10k pagesAdmin dashboard бавенPagination + search + filters. Lazy load
100k mediaПапка с 100k файлаПоддиректории по ID: media/0-999/, media/1000-1999/
50 content typesSidebar неизползваемГрупиране, favorites, search в sidebar
100 builds/денQueue натоваренMerge pending builds. Debounce 5 сек
💡 SQLite лимити

Max DB size: 281 TB. Max rows: 1 билион. За нашия use case — практически неограничено. 10k pages с 50 секции всяка = ~50MB database.

25.4 Открити конфликти в спецификацията

КонфликтРешение
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/

26 Admin Panel — дизайн и брандинг

26.1 WebFactor CMS брандинг

ЕлементДетайл
Sidebar логоWebFactor logo (бяло) горе ляво + "CMS" label
Login страницаWebFactor лого + "Content Management System" + тъмен фон
FaviconWebFactor icon
Login footer"Powered by WebFactor"
Скрито от клиентаНикъде "Go", "Astro", "SQLite" — клиентът не трябва да знае технологията

26.2 Дизайн система (модерен, 2026 ниво)

Референции: Linear, Vercel dashboard, Payload CMS, Notion

Цветова палитра: Dark theme (default): bg: #0f172a card: #1e293b border: #334155 text: #e2e8f0 accent: #4361ee (WebFactor blue) Light theme (toggle): bg: #f8fafc card: #ffffff Status colors: success: #22c55e error: #ef4444 warning: #f59e0b info: #60a5fa Typography: Inter (Google Fonts) / system-ui fallback 14px base, 1.5 line-height Spacing: 4px grid. Padding: 8/12/16/20/24/32 Radius: 8px cards, 6px inputs, 12px modals, 50% avatars Animations: 150ms transitions, skeleton loading, toast slide-in

26.3 Ключови UI елементи

Command Palette (Cmd+K / Ctrl+K): ┌─────────────────────────────────────────────┐ │ 🔍 Type a command or search... │ ├─────────────────────────────────────────────┤ │ Pages │ │ 📄 Home │ │ 📄 About Us │ │ 📄 Contact │ │ Actions │ │ ➕ Create new Article │ │ 🔨 Trigger Build │ │ ⚙ Go to Settings │ │ Recent │ │ 📰 "New Summer Collection" (edited 5m ago)│ └─────────────────────────────────────────────┘ Търси навсякъде: страници, настройки, действия, медия. Най-бързият начин за навигация. Като Linear/Notion.
Sidebar layout: ┌──────────────────────┬──────────────────────────────────────────┐ │ [WF] WebFactor │ Home > Articles > Edit "My Post" [Save]│ │ CMS │─────────────────────────────────────────│ │ │ │ │ 🔍 Search... (⌘K) │ Title: [My Amazing Article____________] │ │ │ Slug: [my-amazing-article_____________] │ │ ⭐ Favorites │ │ │ 📄 Pages (24) │ ┌─ Hero ──────────────── [≡] [▾] [×]─┐│ │ 📰 Articles (156) │ │ Title: [Welcome] ││ │ │ │ Image: [📷 hero.jpg] ││ │ Content │ └──────────────────────────────────────┘│ │ 🎨 Portfolio (32) │ │ │ 👥 Team (8) │ ┌─ Text ──────────────── [≡] [▾] [×]─┐│ │ ⭐ Reviews (45) │ │ [WYSIWYG editor content here...] ││ │ │ └──────────────────────────────────────┘│ │ System │ │ │ 📁 Media │ [+ Add Section ▼] │ │ 📬 Forms (3) 🔴 │ │ │ 🔀 Redirects │ ▸ SEO │ │ 📋 Menus │ ▸ Advanced │ │ ⚙ Settings │ │ │ 👤 Users │──────────────────────────────────────────│ │ 📊 Activity │ 📁 Media [Drop files here or click] │ │ │ [📷][📷][📷][📷] │ │ admin@wf [🌙/☀] │ │ └──────────────────────┴──────────────────────────────────────────┘
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 shortcutsCmd+K search, Cmd+S save, Cmd+N new, Escape close
Dark/Light toggleБутон в sidebar footer. Preference в localStorage
ResponsiveSidebar → hamburger на mobile. Touch-friendly бутони (44px min)

26.4 Какво вижда админът при грешки

СитуацияUI отговор
Save failToast (red): "Failed to save. Check your connection."
Build failToast (red) + error log в modal при клик
Upload failToast (red): "Upload failed: file too large (max 10MB)"
Session expiredRedirect → login: "Session expired. Please log in again."
Concurrent editModal: "John updated this 2 min ago. View changes / Overwrite / Cancel"
DB busyRetry 3× (busy_timeout), после Toast: "Database busy, please try again"

27 Implementation Roadmap

Ред на имплементация — от MVP до пълна система:

Фаза 1: MVP Go: socket activation, idle timeout, JWT login, content CRUD, API, build trigger (npx astro build, rsync).
Astro: Base layout, [...slug].astro, WfRender + 5 блока (hero, text, image, gallery, contact).
Deploy: setup.sh, systemd, nginx.
Резултат: работещ CMS, създаваш страници, те се показват.
Фаза 2: Visual Editor admin.js: section editor с field types (text, textarea, image, gallery, richtext).
Section types конфигурация (DB). Media upload + library. Drag/drop reorder.
Резултат: визуален редактор вместо JSON textarea.
Фаза 3: Content System Content types конфигурация (admin UI). Taxonomies + terms. Relationship field type.
Selective build + dependency tracking. Build queue с merge.
Резултат: универсален CMS — всякакви типове съдържание.
Фаза 4: Multi-language + SEO Languages config. Translation groups + side-by-side editor.
hreflang, og:locale, JSON-LD автоматични. SEO полета. Dynamic sitemap/robots/feed.
Redirects management с auto-redirect при slug промяна.
Резултат: multi-language сайтове с пълно SEO.
Фаза 5: Polish Roles & permissions (5 роли + granular). Revisions + autosave.
Activity log. Menu builder. Global settings (per-locale).
Form submissions + email notifications. WebFactor брандинг + dark/light theme.
Резултат: production-ready CMS.
Фаза 6: Advanced Image optimization (WebP/AVIF при upload). Focal point + dominant color.
2FA (TOTP). TypeScript generation. Developer CLI. Command palette (Cmd+K).
Scheduled publishing + cron. Import/Export. Notifications. GDPR compliance.
Резултат: пълната спецификация имплементирана.

28 Разяснения за имплементатора

Къде се дефинират section types?

В DB таблицата section_types — source of truth. Admin може да ги създава/редактира в UI. При нов проект — seed от JSON файл. Astro НЕ знае за DB-то — има component map в WfRender.astro. Нов section type = 1) добави в DB, 2) създай .astro компонент, 3) добави в map.

Какво става при промяна на field дефиниция?

Преименуване на field: ./cms migrate-field article subtitle subheading → Обновява fields_json на ВСИЧКИ articles: "subtitle" → "subheading" Изтриване на field: ./cms clean-fields article → Маха deprecated fields от fields_json (данните са останали от стар конфиг) Добавяне на field: Без миграция нужна — нови записи ще го имат, стари не. Go/Astro трябва да handle-ва missing fields gracefully (default values).

Как се обновява CMS binary на 20 сайта?

# Компилирай нова версия GOOS=linux GOARCH=amd64 go build -o cms . # Деплойни на всички сайтове for site in site1.com site2.com site3.com; do scp cms root@server:/var/www/$site/backend/cms ssh root@server "systemctl restart ${site//./-}-cms.service" done # Socket activation = 0 downtime # systemd държи socket-а → нов binary → instant restart

Как Astro знае кой template да ползва per content type?

# [...slug].astro проверява page.type: if (page.type === 'article') → ArticleLayout if (page.type === 'product') → ProductLayout if (page.type === 'team') → TeamLayout default → DefaultLayout (sections + WfRender) # Или: всички типове ползват WfRender (block-based). # Layout-ът е определен от секциите, не от типа. # Само ако типът има специфичен layout → custom template.

29 Field System — пълна спецификация (ACF Pro+)

Секция 21.3 изброи типовете полета. Тук описваме ПЪЛНАТА система — всяка настройка, всеки edge case, всеки UI елемент. Ако се имплементира по тази секция — нищо няма да липсва.

29.1 Универсални настройки (ВСЯКО поле ги има)

НастройкаТипОписание
keystringУникален идентификатор. Snake_case: hero_title
labelstringПоказвано име: "Hero Title"
typestringТип: text, image, repeater, и т.н.
instructionsstringПомощен текст под label-а. HTML позволен
requiredbooleanЗадължително при Publish. Validation error ако е празно
default_valueanyСтойност по подразбиране при нов запис
placeholderstringСив текст в празен input
conditionalobjectПокажи/скрий по правила (виж 29.6)
wrapper_widthnumberШирина в % (25, 33, 50, 75, 100). За side-by-side layout
wrapper_classstringCustom CSS класове

29.2 Настройки per тип поле

text / email / url / password

НастройкаОписание
prependТекст ПРЕД input-а. Пример: "$" за цена, "https://" за URL
appendТекст СЛЕД input-а. Пример: "лв.", "kg", "px"
max_lengthМакс символи. Показва брояч "45/60"
Визуално: ┌──────────────────────────────────────────┐ │ Price │ │ ┌─────┬─────────────────────────┬──────┐ │ │ │ $ │ 29.99 │ USD │ │ │ └─────┴─────────────────────────┴──────┘ │ │ ↑ prepend ↑ append │ │ 42/100 │ ← max_length counter └──────────────────────────────────────────┘

number

НастройкаОписание
minМинимална стойност
maxМаксимална стойност
stepСтъпка (1, 0.01, 5, 100...)
prepend / appendКато text

textarea

НастройкаОписание
rowsВисочина (брой редове). Default: 4
max_lengthМакс символи с брояч

richtext (WYSIWYG)

НастройкаОписание
toolbarfull (всички бутони) или basic (bold, italic, link, lists)
media_uploadПокажи бутон за вмъкване на медия (bool)
max_lengthМакс символи (strip HTML за броене)

select

НастройкаОписание
choicesМасив от опции: [{"value":"sm","label":"Small"},{"value":"lg","label":"Large"}]
allow_nullПразна опция "— Select —" (bool)
multipleМножествен избор (bool)
searchableEnhanced UI с търсене (като Select2). За 20+ опции

image

НастройкаОписание
preview_sizeThumbnail size в админа: thumb, medium
min_width / min_heightМинимални px. Validation при избор
max_sizeМакс MB. Пример: 5
allowed_typesMIME типове: jpg,png,webp

gallery

НастройкаОписание
min / maxМин/макс брой снимки. Validation
min_width / min_heightPer image валидация
allowed_typesMIME типове
insertbeginning или end — къде отиват нови

link

Link полето генерира 3 подполета: ┌─────────────────────────────────────────────┐ │ Call to Action │ │ ┌──────────────────┐ ┌────────────────────┐ │ │ │ Text │ │ URL │ │ │ │ [Learn More ] │ │ [/about ] │ │ │ └──────────────────┘ └────────────────────┘ │ │ ☐ Open in new tab ☐ Nofollow │ └─────────────────────────────────────────────┘ Стойност: {"text":"Learn More", "url":"/about", "target":"_blank", "nofollow":false}

relation / relation_multi

НастройкаОписание
related_typeКой content type да се показва: article, product, team
related_taxonomyФилтър по таксономия
filtersКакво може да търси: ["search","type","taxonomy"]
show_thumbnailПокажи cover image в dropdown (bool)
min / maxМин/макс избрани items
bidirectionalAuto-update обратната връзка (bool)

date / datetime / time

НастройкаОписание
display_formatКак се показва: d.m.Y, m/d/Y
return_formatКак се запазва: Y-m-d (ISO)
first_dayНачало на седмицата: 1=Понеделник, 0=Неделя

color

НастройкаОписание
enable_opacityrgba vs hex (bool). rgba дава прозрачност
presetsПалитра от предефинирани цветове за бърз избор

range (slider)

НастройкаОписание
min / max / stepГраници и стъпка
prepend / appendТекст пред/след

29.3 Repeater — подробно

Repeater = масив от еднакви реда: ┌─ Team Members ────────────────────── [+ Add Row] ─┐ │ │ │ ┌─ Row 1 ──────────────────────── [≡] [×] ──┐ │ │ │ Name: [John Smith] Position: [CEO] │ │ │ │ Photo: [📷 john.jpg] Bio: [Lorem...] │ │ │ └────────────────────────────────────────────┘ │ │ │ │ ┌─ Row 2 ──────────────────────── [≡] [×] ──┐ │ │ │ Name: [Jane Doe] Position: [CTO] │ │ │ │ Photo: [📷 jane.jpg] Bio: [Lorem...] │ │ │ └────────────────────────────────────────────┘ │ │ │ │ [+ Add Row] │ └─────────────────────────────────────────────────────┘
НастройкаОписание
sub_fieldsМасив от полета вътре (ВСЕКИ тип, включително друг repeater)
min / maxМин/макс реда. Validation
button_labelТекст на бутона: "+ Add Team Member" вместо "+ Add Row"
layoutblock (пълна ширина) или table (компактна таблица) или row (inline)
collapsedКое sub_field да се показва като заглавие при collapsed ред (напр. "name")
✅ Неограничено nesting

Repeater вътре в repeater вътре в repeater. Без лимит на дълбочината. Пример: Courses → Lessons → Chapters → Exercises. ACF Pro го поддържа. Ние също.

29.4 Flexible Content — подробно

Flexible Content = масив от РАЗЛИЧНИ layouts: ┌─ Page Content ──────── [+ Add Layout ▼] ──────────┐ │ │ │ ┌─ Hero Banner ──────────────── [≡] [▾] [×] ──┐ │ │ │ Title: [Welcome] │ │ │ │ Subtitle: [To our amazing site] │ │ │ │ Background: [📷 hero.jpg] │ │ │ │ CTA: [Learn More] → [/about] │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ ┌─ Text Block ──────────────── [≡] [▾] [×] ──┐ │ │ │ [WYSIWYG editor with rich content...] │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ ┌─ Gallery Grid ─────────────── [≡] [▾] [×] ──┐ │ │ │ Columns: [3] │ │ │ │ Images: [📷 📷 📷 📷 📷 📷] │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ [+ Add Layout ▼] │ │ ┌────────────────┐ │ │ │ ★ Hero Banner │ ← dropdown с наличните layouts │ │ │ ◉ Text Block │ │ │ │ ▦ Gallery Grid │ │ │ │ ☰ Features │ │ │ │ ✎ CTA Section │ │ │ │ ❝ Testimonials │ │ │ └────────────────┘ │ └─────────────────────────────────────────────────────┘
НастройкаОписание
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
💡 Flexible Content = нашият Section Editor

sections_json на страница е точно Flexible Content поле. Всяка "секция" е layout. Разликата: Flexible Content може да е ВЛОЖЕН field (вътре в repeater, group, или друг flexible). Секционният editor е top-level. И двете работят по същия начин.

29.5 Wrapper Width — side-by-side полета

wrapper_width позволява полета едно до друго: wrapper_width: 50% wrapper_width: 50% ┌────────────────────────┐ ┌────────────────────────┐ │ First Name │ │ Last Name │ │ [John_______________] │ │ [Smith______________] │ └────────────────────────┘ └────────────────────────┘ wrapper_width: 33% wrapper_width: 33% wrapper_width: 33% ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ City │ │ State │ │ ZIP │ │ [Varna________] │ │ [BG ▼_________] │ │ [9000_________] │ └──────────────────┘ └──────────────────┘ └──────────────────┘ wrapper_width: 75% wrapper_width: 25% ┌──────────────────────────────────────┐ ┌──────────────────┐ │ Street Address │ │ Number │ │ [ул. Осми Приморски Полк___________] │ │ [42__________] │ └──────────────────────────────────────┘ └──────────────────┘

Стойности: 25%, 33%, 50%, 66%, 75%, 100% (default). CSS flexbox row с flex-wrap.

29.6 Conditional Logic — подробно

Пример: покажи "Video URL" само ако "Content Type" = "Video" { "key": "video_url", "type": "url", "label": "Video URL", "conditional": { "rules": [ {"field": "content_type", "operator": "==", "value": "video"} ], "logic": "AND" } } Оператори: == равно != различно > < по-голямо / по-малко (за number) contains съдържа текст not_empty полето НЕ е празно empty полето Е празно Логика: AND — ВСИЧКИ правила трябва да са верни OR — поне ЕДНО правило да е вярно Множество правила: "rules": [ {"field": "has_video", "operator": "==", "value": true}, {"field": "video_platform", "operator": "!=", "value": "none"} ] → Video URL се показва САМО ако has_video=true И video_platform≠none

29.7 Layout полета (Tab, Accordion, Message)

Тези полета НЕ пазят данни — организират визуално другите полета:

Tab поле:[General] [Media] [SEO] ──────────────────────┐ │ Title: [___________] │ │ Subtitle: [________] │ │ Content: [WYSIWYG..] │ └─────────────────────────────────────────────────────┘ Accordion поле: ┌ ▾ General Settings ────────────────────────────────┐ │ Title: [___________] │ │ Subtitle: [________] │ └─────────────────────────────────────────────────────┘ ┌ ▸ Media Settings (collapsed) ───────────────────────┐ └─────────────────────────────────────────────────────┘ ┌ ▸ SEO Settings (collapsed) ────────────────────────┐ └─────────────────────────────────────────────────────┘ Message поле: ┌─────────────────────────────────────────────────────┐ │ Upload a cover image at least 1200×630 pixels │ │ for optimal social media sharing. │ └─────────────────────────────────────────────────────┘ Не пази данни — просто показва инструкции.

29.8 Field Groups — как полета се свързват с content types

В ACF Pro, Field Group = колекция от полета, прикрепена към определени типове. При нас — fields_json е в самия content type. НО можем и external field groups:

Field group с location rules: { "id": "product_details", "label": "Product Details", "location": [ {"content_type": "product"}, ← показвай за products {"content_type": "page", "slug": "shop"} ← И за page "shop" ], "position": "normal", ← normal | side | after_title "label_placement": "top", ← top | left "fields": [ {"key": "price", "type": "number", "prepend": "$"}, {"key": "sku", "type": "text"}, ... ] } Location rules (кога да се показва): content_type == "product" Всички продукти content_type == "page" AND slug == "shop" Конкретна страница user_role == "admin" Само за админи locale == "bg" Само за български taxonomy:category == "featured" Само featured items

29.9 JSON структура на field дефиниция

Пълен пример — product content type с всички видове полета: { "id": "product", "label": "Product", "slug_prefix": "products/", "fields_json": [ // --- Tab: General --- {"key": "_tab_general", "type": "tab", "label": "General"}, {"key": "subtitle", "type": "text", "label": "Subtitle", "placeholder": "Short product description", "max_length": 120, "wrapper_width": 100}, {"key": "price", "type": "number", "label": "Price", "min": 0, "step": 0.01, "prepend": "$", "required": true, "wrapper_width": 33}, {"key": "sale_price", "type": "number", "label": "Sale Price", "min": 0, "step": 0.01, "prepend": "$", "wrapper_width": 33, "conditional": {"rules": [{"field": "on_sale", "operator": "==", "value": true}]}}, {"key": "on_sale", "type": "toggle", "label": "On Sale", "wrapper_width": 33}, {"key": "sku", "type": "text", "label": "SKU", "wrapper_width": 50}, {"key": "in_stock", "type": "toggle", "label": "In Stock", "default_value": true, "wrapper_width": 50}, // --- Tab: Media --- {"key": "_tab_media", "type": "tab", "label": "Media"}, {"key": "cover", "type": "image", "label": "Cover Image", "required": true, "min_width": 800, "min_height": 600}, {"key": "gallery", "type": "gallery", "label": "Product Gallery", "min": 1, "max": 20}, // --- Tab: Details --- {"key": "_tab_details", "type": "tab", "label": "Details"}, {"key": "_msg_specs", "type": "message", "label": "Add technical specifications below."}, {"key": "specs", "type": "repeater", "label": "Specifications", "button_label": "+ Add Spec", "layout": "table", "sub_fields": [ {"key": "name", "type": "text", "label": "Name", "wrapper_width": 40}, {"key": "value", "type": "text", "label": "Value", "wrapper_width": 60} ]}, {"key": "variants", "type": "flexible", "label": "Variants", "button_label": "+ Add Variant", "layouts": [ { "name": "size_variant", "label": "Size", "sub_fields": [ {"key": "sizes", "type": "checkbox", "label": "Available Sizes", "choices": [{"value":"xs","label":"XS"},{"value":"s","label":"S"}, {"value":"m","label":"M"},{"value":"l","label":"L"},{"value":"xl","label":"XL"}]} ] }, { "name": "color_variant", "label": "Color", "sub_fields": [ {"key": "color_name", "type": "text", "label": "Color Name"}, {"key": "color_value", "type": "color", "label": "Color"}, {"key": "color_image", "type": "image", "label": "Color Image"} ] } ]}, {"key": "related", "type": "relation_multi", "label": "Related Products", "related_type": "product", "max": 4, "show_thumbnail": true, "bidirectional": true} ] }

29.10 Какво ние имаме, а ACF Pro НЯМА

ФункцияACF ProWebFactor CMS
Headless APIПлъгин (WPGraphQL)Вградено (REST)
TypeScript типовеРъчноAuto-generated
Multi-language fieldsWPML плъгинВградено
SEO полетаYoast плъгинВградено
Media optimizationПлъгинWebP/AVIF при upload
Focal point cropНеВградено
Build/deploy— (PHP dynamic)Selective SSG build
0 RAM в покой50-200MB 24/70 MB
DB ефективност2 meta записа per field1 JSON колона per item
Section видимост per localeНеvisible_locales
Global reusable sectionsReusable blocks (limited)global_ref с auto-rebuild
Build dependencies— (не е SSG)Декларативни depends_on
Repeater performanceБавно при 10+ редаJSON = бързо при 1000+
✅ Защо JSON е по-бързо от ACF

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.


WebFactor CMS — пълна спецификация

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 трябва само за компилация. Може на друг компютър.