vault backup: 2025-08-04 15:28:34

This commit is contained in:
Andrey Epifancev
2025-08-04 15:28:34 +04:00
parent 7e1fbcf087
commit e46521074c
3 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,764 @@
# Hugo миграция и настройка
## 1. Миграция контента
### 1.1 Структура контента
**Текущая структура (Quartz):**
```
content/
├── notes/
│ ├── Идеи/
│ ├── Мой сервер/
│ └── index.md
├── daily/
└── templates/
```
**Новая структура (Hugo):**
```
content/
├── notes/
│ ├── идеи/
│ │ ├── obsidian-telegram-bot/
│ │ │ ├── mvp-telegram-bota.md
│ │ │ └── telegram-bot-dlya-obsidian.md
│ │ └── pereezd-na-hugo/
│ │ ├── plan-pereezda-na-hugo.md
│ │ ├── webhook-server-na-go.md
│ │ └── hugo-migratsiya-i-nastrojka.md
│ ├── мой-сервер/
│ │ ├── authelia-authentication/
│ │ ├── git-service/
│ │ ├── second-mind-setup/
│ │ └── traefik-reverse-proxy/
│ └── _index.md
├── daily/
│ └── _index.md
└── templates/
└── _index.md
```
### 1.2 Скрипт миграции
```python
#!/usr/bin/env python3
# scripts/migrate_content.py
import os
import re
import shutil
from pathlib import Path
def slugify(text):
"""Преобразование текста в slug для URL"""
text = text.lower()
text = re.sub(r'[^\w\s-]', '', text)
text = re.sub(r'[-\s]+', '-', text)
return text.strip('-')
def migrate_content():
"""Миграция контента из Quartz в Hugo"""
# Пути
quartz_content = "quartz/content"
hugo_content = "hugo-site/content"
# Создание структуры Hugo
os.makedirs(hugo_content, exist_ok=True)
# Обработка каждой директории
for root, dirs, files in os.walk(quartz_content):
# Пропускаем служебные директории
if any(skip in root for skip in ['.obsidian', '.git', '__pycache__']):
continue
# Определяем относительный путь
rel_path = os.path.relpath(root, quartz_content)
# Создаем соответствующую структуру в Hugo
hugo_path = os.path.join(hugo_content, rel_path)
os.makedirs(hugo_path, exist_ok=True)
# Обрабатываем файлы
for file in files:
if file.endswith('.md'):
migrate_markdown_file(
os.path.join(root, file),
os.path.join(hugo_path, file)
)
def migrate_markdown_file(src_path, dst_path):
"""Миграция отдельного Markdown файла"""
with open(src_path, 'r', encoding='utf-8') as f:
content = f.read()
# Обработка frontmatter
content = process_frontmatter(content)
# Обработка внутренних ссылок
content = process_internal_links(content)
# Обработка изображений
content = process_images(content)
# Сохранение файла
with open(dst_path, 'w', encoding='utf-8') as f:
f.write(content)
def process_frontmatter(content):
"""Обработка YAML frontmatter для Hugo"""
# Проверяем наличие frontmatter
if content.startswith('---'):
# Извлекаем frontmatter
parts = content.split('---', 2)
if len(parts) >= 3:
frontmatter = parts[1]
body = parts[2]
# Добавляем Hugo-специфичные поля
frontmatter = add_hugo_frontmatter(frontmatter)
return f"---\n{frontmatter}\n---\n{body}"
# Если frontmatter отсутствует, создаем базовый
title = extract_title_from_content(content)
return f"""---
title: "{title}"
date: {get_current_date()}
draft: false
---
{content}"""
def add_hugo_frontmatter(frontmatter):
"""Добавление Hugo-специфичных полей в frontmatter"""
# Добавляем базовые поля если их нет
if 'draft' not in frontmatter:
frontmatter += '\ndraft: false'
if 'date' not in frontmatter:
frontmatter += f'\ndate: {get_current_date()}'
return frontmatter
def process_internal_links(content):
"""Обработка внутренних ссылок для Hugo"""
# Заменяем [[wiki links]] на Hugo ссылки
content = re.sub(r'\[\[([^\]]+)\]\]', r'[{{< ref "\1" >}}]', content)
# Обрабатываем обычные ссылки
content = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', process_link, content)
return content
def process_link(match):
"""Обработка ссылок"""
text = match.group(1)
url = match.group(2)
# Если это внутренняя ссылка на .md файл
if url.endswith('.md'):
# Преобразуем в Hugo ссылку
slug = slugify(url.replace('.md', ''))
return f'[{text}]({{{{< ref "{slug}" >}}}})'
return match.group(0)
def process_images(content):
"""Обработка изображений"""
# Перемещаем изображения в static директорию
# и обновляем ссылки
content = re.sub(r'!\[([^\]]*)\]\(([^)]+)\)', process_image, content)
return content
def process_image(match):
"""Обработка изображений"""
alt = match.group(1)
src = match.group(2)
# Если изображение локальное, перемещаем в static
if not src.startswith(('http://', 'https://')):
# Копируем изображение в static/images
static_path = f"static/images/{os.path.basename(src)}"
if os.path.exists(src):
shutil.copy2(src, static_path)
# Обновляем ссылку
src = f"/images/{os.path.basename(src)}"
return f'![{alt}]({src})'
if __name__ == "__main__":
migrate_content()
print("Миграция контента завершена!")
```
## 2. Настройка Hugo
### 2.1 Базовая конфигурация
```toml
# config.toml
baseURL = "https://aepif.ru"
languageCode = "ru-ru"
title = "Second Mind"
theme = "custom-theme"
[params]
description = "Персональная база знаний и заметки"
author = "AEP"
github = "https://github.com/username"
# Настройки поиска
enableSearch = true
searchResultsLength = 10
# Настройки навигации
showBreadcrumb = true
showReadingTime = true
showWordCount = true
[menu]
[[menu.main]]
identifier = "notes"
name = "Заметки"
url = "/notes/"
weight = 1
[[menu.main]]
identifier = "daily"
name = "Дневник"
url = "/daily/"
weight = 2
[[menu.main]]
identifier = "templates"
name = "Шаблоны"
url = "/templates/"
weight = 3
[markup]
[markup.goldmark]
[markup.goldmark.renderer]
unsafe = true
[markup.highlight]
style = "dracula"
lineNos = true
lineNumbersInTable = true
[outputs]
home = ["HTML", "RSS", "JSON"]
[outputFormats]
[outputFormats.JSON]
mediaType = "application/json"
baseName = "index"
isPlainText = true
notAlternative = true
```
### 2.2 Структура темы
```
themes/custom-theme/
├── assets/
│ ├── css/
│ │ ├── main.scss
│ │ └── _variables.scss
│ ├── js/
│ │ └── main.js
│ └── images/
├── layouts/
│ ├── _default/
│ │ ├── baseof.html
│ │ ├── list.html
│ │ └── single.html
│ ├── partials/
│ │ ├── head.html
│ │ ├── header.html
│ │ ├── footer.html
│ │ ├── navigation.html
│ │ ├── search.html
│ │ └── breadcrumb.html
│ ├── shortcodes/
│ │ ├── note.html
│ │ └── warning.html
│ └── index.html
├── static/
│ ├── css/
│ ├── js/
│ └── images/
└── theme.toml
```
### 2.3 Базовая тема
```html
<!-- layouts/_default/baseof.html -->
<!DOCTYPE html>
<html lang="{{ .Site.LanguageCode }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} | {{ .Site.Title }}{{ end }}</title>
{{ $style := resources.Get "css/main.scss" | resources.ToCSS | resources.Minify }}
<link rel="stylesheet" href="{{ $style.RelPermalink }}">
{{ partial "head.html" . }}
</head>
<body>
{{ partial "header.html" . }}
<main class="container">
{{ block "main" . }}{{ end }}
</main>
{{ partial "footer.html" . }}
{{ $script := resources.Get "js/main.js" | resources.Minify }}
<script src="{{ $script.RelPermalink }}"></script>
</body>
</html>
```
```html
<!-- layouts/_default/single.html -->
{{ define "main" }}
<article class="note">
<header class="note-header">
<h1>{{ .Title }}</h1>
{{ if .Params.date }}
<time datetime="{{ .Date.Format "2006-01-02" }}">
{{ .Date.Format "2 January 2006" }}
</time>
{{ end }}
</header>
{{ partial "breadcrumb.html" . }}
<div class="note-content">
{{ .Content }}
</div>
{{ if .Params.tags }}
<footer class="note-footer">
<div class="tags">
{{ range .Params.tags }}
<a href="{{ "tags/" | relLangURL }}{{ . | urlize }}" class="tag">{{ . }}</a>
{{ end }}
</div>
</footer>
{{ end }}
</article>
{{ end }}
```
### 2.4 Стили
```scss
// assets/css/main.scss
@import "variables";
// Базовые стили
body {
font-family: $font-family-base;
line-height: 1.6;
color: $text-color;
background-color: $bg-color;
}
.container {
max-width: $container-width;
margin: 0 auto;
padding: 0 $spacing;
}
// Заметки
.note {
background: $note-bg;
border-radius: $border-radius;
padding: $spacing * 2;
margin-bottom: $spacing * 2;
box-shadow: $box-shadow;
&-header {
border-bottom: 1px solid $border-color;
padding-bottom: $spacing;
margin-bottom: $spacing * 2;
h1 {
margin: 0 0 $spacing 0;
color: $heading-color;
}
time {
color: $muted-color;
font-size: 0.9em;
}
}
&-content {
font-size: 1.1em;
line-height: 1.7;
h1, h2, h3, h4, h5, h6 {
color: $heading-color;
margin-top: $spacing * 2;
margin-bottom: $spacing;
}
p {
margin-bottom: $spacing;
}
code {
background: $code-bg;
padding: 0.2em 0.4em;
border-radius: 3px;
font-size: 0.9em;
}
pre {
background: $code-bg;
padding: $spacing;
border-radius: $border-radius;
overflow-x: auto;
code {
background: none;
padding: 0;
}
}
}
}
// Навигация
.navigation {
background: $nav-bg;
padding: $spacing;
margin-bottom: $spacing * 2;
border-radius: $border-radius;
ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
gap: $spacing;
}
a {
color: $nav-link-color;
text-decoration: none;
&:hover {
color: $nav-link-hover-color;
}
}
}
// Поиск
.search {
margin-bottom: $spacing * 2;
input {
width: 100%;
padding: $spacing;
border: 1px solid $border-color;
border-radius: $border-radius;
font-size: 1em;
&:focus {
outline: none;
border-color: $primary-color;
}
}
}
// Хлебные крошки
.breadcrumb {
margin-bottom: $spacing;
font-size: 0.9em;
color: $muted-color;
a {
color: $link-color;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
```
## 3. Оптимизации
### 3.1 Производительность
```toml
# config.toml (дополнительные настройки)
[build]
writeStats = true
[imaging]
quality = 85
resampleFilter = "Lanczos"
[minify]
[minify.tdewolff]
[minify.tdewolff.css]
keepCSS2 = true
precision = 0
[minify.tdewolff.html]
keepConditionalComments = true
keepDefaultDoctype = true
keepDocumentTags = true
keepEndTags = true
keepQuotes = false
keepWhitespace = false
[minify.tdewolff.js]
keepVarNames = false
precision = 0
[minify.tdewolff.json]
keepNumbers = false
precision = 0
[minify.tdewolff.svg]
keepComments = false
[minify.tdewolff.xml]
keepWhitespace = false
```
### 3.2 SEO оптимизация
```html
<!-- layouts/partials/head.html -->
{{ if .IsHome }}
<meta name="description" content="{{ .Site.Params.description }}">
{{ else }}
<meta name="description" content="{{ .Summary }}">
{{ end }}
<meta name="author" content="{{ .Site.Params.author }}">
<meta name="robots" content="index, follow">
<!-- Open Graph -->
<meta property="og:title" content="{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }}{{ end }}">
<meta property="og:description" content="{{ if .IsHome }}{{ .Site.Params.description }}{{ else }}{{ .Summary }}{{ end }}">
<meta property="og:type" content="{{ if .IsHome }}website{{ else }}article{{ end }}">
<meta property="og:url" content="{{ .Permalink }}">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }}{{ end }}">
<meta name="twitter:description" content="{{ if .IsHome }}{{ .Site.Params.description }}{{ else }}{{ .Summary }}{{ end }}">
<!-- Canonical URL -->
<link rel="canonical" href="{{ .Permalink }}">
```
### 3.3 Поиск
```javascript
// assets/js/search.js
class Search {
constructor() {
this.searchIndex = null;
this.searchInput = document.getElementById('search-input');
this.searchResults = document.getElementById('search-results');
this.init();
}
async init() {
await this.loadSearchIndex();
this.bindEvents();
}
async loadSearchIndex() {
try {
const response = await fetch('/index.json');
this.searchIndex = await response.json();
} catch (error) {
console.error('Failed to load search index:', error);
}
}
bindEvents() {
this.searchInput.addEventListener('input', (e) => {
this.search(e.target.value);
});
}
search(query) {
if (!this.searchIndex || !query.trim()) {
this.hideResults();
return;
}
const results = this.searchIndex.filter(item => {
const searchText = `${item.title} ${item.content}`.toLowerCase();
return searchText.includes(query.toLowerCase());
});
this.displayResults(results.slice(0, 10));
}
displayResults(results) {
if (results.length === 0) {
this.searchResults.innerHTML = '<p>Ничего не найдено</p>';
} else {
const html = results.map(item => `
<div class="search-result">
<h3><a href="${item.url}">${item.title}</a></h3>
<p>${item.content.substring(0, 150)}...</p>
</div>
`).join('');
this.searchResults.innerHTML = html;
}
this.showResults();
}
showResults() {
this.searchResults.style.display = 'block';
}
hideResults() {
this.searchResults.style.display = 'none';
}
}
// Инициализация поиска
document.addEventListener('DOMContentLoaded', () => {
new Search();
});
```
## 4. Тестирование
### 4.1 Локальная разработка
```bash
#!/bin/bash
# scripts/dev.sh
echo "Запуск Hugo в режиме разработки..."
# Установка зависимостей
hugo mod get
# Запуск сервера разработки
hugo server \
--bind 0.0.0.0 \
--port 1313 \
--disableFastRender \
--noHTTPCache \
--buildDrafts \
--buildFuture
```
### 4.2 Тестирование сборки
```bash
#!/bin/bash
# scripts/test-build.sh
set -e
echo "Тестирование сборки Hugo..."
# Очистка предыдущей сборки
rm -rf public/
# Сборка сайта
hugo --minify
# Проверка размера
echo "Размер сборки:"
du -sh public/
# Проверка количества страниц
echo "Количество страниц:"
find public/ -name "*.html" | wc -l
echo "Сборка завершена успешно!"
```
## 5. Развертывание
### 5.1 Скрипт сборки для production
```bash
#!/bin/bash
# scripts/build-production.sh
set -e
echo "Сборка Hugo для production..."
# Очистка
rm -rf public/
# Сборка с оптимизациями
hugo \
--minify \
--gc \
--cleanDestinationDir \
--environment production
# Оптимизация изображений
find public/ -name "*.jpg" -o -name "*.png" | xargs -I {} convert {} -strip -quality 85 {}
# Сжатие статических файлов
find public/ -name "*.css" -o -name "*.js" | xargs gzip -9
echo "Production сборка завершена!"
```
### 5.2 Nginx конфигурация
```nginx
# configs/nginx.conf
server {
listen 80;
server_name aepif.ru www.aepif.ru;
root /var/www/html;
index index.html;
# Gzip сжатие
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
# Кэширование статических файлов
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Обработка Hugo страниц
location / {
try_files $uri $uri/ $uri.html =404;
}
# Обработка 404 ошибок
error_page 404 /404.html;
# Безопасность
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}
```

View File

@@ -0,0 +1,549 @@
# Webhook Server на Go
## 1. Обзор проекта
### 1.1 Назначение
Webhook сервер на Go для автоматической сборки и деплоя Hugo сайта при изменениях в Git репозитории.
### 1.2 Ключевые функции
- Обработка webhook от Git (GitHub, GitLab, Gitea)
- Автоматическое клонирование/обновление репозитория
- Запуск Hugo сборки
- Валидация и безопасность
- Логирование и мониторинг
- Обработка ошибок и retry логика
## 2. Архитектура
### 2.1 Компонентная диаграмма
```mermaid
graph TB
A[Git Webhook] --> B[Webhook Handler]
B --> C[Validator]
C --> D[Git Client]
D --> E[Repository Manager]
E --> F[Hugo Builder]
F --> G[File System]
G --> H[Nginx Reload]
subgraph "Webhook Server"
B
C
D
E
F
end
subgraph "External"
A
G
H
end
```
### 2.2 Структура проекта
```
webhook-server/
├── cmd/
│ └── server/
│ └── main.go # Точка входа приложения
├── internal/
│ ├── handler/
│ │ ├── webhook.go # Обработчик webhook
│ │ └── health.go # Health check endpoint
│ ├── builder/
│ │ ├── hugo.go # Hugo сборка
│ │ └── git.go # Git операции
│ ├── config/
│ │ └── config.go # Конфигурация
│ ├── validator/
│ │ └── webhook.go # Валидация webhook
│ └── logger/
│ └── logger.go # Структурированное логирование
├── pkg/
│ ├── git/
│ │ └── client.go # Git клиент
│ └── metrics/
│ └── prometheus.go # Метрики Prometheus
├── configs/
│ ├── config.yaml # Конфигурация
│ └── nginx.conf # Nginx конфигурация
├── scripts/
│ ├── build.sh # Скрипт сборки
│ └── deploy.sh # Скрипт деплоя
├── go.mod
├── go.sum
├── Dockerfile
├── docker-compose.yml
└── README.md
```
## 3. Техническая реализация
### 3.1 Основные зависимости
```go
// go.mod
module webhook-server
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
github.com/go-git/go-git/v5 v5.8.1
github.com/prometheus/client_golang v1.17.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.17.0
gopkg.in/go-playground/webhooks.v5 v5.17.0
)
```
### 3.2 Конфигурация
```yaml
# configs/config.yaml
server:
port: 8080
host: "0.0.0.0"
git:
repository: "https://github.com/user/second-mind"
branch: "main"
webhook_secret: "your-webhook-secret"
ssh_key_path: "/root/.ssh/id_rsa"
hugo:
source_path: "/app/hugo-site"
output_path: "/var/www/html"
config_file: "config.toml"
base_url: "https://aepif.ru"
nginx:
config_path: "/etc/nginx/nginx.conf"
reload_command: "nginx -s reload"
logging:
level: "info"
format: "json"
metrics:
enabled: true
port: 9090
```
### 3.3 Основные компоненты
#### Webhook Handler
```go
// internal/handler/webhook.go
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
type WebhookHandler struct {
gitClient *git.Client
hugoBuilder *builder.HugoBuilder
validator *validator.WebhookValidator
logger *logrus.Logger
}
func (h *WebhookHandler) HandleWebhook(c *gin.Context) {
// Валидация webhook
if err := h.validator.Validate(c.Request); err != nil {
h.logger.WithError(err).Error("Webhook validation failed")
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Асинхронная обработка
go h.processWebhook()
c.JSON(http.StatusOK, gin.H{"status": "processing"})
}
func (h *WebhookHandler) processWebhook() {
// Клонирование/обновление репозитория
if err := h.gitClient.Pull(); err != nil {
h.logger.WithError(err).Error("Git pull failed")
return
}
// Hugo сборка
if err := h.hugoBuilder.Build(); err != nil {
h.logger.WithError(err).Error("Hugo build failed")
return
}
// Перезагрузка Nginx
if err := h.reloadNginx(); err != nil {
h.logger.WithError(err).Error("Nginx reload failed")
return
}
h.logger.Info("Deployment completed successfully")
}
```
#### Hugo Builder
```go
// internal/builder/hugo.go
package builder
import (
"os/exec"
"path/filepath"
"github.com/sirupsen/logrus"
)
type HugoBuilder struct {
sourcePath string
outputPath string
configFile string
logger *logrus.Logger
}
func (h *HugoBuilder) Build() error {
h.logger.Info("Starting Hugo build")
cmd := exec.Command("hugo",
"--source", h.sourcePath,
"--destination", h.outputPath,
"--config", filepath.Join(h.sourcePath, h.configFile),
"--minify",
"--cleanDestinationDir",
)
output, err := cmd.CombinedOutput()
if err != nil {
h.logger.WithError(err).WithField("output", string(output)).Error("Hugo build failed")
return err
}
h.logger.WithField("output", string(output)).Info("Hugo build completed")
return nil
}
```
#### Git Client
```go
// pkg/git/client.go
package git
import (
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/sirupsen/logrus"
)
type Client struct {
repositoryPath string
remoteURL string
branch string
logger *logrus.Logger
}
func (c *Client) Pull() error {
c.logger.Info("Starting git pull")
repo, err := git.PlainOpen(c.repositoryPath)
if err != nil {
return err
}
worktree, err := repo.Worktree()
if err != nil {
return err
}
err = worktree.Pull(&git.PullOptions{
RemoteName: "origin",
BranchName: plumbing.NewBranchReferenceName(c.branch),
})
if err != nil && err != git.NoErrAlreadyUpToDate {
c.logger.WithError(err).Error("Git pull failed")
return err
}
c.logger.Info("Git pull completed")
return nil
}
```
### 3.4 Метрики и мониторинг
```go
// pkg/metrics/prometheus.go
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
BuildDuration = promauto.NewHistogram(prometheus.HistogramOpts{
Name: "hugo_build_duration_seconds",
Help: "Duration of Hugo build in seconds",
})
BuildSuccess = promauto.NewCounter(prometheus.CounterOpts{
Name: "hugo_build_success_total",
Help: "Total number of successful builds",
})
BuildFailures = promauto.NewCounter(prometheus.CounterOpts{
Name: "hugo_build_failures_total",
Help: "Total number of failed builds",
})
WebhookRequests = promauto.NewCounter(prometheus.CounterOpts{
Name: "webhook_requests_total",
Help: "Total number of webhook requests",
})
)
```
## 4. Docker контейнеризация
### 4.1 Dockerfile
```dockerfile
# Многоэтапная сборка
FROM golang:1.21-alpine AS builder
# Установка зависимостей
RUN apk add --no-cache git ca-certificates tzdata
WORKDIR /app
# Копирование go.mod и go.sum
COPY go.mod go.sum ./
RUN go mod download
# Копирование исходного кода
COPY . .
# Сборка приложения
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd/server
# Финальный образ
FROM alpine:latest
# Установка необходимых пакетов
RUN apk --no-cache add ca-certificates tzdata hugo git openssh-client nginx
# Создание пользователя
RUN addgroup -g 1000 -S appgroup && \
adduser -u 1000 -S appuser -G appgroup
WORKDIR /app
# Копирование бинарника
COPY --from=builder /app/main .
# Копирование конфигурации
COPY configs/ /app/configs/
# Создание директорий
RUN mkdir -p /var/www/html /root/.ssh && \
chown -R appuser:appgroup /app /var/www/html
# Переключение на пользователя
USER appuser
EXPOSE 8080 9090
CMD ["./main"]
```
### 4.2 Docker Compose
```yaml
# docker-compose.yml
version: '3.8'
services:
webhook-server:
build: .
ports:
- "8080:8080"
- "9090:9090"
volumes:
- ./configs:/app/configs
- /var/www/html:/var/www/html
- ~/.ssh:/root/.ssh:ro
environment:
- CONFIG_PATH=/app/configs/config.yaml
restart: unless-stopped
networks:
- webhook-network
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- /var/www/html:/usr/share/nginx/html
- ./configs/nginx.conf:/etc/nginx/nginx.conf
depends_on:
- webhook-server
restart: unless-stopped
networks:
- webhook-network
networks:
webhook-network:
driver: bridge
```
## 5. Безопасность
### 5.1 Webhook валидация
- Проверка подписи webhook
- Валидация payload
- Rate limiting
- IP whitelist
### 5.2 Git безопасность
- SSH ключи для аутентификации
- Проверка подписи коммитов
- Ограничение доступа к репозиторию
### 5.3 Системная безопасность
- Запуск от непривилегированного пользователя
- Минимальные права доступа
- Регулярные обновления зависимостей
## 6. Мониторинг и логирование
### 6.1 Структурированное логирование
```go
// internal/logger/logger.go
func SetupLogger(level, format string) *logrus.Logger {
logger := logrus.New()
// Уровень логирования
if level, err := logrus.ParseLevel(level); err == nil {
logger.SetLevel(level)
}
// Формат логирования
if format == "json" {
logger.SetFormatter(&logrus.JSONFormatter{})
}
return logger
}
```
### 6.2 Health Check
```go
// internal/handler/health.go
func (h *HealthHandler) HealthCheck(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
"timestamp": time.Now().Unix(),
"version": "1.0.0",
})
}
```
## 7. Развертывание
### 7.1 Скрипт сборки
```bash
#!/bin/bash
# scripts/build.sh
set -e
echo "Building webhook server..."
# Сборка Docker образа
docker build -t webhook-server:latest .
echo "Build completed successfully"
```
### 7.2 Скрипт деплоя
```bash
#!/bin/bash
# scripts/deploy.sh
set -e
echo "Deploying webhook server..."
# Остановка существующих контейнеров
docker-compose down
# Запуск новых контейнеров
docker-compose up -d
echo "Deployment completed successfully"
```
## 8. Тестирование
### 8.1 Unit тесты
```go
// internal/handler/webhook_test.go
func TestWebhookHandler_HandleWebhook(t *testing.T) {
// Тесты валидации webhook
// Тесты обработки ошибок
// Тесты успешной обработки
}
```
### 8.2 Integration тесты
```go
// tests/integration_test.go
func TestFullDeploymentFlow(t *testing.T) {
// Тест полного цикла деплоя
// Тест обработки ошибок
// Тест метрик
}
```
## 9. Документация API
### 9.1 Endpoints
**POST /webhook**
- Обработка webhook от Git
- Валидация подписи
- Асинхронная обработка
**GET /health**
- Health check endpoint
- Статус сервиса
- Версия приложения
**GET /metrics**
- Prometheus метрики
- Метрики сборки
- Метрики webhook
### 9.2 Примеры запросов
```bash
# Health check
curl http://localhost:8080/health
# Метрики
curl http://localhost:9090/metrics
# Webhook (пример)
curl -X POST http://localhost:8080/webhook \
-H "Content-Type: application/json" \
-H "X-GitHub-Event: push" \
-d '{"ref":"refs/heads/main"}'
```

View File

@@ -0,0 +1,222 @@
# План переезда с Quartz на Hugo
## 1. Обоснование переезда
### 1.1 Проблемы текущего решения (Quartz)
- **Высокое потребление ресурсов**: Node.js + множество зависимостей
- **Медленная сборка**: Особенно при большом количестве заметок
- **Сложность настройки**: Множество конфигурационных файлов
- **Ограниченная кастомизация**: Зависимость от готовых компонентов
- **Проблемы с производительностью**: На VPS с ограниченными ресурсами
### 1.2 Преимущества Hugo
- **Быстрая сборка**: Написан на Go, компилируется в бинарник
- **Низкое потребление ресурсов**: Минимальные требования к CPU/RAM
- **Простота развертывания**: Один бинарник + статические файлы
- **Гибкость**: Полный контроль над шаблонами и стилями
- **SEO-оптимизация**: Встроенные возможности для SEO
- **Богатая экосистема**: Множество готовых тем и плагинов
## 2. Архитектура нового решения
### 2.1 Общая схема
```mermaid
graph TB
A[Git Repository] --> B[Webhook Server Go]
B --> C[Hugo Builder]
C --> D[Static Files]
D --> E[Nginx Server]
F[Traefik Proxy] --> E
F --> B
subgraph "Second Mind Container"
B
C
end
subgraph "External Services"
A
F
end
```
### 2.2 Компоненты системы
**Webhook Server (Go)**
- Обработка webhook от Git
- Запуск Hugo сборки
- Управление процессом деплоя
- Логирование и мониторинг
**Hugo Builder**
- Генерация статических файлов
- Обработка Markdown
- Применение темы и стилей
- Оптимизация ресурсов
**Nginx Server**
- Раздача статических файлов
- Кэширование
- Gzip сжатие
- SSL/TLS терминация
## 3. Техническая реализация
### 3.1 Webhook Server на Go
**Структура проекта:**
```
webhook-server/
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ ├── handler/
│ │ └── webhook.go
│ ├── builder/
│ │ └── hugo.go
│ └── config/
│ └── config.go
├── pkg/
│ └── git/
│ └── client.go
├── go.mod
├── go.sum
└── Dockerfile
```
**Основные функции:**
- Валидация webhook payload
- Клонирование/обновление репозитория
- Запуск Hugo сборки
- Обработка ошибок и retry логика
- Метрики и логирование
### 3.2 Hugo конфигурация
**Структура Hugo проекта:**
```
hugo-site/
├── config.toml
├── content/
│ ├── notes/
│ ├── daily/
│ └── templates/
├── layouts/
│ ├── _default/
│ ├── partials/
│ └── shortcodes/
├── static/
│ ├── css/
│ ├── js/
│ └── images/
└── themes/
└── custom-theme/
```
**Оптимизации:**
- Минификация CSS/JS
- Оптимизация изображений
- Кэширование статических ресурсов
- Lazy loading для изображений
### 3.3 Docker контейнеризация
**Dockerfile для webhook сервера:**
```dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd/server
FROM alpine:latest
RUN apk --no-cache add ca-certificates hugo
WORKDIR /root/
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]
```
## 4. План миграции
### 4.1 Этап 1: Подготовка (1-2 дня)
- [ ] Создание Hugo проекта
- [ ] Настройка базовой темы
- [ ] Миграция контента из Quartz
- [ ] Настройка URL структуры
### 4.2 Этап 2: Webhook Server (2-3 дня)
- [ ] Разработка Go webhook сервера
- [ ] Интеграция с Git API
- [ ] Настройка Hugo сборки
- [ ] Тестирование в dev окружении
### 4.3 Этап 3: Развертывание (1 день)
- [ ] Настройка Docker контейнеров
- [ ] Конфигурация Nginx
- [ ] Настройка SSL сертификатов
- [ ] Тестирование production окружения
### 4.4 Этап 4: Переключение (1 день)
- [ ] Обновление DNS записей
- [ ] Мониторинг производительности
- [ ] Резервное копирование старого решения
- [ ] Документирование изменений
## 5. Ожидаемые улучшения
### 5.1 Производительность
- **Время сборки**: С 30-60 секунд до 5-10 секунд
- **Потребление памяти**: С 512MB до 128MB
- **CPU нагрузка**: Снижение на 70-80%
- **Время загрузки страниц**: Улучшение на 40-60%
### 5.2 Операционные улучшения
- **Простота развертывания**: Один контейнер вместо множества сервисов
- **Мониторинг**: Встроенные метрики Go приложения
- **Безопасность**: Меньше зависимостей = меньше уязвимостей
- **Масштабируемость**: Легкое горизонтальное масштабирование
## 6. Риски и митигация
### 6.1 Технические риски
- **Потеря функциональности**: Тщательное тестирование всех компонентов
- **Проблемы с темой**: Создание собственной темы на основе существующей
- **SEO регрессия**: Сохранение URL структуры и метаданных
### 6.2 Операционные риски
- **Downtime при переключении**: Использование blue-green deployment
- **Проблемы с контентом**: Полное резервное копирование
- **Производительность**: Мониторинг и оптимизация
## 7. Мониторинг и метрики
### 7.1 Ключевые метрики
- Время сборки Hugo
- Размер генерируемых файлов
- Время ответа webhook сервера
- Потребление ресурсов контейнеров
- Время загрузки страниц
### 7.2 Алерты
- Сборка не завершилась в течение 60 секунд
- Ошибки webhook сервера
- Высокое потребление ресурсов
- Недоступность сайта
## 8. Документация и обучение
### 8.1 Техническая документация
- Архитектура системы
- Конфигурация компонентов
- Процедуры развертывания
- Troubleshooting guide
### 8.2 Операционная документация
- Процедуры мониторинга
- План аварийного восстановления
- Процедуры обновления
- Контакты ответственных лиц