feat: series-таксономия, переключатель темы вправо, соцсети в меню

- hugo.toml: [taxonomies] series = "series"; [params.socialIcons] (github, telegram, rss)
- layouts/partials/series_nav.html: модуль «Серия: N из M» с навигацией prev/next
  (сортировка по series_order), поддержка кириллических названий серий
- layouts/_default/single.html: override темы — подключение series_nav перед контентом
- layouts/partials/extend_footer.html: JS переносит .logo-switches из .logo в конец
  .nav (переключатель темы — справа после меню); server-rendered блок соцсетей
  переносится к меню; иконки — через штатный svg.html темы (github/telegram/rss)
- assets/css/extended/typewriter.css: стили .social-icons-inline, .series-nav;
  .logo-switches после переноса попадает под .nav > .logo-switches (order:2)

Closes openspec change series-taxonomy-and-header
This commit is contained in:
2026-09-20 19:23:06 +00:00
parent 1f58f8ccc7
commit 0fbc0f93a4
9 changed files with 376 additions and 9 deletions
+66
View File
@@ -0,0 +1,66 @@
{{- define "main" }}
<article class="post-single">
<header class="post-header">
{{ partial "breadcrumbs.html" . }}
<h1 class="post-title entry-hint-parent">
{{ .Title }}
{{- if .Draft }}
<span class="entry-hint" title="Draft">
<svg xmlns="http://www.w3.org/2000/svg" height="35" viewBox="0 -960 960 960" fill="currentColor">
<path
d="M160-410v-60h300v60H160Zm0-165v-60h470v60H160Zm0-165v-60h470v60H160Zm360 580v-123l221-220q9-9 20-13t22-4q12 0 23 4.5t20 13.5l37 37q9 9 13 20t4 22q0 11-4.5 22.5T862.09-380L643-160H520Zm300-263-37-37 37 37ZM580-220h38l121-122-18-19-19-18-122 121v38Zm141-141-19-18 37 37-18-19Z" />
</svg>
</span>
{{- end }}
</h1>
{{- if .Description }}
<div class="post-description">
{{ .Description }}
</div>
{{- end }}
{{- if not (.Param "hideMeta") }}
<div class="post-meta">
{{- partial "post_meta.html" . -}}
{{- partial "translation_list.html" . -}}
{{- partial "edit_post.html" . -}}
{{- partial "post_canonical.html" . -}}
</div>
{{- end }}
</header>
{{- $isHidden := (.Param "cover.hiddenInSingle") | default (.Param "cover.hidden") | default false }}
{{- partial "cover.html" (dict "cxt" . "IsSingle" true "isHidden" $isHidden) }}
{{- if (.Param "ShowToc") }}
{{- partial "toc.html" . }}
{{- end }}
{{- if .Content }}
{{- partial "series_nav.html" . -}}
<div class="post-content">
{{- if not (.Param "disableAnchoredHeadings") }}
{{- partial "anchored_headings.html" .Content -}}
{{- else }}{{ .Content }}{{ end }}
</div>
{{- end }}
<footer class="post-footer">
{{- $tags := .Language.Params.Taxonomies.tag | default "tags" }}
<ul class="post-tags">
{{- range ($.GetTerms $tags) }}
<li><a href="{{ .Permalink }}">{{ .LinkTitle }}</a></li>
{{- end }}
</ul>
{{- if (.Param "ShowPostNavLinks") }}
{{- partial "post_nav_links.html" . }}
{{- end }}
{{- if (and site.Params.ShowShareButtons (ne .Params.disableShare true)) }}
{{- partial "share_icons.html" . -}}
{{- end }}
</footer>
{{- if (.Param "comments") }}
{{- partial "comments.html" . }}
{{- end }}
</article>
{{- end }}{{/* end main */}}
+47 -9
View File
@@ -1,10 +1,14 @@
{{- /* dedinit() typewriter: animated topics inside logo parens.
Pure JS + CSS, no Alpine. Progressive enhancement — no-JS keeps
the plain "dedinit()" logo. Words configurable via hugo.toml:
[params.typewriter] words = ["linux", "llm", "politics"] */ -}}
the plain "dedinit()" logo. Words configurable via hugo.toml.
Also: moves theme toggle to the right end of .nav and injects
social icons (from [params.socialIcons]) next to the menu.
Icons are rendered server-side via the theme's svg.html partial. */ -}}
{{- $tw_words := site.Params.typewriter.words | default (slice "linux" "llm" "politics" "homelab" "nix") -}}
{{- $social := site.Params.socialIcons | default (slice) -}}
<script>
(() => {
/* 1) typewriter */
const LOGO_SEL = 'header .logo a, .logo a';
const queue = [() => {
const a = document.querySelector(LOGO_SEL);
@@ -18,21 +22,17 @@
const box = document.createElement('span');
box.id = 'dedinit-typewriter';
box.className = 'tw';
box.textContent = '';
box.appendChild(Object.assign(document.createElement('span'), { className: 'tw-cursor' }));
// rebuild: <prefix>(<typewriter>)<suffix>
a.textContent = '';
a.appendChild(document.createTextNode(text.slice(0, open + 1)));
a.appendChild(box);
a.appendChild(document.createTextNode(text.slice(close)));
// reduced motion: blink is disabled via CSS; keep typing but no blink
const TYPE = 90, ERASE = 50, HOLD = 1400, GAP = 400;
let w = 0, i = 0, dir = 1;
box.querySelector('.tw-cursor').style.display = 'inline-block';
const tick = () => {
const word = words[w];
// keep cursor as the last child; typed text goes before it
box.replaceChildren(document.createTextNode(word.slice(0, i)), box.querySelector('.tw-cursor'));
let delay;
if (dir === 1) {
@@ -46,7 +46,45 @@
};
tick();
}];
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => queue.shift()());
else queue.shift()();
/* 2) move theme toggle (logo-switches) to the end of .nav, after #menu.
It is nested inside .logo in the theme; appending to .nav moves it
visually to the far right (flex order handled by CSS). */
const toggleQueue = [() => {
const nav = document.querySelector('header .nav, .nav');
const sw = nav && nav.querySelector('.logo-switches');
if (!nav || !sw || sw.dataset.moved) return;
sw.dataset.moved = '1';
nav.appendChild(sw);
}];
/* 3) social icons: a hidden server-rendered container is moved into nav
next to the menu. The icons are rendered by Hugo (theme svg.html),
so no client-side icon data is needed. */
const socialQueue = [() => {
const nav = document.querySelector('header .nav, .nav');
const box = document.getElementById('__social-icons');
if (!nav || !box) return;
box.id = '';
const menu = nav.querySelector('#menu');
if (menu) menu.after(box);
else nav.appendChild(box);
}];
const run = () => { queue.shift()(); toggleQueue.shift()(); socialQueue.shift()(); };
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', run);
else run();
})();
</script>
</script>
{{- /* server-rendered social icons; moved into the nav by the JS above.
display:none so no-JS visitors never see a stray block. */ -}}
{{- if $social -}}
<div id="__social-icons" class="social-icons-inline" style="display:none">
{{- range $social -}}
<a href="{{ trim .url " " | safeURL }}" target="_blank" rel="noopener noreferrer me"
title="{{ (.title | default .name) | title }}" class="social-icon">
{{- partial "svg.html" . -}}
</a>
{{- end -}}
</div>
{{- end -}}
+42
View File
@@ -0,0 +1,42 @@
{{- /* Серия: блок навигации по серии (как в Blowfish).
Использование: в single.html перед .post-content:
{{- partial "series_nav.html" . -}}
Пост помечается во front-matter:
series: ["Имя серии"]
series_order: 1
Выводит блок «Серия: N из M» со ссылками на предыдущую/следующую часть.
Если серий нет или пост не в серии — выводит пустоту.
*/ -}}
{{- $series := .Params.series -}}
{{- if $series -}}
{{- range $series -}}
{{- $seriesName := . -}}
{{- /* термины хранятся с URL-слизом: дооснастить и urlize, и lower */ -}}
{{- $slug := ($seriesName | urlize) -}}
{{- $term := (site.GetPage (printf "/series/%s/" $slug)) | default (site.GetPage (printf "/series/%s/" ($seriesName | lower | urlize))) -}}
{{- if not $term }}{{- $term = site.GetPage (printf "/series/%s" ($seriesName | lower)) -}}{{- end -}}
{{- if $term -}}
{{- $pages := (sort $term.Pages "Params.series_order" "asc") -}}
{{- $current := $.Params.series_order -}}
{{- $total := $pages | len -}}
{{- $idx := 0 -}}
{{- $prev := "" -}}
{{- $next := "" -}}
{{- range $i, $p := $pages -}}
{{- if eq $current $p.Params.series_order -}}
{{- $idx = add $i 1 -}}
{{- $prev = (index $pages (sub $i 1)) | default "" -}}
{{- $next = (index $pages (add $i 1)) | default "" -}}
{{- end -}}
{{- end -}}
<div class="series-nav" data-series="{{ $seriesName }}" data-order="{{ $current }}" data-total="{{ $total }}">
<div class="series-title">Серия: <a href="{{ $term.Permalink }}">{{ $seriesName }}</a></div>
<div class="series-progress">часть {{ $idx }} из {{ $total }}</div>
<div class="series-links">
{{- if $prev }}<a class="series-prev" href="{{ $prev.Permalink }}">← {{ $prev.Title }}</a>{{- end }}
{{- if $next }}<a class="series-next" href="{{ $next.Permalink }}">{{ $next.Title }} →</a>{{- end }}
</div>
</div>
{{- end -}}
{{- end -}}
{{- end -}}