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