feat(link): add external link guard feature support (#729)

* feat(external-link-guard): add native redirect mode and improve external-link guard UX

* fix(external-link-guard): remove redirect_url and clarify scope/new_tab_policy docs

* i18n(external-link-guard): localize guard texts across non-English locales

* feat(config)!: migrate `site.Params.externalIcon` to `site.Params.link.external_icon`

* refactor: implement regular and card link rendering

* feat(link-guard): implement link redirection page with copy and confirm functionality

* revert

* feat: implement link guard dialog for external links

---------

Co-authored-by: Cell <1024@lruihao.cn>
This commit is contained in:
0x5c0f
2026-04-11 14:25:17 +08:00
committed by Cell
parent d45f6b1550
commit 75ee1624bd
43 changed files with 786 additions and 98 deletions
+88
View File
@@ -0,0 +1,88 @@
---
title: Link Rendering Test
date: 2026-04-10T11:40:06+08:00
collections:
- Tests
categories:
- Markdown
tags:
- Link
---
This post validates link rendering paths in FixIt, including Markdown render hook and link shortcode modes.
<!--more-->
## Markdown Render Hook (Regular Link)
These links are rendered by `layouts/_markup/render-link.html` and should always produce regular links.
- Internal relative link: [Task list test](/test/units/task-list-test/)
- Internal anchor link: [Jump to Card Link section](#card-link-shortcode)
- Absolute same-site link: [Test site home](http://demo.fixit.lruihao.cn/test/)
- External HTTP link: [FixIt Repository](https://github.com/hugo-fixit/FixIt)
- Mail link: [Email test](mailto:test@example.com)
- JavaScript link: [JavaScript test](javascript:void(0))
- Tel link: [Phone test](tel:+1234567890)
- File link: [File test](file:///path/to/file.txt)
- Data link: [Data URI test](data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==)
### Title Attribute (Markdown)
[External link with title](https://example.com "Example Title")
## Link Shortcode (Regular)
### Named parameters
{{< link href="https://github.com/hugo-fixit/FixIt" content="GitHub via shortcode" title="GitHub title" >}}
{{< link href="/test/units/codeblock-test/" content="Internal shortcode link" class="shortcode-link-test" >}}
{{< link href="https://example.com" content="Custom rel + no noreferrer" rel="ugc" noreferrer=false >}}
{{< link href="/images/fixit.svg" content="Download local file" download="fixit.svg" >}}
### Positional parameters
{{< link "https://example.org" "Positional regular link" "Positional title" >}}
## Card Link Shortcode
### Basic card
{{< link href="https://fixit.lruihao.cn" content="FixIt Documentation" card=true >}}
### Card with custom icon class
{{< link href="https://github.com/hugo-fixit/FixIt" content="FixIt on GitHub" card=true card-icon="fa-brands fa-github" >}}
### Card with custom image icon
{{< link href="https://gohugo.io" content="Hugo Official Site" card=true card-icon="/test/images/hugo.min.svg" >}}
### Card with download state
{{< link href="/images/fixit.svg" content="Download as card" card=true download="fixit.svg" >}}
### Positional card parameters
{{< link "https://example.com" "Positional card link" "" true "fa-solid fa-link" >}}
## Edge Cases
- URL with query string:
[Search query test](https://example.com/search?q=fixit&lang=en)
- URL with fragment:
[Fragment test](https://example.com/docs#installation)
- Long URL display (card meta should trim protocol automatically):
{{< link href="https://example.com/a/very/long/path/for/testing/card/url/display?foo=bar&baz=qux" content="Long URL card" card=true >}}
## Manual Verification Checklist
- Regular Markdown external links include external behavior and icon policy.
- Internal links are not treated as external.
- Shortcode card links render `.card-link` structure and card meta URL.
- `download` shows download icon/state in both regular and card modes.
- `noreferrer=false` removes `noopener noreferrer` from generated rel values.
+4
View File
@@ -49,3 +49,7 @@ avatar = "https://lruihao.cn/images/avatar.jpg"
[params.mermaid]
zenuml = "https://cdn.jsdelivr.net/npm/@mermaid-js/mermaid-zenuml/dist/mermaid-zenuml.esm.min.mjs"
[params.dev]
enable = true
debug = true
+9
View File
@@ -12,6 +12,10 @@ html {
container-type: scroll-state;
container-name: scroller;
&:has(dialog[open]) {
overflow: hidden;
}
@include media('reduce-motion') {
scroll-behavior: auto;
}
@@ -41,6 +45,11 @@ button {
cursor: pointer;
}
button:disabled {
opacity: 0.65;
cursor: not-allowed;
}
summary {
cursor: pointer;
}
+1
View File
@@ -3,6 +3,7 @@
@import 'box-shadow';
@import 'compatibility';
@import 'link';
@import 'link-guard';
@import 'loading';
@import 'media';
@import 'scrollbar-width';
+83
View File
@@ -0,0 +1,83 @@
/// Shared styles for link guard content blocks.
/// Keep this as the single source of truth for both link page card and guard dialog body.
@mixin link-guard-content {
padding: 1.5rem;
border: 1px solid fixit-var(global-border-color-light);
background: light-dark(#fff, #1d2230);
box-shadow: 0 1rem 2rem light-dark(rgba(15, 23, 42, 0.18), rgba(0, 0, 0, 0.35));
@include border-radius(1rem);
.title {
margin-block: 0 0.75rem;
font-size: 1.25rem;
}
.message {
font-weight: 600;
margin-block: 0 0.75rem;
}
.target-wrapper {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.875rem;
}
.target {
margin: 0;
padding: 0.5rem 1rem;
flex: 1 1 auto;
min-width: 0;
background: light-dark(#f8fafc, #171b25);
border: 1px solid fixit-var(global-border-color);
font-family: fixit-var(code-font-family);
font-size: 0.875rem;
line-height: 1rem;
word-break: break-all;
@include border-radius;
}
.copy-btn {
box-sizing: content-box;
width: 1rem;
height: 1rem;
flex-shrink: 0;
padding: 0.5rem;
line-height: 1;
background: light-dark(#f8fafc, #171b25);
border: 1px solid fixit-var(global-border-color);
color: fixit-var(global-font-secondary-color);
@include border-radius;
@include user-select(none);
&[data-copied] .fa-clone {
--fa: "\f00c";
font-weight: 900;
color: fixit-var(global-link-hover-color);
}
}
.actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
font-size: 0.875rem;
}
.cancel-btn,
.confirm-btn {
padding: 0.5rem 0.875rem;
border: 1px solid fixit-var(global-border-color-light);
background: light-dark(#f6f8fa, #2a313c);
color: fixit-var(global-font-color);
@include border-radius;
@include user-select(none);
}
.confirm-btn {
color: #fff;
background-color: fixit-var(primary);
border-color: fixit-var(primary);
}
}
+1
View File
@@ -4,6 +4,7 @@
@import 'fixed-button';
@import 'github-corner';
@import 'icon';
@import 'link-guard';
@import 'mask';
@import 'noscript-warning';
@import 'post-chat';
@@ -0,0 +1,70 @@
#link-guard-dialog {
width: MIN(600px, calc(100% - 2rem));
max-width: 100%;
border: none;
padding: 0;
color: fixit-var(global-font-color);
background-color: transparent;
opacity: 0;
transform: translateY(12px) scale(0.98);
transition:
display 0.2s allow-discrete,
overlay 0.2s allow-discrete,
transform 0.2s,
opacity 0.2s;
.guard-content {
@include link-guard-content;
}
&::backdrop {
background-color: rgba(0, 0, 0, 0.25);
backdrop-filter: blur(2px);
opacity: 0;
transition:
display 0.2s allow-discrete,
overlay 0.2s allow-discrete,
opacity 0.2s;
}
&[open] {
opacity: 1;
transform: translateY(0) scale(1);
}
&[open]::backdrop {
opacity: 1;
}
@starting-style {
&[open],
&[open]::backdrop {
opacity: 0;
}
&[open] {
transform: translateY(12px) scale(0.98);
}
}
}
@include media('xs') {
#link-guard-dialog {
width: calc(100% - 2rem);
.guard-content {
padding: 1rem;
.actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.cancel-btn,
.confirm-btn {
width: 100%;
min-width: 0;
}
}
}
}
+2 -1
View File
@@ -41,7 +41,8 @@
@import '_single';
@import '_special';
@import '_archive';
@import 'home';
@import '404';
@import 'home';
@import 'link';
@import 'offline';
@import "patch";
+19
View File
@@ -0,0 +1,19 @@
#content-link {
margin: auto 0;
.card {
width: MIN(600px, calc(100% - 2rem));
box-sizing: border-box;
margin: 0 auto;
@include link-guard-content;
@include media('xs') {
margin-inline: 0;
padding: 1rem;
}
}
body:has(&) {
@include set-fixit-var(global-background-color, fixit-var(header-background-color));
}
}
+6 -10
View File
@@ -216,11 +216,6 @@
}
#toc-dialog {
transition:
display 0.2s allow-discrete,
overlay 0.2s allow-discrete,
translate 0.2s,
opacity 0.2s 0.4s;
opacity: 0;
translate: 100vw 0;
width: MIN(calc(100% - 4rem), 460px);
@@ -231,6 +226,11 @@
border: none;
border-left: 1px solid fixit-var(global-border-color-light);
background-color: fixit-var(global-background-color);
transition:
display 0.2s allow-discrete,
overlay 0.2s allow-discrete,
translate 0.2s,
opacity 0.2s 0.4s;
.toc {
max-width: 100%;
@@ -255,11 +255,11 @@
&::backdrop {
background-color: rgba(0, 0, 0, 0.25);
backdrop-filter: blur(2px);
opacity: 0;
transition:
display 0.5s allow-discrete,
overlay 0.5s allow-discrete,
opacity 0.2s 0.4s;
opacity: 0;
}
&[open],
@@ -287,7 +287,3 @@
}
}
}
html:has(#toc-dialog[open]) {
overflow: hidden;
}
+44
View File
@@ -0,0 +1,44 @@
/**
* for link redirection page
*/
import { createCopyText } from '../utils/common';
const copyText = createCopyText();
function initLinkGuard() {
const params = new URLSearchParams(window.location.search);
const target = params.get('target');
const targetElement = document.querySelector('.target');
const copyBtn = document.querySelector('.copy-btn');
const confirmBtn = document.querySelector('.confirm-btn');
if (target) {
targetElement.textContent = target;
copyBtn.disabled = false;
confirmBtn.disabled = false;
copyBtn.addEventListener('click', () =>{
copyText(target).then(() => {
copyBtn.toggleAttribute('data-copied', true);
setTimeout(() => {
copyBtn.toggleAttribute('data-copied', false);
}, 2000);
}, () => {
console.error('Clipboard write failed!', 'Your browser does not support clipboard API!');
});
});
confirmBtn.addEventListener('click', () => {
window.location.href = target;
});
} else {
targetElement.textContent = 'Invalid target URL';
}
}
if (document.readyState !== 'loading') {
initLinkGuard();
} else {
document.addEventListener('DOMContentLoaded', initLinkGuard, false);
}
+72
View File
@@ -1578,6 +1578,77 @@ class FixIt {
this.initFootnotes();
}
/**
* Initialize link guard dialog for links with target="_blank" and data-guard="modal"
* @param {Element} target - The target element to initialize within (optional, defaults to document)
*/
initLinkGuardDialog(target = document) {
const dialog = document.getElementById('link-guard-dialog');
if (!dialog) return;
const $target = dialog.querySelector('.target');
const $copy = dialog.querySelector('.copy-btn');
const $confirm = dialog.querySelector('.confirm-btn');
const $cancel = dialog.querySelector('.cancel-btn');
const _closeDialog = () => {
if (dialog.open) dialog.close();
dialog._target = null;
if ($target) {
$target.textContent = '-';
}
};
if (!dialog.dataset.init) {
dialog.dataset.init = 'true';
$confirm?.addEventListener('click', () => {
if (dialog._target) {
window.open(dialog._target, '_blank', 'noopener,noreferrer');
}
_closeDialog();
});
$cancel?.addEventListener('click', _closeDialog);
$copy?.addEventListener('click', () => {
const textToCopy = dialog._target || '';
if (!textToCopy) return;
copyText(textToCopy).then(() => {
$copy.toggleAttribute('data-copied', true);
window.setTimeout(() => {
$copy.toggleAttribute('data-copied', false);
}, 2000);
});
});
dialog.addEventListener('click', (e) => {
if (e.target === dialog) _closeDialog();
});
}
forEach(target.querySelectorAll('a[target="_blank"][data-guard="modal"]:not([data-init])'), ($link) => {
$link.dataset.init = 'true';
$link.addEventListener('click', (e) => {
e.preventDefault();
let target = $link.href;
try {
const guardUrl = new URL($link.href);
target = guardUrl.searchParams.get('target') || target;
} catch (err) {
// Ignore malformed URLs and fall back to the original href.
}
dialog._target = target;
if ($target) {
$target.textContent = target;
}
dialog.showModal();
document.activeElement?.blur();
}, false);
});
}
/**
* Helper method to initialize content components
* @param {Element} target - The target element (optional, defaults to document)
@@ -1606,6 +1677,7 @@ class FixIt {
this.initMathJax();
this.initJsonViewer();
this.initTabEvents(target);
this.initLinkGuardDialog(target);
FileTree.init(target);
window.FixItMermaid?.init?.();
window.FixItAPlayer?.init?.();
+58 -3
View File
@@ -341,6 +341,16 @@ isHTML = true
permalinkable = true
notAlternative = true
# Fixit 1.0.0 | NEW Options to make output /link/index.html file
[outputFormats.link]
path = "link"
baseName = "index"
mediaType = "text/html"
isPlainText = false
isHTML = true
permalinkable = true
notAlternative = true
# FixIt 0.3.0 | NEW Options to make output readme.md file
[outputFormats.readme]
baseName = "readme"
@@ -372,7 +382,7 @@ permalinkable = true
# -------------------------------------------------------------------------------------
# Options to make hugo output files, the optional values are below:
# home = ["html", "rss", "archives", "offline", "readme", "baidu_urls", "search"]
# home = ["html", "rss", "archives", "offline", "link", "search", "readme", "baidu_urls"]
# page = ["html", "markdown"]
# section = ["html", "rss"]
# taxonomy = ["html"]
@@ -384,6 +394,7 @@ home = [
"rss",
"archives",
"offline",
"link",
"search"
]
page = [
@@ -434,8 +445,6 @@ dateFormat = "2006-01-02"
images = []
# FixIt 0.2.12 | NEW enable PWA
enablePWA = false
# FixIt 0.2.14 | NEW whether to add external Icon for external links automatically
externalIcon = false
# FixIt 0.3.13 | NEW whether to capitalize titles
capitalizeTitles = true
# FixIt 0.3.0 | NEW whether to add site title to the title of every page
@@ -1129,6 +1138,21 @@ optimise = false
# e.g. ["no-optimize.jpg", "*.tiff", "images/exclude-*.webp"]
blackList = []
# FixIt 1.0.0 | NEW Link config
[params.link]
# whether to add external Icon for external links automatically
external_icon = true
# external link guard config
[params.link.guard]
enable = false
# external link guard mode, available modes: ["modal" "redirect"]
# in "modal" mode, a dialog will pop up to ask for confirmation when users click external links
# in "redirect" mode, users will be redirected to an intermediate page before visiting external links
mode = "modal"
# allowed domains (exact host or parent domain) will skip confirmation
allow_domains = []
# FixIt 0.4.0 | NEW Code block wrapper config
# you can override the global config via Markdown attributes, for example:
# ```lang {mode="mac", maxShownLines=5}
@@ -1279,6 +1303,37 @@ collectionList = false
# FixIt 0.3.0 | NEW whether to enable collection navigation at the end of the post
collectionNavigation = false
# FixIt 0.4.6 | NEW External link guard config
[params.page.externalLinkGuard]
# whether to enable external link guard for post content
enable = false
# scope:
# - "content": only post content area (e.g. #content and decryptor content)
# - "site": all external links on the whole site
scope = "content"
# mode: "modal" (current page dialog) | "redirect" (intermediate page)
mode = "modal"
# allowed domains (exact host or parent domain) will skip confirmation
allow_domains = []
# blocked URL schemes for security
block_schemes = [
"javascript",
"data",
"file"
]
# how to open target link after confirm:
# - "follow": follow original link behavior
# - open in new tab when link has target="_blank"
# - otherwise open in current tab
# - "newtab": always open in a new tab
# - "self": always open in current tab
# note: Ctrl/Cmd/Shift + click is treated as force-new-tab and has higher priority than the policy above.
new_tab_policy = "follow"
# whether to show full URL in dialog
show_full_url = true
# custom message text, empty to use i18n default
message = ""
# FixIt 0.2.15 | NEW Repost config
[params.page.repost]
enable = false
+7
View File
@@ -183,6 +183,13 @@ other = "Offline"
other = "Sie sind nicht mit dem Internet verbunden, es stehen nur zwischengespeicherte Seiten zur Verfügung."
# === offline ===
# === link redirection ===
[linkRedirection]
title = "Hinweis zur Weiterleitung"
message = "Sie sind dabei, {{ .Title }} zu verlassen. Bitte achten Sie auf die Sicherheit Ihres Kontos und Vermögens."
confirm = "Weiter besuchen"
# === link redirection ===
# === GitHub Alert ===
[alert]
note = "Hinweis"
+7
View File
@@ -183,6 +183,13 @@ other = "Offline"
other = "You are not connected to the Internet, only cached pages will be available."
# === offline ===
# === link redirection ===
[linkRedirection]
title = "Redirection Notice"
message = "You are about to leave {{ .Title }}, please be aware of your account and property security."
confirm = "Continue visiting"
# === link redirection ===
# === GitHub Alert ===
[alert]
note = "Note"
+7
View File
@@ -183,6 +183,13 @@ other = "desconectado"
other = "No está conectado a Internet, solo estarán disponibles las páginas almacenadas en caché."
# === offline ===
# === link redirection ===
[linkRedirection]
title = "Aviso de redirección"
message = "Está a punto de salir de {{ .Title }}. Proteja la seguridad de su cuenta y sus bienes."
confirm = "Continuar visitando"
# === link redirection ===
# === GitHub Alert ===
[alert]
note = "Nota"
+7
View File
@@ -183,6 +183,13 @@ other = "Hors ligne"
other = "Vous n'êtes pas connecté à Internet, seules les pages mises en cache seront disponibles."
# === offline ===
# === link redirection ===
[linkRedirection]
title = "Avis de redirection"
message = "Vous êtes sur le point de quitter {{ .Title }}. Veuillez protéger la sécurité de votre compte et de vos biens."
confirm = "Continuer la visite"
# === link redirection ===
# === GitHub Alert ===
[alert]
note = "Remarque"
+7
View File
@@ -184,6 +184,13 @@ other = "ऑफलाइन"
other = "आप इंटरनेट से कनेक्ट नहीं हैं, केवल कैश्ड पेज ही उपलब्ध होंगे।"
# === offline ===
# === link redirection ===
[linkRedirection]
title = "रीडायरेक्शन सूचना"
message = "आप {{ .Title }} छोड़ने वाले हैं, कृपया अपने खाते और संपत्ति की सुरक्षा पर ध्यान दें।"
confirm = "देखना जारी रखें"
# === link redirection ===
# === GitHub Alert ===
[alert]
note = "ध्यान दें"
+7
View File
@@ -183,6 +183,13 @@ other = "disconnesso"
other = "Non sei connesso a Internet, saranno disponibili solo le pagine memorizzate nella cache."
# === offline ===
# === link redirection ===
[linkRedirection]
title = "Avviso di reindirizzamento"
message = "Stai per lasciare {{ .Title }}. Presta attenzione alla sicurezza del tuo account e dei tuoi beni."
confirm = "Continua a visitare"
# === link redirection ===
# === GitHub Alert ===
[alert]
note = "Nota"
+7
View File
@@ -179,6 +179,13 @@ other = "オフライン"
other = "インターネットに接続されていません。キャッシュされたページのみが利用可能です。"
# === offline ===
# === link redirection ===
[linkRedirection]
title = "リダイレクト通知"
message = "{{ .Title }} から離れようとしています。アカウントと資産の安全にご注意ください。"
confirm = "閲覧を続ける"
# === link redirection ===
# === GitHub Alert ===
[alert]
note = "注意"
+7
View File
@@ -179,6 +179,13 @@ other = "오프라인"
other = "인터넷에 연결되어 있지 않으며, 캐시된 페이지만 사용할 수 있습니다."
# === offline ===
# === link redirection ===
[linkRedirection]
title = "리디렉션 안내"
message = "{{ .Title }}에서 벗어나려고 합니다. 계정과 자산 보안에 유의해 주세요."
confirm = "계속 방문"
# === link redirection ===
# === GitHub Alert ===
[alert]
note = "노트"
+7
View File
@@ -183,6 +183,13 @@ other = "Offline"
other = "Nie masz połączenia z Internetem, dostępne będą tylko strony z pamięci podręcznej."
# === offline ===
# === link redirection ===
[linkRedirection]
title = "Powiadomienie o przekierowaniu"
message = "Za chwilę opuścisz {{ .Title }}. Zadbaj o bezpieczeństwo konta i swoich środków."
confirm = "Kontynuuj odwiedzanie"
# === link redirection ===
# === GitHub Alert ===
[alert]
note = "Notatka"
+7
View File
@@ -184,6 +184,13 @@ other = "Offline"
other = "Você não está conectado à Internet, apenas as páginas em cache estarão disponíveis."
# === offline ===
# === link redirection ===
[linkRedirection]
title = "Aviso de redirecionamento"
message = "Você está prestes a sair de {{ .Title }}. Fique atento à segurança da sua conta e dos seus bens."
confirm = "Continuar visitando"
# === link redirection ===
# === GitHub Alert ===
[alert]
note = "Nota"
+7
View File
@@ -183,6 +183,13 @@ other = "Deconectat"
other = "Nu sunteți conectat la Internet, vor fi disponibile doar paginile stocate în cache."
# === offline ===
# === link redirection ===
[linkRedirection]
title = "Notificare de redirecționare"
message = "Urmează să părăsiți {{ .Title }}. Vă rugăm să acordați atenție securității contului și bunurilor dvs."
confirm = "Continuă vizitarea"
# === link redirection ===
# === GitHub Alert ===
[alert]
note = "Notă"
+7
View File
@@ -183,6 +183,13 @@ other = "Не в сети"
other = "Вы не подключены к интернету, будут доступны только кешированные страницы."
# === offline ===
# === link redirection ===
[linkRedirection]
title = "Уведомление о перенаправлении"
message = "Вы собираетесь покинуть {{ .Title }}. Пожалуйста, обратите внимание на безопасность вашего аккаунта и имущества."
confirm = "Продолжить посещение"
# === link redirection ===
# === GitHub Alert ===
[alert]
note = "Примечание"
+7
View File
@@ -183,6 +183,13 @@ other = "Оффлине"
other = "Нисте повезани на Интернет, биће доступне само кеширане странице."
# === offline ===
# === link redirection ===
[linkRedirection]
title = "Обавештење о преусмеравању"
message = "Управо ћете напустити {{ .Title }}. Обратите пажњу на безбедност свог налога и имовине."
confirm = "Настави посету"
# === link redirection ===
# === GitHub Alert ===
[alert]
note = "Напомена"
+7
View File
@@ -182,6 +182,13 @@ other = "ngoại tuyến"
other = "Bạn chưa kết nối với Internet, chỉ các trang được lưu trong bộ nhớ cache sẽ khả dụng."
# === offline ===
# === link redirection ===
[linkRedirection]
title = "Thông báo chuyển hướng"
message = "Bạn sắp rời khỏi {{ .Title }}, vui lòng chú ý an toàn cho tài khoản và tài sản của mình."
confirm = "Tiếp tục truy cập"
# === link redirection ===
# === GitHub Alert ===
[alert]
note = "Ghi chú"
+7
View File
@@ -179,6 +179,13 @@ other = "离线"
other = "你没有连接到 Internet,只有缓存的页面可用。"
# === offline ===
# === link redirection ===
[linkRedirection]
title = "跳转提示"
message = "即将离开{{ .Title }},请注意账号财产安全。"
confirm = "继续访问"
# === link redirection ===
# === GitHub Alert ===
[alert]
note = "注意"
+7
View File
@@ -179,6 +179,13 @@ other = "離線"
other = "你沒有連接到 Internet,只有緩存的頁面可用。"
# === offline ===
# === link redirection ===
[linkRedirection]
title = "跳轉提示"
message = "即將離開{{ .Title }},請注意帳號財產安全。"
confirm = "繼續訪問"
# === link redirection ===
# === GitHub Alert ===
[alert]
note = "注意"
+1 -1
View File
@@ -2,5 +2,5 @@
{{- with dict "Path" $destination | partial "function/resource.html" -}}
{{- $destination = .RelPermalink -}}
{{- end -}}
{{- $options := dict "Destination" $destination "Title" .Title "Content" .Text "ExternalIcon" .Page.Site.Params.externalIcon -}}
{{- $options := dict "Destination" $destination "Title" .Title "Content" .Text -}}
{{- partial "plugin/link.html" $options -}}
+10
View File
@@ -0,0 +1,10 @@
{{- /*
This is a debug partial that dumps the current context.
@example
{{- partial "_debug/dump.html" . -}}
*/ -}}
{{- $debug := (site.Store.Get "devOpts").debug -}}
{{- if $debug | and hugo.IsServer -}}
<pre>{{ debug.Dump . }}</pre>
{{- end -}}
@@ -1,6 +1,7 @@
{{- /*
Render template call stack for debugging purpose.
Call the partial from any template: {{- partial "_debug/template-call-stack.html" . -}}
@example
{{- partial "_debug/template-call-stack.html" . -}}
*/ -}}
{{- $debug := (site.Store.Get "devOpts").debug -}}
@@ -84,7 +84,7 @@
{{- end -}}
{{- /* Build children recursively */ -}}
{{- if $_debug -}}{{- warnf "> %v:" $rootLabel -}}{{- end -}}
{{- if $_debug -}}{{- warnf "[file-tree]> %v:" $rootLabel -}}{{- end -}}
{{- $childOptions := dict "path" $root "deep" (add $deep 1) "level" $level "ignoreList" $ignoreList "isRecursive" true -}}
{{- $children := partial "function/get-file-tree.html" $childOptions -}}
+1 -1
View File
@@ -42,7 +42,7 @@
{{- /* Author */ -}}
{{- if ne $footerConfig.author false -}}
<span class="author" itemprop="copyrightHolder">
{{ partial "plugin/link.html" (dict "Destination" ($.Site.Params.author.link | default (relLangURL "")) "Content" .Site.Params.author.name) -}}
{{ partial "plugin/link.html" (dict "Destination" ($.Site.Params.author.link | default (relLangURL "")) "Content" .Site.Params.author.name "ExternalIcon" false) -}}
</span>
{{- end -}}
+21 -1
View File
@@ -36,7 +36,7 @@
{{- if $githubCorner.enable -}}
{{- with $githubCorner -}}
{{- $githubImage := `<svg viewBox="0 0 250 250" aria-hidden="true" width="56" height="56"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg>` -}}
{{- $options := dict "Destination" .Permalink "Title" .Title "Content" $githubImage "Class" (cond (eq .Position "left") "left" "right" | printf "github-corner %v d-none-mobile") "Noreferrer" false -}}
{{- $options := dict "Destination" .Permalink "Title" .Title "Content" $githubImage "ExternalIcon" false "Class" (cond (eq .Position "left") "left" "right" | printf "github-corner %v d-none-mobile") "Noreferrer" false -}}
{{- partial "plugin/link.html" $options -}}
{{- end -}}
{{- end -}}
@@ -65,6 +65,26 @@
{{- end -}}
{{- end -}}
{{- /* Link Guard Dialog */ -}}
{{- if .Site.Params.link.guard.enable -}}
<dialog id="link-guard-dialog" aria-labelledby="link-guard-dialog-title" role="dialog">
<div class="guard-content">
<h2 class="title">{{ T "linkRedirection.title" }}</h2>
<p class="message">{{ dict "Title" .Site.Title | T "linkRedirection.message" }}</p>
<div class="target-wrapper">
<p class="target">-</p>
<button type="button" class="copy-btn" aria-label="{{ T "assets.copyToClipboard" }}">
{{- dict "Class" "fa-regular fa-clone fa-width-auto" | partial "plugin/icon.html" -}}
</button>
</div>
<div class="actions">
<button type="button" class="cancel-btn">{{ T "assets.cancel" }}</button>
<button type="button" class="confirm-btn">{{ T "linkRedirection.confirm" }}</button>
</div>
</div>
</dialog>
{{- end -}}
{{- /* Custom widgets */ -}}
{{- block "custom-widgets" . }}{{ end -}}
+143 -74
View File
@@ -1,88 +1,157 @@
{{- $rel := "" -}}
{{- $external := false -}}
{{- /*
Link rendering partial for regular and card-style links.
- Auto-detects external links and applies target/rel policies.
- Supports optional icon, download hint, and external icon display.
- Supports card mode with URL meta and favicon fallback.
@param {String} .Destination target URL
@param {String} [.Content] link label HTML
@param {String} [.Title] title attribute
@param {String} [.Class] custom CSS classes
@param {Boolean} [.Newtab] force external behavior
@param {Boolean} [.Noreferrer] whether to include noreferrer (default: true)
@param {Boolean} [.ExternalIcon] whether to show external-link icon
@param {Object} [.Icon] icon params for plugin/icon.html
@param {Boolean} [.Card] whether to render card link layout
@param {String} [.CardIcon] card icon URL or icon class
@param {String} [.Download] download attribute value
*/ -}}
{{- /* Regular link rendering */ -}}
{{- define "render-link-regular" -}}
{{- $ctx := .ctx -}}
{{- $config := .config -}}
{{- $isExternal := .isExternal -}}
{{- $externalIcon := $ctx.ExternalIcon | default $config.externalIcon -}}
{{- with $ctx.Icon -}}
{{- partial "plugin/icon.html" . }}
{{ end -}}
{{- with $ctx.Content -}}
{{- . | safeHTML -}}
{{- end -}}
{{- if
$externalIcon
| and $isExternal
| and (not (hasPrefix $ctx.Content `<img`))
| and (not (strings.HasSuffix $ctx.Content `</i>`))
| and (not (strings.HasSuffix $ctx.Content `):`))
-}}
{{- if $ctx.Download -}}
{{- dict "Class" "fa-solid fa-cloud-download-alt ms-1 text-secondary" | partial "plugin/icon.html" -}}
{{- else -}}
{{- dict "Class" "fa-solid fa-external-link-alt fa-xs ms-1 text-secondary" | partial "plugin/icon.html" -}}
{{- end -}}
{{- else if $ctx.Download }}
{{- dict "Class" "fa-solid fa-download ms-1 text-secondary" | partial "plugin/icon.html" -}}
{{- end -}}
{{- end -}}
{{- /* Card link rendering */ -}}
{{- define "render-link-card" -}}
{{- $ctx := .ctx -}}
{{- $url := .url -}}
{{- $cardIcon := $ctx.CardIcon -}}
{{- if (not $cardIcon) | and $url.Host -}}
{{- with partial "function/get-remote-image.html" (dict "Src" (add "https://favicon.im/" $url.Host)) -}}
{{- $cardIcon = .RelPermalink -}}
{{- else with partial "function/get-remote-image.html" (dict "Src" (add "https://api.lruihao.cn/google/s2/favicons?sz=64&domain=" $url.Host)) -}}
{{- $cardIcon = .RelPermalink -}}
{{- end -}}
{{- end -}}
<span class="cl-backdrop"></span>
<span class="cl-content">
<span class="cl-text">
<span class="cl-title">
{{- with $ctx.Icon -}}
{{- partial "plugin/icon.html" . }}
{{ end -}}
{{- with $ctx.Content -}}
{{- . | safeHTML -}}
{{- end -}}
</span>
<span class="cl-meta">
<svg class="cl-icon-link" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M574 665.4c-3.1-3.1-8.2-3.1-11.3 0L446.5 781.6c-53.8 53.8-144.6 59.5-204 0-59.5-59.5-53.8-150.2 0-204l116.2-116.2c3.1-3.1 3.1-8.2 0-11.3l-39.8-39.8c-3.1-3.1-8.2-3.1-11.3 0L191.4 526.5c-84.6 84.6-84.6 221.5 0 306s221.5 84.6 306 0l116.2-116.2c3.1-3.1 3.1-8.2 0-11.3L574 665.4zM832.6 191.4c-84.6-84.6-221.5-84.6-306 0L410.3 307.6c-3.1 3.1-3.1 8.2 0 11.3l39.7 39.7c3.1 3.1 8.2 3.1 11.3 0l116.2-116.2c53.8-53.8 144.6-59.5 204 0 59.5 59.5 53.8 150.2 0 204L665.3 562.6c-3.1 3.1-3.1 8.2 0 11.3l39.8 39.8c3.1 3.1 8.2 3.1 11.3 0l116.2-116.2c84.5-84.6 84.5-221.5 0-306.1z" fill="#a9a9b3"></path><path d="M610.1 372.3c-3.1-3.1-8.2-3.1-11.3 0L372.3 598.7c-3.1 3.1-3.1 8.2 0 11.3l39.6 39.6c3.1 3.1 8.2 3.1 11.3 0l226.4-226.4c3.1-3.1 3.1-8.2 0-11.3l-39.5-39.6z" fill="#a9a9b3"></path></svg>
<span class="cl-url">
{{- $url.String | safeURL | strings.TrimPrefix (add $url.Scheme "://") -}}
</span>
</span>
</span>
{{- if $ctx.Download -}}
<svg class="cl-shortcut-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path d="M824.32 473.6c-15.36-2.56-25.6-15.36-25.6-30.72 0-112.64-71.68-217.6-179.2-250.88-148.48-46.08-289.28 38.4-332.8 166.4-2.56 10.24-12.8 17.92-23.04 20.48C143.36 401.92 51.2 509.44 51.2 637.44v2.56c0 115.2 97.28 204.8 209.92 204.8h524.8c102.4-2.56 184.32-84.48 184.32-186.88 2.56-89.6-61.44-163.84-145.92-184.32z m-273.92 225.28c-12.8 12.8-30.72 15.36-46.08 10.24H501.76c-5.12-2.56-10.24-5.12-15.36-10.24L366.08 578.56c-15.36-15.36-15.36-43.52 0-58.88 15.36-15.36 43.52-15.36 58.88 0l51.2 51.2V352.4608c0-23.04 17.92-40.96 40.96-40.96 23.04 0 40.96 17.92 40.96 40.96v218.4192l51.2-51.2c17.92-17.92 43.52-17.92 61.44 0 17.92 15.36 17.92 40.96 0 58.88l-120.32 120.32z" fill="#4FC089"></path></svg>
{{- else -}}
{{- with $cardIcon -}}
{{- if (strings.HasPrefix . "fa") -}}
<i class="{{ . }} cl-shortcut-icon"></i>
{{- else -}}
<img src="{{ . }}" alt="card image" class="cl-shortcut-image">
{{- end -}}
{{- else -}}
<svg class="cl-shortcut-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path d="M960 512c0 249.408-203.2 448-448 448-244.778667 0-448-198.592-448-448S262.592 64 512 64s448 198.592 448 448" fill="#2196F3"></path><path d="M507.52 718.08c0-8.96-4.48-13.44-13.44-17.92-26.88-8.96-53.76-8.96-76.16-31.381333-4.48-8.96-4.48-17.92-8.96-26.88-8.96-8.96-31.36-13.44-44.8-17.92h-89.6c-13.44-4.48-22.4-22.4-31.36-35.84 0-4.48 0-13.461333-8.96-13.461334-8.96-4.458667-17.92 4.501333-26.88 0-4.48-4.458667-4.48-8.96-4.48-13.418666 0-13.461333 8.96-26.901333 17.92-35.861334 13.44-8.96 26.88 4.48 40.32 4.48 4.48 0 4.48 0 8.96 4.48 13.44 4.48 17.92 22.4 17.92 35.861334v8.96c0 4.48 4.48 4.48 8.96 4.48 4.48-22.4 4.48-44.821333 8.96-67.2 0-26.88 26.88-53.781333 49.28-62.72 8.96-4.458667 13.44 4.501333 22.4 0 26.88-8.96 94.08-35.84 80.64-71.658667-8.96-31.381333-35.84-62.698667-71.68-58.24-8.96 4.501333-13.44 8.96-22.4 13.461333-13.44 8.96-40.32 35.84-53.76 35.84-22.4-4.48-22.4-35.84-17.92-49.301333 4.48-17.92 44.8-76.138667 71.68-67.178667l17.92 17.92c8.96 4.48 22.4 4.48 35.84 4.48 4.48 0 8.96 0 13.44-4.48 4.48-4.48 4.48-4.48 4.48-8.96 0-13.44-13.44-26.901333-22.4-35.861333s-22.4-17.92-35.84-22.378667c-44.8-13.461333-116.48 4.458667-152.32 35.84-35.84 31.36-62.72 85.12-80.64 129.92-8.96 26.88-17.92 62.698667-22.4 94.08-4.48 22.4-8.96 40.32 4.48 62.698667 13.44 26.88 40.32 53.781333 67.2 71.68 17.92 13.44 53.76 13.44 71.68 35.84 13.44 17.941333 8.96 40.32 8.96 62.72 0 26.88 17.92 49.28 26.88 71.658667 4.48 13.461333 8.96 31.381333 13.44 44.821333 0 4.48 4.48 31.36 4.48 35.84 26.88 13.44 49.28 26.901333 80.64 35.861333 4.48 0 22.4-26.901333 22.4-31.381333 13.44-13.44 22.4-31.36 35.84-40.32 8.96-4.48 17.92-8.96 26.88-17.941333 8.96-8.96 13.44-26.88 17.92-40.32 4.48-8.938667 8.96-26.858667 4.48-40.298667M516.48 305.92c4.48 0 8.96-4.48 17.92-8.96 13.44-8.96 26.901333-22.4 40.32-31.36 13.461333-8.96 26.901333-22.4 35.861333-31.36 13.44-8.96 22.4-26.88 26.88-40.341333 4.48-8.96 17.941333-26.88 13.44-40.32-4.48-8.96-26.88-13.44-35.84-17.92C579.2 126.698667 547.84 122.24 512 122.24c-13.44 0-31.36 4.458667-35.84 17.92-4.48 22.4 13.44 17.92 31.36 22.4 0 0 4.48 35.84 4.48 40.32 4.48 22.421333-8.96 35.84-8.96 58.24 0 13.44 0 35.84 8.96 44.8h4.48zM892.8 619.52c4.501333-8.96 4.501333-22.4 8.96-31.36 4.501333-22.421333 4.501333-44.8 4.501333-67.2 0-44.8-4.501333-89.578667-17.92-129.92-8.96-13.44-13.461333-26.88-17.941333-40.341333-8.96-22.378667-22.4-44.8-40.32-62.698667-17.92-22.4-40.341333-85.12-80.64-67.2-13.44 4.501333-22.4 22.421333-31.36 31.381333l-26.88 40.32c-4.501333 4.48-8.96 13.44-4.501333 17.92 0 4.48 4.501333 4.48 8.96 4.48 8.96 4.501333 13.461333 4.501333 22.421333 8.96 4.48 0 8.96 4.501333 4.48 8.96 0 0 0 4.501333-4.48 4.501334-22.421333 22.4-44.8 40.32-67.2 62.698666-4.48 4.48-8.96 13.44-8.96 17.92s4.48 4.48 4.48 8.96c0 4.501333-4.48 4.501333-8.96 8.96-8.96 4.501333-17.92 8.96-22.4 13.461334-4.48 8.96 0 22.4-4.48 31.36-4.48 22.4-17.941333 40.32-26.901333 62.72-8.96 13.418667-13.418667 26.88-22.378667 40.32 0 17.92-4.501333 31.36 4.458667 44.8 22.421333 31.36 62.72 13.44 94.08 26.901333 8.96 4.458667 17.92 4.458667 22.421333 13.418667 13.418667 13.461333 13.418667 35.861333 17.92 49.301333 4.458667 17.92 8.96 35.84 17.92 53.76 4.48 22.421333 13.44 44.821333 17.92 62.72 40.341333-31.36 76.16-67.178667 103.04-112 26.88-31.424 40.341333-67.242667 53.76-103.104" fill="#CDDC39"></path></svg>
{{- end -}}
{{- end -}}
</span>
{{- end -}}
{{- $blockSchemes := slice "javascript" "mailto" "tel" "file" "data" "vscode" "trae" -}}
{{- $config := partial "function/snake2camel.html" site.Params.link -}}
{{- $rel := .Rel | default "" -}}
{{- $isExternal := false -}}
{{- $noreferrer := true -}}
{{- if eq .Noreferrer false -}}
{{- $noreferrer = false -}}
{{- end -}}
{{- $url := urls.Parse .Destination -}}
{{- if $url.IsAbs | or .Newtab -}}
{{- $rel = cond $noreferrer "external nofollow noopener noreferrer" "external nofollow" -}}
{{- $external = true -}}
{{/* {{ partial "_debug/dump" $url }} */}}
{{- if $url.IsAbs | and (not (in $blockSchemes $url.Scheme)) -}}
{{- $isExternal = not (strings.HasPrefix .Destination site.BaseURL) -}}
{{- end -}}
{{- if $isExternal -}}
{{- $rel = add $rel " external nofollow" -}}
{{- if $noreferrer -}}
{{- $rel = add $rel " noopener noreferrer" -}}
{{- end -}}
{{- end -}}
{{- $class := .Class | default "" -}}
{{- if eq .Card true -}}
{{- $class = trim (printf "%v %v" "card-link" $class) " " -}}
{{- end -}}
<a href="{{ .Destination | safeURL }}"
{{- with .Title }} title="{{ . }}"{{- end -}}
{{- if $external }} target="_blank"{{- end -}}
{{- if $rel | or .Rel }} rel="{{ $rel }}{{ with .Rel }} {{ . }}{{ end }}"{{- end -}}
{{- with $class }} class="{{ with $class }}{{ . }}{{ end }}"{{- end -}}
{{- with .Download }} download="{{ . }}"{{- end -}}
>
{{- if ne .Card true -}}
{{- with .Icon -}}
{{- partial "plugin/icon.html" . }}
{{ end -}}
{{- with .Content -}}
{{- . | safeHTML -}}
{{- end -}}
{{- if
(.ExternalIcon | default false)
| and $external | and (eq (hasPrefix .Content `<img`) false)
| and (eq ((strings.HasSuffix .Content `</i>`) | or (strings.HasSuffix .Content `):`)) false)
-}}
{{- if .Download -}}
{{- dict "Class" "fa-solid fa-cloud-download-alt ms-1 text-secondary" | partial "plugin/icon.html" -}}
{{- else -}}
{{- dict "Class" "fa-solid fa-external-link-alt fa-xs ms-1 text-secondary" | partial "plugin/icon.html" -}}
{{- end -}}
{{- else if .Download }}
{{- dict "Class" "fa-solid fa-download ms-1 text-secondary" | partial "plugin/icon.html" -}}
{{- end -}}
{{- else -}}
{{- $cardIcon := .CardIcon -}}
{{- if (not $cardIcon) | and $url.Host -}}
{{- with partial "function/get-remote-image.html" (dict "Src" (add "https://favicon.im/" $url.Host)) -}}
{{- $cardIcon = .RelPermalink -}}
{{- else with partial "function/get-remote-image.html" (dict "Src" (add "https://api.lruihao.cn/google/s2/favicons?sz=64&domain=" $url.Host)) -}}
{{- $cardIcon = .RelPermalink -}}
{{- end -}}
{{- end -}}
<span class="cl-backdrop"></span>
<span class="cl-content">
<span class="cl-text">
<span class="cl-title">
{{- with .Icon -}}
{{- partial "plugin/icon.html" . }}
{{ end -}}
{{- with .Content -}}
{{- . | safeHTML -}}
{{- end -}}
</span>
<span class="cl-meta">
<svg class="cl-icon-link" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M574 665.4c-3.1-3.1-8.2-3.1-11.3 0L446.5 781.6c-53.8 53.8-144.6 59.5-204 0-59.5-59.5-53.8-150.2 0-204l116.2-116.2c3.1-3.1 3.1-8.2 0-11.3l-39.8-39.8c-3.1-3.1-8.2-3.1-11.3 0L191.4 526.5c-84.6 84.6-84.6 221.5 0 306s221.5 84.6 306 0l116.2-116.2c3.1-3.1 3.1-8.2 0-11.3L574 665.4zM832.6 191.4c-84.6-84.6-221.5-84.6-306 0L410.3 307.6c-3.1 3.1-3.1 8.2 0 11.3l39.7 39.7c3.1 3.1 8.2 3.1 11.3 0l116.2-116.2c53.8-53.8 144.6-59.5 204 0 59.5 59.5 53.8 150.2 0 204L665.3 562.6c-3.1 3.1-3.1 8.2 0 11.3l39.8 39.8c3.1 3.1 8.2 3.1 11.3 0l116.2-116.2c84.5-84.6 84.5-221.5 0-306.1z" fill="#a9a9b3"></path><path d="M610.1 372.3c-3.1-3.1-8.2-3.1-11.3 0L372.3 598.7c-3.1 3.1-3.1 8.2 0 11.3l39.6 39.6c3.1 3.1 8.2 3.1 11.3 0l226.4-226.4c3.1-3.1 3.1-8.2 0-11.3l-39.5-39.6z" fill="#a9a9b3"></path></svg>
<span class="cl-url">
{{- $u := urls.Parse .Destination -}}
{{- $u.String | safeURL | strings.TrimPrefix (add $u.Scheme "://") -}}
</span>
</span>
</span>
{{- if .Download -}}
<svg class="cl-shortcut-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path d="M824.32 473.6c-15.36-2.56-25.6-15.36-25.6-30.72 0-112.64-71.68-217.6-179.2-250.88-148.48-46.08-289.28 38.4-332.8 166.4-2.56 10.24-12.8 17.92-23.04 20.48C143.36 401.92 51.2 509.44 51.2 637.44v2.56c0 115.2 97.28 204.8 209.92 204.8h524.8c102.4-2.56 184.32-84.48 184.32-186.88 2.56-89.6-61.44-163.84-145.92-184.32z m-273.92 225.28c-12.8 12.8-30.72 15.36-46.08 10.24H501.76c-5.12-2.56-10.24-5.12-15.36-10.24L366.08 578.56c-15.36-15.36-15.36-43.52 0-58.88 15.36-15.36 43.52-15.36 58.88 0l51.2 51.2V352.4608c0-23.04 17.92-40.96 40.96-40.96 23.04 0 40.96 17.92 40.96 40.96v218.4192l51.2-51.2c17.92-17.92 43.52-17.92 61.44 0 17.92 15.36 17.92 40.96 0 58.88l-120.32 120.32z" fill="#4FC089"></path></svg>
{{- else -}}
{{- with $cardIcon -}}
{{- if (strings.HasPrefix . "fa") -}}
<i class="{{ . }} cl-shortcut-icon"></i>
{{- else -}}
<img src="{{ . }}" alt="card image" class="cl-shortcut-image">
{{- end -}}
{{- else -}}
<svg class="cl-shortcut-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path d="M960 512c0 249.408-203.2 448-448 448-244.778667 0-448-198.592-448-448S262.592 64 512 64s448 198.592 448 448" fill="#2196F3"></path><path d="M507.52 718.08c0-8.96-4.48-13.44-13.44-17.92-26.88-8.96-53.76-8.96-76.16-31.381333-4.48-8.96-4.48-17.92-8.96-26.88-8.96-8.96-31.36-13.44-44.8-17.92h-89.6c-13.44-4.48-22.4-22.4-31.36-35.84 0-4.48 0-13.461333-8.96-13.461334-8.96-4.458667-17.92 4.501333-26.88 0-4.48-4.458667-4.48-8.96-4.48-13.418666 0-13.461333 8.96-26.901333 17.92-35.861334 13.44-8.96 26.88 4.48 40.32 4.48 4.48 0 4.48 0 8.96 4.48 13.44 4.48 17.92 22.4 17.92 35.861334v8.96c0 4.48 4.48 4.48 8.96 4.48 4.48-22.4 4.48-44.821333 8.96-67.2 0-26.88 26.88-53.781333 49.28-62.72 8.96-4.458667 13.44 4.501333 22.4 0 26.88-8.96 94.08-35.84 80.64-71.658667-8.96-31.381333-35.84-62.698667-71.68-58.24-8.96 4.501333-13.44 8.96-22.4 13.461333-13.44 8.96-40.32 35.84-53.76 35.84-22.4-4.48-22.4-35.84-17.92-49.301333 4.48-17.92 44.8-76.138667 71.68-67.178667l17.92 17.92c8.96 4.48 22.4 4.48 35.84 4.48 4.48 0 8.96 0 13.44-4.48 4.48-4.48 4.48-4.48 4.48-8.96 0-13.44-13.44-26.901333-22.4-35.861333s-22.4-17.92-35.84-22.378667c-44.8-13.461333-116.48 4.458667-152.32 35.84-35.84 31.36-62.72 85.12-80.64 129.92-8.96 26.88-17.92 62.698667-22.4 94.08-4.48 22.4-8.96 40.32 4.48 62.698667 13.44 26.88 40.32 53.781333 67.2 71.68 17.92 13.44 53.76 13.44 71.68 35.84 13.44 17.941333 8.96 40.32 8.96 62.72 0 26.88 17.92 49.28 26.88 71.658667 4.48 13.461333 8.96 31.381333 13.44 44.821333 0 4.48 4.48 31.36 4.48 35.84 26.88 13.44 49.28 26.901333 80.64 35.861333 4.48 0 22.4-26.901333 22.4-31.381333 13.44-13.44 22.4-31.36 35.84-40.32 8.96-4.48 17.92-8.96 26.88-17.941333 8.96-8.96 13.44-26.88 17.92-40.32 4.48-8.938667 8.96-26.858667 4.48-40.298667M516.48 305.92c4.48 0 8.96-4.48 17.92-8.96 13.44-8.96 26.901333-22.4 40.32-31.36 13.461333-8.96 26.901333-22.4 35.861333-31.36 13.44-8.96 22.4-26.88 26.88-40.341333 4.48-8.96 17.941333-26.88 13.44-40.32-4.48-8.96-26.88-13.44-35.84-17.92C579.2 126.698667 547.84 122.24 512 122.24c-13.44 0-31.36 4.458667-35.84 17.92-4.48 22.4 13.44 17.92 31.36 22.4 0 0 4.48 35.84 4.48 40.32 4.48 22.421333-8.96 35.84-8.96 58.24 0 13.44 0 35.84 8.96 44.8h4.48zM892.8 619.52c4.501333-8.96 4.501333-22.4 8.96-31.36 4.501333-22.421333 4.501333-44.8 4.501333-67.2 0-44.8-4.501333-89.578667-17.92-129.92-8.96-13.44-13.461333-26.88-17.941333-40.341333-8.96-22.378667-22.4-44.8-40.32-62.698667-17.92-22.4-40.341333-85.12-80.64-67.2-13.44 4.501333-22.4 22.421333-31.36 31.381333l-26.88 40.32c-4.501333 4.48-8.96 13.44-4.501333 17.92 0 4.48 4.501333 4.48 8.96 4.48 8.96 4.501333 13.461333 4.501333 22.421333 8.96 4.48 0 8.96 4.501333 4.48 8.96 0 0 0 4.501333-4.48 4.501334-22.421333 22.4-44.8 40.32-67.2 62.698666-4.48 4.48-8.96 13.44-8.96 17.92s4.48 4.48 4.48 8.96c0 4.501333-4.48 4.501333-8.96 8.96-8.96 4.501333-17.92 8.96-22.4 13.461334-4.48 8.96 0 22.4-4.48 31.36-4.48 22.4-17.941333 40.32-26.901333 62.72-8.96 13.418667-13.418667 26.88-22.378667 40.32 0 17.92-4.501333 31.36 4.458667 44.8 22.421333 31.36 62.72 13.44 94.08 26.901333 8.96 4.458667 17.92 4.458667 22.421333 13.418667 13.418667 13.461333 13.418667 35.861333 17.92 49.301333 4.458667 17.92 8.96 35.84 17.92 53.76 4.48 22.421333 13.44 44.821333 17.92 62.72 40.341333-31.36 76.16-67.178667 103.04-112 26.88-31.424 40.341333-67.242667 53.76-103.104" fill="#CDDC39"></path></svg>
{{- end -}}
{{- end -}}
</span>
{{- /* External link guard */ -}}
{{- $isGuarded := false -}}
{{- if
$config.guard.enable
| and $isExternal
| and (not (in $config.guard.allowDomains $url.Host))
-}}
{{- $isGuarded = true -}}
{{- end -}}
{{- $destination := cond $isGuarded (add (relLangURL "link") "?target=" (urlquery .Destination)) .Destination -}}
{{- $attrs := printf `href="%s"` ($destination | safeURL) -}}
{{- with .Title -}}
{{- $attrs = printf ` title="%s"` . | add $attrs -}}
{{- end -}}
{{- if $isExternal | or .Newtab -}}
{{- $attrs = add $attrs ` target="_blank"` -}}
{{- end -}}
{{- with $rel -}}
{{- $attrs = printf ` rel="%s"` (trim . " ") | add $attrs -}}
{{- end -}}
{{- with $class -}}
{{- $attrs = printf ` class="%s"` . | add $attrs -}}
{{- end -}}
{{- with .Download -}}
{{- $attrs = printf ` download="%s"` . | add $attrs -}}
{{- end -}}
{{- if $isGuarded | and (eq $config.guard.mode "modal") -}}
{{- $attrs = add $attrs ` data-guard="modal"` -}}
{{- end -}}
<a {{ $attrs | safeHTMLAttr }}>
{{- if eq .Card true -}}
{{- template "render-link-card" (dict "ctx" . "url" $url) -}}
{{- else -}}
{{- template "render-link-regular" (dict "ctx" . "config" $config "isExternal" $isExternal) -}}
{{- end -}}
</a>
{{- /* EOF */ -}}
+1 -1
View File
@@ -8,4 +8,4 @@
{{- end -}}
{{- $destination = printf (string $template) .Id -}}
{{- end -}}
{{- dict "Destination" $destination "Rel" "me" | merge . | partial "plugin/link.html" -}}
{{- dict "Destination" $destination "Rel" "me" "ExternalIcon" false | merge . | partial "plugin/link.html" -}}
+2 -1
View File
@@ -81,7 +81,8 @@
{{- $linkOptions = $linkOptions | append $options -}}
{{- end -}}
{{- range $linkOptions -}}
<span>{{ partial "plugin/link.html" . }}</span>
{{- $commonOptions := dict "ExternalIcon" false -}}
<span>{{ partial "plugin/link.html" (. | merge $commonOptions) }}</span>
{{- end -}}
</div>
<div class="post-info-share">
+1 -1
View File
@@ -16,7 +16,7 @@
{{- $icon = "" -}}
{{- end -}}
{{- if $author.link -}}
{{- $options := dict "Class" "author" "Destination" $author.link "Title" (T "single.author") "Rel" "author" "Icon" $icon "Content" $content -}}
{{- $options := dict "Class" "author" "Destination" $author.link "Title" (T "single.author") "Rel" "author" "Icon" $icon "Content" $content "ExternalIcon" false -}}
{{- partial "plugin/link.html" $options -}}
{{- else -}}
<span class="author">
+1 -1
View File
@@ -18,5 +18,5 @@
{{- $options = dict "Card" (.Get 3) | merge $options -}}
{{- $options = dict "CardIcon" (.Get 4) | merge $options -}}
{{- end -}}
{{- $options = dict "ExternalIcon" (.Get "external-icon" | default .Site.Params.externalIcon) | merge $options -}}
{{- $options = dict "ExternalIcon" (.Get "external-icon") | merge $options -}}
{{- partial "plugin/link.html" $options -}}
+30
View File
@@ -0,0 +1,30 @@
{{- define "title" -}}
{{- T "linkRedirection.title" -}}
{{- if .Site.Params.withSiteTitle }} {{ .Site.Params.titleDelimiter }} {{ .Site.Title }}{{- end -}}
{{- end -}}
{{- define "content" -}}
<article class="page" id="content-link">
<main class="card">
<h2 class="title">{{ T "linkRedirection.title" }}</h2>
<p class="message">{{ dict "Title" .Site.Title | T "linkRedirection.message" }}</p>
<div class="target-wrapper">
<p class="target">-</p>
<button type="button" class="copy-btn" aria-label="{{ T "assets.copyToClipboard" }}" disabled>
{{- dict "Class" "fa-regular fa-clone fa-width-auto" | partial "plugin/icon.html" -}}
</button>
</div>
<div class="actions">
<button type="button" class="confirm-btn" aria-label="{{ T "linkRedirection.confirm" }}" disabled>{{ T "linkRedirection.confirm" }}</button>
</div>
</main>
</article>
{{- /* link redirection page script */ -}}
{{- $fingerprint := .Site.Store.Get "fingerprint" -}}
{{- $options := dict "targetPath" "js/page/link.min.js" "minify" hugo.IsProduction -}}
{{- if not hugo.IsProduction -}}
{{- $options = dict "sourceMap" "external" | merge $options -}}
{{- end -}}
{{- dict "Source" (resources.Get "js/page/link.js") "Build" $options "Fingerprint" $fingerprint "Defer" true | dict "Page" . "Data" | partial "store/script.html" -}}
{{- end -}}
+2 -1
View File
@@ -33,7 +33,7 @@
{{- $icon := dict "Class" "fa-solid fa-share" -}}
{{- $iconTitle := cond (hasPrefix .Url "http") (printf "%v -> %v" (T "single.repost") .Url ) (T "single.repost") -}}
{{- if hasPrefix .Url "http" -}}
{{ dict "Destination" .Url "Icon" $icon "Class" "icon-repost" "Title" $iconTitle | partial "plugin/link.html" -}}
{{ dict "Destination" .Url "Icon" $icon "Class" "icon-repost" "Title" $iconTitle "ExternalIcon" false | partial "plugin/link.html" -}}
{{- else -}}
<span title="{{ $iconTitle }}" class="icon-repost">{{- $icon | partial "plugin/icon.html" -}}</span>
{{- end -}}
@@ -238,6 +238,7 @@
</aside>
{{- /* TOC Dialog */ -}}
{{- /* [todo] move to widgets.html */ -}}
{{- if $showToc -}}
<dialog id="toc-dialog" aria-labelledby="toc-dialog-title" role="dialog">
<div class="toc">
+1
View File
@@ -4,6 +4,7 @@ Disallow: /images/
Disallow: /js/
Disallow: /css/
Disallow: /*link/
Disallow: /*offline/
Disallow: /*404.html$
Disallow: /*.md$