From e12d3b98ccceb4afc95492e709503bd52541a76e Mon Sep 17 00:00:00 2001
From: Xin <5097752+imfing@users.noreply.github.com>
Date: Fri, 6 Mar 2026 22:41:40 +0000
Subject: [PATCH] refactor(search): support local/mirrored FlexSearch assets
(#956)
* feat(search): support local and mirrored FlexSearch assets
Add params.search.flexsearch.base/js overrides in search script loading.
Document offline/local and mirror-based script asset configuration examples.
* refactor(scripts): normalize remote asset URL joins
* docs(config): clarify local asset examples
* docs(i18n): add local asset config guidance
---
docs/content/docs/guide/configuration.fa.md | 78 +++++++++++++++++++
docs/content/docs/guide/configuration.ja.md | 78 +++++++++++++++++++
docs/content/docs/guide/configuration.md | 78 +++++++++++++++++++
.../content/docs/guide/configuration.zh-cn.md | 78 +++++++++++++++++++
layouts/_partials/scripts/asciinema.html | 4 +-
layouts/_partials/scripts/katex.html | 6 +-
layouts/_partials/scripts/medium-zoom.html | 2 +-
layouts/_partials/scripts/mermaid.html | 2 +-
layouts/_partials/scripts/search.html | 61 ++++++++++++---
9 files changed, 368 insertions(+), 19 deletions(-)
diff --git a/docs/content/docs/guide/configuration.fa.md b/docs/content/docs/guide/configuration.fa.md
index d8264bb..3ceae51 100644
--- a/docs/content/docs/guide/configuration.fa.md
+++ b/docs/content/docs/guide/configuration.fa.md
@@ -340,6 +340,73 @@ params:
# js: "js/medium-zoom.min.js" # اختیاری، نسبت به base یا assetهای محلی
```
+### اسکریپتهای محلی و آینهشده
+
+Hextra میتواند وابستگیهای اختیاری فرانتاند را از منابع مختلف بارگیری کند:
+
+- تنظیمات پیشفرض قالب (CDN)
+- URLهای آینه داخلی از طریق `base`
+- assetهای محلی Hugo از طریق `js` / `css`
+
+برای assetهای محلی، فایلهای vendor را در پوشه `assets/` سایت خود قرار دهید و مقادیر پیکربندی را به همان مسیرهای asset اشاره دهید:
+
+```yaml {filename="hugo.yaml"}
+params:
+ imageZoom:
+ enable: true
+ js: "js/vendor/medium-zoom.min.js"
+
+ mermaid:
+ js: "js/vendor/mermaid.min.js"
+
+ asciinema:
+ js: "js/vendor/asciinema-player.min.js"
+ css: "css/vendor/asciinema-player.css"
+
+ math:
+ engine: katex
+ katex:
+ css: "css/vendor/katex.min.css"
+ assets:
+ - "fonts/KaTeX_Main-Regular.woff2"
+ - "fonts/KaTeX_Math-Italic.woff2"
+
+ search:
+ type: flexsearch
+ flexsearch:
+ js: "js/vendor/flexsearch.bundle.min.js"
+```
+
+`imageZoom.enable: true` فقط به این دلیل لازم است که بزرگنمایی تصویر بهصورت پیشفرض غیرفعال است.
+برای KaTeX، مطمئن شوید همه فایلهای فونتی که فایل CSS انتخابی شما به آنها ارجاع میدهد منتشر میشوند، نه فقط دو موردی که در این مثال آمدهاند.
+
+برای استفاده از یک آینه داخلی، `base` را تنظیم کنید (و در صورت تفاوت نام فایل، در صورت نیاز `js` / `css` را نیز مشخص کنید):
+
+```yaml {filename="hugo.yaml"}
+params:
+ imageZoom:
+ base: "https://mirror.example.com/medium-zoom/dist"
+
+ mermaid:
+ base: "https://mirror.example.com/mermaid/dist"
+
+ asciinema:
+ base: "https://mirror.example.com/asciinema-player/dist/bundle"
+
+ math:
+ engine: katex
+ katex:
+ base: "https://mirror.example.com/katex/dist"
+
+ search:
+ flexsearch:
+ base: "https://mirror.example.com/flexsearch/dist"
+ # js: "flexsearch.bundle.min.js"
+```
+
+> [!NOTE]
+> برای سفارشیسازی منبع بارگذاری MathJax، قالب `layouts/_partials/scripts/mathjax.html` را در پروژه خود بازنویسی کنید.
+
### عرض صفحه
عرض پوستهٔ صفحه را میتوان با پارامتر `params.page.width` تنظیم کرد:
@@ -440,6 +507,17 @@ params:
index: content
```
+همچنین میتوانید محل بارگیری runtime مربوط به FlexSearch را کنترل کنید:
+
+```yaml {filename="hugo.yaml"}
+params:
+ search:
+ flexsearch:
+ version: "0.8.143" # نسخه پیشفرض CDN
+ # base: "https://mirror.example.com/flexsearch/dist" # آدرس پایهٔ remote اختیاری
+ # js: "js/vendor/flexsearch.bundle.min.js" # مسیر asset محلی یا فایل سفارشی زیر base راه دور
+```
+
گزینههای `flexsearch.index`:
- `content` - محتوای کامل صفحه (پیشفرض)
diff --git a/docs/content/docs/guide/configuration.ja.md b/docs/content/docs/guide/configuration.ja.md
index 9175796..b273944 100644
--- a/docs/content/docs/guide/configuration.ja.md
+++ b/docs/content/docs/guide/configuration.ja.md
@@ -340,6 +340,73 @@ params:
# js: "js/medium-zoom.min.js" # オプション、base またはローカルアセットからの相対パス
```
+### ローカルおよびミラー済みスクリプトアセット
+
+Hextra はオプションのフロントエンド依存ファイルを複数のソースから読み込めます:
+
+- テーマのデフォルト設定(CDN)
+- `base` を使った社内ミラー URL
+- `js` / `css` を使った Hugo のローカルアセット
+
+ローカルアセットを使う場合は、vendor ファイルをサイトの `assets/` ディレクトリに配置し、そのアセットパスを設定値に指定します:
+
+```yaml {filename="hugo.yaml"}
+params:
+ imageZoom:
+ enable: true
+ js: "js/vendor/medium-zoom.min.js"
+
+ mermaid:
+ js: "js/vendor/mermaid.min.js"
+
+ asciinema:
+ js: "js/vendor/asciinema-player.min.js"
+ css: "css/vendor/asciinema-player.css"
+
+ math:
+ engine: katex
+ katex:
+ css: "css/vendor/katex.min.css"
+ assets:
+ - "fonts/KaTeX_Main-Regular.woff2"
+ - "fonts/KaTeX_Math-Italic.woff2"
+
+ search:
+ type: flexsearch
+ flexsearch:
+ js: "js/vendor/flexsearch.bundle.min.js"
+```
+
+`imageZoom.enable: true` が必要なのは、画像ズームがデフォルトで無効になっているためです。
+KaTeX については、この例の 2 つだけでなく、選択した CSS が参照するすべてのフォントファイルを公開してください。
+
+社内ミラーを使う場合は `base` を設定し、ファイル名が異なる場合のみ `js` / `css` を追加してください:
+
+```yaml {filename="hugo.yaml"}
+params:
+ imageZoom:
+ base: "https://mirror.example.com/medium-zoom/dist"
+
+ mermaid:
+ base: "https://mirror.example.com/mermaid/dist"
+
+ asciinema:
+ base: "https://mirror.example.com/asciinema-player/dist/bundle"
+
+ math:
+ engine: katex
+ katex:
+ base: "https://mirror.example.com/katex/dist"
+
+ search:
+ flexsearch:
+ base: "https://mirror.example.com/flexsearch/dist"
+ # js: "flexsearch.bundle.min.js"
+```
+
+> [!NOTE]
+> MathJax の読み込み元をカスタマイズするには、プロジェクト内で `layouts/_partials/scripts/mathjax.html` を上書きしてください。
+
### ページ幅
レイアウト全体の幅は `params.page.width` で設定できます:
@@ -440,6 +507,17 @@ params:
index: content
```
+FlexSearch ランタイムの読み込み元も制御できます:
+
+```yaml {filename="hugo.yaml"}
+params:
+ search:
+ flexsearch:
+ version: "0.8.143" # デフォルトの CDN バージョン
+ # base: "https://mirror.example.com/flexsearch/dist" # オプションのリモート base URL
+ # js: "js/vendor/flexsearch.bundle.min.js" # ローカルアセットのパス、またはリモート base 配下のカスタムファイル
+```
+
`flexsearch.index` のオプション:
- `content` - ページの全文(デフォルト)
diff --git a/docs/content/docs/guide/configuration.md b/docs/content/docs/guide/configuration.md
index 8d74d9d..0be8fc0 100644
--- a/docs/content/docs/guide/configuration.md
+++ b/docs/content/docs/guide/configuration.md
@@ -362,6 +362,73 @@ params:
# js: "js/medium-zoom.min.js" # optional, relative to the base or local assets
```
+### Local and Mirrored Script Assets
+
+Hextra can load optional frontend dependencies from different sources:
+
+- Theme defaults (CDN)
+- Internal mirror URLs via `base`
+- Local Hugo assets via `js` / `css`
+
+For local assets, place vendor files under your site's `assets/` directory and point config values to those asset paths:
+
+```yaml {filename="hugo.yaml"}
+params:
+ imageZoom:
+ enable: true
+ js: "js/vendor/medium-zoom.min.js"
+
+ mermaid:
+ js: "js/vendor/mermaid.min.js"
+
+ asciinema:
+ js: "js/vendor/asciinema-player.min.js"
+ css: "css/vendor/asciinema-player.css"
+
+ math:
+ engine: katex
+ katex:
+ css: "css/vendor/katex.min.css"
+ assets:
+ - "fonts/KaTeX_Main-Regular.woff2"
+ - "fonts/KaTeX_Math-Italic.woff2"
+
+ search:
+ type: flexsearch
+ flexsearch:
+ js: "js/vendor/flexsearch.bundle.min.js"
+```
+
+`imageZoom.enable: true` is only needed here because image zoom is disabled by default.
+For KaTeX, make sure to publish all font files referenced by your chosen CSS file, not just the two shown in this example.
+
+To use an internal mirror instead, set `base` (and optionally `js` / `css` when the filename differs):
+
+```yaml {filename="hugo.yaml"}
+params:
+ imageZoom:
+ base: "https://mirror.example.com/medium-zoom/dist"
+
+ mermaid:
+ base: "https://mirror.example.com/mermaid/dist"
+
+ asciinema:
+ base: "https://mirror.example.com/asciinema-player/dist/bundle"
+
+ math:
+ engine: katex
+ katex:
+ base: "https://mirror.example.com/katex/dist"
+
+ search:
+ flexsearch:
+ base: "https://mirror.example.com/flexsearch/dist"
+ # js: "flexsearch.bundle.min.js"
+```
+
+> [!NOTE]
+> To customize MathJax source loading, override `layouts/_partials/scripts/mathjax.html` in your site.
+
### Page Width
The layout shell width can be customized by the `params.page.width` parameter in the config file:
@@ -462,6 +529,17 @@ params:
index: content
```
+You can also control where the FlexSearch runtime is loaded from:
+
+```yaml {filename="hugo.yaml"}
+params:
+ search:
+ flexsearch:
+ version: "0.8.143" # default CDN version
+ # base: "https://mirror.example.com/flexsearch/dist" # optional remote base URL
+ # js: "js/vendor/flexsearch.bundle.min.js" # local asset path, or custom file under remote base
+```
+
Options for `flexsearch.index`:
- `content` - full content of the page (default)
diff --git a/docs/content/docs/guide/configuration.zh-cn.md b/docs/content/docs/guide/configuration.zh-cn.md
index e936b29..f689e89 100644
--- a/docs/content/docs/guide/configuration.zh-cn.md
+++ b/docs/content/docs/guide/configuration.zh-cn.md
@@ -340,6 +340,73 @@ params:
# js: "js/medium-zoom.min.js" # 可选,相对于 base 或本地资源
```
+### 本地与镜像脚本资源
+
+Hextra 可以从多种来源加载可选的前端依赖:
+
+- 主题默认配置(CDN)
+- 通过 `base` 指定的内部镜像 URL
+- 通过 `js` / `css` 指定的 Hugo 本地资源
+
+对于本地资源,请将 vendor 文件放在站点的 `assets/` 目录下,并在配置中引用这些资源路径:
+
+```yaml {filename="hugo.yaml"}
+params:
+ imageZoom:
+ enable: true
+ js: "js/vendor/medium-zoom.min.js"
+
+ mermaid:
+ js: "js/vendor/mermaid.min.js"
+
+ asciinema:
+ js: "js/vendor/asciinema-player.min.js"
+ css: "css/vendor/asciinema-player.css"
+
+ math:
+ engine: katex
+ katex:
+ css: "css/vendor/katex.min.css"
+ assets:
+ - "fonts/KaTeX_Main-Regular.woff2"
+ - "fonts/KaTeX_Math-Italic.woff2"
+
+ search:
+ type: flexsearch
+ flexsearch:
+ js: "js/vendor/flexsearch.bundle.min.js"
+```
+
+这里只有因为图片缩放默认关闭,才需要设置 `imageZoom.enable: true`。
+对于 KaTeX,请确保发布你所选 CSS 文件引用的全部字体文件,而不仅仅是此示例中的两个。
+
+如果要使用内部镜像,请设置 `base`;只有当文件名不同时,才需要额外设置 `js` / `css`:
+
+```yaml {filename="hugo.yaml"}
+params:
+ imageZoom:
+ base: "https://mirror.example.com/medium-zoom/dist"
+
+ mermaid:
+ base: "https://mirror.example.com/mermaid/dist"
+
+ asciinema:
+ base: "https://mirror.example.com/asciinema-player/dist/bundle"
+
+ math:
+ engine: katex
+ katex:
+ base: "https://mirror.example.com/katex/dist"
+
+ search:
+ flexsearch:
+ base: "https://mirror.example.com/flexsearch/dist"
+ # js: "flexsearch.bundle.min.js"
+```
+
+> [!NOTE]
+> 如需自定义 MathJax 的加载来源,请在项目中覆盖 `layouts/_partials/scripts/mathjax.html`。
+
### 页面宽度
页面整体布局宽度可通过 `params.page.width` 配置:
@@ -440,6 +507,17 @@ params:
index: content
```
+你也可以控制 FlexSearch runtime 的加载来源:
+
+```yaml {filename="hugo.yaml"}
+params:
+ search:
+ flexsearch:
+ version: "0.8.143" # 默认 CDN 版本
+ # base: "https://mirror.example.com/flexsearch/dist" # 可选的远程 base URL
+ # js: "js/vendor/flexsearch.bundle.min.js" # 本地资源路径,或远程 base 下的自定义文件
+```
+
`flexsearch.index` 的选项:
- `content` - 页面的完整内容(默认)
diff --git a/layouts/_partials/scripts/asciinema.html b/layouts/_partials/scripts/asciinema.html
index f982699..6db5a8b 100644
--- a/layouts/_partials/scripts/asciinema.html
+++ b/layouts/_partials/scripts/asciinema.html
@@ -33,7 +33,7 @@
{{- /* CSS retrieval: get raw CSS from either local asset or remote, then process */ -}}
{{- if $isRemoteBase -}}
{{- $cssPath := cond (ne $asciinemaCssAsset "") $asciinemaCssAsset "asciinema-player.css" -}}
- {{- $asciinemaCssUrl := printf "%s/%s" $asciinemaBase $cssPath -}}
+ {{- $asciinemaCssUrl := urls.JoinPath $asciinemaBase $cssPath -}}
{{- with try (resources.GetRemote $asciinemaCssUrl) -}}
{{- with .Err -}}
{{- errorf "Could not retrieve Asciinema css file from %s. Reason: %s." $asciinemaCssUrl . -}}
@@ -56,7 +56,7 @@
{{- /* JS retrieval: get raw JS from either local asset or remote, then process */ -}}
{{- if $isRemoteBase -}}
{{- $jsPath := cond (ne $asciinemaJsAsset "") $asciinemaJsAsset (printf "asciinema-player%s.js" $minSuffix) -}}
- {{- $asciinemaJsUrl := printf "%s/%s" $asciinemaBase $jsPath -}}
+ {{- $asciinemaJsUrl := urls.JoinPath $asciinemaBase $jsPath -}}
{{- with try (resources.GetRemote $asciinemaJsUrl) -}}
{{- with .Err -}}
{{- errorf "Could not retrieve Asciinema js file from %s. Reason: %s." $asciinemaJsUrl . -}}
diff --git a/layouts/_partials/scripts/katex.html b/layouts/_partials/scripts/katex.html
index 2cd24fc..add36c0 100644
--- a/layouts/_partials/scripts/katex.html
+++ b/layouts/_partials/scripts/katex.html
@@ -39,7 +39,7 @@
{{- $minSuffix := cond hugo.IsProduction ".min" "" -}}
{{- if $isRemoteBase -}}
{{- $cssPath := cond (ne $katexCssAsset "") $katexCssAsset (printf "katex%s.css" $minSuffix) -}}
- {{- $katexCssUrl := printf "%s/%s" $katexBase $cssPath -}}
+ {{- $katexCssUrl := urls.JoinPath $katexBase $cssPath -}}
{{- with try (resources.GetRemote $katexCssUrl) -}}
{{- with .Err -}}
{{- errorf "Could not retrieve KaTeX css file from %s. Reason: %s." $katexCssUrl . -}}
@@ -59,7 +59,7 @@
{{- $cssContent := . -}}
{{- if $isRemoteBase -}}
{{- $fontPattern := "url(fonts/" -}}
- {{- $fontSub := printf "url(%s/fonts/" $katexBase -}}
+ {{- $fontSub := printf "url(%s/" (urls.JoinPath $katexBase "fonts") -}}
{{- $cssContent = strings.Replace $cssContent $fontPattern $fontSub -}}
{{- end -}}
{{- with resources.FromString (printf "css/katex%s.css" $minSuffix) $cssContent -}}
@@ -69,7 +69,7 @@
{{- else -}}
{{- if not $isRemoteBase -}}
{{- $cssPath := cond (ne $katexCssAsset "") $katexCssAsset (printf "katex%s.css" $minSuffix) -}}
-
+
{{- end -}}
{{- end -}}
diff --git a/layouts/_partials/scripts/medium-zoom.html b/layouts/_partials/scripts/medium-zoom.html
index 6203442..c11925c 100644
--- a/layouts/_partials/scripts/medium-zoom.html
+++ b/layouts/_partials/scripts/medium-zoom.html
@@ -28,7 +28,7 @@
{{- /* JS retrieval: get raw JS from either local asset or remote, then process */ -}}
{{- if $isRemoteBase -}}
{{- $jsPath := cond (ne $zoomJsAsset "") $zoomJsAsset (printf "medium-zoom%s.js" $minSuffix) -}}
- {{- $zoomJsUrl := printf "%s/%s" $zoomBase $jsPath -}}
+ {{- $zoomJsUrl := urls.JoinPath $zoomBase $jsPath -}}
{{- with try (resources.GetRemote $zoomJsUrl) -}}
{{- with .Err -}}
{{- errorf "Could not retrieve Medium Zoom js file from %s. Reason: %s." $zoomJsUrl . -}}
diff --git a/layouts/_partials/scripts/mermaid.html b/layouts/_partials/scripts/mermaid.html
index b4f957b..60bc230 100644
--- a/layouts/_partials/scripts/mermaid.html
+++ b/layouts/_partials/scripts/mermaid.html
@@ -28,7 +28,7 @@
{{- /* JS retrieval: get raw JS from either local asset or remote, then process */ -}}
{{- if $isRemoteBase -}}
{{- $jsPath := cond (ne $mermaidJsAsset "") $mermaidJsAsset (printf "mermaid%s.js" $minSuffix) -}}
- {{- $mermaidJsUrl := printf "%s/%s" $mermaidBase $jsPath -}}
+ {{- $mermaidJsUrl := urls.JoinPath $mermaidBase $jsPath -}}
{{- with try (resources.GetRemote $mermaidJsUrl) -}}
{{- with .Err -}}
{{- errorf "Could not retrieve Mermaid js file from %s. Reason: %s." $mermaidJsUrl . -}}
diff --git a/layouts/_partials/scripts/search.html b/layouts/_partials/scripts/search.html
index 15ec9a0..363b3bc 100644
--- a/layouts/_partials/scripts/search.html
+++ b/layouts/_partials/scripts/search.html
@@ -7,18 +7,55 @@
{{- if hugo.IsProduction -}}
{{- $jsSearch = $jsSearch | minify | fingerprint -}}
{{- end -}}
- {{- $flexSearchVersion := site.Params.search.flexsearch.version | default "0.8.143" -}}
- {{- $flexSearchJsUrl := printf "https://cdn.jsdelivr.net/npm/flexsearch@%s/dist/flexsearch.bundle%s.js" $flexSearchVersion (cond hugo.IsProduction ".min" ".debug") -}}
- {{ with try (resources.GetRemote $flexSearchJsUrl) -}}
- {{ with .Err -}}
- {{ errorf "Could not retrieve FlexSearch js file from %s. Reason: %s." $flexSearchJsUrl . -}}
- {{ else with.Value -}}
- {{ with resources.Copy (printf "js/flexsearch.js") . -}}
- {{ $flexSearchJs := . | fingerprint -}}
-
- {{ end -}}
- {{ end -}}
- {{ end -}}
+
+ {{- $flexSearchBase := "" -}}
+ {{- $useDefaultCdn := true -}}
+ {{- with site.Params.search.flexsearch.base -}}
+ {{- $flexSearchBase = . -}}
+ {{- $useDefaultCdn = false -}}
+ {{- end -}}
+
+ {{- $flexSearchJsAsset := "" -}}
+ {{- with site.Params.search.flexsearch.js -}}
+ {{- $flexSearchJsAsset = . -}}
+ {{- end -}}
+
+ {{- /* If only js is set without base, use local asset loading. */ -}}
+ {{- if and $useDefaultCdn (ne $flexSearchJsAsset "") -}}
+ {{- $useDefaultCdn = false -}}
+ {{- end -}}
+
+ {{- $bundleSuffix := cond hugo.IsProduction ".min" ".debug" -}}
+ {{- if $useDefaultCdn -}}
+ {{- $flexSearchVersion := site.Params.search.flexsearch.version | default "0.8.143" -}}
+ {{- $flexSearchBase = printf "https://cdn.jsdelivr.net/npm/flexsearch@%s/dist" $flexSearchVersion -}}
+ {{- end -}}
+
+ {{- $isRemoteBase := or (strings.HasPrefix $flexSearchBase "http://") (strings.HasPrefix $flexSearchBase "https://") -}}
+ {{- if $isRemoteBase -}}
+ {{- $jsPath := cond (ne $flexSearchJsAsset "") $flexSearchJsAsset (printf "flexsearch.bundle%s.js" $bundleSuffix) -}}
+ {{- $flexSearchJsUrl := urls.JoinPath $flexSearchBase $jsPath -}}
+ {{- with try (resources.GetRemote $flexSearchJsUrl) -}}
+ {{- with .Err -}}
+ {{- errorf "Could not retrieve FlexSearch js file from %s. Reason: %s." $flexSearchJsUrl . -}}
+ {{- else with .Value -}}
+ {{- with resources.Copy "js/flexsearch.js" . -}}
+ {{- $flexSearchJs := . | fingerprint -}}
+
+ {{- end -}}
+ {{- end -}}
+ {{- end -}}
+ {{- else if $flexSearchJsAsset -}}
+ {{- with resources.Get $flexSearchJsAsset -}}
+ {{- $flexSearchJs := . | fingerprint -}}
+
+ {{- else -}}
+ {{- errorf "FlexSearch js asset not found at %q" $flexSearchJsAsset -}}
+ {{- end -}}
+ {{- else if not $useDefaultCdn -}}
+ {{- errorf "FlexSearch local loading requires params.search.flexsearch.js when using non-remote base %q" $flexSearchBase -}}
+ {{- end -}}
+
{{- else -}}
{{- warnf `search type "%s" is not supported` $searchType -}}