diff --git a/Идеи/Переезд на Hugo/Hugo миграция и настройка.md b/Идеи/Переезд на Hugo/Hugo миграция и настройка.md new file mode 100644 index 0000000..d1a9a58 --- /dev/null +++ b/Идеи/Переезд на Hugo/Hugo миграция и настройка.md @@ -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 + + + + + + + {{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} | {{ .Site.Title }}{{ end }} + + {{ $style := resources.Get "css/main.scss" | resources.ToCSS | resources.Minify }} + + + {{ partial "head.html" . }} + + + {{ partial "header.html" . }} + +
+ {{ block "main" . }}{{ end }} +
+ + {{ partial "footer.html" . }} + + {{ $script := resources.Get "js/main.js" | resources.Minify }} + + + +``` + +```html + +{{ define "main" }} +
+
+

{{ .Title }}

+ {{ if .Params.date }} + + {{ end }} +
+ + {{ partial "breadcrumb.html" . }} + +
+ {{ .Content }} +
+ + {{ if .Params.tags }} + + {{ end }} +
+{{ 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 + +{{ if .IsHome }} + +{{ else }} + +{{ end }} + + + + + + + + + + + + + + + + + +``` + +### 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 = '

Ничего не найдено

'; + } else { + const html = results.map(item => ` +
+

${item.title}

+

${item.content.substring(0, 150)}...

+
+ `).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; +} +``` \ No newline at end of file