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;
}
```