Темизация Hugo
Для Hugo создано очень много тем. Впрочем не трудно создать собственную:
hugo new theme mytheme
И прописать в файл, config/_default/config.toml. Либо использовать команды:
hugo server -t themename
hugo -t themename
Если использовать тему, клонированную из репозитория git, не стоит редактировать файлы темы напрямую. Вместо этого настройка темы в Hugo - это вопрос переопределения шаблонов, доступных в теме. Это обеспечивает дополнительную гибкость настройки темы в соответствии с потребностями, оставаясь при этом в курсе исходной темы. Рабочий каталог проекта имеет приоритет, перед каталогами темы и файлы и шаблоны в нем перепишут такие-же в теме.
Компоненты темы
mytheme/
├── archetypes
│ └── default.md
├── layouts
│ ├── 404.html
│ ├── _default
│ │ ├── baseof.html
│ │ ├── list.html
│ │ └── single.html
│ ├── index.html
│ └── partials
│ ├── footer.html
│ ├── header.html
│ └── head.html
├── LICENSE
├── static
│ ├── css
│ ├── js
└── theme.toml
theme.toml - файл с настройками авторства и т.д.
archetypes - имеет такое-же значение как и в основе сайта, но меньший приоритет.
static - аналогично каталогу в корне проекта сайта.
layouts - тоже самое что и в корне сайта.
Желательно заполнить файлы theme.toml и LICENSE. Это необязательно, но если распространять свою тему, там сообщается миру, кого хвалить (или винить). Также неплохо объявить лицензию, чтобы люди знали, как они могут использовать тему.
Статические файлы
В папку mytheme/static сохраним необходимые файлы изображений, CSS и JS. Используем популярный фреймворк bootstrap. В директории проекта используем команду:
npm install bootstrap bootstrap-icons
Из каталога node_modules/bootstrap/dist/ скопируем нужные CSS и JS файлы в mytheme/static и добавим собственный файл logo.svg. Также скопируем из node_modules/bootstrap-icons/font каталог fonts и bootstrap-icons.css.
Шаблоны
В Hugo есть несколько основных типа шаблонов. Это шаблон домашней страницы - mytheme/layouts/index.html. Используется только главной страницей. Также есть "одиночные" шаблоны, которые используются для создания выходных данных для одного файла содержимого - mytheme/layouts/_default/single.html. А также шаблоны "списков", которые используются для группировки нескольких частей контента перед созданием вывода - mytheme/layouts/_default/list.html.
Существует еще три типа шаблонов: частичные - mytheme/layouts/partials, представления содержимого - summary.html и термины - terms.html.
Макеты, блоки и части
Изменим самый главный базовый шаблон в themes/mytheme/layouts/_default/baseof.html
<!doctype html>
<html lang="ru" class="h-100">
{{- partial "head.html" . -}}
<body class="d-flex flex-column h-100">
<i class="bi bi-caret-up-fill bg-secondary text-white" onclick="topFunction()" id="btnTop" title="Вверх"></i>
{{- partial "header.html" . -}}
<main class="flex-shrink-0 m-4">
<div class="container">
<div id="content">
{{- block "main" . }}{{- end }}
</div>
</div>
</main>
{{- partial "footer.html" . -}}
{{- partial "script.html" . -}}
{{ if (ne .IsHome true) }}
{{- partial "btntop.html" . -}}
{{ end -}}
</body>
</html>
Это базовый макет по умолчанию для всего на сайте. Можно определить макеты для разных типов контента, это запасной вариант, когда ничего другого не указано. Внутри этого мы определяем несколько partials и blocks.
{{- partial "head.html" . -}}
Эта строчка означает, что hugo вставит сюда содержимое из файла partials/head.html. Очень похоже на технологию - SSI.
{{- block "main" . }}{{- end }}
Эта строка добавит содержимое из файлов - mytheme/layouts/_default/single.html или mytheme/layouts/_default/list.html, в которых определен свой вывод блока - main.
head.html
Внутри themes/mytheme/layouts/partials/head.html добавим bootstrap.
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="robots" content="index, follow" />
{{ $keywords := .Site.Params.keywords -}}
{{ if .Params.tags }}{{ $keywords = .Params.tags }}{{ end -}}
<meta name="keywords" content="{{ delimit $keywords " , " }}" />
{{ $description := .Site.Params.description -}}
{{ if .Params.description }}{{ $description = .Params.description }}{{ end -}}
<meta name="description" content="{{ $description }}">
{{ $author := .Site.Author.name -}}
{{ if .Params.Author }}{{ $author = .Params.Author }}{{ end -}}
<meta name="author" content="{{ $author }}">
{{ template "_internal/opengraph.html" . }}
<link rel="canonical" href="{{ .Permalink }}">
<link rel="shortlink" href="{{ .Site.BaseURL }}" />
<link rel="image_src" href="{{ .Site.BaseURL }}images/logo.svg" />
{{ $title := print .Title " | " .Site.Title -}}
{{ if .IsHome }}{{ $title = .Site.Title }}{{ end -}}
<title>{{ $title }}</title>
<link rel="stylesheet" type="text/css" href="{{ .Site.BaseURL }}css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="{{ .Site.BaseURL }}css/bootstrap-icons.css">
<link rel="stylesheet" type="text/css" href="{{ .Site.BaseURL }}css/style.css">
{{ partial "favicon.html" . }}
{{ with .OutputFormats.Get "rss" -}}{{ printf `
<link rel="%s" type="%s" href="%s" title="%s" />` .Rel .MediaType.Type .Permalink $.Site.Title | safeHTML }}{{ end
}}
{{- partial "copypasta.html" . -}}
</head>
Это настраивает bootstrap для загрузки из каталога static/css, а также добавляет ссылку на сгенерированный RSS-канал. Определяем метатеги keywords, description и author, изначально берем значения из файла конфигурации config/_default/params.toml, если в заголовке текущей страницы они определены, то используем их. Те же действия производим с заголовком страницы - title.
{{ template "_internal/opengraph.html" . }}
Эта строчка вставляет внутренний шаблон Hugo.
Внутренние шаблоны:
- _internal/disqus.html
- _internal/google_analytics.html
- _internal/google_analytics_async.html
- _internal/opengraph.html
- _internal/pagination.html
- _internal/schema.html
- _internal/twitter_cards.html
favicon.html
Отдельно сформируем ссылки на иконки для разных устройств.
<link rel="apple-touch-icon" sizes="180x180" href="{{ .Site.BaseURL }}apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{ .Site.BaseURL }}favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ .Site.BaseURL }}favicon-16x16.png">
<link rel="icon" type="image/vnd.microsoft.icon" href="{{ .Site.BaseURL }}favicon.ico">
<link rel="manifest" href="{{ .Site.BaseURL }}site.webmanifest">
<link rel="mask-icon" href="{{ .Site.BaseURL }}safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
copypasta.html
Небольшой скрипт, который добавит ссылку на источник, при копировании с сайта.
<script type="text/javascript">
document.oncopy = function(){
var body = document.getElementsByTagName('body')[0];
var selection = window.getSelection();
var div = document.createElement('div');
div.style.position = 'absolute';
div.style.left = '-99999px';
body.appendChild(div);
div.innerHTML = selection + ' | Источник: ' + window.location.href;
selection.selectAllChildren(div);
window.setTimeout(function(){
body.removeChild(div);
}, 0);
}
</script>
header.html
Основное меню сайта.
<nav class="navbar navbar-expand-md navbar-light bg-light">
<div class="container">
<a class="navbar-brand" href="{{ .Site.BaseURL }}">
<img src="{{ .Site.BaseURL }}images/logo.png" alt="{{ .Site.Title }}">
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
{{ range .Site.Menus.main }}
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="{{ absURL .URL }}">
{{ $text := print .Name | safeHTML }}
{{ $text }}
</a>
</li>
{{ end }}
</ul>
</div>
</div>
</nav>
Отключим подчеркивание ссылок и ограничим высоту логотипа, добавим в style.css.
.navbar-brand img {
height: 50px;
}
a {
text-decoration: none;
}
Подвал сайта
В файл themes/mytheme/layouts/partials/footer.html добавим:
<footer class="footer mt-auto py-3 bg-light">
<div class="container">
<span class="text-muted footer text-center">Copyright (c) {{ now.Format "2006"}} {{ .Site.Author.name | markdownify }}</span>
</div>
</footer>
script.html
Подключаем js скрипты.
<script src="{{ .Site.BaseURL }}js/bootstrap.bundle.min.js"></script>
Кнопка вверх
{{ if (ne .IsHome true) }}
{{- partial "btntop.html" . -}}
{{ end -}}
В этих строчках определяется, что текущая страница не главная и только тогда подключается файл btntop.html, в котором простой скрипт для кнопки вверх.
<script type="text/javascript">
window.onscroll = function() {scrollFunction()};
function scrollFunction() {
if (document.body.scrollTop > 250 || document.documentElement.scrollTop > 250) {
document.getElementById("btnTop").style.display = "block";
} else {
document.getElementById("btnTop").style.display = "none";
}
}
function topFunction() {
document.body.scrollTop = 0;
document.documentElement.scrollTop = 0;
}
</script>
Сама кнопка определяется в начале страницы, а внешний вид настраивается в style.css.
<i class="bi bi-caret-up-fill bg-secondary text-white" onclick="topFunction()" id="btnTop" title="Вверх"></i>
#btnTop {
display: none;
position: fixed;
bottom: 60px;
right: 30px;
z-index: 99;
border: none;
outline: none;
cursor: pointer;
padding: 10px;
border-radius: 10px;
font-size: 20px;
}
Блоки
Блоки в Hugo позволяют переопределить части основного шаблона. Например для отдельных страниц блок будет взят из single.html, а для списка статей на сайте из list.html.
Отдельные страницы
Определим шаблон по умолчанию для отдельных страниц, например для страницы - О проекте, layouts/_default/single.html.
{{ define "main" }}
<div class="row g-5">
<div class="col-md-8">
<h3 class="pb-4 mb-4 border-bottom">
{{ .Title }}
</h3>
{{ partial "breadcrumb.html" . }}
{{ if .Params.toc }}
<div class="p-4 mb-3 bg-light rounded">
{{- partial "toc.html" . -}}
</div>
{{ end }}
<article class="post">
{{ .Content }}
</article>
</div>
<div class="col-md-4">
<div class="p-4 mb-3 bg-light rounded">
{{- partial "tags.html" . -}}
</div>
</div>
</div>
{{ end }}
Выводим заголовок статьи, далее "хлебные крошки" упрощающие навигацию по сайту.
<div class="mt-3">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{{ template "breadcrumbnav" (dict "p1" . "p2" .) }}
</ol>
{{ define "breadcrumbnav" }}
{{ if .p1.Parent }}
{{ template "breadcrumbnav" (dict "p1" .p1.Parent "p2" .p2 ) }}
{{ else if not .p1.IsHome }}
{{ template "breadcrumbnav" (dict "p1" .p1.Site.Home "p2" .p2 ) }}
{{ end }}
<li{{ if eq .p1 .p2 }} class="breadcrumb-item active" aria-current="page" {{ end }}>
<a href="{{ .p1.Permalink }}">{{ .p1.Title }}</a>
</li>
{{ end }}
</nav>
</div>
Добавим в css.
.breadcrumb {
list-style: none;
display: flex;
flex-wrap: wrap;
align-items: baseline;
}
.breadcrumb li {
display: inline;
white-space: nowrap;
}
.breadcrumb li + li:before {
content: ">";
padding: 0.3rem;
}
Выводим саму статью и если в заголовке статьи установлен параметр toc: true тогда Hugo сформирует содержание статьи на основе заголовков - toc.html.
<aside>
<header>
<h4>Содержание</h4>
</header>
{{.TableOfContents}}
</aside>
Также добавим вывод всех терминов таксономии.
{{/*
Вывод терминов словаря таксономии
в зависимости от частоты использования
*/}}
<aside>
{{- range $name, $taxonomy := .Site.Taxonomies }}
{{/* Проверка размера словаря таксономии */}}
{{- if not (eq (len $taxonomy) 0) }}
<div class="wrapper tagclouds">
{{- range $key, $value := $taxonomy }}
{{ $count := .Count }}
{{/* Проверка уровня термина таксономии */}}
{{ if ge $count 2 }}
{{/* Ограничение размера уровня термина */}}
{{ if ge $count 10 }}{{ $count = 10 }}{{ end }}
<span class="tagclouds-term"><a href="{{ .Page.Permalink }}" class="tagclouds level{{ $count }}">
{{ .Page.Title }}
</a> </span>
{{ end }}
{{- end }}
</div>
{{- end }}
{{- end }}
</aside>
И добавим в style.css параметры терминов как в модуле Drupal.
.wrapper.tagclouds {
text-align: justify;
margin-right: 1em;
}
.tagclouds.level1 {
font-size: 1em;
}
.tagclouds.level2 {
font-size: 1.2em;
}
.tagclouds.level3 {
font-size: 1.4em;
}
.tagclouds.level4 {
font-size: 1.6em;
}
.tagclouds.level5 {
font-size: 1.8em;
}
.tagclouds.level6 {
font-size: 2em;
}
.tagclouds.level7 {
font-size: 2.2em;
}
.tagclouds.level8 {
font-size: 2.4em;
}
.tagclouds.level9 {
font-size: 2.6em;
}
.tagclouds.level10 {
font-size: 2.8em;
}
.tagclouds.level11 {
font-size: 3em;
}
.tagclouds.level12 {
font-size: 3.2em;
}
.tagclouds.level13 {
font-size: 3.4em;
}
.tagclouds.level14 {
font-size: 3.6em;
}
.tagclouds.level15 {
font-size: 3.8em;
}
Для страниц типа статья создадим отдельный шаблон articles/single.html.
{{ define "main" }}
<div class="row g-5">
<div class="col-md-8">
<h3 class="pb-4 mb-4 border-bottom">
{{ .Title }}
</h3>
{{ partial "metadata.html" . }}
<br>
{{ partial "breadcrumb.html" . }}
{{ if .Params.toc }}
<div class="p-4 mb-3 bg-light rounded">
{{- partial "toc.html" . -}}
</div>
{{ end }}
<article class="blog-post">
{{ .Content }}
{{ partial "social.html" . }}
</article>
</div>
<div class="col-md-4">
<div class="p-4 mb-3 bg-light rounded">
{{- partial "tags.html" . -}}
</div>
{{ $related := .Site.RegularPages.RelatedIndices . "tags" | first 3 }}
{{ with $related }}
<div class="p-4 mb-3 bg-light rounded">
<h5>Рекомендуем</h5>
<ul class="list-group list-group-flush">
{{ range . }}
<li class="list-group-item"><a href="{{ .RelPermalink }}">{{ .Title }}</a></li>
{{ end }}
</ul>
</div>
{{ end }}
</div>
</div>
{{ end }}
Отличие от стандартного блока подключение metadata.html и social.html.
{{ $dateTime := .PublishDate.Format "2006-01-02" }}
{{ $dateFormat := .Site.Params.DateForm | default "Jan 2, 2006" }}
<i class="bi bi-calendar-check"></i> <time datetime="{{ $dateTime }}">{{ .PublishDate.Format $dateFormat }}</time>
{{ with .Params.tags }}
<i class="bi bi-tags"></i>
{{ range . }}
{{ $href := print (absURL "tags/") (urlize .) }}
<a class="badge bg-secondary text-uppercase" href="{{ $href }}">{{ . }}</a>
{{ end }}
{{ end }}
Этот файл формирует строку с датой статьи и связанными тегами таксономии.
Перед подключением social.html переходим на сервис Yandex и выбираем нужные кнопки.
<script src="https://yastatic.net/share2/share.js"></script>
<div class="ya-share2" data-curtain data-size="l" data-shape="round" data-services="messenger,vkontakte,odnoklassniki,telegram,moimir"></div>
Таксономия
Для отображения терминов таксономии создадим шаблон блока в файле layouts/_default/terms.html.
{{ define "main" }}
<div class="row g-5">
<div class="col-md-8">
{{ with .Content }}
<div class="pb-2 mb-3 border-bottom">
{{- . -}}
</div>
{{ end }}
<div class="wrapper tagclouds">
{{ range .Data.Terms.Alphabetical }}
<span class="tagclouds-term"><a href="{{ .Page.Permalink }}" class="tagclouds level{{ .Count }}">
{{ .Page.Title }}
</a> </span>
{{ end }}
</div>
</div>
<div class="col-md-4">
<div class="p-4 mb-3 bg-light rounded">
{{- partial "tags.html" . -}}
</div>
</div>
</div>
{{ end }}
Списки
Шаблон из файла layouts/_default/list.html используется для отображения списков материалов из разделов сайта.
{{ define "main" }}
<div class="row g-5">
<div class="col-md-8">
{{ with .Content }}
<div class="pb-2 mb-3 border-bottom">
{{- . -}}
</div>
{{ end }}
{{ range where .Paginator.Pages "Type" "!=" "page" }}
<article class="post">
<h3 class="post-title mb-1"><a class="title" href="{{ .RelPermalink }}">{{ .Title }}</a></h3>
<p class="post-meta">{{ partial "metadata.html" . }}</p>
<p>{{ .Summary }}</p>
{{ end }}
</article>
{{ template "_internal/pagination.html" . }}
</div>
<div class="col-md-4">
<div class="p-4 mb-3 bg-light rounded">
{{- partial "tags.html" . -}}
</div>
</div>
</div>
{{ end }}
В раздел можно поместить файл _index.md, для описания самого раздела, также можно добавить описание терминов таксономии, создав соответствующие разделы и файлы. Для вывода этой информации добавим в блок проверку:
{{ with .Content }}
<div class="pb-2 mb-3 border-bottom">
{{- . -}}
</div>
{{ end }}
Далее формируем списком краткое содержание всех статей и выводим пагинацию из внутреннего шаблона Hugo.
{{ template "_internal/pagination.html" . }}
404
Для ошибки 404 свой шаблон блока.
{{ define "main" }}
<div class="row g-5">
<div class="col-md-8">
<h3 class="pb-4 mb-4 border-bottom">
404
</h3>
<p>Сожалеем, но страница, которую Вы запрашиваете, не существует. Возможно она была удалена или перемещена, возможно
Вы набрали неверный адрес.</p>
<h5>Рекомендуем</h5>
{{ $query := site.RegularPages }}
{{ $count := len $query }}
{{ if gt $count 0 }}
<ul class="list-group list-group-flush">
{{ range first 3 $query }}
<li class="list-group-item"><a href="{{ .Permalink }}">{{ .Title }}</a></li>
{{ end }}
</ul>
{{ end }}
</div>
<div class="col-md-4">
<div class="p-4 mb-3 bg-light rounded">
{{- partial "tags.html" . -}}
</div>
</div>
</div>
{{ end }}
Для Apache свою страницу ошибок можно прописать в файле .htaccess и разместить его в папке static
ErrorDocument 404 /404.html