Compare commits

...

30 Commits

Author SHA1 Message Date
雨奇晴好
d25776ea38
Fix 1px scroll offset on page navigation when Pjax is enabled (#995) 2026-09-08 10:35:06 +08:00
Mimi
3e4d43597d Update _vendors.yml 2026-09-02 09:26:54 +08:00
Mimi
6037f68b7a New animation next-bounce 2026-08-25 01:36:07 +08:00
Mimi
6468f139cb Update default icon 2026-08-20 22:10:51 +08:00
Mimi
d62fdfa759 Simplify site-nav-toggle element 2026-08-20 14:10:46 +08:00
Mimi
96d34a0bb9 Update site-nav-toggle style 2026-08-20 11:12:58 +08:00
Mimi
6560fa6dd6 Fix sidebar-dimmer animation 2026-08-14 17:39:50 +08:00
Mimi
66e3095a31 Fix search-popup animation using template 2026-08-14 16:45:16 +08:00
Mimi
120d41d0fa Fix preload condition for theme preconnect option 2026-08-14 01:20:31 +08:00
Mimi
4b249983ed Release v8.29.0 2026-08-05 15:05:48 +08:00
where where
ba0ab0e8f7
Support optional async and defer attributes in script helpers (#952)
Co-authored-by: Mimi <1119186082@qq.com>
2026-08-05 15:03:54 +08:00
renovate[bot]
41c19bfc47
Update dependency mocha to v11.8.0 (#980)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-05 13:03:55 +08:00
renovate[bot]
88cdd52f52
Update dependency js-yaml to v4.3.0 [SECURITY] (#979)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-05 12:58:07 +08:00
renovate[bot]
b3ac16962d
Update actions/labeler action to v7 (#977)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-05 12:57:52 +08:00
renovate[bot]
5b71ed5eae
Update actions/setup-python action to v7 (#976)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-05 12:57:40 +08:00
renovate[bot]
5a476ca06f
Update dependency c8 to v12 (#975)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-05 12:57:29 +08:00
renovate[bot]
abf7ec2aeb
Update actions/setup-node action to v7 (#974)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-05 12:57:15 +08:00
renovate[bot]
f7f13675e5
Update dependency eslint to v10.8.0 (#972)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-05 12:56:54 +08:00
Mimi
db11583d55 Update _vendors.yml 2026-08-05 12:04:44 +08:00
Mimi
abd6a790ba
Configure MathJax overflow handling (#986) 2026-08-05 11:07:25 +08:00
Mimi
c0232764b3
Support MathJax 4 (#985) 2026-08-05 10:28:55 +08:00
Mimi
091af0fd4f
Fix TOC height calculation for wrapped headings (#984) 2026-08-05 01:15:32 +08:00
Mimi
854e2006b6
Disable Fancybox hash handling with PJAX (#983) 2026-08-04 17:26:12 +08:00
Mimi
5c7574fd3f Improve Algolia search concurrency 2026-08-04 14:55:10 +08:00
Mimi
4352696dba refactor: use Firestore REST API 2026-08-04 14:21:17 +08:00
Mimi
940b2263f6 Deprecate body_scrollbar.stable option 2026-08-04 12:32:47 +08:00
Mimi
8fa97a173a
Remove animejs (#982) 2026-08-03 22:51:17 +08:00
Mimi
1b306b9824
Replace Anime.js scrolling with native APIs (#981) 2026-08-03 18:30:05 +08:00
Mimi
d54ab89ea2 Deprecate lazyload option 2026-08-02 23:57:30 +08:00
Mimi
333b2c7908 Update Netlify logo URL 2026-07-01 21:54:50 +08:00
40 changed files with 475 additions and 311 deletions

View File

@ -7,7 +7,7 @@ jobs:
triage: triage:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/labeler@v6 - uses: actions/labeler@v7
# https://github.com/peaceiris/actions-label-commenter#work-with-other-auto-label-actions # https://github.com/peaceiris/actions-label-commenter#work-with-other-auto-label-actions
with: with:
repo-token: "${{ secrets.GH_PAT }}" repo-token: "${{ secrets.GH_PAT }}"

View File

@ -8,7 +8,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v7
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v7
- name: Install Dependencies - name: Install Dependencies
run: npm install run: npm install
- run: npm run eslint - run: npm run eslint
@ -21,7 +21,7 @@ jobs:
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v7
with: with:
python-version: '3.x' python-version: '3.x'
- name: Install Dependencies - name: Install Dependencies

View File

@ -15,7 +15,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v7
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v7
with: with:
node-version: '24' node-version: '24'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'

View File

@ -14,7 +14,7 @@ jobs:
with: with:
repository: hexojs/hexo-starter repository: hexojs/hexo-starter
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v7
- name: Install Dependencies - name: Install Dependencies
run: npm install run: npm install
- name: Install hexo-tag-embed - name: Install hexo-tag-embed

View File

@ -148,9 +148,9 @@ NexT send special thanks to these great services that sponsor our core infrastru
<a href="https://www.netlify.com"> <a href="https://www.netlify.com">
<picture> <picture>
<source media="(prefers-color-scheme: dark)" height="40" srcset="https://www.netlify.com/v3/img/components/full-logo-dark.svg"> <source media="(prefers-color-scheme: dark)" height="40" srcset="https://raw.githubusercontent.com/netlify/netlify-example-styles/main/public/images/logo-netlify-small-fullcolor-darkmode.svg">
<source media="(prefers-color-scheme: light)" height="40" srcset="https://www.netlify.com/v3/img/components/full-logo-light.svg"> <source media="(prefers-color-scheme: light)" height="40" srcset="https://raw.githubusercontent.com/netlify/netlify-example-styles/main/public/images/logo-netlify-small-fullcolor-lightmode.svg">
<img alt="Netlify Logo" height="40" src="https://www.netlify.com/v3/img/components/full-logo-light.svg"> <img alt="Netlify Logo" height="40" src="https://raw.githubusercontent.com/netlify/netlify-example-styles/main/public/images/logo-netlify-small-fullcolor-lightmode.svg">
</picture> </picture>
</a> </a>

View File

@ -88,7 +88,6 @@ open_graph:
#twitter_id: <twitter:creator> #twitter_id: <twitter:creator>
#twitter_site: <twitter:site> #twitter_site: <twitter:site>
#twitter_image: <twitter:image> #twitter_image: <twitter:image>
#google_plus: <g+:profile_link>
#fb_admins: <fb:admin_id> #fb_admins: <fb:admin_id>
#fb_app_id: <fb:app_id> #fb_app_id: <fb:app_id>
@ -105,7 +104,7 @@ menu:
#home: / || fa fa-home #home: / || fa fa-home
#about: /about/ || fa fa-user #about: /about/ || fa fa-user
#tags: /tags/ || fa fa-tags #tags: /tags/ || fa fa-tags
#categories: /categories/ || fa fa-th #categories: /categories/ || fa fa-layer-group
#archives: /archives/ || fa fa-archive #archives: /archives/ || fa fa-archive
#schedule: /schedule/ || fa fa-calendar #schedule: /schedule/ || fa fa-calendar
#sitemap: /sitemap.xml || fa fa-sitemap #sitemap: /sitemap.xml || fa fa-sitemap
@ -365,8 +364,6 @@ theme_color:
body_scrollbar: body_scrollbar:
# Place the scrollbar over the content. # Place the scrollbar over the content.
overlay: false overlay: false
# Reserving space for the scrollbar gutter even if the content is not overflowing.
stable: false
codeblock: codeblock:
# Code Highlight theme # Code Highlight theme
@ -575,6 +572,8 @@ math:
enable: false enable: false
# Available values: none | ams | all # Available values: none | ams | all
tags: none tags: none
# Available values: overflow | scroll | scale | truncate | linebreak
display_overflow: scroll
katex: katex:
enable: false enable: false
@ -600,10 +599,6 @@ fancybox: false
# For more information: https://medium-zoom.francoischalifour.com # For more information: https://medium-zoom.francoischalifour.com
mediumzoom: false mediumzoom: false
# Vanilla JavaScript plugin for lazyloading images.
# For more information: https://apoorv.pro/lozad.js/demo/
lazyload: false
# Automatically insert whitespace between CJK and half-width characters. # Automatically insert whitespace between CJK and half-width characters.
# For more information: https://github.com/vinta/pangu.js # For more information: https://github.com/vinta/pangu.js
# Server-side plugin: https://github.com/next-theme/hexo-pangu # Server-side plugin: https://github.com/next-theme/hexo-pangu
@ -814,13 +809,13 @@ plausible:
script_url: # https://plausible.io/js/script.js script_url: # https://plausible.io/js/script.js
site_domain: # www.example.com site_domain: # www.example.com
# Another tool to show number of visitors to each article. # Show the number of visitors to each article using the Firestore REST API.
# Visit https://console.firebase.google.com/u/0/ to get apiKey and projectId. # Visit https://console.firebase.google.com/ to get projectId.
# Visit https://firebase.google.com/docs/firestore/ to get more information about firestore. # Visit https://firebase.google.com/docs/firestore/use-rest-api for more information.
# Anonymous requests are authorized by your Firestore Security Rules.
firestore: firestore:
enable: false enable: false
collection: articles # Required, a string collection name to access firestore database collection: articles # Required, a string collection name to access firestore database
apiKey: # Required
projectId: # Required projectId: # Required
# Show Views / Visitors of the website / page with busuanzi. # Show Views / Visitors of the website / page with busuanzi.

View File

@ -1,17 +1,12 @@
# This file is automatically generated # This file is automatically generated
# See https://github.com/next-theme/plugins # See https://github.com/next-theme/plugins
anime:
name: animejs
version: 3.2.1
file: lib/anime.min.js
integrity: sha256-XL2inqUJaslATFnHdJOi9GfQ60on8Wx1C2H8DYiN1xY=
fontawesome: fontawesome:
name: '@fortawesome/fontawesome-free' name: '@fortawesome/fontawesome-free'
version: 7.0.0 version: 7.3.1
file: css/all.min.css file: css/all.min.css
alias: font-awesome alias: font-awesome
integrity: sha256-VHqXKFhhMxcpubYf9xiWdCiojEbY9NexQ4jh8AxbvcM= integrity: sha256-4Lad8m4ZWW1Lgb9+sMVLYEfnIh7BjV1NQMEe79Pviks=
prism: prism:
name: prismjs name: prismjs
version: 1.30.0 version: 1.30.0
@ -32,21 +27,21 @@ prism_line_numbers:
integrity: sha256-9cmf7tcLdXpKsPi/2AWE93PbZpTp4M4tqzFk+lWomjU= integrity: sha256-9cmf7tcLdXpKsPi/2AWE93PbZpTp4M4tqzFk+lWomjU=
mathjax: mathjax:
name: mathjax name: mathjax
version: 3.2.2 version: 4.1.3
file: es5/tex-mml-chtml.js file: tex-mml-chtml.js
integrity: sha256-MASABpB4tYktI2Oitl4t+78w/lyA+D7b/s9GEP0JOGI= integrity: sha256-ihPtDDm7pTmSF9ViEsItlvsrBTsNkz0MOXMchzOwCQw=
katex: katex:
name: katex name: katex
version: 0.16.9 version: 0.18.4
file: dist/katex.min.css file: dist/katex.min.css
alias: KaTeX alias: KaTeX
integrity: sha256-UF1fgpAiu3tPJN/uCqEUHNe7pnr+QR0SQDNfgglgtcM= integrity: sha256-GAwtd9Q019pR1mJcUKlk1P1v29ubyHlqCgFsMMSZMfs=
copy_tex_js: copy_tex_js:
name: katex name: katex
version: 0.16.9 version: 0.18.4
file: dist/contrib/copy-tex.min.js file: dist/contrib/copy-tex.min.js
alias: KaTeX alias: KaTeX
integrity: sha256-Us54+rSGDSTvIhKKUs4kygE2ipA0RXpWWh0/zLqw3bs= integrity: sha256-xXHEm1IMuOkEAoczgoayF4Pjm0dmYLi1bZ4cA37q6v8=
pjax: pjax:
name: '@next-theme/pjax' name: '@next-theme/pjax'
version: 0.6.0 version: 0.6.0
@ -55,16 +50,16 @@ pjax:
integrity: sha256-vxLn1tSKWD4dqbMRyv940UYw4sXgMtYcK6reefzZrao= integrity: sha256-vxLn1tSKWD4dqbMRyv940UYw4sXgMtYcK6reefzZrao=
fancybox_js: fancybox_js:
name: '@fancyapps/ui' name: '@fancyapps/ui'
version: 5.0.31 version: 6.1.14
file: dist/fancybox/fancybox.umd.js file: dist/fancybox/fancybox.umd.js
alias: fancyapps-ui alias: fancyapps-ui
integrity: sha256-a+H7FYzJv6oU2hfsfDGM2Ohw/cR9v+hPfxHCLdmCrE8= integrity: sha256-QnJV4HT5r8i89ovZGRD86iynZqHerEbT2xt69vDM36I=
fancybox_css: fancybox_css:
name: '@fancyapps/ui' name: '@fancyapps/ui'
version: 5.0.31 version: 6.1.14
file: dist/fancybox/fancybox.css file: dist/fancybox/fancybox.css
alias: fancyapps-ui alias: fancyapps-ui
integrity: sha256-gkQVf8UKZgQ0HyuxL/VnacadJ+D2Kox2TCEBuNQg5+w= integrity: sha256-mdaMxrItEW6hMIVJJNPKuX0B7KCSJQVEv3mVY4N2we4=
mediumzoom: mediumzoom:
name: medium-zoom name: medium-zoom
version: 1.1.0 version: 1.1.0
@ -78,24 +73,24 @@ lazyload:
integrity: sha256-mOFREFhqmHeQbXpK2lp4nA3qooVgACfh88fpJftLBbc= integrity: sha256-mOFREFhqmHeQbXpK2lp4nA3qooVgACfh88fpJftLBbc=
pangu: pangu:
name: pangu name: pangu
version: 7.2.0 version: 7.2.1
file: dist/browser/pangu.umd.js file: dist/browser/pangu.umd.js
integrity: sha256-JnmRRnJK7DC6RQJbAJb6AXOM9OmWzS6z8eYultk/48Y= integrity: sha256-kRsxecBnwG6mULFPe2E6UKnNm8XfDOuDwCOprjLZuX8=
quicklink: quicklink:
name: quicklink name: quicklink
version: 3.0.1 version: 3.0.2
file: dist/quicklink.umd.js file: dist/quicklink.umd.js
integrity: sha256-44BednzIpUeQJcY8qtLyarFu0UCCTbgmWOvaoehiFQQ= integrity: sha256-Qo2BTvlsBVXIr5fEHp8xlBpUmkbDINr5cQZeSfMTES8=
disqusjs_js: disqusjs_js:
name: disqusjs name: disqusjs
version: 3.1.0 version: 3.2.1
file: dist/browser/disqusjs.es2015.umd.min.js file: dist/browser/disqusjs.es2015.umd.min.js
integrity: sha256-gdbBT62HLzqWTnXQED3WQL/ItFCuWsA5gd6dM/wgdiE= integrity: sha256-6a5jzAWTjd18Nq6rZZn1x9hZLDZAilRDnd0arUFteBM=
disqusjs_css: disqusjs_css:
name: disqusjs name: disqusjs
version: 3.1.0 version: 3.2.1
file: dist/browser/styles/disqusjs.css file: dist/browser/styles/disqusjs.css
integrity: sha256-E4RzceSomICFY1PT/yDQrN8DsLSwdScDEiQAevHabp0= integrity: sha256-9SkCf6N7ls5uSFyENCeHM+m7Qv3lHH9Eg/CxdCajarI=
gitalk_js: gitalk_js:
name: gitalk name: gitalk
version: 1.8.0 version: 1.8.0
@ -106,21 +101,11 @@ gitalk_css:
version: 1.8.0 version: 1.8.0
file: dist/gitalk.css file: dist/gitalk.css
integrity: sha256-AJnUHL7dBv6PGaeyPQJcgQPDjt/Hn/PvYZde1iqfp8U= integrity: sha256-AJnUHL7dBv6PGaeyPQJcgQPDjt/Hn/PvYZde1iqfp8U=
firebase_app:
name: firebase
version: 12.2.1
file: firebase-app-compat.js
integrity: sha256-d21UgtIcA6c6qwr8nWk6lxs/KrpbWhknQN7qBjWA2Kk=
firebase_firestore:
name: firebase
version: 12.2.1
file: firebase-firestore-compat.js
integrity: sha256-leHatffkFuVGIg7ABaA5BDsqsEUtktL4tftb3OdQfg0=
algolia_search: algolia_search:
name: algoliasearch name: algoliasearch
version: 5.36.0 version: 5.57.0
file: dist/lite/builds/browser.umd.js file: dist/lite/builds/browser.umd.js
integrity: sha256-o49zoHLDPBm9WLg6AiliAVi3axCRTqe3ltTxLvhzAVg= integrity: sha256-NJrnXIMFVRGMM2IieD83YpTvsPPIFKKyj4K/MNyTRQU=
local_search: local_search:
name: hexo-generator-searchdb name: hexo-generator-searchdb
version: 1.5.0 version: 1.5.0
@ -164,11 +149,11 @@ creative_commons:
alias: creativecommons-vocabulary alias: creativecommons-vocabulary
wavedrom: wavedrom:
name: wavedrom name: wavedrom
version: 3.5.0 version: 3.6.2
file: wavedrom.min.js file: wavedrom.min.js
integrity: sha256-INLAoJc6quTNfiMWkGZniYO2cxE8mHpddnLow1m6RFs= integrity: sha256-3hPWxTqi1K1sjmlESiOTH6lfGwYmz8/SFqoAcPxrjwg=
wavedrom_skin: wavedrom_skin:
name: wavedrom name: wavedrom
version: 3.5.0 version: 3.6.2
file: skins/default.js file: skins/default.js
integrity: sha256-fduc/Zszk5ezWws2uInY/ALWVmIrmV6VTgXbsYSReFI= integrity: sha256-bUKjUpel+qLgflZd3EfdbhIorwRYgSkaZKM2s/Bi0VI=

View File

@ -147,9 +147,9 @@ Also, we welcome Issue or PR to our [official-plugins][official-plugins-url].
<a href="https://www.netlify.com"> <a href="https://www.netlify.com">
<picture> <picture>
<source media="(prefers-color-scheme: dark)" height="40" srcset="https://www.netlify.com/v3/img/components/full-logo-dark.svg"> <source media="(prefers-color-scheme: dark)" height="40" srcset="https://raw.githubusercontent.com/netlify/netlify-example-styles/main/public/images/logo-netlify-small-fullcolor-darkmode.svg">
<source media="(prefers-color-scheme: light)" height="40" srcset="https://www.netlify.com/v3/img/components/full-logo-light.svg"> <source media="(prefers-color-scheme: light)" height="40" srcset="https://raw.githubusercontent.com/netlify/netlify-example-styles/main/public/images/logo-netlify-small-fullcolor-lightmode.svg">
<img alt="Netlify Logo" height="40" src="https://www.netlify.com/v3/img/components/full-logo-light.svg"> <img alt="Netlify Logo" height="40" src="https://raw.githubusercontent.com/netlify/netlify-example-styles/main/public/images/logo-netlify-small-fullcolor-lightmode.svg">
</picture> </picture>
</a> </a>

View File

@ -148,9 +148,9 @@ NexT 特别感谢这些支持我们核心基础设施的优质服务:
<a href="https://www.netlify.com"> <a href="https://www.netlify.com">
<picture> <picture>
<source media="(prefers-color-scheme: dark)" height="40" srcset="https://www.netlify.com/v3/img/components/full-logo-dark.svg"> <source media="(prefers-color-scheme: dark)" height="40" srcset="https://raw.githubusercontent.com/netlify/netlify-example-styles/main/public/images/logo-netlify-small-fullcolor-darkmode.svg">
<source media="(prefers-color-scheme: light)" height="40" srcset="https://www.netlify.com/v3/img/components/full-logo-light.svg"> <source media="(prefers-color-scheme: light)" height="40" srcset="https://raw.githubusercontent.com/netlify/netlify-example-styles/main/public/images/logo-netlify-small-fullcolor-lightmode.svg">
<img alt="Netlify Logo" height="40" src="https://www.netlify.com/v3/img/components/full-logo-light.svg"> <img alt="Netlify Logo" height="40" src="https://raw.githubusercontent.com/netlify/netlify-example-styles/main/public/images/logo-netlify-small-fullcolor-lightmode.svg">
</picture> </picture>
</a> </a>

View File

@ -10,7 +10,7 @@
{{- meta_generator() }} {{- meta_generator() }}
{%- endif %} {%- endif %}
{%- if config.preconnect %} {%- if theme.preconnect %}
<link rel="preload" href="{{ url_for(theme.css) }}/main.css" as="style"> <link rel="preload" href="{{ url_for(theme.css) }}/main.css" as="style">
{%- endif %} {%- endif %}
{{ next_pre() }} {{ next_pre() }}

View File

@ -1,13 +1,9 @@
<div class="site-brand-container"> <div class="site-brand-container">
<div class="site-nav-toggle"> <div class="site-nav-search popup-trigger" aria-label="{{ __('menu.search') }}" role="button">
<div class="toggle" aria-label="{{ __('accessibility.nav_toggle') }}" role="button"> {%- if theme.algolia_search.enable or theme.local_search.enable %}
{%- if theme.menu %} <i class="fa fa-search fa-fw fa-lg"></i>
<span class="toggle-line"></span>
<span class="toggle-line"></span>
<span class="toggle-line"></span>
{%- endif %} {%- endif %}
</div> </div>
</div>
<div class="site-meta"> <div class="site-meta">
{%- if theme.custom_logo and theme.scheme === 'Muse' %} {%- if theme.custom_logo and theme.scheme === 'Muse' %}
@ -27,11 +23,9 @@
{%- endif %} {%- endif %}
</div> </div>
<div class="site-nav-right"> <div class="site-nav-toggle" aria-label="{{ __('accessibility.nav_toggle') }}" role="button">
<div class="toggle popup-trigger" aria-label="{{ __('menu.search') }}" role="button"> {%- if theme.menu %}
{%- if theme.algolia_search.enable or theme.local_search.enable %} <i class="fa fa-ellipsis fa-fw fa-lg"></i>
<i class="fa fa-search fa-fw fa-lg"></i>
{%- endif %} {%- endif %}
</div> </div>
</div> </div>
</div>

View File

@ -88,7 +88,7 @@
{%- if config.symbols_count_time.symbols %} {%- if config.symbols_count_time.symbols %}
<span class="post-meta-item" title="{{ __('symbols_count_time.count') }}"> <span class="post-meta-item" title="{{ __('symbols_count_time.count') }}">
<span class="post-meta-item-icon"> <span class="post-meta-item-icon">
<i class="far fa-file-word"></i> <i class="far fa-file-lines"></i>
</span> </span>
<span class="post-meta-item-text">{{ __('symbols_count_time.count') + __('symbol.colon') }}</span> <span class="post-meta-item-text">{{ __('symbols_count_time.count') + __('symbol.colon') }}</span>
<span>{{ symbolsCount(post) }}</span> <span>{{ symbolsCount(post) }}</span>

View File

@ -1,5 +1,7 @@
{%- if theme.algolia_search.enable or theme.local_search.enable %} {%- if theme.algolia_search.enable or theme.local_search.enable %}
<div class="search-pop-overlay"> {# Keep the popup out of the render tree to prevent Safari from animating it before main.css is applied.
When main.css loads slowly, the unstyled search popup briefly appears and shrinks away. #}
<template id="search-popup-template"><div class="search-pop-overlay">
<div class="popup search-popup"> <div class="popup search-popup">
<div class="search-header"> <div class="search-header">
<span class="search-icon"> <span class="search-icon">
@ -20,5 +22,5 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div></template>
{%- endif %} {%- endif %}

View File

@ -6,7 +6,6 @@
<span class="toggle-line"></span> <span class="toggle-line"></span>
<span class="toggle-line"></span> <span class="toggle-line"></span>
</div> </div>
<div class="sidebar-dimmer"></div>
{%- endif %} {%- endif %}
{%- if theme.back2top.enable and not theme.back2top.sidebar %} {%- if theme.back2top.enable and not theme.back2top.sidebar %}

View File

@ -1,4 +1,5 @@
{{ next_data('mathjax', theme.math.mathjax, { {{ next_data('mathjax', theme.math.mathjax, {
js: theme.vendors.mathjax js: theme.vendors.mathjax,
font_path: theme.vendors.mathjax_font_path
}) }} }) }}
{{ next_js('third-party/math/mathjax.js') }} {{ next_js('third-party/math/mathjax.js') }}

View File

@ -1,6 +1,7 @@
{%- if theme.firestore.enable %} {%- if theme.firestore.enable %}
{{ next_vendors('firebase_app') }} {{ next_data('firestore', {
{{ next_vendors('firebase_firestore') }} collection: theme.firestore.collection,
{{ next_data('firestore', theme.firestore) }} projectId: theme.firestore.projectId
}) }}
{{ next_js('third-party/statistics/firestore.js') }} {{ next_js('third-party/statistics/firestore.js') }}
{%- endif %} {%- endif %}

View File

@ -1,6 +1,6 @@
{ {
"name": "hexo-theme-next", "name": "hexo-theme-next",
"version": "8.28.0", "version": "8.29.0",
"description": "Elegant and powerful theme for Hexo.", "description": "Elegant and powerful theme for Hexo.",
"main": "package.json", "main": "package.json",
"files": [ "files": [
@ -36,13 +36,13 @@
"homepage": "https://theme-next.js.org", "homepage": "https://theme-next.js.org",
"devDependencies": { "devDependencies": {
"@next-theme/eslint-config": "0.0.5", "@next-theme/eslint-config": "0.0.5",
"c8": "11.0.0", "c8": "12.0.0",
"chai": "6.2.2", "chai": "6.2.2",
"eslint": "10.6.0", "eslint": "10.8.0",
"hexo": "8.1.2", "hexo": "8.1.2",
"hexo-renderer-marked": "7.0.1", "hexo-renderer-marked": "7.0.1",
"js-yaml": "4.2.0", "js-yaml": "4.3.0",
"mocha": "11.7.6", "mocha": "11.8.0",
"stylelint": "16.26.1", "stylelint": "16.26.1",
"stylelint-stylus": "1.0.0" "stylelint-stylus": "1.0.0"
} }

View File

@ -10,7 +10,7 @@ module.exports = hexo => {
hexo.log.warn('Documentation: https://theme-next.js.org/docs/getting-started/configuration.html'); hexo.log.warn('Documentation: https://theme-next.js.org/docs/getting-started/configuration.html');
} }
const { cache, changyan, growingio_analytics, language_switcher, leancloud_visitors } = hexo.theme.config; const { body_scrollbar, cache, changyan, growingio_analytics, language_switcher, lazyload, leancloud_visitors } = hexo.theme.config;
const warning = function(...args) { const warning = function(...args) {
hexo.log.warn(`Since ${args[0]} is turned on, the ${args[1]} is disabled to avoid potential hazards.`); hexo.log.warn(`Since ${args[0]} is turned on, the ${args[1]} is disabled to avoid potential hazards.`);
}; };
@ -29,6 +29,14 @@ module.exports = hexo => {
if (growingio_analytics) { if (growingio_analytics) {
hexo.log.warn('growingio_analytics is deprecated. Please migrate to another analytics provider.'); hexo.log.warn('growingio_analytics is deprecated. Please migrate to another analytics provider.');
} }
if (body_scrollbar?.stable) {
hexo.log.warn('The `body_scrollbar.stable` theme option is deprecated and will be removed in the next version.');
}
if (lazyload) {
hexo.log.warn('The `lazyload` theme option is deprecated and will be removed in the next version.');
hexo.log.warn('Use `marked.lazyload: true` in Hexo `_config.yml` instead. hexo-renderer-marked will add `loading="lazy"` to images.');
hexo.log.warn('Documentation: https://github.com/hexojs/hexo-renderer-marked#options');
}
if (leancloud_visitors?.enable) { if (leancloud_visitors?.enable) {
hexo.log.warn('LeanCloud will stop public services on 2027-01-12. Please migrate your data as soon as possible.'); hexo.log.warn('LeanCloud will stop public services on 2027-01-12. Please migrate your data as soon as possible.');
hexo.log.warn('Announcement: https://docs.leancloud.app/sdk/announcements/sunset-announcement/'); hexo.log.warn('Announcement: https://docs.leancloud.app/sdk/announcements/sunset-announcement/');

View File

@ -57,5 +57,10 @@ module.exports = hexo => {
// Omit integrity for local assets. // Omit integrity for local assets.
integrity: plugins === 'local' ? undefined : value.integrity integrity: plugins === 'local' ? undefined : value.integrity
}; };
if (key === 'mathjax') {
vendors.mathjax_font_path = plugins === 'local'
? url_for.call(hexo, 'lib/@mathjax/mathjax-newcm-font')
: undefined;
}
} }
}; };

View File

@ -19,7 +19,9 @@ hexo.extend.helper.register('next_inject', function(point) {
hexo.extend.helper.register('next_js', function(file, { hexo.extend.helper.register('next_js', function(file, {
pjax = false, pjax = false,
module = false module = false,
async = false,
defer = !async
} = {}) { } = {}) {
const { next_version } = this; const { next_version } = this;
const { internal, custom_cdn_url } = this.theme.vendors; const { internal, custom_cdn_url } = this.theme.vendors;
@ -32,18 +34,21 @@ hexo.extend.helper.register('next_js', function(file, {
custom : custom_cdn_url custom : custom_cdn_url
}); });
const src = links[internal] || links.local; const src = links[internal] || links.local;
return `<script ${pjax ? 'data-pjax ' : ''}${module ? 'type="module" ' : ''}src="${src}" defer></script>`; return `<script ${pjax ? 'data-pjax ' : ''}${module ? 'type="module" ' : ''}src="${src}"${defer ? ' defer' : ''}${async ? ' async' : ''}></script>`;
}); });
hexo.extend.helper.register('next_vendors', function(name) { hexo.extend.helper.register('next_vendors', function(name, {
async = false,
defer = !async
} = {}) {
const { url, integrity } = this.theme.vendors[name]; const { url, integrity } = this.theme.vendors[name];
const type = url.endsWith('css') ? 'css' : 'js'; const type = url.endsWith('css') ? 'css' : 'js';
if (type === 'css') { if (type === 'css') {
if (integrity) return `<link rel="stylesheet" href="${url}" integrity="${integrity}" crossorigin="anonymous">`; if (integrity) return `<link rel="stylesheet" href="${url}" integrity="${integrity}" crossorigin="anonymous">`;
return `<link rel="stylesheet" href="${url}">`; return `<link rel="stylesheet" href="${url}">`;
} }
if (integrity) return `<script src="${url}" integrity="${integrity}" crossorigin="anonymous" defer></script>`; if (integrity) return `<script src="${url}" integrity="${integrity}" crossorigin="anonymous"${defer ? ' defer' : ''}${async ? ' async' : ''}></script>`;
return `<script src="${url}" defer></script>`; return `<script src="${url}"${defer ? ' defer' : ''}${async ? ' async' : ''}></script>`;
}); });
hexo.extend.helper.register('next_data', function(name, ...data) { hexo.extend.helper.register('next_data', function(name, ...data) {

View File

@ -4,7 +4,7 @@
hexo.extend.helper.register('js_vendors', function() { hexo.extend.helper.register('js_vendors', function() {
const { config, theme } = this; const { config, theme } = this;
const vendors = ['anime']; const vendors = [];
if (theme.prism.enable && !config.prismjs.preprocess) { if (theme.prism.enable && !config.prismjs.preprocess) {
vendors.push('prism', 'prism_autoloader'); vendors.push('prism', 'prism_autoloader');
if (config.prismjs.line_number) { if (config.prismjs.line_number) {

View File

@ -28,11 +28,21 @@
} }
if (hexo-config('reward_settings.animation')) { if (hexo-config('reward_settings.animation')) {
span {
transform-origin: center bottom;
}
&:hover span { &:hover span {
animation: next-roll .1s infinite linear; animation: next-bounce 1.4s infinite ease-in-out;
// The animation may affect :hover of img in dark mode // The animation may affect :hover of img in dark mode
pointer-events: none; pointer-events: none;
} }
@media (prefers-reduced-motion: reduce) {
&:hover span {
animation: none;
}
}
} }
} }
@ -44,12 +54,28 @@
} }
} }
@keyframes next-roll { @keyframes next-bounce {
from { 0%, 100% {
transform: rotateZ(30deg); transform: translateY(0) scale(1);
} }
to { 12% {
transform: rotateZ(-30deg); transform: translateY(0) scale(1.04, .96);
}
36% {
transform: translateY(-7px) scale(.98, 1.02);
}
58% {
transform: translateY(0) scale(1.03, .97);
}
72% {
transform: translateY(-2px) scale(1);
}
84% {
transform: translateY(0) scale(1);
} }
} }

View File

@ -1,5 +1,5 @@
if (hexo-config('math.mathjax.enable')) { if (hexo-config('math.mathjax.enable')) {
mjx-container[jax='CHTML'][display='true'], .has-jax { .has-jax {
overflow: auto hidden; overflow: auto hidden;
} }

View File

@ -12,11 +12,10 @@
.site-brand-container { .site-brand-container {
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
padding: 0 10px;
} }
.use-motion { .use-motion {
.column, .site-brand-container .toggle { .column {
opacity: 0; opacity: 0;
} }
} }

View File

@ -1,20 +1,19 @@
.site-nav-toggle, .site-nav-right { .site-nav-toggle, .site-nav-search {
color: var(--text-color);
display: none; display: none;
padding: 0 10px;
.fa {
transition: transform .5s;
}
+mobile() { +mobile() {
flex-column(); flex-column();
} }
.toggle {
color: var(--text-color);
padding: 10px;
width: 22px;
.toggle-line {
background: var(--text-color);
border-radius: 1px;
}
} }
.site-nav-on .site-nav-toggle .fa {
transform: rotate(-90deg);
} }
.site-nav { .site-nav {

View File

@ -20,16 +20,10 @@
padding: 20px 0; padding: 20px 0;
} }
.site-nav-toggle, .site-nav-right { .site-nav-toggle, .site-nav-search {
color: white;
+tablet() { +tablet() {
flex-column(); flex-column();
} }
.toggle {
color: white;
.toggle-line {
background: white;
}
}
} }

View File

@ -15,7 +15,6 @@ body { margin-top: 2rem; }
} }
.use-motion .column, .use-motion .column,
.use-motion .site-brand-container .toggle,
.use-motion .footer { opacity: initial; } .use-motion .footer { opacity: initial; }
.use-motion .site-title, .use-motion .site-title,

View File

@ -1,4 +1,4 @@
/* global CONFIG */ /* global NexT, CONFIG */
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
'use strict'; 'use strict';
@ -13,12 +13,7 @@ document.addEventListener('DOMContentLoaded', () => {
// If the page opens with a specific hash, just jump out // If the page opens with a specific hash, just jump out
if (!isNaN(top) && location.hash === '') { if (!isNaN(top) && location.hash === '') {
// Auto scroll to the position // Auto scroll to the position
window.anime({ NexT.utils.scrollTo(window, top);
targets : document.scrollingElement,
duration : 200,
easing : 'linear',
scrollTop: top
});
} }
}; };
// Register everything // Register everything
@ -36,17 +31,25 @@ document.addEventListener('DOMContentLoaded', () => {
// Save the position by clicking the icon // Save the position by clicking the icon
link.addEventListener('click', () => { link.addEventListener('click', () => {
doSaveScroll(); doSaveScroll();
window.anime({ if (typeof link.animate !== 'function') {
targets : link, link.style.top = '-30px';
setTimeout(() => {
link.style.top = '';
}, 600);
return;
}
const animation = link.animate([{}, { top: '-30px' }], {
duration: 200, duration: 200,
easing : 'linear', easing : 'linear',
top : -30, fill : 'forwards'
complete: () => { });
animation.finished.then(() => {
link.style.top = '-30px';
animation.cancel();
setTimeout(() => { setTimeout(() => {
link.style.top = ''; link.style.top = '';
}, 400); }, 400);
} }).catch(() => {});
});
}); });
scrollToMark(); scrollToMark();
document.addEventListener('pjax:success', scrollToMark); document.addEventListener('pjax:success', scrollToMark);

View File

@ -14,16 +14,38 @@ NexT.motion.integrator = {
return this; return this;
}, },
bootstrap() { bootstrap() {
if (typeof Element.prototype.animate !== 'function') {
document.body.classList.remove('use-motion');
CONFIG.motion.enable = false;
return;
}
if (!CONFIG.motion.async) this.queue = [this.queue.flat()]; if (!CONFIG.motion.async) this.queue = [this.queue.flat()];
this.queue.forEach(sequence => { this.queue.forEach(sequence => this.schedule(sequence));
const timeline = window.anime.timeline({ },
duration: CONFIG.motion?.duration ?? 200, schedule(sequence) {
easing : 'linear' let cursor = 0;
});
sequence.forEach(item => { sequence.forEach(item => {
if (item.deltaT) timeline.add(item, item.deltaT); const duration = item.duration ?? CONFIG.motion?.duration ?? 200;
else timeline.add(item); const start = Math.max(0, cursor - (item.overlap ?? 0));
const end = start + duration;
cursor = Math.max(cursor, end);
if (item.styles) {
const targets = typeof item.targets === 'string' ? document.querySelectorAll(item.targets) : [item.targets].filter(Boolean);
targets.forEach(target => {
const animation = target.animate([{}, item.styles], {
delay : start,
duration,
easing: 'linear',
fill : 'forwards'
}); });
animation.finished.then(() => {
Object.assign(target.style, item.styles);
animation.cancel();
}).catch(() => {});
});
}
if (item.complete) setTimeout(item.complete, end);
}); });
} }
}; };
@ -35,18 +57,17 @@ NexT.motion.middleWares = {
function getMistLineSettings(targets) { function getMistLineSettings(targets) {
sequence.push({ sequence.push({
targets, targets,
scaleX : [0, 1], styles : { transform: 'scaleX(1)' },
duration: 500, duration: 500,
deltaT : '-=200' overlap : 200
}); });
} }
function pushToSequence(targets, sequenceQueue = false) { function pushToSequence(targets, sequenceQueue = false) {
sequence.push({ sequence.push({
targets, targets,
opacity: 1, styles : { opacity: 1, top: '0px' },
top : 0, overlap: sequenceQueue ? 200 : 0
deltaT : sequenceQueue ? '-=200' : '-=0'
}); });
} }
@ -64,7 +85,7 @@ NexT.motion.middleWares = {
sequence.push({ sequence.push({
targets, targets,
complete: () => targets.classList.add('animated', menuItemTransition), complete: () => targets.classList.add('animated', menuItemTransition),
deltaT : '-=200' overlap : 200
}); });
}); });
} }
@ -92,7 +113,7 @@ NexT.motion.middleWares = {
sequence.push({ sequence.push({
targets, targets,
complete: () => targets.classList.add('animated', animation), complete: () => targets.classList.add('animated', animation),
deltaT : '-=100' overlap : 100
}); });
}); });
} }
@ -101,7 +122,7 @@ NexT.motion.middleWares = {
sequence.push({ sequence.push({
targets, targets,
complete: () => targets.classList.add('animated', post_block), complete: () => targets.classList.add('animated', post_block),
deltaT : '-=100' overlap : 100
}); });
animate(coll_header, targets.querySelectorAll('.collection-header')); animate(coll_header, targets.querySelectorAll('.collection-header'));
animate(post_header, targets.querySelectorAll('.post-header')); animate(post_header, targets.querySelectorAll('.post-header'));
@ -123,7 +144,7 @@ NexT.motion.middleWares = {
sequence.push({ sequence.push({
targets, targets,
complete: () => targets.classList.add('animated', sidebarTransition), complete: () => targets.classList.add('animated', sidebarTransition),
deltaT : '-=100' overlap : 100
}); });
}); });
} }
@ -133,7 +154,7 @@ NexT.motion.middleWares = {
footer() { footer() {
return [{ return [{
targets: document.querySelector('.footer'), targets: document.querySelector('.footer'),
opacity: 1 styles : { opacity: 1 }
}]; }];
} }
}; };

View File

@ -9,8 +9,7 @@ NexT.boot.registerEvents = function() {
NexT.utils.updateFooterPosition(); NexT.utils.updateFooterPosition();
// Mobile top menu bar. // Mobile top menu bar.
document.querySelector('.site-nav-toggle .toggle').addEventListener('click', event => { document.querySelector('.site-nav-toggle').addEventListener('click', event => {
event.currentTarget.classList.toggle('toggle-close');
const siteNav = document.querySelector('.site-nav'); const siteNav = document.querySelector('.site-nav');
if (!siteNav) return; if (!siteNav) return;
siteNav.style.setProperty('--scroll-height', siteNav.scrollHeight + 'px'); siteNav.style.setProperty('--scroll-height', siteNav.scrollHeight + 'px');

View File

@ -26,7 +26,8 @@ const pjax = new Pjax({
}, },
analytics: false, analytics: false,
cacheBust: false, cacheBust: false,
scrollTo : !CONFIG.bookmark.enable // scrollTo accepts a number (vertical offset), a number array ([x, y]), or false (do not scroll).
scrollTo : CONFIG.bookmark.enable ? false : 0
}); });
document.addEventListener('pjax:success', () => { document.addEventListener('pjax:success', () => {

View File

@ -3,13 +3,17 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const isRight = CONFIG.sidebar.position === 'right'; const isRight = CONFIG.sidebar.position === 'right';
// Create the dimmer dynamically to prevent an unwanted animation before main.css is applied.
const sidebarDimmer = document.createElement('div');
sidebarDimmer.className = 'sidebar-dimmer';
document.body.appendChild(sidebarDimmer);
const sidebarToggleMotion = { const sidebarToggleMotion = {
mouse: {}, mouse: {},
init() { init() {
window.addEventListener('mousedown', this.mousedownHandler.bind(this)); window.addEventListener('mousedown', this.mousedownHandler.bind(this));
window.addEventListener('mouseup', this.mouseupHandler.bind(this)); window.addEventListener('mouseup', this.mouseupHandler.bind(this));
document.querySelector('.sidebar-dimmer').addEventListener('click', this.clickHandler.bind(this)); sidebarDimmer.addEventListener('click', this.clickHandler.bind(this));
document.querySelector('.sidebar-toggle').addEventListener('click', this.clickHandler.bind(this)); document.querySelector('.sidebar-toggle').addEventListener('click', this.clickHandler.bind(this));
window.addEventListener('sidebar:show', this.showSidebar); window.addEventListener('sidebar:show', this.showSidebar);
window.addEventListener('sidebar:hide', this.hideSidebar); window.addEventListener('sidebar:hide', this.hideSidebar);

View File

@ -31,5 +31,8 @@ document.addEventListener('page:loaded', () => {
image.wrap(imageWrapLink); image.wrap(imageWrapLink);
}); });
Fancybox.bind('[data-fancybox]'); // Disable hash handling to avoid conflicts with PJAX navigation.
Fancybox.bind('[data-fancybox]', {
Hash: false
});
}); });

View File

@ -4,6 +4,12 @@ document.addEventListener('page:loaded', () => {
if (!CONFIG.enableMath) return; if (!CONFIG.enableMath) return;
if (typeof MathJax === 'undefined') { if (typeof MathJax === 'undefined') {
const output = {
displayOverflow: CONFIG.mathjax.display_overflow
};
if (CONFIG.mathjax.font_path) {
output.fontPath = CONFIG.mathjax.font_path;
}
window.MathJax = { window.MathJax = {
tex: { tex: {
inlineMath: { '[+]': [['$', '$']] }, inlineMath: { '[+]': [['$', '$']] },
@ -12,7 +18,7 @@ document.addEventListener('page:loaded', () => {
options: { options: {
renderActions: { renderActions: {
insertedScript: [200, () => { insertedScript: [200, () => {
document.querySelectorAll('mjx-container').forEach(node => { document.querySelectorAll('mjx-container:not([display])').forEach(node => {
const target = node.parentNode; const target = node.parentNode;
if (target.nodeName.toLowerCase() === 'li') { if (target.nodeName.toLowerCase() === 'li') {
target.parentNode.classList.add('has-jax'); target.parentNode.classList.add('has-jax');
@ -20,7 +26,8 @@ document.addEventListener('page:loaded', () => {
}); });
}, '', false] }, '', false]
} }
} },
output
}; };
NexT.utils.getScript(CONFIG.mathjax.js, { NexT.utils.getScript(CONFIG.mathjax.js, {
attributes: { attributes: {

View File

@ -1,6 +1,7 @@
/* global CONFIG, NexT, pjax */ /* global CONFIG, NexT, pjax */
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
document.body.appendChild(document.querySelector('#search-popup-template').content.cloneNode(true));
const { indexName, appID, apiKey, hits } = CONFIG.algolia; const { indexName, appID, apiKey, hits } = CONFIG.algolia;
const client = window['algoliasearch/lite'].liteClient(appID, apiKey); const client = window['algoliasearch/lite'].liteClient(appID, apiKey);
@ -19,16 +20,13 @@ document.addEventListener('DOMContentLoaded', () => {
return result; return result;
}; };
let isSearching = false; let latestSearchId = 0;
let pendingQuery = null;
const searchAlgolia = async (searchText, page = 0) => { const searchAlgolia = async (searchText, page, searchId) => {
if (isSearching) { if (searchId !== latestSearchId) return;
pendingQuery = { searchText, page }; container.setAttribute('aria-busy', 'true');
return;
}
isSearching = true;
const startTime = Date.now(); const startTime = Date.now();
try {
const result = await client.search({ const result = await client.search({
requests: [{ requests: [{
indexName, indexName,
@ -41,6 +39,8 @@ document.addEventListener('DOMContentLoaded', () => {
highlightPostTag : '</mark>' highlightPostTag : '</mark>'
}] }]
}); });
if (searchId !== latestSearchId) return;
const data = result.results[0]; const data = result.results[0];
if (data.nbHits === 0) { if (data.nbHits === 0) {
container.innerHTML = '<div class="search-result-icon"><i class="far fa-frown fa-5x"></i></div>'; container.innerHTML = '<div class="search-result-icon"><i class="far fa-frown fa-5x"></i></div>';
@ -72,30 +72,40 @@ document.addEventListener('DOMContentLoaded', () => {
container.querySelectorAll('.page-number').forEach(element => { container.querySelectorAll('.page-number').forEach(element => {
element.addEventListener('click', async event => { element.addEventListener('click', async event => {
event.preventDefault(); event.preventDefault();
await searchAlgolia(searchText, Number(element.dataset.index)); if (input.value.trim() !== searchText) return;
const searchId = ++latestSearchId;
await searchAlgolia(searchText, Number(element.dataset.index), searchId);
}); });
}); });
} }
isSearching = false; } catch (error) {
if (pendingQuery !== null && (pendingQuery.searchText !== searchText || pendingQuery.page !== page)) { if (searchId !== latestSearchId) return;
const { searchText, page } = pendingQuery; console.warn('Algolia search failed:', error);
pendingQuery = null; container.innerHTML = '<div class="search-result-icon"><i class="far fa-frown fa-5x"></i></div>';
searchAlgolia(searchText, page); } finally {
if (searchId === latestSearchId) container.removeAttribute('aria-busy');
} }
}; };
const inputEventFunction = async () => { const inputEventFunction = async searchId => {
if (searchId !== latestSearchId) return;
const searchText = input.value.trim(); const searchText = input.value.trim();
if (searchText === '') { if (searchText === '') return;
container.innerHTML = '<div class="search-result-icon"><i class="fab fa-algolia fa-5x"></i></div>';
return;
}
// Algolia client will automatically cache the data for same queries // Algolia client will automatically cache the data for same queries
await searchAlgolia(searchText, 0); await searchAlgolia(searchText, 0, searchId);
}; };
const debouncedSearch = NexT.utils.debounce(inputEventFunction, 500); const debouncedSearch = NexT.utils.debounce(inputEventFunction, 500);
input.addEventListener('input', debouncedSearch); input.addEventListener('input', () => {
const searchId = ++latestSearchId;
if (input.value.trim() === '') {
container.removeAttribute('aria-busy');
container.innerHTML = '<div class="search-result-icon"><i class="fab fa-algolia fa-5x"></i></div>';
} else {
container.setAttribute('aria-busy', 'true');
}
debouncedSearch(searchId);
});
// Handle and trigger popup window // Handle and trigger popup window
document.querySelectorAll('.popup-trigger').forEach(element => { document.querySelectorAll('.popup-trigger').forEach(element => {

View File

@ -6,6 +6,7 @@ document.addEventListener('DOMContentLoaded', () => {
console.warn('`hexo-generator-searchdb` plugin is not installed!'); console.warn('`hexo-generator-searchdb` plugin is not installed!');
return; return;
} }
document.body.appendChild(document.querySelector('#search-popup-template').content.cloneNode(true));
const localSearch = new LocalSearch({ const localSearch = new LocalSearch({
path : CONFIG.path, path : CONFIG.path,
top_n_per_article: CONFIG.localsearch.top_n_per_article, top_n_per_article: CONFIG.localsearch.top_n_per_article,

View File

@ -1,37 +1,76 @@
/* global CONFIG, firebase */ /* global CONFIG */
firebase.initializeApp({
apiKey : CONFIG.firestore.apiKey,
projectId: CONFIG.firestore.projectId
});
(function() { (function() {
const getCount = async (doc, increaseCount) => { const database = `projects/${CONFIG.firestore.projectId}/databases/(default)`;
// IncreaseCount will be false when not in article page const documents = `${database}/documents`;
const d = await doc.get(); const api = `https://firestore.googleapis.com/v1/${documents}`;
// Has no data, initialize count
let count = d.exists ? d.data().count : 0; const fetchFirestore = async (path, options) => {
// If first view this article const response = await fetch(`${api}${path}`, options);
if (increaseCount) { if (response.status === 404) return null;
// Increase count if (!response.ok) throw new Error(`Firestore request failed with status ${response.status}`);
count++; return response.json();
doc.set({
count
});
}
return count;
}; };
const db = firebase.firestore(); const documentName = title => `${documents}/${CONFIG.firestore.collection}/${title}`;
const articles = db.collection(CONFIG.firestore.collection); const getValue = value => Number(value?.integerValue ?? value?.doubleValue ?? 0);
const getCount = async (title, increaseCount) => {
if (increaseCount) {
const data = await fetchFirestore(':commit', {
method : 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
writes: [{
transform: {
document : documentName(title),
fieldTransforms: [{
fieldPath: 'count',
increment: {
integerValue: '1'
}
}]
}
}]
})
});
return getValue(data.writeResults[0].transformResults[0]);
}
const collection = encodeURIComponent(CONFIG.firestore.collection);
const document = await fetchFirestore(`/${collection}/${encodeURIComponent(title)}`);
return getValue(document?.fields?.count);
};
const getCounts = async titles => {
if (titles.length === 0) return [];
const data = await fetchFirestore(':batchGet', {
method : 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
documents: titles.map(documentName),
mask : {
fieldPaths: ['count']
}
})
});
const counts = new Map(data.map(result => {
const document = result.found;
return [document?.name ?? result.missing, getValue(document?.fields?.count)];
}));
return titles.map(title => counts.get(documentName(title)) ?? 0);
};
document.addEventListener('page:loaded', async () => { document.addEventListener('page:loaded', async () => {
try {
if (CONFIG.page.isPost) { if (CONFIG.page.isPost) {
// Fix issue #118 // Fix issue #118
// https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent // https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent
const title = document.querySelector('.post-title').textContent.trim(); const title = document.querySelector('.post-title').textContent.trim();
const doc = articles.doc(title);
let increaseCount = CONFIG.hostname === location.hostname; let increaseCount = CONFIG.hostname === location.hostname;
if (localStorage.getItem(title)) { if (localStorage.getItem(title)) {
increaseCount = false; increaseCount = false;
@ -39,19 +78,18 @@ firebase.initializeApp({
// Mark as visited // Mark as visited
localStorage.setItem(title, true); localStorage.setItem(title, true);
} }
const count = await getCount(doc, increaseCount); const count = await getCount(title, increaseCount);
document.querySelector('.firestore-visitors-count').innerText = count; document.querySelector('.firestore-visitors-count').innerText = count;
} else if (CONFIG.page.isHome) { } else if (CONFIG.page.isHome) {
const promises = [...document.querySelectorAll('.post-title')].map(element => { const titles = [...document.querySelectorAll('.post-title')].map(element => element.textContent.trim());
const title = element.textContent.trim(); const counts = await getCounts(titles);
const doc = articles.doc(title);
return getCount(doc);
});
const counts = await Promise.all(promises);
const metas = document.querySelectorAll('.firestore-visitors-count'); const metas = document.querySelectorAll('.firestore-visitors-count');
counts.forEach((val, idx) => { counts.forEach((val, idx) => {
metas[idx].innerText = val; metas[idx].innerText = val;
}); });
} }
} catch (error) {
console.warn('Failed to load Firestore visitor count:', error);
}
}); });
})(); })();

View File

@ -19,6 +19,17 @@ HTMLElement.prototype.wrap = function(wrapper) {
NexT.utils = { NexT.utils = {
scrollTo(target, top) {
if (window.CSS?.supports('scroll-behavior', 'smooth')) {
target.scrollTo({
top,
behavior: 'smooth'
});
} else {
target.scrollTo(0, top);
}
},
registerExtURL() { registerExtURL() {
document.querySelectorAll('span.exturl').forEach(element => { document.querySelectorAll('span.exturl').forEach(element => {
const link = document.createElement('a'); const link = document.createElement('a');
@ -193,12 +204,7 @@ NexT.utils = {
}, { passive: true }); }, { passive: true });
backToTop?.addEventListener('click', () => { backToTop?.addEventListener('click', () => {
window.anime({ NexT.utils.scrollTo(window, 0);
targets : document.scrollingElement,
duration : 500,
easing : 'linear',
scrollTop: 0
});
}); });
}, },
@ -258,12 +264,7 @@ NexT.utils = {
}, 1000); }, 1000);
if (!CONFIG.stickytabs) return; if (!CONFIG.stickytabs) return;
const offset = nav.parentNode.getBoundingClientRect().top + window.scrollY + 10; const offset = nav.parentNode.getBoundingClientRect().top + window.scrollY + 10;
window.anime({ NexT.utils.scrollTo(window, offset);
targets : document.scrollingElement,
duration : 500,
easing : 'linear',
scrollTop: offset
});
}); });
}); });
@ -311,15 +312,8 @@ NexT.utils = {
element.addEventListener('click', event => { element.addEventListener('click', event => {
event.preventDefault(); event.preventDefault();
const offset = target.getBoundingClientRect().top + window.scrollY; const offset = target.getBoundingClientRect().top + window.scrollY;
window.anime({ NexT.utils.scrollTo(window, offset);
targets : document.scrollingElement,
duration : 500,
easing : 'linear',
scrollTop: offset,
complete : () => {
history.pushState(null, document.title, element.href); history.pushState(null, document.title, element.href);
}
});
}); });
return target; return target;
}); });
@ -342,8 +336,6 @@ NexT.utils = {
const target = navItemList[index]; const target = navItemList[index];
if (!target || target.classList.contains('active-current')) return; if (!target || target.classList.contains('active-current')) return;
const singleHeight = navItemList[navItemList.length - 1].offsetHeight;
nav.querySelectorAll('.active').forEach(navItem => { nav.querySelectorAll('.active').forEach(navItem => {
navItem.classList.remove('active', 'active-current'); navItem.classList.remove('active', 'active-current');
}); });
@ -356,9 +348,13 @@ NexT.utils = {
if (activateEle.classList.contains('nav-item')) { if (activateEle.classList.contains('nav-item')) {
activateEle.classList.add('active'); activateEle.classList.add('active');
} else { // .nav-child or .nav } else { // .nav-child or .nav
// scrollHeight isn't reliable for transitioning child items. // Exclude nested lists so each item's own wrapped row is counted once.
const listItemHeight = [...activateEle.children].reduce((height, navItem) => {
const navChild = [...navItem.children].find(element => element.classList.contains('nav-child'));
return height + navItem.getBoundingClientRect().height - (navChild?.getBoundingClientRect().height || 0);
}, 0);
// The last nav-item in a list has a margin-bottom of 5px. // The last nav-item in a list has a margin-bottom of 5px.
navChildHeight += (singleHeight * activateEle.childElementCount) + 5; navChildHeight += listItemHeight + 5;
activateEle.style.setProperty('--height', `${navChildHeight}px`); activateEle.style.setProperty('--height', `${navChildHeight}px`);
} }
activateEle = activateEle.parentElement; activateEle = activateEle.parentElement;
@ -367,12 +363,7 @@ NexT.utils = {
// Scrolling to center active TOC element if TOC content is taller then viewport. // Scrolling to center active TOC element if TOC content is taller then viewport.
const tocElement = document.querySelector(CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini' ? '.sidebar-panel-container' : '.sidebar'); const tocElement = document.querySelector(CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini' ? '.sidebar-panel-container' : '.sidebar');
if (!document.querySelector('.sidebar-toc-active')) return; if (!document.querySelector('.sidebar-toc-active')) return;
window.anime({ NexT.utils.scrollTo(tocElement, tocElement.scrollTop - (tocElement.offsetHeight / 2) + target.getBoundingClientRect().top - tocElement.getBoundingClientRect().top);
targets : tocElement,
duration : 200,
easing : 'linear',
scrollTop: tocElement.scrollTop - (tocElement.offsetHeight / 2) + target.getBoundingClientRect().top - tocElement.getBoundingClientRect().top
});
}, },
updateSidebarPosition() { updateSidebarPosition() {

73
test/helpers/engine.js Normal file
View File

@ -0,0 +1,73 @@
'use strict';
const helpers = {};
global.hexo = {
extend: {
helper: {
register(name, helper) {
helpers[name] = helper;
}
}
}
};
require('../../scripts/helpers/engine');
delete global.hexo;
describe('engine', () => {
const context = {
next_version: '8.28.0',
theme : {
js : 'js',
vendors: {
internal : 'local',
custom_cdn_url: '',
library : {
url: 'https://cdn.example.com/library.js'
},
secure_library: {
url : 'https://cdn.example.com/secure.js',
integrity: 'sha256-example'
},
stylesheet: {
url: 'https://cdn.example.com/library.css'
}
}
},
url_for(path) {
return '/' + path;
}
};
const nextJs = helpers.next_js.bind(context);
const nextVendors = helpers.next_vendors.bind(context);
it('keeps defer as the default for NexT scripts', () => {
nextJs('main.js').should.eql('<script src="/js/main.js" defer></script>');
});
it('supports custom loading options for NexT scripts', () => {
nextJs('main.js', { defer: false }).should.eql('<script src="/js/main.js"></script>');
nextJs('main.js', { async: true }).should.eql('<script src="/js/main.js" async></script>');
nextJs('main.js', { defer: true, async: true }).should.eql('<script src="/js/main.js" defer async></script>');
});
it('preserves existing NexT script options', () => {
nextJs('main.js', { pjax: true, module: true }).should.eql('<script data-pjax type="module" src="/js/main.js" defer></script>');
});
it('keeps defer as the default for vendor scripts', () => {
nextVendors('library').should.eql('<script src="https://cdn.example.com/library.js" defer></script>');
nextVendors('secure_library').should.eql('<script src="https://cdn.example.com/secure.js" integrity="sha256-example" crossorigin="anonymous" defer></script>');
});
it('supports custom loading options for vendor scripts', () => {
nextVendors('library', { defer: false }).should.eql('<script src="https://cdn.example.com/library.js"></script>');
nextVendors('library', { async: true }).should.eql('<script src="https://cdn.example.com/library.js" async></script>');
});
it('does not apply script loading options to stylesheets', () => {
nextVendors('stylesheet', { async: true }).should.eql('<link rel="stylesheet" href="https://cdn.example.com/library.css">');
});
});

View File

@ -1,6 +1,7 @@
'use strict'; 'use strict';
describe('Helpers', () => { describe('Helpers', () => {
require('./engine');
require('./font'); require('./font');
require('./next-url'); require('./next-url');
}); });