Дата: 2026-01-02
Статус: Практические выводы
Уровень: Архитектор
ЧАСТО МЕНЯЕТСЯ РЕДКО МЕНЯЕТСЯ
↓ ↓
ВНЕШНИЙ ИСТОЧНИК Ссылка/Macro Ссылка (с cache)
(fetch on render) (fetch on deploy)
НАША СИСТЕМА Встроено Встроено
(copy on import) (snapshot)
JIRA (external system with changing data)
↓ Reference via macro
Confluence page: jira-issue(key=PROJ-123)
↓ On render (dynamic)
User sees: Latest issue status, assignee, etc.
Почему macro (ссылка)?
- JIRA обновляется часто
- Confluence не должна содержать копию
- При render → fetch latest state
SCENARIO A: Часто меняются цены
Price source (external)
↓ API on render
Webpage shows: Latest price
SCENARIO B: Fixed pricing для документации
Copy into document (embed)
↓ Manual sync (or CI)
Document shows: Price "frozen" at publish time
Выбор:
- A: Real-time → Links/API
- B: Historical → Embed
Когда:
- Контент редко меняется
- Нужна независимость от источника
- Performance критична (нет extra requests)
- Документ должен быть "frozen" на момент публикации
Реализация:
1. Source → Document (import/copy)
2. Store in DB
3. On render → Show stored version
4. Update → Manual trigger or scheduled import
Example: GitBook
├── Source: Git repository
├── Storage: GitBook DB + Git (synced)
├── Render: Stored markdown
└── Update: On push to Git
Плюсы:
- Быстро (нет extra requests)
- Independent (не зависит от external)
- Versioned (история всех versions)
- Clear attribution (видно что и когда импортировали)
Минусы:
- Outdated risk (если source изменится)
- Data duplication
- Sync overhead
Когда:
- Контент часто меняется
- Нужны всегда актуальные данные
- External source достаточно стабилен
- Performance не критична
Реализация:
1. Document contains reference (URL, ID, macro)
2. On render: Fetch from source
3. Show fetched content to user
4. Caching: Optional (для performance)
Example: Confluence macro
├── Page: jira-issue(key=PROJ-123)
├── On render: Fetch from JIRA API
├── Show: Live issue data
└── Update: Automatic (next page load)
Плюсы:
- Always fresh (latest data)
- No duplication
- No sync problems
- Single source of truth
Минусы:
- Slower (extra API call)
- Depends on external (downtime risk)
- No offline access
- Version history complex
Рекомендуемый паттерн для документ-систем.
Концепция:
Git Repository
(Source of truth, versioned)
↓
├─→ [Web server] → Render & Cache
│ → Show to user
├─→ [Database] → Metadata, search index
└─→ [Export] → Markdown, HTML, JSON
Преимущества:
| Аспект | Выгода |
|---|---|
| Версионирование | Git commits (полная история) |
| Portability | Markdown в git (export anytime) |
| Collaboration | Git workflow + UI edits |
| Backup | Git is inherent backup |
| Performance | Cache on server |
| Independence | Not tied to one platform |
Реализация: GitBook approach
.gitbook.yaml
├─ github:
│ owner: org
│ repo: docs
│ branch: main
└─ publish: on-merge
Workflow:
1. Edit in GitBook UI
↓
2. Save → auto-commit to GitHub
↓
3. CI/CD pulls latest
↓
4. Render & cache
↓
5. Serve to users
Sync strategy (2-way):
GitBook UI change
↓ Save
Git commit
↓ Pull
Web server refresh
↓
User sees latest
Используют: Notion, Coda
Document
├── Block 1 (type: paragraph)
│ ├── id: UUID
│ ├── type: "paragraph"
│ ├── content: RichText[]
│ ├── properties: {}
│ └── parent_id: UUID
├── Block 2 (type: heading_1)
│ └── ...
├── Block N (type: database_page)
│ └── Fields: [field1, field2, ...]
Хранение:
blocks table:
id UUID PRIMARY KEY
parent_id UUID (nullable)
type VARCHAR
properties JSONB
created_by UUID
edited_by UUID
created_at TIMESTAMP
updated_at TIMESTAMP
position INTEGER (for ordering)
Плюсы:
- Гибкость (трансформация блоков)
- Granular versioning
- Nested structure
- Rich metadata
Минусы:
- Сложнее в query (много JOINs)
- Performance on huge documents
- Complex consistency checks
Используют: Confluence, traditional wikis
Document (Space)
├── Page 1
│ ├── title: str
│ ├── body: XHTML
│ ├── metadata: {}
│ └── attachments: [File]
├── Page 2
│ └── children: [Page 3, Page 4]
├── Page 3
└── Page 4
Хранение:
pages table:
id BIGINT PRIMARY KEY
space_id INT
parent_id BIGINT (nullable)
title VARCHAR
body CLOB (XHTML)
created_by INT
updated_by INT
created_date TIMESTAMP
updated_date TIMESTAMP
attachments table:
id INT
page_id BIGINT
filename VARCHAR
data BLOB
comments table:
id INT
page_id BIGINT
body XHTML
author INT
Плюсы:
- Простая иерархия (tree)
- Clear permissions per page
- Easy to archive
- Efficient for document organization
Минусы:
- Comments as separate entities
- XHTML not portable
- Less granular tracking
Используют: GitBook, Obsidian, GitHub Pages
Repository
├── README.md (root page)
├── docs/
│ ├── guide/
│ │ ├── index.md
│ │ ├── getting-started.md
│ │ └── advanced.md
│ ├── api/
│ │ ├── overview.md
│ │ └── endpoints.md
│ └── faq.md
├── .gitbook.yaml (or jekyll config)
└── assets/
└── images/
Хранение:
File system (Git repository)
├── Markdown files
├── Binary assets
└── Config files (YAML)
Database (optional):
├── Metadata (search index)
├── Comments
├── Permissions
└── Analytics
Плюсы:
- Portable (plain markdown)
- Version control (git commits)
- Easy backup
- Works with any text editor
- Portability
Минусы:
- Comments not in markdown (separate DB)
- Search requires indexing
- No real-time collaboration (depends on sync)
Вопрос 1: Контент часто меняется?
├─ ДА → Паттерн 2 (Links)
└─ НЕТ → Паттерн 1 (Embedded)
Вопрос 2: Нужна portability?
├─ ДА → Паттерн C (Markdown)
└─ НЕТ → Паттерн A или B
Вопрос 3: Нужна full versioning?
├─ ДА → Паттерн 3 (Git + Embedded)
└─ НЕТ → Паттерн A (Blocks)
| Выбор | Хранилище | Структура |
|---|---|---|
| Максимальная гибкость | PostgreSQL | Blocks (A) |
| Традиционные docs | Database | Pages (B) |
| Максимальная portability | Git | Markdown (C) |
| Recommended для нового | Git + DB | Hybrid (C + metadata) |
GET /documents/{id}/blocks
POST /documents/{id}/blocks
PATCH /blocks/{block_id}
DELETE /blocks/{block_id}
GET /blocks/{block_id}/children
GET /spaces/{space_id}/pages
POST /spaces/{space_id}/pages
PATCH /pages/{page_id}
GET /pages/{page_id}/children
GET /pages/{page_id}/comments
GET /documents/{id}
GET /documents/{id}/pages
PATCH /documents/{id}/pages/{page_id}
GET /documents/{id}/revisions
class BlockEditor {
blocks: Block[];
addBlock(type: string, position: number);
removeBlock(id: string);
updateBlock(id: string, content: unknown);
transformBlock(id: string, newType: string);
// Collaboration
onRemoteChange(change: Change);
publishChange(change: Change);
}
class DocumentEditor {
modes: 'wysiwyg' | 'markdown' | 'split';
// WYSIWYG side
blocks: Block[];
// Markdown side
source: string;
syncWYSIWYG2Markdown();
syncMarkdown2WYSIWYG();
}
Conflict-free approach (CRDTs):
User A changes block 1
User B changes block 2
Both changes merge automatically
(no conflict - different blocks)
WebSocket protocol:
Client → Server: {action: "update_block", id: "...", content: "..."}
Server → All clients: {action: "block_updated", ...}
┌─────────────────────────────────────────────┐
│ Document Management System │
└─────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────┐
│ Storage Layer │
├──────────────────────────────────────────────────┤
│ Primary: Git repository (markdown) │
│ Secondary: PostgreSQL (metadata, blocks, collab) │
│ Tertiary: S3/local (binary assets) │
└──────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────┐
│ Sync Layer │
├──────────────────────────────────────────────────┤
│ Git ↔ DB (bidirectional): │
│ - On Git push → update DB metadata │
│ - On UI change → commit to Git │
│ Conflict resolution: Last-write-wins │
└──────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────┐
│ API Layer │
├──────────────────────────────────────────────────┤
│ REST: CRUD documents, blocks, pages │
│ WebSocket: Real-time collaboration │
│ Webhooks: External integrations │
└──────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────┐
│ Editor Layer (Frontend) │
├──────────────────────────────────────────────────┤
│ Block-based editor (WYSIWYG) │
│ Markdown source view │
│ Real-time cursor tracking │
│ Comments/discussions │
└──────────────────────────────────────────────────┘
Решение:
❌ Load entire document
→ Document.blocks.length = 10,000
→ Render: O(n) = slow
✅ Lazy load + virtualization
→ Load only visible blocks
→ Render: O(visible) = fast
✅ Split into pages
→ Document → Pages
→ Each page ~200 blocks
→ Load on demand
Решение: Operational Transform or CRDT
User A: Insert "hello" at pos 0
Document: "hello world"
User B: Delete char at pos 0
Document: "ello world"
Conflict?
A sees: "hello world"
B sees: "ello world"
Solution (CRDT): Assign unique IDs to chars
A inserts: {id: A-1, char: 'h', after: null}
B deletes: {id: A-2} ← knows which char
Result: Both converge to "ello world"
Решение: Git-based versioning
Real-time edits in memory/DB
↓ Every N seconds or on explicit save
Commit to Git
↓
Full history preserved
↓
Can rollback anytime
Решение: Full-text index
Documents → Extract text
→ Index (Elasticsearch, PostgreSQL FTS)
→ Query
→ Return results with highlighting
Example (PostgreSQL FTS):
CREATE INDEX documents_search_idx
ON documents USING GIN(to_tsvector('english', content));
SELECT * FROM documents
WHERE to_tsvector('english', content) @@ to_tsquery('english', 'search term')
ORDER BY ts_rank(...);
Перед выбором системы/паттерна:
✅ Obsidian (Markdown files + local + optional sync)
Причины:
- Полная контроль (local files)
- Портативность (markdown)
- Extensible (plugins)
- Cheap ($0-48/год)
✅ GitBook (Git + Markdown)
Причины:
- Git integration (versioning)
- Markdown (portable)
- Pretty publishing
- Easy collaboration (Git flow)
✅ Confluence (Page hierarchy + XHTML + macros)
Причины:
- Enterprise features (permissions, SSO)
- Macros (JIRA, Trello integration)
- Full-text search
- Built-in comments
✅ Notion (Blocks + Databases + Automation)
Причины:
- Unified platform (docs + db)
- Automation (automation engine)
- Flexible (blocks)
- Great for teams
✅ Coda (Canvas + Tables + Automations)
Причины:
- Interactive (buttons, automations)
- Real-time collaboration
- Lightweight than Notion
Конец документа