Files
second-mind-aep/Идеи/Переезд на Hugo/Hugo миграция и настройка.md
2025-08-04 15:25:16 +04:00

764 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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