{{ T "linkRedirection.title" }}
+ +-
+ +diff --git a/apps/test/content/posts/link-test.md b/apps/test/content/posts/link-test.md new file mode 100644 index 00000000..f76256bb --- /dev/null +++ b/apps/test/content/posts/link-test.md @@ -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. + + + +## 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. diff --git a/apps/test/hugo.toml b/apps/test/hugo.toml index 7c0bf6a6..5445dbcd 100644 --- a/apps/test/hugo.toml +++ b/apps/test/hugo.toml @@ -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 diff --git a/assets/css/_core/_base.scss b/assets/css/_core/_base.scss index cc1e24d2..dacbbf25 100644 --- a/assets/css/_core/_base.scss +++ b/assets/css/_core/_base.scss @@ -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; } diff --git a/assets/css/_core/_mixin/_index.scss b/assets/css/_core/_mixin/_index.scss index d7f77fc6..45696ca4 100644 --- a/assets/css/_core/_mixin/_index.scss +++ b/assets/css/_core/_mixin/_index.scss @@ -3,6 +3,7 @@ @import 'box-shadow'; @import 'compatibility'; @import 'link'; +@import 'link-guard'; @import 'loading'; @import 'media'; @import 'scrollbar-width'; diff --git a/assets/css/_core/_mixin/_link-guard.scss b/assets/css/_core/_mixin/_link-guard.scss new file mode 100644 index 00000000..88314262 --- /dev/null +++ b/assets/css/_core/_mixin/_link-guard.scss @@ -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); + } +} diff --git a/assets/css/_core/_widgets/_index.scss b/assets/css/_core/_widgets/_index.scss index 978d608a..bea665ad 100644 --- a/assets/css/_core/_widgets/_index.scss +++ b/assets/css/_core/_widgets/_index.scss @@ -4,6 +4,7 @@ @import 'fixed-button'; @import 'github-corner'; @import 'icon'; +@import 'link-guard'; @import 'mask'; @import 'noscript-warning'; @import 'post-chat'; diff --git a/assets/css/_core/_widgets/_link-guard.scss b/assets/css/_core/_widgets/_link-guard.scss new file mode 100644 index 00000000..e42addef --- /dev/null +++ b/assets/css/_core/_widgets/_link-guard.scss @@ -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; + } + } + } +} diff --git a/assets/css/_page/_index.scss b/assets/css/_page/_index.scss index 20187772..3096e912 100644 --- a/assets/css/_page/_index.scss +++ b/assets/css/_page/_index.scss @@ -41,7 +41,8 @@ @import '_single'; @import '_special'; @import '_archive'; -@import 'home'; @import '404'; +@import 'home'; +@import 'link'; @import 'offline'; @import "patch"; diff --git a/assets/css/_page/_link.scss b/assets/css/_page/_link.scss new file mode 100644 index 00000000..634ade52 --- /dev/null +++ b/assets/css/_page/_link.scss @@ -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)); + } +} diff --git a/assets/css/_page/_single/_toc.scss b/assets/css/_page/_single/_toc.scss index b4da861a..ad0f018d 100644 --- a/assets/css/_page/_single/_toc.scss +++ b/assets/css/_page/_single/_toc.scss @@ -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; -} diff --git a/assets/js/page/link.js b/assets/js/page/link.js new file mode 100644 index 00000000..95d1e833 --- /dev/null +++ b/assets/js/page/link.js @@ -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); +} diff --git a/assets/js/theme.js b/assets/js/theme.js index 1868d209..c2e15a9f 100644 --- a/assets/js/theme.js +++ b/assets/js/theme.js @@ -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?.(); diff --git a/hugo.toml b/hugo.toml index b097968f..8456e26d 100644 --- a/hugo.toml +++ b/hugo.toml @@ -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 diff --git a/i18n/de.toml b/i18n/de.toml index 02aa17ae..26f0335c 100644 --- a/i18n/de.toml +++ b/i18n/de.toml @@ -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" diff --git a/i18n/en.toml b/i18n/en.toml index f883ae09..8a921585 100644 --- a/i18n/en.toml +++ b/i18n/en.toml @@ -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" diff --git a/i18n/es.toml b/i18n/es.toml index 832665a9..c80d62b3 100644 --- a/i18n/es.toml +++ b/i18n/es.toml @@ -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" diff --git a/i18n/fr.toml b/i18n/fr.toml index 6d009a28..6ef00792 100644 --- a/i18n/fr.toml +++ b/i18n/fr.toml @@ -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" diff --git a/i18n/hi.toml b/i18n/hi.toml index d1bcc7da..6b0d976b 100644 --- a/i18n/hi.toml +++ b/i18n/hi.toml @@ -184,6 +184,13 @@ other = "ऑफलाइन" other = "आप इंटरनेट से कनेक्ट नहीं हैं, केवल कैश्ड पेज ही उपलब्ध होंगे।" # === offline === +# === link redirection === +[linkRedirection] +title = "रीडायरेक्शन सूचना" +message = "आप {{ .Title }} छोड़ने वाले हैं, कृपया अपने खाते और संपत्ति की सुरक्षा पर ध्यान दें।" +confirm = "देखना जारी रखें" +# === link redirection === + # === GitHub Alert === [alert] note = "ध्यान दें" diff --git a/i18n/it.toml b/i18n/it.toml index fc502f96..b7678cc9 100644 --- a/i18n/it.toml +++ b/i18n/it.toml @@ -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" diff --git a/i18n/ja.toml b/i18n/ja.toml index 8a9f5615..bb5b50d8 100644 --- a/i18n/ja.toml +++ b/i18n/ja.toml @@ -179,6 +179,13 @@ other = "オフライン" other = "インターネットに接続されていません。キャッシュされたページのみが利用可能です。" # === offline === +# === link redirection === +[linkRedirection] +title = "リダイレクト通知" +message = "{{ .Title }} から離れようとしています。アカウントと資産の安全にご注意ください。" +confirm = "閲覧を続ける" +# === link redirection === + # === GitHub Alert === [alert] note = "注意" diff --git a/i18n/ko.toml b/i18n/ko.toml index ef9fa258..963e46fc 100644 --- a/i18n/ko.toml +++ b/i18n/ko.toml @@ -179,6 +179,13 @@ other = "오프라인" other = "인터넷에 연결되어 있지 않으며, 캐시된 페이지만 사용할 수 있습니다." # === offline === +# === link redirection === +[linkRedirection] +title = "리디렉션 안내" +message = "{{ .Title }}에서 벗어나려고 합니다. 계정과 자산 보안에 유의해 주세요." +confirm = "계속 방문" + +# === link redirection === # === GitHub Alert === [alert] note = "노트" diff --git a/i18n/pl.toml b/i18n/pl.toml index af4388b7..c485869f 100644 --- a/i18n/pl.toml +++ b/i18n/pl.toml @@ -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" diff --git a/i18n/pt-BR.toml b/i18n/pt-BR.toml index a40abb9c..b97d9fde 100644 --- a/i18n/pt-BR.toml +++ b/i18n/pt-BR.toml @@ -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" diff --git a/i18n/ro.toml b/i18n/ro.toml index 0b5d05cc..04b6f051 100644 --- a/i18n/ro.toml +++ b/i18n/ro.toml @@ -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ă" diff --git a/i18n/ru.toml b/i18n/ru.toml index c28b975f..b8932b88 100644 --- a/i18n/ru.toml +++ b/i18n/ru.toml @@ -183,6 +183,13 @@ other = "Не в сети" other = "Вы не подключены к интернету, будут доступны только кешированные страницы." # === offline === +# === link redirection === +[linkRedirection] +title = "Уведомление о перенаправлении" +message = "Вы собираетесь покинуть {{ .Title }}. Пожалуйста, обратите внимание на безопасность вашего аккаунта и имущества." +confirm = "Продолжить посещение" +# === link redirection === + # === GitHub Alert === [alert] note = "Примечание" diff --git a/i18n/sr.toml b/i18n/sr.toml index 14d0343f..84eac1ce 100644 --- a/i18n/sr.toml +++ b/i18n/sr.toml @@ -183,6 +183,13 @@ other = "Оффлине" other = "Нисте повезани на Интернет, биће доступне само кеширане странице." # === offline === +# === link redirection === +[linkRedirection] +title = "Обавештење о преусмеравању" +message = "Управо ћете напустити {{ .Title }}. Обратите пажњу на безбедност свог налога и имовине." +confirm = "Настави посету" +# === link redirection === + # === GitHub Alert === [alert] note = "Напомена" diff --git a/i18n/vi.toml b/i18n/vi.toml index fb3d2f4a..f41831ec 100644 --- a/i18n/vi.toml +++ b/i18n/vi.toml @@ -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ú" diff --git a/i18n/zh-CN.toml b/i18n/zh-CN.toml index 3cb58015..05f90eaf 100644 --- a/i18n/zh-CN.toml +++ b/i18n/zh-CN.toml @@ -179,6 +179,13 @@ other = "离线" other = "你没有连接到 Internet,只有缓存的页面可用。" # === offline === +# === link redirection === +[linkRedirection] +title = "跳转提示" +message = "即将离开{{ .Title }},请注意账号财产安全。" +confirm = "继续访问" +# === link redirection === + # === GitHub Alert === [alert] note = "注意" diff --git a/i18n/zh-TW.toml b/i18n/zh-TW.toml index 6705d72e..121ccf53 100644 --- a/i18n/zh-TW.toml +++ b/i18n/zh-TW.toml @@ -179,6 +179,13 @@ other = "離線" other = "你沒有連接到 Internet,只有緩存的頁面可用。" # === offline === +# === link redirection === +[linkRedirection] +title = "跳轉提示" +message = "即將離開{{ .Title }},請注意帳號財產安全。" +confirm = "繼續訪問" +# === link redirection === + # === GitHub Alert === [alert] note = "注意" diff --git a/layouts/_markup/render-link.html b/layouts/_markup/render-link.html index 26b1ea68..c53d175b 100644 --- a/layouts/_markup/render-link.html +++ b/layouts/_markup/render-link.html @@ -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 -}} diff --git a/layouts/_partials/_debug/dump.html b/layouts/_partials/_debug/dump.html new file mode 100644 index 00000000..6f6de489 --- /dev/null +++ b/layouts/_partials/_debug/dump.html @@ -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 -}} +
{{ debug.Dump . }}
+{{- end -}}
diff --git a/layouts/_partials/_debug/template-call-stack.html b/layouts/_partials/_debug/template-call-stack.html
index f9d5543a..0f91b13f 100644
--- a/layouts/_partials/_debug/template-call-stack.html
+++ b/layouts/_partials/_debug/template-call-stack.html
@@ -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 -}}
diff --git a/layouts/_partials/function/get-file-tree.html b/layouts/_partials/function/get-file-tree.html
index 192c4102..08b2dac6 100644
--- a/layouts/_partials/function/get-file-tree.html
+++ b/layouts/_partials/function/get-file-tree.html
@@ -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 -}}
diff --git a/layouts/_partials/layout/footer.html b/layouts/_partials/layout/footer.html
index 862501fb..326806fa 100644
--- a/layouts/_partials/layout/footer.html
+++ b/layouts/_partials/layout/footer.html
@@ -42,7 +42,7 @@
{{- /* Author */ -}}
{{- if ne $footerConfig.author false -}}
{{- end -}}
diff --git a/layouts/_partials/layout/widgets.html b/layouts/_partials/layout/widgets.html
index c87bdb28..103bbf8a 100644
--- a/layouts/_partials/layout/widgets.html
+++ b/layouts/_partials/layout/widgets.html
@@ -36,7 +36,7 @@
{{- if $githubCorner.enable -}}
{{- with $githubCorner -}}
{{- $githubImage := `` -}}
- {{- $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 -}}
+
+ {{- end -}}
+
{{- /* Custom widgets */ -}}
{{- block "custom-widgets" . }}{{ end -}}
diff --git a/layouts/_partials/plugin/link.html b/layouts/_partials/plugin/link.html
index 48dad275..60fca7bc 100644
--- a/layouts/_partials/plugin/link.html
+++ b/layouts/_partials/plugin/link.html
@@ -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 `