Compare commits

..

86 Commits

Author SHA1 Message Date
36cf092ab0 Добавлена новая статья 2025-11-28 17:20:32 +07:00
934b471c36 Изменение адреса ссылки git 2025-11-28 15:50:28 +07:00
c3858fb750 Update screenshot 2025-11-26 20:21:08 +07:00
b9507378ba Added reference to git 2025-11-26 14:07:56 +07:00
56a417ece8 Увеличение размера иконок 2025-11-23 13:44:01 +07:00
67e3941bcc Исправление размеров иконок 2025-11-23 13:41:10 +07:00
4aaa1fe32d Replace social icons in footer 2025-11-23 13:03:35 +07:00
0f4eca7dff Edit post fix tag 2025-11-22 20:57:34 +07:00
3718999740 Fix images 2025-11-22 18:35:26 +07:00
2cd062bce0 Fix syntax 2025-11-22 18:34:52 +07:00
ec2c5dda98 Fix sharkey link - microblog link 2025-11-22 18:33:12 +07:00
7f79015199 Add post Переписал скрипт stop-screensaver и добавил на панель Polybar 2025-11-22 18:14:41 +07:00
8a2b8c3532 ⬆️ upgrade baguetteBox.min.js to v1.13.0 2025-11-10 15:41:18 +07:00
742c61aba2 ⬆️ chore(deps): upgrade mermaid to v11.12.1 (#574) 2025-10-31 14:51:07 +07:00
83a8f9e438 ⬆️ chore(deps): upgrade KaTeX to v0.16.25 (#571) 2025-10-31 14:50:14 +07:00
6cc247dd5f Added article Блокируем скринсейвер в полноэкранном режиме браузера Vivaldi 2025-10-31 14:49:04 +07:00
1ae4c16782 Add project microblog 2025-10-31 14:47:34 +07:00
1673133bba Delete project sharkey 2025-10-31 14:46:14 +07:00
f20ec2f258 Delete peertube 2025-10-21 16:39:32 +07:00
509943577e ️ feat(a11y): add skip to content link for keyboard navigation (#552) 2025-09-30 18:26:30 +07:00
bf46ee0219 feat: load extra features in section template. Allows using copy code block options, KaTex, etc. in the homepage 2025-09-23 18:23:14 +07:00
c335577045 ⬆️ chore(deps): upgrade mermaid to v11.12.0 (#569) 2025-09-23 18:20:12 +07:00
f77e1f64ad feat(i18n): display lcode in language switcher (#565) fix 2025-09-18 20:36:50 +07:00
9c8b914884 feat(i18n): display lcode in language switcher (#565) 2025-09-18 20:30:15 +07:00
2debeb7f85 ⬆️ chore(deps): upgrade mermaid to v11.11.0 (#566) 2025-09-18 20:23:33 +07:00
bbe9726968 Обнровлен аватар на главной странице 2025-09-13 20:06:08 +07:00
dc51fa3ba7 Добавлена новая статья 2025-09-08 17:26:53 +07:00
64c79d9eb9 ⬆️ chore(deps): upgrade mermaid to v11.10.1 2025-08-23 20:27:10 +07:00
3fcf0f71b0 feat: allow custom archive date format (#557) 2025-08-16 18:31:48 +07:00
ddf2c54880 ♻️ refactor: prevent HTML escaping of joined CSP strings (#553) 2025-08-11 18:20:50 +07:00
762c309760 feat: add per-language date format configuration (#556) 2025-08-10 19:06:46 +07:00
1f964e9b88 feat(iine): add like buttons (#550) 2025-08-05 18:31:05 +07:00
438c91d226 feat(iine): add like buttons (#550) 2025-08-05 18:18:12 +07:00
92c1b75480 ♻️ refactor: force a trailing slash on the nav home title (#547) 2025-07-21 16:02:24 +07:00
3b3d93e089 🐛 fix(feed): conditionally render updated field 2025-07-21 16:00:23 +07:00
14103bc551 Delete trash 2025-07-12 19:25:44 +07:00
07e612699e fix base.html 2025-07-12 19:23:34 +07:00
ddfcde30c3 ⬆️ chore(deps): upgrade mermaid to v11.8.1 2025-07-12 19:12:34 +07:00
67cf24cf42 feat(analytics): make Umami DNT behavior configurable 2025-07-12 19:05:59 +07:00
8a6ccebbaa 💄 style: remove extra spacing after author 2025-07-12 18:59:29 +07:00
828b8ccf8a 🐛 fix: render author HTML on page metadata 2025-07-12 18:55:47 +07:00
f18f8d7d24 feat: add support for webmentions 2025-07-12 18:54:02 +07:00
02c3adb06e 🐛 fix: allow feed icon to be hidden 2025-06-06 18:12:55 +07:00
a9a6589f7f feat: extend tabi <head> and <body> elements 2025-06-06 18:08:44 +07:00
7ada67433e 🐛 fix(feed): prioritise description > summary 2025-05-11 20:02:21 +07:00
7c56b73716 feat(webmentions): add hcard in post page 2025-05-11 15:55:56 +07:00
420a3484ca feat(feed): add feed icon tag pages 2025-04-30 18:46:59 +07:00
74abf0aac6 ♻️ refactor: add target_attribute macro 2025-04-28 16:11:01 +07:00
cbb52d74a4 feat: parse markdown in post summary & description 2025-04-28 16:03:17 +07:00
1b1ea501a8 Add icon xmpp.png 2025-04-17 16:55:09 +07:00
3f249936ea upgrade KaTeX to v0.16.22 2025-04-17 16:53:51 +07:00
4e4ad45acd feat(indieweb): add hidden h-card 2025-04-08 19:53:34 +07:00
a9feece9b7 refactor: improve error message when title is unset 2025-04-08 19:31:39 +07:00
db68be7338 Корректировка вывода изображений через baguetteBox.js 2025-04-05 14:49:45 +07:00
512520dad4 Интеграция baguetteBox.js для просмотра изображений во вспывающем окне 2025-04-05 14:47:40 +07:00
ac2bedac72 feat: cache bust images in shortcodes (#504) 2025-04-03 14:44:01 +07:00
e3b1d7dc89 Обновление mermaid до v11.6.0 2025-03-26 16:25:12 +07:00
ebe30e375b Добавлена запись о Lossless-cut 2025-03-22 15:34:06 +07:00
94cd879449 Добавлена статистика посещаемости сайта 2025-03-20 21:15:19 +07:00
189ca4c94b Удалить projects.jpg 2025-03-19 16:50:21 +07:00
510e122a9f Обновлено превью 2025-03-19 16:48:48 +07:00
b8bf5845c6 Обновлен screenshot 2025-03-18 21:31:10 +07:00
2d3f967a10 Добавлено превью страницы Проекты 2025-03-18 21:25:02 +07:00
0d3c24da02 Добавлена страница проекты 2025-03-18 21:21:41 +07:00
8052675c1e Добавлена страница проекты 2025-03-18 21:20:12 +07:00
1f9708b5f2 fix: fix external link icon breaking word wrapping 2025-03-18 15:47:49 +07:00
1592e7cb28 Обновление mermaid до v11.5.0 2025-03-18 15:36:11 +07:00
f010b18883 Добавлена новая статья о Dunst 2025-03-14 14:22:52 +07:00
93b56fab81 Встроено видео 2025-03-09 19:28:17 +07:00
fa15504505 Добавлено превью статьи 2025-03-09 15:26:54 +07:00
4ff09f124c Добавлена статья Авторизация на сервере с использованием SSH-ключей 2025-03-09 15:15:51 +07:00
fcf23f3ebf Небольшой fix в статье о sshfs 2025-03-09 13:52:45 +07:00
dbab12d1b9 Добавлена статья о sshfs 2025-03-09 13:49:52 +07:00
5e69e952ec Изменено превью 2025-03-09 13:49:15 +07:00
ddc6aefa4d Добавлена инструкция по установке fix5 2025-03-07 15:15:02 +07:00
6b412749ea Добавлена инструкция по установке fix4 2025-03-07 15:14:28 +07:00
ff9c51200e Добавлена инструкция по установке fix3 2025-03-07 15:13:32 +07:00
9836a3ddd5 Добавлена инструкция по установке fix2 2025-03-06 20:14:20 +07:00
9a8cc9157a Добавлена инструкция по установке fix 2025-03-06 19:58:06 +07:00
8e74399c75 Добавлена инструкция по установке 2025-03-06 19:56:47 +07:00
b5b96600c9 Исправлен путь до main.scss для работы галлереи 2025-03-06 19:43:38 +07:00
1bf4917f7a Корректировка цвета заголовка h2 2025-03-06 17:09:13 +07:00
788ec81cb9 Корректировка цвета выделенных слов 2025-03-06 17:01:21 +07:00
ca65105cf9 Корректировка 2025-03-06 16:54:11 +07:00
c315d7d5ac Добавлена новая запись в блоге 2025-03-06 16:47:32 +07:00
d0263e8a05 Добавлена настройка системы комментирования Isso 2025-03-06 15:29:17 +07:00
121 changed files with 8963 additions and 921 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
zola
zbuild.sh
static/processed_images/
themes/yunohost/
public/

View File

@@ -1,4 +1,23 @@
# blog.zlinux.ru
Мой блог на [zola](https://github.com/getzola/zola) с измененной темой оформления (gruvbox) от [tabi](https://github.com/welpo/tabi).
## Установка
```$ sudo pacman -S zola```
Клонируем репозиторий
```$ git clone https://gitea.zlinux.ru/zloy_linux/blog.zlinux.ru.git ~/blog```
переходим в директорию нашего блога
```$ cd ~/blog```
Собираем и запускаем
```
$ zola build
$ zola serve
```
<img src="screenshot.png">

View File

@@ -200,6 +200,19 @@ fediverse_creator = { handle = "zloy_linux", domain = "zlinux.ru" }
show_previous_next_article_links = true
invert_previous_next_article_links = true
# For multilingual sites: show current language code on the language switcher.
show_selected_language_code_in_language_switcher = false
# Enable iine like buttons on all posts: https://iine.to/
# Can be set at page or section levels, following the hierarchy: page > section > config. See: https://welpo.github.io/tabi/blog/mastering-tabi-settings/#settings-hierarchy
iine = true
iine_icon = "thumbs_up" # See https://iine.to/#customise
# Unify like counts across all language versions of the same page.
# When enabled, likes on /es/blog/hello/ will count towards /blog/hello/ (default language).
iine_unified_languages = true
enable_csp = false
@@ -213,7 +226,7 @@ enable_csp = false
#]
remote_repository_url = "https://gitea.zlinux.ru/gitea/zloy_linux/blog.zlinux.ru.git"
remote_repository_url = "https://gitea.zlinux.ru/zloy_linux/blog.zlinux.ru.git"
remote_repository_git_platform = "auto"
remote_repository_branch = "main"
show_remote_changes = true
@@ -227,6 +240,20 @@ code_block_name_links = true
compact_tags = false
short_date_format = ""
# Date format used for the archive page.
# Default is "06 July" in English and "%d %b" in other languages.
archive_date_format = ""
# Per-language date format overrides.
# Examples: Spanish uses "3 de febrero de 2024", German uses "3. Februar 2024"
date_formats = [
{ lang = "ru", long = "%d %B %Y", short = "%-d %b %Y", archive = "%d %b" },
{ lang = "en", long = "%d. %B %Y", short = "%d.%m.%Y" },
]
tag_sorting = "frequency"
menu = [
@@ -235,6 +262,7 @@ menu = [
{ name = "articles", url = "articles", trailing_slash = true },
{ name = "tags", url = "tags", trailing_slash = true },
{ name = "archive", url = "archive", trailing_slash = true },
{ name = "git", url = "https://gitea.zlinux.ru/zloy_linux", trailing_slash = true },
]
feed_icon = true
@@ -242,20 +270,108 @@ email = "emxveV9saW51eEB6bGludXgucnU="
encode_plaintext_email = true
socials = [
{ name = "sharkey", url = "https://zlinux.ru/@zloy_linux", icon = "sharkey" },
{ name = "microblog", url = "https://log.zlinux.ru/", icon = "microblog" },
{ name = "telegram", url = "https://t.me/#", icon = "telegram" },
{ name = "peertube", url = "https://video.zlinux.ru/a/zloy_linux/video-channels", icon = "peertube" },
# { name = "peertube", url = "https://video.zlinux.ru/a/zloy_linux/video-channels", icon = "peertube" },
# { name = "youtube", url = "https://youtube.com/#", icon = "youtube" },
{ name = "pixelfed", url = "https://pixelfed.social/zloy_linuxoid", icon = "pixelfeed" },
{ name = "pixelfed", url = "https://pixelfed.social/zloy_linuxoid", icon = "pixelfed" },
]
footer_menu = [
# {url = "about", name = "about", trailing_slash = true},
# {url = "privacy", name = "privacy", trailing_slash = true},
{ name = "projects", url = "projects", trailing_slash = true },
{ url = "https://zloy.goatcounter.com", name = "статистика", trailing_slash = true },
{ url = "sitemap.xml", name = "sitemap", trailing_slash = false },
]
[extra.analytics]
service = "umami"
id = "4ee44323-925a-401b-996d-2b6a63dfc527"
self_hosted_url = "https://stats.zlinux.ru"
#service = "umami"
#id = "4ee44323-925a-401b-996d-2b6a63dfc527"
#self_hosted_url = "https://stats.zlinux.ru"
service = "goatcounter"
id = "zloy"
#self_hosted_url = "https://zloy.goatcounter.com"
# Optional: For Umami, enable this option to respect users' Do Not Track (DNT) settings. The default is true.
do_not_track = true
[extra.isso]
enabled_for_all_posts = true # Enables Isso on all posts. It can be enabled on individual posts by setting `isso = true` in the [extra] section of a post's front matter.
automatic_loading = true # If set to false, a "Load comments" button will be shown.
endpoint_url = "https://comment.zlinux.ru/" # Accepts relative paths like "/comments/" or "/isso/", as well as full urls like "https://example.com/comments/". Include the trailing slash.
page_id_is_slug = true # If true, it will use the relative path for the default language as id; this is the only way to share comments between languages. If false, it will use the entire url as id.
lang = "" # Leave blank to match the page's language.
max_comments_top = "inf" # Number of top level comments to show by default. If some comments are not shown, an “X Hidden” link is shown.
max_comments_nested = "5" # Number of nested comments to show by default. If some comments are not shown, an “X Hidden” link is shown.
avatar = true
voting = true
page_author_hashes = "" # hash (or list of hashes) of the author.
lazy_loading = true # Loads when the comments are in the viewport (using the Intersection Observer API).
[extra.webmentions]
# To disable for a specific section or page, set webmentions = false in that page/section's front matter's [extra] section.
enable = false
# Specify the domain registered with webmention.io.
domain = ""
# The HTML ID for the object to fill in with the webmention data.
# Defaults to "webmentions"
# id = "webmentions"
# data configuration for the webmention.min.js script
# The base URL to use for this page. Defaults to window.location
# page_url =
# Additional URLs to check, separated by |s
# add_urls
# The maximum number of words to render in reply mentions.
# wordcount = 20
# The maximum number of mentions to retrieve. Defaults to 30.
# max_webmentions = 30
# By default, Webmentions render using the mf2 'url' element, which plays
# nicely with webmention bridges (such as brid.gy and telegraph)
# but allows certain spoofing attacks. If you would like to prevent
# spoofing, set this to a non-empty string (e.g. "true").
# prevent_spoofing
# What to order the responses by; defaults to 'published'. See
# https://github.com/aaronpk/webmention.io#api
# sort_by
# The order to sort the responses by; defaults to 'up' (i.e. oldest
# first). See https://github.com/aaronpk/webmention.io#api
# sort_dir
# If set to a non-empty string (e.g. "true"), will display comment-type responses
# (replies/mentions/etc.) as being part of the reactions
# (favorites/bookmarks/etc.) instead of in a separate comment list.
# comments_are_reactions = "true"
# h-card configuration
# Will identify you on the indieweb (see https://microformats.org/wiki/h-card)
[extra.hcard]
# Enable home page h-card.
enable = true
# Add your email to the card if extra.email is set and not encoded.
with_mail = true
# Add your social links ('socials' config) to the card.
with_social_links = true
# Homepage url. Defaults to the value of 'base_url'.
homepage = "https://zlinux.ru"
avatar = "img/avator.webp"
# Display name, default to the value of 'author'.
full_name = "Zloy Linux"
# Small bio, as shown on social media profiles.
biography = "Linux Man"
#
# You can add any property from https://microformats.org/wiki/h-card#Properties
# Make sure to replace all '-' characters by '_'
# Examples:
p_nickname = "zloy_linux"
# p_locality = "Bordeaux"
p_country_name = "Russia"

View File

@@ -6,6 +6,9 @@ sort_by = "date"
header = {title = "Привет, я Злой!", img = "img/avator.webp", img_alt = "I'm ZLOY" }
section_path = "blog/_index.md"
max_posts = 5
projects_path = "projects/_index.md"
max_projects = 3
show_projects_first = false
social_media_card = "index.jpg"
+++

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -0,0 +1,210 @@
+++
title = "Авторизация на сервере с использованием SSH-ключей"
date = 2025-03-09
description = "При установлении соединения с удалённым сервером, будь то через SSH или другие протоколы, критически важно подтвердить свою идентичность для получения доступа к системе. Существует несколько методов аутентификации, но наиболее безопасным и удобным считается использование SSH-ключей. В данном материале мы разберём принципы работы данной технологии, её преимущества перед парольной аутентификацией, а также порядок настройки на практике."
[taxonomies]
tags = ["authorization", "ssh", "server"]
[extra]
quick_navigation_buttons = true
toc = false
mermaid = false
social_media_card = "social_cards/index_authorized-ssh-key.webp"
+++
## Что такое авторизация с ключами?
Авторизация с ключами (или метод, основанный на паре открытого и закрытого ключа) — это способ идентификации, при котором вместо обычного пароля используются два криптографических ключа:
- `Закрытый ключ (private key)` — ваш личный "секрет", который остаётся только у вас и никому не разглашается.
- `Открытый ключ (public key)` — своего рода "замок", который вы устанавливаете на сервере, чтобы он распознавал вас как авторизованного пользователя.
Когда вы подключаетесь, сервер сравнивает ваш закрытый ключ с открытым ключом, который у него есть. Если они совпадают, вы получаете доступ. Это похоже на то, как если бы у вас был ключ от двери, а серверу достаточно проверить, подходит ли он к замку.
## Чем это лучше паролей?
1. **Надёжность:** Пароли уязвимы к угадыванию или взлому через перебор (brute force), тогда как ключи — это сложные зашифрованные данные, которые почти невозможно разгадать.
2. **Простота использования:** После настройки процесс подключения становится автоматическим, и вам не приходится каждый раз вводить пароль.
3. **Защита от перехвата:** Даже если кто-то взломает соединение, закрытый ключ остаётся в безопасности, так как он не передаётся по сети.
## Принцип работы SSH-аутентификации по ключам:
1. **Создание ключевой пары**
На вашем компьютере генерируется пара ключей — открытый и закрытый. Это можно сделать с помощью команды `ssh-keygen`. Важно: закрытый ключ хранится только на вашем компьютере, а открытый нужно передать на сервер.
2. **Установка открытого ключа на сервер**
Открытый ключ добавляется в специальный файл `authorized_keys` на сервере. Это можно сделать через команду `ssh-copy-id` или вручную, скопировав содержимое файла с открытым ключом.
3. **Процесс аутентификации**
При попытке подключения происходит следующее:
- Сервер генерирует случайное сообщение
- Ваш компьютер “подписывает” это сообщение закрытым ключом
- Подписанное сообщение отправляется обратно на сервер
- Сервер проверяет подпись с помощью открытого ключа
- Если подпись верна — доступ предоставляется
Такая система обеспечивает высокий уровень безопасности, поскольку закрытый ключ никогда не передаётся по сети, а открытый ключ не может быть использован для дешифровки данных.
## Настройка SSH-доступа с использованием ключей
Этот гайд поможет вам настроить безопасный способ подключения к серверу через SSH с использованием ключей вместо паролей. Мы рассмотрим процесс на примере Linux/macOS, но те же принципы применимы и для Windows.
### 1. Создание ключевой пары
1. **Запустите терминал на вашем компьютере**
2. **Выполните команду генерации ключей:**
```
ssh-keygen -t rsa -b 4096
```
где:
- `-t rsa` определяет тип ключа
- `-b 4096` устанавливает длину ключа для повышенной безопасности
3. **Следуйте инструкциям:**
- Выберите место сохранения (по умолчанию `~/.ssh/id_rsa`)
- При желании установите пароль для дополнительной защиты ключа
**В результате вы получите:**
- Закрытый ключ (`id_rsa`) - храните в надёжном месте
- Открытый ключ (`id_rsa.pub`) - будет отправлен на сервер
### 2. Установка ключа на сервере
1. Подключитесь к серверу обычным способом:
```
ssh username@server_ip
```
2. Перенесите открытый ключ:
```
ssh-copy-id username@server_ip
```
**Альтернативный способ:**
- Откройте файл `~/.ssh/authorized_keys` на сервере
- Добавьте содержимое вашего `id_rsa.pub`
### 3. Тестирование подключения
1. Завершите текущую SSH-сессию:
```
exit
```
2. Попробуйте подключиться снова:
```
ssh username@server_ip
```
Если всё настроено верно, вы войдёте автоматически. При наличии пароля для ключа система запросит его ввод.
### 4. Усиление безопасности (опционально)
1. Отредактируйте конфигурационный файл SSH:
```
sudo nano /etc/ssh/sshd_config
```
2. Измените параметры:
```
PasswordAuthentication no
```
3. Перезапустите SSH-сервер:
```
sudo systemctl restart sshd
```
После этих действий доступ к серверу будет возможен только с использованием SSH-ключей, что значительно повысит безопасность системы.
### Важные рекомендации:
- Регулярно проверяйте список авторизованных ключей
- Храните резервные копии приватных ключей
- Используйте разные ключи для разных серверов
- Не передавайте приватные ключи по незащищённым каналам
## Рекомендации по работе с SSH-ключами
### Основные правила безопасности
1. **Защита приватного ключа**
- Храните его с правами доступа 600
- Не передавайте никому
- Делайте резервные копии в надёжных местах
2. **Организация ключей**
- Создавайте отдельные пары ключей для разных серверов
- Используйте понятную систему наименования
- Регулярно проверяйте список авторизованных ключей
### Работа с SSH-агентом
Для удобства работы можно использовать `ssh-agent`:
```
eval $(ssh-agent)
ssh-add ~/.ssh/id_rsa
```
Это позволит не вводить пароль от приватного ключа при каждом подключении.
### Устранение типичных проблем
1. **Проверьте права доступа:**
- На клиенте: `chmod 700 ~/.ssh` и `chmod 600 ~/.ssh/id_rsa`
- На сервере: `chmod 700 ~/.ssh` и `chmod 600 ~/.ssh/authorized_keys`
2. **Проверьте настройки сервера:**
- В файле `/etc/ssh/sshd_config` должны быть строки:
```
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
```
3. **Используйте отладочную информацию:**
- Подключайтесь с параметром -v: `ssh -v username@server`
- Проверяйте логи сервера: `sudo tail -f /var/log/auth.log`
### Дополнительные меры безопасности
<br>
- Отключите вход по паролю:
```
PasswordAuthentication no
```
- Ограничьте доступ по IP:
```
AllowUsers username@192.168.1.1
```
- Используйте ключи длиной не менее 4096 бит
- Регулярно проверяйте файлы `authorized_keys` на наличие посторонних ключей
- При утере приватного ключа немедленно удалите соответствующий публичный ключ с серверов
Следуя этим рекомендациям, вы сможете обеспечить надёжную и безопасную работу с SSH-ключами.

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -0,0 +1,123 @@
+++
title = "Автоматическое переключение звука на Bluetooth-наушники в Linux с помощью udev и wpctl"
date = 2025-09-07
description = "Если вы используете Linux и часто подключаете Bluetooth-наушники, наверняка замечали, что звук не всегда автоматически переключается на новое устройство. В этом посте мы разберём простой и надёжный способ автоматизации этого процесса с помощью udev и wpctl (PipeWire)."
[taxonomies]
tags = ["bluetooth", "udev", "wpctl", "pipewire"]
[extra]
quick_navigation_buttons = true
toc = true
mermaid = false
social_media_card = "social_cards/index_autoconnect-bluetooth-phones.webp"
+++
## Вступление
В Linux есть два популярных звуковых сервиса: PulseAudio и PipeWire. PipeWire постепенно становится стандартом, и в сочетании с WirePlumber можно настроить автоматическое переключение. Но иногда WirePlumber не срабатывает, или событие обрабатывается с задержкой. Решение — использовать udev, который отслеживает события подключения устройств и запускает скрипт.
## Шаг 1: Проверяем PipeWire и wpctl
Убедитесь, что PipeWire и WirePlumber запущены:
```bash
systemctl --user status pipewire
systemctl --user status wireplumber
```
Также полезно посмотреть список аудиоустройств:
```bash
wpctl status
```
Вы увидите свои колонки и Bluetooth-устройства:
```ini
Audio
├─ Sinks:
│ * 136. alsa_output.usb-ARTURIA_ArturiaMsd-00.analog-stereo
│ 55. bluez_output.XX_XX_XX_XX_XX_XX.a2dp-sink
```
## Шаг 2: Создаём скрипт для переключения звука
Создаём скрипт `/usr/local/bin/bt-audio-autoswitch.sh`:
```bash
#!/bin/bash
ACTION=$1
STATEFILE="/run/user/$(id -u)/bt-audio-last-sink"
LOGTAG="bt-audio-autoswitch"
# Текущее устройство вывода
get_default_sink() {
wpctl status | awk '/\*.*sink/ {print $2; exit}' | tr -d '[]'
}
# Идентификатор первого Bluetooth sink
get_bt_sink() {
wpctl status | awk '/bluez_output/ {print $2; exit}' | tr -d '[]'
}
# Ждём обновления PipeWire
sleep 2
if [ "$ACTION" = "add" ]; then
CURRENT=$(get_default_sink)
if [ -n "$CURRENT" ]; then
echo "$CURRENT" > "$STATEFILE"
logger "$LOGTAG: сохранил текущее устройство $CURRENT"
fi
BT_ID=$(get_bt_sink)
if [ -n "$BT_ID" ]; then
wpctl set-default "$BT_ID"
logger "$LOGTAG: переключил звук на $BT_ID"
fi
elif [ "$ACTION" = "remove" ]; then
if [ -f "$STATEFILE" ]; then
PREV=$(cat "$STATEFILE")
if [ -n "$PREV" ]; then
wpctl set-default "$PREV"
logger "$LOGTAG: вернул звук на $PREV"
fi
rm -f "$STATEFILE"
fi
fi
```
Делаем скрипт исполняемым:
```bash
sudo chmod +x /usr/local/bin/bt-audio-autoswitch.sh
```
## Шаг 3: Создаём udev-правило
```ini
# Подключение Bluetooth аудио
ACTION=="add", SUBSYSTEM=="sound", KERNEL=="card*", RUN+="/usr/local/bin/bt-audio-autoswitch.sh add"
# Отключение Bluetooth аудио
ACTION=="remove", SUBSYSTEM=="sound", KERNEL=="card*", RUN+="/usr/local/bin/bt-audio-autoswitch.sh remove"
```
Применяем новые правила:
```bash
sudo udevadm control --reload-rules
sudo udevadm trigger
```
## Шаг 4: Проверяем работу
Подключаем Bluetooth-наушники. Скрипт автоматически сохранит текущее устройство и переключит звук на наушники.
Отключаем наушники. Скрипт вернёт звук на сохранённое устройство (например, ваши колонки).
## Заключение
Использование udev + wpctl позволяет надёжно автоматизировать переключение аудио на Bluetooth-наушники даже в случае, если WirePlumber не успевает сработать. Этот подход легко настраивается и не требует ручного выбора устройства при каждом подключении.

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

View File

@@ -0,0 +1,108 @@
+++
title = "Блокируем скринсейвер в полноэкранном режиме браузера Vivaldi"
date = 2025-10-31
description = "Если при просмотре видео в полноэкранном режиме в Vivaldi (или любом другом браузере) скринсейвер всё равно включается, то в этой статье вы узнаете как это исправить"
[taxonomies]
tags = ["xscreensaver", "wmctrl", "xprop", "X11", "vivaldi", "browser"]
[extra]
quick_navigation_buttons = true
toc = false
mermaid = false
social_media_card = "social_cards/disable-screensaver-fullscreen.png"
+++
Иногда во время просмотра видео или презентации в браузере активируется `xscreensaver` — экран блокируется, несмотря на то, что пользователь фактически «активен».
Чтобы этого не происходило, можно написать небольшой bash-скрипт, который будет следить за окном браузера и отключать скринсейвер, когда он в полноэкранном режиме.
## 💾 Установка необходимых пакетов
```bash
sudo pacman -S wmctrl xorg-xprop
```
## 🔧 Скрипт
Создаём файл `/usr/local/bin/disable_screensaver_fullscreen.sh` и вставляем следующее содержимое:
```bash
#!/bin/bash
XSCREENSAVER_CMD="xscreensaver-command"
CHECK_INTERVAL=30
# Функция для деактивации скринсейвера (вызывается постоянно, пока Vivaldi в полноэкранном режиме)
deactivate_screensaver() {
# echo "Vivaldi в полноэкранном режиме. Отключаем скринсейвер."
"$XSCREENSAVER_CMD" -deactivate >/dev/null 2>&1
}
echo "Мониторинг полноэкранного режима Vivaldi запущен (только деактивация скринсейвера)."
while true; do
VIVALDI_WINDOW_ID=$(wmctrl -l -x | grep "vivaldi-stable.Vivaldi" | awk '{print $1}' | head -n 1)
if [ -n "$VIVALDI_WINDOW_ID" ]; then # Проверяем, что Vivaldi запущен
# Vivaldi запущен, проверяем полноэкранный режим
if xprop -id "$VIVALDI_WINDOW_ID" _NET_WM_STATE | grep -q "_NET_WM_STATE_FULLSCREEN"; then
deactivate_screensaver # Vivaldi в полноэкранном режиме: деактивируем
fi
fi
sleep "$CHECK_INTERVAL"
done
```
Сделаем скрипт исполняемым:
```bash
chmod +x /usr/local/bin/disable_screensaver_fullscreen.sh
```
## 🚀 Автозапуск при старте системы
Создайте файл:
`~/.config/autostart/block-screensaver-vivaldi.desktop`
```ini
[Desktop Entry]
Type=Application
Name=Block Screensaver in Vivaldi Fullscreen
Exec=/usr/local/bin/block-screensaver-vivaldi.sh
Hidden=false
NoDisplay=false
X-GNOME-Autostart-enabled=true
```
В bspwm достаточно добавить в `~/.config/bspwm/bspwm.conf` строчку
```bash
/usr/local/bin/vivaldi-stop-screensaver.sh &
```
## 🧠 Как это работает
Скрипт каждые 30 секунд:
- Проверяет, запущен ли браузер Vivaldi.
- Если окно в состоянии fullscreen, выполняет `xscreensaver-command -deactivate`, тем самым сбрасывая таймер бездействия.
Такой подход не мешает обычной работе скринсейвера, а только предотвращает его запуск во время полноэкранного просмотра видео.
- Нагрузка: sleep 30 → менее 0.1% CPU.
- Безопасность: только чтение X11, не требует root.
- Логи: раскомментируйте echo в функции для отладки.
## 🧩 Совместимость
Скрипт можно адаптировать под:
- Chromium / Brave / Chrome — заменив vivaldi-stable.Vivaldi на chromium.Chromium или соответствующее имя.
- Firefox — используйте Navigator.Firefox.
## ✅ Вывод
Простой, но эффективный способ избавить себя от внезапного включения заставки во время фильмов, YouTube и стримов.
Пусть xscreensaver отдыхает, когда ты смотришь видео — а не мешает! 😄

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -0,0 +1,80 @@
+++
title = "Dunst - добавляем к уведомлениям звуковое сопровождение"
date = 2025-03-14
description = "Dunst — это легковесный и настраиваемый демон уведомлений для Linux. В этой статье для примера мы рассмотрим, как настроить Dunst так, чтобы он проигрывал звук после завершения загрузки файла в браузере Vivaldi."
[taxonomies]
tags = ["dunst", "vivaldi", "notification"]
[extra]
quick_navigation_buttons = true
toc = true
mermaid = false
social_media_card = "social_cards/index_dunst.webp"
+++
## Определение уведомления
Прежде чем создать правило для Dunst, необходимо выяснить, какие параметры содержит уведомление о завершении загрузки.
Откройте терминал и запустите одну из следующих команд:
```bash
dunst -print
```
или
```bash
dbus-monitor "interface='org.freedesktop.Notifications'"
```
Затем загрузите файл через Vivaldi и обратите внимание на появившееся уведомление. Важно определить 'appname' и 'summary', которые используются для фильтрации уведомлений.
Получим вот такой вывод
```conf
...
string "Vivaldi"
uint32 0
string "file:///tmp/..com.vivaldi.Vivaldi.aPjVzZ"
string "Загрузка завершена"
string "Vivaldi
...
```
## Настройка правила в Dunst
Открываем конфигурационный файл Dunst `~/.config/dunst/dunstrc` добавляем в конец файла следующее правило:
```conf
[vivaldi_sound]
appname = "Vivaldi"
summary = "Загрузка завершена"
script = "~/.config/dunst/scripts/vivaldi-sound.sh"
# new_icon = "~/.config/dunst/scripts/download.svg"
```
**Разбор параметров:**
- `appname` = "vivaldi" — фильтр по приложению (может быть Vivaldi-stable, уточните в отладчике dunst -print).
- `summary` = "Загрузка завершена" — заголовок уведомления (может отличаться, проверьте с помощью dbus-monitor).
- `script` = "vivaldi-sound.sh" — ссылка на скрипт
- `new_icon` = "путь до изображения" - при желании можно сменить иконку в уведомлении
Содержимое `vivaldi-sound.sh`
```bash
#!/bin/sh
paplay ~/.config/dunst/scripts/sounds/zvonkiy-korotkiy-zvuk-uvedomleniya.ogg
```
## Перезапуск Dunst
После сохранения изменений нужно перезапустить Dunst
`pkill dunst && dunst &`
## Заключение
Теперь после завершения загрузки файла в Vivaldi будет воспроизводить звуковой сигнал через Dunst. Вы можете настроить другие уведомления аналогичным способом, например, добавить звуки для других событий, таких как ошибки или завершение работы программ.

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@@ -0,0 +1,116 @@
+++
title = "SSHFS: Монтирование удаленных файловых систем через SSH"
date = 2025-03-09
description = "SSHFS (SSH File System) позволяет монтировать удаленные файловые системы через SSH, обеспечивая безопасный и удобный доступ к файлам на удаленных серверах."
[taxonomies]
tags = ["sshfs", "ssh", "server"]
[extra]
quick_navigation_buttons = true
toc = false
mermaid = false
social_media_card = "social_cards/index-sshfs.webp"
+++
## Введение
[SSHFS](https://github.com/libfuse/sshfs) это мощный инструмент для работы с удаленными файлами, обеспечивающий удобство и безопасность благодаря использованию SSH. Он полезен для резервного копирования, совместной работы и доступа к файлам на удаленных серверах без сложной настройки.
## Установка SSHFS
```bash
sudo pacman -S sshfs
```
## Монтирование удаленной файловой системы
1. Создайте локальную папку для монтирования:
```bash
sudo mkdir /mnt/sshfs
```
2. Подключите удаленную файловую систему:
```bash
sshfs user@remote_host:/remote_folder /mnt/sshfs
```
- `user` имя пользователя на удаленном сервере
- `remote_host` IP-адрес или доменное имя сервера
- `/remote_folder` каталог, который нужно смонтировать
- `/mnt/sshfs` локальная папка, куда будет смонтирован удаленный ресурс
Если используется нестандартный порт указать можно так:
```bash
sshfs -p 223 user@remote_host:/remote_folder /mnt/sshfs
```
## Автоматическое монтирование при загрузке
Добавьте в `/etc/fstab` строку:
```ini
user@remote_host:/remote_folder /mnt/sshfs fuse.sshfs defaults,_netdev,user,idmap=user,allow_other,reconnect 0 0
```
Создайте файл с учетными данными `~/.sshfs_credentials`:
```bash
echo "password" > ~/.sshfs_credentials
chmod 600 ~/.sshfs_credentials
```
с содежимым:
```ini
username=ТВОЙ_ЛОГИН
password=ТВОЙ_ПАРОЛЬ
```
Монтирование с учётом `~/.sshfs_credentials`
```bash
sshfs -o password_stdin,credentials=~/.sshfs_credentials user@remote_host:/remote_folder /mnt/sshfs
```
## Размонтирование файловой системы
```bash
fusermount -u /mnt/sshfs
```
**Дополнительные параметры**
- `-o reconnect` автоматически переподключаться при разрыве связи
- `-o allow_other` разрешить доступ другим пользователям системы
- `-o idmap=user` сопоставлять идентификаторы пользователей
## Монтирование с помощью systemd
{{ admonition(type="danger", icon="tip", title="Внимание!", text="Авторизация на сервере должна проходить по ключу") }}
Создаём юнит-файл systemd `~/.config/systemd/user/server.service`
```ini
[Unit]
Description=Mount /mnt/sshfs/
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=sshfs -p 666 user@remote_host:/remote_folder /mnt/sshfs
ExecStop=umount /mnt/sshfs/
[Install]
WantedBy=default.target
```
# Запуск и автозагрузка
```bash
systemctl --user daemon-reload
systemctl --user enable now server.service
```
Теперь `/mnt/sshfs/` автоматически монтируется при загрузке.

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -19,22 +19,31 @@ social_media_card = "social_cards/index-post-ia.webp"
Этот ИИ оказался самым тупым и выдал
<img src="img/perplexity.png">
<div class="gallery">
<a href="img/perplexity.png">
<img src="img/perplexity.png" alt="Превью">
</a>
</div>
## Grok
Оказался поумнее
<img src="img/grok.png">
<div class="gallery">
<a href="img/grok.png">
<img src="img/grok.png" alt="Grok">
</a>
</div>
## chatGPT
Выдал примерно тоже самое, что и Grok
<img src="img/chat_gpt.png">
<div class="gallery">
<a href="img/chat_gpt.png">
<img src="img/chat_gpt.png" alt="ChatGPT">
</a>
</div>
## Свой вариант

View File

@@ -0,0 +1,128 @@
+++
title = "Переписал скрипт stop-screensaver и добавил на панель Polybar"
date = 2025-11-22
description = "В прошлом [посте](https://blog.zlinux.ru/articles/disable-screensaver-fullscreen/) выкладывал скрипт, который отключал скринсейвер при просмотре видео в браузере. Позже понял, что держать его запущенным постоянно - не лучшая идея."
[taxonomies]
tags = ["xscreensaver", "polybar", "playerctl"]
[extra]
quick_navigation_buttons = true
toc = false
mermaid = false
social_media_card = "social_cards/polybar-stop-screensaver.png"
+++
В прошлом [посте](https://blog.zlinux.ru/articles/disable-screensaver-fullscreen/) выкладывал скрипт, который отключал скринсейвер при просмотре видео в браузере. Позже понял, что держать его запущенным постоянно - не лучшая идея.
## Новый скрипт
Переписал скрипт `/usr/local/bin/stop-screensaver.sh` и максимально упростил:
```bash
#!/bin/bash
while true; do
status=$(playerctl --player=vivaldi status 2>/dev/null)
if [[ "$status" == "Playing" ]]; then
xscreensaver-command -deactivate >/dev/null
fi
sleep 30
done
```
Для удобства написал полноценный модуль для `Polybar`, который позволяет включать и выключать эту функцию по клику. Для работы понадобится всего два небольших скрипта.
## Статусный скрипт
Показывает иконку "ICON_ON" когда демон запущен, и "ICON_OFF" - когда нет) `/home/$USER/.config/polybar/scripts/stop_screensaver_status.sh`
```bash
#!/usr/bin/env bash
STATEFILE="/tmp/stop-screensaver.state"
ICON_ON=""
ICON_OFF=""
if [ -f "$STATEFILE" ]; then
state=$(cat "$STATEFILE")
else
state=""
fi
# fallback проверка процесса, если statefile отсутствует/неактуален
if pgrep -f "/usr/local/bin/stop-screensaver.sh" >/dev/null; then
state="running"
fi
if [ "$state" = "running" ]; then
echo " $ICON_ON "
else
echo " $ICON_OFF "
fi
```
## Кликабельный скрипт
Скрипт для запуска/остановки демона. `/home/$USER/.config/polybar/scripts/stop_screensaver_toggle.sh`
```bash
#!/usr/bin/env bash
SCRIPT="/usr/local/bin/stop-screensaver.sh"
STATEFILE="/tmp/stop-screensaver.state"
if pgrep -f "$SCRIPT" >/dev/null; then
pkill -f "$SCRIPT"
else
nohup "$SCRIPT" >/dev/null 2>&1 &
fi
# записать текущее состояние
if pgrep -f "$SCRIPT" >/dev/null; then
echo "running" >"$STATEFILE"
else
echo "stopped" >"$STATEFILE"
fi
```
Делаем файлы исполняемыми
```bash
chmod +x ~/.config/polybar/scripts/stop_screensaver_*.sh
```
## Добавляем в polybar
В конфиге `/home/$USER/.config/polybar/config.ini` прописываем:
```ini
[module/stop_screensaver]
type = custom/script
exec = ~/.config/polybar/scripts/stop_screensaver_status.sh
interval = 2
click-left = ~/.config/polybar/scripts/stop_screensaver_toggle.sh
label-background = ${gruvbox.green-alt}
label-foreground = ${gruvbox.black}
```
и добавляем расположение модуля `stop_screensaver` в modules-left/modules-center/modules-right
<div class="gallery">
<a href="stop-screensaver-off.png" data-caption="Значок на панеле Polybar">
<img src="stop-screensaver-off.png" alt="stop_screensaver выключен">
</a>
<a href="stop-screensaver-on.png">
<img src="stop-screensaver-on.png" alt="stop_screensaver включен">
</a>
...
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,100 @@
+++
title = "Отказался от Syncthing"
date = 2025-11-28
description = "За последние годы я был преданным пользователем Syncthing. Он честно отрабатывал синхронизацию дотфайлов, музыки, баз паролей и хранилища Obsidian между ПК, домашним сервером и телефоном."
[taxonomies]
tags = ["syncthing", "rsync", "music", "keepass", "seafile", "webdav", "git", "obsidian", "sync", "android", "archlinux"]
[extra]
quick_navigation_buttons = true
toc = false
mermaid = false
social_media_card = "social_cards/refused-syncthing.png"
+++
За последние годы я был преданным пользователем `Syncthing`. Он честно отрабатывал синхронизацию дотфайлов, музыки, баз паролей и хранилища `Obsidian` между ПК, домашним сервером и телефоном.
Но со временем накопился «технический долг»: постоянные конфликты в `KeePassXC`, лишний трафик, батарея на телефоне, а главное — ощущение, что я использую универсальный молоток там, где достаточно отвёртки. В итоге я полностью отказался от `Syncthing` и перешёл на три узкоспециализированных, но гораздо более надёжных и лёгких решения.
## 1. Музыка для Navidrome → rsync (односторонняя синхронизация)
Раньше я синхронизировал ~60 ГБ музыки в обе стороны через `Syncthing`.
Теперь всё просто: на ПК лежит «золотая» копия библиотеки, а на сервере я обновляю её только когда хочу.
```bash
rsync -avzh --progress --stats --delete --itemize-changes --exclude='ненужное' /home/zloy_linux/Музыка/ user@server.ru:/home/user/music/
```
Почему `rsync`, а не `Syncthing`:
- Никаких конфликтов и дублей
- --delete убирает удалённые файлы
- Полный контроль: я решаю, когда и что синхронизировать
- Можно запускать по расписанию через cron/anacron или вручную после добавления альбомов
**Значение опций:**
**-z** сжатие (полезно, если канал медленный).
**-h** человекочитаемые размеры.
**--stats** - статистика по передаче.
**--delete** - убирает удалённые файлы
**--exclude** исключения.
## 2. Пароли KeePassXC → Seafile + WebDAV (одна общая база)
`Syncthing` постоянно создавал .sync-conflict файлы при одновременном редактировании базы с телефона и ПК. Решение оказалось проще простого:
1. Поднял `Seafile`
2. Включил `WebDAV` в библиотеке
3. В `KeePassXC` на всех устройствах указал один и тот же URL:
```ini
https://cloud.userdomain.ru:/seafdav/library/sync/paroli.kdbx
```
Теперь:
- Одна база, никаких конфликтов
- Автоматическая блокировка при открытии
- История изменений в Seafile на 180 дней (если вдруг что-то удалишь)
## 3. Заметки Obsidian → Git
Раньше я синхронизировал всё хранилице через `Syncthing` → конфликты, битые ссылки, тормоза на мобильном. Сейчас:
- Хранилище лежит в приватном репозитории на своём Gitea (можно GitHub, GitLab, Codeberg)
- В `Obsidian` установлен плагин `Git`
- Настройки плагина:
```ini
Commit message: "auto commit: {{date}}"
Auto commit every: 3 минуты
Auto pull/push on startup: включено
Pull on mobile: включено
```
На телефоне - приложение `Obsidian` + `git`. Плюсы:
- Полная история изменений (можно откатить удалённую заметку за секунду)
- Никаких конфликтов - `git` сам делает merge
- Работает даже без интернета (коммиты локально, пуш при подключении)
## Итог: что я получил после отказа от Syncthing
| Что синхронизирую | Было (Syncthing) | Стало (2025) | Выигрыш |
|-------------------------|------------------------------------------|---------------------------------------|----------------------------------------------------|
| Музыка (~60 ГБ) | Двусторонняя синхронизация, конфликты | rsync (односторонне) | -90 % трафика, нет дублей, полный контроль |
| Пароли (KeePassXC) | Постоянные .kdbx.sync-conflict файлы | Seafile + WebDAV (одна общая база) | Ноль конфликтов, блокировка, история изменений |
| Заметки (Obsidian) | Конфликты, битые ссылки, тормоза | Obsidian Git + личный Gitea | История коммитов, ветки, оффлайн-работа, автопуш |
| Батарея телефона | Syncthing в фоне всё время | Только rsync по необходимости | +23 часа автономности |
| Трафик и нагрузка | Постоянный скан и передача | Только когда я сам запускаю | Минимальный фоновый трафик |
**Syncthing** - отличный инструмент, но он решает задачу «синхронизировать всё со всем».
А мне оказалось достаточно трёх точечных решений, которые делают ровно то, что нужно - и ничего лишнего.
2025 год — год простых и надёжных решений. `rsync` + `Seafile` `WebDAV` + `Obsidian` `Git`.

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

@@ -17,7 +17,11 @@ social_media_card = "social_cards/index-post-predprosmotr.webp"
При добавлении в пост ссылки должна отображаться предварительная информация о содержимом веб-страницы, а в данном случае получалось вот это
<img src="not_preview.webp" />
<div class="gallery">
<a href="not_preview.webp">
<img src="not_preview.webp" alt="Предварительный просмотр недоступен">
</a>
</div>
Поначалу копался в настройках инстанса, но оказалось, проблема крылась во внутреннем DNS, который возвращал для домена локальный IP-адрес
@@ -34,7 +38,11 @@ Address: ::1
Сменил DNS сервер, стало выдавать глобальный IP и всё заработало.
<img src="yes_preview.webp" />
<div class="gallery">
<a href="yes_preview.webp">
<img src="yes_preview.webp" alt="Предварительный просмотр доступен">
</a>
</div>
Поигрался с ссылками и оказалось

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

View File

@@ -0,0 +1,46 @@
+++
title = "Замена SSD на сервере"
date = 2025-03-06
description = "Пришло время для расширения дискового пространства, поскольку свободных осталось ~30GB, осталось опеределиться с выбором, что покупать."
[taxonomies]
tags = ["server", "ssd", "clonezilla"]
[extra]
quick_navigation_buttons = true
toc = false
mermaid = false
social_media_card = "social_cards/index_zamena-ssd-server.webp"
+++
## Выбор
Не хотелось долго ждать заказ, поэтому решил воспользоваться ближайшим магазином DNS. В моём мини-ПК сервере есть поддержка как `SATA`, так и `M.2 SATA`, и, конечно, приоритет был за `M.2`. Однако по сути скорость у них одинаковая — разница лишь в интерфейсе подключения.
В последние годы появилось много сомнительных китайских SSD, которые могут выйти из строя буквально после первого включения. Как оказалось, выбор не так велик: `ADATA`, `Apacer`, `TEAMGROUP` и `Tammuz` — не самые внушающие доверие бренды.
Среди SATA SSD ассортимент оказался шире, и в наличии был [Samsung 870 EVO](https://www.dns-shop.ru/product/cd3ad695f76bed20/500-gb-25-sata-nakopitel-samsung-870-evo-mz-77e500bw/). Такой же, но в формате `M.2 NVMe`, уже два года работает в моём основном ПК без проблем. Выбор очевиден.
<div class="gallery">
<a href="samsung-ssd.webp">
<img src="samsung-ssd.webp" alt="Samsung 870 EVO и какой - то ноунейм">
</a>
</div>
## Замена SSD
На фото выше — комплектный SSD неизвестного бренда и сомнительного происхождения. Удивительно, но он проработал почти год.
Интересный момент: диск, подключённый через `SATA`, определяется как `sda1`, а `M.2 SATA` — как `sdb1`. Выходит, что первым по приоритету считается именно SATA-интерфейс.
## Перенос данных
Привет и уважение разработчикам замечательной [Clonezilla](https://clonezilla.org) — просто, быстро и удобно (ну, если не считать великолепный `dd`).
<div class="gallery">
<a href="clonezilla.webp">
<img src="clonezilla.webp" alt="Клонировние старого ssd">
</a>
</div>
## Итог
На всё ушло около 10 минут, зато теперь у меня есть свободные 400 ГБ — можно продолжать заполнять данными.

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@@ -32,7 +32,9 @@ Looking for detailed instructions or tips on using **tabi**? The [blog](https://
Contributions are much appreciated! We appreciate bug reports, improvements to translations or documentation (however minor), feature requests… Check out the [Contributing Guidelines](https://github.com/welpo/tabi/blob/main/CONTRIBUTING.md) to learn how you can help. Thank you!
## License
## license
![](https://img.zlinux.ru/lutim/Des7Y0YD/13xLIolT)
The code is available under the [MIT license](https://choosealicense.com/licenses/mit/).

View File

@@ -0,0 +1,11 @@
+++
title = "Проекты"
sort_by = "weight"
template = "cards.html"
insert_anchor_links = "left"
[extra]
social_media_card = "projects/projects.jpg"
show_reading_time = false
quick_navigation_buttons = true
+++

View File

@@ -0,0 +1,9 @@
+++
title = "Микроблог"
description = "Zloy про Arch, терминал, велосипеды и всё, что движется (и компилируется)."
weight = 1
[extra]
local_image = "projects/microblog.svg"
link_to = "https://log.zlinux.ru"
+++

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@@ -0,0 +1,9 @@
+++
title = "Searxng"
description = "Поисковая система"
weight = 3
[extra]
local_image = "projects/searxng.svg"
link_to = "https://search.zlinux.ru/searxng/"
+++

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -0,0 +1,70 @@
+++
title = "LosslessCut Лёгкий и быстрый редактор видео"
date = 2025-03-22
description = "LosslessCut — это бесплатный и кроссплатформенный инструмент для быстрой обрезки и монтажа видео и аудио файлов без перекодирования. Он разработан для тех, кому нужно быстро вырезать фрагменты из медиафайлов, сохранив оригинальное качество."
[taxonomies]
tags = ["video", "editor", "ffmpeg"]
[extra]
quick_navigation_buttons = true
toc = true
mermaid = true
social_media_card = "social_cards/index-lossless-cut.webp"
+++
<div class="gallery">
<a href="img/lossless-cut-main.webp">
<img src="img/lossless-cut-main.webp" alt="Внешний вид Lossless-Cut">
</a>
</div>
## Основные возможности
- Быстрая обрезка и нарезка позволяет вырезать нужные фрагменты видео и аудио без потерь.
- Поддержка множества форматов работает с MP4, MKV, WEBM, MOV, AAC, MP3 и другими контейнерами.
- Без перекодирования (lossless) не ухудшает качество, поскольку не выполняет повторную компрессию.
- Извлечение аудиодорожек позволяет выделить аудиофайлы из видео.
- Поддержка субтитров и метаданных можно сохранять и удалять субтитры и другие потоки.
- Создание скриншотов позволяет делать кадры из видео в высоком разрешении.
- Слияние файлов объединяет несколько медиафайлов одинакового формата.
- Разделение по главам поддерживает работу с файлами, содержащими главы (например, Blu-ray рипы).
<div class="gallery">
<a href="img/lossless-cut-export.webp">
<img src="img/lossless-cut-export.webp" alt="Параметры экспорта">
</a>
</div>
## Преимущества
✔️ Скорость операции выполняются моментально, так как нет необходимости в перекодировании.
✔️ Лёгкость использования интуитивно понятный интерфейс, не требует сложной настройки.
✔️ Кроссплатформенность доступен для Windows, macOS и Linux.
✔️ Open Source проект с открытым исходным кодом, доступный на GitHub.
## Недостатки
Не поддерживает сложный монтаж (например, переходы или наложение эффектов).
❌ Возможны проблемы с редактированием некоторых кодеков (например, HEVC).
❌ Нет автоматического исправления ошибок в файлах.
## Установка
```bash
yay losslesscut-bin
```
## Заключение
LosslessCut идеально подходит для быстрой и эффективной нарезки видео и аудио без потерь качества. Это простой инструмент для тех, кто не хочет возиться с громоздкими видеоредакторами.
**Подходит для:**
✅ Быстрой нарезки записей с экрана или камеры.
✅ Вырезания рекламы из записей трансляций.
✅ Извлечения звука из видео.
* GitHub: [https://github.com/mifi/lossless-cut](https://github.com/mifi/lossless-cut)

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

View File

@@ -13,7 +13,11 @@ mermaid = true
social_media_card = "social_cards/index-soft-nmail.webp"
+++
<img src="img/nmail.png">
<div class="gallery">
<a href="img/nmail.png">
<img src="img/nmail.png" alt="Внешний вид Nmail">
</a>
</div>
## Описание

View File

@@ -1,6 +1,7 @@
+++
title = "Файловый менеджер Yazi"
date = 2025-02-25
updated = 2025-03-09
description = "Yazi — это современный, быстрый и настраиваемый файловый менеджер для терминала, написанный на Rust. Он поддерживает удобную навигацию, предпросмотр файлов, работу с вкладками и мощные клавиатурные сочетания, вдохновлённые ranger и nnn."
[taxonomies]
@@ -31,7 +32,11 @@ Yazi (что означает "утка") - это файловый менедж
- SCP и другие протоколы 🌐 можно настроить для удалённой работы с файлами.
<img src="img/yazi_screen.png">
<div class="gallery">
<a href="img/yazi_screen.png">
<img src="img/yazi_screen.png" alt="Внешний вид приложения">
</a>
</div>
## Установка
@@ -39,6 +44,10 @@ Yazi (что означает "утка") - это файловый менедж
sudo pacman -S yazi
```
## Демо
<iframe title="Демо менеджера файлов Yazi" width="560" height="315" src="https://video.zlinux.ru/videos/embed/fc3ee4db-8899-4c76-9310-39e59551164b?warningTitle=0" frameborder="0" allowfullscreen="" sandbox="allow-same-origin allow-scripts allow-popups allow-forms"></iframe>
## Ссылки

237
sass/baguetteBox.scss Normal file
View File

@@ -0,0 +1,237 @@
/*!
* baguetteBox.js
* @author feimosi
* @version 1.13.0
* @url https://github.com/feimosi/baguetteBox.js
*/
#baguetteBox-overlay {
display: none;
opacity: 0;
position: fixed;
overflow: hidden;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000000;
background-color: #222;
background-color: rgba(0, 0, 0, 0.8);
-webkit-transition: opacity 0.5s ease;
transition: opacity 0.5s ease;
}
#baguetteBox-overlay.visible {
opacity: 1;
}
#baguetteBox-overlay .full-image {
display: inline-block;
position: relative;
width: 100%;
height: 100%;
text-align: center;
}
#baguetteBox-overlay .full-image figure {
display: inline;
margin: 0;
height: 100%;
}
#baguetteBox-overlay .full-image img {
display: inline-block;
width: auto;
height: auto;
max-height: 100%;
max-width: 100%;
vertical-align: middle;
-webkit-box-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
-moz-box-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
box-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
}
#baguetteBox-overlay .full-image figcaption {
display: block;
position: absolute;
bottom: 0;
width: 100%;
text-align: center;
line-height: 1.8;
white-space: normal;
color: #ccc;
background-color: #000;
background-color: rgba(0, 0, 0, 0.6);
font-family: sans-serif;
}
#baguetteBox-overlay .full-image:before {
content: "";
display: inline-block;
height: 50%;
width: 1px;
margin-right: -1px;
}
#baguetteBox-slider {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
white-space: nowrap;
-webkit-transition: left 0.4s ease, -webkit-transform 0.4s ease;
transition: left 0.4s ease, -webkit-transform 0.4s ease;
transition: left 0.4s ease, transform 0.4s ease;
transition: left 0.4s ease, transform 0.4s ease, -webkit-transform 0.4s ease, -moz-transform 0.4s ease;
}
#baguetteBox-slider.bounce-from-right {
-webkit-animation: bounceFromRight 0.4s ease-out;
animation: bounceFromRight 0.4s ease-out;
}
#baguetteBox-slider.bounce-from-left {
-webkit-animation: bounceFromLeft 0.4s ease-out;
animation: bounceFromLeft 0.4s ease-out;
}
@-webkit-keyframes bounceFromRight {
0% {
margin-left: 0;
}
50% {
margin-left: -30px;
}
100% {
margin-left: 0;
}
}
@keyframes bounceFromRight {
0% {
margin-left: 0;
}
50% {
margin-left: -30px;
}
100% {
margin-left: 0;
}
}
@-webkit-keyframes bounceFromLeft {
0% {
margin-left: 0;
}
50% {
margin-left: 30px;
}
100% {
margin-left: 0;
}
}
@keyframes bounceFromLeft {
0% {
margin-left: 0;
}
50% {
margin-left: 30px;
}
100% {
margin-left: 0;
}
}
.baguetteBox-button#previous-button, .baguetteBox-button#next-button {
top: 50%;
top: calc(50% - 30px);
width: 44px;
height: 60px;
}
.baguetteBox-button {
position: absolute;
cursor: pointer;
outline: none;
padding: 0;
margin: 0;
border: 0;
-moz-border-radius: 15%;
border-radius: 15%;
background-color: #323232;
background-color: rgba(50, 50, 50, 0.5);
color: #ddd;
font: 1.6em sans-serif;
-webkit-transition: background-color 0.4s ease;
transition: background-color 0.4s ease;
}
.baguetteBox-button:focus, .baguetteBox-button:hover {
background-color: rgba(50, 50, 50, 0.9);
}
.baguetteBox-button#next-button {
right: 2%;
}
.baguetteBox-button#previous-button {
left: 2%;
}
.baguetteBox-button#close-button {
top: 20px;
right: 2%;
right: calc(2% + 6px);
width: 30px;
height: 30px;
}
.baguetteBox-button svg {
position: absolute;
left: 0;
top: 0;
}
/*
Preloader
Borrowed from http://tobiasahlin.com/spinkit/
*/
.baguetteBox-spinner {
width: 40px;
height: 40px;
display: inline-block;
position: absolute;
top: 50%;
left: 50%;
margin-top: -20px;
margin-left: -20px;
}
.baguetteBox-double-bounce1,
.baguetteBox-double-bounce2 {
width: 100%;
height: 100%;
-moz-border-radius: 50%;
border-radius: 50%;
background-color: #fff;
opacity: 0.6;
position: absolute;
top: 0;
left: 0;
-webkit-animation: bounce 2s infinite ease-in-out;
animation: bounce 2s infinite ease-in-out;
}
.baguetteBox-double-bounce2 {
-webkit-animation-delay: -1s;
animation-delay: -1s;
}
@-webkit-keyframes bounce {
0%, 100% {
-webkit-transform: scale(0);
transform: scale(0);
}
50% {
-webkit-transform: scale(1);
transform: scale(1);
}
}
@keyframes bounce {
0%, 100% {
-webkit-transform: scale(0);
-moz-transform: scale(0);
transform: scale(0);
}
50% {
-webkit-transform: scale(1);
-moz-transform: scale(1);
transform: scale(1);
}
}

View File

@@ -1,4 +1,5 @@
@import "/home/zloy_linux/blog//themes/tabi/sass/main.scss";
@import "../themes/tabi/sass/main.scss";
@import "./baguetteBox.scss";
// main.scss
@@ -34,20 +35,12 @@ article {
}
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 4fr));
grid-auto-rows: auto;
grid-auto-flow: dense;
gap: 18px;
margin-top: 4vmin;
padding: 20px 0;
}
.gallery img {
border-radius: 1rem;
box-shadow: rgba(50, 50, 93, 0.25) 0px 2px 5px -1px, rgba(0, 0, 0, 0.3) 0px 1px 3px -1px;
transition: transform 200ms ease;
max-width: 100%;
height: auto;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
cursor: zoom-in;
}
.gallery img:hover {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 216 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 360 KiB

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 505 505" xml:space="preserve">
<circle style="fill:#54C0EB;" cx="252.5" cy="252.5" r="252.5"/>
<path style="fill:#324A5E;" d="M395.3,113.6H109.7c-10.8,0-19.5,8.7-19.5,19.5V372c0,10.8,8.7,19.5,19.5,19.5h285.6
c10.8,0,19.5-8.7,19.5-19.5V133C414.8,122.3,406.1,113.6,395.3,113.6z"/>
<g>
<path style="fill:#FFFFFF;" d="M389.3,312H115.7c-0.5,0-0.9-0.4-0.9-0.9V193.9c0-0.5,0.4-0.9,0.9-0.9h273.6c0.5,0,0.9,0.4,0.9,0.9
v117.3C390.2,311.6,389.8,312,389.3,312z"/>
<path style="fill:#FFFFFF;" d="M152.7,171h-36.9c-0.5,0-0.9-0.4-0.9-0.9v-31.9c0-0.5,0.4-0.9,0.9-0.9h36.9c0.5,0,0.9,0.4,0.9,0.9
v31.9C153.5,170.6,153.1,171,152.7,171z"/>
<path style="fill:#FFFFFF;" d="M231.5,171h-36.9c-0.5,0-0.9-0.4-0.9-0.9v-31.9c0-0.5,0.4-0.9,0.9-0.9h36.9c0.5,0,0.9,0.4,0.9,0.9
v31.9C232.4,170.6,232,171,231.5,171z"/>
<path style="fill:#FFFFFF;" d="M310.4,171h-36.9c-0.5,0-0.9-0.4-0.9-0.9v-31.9c0-0.5,0.4-0.9,0.9-0.9h36.9c0.5,0,0.9,0.4,0.9,0.9
v31.9C311.3,170.6,310.9,171,310.4,171z"/>
<path style="fill:#FFFFFF;" d="M389.3,171h-36.9c-0.5,0-0.9-0.4-0.9-0.9v-31.9c0-0.5,0.4-0.9,0.9-0.9h36.9c0.5,0,0.9,0.4,0.9,0.9
v31.9C390.2,170.6,389.8,171,389.3,171z"/>
<path style="fill:#FFFFFF;" d="M152.7,367.6h-36.9c-0.5,0-0.9-0.4-0.9-0.9v-31.9c0-0.5,0.4-0.9,0.9-0.9h36.9c0.5,0,0.9,0.4,0.9,0.9
v31.9C153.5,367.3,153.1,367.6,152.7,367.6z"/>
<path style="fill:#FFFFFF;" d="M231.5,367.6h-36.9c-0.5,0-0.9-0.4-0.9-0.9v-31.9c0-0.5,0.4-0.9,0.9-0.9h36.9c0.5,0,0.9,0.4,0.9,0.9
v31.9C232.4,367.3,232,367.6,231.5,367.6z"/>
<path style="fill:#FFFFFF;" d="M310.4,367.6h-36.9c-0.5,0-0.9-0.4-0.9-0.9v-31.9c0-0.5,0.4-0.9,0.9-0.9h36.9c0.5,0,0.9,0.4,0.9,0.9
v31.9C311.3,367.3,310.9,367.6,310.4,367.6z"/>
<path style="fill:#FFFFFF;" d="M389.3,367.6h-36.9c-0.5,0-0.9-0.4-0.9-0.9v-31.9c0-0.5,0.4-0.9,0.9-0.9h36.9c0.5,0,0.9,0.4,0.9,0.9
v31.9C390.2,367.3,389.8,367.6,389.3,367.6z"/>
</g>
<path style="fill:#FF7058;" d="M228.1,225.3v54.4c0,6.6,7.4,10.5,12.8,6.8l39.8-27.2c4.8-3.3,4.8-10.3,0-13.6l-39.8-27.2
C235.5,214.8,228.1,218.7,228.1,225.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg8"
version="1.1"
viewBox="0 0 92 92"
height="92mm"
width="92mm">
<defs
id="defs2" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="translate(-40.921303,-17.416526)"
id="layer1">
<circle
r="0"
style="fill:none;stroke:#000000;stroke-width:12;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
cy="92"
cx="75"
id="path3713" />
<circle
r="30"
cy="53.902557"
cx="75.921303"
id="path834"
style="fill:none;fill-opacity:1;stroke:#3050ff;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
d="m 67.514849,37.91524 a 18,18 0 0 1 21.051475,3.312407 18,18 0 0 1 3.137312,21.078282"
id="path852"
style="fill:none;fill-opacity:1;stroke:#3050ff;stroke-width:5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
transform="rotate(-46.234709)"
ry="1.8669105e-13"
y="122.08995"
x="3.7063529"
height="39.963303"
width="18.846331"
id="rect912"
style="opacity:1;fill:#3050ff;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512" xml:space="preserve">
<circle style="fill:#C2D5D8;" cx="256" cy="256" r="256"/>
<path style="fill:#406A80;" d="M220.428,126.024c29.388-9.24,131.336-20.624,166.144,21.992
c-37.188-2.208-73.928,7.048-88.812,32.544C276.272,170.788,220.428,126.024,220.428,126.024z"/>
<path style="fill:#395F73;" d="M284.676,173.172c5.012,3.152,9.544,5.78,13.084,7.388c12.252-20.972,39.28-30.936,69.208-32.588
C334.028,146.224,301.68,153.532,284.676,173.172z"/>
<path style="fill:#406A80;" d="M0,107.6c4.58,19.296,12.048,36.22,21.616,51.332c25.992-0.528,57.968,4.224,77.828,16.136
c-13.956,7.488-33.664,13.316-51.428,16.284c84.928,83.304,246.884,96.6,300.288,168.308l29.832-64.176
C243.936,46.028,75.684,76.38,0,107.6z"/>
<path style="fill:#395F73;" d="M370.892,311.084l7.252-15.592C270.392,95.168,140.664,75.288,54.552,91.396
C143.28,85.752,269.964,124.304,370.892,311.084z"/>
<path style="fill:#406A80;" d="M136.668,226.996c-22.052,7.348-38.216,37.724-40.664,57.312
c16.168-6.372,51.924-34.284,70.06-28.652C150.868,235.816,136.668,226.996,136.668,226.996z"/>
<path style="fill:#395F73;" d="M136.668,226.996c-3.812,1.272-7.432,3.268-10.852,5.748c5.552,4.616,13.464,12.064,21.776,22.916
c-12.14-3.772-32.204,7.52-48.984,17.292c-1.268,3.992-2.164,7.856-2.604,11.36c16.168-6.376,51.924-34.284,70.06-28.652
C150.868,235.816,136.668,226.996,136.668,226.996z"/>
<path style="fill:#F5F5F5;" d="M348.304,359.668l6.132-13.188c-13.084-48.716-217.768-119.484-238.768-161.364
c23.576-33.428-67.044-39.724-97.856-32.752c1.256,2.2,2.46,4.436,3.804,6.572c25.992-0.528,57.968,4.224,77.828,16.136
c-13.956,7.488-33.664,13.316-51.428,16.284C132.944,274.664,294.9,287.96,348.304,359.668z"/>
<path style="fill:#406A80;" d="M378.136,295.492c15.704-17.064,72.028-41.556,114.156-40.084
c-10.284,19.104-27.432,35.52-42.988,29.64c-13.72,12.252-46.964,32.064-55.592,58.824c-8.428,26.116-11.78,89.596-7.132,110.904
c-13.96-12.48-40.688-70.924-38.264-95.096S378.136,295.492,378.136,295.492z"/>
<g>
<path style="fill:#395F73;" d="M381.408,343.872c-6.5,20.136-9.96,62.404-9.084,90.5c5.136,9.312,10.232,16.796,14.252,20.404
c-4.64-21.296-1.288-84.776,7.132-110.904c8.5-26.368,40.852-45.964,54.928-58.252C444.752,286.58,390.032,317.1,381.408,343.872z"
/>
<path style="fill:#395F73;" d="M479.8,255.744c-7.472,13.752-18.488,25.992-29.872,29.484c15.388,5.228,32.216-10.96,42.364-29.82
C488.252,255.264,484.056,255.428,479.8,255.744z"/>
</g>
<path style="fill:#F5F5F5;" d="M121.548,137.78c-3.056,0-6.1-1.312-8.36-3.424c-2.212-2.252-3.396-5.324-3.396-8.404
c0-3.192,1.184-6.268,3.396-8.416c4.476-4.36,12.244-4.36,16.584,0c2.244,2.144,3.528,5.228,3.528,8.416
c0,3.08-1.284,6.152-3.528,8.272C127.66,136.468,124.592,137.78,121.548,137.78z"/>
<path style="fill:#242424;" d="M121.548,131.9c-1.528,0-3.056-0.648-4.176-1.708c-1.112-1.124-1.704-2.664-1.704-4.2
c0-1.6,0.588-3.14,1.704-4.216c2.24-2.184,6.116-2.184,8.292,0c1.116,1.076,1.756,2.608,1.756,4.216c0,1.536-0.64,3.076-1.756,4.132
C124.592,131.252,123.08,131.9,121.548,131.9z"/>
<path style="fill:#406A80;" d="M160.668,218.916c-9.888,36.744,35.252,98.212,28.404,118.784
c12.752-15.188,43.588-58.916,35.268-84.876C216.016,226.868,160.668,218.916,160.668,218.916z"/>
<path style="fill:#395F73;" d="M224.34,252.828c-8.324-25.96-63.672-33.916-63.672-33.916c-0.204,0.756-0.252,1.584-0.412,2.368
c16.748,3.924,45.912,13.252,51.768,31.548c6.34,19.756-10.024,49.796-23.824,69.728c1.6,6.156,2.12,11.36,0.86,15.148
C201.816,322.516,232.66,278.78,224.34,252.828z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -83,6 +83,9 @@ load_comments = "إظهار التعليقات"
copied = "تم النسخ!"
copy_code_to_clipboard = "نسخ الشِفرة إلى الحافظة"
# iine appreciation button.
like_this_post = "M'agrada aquesta publicació"
# Footer: Powered by Zola and tabi.
powered_by = "مُشَغل بواسطة"
and = "و"

View File

@@ -73,6 +73,9 @@ powered_by = "Propulsat per"
and = "i"
site_source = "Codi del lloc"
# iine appreciation button.
like_this_post = "M'agrada aquesta publicació"
# 404 error.
# https://welpo.github.io/tabi/404.html
page_missing = "La pàgina que has sol·licitat sembla que no existeix"

View File

@@ -72,6 +72,9 @@ load_comments = "Kommentare laden"
copied = "Kopiert!"
copy_code_to_clipboard = "Code in die Zwischenablage kopieren"
# iine appreciation button.
like_this_post = "Dieser Beitrag gefällt mir"
# Footer.
powered_by = "Angetrieben von"
and = "und"

View File

@@ -68,6 +68,9 @@ load_comments = "Load comments"
copied = "Copied!"
copy_code_to_clipboard = "Copy code to clipboard"
# iine appreciation button.
like_this_post = "Like this post"
# Footer: Powered by Zola and tabi.
powered_by = "Powered by"
and = "&"

View File

@@ -68,6 +68,9 @@ load_comments = "Cargar comentarios"
copied = "Copiado!"
copy_code_to_clipboard = "Copiar código al portapapeles"
# iine appreciation button.
like_this_post = "Me gusta esta publicación"
# Footer: Powered by Zola and tabi.
powered_by = "Impulsado por"
and = "y"

View File

@@ -68,6 +68,9 @@ load_comments = "Lae kommentaarid"
copied = "Kopeeritud!"
copy_code_to_clipboard = "Kopeeri kood lõikelauale"
# iine appreciation button.
like_this_post = "Mulle meeldib see postitus"
# Footer: Powered by Zola and tabi.
powered_by = "Toetab"
and = "ja"

View File

@@ -69,6 +69,9 @@ load_comments = "بارگذاری نظرات"
copied = "کپی شد!"
copy_code_to_clipboard = "کپی کد به کلیپ‌بورد"
# iine appreciation button.
like_this_post = "این مقاله را دوست دارم"
# Footer: Powered by Zola and tabi.
powered_by = "قدرت گرفته از"
and = "و"

View File

@@ -68,6 +68,9 @@ load_comments = "Afficher les commentaires"
copied = "Copié !"
copy_code_to_clipboard = "Copier le code dans le presse-papier"
# iine appreciation button.
like_this_post = "J'aime cet article"
# Footer: Powered by Zola and tabi.
powered_by = "Propulsé par"
and = "et"

View File

@@ -70,6 +70,9 @@ load_comments = "कमेंट्स लोड करें"
copied = "कॉपी किया गया!"
copy_code_to_clipboard = "कोड क्लिपबोर्ड में कॉपी करें"
# iine appreciation button.
like_this_post = "मुझे यह पोस्ट पसंद है"
# Footer: Powered by Zola and tabi.
powered_by = "चालित द्वारा"
and = "और"

View File

@@ -68,6 +68,9 @@ load_comments = "Carica commenti"
copied = "Copiato!"
copy_code_to_clipboard = "Copia codice negli appunti"
# iine appreciation button.
like_this_post = "Mi piace questo post"
# Footer: Powered by Zola and tabi.
powered_by = "Alimentato da"
and = "e"

View File

@@ -72,6 +72,9 @@ load_comments = "コメントを読む"
copied = "コピーしました!"
copy_code_to_clipboard = "コードをクリップボードにコピー"
# iine appreciation button.
like_this_post = "いいね!"
# Footer: Powered by Zola and tabi.
powered_by = "Powered by"
and = "と"

View File

@@ -72,6 +72,9 @@ load_comments = "댓글 불러오기"
copied = "복사됨!"
copy_code_to_clipboard = "코드를 클립보드에 복사"
# iine appreciation button.
like_this_post = "이 글이 좋아요"
# Footer: Powered by Zola and tabi.
powered_by = "제공됨"
and = "&"

View File

@@ -68,6 +68,9 @@ load_comments = "Laad opmerkingen"
copied = "Gekopieerd!"
copy_code_to_clipboard = "Kopieer code naar klembord"
# iine appreciation button.
like_this_post = "Vind ik leuk"
# Footer: Powered by Zola and tabi.
powered_by = "Aangedreven door"
and = "&"

View File

@@ -68,6 +68,9 @@ load_comments = "ମତାମତ ଲୋଡ କରନ୍ତୁ"
copied = "କପି ହେଲା!"
copy_code_to_clipboard = "କ୍ଲିପବୋର୍ଡକୁ କପି କରନ୍ତୁ"
# iine appreciation button.
like_this_post = "ମୋର ଏହି ପୋସ୍ଟ ଭଲ ଲାଗେ"
# Footer: Powered by Zola and tabi.
powered_by = "ଚାଳିତ ଦ୍ୱାରା"
and = "ଏବଂ"

View File

@@ -68,6 +68,9 @@ load_comments = "Carregar comentários"
copied = "Copiado!"
copy_code_to_clipboard = "Copiar código para a área de transferência"
# iine appreciation button.
like_this_post = "Gosto desta publicação"
# Footer: Powered by Zola and tabi.
powered_by = "Impulsionado por"
and = "e"

View File

@@ -30,6 +30,7 @@ few_results = "$NUMBER результата" # 2, 3, 4 but not 12-14
many_results = "$NUMBER результатов" # 5-9, 0, 11-14, and others
# Navigation.
skip_to_content = "Перейти к содержанию"
pinned = "Закреплено"
jump_to_posts = "Перейти к записям"
read_more = "Читать далее"
@@ -79,6 +80,9 @@ load_comments = "Загрузить комментарии"
copied = "Скопировано!"
copy_code_to_clipboard = "Скопировать код в буфер обмена"
# iine appreciation button.
like_this_post = "Мне нравится эта статья"
# Footer: Powered by Zola and tabi.
powered_by = "Под управлением"
and = "&"

View File

@@ -81,6 +81,9 @@ load_comments = "Завантажити коментарі"
copied = "Скопійовано!"
copy_code_to_clipboard = "Копіювати код у буфер обміну"
# iine appreciation button.
like_this_post = "Мені подобається ця стаття"
# Footer: Powered by Zola and tabi.
powered_by = "Під управлінням"
and = "та"

View File

@@ -68,6 +68,9 @@ load_comments = "载入留言"
copied = "已复制!" # Machine translated.
copy_code_to_clipboard = "复制代码到剪贴板" # Machine translated.
# iine appreciation button.
like_this_post = "喜欢这篇文章"
# Footer: Powered by Zola and tabi.
powered_by = "网站基于"
and = "和"

View File

@@ -68,6 +68,9 @@ load_comments = "載入留言"
copied = "已复制!" # Machine translated.
copy_code_to_clipboard = "复制代码到剪贴板" # Machine translated.
# iine appreciation button.
like_this_post = "喜歡這篇文章"
# Footer: Powered by Zola and tabi.
powered_by = "網站基於"
and = "和"

View File

@@ -8,6 +8,7 @@
@use 'parts/_header-anchor.scss';
@use 'parts/_header.scss';
@use 'parts/_home-banner.scss';
@use 'parts/_iine.scss';
@use 'parts/_image-hover.scss';
@use 'parts/_image-toggler.scss';
@use 'parts/_image.scss';
@@ -22,6 +23,7 @@
@use 'parts/_table.scss';
@use 'parts/_tags.scss';
@use 'parts/_theme-switch.scss';
@use 'parts/_webmention.scss';
@use 'parts/_zola-error.scss';
@font-face {
@@ -70,7 +72,7 @@
@else {
--background-color: #282828;
--bg-0: #2f2f2f;
--bg-1: #3c3c3c;
--bg-1: #1d2021;
--bg-2: #171717;
--bg-3: #535555;
--hover-color: black;
@@ -196,7 +198,7 @@ article {
}
.section-title {
display: block;
display: flex;
margin: 0;
margin-top: -0.15em;
color: var(--text-color-high-contrast);
@@ -230,6 +232,7 @@ h2 {
margin-top: 0.5em;
font-weight: 550;
font-size: 1.4rem;
color: #fabd2f;
}
h3 {

View File

@@ -53,7 +53,7 @@ footer nav {
.social > img {
aspect-ratio: 1/1;
width: 1.5rem;
width: 2.2rem;
height: auto;
color: #000000;
}

View File

@@ -98,12 +98,11 @@ header {
.tag {
margin-inline-end: 0;
}
}
.separator {
margin-inline-end: 0.2rem;
user-select: none;
}
}
.language-switcher {
display: flex;
@@ -125,6 +124,28 @@ header {
background: var(--meta-color);
}
}
.language-switcher-icon-with-code {
margin-inline-end: 0.3rem;
width: 0.7rem;
height: 0.7rem;
}
}
.language-switcher-icon-code {
position: absolute;
top: -0.15rem;
z-index: 10;
inset-inline-start: 0.7rem;
width: 100%;
height: 100%;
color: var(--text-color);
font-size: 0.5rem;
text-transform: uppercase;
&:hover {
color: var(--meta-color);
}
}
.dropdown {

View File

@@ -0,0 +1,37 @@
.iine-button {
display: inline-flex;
align-items: center;
gap: 5px;
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
cursor: pointer;
border: none;
background: transparent;
color: inherit;
font-family: var(--sans-serif-font);
-webkit-tap-highlight-color: transparent;
appearance: none;
margin: 0;
padding: 0;
font-size: inherit;
line-height: inherit;
.icon {
display: inline-flex;
align-items: center;
}
.counter {
margin-left: .2rem;
font-size: 0.8rem;
}
svg {
width: 1em;
height: 1em;
}
}
.iine-auto-buttons {
margin-top: 2rem;
padding: 1rem 0;
}

View File

@@ -48,6 +48,9 @@ ul {
.title-container {
padding-bottom: 8px;
.social {
margin-inline-start: 0.5rem;
}
}
.bottom-divider {
@@ -85,30 +88,21 @@ a {
// External link styles with `external_links_class = "external"`.
main {
--external-link-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='M11 5h-6v14h14v-6'/%3E%3Cpath d='M13 11l7 -7'/%3E%3Cpath d='M21 3h-6M21 3v6'/%3E%3C/g%3E%3C/svg%3E");
a.external:not(:has(img, svg, video, picture, figure)) {
display: inline-block;
padding-inline-end: 0.9em;
}
a.external:not(:has(img, svg, video, picture, figure))::after {
-webkit-mask-image: var(--external-link-icon);
-webkit-mask-size: 100% 100%;
display: inline-block;
position: absolute;
top: 50%;
transform: translateY(-50%);
mask-image: var(--external-link-icon);
mask-size: 100% 100%;
margin-inline-start: 0.2em;
inset-inline-end: 0;
vertical-align: -0.05em;
margin-inline-start: 0.1em;
background-color: currentColor;
width: 0.8em;
height: 0.8em;
content: '';
-webkit-mask-image: var(--external-link-icon);
-webkit-mask-size: 100% 100%;
}
&:dir(rtl) a.external:not(:has(img, svg, video, picture, figure))::after {
transform: translateY(-50%) rotate(-90deg);
transform: rotate(-90deg);
}
.meta a.external:not(:has(img, svg, video, picture, figure))::after {
@@ -334,3 +328,28 @@ details summary {
flex-direction: column;
}
}
#skip-link {
position: absolute;
top: -40px;
left: 0;
transform: translateY(-100%);
opacity: 0;
z-index: 9999;
transition: all 0.1s ease;
border-radius: 0 0 5px 0;
background-color: var(--primary-color);
padding: 4px 8px;
color: var(--hover-color);
font-weight: 500;
font-size: 0.9rem;
text-decoration: none;
}
#skip-link:focus {
top: 0;
transform: translateY(0);
opacity: 1;
outline: 2px solid var(--text-color);
outline-offset: 2px;
}

View File

@@ -126,7 +126,7 @@ $padding: 2.5rem;
@media only screen and (max-width: 1100px) {
.bloglist-container {
grid-template-columns: 1fr;
display: block;
}
.pinned-label svg {

View File

@@ -0,0 +1,149 @@
#webmentions {
position: relative;
z-index: 100;
margin: 0;
background-color: var(--background-color);
color: var(--text-color);
line-height: 1.2em;
h2 {
margin-bottom: 1.5em;
font-size: 1.1em;
}
h3 {
display: flex;
align-items: center;
font-size: 0.9em;
svg {
margin-inline-end: 0.2rem;
}
.svg-icon,
span {
margin-inline-end: .3rem;
}
}
ol {
padding: 0;
}
li,
p {
font-family: inherit;
}
.likes {
display: flex;
flex-wrap: wrap;
margin-top: 0.5rem;
padding: 0;
list-style: none;
li {
position: relative;
transition: transform 0.8s ease-out, z-index 0s linear 0.4s;
margin-bottom: .375rem;
margin-inline-start: -.75rem;
&:first-child {
margin-inline-start: 0;
}
&:hover {
transform: scale(1.3) translateY(-4px);
z-index: 10;
transition: transform 0.05s ease-out, z-index 0s linear 0s;
}
img {
display: block;
border: 2px solid var(--background-color, white);
border-radius: 50%;
aspect-ratio: 1/1;
width: 2.5rem;
height: 2.5rem;
object-fit: cover;
}
}
}
.comment {
margin-bottom: 1rem;
border-radius: 10px;
background: var(--bg-0);
padding: 1rem;
overflow: hidden;
font-size: 80%;
div {
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
align-items: center;
}
p {
margin-bottom: 0;
line-height: 1.5em;
}
.p-author {
font-style: bold;
font-size: 1.3em;
}
.u-url {
font-style: italic;
text-decoration: underline;
}
.u-author {
display: flex;
align-items: center;
img {
display: block;
margin-inline-end: .625rem;
width: 2rem;
max-width: 100%;
height: 2rem;
}
}
}
form {
input {
flex: 1;
border: 1px solid var(--divider-color);
border-radius: 20px 0px 0px 20px;
background-color: var(--input-background-color);
padding-inline: 1rem 1rem;
padding-block: .75rem;
width: calc(60% - 2rem);
color: var(--text-color);
font-size: 1rem;
}
button {
flex: 1;
border: 1px solid var(--divider-color);
border-radius: 0px 20px 20px 0px;
background-color: var(--input-background-color);
padding-inline: 0.7rem 0.7rem;
padding-block: .75rem;
width: 7rem;
color: var(--text-color);
font-size: 1rem;
}
button:hover {
cursor: pointer;
background-color: var(--primary-color);
color: var(--hover-color);
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,412 @@
/* webmention.js
Simple thing for embedding webmentions from webmention.io into a page, client-side.
(c)2018-2022 fluffy (http://beesbuzz.biz)
2025 mmai (https://misc.rhumbs.fr)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Basic usage:
<script src="/path/to/webmention.js" data-param="val" ... async />
<div id="webmentions"></div>
Allowed parameters:
page-url:
The base URL to use for this page. Defaults to window.location
add-urls:
Additional URLs to check, separated by |s
id:
The HTML ID for the object to fill in with the webmention data.
Defaults to "webmentions"
wordcount:
The maximum number of words to render in reply mentions.
max-webmentions:
The maximum number of mentions to retrieve. Defaults to 30.
prevent-spoofing:
By default, Webmentions render using the mf2 'url' element, which plays
nicely with webmention bridges (such as brid.gy and telegraph)
but allows certain spoofing attacks. If you would like to prevent
spoofing, set this to a non-empty string (e.g. "true").
sort-by:
What to order the responses by; defaults to 'published'. See
https://github.com/aaronpk/webmention.io#api
sort-dir:
The order to sort the responses by; defaults to 'up' (i.e. oldest
first). See https://github.com/aaronpk/webmention.io#api
comments-are-reactions:
If set to a non-empty string (e.g. "true"), will display comment-type responses
(replies/mentions/etc.) as being part of the reactions
(favorites/bookmarks/etc.) instead of in a separate comment list.
A more detailed example:
<!-- If you want to translate the UI -->
<script src="/path/to/umd/i18next.js"></script>
<script>
// Setup i18next as described in https://www.i18next.com/overview/getting-started#basic-sample
</script>
<!-- Otherwise, only using the following is fine -->
<script src="/path/to/webmention.min.js"
data-id="webmentionContainer"
data-wordcount="30"
data-prevent-spoofing="true"
data-comments-are-reactions="true"
/>
*/
// Begin LibreJS code licensing
// @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt
(function () {
"use strict";
// Shim i18next
window.i18next = window.i18next || {
t: function t(/** @type {string} */key) { return key; }
}
const t = window.i18next.t.bind(window.i18next);
/**
* Read the configuration value.
*
* @param {string} key The configuration key.
* @param {string} dfl The default value.
* @returns {string}
*/
function getCfg(key, dfl) {
return document.currentScript.getAttribute("data-" + key) || dfl;
}
const refurl = getCfg("page-url", window.location.href.replace(/#.*$/, ""));
const addurls = getCfg("add-urls", undefined);
const containerID = getCfg("id", "webmentions");
/** @type {Number} */
const textMaxWords = getCfg("wordcount");
const maxWebmentions = getCfg("max-webmentions", 30);
const mentionSource = getCfg("prevent-spoofing") ? "wm-source" : "url";
const sortBy = getCfg("sort-by", "published");
const sortDir = getCfg("sort-dir", "up");
/**
* Strip the protocol off a URL.
*
* @param {string} url The URL to strip protocol off.
* @returns {string}
*/
function stripurl(url) {
return url.substr(url.indexOf('//'));
}
/**
* Deduplicate multiple mentions from the same source URL.
*
* @param {Array<Reaction>} mentions Mentions of the source URL.
* @return {Array<Reaction>}
*/
function dedupe(mentions) {
/** @type {Array<Reaction>} */
const filtered = [];
/** @type {Record<string, boolean>} */
const seen = {};
mentions.forEach(function (r) {
// Strip off the protocol (i.e. treat http and https the same)
const source = stripurl(r.url);
if (!seen[source]) {
filtered.push(r);
seen[source] = true;
}
});
return filtered;
}
/**
* Format comments as HTML.
*
* @param {Array<Reaction>} comments The comments to format.
* @returns string
*/
function formatComments(type, comments) {
let html = `
<div class="webcomments">
<h3 id="replies-header">` + getIcon('comment') + `&nbsp;<span>` + comments.length + `</span> ` + type + `s </h3>
<ol aria-labelledby="replies-header" role="list">`;
comments.forEach(function (comment) {
let content = '';
if (comment.hasOwnProperty('content')) {
if (comment.content.hasOwnProperty('html')) {
content = comment.content.html;
} else if (comment.content.hasOwnProperty('text')) {
content = comment.content.text;
}
}
html += `
<li class="comment h-entry">
<div>
<a class="comment_author u-author"
href="`+ comment.author.url + `"
target="_blank"
title="`+ comment.author.name + `"
rel="noreferrer">
<img
src="`+ comment.author.photo + `"
alt=""
class="u-photo"
loading="lazy"
decoding="async"
width="48"
height="48"
>
<span class="p-author">`+ comment.author.name + `</span>
</a>`;
if (comment.published) {
const published = new Date(comment.published);
html += `
<time class="dt-published" datetime="`+ comment.published + `">` + published.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" }) + `</time>`;
}
html += `
</div>
<p class="e-entry">`+ content + `
<a class="u-url"
href="`+ comment.url + `"
target="_blank"
rel="noreferrer">
source
</a>
</p>
</li>
`;
});
html += `
</ol >
</div >
`;
return html;
}
/**
* @typedef {Object} Reaction
* @property {string} url
* @property {Object?} author
* @property {string?} author.name
* @property {string?} author.photo
* @property {Object?} content
* @property {string?} content.text
* @property {RSVPEmoji?} rsvp
* @property {MentionType?} wm-property
* @property {string?} wm-source
*/
function getIcon(name) {
if (name == 'like') {
return `<svg focusable = "false" width = "24" height = "24" viewBox = "0 0 192 192" xmlns = "http://www.w3.org/2000/svg" > <path d="M95.997 41.986l-.026-.035C85.746 28.36 68.428 21.423 51.165 24.881 30.138 29.094 15.004 47.558 15 69.003c0 24.413 14.906 47.964 39.486 70.086 8.43 7.586 17.437 14.468 26.444 20.533.728.49 1.444.967 2.148 1.43l1.39.909 1.355.872 1.317.835.645.403 1.259.78 1.194.726 1.032.619 1.38.807.418.236a6 6 0 005.864 0l1.138-.654 1.154-.684 1.118-.675.614-.376 1.26-.779a212 212 0 00.644-.403l1.317-.835 1.355-.872 1.39-.909c.704-.463 1.42-.94 2.148-1.43 9.007-6.065 18.015-12.947 26.444-20.533C162.094 116.967 177 93.416 177 69.004c-.004-21.446-15.138-39.91-36.165-44.123-17.07-3.42-34.174 3.323-44.43 16.568l-.408.537zm42.48-5.338c15.421 3.09 26.52 16.63 26.523 32.357 0 19.607-12.438 39.847-33.532 59.357l-1.316 1.205c-.22.201-.443.402-.666.603-7.977 7.18-16.548 13.727-25.118 19.498l-.745.5c-.74.494-1.466.973-2.177 1.437l-1.402.906-1.359.864-.662.416-1.292.8-.732.446-.73-.446-1.292-.8-.662-.416-1.36-.864-1.4-.906a235.406 235.406 0 01-2.923-1.937c-8.57-5.77-17.14-12.319-25.118-19.498l-.666-.603-1.316-1.205C39.438 108.852 27 88.612 27 69.004c.003-15.726 11.102-29.267 26.523-32.356 15.253-3.056 30.565 4.954 36.756 19.208l.204.478c2.084 4.878 9.009 4.85 11.053-.045 6.062-14.511 21.52-22.73 36.941-19.641z" fill="currentColor" /></svg> `;
} else if (name == 'repost') {
return `<svg focusable = "false" width = "24" height = "24" viewBox = "0 0 192 192" xmlns = "http://www.w3.org/2000/svg" > <path d="M18.472 146.335l-.075-.184a5.968 5.968 0 01-.216-.684l-.014-.056a5.643 5.643 0 01-.082-.397l-.013-.083a5.886 5.886 0 01-.072-.96V144c0-.157.006-.313.018-.467l.006-.075c.012-.132.028-.261.048-.39l.016-.095c.008-.05.017-.1.027-.149.005-.019.008-.038.012-.058.028-.133.06-.264.096-.393l.026-.088a5.86 5.86 0 01.482-1.159l.043-.077a5.642 5.642 0 01.31-.49l.015-.022.076-.104.044-.059a3.856 3.856 0 01.165-.208l.052-.061c.102-.12.21-.236.321-.348l18-18a6 6 0 018.661 8.303l-.175.183L38.484 138H120c23.196 0 42-18.804 42-42a6 6 0 0112 0c0 29.525-23.696 53.516-53.107 53.993L120 150H38.486l7.757 7.757a6 6 0 01.175 8.303l-.175.183a6 6 0 01-8.303.175l-.183-.175-18-18-.145-.151a6.036 6.036 0 01-.829-1.125l-.058-.105a4.08 4.08 0 01-.06-.114l-.04-.077a4.409 4.409 0 01-.139-.3l-.014-.036zM154.06 25.582l.183.175 18 18a6.036 6.036 0 01.974 1.276l.058.105c.02.035.038.07.056.105l.043.086a4.411 4.411 0 01.14.3l.014.036a5.965 5.965 0 01.291.868l.014.056c.032.13.059.263.082.397l.013.083a5.886 5.886 0 01.067.692v.014a6.11 6.11 0 01-.013.692l-.006.075a5.856 5.856 0 01-.048.39l-.016.095c-.008.05-.017.1-.027.149-.005.019-.008.038-.012.058-.028.133-.06.264-.096.393l-.026.088a5.86 5.86 0 01-.482 1.159l-.043.077-.052.09-.029.048a6.006 6.006 0 01-.32.478l-.044.059a3.857 3.857 0 01-.165.208l-.052.061a6.34 6.34 0 01-.176.197l-.145.15-18 18a6 6 0 01-8.661-8.302l.175-.183L153.514 54H72c-23.196 0-42 18.804-42 42a6 6 0 11-12 0c0-29.525 23.696-53.516 53.107-53.993L72 42h81.516l-7.759-7.757a6 6 0 01-.175-8.303l.175-.183a6 6 0 018.303-.175z" fill="currentColor" /></svg> `;
} else if (name == 'comment') {
return `<svg width = "24" height = "24" viewBox = "0 0 150 150" xmlns = "http://www.w3.org/2000/svg" > <path d="M75-.006a75 75 0 0174.997 74.31l.003.69c0 41.422-33.579 75-75 75H11.75c-6.49 0-11.75-5.26-11.75-11.75v-63.25a75 75 0 0175-75zm0 12a63 63 0 00-63 63v63h63c34.446 0 62.435-27.645 62.992-61.93l.008-1.041-.003-.633A63 63 0 0075 11.994zm21 72a6 6 0 01.225 11.996l-.225.004H51a6 6 0 01-.225-11.996l.225-.004h45zm0-24a6 6 0 01.225 11.996l-.225.004H51a6 6 0 01-.225-11.996l.225-.004h45z" fill="currentColor" /></svg> `;
} else if (name == 'bookmark') {
return `<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24" height="24" viewBox="0 0 24 24">
<path d="M 6.0097656 2 C 4.9143111 2 4.0097656 2.9025988 4.0097656 3.9980469 L 4 22 L 12 19 L 20 22 L 20 20.556641 L 20 4 C 20 2.9069372 19.093063 2 18 2 L 6.0097656 2 z M 6.0097656 4 L 18 4 L 18 19.113281 L 12 16.863281 L 6.0019531 19.113281 L 6.0097656 4 z" fill="currentColor"></path>
</svg>`
}
}
/**
* Formats a list of reactions as HTML.
*
* @param {Array<Reaction>} reacts List of reactions to format
* @returns string
*/
function formatReactions(type, reacts) {
let html = `
<div class="color--primary" >
<h3 id=`+ type + ` - header"> ` + getIcon(type) + `&nbsp; <span>` + reacts.length + `</span> ` + type + `s </h3>
<ol class="likes" role = "list" aria - labelledby="`+ type + `-header"> `;
reacts.forEach(function (react) {
html += `
<li class="h-card">
<a class="u-url"
href="`+ react.author.url + `
target="_blank"
rel = "noreferrer"
title = "`+ react.author.name + `" >
<img
alt=""
class="lazy mentions__image u-photo"
src="`+ react.author.photo + `"
loading="lazy"
decoding="async"
width="48"
height="48"
>
<span class="p-author visually-hidden" aria-hidden="true">{{ author }}</span>
</a>
</li>
`;
});
html += `
</ol >
</div >
`;
return html;
}
/**
* @typedef WebmentionResponse
* @type {Object}
* @property {Array<Reaction>} children
*/
/**
* Register event listener.
*/
window.addEventListener("load", async function () {
const container = document.getElementById(containerID);
if (!container) {
// no container, so do nothing
return;
}
const pages = [stripurl(refurl)];
if (!!addurls) {
addurls.split('|').forEach(function (url) {
pages.push(stripurl(url));
});
}
let apiURL = `https://webmention.io/api/mentions.jf2?per-page=${maxWebmentions}&sort-by=${sortBy}&sort-dir=${sortDir}`;
pages.forEach(function (path) {
apiURL += `&target[]=${encodeURIComponent('http:' + path)}&target[]=${encodeURIComponent('https:' + path)}`;
});
// apiURL = 'http://127.0.0.1:1111/test_webmentions.jf2';
/** @type {WebmentionResponse} */
let json = {};
try {
// const response = await window.fetch(apiURL);
const response = await window.fetch(apiURL);
if (response.status >= 200 && response.status < 300) {
json = await response.json();
} else {
console.error("Could not parse response");
new Error(response.statusText);
}
} catch (error) {
// Purposefully not escalate further, i.e. no UI update
console.error("Request failed", error);
}
/** @type {Array<Reaction>} */
let comments = [];
/** @type {Array<Reaction>} */
let mentions = [];
/** @type {Array<Reaction>} */
const bookmarks = [];
/** @type {Array<Reaction>} */
const likes = [];
/** @type {Array<Reaction>} */
const reposts = [];
/** @type {Array<Reaction>} */
const follows = [];
/** @type {Record<MentionType, Array<Reaction>>} */
const mapping = {
"in-reply-to": comments,
"like-of": likes,
"repost-of": reposts,
"bookmark-of": bookmarks,
"follow-of": follows,
"mention-of": mentions,
"rsvp": comments
};
json.children.forEach(function (child) {
// Map each mention into its respective container
const store = mapping[child['wm-property']];
if (store) {
store.push(child);
}
});
// format the comment-type things
let formattedMentions = '';
if (mentions.length > 0) {
formattedMentions = formatComments('mention', dedupe(mentions));
}
let formattedComments = '';
if (comments.length > 0) {
formattedComments = formatComments('comment', dedupe(comments));
}
// format likes
let likesStr = '';
if (likes.length > 0) {
likesStr = formatReactions('like', dedupe(likes));
}
// format reposts
let repostsStr = '';
if (reposts.length > 0) {
repostsStr = formatReactions('repost', dedupe(reposts));
}
// format bookmarks
let bookmarksStr = '';
if (bookmarks.length > 0) {
bookmarksStr = formatReactions('bookmark', dedupe(bookmarks));
}
container.innerHTML = `<div id="webmentions">${repostsStr}${likesStr}${bookmarksStr}${formattedComments}${formattedMentions}</div>`;
});
}());
// End-of-file marker for LibreJS
// @license-end

66
themes/tabi/static/js/webmention.min.js vendored Normal file
View File

@@ -0,0 +1,66 @@
(()=>{function t(t,e){return document.currentScript.getAttribute("data-"+t)||e}window.i18next=window.i18next||{t:function(t){return t}},window.i18next.t.bind(window.i18next);let m=t("page-url",window.location.href.replace(/#.*$/,"")),f=t("add-urls",void 0),e=t("id","webmentions"),g=(t("wordcount"),t("max-webmentions",30)),v=(t("prevent-spoofing"),t("sort-by","published")),b=t("sort-dir","up");function y(t){return t.substr(t.indexOf("//"))}function x(t){let l=[],a={};return t.forEach(function(t){var e=y(t.url);a[e]||(l.push(t),a[e]=!0)}),l}function L(t,e){let a=`
<div class="webcomments">
<h3 id="replies-header">`+o("comment")+"&nbsp;<span>"+e.length+"</span> "+t+`s </h3>
<ol aria-labelledby="replies-header" role="list">`;return e.forEach(function(t){let e="";var l;t.hasOwnProperty("content")&&(t.content.hasOwnProperty("html")?e=t.content.html:t.content.hasOwnProperty("text")&&(e=t.content.text)),a+=`
<li class="comment h-entry">
<div>
<a class="comment_author u-author"
href="`+t.author.url+`"
target="_blank"
title="`+t.author.name+`"
rel="noreferrer">
<img
src="`+t.author.photo+`"
alt=""
class="u-photo"
loading="lazy"
decoding="async"
width="48"
height="48"
>
<span class="p-author">`+t.author.name+`</span>
</a>`,t.published&&(l=new Date(t.published),a+=`
<time class="dt-published" datetime="`+t.published+'">'+l.toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"})+"</time>"),a+=`
</div>
<p class="e-entry">`+e+`
<a class="u-url"
href="`+t.url+`"
target="_blank"
rel="noreferrer">
source
</a>
</p>
</li>
`}),a+=`
</ol >
</div >
`}function o(t){return"like"==t?'<svg focusable = "false" width = "24" height = "24" viewBox = "0 0 192 192" xmlns = "http://www.w3.org/2000/svg" > <path d="M95.997 41.986l-.026-.035C85.746 28.36 68.428 21.423 51.165 24.881 30.138 29.094 15.004 47.558 15 69.003c0 24.413 14.906 47.964 39.486 70.086 8.43 7.586 17.437 14.468 26.444 20.533.728.49 1.444.967 2.148 1.43l1.39.909 1.355.872 1.317.835.645.403 1.259.78 1.194.726 1.032.619 1.38.807.418.236a6 6 0 005.864 0l1.138-.654 1.154-.684 1.118-.675.614-.376 1.26-.779a212 212 0 00.644-.403l1.317-.835 1.355-.872 1.39-.909c.704-.463 1.42-.94 2.148-1.43 9.007-6.065 18.015-12.947 26.444-20.533C162.094 116.967 177 93.416 177 69.004c-.004-21.446-15.138-39.91-36.165-44.123-17.07-3.42-34.174 3.323-44.43 16.568l-.408.537zm42.48-5.338c15.421 3.09 26.52 16.63 26.523 32.357 0 19.607-12.438 39.847-33.532 59.357l-1.316 1.205c-.22.201-.443.402-.666.603-7.977 7.18-16.548 13.727-25.118 19.498l-.745.5c-.74.494-1.466.973-2.177 1.437l-1.402.906-1.359.864-.662.416-1.292.8-.732.446-.73-.446-1.292-.8-.662-.416-1.36-.864-1.4-.906a235.406 235.406 0 01-2.923-1.937c-8.57-5.77-17.14-12.319-25.118-19.498l-.666-.603-1.316-1.205C39.438 108.852 27 88.612 27 69.004c.003-15.726 11.102-29.267 26.523-32.356 15.253-3.056 30.565 4.954 36.756 19.208l.204.478c2.084 4.878 9.009 4.85 11.053-.045 6.062-14.511 21.52-22.73 36.941-19.641z" fill="currentColor" /></svg> ':"repost"==t?'<svg focusable = "false" width = "24" height = "24" viewBox = "0 0 192 192" xmlns = "http://www.w3.org/2000/svg" > <path d="M18.472 146.335l-.075-.184a5.968 5.968 0 01-.216-.684l-.014-.056a5.643 5.643 0 01-.082-.397l-.013-.083a5.886 5.886 0 01-.072-.96V144c0-.157.006-.313.018-.467l.006-.075c.012-.132.028-.261.048-.39l.016-.095c.008-.05.017-.1.027-.149.005-.019.008-.038.012-.058.028-.133.06-.264.096-.393l.026-.088a5.86 5.86 0 01.482-1.159l.043-.077a5.642 5.642 0 01.31-.49l.015-.022.076-.104.044-.059a3.856 3.856 0 01.165-.208l.052-.061c.102-.12.21-.236.321-.348l18-18a6 6 0 018.661 8.303l-.175.183L38.484 138H120c23.196 0 42-18.804 42-42a6 6 0 0112 0c0 29.525-23.696 53.516-53.107 53.993L120 150H38.486l7.757 7.757a6 6 0 01.175 8.303l-.175.183a6 6 0 01-8.303.175l-.183-.175-18-18-.145-.151a6.036 6.036 0 01-.829-1.125l-.058-.105a4.08 4.08 0 01-.06-.114l-.04-.077a4.409 4.409 0 01-.139-.3l-.014-.036zM154.06 25.582l.183.175 18 18a6.036 6.036 0 01.974 1.276l.058.105c.02.035.038.07.056.105l.043.086a4.411 4.411 0 01.14.3l.014.036a5.965 5.965 0 01.291.868l.014.056c.032.13.059.263.082.397l.013.083a5.886 5.886 0 01.067.692v.014a6.11 6.11 0 01-.013.692l-.006.075a5.856 5.856 0 01-.048.39l-.016.095c-.008.05-.017.1-.027.149-.005.019-.008.038-.012.058-.028.133-.06.264-.096.393l-.026.088a5.86 5.86 0 01-.482 1.159l-.043.077-.052.09-.029.048a6.006 6.006 0 01-.32.478l-.044.059a3.857 3.857 0 01-.165.208l-.052.061a6.34 6.34 0 01-.176.197l-.145.15-18 18a6 6 0 01-8.661-8.302l.175-.183L153.514 54H72c-23.196 0-42 18.804-42 42a6 6 0 11-12 0c0-29.525 23.696-53.516 53.107-53.993L72 42h81.516l-7.759-7.757a6 6 0 01-.175-8.303l.175-.183a6 6 0 018.303-.175z" fill="currentColor" /></svg> ':"comment"==t?'<svg width = "24" height = "24" viewBox = "0 0 150 150" xmlns = "http://www.w3.org/2000/svg" > <path d="M75-.006a75 75 0 0174.997 74.31l.003.69c0 41.422-33.579 75-75 75H11.75c-6.49 0-11.75-5.26-11.75-11.75v-63.25a75 75 0 0175-75zm0 12a63 63 0 00-63 63v63h63c34.446 0 62.435-27.645 62.992-61.93l.008-1.041-.003-.633A63 63 0 0075 11.994zm21 72a6 6 0 01.225 11.996l-.225.004H51a6 6 0 01-.225-11.996l.225-.004h45zm0-24a6 6 0 01.225 11.996l-.225.004H51a6 6 0 01-.225-11.996l.225-.004h45z" fill="currentColor" /></svg> ':"bookmark"==t?`<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24" height="24" viewBox="0 0 24 24">
<path d="M 6.0097656 2 C 4.9143111 2 4.0097656 2.9025988 4.0097656 3.9980469 L 4 22 L 12 19 L 20 22 L 20 20.556641 L 20 4 C 20 2.9069372 19.093063 2 18 2 L 6.0097656 2 z M 6.0097656 4 L 18 4 L 18 19.113281 L 12 16.863281 L 6.0019531 19.113281 L 6.0097656 4 z" fill="currentColor"></path>
</svg>`:void 0}function k(t,e){let l=`
<div class="color--primary" >
<h3 id=`+t+' - header"> '+o(t)+"&nbsp; <span>"+e.length+"</span> "+t+`s </h3>
<ol class="likes" role = "list" aria - labelledby="`+t+'-header"> ';return e.forEach(function(t){l+=`
<li class="h-card">
<a class="u-url"
href="`+t.author.url+`
target="_blank"
rel = "noreferrer"
title = "`+t.author.name+`" >
<img
alt=""
class="lazy mentions__image u-photo"
src="`+t.author.photo+`"
loading="lazy"
decoding="async"
width="48"
height="48"
>
<span class="p-author visually-hidden" aria-hidden="true">{{ author }}</span>
</a>
</li>
`}),l+=`
</ol >
</div >
`}window.addEventListener("load",async function(){var c=document.getElementById(e);if(c){let e=[y(m)],l=(f&&f.split("|").forEach(function(t){e.push(y(t))}),`https://webmention.io/api/mentions.jf2?per-page=${g}&sort-by=${v}&sort-dir=`+b),t=(e.forEach(function(t){l+=`&target[]=${encodeURIComponent("http:"+t)}&target[]=`+encodeURIComponent("https:"+t)}),{});try{200<=(h=await window.fetch(l)).status&&h.status<300?t=await h.json():(console.error("Could not parse response"),new Error(h.statusText))}catch(t){console.error("Request failed",t)}var h,d=[],u=[],p=[],w=[];let a={"in-reply-to":h=[],"like-of":p,"repost-of":w,"bookmark-of":u,"follow-of":[],"mention-of":d,rsvp:h},o=(t.children.forEach(function(t){var e=a[t["wm-property"]];e&&e.push(t)}),""),n=(0<d.length&&(o=L("mention",x(d))),""),r=(0<h.length&&(n=L("comment",x(h))),""),i=(0<p.length&&(r=k("like",x(p))),""),s=(0<w.length&&(i=k("repost",x(w))),"");0<u.length&&(s=k("bookmark",x(u))),c.innerHTML=`<div id="webmentions">${i}${r}${s}${n}${o}</div>`}})})();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -7,6 +7,18 @@
{# Set locale for date #}
{% set date_locale = macros_translate::translate(key="date_locale", default="en_GB", language_strings=language_strings) %}
{#- Check for language-specific date formats -#}
{%- set language_format = "" -%}
{%- if config.extra.date_formats -%}
{%- for format_config in config.extra.date_formats -%}
{%- if format_config.lang == lang -%}
{%- if format_config.archive -%}
{%- set_global language_format = format_config.archive -%}
{%- endif -%}
{%- endif -%}
{%- endfor -%}
{%- endif -%}
<div class="archive">
<ul class="list-with-title">
{%- set source_paths = section.extra.section_path | default(value="blog/") -%}
@@ -55,7 +67,13 @@
<li class="listing-item">
<div class="post-time">
<span class="date">
{%- if language_format -%}
{{ post.date | date(format=language_format, locale=date_locale) }}
{%- elif config.extra.archive_date_format -%}
{{ post.date | date(format=config.extra.archive_date_format, locale=date_locale) }}
{%- else -%}
{{ post.date | date(format="%d %b", locale=date_locale) }}
{%- endif -%}
</span>
</div>
<a href="{{ post.permalink }}" title="{{ post.title }}">{{ post.title | markdown(inline=true) | safe }}</a>

View File

@@ -66,7 +66,9 @@
{%- endif -%}
" rel="alternate" type="text/html"/>
<generator uri="https://www.getzola.org/">Zola</generator>
{%- if last_updated -%}
<updated>{{ last_updated | date(format="%+") }}</updated>
{%- endif -%}
<id>{{ feed_url | safe }}</id>
{%- for page in pages %}
{%- if macros_settings::evaluate_setting_priority(setting="hide_from_feed", page=page, default_global_value=false) == "true" -%}
@@ -98,10 +100,10 @@
{% if config.extra.full_content_in_feed %}
<content type="html">{{ page.content }}</content>
{% endif -%}
{% if page.summary -%}
<summary type="html">{{ page.summary | striptags | trim_end_matches(pat=".") | safe }}…</summary>
{% elif page.description -%}
{% if page.description -%}
<summary type="html">{{ page.description }}</summary>
{% elif page.summary -%}
<summary type="html">{{ page.summary | striptags | trim_end_matches(pat=".") | safe }}…</summary>
{% endif -%}
</entry>
{%- endfor %}

View File

@@ -1,11 +1,13 @@
{% import "macros/feed_utils.html" as feed_utils %}
{% import "macros/format_date.html" as macros_format_date %}
{% import "macros/list_posts.html" as macros_list_posts %}
{% import "macros/page_header.html" as macros_page_header %}
{% import "macros/rel_attributes.html" as macros_rel_attributes %}
{% import "macros/series_page.html" as macros_series_page %}
{% import "macros/settings.html" as macros_settings %}
{% import "macros/table_of_contents.html" as macros_toc %}
{% import "macros/target_attribute.html" as macros_target_attribute %}
{% import "macros/translate.html" as macros_translate %}
{% import "macros/series_page.html" as macros_series_page %}
{# Load the internationalisation data for the current language from
the .toml files in the user's '/i18n' folder, falling back to the theme's.
@@ -32,8 +34,9 @@ This variable will hold all the text strings for the language #}
{% include "partials/header.html" %}
<body{% if lang in rtl_languages %} dir="rtl"{% endif %}{% if config.extra.override_serif_with_sans %} class="use-sans-serif"{% endif %}>
<a href="#main-content" id="skip-link">{{ macros_translate::translate(key="skip_to_content", default="Skip to content", language_strings=language_strings) }}</a>
{% include "partials/nav.html" %}
<div class="content">
<div class="content" id="main-content">
{# Post page is the default #}
{% block main_content %}
@@ -41,6 +44,18 @@ This variable will hold all the text strings for the language #}
{% endblock main_content %}
</div>
{% include "partials/footer.html" %}
{# Users can optionally provide this template to add content to the body element. #}
{% include "tabi/extend_body.html" ignore missing %}
<!-- baguetteBox JS -->
<script defer src="{{ get_url(path='js/baguetteBox.min.js', trailing_slash=false) | safe }}"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
baguetteBox.run('.gallery');
});
</script>
</body>
</html>

View File

@@ -0,0 +1,17 @@
{#- Feed utility macros -#}
{#- Zola 0.19.0 uses `generate_feeds`. Prior versions use `generate_feed` -#}
{%- macro get_generate_feed() -%}
{{- config.generate_feeds | default(value=config.generate_feed) -}}
{%- endmacro get_generate_feed -%}
{%- macro get_feed_url() -%}
{{- config.feed_filenames[0] | default(value=(config.feed_filename)) -}}
{%- endmacro get_feed_url -%}
{#- Check footer feed icon conditions -#}
{%- macro should_show_footer_feed_icon() -%}
{%- set generate_feed = feed_utils::get_generate_feed() == "true" -%}
{%- set feed_url = feed_utils::get_feed_url() -%}
{{- generate_feed and config.extra.feed_icon and feed_url -}}
{%- endmacro should_show_footer_feed_icon -%}

View File

@@ -3,7 +3,23 @@
{#- Set locale -#}
{%- set date_locale = macros_translate::translate(key="date_locale", default="en_GB", language_strings=language_strings) -%}
{%- if config.extra.short_date_format and short -%}
{#- Check for language-specific date formats -#}
{%- set language_format = "" -%}
{%- if config.extra.date_formats -%}
{%- for format_config in config.extra.date_formats -%}
{%- if format_config.lang == lang -%}
{%- if short and format_config.short -%}
{%- set_global language_format = format_config.short -%}
{%- elif not short and format_config.long -%}
{%- set_global language_format = format_config.long -%}
{%- endif -%}
{%- endif -%}
{%- endfor -%}
{%- endif -%}
{%- if language_format -%}
{{ date | date(format=language_format, locale=date_locale) }}
{%- elif config.extra.short_date_format and short -%}
{{ date | date(format=config.extra.short_date_format, locale=date_locale) }}
{%- elif config.extra.long_date_format and not short -%}
{{ date | date(format=config.extra.long_date_format, locale=date_locale) }}

View File

@@ -83,7 +83,7 @@
<li class="date">{{- macros_format_date::format_date(date=post.date, short=false, language_strings=language_strings) -}}</li>
{%- endif -%}
{%- if show_date and show_updated -%}
<li class="mobile-only">{{- separator -}}</li>
<li class="mobile-only separator">{{- separator -}}</li>
{%- endif -%}
{%- if show_updated -%}
{%- set last_updated_str = macros_translate::translate(key="last_updated_on", default="Updated on $DATE", language_strings=language_strings) -%}
@@ -142,9 +142,9 @@
<div class="description">
{% if post.description %}
<p>{{ post.description }}</p>
<p>{{ post.description | markdown(inline=true) | safe }}</p>
{% elif post.summary %}
<p>{{ post.summary | striptags | trim_end_matches(pat=".") | safe }}…</p>
<p>{{ post.summary | markdown(inline=true) | trim_end_matches(pat=".") | safe }}…</p>
{% endif %}
</div>
<a class="readmore" href="{{ post.permalink }}">{{ macros_translate::translate(key="read_more", default="Read more", language_strings=language_strings) }}&nbsp;<span class="arrow"></span></a>

View File

@@ -1,5 +1,18 @@
{% macro page_header(title) %}
{% macro page_header(title, show_feed_icon=false) %}
{% set rel_attributes = macros_rel_attributes::rel_attributes() | trim %}
{%- set blank_target = macros_target_attribute::target_attribute(new_tab=config.markdown.external_links_target_blank) -%}
<h1 class="title-container section-title bottom-divider">
{{ title }}
{{ title -}}
{% if show_feed_icon %}
{%- set feed_url = feed_utils::get_feed_url() -%}
<a class="no-hover-padding social" rel="{{ rel_attributes }}" {{ blank_target }} href="{{ get_url(path=term.path ~ feed_url, lang=lang, trailing_slash=false) | safe }}">
<img loading="lazy" alt="feed" title="feed" src="{{ get_url(path='/social_icons/rss.svg') }}">
</a>
{% endif %}
</h1>
{% endmacro page_header %}

View File

@@ -0,0 +1,11 @@
{% macro target_attribute(new_tab) %}
{%- set blank_target = "" -%}
{%- if new_tab -%}
{%- set blank_target = "target=_blank" -%}
{%- endif -%}
{{ blank_target }}
{% endmacro target_attribute %}

View File

@@ -5,11 +5,7 @@
{%- set rel_attributes = macros_rel_attributes::rel_attributes() | trim -%}
{%- if config.markdown.external_links_target_blank -%}
{%- set blank_target = "target=_blank" -%}
{%- else -%}
{%- set blank_target = "" -%}
{%- endif -%}
{%- set blank_target = macros_target_attribute::target_attribute(new_tab=config.markdown.external_links_target_blank) -%}
{# Debugging #}
{# <div><pre>
@@ -61,6 +57,8 @@ Current section extra: {% if current_section %}{{ current_section.extra | json_e
</pre></div>
{% set settings_to_test = [
"iine",
"iine_icon",
"enable_cards_tag_filtering",
"footnote_backlinks",
"add_src_to_code_block",
@@ -102,14 +100,16 @@ Current section extra: {% if current_section %}{{ current_section.extra | json_e
</tbody>
</table>
</div> #}
{# {{ __tera_context }} #}
{# End debugging #}
<main>
<article>
<h1 class="article-title">
<article class="h-entry">
<h1 class="p-name article-title">
{{ page.title | markdown(inline=true) | safe }}
</h1>
<a class="u-url u-uid" href="{{ page.permalink | safe }}"></a>
<ul class="meta">
{#- Draft indicator -#}
@@ -118,7 +118,7 @@ Current section extra: {% if current_section %}{{ current_section.extra | json_e
{% endif %}
{#- Author(s) -#}
{% if page.authors or config.author and macros_settings::evaluate_setting_priority(setting="show_author", page=page, default_global_value=false) == "true" %}
{%- if page.authors or config.author and macros_settings::evaluate_setting_priority(setting="show_author", page=page, default_global_value=false) == "true" -%}
{%- if page.authors -%}
{%- set author_list = page.authors -%}
{%- else -%}
@@ -126,29 +126,36 @@ Current section extra: {% if current_section %}{{ current_section.extra | json_e
{%- endif -%}
{%- if author_list | length == 1 -%}
{%- set author_string = author_list.0 -%}
{%- set author_string = '<span class="p-author">' ~ author_list.0 ~ '</span>' -%}
{%- else -%}
{%- set last_author = author_list | last -%}
{%- set other_authors = author_list | slice(end=-1) -%}
{%- set author_separator = macros_translate::translate(key="author_separator", default=", ", language_strings=language_strings) -%}
{%- set author_separator = '</span>' ~ author_separator ~ '<span class="p-author">' -%}
{%- set conjunction = macros_translate::translate(key="author_conjunction", default=" and ", language_strings=language_strings) -%}
{%- set conjunction = '</span>' ~ conjunction ~ '<span class="p-author">' -%}
{%- set author_string = other_authors | join(sep=author_separator) -%}
{%- set author_string = author_string ~ conjunction ~ last_author -%}
{%- set author_string = '<span class="p-author">' ~ author_string ~ '</span>' -%}
{%- endif -%}
{%- set by_author = macros_translate::translate(key="by_author", default="By $AUTHOR", language_strings=language_strings) -%}
<li>{{ by_author | replace(from="$AUTHOR", to=author_string) }}</li>
<li>{{ by_author | replace(from="$AUTHOR", to=author_string) | safe }}</li>
{%- set previous_visible = true -%}
{% endif %}
{%- endif -%}
{%- if config.extra.hcard and config.extra.hcard.enable and ( not author_list or author_list is containing(config.author)) -%}
{% include "partials/hcard_small.html" %}
{%- endif -%}
{%- set separator_with_class = "<span class='separator' aria-hidden='true'>" ~ separator ~ "</span>"-%}
{#- Date -#}
{% if page.date and macros_settings::evaluate_setting_priority(setting="show_date", page=page, default_global_value=true) == "true" %}
<li>{%- if previous_visible -%}{{ separator_with_class | safe }}{%- endif -%}{{ macros_format_date::format_date(date=page.date, short=true, language_strings=language_strings) }}</li>
{%- if page.date and macros_settings::evaluate_setting_priority(setting="show_date", page=page, default_global_value=true) == "true" -%}
<li><time class="dt-published" datetime="{{ page.date }}">{%- if previous_visible -%}{{ separator_with_class | safe }}{%- endif -%}{{ macros_format_date::format_date(date=page.date, short=true, language_strings=language_strings) }}</time></li>
{#- Variable to keep track of whether we've shown a section, to avoid separators as the first element -#}
{%- set previous_visible = true -%}
{% endif %}
{%- endif -%}
{#- Reading time -#}
{%- if macros_settings::evaluate_setting_priority(setting="show_reading_time", page=page, default_global_value=true) == "true" -%}
@@ -160,7 +167,7 @@ Current section extra: {% if current_section %}{{ current_section.extra | json_e
{%- if page.taxonomies and page.taxonomies.tags -%}
<li class="tag">{%- if previous_visible -%}{{ separator_with_class | safe }}{%- endif -%}{{- macros_translate::translate(key="tags", default="tags", language_strings=language_strings) | capitalize -}}:&nbsp;</li>
{%- for tag in page.taxonomies.tags -%}
<li class="tag"><a href="{{ get_taxonomy_url(kind='tags', name=tag, lang=lang) | safe }}">{{ tag }}</a>
<li class="tag"><a class="p-category" href="{{ get_taxonomy_url(kind='tags', name=tag, lang=lang) | safe }}">{{ tag }}</a>
{%- if not loop.last -%}
,&nbsp;
{%- endif -%}
@@ -175,7 +182,7 @@ Current section extra: {% if current_section %}{{ current_section.extra | json_e
{%- set formatted_date = macros_format_date::format_date(date=page.updated, short=true, language_strings=language_strings) -%}
{%- set updated_str = last_updated_str | replace(from="$DATE", to=formatted_date) -%}
{%- set previous_visible = true -%}
</ul><ul class="meta last-updated"><li>{{ updated_str }}</li>
</ul><ul class="meta last-updated"><li><time class="dt-updated" datetime="{{ page.updated }}">{{ updated_str }}</time></li>
{#- Show link to remote changes if enabled -#}
{%- if config.extra.remote_repository_url and macros_settings::evaluate_setting_priority(setting="show_remote_changes", page=page, default_global_value=true) == "true" -%}
<li>{%- if previous_visible -%}{{ separator_with_class | safe }}{%- endif -%}<a class="external" href="{% include "partials/history_url.html" %}" {{ blank_target }} rel="{{ rel_attributes }}">{{ macros_translate::translate(key="see_changes", default="See changes", language_strings=language_strings) }}</a></li>
@@ -227,7 +234,12 @@ Current section extra: {% if current_section %}{{ current_section.extra | json_e
{{ macros_toc::toc(page=page, header=true, language_strings=language_strings) }}
{% endif %}
<section class="body">
{#- Optional Summary paragraph for readers -#}
{% if page.description %}
<p class="p-summary" hidden>{{ page.description }}</p>
{%- endif -%}
<section class="e-content body">
{#- Replace series_intro placeholder -#}
{%- set content_with_intro = page.content -%}
{%- if "<!-- series_intro -->" in page.content -%}
@@ -263,6 +275,11 @@ Current section extra: {% if current_section %}{{ current_section.extra | json_e
{{ processed_content | replace(from="<!-- toc -->", to=macros_toc::toc(page=page, header=false, language_strings=language_strings)) | safe }}
</section>
{#- iine button -#}
{%- if macros_settings::evaluate_setting_priority(setting="iine", page=page, default_global_value=false) == "true" -%}
{% include "partials/iine_button.html" %}
{%- endif -%}
{% if macros_settings::evaluate_setting_priority(setting="show_previous_next_article_links", page=page, default_global_value=true) == "true" %}
{%- if page.lower or page.higher -%}
{% set next_label = macros_translate::translate(key="next", default="Next", language_strings=language_strings) %}
@@ -333,6 +350,13 @@ Current section extra: {% if current_section %}{{ current_section.extra | json_e
{% if comment_system %}
{% include "partials/comments.html" %}
{% endif %}
{#- Webmentions -#}
{%- set global_webmentions_enabled = config.extra.webmentions.enable | default(value=false) -%}
{%- set page_webmentions_enabled = page.extra.webmentions | default(value=global_webmentions_enabled) -%}
{%- set webmentions_enabled = global_webmentions_enabled and page_webmentions_enabled != false or page_webmentions_enabled == true -%}
{%- if webmentions_enabled -%}
{%- include "partials/webmentions.html" -%}
{%- endif -%}
</article>
</main>

View File

@@ -25,7 +25,7 @@
data-website-id="{{ analytics_id }}"
src="https://cloud.umami.is/script.js"
{% endif %}
data-do-not-track="true">
{% if config.extra.analytics.do_not_track %}data-do-not-track="true"{% endif %}>
</script>
{% elif analytics_service == "plausible" %}

View File

@@ -7,11 +7,7 @@
{% break %}
{% endif %}
{# Determine which URL to use, default is page.permalink #}
{%- if page.extra.link_to and config.markdown.external_links_target_blank -%}
{%- set blank_target = "target=_blank" -%}
{%- else -%}
{%- set blank_target = "" -%}
{%- endif -%}
{%- set blank_target = macros_target_attribute::target_attribute(new_tab=config.markdown.external_links_target_blank and page.extra.link_to) -%}
{% set target_url = page.extra.link_to | default(value=page.permalink) %}

View File

@@ -7,6 +7,9 @@ content="default-src 'self'
{%- set giscus_enabled = config.extra.giscus.enabled_for_all_posts or page.extra.giscus -%}
{%- set hyvortalk_enabled = config.extra.hyvortalk.enabled_for_all_posts or page.extra.hyvortalk -%}
{%- set isso_enabled = config.extra.isso.enabled_for_all_posts or page.extra.isso -%}
{%- if page -%}
{%- set iine_enabled = macros_settings::evaluate_setting_priority(setting="iine", page=page, default_global_value=false) == "true" -%}
{%- endif -%}
{%- if page -%}
{%- set mermaid_enabled = macros_settings::evaluate_setting_priority(setting="mermaid", page=page, default_global_value=false) == "true" -%}
{%- endif -%}
@@ -50,10 +53,21 @@ content="default-src 'self'
{%- set script_src = script_src ~ " " ~ " utteranc.es" -%}
{%- endif -%}
{%- if mermaid_enabled and not serve_local_mermaid -%}
{%- if (mermaid_enabled and not serve_local_mermaid) or iine_enabled -%}
{%- set script_src = script_src ~ " " ~ " cdn.jsdelivr.net" -%}
{%- endif -%}
{#- Check if a webmention system is enabled to allow the necessary domains and directives -#}
{%- if config.extra.webmentions.enable -%}
{%- set connect_src = connect_src ~ " webmention.io" -%}
{%- endif -%}
{#- Check if iine like buttons are enabled to allow the necessary domains -#}
{%- if iine_enabled -%}
{%- set connect_src = connect_src ~ " vhiweeypifbwacashxjz.supabase.co" -%}
{%- endif -%}
{#- Append WebSocket for Zola serve mode -#}
{%- if config.mode == "serve" -%}
{%- set connect_src = connect_src ~ " ws:" -%}
@@ -61,19 +75,19 @@ content="default-src 'self'
{%- for domain in config.extra.allowed_domains -%}
{%- if domain.directive == "connect-src" -%}
{%- set configured_connect_src = domain.domains | join(sep=' ') -%}
{%- set configured_connect_src = domain.domains | join(sep=' ') | safe -%}
{%- set_global connect_src = connect_src ~ " " ~ configured_connect_src -%}
{%- continue -%}
{%- endif -%}
{%- if domain.directive == "script-src" -%}
{%- set configured_script_src = domain.domains | join(sep=' ') -%}
{%- set configured_script_src = domain.domains | join(sep=' ') | safe -%}
{%- set_global script_src = script_src ~ " " ~ configured_script_src -%}
{%- continue -%}
{%- endif -%}
{#- Handle directives that are not connect-src -#}
{{ domain.directive }} {{ domain.domains | join(sep=' ') -}}
{{ domain.directive }} {{ domain.domains | join(sep=' ') | safe -}}
{%- if domain.directive == "style-src" -%}
{%- if utterances_enabled or hyvortalk_enabled or mermaid_enabled %} 'unsafe-inline'

Some files were not shown because too many files have changed in this diff Show More