vault backup: 2025-08-04 15:25:16
This commit is contained in:
764
Идеи/Переезд на Hugo/Hugo миграция и настройка.md
Normal file
764
Идеи/Переезд на Hugo/Hugo миграция и настройка.md
Normal 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''
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user