Русский · English
Public architectural description — principles and structure only. No client data, source code, or proprietary detail.
-
Git — source of truth. Everything that defines the system is a file in the repo: pipeline scripts, niche DNA (taxonomies), client master taxonomies, branch geo configs, templates, DB migrations. Why: git log is a built-in audit trail; rollback is free; routine edits need no database; cloning the repo provisions the whole system.
-
Pipeline — deterministic scripts. Idempotent Python turns sources into intermediate artifacts:
mapper(taxonomy + DNA → site blueprint),generator(blueprint → per-page specs),tech_deploy(sitemap.xml + robots.txt). Re-running yields identical output. -
Database — runtime copy. A
syncstep loads yaml + artifacts into PostgreSQL; from then on the CMS reads only from the DB (parsing yaml per HTTP request is slow). The DB enables fast SSR reads, manual edits, change history, multi-tenancy, and analytics. -
Admin — SQLAdmin + RBAC. A web UI over the ORM models: list / form / filters per model, custom actions (re-render, trigger generation), authorization with roles (owner / admin / editor / viewer) and per-row tenant scoping.
-
Frontend — server-side rendering (Jinja2 + HTMX). Every page is rendered on the server; the browser receives complete HTML with no JS content-loading. Critical for GEO: AI crawlers parse HTML without executing JS. A page = a DB record + an ordered list of typed blocks
- meta (title, description, Schema.org JSON-LD). Brand surfaces use the client brand; geo surfaces use the branch — no client hardcoding.
-
Reverse proxy — Caddy. One instance routes each domain to the backend by Host header, terminates automatic Let's Encrypt SSL per domain, and handles HTTP→HTTPS, compression, and static caching.
inputs (catalog, geo) + niche DNA
| deterministic pipeline
v
per-page specs + sitemap + robots
| sync
v
PostgreSQL (content + per-field source)
|--> editor (admin UI: edit / preview / publish)
`--> deploy -> versioned build -> Caddy -> visitor / AI crawler
All tables carry tenant_id + timestamps (omitted below).
- clients — slug, name, brand_name, niche, plan, active
- branches — client_id, slug, name, domain (unique), city, district, address, metros, og_image_url
- services — branch_id, category, slug, name, price, source, order
- pages — branch_id, slug, template_name, title, description, h1, canonical, meta_json, schema_org_json_ld, published, source, locked
- blocks — page_id, block_type, order, content_json, source
- lsi_phrases — page_id, phrase, volume, source
- users — email, role, tenant_scope, password_hash
- audit_log — user_id, entity, action, diff_json, at
- deploy_history — client_id, branch_id, version, status, log
- jobs — type, payload, status (async work: content generation, etc.)
Every field that can come from multiple origins records a source
(yaml / csv / llm / manual / api). On sync: a record whose
source ≠ yaml is skipped; new or yaml-sourced records are updated —
per field, not per row. "Reset to auto" returns a field to yaml so the
next sync refreshes it. This is what lets regeneration and human editing
coexist safely.
One DB to start; every query is automatically scoped with
WHERE tenant_id = ? via middleware + an ORM filter event. When a
client outgrows the shared resource: spin up a separate compose
instance, ETL-copy that tenant's rows, point its domains at the new
instance through the top-level proxy, drop the old copy. The code always
filters, so escalation is ETL — not a rewrite.
- Pre-flight validate — required fields present, no broken links, sitemap valid.
- Build — render all tenant pages into a versioned directory.
- Atomic switch — the proxy flips to the new directory by symlink replace.
- Health check — fetch
/,/sitemap.xml,/robots.txt+ a few random pages; a non-200 rolls back to the previous build. - Record — write to
deploy_history. The last N builds are retained for one-click rollback.
- FileLock — a cross-platform advisory lock (sidecar lockfile with pid + timestamp, stale-reclaim when the owner is dead) serializes load-merge-write on shared semantic banks, preventing lost updates when parallel jobs touch the same file.
- Atomic writes — each write is tmpfile +
os.replace. - Retry — transient failures are retried around the lock.
Each published domain serves: SSR home + service / category / article
pages, /sitemap.xml, a forward-compatible /sitemap-index.xml,
/robots.txt (with Sitemap lines), and a brand-styled 404 (HTML or
JSON). A CI smoke step exercises these contracts via in-process ASGI on
every push, failing before merge if a contract breaks.
A test pyramid: unit (pipeline functions, models), integration (API via in-process ASGI + SQLite, plus the same suite on real PostgreSQL for prod parity), smoke endpoints (SEO contracts), and post-deploy health checks. CI runs pytest + the full pipeline + SEO guards (no duplicate H1, sitemap-equals-pages, content filters) on every push.
SPA frameworks (hurt TTFB and GEO), a headless CMS (extra stack), microservices (overkill at this scale), a custom ORM / queue / RBAC (SQLAlchemy / arq / SQLAdmin already fit). The system stays a lean monolith with a clear path to scale out only where needed.