mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-05-14 07:26:52 +00:00
合并全部代码
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
# 环境变量 @see https://www.nextjs.cn/docs/basic-features/environment-variables
|
||||
NEXT_PUBLIC_VERSION=3.13.4
|
||||
NEXT_PUBLIC_VERSION=4.0.6
|
||||
@@ -26,6 +26,7 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'react/no-unknown-property': 'off', // <style jsx>
|
||||
'react/prop-types': 'off',
|
||||
'space-before-function-paren': 0,
|
||||
'react-hooks/rules-of-hooks': 'error' // Checks rules of Hooks
|
||||
|
||||
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -21,13 +21,14 @@ assignees: tangly1024
|
||||
希望按这个步骤,正常操作结果是什么
|
||||
|
||||
**截图**
|
||||
相关的页面,应该用结果
|
||||
相关的页面,应该的结果
|
||||
|
||||
**环境**
|
||||
|
||||
- 操作系统: [例如. iOS, Android, macOS, windows]
|
||||
- 浏览器 [例如. chrome, safari, firefox]
|
||||
- 版本 [e.g. 22]
|
||||
- NotionNext版本 [e.g. 3.13.6]
|
||||
- 主题 [例如. hexo]
|
||||
|
||||
**补充说明**
|
||||
与问题相关的其它说明
|
||||
43
README.md
43
README.md
@@ -15,6 +15,11 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
中文文档 | [README in English](./README_EN.md)
|
||||
|
||||
<hr/>
|
||||
|
||||
一个使用 NextJS + Notion API 实现的,部署在 Vercel 上的静态博客系统。为Notion和所有创作者设计。
|
||||
|
||||
|
||||
@@ -24,18 +29,21 @@
|
||||
|
||||
| Next | Medium | Hexo | Fukasawa |
|
||||
|--|--|--|--|
|
||||
| <img src='./docs/theme-next.png' width='300'/> [预览NEXT](https://preview.tangly1024.com/?theme=next) | <img src='./docs/theme-medium.png' width='300'/> [预览MEDIUM](https://preview.tangly1024.com/?theme=medium) | <img src='./docs/theme-hexo.png' width='300'/> [预览HEXO](https://preview.tangly1024.com/?theme=hexo) | <img src='./docs/theme-fukasawa.png' width='300'/> [预览FUKASAWA](https://preview.tangly1024.com/?theme=fukasawa) |
|
||||
| <img src='./docs/theme-next.png' width='300'/> [预览NEXT](https://tangly1024.com/?theme=next) | <img src='./docs/theme-medium.png' width='300'/> [预览MEDIUM](https://tangly1024.com/?theme=medium) | <img src='./docs/theme-hexo.png' width='300'/> [预览HEXO](https://tangly1024.com/?theme=hexo) | <img src='./docs/theme-fukasawa.png' width='300'/> [预览FUKASAWA](https://tangly1024.com/?theme=fukasawa) |
|
||||
|
||||
## 我要如何开始?
|
||||
|
||||
只需几分钟即可搭建您的个人站点:
|
||||
|
||||
- [快速部署教程 - 多种方案可供选择](https://tangly1024.com/article/notion-next)
|
||||
- [部署教程 (支持多方案)](https://docs.tangly1024.com/)
|
||||
|
||||
- [个性配置手册 - 如何配置功能插件](https://tangly1024.com/article/notion-next-guide)
|
||||
- [配置手册 - (自定义插件)](https://docs.tangly1024.com/article/notion-next-guide)
|
||||
|
||||
- [二次开发指引 - 如何进行本地开发](https://tangly1024.com/article/how-to-develop-with-notion-next)
|
||||
|
||||
- [二次开发 - (开发手册)](https://docs.tangly1024.com/article/notion-next-secondary-menu)
|
||||
|
||||
- [更新指南 - (升级您的代码)](https://docs.tangly1024.com/article/how-to-update-notionnext)
|
||||
|
||||
- [版本汇总 - (查询变动功能)](https://docs.tangly1024.com/article/notion-next-changelogs)
|
||||
|
||||
## 致谢
|
||||
感谢Craig Hart发起的Nobelium项目
|
||||
@@ -155,6 +163,31 @@
|
||||
<td align="center">
|
||||
<a href="https://github.com/hongzzz" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/25585061" width="64px;" alt="Hongzzz"/><br/><sub><b>Hongzzz</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=hongzzz" title="hongzzz" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/RedhairHambagu" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/129669334" width="64px;" alt="RedhairHambagu"/><br/><sub><b>RedhairHambagu</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=RedhairHambagu" title="RedhairHambagu" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/Allengl" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/58612763" width="64px;" alt="Allen"/><br/><sub><b>Allen</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=Allengl" title="Allengl" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/zdf1230" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/5999425" width="64px;" alt="zdf1230"/><br/><sub><b>zdf</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=zdf1230" title="zdf1230" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/emengweb" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/31469739" width="64px;" alt="emengweb"/><br/><sub><b>emengweb</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=emengweb" title="emengweb" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/kitety" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/22906933" width="64px;" alt="kitety"/><br/><sub><b>kitety</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=kitety" title="kitety" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/jxpeng98" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/83734772" width="64px;" alt=" Jiaxin Peng"/><br/><sub><b> Jiaxin Peng</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=jxpeng98" title="jxpeng98" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
187
README_EN.md
Normal file
187
README_EN.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# NotionNext
|
||||
|
||||
<p>
|
||||
<a aria-label="GitHub commit activity" href="https://github.com/tangly1024/NotionNext/commits/main" title="GitHub commit activity">
|
||||
<img src="https://img.shields.io/github/commit-activity/m/tangly1024/NotionNext?style=for-the-badge"/>
|
||||
</a>
|
||||
<a aria-label="GitHub contributors" href="https://github.com/tangly1024/NotionNext/graphs/contributors" title="GitHub contributors">
|
||||
<img src="https://img.shields.io/github/contributors/tangly1024/NotionNext?color=orange&style=for-the-badge"/>
|
||||
</a>
|
||||
<a aria-label="Build status" href="#" title="Build status">
|
||||
<img src="https://img.shields.io/github/deployments/tangly1024/NotionNext/Production?logo=Vercel&style=for-the-badge"/>
|
||||
</a>
|
||||
<a aria-label="Powered by Vercel" href="https://vercel.com?utm_source=Craigary&utm_campaign=oss" title="Powered by Vercel">
|
||||
<img src="https://www.datocms-assets.com/31049/1618983297-powered-by-vercel.svg" height="28"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
[中文文档](./README.md) | README in English
|
||||
|
||||
<hr/>
|
||||
|
||||
A static blog system built with NextJS and Notion API, deployed on Vercel. Designed for Notion and all creators.
|
||||
|
||||
|
||||
## Preview
|
||||
|
||||
Live Demo:[https://preview.tangly1024.com/](https://preview.tangly1024.com/) ,Project supports switching between multiple themes. Can't find a theme you like? How about [contributing](/CONTRIBUTING.md) one?~
|
||||
|
||||
| Next | Medium | Hexo | Fukasawa |
|
||||
|--|--|--|--|
|
||||
| <img src='./docs/theme-next.png' width='300'/> [NEXT](https://preview.tangly1024.com/?theme=next) | <img src='./docs/theme-medium.png' width='300'/> [MEDIUM](https://preview.tangly1024.com/?theme=medium) | <img src='./docs/theme-hexo.png' width='300'/> [HEXO](https://preview.tangly1024.com/?theme=hexo) | <img src='./docs/theme-fukasawa.png' width='300'/> [FUKASAWA](https://preview.tangly1024.com/?theme=fukasawa) |
|
||||
|
||||
## Get Start!
|
||||
|
||||
It only takes a few minutes to set up your personal site:
|
||||
|
||||
- [Quick Deployment Tutorial - Multiple Options Available](https://tangly1024.com/article/notion-next)
|
||||
|
||||
- [Customization Guide - How to Configure Feature Plugins](https://tangly1024.com/article/notion-next-guide)
|
||||
|
||||
- [Development Guide - How to Conduct Local Development](https://tangly1024.com/article/how-to-develop-with-notion-next)
|
||||
|
||||
- [Update Guide - How to Get the Latest Upgrade Patch](https://tangly1024.com/article/how-to-update-notionnext)
|
||||
|
||||
- [Version History - Check Feature Highlights for Each Version](https://tangly1024.com/article/notion-next-changelogs)
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Special thanks to Craig Hart for initiating the Nobelium project.
|
||||
|
||||
<table><tr align="left">
|
||||
<td align="center"><a href="https://github.com/craigary" title="Craig Hart"><img src="https://avatars.githubusercontent.com/u/10571717" width="64px;"alt="Craig Hart"/></a><br/><a href="https://github.com/craigary" title="Craig Hart">Craig Hart</a></td>
|
||||
</tr></table>
|
||||
|
||||
## Contributors
|
||||
|
||||
<table>
|
||||
<tr align="left">
|
||||
<td align="center">
|
||||
<a href="https://github.com/tangly1024" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/15920488" width="64px;"alt="tangly1024"/><br/><sub><b>tangly1024</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=tangly1024" title="Owner" >🎫 🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/uWayLu" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/21689326" width="64px;" alt="uWayLu"/><br/><sub><b>uWayLu</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=uWayLu" title="uWayLu" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/txs" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/554329" width="64px;" alt="txs"/><br/><sub><b>txs</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=txs" title="txs" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/yuzhanglong" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/56540811" width="64px;" alt="yuzhanglong"/><br/><sub><b>yuzhanglong</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=yuzhanglong" title="yuzhanglong" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/Hscpro" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/13926044" width="64px;" alt="Hscpro"/><br/><sub><b>Hscpro</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=Hscpro" title="Hscpro" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/JensonMiao" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/46488783" width="64px;" alt="JensonMiao"/><br/><sub><b>JensonMiao</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=JensonMiao" title="JensonMiao" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/haixin1225" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/28828438" width="64px;" alt="haixin1225"/><br/><sub><b>haixin1225</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=haixin1225" title="haixin1225" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/mouyase" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/7844572" width="64px;" alt="mouyase"/><br/><sub><b>mouyase</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=mouyase" title="mouyase" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/qfdk" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/2404478" width="64px;" alt="qfdk"/><br/><sub><b>qfdk</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=qfdk" title="qfdk" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/ifyz" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/118271360" width="64px;" alt="ifyz"/><br/><sub><b>ifyz</b></sub></a><br><a href="https://github.com/tangly1024/NotionNext/commits?author=ifyz" title="ifyz" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/liqun98" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/71168966" width="64px;" alt="Liqun Zhao"/><br/><sub><b>Liqun Zhao</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=liqun98" title="liqun98" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/Ylarod" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/30978685" width="64px;" alt="Ylarod"/><br/><sub><b>Ylarod</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=Ylarod" title="Ylarod" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/lifeafter619" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/65111206" width="64px;" alt="Etherrreal."/><br/><sub><b>Etherrreal.</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=lifeafter619" title="Etherrreal." >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/ykxkykx" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/17985993" width="64px;" alt="Joshua Astray"/><br/><sub><b>Joshua Astray</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=ykxkykx" title="ykxkykx" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/Vixcity" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/57704177" width="64px;" alt="Vixcity"/><br/><sub><b>Vixcity</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=Vixcity" title="Vixcity" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/ipatpat" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/39089551" width="64px;" alt="ipatpat"/><br/><sub><b>ipatpat</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=ipatpat" title="ipatpat" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/xloong" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/8479955" width="64px;" alt="xloong"/><br/><sub><b>xloong</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=xloong" title="xloong" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/expoli" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/31023767" width="64px;" alt="expoli"/><br/><sub><b>expoli</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=expoli" title="expoli" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/SuperHuangXu" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/14816052" width="64px;" alt="SuperHuangXu"/><br/><sub><b>bUBBLE</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=SuperHuangXu" title="SuperHuangXu" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/Pylogmon" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/59004461" width="64px;" alt="Pylogmon"/><br/><sub><b>派了个萌 </b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=Pylogmon" title="Pylogmon" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/SkysCrystal" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/49473463" width="64px;" alt="SkysCrystal"/><br/><sub><b>Simon Shi</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=SkysCrystal" title="SkysCrystal" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/siygle" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/173408" width="64px;" alt="S.Y. Lee"/><br/><sub><b>S.Y. Lee</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=siygle" title="siygle" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/fighting-bug" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/56441589" width="64px;" alt="fighting-buf"/><br/><sub><b>fighting-buf</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=fighting-bug" title="fighting-buf" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/cliouo" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/71540889" width="64px;" alt="cliouo"/><br/><sub><b>cliouo</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=cliouo" title="cliouo" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/sudeakq" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/102792219" width="64px;" alt="Sude Akgün"/><br/><sub><b>Sude Akgün</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=sudeakq" title="sudeakq" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/fgprodigal" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/769910" width="64px;" alt="Ray"/><br/><sub><b>Ray</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=fgprodigal" title="Ray" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/hongzzz" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/25585061" width="64px;" alt="Hongzzz"/><br/><sub><b>Hongzzz</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=hongzzz" title="hongzzz" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
<td align="center">
|
||||
<a href="https://github.com/RedhairHambagu" style="display:inline-block;width:80px"><img src="https://avatars.githubusercontent.com/u/129669334" width="64px;" alt="RedhairHambagu"/><br/><sub><b>RedhairHambagu</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=RedhairHambagu" title="RedhairHambagu" >🔧 🐛</a>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
## Technologies Used
|
||||
|
||||
- **Technical Framework**: [Next.js](https://nextjs.org)
|
||||
- **Styles**: [Tailwind CSS](https://www.tailwindcss.cn/)
|
||||
- **Rendering Tool**: [React-notion-x](https://github.com/NotionX/react-notion-x)
|
||||
- **COMMENT**: [Twikoo](https://github.com/imaegoo/twikoo), [Giscus](https://giscus.app/zh-CN), [Gitalk](https://gitalk.github.io), [Cusdis](https://cusdis.com), [Utterances](https://utteranc.es)
|
||||
- **ICON**: [Fontawesome](https://fontawesome.com/v6/icons/)
|
||||
|
||||
|
||||
## License
|
||||
|
||||
The MIT License.
|
||||
708
blog.config.js
708
blog.config.js
@@ -1,333 +1,383 @@
|
||||
// 注: process.env.XX是Vercel的环境变量,配置方式见:https://docs.tangly1024.com/zh/features/personality
|
||||
const BLOG = {
|
||||
// Important page_id!!!Duplicate Template from https://www.notion.so/tanghh/02ab3b8678004aa69e9e415905ef32a5
|
||||
NOTION_PAGE_ID:
|
||||
process.env.NOTION_PAGE_ID || 'fb7bf0cd0563410e862e5ee67b8a8d33',
|
||||
PSEUDO_STATIC: false, // 伪静态路径,开启后所有文章URL都以 .html 结尾。
|
||||
NEXT_REVALIDATE_SECOND: process.env.NEXT_PUBLIC_REVALIDATE_SECOND || 5, // 更新内容缓存间隔 单位(秒);即每个页面有5秒的纯静态期、此期间无论多少次访问都不会抓取notion数据;调大该值有助于节省Vercel资源、同时提升访问速率,但也会使文章更新有延迟。
|
||||
THEME: process.env.NEXT_PUBLIC_THEME || 'hexo', // 主题, 支持 ['next','hexo',"fukasawa','medium','example'] @see https://preview.tangly1024.com
|
||||
THEME_SWITCH: process.env.NEXT_PUBLIC_THEME_SWITCH || false, // 是否显示切换主题按钮
|
||||
LANG: process.env.NEXT_PUBLIC_LANG || 'zh-CN', // e.g 'zh-CN','en-US' see /lib/lang.js for more.
|
||||
SINCE: 2022, // e.g if leave this empty, current year will be used.
|
||||
APPEARANCE: process.env.NEXT_PUBLIC_APPEARANCE || 'auto', // ['light', 'dark', 'auto'], // light 日间模式 , dark夜间模式, auto根据时间和主题自动夜间模式
|
||||
APPEARANCE_DARK_TIME: process.env.NEXT_PUBLIC_APPEARANCE_DARK_TIME || [18, 6], // 夜间模式起至时间,false时关闭根据时间自动切换夜间模式
|
||||
CUSTOM_MENU: process.env.NEXT_PUBLIC_CUSTOM_MENU || false, // 支持Menu 类型,从3.12.0版本起,各主题将逐步支持灵活的二级菜单配置,替代了原来的Page类型,此配置是试验功能、默认关闭。
|
||||
|
||||
AUTHOR: process.env.NEXT_PUBLIC_AUTHOR || 'Vixcity', // 您的昵称 例如 tangly1024
|
||||
BIO: process.env.NEXT_PUBLIC_BIO || '一个普通的干饭人🍚', // 作者简介
|
||||
LINK: process.env.NEXT_PUBLIC_LINK || 'https://bolg.vixcitycc.top/', // 网站地址
|
||||
KEYWORDS: process.env.NEXT_PUBLIC_KEYWORD || 'Notion, 博客', // 网站关键词 英文逗号隔开
|
||||
// 社交链接,不需要可留空白,例如 CONTACT_WEIBO:''
|
||||
CONTACT_EMAIL: process.env.NEXT_PUBLIC_CONTACT_EMAIL || '2091283625@qq.com', // 邮箱地址 例如mail@tangly1024.com
|
||||
CONTACT_WEIBO: process.env.NEXT_PUBLIC_CONTACT_WEIBO || '', // 你的微博个人主页
|
||||
CONTACT_TWITTER: process.env.NEXT_PUBLIC_CONTACT_TWITTER || '', // 你的twitter个人主页
|
||||
CONTACT_GITHUB: process.env.NEXT_PUBLIC_CONTACT_GITHUB || 'https://github.com/Vixcity', // 你的github个人主页 例如 https://github.com/tangly1024
|
||||
CONTACT_TELEGRAM: process.env.NEXT_PUBLIC_CONTACT_TELEGRAM || '', // 你的telegram 地址 例如 https://t.me/tangly_1024
|
||||
CONTACT_LINKEDIN: process.env.NEXT_PUBLIC_CONTACT_LINKEDIN || '', // 你的linkedIn 首页
|
||||
|
||||
// 网站字体
|
||||
FONT_STYLE: process.env.NEXT_PUBLIC_FONT_STYLE || 'font-serif', // ['font-serif','font-sans'] 两种可选,分别是衬线和无衬线: 参考 https://www.jianshu.com/p/55e410bd2115
|
||||
FONT_URL: [
|
||||
// Important page_id!!!Duplicate Template from https://www.notion.so/tanghh/02ab3b8678004aa69e9e415905ef32a5
|
||||
NOTION_PAGE_ID:
|
||||
process.env.NOTION_PAGE_ID || '02ab3b8678004aa69e9e415905ef32a5',
|
||||
PSEUDO_STATIC: process.env.NEXT_PUBLIC_PSEUDO_STATIC || false, // 伪静态路径,开启后所有文章URL都以 .html 结尾。
|
||||
NEXT_REVALIDATE_SECOND: process.env.NEXT_PUBLIC_REVALIDATE_SECOND || 5, // 更新内容缓存间隔 单位(秒);即每个页面有5秒的纯静态期、此期间无论多少次访问都不会抓取notion数据;调大该值有助于节省Vercel资源、同时提升访问速率,但也会使文章更新有延迟。
|
||||
THEME: process.env.NEXT_PUBLIC_THEME || 'hexo', // 主题, 支持 ['next','hexo',"fukasawa','medium','example','matery','gitbook','simple'] @see https://preview.tangly1024.com
|
||||
THEME_SWITCH: process.env.NEXT_PUBLIC_THEME_SWITCH || false, // 是否显示切换主题按钮
|
||||
LANG: process.env.NEXT_PUBLIC_LANG || 'zh-CN', // e.g 'zh-CN','en-US' see /lib/lang.js for more.
|
||||
SINCE: 2021, // e.g if leave this empty, current year will be used.
|
||||
APPEARANCE: process.env.NEXT_PUBLIC_APPEARANCE || 'light', // ['light', 'dark', 'auto'], // light 日间模式 , dark夜间模式, auto根据时间和主题自动夜间模式
|
||||
APPEARANCE_DARK_TIME: process.env.NEXT_PUBLIC_APPEARANCE_DARK_TIME || [18, 6], // 夜间模式起至时间,false时关闭根据时间自动切换夜间模式
|
||||
|
||||
// 3.14.1版本后,欢迎语在此配置,英文逗号隔开 , 即可支持多个欢迎语打字效果。
|
||||
GREETING_WORDS: process.env.NEXT_PUBLIC_GREETING_WORDS || 'Hi,我是一个程序员, Hi,我是一个打工人,Hi,我是一个干饭人,欢迎来到我的博客🎉',
|
||||
|
||||
CUSTOM_MENU: process.env.NEXT_PUBLIC_CUSTOM_MENU || false, // 支持Menu 类型,从3.12.0版本起,各主题将逐步支持灵活的二级菜单配置,替代了原来的Page类型,此配置是试验功能、默认关闭。
|
||||
|
||||
AUTHOR: process.env.NEXT_PUBLIC_AUTHOR || 'NotionNext', // 您的昵称 例如 tangly1024
|
||||
BIO: process.env.NEXT_PUBLIC_BIO || '一个普通的干饭人🍚', // 作者简介
|
||||
LINK: process.env.NEXT_PUBLIC_LINK || 'https://tangly1024.com', // 网站地址
|
||||
KEYWORDS: process.env.NEXT_PUBLIC_KEYWORD || 'Notion, 博客', // 网站关键词 英文逗号隔开
|
||||
// 社交链接,不需要可留空白,例如 CONTACT_WEIBO:''
|
||||
CONTACT_EMAIL: process.env.NEXT_PUBLIC_CONTACT_EMAIL || '', // 邮箱地址 例如mail@tangly1024.com
|
||||
CONTACT_WEIBO: process.env.NEXT_PUBLIC_CONTACT_WEIBO || '', // 你的微博个人主页
|
||||
CONTACT_TWITTER: process.env.NEXT_PUBLIC_CONTACT_TWITTER || '', // 你的twitter个人主页
|
||||
CONTACT_GITHUB: process.env.NEXT_PUBLIC_CONTACT_GITHUB || '', // 你的github个人主页 例如 https://github.com/tangly1024
|
||||
CONTACT_TELEGRAM: process.env.NEXT_PUBLIC_CONTACT_TELEGRAM || '', // 你的telegram 地址 例如 https://t.me/tangly_1024
|
||||
CONTACT_LINKEDIN: process.env.NEXT_PUBLIC_CONTACT_LINKEDIN || '', // 你的linkedIn 首页
|
||||
CONTACT_INSTAGRAM: process.env.NEXT_PUBLIC_CONTACT_INSTAGRAM || '', // 您的instagram地址
|
||||
CONTACT_BILIBILI: process.env.NEXT_PUBLIC_CONTACT_BILIBILI || '', // B站主页
|
||||
CONTACT_YOUTUBE: process.env.NEXT_PUBLIC_CONTACT_YOUTUBE || '', // Youtube主页
|
||||
|
||||
NOTION_HOST: process.env.NEXT_PUBLIC_NOTION_HOST || 'https://www.notion.so', // Notion域名,您可以选择用自己的域名进行反向代理,如果不懂得什么是反向代理,请勿修改此项
|
||||
|
||||
BLOG_FAVICON: process.env.NEXT_PUBLIC_FAVICON || '/favicon.ico', // blog favicon 配置, 默认使用 /public/favicon.ico,支持在线图片,如 https://img.imesong.com/favicon.png
|
||||
|
||||
// START ************网站字体*****************
|
||||
|
||||
FONT_STYLE: process.env.NEXT_PUBLIC_FONT_STYLE || 'font-sans', // ['font-serif','font-sans'] 两种可选,分别是衬线和无衬线: 参考 https://www.jianshu.com/p/55e410bd2115
|
||||
// 字体CSS 例如 https://npm.elemecdn.com/lxgw-wenkai-webfont@1.6.0/style.css
|
||||
'https://fonts.googleapis.com/css?family=Bitter&display=swap',
|
||||
'https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300&display=swap',
|
||||
'https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300&display=swap'
|
||||
],
|
||||
FONT_SANS: [
|
||||
// 无衬线字体 例如'LXGW WenKai'
|
||||
'Bitter',
|
||||
'"PingFang SC"',
|
||||
'-apple-system',
|
||||
'BlinkMacSystemFont',
|
||||
'"Hiragino Sans GB"',
|
||||
'"Segoe UI Emoji"',
|
||||
'"Segoe UI Symbol"',
|
||||
'"Segoe UI"',
|
||||
'"Noto Sans SC"',
|
||||
'HarmonyOS_Regular',
|
||||
'"Microsoft YaHei"',
|
||||
'"Helvetica Neue"',
|
||||
'Helvetica',
|
||||
'"Source Han Sans SC"',
|
||||
'Arial',
|
||||
'sans-serif',
|
||||
'"Apple Color Emoji"'
|
||||
],
|
||||
FONT_SERIF: [
|
||||
// 衬线字体 例如'LXGW WenKai'
|
||||
'Bitter',
|
||||
'"Noto Serif SC"',
|
||||
'SimSun',
|
||||
'"Times New Roman"',
|
||||
'Times',
|
||||
'serif',
|
||||
'"Segoe UI Emoji"',
|
||||
'"Segoe UI Symbol"',
|
||||
'"Apple Color Emoji"'
|
||||
],
|
||||
FONT_AWESOME: '/css/all.min.css', // font-awesome 字体图标地址
|
||||
|
||||
// 自定义外部脚本,外部样式
|
||||
CUSTOM_EXTERNAL_JS: [''], // e.g. ['http://xx.com/script.js','http://xx.com/script.js']
|
||||
CUSTOM_EXTERNAL_CSS: [''], // e.g. ['http://xx.com/style.css','http://xx.com/style.css']
|
||||
|
||||
// 侧栏布局 是否反转(左变右,右变左) 已支持主题: hexo next medium fukasawa example
|
||||
LAYOUT_SIDEBAR_REVERSE: false,
|
||||
|
||||
// 一个小插件展示你的facebook fan page~ @see https://tw.andys.pro/article/add-facebook-fanpage-notionnext
|
||||
FACEBOOK_PAGE_TITLE: process.env.NEXT_PUBLIC_FACEBOOK_PAGE_TITLE || null, // 邊欄 Facebook Page widget 的標題欄,填''則無標題欄 e.g FACEBOOK 粉絲團'
|
||||
FACEBOOK_PAGE: process.env.NEXT_PUBLIC_FACEBOOK_PAGE || null, // Facebook Page 的連結 e.g https://www.facebook.com/tw.andys.pro
|
||||
FACEBOOK_PAGE_ID: process.env.NEXT_PUBLIC_FACEBOOK_PAGE_ID || '', // Facebook Page ID 來啟用 messenger 聊天功能
|
||||
FACEBOOK_APP_ID: process.env.NEXT_PUBLIC_FACEBOOK_APP_ID || '', // Facebook App ID 來啟用 messenger 聊天功能 获取: https://developers.facebook.com/
|
||||
|
||||
BEI_AN: process.env.NEXT_PUBLIC_BEI_AN || '', // 备案号 闽ICP备XXXXXXX
|
||||
|
||||
// PrismJs 代码相关
|
||||
PRISM_JS_AUTO_LOADER:
|
||||
'https://npm.elemecdn.com/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js',
|
||||
PRISM_JS_PATH: 'https://npm.elemecdn.com/prismjs@1.29.0/components/',
|
||||
PRISM_THEME_PATH:
|
||||
'https://npm.elemecdn.com/prism-themes/themes/prism-a11y-dark.min.css', // 代码样式主题 更多参考 https://github.com/PrismJS/prism-themes
|
||||
CODE_MAC_BAR: true, // 代码左上角显示mac的红黄绿图标
|
||||
CODE_LINE_NUMBERS: process.env.NEXT_PUBLIC_CODE_LINE_NUMBERS || 'false', // 是否显示行号
|
||||
|
||||
BACKGROUND_LIGHT: '#eeeeee', // use hex value, don't forget '#' e.g #fffefc
|
||||
BACKGROUND_DARK: '#000000', // use hex value, don't forget '#'
|
||||
SUB_PATH: '', // leave this empty unless you want to deploy in a folder
|
||||
|
||||
POST_URL_PREFIX: process.env.NEXT_PUBLIC_POST_URL_PREFIX || 'article',
|
||||
// POST类型文章的默认路径前缀,例如默认POST类型的路径是 /article/[slug]
|
||||
// 如果此项配置为 '' 空, 则文章将没有前缀路径,使用场景: 希望文章前缀路径为 /post 的情况 支持多级
|
||||
// 支援類似 WP 可自訂文章連結格式的功能:https://wordpress.org/documentation/article/customize-permalinks/,目前只先實作 %year%/%month%/%day%
|
||||
// 例:如想連結改成前綴 article + 時間戳記,可變更為: 'article/%year%/%month%/%day%'
|
||||
|
||||
POST_LIST_STYLE: process.env.NEXT_PUBLIC_PPOST_LIST_STYLE || 'page', // ['page','scroll] 文章列表样式:页码分页、单页滚动加载
|
||||
POST_LIST_PREVIEW: process.env.NEXT_PUBLIC_POST_PREVIEW || 'false', // 是否在列表加载文章预览
|
||||
POST_PREVIEW_LINES: 12, // 预览博客行数
|
||||
POST_RECOMMEND_COUNT: 6, // 推荐文章数量
|
||||
POSTS_PER_PAGE: 10, // post counts per page
|
||||
POSTS_SORT_BY: process.env.NEXT_PUBLIC_POST_SORT_BY || 'date', // 排序方式 'date'按时间,'notion'由notion控制
|
||||
|
||||
PREVIEW_CATEGORY_COUNT: 16, // 首页最多展示的分类数量,0为不限制
|
||||
PREVIEW_TAG_COUNT: 16, // 首页最多展示的标签数量,0为不限制
|
||||
|
||||
// 鼠标点击烟花特效
|
||||
FIREWORKS: process.env.NEXT_PUBLIC_FIREWORKS || true, // 开关
|
||||
// 烟花色彩,感谢 https://github.com/Vixcity 提交的色彩
|
||||
FIREWORKS_COLOR: [
|
||||
'255, 20, 97',
|
||||
'24, 255, 146',
|
||||
'90, 135, 255',
|
||||
'251, 243, 140'
|
||||
],
|
||||
|
||||
// 樱花飘落特效
|
||||
SAKURA: process.env.NEXT_PUBLIC_SAKURA || true, // 开关
|
||||
|
||||
// 漂浮线段特效
|
||||
NEST: process.env.NEXT_PUBLIC_NEST || true, // 开关
|
||||
|
||||
// 动态彩带特效
|
||||
FLUTTERINGRIBBON: process.env.NEXT_PUBLIC_FLUTTERINGRIBBON || true, // 开关
|
||||
// 静态彩带特效
|
||||
RIBBON: process.env.NEXT_PUBLIC_RIBBON || false, // 开关
|
||||
|
||||
// 星空雨特效 黑夜模式才会生效
|
||||
STARRY_SKY: process.env.NEXT_PUBLIC_STARRY_SKY || true, // 开关
|
||||
|
||||
// 悬浮挂件
|
||||
WIDGET_PET: process.env.NEXT_PUBLIC_WIDGET_PET || true, // 是否显示宠物挂件
|
||||
WIDGET_PET_LINK:
|
||||
process.env.NEXT_PUBLIC_WIDGET_PET_LINK ||
|
||||
'https://cdn.jsdelivr.net/npm/live2d-widget-model-unitychan@1.0.5/assets/unitychan.model.json', // 挂件模型地址 @see https://github.com/xiazeyu/live2d-widget-models
|
||||
WIDGET_PET_SWITCH_THEME: false, // 点击宠物挂件切换博客主题
|
||||
// 好看的主题
|
||||
// https://cdn.jsdelivr.net/npm/live2d-widget-model-ni-j@1.0.5/assets/ni-j.model.json
|
||||
// https://cdn.jsdelivr.net/npm/live2d-widget-model-nipsilon@1.0.5/assets/nipsilon.model.json
|
||||
// https://cdn.jsdelivr.net/npm/live2d-widget-model-nito@1.0.5/assets/nito.model.json
|
||||
// https://cdn.jsdelivr.net/npm/live2d-widget-model-shizuku@1.0.5/assets/shizuku.model.json
|
||||
// https://cdn.jsdelivr.net/npm/live2d-widget-model-unitychan@1.0.5/assets/unitychan.model.json
|
||||
// https://cdn.jsdelivr.net/npm/live2d-widget-model-koharu@1.0.5/assets/koharu.model.json
|
||||
// https://cdn.jsdelivr.net/npm/live2d-widget-model-haruto@1.0.5/assets/haruto.model.json
|
||||
|
||||
// 音乐播放插件
|
||||
MUSIC_PLAYER: process.env.NEXT_PUBLIC_MUSIC_PLAYER || false, // 是否使用音乐播放插件
|
||||
MUSIC_PLAYER_VISIBLE: process.env.NEXT_PUBLIC_MUSIC_PLAYER_VISIBLE || true, // 是否在左下角显示播放和切换,如果使用播放器,打开自动播放再隐藏,就会以类似背景音乐的方式播放,无法取消和暂停
|
||||
MUSIC_PLAYER_AUTO_PLAY:
|
||||
process.env.NEXT_PUBLIC_MUSIC_PLAYER_AUTO_PLAY || true, // 是否自动播放,不过自动播放时常不生效(移动设备不支持自动播放)
|
||||
MUSIC_PLAYER_LRC_TYPE: process.env.NEXT_PUBLIC_MUSIC_PLAYER_LRC_TYPE || '0', // 歌词显示类型,可选值: 3 | 1 | 0(0:禁用 lrc 歌词,1:lrc 格式的字符串,3:lrc 文件 url)(前提是有配置歌词路径,对 meting 无效)
|
||||
MUSIC_PLAYER_CDN_URL:
|
||||
process.env.NEXT_PUBLIC_MUSIC_PLAYER_CDN_URL ||
|
||||
'https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/aplayer/1.10.1/APlayer.min.js',
|
||||
MUSIC_PLAYER_ORDER: process.env.NEXT_PUBLIC_MUSIC_PLAYER_ORDER || 'list', // 默认播放方式,顺序 list,随机 random
|
||||
MUSIC_PLAYER_AUDIO_LIST: [
|
||||
// 示例音乐列表。除了以下配置外,还可配置歌词,具体配置项看此文档 https://aplayer.js.org/#/zh-Hans/
|
||||
{
|
||||
name: '风を共に舞う気持ち',
|
||||
artist: 'Falcom Sound Team jdk',
|
||||
url: 'https://music.163.com/song/media/outer/url?id=731419.mp3',
|
||||
cover:
|
||||
'https://p2.music.126.net/kn6ugISTonvqJh3LHLaPtQ==/599233837187278.jpg'
|
||||
FONT_URL: [
|
||||
// 'https://npm.elemecdn.com/lxgw-wenkai-webfont@1.6.0/style.css'
|
||||
'https://fonts.googleapis.com/css?family=Bitter&display=swap',
|
||||
'https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300&display=swap',
|
||||
'https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300&display=swap'
|
||||
],
|
||||
// 无衬线字体 例如'"LXGW WenKai"'
|
||||
FONT_SANS: [
|
||||
// '"LXGW WenKai"',
|
||||
'"PingFang SC"',
|
||||
'-apple-system',
|
||||
'BlinkMacSystemFont',
|
||||
'"Hiragino Sans GB"',
|
||||
'"Microsoft YaHei"',
|
||||
'"Segoe UI Emoji"',
|
||||
'"Segoe UI Symbol"',
|
||||
'"Segoe UI"',
|
||||
'"Noto Sans SC"',
|
||||
'HarmonyOS_Regular',
|
||||
'"Helvetica Neue"',
|
||||
'Helvetica',
|
||||
'"Source Han Sans SC"',
|
||||
'Arial',
|
||||
'sans-serif',
|
||||
'"Apple Color Emoji"'
|
||||
],
|
||||
// 衬线字体 例如'"LXGW WenKai"'
|
||||
FONT_SERIF: [
|
||||
// '"LXGW WenKai"',
|
||||
'Bitter',
|
||||
'"Noto Serif SC"',
|
||||
'SimSun',
|
||||
'"Times New Roman"',
|
||||
'Times',
|
||||
'serif',
|
||||
'"Segoe UI Emoji"',
|
||||
'"Segoe UI Symbol"',
|
||||
'"Apple Color Emoji"'
|
||||
],
|
||||
FONT_AWESOME: process.env.NEXT_PUBLIC_FONT_AWESOME_PATH || 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css', // font-awesome 字体图标地址; 可选 /css/all.min.css , https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/font-awesome/6.0.0/css/all.min.css
|
||||
|
||||
// END ************网站字体*****************
|
||||
|
||||
CUSTOM_RIGHT_CLICK_CONTEXT_MENU: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU || true, // 自定义右键菜单,覆盖系统菜单
|
||||
|
||||
// 自定义外部脚本,外部样式
|
||||
CUSTOM_EXTERNAL_JS: [''], // e.g. ['http://xx.com/script.js','http://xx.com/script.js']
|
||||
CUSTOM_EXTERNAL_CSS: [''], // e.g. ['http://xx.com/style.css','http://xx.com/style.css']
|
||||
|
||||
// 侧栏布局 是否反转(左变右,右变左) 已支持主题: hexo next medium fukasawa example
|
||||
LAYOUT_SIDEBAR_REVERSE: false,
|
||||
|
||||
// 一个小插件展示你的facebook fan page~ @see https://tw.andys.pro/article/add-facebook-fanpage-notionnext
|
||||
FACEBOOK_PAGE_TITLE: process.env.NEXT_PUBLIC_FACEBOOK_PAGE_TITLE || null, // 邊欄 Facebook Page widget 的標題欄,填''則無標題欄 e.g FACEBOOK 粉絲團'
|
||||
FACEBOOK_PAGE: process.env.NEXT_PUBLIC_FACEBOOK_PAGE || null, // Facebook Page 的連結 e.g https://www.facebook.com/tw.andys.pro
|
||||
FACEBOOK_PAGE_ID: process.env.NEXT_PUBLIC_FACEBOOK_PAGE_ID || '', // Facebook Page ID 來啟用 messenger 聊天功能
|
||||
FACEBOOK_APP_ID: process.env.NEXT_PUBLIC_FACEBOOK_APP_ID || '', // Facebook App ID 來啟用 messenger 聊天功能 获取: https://developers.facebook.com/
|
||||
|
||||
BEI_AN: process.env.NEXT_PUBLIC_BEI_AN || '', // 备案号 闽ICP备XXXXXXX
|
||||
|
||||
// START********代码相关********
|
||||
// PrismJs 代码相关
|
||||
PRISM_JS_PATH: 'https://npm.elemecdn.com/prismjs@1.29.0/components/',
|
||||
PRISM_JS_AUTO_LOADER: 'https://npm.elemecdn.com/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js',
|
||||
|
||||
// 代码主题 @see https://github.com/PrismJS/prism-themes
|
||||
PRISM_THEME_PREFIX_PATH: 'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-okaidia.css', // 代码块默认主题
|
||||
PRISM_THEME_SWITCH: process.env.NEXT_PUBLIC_PRISM_THEME_SWITCH || true, // 是否开启浅色/深色模式代码主题切换; 开启后将显示以下两个主题
|
||||
PRISM_THEME_LIGHT_PATH: 'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-solarizedlight.css', // 浅色模式主题
|
||||
PRISM_THEME_DARK_PATH: 'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-okaidia.min.css', // 深色模式主题
|
||||
|
||||
CODE_MAC_BAR: process.env.NEXT_PUBLIC_CODE_MAC_BAR || true, // 代码左上角显示mac的红黄绿图标
|
||||
CODE_LINE_NUMBERS: process.env.NEXT_PUBLIC_CODE_LINE_NUMBERS || false, // 是否显示行号
|
||||
CODE_COLLAPSE: process.env.NEXT_PUBLIC_CODE_COLLAPSE || true, // 是否折叠代码框
|
||||
// END********代码相关********
|
||||
|
||||
// Mermaid 图表CDN
|
||||
MERMAID_CDN: process.env.NEXT_PUBLIC_MERMAID_CDN || 'https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.2.4/mermaid.min.js', // CDN
|
||||
|
||||
BACKGROUND_LIGHT: '#eeeeee', // use hex value, don't forget '#' e.g #fffefc
|
||||
BACKGROUND_DARK: '#000000', // use hex value, don't forget '#'
|
||||
SUB_PATH: '', // leave this empty unless you want to deploy in a folder
|
||||
|
||||
POST_SHARE_BAR_ENABLE: process.env.NEXT_PUBLIC_POST_SHARE_BAR || 'true', // 文章分享功能 ,将在底部显示一个分享条
|
||||
POSTS_SHARE_SERVICES: process.env.NEXT_PUBLIC_POST_SHARE_SERVICES || 'link,wechat,qq,weibo,email,facebook,twitter,telegram,messenger,line,reddit,whatsapp,linkedin', // 分享的服務,按顺序显示,逗号隔开
|
||||
// 所有支持的分享服务:link(复制链接),wechat(微信),qq,weibo(微博),email(邮件),facebook,twitter,telegram,messenger,line,reddit,whatsapp,linkedin,vkshare,okshare,tumblr,livejournal,mailru,viber,workplace,pocket,instapaper,hatena
|
||||
|
||||
POST_URL_PREFIX: process.env.NEXT_PUBLIC_POST_URL_PREFIX || 'article',
|
||||
// POST类型文章的默认路径前缀,例如默认POST类型的路径是 /article/[slug]
|
||||
// 如果此项配置为 '' 空, 则文章将没有前缀路径,使用场景: 希望文章前缀路径为 /post 的情况 支持多级
|
||||
// 支援類似 WP 可自訂文章連結格式的功能:https://wordpress.org/documentation/article/customize-permalinks/,目前只先實作 %year%/%month%/%day%
|
||||
// 例:如想連結改成前綴 article + 時間戳記,可變更為: 'article/%year%/%month%/%day%'
|
||||
|
||||
POST_LIST_STYLE: process.env.NEXT_PUBLIC_POST_LIST_STYLE || 'page', // ['page','scroll] 文章列表样式:页码分页、单页滚动加载
|
||||
POST_LIST_PREVIEW: process.env.NEXT_PUBLIC_POST_PREVIEW || 'false', // 是否在列表加载文章预览
|
||||
POST_PREVIEW_LINES: 12, // 预览博客行数
|
||||
POST_RECOMMEND_COUNT: 6, // 推荐文章数量
|
||||
POSTS_PER_PAGE: 12, // post counts per page
|
||||
POSTS_SORT_BY: process.env.NEXT_PUBLIC_POST_SORT_BY || 'notion', // 排序方式 'date'按时间,'notion'由notion控制
|
||||
|
||||
ALGOLIA_APP_ID: process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || null, // 在这里查看 https://dashboard.algolia.com/account/api-keys/
|
||||
ALGOLIA_ADMIN_APP_KEY: process.env.ALGOLIA_ADMIN_APP_KEY || null, // 管理后台的KEY,不要暴露在代码中,在这里查看 https://dashboard.algolia.com/account/api-keys/
|
||||
ALGOLIA_SEARCH_ONLY_APP_KEY: process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_APP_KEY || null, // 客户端搜索用的KEY
|
||||
ALGOLIA_INDEX: process.env.NEXT_PUBLIC_ALGOLIA_INDEX || null, // 在Algolia中创建一个index用作数据库
|
||||
ALGOLIA_RECREATE_DATA: process.env.ALGOLIA_RECREATE_DATA || process.env.npm_lifecycle_event === 'build', // 为true时重新构建索引数据; 默认在build时会构建
|
||||
|
||||
PREVIEW_CATEGORY_COUNT: 16, // 首页最多展示的分类数量,0为不限制
|
||||
PREVIEW_TAG_COUNT: 16, // 首页最多展示的标签数量,0为不限制
|
||||
|
||||
POST_DISABLE_GALLERY_CLICK: process.env.NEXT_PUBLIC_POST_DISABLE_GALLERY_CLICK || false, // 画册视图禁止点击,方便在友链页面的画册插入链接
|
||||
|
||||
// ********动态特效相关********
|
||||
// 鼠标点击烟花特效
|
||||
FIREWORKS: process.env.NEXT_PUBLIC_FIREWORKS || false, // 开关
|
||||
// 烟花色彩,感谢 https://github.com/Vixcity 提交的色彩
|
||||
FIREWORKS_COLOR: [
|
||||
'255, 20, 97',
|
||||
'24, 255, 146',
|
||||
'90, 135, 255',
|
||||
'251, 243, 140'
|
||||
],
|
||||
|
||||
// 樱花飘落特效
|
||||
SAKURA: process.env.NEXT_PUBLIC_SAKURA || false, // 开关
|
||||
// 漂浮线段特效
|
||||
NEST: process.env.NEXT_PUBLIC_NEST || false, // 开关
|
||||
// 动态彩带特效
|
||||
FLUTTERINGRIBBON: process.env.NEXT_PUBLIC_FLUTTERINGRIBBON || false, // 开关
|
||||
// 静态彩带特效
|
||||
RIBBON: process.env.NEXT_PUBLIC_RIBBON || false, // 开关
|
||||
// 星空雨特效 黑夜模式才会生效
|
||||
STARRY_SKY: process.env.NEXT_PUBLIC_STARRY_SKY || false, // 开关
|
||||
|
||||
// ********挂件组件相关********
|
||||
// Chatbase
|
||||
CHATBASE_ID: process.env.NEXT_PUBLIC_CHATBASE_ID || null, // 是否显示chatbase机器人 https://www.chatbase.co/
|
||||
// 悬浮挂件
|
||||
WIDGET_PET: process.env.NEXT_PUBLIC_WIDGET_PET || true, // 是否显示宠物挂件
|
||||
WIDGET_PET_LINK:
|
||||
process.env.NEXT_PUBLIC_WIDGET_PET_LINK ||
|
||||
'https://cdn.jsdelivr.net/npm/live2d-widget-model-wanko@1.0.5/assets/wanko.model.json', // 挂件模型地址 @see https://github.com/xiazeyu/live2d-widget-models
|
||||
WIDGET_PET_SWITCH_THEME: process.env.NEXT_PUBLIC_WIDGET_PET_SWITCH_THEME || true, // 点击宠物挂件切换博客主题
|
||||
|
||||
// 音乐播放插件
|
||||
MUSIC_PLAYER: process.env.NEXT_PUBLIC_MUSIC_PLAYER || false, // 是否使用音乐播放插件
|
||||
MUSIC_PLAYER_VISIBLE: process.env.NEXT_PUBLIC_MUSIC_PLAYER_VISIBLE || true, // 是否在左下角显示播放和切换,如果使用播放器,打开自动播放再隐藏,就会以类似背景音乐的方式播放,无法取消和暂停
|
||||
MUSIC_PLAYER_AUTO_PLAY:
|
||||
process.env.NEXT_PUBLIC_MUSIC_PLAYER_AUTO_PLAY || true, // 是否自动播放,不过自动播放时常不生效(移动设备不支持自动播放)
|
||||
MUSIC_PLAYER_LRC_TYPE: process.env.NEXT_PUBLIC_MUSIC_PLAYER_LRC_TYPE || '0', // 歌词显示类型,可选值: 3 | 1 | 0(0:禁用 lrc 歌词,1:lrc 格式的字符串,3:lrc 文件 url)(前提是有配置歌词路径,对 meting 无效)
|
||||
MUSIC_PLAYER_CDN_URL:
|
||||
process.env.NEXT_PUBLIC_MUSIC_PLAYER_CDN_URL ||
|
||||
'https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/aplayer/1.10.1/APlayer.min.js',
|
||||
MUSIC_PLAYER_ORDER: process.env.NEXT_PUBLIC_MUSIC_PLAYER_ORDER || 'list', // 默认播放方式,顺序 list,随机 random
|
||||
MUSIC_PLAYER_AUDIO_LIST: [
|
||||
// 示例音乐列表。除了以下配置外,还可配置歌词,具体配置项看此文档 https://aplayer.js.org/#/zh-Hans/
|
||||
{
|
||||
name: '风を共に舞う気持ち',
|
||||
artist: 'Falcom Sound Team jdk',
|
||||
url: 'https://music.163.com/song/media/outer/url?id=731419.mp3',
|
||||
cover:
|
||||
'https://p2.music.126.net/kn6ugISTonvqJh3LHLaPtQ==/599233837187278.jpg'
|
||||
},
|
||||
{
|
||||
name: '王都グランセル',
|
||||
artist: 'Falcom Sound Team jdk',
|
||||
url: 'https://music.163.com/song/media/outer/url?id=731355.mp3',
|
||||
cover:
|
||||
'https://p1.music.126.net/kn6ugISTonvqJh3LHLaPtQ==/599233837187278.jpg'
|
||||
}
|
||||
],
|
||||
MUSIC_PLAYER_METING: process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING || false, // 是否要开启 MetingJS,从平台获取歌单。会覆盖自定义的 MUSIC_PLAYER_AUDIO_LIST,更多配置信息:https://github.com/metowolf/MetingJS
|
||||
MUSIC_PLAYER_METING_SERVER:
|
||||
process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_SERVER || 'netease', // 音乐平台,[netease, tencent, kugou, xiami, baidu]
|
||||
MUSIC_PLAYER_METING_ID:
|
||||
process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_ID || '60198', // 对应歌单的 id
|
||||
MUSIC_PLAYER_METING_LRC_TYPE:
|
||||
process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_LRC_TYPE || '1', // 可选值: 3 | 1 | 0(0:禁用 lrc 歌词,1:lrc 格式的字符串,3:lrc 文件 url)
|
||||
|
||||
// ********挂件组件相关********
|
||||
// ----> 评论互动 可同时开启多个支持 WALINE VALINE GISCUS CUSDIS UTTERRANCES GITALK
|
||||
|
||||
// twikoo
|
||||
COMMENT_TWIKOO_ENV_ID: process.env.NEXT_PUBLIC_COMMENT_ENV_ID || '', // TWIKOO后端地址 腾讯云环境填envId;Vercel环境填域名,教程:https://tangly1024.com/article/notionnext-twikoo
|
||||
COMMENT_TWIKOO_COUNT_ENABLE: process.env.NEXT_PUBLIC_COMMENT_TWIKOO_COUNT_ENABLE || false, // 博客列表是否显示评论数
|
||||
COMMENT_TWIKOO_CDN_URL: process.env.NEXT_PUBLIC_COMMENT_TWIKOO_CDN_URL || 'https://cdn.staticfile.org/twikoo/1.6.16/twikoo.all.min.js', // twikoo客户端cdn
|
||||
|
||||
// utterance
|
||||
COMMENT_UTTERRANCES_REPO:
|
||||
process.env.NEXT_PUBLIC_COMMENT_UTTERRANCES_REPO || '', // 你的代码仓库名, 例如我是 'tangly1024/NotionNext'; 更多文档参考 https://utteranc.es/
|
||||
|
||||
// giscus @see https://giscus.app/
|
||||
COMMENT_GISCUS_REPO: process.env.NEXT_PUBLIC_COMMENT_GISCUS_REPO || '', // 你的Github仓库名 e.g 'tangly1024/NotionNext'
|
||||
COMMENT_GISCUS_REPO_ID: process.env.NEXT_PUBLIC_COMMENT_GISCUS_REPO_ID || '', // 你的Github Repo ID e.g ( 設定完 giscus 即可看到 )
|
||||
COMMENT_GISCUS_CATEGORY_ID:
|
||||
process.env.NEXT_PUBLIC_COMMENT_GISCUS_CATEGORY_ID || '', // 你的Github Discussions 內的 Category ID ( 設定完 giscus 即可看到 )
|
||||
COMMENT_GISCUS_MAPPING:
|
||||
process.env.NEXT_PUBLIC_COMMENT_GISCUS_MAPPING || 'pathname', // 你的Github Discussions 使用哪種方式來標定文章, 預設 'pathname'
|
||||
COMMENT_GISCUS_REACTIONS_ENABLED:
|
||||
process.env.NEXT_PUBLIC_COMMENT_GISCUS_REACTIONS_ENABLED || '1', // 你的 Giscus 是否開啟文章表情符號 '1' 開啟 "0" 關閉 預設開啟
|
||||
COMMENT_GISCUS_EMIT_METADATA:
|
||||
process.env.NEXT_PUBLIC_COMMENT_GISCUS_EMIT_METADATA || '0', // 你的 Giscus 是否提取 Metadata '1' 開啟 '0' 關閉 預設關閉
|
||||
COMMENT_GISCUS_INPUT_POSITION:
|
||||
process.env.NEXT_PUBLIC_COMMENT_GISCUS_INPUT_POSITION || 'bottom', // 你的 Giscus 發表留言位置 'bottom' 尾部 'top' 頂部, 預設 'bottom'
|
||||
COMMENT_GISCUS_LANG: process.env.NEXT_PUBLIC_COMMENT_GISCUS_LANG || 'zh-CN', // 你的 Giscus 語言 e.g 'en', 'zh-TW', 'zh-CN', 預設 'en'
|
||||
COMMENT_GISCUS_LOADING:
|
||||
process.env.NEXT_PUBLIC_COMMENT_GISCUS_LOADING || 'lazy', // 你的 Giscus 載入是否漸進式載入, 預設 'lazy'
|
||||
COMMENT_GISCUS_CROSSORIGIN:
|
||||
process.env.NEXT_PUBLIC_COMMENT_GISCUS_CROSSORIGIN || 'anonymous', // 你的 Giscus 可以跨網域, 預設 'anonymous'
|
||||
|
||||
COMMENT_CUSDIS_APP_ID: process.env.NEXT_PUBLIC_COMMENT_CUSDIS_APP_ID || '', // data-app-id 36位 see https://cusdis.com/
|
||||
COMMENT_CUSDIS_HOST:
|
||||
process.env.NEXT_PUBLIC_COMMENT_CUSDIS_HOST || 'https://cusdis.com', // data-host, change this if you're using self-hosted version
|
||||
COMMENT_CUSDIS_SCRIPT_SRC:
|
||||
process.env.NEXT_PUBLIC_COMMENT_CUSDIS_SCRIPT_SRC ||
|
||||
'https://cusdis.com/js/cusdis.es.js', // change this if you're using self-hosted version
|
||||
|
||||
// gitalk评论插件 更多参考 https://gitalk.github.io/
|
||||
COMMENT_GITALK_REPO: process.env.NEXT_PUBLIC_COMMENT_GITALK_REPO || '', // 你的Github仓库名,例如 'NotionNext'
|
||||
COMMENT_GITALK_OWNER: process.env.NEXT_PUBLIC_COMMENT_GITALK_OWNER || '', // 你的用户名 e.g tangly1024
|
||||
COMMENT_GITALK_ADMIN: process.env.NEXT_PUBLIC_COMMENT_GITALK_ADMIN || '', // 管理员用户名、一般是自己 e.g 'tangly1024'
|
||||
COMMENT_GITALK_CLIENT_ID:
|
||||
process.env.NEXT_PUBLIC_COMMENT_GITALK_CLIENT_ID || '', // e.g 20位ID , 在gitalk后台获取
|
||||
COMMENT_GITALK_CLIENT_SECRET:
|
||||
process.env.NEXT_PUBLIC_COMMENT_GITALK_CLIENT_SECRET || '', // e.g 40位ID, 在gitalk后台获取
|
||||
COMMENT_GITALK_DISTRACTION_FREE_MODE: false, // 类似facebook的无干扰模式
|
||||
COMMENT_GITALK_JS_CDN_URL: process.env.NEXT_PUBLIC_COMMENT_GITALK_JS_CDN_URL || 'https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.min.js', // gitalk客户端 js cdn
|
||||
COMMENT_GITALK_CSS_CDN_URL: process.env.NEXT_PUBLIC_COMMENT_GITALK_CSS_CDN_URL || 'https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.css', // gitalk客户端 css cdn
|
||||
|
||||
COMMENT_GITTER_ROOM: process.env.NEXT_PUBLIC_COMMENT_GITTER_ROOM || '', // gitter聊天室 see https://gitter.im/ 不需要则留空
|
||||
COMMENT_DAO_VOICE_ID: process.env.NEXT_PUBLIC_COMMENT_DAO_VOICE_ID || '', // DaoVoice http://dashboard.daovoice.io/get-started
|
||||
COMMENT_TIDIO_ID: process.env.NEXT_PUBLIC_COMMENT_TIDIO_ID || '', // [tidio_id] -> //code.tidio.co/[tidio_id].js
|
||||
|
||||
COMMENT_VALINE_CDN: process.env.NEXT_PUBLIC_VALINE_CDN || 'https://unpkg.com/valine@1.5.1/dist/Valine.min.js',
|
||||
COMMENT_VALINE_APP_ID: process.env.NEXT_PUBLIC_VALINE_ID || '', // Valine @see https://valine.js.org/quickstart.html 或 https://github.com/stonehank/react-valine#%E8%8E%B7%E5%8F%96app-id-%E5%92%8C-app-key
|
||||
COMMENT_VALINE_APP_KEY: process.env.NEXT_PUBLIC_VALINE_KEY || '',
|
||||
COMMENT_VALINE_SERVER_URLS: process.env.NEXT_PUBLIC_VALINE_SERVER_URLS || '', // 该配置适用于国内自定义域名用户, 海外版本会自动检测(无需手动填写) @see https://valine.js.org/configuration.html#serverURLs
|
||||
COMMENT_VALINE_PLACEHOLDER:
|
||||
process.env.NEXT_PUBLIC_VALINE_PLACEHOLDER || '抢个沙发吧~', // 可以搭配后台管理评论 https://github.com/DesertsP/Valine-Admin 便于查看评论,以及邮件通知,垃圾评论过滤等功能
|
||||
|
||||
COMMENT_WALINE_SERVER_URL: process.env.NEXT_PUBLIC_WALINE_SERVER_URL || '', // 请配置完整的Waline评论地址 例如 hhttps://preview-waline.tangly1024.com @see https://waline.js.org/guide/get-started.html
|
||||
COMMENT_WALINE_RECENT: process.env.NEXT_PUBLIC_WALINE_RECENT || false, // 最新评论
|
||||
|
||||
// 此评论系统基于WebMention,细节可参考https://webmention.io
|
||||
// 它是一个基于IndieWeb理念的开放式评论系统,下方COMMENT_WEBMENTION包含的属性皆需配置:
|
||||
// ENABLE: 是否开启
|
||||
// AUTH: Webmention使用的IndieLogin,可使用Twitter或Github个人页面连结
|
||||
// HOSTNAME: Webmention绑定之网域,通常即为本站网址
|
||||
// TWITTER_USERNAME: 评论显示区域需要的资讯
|
||||
// TOKEN: Webmention的API token
|
||||
COMMENT_WEBMENTION: {
|
||||
ENABLE: process.env.NEXT_PUBLIC_WEBMENTION_ENABLE || false,
|
||||
AUTH: process.env.NEXT_PUBLIC_WEBMENTION_AUTH || '',
|
||||
HOSTNAME: process.env.NEXT_PUBLIC_WEBMENTION_HOSTNAME || '',
|
||||
TWITTER_USERNAME: process.env.NEXT_PUBLIC_TWITTER_USERNAME || '',
|
||||
TOKEN: process.env.NEXT_PUBLIC_WEBMENTION_TOKEN || ''
|
||||
},
|
||||
{
|
||||
name: '王都グランセル',
|
||||
artist: 'Falcom Sound Team jdk',
|
||||
url: 'https://music.163.com/song/media/outer/url?id=731355.mp3',
|
||||
cover:
|
||||
'https://p1.music.126.net/kn6ugISTonvqJh3LHLaPtQ==/599233837187278.jpg'
|
||||
}
|
||||
],
|
||||
MUSIC_PLAYER_METING: process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING || false, // 是否要开启 MetingJS,从平台获取歌单。会覆盖自定义的 MUSIC_PLAYER_AUDIO_LIST,更多配置信息:https://github.com/metowolf/MetingJS
|
||||
MUSIC_PLAYER_METING_SERVER:
|
||||
process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_SERVER || 'netease', // 音乐平台,[netease, tencent, kugou, xiami, baidu]
|
||||
MUSIC_PLAYER_METING_ID:
|
||||
process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_ID || '60198', // 对应歌单的 id
|
||||
MUSIC_PLAYER_METING_LRC_TYPE:
|
||||
process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_LRC_TYPE || '1', // 可选值: 3 | 1 | 0(0:禁用 lrc 歌词,1:lrc 格式的字符串,3:lrc 文件 url)
|
||||
|
||||
// ----> 评论互动 可同时开启多个支持 WALINE VALINE GISCUS CUSDIS UTTERRANCES GITALK
|
||||
|
||||
// twikoo
|
||||
COMMENT_TWIKOO_ENV_ID: process.env.NEXT_PUBLIC_COMMENT_ENV_ID || '', // TWIKOO地址 腾讯云环境填 envId;Vercel 环境域名地址(https://xxx.vercel.app)
|
||||
|
||||
// utterance
|
||||
COMMENT_UTTERRANCES_REPO:
|
||||
process.env.NEXT_PUBLIC_COMMENT_UTTERRANCES_REPO || '', // 你的代码仓库名, 例如我是 'tangly1024/NotionNext'; 更多文档参考 https://utteranc.es/
|
||||
|
||||
// giscus @see https://giscus.app/
|
||||
COMMENT_GISCUS_REPO: process.env.NEXT_PUBLIC_COMMENT_GISCUS_REPO || '', // 你的Github仓库名 e.g 'tangly1024/NotionNext'
|
||||
COMMENT_GISCUS_REPO_ID: process.env.NEXT_PUBLIC_COMMENT_GISCUS_REPO_ID || '', // 你的Github Repo ID e.g ( 設定完 giscus 即可看到 )
|
||||
COMMENT_GISCUS_CATEGORY_ID:
|
||||
process.env.NEXT_PUBLIC_COMMENT_GISCUS_CATEGORY_ID || '', // 你的Github Discussions 內的 Category ID ( 設定完 giscus 即可看到 )
|
||||
COMMENT_GISCUS_MAPPING:
|
||||
process.env.NEXT_PUBLIC_COMMENT_GISCUS_MAPPING || 'pathname', // 你的Github Discussions 使用哪種方式來標定文章, 預設 'pathname'
|
||||
COMMENT_GISCUS_REACTIONS_ENABLED:
|
||||
process.env.NEXT_PUBLIC_COMMENT_GISCUS_REACTIONS_ENABLED || '1', // 你的 Giscus 是否開啟文章表情符號 '1' 開啟 "0" 關閉 預設開啟
|
||||
COMMENT_GISCUS_EMIT_METADATA:
|
||||
process.env.NEXT_PUBLIC_COMMENT_GISCUS_EMIT_METADATA || '0', // 你的 Giscus 是否提取 Metadata '1' 開啟 '0' 關閉 預設關閉
|
||||
COMMENT_GISCUS_INPUT_POSITION:
|
||||
process.env.NEXT_PUBLIC_COMMENT_GISCUS_INPUT_POSITION || 'bottom', // 你的 Giscus 發表留言位置 'bottom' 尾部 'top' 頂部, 預設 'bottom'
|
||||
COMMENT_GISCUS_LANG: process.env.NEXT_PUBLIC_COMMENT_GISCUS_LANG || 'zh-CN', // 你的 Giscus 語言 e.g 'en', 'zh-TW', 'zh-CN', 預設 'en'
|
||||
COMMENT_GISCUS_LOADING:
|
||||
process.env.NEXT_PUBLIC_COMMENT_GISCUS_LOADING || 'lazy', // 你的 Giscus 載入是否漸進式載入, 預設 'lazy'
|
||||
COMMENT_GISCUS_CROSSORIGIN:
|
||||
process.env.NEXT_PUBLIC_COMMENT_GISCUS_CROSSORIGIN || 'anonymous', // 你的 Giscus 可以跨網域, 預設 'anonymous'
|
||||
|
||||
COMMENT_CUSDIS_APP_ID: process.env.NEXT_PUBLIC_COMMENT_CUSDIS_APP_ID || '', // data-app-id 36位 see https://cusdis.com/
|
||||
COMMENT_CUSDIS_HOST:
|
||||
process.env.NEXT_PUBLIC_COMMENT_CUSDIS_HOST || 'https://cusdis.com', // data-host, change this if you're using self-hosted version
|
||||
COMMENT_CUSDIS_SCRIPT_SRC:
|
||||
process.env.NEXT_PUBLIC_COMMENT_CUSDIS_SCRIPT_SRC ||
|
||||
'https://cusdis.com/js/cusdis.es.js', // change this if you're using self-hosted version
|
||||
|
||||
// gitalk评论插件 更多参考 https://gitalk.github.io/
|
||||
COMMENT_GITALK_REPO: process.env.NEXT_PUBLIC_COMMENT_GITALK_REPO || '', // 你的Github仓库名,例如 'NotionNext'
|
||||
COMMENT_GITALK_OWNER: process.env.NEXT_PUBLIC_COMMENT_GITALK_OWNER || '', // 你的用户名 e.g tangly1024
|
||||
COMMENT_GITALK_ADMIN: process.env.NEXT_PUBLIC_COMMENT_GITALK_ADMIN || '', // 管理员用户名、一般是自己 e.g 'tangly1024'
|
||||
COMMENT_GITALK_CLIENT_ID:
|
||||
process.env.NEXT_PUBLIC_COMMENT_GITALK_CLIENT_ID || '', // e.g 20位ID , 在gitalk后台获取
|
||||
COMMENT_GITALK_CLIENT_SECRET:
|
||||
process.env.NEXT_PUBLIC_COMMENT_GITALK_CLIENT_SECRET || '', // e.g 40位ID, 在gitalk后台获取
|
||||
COMMENT_GITALK_DISTRACTION_FREE_MODE: false, // 类似facebook的无干扰模式
|
||||
|
||||
COMMENT_GITTER_ROOM: process.env.NEXT_PUBLIC_COMMENT_GITTER_ROOM || '', // gitter聊天室 see https://gitter.im/ 不需要则留空
|
||||
COMMENT_DAO_VOICE_ID: process.env.NEXT_PUBLIC_COMMENT_DAO_VOICE_ID || '', // DaoVoice http://dashboard.daovoice.io/get-started
|
||||
COMMENT_TIDIO_ID: process.env.NEXT_PUBLIC_COMMENT_TIDIO_ID || '', // [tidio_id] -> //code.tidio.co/[tidio_id].js
|
||||
|
||||
COMMENT_VALINE_APP_ID: process.env.NEXT_PUBLIC_VALINE_ID || '', // Valine @see https://valine.js.org/quickstart.html 或 https://github.com/stonehank/react-valine#%E8%8E%B7%E5%8F%96app-id-%E5%92%8C-app-key
|
||||
COMMENT_VALINE_APP_KEY: process.env.NEXT_PUBLIC_VALINE_KEY || '',
|
||||
COMMENT_VALINE_SERVER_URLS: process.env.NEXT_PUBLIC_VALINE_SERVER_URLS || '', // 该配置适用于国内自定义域名用户, 海外版本会自动检测(无需手动填写) @see https://valine.js.org/configuration.html#serverURLs
|
||||
COMMENT_VALINE_PLACEHOLDER:
|
||||
process.env.NEXT_PUBLIC_VALINE_PLACEHOLDER || '抢个沙发吧~', // 可以搭配后台管理评论 https://github.com/DesertsP/Valine-Admin 便于查看评论,以及邮件通知,垃圾评论过滤等功能
|
||||
|
||||
COMMENT_WALINE_SERVER_URL: process.env.NEXT_PUBLIC_WALINE_SERVER_URL || '', // 请配置完整的Waline评论地址 例如 hhttps://preview-waline.tangly1024.com @see https://waline.js.org/guide/get-started.html
|
||||
COMMENT_WALINE_RECENT: process.env.NEXT_PUBLIC_WALINE_RECENT || false, // 最新评论
|
||||
|
||||
// 此评论系统基于WebMention,细节可参考https://webmention.io
|
||||
// 它是一个基于IndieWeb理念的开放式评论系统,下方COMMENT_WEBMENTION包含的属性皆需配置:
|
||||
// ENABLE: 是否开启
|
||||
// AUTH: Webmention使用的IndieLogin,可使用Twitter或Github个人页面连结
|
||||
// HOSTNAME: Webmention绑定之网域,通常即为本站网址
|
||||
// TWITTER_USERNAME: 评论显示区域需要的资讯
|
||||
// TOKEN: Webmention的API token
|
||||
COMMENT_WEBMENTION: {
|
||||
ENABLE: process.env.NEXT_PUBLIC_WEBMENTION_ENABLE || false,
|
||||
AUTH: process.env.NEXT_PUBLIC_WEBMENTION_AUTH || '',
|
||||
HOSTNAME: process.env.NEXT_PUBLIC_WEBMENTION_HOSTNAME || '',
|
||||
TWITTER_USERNAME: process.env.NEXT_PUBLIC_TWITTER_USERNAME || '',
|
||||
TOKEN: process.env.NEXT_PUBLIC_WEBMENTION_TOKEN || ''
|
||||
},
|
||||
|
||||
// <---- 评论插件
|
||||
|
||||
// ----> 站点统计
|
||||
ANALYTICS_VERCEL: process.env.NEXT_PUBLIC_ANALYTICS_VERCEL || true, // vercel自带的统计 https://vercel.com/docs/concepts/analytics/quickstart https://github.com/tangly1024/NotionNext/issues/897
|
||||
ANALYTICS_BUSUANZI_ENABLE: true, // 展示网站阅读量、访问数 see http://busuanzi.ibruce.info/
|
||||
ANALYTICS_BAIDU_ID: process.env.NEXT_PUBLIC_ANALYTICS_BAIDU_ID || '', // e.g 只需要填写百度统计的id,[baidu_id] -> https://hm.baidu.com/hm.js?[baidu_id]
|
||||
ANALYTICS_CNZZ_ID: process.env.NEXT_PUBLIC_ANALYTICS_CNZZ_ID || '', // 只需要填写站长统计的id, [cnzz_id] -> https://s9.cnzz.com/z_stat.php?id=[cnzz_id]&web_id=[cnzz_id]
|
||||
ANALYTICS_GOOGLE_ID: process.env.NEXT_PUBLIC_ANALYTICS_GOOGLE_ID || '', // 谷歌Analytics的id e.g: G-XXXXXXXXXX
|
||||
|
||||
ANALYTICS_ACKEE_TRACKER:
|
||||
process.env.NEXT_PUBLIC_ANALYTICS_ACKEE_TRACKER || '', // e.g 'https://ackee.tangly1024.net/tracker.js'
|
||||
ANALYTICS_ACKEE_DATA_SERVER:
|
||||
process.env.NEXT_PUBLIC_ANALYTICS_ACKEE_DATA_SERVER || '', // e.g https://ackee.tangly1024.net , don't end with a slash
|
||||
ANALYTICS_ACKEE_DOMAIN_ID:
|
||||
process.env.NEXT_PUBLIC_ANALYTICS_ACKEE_DOMAIN_ID || '', // e.g '0e2257a8-54d4-4847-91a1-0311ea48cc7b'
|
||||
|
||||
SEO_GOOGLE_SITE_VERIFICATION:
|
||||
process.env.NEXT_PUBLIC_SEO_GOOGLE_SITE_VERIFICATION || '', // Remove the value or replace it with your own google site verification code
|
||||
|
||||
// <---- 站点统计
|
||||
|
||||
// 谷歌广告
|
||||
ADSENSE_GOOGLE_ID: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_ID || '', // 谷歌广告ID e.g ca-pub-xxxxxxxxxxxxxxxx
|
||||
|
||||
// 自定义配置notion数据库字段名
|
||||
NOTION_PROPERTY_NAME: {
|
||||
password: process.env.NEXT_PUBLIC_NOTION_PROPERTY_PASSWORD || 'password',
|
||||
type: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE || 'type', // 文章类型,
|
||||
type_post: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_POST || 'Post', // 当type文章类型与此值相同时,为博文。
|
||||
type_page: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_PAGE || 'Page', // 当type文章类型与此值相同时,为单页。
|
||||
type_notice:
|
||||
process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_NOTICE || 'Notice', // 当type文章类型与此值相同时,为公告。
|
||||
type_menu: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_MENU || 'Menu', // 当type文章类型与此值相同时,为菜单。
|
||||
type_sub_menu:
|
||||
process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_SUB_MENU || 'SubMenu', // 当type文章类型与此值相同时,为子菜单。
|
||||
title: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TITLE || 'title', // 文章标题
|
||||
status: process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS || 'status',
|
||||
status_publish:
|
||||
process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS_PUBLISH || 'Published', // 当status状态值与此相同时为发布,可以为中文
|
||||
status_invisible:
|
||||
process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS_INVISIBLE || 'Invisible', // 当status状态值与此相同时为隐藏发布,可以为中文 , 除此之外其他页面状态不会显示在博客上
|
||||
summary: process.env.NEXT_PUBLIC_NOTION_PROPERTY_SUMMARY || 'summary',
|
||||
slug: process.env.NEXT_PUBLIC_NOTION_PROPERTY_SLUG || 'slug',
|
||||
category: process.env.NEXT_PUBLIC_NOTION_PROPERTY_CATEGORY || 'category',
|
||||
date: process.env.NEXT_PUBLIC_NOTION_PROPERTY_DATE || 'date',
|
||||
tags: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TAGS || 'tags',
|
||||
icon: process.env.NEXT_PUBLIC_NOTION_PROPERTY_ICON || 'icon'
|
||||
},
|
||||
|
||||
// RSS
|
||||
ENABLE_RSS: process.env.NEXT_PUBLIC_ENABLE_RSS || true, // 是否开启RSS订阅功能
|
||||
|
||||
// 作废配置
|
||||
AVATAR: process.env.NEXT_PUBLIC_AVATAR || '/avatar.svg', // 作者头像,被notion中的ICON覆盖。若无ICON则取public目录下的avatar.png
|
||||
TITLE: process.env.NEXT_PUBLIC_TITLE || 'VIXCITY BLOG', // 站点标题 ,被notion中的页面标题覆盖
|
||||
HOME_BANNER_IMAGE:
|
||||
process.env.NEXT_PUBLIC_HOME_BANNER_IMAGE || './bg_image.jpg', // 首页背景大图, 会被notion中的封面图覆盖,若无封面图则会使用代码中的 /public/bg_image.jpg 文件
|
||||
DESCRIPTION:
|
||||
process.env.NEXT_PUBLIC_DESCRIPTION || '我的小破博客', // 站点描述,被notion中的页面描述覆盖
|
||||
|
||||
// 网站图片
|
||||
IMG_URL_TYPE: process.env.NEXT_PUBLIC_IMG_TYPE || 'Notion', // 此配置已失效,请勿使用;AMAZON方案不再支持,仅支持Notion方案。 ['Notion','AMAZON'] 站点图片前缀 默认 Notion:(https://notion.so/images/xx) , AMAZON(https://s3.us-west-2.amazonaws.com/xxx)
|
||||
IMG_SHADOW: process.env.NEXT_PUBLIC_IMG_SHADOW || false, // 文章图片是否自动添加阴影
|
||||
|
||||
// 开发相关
|
||||
NOTION_ACCESS_TOKEN: process.env.NOTION_ACCESS_TOKEN || '', // Useful if you prefer not to make your database public
|
||||
DEBUG: process.env.NEXT_PUBLIC_DEBUG || false, // 是否显示调试按钮
|
||||
ENABLE_CACHE: process.env.ENABLE_CACHE || false, // 开启缓存会将Notion数据缓存在内存中,通常在开发调试中使用,正式部署开启此功能意义不大。
|
||||
isProd: process.env.VERCEL_ENV === 'production', // distinguish between development and production environment (ref: https://vercel.com/docs/environment-variables#system-environment-variables) isProd: process.env.VERCEL_ENV === 'production' // distinguish between development and production environment (ref: https://vercel.com/docs/environment-variables#system-environment-variables)
|
||||
VERSION: process.env.NEXT_PUBLIC_VERSION // 版本号
|
||||
}
|
||||
|
||||
module.exports = BLOG
|
||||
|
||||
// <---- 评论插件
|
||||
|
||||
// ----> 站点统计
|
||||
ANALYTICS_VERCEL: process.env.NEXT_PUBLIC_ANALYTICS_VERCEL || false, // vercel自带的统计 https://vercel.com/docs/concepts/analytics/quickstart https://github.com/tangly1024/NotionNext/issues/897
|
||||
ANALYTICS_BUSUANZI_ENABLE: process.env.NEXT_PUBLIC_ANALYTICS_BUSUANZI_ENABLE || true, // 展示网站阅读量、访问数 see http://busuanzi.ibruce.info/
|
||||
ANALYTICS_BAIDU_ID: process.env.NEXT_PUBLIC_ANALYTICS_BAIDU_ID || '', // e.g 只需要填写百度统计的id,[baidu_id] -> https://hm.baidu.com/hm.js?[baidu_id]
|
||||
ANALYTICS_CNZZ_ID: process.env.NEXT_PUBLIC_ANALYTICS_CNZZ_ID || '', // 只需要填写站长统计的id, [cnzz_id] -> https://s9.cnzz.com/z_stat.php?id=[cnzz_id]&web_id=[cnzz_id]
|
||||
ANALYTICS_GOOGLE_ID: process.env.NEXT_PUBLIC_ANALYTICS_GOOGLE_ID || '', // 谷歌Analytics的id e.g: G-XXXXXXXXXX
|
||||
|
||||
ANALYTICS_ACKEE_TRACKER:
|
||||
process.env.NEXT_PUBLIC_ANALYTICS_ACKEE_TRACKER || '', // e.g 'https://ackee.tangly1024.net/tracker.js'
|
||||
ANALYTICS_ACKEE_DATA_SERVER:
|
||||
process.env.NEXT_PUBLIC_ANALYTICS_ACKEE_DATA_SERVER || '', // e.g https://ackee.tangly1024.net , don't end with a slash
|
||||
ANALYTICS_ACKEE_DOMAIN_ID:
|
||||
process.env.NEXT_PUBLIC_ANALYTICS_ACKEE_DOMAIN_ID || '', // e.g '0e2257a8-54d4-4847-91a1-0311ea48cc7b'
|
||||
|
||||
SEO_GOOGLE_SITE_VERIFICATION:
|
||||
process.env.NEXT_PUBLIC_SEO_GOOGLE_SITE_VERIFICATION || '', // Remove the value or replace it with your own google site verification code
|
||||
|
||||
SEO_BAIDU_SITE_VERIFICATION:
|
||||
process.env.NEXT_PUBLIC_SEO_BAIDU_SITE_VERIFICATION || '', // Remove the value or replace it with your own google site verification code
|
||||
|
||||
// <---- 站点统计
|
||||
|
||||
// 谷歌广告
|
||||
ADSENSE_GOOGLE_ID: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_ID || '', // 谷歌广告ID e.g ca-pub-xxxxxxxxxxxxxxxx
|
||||
ADSENSE_GOOGLE_TEST: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_TEST || false, // 谷歌广告ID测试模式,这种模式获取假的测试广告,用于开发 https://www.tangly1024.com/article/local-dev-google-adsense
|
||||
ADSENSE_GOOGLE_SLOT_IN_ARTICLE: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_IN_ARTICLE || '3806269138', // Google AdScene>广告>按单元广告>新建文章内嵌广告 粘贴html代码中的data-ad-slot值
|
||||
ADSENSE_GOOGLE_SLOT_FLOW: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_FLOW || '1510444138', // Google AdScene>广告>按单元广告>新建信息流广告
|
||||
ADSENSE_GOOGLE_SLOT_NATIVE: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_NATIVE || '4980048999', // Google AdScene>广告>按单元广告>新建原生广告
|
||||
ADSENSE_GOOGLE_SLOT_AUTO: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_AUTO || '8807314373', // Google AdScene>广告>按单元广告>新建展示广告 (自动广告)
|
||||
|
||||
// 自定义配置notion数据库字段名
|
||||
NOTION_PROPERTY_NAME: {
|
||||
password: process.env.NEXT_PUBLIC_NOTION_PROPERTY_PASSWORD || 'password',
|
||||
type: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE || 'type', // 文章类型,
|
||||
type_post: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_POST || 'Post', // 当type文章类型与此值相同时,为博文。
|
||||
type_page: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_PAGE || 'Page', // 当type文章类型与此值相同时,为单页。
|
||||
type_notice:
|
||||
process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_NOTICE || 'Notice', // 当type文章类型与此值相同时,为公告。
|
||||
type_menu: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_MENU || 'Menu', // 当type文章类型与此值相同时,为菜单。
|
||||
type_sub_menu:
|
||||
process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_SUB_MENU || 'SubMenu', // 当type文章类型与此值相同时,为子菜单。
|
||||
title: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TITLE || 'title', // 文章标题
|
||||
status: process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS || 'status',
|
||||
status_publish:
|
||||
process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS_PUBLISH || 'Published', // 当status状态值与此相同时为发布,可以为中文
|
||||
status_invisible:
|
||||
process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS_INVISIBLE || 'Invisible', // 当status状态值与此相同时为隐藏发布,可以为中文 , 除此之外其他页面状态不会显示在博客上
|
||||
summary: process.env.NEXT_PUBLIC_NOTION_PROPERTY_SUMMARY || 'summary',
|
||||
slug: process.env.NEXT_PUBLIC_NOTION_PROPERTY_SLUG || 'slug',
|
||||
category: process.env.NEXT_PUBLIC_NOTION_PROPERTY_CATEGORY || 'category',
|
||||
date: process.env.NEXT_PUBLIC_NOTION_PROPERTY_DATE || 'date',
|
||||
tags: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TAGS || 'tags',
|
||||
icon: process.env.NEXT_PUBLIC_NOTION_PROPERTY_ICON || 'icon'
|
||||
},
|
||||
|
||||
// RSS订阅
|
||||
ENABLE_RSS: process.env.NEXT_PUBLIC_ENABLE_RSS || true, // 是否开启RSS订阅功能
|
||||
MAILCHIMP_LIST_ID: process.env.MAILCHIMP_LIST_ID || null, // 开启mailichimp邮件订阅 客户列表ID ,具体使用方法参阅文档
|
||||
MAILCHIMP_API_KEY: process.env.MAILCHIMP_API_KEY || null, // 开启mailichimp邮件订阅 APIkey
|
||||
|
||||
// 作废配置
|
||||
AVATAR: process.env.NEXT_PUBLIC_AVATAR || '/avatar.svg', // 作者头像,被notion中的ICON覆盖。若无ICON则取public目录下的avatar.png
|
||||
TITLE: process.env.NEXT_PUBLIC_TITLE || 'NotionNext BLOG', // 站点标题 ,被notion中的页面标题覆盖;此处请勿留空白,否则服务器无法编译
|
||||
HOME_BANNER_IMAGE:
|
||||
process.env.NEXT_PUBLIC_HOME_BANNER_IMAGE || '/bg_image.jpg', // 首页背景大图, 会被notion中的封面图覆盖,若无封面图则会使用代码中的 /public/bg_image.jpg 文件
|
||||
DESCRIPTION:
|
||||
process.env.NEXT_PUBLIC_DESCRIPTION || '这是一个由NotionNext生成的站点', // 站点描述,被notion中的页面描述覆盖
|
||||
|
||||
// 网站图片
|
||||
IMG_LAZY_LOAD_PLACEHOLDER: process.env.NEXT_PUBLIC_IMG_LAZY_LOAD_PLACEHOLDER || 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==', // 懒加载占位图片地址,支持base64或url
|
||||
IMG_URL_TYPE: process.env.NEXT_PUBLIC_IMG_TYPE || 'Notion', // 此配置已失效,请勿使用;AMAZON方案不再支持,仅支持Notion方案。 ['Notion','AMAZON'] 站点图片前缀 默认 Notion:(https://notion.so/images/xx) , AMAZON(https://s3.us-west-2.amazonaws.com/xxx)
|
||||
IMG_SHADOW: process.env.NEXT_PUBLIC_IMG_SHADOW || false, // 文章图片是否自动添加阴影
|
||||
|
||||
// 开发相关
|
||||
NOTION_ACCESS_TOKEN: process.env.NOTION_ACCESS_TOKEN || '', // Useful if you prefer not to make your database public
|
||||
DEBUG: process.env.NEXT_PUBLIC_DEBUG || false, // 是否显示调试按钮
|
||||
ENABLE_CACHE: process.env.ENABLE_CACHE || false, // 开启缓存会将Notion数据缓存在内存中,通常在开发调试中使用,正式部署开启此功能意义不大。
|
||||
isProd: process.env.VERCEL_ENV === 'production', // distinguish between development and production environment (ref: https://vercel.com/docs/environment-variables#system-environment-variables) isProd: process.env.VERCEL_ENV === 'production' // distinguish between development and production environment (ref: https://vercel.com/docs/environment-variables#system-environment-variables)
|
||||
VERSION: process.env.NEXT_PUBLIC_VERSION // 版本号
|
||||
}
|
||||
|
||||
module.exports = BLOG
|
||||
92
components/AlgoliaSearchModal.js
Normal file
92
components/AlgoliaSearchModal.js
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useState, useImperativeHandle } from 'react'
|
||||
import BLOG from '@/blog.config'
|
||||
import algoliasearch from 'algoliasearch'
|
||||
import replaceSearchResult from '@/components/Mark'
|
||||
|
||||
/**
|
||||
* 结合 Algolia 实现的弹出式搜索框
|
||||
* 打开方式 cRef.current.openSearch()
|
||||
* https://www.algolia.com/doc/api-reference/search-api-parameters/
|
||||
*/
|
||||
export default function AlgoliaSearchModal({ cRef }) {
|
||||
const [searchResults, setSearchResults] = useState([])
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
/**
|
||||
* 对外暴露方法
|
||||
*/
|
||||
useImperativeHandle(cRef, () => {
|
||||
return {
|
||||
openSearch: () => {
|
||||
setIsModalOpen(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!BLOG.ALGOLIA_APP_ID) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
const client = algoliasearch(BLOG.ALGOLIA_APP_ID, BLOG.ALGOLIA_SEARCH_ONLY_APP_KEY)
|
||||
const index = client.initIndex(BLOG.ALGOLIA_INDEX)
|
||||
|
||||
const handleSearch = async (query) => {
|
||||
try {
|
||||
const res = await index.search(query)
|
||||
console.log(res)
|
||||
const { hits } = res
|
||||
setSearchResults(hits)
|
||||
const doms = document.getElementById('search-wrapper').getElementsByClassName('replace')
|
||||
replaceSearchResult({
|
||||
doms,
|
||||
search: query,
|
||||
target: {
|
||||
element: 'span',
|
||||
className: 'text-blue-600 border-b border-dashed'
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Algolia search error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
setIsModalOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div id='search-wrapper' className={`${isModalOpen ? 'opacity-100' : 'invisible opacity-0 pointer-events-none'} fixed h-screen w-screen left-0 top-0 flex items-center justify-center`}>
|
||||
{/* 内容 */}
|
||||
<div className={`${isModalOpen ? 'opacity-100' : 'invisible opacity-0 translate-y-10'} flex flex-col justify-between w-full min-h-[10rem] max-w-xl dark:bg-hexo-black-gray dark:border-gray-800 bg-white dark:bg- p-5 rounded-lg z-50 shadow border hover:border-blue-600 duration-300 transition-all `}>
|
||||
|
||||
<div className='flex justify-between items-center'>
|
||||
<div className='text-2xl text-blue-600 font-bold'>搜索</div>
|
||||
<div><i class="text-gray-600 fa-solid fa-xmark p-1 cursor-pointer hover:text-blue-600" onClick={closeModal} ></i></div>
|
||||
</div>
|
||||
|
||||
<input type="text" placeholder="在这里输入搜索关键词..." onChange={(e) => handleSearch(e.target.value)}
|
||||
className="bg-gray-50 dark:bg-gray-600 outline-blue-500 w-full px-4 my-2 py-1 mb-4 border rounded-md" />
|
||||
|
||||
{/* 标签组 */}
|
||||
<div>
|
||||
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
{searchResults.map((result) => (
|
||||
<li key={result.objectID} className="replace my-2">
|
||||
<a href={`${BLOG.SUB_PATH}/${result.slug}`} className="font-bold hover:text-blue-600 ">
|
||||
{result.title}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className='text-gray-600'><i class="fa-brands fa-algolia"></i> Algolia 提供搜索服务</div>
|
||||
</div>
|
||||
|
||||
{/* 遮罩 */}
|
||||
<div onClick={closeModal} className="z-30 fixed top-0 left-0 w-full h-full flex items-center justify-center glassmorphism" />
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
19
components/ChatBase.js
Normal file
19
components/ChatBase.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import BLOG from '@/blog.config'
|
||||
|
||||
/**
|
||||
* 这是一个嵌入组件,可以在任意位置全屏显示您的chat-base对话框
|
||||
* 暂时没有页面引用
|
||||
* 因为您可以直接用内嵌网页的方式放入您的notion中 https://www.chatbase.co/chatbot-iframe/${BLOG.CHATBASE_ID}
|
||||
*/
|
||||
export default function ChatBase() {
|
||||
if (!BLOG.CHATBASE_ID) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return <iframe
|
||||
src={`https://www.chatbase.co/chatbot-iframe/${BLOG.CHATBASE_ID}`}
|
||||
width="100%"
|
||||
style={{ height: '100%', minHeight: '700px' }}
|
||||
frameborder="0"
|
||||
></iframe>
|
||||
}
|
||||
@@ -84,7 +84,7 @@ const Collapse = props => {
|
||||
}, [props.isOpen])
|
||||
|
||||
return (
|
||||
<div ref={ref} style={type === 'vertical' ? { height: '0px' } : { width: '0px' }} className={'overflow-hidden duration-200 ' + props.className}>
|
||||
<div ref={ref} style={type === 'vertical' ? { height: '0px', willChange: 'height' } : { width: '0px', willChange: 'width' }} className={`${props.className || ''} overflow-hidden duration-200 `}>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import dynamic from 'next/dynamic'
|
||||
import Tabs from '@/components/Tabs'
|
||||
import React from 'react'
|
||||
import { isBrowser } from '@/lib/utils'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
const WalineComponent = dynamic(
|
||||
@@ -54,63 +54,64 @@ const ValineComponent = dynamic(() => import('@/components/ValineComponent'), {
|
||||
ssr: false
|
||||
})
|
||||
|
||||
const Comment = ({ frontMatter }) => {
|
||||
/**
|
||||
* 评论组件
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const Comment = ({ frontMatter, className }) => {
|
||||
const router = useRouter()
|
||||
|
||||
React.useEffect(() => {
|
||||
// 跳转到评论区
|
||||
if (isBrowser() && ('giscus' in router.query || router.query.target === 'comment')) {
|
||||
setTimeout(() => {
|
||||
if (window.location.href.indexOf('target=comment') > -1) {
|
||||
const url = router.asPath.replace('?target=comment', '')
|
||||
history.replaceState({}, '', url)
|
||||
const commentNode = document.getElementById('comment')
|
||||
commentNode.scrollIntoView({ block: 'start', behavior: 'smooth' })
|
||||
}
|
||||
}, 200)
|
||||
}, [])
|
||||
const url = router.asPath.replace('?target=comment', '')
|
||||
history.replaceState({}, '', url)
|
||||
document?.getElementById('comment')?.scrollIntoView({ block: 'start', behavior: 'smooth' })
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
if (!frontMatter) {
|
||||
return <>Loading...</>
|
||||
}
|
||||
|
||||
return (
|
||||
<div id='comment' className='comment mt-5 text-gray-800 dark:text-gray-300'>
|
||||
<Tabs>
|
||||
<div key={frontMatter?.id} id='comment' className={`comment mt-5 text-gray-800 dark:text-gray-300 ${className || ''}`}>
|
||||
<Tabs>
|
||||
|
||||
{ BLOG.COMMENT_TWIKOO_ENV_ID && (<div key='Twikoo'>
|
||||
<TwikooCompenent/>
|
||||
</div>)}
|
||||
{BLOG.COMMENT_TWIKOO_ENV_ID && (<div key='Twikoo'>
|
||||
<TwikooCompenent />
|
||||
</div>)}
|
||||
|
||||
{ BLOG.COMMENT_WALINE_SERVER_URL && (<div key='Waline'>
|
||||
<WalineComponent/>
|
||||
</div>) }
|
||||
{BLOG.COMMENT_WALINE_SERVER_URL && (<div key='Waline'>
|
||||
<WalineComponent />
|
||||
</div>)}
|
||||
|
||||
{BLOG.COMMENT_VALINE_APP_ID && (<div key='Valine' name='reply'>
|
||||
<ValineComponent path={frontMatter.id}/>
|
||||
</div>)}
|
||||
{BLOG.COMMENT_VALINE_APP_ID && (<div key='Valine' name='reply'>
|
||||
<ValineComponent path={frontMatter.id} />
|
||||
</div>)}
|
||||
|
||||
{BLOG.COMMENT_GISCUS_REPO && (
|
||||
<div key="Giscus">
|
||||
<GiscusComponent className="px-2" />
|
||||
</div>
|
||||
)}
|
||||
{BLOG.COMMENT_GISCUS_REPO && (
|
||||
<div key="Giscus">
|
||||
<GiscusComponent className="px-2" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{BLOG.COMMENT_CUSDIS_APP_ID && (<div key='Cusdis'>
|
||||
<CusdisComponent frontMatter={frontMatter}/>
|
||||
</div>)}
|
||||
{BLOG.COMMENT_CUSDIS_APP_ID && (<div key='Cusdis'>
|
||||
<CusdisComponent frontMatter={frontMatter} />
|
||||
</div>)}
|
||||
|
||||
{BLOG.COMMENT_UTTERRANCES_REPO && (<div key='Utterance'>
|
||||
<UtterancesComponent issueTerm={frontMatter.id} className='px-2' />
|
||||
</div>)}
|
||||
{BLOG.COMMENT_UTTERRANCES_REPO && (<div key='Utterance'>
|
||||
<UtterancesComponent issueTerm={frontMatter.id} className='px-2' />
|
||||
</div>)}
|
||||
|
||||
{BLOG.COMMENT_GITALK_CLIENT_ID && (<div key='GitTalk'>
|
||||
<GitalkComponent frontMatter={frontMatter}/>
|
||||
</div>)}
|
||||
{BLOG.COMMENT_GITALK_CLIENT_ID && (<div key='GitTalk'>
|
||||
<GitalkComponent frontMatter={frontMatter} />
|
||||
</div>)}
|
||||
|
||||
{BLOG.COMMENT_WEBMENTION.ENABLE && (<div key='WebMention'>
|
||||
<WebMentionComponent frontMatter={frontMatter} className="px-2" />
|
||||
</div>)}
|
||||
</Tabs>
|
||||
{BLOG.COMMENT_WEBMENTION.ENABLE && (<div key='WebMention'>
|
||||
<WebMentionComponent frontMatter={frontMatter} className="px-2" />
|
||||
</div>)}
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,58 +16,57 @@ const CommonHead = ({ meta, children }) => {
|
||||
const category = meta?.category || BLOG.KEYWORDS || '軟體科技' // section 主要是像是 category 這樣的分類,Facebook 用這個來抓連結的分類
|
||||
|
||||
return (
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
<meta name="theme-color" content={BLOG.BACKGROUND_DARK} />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<meta name="robots" content="follow, index" />
|
||||
<meta charSet="UTF-8" />
|
||||
{BLOG.SEO_GOOGLE_SITE_VERIFICATION && (
|
||||
<meta
|
||||
name="google-site-verification"
|
||||
content={BLOG.SEO_GOOGLE_SITE_VERIFICATION}
|
||||
/>
|
||||
)}
|
||||
<meta name="keywords" content={keywords} />
|
||||
<meta name="description" content={description} />
|
||||
<meta property="og:locale" content={lang} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:url" content={url} />
|
||||
<meta property="og:image" content={image} />
|
||||
<meta property="og:site_name" content={BLOG.TITLE} />
|
||||
<meta property="og:type" content={type} />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
<meta name="theme-color" content={BLOG.BACKGROUND_DARK} />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, minimum-scale=1.0" />
|
||||
<meta name="robots" content="follow, index" />
|
||||
<meta charSet="UTF-8" />
|
||||
{BLOG.SEO_GOOGLE_SITE_VERIFICATION && (
|
||||
<meta
|
||||
name="google-site-verification"
|
||||
content={BLOG.SEO_GOOGLE_SITE_VERIFICATION}
|
||||
/>
|
||||
)}
|
||||
{BLOG.SEO_BAIDU_SITE_VERIFICATION && (<meta name="baidu-site-verification" content={BLOG.SEO_BAIDU_SITE_VERIFICATION} />)}
|
||||
<meta name="keywords" content={keywords} />
|
||||
<meta name="description" content={description} />
|
||||
<meta property="og:locale" content={lang} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:url" content={url} />
|
||||
<meta property="og:image" content={image} />
|
||||
<meta property="og:site_name" content={BLOG.TITLE} />
|
||||
<meta property="og:type" content={type} />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:title" content={title} />
|
||||
|
||||
{BLOG.COMMENT_WEBMENTION.ENABLE && (
|
||||
<>
|
||||
<link rel="webmention" href={`https://webmention.io/${BLOG.COMMENT_WEBMENTION.HOSTNAME}/webmention`} />
|
||||
<link rel="pingback" href={`https://webmention.io/${BLOG.COMMENT_WEBMENTION.HOSTNAME}/xmlrpc`} />
|
||||
</>
|
||||
)}
|
||||
{BLOG.COMMENT_WEBMENTION.ENABLE && BLOG.COMMENT_WEBMENTION.AUTH !== '' && (
|
||||
<link href={BLOG.COMMENT_WEBMENTION.AUTH} rel="me" />
|
||||
)}
|
||||
{BLOG.COMMENT_WEBMENTION.ENABLE && (
|
||||
<>
|
||||
<link rel="webmention" href={`https://webmention.io/${BLOG.COMMENT_WEBMENTION.HOSTNAME}/webmention`} />
|
||||
<link rel="pingback" href={`https://webmention.io/${BLOG.COMMENT_WEBMENTION.HOSTNAME}/xmlrpc`} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{JSON.parse(BLOG.ANALYTICS_BUSUANZI_ENABLE) && <meta name="referrer" content="no-referrer-when-downgrade" />}
|
||||
{meta?.type === 'Post' && (
|
||||
<>
|
||||
<meta
|
||||
property="article:published_time"
|
||||
content={meta.date || meta.createdTime}
|
||||
/>
|
||||
<meta property="article:author" content={BLOG.AUTHOR} />
|
||||
<meta property="article:section" content={category} />
|
||||
<meta property="article:publisher" content={BLOG.FACEBOOK_PAGE} />
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
</Head>
|
||||
{BLOG.COMMENT_WEBMENTION.ENABLE && BLOG.COMMENT_WEBMENTION.AUTH !== '' && (
|
||||
<link href={BLOG.COMMENT_WEBMENTION.AUTH} rel="me" />
|
||||
)}
|
||||
|
||||
{JSON.parse(BLOG.ANALYTICS_BUSUANZI_ENABLE) && <meta name="referrer" content="no-referrer-when-downgrade" />}
|
||||
{meta?.type === 'Post' && (
|
||||
<>
|
||||
<meta
|
||||
property="article:published_time"
|
||||
content={meta.publishTime}
|
||||
/>
|
||||
<meta property="article:author" content={BLOG.AUTHOR} />
|
||||
<meta property="article:section" content={category} />
|
||||
<meta property="article:publisher" content={BLOG.FACEBOOK_PAGE} />
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
</Head>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { Analytics } from '@vercel/analytics/react'
|
||||
|
||||
/**
|
||||
* 第三方代码 统计脚本
|
||||
@@ -8,7 +7,17 @@ import { Analytics } from '@vercel/analytics/react'
|
||||
*/
|
||||
const CommonScript = () => {
|
||||
return (<>
|
||||
{BLOG.ANALYTICS_VERCEL && <Analytics />}
|
||||
|
||||
{BLOG.CHATBASE_ID && (<>
|
||||
<script id={BLOG.CHATBASE_ID} src="https://www.chatbase.co/embed.min.js" defer/>
|
||||
<script async dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window.chatbaseConfig = {
|
||||
chatbotId: "${BLOG.CHATBASE_ID}",
|
||||
}
|
||||
`
|
||||
}}/>
|
||||
</>)}
|
||||
|
||||
{BLOG.COMMENT_DAO_VOICE_ID && (<>
|
||||
{/* DaoVoice 反馈 */}
|
||||
@@ -29,10 +38,6 @@ const CommonScript = () => {
|
||||
/>
|
||||
</>)}
|
||||
|
||||
{/* GoogleAdsense */}
|
||||
{BLOG.ADSENSE_GOOGLE_ID && <script async src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${BLOG.ADSENSE_GOOGLE_ID}`}
|
||||
crossOrigin="anonymous" />}
|
||||
|
||||
{BLOG.COMMENT_CUSDIS_APP_ID && <script defer src='https://cusdis.com/js/widget/lang/zh-cn.js' />}
|
||||
|
||||
{BLOG.COMMENT_TIDIO_ID && <script async src={`//code.tidio.co/${BLOG.COMMENT_TIDIO_ID}.js`} />}
|
||||
|
||||
162
components/CustomContextMenu.js
Normal file
162
components/CustomContextMenu.js
Normal file
@@ -0,0 +1,162 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { saveDarkModeToCookies, THEMES } from '@/themes/theme'
|
||||
import BLOG from '@/blog.config'
|
||||
|
||||
/**
|
||||
* 自定义右键菜单
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
export default function CustomContextMenu(props) {
|
||||
const [position, setPosition] = useState({ x: '0px', y: '0px' })
|
||||
const [show, setShow] = useState(false)
|
||||
const { isDarkMode, updateDarkMode, locale } = useGlobal()
|
||||
const menuRef = useRef(null)
|
||||
|
||||
const { latestPosts } = props
|
||||
const router = useRouter()
|
||||
/**
|
||||
* 随机跳转文章
|
||||
*/
|
||||
function handleJumpToRandomPost() {
|
||||
const randomIndex = Math.floor(Math.random() * latestPosts.length)
|
||||
const randomPost = latestPosts[randomIndex]
|
||||
router.push(`${BLOG.SUB_PATH}/${randomPost?.slug}`)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleContextMenu = (event) => {
|
||||
event.preventDefault()
|
||||
setPosition({ y: `${event.clientY}px`, x: `${event.clientX}px` })
|
||||
setShow(true)
|
||||
}
|
||||
|
||||
const handleClick = (event) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target)) {
|
||||
setShow(false)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('contextmenu', handleContextMenu)
|
||||
window.addEventListener('click', handleClick)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('contextmenu', handleContextMenu)
|
||||
window.removeEventListener('click', handleClick)
|
||||
}
|
||||
}, [])
|
||||
|
||||
function handleBack() {
|
||||
window.history.back()
|
||||
}
|
||||
|
||||
function handleForward() {
|
||||
window.history.forward()
|
||||
}
|
||||
|
||||
function handleRefresh() {
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
function handleScrollTop() {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
setShow(false)
|
||||
}
|
||||
|
||||
function handleCopyLink() {
|
||||
const url = window.location.href
|
||||
navigator.clipboard.writeText(url)
|
||||
.then(() => {
|
||||
console.log('页面地址已复制')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('复制页面地址失败:', error)
|
||||
})
|
||||
setShow(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换主题
|
||||
*/
|
||||
function handeChangeTheme() {
|
||||
const randomTheme = THEMES[Math.floor(Math.random() * THEMES.length)] // 从THEMES数组中 随机取一个主题
|
||||
const query = router.query
|
||||
query.theme = randomTheme
|
||||
router.push({ pathname: router.pathname, query })
|
||||
}
|
||||
|
||||
function handleChangeDarkMode() {
|
||||
const newStatus = !isDarkMode
|
||||
saveDarkModeToCookies(newStatus)
|
||||
updateDarkMode(newStatus)
|
||||
const htmlElement = document.getElementsByTagName('html')[0]
|
||||
htmlElement.classList?.remove(newStatus ? 'light' : 'dark')
|
||||
htmlElement.classList?.add(newStatus ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={menuRef}
|
||||
style={{ top: position.y, left: position.x }}
|
||||
className={`${show ? '' : 'invisible opacity-0'} select-none transition-opacity duration-200 fixed z-50`}
|
||||
>
|
||||
|
||||
{/* 菜单内容 */}
|
||||
<div className='rounded-xl w-52 dark:hover:border-yellow-600 bg-white dark:bg-[#040404] dark:text-gray-200 dark:border-gray-600 p-3 border drop-shadow-lg flex-col duration-300 transition-colors'>
|
||||
{/* 顶部导航按钮 */}
|
||||
<div className='flex justify-between'>
|
||||
<i onClick={handleBack} className="hover:bg-blue-600 hover:text-white px-2 py-2 text-center w-8 rounded cursor-pointer fa-solid fa-arrow-left"></i>
|
||||
<i onClick={handleForward} className="hover:bg-blue-600 hover:text-white px-2 py-2 text-center w-8 rounded cursor-pointer fa-solid fa-arrow-right"></i>
|
||||
<i onClick={handleRefresh} className="hover:bg-blue-600 hover:text-white px-2 py-2 text-center w-8 rounded cursor-pointer fa-solid fa-rotate-right"></i>
|
||||
<i onClick={handleScrollTop} className="hover:bg-blue-600 hover:text-white px-2 py-2 text-center w-8 rounded cursor-pointer fa-solid fa-arrow-up"></i>
|
||||
</div>
|
||||
|
||||
<hr className='my-2 border-dashed' />
|
||||
|
||||
{/* 跳转导航按钮 */}
|
||||
<div className='w-full px-2'>
|
||||
|
||||
<div onClick={handleJumpToRandomPost} title={locale.MENU.WALK_AROUND} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
|
||||
<i className="fa-solid fa-podcast mr-2" />
|
||||
<div className='whitespace-nowrap'>{locale.MENU.WALK_AROUND}</div>
|
||||
</div>
|
||||
|
||||
<Link href='/category' title={locale.MENU.CATEGORY} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
|
||||
<i className="fa-solid fa-square-minus mr-2" />
|
||||
<div className='whitespace-nowrap'>{locale.MENU.CATEGORY}</div>
|
||||
</Link>
|
||||
|
||||
<Link href='/tag' title={locale.MENU.TAGS} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
|
||||
<i className="fa-solid fa-tag mr-2" />
|
||||
<div className='whitespace-nowrap'>{locale.MENU.TAGS}</div>
|
||||
</Link>
|
||||
|
||||
</div>
|
||||
|
||||
<hr className='my-2 border-dashed' />
|
||||
|
||||
{/* 功能按钮 */}
|
||||
<div className='w-full px-2'>
|
||||
|
||||
<div onClick={handleCopyLink} title={locale.MENU.COPY_URL} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
|
||||
<i className="fa-solid fa-arrow-up-right-from-square mr-2" />
|
||||
<div className='whitespace-nowrap'>{locale.MENU.COPY_URL}</div>
|
||||
</div>
|
||||
|
||||
<div onClick={handleChangeDarkMode} title={isDarkMode ? locale.MENU.LIGHT_MODE : locale.MENU.DARK_MODE} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
|
||||
{isDarkMode ? <i className="fa-regular fa-sun mr-2" /> : <i className="fa-regular fa-moon mr-2" />}
|
||||
<div className='whitespace-nowrap'> {isDarkMode ? locale.MENU.LIGHT_MODE : locale.MENU.DARK_MODE}</div>
|
||||
</div>
|
||||
<div onClick={handeChangeTheme} title={locale.MENU.THEME_SWITCH} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
|
||||
<i className="fa-solid fa-palette mr-2" />
|
||||
<div className='whitespace-nowrap'>{locale.MENU.THEME_SWITCH}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,26 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { saveDarkModeToCookies } from '@/lib/theme'
|
||||
import { saveDarkModeToCookies } from '@/themes/theme'
|
||||
import { Moon, Sun } from './HeroIcons'
|
||||
import { useImperativeHandle } from 'react'
|
||||
|
||||
/**
|
||||
* 深色模式按钮
|
||||
*/
|
||||
const DarkModeButton = (props) => {
|
||||
const { cRef, className } = props
|
||||
const { isDarkMode, updateDarkMode } = useGlobal()
|
||||
|
||||
/**
|
||||
* 对外暴露方法
|
||||
*/
|
||||
useImperativeHandle(cRef, () => {
|
||||
return {
|
||||
handleChangeDarkMode: () => {
|
||||
handleChangeDarkMode()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 用户手动设置主题
|
||||
const handleChangeDarkMode = () => {
|
||||
const newStatus = !isDarkMode
|
||||
@@ -13,9 +31,8 @@ const DarkModeButton = (props) => {
|
||||
htmlElement.classList?.add(newStatus ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
return <div className={'dark:text-gray-200 z-10 duration-200 text-xl py-2 ' + props.className}>
|
||||
<i id='darkModeButton' className={`hover:scale-125 cursor-pointer transform duration-200 fas ${isDarkMode ? 'fa-sun' : 'fa-moon'}`}
|
||||
onClick={handleChangeDarkMode} />
|
||||
</div>
|
||||
return <div onClick={handleChangeDarkMode} className={`${className || ''} flex justify-center dark:text-gray-200 text-gray-800`}>
|
||||
<div id='darkModeButton' className=' hover:scale-110 cursor-pointer transform duration-200 w-5 h-5'> {isDarkMode ? <Sun /> : <Moon />}</div>
|
||||
</div>
|
||||
}
|
||||
export default DarkModeButton
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import * as ThemeMap from '@/themes'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Select from './Select'
|
||||
import { ALL_THEME } from '@/themes'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { THEMES } from '@/themes/theme'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns 调试面板
|
||||
*/
|
||||
export function DebugPanel() {
|
||||
const DebugPanel = () => {
|
||||
const [show, setShow] = useState(false)
|
||||
const { theme, changeTheme, switchTheme, locale } = useGlobal()
|
||||
const { theme, switchTheme, locale } = useGlobal()
|
||||
const router = useRouter()
|
||||
const [siteConfig, updateSiteConfig] = useState({})
|
||||
const [themeConfig, updateThemeConfig] = useState({})
|
||||
const [debugTheme, updateDebugTheme] = useState(BLOG.THEME)
|
||||
|
||||
// 主题下拉框
|
||||
const themeOptions = ALL_THEME.map(t => ({ value: t, text: t }))
|
||||
const themeOptions = THEMES?.map(t => ({ value: t, text: t }))
|
||||
|
||||
useEffect(() => {
|
||||
changeTheme(BLOG.THEME)
|
||||
updateSiteConfig(Object.assign({}, BLOG))
|
||||
updateThemeConfig(Object.assign({}, ThemeMap[BLOG.THEME].THEME_CONFIG))
|
||||
// updateThemeConfig(Object.assign({}, ThemeMap[BLOG.THEME].THEME_CONFIG))
|
||||
}, [])
|
||||
|
||||
function toggleShow() {
|
||||
@@ -29,15 +28,13 @@ export function DebugPanel() {
|
||||
}
|
||||
|
||||
function handleChangeDebugTheme() {
|
||||
const newTheme = switchTheme()
|
||||
updateThemeConfig(Object.assign({}, ThemeMap[newTheme].THEME_CONFIG))
|
||||
updateDebugTheme(newTheme)
|
||||
switchTheme()
|
||||
}
|
||||
|
||||
function handleUpdateDebugTheme(e) {
|
||||
changeTheme(e)
|
||||
updateThemeConfig(Object.assign({}, ThemeMap[theme].THEME_CONFIG))
|
||||
updateDebugTheme(e)
|
||||
function handleUpdateDebugTheme(newTheme) {
|
||||
console.log('切换主题', newTheme)
|
||||
const query = { ...router.query, theme: newTheme }
|
||||
router.push({ pathname: router.pathname, query })
|
||||
}
|
||||
|
||||
function filterResult(text) {
|
||||
@@ -58,7 +55,7 @@ export function DebugPanel() {
|
||||
<div>
|
||||
<div
|
||||
style={{ writingMode: 'vertical-lr' }}
|
||||
className={`bg-black text-xs text-white shadow-2xl p-1.5 rounded-l-xl cursor-pointer ${show ? 'right-96' : 'right-0'} fixed bottom-56 duration-200 z-50`}
|
||||
className={`bg-black text-xs text-white shadow-2xl p-1.5 rounded-l-xl cursor-pointer ${show ? 'right-96' : 'right-0'} fixed bottom-72 duration-200 z-50`}
|
||||
onClick={toggleShow}
|
||||
>
|
||||
{show
|
||||
@@ -75,7 +72,7 @@ export function DebugPanel() {
|
||||
<div className='flex'>
|
||||
<Select
|
||||
label={locale.COMMON.THEME_SWITCH}
|
||||
value={debugTheme}
|
||||
value={theme}
|
||||
options={themeOptions}
|
||||
onChange={handleUpdateDebugTheme}
|
||||
/>
|
||||
@@ -90,7 +87,7 @@ export function DebugPanel() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
{/* <div>
|
||||
<div className="font-bold w-18 border-b my-2">
|
||||
主题配置{`config_${debugTheme}.js`}:
|
||||
</div>
|
||||
@@ -106,7 +103,7 @@ export function DebugPanel() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="font-bold w-18 border-b my-2">
|
||||
站点配置[blog.config.js]
|
||||
</div>
|
||||
@@ -128,3 +125,4 @@ export function DebugPanel() {
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default DebugPanel
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import React from 'react'
|
||||
import { useRef, useEffect, useState } from 'react'
|
||||
/**
|
||||
* 可拖拽组件
|
||||
*/
|
||||
|
||||
export const Draggable = (props) => {
|
||||
const { children } = props
|
||||
let currentObj, offsetX, offsetY// 初始化变量,定义备用变量
|
||||
const draggableRef = useRef(null)
|
||||
const rafRef = useRef(null)
|
||||
const [moving, setMoving] = useState(false)
|
||||
let currentObj, offsetX, offsetY
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
const draggableElements = document.getElementsByClassName('draggable')
|
||||
|
||||
// 标准化鼠标事件对象
|
||||
@@ -49,8 +52,11 @@ export const Draggable = (props) => {
|
||||
}
|
||||
if (currentObj) {
|
||||
if (event.type === 'touchstart') {
|
||||
event.preventDefault() // 阻止默认的滚动行为
|
||||
document.documentElement.style.overflow = 'hidden' // 防止页面一起滚动
|
||||
}
|
||||
|
||||
setMoving(true)
|
||||
offsetX = event.mx - currentObj.offsetLeft
|
||||
offsetY = event.my - currentObj.offsetTop
|
||||
|
||||
@@ -63,22 +69,27 @@ export const Draggable = (props) => {
|
||||
|
||||
function move(event) { // 鼠标移动处理函数
|
||||
event = e(event)
|
||||
if (currentObj) {
|
||||
const left = event.mx - offsetX// 定义拖动元素的x轴距离
|
||||
const top = event.my - offsetY// 定义拖动元素的y轴距离
|
||||
currentObj.style.left = left + 'px'// 定义拖动元素的x轴距离
|
||||
currentObj.style.top = top + 'px'// 定义拖动元素的y轴距离
|
||||
checkInWindow()
|
||||
}
|
||||
rafRef.current = requestAnimationFrame(() => updatePosition(event))
|
||||
}
|
||||
|
||||
function stop(event) { // 松开鼠标处理函数
|
||||
const stop = (event) => {
|
||||
event = e(event)
|
||||
// 释放所有操作对象
|
||||
document.documentElement.style.overflow = 'auto' // 解除页面滚动限制
|
||||
document.documentElement.style.overflow = 'auto' // 恢复默认的滚动行为
|
||||
cancelAnimationFrame(rafRef.current)
|
||||
setMoving(false)
|
||||
currentObj = document.ontouchmove = document.ontouchend = document.onmousemove = document.onmouseup = null
|
||||
}
|
||||
|
||||
const updatePosition = (event) => {
|
||||
if (currentObj) {
|
||||
const left = event.mx - offsetX
|
||||
const top = event.my - offsetY
|
||||
currentObj.style.left = left + 'px'
|
||||
currentObj.style.top = top + 'px'
|
||||
checkInWindow()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 鼠标是否在可拖拽区域内
|
||||
* @param {*} event
|
||||
@@ -126,11 +137,12 @@ export const Draggable = (props) => {
|
||||
return () => {
|
||||
return () => {
|
||||
window.removeEventListener('resize', checkInWindow)
|
||||
cancelAnimationFrame(rafRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <div className='draggable cursor-move'>
|
||||
return <div className={`draggable ${moving ? 'cursor-grabbing' : 'cursor-grab'} select-none`} ref={draggableRef}>
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
|
||||
62
components/ExternalPlugins.js
Normal file
62
components/ExternalPlugins.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import BLOG from 'blog.config'
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
// import TwikooCommentCounter from '@/components/TwikooCommentCounter'
|
||||
// import { DebugPanel } from '@/components/DebugPanel'
|
||||
// import { ThemeSwitch } from '@/components/ThemeSwitch'
|
||||
// import { Fireworks } from '@/components/Fireworks'
|
||||
// import { Nest } from '@/components/Nest'
|
||||
// import { FlutteringRibbon } from '@/components/FlutteringRibbon'
|
||||
// import { Ribbon } from '@/components/Ribbon'
|
||||
// import { Sakura } from '@/components/Sakura'
|
||||
// import { StarrySky } from '@/components/StarrySky'
|
||||
// import { Analytics } from '@vercel/analytics/react'
|
||||
|
||||
const TwikooCommentCounter = dynamic(() => import('@/components/TwikooCommentCounter'), { ssr: false })
|
||||
const DebugPanel = dynamic(() => import('@/components/DebugPanel'), { ssr: false })
|
||||
const ThemeSwitch = dynamic(() => import('@/components/ThemeSwitch'), { ssr: false })
|
||||
const Fireworks = dynamic(() => import('@/components/Fireworks'), { ssr: false })
|
||||
const Nest = dynamic(() => import('@/components/Nest'), { ssr: false })
|
||||
const FlutteringRibbon = dynamic(() => import('@/components/FlutteringRibbon'), { ssr: false })
|
||||
const Ribbon = dynamic(() => import('@/components/Ribbon'), { ssr: false })
|
||||
const Sakura = dynamic(() => import('@/components/Sakura'), { ssr: false })
|
||||
const StarrySky = dynamic(() => import('@/components/StarrySky'), { ssr: false })
|
||||
const Analytics = dynamic(() => import('@vercel/analytics/react').then(async (m) => { return m.Analytics }), { ssr: false })
|
||||
const MusicPlayer = dynamic(() => import('@/components/Player'), { ssr: false })
|
||||
const Ackee = dynamic(() => import('@/components/Ackee'), { ssr: false })
|
||||
const Gtag = dynamic(() => import('@/components/Gtag'), { ssr: false })
|
||||
const Busuanzi = dynamic(() => import('@/components/Busuanzi'), { ssr: false })
|
||||
const GoogleAdsense = dynamic(() => import('@/components/GoogleAdsense'), { ssr: false })
|
||||
const Messenger = dynamic(() => import('@/components/FacebookMessenger'), { ssr: false })
|
||||
const VConsole = dynamic(() => import('@/components/VConsole'), { ssr: false })
|
||||
const CustomContextMenu = dynamic(() => import('@/components/CustomContextMenu'), { ssr: false })
|
||||
|
||||
/**
|
||||
* 各种第三方组件
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const ExternalPlugin = (props) => {
|
||||
return <>
|
||||
{JSON.parse(BLOG.THEME_SWITCH) && <ThemeSwitch />}
|
||||
{JSON.parse(BLOG.DEBUG) && <DebugPanel />}
|
||||
{BLOG.ANALYTICS_ACKEE_TRACKER && <Ackee />}
|
||||
{BLOG.ANALYTICS_GOOGLE_ID && <Gtag />}
|
||||
{BLOG.ANALYTICS_VERCEL && <Analytics />}
|
||||
{JSON.parse(BLOG.ANALYTICS_BUSUANZI_ENABLE) && <Busuanzi />}
|
||||
{BLOG.ADSENSE_GOOGLE_ID && <GoogleAdsense />}
|
||||
{BLOG.FACEBOOK_APP_ID && BLOG.FACEBOOK_PAGE_ID && <Messenger />}
|
||||
{JSON.parse(BLOG.FIREWORKS) && <Fireworks />}
|
||||
{JSON.parse(BLOG.SAKURA) && <Sakura />}
|
||||
{JSON.parse(BLOG.STARRY_SKY) && <StarrySky />}
|
||||
{JSON.parse(BLOG.MUSIC_PLAYER) && <MusicPlayer />}
|
||||
{JSON.parse(BLOG.NEST) && <Nest />}
|
||||
{JSON.parse(BLOG.FLUTTERINGRIBBON) && <FlutteringRibbon />}
|
||||
{JSON.parse(BLOG.COMMENT_TWIKOO_COUNT_ENABLE) && <TwikooCommentCounter {...props}/>}
|
||||
{JSON.parse(BLOG.RIBBON) && <Ribbon />}
|
||||
{JSON.parse(BLOG.CUSTOM_RIGHT_CLICK_CONTEXT_MENU) && <CustomContextMenu {...props} />}
|
||||
<VConsole/>
|
||||
</>
|
||||
}
|
||||
|
||||
export default ExternalPlugin
|
||||
@@ -1,15 +1,15 @@
|
||||
'use client'
|
||||
|
||||
import BLOG from '@/blog.config'
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
import { useEffect } from 'react'
|
||||
import { isBrowser, loadExternalResource } from '@/lib/utils'
|
||||
|
||||
/**
|
||||
* 自定义引入外部JS 和 CSS
|
||||
* @returns
|
||||
*/
|
||||
const ExternalScript = () => {
|
||||
useEffect(() => {
|
||||
if (isBrowser()) {
|
||||
// 静态导入本地自定义样式
|
||||
loadExternalResource(BLOG.FONT_AWESOME, 'css')
|
||||
loadExternalResource('/css/custom.css', 'css')
|
||||
loadExternalResource('/js/custom.js', 'js')
|
||||
|
||||
@@ -28,12 +28,7 @@ const ExternalScript = () => {
|
||||
loadExternalResource(url, 'css')
|
||||
}
|
||||
}
|
||||
// 渲染所有字体
|
||||
BLOG.FONT_URL?.forEach(e => {
|
||||
loadExternalResource(e, 'css')
|
||||
})
|
||||
}, [])
|
||||
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
* https://codepen.io/juliangarnier/pen/gmOwJX
|
||||
* custom by hexo-theme-yun @YunYouJun
|
||||
*/
|
||||
import React from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import anime from 'animejs'
|
||||
import BLOG from 'blog.config'
|
||||
|
||||
export const Fireworks = () => {
|
||||
React.useEffect(() => {
|
||||
const Fireworks = () => {
|
||||
useEffect(() => {
|
||||
createFireworks({})
|
||||
}, [])
|
||||
return <canvas id='fireworks' className='fireworks'></canvas>
|
||||
}
|
||||
export default Fireworks
|
||||
|
||||
/**
|
||||
* 创建烟花
|
||||
|
||||
56
components/FlipCard.js
Normal file
56
components/FlipCard.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import React, { useState } from 'react'
|
||||
|
||||
/**
|
||||
* 翻转组件
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
export default function FlipCard(props) {
|
||||
const [isFlipped, setIsFlipped] = useState(false)
|
||||
|
||||
function handleCardFlip() {
|
||||
setIsFlipped(!isFlipped)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flip-card ${isFlipped ? 'flipped' : ''}`} >
|
||||
<div className={`flip-card-front ${props.className || ''}`} onMouseEnter={handleCardFlip}>
|
||||
{props.frontContent}
|
||||
</div>
|
||||
<div className={`flip-card-back ${props.className || ''}`} onMouseLeave={handleCardFlip}>
|
||||
{props.backContent}
|
||||
</div>
|
||||
<style jsx>{`
|
||||
.flip-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
transform-style: preserve-3d;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.flip-card-front,
|
||||
.flip-card-back {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.flip-card-front {
|
||||
z-index: 2;
|
||||
transform: rotateY(0);
|
||||
}
|
||||
|
||||
.flip-card-back {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.flip-card.flipped {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable */
|
||||
import React from 'react'
|
||||
import { useEffect } from 'react'
|
||||
const id = 'canvasFlutteringRibbon'
|
||||
export const FlutteringRibbon = () => {
|
||||
const destroyRibbon = ()=>{
|
||||
@@ -9,15 +9,17 @@ export const FlutteringRibbon = () => {
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
createFlutteringRibbon()
|
||||
return () => destroyRibbon()
|
||||
|
||||
}, [])
|
||||
return <></>
|
||||
|
||||
}
|
||||
|
||||
export default FlutteringRibbon
|
||||
|
||||
|
||||
/**
|
||||
* 创建连接点
|
||||
* @param config
|
||||
|
||||
48
components/FullScreenButton.js
Normal file
48
components/FullScreenButton.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import { isBrowser } from '@/lib/utils'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
/**
|
||||
* 全屏按钮
|
||||
* @returns
|
||||
*/
|
||||
const FullScreenButton = () => {
|
||||
const [isFullScreen, setIsFullScreen] = useState(false)
|
||||
|
||||
const handleFullScreenClick = () => {
|
||||
if (!isBrowser()) {
|
||||
return
|
||||
}
|
||||
const element = document.documentElement
|
||||
if (!isFullScreen) {
|
||||
if (element.requestFullscreen) {
|
||||
element.requestFullscreen()
|
||||
} else if (element.webkitRequestFullscreen) {
|
||||
element.webkitRequestFullscreen()
|
||||
} else if (element.mozRequestFullScreen) {
|
||||
element.mozRequestFullScreen()
|
||||
} else if (element.msRequestFullscreen) {
|
||||
element.msRequestFullscreen()
|
||||
}
|
||||
setIsFullScreen(true)
|
||||
} else {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen()
|
||||
} else if (document.webkitExitFullscreen) {
|
||||
document.webkitExitFullscreen()
|
||||
} else if (document.mozCancelFullScreen) {
|
||||
document.mozCancelFullScreen()
|
||||
} else if (document.msExitFullscreen) {
|
||||
document.msExitFullscreen()
|
||||
}
|
||||
setIsFullScreen(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={handleFullScreenClick} className='dark:text-gray-300'>
|
||||
{isFullScreen ? '退出全屏' : <i className="fa-solid fa-expand"></i>}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default FullScreenButton
|
||||
@@ -1,18 +1,44 @@
|
||||
import 'gitalk/dist/gitalk.css'
|
||||
// import 'gitalk/dist/gitalk.css'
|
||||
import BLOG from '@/blog.config'
|
||||
import GitalkComponent from 'gitalk/dist/gitalk-component'
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
import { useEffect } from 'react'
|
||||
// import GitalkComponent from 'gitalk/dist/gitalk-component'
|
||||
|
||||
const Gitalk = ({ frontMatter }) => {
|
||||
return <GitalkComponent options={{
|
||||
id: frontMatter.id,
|
||||
title: frontMatter.title,
|
||||
clientID: BLOG.COMMENT_GITALK_CLIENT_ID,
|
||||
clientSecret: BLOG.COMMENT_GITALK_CLIENT_SECRET,
|
||||
repo: BLOG.COMMENT_GITALK_REPO,
|
||||
owner: BLOG.COMMENT_GITALK_OWNER,
|
||||
admin: BLOG.COMMENT_GITALK_ADMIN.split(','),
|
||||
distractionFreeMode: JSON.parse(BLOG.COMMENT_GITALK_DISTRACTION_FREE_MODE)
|
||||
}} />
|
||||
// return <GitalkComponent options={{
|
||||
// id: frontMatter.id,
|
||||
// title: frontMatter.title,
|
||||
// clientID: BLOG.COMMENT_GITALK_CLIENT_ID,
|
||||
// clientSecret: BLOG.COMMENT_GITALK_CLIENT_SECRET,
|
||||
// repo: BLOG.COMMENT_GITALK_REPO,
|
||||
// owner: BLOG.COMMENT_GITALK_OWNER,
|
||||
// admin: BLOG.COMMENT_GITALK_ADMIN.split(','),
|
||||
// distractionFreeMode: JSON.parse(BLOG.COMMENT_GITALK_DISTRACTION_FREE_MODE)
|
||||
// }} />
|
||||
const loadGitalk = async() => {
|
||||
const css = await loadExternalResource(BLOG.COMMENT_GITALK_CSS_CDN_URL, 'css')
|
||||
const js = await loadExternalResource(BLOG.COMMENT_GITALK_JS_CDN_URL, 'js')
|
||||
|
||||
console.log('gitalk 加载成功', css, js)
|
||||
const Gitalk = window.Gitalk
|
||||
|
||||
const gitalk = new Gitalk({
|
||||
clientID: BLOG.COMMENT_GITALK_CLIENT_ID,
|
||||
clientSecret: BLOG.COMMENT_GITALK_CLIENT_SECRET,
|
||||
repo: BLOG.COMMENT_GITALK_REPO,
|
||||
owner: BLOG.COMMENT_GITALK_OWNER,
|
||||
admin: BLOG.COMMENT_GITALK_ADMIN.split(','),
|
||||
id: frontMatter.id, // Ensure uniqueness and length less than 50
|
||||
distractionFreeMode: JSON.parse(BLOG.COMMENT_GITALK_DISTRACTION_FREE_MODE) // Facebook-like distraction free mode
|
||||
})
|
||||
|
||||
gitalk.render('gitalk-container')
|
||||
}
|
||||
useEffect(() => {
|
||||
loadGitalk()
|
||||
}, [])
|
||||
|
||||
return <div id="gitalk-container"></div>
|
||||
}
|
||||
|
||||
export default Gitalk
|
||||
|
||||
@@ -1,29 +1,98 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export default function GoogleAdsense () {
|
||||
/**
|
||||
* 初始化谷歌广告
|
||||
* @returns
|
||||
*/
|
||||
export default function GoogleAdsense() {
|
||||
const initGoogleAdsense = () => {
|
||||
const ads = document.getElementsByClassName('adsbygoogle').length
|
||||
const newAdsCount = ads
|
||||
if (newAdsCount > 0) {
|
||||
for (let i = 0; i <= newAdsCount; i++) {
|
||||
try {
|
||||
// eslint-disable-next-line no-undef
|
||||
(adsbygoogle = window.adsbygoogle || []).push({})
|
||||
} catch (e) {
|
||||
// GoogleAdsense 本地开发请加入 data-adbreak-test="on"
|
||||
// {BLOG.ADSENSE_GOOGLE_ID && <script async src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${BLOG.ADSENSE_GOOGLE_ID}`}
|
||||
// crossOrigin="anonymous" />}
|
||||
|
||||
loadExternalResource(`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${BLOG.ADSENSE_GOOGLE_ID}`, 'js').then(url => {
|
||||
setTimeout(() => {
|
||||
const ads = document.getElementsByClassName('adsbygoogle')
|
||||
const adsbygoogle = window.adsbygoogle
|
||||
console.log('google-ads', adsbygoogle)
|
||||
if (ads.length > 0) {
|
||||
for (let i = 0; i <= ads.length; i++) {
|
||||
try {
|
||||
adsbygoogle.push(ads[i])
|
||||
console.log('adsbygoogle', i, ads[i], adsbygoogle)
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
useEffect(() => {
|
||||
initGoogleAdsense()
|
||||
router.events.on('routeChangeComplete', initGoogleAdsense)
|
||||
return () => {
|
||||
router.events.off('routeChangeComplete', initGoogleAdsense)
|
||||
}
|
||||
}, [router.events])
|
||||
// 延迟3秒加载
|
||||
setTimeout(() => {
|
||||
initGoogleAdsense()
|
||||
}, 3000)
|
||||
}, [router])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章内嵌广告单元
|
||||
* 请在GoogleAdsense后台配置创建对应广告,并且获取相应代码
|
||||
* 修改下面广告单元中的 data-ad-slot data-ad-format data-ad-layout-key(如果有)
|
||||
* 添加 可以在本地调试
|
||||
*/
|
||||
const AdSlot = ({ type = 'show' }) => {
|
||||
if (!BLOG.ADSENSE_GOOGLE_ID) {
|
||||
return null
|
||||
}
|
||||
// 文章内嵌广告
|
||||
if (type === 'in-article') {
|
||||
return <ins className="adsbygoogle"
|
||||
style={{ display: 'block', textAlign: 'center' }}
|
||||
data-ad-layout="in-article"
|
||||
data-ad-format="fluid"
|
||||
data-adtest={BLOG.ADSENSE_GOOGLE_TEST ? 'on' : 'off'}
|
||||
data-ad-client={BLOG.ADSENSE_GOOGLE_ID}
|
||||
data-ad-slot={BLOG.ADSENSE_GOOGLE_SLOT_IN_ARTICLE}></ins>
|
||||
}
|
||||
|
||||
// 信息流广告
|
||||
if (type === 'flow') {
|
||||
return <ins className="adsbygoogle"
|
||||
data-ad-format="fluid"
|
||||
data-ad-layout-key="-5j+cz+30-f7+bf"
|
||||
style={{ display: 'block' }}
|
||||
data-adtest={BLOG.ADSENSE_GOOGLE_TEST ? 'on' : 'off'}
|
||||
data-ad-client={BLOG.ADSENSE_GOOGLE_ID}
|
||||
data-ad-slot={BLOG.ADSENSE_GOOGLE_SLOT_FLOW}></ins>
|
||||
}
|
||||
|
||||
// 原生广告
|
||||
if (type === 'native') {
|
||||
return <ins className="adsbygoogle"
|
||||
style={{ display: 'block', textAlign: 'center' }}
|
||||
data-ad-format="autorelaxed"
|
||||
data-adtest={BLOG.ADSENSE_GOOGLE_TEST ? 'on' : 'off'}
|
||||
data-ad-client={BLOG.ADSENSE_GOOGLE_ID}
|
||||
data-ad-slot={BLOG.ADSENSE_GOOGLE_SLOT_NATIVE}></ins>
|
||||
}
|
||||
|
||||
// 展示广告
|
||||
return <ins className="adsbygoogle"
|
||||
style={{ display: 'block' }}
|
||||
data-ad-client={BLOG.ADSENSE_GOOGLE_ID}
|
||||
data-adtest={BLOG.ADSENSE_GOOGLE_TEST ? 'on' : 'off'}
|
||||
data-ad-slot={BLOG.ADSENSE_GOOGLE_SLOT_AUTO}
|
||||
data-ad-format="auto"
|
||||
data-full-width-responsive="true"></ins>
|
||||
}
|
||||
|
||||
export { AdSlot }
|
||||
|
||||
100
components/HeroIcons.js
Normal file
100
components/HeroIcons.js
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* @see https://heroicons.com/
|
||||
* @returns
|
||||
*/
|
||||
|
||||
export const Moon = () => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const Sun = () => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const Home = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const User = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const ArrowPath = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const ChevronLeft = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const ChevronRight = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const ChevronDoubleLeft = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M18.75 19.5l-7.5-7.5 7.5-7.5m-6 15L5.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const ChevronDoubleRight = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M11.25 4.5l7.5 7.5-7.5 7.5m-6-15l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const InformationCircle = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const HashTag = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5.25 8.25h15m-16.5 7.5h15m-1.8-13.5l-3.9 19.5m-2.1-19.5l-3.9 19.5" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const GlobeAlt = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const ArrowRightCircle = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12.75 15l3-3m0 0l-3-3m3 3h-7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const PlusSmall = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v12m6-6H6" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const ArrowSmallRight = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const ArrowSmallUp = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 19.5v-15m0 0l-6.75 6.75M12 4.5l6.75 6.75" />
|
||||
</svg>
|
||||
}
|
||||
95
components/LazyImage.js
Normal file
95
components/LazyImage.js
Normal file
@@ -0,0 +1,95 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import Head from 'next/head'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
|
||||
/**
|
||||
* 图片懒加载
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export default function LazyImage({
|
||||
priority,
|
||||
id,
|
||||
src,
|
||||
alt,
|
||||
placeholderSrc = BLOG.IMG_LAZY_LOAD_PLACEHOLDER,
|
||||
className,
|
||||
width,
|
||||
height,
|
||||
title,
|
||||
onLoad,
|
||||
style
|
||||
}) {
|
||||
const imageRef = useRef(null)
|
||||
const [imageLoaded, setImageLoaded] = useState(false)
|
||||
|
||||
const handleImageLoad = () => {
|
||||
setImageLoaded(true)
|
||||
if (typeof onLoad === 'function') {
|
||||
onLoad() // 触发传递的onLoad回调函数
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const lazyImage = entry.target
|
||||
lazyImage.src = src
|
||||
observer.unobserve(lazyImage)
|
||||
}
|
||||
})
|
||||
},
|
||||
{ rootMargin: '50px 0px' } // Adjust the rootMargin as needed to trigger the loading earlier or later
|
||||
)
|
||||
|
||||
if (imageRef.current) {
|
||||
observer.observe(imageRef.current)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (imageRef.current) {
|
||||
observer.unobserve(imageRef.current)
|
||||
}
|
||||
}
|
||||
}, [src])
|
||||
|
||||
// 动态添加width、height和className属性,仅在它们为有效值时添加
|
||||
const imgProps = {
|
||||
ref: imageRef,
|
||||
src: imageLoaded ? src : placeholderSrc,
|
||||
alt: alt,
|
||||
onLoad: handleImageLoad
|
||||
}
|
||||
|
||||
if (id) {
|
||||
imgProps.id = id
|
||||
}
|
||||
|
||||
if (title) {
|
||||
imgProps.title = title
|
||||
}
|
||||
|
||||
if (width && width !== 'auto') {
|
||||
imgProps.width = width
|
||||
}
|
||||
|
||||
if (height && height !== 'auto') {
|
||||
imgProps.height = height
|
||||
}
|
||||
if (className) {
|
||||
imgProps.className = className
|
||||
}
|
||||
if (style) {
|
||||
imgProps.style = style
|
||||
}
|
||||
return (<>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img {...imgProps} />
|
||||
{/* 预加载 */}
|
||||
{priority && <Head>
|
||||
<link rel='preload' as='image' src={src} />
|
||||
</Head>}
|
||||
</>)
|
||||
}
|
||||
@@ -2,48 +2,42 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
import React from 'react'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export default function Live2D() {
|
||||
const { switchTheme } = useGlobal()
|
||||
const { theme, switchTheme } = useGlobal()
|
||||
const showPet = JSON.parse(BLOG.WIDGET_PET)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (BLOG.WIDGET_PET) {
|
||||
window.addEventListener('scroll', initLive2D)
|
||||
return () => {
|
||||
window.removeEventListener('scroll', initLive2D)
|
||||
}
|
||||
useEffect(() => {
|
||||
if (showPet) {
|
||||
Promise.all([
|
||||
loadExternalResource('https://cdn.jsdelivr.net/gh/stevenjoezhang/live2d-widget@latest/live2d.min.js', 'js')
|
||||
]).then((e) => {
|
||||
if (typeof window?.loadlive2d !== 'undefined') {
|
||||
// https://github.com/xiazeyu/live2d-widget-models
|
||||
try {
|
||||
loadlive2d('live2d', BLOG.WIDGET_PET_LINK)
|
||||
} catch (error) {
|
||||
console.error('读取PET模型', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
}, [theme])
|
||||
|
||||
function handleClick() {
|
||||
if (BLOG.WIDGET_PET_SWITCH_THEME) {
|
||||
if (JSON.parse(BLOG.WIDGET_PET_SWITCH_THEME)) {
|
||||
switchTheme()
|
||||
}
|
||||
}
|
||||
|
||||
if (!BLOG.WIDGET_PET || !JSON.parse(BLOG.WIDGET_PET)) {
|
||||
if (!showPet) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return <canvas id="live2d" className='cursor-pointer' width="280" height="250" onClick={handleClick} alt='切换主题' title='切换主题' />
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载宠物
|
||||
*/
|
||||
function initLive2D() {
|
||||
window.removeEventListener('scroll', initLive2D)
|
||||
setTimeout(() => {
|
||||
// 加载 waifu.css live2d.min.js waifu-tips.js
|
||||
// if (screen.width >= 768) {
|
||||
Promise.all([
|
||||
// loadExternalResource('https://cdn.zhangxinxu.com/sp/demo/live2d/live2d/js/live2d.js', 'js')
|
||||
loadExternalResource('https://cdn.jsdelivr.net/gh/stevenjoezhang/live2d-widget@latest/live2d.min.js', 'js')
|
||||
]).then((e) => {
|
||||
// https://github.com/xiazeyu/live2d-widget-models
|
||||
loadlive2d('live2d', BLOG.WIDGET_PET_LINK)
|
||||
})
|
||||
// }
|
||||
}, 300)
|
||||
return <canvas id="live2d" width="280" height="250" onClick={handleClick}
|
||||
className="cursor-grab"
|
||||
onMouseDown={(e) => e.target.classList.add('cursor-grabbing')}
|
||||
onMouseUp={(e) => e.target.classList.remove('cursor-grabbing')}
|
||||
/>
|
||||
}
|
||||
|
||||
13
components/Loading.js
Normal file
13
components/Loading.js
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
/**
|
||||
* 异步文件加载时的占位符
|
||||
* @returns
|
||||
*/
|
||||
const Loading = (props) => {
|
||||
return <div id="loading-container" className="-z-10 w-screen h-screen flex justify-center items-center fixed left-0 top-0">
|
||||
<div id="loading-wrapper">
|
||||
<div className="loading"> <i className="fas fa-spinner animate-spin text-3xl "/></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
export default Loading
|
||||
31
components/Mark.js
Normal file
31
components/Mark.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
|
||||
/**
|
||||
* 将搜索结果的关键词高亮
|
||||
*/
|
||||
export default async function replaceSearchResult({ doms, search, target }) {
|
||||
if (!doms || !search || !target) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const url = await loadExternalResource('https://cdnjs.cloudflare.com/ajax/libs/mark.js/8.11.1/mark.min.js', 'js')
|
||||
console.log('markjs 加载成功', url, window.Mark)
|
||||
console.log('------', doms)
|
||||
|
||||
const Mark = window.Mark
|
||||
if (doms instanceof HTMLCollection) {
|
||||
for (const container of doms) {
|
||||
const re = new RegExp(search, 'gim')
|
||||
const instance = new Mark(container)
|
||||
instance.markRegExp(re, target)
|
||||
}
|
||||
} else {
|
||||
const re = new RegExp(search, 'gim')
|
||||
const instance = new Mark(doms)
|
||||
instance.markRegExp(re, target)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('markjs 加载失败', error)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const MusicPlayer = dynamic(
|
||||
() => import('@/components/Player'),
|
||||
{ ssr: false }
|
||||
)
|
||||
export default MusicPlayer
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect } from 'react'
|
||||
const id = 'canvasNestCreated'
|
||||
export const Nest = () => {
|
||||
const Nest = () => {
|
||||
const destroyNest = ()=>{
|
||||
const nest = document.getElementById(id)
|
||||
if(nest && nest.parentNode){
|
||||
@@ -16,6 +16,8 @@ export const Nest = () => {
|
||||
return <></>
|
||||
}
|
||||
|
||||
export default Nest
|
||||
|
||||
/**
|
||||
* 创建连接点
|
||||
* @param config
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import LazyImage from './LazyImage'
|
||||
|
||||
/**
|
||||
* notion的图标icon
|
||||
* 可能是emoji 可能是 svg 也可能是 图片
|
||||
@@ -9,9 +11,7 @@ const NotionIcon = ({ icon }) => {
|
||||
}
|
||||
|
||||
if (icon.startsWith('http') || icon.startsWith('data:')) {
|
||||
// return <Image src={icon} width={30} height={30}/>
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
return <img src={icon} className='w-8 inline mr-1'/>
|
||||
return <LazyImage src={icon} className='w-8 h-8 my-auto inline mr-1'/>
|
||||
}
|
||||
|
||||
return <span className='mr-1'>{icon}</span>
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
import { NotionRenderer } from 'react-notion-x'
|
||||
import dynamic from 'next/dynamic'
|
||||
// import mediumZoom from '@fisch0920/medium-zoom'
|
||||
import React, { useEffect } from 'react'
|
||||
import { isBrowser } from '@/lib/utils'
|
||||
import { Code } from 'react-notion-x/build/third-party/code'
|
||||
import mediumZoom from '@fisch0920/medium-zoom'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
// import { Code } from 'react-notion-x/build/third-party/code'
|
||||
import TweetEmbed from 'react-tweet-embed'
|
||||
|
||||
import BLOG from '@/blog.config'
|
||||
import 'katex/dist/katex.min.css'
|
||||
import { mapImgUrl } from '@/lib/notion/mapImage'
|
||||
import { isBrowser } from '@/lib/utils'
|
||||
|
||||
const Code = dynamic(() =>
|
||||
import('react-notion-x/build/third-party/code').then(async (m) => {
|
||||
return m.Code
|
||||
}), { ssr: false }
|
||||
)
|
||||
|
||||
const Equation = dynamic(() =>
|
||||
import('@/components/Equation').then(async (m) => {
|
||||
// 化学方程式
|
||||
await import('@/lib/mhchem')
|
||||
return m.Equation
|
||||
})
|
||||
}), { ssr: false }
|
||||
)
|
||||
|
||||
const Pdf = dynamic(
|
||||
() => import('react-notion-x/build/third-party/pdf').then((m) => m.Pdf),
|
||||
{
|
||||
@@ -26,7 +34,7 @@ const Pdf = dynamic(
|
||||
// https://github.com/txs
|
||||
// import PrismMac from '@/components/PrismMac'
|
||||
const PrismMac = dynamic(() => import('@/components/PrismMac'), {
|
||||
ssr: true
|
||||
ssr: false
|
||||
})
|
||||
|
||||
const Collection = dynamic(() =>
|
||||
@@ -42,49 +50,43 @@ const Tweet = ({ id }) => {
|
||||
}
|
||||
|
||||
const NotionPage = ({ post, className }) => {
|
||||
// const zoom = isBrowser() && mediumZoom({
|
||||
// container: '.notion-viewport',
|
||||
// background: 'rgba(0, 0, 0, 0.2)',
|
||||
// scrollOffset: 200,
|
||||
// margin: getMediumZoomMargin()
|
||||
// })
|
||||
useEffect(() => {
|
||||
autoScrollToTarget()
|
||||
}, [])
|
||||
|
||||
// const zoomRef = React.useRef(zoom ? zoom.clone() : null)
|
||||
const zoom = typeof window !== 'undefined' && mediumZoom({
|
||||
container: '.notion-viewport',
|
||||
background: 'rgba(0, 0, 0, 0.2)',
|
||||
margin: getMediumZoomMargin()
|
||||
})
|
||||
const zoomRef = useRef(zoom ? zoom.clone() : null)
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
if (window.location.hash) {
|
||||
const tocNode = document.getElementById(window.location.hash.substring(1))
|
||||
if (tocNode && tocNode?.className?.indexOf('notion') > -1) {
|
||||
tocNode.scrollIntoView({ block: 'start', behavior: 'smooth' })
|
||||
// 将相册gallery下的图片加入放大功能
|
||||
if (JSON.parse(BLOG.POST_DISABLE_GALLERY_CLICK)) {
|
||||
setTimeout(() => {
|
||||
if (isBrowser()) {
|
||||
const imgList = document?.querySelectorAll('.notion-collection-card-cover img')
|
||||
if (imgList && zoomRef.current) {
|
||||
for (let i = 0; i < imgList.length; i++) {
|
||||
(zoomRef.current).attach(imgList[i])
|
||||
}
|
||||
}
|
||||
|
||||
const cards = document.getElementsByClassName('notion-collection-card')
|
||||
for (const e of cards) {
|
||||
e.removeAttribute('href')
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 180)
|
||||
|
||||
setTimeout(() => {
|
||||
if (isBrowser()) {
|
||||
// 将相册gallery下的图片加入放大功能
|
||||
// const imgList = document.querySelectorAll('.notion-collection-card-cover img')
|
||||
// if (imgList && zoomRef.current) {
|
||||
// for (let i = 0; i < imgList.length; i++) {
|
||||
// (zoomRef.current).attach(imgList[i])
|
||||
// }
|
||||
// }
|
||||
|
||||
// 相册图片禁止跳转页面,改为放大图片功能功能
|
||||
// const cards = document.getElementsByClassName('notion-collection-card')
|
||||
// for (const e of cards) {
|
||||
// e.removeAttribute('href')
|
||||
// }
|
||||
}
|
||||
}, 800)
|
||||
}, 800)
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (!post || !post.blockMap) {
|
||||
return <>{post?.summary || ''}</>
|
||||
}
|
||||
|
||||
return <div id='container' className={`mx-auto ${className}`}>
|
||||
return <div id='notion-article' className={`mx-auto overflow-hidden ${className || ''}`}>
|
||||
<NotionRenderer
|
||||
recordMap={post.blockMap}
|
||||
mapPageUrl={mapPageUrl}
|
||||
@@ -98,11 +100,27 @@ const NotionPage = ({ post, className }) => {
|
||||
Tweet
|
||||
}} />
|
||||
|
||||
<PrismMac />
|
||||
<PrismMac/>
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据url参数自动滚动到指定区域
|
||||
*/
|
||||
const autoScrollToTarget = () => {
|
||||
setTimeout(() => {
|
||||
// 跳转到指定标题
|
||||
const needToJumpToTitle = window.location.hash
|
||||
if (needToJumpToTitle) {
|
||||
const tocNode = document.getElementById(window.location.hash.substring(1))
|
||||
if (tocNode && tocNode?.className?.indexOf('notion') > -1) {
|
||||
tocNode.scrollIntoView({ block: 'start', behavior: 'smooth' })
|
||||
}
|
||||
}
|
||||
}, 180)
|
||||
}
|
||||
|
||||
/**
|
||||
* 将id映射成博文内部链接。
|
||||
* @param {*} id
|
||||
@@ -113,22 +131,25 @@ const mapPageUrl = id => {
|
||||
return '/' + id.replace(/-/g, '')
|
||||
}
|
||||
|
||||
// function getMediumZoomMargin() {
|
||||
// const width = window.innerWidth
|
||||
|
||||
// if (width < 500) {
|
||||
// return 8
|
||||
// } else if (width < 800) {
|
||||
// return 20
|
||||
// } else if (width < 1280) {
|
||||
// return 30
|
||||
// } else if (width < 1600) {
|
||||
// return 40
|
||||
// } else if (width < 1920) {
|
||||
// return 48
|
||||
// } else {
|
||||
// return 72
|
||||
// }
|
||||
// }
|
||||
/**
|
||||
* 缩放
|
||||
* @returns
|
||||
*/
|
||||
function getMediumZoomMargin() {
|
||||
const width = window.innerWidth
|
||||
|
||||
if (width < 500) {
|
||||
return 8
|
||||
} else if (width < 800) {
|
||||
return 20
|
||||
} else if (width < 1280) {
|
||||
return 30
|
||||
} else if (width < 1600) {
|
||||
return 40
|
||||
} else if (width < 1920) {
|
||||
return 48
|
||||
} else {
|
||||
return 72
|
||||
}
|
||||
}
|
||||
export default NotionPage
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
const Player = () => {
|
||||
const [player, setPlayer] = React.useState()
|
||||
const ref = React.useRef(null)
|
||||
const [player, setPlayer] = useState()
|
||||
const ref = useRef(null)
|
||||
|
||||
const lrcType = JSON.parse(BLOG.MUSIC_PLAYER_LRC_TYPE)
|
||||
const playerVisible = JSON.parse(BLOG.MUSIC_PLAYER_VISIBLE)
|
||||
@@ -11,7 +11,7 @@ const Player = () => {
|
||||
|
||||
const meting = JSON.parse(BLOG.MUSIC_PLAYER_METING)
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (!meting && window.APlayer) {
|
||||
setPlayer(new window.APlayer({
|
||||
container: ref.current,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import Prism from 'prismjs'
|
||||
// 所有语言的prismjs 使用autoloader引入
|
||||
// import 'prismjs/plugins/autoloader/prism-autoloader'
|
||||
@@ -11,68 +11,154 @@ import 'prismjs/plugins/line-numbers/prism-line-numbers.css'
|
||||
|
||||
// mermaid图
|
||||
import BLOG from '@/blog.config'
|
||||
import { isBrowser, loadExternalResource } from '@/lib/utils'
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
|
||||
/**
|
||||
* 代码美化相关
|
||||
* @author https://github.com/txs/
|
||||
* @returns
|
||||
*/
|
||||
const PrismMac = () => {
|
||||
if (isBrowser()) {
|
||||
if (BLOG.CODE_MAC_BAR) {
|
||||
const router = useRouter()
|
||||
const { isDarkMode } = useGlobal()
|
||||
|
||||
useEffect(() => {
|
||||
if (JSON.parse(BLOG.CODE_MAC_BAR)) {
|
||||
loadExternalResource('/css/prism-mac-style.css', 'css')
|
||||
}
|
||||
loadExternalResource(BLOG.PRISM_THEME_PATH, 'css')
|
||||
loadExternalResource(BLOG.PRISM_JS_AUTO_LOADER, 'js').then((e) => {
|
||||
Prism.plugins.autoloader.languages_path = BLOG.PRISM_JS_PATH
|
||||
// 加载prism样式
|
||||
loadPrismThemeCSS(isDarkMode)
|
||||
// 折叠代码
|
||||
loadExternalResource(BLOG.PRISM_JS_AUTO_LOADER, 'js').then((url) => {
|
||||
if (window?.Prism?.plugins?.autoloader) {
|
||||
window.Prism.plugins.autoloader.languages_path = BLOG.PRISM_JS_PATH
|
||||
}
|
||||
|
||||
renderPrismMac()
|
||||
renderMermaid()
|
||||
renderCollapseCode()
|
||||
})
|
||||
}, [router, isDarkMode])
|
||||
|
||||
return <></>
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载样式
|
||||
*/
|
||||
const loadPrismThemeCSS = (isDarkMode) => {
|
||||
let PRISM_THEME
|
||||
let PRISM_PREVIOUS
|
||||
if (JSON.parse(BLOG.PRISM_THEME_SWITCH)) {
|
||||
if (isDarkMode) {
|
||||
PRISM_THEME = BLOG.PRISM_THEME_DARK_PATH
|
||||
PRISM_PREVIOUS = BLOG.PRISM_THEME_LIGHT_PATH
|
||||
} else {
|
||||
PRISM_THEME = BLOG.PRISM_THEME_LIGHT_PATH
|
||||
PRISM_PREVIOUS = BLOG.PRISM_THEME_DARK_PATH
|
||||
}
|
||||
const previousTheme = document.querySelector(`link[href="${PRISM_PREVIOUS}"]`)
|
||||
if (previousTheme) {
|
||||
previousTheme.parentNode.removeChild(previousTheme)
|
||||
}
|
||||
loadExternalResource(PRISM_THEME, 'css')
|
||||
} else {
|
||||
loadExternalResource(BLOG.PRISM_THEME_PREFIX_PATH, 'css')
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 将代码块转为可折叠对象
|
||||
*/
|
||||
const renderCollapseCode = () => {
|
||||
if (!JSON.parse(BLOG.CODE_COLLAPSE)) {
|
||||
return
|
||||
}
|
||||
const codeBlocks = document.querySelectorAll('.code-toolbar')
|
||||
for (const codeBlock of codeBlocks) {
|
||||
// 判断当前元素是否被包裹
|
||||
if (codeBlock.closest('.collapse-wrapper')) {
|
||||
continue // 如果被包裹了,跳过当前循环
|
||||
}
|
||||
|
||||
const code = codeBlock.querySelector('code')
|
||||
const language = code.getAttribute('class').match(/language-(\w+)/)[1]
|
||||
|
||||
const collapseWrapper = document.createElement('div')
|
||||
collapseWrapper.className = 'collapse-wrapper w-full py-2'
|
||||
const panelWrapper = document.createElement('div')
|
||||
panelWrapper.className = 'border dark:border-gray-600 rounded-md hover:border-indigo-500 duration-200 transition-colors'
|
||||
|
||||
const header = document.createElement('div')
|
||||
header.className = 'flex justify-between items-center px-4 py-2 cursor-pointer select-none'
|
||||
header.innerHTML = `<h3 class="text-lg font-medium">${language}</h3><svg class="transition-all duration-200 w-5 h-5 transform rotate-0" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M6.293 6.293a1 1 0 0 1 1.414 0L10 8.586l2.293-2.293a1 1 0 0 1 1.414 1.414l-3 3a1 1 0 0 1-1.414 0l-3-3a1 1 0 0 1 0-1.414z" clip-rule="evenodd"/></svg>`
|
||||
|
||||
const panel = document.createElement('div')
|
||||
panel.className = 'invisible h-0 transition-transform duration-200 border-t border-gray-300'
|
||||
|
||||
panelWrapper.appendChild(header)
|
||||
panelWrapper.appendChild(panel)
|
||||
collapseWrapper.appendChild(panelWrapper)
|
||||
|
||||
codeBlock.parentNode.insertBefore(collapseWrapper, codeBlock)
|
||||
panel.appendChild(codeBlock)
|
||||
|
||||
header.addEventListener('click', () => {
|
||||
panel.classList.toggle('invisible')
|
||||
panel.classList.toggle('h-0')
|
||||
panel.classList.toggle('h-auto')
|
||||
header.querySelector('svg').classList.toggle('rotate-180')
|
||||
panelWrapper.classList.toggle('border-gray-300')
|
||||
})
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
renderMermaid()
|
||||
}, [])
|
||||
return <></>
|
||||
}
|
||||
|
||||
/**
|
||||
* 将mermaid语言 渲染成图片
|
||||
*/
|
||||
const renderMermaid = async() => {
|
||||
// 支持 Mermaid
|
||||
const mermaidPres = document.querySelectorAll('pre.notion-code.language-mermaid')
|
||||
if (mermaidPres) {
|
||||
for (const e of mermaidPres) {
|
||||
const chart = e.querySelector('code').textContent
|
||||
if (chart && !e.querySelector('.mermaid')) {
|
||||
const m = document.createElement('div')
|
||||
m.className = 'mermaid'
|
||||
m.innerHTML = chart
|
||||
e.appendChild(m)
|
||||
}
|
||||
}
|
||||
}
|
||||
const observer = new MutationObserver(async mutationsList => {
|
||||
for (const m of mutationsList) {
|
||||
if (m.target.className === 'notion-code language-mermaid') {
|
||||
const chart = m.target.querySelector('code').textContent
|
||||
if (chart && !m.target.querySelector('.mermaid')) {
|
||||
const mermaidChart = document.createElement('div')
|
||||
mermaidChart.className = 'mermaid'
|
||||
mermaidChart.innerHTML = chart
|
||||
m.target.appendChild(mermaidChart)
|
||||
}
|
||||
|
||||
const mermaidsSvg = document.querySelectorAll('.mermaid')
|
||||
if (mermaidsSvg) {
|
||||
let needLoad = false
|
||||
for (const e of mermaidsSvg) {
|
||||
if (e?.firstChild?.nodeName !== 'svg') {
|
||||
needLoad = true
|
||||
const mermaidsSvg = document.querySelectorAll('.mermaid')
|
||||
if (mermaidsSvg) {
|
||||
let needLoad = false
|
||||
for (const e of mermaidsSvg) {
|
||||
if (e?.firstChild?.nodeName !== 'svg') {
|
||||
needLoad = true
|
||||
}
|
||||
}
|
||||
if (needLoad) {
|
||||
loadExternalResource(BLOG.MERMAID_CDN, 'js').then(url => {
|
||||
// console.log('mermaid加载成功', url, mermaid)
|
||||
const mermaid = window.mermaid
|
||||
mermaid.contentLoaded()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (needLoad) {
|
||||
const asyncMermaid = await import('mermaid')
|
||||
asyncMermaid.default.contentLoaded()
|
||||
}
|
||||
})
|
||||
if (document.querySelector('#notion-article')) {
|
||||
observer.observe(document.querySelector('#notion-article'), { attributes: true, subtree: true })
|
||||
}
|
||||
}
|
||||
|
||||
function renderPrismMac() {
|
||||
const container = document?.getElementById('container-inner')
|
||||
const container = document?.getElementById('notion-article')
|
||||
|
||||
// Add line numbers
|
||||
if (BLOG.CODE_LINE_NUMBERS === 'true') {
|
||||
if (JSON.parse(BLOG.CODE_LINE_NUMBERS)) {
|
||||
const codeBlocks = container?.getElementsByTagName('pre')
|
||||
if (codeBlocks) {
|
||||
Array.from(codeBlocks).forEach(item => {
|
||||
@@ -106,7 +192,7 @@ function renderPrismMac() {
|
||||
}
|
||||
|
||||
// 折叠代码行号bug
|
||||
if (BLOG.CODE_LINE_NUMBERS === 'true') {
|
||||
if (JSON.parse(BLOG.CODE_LINE_NUMBERS)) {
|
||||
fixCodeLineStyle()
|
||||
}
|
||||
}
|
||||
@@ -126,11 +212,11 @@ const fixCodeLineStyle = () => {
|
||||
}
|
||||
}
|
||||
})
|
||||
observer.observe(document.querySelector('#container'), { attributes: true, subtree: true })
|
||||
observer.observe(document.querySelector('#notion-article'), { attributes: true, subtree: true })
|
||||
setTimeout(() => {
|
||||
const preCodes = document.querySelectorAll('pre.notion-code')
|
||||
for (const preCode of preCodes) {
|
||||
console.log('code', preCode)
|
||||
// console.log('code', preCode)
|
||||
Prism.plugins.lineNumbers.resize(preCode)
|
||||
}
|
||||
}, 10)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useEffect } from 'react'
|
||||
const id = 'canvasRibbon'
|
||||
|
||||
export const Ribbon = () => {
|
||||
const Ribbon = () => {
|
||||
const destroyRibbon = ()=>{
|
||||
const ribbon = document.getElementById(id)
|
||||
if(ribbon && ribbon.parentNode){
|
||||
@@ -17,6 +17,8 @@ export const Ribbon = () => {
|
||||
return <></>
|
||||
}
|
||||
|
||||
export default Ribbon
|
||||
|
||||
/**
|
||||
* 创建连接点
|
||||
* @param config
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect } from 'react'
|
||||
const id = 'canvas_sakura'
|
||||
export const Sakura = () => {
|
||||
const Sakura = () => {
|
||||
const destroySakura = ()=>{
|
||||
const sakura = document.getElementById(id)
|
||||
if(sakura && sakura.parentNode){
|
||||
@@ -16,6 +16,8 @@ export const Sakura = () => {
|
||||
return <></>
|
||||
}
|
||||
|
||||
export default Sakura
|
||||
|
||||
/**
|
||||
* 创建樱花雨
|
||||
* @param config
|
||||
|
||||
29
components/ShareBar.js
Normal file
29
components/ShareBar.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import ShareButtons from './ShareButtons'
|
||||
|
||||
const ShareBar = ({ post }) => {
|
||||
const router = useRouter()
|
||||
|
||||
if (!JSON.parse(BLOG.POST_SHARE_BAR_ENABLE) || !post || post?.type !== 'Post') {
|
||||
return <></>
|
||||
}
|
||||
|
||||
const shareUrl = BLOG.LINK + router.asPath
|
||||
|
||||
return <div className='m-1 overflow-x-auto'>
|
||||
<div className='flex w-full md:justify-end'>
|
||||
<ShareButtons shareUrl={shareUrl} title={post.title} image={post.pageCover} body={
|
||||
post?.title +
|
||||
' | ' +
|
||||
BLOG.TITLE +
|
||||
' ' +
|
||||
shareUrl +
|
||||
' ' +
|
||||
post?.summary
|
||||
} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
export default ShareBar
|
||||
379
components/ShareButtons.js
Normal file
379
components/ShareButtons.js
Normal file
@@ -0,0 +1,379 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useState } from 'react'
|
||||
|
||||
import {
|
||||
FacebookShareButton,
|
||||
FacebookIcon,
|
||||
FacebookMessengerShareButton,
|
||||
FacebookMessengerIcon,
|
||||
RedditShareButton,
|
||||
RedditIcon,
|
||||
LineShareButton,
|
||||
LineIcon,
|
||||
EmailShareButton,
|
||||
EmailIcon,
|
||||
TwitterShareButton,
|
||||
TwitterIcon,
|
||||
TelegramShareButton,
|
||||
TelegramIcon,
|
||||
WhatsappShareButton,
|
||||
WhatsappIcon,
|
||||
LinkedinShareButton,
|
||||
LinkedinIcon,
|
||||
PinterestShareButton,
|
||||
PinterestIcon,
|
||||
VKIcon,
|
||||
VKShareButton,
|
||||
OKShareButton,
|
||||
OKIcon,
|
||||
TumblrShareButton,
|
||||
TumblrIcon,
|
||||
LivejournalIcon,
|
||||
LivejournalShareButton,
|
||||
MailruShareButton,
|
||||
MailruIcon,
|
||||
ViberIcon,
|
||||
ViberShareButton,
|
||||
WorkplaceShareButton,
|
||||
WorkplaceIcon,
|
||||
WeiboShareButton,
|
||||
WeiboIcon,
|
||||
PocketShareButton,
|
||||
PocketIcon,
|
||||
InstapaperShareButton,
|
||||
InstapaperIcon,
|
||||
HatenaShareButton,
|
||||
HatenaIcon
|
||||
} from 'react-share'
|
||||
|
||||
const QRCode = dynamic(
|
||||
() => {
|
||||
return import('qrcode.react')
|
||||
},
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
/**
|
||||
* @author https://github.com/txs
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const ShareButtons = ({ shareUrl, title, body, image }) => {
|
||||
const services = BLOG.POSTS_SHARE_SERVICES.split(',')
|
||||
const titleWithSiteInfo = title + ' | ' + BLOG.TITLE
|
||||
const { locale } = useGlobal()
|
||||
const [qrCodeShow, setQrCodeShow] = useState(false)
|
||||
|
||||
const copyUrl = () => {
|
||||
copy(shareUrl)
|
||||
alert(locale.COMMON.URL_COPIED)
|
||||
}
|
||||
|
||||
const openPopover = () => {
|
||||
setQrCodeShow(true)
|
||||
}
|
||||
const closePopover = () => {
|
||||
setQrCodeShow(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{services.map(singleService => {
|
||||
if (singleService === 'facebook') {
|
||||
return (
|
||||
<FacebookShareButton
|
||||
key={singleService}
|
||||
url={shareUrl}
|
||||
className="mx-1"
|
||||
>
|
||||
<FacebookIcon size={32} round />
|
||||
</FacebookShareButton>
|
||||
)
|
||||
}
|
||||
if (singleService === 'messenger') {
|
||||
return (
|
||||
<FacebookMessengerShareButton
|
||||
key={singleService}
|
||||
url={shareUrl}
|
||||
appId={BLOG.FACEBOOK_APP_ID}
|
||||
className="mx-1"
|
||||
>
|
||||
<FacebookMessengerIcon size={32} round />
|
||||
</FacebookMessengerShareButton>
|
||||
)
|
||||
}
|
||||
if (singleService === 'line') {
|
||||
return (
|
||||
<LineShareButton
|
||||
key={singleService}
|
||||
url={shareUrl}
|
||||
className="mx-1"
|
||||
>
|
||||
<LineIcon size={32} round />
|
||||
</LineShareButton>
|
||||
)
|
||||
}
|
||||
if (singleService === 'reddit') {
|
||||
return (
|
||||
<RedditShareButton
|
||||
key={singleService}
|
||||
url={shareUrl}
|
||||
title={titleWithSiteInfo}
|
||||
windowWidth={660}
|
||||
windowHeight={460}
|
||||
className="mx-1"
|
||||
>
|
||||
<RedditIcon size={32} round />
|
||||
</RedditShareButton>
|
||||
)
|
||||
}
|
||||
if (singleService === 'email') {
|
||||
return (
|
||||
<EmailShareButton
|
||||
key={singleService}
|
||||
url={shareUrl}
|
||||
subject={titleWithSiteInfo}
|
||||
body={body}
|
||||
className="mx-1"
|
||||
>
|
||||
<EmailIcon size={32} round />
|
||||
</EmailShareButton>
|
||||
)
|
||||
}
|
||||
if (singleService === 'twitter') {
|
||||
return (
|
||||
<TwitterShareButton
|
||||
key={singleService}
|
||||
url={shareUrl}
|
||||
title={titleWithSiteInfo}
|
||||
className="mx-1"
|
||||
>
|
||||
<TwitterIcon size={32} round />
|
||||
</TwitterShareButton>
|
||||
)
|
||||
}
|
||||
if (singleService === 'telegram') {
|
||||
return (
|
||||
<TelegramShareButton
|
||||
key={singleService}
|
||||
url={shareUrl}
|
||||
title={titleWithSiteInfo}
|
||||
className="mx-1"
|
||||
>
|
||||
<TelegramIcon size={32} round />
|
||||
</TelegramShareButton>
|
||||
)
|
||||
}
|
||||
if (singleService === 'whatsapp') {
|
||||
return (
|
||||
<WhatsappShareButton
|
||||
key={singleService}
|
||||
url={shareUrl}
|
||||
title={titleWithSiteInfo}
|
||||
separator=":: "
|
||||
className="mx-1"
|
||||
>
|
||||
<WhatsappIcon size={32} round />
|
||||
</WhatsappShareButton>
|
||||
)
|
||||
}
|
||||
if (singleService === 'linkedin') {
|
||||
return (
|
||||
<LinkedinShareButton
|
||||
key={singleService}
|
||||
url={shareUrl}
|
||||
className="mx-1"
|
||||
>
|
||||
<LinkedinIcon size={32} round />
|
||||
</LinkedinShareButton>
|
||||
)
|
||||
}
|
||||
if (singleService === 'pinterest') {
|
||||
return (
|
||||
<PinterestShareButton
|
||||
key={singleService}
|
||||
url={shareUrl}
|
||||
media={image}
|
||||
className="mx-1"
|
||||
>
|
||||
<PinterestIcon size={32} round />
|
||||
</PinterestShareButton>
|
||||
)
|
||||
}
|
||||
if (singleService === 'vkshare') {
|
||||
return (
|
||||
<VKShareButton
|
||||
key={singleService}
|
||||
url={shareUrl}
|
||||
image={image}
|
||||
className="mx-1"
|
||||
>
|
||||
<VKIcon size={32} round />
|
||||
</VKShareButton>
|
||||
)
|
||||
}
|
||||
if (singleService === 'okshare') {
|
||||
return (
|
||||
<OKShareButton
|
||||
key={singleService}
|
||||
url={shareUrl}
|
||||
image={image}
|
||||
className="mx-1"
|
||||
>
|
||||
<OKIcon size={32} round />
|
||||
</OKShareButton>
|
||||
)
|
||||
}
|
||||
if (singleService === 'tumblr') {
|
||||
return (
|
||||
<TumblrShareButton
|
||||
key={singleService}
|
||||
url={shareUrl}
|
||||
title={titleWithSiteInfo}
|
||||
className="mx-1"
|
||||
>
|
||||
<TumblrIcon size={32} round />
|
||||
</TumblrShareButton>
|
||||
)
|
||||
}
|
||||
if (singleService === 'livejournal') {
|
||||
return (
|
||||
<LivejournalShareButton
|
||||
key={singleService}
|
||||
url={shareUrl}
|
||||
title={titleWithSiteInfo}
|
||||
description={shareUrl}
|
||||
className="mx-1"
|
||||
>
|
||||
<LivejournalIcon size={32} round />
|
||||
</LivejournalShareButton>
|
||||
)
|
||||
}
|
||||
if (singleService === 'mailru') {
|
||||
return (
|
||||
<MailruShareButton
|
||||
key={singleService}
|
||||
url={shareUrl}
|
||||
title={titleWithSiteInfo}
|
||||
className="mx-1"
|
||||
>
|
||||
<MailruIcon size={32} round />
|
||||
</MailruShareButton>
|
||||
)
|
||||
}
|
||||
if (singleService === 'viber') {
|
||||
return (
|
||||
<ViberShareButton
|
||||
key={singleService}
|
||||
url={shareUrl}
|
||||
title={titleWithSiteInfo}
|
||||
className="mx-1"
|
||||
>
|
||||
<ViberIcon size={32} round />
|
||||
</ViberShareButton>
|
||||
)
|
||||
}
|
||||
if (singleService === 'workplace') {
|
||||
return (
|
||||
<WorkplaceShareButton
|
||||
key={singleService}
|
||||
url={shareUrl}
|
||||
quote={titleWithSiteInfo}
|
||||
className="mx-1"
|
||||
>
|
||||
<WorkplaceIcon size={32} round />
|
||||
</WorkplaceShareButton>
|
||||
)
|
||||
}
|
||||
if (singleService === 'weibo') {
|
||||
return (
|
||||
<WeiboShareButton
|
||||
key={singleService}
|
||||
url={shareUrl}
|
||||
title={titleWithSiteInfo}
|
||||
image={image}
|
||||
className="mx-1"
|
||||
>
|
||||
<WeiboIcon size={32} round />
|
||||
</WeiboShareButton>
|
||||
)
|
||||
}
|
||||
if (singleService === 'pocket') {
|
||||
return (
|
||||
<PocketShareButton
|
||||
key={singleService}
|
||||
url={shareUrl}
|
||||
title={titleWithSiteInfo}
|
||||
className="mx-1"
|
||||
>
|
||||
<PocketIcon size={32} round />
|
||||
</PocketShareButton>
|
||||
)
|
||||
}
|
||||
if (singleService === 'instapaper') {
|
||||
return (
|
||||
<InstapaperShareButton
|
||||
key={singleService}
|
||||
url={shareUrl}
|
||||
title={titleWithSiteInfo}
|
||||
className="mx-1"
|
||||
>
|
||||
<InstapaperIcon size={32} round />
|
||||
</InstapaperShareButton>
|
||||
)
|
||||
}
|
||||
if (singleService === 'hatena') {
|
||||
return (
|
||||
<HatenaShareButton
|
||||
key={singleService}
|
||||
url={shareUrl}
|
||||
title={titleWithSiteInfo}
|
||||
windowWidth={660}
|
||||
windowHeight={460}
|
||||
className="mx-1"
|
||||
>
|
||||
<HatenaIcon size={32} round />
|
||||
</HatenaShareButton>
|
||||
)
|
||||
}
|
||||
if (singleService === 'qq') {
|
||||
return <button key={singleService} className='cursor-pointer bg-blue-600 text-white rounded-full mx-1'>
|
||||
<a target='_blank' rel='noreferrer' href={`http://connect.qq.com/widget/shareqq/index.html?url=${shareUrl}&sharesource=qzone&title=${title}&desc=${body}`} >
|
||||
<i className='fab fa-qq w-8' />
|
||||
</a>
|
||||
</button>
|
||||
}
|
||||
if (singleService === 'wechat') {
|
||||
return <button onMouseEnter={openPopover} onMouseLeave={closePopover} aria-label={singleService} key={singleService} className='cursor-pointer bg-green-600 text-white rounded-full mx-1'>
|
||||
<div id='wechat-button'>
|
||||
<i className='fab fa-weixin w-8' />
|
||||
</div>
|
||||
<div className='absolute'>
|
||||
<div id='pop' className={(qrCodeShow ? 'opacity-100 ' : ' invisible opacity-0') + ' z-40 absolute bottom-10 -left-10 bg-white shadow-xl transition-all duration-200 text-center'}>
|
||||
<div className='p-2 mt-1'>
|
||||
<QRCode value={shareUrl} fgColor='#000000' />
|
||||
</div>
|
||||
<span className='text-black font-semibold p-1 rounded-t-lg text-sm mx-auto mb-1'>
|
||||
{locale.COMMON.SCAN_QR_CODE}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
if (singleService === 'link') {
|
||||
return <button aria-label={singleService} key={singleService} className='cursor-pointer bg-yellow-500 text-white rounded-full mx-1'>
|
||||
<div alt={locale.COMMON.URL_COPIED} onClick={copyUrl} >
|
||||
<i className='fas fa-link w-8' />
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
return <></>
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ShareButtons
|
||||
@@ -29,11 +29,11 @@ const SideBarDrawer = ({ children, isOpen, onOpen, onClose, className }) => {
|
||||
const sideBarDrawerBackground = window.document.getElementById('sidebar-drawer-background')
|
||||
|
||||
if (showStatus) {
|
||||
sideBarDrawer.classList.replace('-ml-60', 'ml-0')
|
||||
sideBarDrawerBackground.classList.replace('hidden', 'block')
|
||||
sideBarDrawer?.classList.replace('-ml-60', 'ml-0')
|
||||
sideBarDrawerBackground?.classList.replace('hidden', 'block')
|
||||
} else {
|
||||
sideBarDrawer.classList.replace('ml-0', '-ml-60')
|
||||
sideBarDrawerBackground.classList.replace('block', 'hidden')
|
||||
sideBarDrawer?.classList.replace('ml-0', '-ml-60')
|
||||
sideBarDrawerBackground?.classList.replace('block', 'hidden')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable */
|
||||
import React from 'react'
|
||||
|
||||
export const StarrySky = () => {
|
||||
const StarrySky = () => {
|
||||
React.useEffect(() => {
|
||||
dark()
|
||||
}, [])
|
||||
@@ -12,6 +12,7 @@ export const StarrySky = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export default StarrySky
|
||||
/**
|
||||
* 创建星空雨
|
||||
* @param config
|
||||
|
||||
@@ -1,32 +1,53 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { ALL_THEME } from '@/themes'
|
||||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { Draggable } from './Draggable'
|
||||
import { THEMES } from '@/themes/theme'
|
||||
import { useRouter } from 'next/router'
|
||||
import DarkModeButton from './DarkModeButton'
|
||||
/**
|
||||
*
|
||||
* @returns 主题切换
|
||||
*/
|
||||
export function ThemeSwitch() {
|
||||
const { theme, changeTheme } = useGlobal()
|
||||
const ThemeSwitch = () => {
|
||||
const { theme } = useGlobal()
|
||||
const router = useRouter()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
// 修改当前路径url中的 theme 参数
|
||||
// 例如 http://localhost?theme=hexo 跳转到 http://localhost?theme=newTheme
|
||||
const onSelectChange = (e) => {
|
||||
changeTheme(e.target.value)
|
||||
setIsLoading(true)
|
||||
const newTheme = e.target.value
|
||||
const query = router.query
|
||||
query.theme = newTheme
|
||||
router.push({ pathname: router.pathname, query }).then(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
return (<>
|
||||
<Draggable>
|
||||
<div id="draggableBox" style={{ left: '10px', top: '85vh' }} className="fixed text-white bg-black z-50 rounded-lg shadow-card">
|
||||
<div className="py-2 flex items-center text-sm">
|
||||
<i className='fas fa-arrows cursor-move px-2' />
|
||||
{/* <div className='uppercase font-sans whitespace-nowrap cursor-pointer ' onClick={switchTheme}> {theme}</div> */}
|
||||
<select value={theme} onChange={onSelectChange} name="cars" className='text-white bg-black uppercase cursor-pointer'>
|
||||
{ALL_THEME.map(t => {
|
||||
return <option key={t} value={t}>{t}</option>
|
||||
})}
|
||||
</select>
|
||||
<div id="draggableBox" style={{ left: '10px', top: '80vh' }} className="fixed z-50 dark:text-white bg-gray-50 dark:bg-black rounded-2xl drop-shadow-lg">
|
||||
<div className="p-3 w-full flex items-center text-sm group duration-200 transition-all">
|
||||
<DarkModeButton className='mr-2' />
|
||||
<div className='w-0 group-hover:w-20 transition-all duration-200 overflow-hidden'>
|
||||
<select value={theme} onChange={onSelectChange} name="themes" className='appearance-none outline-none dark:text-white bg-gray-50 dark:bg-black uppercase cursor-pointer'>
|
||||
{THEMES?.map(t => {
|
||||
return <option key={t} value={t}>{t}</option>
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<i className="fa-solid fa-palette pl-2"></i>
|
||||
</div>
|
||||
</div>
|
||||
{/* 切换主题加载时的全屏遮罩 */}
|
||||
<div className={`${isLoading ? 'opacity-50 ' : 'opacity-0'} w-screen h-screen bg-black text-white shadow-text flex justify-center items-center
|
||||
transition-all fixed top-0 left-0 pointer-events-none duration-1000 z-50 shadow-inner`}>
|
||||
<i className='text-3xl mr-5 fas fa-spinner animate-spin' />
|
||||
</div>
|
||||
</Draggable>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ThemeSwitch
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import React from 'react'
|
||||
import twikoo from 'twikoo'
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
import { useEffect } from 'react'
|
||||
// import twikoo from 'twikoo'
|
||||
|
||||
/**
|
||||
* Giscus评论 @see https://giscus.app/zh-CN
|
||||
@@ -10,17 +11,28 @@ import twikoo from 'twikoo'
|
||||
*/
|
||||
|
||||
const Twikoo = ({ isDarkMode }) => {
|
||||
React.useEffect(() => {
|
||||
twikoo({
|
||||
envId: BLOG.COMMENT_TWIKOO_ENV_ID, // 腾讯云环境填 envId;Vercel 环境填地址(https://xxx.vercel.app)
|
||||
el: '#twikoo', // 容器元素
|
||||
lang: BLOG.LANG // 用于手动设定评论区语言,支持的语言列表 https://github.com/imaegoo/twikoo/blob/main/src/client/utils/i18n/index.js
|
||||
// region: 'ap-guangzhou', // 环境地域,默认为 ap-shanghai,腾讯云环境填 ap-shanghai 或 ap-guangzhou;Vercel 环境不填
|
||||
// path: location.pathname, // 用于区分不同文章的自定义 js 路径,如果您的文章路径不是 location.pathname,需传此参数
|
||||
})
|
||||
})
|
||||
const loadTwikoo = async () => {
|
||||
try {
|
||||
const url = await loadExternalResource(BLOG.COMMENT_TWIKOO_CDN_URL, 'js')
|
||||
console.log('twikoo 加载成功', url)
|
||||
const twikoo = window.twikoo
|
||||
twikoo.init({
|
||||
envId: BLOG.COMMENT_TWIKOO_ENV_ID, // 腾讯云环境填 envId;Vercel 环境填地址(https://xxx.vercel.app)
|
||||
el: '#twikoo', // 容器元素
|
||||
lang: BLOG.LANG // 用于手动设定评论区语言,支持的语言列表 https://github.com/imaegoo/twikoo/blob/main/src/client/utils/i18n/index.js
|
||||
// region: 'ap-guangzhou', // 环境地域,默认为 ap-shanghai,腾讯云环境填 ap-shanghai 或 ap-guangzhou;Vercel 环境不填
|
||||
// path: location.pathname, // 用于区分不同文章的自定义 js 路径,如果您的文章路径不是 location.pathname,需传此参数
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('twikoo 加载失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadTwikoo()
|
||||
}, [])
|
||||
return (
|
||||
<div id="twikoo"></div>
|
||||
<div id="twikoo"></div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
22
components/TwikooCommentCount.js
Normal file
22
components/TwikooCommentCount.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import BLOG from '@/blog.config'
|
||||
// import twikoo from 'twikoo'
|
||||
|
||||
/**
|
||||
* 获取博客的评论数,用与在列表中展示
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
|
||||
const TwikooCommentCount = ({ post, className }) => {
|
||||
if (!JSON.parse(BLOG.COMMENT_TWIKOO_COUNT_ENABLE)) {
|
||||
return null
|
||||
}
|
||||
return <a href={`${post.slug}?target=comment`} className={`mx-1 hidden comment-count-wrapper-${post.id} ${className || ''}`}>
|
||||
<i className="far fa-comment mr-1"></i>
|
||||
<span className={`comment-count-text-${post.id}`}>
|
||||
{/* <i className='fa-solid fa-spinner animate-spin' /> */}
|
||||
</span>
|
||||
</a>
|
||||
}
|
||||
|
||||
export default TwikooCommentCount
|
||||
79
components/TwikooCommentCounter.js
Normal file
79
components/TwikooCommentCounter.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
/**
|
||||
* 获取博客的评论数,用与在列表中展示
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
|
||||
const TwikooCommentCounter = (props) => {
|
||||
let commentsData = []
|
||||
const { theme } = useGlobal()
|
||||
|
||||
const fetchTwikooData = async (posts) => {
|
||||
posts.forEach(post => {
|
||||
post.slug = post.slug.startsWith('/') ? post.slug : `/${post.slug}`
|
||||
})
|
||||
try {
|
||||
await loadExternalResource(BLOG.COMMENT_TWIKOO_CDN_URL, 'js')
|
||||
const twikoo = window.twikoo
|
||||
twikoo.getCommentsCount({
|
||||
envId: BLOG.COMMENT_TWIKOO_ENV_ID, // 环境 ID
|
||||
// region: 'ap-guangzhou', // 环境地域,默认为 ap-shanghai,如果您的环境地域不是上海,需传此参数
|
||||
urls: posts?.map(post => post.slug), // 不包含协议、域名、参数的文章路径列表,必传参数
|
||||
includeReply: true // 评论数是否包括回复,默认:false
|
||||
}).then(function (res) {
|
||||
// console.log('查询', res)
|
||||
commentsData = res
|
||||
updateCommentCount()
|
||||
}).catch(function (err) {
|
||||
// 发生错误
|
||||
console.error(err)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('twikoo 加载失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
const updateCommentCount = () => {
|
||||
if (commentsData.length === 0) {
|
||||
return
|
||||
}
|
||||
props.posts.forEach(post => {
|
||||
const matchingRes = commentsData.find(r => r.url === post.slug)
|
||||
if (matchingRes) {
|
||||
// 修改评论数量div
|
||||
const textElements = document.querySelectorAll(`.comment-count-text-${post.id}`)
|
||||
textElements.forEach(element => {
|
||||
element.innerHTML = matchingRes.count
|
||||
})
|
||||
// 取消隐藏
|
||||
const wrapperElements = document.querySelectorAll(`.comment-count-wrapper-${post.id}`)
|
||||
wrapperElements.forEach(element => {
|
||||
element.classList.remove('hidden')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
// console.log('路由触发评论计数')
|
||||
if (props?.posts && props?.posts?.length > 0) {
|
||||
fetchTwikooData(props.posts)
|
||||
}
|
||||
}, [router.events])
|
||||
|
||||
// 监控主题变化时的的评论数
|
||||
useEffect(() => {
|
||||
// console.log('主题触发评论计数', commentsData)
|
||||
updateCommentCount()
|
||||
}, [theme])
|
||||
return null
|
||||
}
|
||||
|
||||
export default TwikooCommentCounter
|
||||
12
components/TwikooRecentComments.js
Normal file
12
components/TwikooRecentComments.js
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
/**
|
||||
* 显示最近评论 TODO
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
|
||||
const TwikooRecentComments = (props) => {
|
||||
return null
|
||||
}
|
||||
|
||||
export default TwikooRecentComments
|
||||
66
components/VConsole.js
Normal file
66
components/VConsole.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
const VConsole = () => {
|
||||
const clickCountRef = useRef(0) // 点击次数
|
||||
const lastClickTimeRef = useRef() // 最近一次点击时间戳
|
||||
const timerRef = useRef() // 定时器引用
|
||||
|
||||
const loadVConsole = async () => {
|
||||
try {
|
||||
const url = await loadExternalResource('https://cdn.bootcss.com/vConsole/3.3.4/vconsole.min.js', 'js')
|
||||
if (!url) {
|
||||
return
|
||||
}
|
||||
const VConsole = window.VConsole
|
||||
const vConsole = new VConsole()
|
||||
return vConsole
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const clickListener = () => {
|
||||
const now = Date.now()
|
||||
// 只监听窗口中心的100x100像素范围内的单击事件
|
||||
const centerX = window.innerWidth / 2
|
||||
const centerY = window.innerHeight / 2
|
||||
const range = 50
|
||||
const inRange = (event.clientX >= centerX - range && event.clientX <= centerX + range) &&
|
||||
(event.clientY >= centerY - range && event.clientY <= centerY + range)
|
||||
|
||||
if (!inRange) {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果在1秒内连续点击了8次
|
||||
if (now - lastClickTimeRef.current < 1000 && clickCountRef.current + 1 === 8) {
|
||||
loadVConsole()
|
||||
clickCountRef.current = 0 // 重置计数器
|
||||
clearTimeout(timerRef.current) // 清除定时器
|
||||
window.removeEventListener('click', clickListener)
|
||||
} else {
|
||||
// 如果不满足条件,则重新设置时间戳和计数器
|
||||
lastClickTimeRef.current = now
|
||||
clickCountRef.current += 1
|
||||
// 如果计数器不为0,则设置定时器
|
||||
if (clickCountRef.current > 0) {
|
||||
clearTimeout(timerRef.current)
|
||||
timerRef.current = setTimeout(() => {
|
||||
clickCountRef.current = 0
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 监听窗口点击事件
|
||||
window.addEventListener('click', clickListener)
|
||||
return () => {
|
||||
window.removeEventListener('click', clickListener)
|
||||
clearTimeout(timerRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default VConsole
|
||||
@@ -1,3 +0,0 @@
|
||||
import { Valine } from 'react-valine'
|
||||
|
||||
export default Valine
|
||||
@@ -1,49 +1,61 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import Valine from 'valine'
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
const ValineComponent = (props) => {
|
||||
const router = useRouter()
|
||||
const initValine = (url) => {
|
||||
const valine = new Valine({
|
||||
el: '#v-comments',
|
||||
appId: BLOG.COMMENT_VALINE_APP_ID,
|
||||
appKey: BLOG.COMMENT_VALINE_APP_KEY,
|
||||
avatar: '',
|
||||
path: url || router.asPath,
|
||||
recordIP: true,
|
||||
placeholder: BLOG.COMMENT_VALINE_PLACEHOLDER,
|
||||
serverURLs: BLOG.COMMENT_VALINE_SERVER_URLS,
|
||||
visitor: true
|
||||
})
|
||||
if (!valine) {
|
||||
console.error('valine错误')
|
||||
const ValineComponent = ({ path }) => {
|
||||
const loadValine = async () => {
|
||||
try {
|
||||
const url = await loadExternalResource(BLOG.COMMENT_VALINE_CDN, 'js')
|
||||
console.log('valine 加载成功', url)
|
||||
const Valine = window.Valine
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const valine = new Valine({
|
||||
el: '#valine', // 容器元素
|
||||
lang: BLOG.LANG, // 用于手动设定评论区语言,支持的语言列表 https://github.com/imaegoo/twikoo/blob/main/src/client/utils/i18n/index.js
|
||||
appId: BLOG.COMMENT_VALINE_APP_ID,
|
||||
appKey: BLOG.COMMENT_VALINE_APP_KEY,
|
||||
avatar: '',
|
||||
path,
|
||||
recordIP: true,
|
||||
placeholder: BLOG.COMMENT_VALINE_PLACEHOLDER,
|
||||
serverURLs: BLOG.COMMENT_VALINE_SERVER_URLS,
|
||||
visitor: true
|
||||
})
|
||||
console.log('初始化valine成功')
|
||||
} catch (error) {
|
||||
console.error('twikoo 加载失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
const updateValine = url => {
|
||||
// 移除旧的评论区,否则会重复渲染。
|
||||
const wrapper = document.getElementById('v-wrapper')
|
||||
const comments = document.getElementById('v-comments')
|
||||
wrapper.removeChild(comments)
|
||||
const newComments = document.createElement('div')
|
||||
newComments.id = 'v-comments'
|
||||
newComments.name = new Date()
|
||||
wrapper.appendChild(newComments)
|
||||
initValine(url)
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
initValine()
|
||||
router.events.on('routeChangeComplete', updateValine)
|
||||
return () => {
|
||||
router.events.off('routeChangeComplete', updateValine)
|
||||
}
|
||||
useEffect(() => {
|
||||
loadValine()
|
||||
}, [])
|
||||
return <div id='v-wrapper'>
|
||||
<div id='v-comments'></div>
|
||||
</div>
|
||||
|
||||
return <div id="valine"></div>
|
||||
|
||||
// const updateValine = url => {
|
||||
// // 移除旧的评论区,否则会重复渲染。
|
||||
// const wrapper = document.getElementById('v-wrapper')
|
||||
// const comments = document.getElementById('v-comments')
|
||||
// wrapper.removeChild(comments)
|
||||
// const newComments = document.createElement('div')
|
||||
// newComments.id = 'v-comments'
|
||||
// newComments.name = new Date()
|
||||
// wrapper.appendChild(newComments)
|
||||
// initValine(url)
|
||||
// }
|
||||
|
||||
// useEffect(() => {
|
||||
// initValine()
|
||||
// router.events.on('routeChangeComplete', updateValine)
|
||||
// return () => {
|
||||
// router.events.off('routeChangeComplete', updateValine)
|
||||
// }
|
||||
// }, [])
|
||||
|
||||
// return <div id='v-wrapper'>
|
||||
// <div id='v-comments'></div>
|
||||
// </div>
|
||||
}
|
||||
|
||||
export default ValineComponent
|
||||
|
||||
67
components/WordCount.js
Normal file
67
components/WordCount.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
/**
|
||||
* 字数统计
|
||||
* @returns
|
||||
*/
|
||||
export default function WordCount() {
|
||||
const { locale } = useGlobal()
|
||||
useEffect(() => {
|
||||
countWords()
|
||||
})
|
||||
|
||||
return <span id='wordCountWrapper' className='flex gap-3 font-light'>
|
||||
<span className='flex whitespace-nowrap items-center'>
|
||||
<i className='pl-1 pr-2 fas fa-file-word' />
|
||||
<span id='wordCount'>0</span>
|
||||
</span>
|
||||
<span className='flex whitespace-nowrap items-center'>
|
||||
<i className='mr-1 fas fa-clock' />
|
||||
<span></span>
|
||||
<span id='readTime'>0</span> {locale.COMMON.MINUTE}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新字数统计和阅读时间
|
||||
*/
|
||||
function countWords() {
|
||||
const articleText = deleteHtmlTag(document.getElementById('notion-article')?.innerHTML)
|
||||
const wordCount = fnGetCpmisWords(articleText)
|
||||
// 阅读速度 300-500每分钟
|
||||
document.getElementById('wordCount').innerHTML = wordCount
|
||||
document.getElementById('readTime').innerHTML = Math.floor(wordCount / 400) + 1
|
||||
const wordCountWrapper = document.getElementById('wordCountWrapper')
|
||||
wordCountWrapper.classList.remove('hidden')
|
||||
}
|
||||
|
||||
// 去除html标签
|
||||
function deleteHtmlTag(str) {
|
||||
if (!str) {
|
||||
return ''
|
||||
}
|
||||
str = str.replace(/<[^>]+>|&[^>]+;/g, '').trim()// 去掉所有的html标签和 之类的特殊符合
|
||||
return str
|
||||
}
|
||||
|
||||
// 用word方式计算正文字数
|
||||
function fnGetCpmisWords(str) {
|
||||
if (!str) {
|
||||
return 0
|
||||
}
|
||||
let sLen = 0
|
||||
try {
|
||||
// eslint-disable-next-line no-irregular-whitespace
|
||||
str = str.replace(/(\r\n+|\s+| +)/g, '龘')
|
||||
// eslint-disable-next-line no-control-regex
|
||||
str = str.replace(/[\x00-\xff]/g, 'm')
|
||||
str = str.replace(/m+/g, '*')
|
||||
str = str.replace(/龘+/g, '')
|
||||
sLen = str.length
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
return sLen
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
"paths": {
|
||||
"@/*": ["./*"],
|
||||
"@/components/*": ["components/*"],
|
||||
"@/theme/*": ["theme/*"],
|
||||
"@/data/*": ["data/*"],
|
||||
"@/lib/*": ["lib/*"],
|
||||
"@/styles/*": ["styles/*"]
|
||||
|
||||
42
lib/algolia.js
Normal file
42
lib/algolia.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { getPageContentText } from '@/pages/search/[keyword]'
|
||||
import algoliasearch from 'algoliasearch'
|
||||
|
||||
/**
|
||||
* 生成全文索引
|
||||
* @param {*} allPages
|
||||
*/
|
||||
const generateAlgoliaSearch = async({ allPages, force = false }) => {
|
||||
allPages?.forEach(p => {
|
||||
// 判断这篇文章是否需要重新创建索引
|
||||
if (p && !p.password) {
|
||||
uploadDataToAlgolia(p)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传数据
|
||||
*/
|
||||
const uploadDataToAlgolia = (post) => {
|
||||
// Connect and authenticate with your Algolia app
|
||||
const client = algoliasearch(BLOG.ALGOLIA_APP_ID, BLOG.ALGOLIA_ADMIN_APP_KEY)
|
||||
|
||||
// Create a new index and add a record
|
||||
const index = client.initIndex(BLOG.ALGOLIA_INDEX)
|
||||
const record = {
|
||||
objectID: post.id,
|
||||
title: post.title,
|
||||
category: post.category,
|
||||
tags: post.tags,
|
||||
pageCover: post.pageCover,
|
||||
slug: post.slug,
|
||||
summary: post.summary,
|
||||
content: getPageContentText(post, post.blockMap)
|
||||
}
|
||||
index.saveObject(record).wait().then(r => {
|
||||
console.log('Algolia索引', r, record)
|
||||
})
|
||||
}
|
||||
|
||||
export { uploadDataToAlgolia, generateAlgoliaSearch }
|
||||
@@ -5,13 +5,15 @@
|
||||
* @returns {string}
|
||||
*/
|
||||
export default function formatDate (date, local) {
|
||||
if (!date) return ''
|
||||
if (!date || !local) return date || ''
|
||||
const d = new Date(date)
|
||||
const options = { year: 'numeric', month: 'short', day: 'numeric' }
|
||||
const res = d.toLocaleDateString(local, options)
|
||||
return local.slice(0, 2).toLowerCase() === 'zh'
|
||||
// 如果格式是中文日期,则转为横杆
|
||||
const format = local.slice(0, 2).toLowerCase() === 'zh'
|
||||
? res.replace('年', '-').replace('月', '-').replace('日', '')
|
||||
: res
|
||||
return format
|
||||
}
|
||||
|
||||
export function formatDateFmt (timestamp, fmt) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { generateLocaleDict, initLocale } from './lang'
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
import Router, { useRouter } from 'next/router'
|
||||
import { useRouter } from 'next/router'
|
||||
import BLOG from '@/blog.config'
|
||||
import { initDarkMode, initTheme, saveThemeToCookies } from '@/lib/theme'
|
||||
import { ALL_THEME } from '@/themes'
|
||||
import { THEMES, initDarkMode } from '@/themes/theme'
|
||||
import NProgress from 'nprogress'
|
||||
import { getQueryVariable, isBrowser } from './utils'
|
||||
|
||||
const GlobalContext = createContext()
|
||||
|
||||
@@ -15,29 +15,35 @@ const GlobalContext = createContext()
|
||||
* @constructor
|
||||
*/
|
||||
export function GlobalContextProvider({ children }) {
|
||||
const [lang, updateLang] = useState(BLOG.LANG)
|
||||
const [locale, updateLocale] = useState(generateLocaleDict(BLOG.LANG))
|
||||
const [theme, setTheme] = useState(BLOG.THEME)
|
||||
const [isDarkMode, updateDarkMode] = useState(BLOG.APPEARANCE === 'dark')
|
||||
const [onLoading, changeLoadingState] = useState(false)
|
||||
const router = useRouter()
|
||||
const [lang, updateLang] = useState(BLOG.LANG) // 默认语言
|
||||
const [locale, updateLocale] = useState(generateLocaleDict(BLOG.LANG)) // 默认语言
|
||||
const [theme, setTheme] = useState(BLOG.THEME) // 默认博客主题
|
||||
const [isDarkMode, updateDarkMode] = useState(BLOG.APPEARANCE === 'dark') // 默认深色模式
|
||||
const [onLoading, setOnLoading] = useState(false) // 抓取文章数据
|
||||
|
||||
useEffect(() => {
|
||||
initLocale(lang, locale, updateLang, updateLocale)
|
||||
initDarkMode(isDarkMode, updateDarkMode)
|
||||
initTheme(theme, changeTheme)
|
||||
initTheme()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const handleStart = (url) => {
|
||||
NProgress.start()
|
||||
changeLoadingState(true)
|
||||
const { theme } = router.query
|
||||
if (theme && !url.includes(`theme=${theme}`)) {
|
||||
const newUrl = `${url}${url.includes('?') ? '&' : '?'}theme=${theme}`
|
||||
router.push(newUrl)
|
||||
}
|
||||
setOnLoading(true)
|
||||
}
|
||||
const handleStop = () => {
|
||||
NProgress.done()
|
||||
changeLoadingState(false)
|
||||
setOnLoading(false)
|
||||
}
|
||||
|
||||
const queryTheme = getQueryVariable('theme') || BLOG.THEME
|
||||
setTheme(queryTheme)
|
||||
router.events.on('routeChangeStart', handleStart)
|
||||
router.events.on('routeChangeError', handleStop)
|
||||
router.events.on('routeChangeComplete', handleStop)
|
||||
@@ -48,29 +54,51 @@ export function GlobalContextProvider({ children }) {
|
||||
}
|
||||
}, [router])
|
||||
|
||||
// 切换主题
|
||||
function switchTheme() {
|
||||
const currentIndex = ALL_THEME.indexOf(theme)
|
||||
const newIndex = currentIndex < ALL_THEME.length - 1 ? currentIndex + 1 : 0
|
||||
const newTheme = ALL_THEME[newIndex]
|
||||
changeTheme(newTheme)
|
||||
const currentIndex = THEMES.indexOf(theme)
|
||||
const newIndex = currentIndex < THEMES.length - 1 ? currentIndex + 1 : 0
|
||||
const newTheme = THEMES[newIndex]
|
||||
const query = router.query
|
||||
query.theme = newTheme
|
||||
router.push({ pathname: router.pathname, query })
|
||||
return newTheme
|
||||
}
|
||||
|
||||
function changeTheme(theme) {
|
||||
Router.query.theme = ''
|
||||
if (ALL_THEME.indexOf(theme) > -1) {
|
||||
setTheme(theme)
|
||||
} else {
|
||||
setTheme(BLOG.THEME)
|
||||
}
|
||||
saveThemeToCookies(theme)
|
||||
}
|
||||
|
||||
return (
|
||||
<GlobalContext.Provider value={{ onLoading, changeLoadingState, locale, updateLocale, isDarkMode, updateDarkMode, theme, setTheme, switchTheme, changeTheme }}>
|
||||
{children}
|
||||
</GlobalContext.Provider>
|
||||
<GlobalContext.Provider value={{
|
||||
onLoading,
|
||||
setOnLoading,
|
||||
locale,
|
||||
updateLocale,
|
||||
isDarkMode,
|
||||
updateDarkMode,
|
||||
theme,
|
||||
setTheme,
|
||||
switchTheme
|
||||
}}>
|
||||
{children}
|
||||
</GlobalContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换主题时的特殊处理
|
||||
* @param {*} setTheme
|
||||
*/
|
||||
const initTheme = () => {
|
||||
if (isBrowser()) {
|
||||
setTimeout(() => {
|
||||
const elements = document.querySelectorAll('[id^="theme-"]')
|
||||
if (elements?.length > 1) {
|
||||
elements[elements.length - 1].scrollIntoView()
|
||||
// 删除前面的元素,只保留最后一个元素
|
||||
for (let i = 0; i < elements.length - 1; i++) {
|
||||
elements[i].parentNode.removeChild(elements[i])
|
||||
}
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
export const useGlobal = () => useContext(GlobalContext)
|
||||
|
||||
53
lib/lang.js
53
lib/lang.js
@@ -4,47 +4,58 @@ import zhHK from './lang/zh-HK'
|
||||
import zhTW from './lang/zh-TW'
|
||||
import frFR from './lang/fr-FR'
|
||||
import trTR from './lang/tr-TR'
|
||||
import jaJP from './lang/ja-JP'
|
||||
import cookie from 'react-cookies'
|
||||
import { getQueryVariable, isBrowser, mergeDeep } from './utils'
|
||||
|
||||
/**
|
||||
* 在这里配置所有支持的语言
|
||||
* 国家-地区
|
||||
*/
|
||||
const lang = {
|
||||
'en-US': enUS,
|
||||
'zh-CN': zhCN,
|
||||
'zh-HK': zhHK,
|
||||
'zh-TW': zhTW,
|
||||
'fr-FR': frFR,
|
||||
'tr-TR': trTR
|
||||
'tr-TR': trTR,
|
||||
'ja-JP': jaJP
|
||||
}
|
||||
|
||||
export default lang
|
||||
|
||||
/**
|
||||
* 获取当前语言字典
|
||||
* 如果匹配到完整的“国家-地区”语言,则显示国家的语言
|
||||
* @returns 不同语言对应字典
|
||||
*/
|
||||
export function generateLocaleDict(langString) {
|
||||
let userLocale = lang['en-US']
|
||||
const supportedLocales = Object.keys(lang)
|
||||
let userLocale
|
||||
|
||||
switch (langString.toLowerCase()) {
|
||||
case 'zh-cn':
|
||||
case 'zh-sg':
|
||||
userLocale = lang['zh-CN']
|
||||
break
|
||||
case 'zh-hk':
|
||||
userLocale = lang['zh-HK']
|
||||
break
|
||||
case 'zh-tw':
|
||||
userLocale = lang['zh-TW']
|
||||
break
|
||||
case 'fr-fr':
|
||||
userLocale = lang['fr-FR']
|
||||
break
|
||||
case 'tr-tr':
|
||||
userLocale = lang['tr-TR']
|
||||
break
|
||||
default:
|
||||
userLocale = lang['en-US']
|
||||
// 将语言字符串拆分为语言和地区代码,例如将 "zh-CN" 拆分为 "zh" 和 "CN"
|
||||
const [language, region] = langString.split(/[-_]/)
|
||||
|
||||
// 优先匹配语言和地区都匹配的情况
|
||||
const specificLocale = `${language}-${region}`
|
||||
if (supportedLocales.includes(specificLocale)) {
|
||||
userLocale = lang[specificLocale]
|
||||
}
|
||||
|
||||
// 然后尝试匹配只有语言匹配的情况
|
||||
if (!userLocale) {
|
||||
const languageOnlyLocales = supportedLocales.filter(locale => locale.startsWith(language))
|
||||
if (languageOnlyLocales.length > 0) {
|
||||
userLocale = lang[languageOnlyLocales[0]]
|
||||
}
|
||||
}
|
||||
|
||||
// 如果还没匹配到,则返回最接近的语言包
|
||||
if (!userLocale) {
|
||||
const fallbackLocale = supportedLocales.find(locale => locale.startsWith('en'))
|
||||
userLocale = lang[fallbackLocale]
|
||||
}
|
||||
|
||||
return mergeDeep({}, lang['en-US'], userLocale)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
export default {
|
||||
LOCALE: 'en-US',
|
||||
MENU: {
|
||||
WALK_AROUND: 'Walk Around',
|
||||
CATEGORY: 'Category',
|
||||
TAGS: 'Tags',
|
||||
COPY_URL: 'Copy URL',
|
||||
DARK_MODE: 'Dark Mode',
|
||||
LIGHT_MODE: 'Light Mode',
|
||||
THEME_SWITCH: 'Theme Switch'
|
||||
},
|
||||
NAV: {
|
||||
INDEX: 'Blog',
|
||||
INDEX: 'Home',
|
||||
RSS: 'RSS',
|
||||
SEARCH: 'Search',
|
||||
ABOUT: 'About',
|
||||
@@ -35,11 +44,16 @@ export default {
|
||||
SUBMIT: 'Submit',
|
||||
POST_TIME: 'Post on',
|
||||
LAST_EDITED_TIME: 'Last edited',
|
||||
COMMENTS: 'Comments',
|
||||
RECENT_COMMENTS: 'Recent Comments',
|
||||
DEBUG_OPEN: 'Debug',
|
||||
DEBUG_CLOSE: 'Close',
|
||||
THEME_SWITCH: 'Theme Switch',
|
||||
ANNOUNCEMENT: 'Announcement'
|
||||
ANNOUNCEMENT: 'Announcement',
|
||||
START_READING: 'Start Reading',
|
||||
MINUTE: 'min',
|
||||
WORD_COUNT: 'W.C.'
|
||||
|
||||
},
|
||||
PAGINATION: {
|
||||
PREV: 'Prev',
|
||||
|
||||
59
lib/lang/ja-JP.js
Normal file
59
lib/lang/ja-JP.js
Normal file
@@ -0,0 +1,59 @@
|
||||
export default {
|
||||
LOCALE: 'ja-JP',
|
||||
NAV: {
|
||||
INDEX: 'ホーム',
|
||||
RSS: '購読',
|
||||
SEARCH: '検索',
|
||||
ABOUT: 'このサイトについて',
|
||||
NAVIGATOR: 'ナビゲーション',
|
||||
MAIL: 'メール',
|
||||
ARCHIVE: 'アーカイブ'
|
||||
},
|
||||
COMMON: {
|
||||
MORE: 'さらに',
|
||||
NO_MORE: 'それ以上ありません',
|
||||
LATEST_POSTS: '最新の記事',
|
||||
TAGS: 'タグ',
|
||||
NO_TAG: 'タグなし',
|
||||
CATEGORY: 'カテゴリ',
|
||||
SHARE: 'シェア',
|
||||
SCAN_QR_CODE: 'WeChatで共有',
|
||||
URL_COPIED: 'リンクがコピーされました!',
|
||||
TABLE_OF_CONTENTS: '目次',
|
||||
RELATE_POSTS: '関連する記事',
|
||||
COPYRIGHT: '免責事項',
|
||||
AUTHOR: '作成者',
|
||||
URL: 'リンク',
|
||||
ANALYTICS: '統計',
|
||||
POSTS: '記事',
|
||||
ARTICLE: '記事',
|
||||
VISITORS: '人の訪問者',
|
||||
VIEWS: '回の閲覧',
|
||||
COPYRIGHT_NOTICE: 'この記事はCC BY-NC-SA 4.0 ライセンスの下でライセンスされています。転載する場合には出典を明らかにしてください。',
|
||||
RESULT_OF_SEARCH: '個の検索結果',
|
||||
ARTICLE_DETAIL: '記事の詳細',
|
||||
PASSWORD_ERROR: 'パスワードが違います!',
|
||||
ARTICLE_LOCK_TIPS: 'この記事はロックされています。アクセスパスワードを入力してください。',
|
||||
SUBMIT: '送信',
|
||||
POST_TIME: '公開日',
|
||||
LAST_EDITED_TIME: '最終更新日',
|
||||
RECENT_COMMENTS: '最近のコメント',
|
||||
DEBUG_OPEN: 'デバッグをオンにする',
|
||||
DEBUG_CLOSE: 'デバッグをオフにする',
|
||||
THEME_SWITCH: 'テーマの切り替え',
|
||||
ANNOUNCEMENT: 'お知らせ',
|
||||
START_READING: '読み始める'
|
||||
},
|
||||
PAGINATION: {
|
||||
PREV: '前のページ',
|
||||
NEXT: '次のページ'
|
||||
},
|
||||
SEARCH: {
|
||||
ARTICLES: '記事を検索',
|
||||
TAGS: 'タグを検索'
|
||||
},
|
||||
POST: {
|
||||
BACK: '前のページに戻る',
|
||||
TOP: '上に戻る'
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,14 @@
|
||||
export default {
|
||||
LOCALE: 'zh-CN',
|
||||
MENU: {
|
||||
WALK_AROUND: '随便逛逛',
|
||||
CATEGORY: '博客分类',
|
||||
TAGS: '博客标签',
|
||||
COPY_URL: '复制地址',
|
||||
DARK_MODE: '深色模式',
|
||||
LIGHT_MODE: '浅色模式',
|
||||
THEME_SWITCH: '主题切换'
|
||||
},
|
||||
NAV: {
|
||||
INDEX: '首页',
|
||||
RSS: '订阅',
|
||||
@@ -12,12 +21,12 @@ export default {
|
||||
COMMON: {
|
||||
MORE: '更多',
|
||||
NO_MORE: '没有更多了',
|
||||
LATEST_POSTS: '最新文章',
|
||||
LATEST_POSTS: '最新发布',
|
||||
TAGS: '标签',
|
||||
NO_TAG: 'NoTag',
|
||||
CATEGORY: '分类',
|
||||
SHARE: '分享',
|
||||
SCAN_QR_CODE: '扫一扫二维码',
|
||||
SCAN_QR_CODE: '微信扫码分享',
|
||||
URL_COPIED: '链接已复制!',
|
||||
TABLE_OF_CONTENTS: '目录',
|
||||
RELATE_POSTS: '相关文章',
|
||||
@@ -37,15 +46,19 @@ export default {
|
||||
SUBMIT: '提交',
|
||||
POST_TIME: '发布于',
|
||||
LAST_EDITED_TIME: '最后更新',
|
||||
COMMENTS: '评论',
|
||||
RECENT_COMMENTS: '最新评论',
|
||||
DEBUG_OPEN: '开启调试',
|
||||
DEBUG_CLOSE: '关闭调试',
|
||||
THEME_SWITCH: '切换主题',
|
||||
ANNOUNCEMENT: '公告'
|
||||
ANNOUNCEMENT: '公告',
|
||||
START_READING: '开始阅读',
|
||||
MINUTE: '分钟',
|
||||
WORD_COUNT: '字数'
|
||||
},
|
||||
PAGINATION: {
|
||||
PREV: '上一页',
|
||||
NEXT: '下一页'
|
||||
PREV: '上页',
|
||||
NEXT: '下页'
|
||||
},
|
||||
SEARCH: {
|
||||
ARTICLES: '搜索文章',
|
||||
|
||||
49
lib/mailchimp.js
Normal file
49
lib/mailchimp.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import BLOG from '@/blog.config'
|
||||
|
||||
/**
|
||||
* 订阅邮件-服务端接口
|
||||
* @param {*} email
|
||||
* @returns
|
||||
*/
|
||||
export default function subscribeToMailchimpApi({ email, first_name = '', last_name = '' }) {
|
||||
const listId = BLOG.MAILCHIMP_LIST_ID // 替换为你的邮件列表 ID
|
||||
const apiKey = BLOG.MAILCHIMP_API_KEY // 替换为你的 API KEY
|
||||
if (!email || !listId || !apiKey) {
|
||||
return {}
|
||||
}
|
||||
const data = {
|
||||
email_address: email,
|
||||
status: 'subscribed',
|
||||
merge_fields: {
|
||||
FNAME: first_name,
|
||||
LNAME: last_name
|
||||
}
|
||||
}
|
||||
return fetch(`https://us18.api.mailchimp.com/3.0/lists/${listId}/members`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `apikey ${apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端接口
|
||||
* @param {*} email
|
||||
* @param {*} firstName
|
||||
* @param {*} lastName
|
||||
* @returns
|
||||
*/
|
||||
export async function subscribeToNewsletter(email, firstName, lastName) {
|
||||
const response = await fetch('/api/subscribe', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email, first_name: firstName, last_name: lastName })
|
||||
})
|
||||
const data = await response.json()
|
||||
return data
|
||||
}
|
||||
0
lib/memorize.js
Normal file
0
lib/memorize.js
Normal file
@@ -19,7 +19,7 @@ export function getAllCategories({ allPages, categoryOptions, sliceCount = 0 })
|
||||
return []
|
||||
}
|
||||
// 计数
|
||||
let categories = allPosts.map(p => p.category)
|
||||
let categories = allPosts?.map(p => p.category)
|
||||
categories = [...categories.flat()]
|
||||
const categoryObj = {}
|
||||
categories.forEach(category => {
|
||||
|
||||
@@ -28,7 +28,7 @@ export async function getAllPosts({ notionPageData, from, pageType }) {
|
||||
if (!value) {
|
||||
continue
|
||||
}
|
||||
const properties = (await getPageProperties(id, block, schema, null, tagOptions, notionPageData.siteInfo)) || null
|
||||
const properties = (await getPageProperties(id, block, schema, null, tagOptions)) || null
|
||||
data.push(properties)
|
||||
}
|
||||
|
||||
@@ -45,9 +45,7 @@ export async function getAllPosts({ notionPageData, from, pageType }) {
|
||||
// Sort by date
|
||||
if (BLOG.POSTS_SORT_BY === 'date') {
|
||||
posts.sort((a, b) => {
|
||||
const dateA = new Date(a?.date?.start_date || a.createdTime)
|
||||
const dateB = new Date(b?.date?.start_date || b.createdTime)
|
||||
return dateB - dateA
|
||||
return b?.publishDate - a?.publishDate
|
||||
})
|
||||
}
|
||||
return posts
|
||||
|
||||
@@ -14,7 +14,7 @@ export function getAllTags({ allPages, sliceCount = 0, tagOptions }) {
|
||||
return []
|
||||
}
|
||||
// 计数
|
||||
let tags = allPosts.map(p => p.tags)
|
||||
let tags = allPosts?.map(p => p.tags)
|
||||
tags = [...tags.flat()]
|
||||
const tagObj = {}
|
||||
tags.forEach(tag => {
|
||||
|
||||
@@ -36,7 +36,7 @@ export async function getNotion(pageId) {
|
||||
function getPageCover(postInfo) {
|
||||
const pageCover = postInfo.format?.page_cover
|
||||
if (pageCover) {
|
||||
if (pageCover.startsWith('/')) return 'https://www.notion.so' + pageCover
|
||||
if (pageCover.startsWith('/')) return BLOG.NOTION_HOST + pageCover
|
||||
if (pageCover.startsWith('http')) return defaultMapImageUrl(pageCover, postInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getAllCategories } from './getAllCategories'
|
||||
import getAllPageIds from './getAllPageIds'
|
||||
import { getAllTags } from './getAllTags'
|
||||
import getPageProperties from './getPageProperties'
|
||||
import { mapImgUrl } from './mapImage'
|
||||
import { mapImgUrl, compressImage } from './mapImage'
|
||||
|
||||
/**
|
||||
* 获取博客数据
|
||||
@@ -20,22 +20,23 @@ import { mapImgUrl } from './mapImage'
|
||||
* @returns
|
||||
*
|
||||
*/
|
||||
export async function getGlobalNotionData({
|
||||
export async function getGlobalData({
|
||||
pageId = BLOG.NOTION_PAGE_ID,
|
||||
from
|
||||
}) {
|
||||
// 获取Notion数据
|
||||
const notionPageData = deepClone(await getNotionPageData({ pageId, from }))
|
||||
delete notionPageData.block
|
||||
delete notionPageData.schema
|
||||
delete notionPageData.rawMetadata
|
||||
delete notionPageData.pageIds
|
||||
delete notionPageData.viewIds
|
||||
delete notionPageData.collection
|
||||
delete notionPageData.collectionQuery
|
||||
delete notionPageData.collectionId
|
||||
delete notionPageData.collectionView
|
||||
return notionPageData
|
||||
// 从notion获取
|
||||
const db = deepClone(await getNotionPageData({ pageId, from }))
|
||||
// 不返回的敏感数据
|
||||
delete db.block
|
||||
delete db.schema
|
||||
delete db.rawMetadata
|
||||
delete db.pageIds
|
||||
delete db.viewIds
|
||||
delete db.collection
|
||||
delete db.collectionQuery
|
||||
delete db.collectionId
|
||||
delete db.collectionView
|
||||
return db
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,11 +45,11 @@ export async function getGlobalNotionData({
|
||||
* @returns
|
||||
*/
|
||||
function getLatestPosts({ allPages, from, latestPostCount }) {
|
||||
const allPosts = allPages.filter(page => page.type === 'Post' && page.status === 'Published')
|
||||
const allPosts = allPages?.filter(page => page.type === 'Post' && page.status === 'Published')
|
||||
|
||||
const latestPosts = Object.create(allPosts).sort((a, b) => {
|
||||
const dateA = new Date(a?.lastEditedTime || a?.createdTime || a?.date?.start_date)
|
||||
const dateB = new Date(b?.lastEditedTime || b?.createdTime || b?.date?.start_date)
|
||||
const dateA = new Date(a?.lastEditedTime || a?.publishDate)
|
||||
const dateB = new Date(b?.lastEditedTime || b?.publishDate)
|
||||
return dateB - dateA
|
||||
})
|
||||
return latestPosts.slice(0, latestPostCount)
|
||||
@@ -65,15 +66,15 @@ export async function getNotionPageData({ pageId, from }) {
|
||||
const cacheKey = 'page_block_' + pageId
|
||||
const data = await getDataFromCache(cacheKey)
|
||||
if (data && data.pageIds?.length > 0) {
|
||||
console.log('[命中缓存]:', `from:${from}`, `root-page-id:${pageId}`)
|
||||
console.log('[缓存]:', `from:${from}`, `root-page-id:${pageId}`)
|
||||
return data
|
||||
}
|
||||
const pageRecordMap = await getPageRecordMapByNotionAPI({ pageId, from })
|
||||
const db = await getDataBaseInfoByNotionAPI({ pageId, from })
|
||||
// 存入缓存
|
||||
if (pageRecordMap) {
|
||||
await setDataToCache(cacheKey, pageRecordMap)
|
||||
if (db) {
|
||||
await setDataToCache(cacheKey, db)
|
||||
}
|
||||
return pageRecordMap
|
||||
return db
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,9 +87,9 @@ function getCustomNav({ allPages }) {
|
||||
if (allPages && allPages.length > 0) {
|
||||
allPages.forEach(p => {
|
||||
if (p?.slug?.indexOf('http') === 0) {
|
||||
customNav.push({ icon: p.icon || null, name: p.title, to: p.slug, show: true })
|
||||
customNav.push({ icon: p.icon || null, name: p.title, to: p.slug, target: '_blank', show: true })
|
||||
} else {
|
||||
customNav.push({ icon: p.icon || null, name: p.title, to: '/' + p.slug, show: true })
|
||||
customNav.push({ icon: p.icon || null, name: p.title, to: '/' + p.slug, target: '_self', show: true })
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -101,11 +102,14 @@ function getCustomNav({ allPages }) {
|
||||
* @returns
|
||||
*/
|
||||
function getCustomMenu({ collectionData }) {
|
||||
const menuPages = collectionData.filter(post => (post.type === BLOG.NOTION_PROPERTY_NAME.type_menu || post.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu) && post.status === 'Published')
|
||||
const menuPages = collectionData.filter(post => (post?.type === BLOG.NOTION_PROPERTY_NAME.type_menu || post?.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu) && post.status === 'Published')
|
||||
const menus = []
|
||||
if (menuPages && menuPages.length > 0) {
|
||||
menuPages.forEach(e => {
|
||||
e.show = true
|
||||
if (e?.slug?.indexOf('http') === 0) {
|
||||
e.target = '_blank'
|
||||
}
|
||||
if (e.type === BLOG.NOTION_PROPERTY_NAME.type_menu) {
|
||||
menus.push(e)
|
||||
} else if (e.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu) {
|
||||
@@ -151,19 +155,54 @@ function getCategoryOptions(schema) {
|
||||
* @param from
|
||||
* @returns {Promise<{title,description,pageCover,icon}>}
|
||||
*/
|
||||
function getBlogInfo({ collection, block }) {
|
||||
function getSiteInfo({ collection, block }) {
|
||||
const title = collection?.name?.[0][0] || BLOG.TITLE
|
||||
const description = collection?.description ? Object.assign(collection).description[0][0] : BLOG.DESCRIPTION
|
||||
const pageCover = collection?.cover ? (mapImgUrl(collection?.cover, block[idToUuid(BLOG.NOTION_PAGE_ID)]?.value)) : BLOG.HOME_BANNER_IMAGE
|
||||
let icon = collection?.icon ? (mapImgUrl(collection?.icon, collection, 'collection')) : BLOG.AVATAR
|
||||
const pageCover = collection?.cover ? mapImgUrl(collection?.cover, block[idToUuid(BLOG.NOTION_PAGE_ID)]?.value) : BLOG.HOME_BANNER_IMAGE
|
||||
let icon = collection?.icon ? mapImgUrl(collection?.icon, collection, 'collection') : BLOG.AVATAR
|
||||
|
||||
// 用户头像压缩一下
|
||||
icon = compressImage(icon)
|
||||
|
||||
// 站点图标不能是emoji情
|
||||
const emojiPattern = /\uD83C[\uDF00-\uDFFF]|\uD83D[\uDC00-\uDE4F]/g
|
||||
if (emojiPattern.test(icon)) {
|
||||
if (!icon || emojiPattern.test(icon)) {
|
||||
icon = BLOG.AVATAR
|
||||
}
|
||||
return { title, description, pageCover, icon }
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取导航pages
|
||||
* 转为gitbook这类文档主题设计,精减的标题和内容
|
||||
* 导航页面的条件,必须是Posts
|
||||
* @param {*} param0
|
||||
*/
|
||||
export function getNavPages({ allPages }) {
|
||||
const allNavPages = allPages.filter(post => {
|
||||
return post && post?.slug && (!post?.slug?.startsWith('http')) && post?.type === 'Post' && post?.status === 'Published'
|
||||
})
|
||||
const result = allNavPages.map(item => ({ id: item.id, title: item.title || '', category: item.category || null, tags: item.tags || null, summary: item.summary || null, slug: item.slug }))
|
||||
|
||||
const groupedArray = result.reduce((groups, item) => {
|
||||
const categoryName = item?.category ? item?.category : '' // 将category转换为字符串
|
||||
const lastGroup = groups[groups.length - 1] // 获取最后一个分组
|
||||
|
||||
if (!lastGroup || lastGroup?.category !== categoryName) { // 如果当前元素的category与上一个元素不同,则创建新分组
|
||||
groups.push({ category: categoryName, items: [] })
|
||||
}
|
||||
|
||||
groups[groups.length - 1].items.push(item) // 将元素加入对应的分组
|
||||
|
||||
return groups
|
||||
}, [])
|
||||
|
||||
return groupedArray
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取公告
|
||||
*/
|
||||
async function getNotice(post) {
|
||||
if (!post) {
|
||||
return null
|
||||
@@ -173,28 +212,54 @@ async function getNotice(post) {
|
||||
return post
|
||||
}
|
||||
|
||||
// 没有数据时返回
|
||||
const EmptyData = (pageId) => {
|
||||
const empty = {
|
||||
notice: null,
|
||||
siteInfo: getSiteInfo({}),
|
||||
allPages: [{ id: 1, title: `无法获取Notion数据,请检查Notion_ID: \n 当前 ${pageId}`, summary: '访问文档获取帮助→ https://tangly1024.com/article/vercel-deploy-notion-next', status: 'Published', type: 'Post', slug: '13a171332816461db29d50e9f575b00d', date: { start_date: '2023-04-24', lastEditedTime: '2023-04-24', tagItems: [] } }],
|
||||
allNavPages: [],
|
||||
collection: [],
|
||||
collectionQuery: {},
|
||||
collectionId: null,
|
||||
collectionView: {},
|
||||
viewIds: [],
|
||||
block: {},
|
||||
schema: {},
|
||||
tagOptions: [],
|
||||
categoryOptions: [],
|
||||
rawMetadata: {},
|
||||
customNav: [],
|
||||
customMenu: [],
|
||||
postCount: 1,
|
||||
pageIds: [],
|
||||
latestPosts: []
|
||||
}
|
||||
return empty
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用NotionAPI获取Page数据
|
||||
* @returns {Promise<JSX.Element|null|*>}
|
||||
*/
|
||||
async function getPageRecordMapByNotionAPI({ pageId, from }) {
|
||||
async function getDataBaseInfoByNotionAPI({ pageId, from }) {
|
||||
const pageRecordMap = await getPostBlocks(pageId, from)
|
||||
if (!pageRecordMap) {
|
||||
return []
|
||||
console.error('can`t get Notion Data ; Which id is: ', pageId)
|
||||
return {}
|
||||
}
|
||||
pageId = idToUuid(pageId)
|
||||
const block = pageRecordMap.block
|
||||
const block = pageRecordMap.block || {}
|
||||
const rawMetadata = block[pageId]?.value
|
||||
// Check Type Page-Database和Inline-Database
|
||||
if (
|
||||
rawMetadata?.type !== 'collection_view_page' &&
|
||||
rawMetadata?.type !== 'collection_view'
|
||||
rawMetadata?.type !== 'collection_view_page' && rawMetadata?.type !== 'collection_view'
|
||||
) {
|
||||
console.warn(`pageId "${pageId}" is not a database`)
|
||||
return null
|
||||
console.error(`pageId "${pageId}" is not a database`)
|
||||
return EmptyData(pageId)
|
||||
}
|
||||
|
||||
const collection = Object.values(pageRecordMap.collection)[0]?.value
|
||||
const collection = Object.values(pageRecordMap.collection)[0]?.value || {}
|
||||
const siteInfo = getSiteInfo({ collection, block })
|
||||
const collectionId = rawMetadata?.collection_id
|
||||
const collectionQuery = pageRecordMap.collection_query
|
||||
const collectionView = pageRecordMap.collection_view
|
||||
@@ -222,7 +287,7 @@ async function getPageRecordMapByNotionAPI({ pageId, from }) {
|
||||
let postCount = 0
|
||||
// 查找所有的Post和Page
|
||||
const allPages = collectionData.filter(post => {
|
||||
if (post.type === BLOG.NOTION_PROPERTY_NAME.type_post && post.status === BLOG.NOTION_PROPERTY_NAME.status_publish) {
|
||||
if (post?.type === 'Post' && post.status === 'Published') {
|
||||
postCount++
|
||||
}
|
||||
return post && post?.slug &&
|
||||
@@ -233,25 +298,25 @@ async function getPageRecordMapByNotionAPI({ pageId, from }) {
|
||||
// Sort by date
|
||||
if (BLOG.POSTS_SORT_BY === 'date') {
|
||||
allPages.sort((a, b) => {
|
||||
const dateA = new Date(a?.date?.start_date || a.createdTime)
|
||||
const dateB = new Date(b?.date?.start_date || b.createdTime)
|
||||
return dateB - dateA
|
||||
return b?.publishDate - a?.publishDate
|
||||
})
|
||||
}
|
||||
|
||||
const notice = await getNotice(collectionData.filter(post => { return post && post?.type && post?.type === 'Notice' && post.status === 'Published' })?.[0])
|
||||
const categoryOptions = getAllCategories({ allPages, categoryOptions: getCategoryOptions(schema) })
|
||||
const tagOptions = getAllTags({ allPages, tagOptions: getTagOptions(schema) })
|
||||
const siteInfo = getBlogInfo({ collection, block })
|
||||
const customNav = getCustomNav({ allPages: collectionData.filter(post => post.type === 'Page' && post.status === 'Published') })
|
||||
// 旧的菜单
|
||||
const customNav = getCustomNav({ allPages: collectionData.filter(post => post?.type === 'Page' && post.status === 'Published') })
|
||||
// 新的菜单
|
||||
const customMenu = await getCustomMenu({ collectionData })
|
||||
const latestPosts = getLatestPosts({ allPages, from, latestPostCount: 5 })
|
||||
const latestPosts = getLatestPosts({ allPages, from, latestPostCount: 6 })
|
||||
const allNavPages = getNavPages({ allPages })
|
||||
|
||||
return {
|
||||
notice,
|
||||
siteInfo,
|
||||
allPages,
|
||||
allNavPages,
|
||||
collection,
|
||||
collectionQuery,
|
||||
collectionId,
|
||||
|
||||
@@ -2,9 +2,9 @@ import { getTextContent, getDateValue } from 'notion-utils'
|
||||
import { NotionAPI } from 'notion-client'
|
||||
import BLOG from '@/blog.config'
|
||||
import formatDate from '../formatDate'
|
||||
import { defaultMapImageUrl } from 'react-notion-x'
|
||||
// import { createHash } from 'crypto'
|
||||
import md5 from 'js-md5'
|
||||
import { mapImgUrl } from './mapImage'
|
||||
|
||||
export default async function getPageProperties(id, block, schema, authToken, tagOptions) {
|
||||
const rawProperties = Object.entries(block?.[id]?.value?.properties || [])
|
||||
@@ -69,13 +69,28 @@ export default async function getPageProperties(id, block, schema, authToken, ta
|
||||
})
|
||||
}
|
||||
|
||||
// type\status是下拉选框 取数组第一个
|
||||
properties.type = properties.type?.[0]
|
||||
properties.status = properties.status?.[0]
|
||||
// type\status\category 是单选下拉框 取数组第一个
|
||||
properties.type = properties.type?.[0] || ''
|
||||
properties.status = properties.status?.[0] || ''
|
||||
properties.category = properties.category?.[0] || ''
|
||||
|
||||
// 映射值:用户个性化type和status字段的下拉框选项,在此映射回代码的英文标识
|
||||
mapProperties(properties)
|
||||
|
||||
properties.publishDate = new Date(properties?.date?.start_date || value.created_time).getTime()
|
||||
properties.publishTime = formatDate(properties.publishDate, BLOG.LANG)
|
||||
properties.lastEditedTime = formatDate(new Date(value?.last_edited_time), BLOG.LANG)
|
||||
properties.fullWidth = value.format?.page_full_width ?? false
|
||||
properties.pageIcon = mapImgUrl(block[id].value?.format?.page_icon, block[id].value) ?? ''
|
||||
properties.pageCover = mapImgUrl(block[id].value?.format?.page_cover, block[id].value) ?? ''
|
||||
properties.pageCoverThumbnail = mapImgUrl(block[id].value?.format?.page_cover, block[id].value, 'block', 'pageCoverThumbnail') ?? ''
|
||||
properties.content = value.content ?? []
|
||||
properties.tagItems = properties?.tags?.map(tag => {
|
||||
return { name: tag, color: tagOptions?.find(t => t.value === tag)?.color || 'gray' }
|
||||
}) || []
|
||||
delete properties.content
|
||||
|
||||
// 处理URL
|
||||
if (properties.type === BLOG.NOTION_PROPERTY_NAME.type_post) {
|
||||
properties.slug = (BLOG.POST_URL_PREFIX) ? generateCustomizeUrl(properties) : (properties.slug ?? properties.id)
|
||||
} else if (properties.type === BLOG.NOTION_PROPERTY_NAME.type_page) {
|
||||
@@ -87,49 +102,18 @@ export default async function getPageProperties(id, block, schema, authToken, ta
|
||||
}
|
||||
|
||||
// 开启伪静态路径
|
||||
if (BLOG.PSEUDO_STATIC) {
|
||||
if (JSON.parse(BLOG.PSEUDO_STATIC)) {
|
||||
if (!properties?.slug?.endsWith('.html') && !properties?.slug?.startsWith('http')) {
|
||||
properties.slug += '.html'
|
||||
}
|
||||
}
|
||||
|
||||
properties.createdTime = formatDate(new Date(value.created_time).toString(), BLOG.LANG)
|
||||
properties.lastEditedTime = formatDate(new Date(value?.last_edited_time).toString(), BLOG.LANG)
|
||||
properties.fullWidth = value.format?.page_full_width ?? false
|
||||
properties.pageIcon = getImageUrl(block[id].value?.format?.page_icon, block[id].value) ?? ''
|
||||
properties.page_cover = getImageUrl(block[id].value?.format?.page_cover, block[id].value) ?? ''
|
||||
properties.content = value.content ?? []
|
||||
properties.password = properties.password
|
||||
? md5(properties.slug + properties.password)
|
||||
: ''
|
||||
properties.tagItems = properties?.tags?.map(tag => {
|
||||
return { name: tag, color: tagOptions?.find(t => t.value === tag)?.color || 'gray' }
|
||||
}) || []
|
||||
delete properties.content
|
||||
properties.password = properties.password ? md5(properties.slug + properties.password) : ''
|
||||
return properties
|
||||
}
|
||||
|
||||
// 从Block获取封面图;优先取PageCover,否则取内容图片
|
||||
function getImageUrl(imgObj, blockVal) {
|
||||
if (!imgObj) {
|
||||
return null
|
||||
}
|
||||
if (imgObj.startsWith('/')) {
|
||||
return 'https://www.notion.so' + imgObj // notion内部图片转相对路径为绝对路径
|
||||
}
|
||||
|
||||
if (imgObj.startsWith('http')) {
|
||||
// 判断如果是notion上传的图片要拼接访问token
|
||||
const u = new URL(imgObj)
|
||||
if (u.pathname.startsWith('/secure.notion-static.com') && u.hostname.endsWith('.amazonaws.com')) {
|
||||
return defaultMapImageUrl(imgObj, blockVal) // notion上传的图片需要转换请求地址
|
||||
}
|
||||
}
|
||||
|
||||
// 其他图片链接 或 emoji
|
||||
return imgObj
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射用户自定义表头
|
||||
*/
|
||||
function mapProperties(properties) {
|
||||
if (properties?.type === BLOG.NOTION_PROPERTY_NAME.type_post) {
|
||||
properties.type = 'Post'
|
||||
@@ -148,29 +132,40 @@ function mapProperties(properties) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取自定义URL
|
||||
* @param {*} postProperties
|
||||
* @returns
|
||||
*/
|
||||
function generateCustomizeUrl(postProperties) {
|
||||
let fullSlug = ''
|
||||
let fullPrefix = ''
|
||||
const allSlugPatterns = BLOG.POST_URL_PREFIX.split('/')
|
||||
allSlugPatterns.forEach((pattern, idx) => {
|
||||
if (pattern === '%year%' && postProperties?.date?.start_date) {
|
||||
const formatPostCreatedDate = new Date(postProperties?.date?.start_date)
|
||||
fullSlug += formatPostCreatedDate.getUTCFullYear()
|
||||
} else if (pattern === '%month%' && postProperties?.date?.start_date) {
|
||||
const formatPostCreatedDate = new Date(postProperties?.date?.start_date)
|
||||
fullSlug += String(formatPostCreatedDate.getUTCMonth() + 1).padStart(2, 0)
|
||||
} else if (pattern === '%day%' && postProperties?.date?.start_date) {
|
||||
const formatPostCreatedDate = new Date(postProperties?.date?.start_date)
|
||||
fullSlug += String(formatPostCreatedDate.getUTCDate()).padStart(2, 0)
|
||||
if (pattern === '%year%' && postProperties?.publishTime) {
|
||||
const formatPostCreatedDate = new Date(postProperties?.publishTime)
|
||||
fullPrefix += formatPostCreatedDate.getUTCFullYear()
|
||||
} else if (pattern === '%month%' && postProperties?.publishTime) {
|
||||
const formatPostCreatedDate = new Date(postProperties?.publishTime)
|
||||
fullPrefix += String(formatPostCreatedDate.getUTCMonth() + 1).padStart(2, 0)
|
||||
} else if (pattern === '%day%' && postProperties?.publishTime) {
|
||||
const formatPostCreatedDate = new Date(postProperties?.publishTime)
|
||||
fullPrefix += String(formatPostCreatedDate.getUTCDate()).padStart(2, 0)
|
||||
} else if (pattern === '%slug%') {
|
||||
fullSlug += (postProperties.slug ?? postProperties.id)
|
||||
fullPrefix += (postProperties.slug ?? postProperties.id)
|
||||
} else if (!pattern.includes('%')) {
|
||||
fullSlug += pattern
|
||||
fullPrefix += pattern
|
||||
} else {
|
||||
return
|
||||
}
|
||||
if (idx !== allSlugPatterns.length - 1) {
|
||||
fullSlug += '/'
|
||||
fullPrefix += '/'
|
||||
}
|
||||
})
|
||||
return `${fullSlug}/${(postProperties.slug ?? postProperties.id)}`
|
||||
if (fullPrefix.startsWith('/')) {
|
||||
fullPrefix = fullPrefix.substring(1) // 去掉头部的"/"
|
||||
}
|
||||
if (fullPrefix.endsWith('/')) {
|
||||
fullPrefix = fullPrefix.substring(0, fullPrefix.length - 1) // 去掉尾部部的"/"
|
||||
}
|
||||
return `${fullPrefix}/${(postProperties.slug ?? postProperties.id)}`
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export async function getPostBlocks(id, from, slice) {
|
||||
const cacheKey = 'page_block_' + id
|
||||
let pageBlock = await getDataFromCache(cacheKey)
|
||||
if (pageBlock) {
|
||||
console.log('[命中缓存]:', `from:${from}`, cacheKey)
|
||||
console.log('[缓存]:', `from:${from}`, cacheKey)
|
||||
return filterPostBlocks(id, pageBlock, slice)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export async function getPageWithRetry(id, from, retryAttempts = 3) {
|
||||
console.log('[请求API]', `from:${from}`, `id:${id}`, retryAttempts < 3 ? `剩余重试次数:${retryAttempts}` : '')
|
||||
try {
|
||||
const authToken = BLOG.NOTION_ACCESS_TOKEN || null
|
||||
const api = new NotionAPI({ authToken, userTimeZone: 'Asia/ShangHai' })
|
||||
const api = new NotionAPI({ authToken, userTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone })
|
||||
const pageData = await api.getPage(id)
|
||||
console.info('[响应成功]:', `from:${from}`)
|
||||
return pageData
|
||||
|
||||
@@ -1,35 +1,76 @@
|
||||
import BLOG from '@/blog.config'
|
||||
|
||||
/**
|
||||
* Notion图片映射处理有emoji的图标
|
||||
* 压缩图片
|
||||
* 1. Notion图床可以通过指定url-query参数来压缩裁剪图片 例如 ?xx=xx&width=400
|
||||
* 2. UnPlash 图片可以通过api q=50 控制压缩质量 width=400 控制图片尺寸
|
||||
* @param {*} image
|
||||
*/
|
||||
const compressImage = (image, width = 400, quality = 50, fmt = 'webp') => {
|
||||
if (!image) {
|
||||
return null
|
||||
}
|
||||
if (image.indexOf(BLOG.NOTION_HOST) === 0 && image.indexOf('amazonaws.com') > 0) {
|
||||
return `${image}&width=${width}`
|
||||
}
|
||||
// 压缩unsplash图片
|
||||
if (image.indexOf('https://images.unsplash.com/') === 0) {
|
||||
// 将URL解析为一个对象
|
||||
const urlObj = new URL(image)
|
||||
// 获取URL参数
|
||||
const params = new URLSearchParams(urlObj.search)
|
||||
// 将q参数的值替换
|
||||
params.set('q', quality)
|
||||
// 尺寸
|
||||
params.set('width', width)
|
||||
// 格式
|
||||
params.set('fmt', fmt)
|
||||
params.set('fm', fmt)
|
||||
// 生成新的URL
|
||||
urlObj.search = params.toString()
|
||||
return urlObj.toString()
|
||||
}
|
||||
|
||||
// 此处还可以添加您的自定义图传的封面图压缩参数。
|
||||
// .e.g
|
||||
if (image.indexOf('https://your_picture_bed') === 0) {
|
||||
return 'do_somethin_here'
|
||||
}
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片映射
|
||||
* 1. 如果是 /xx.xx 相对路径格式,则转化为 完整notion域名图片
|
||||
* 2. 如果是 bookmark类型的block 图片封面无需处理
|
||||
* @param {*} img
|
||||
* @param {*} value
|
||||
* @returns
|
||||
*/
|
||||
const mapImgUrl = (img, block, type = 'block') => {
|
||||
let ret = null
|
||||
const mapImgUrl = (img, block, type = 'block', from) => {
|
||||
if (!img) {
|
||||
return ret
|
||||
return null
|
||||
}
|
||||
let ret = null
|
||||
// 相对目录,则视为notion的自带图片
|
||||
if (img.startsWith('/')) ret = 'https://www.notion.so' + img
|
||||
|
||||
// 书签的地址本身就是永久链接,无需处理
|
||||
if (!ret && block?.type === 'bookmark') {
|
||||
if (img.startsWith('/')) {
|
||||
ret = BLOG.NOTION_HOST + img
|
||||
} else {
|
||||
ret = img
|
||||
}
|
||||
|
||||
// notion永久图床地址
|
||||
if (!ret && img.indexOf('secure.notion-static.com') > 0 && (BLOG.IMG_URL_TYPE === 'Notion' || type !== 'block')) {
|
||||
ret = 'https://www.notion.so/image/' + encodeURIComponent(img) + '?table=' + type + '&id=' + block.id
|
||||
// Notion 图床转换为永久地址
|
||||
if (ret.indexOf('secure.notion-static.com') > 0 && (BLOG.IMG_URL_TYPE === 'Notion' || type !== 'block')) {
|
||||
ret = BLOG.NOTION_HOST + '/image/' + encodeURIComponent(ret) + '?table=' + type + '&id=' + block.id
|
||||
}
|
||||
|
||||
// 剩余的是第三方图片url或emoji
|
||||
if (!ret) {
|
||||
ret = img
|
||||
// 文章封面
|
||||
if (from === 'pageCoverThumbnail') {
|
||||
ret = compressImage(ret)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
export { mapImgUrl }
|
||||
export { mapImgUrl, compressImage }
|
||||
|
||||
@@ -46,7 +46,7 @@ export async function generateRss(posts) {
|
||||
link: `${BLOG.LINK}/${post.slug}`,
|
||||
description: post.summary,
|
||||
content: await createFeedContent(post),
|
||||
date: new Date(post?.date?.start_date || post?.createdTime)
|
||||
date: new Date(post?.publishTime)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,12 @@ export async function generateSitemapXml({ allPages }) {
|
||||
changefreq: 'daily'
|
||||
}]
|
||||
|
||||
// 循环页面生成
|
||||
allPages?.forEach(post => {
|
||||
const slugWithoutLeadingSlash = post?.slug?.startsWith('/') ? post?.slug?.slice(1) : post.slug
|
||||
urls.push({
|
||||
loc: `${BLOG.LINK}/${post.slug}`,
|
||||
lastmod: new Date(post?.date?.start_date || post?.createdTime).toISOString().split('T')[0],
|
||||
loc: `${BLOG.LINK}/${slugWithoutLeadingSlash}`,
|
||||
lastmod: new Date(post?.publishTime).toISOString().split('T')[0],
|
||||
changefreq: 'daily'
|
||||
})
|
||||
})
|
||||
@@ -36,6 +38,12 @@ export async function generateSitemapXml({ allPages }) {
|
||||
console.warn('无法写入文件', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成站点地图
|
||||
* @param {*} urls
|
||||
* @returns
|
||||
*/
|
||||
function createSitemapXml(urls) {
|
||||
let urlsXml = ''
|
||||
urls.forEach(u => {
|
||||
|
||||
30
lib/utils.js
30
lib/utils.js
@@ -1,5 +1,15 @@
|
||||
// 封装异步加载资源的方法
|
||||
import { memo } from 'react'
|
||||
|
||||
/**
|
||||
* 组件持久化
|
||||
*/
|
||||
export const memorize = (Component) => {
|
||||
const MemoizedComponent = (props) => {
|
||||
return <Component {...props} />
|
||||
}
|
||||
return memo(MemoizedComponent)
|
||||
}
|
||||
/**
|
||||
* 加载外部资源
|
||||
* @param url 地址 例如 https://xx.com/xx.js
|
||||
@@ -7,6 +17,11 @@
|
||||
* @returns {Promise<unknown>}
|
||||
*/
|
||||
export function loadExternalResource(url, type) {
|
||||
// 检查是否已存在
|
||||
const elements = document.querySelectorAll(`[href='${url}']`)
|
||||
if (elements.length > 0 || !url) {
|
||||
return
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
let tag
|
||||
|
||||
@@ -36,16 +51,27 @@ export function loadExternalResource(url, type) {
|
||||
* @param {}} variable
|
||||
* @returns
|
||||
*/
|
||||
export function getQueryVariable(variable) {
|
||||
export function getQueryVariable(key) {
|
||||
const query = isBrowser() ? window.location.search.substring(1) : ''
|
||||
const vars = query.split('&')
|
||||
for (let i = 0; i < vars.length; i++) {
|
||||
const pair = vars[i].split('=')
|
||||
if (pair[0] === variable) { return pair[1] }
|
||||
if (pair[0] === key) { return pair[1] }
|
||||
}
|
||||
return (false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 URL 中指定参数的值
|
||||
* @param {string} url
|
||||
* @param {string} param
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function getQueryParam(url, param) {
|
||||
const searchParams = new URLSearchParams(url.split('?')[1])
|
||||
return searchParams.get(param)
|
||||
}
|
||||
|
||||
/**
|
||||
* 深度合并两个对象
|
||||
* @param target
|
||||
|
||||
@@ -2,6 +2,32 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
enabled: process.env.ANALYZE === 'true'
|
||||
})
|
||||
|
||||
const { THEME } = require('./blog.config')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
/**
|
||||
* 扫描指定目录下的文件夹名,用于获取当前有几个主题
|
||||
* @param {*} directory
|
||||
* @returns
|
||||
*/
|
||||
function scanSubdirectories(directory) {
|
||||
const subdirectories = []
|
||||
|
||||
fs.readdirSync(directory).forEach(file => {
|
||||
const fullPath = path.join(directory, file)
|
||||
const stats = fs.statSync(fullPath)
|
||||
|
||||
// landing主题比较特殊,不在可切换的主题中显示
|
||||
if (stats.isDirectory() && file !== 'landing') {
|
||||
subdirectories.push(file)
|
||||
}
|
||||
})
|
||||
|
||||
return subdirectories
|
||||
}
|
||||
// 扫描项目 /themes下的目录名
|
||||
const themes = scanSubdirectories(path.resolve(__dirname, 'themes'))
|
||||
module.exports = withBundleAnalyzer({
|
||||
images: {
|
||||
// 图片压缩
|
||||
@@ -64,6 +90,22 @@ module.exports = withBundleAnalyzer({
|
||||
// 'react-dom': 'preact/compat'
|
||||
// })
|
||||
// }
|
||||
|
||||
// 动态主题:添加 resolve.alias 配置,将动态路径映射到实际路径
|
||||
config.resolve.alias['@theme-components'] = path.resolve(__dirname, 'themes', THEME)
|
||||
return config
|
||||
},
|
||||
experimental: {
|
||||
scrollRestoration: true
|
||||
},
|
||||
exportPathMap: async function (defaultPathMap, { dev, dir, outDir, distDir, buildId }) {
|
||||
// 导出时 忽略/pages/sitemap.xml.js , 否则报错getServerSideProps
|
||||
const pages = { ...defaultPathMap }
|
||||
delete pages['/sitemap.xml']
|
||||
return pages
|
||||
},
|
||||
publicRuntimeConfig: { // 这里的配置既可以服务端获取到,也可以在浏览器端获取到
|
||||
NODE_ENV_API: process.env.NODE_ENV_API || 'prod',
|
||||
THEMES: themes
|
||||
}
|
||||
})
|
||||
|
||||
21
package.json
21
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "notion-next",
|
||||
"version": "3.13.4",
|
||||
"version": "4.0.6",
|
||||
"homepage": "https://github.com/tangly1024/NotionNext.git",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
@@ -17,29 +17,27 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"post-build": "next-sitemap --config next-sitemap.config.js",
|
||||
"export": "next build && next-sitemap --config next-sitemap.config.js && next export",
|
||||
"bundle-report": "ANALYZE=true yarn build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@giscus/react": "^2.2.6",
|
||||
"@headlessui/react": "^1.7.15",
|
||||
"@next/bundle-analyzer": "^12.1.1",
|
||||
"@popperjs/core": "^2.9.3",
|
||||
"@vercel/analytics": "^1.0.0",
|
||||
"animate.css": "^4.1.1",
|
||||
"algoliasearch": "^4.18.0",
|
||||
"animejs": "^3.2.1",
|
||||
"aos": "^3.0.0-beta.6",
|
||||
"axios": ">=0.21.1",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"feed": "^4.2.2",
|
||||
"gitalk": "^1.7.2",
|
||||
"js-md5": "^0.7.3",
|
||||
"localStorage": "^1.0.4",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"mark.js": "^8.11.1",
|
||||
"memory-cache": "^0.2.0",
|
||||
"mermaid": "9.2.2",
|
||||
"mongodb": "^4.6.0",
|
||||
"next": "^13.1.1",
|
||||
"next": "13.3.1",
|
||||
"notion-client": "6.15.6",
|
||||
"notion-utils": "6.15.6",
|
||||
"nprogress": "^0.2.0",
|
||||
@@ -53,13 +51,10 @@
|
||||
"react-facebook": "^8.1.4",
|
||||
"react-messenger-customer-chat": "^0.8.0",
|
||||
"react-notion-x": "6.16.0",
|
||||
"react-share": "^4.4.0",
|
||||
"react-share": "^4.4.1",
|
||||
"react-tweet-embed": "~2.0.0",
|
||||
"smoothscroll-polyfill": "^0.4.4",
|
||||
"twikoo": "^1.6.16",
|
||||
"typed.js": "^2.0.12",
|
||||
"use-ackee": "^3.0.0",
|
||||
"valine": "^1.4.18"
|
||||
"use-ackee": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@waline/client": "^2.5.1",
|
||||
@@ -73,7 +68,7 @@
|
||||
"eslint-plugin-react": "^7.23.2",
|
||||
"next-sitemap": "^1.6.203",
|
||||
"postcss": "^8.4.20",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
},
|
||||
"resolutions": {
|
||||
|
||||
18
pages/404.js
18
pages/404.js
@@ -1,6 +1,7 @@
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import * as ThemeMap from '@/themes'
|
||||
import { getGlobalData } from '@/lib/notion/getNotionData'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useRouter } from 'next/router'
|
||||
import { getLayoutByTheme } from '@/themes/theme'
|
||||
|
||||
/**
|
||||
* 404
|
||||
@@ -8,14 +9,19 @@ import { useGlobal } from '@/lib/global'
|
||||
* @returns
|
||||
*/
|
||||
const NoFound = props => {
|
||||
const { theme, siteInfo } = useGlobal()
|
||||
const ThemeComponents = ThemeMap[theme]
|
||||
const { siteInfo } = useGlobal()
|
||||
const meta = { title: `${props?.siteInfo?.title} | 页面找不到啦`, image: siteInfo?.pageCover }
|
||||
return <ThemeComponents.Layout404 {...props} meta={meta}/>
|
||||
|
||||
props = { ...props, meta }
|
||||
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
|
||||
return <Layout {...props} />
|
||||
}
|
||||
|
||||
export async function getStaticProps () {
|
||||
const props = (await getGlobalNotionData({ from: '404' })) || {}
|
||||
const props = (await getGlobalData({ from: '404' })) || {}
|
||||
return { props }
|
||||
}
|
||||
|
||||
|
||||
87
pages/[prefix]/[slug].js
Normal file
87
pages/[prefix]/[slug].js
Normal file
@@ -0,0 +1,87 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { getPostBlocks } from '@/lib/notion'
|
||||
import { getGlobalData } from '@/lib/notion/getNotionData'
|
||||
import { idToUuid } from 'notion-utils'
|
||||
import { getNotion } from '@/lib/notion/getNotion'
|
||||
import Slug, { getRecommendPost } from '.'
|
||||
|
||||
/**
|
||||
* 根据notion的slug访问页面
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const PrefixSlug = props => {
|
||||
return <Slug {...props}/>
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
if (!BLOG.isProd) {
|
||||
return {
|
||||
paths: [],
|
||||
fallback: true
|
||||
}
|
||||
}
|
||||
|
||||
const from = 'slug-paths'
|
||||
const { allPages } = await getGlobalData({ from })
|
||||
return {
|
||||
paths: allPages?.filter(row => row.slug.indexOf('/') > 0).map(row => ({ params: { prefix: row.slug.split('/')[0], slug: row.slug.split('/')[1] } })),
|
||||
fallback: true
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStaticProps({ params: { prefix, slug } }) {
|
||||
let fullSlug = prefix + '/' + slug
|
||||
if (JSON.parse(BLOG.PSEUDO_STATIC)) {
|
||||
if (!fullSlug.endsWith('.html')) {
|
||||
fullSlug += '.html'
|
||||
}
|
||||
}
|
||||
const from = `slug-props-${fullSlug}`
|
||||
const props = await getGlobalData({ from })
|
||||
// 在列表内查找文章
|
||||
props.post = props?.allPages?.find((p) => {
|
||||
return p.slug === fullSlug || p.id === idToUuid(fullSlug)
|
||||
})
|
||||
|
||||
// 处理非列表内文章的内信息
|
||||
if (!props?.post) {
|
||||
const pageId = slug.slice(-1)[0]
|
||||
if (pageId.length >= 32) {
|
||||
const post = await getNotion(pageId)
|
||||
props.post = post
|
||||
}
|
||||
}
|
||||
|
||||
// 无法获取文章
|
||||
if (!props?.post) {
|
||||
props.post = null
|
||||
return { props, revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND) }
|
||||
}
|
||||
|
||||
// 文章内容加载
|
||||
if (!props?.posts?.blockMap) {
|
||||
props.post.blockMap = await getPostBlocks(props.post.id, from)
|
||||
}
|
||||
|
||||
// 推荐关联文章处理
|
||||
const allPosts = props.allPages.filter(page => page.type === 'Post' && page.status === 'Published')
|
||||
if (allPosts && allPosts.length > 0) {
|
||||
const index = allPosts.indexOf(props.post)
|
||||
props.prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0]
|
||||
props.next = allPosts.slice(index + 1, index + 2)[0] ?? allPosts[0]
|
||||
props.recommendPosts = getRecommendPost(props.post, allPosts, BLOG.POST_RECOMMEND_COUNT)
|
||||
} else {
|
||||
props.prev = null
|
||||
props.next = null
|
||||
props.recommendPosts = []
|
||||
}
|
||||
|
||||
delete props.allPages
|
||||
return {
|
||||
props,
|
||||
revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND)
|
||||
}
|
||||
}
|
||||
|
||||
export default PrefixSlug
|
||||
@@ -1,15 +1,15 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { getPostBlocks } from '@/lib/notion'
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import * as ThemeMap from '@/themes'
|
||||
import React from 'react'
|
||||
import { getGlobalData } from '@/lib/notion/getNotionData'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { idToUuid } from 'notion-utils'
|
||||
import Router from 'next/router'
|
||||
import { isBrowser } from '@/lib/utils'
|
||||
import { useRouter } from 'next/router'
|
||||
import { getNotion } from '@/lib/notion/getNotion'
|
||||
import { getPageTableOfContents } from '@/lib/notion/getPageTableOfContents'
|
||||
import { getLayoutByTheme } from '@/themes/theme'
|
||||
import md5 from 'js-md5'
|
||||
import { isBrowser } from '@/lib/utils'
|
||||
import { uploadDataToAlgolia } from '@/lib/algolia'
|
||||
|
||||
/**
|
||||
* 根据notion的slug访问页面
|
||||
@@ -17,50 +17,18 @@ import md5 from 'js-md5'
|
||||
* @returns
|
||||
*/
|
||||
const Slug = props => {
|
||||
const { theme, changeLoadingState } = useGlobal()
|
||||
const ThemeComponents = ThemeMap[theme]
|
||||
const { post, siteInfo } = props
|
||||
const router = Router.useRouter()
|
||||
const router = useRouter()
|
||||
|
||||
// 文章锁🔐
|
||||
const [lock, setLock] = React.useState(post?.password && post?.password !== '')
|
||||
|
||||
React.useEffect(() => {
|
||||
changeLoadingState(false)
|
||||
if (post?.password && post?.password !== '') {
|
||||
setLock(true)
|
||||
} else {
|
||||
if (!lock && post?.blockMap?.block) {
|
||||
post.content = Object.keys(post.blockMap.block)
|
||||
post.toc = getPageTableOfContents(post, post.blockMap)
|
||||
}
|
||||
|
||||
setLock(false)
|
||||
}
|
||||
}, [post])
|
||||
|
||||
if (!post) {
|
||||
setTimeout(() => {
|
||||
if (isBrowser()) {
|
||||
const article = document.getElementById('container')
|
||||
if (!article) {
|
||||
router.push('/404').then(() => {
|
||||
console.warn('找不到页面', router.asPath)
|
||||
})
|
||||
}
|
||||
}
|
||||
}, 8 * 1000) // 404时长 8秒
|
||||
const meta = { title: `${props?.siteInfo?.title || BLOG.TITLE} | loading`, image: siteInfo?.pageCover || BLOG.HOME_BANNER_IMAGE }
|
||||
return <ThemeComponents.LayoutSlug {...props} showArticleInfo={true} meta={meta} />
|
||||
}
|
||||
const [lock, setLock] = useState(post?.password && post?.password !== '')
|
||||
|
||||
/**
|
||||
* 验证文章密码
|
||||
* @param {*} result
|
||||
*/
|
||||
*/
|
||||
const validPassword = passInput => {
|
||||
const encrypt = md5(post.slug + passInput)
|
||||
|
||||
if (passInput && encrypt === post.password) {
|
||||
setLock(false)
|
||||
return true
|
||||
@@ -68,25 +36,47 @@ const Slug = props => {
|
||||
return false
|
||||
}
|
||||
|
||||
props = { ...props, lock, setLock, validPassword }
|
||||
// 文章加载
|
||||
useEffect(() => {
|
||||
// 404
|
||||
if (!post) {
|
||||
setTimeout(() => {
|
||||
if (isBrowser()) {
|
||||
const article = document.getElementById('notion-article')
|
||||
if (!article) {
|
||||
router.push('/404').then(() => {
|
||||
console.warn('找不到页面', router.asPath)
|
||||
})
|
||||
}
|
||||
}
|
||||
}, 8 * 1000) // 404时长 8秒
|
||||
}
|
||||
|
||||
// 文章加密
|
||||
if (post?.password && post?.password !== '') {
|
||||
setLock(true)
|
||||
} else {
|
||||
setLock(false)
|
||||
if (!lock && post?.blockMap?.block) {
|
||||
post.content = Object.keys(post.blockMap.block).filter(key => post.blockMap.block[key]?.value?.parent_id === post.id)
|
||||
post.toc = getPageTableOfContents(post, post.blockMap)
|
||||
}
|
||||
}
|
||||
}, [post])
|
||||
|
||||
const meta = {
|
||||
title: `${post?.title} | ${siteInfo?.title}`,
|
||||
title: post ? `${post?.title} | ${siteInfo?.title}` : `${props?.siteInfo?.title || BLOG.TITLE} | loading`,
|
||||
description: post?.summary,
|
||||
type: post?.type,
|
||||
slug: post?.slug,
|
||||
image: post?.page_cover,
|
||||
image: post?.pageCoverThumbnail || (siteInfo?.pageCover || BLOG.HOME_BANNER_IMAGE),
|
||||
category: post?.category?.[0],
|
||||
tags: post?.tags
|
||||
}
|
||||
|
||||
Router.events.on('routeChangeComplete', () => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
})
|
||||
|
||||
return (
|
||||
<ThemeComponents.LayoutSlug {...props} showArticleInfo={true} meta={meta} />
|
||||
)
|
||||
props = { ...props, lock, meta, setLock, validPassword }
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
return <Layout {...props} />
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
@@ -98,30 +88,30 @@ export async function getStaticPaths() {
|
||||
}
|
||||
|
||||
const from = 'slug-paths'
|
||||
const { allPages } = await getGlobalNotionData({ from })
|
||||
const { allPages } = await getGlobalData({ from })
|
||||
return {
|
||||
paths: allPages?.map(row => ({ params: { slug: [row.slug] } })),
|
||||
paths: allPages?.filter(row => row.slug.indexOf('/') < 0).map(row => ({ params: { prefix: row.slug } })),
|
||||
fallback: true
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStaticProps({ params: { slug } }) {
|
||||
let fullSlug = slug.join('/')
|
||||
if (BLOG.PSEUDO_STATIC) {
|
||||
export async function getStaticProps({ params: { prefix } }) {
|
||||
let fullSlug = prefix
|
||||
if (JSON.parse(BLOG.PSEUDO_STATIC)) {
|
||||
if (!fullSlug.endsWith('.html')) {
|
||||
fullSlug += '.html'
|
||||
}
|
||||
}
|
||||
const from = `slug-props-${fullSlug}`
|
||||
const props = await getGlobalNotionData({ from })
|
||||
const props = await getGlobalData({ from })
|
||||
// 在列表内查找文章
|
||||
props.post = props.allPages.find((p) => {
|
||||
props.post = props?.allPages?.find((p) => {
|
||||
return p.slug === fullSlug || p.id === idToUuid(fullSlug)
|
||||
})
|
||||
|
||||
// 处理非列表内文章的内信息
|
||||
if (!props?.post) {
|
||||
const pageId = slug.slice(-1)[0]
|
||||
const pageId = prefix.slice(-1)[0]
|
||||
if (pageId.length >= 32) {
|
||||
const post = await getNotion(pageId)
|
||||
props.post = post
|
||||
@@ -130,6 +120,7 @@ export async function getStaticProps({ params: { slug } }) {
|
||||
|
||||
// 无法获取文章
|
||||
if (!props?.post) {
|
||||
props.post = null
|
||||
return { props, revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND) }
|
||||
}
|
||||
|
||||
@@ -138,6 +129,10 @@ export async function getStaticProps({ params: { slug } }) {
|
||||
props.post.blockMap = await getPostBlocks(props.post.id, from)
|
||||
}
|
||||
|
||||
if (BLOG.ALGOLIA_APP_ID && BLOG.ALGOLIA_APP_KEY) {
|
||||
uploadDataToAlgolia(props?.post)
|
||||
}
|
||||
|
||||
// 推荐关联文章处理
|
||||
const allPosts = props.allPages.filter(page => page.type === 'Post' && page.status === 'Published')
|
||||
if (allPosts && allPosts.length > 0) {
|
||||
@@ -165,7 +160,7 @@ export async function getStaticProps({ params: { slug } }) {
|
||||
* @param {*} count
|
||||
* @returns
|
||||
*/
|
||||
function getRecommendPost(post, allPosts, count = 6) {
|
||||
export function getRecommendPost(post, allPosts, count = 6) {
|
||||
let recommendPosts = []
|
||||
const postIds = []
|
||||
const currentTags = post?.tags || []
|
||||
@@ -1,73 +1,36 @@
|
||||
import BLOG from 'blog.config'
|
||||
import React, { useEffect } from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import 'animate.css'
|
||||
import '@/styles/animate.css' // @see https://animate.style/
|
||||
import '@/styles/globals.css'
|
||||
import '@/styles/nprogress.css'
|
||||
import '@/styles/utility-patterns.css'
|
||||
|
||||
// core styles shared by all of react-notion-x (required)
|
||||
import 'react-notion-x/src/styles.css'
|
||||
import '@/styles/notion.css' // 重写部分样式
|
||||
|
||||
import { GlobalContextProvider } from '@/lib/global'
|
||||
import { DebugPanel } from '@/components/DebugPanel'
|
||||
import { ThemeSwitch } from '@/components/ThemeSwitch'
|
||||
import { Fireworks } from '@/components/Fireworks'
|
||||
import { Nest } from '@/components/Nest'
|
||||
import { FlutteringRibbon } from '@/components/FlutteringRibbon'
|
||||
import { Ribbon } from '@/components/Ribbon'
|
||||
import { Sakura } from '@/components/Sakura'
|
||||
import { StarrySky } from '@/components/StarrySky'
|
||||
import MusicPlayer from '@/components/MusicPlayer'
|
||||
import ExternalScript from '@/components/ExternalScript'
|
||||
import smoothscroll from 'smoothscroll-polyfill'
|
||||
|
||||
import AOS from 'aos'
|
||||
import 'aos/dist/aos.css' // You can also use <link> for styles
|
||||
import { isMobile } from '@/lib/utils'
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const Ackee = dynamic(() => import('@/components/Ackee'), { ssr: false })
|
||||
const Gtag = dynamic(() => import('@/components/Gtag'), { ssr: false })
|
||||
const Busuanzi = dynamic(() => import('@/components/Busuanzi'), { ssr: false })
|
||||
const GoogleAdsense = dynamic(() => import('@/components/GoogleAdsense'), {
|
||||
ssr: false
|
||||
})
|
||||
const Messenger = dynamic(() => import('@/components/FacebookMessenger'), {
|
||||
ssr: false
|
||||
})
|
||||
// 自定义样式css和js引入
|
||||
import ExternalScript from '@/components/ExternalScript'
|
||||
|
||||
// 各种扩展插件 动画等
|
||||
const ExternalPlugins = dynamic(() => import('@/components/ExternalPlugins'))
|
||||
|
||||
const MyApp = ({ Component, pageProps }) => {
|
||||
// 外部插件
|
||||
const externalPlugins = <>
|
||||
{JSON.parse(BLOG.THEME_SWITCH) && <ThemeSwitch />}
|
||||
{JSON.parse(BLOG.DEBUG) && <DebugPanel />}
|
||||
{BLOG.ANALYTICS_ACKEE_TRACKER && <Ackee />}
|
||||
{BLOG.ANALYTICS_GOOGLE_ID && <Gtag />}
|
||||
{JSON.parse(BLOG.ANALYTICS_BUSUANZI_ENABLE) && <Busuanzi />}
|
||||
{BLOG.ADSENSE_GOOGLE_ID && <GoogleAdsense />}
|
||||
{BLOG.FACEBOOK_APP_ID && BLOG.FACEBOOK_PAGE_ID && <Messenger />}
|
||||
{JSON.parse(BLOG.FIREWORKS) && <Fireworks />}
|
||||
{JSON.parse(BLOG.SAKURA) && <Sakura />}
|
||||
{JSON.parse(BLOG.STARRY_SKY) && <StarrySky />}
|
||||
{JSON.parse(BLOG.MUSIC_PLAYER) && <MusicPlayer />}
|
||||
{JSON.parse(BLOG.NEST) && <Nest />}
|
||||
{JSON.parse(BLOG.FLUTTERINGRIBBON) && <FlutteringRibbon />}
|
||||
{JSON.parse(BLOG.RIBBON) && <Ribbon />}
|
||||
<ExternalScript/>
|
||||
</>
|
||||
|
||||
useEffect(() => {
|
||||
AOS.init()
|
||||
if (isMobile()) {
|
||||
smoothscroll.polyfill()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<GlobalContextProvider>
|
||||
<ExternalScript />
|
||||
<Component {...pageProps} />
|
||||
{externalPlugins}
|
||||
<ExternalPlugins {...pageProps} />
|
||||
</GlobalContextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,17 +11,30 @@ class MyDocument extends Document {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Html lang={BLOG.LANG}>
|
||||
<Head>
|
||||
<link rel='icon' href='/favicon.ico' />
|
||||
<CommonScript />
|
||||
</Head>
|
||||
<Html lang={BLOG.LANG}>
|
||||
<Head>
|
||||
<link rel='icon' href= {`${BLOG.BLOG_FAVICON}`} />
|
||||
<CommonScript />
|
||||
{/* 预加载字体 */}
|
||||
{BLOG.FONT_AWESOME && <>
|
||||
<link rel='preload' href={BLOG.FONT_AWESOME} as="style" crossOrigin="anonymous" />
|
||||
<link rel="stylesheet" href={BLOG.FONT_AWESOME} crossOrigin="anonymous" referrerPolicy="no-referrer" />
|
||||
</>}
|
||||
|
||||
<body className={`${BLOG.FONT_STYLE} font-light bg-day dark:bg-night`}>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
{BLOG.FONT_URL?.map((fontUrl, index) => {
|
||||
if (fontUrl.endsWith('.css')) {
|
||||
return <link key={index} rel="stylesheet" href={fontUrl} />
|
||||
} else {
|
||||
return <link key={index} rel="preload" href={fontUrl} as="font" type="font/woff2" />
|
||||
}
|
||||
})}
|
||||
</Head>
|
||||
|
||||
<body className={`${BLOG.FONT_STYLE} font-light scroll-smooth`}>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
22
pages/api/subscribe.js
Normal file
22
pages/api/subscribe.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import subscribeToMailchimpApi from '@/lib/mailchimp'
|
||||
|
||||
/**
|
||||
* 接受邮件订阅
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
export default async function handler(req, res) {
|
||||
if (req.method === 'POST') {
|
||||
const { email, firstName, lastName } = req.body
|
||||
try {
|
||||
const response = await subscribeToMailchimpApi({ email, first_name: firstName, last_name: lastName })
|
||||
const data = await response.json()
|
||||
console.log('data', data)
|
||||
res.status(200).json({ status: 'success', message: 'Subscription successful!' })
|
||||
} catch (error) {
|
||||
res.status(400).json({ status: 'error', message: 'Subscription failed!', error })
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ status: 'error', message: 'Method not allowed' })
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,33 @@
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import React from 'react'
|
||||
import { getGlobalData } from '@/lib/notion/getNotionData'
|
||||
import { useEffect } from 'react'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import * as ThemeMap from '@/themes'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import { getLayoutByTheme } from '@/themes/theme'
|
||||
import { isBrowser } from '@/lib/utils'
|
||||
import { formatDateFmt } from '@/lib/formatDate'
|
||||
|
||||
const ArchiveIndex = props => {
|
||||
const { theme, locale } = useGlobal()
|
||||
const ThemeComponents = ThemeMap[theme]
|
||||
const { siteInfo } = props
|
||||
const { locale } = useGlobal()
|
||||
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
|
||||
useEffect(() => {
|
||||
if (isBrowser()) {
|
||||
const anchor = window.location.hash
|
||||
if (anchor) {
|
||||
setTimeout(() => {
|
||||
const anchorElement = document.getElementById(anchor.substring(1))
|
||||
if (anchorElement) {
|
||||
anchorElement.scrollIntoView({ block: 'start', behavior: 'smooth' })
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const meta = {
|
||||
title: `${locale.NAV.ARCHIVE} | ${siteInfo?.title}`,
|
||||
description: siteInfo?.description,
|
||||
@@ -16,11 +36,13 @@ const ArchiveIndex = props => {
|
||||
type: 'website'
|
||||
}
|
||||
|
||||
return <ThemeComponents.LayoutArchive {...props} meta={meta} />
|
||||
props = { ...props, meta }
|
||||
|
||||
return <Layout {...props} />
|
||||
}
|
||||
|
||||
export async function getStaticProps() {
|
||||
const props = await getGlobalNotionData({ from: 'archive-index' })
|
||||
const props = await getGlobalData({ from: 'archive-index' })
|
||||
// 处理分页
|
||||
props.posts = props.allPages.filter(page => page.type === 'Post' && page.status === 'Published')
|
||||
delete props.allPages
|
||||
@@ -28,15 +50,13 @@ export async function getStaticProps() {
|
||||
const postsSortByDate = Object.create(props.posts)
|
||||
|
||||
postsSortByDate.sort((a, b) => {
|
||||
const dateA = new Date(a?.date?.start_date || a.createdTime)
|
||||
const dateB = new Date(b?.date?.start_date || b.createdTime)
|
||||
return dateB - dateA
|
||||
return b?.publishDate - a?.publishDate
|
||||
})
|
||||
|
||||
const archivePosts = {}
|
||||
|
||||
postsSortByDate.forEach(post => {
|
||||
const date = post.date?.start_date?.slice(0, 7) || post.createdTime
|
||||
const date = formatDateFmt(post.publishDate, 'yyyy-MM')
|
||||
if (archivePosts[date]) {
|
||||
archivePosts[date].push(post)
|
||||
} else {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import { getGlobalData } from '@/lib/notion/getNotionData'
|
||||
import React from 'react'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import * as ThemeMap from '@/themes'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import { getLayoutByTheme } from '@/themes/theme'
|
||||
|
||||
/**
|
||||
* 分类页
|
||||
@@ -10,13 +11,12 @@ import BLOG from '@/blog.config'
|
||||
* @returns
|
||||
*/
|
||||
export default function Category(props) {
|
||||
const { theme } = useGlobal()
|
||||
const ThemeComponents = ThemeMap[theme]
|
||||
const { siteInfo, posts } = props
|
||||
const { siteInfo } = props
|
||||
const { locale } = useGlobal()
|
||||
if (!posts) {
|
||||
return <ThemeComponents.Layout404 {...props} />
|
||||
}
|
||||
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
|
||||
const meta = {
|
||||
title: `${props.category} | ${locale.COMMON.CATEGORY} | ${
|
||||
siteInfo?.title || ''
|
||||
@@ -26,12 +26,15 @@ export default function Category(props) {
|
||||
image: siteInfo?.pageCover,
|
||||
type: 'website'
|
||||
}
|
||||
return <ThemeComponents.LayoutCategory {...props} meta={meta} />
|
||||
|
||||
props = { ...props, meta }
|
||||
|
||||
return <Layout {...props} />
|
||||
}
|
||||
|
||||
export async function getStaticProps({ params: { category } }) {
|
||||
const from = 'category-props'
|
||||
let props = await getGlobalNotionData({ from })
|
||||
let props = await getGlobalData({ from })
|
||||
|
||||
// 过滤状态
|
||||
props.posts = props.allPages.filter(page => page.type === 'Post' && page.status === 'Published')
|
||||
@@ -58,7 +61,7 @@ export async function getStaticProps({ params: { category } }) {
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const from = 'category-paths'
|
||||
const { categoryOptions } = await getGlobalNotionData({ from })
|
||||
const { categoryOptions } = await getGlobalData({ from })
|
||||
return {
|
||||
paths: Object.keys(categoryOptions).map(category => ({
|
||||
params: { category: categoryOptions[category]?.name }
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import { getGlobalData } from '@/lib/notion/getNotionData'
|
||||
import React from 'react'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import * as ThemeMap from '@/themes'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import { getLayoutByTheme } from '@/themes/theme'
|
||||
|
||||
/**
|
||||
* 分类页
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
|
||||
export default function Category(props) {
|
||||
const { theme } = useGlobal()
|
||||
const ThemeComponents = ThemeMap[theme]
|
||||
const { siteInfo, posts } = props
|
||||
const { siteInfo } = props
|
||||
const { locale } = useGlobal()
|
||||
if (!posts) {
|
||||
return <ThemeComponents.Layout404 {...props} />
|
||||
}
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
|
||||
const meta = {
|
||||
title: `${props.category} | ${locale.COMMON.CATEGORY} | ${
|
||||
siteInfo?.title || ''
|
||||
@@ -26,12 +26,15 @@ export default function Category(props) {
|
||||
image: siteInfo?.pageCover,
|
||||
type: 'website'
|
||||
}
|
||||
return <ThemeComponents.LayoutCategory {...props} meta={meta} />
|
||||
|
||||
props = { ...props, meta }
|
||||
|
||||
return <Layout {...props} />
|
||||
}
|
||||
|
||||
export async function getStaticProps({ params: { category, page } }) {
|
||||
const from = 'category-page-props'
|
||||
let props = await getGlobalNotionData({ from })
|
||||
let props = await getGlobalData({ from })
|
||||
|
||||
// 过滤状态类型
|
||||
props.posts = props.allPages.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post.category && post.category.includes(category))
|
||||
@@ -53,7 +56,7 @@ export async function getStaticProps({ params: { category, page } }) {
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const from = 'category-paths'
|
||||
const { categoryOptions, allPages } = await getGlobalNotionData({ from })
|
||||
const { categoryOptions, allPages } = await getGlobalData({ from })
|
||||
const paths = []
|
||||
|
||||
categoryOptions?.forEach(category => {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import { getGlobalData } from '@/lib/notion/getNotionData'
|
||||
import React from 'react'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import * as ThemeMap from '@/themes'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import { getLayoutByTheme } from '@/themes/theme'
|
||||
|
||||
/**
|
||||
* 分类首页
|
||||
@@ -10,10 +11,12 @@ import BLOG from '@/blog.config'
|
||||
* @returns
|
||||
*/
|
||||
export default function Category(props) {
|
||||
const { theme } = useGlobal()
|
||||
const ThemeComponents = ThemeMap[theme]
|
||||
const { locale } = useGlobal()
|
||||
const { siteInfo } = props
|
||||
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
|
||||
const meta = {
|
||||
title: `${locale.COMMON.CATEGORY} | ${siteInfo?.title}`,
|
||||
description: siteInfo?.description,
|
||||
@@ -21,11 +24,13 @@ export default function Category(props) {
|
||||
slug: 'category',
|
||||
type: 'website'
|
||||
}
|
||||
return <ThemeComponents.LayoutCategoryIndex {...props} meta={meta} />
|
||||
props = { ...props, meta }
|
||||
|
||||
return <Layout {...props} />
|
||||
}
|
||||
|
||||
export async function getStaticProps() {
|
||||
const props = await getGlobalNotionData({ from: 'category-index-props' })
|
||||
const props = await getGlobalData({ from: 'category-index-props' })
|
||||
delete props.allPages
|
||||
return {
|
||||
props,
|
||||
|
||||
@@ -1,19 +1,32 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { getPostBlocks } from '@/lib/notion'
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import * as ThemeMap from '@/themes'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { getGlobalData } from '@/lib/notion/getNotionData'
|
||||
import { generateRss } from '@/lib/rss'
|
||||
import { generateRobotsTxt } from '@/lib/robots.txt'
|
||||
|
||||
import { useRouter } from 'next/router'
|
||||
import { getLayoutByTheme } from '@/themes/theme'
|
||||
import { generateAlgoliaSearch } from '@/lib/algolia'
|
||||
|
||||
/**
|
||||
* 首页布局
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
|
||||
const Index = props => {
|
||||
const { theme } = useGlobal()
|
||||
const ThemeComponents = ThemeMap[theme]
|
||||
return <ThemeComponents.LayoutIndex {...props} />
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
return <Layout {...props} />
|
||||
}
|
||||
|
||||
/**
|
||||
* SSG 获取数据
|
||||
* @returns
|
||||
*/
|
||||
export async function getStaticProps() {
|
||||
const from = 'index'
|
||||
const props = await getGlobalNotionData({ from })
|
||||
const props = await getGlobalData({ from })
|
||||
|
||||
const { siteInfo } = props
|
||||
props.posts = props.allPages.filter(page => page.type === 'Post' && page.status === 'Published')
|
||||
@@ -50,6 +63,11 @@ export async function getStaticProps() {
|
||||
generateRss(props?.latestPosts || [])
|
||||
}
|
||||
|
||||
// 生成全文索引 - 仅在 yarn build 时执行 && process.env.npm_lifecycle_event === 'build'
|
||||
if (BLOG.ALGOLIA_APP_ID && JSON.parse(BLOG.ALGOLIA_RECREATE_DATA)) {
|
||||
generateAlgoliaSearch({ allPages: props.allPages })
|
||||
}
|
||||
|
||||
delete props.allPages
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,29 +1,36 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { getPostBlocks } from '@/lib/notion'
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import * as ThemeMap from '@/themes'
|
||||
import { getGlobalData } from '@/lib/notion/getNotionData'
|
||||
import { useRouter } from 'next/router'
|
||||
import { getLayoutByTheme } from '@/themes/theme'
|
||||
|
||||
/**
|
||||
* 文章列表分页
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const Page = props => {
|
||||
const { theme } = useGlobal()
|
||||
const { siteInfo } = props
|
||||
const ThemeComponents = ThemeMap[theme]
|
||||
if (!siteInfo) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
|
||||
const meta = {
|
||||
title: `${props.page} | Page | ${siteInfo?.title}`,
|
||||
title: `${props?.page} | Page | ${siteInfo?.title}`,
|
||||
description: siteInfo?.description,
|
||||
image: siteInfo?.pageCover,
|
||||
slug: 'page/' + props.page,
|
||||
type: 'website'
|
||||
}
|
||||
return <ThemeComponents.LayoutPage {...props} meta={meta} />
|
||||
|
||||
props = { ...props, meta }
|
||||
|
||||
return <Layout {...props} />
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const from = 'page-paths'
|
||||
const { postCount } = await getGlobalNotionData({ from })
|
||||
const { postCount } = await getGlobalData({ from })
|
||||
const totalPages = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
|
||||
return {
|
||||
// remove first page, we 're not gonna handle that.
|
||||
@@ -36,7 +43,7 @@ export async function getStaticPaths() {
|
||||
|
||||
export async function getStaticProps({ params: { page } }) {
|
||||
const from = `page-${page}`
|
||||
const props = await getGlobalNotionData({ from })
|
||||
const props = await getGlobalData({ from })
|
||||
const { allPages } = props
|
||||
const allPosts = allPages.filter(page => page.type === 'Post' && page.status === 'Published')
|
||||
// 处理分页
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import { getGlobalData } from '@/lib/notion/getNotionData'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { getDataFromCache } from '@/lib/cache/cache_manager'
|
||||
import * as ThemeMap from '@/themes'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import { getLayoutByTheme } from '@/themes/theme'
|
||||
|
||||
const Index = props => {
|
||||
const { keyword, siteInfo } = props
|
||||
const { locale } = useGlobal()
|
||||
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
|
||||
const meta = {
|
||||
title: `${keyword || ''}${keyword ? ' | ' : ''}${locale.NAV.SEARCH} | ${siteInfo?.title}`,
|
||||
description: siteInfo?.title,
|
||||
@@ -14,15 +19,10 @@ const Index = props => {
|
||||
slug: 'search/' + (keyword || ''),
|
||||
type: 'website'
|
||||
}
|
||||
const { theme } = useGlobal()
|
||||
const ThemeComponents = ThemeMap[theme]
|
||||
return (
|
||||
<ThemeComponents.LayoutSearch
|
||||
{...props}
|
||||
meta={meta}
|
||||
currentSearch={keyword}
|
||||
/>
|
||||
)
|
||||
|
||||
props = { ...props, meta }
|
||||
|
||||
return <Layout {...props} />
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,7 +31,7 @@ const Index = props => {
|
||||
* @returns
|
||||
*/
|
||||
export async function getStaticProps({ params: { keyword } }) {
|
||||
const props = await getGlobalNotionData({
|
||||
const props = await getGlobalData({
|
||||
from: 'search-props',
|
||||
pageType: ['Post']
|
||||
})
|
||||
@@ -117,20 +117,11 @@ async function filterByMemCache(allPosts, keyword) {
|
||||
for (const post of allPosts) {
|
||||
const cacheKey = 'page_block_' + post.id
|
||||
const page = await getDataFromCache(cacheKey, true)
|
||||
const tagContent = post.tags && Array.isArray(post.tags) ? post.tags.join(' ') : ''
|
||||
const tagContent = post?.tags && Array.isArray(post?.tags) ? post?.tags.join(' ') : ''
|
||||
const categoryContent = post.category && Array.isArray(post.category) ? post.category.join(' ') : ''
|
||||
const articleInfo = post.title + post.summary + tagContent + categoryContent
|
||||
let hit = articleInfo.toLowerCase().indexOf(keyword) > -1
|
||||
let indexContent = [post.summary]
|
||||
// 防止搜到加密文章的内容
|
||||
if (page && page.block && !post.password) {
|
||||
const contentIds = Object.keys(page.block)
|
||||
contentIds.forEach(id => {
|
||||
const properties = page?.block[id]?.value?.properties
|
||||
indexContent = appendText(indexContent, properties, 'title')
|
||||
indexContent = appendText(indexContent, properties, 'caption')
|
||||
})
|
||||
}
|
||||
const indexContent = getPageContentText(post, page)
|
||||
// console.log('全文搜索缓存', cacheKey, page != null)
|
||||
post.results = []
|
||||
let hitCount = 0
|
||||
@@ -157,4 +148,18 @@ async function filterByMemCache(allPosts, keyword) {
|
||||
return filterPosts
|
||||
}
|
||||
|
||||
export function getPageContentText(post, pageBlockMap) {
|
||||
let indexContent = []
|
||||
// 防止搜到加密文章的内容
|
||||
if (pageBlockMap && pageBlockMap.block && !post.password) {
|
||||
const contentIds = Object.keys(pageBlockMap.block)
|
||||
contentIds.forEach(id => {
|
||||
const properties = pageBlockMap?.block[id]?.value?.properties
|
||||
indexContent = appendText(indexContent, properties, 'title')
|
||||
indexContent = appendText(indexContent, properties, 'caption')
|
||||
})
|
||||
}
|
||||
return indexContent.join('')
|
||||
}
|
||||
|
||||
export default Index
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import { getGlobalData } from '@/lib/notion/getNotionData'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { getDataFromCache } from '@/lib/cache/cache_manager'
|
||||
import * as ThemeMap from '@/themes'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import { getLayoutByTheme } from '@/themes/theme'
|
||||
|
||||
const Index = props => {
|
||||
const { keyword, siteInfo } = props
|
||||
const { locale } = useGlobal()
|
||||
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
|
||||
const meta = {
|
||||
title: `${keyword || ''}${keyword ? ' | ' : ''}${locale.NAV.SEARCH} | ${siteInfo?.title}`,
|
||||
description: siteInfo?.title,
|
||||
@@ -14,15 +19,10 @@ const Index = props => {
|
||||
slug: 'search/' + (keyword || ''),
|
||||
type: 'website'
|
||||
}
|
||||
const { theme } = useGlobal()
|
||||
const ThemeComponents = ThemeMap[theme]
|
||||
return (
|
||||
<ThemeComponents.LayoutSearch
|
||||
{...props}
|
||||
meta={meta}
|
||||
currentSearch={keyword}
|
||||
/>
|
||||
)
|
||||
|
||||
props = { ...props, meta, currentSearch: keyword }
|
||||
|
||||
return <Layout {...props} />
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,7 +31,7 @@ const Index = props => {
|
||||
* @returns
|
||||
*/
|
||||
export async function getStaticProps({ params: { keyword, page } }) {
|
||||
const props = await getGlobalNotionData({
|
||||
const props = await getGlobalData({
|
||||
from: 'search-props',
|
||||
pageType: ['Post']
|
||||
})
|
||||
@@ -115,7 +115,7 @@ async function filterByMemCache(allPosts, keyword) {
|
||||
for (const post of allPosts) {
|
||||
const cacheKey = 'page_block_' + post.id
|
||||
const page = await getDataFromCache(cacheKey, true)
|
||||
const tagContent = post.tags && Array.isArray(post.tags) ? post.tags.join(' ') : ''
|
||||
const tagContent = post?.tags && Array.isArray(post?.tags) ? post?.tags.join(' ') : ''
|
||||
const categoryContent = post.category && Array.isArray(post.category) ? post.category.join(' ') : ''
|
||||
const articleInfo = post.title + post.summary + tagContent + categoryContent
|
||||
let hit = articleInfo.indexOf(keyword) > -1
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import { getGlobalData } from '@/lib/notion/getNotionData'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useRouter } from 'next/router'
|
||||
import * as ThemeMap from '@/themes'
|
||||
import BLOG from '@/blog.config'
|
||||
import { getLayoutByTheme } from '@/themes/theme'
|
||||
|
||||
/**
|
||||
* 搜索路由
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const Search = props => {
|
||||
const { posts, siteInfo } = props
|
||||
const { locale } = useGlobal()
|
||||
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
|
||||
const router = useRouter()
|
||||
const keyword = getSearchKey(router)
|
||||
|
||||
let filteredPosts
|
||||
const searchKey = getSearchKey(router)
|
||||
// 静态过滤
|
||||
if (searchKey) {
|
||||
if (keyword) {
|
||||
filteredPosts = posts.filter(post => {
|
||||
const tagContent = post.tags ? post.tags.join(' ') : ''
|
||||
const tagContent = post?.tags ? post?.tags.join(' ') : ''
|
||||
const categoryContent = post.category ? post.category.join(' ') : ''
|
||||
const searchContent =
|
||||
post.title + post.summary + tagContent + categoryContent
|
||||
return searchContent.toLowerCase().includes(searchKey.toLowerCase())
|
||||
post.title + post.summary + tagContent + categoryContent
|
||||
return searchContent.toLowerCase().includes(keyword.toLowerCase())
|
||||
})
|
||||
} else {
|
||||
filteredPosts = []
|
||||
}
|
||||
|
||||
const { locale } = useGlobal()
|
||||
const meta = {
|
||||
title: `${searchKey || ''}${searchKey ? ' | ' : ''}${locale.NAV.SEARCH} | ${
|
||||
siteInfo?.title
|
||||
}`,
|
||||
title: `${keyword || ''}${keyword ? ' | ' : ''}${locale.NAV.SEARCH} | ${siteInfo?.title}`,
|
||||
description: siteInfo?.description,
|
||||
image: siteInfo?.pageCover,
|
||||
slug: 'search',
|
||||
type: 'website'
|
||||
}
|
||||
|
||||
const { theme } = useGlobal()
|
||||
const ThemeComponents = ThemeMap[theme]
|
||||
props = { ...props, meta, posts: filteredPosts }
|
||||
|
||||
return (
|
||||
<ThemeComponents.LayoutSearch
|
||||
{...props}
|
||||
posts={filteredPosts}
|
||||
currentSearch={searchKey}
|
||||
meta={meta}
|
||||
/>
|
||||
)
|
||||
return <Layout {...props} />
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览器前端搜索
|
||||
*/
|
||||
export async function getStaticProps() {
|
||||
const props = await getGlobalNotionData({
|
||||
const props = await getGlobalData({
|
||||
from: 'search-props',
|
||||
pageType: ['Post']
|
||||
})
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// pages/sitemap.xml.js
|
||||
import { getServerSideSitemap } from 'next-sitemap'
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import { getGlobalData } from '@/lib/notion/getNotionData'
|
||||
import BLOG from '@/blog.config'
|
||||
|
||||
export const getServerSideProps = async (ctx) => {
|
||||
const { allPages } = await getGlobalNotionData({ from: 'rss' })
|
||||
const { allPages } = await getGlobalData({ from: 'rss' })
|
||||
const defaultFields = [
|
||||
{
|
||||
loc: `${BLOG.LINK}`,
|
||||
@@ -39,9 +39,10 @@ export const getServerSideProps = async (ctx) => {
|
||||
}
|
||||
]
|
||||
const postFields = allPages?.filter(p => p.status === BLOG.NOTION_PROPERTY_NAME.status_publish)?.map(post => {
|
||||
const slugWithoutLeadingSlash = post?.slug.startsWith('/') ? post?.slug?.slice(1) : post.slug
|
||||
return {
|
||||
loc: `${BLOG.LINK}/${post.slug}`,
|
||||
lastmod: new Date(post?.date?.start_date || post?.createdTime).toISOString().split('T')[0],
|
||||
loc: `${BLOG.LINK}/${slugWithoutLeadingSlash}`,
|
||||
lastmod: new Date(post?.publishTime).toISOString().split('T')[0],
|
||||
changefreq: 'daily',
|
||||
priority: '0.7'
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import * as ThemeMap from '@/themes'
|
||||
import { getGlobalData } from '@/lib/notion/getNotionData'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import { getLayoutByTheme } from '@/themes/theme'
|
||||
|
||||
/**
|
||||
* 标签下的文章列表
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const Tag = props => {
|
||||
const { theme } = useGlobal()
|
||||
const ThemeComponents = ThemeMap[theme]
|
||||
const { locale } = useGlobal()
|
||||
const { tag, siteInfo, posts } = props
|
||||
const { tag, siteInfo } = props
|
||||
|
||||
if (!posts) {
|
||||
return <ThemeComponents.Layout404 {...props} />
|
||||
}
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
|
||||
const meta = {
|
||||
title: `${tag} | ${locale.COMMON.TAGS} | ${siteInfo?.title}`,
|
||||
@@ -20,15 +23,17 @@ const Tag = props => {
|
||||
slug: 'tag/' + tag,
|
||||
type: 'website'
|
||||
}
|
||||
return <ThemeComponents.LayoutTag {...props} meta={meta} />
|
||||
props = { ...props, meta }
|
||||
|
||||
return <Layout {...props} />
|
||||
}
|
||||
|
||||
export async function getStaticProps({ params: { tag } }) {
|
||||
const from = 'tag-props'
|
||||
const props = await getGlobalNotionData({ from })
|
||||
const props = await getGlobalData({ from })
|
||||
|
||||
// 过滤状态
|
||||
props.posts = props.allPages.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post.tags && post.tags.includes(tag))
|
||||
props.posts = props.allPages.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post?.tags && post?.tags.includes(tag))
|
||||
|
||||
// 处理文章页数
|
||||
props.postCount = props.posts.length
|
||||
@@ -63,7 +68,7 @@ function getTagNames(tags) {
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const from = 'tag-static-path'
|
||||
const { tagOptions } = await getGlobalNotionData({ from })
|
||||
const { tagOptions } = await getGlobalData({ from })
|
||||
const tagNames = getTagNames(tagOptions)
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import * as ThemeMap from '@/themes'
|
||||
import { getGlobalData } from '@/lib/notion/getNotionData'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import { getLayoutByTheme } from '@/themes/theme'
|
||||
|
||||
const Tag = props => {
|
||||
const { theme } = useGlobal()
|
||||
const ThemeComponents = ThemeMap[theme]
|
||||
const { locale } = useGlobal()
|
||||
const { tag, siteInfo, posts } = props
|
||||
const { tag, siteInfo } = props
|
||||
|
||||
if (!posts) {
|
||||
return <ThemeComponents.Layout404 {...props} />
|
||||
}
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
|
||||
const meta = {
|
||||
title: `${tag} | ${locale.COMMON.TAGS} | ${siteInfo?.title}`,
|
||||
@@ -20,14 +18,16 @@ const Tag = props => {
|
||||
slug: 'tag/' + tag,
|
||||
type: 'website'
|
||||
}
|
||||
return <ThemeComponents.LayoutTag {...props} meta={meta} />
|
||||
props = { ...props, meta }
|
||||
|
||||
return <Layout {...props} />
|
||||
}
|
||||
|
||||
export async function getStaticProps({ params: { tag, page } }) {
|
||||
const from = 'tag-page-props'
|
||||
const props = await getGlobalNotionData({ from })
|
||||
const props = await getGlobalData({ from })
|
||||
// 过滤状态、标签
|
||||
props.posts = props.allPages.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post.tags && post.tags.includes(tag))
|
||||
props.posts = props.allPages.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post?.tags && post?.tags.includes(tag))
|
||||
// 处理文章数
|
||||
props.postCount = props.posts.length
|
||||
// 处理分页
|
||||
@@ -44,11 +44,11 @@ export async function getStaticProps({ params: { tag, page } }) {
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const from = 'tag-page-static-path'
|
||||
const { tagOptions, allPages } = await getGlobalNotionData({ from })
|
||||
const { tagOptions, allPages } = await getGlobalData({ from })
|
||||
const paths = []
|
||||
tagOptions?.forEach(tag => {
|
||||
// 过滤状态类型
|
||||
const tagPosts = allPages.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post.tags && post.tags.includes(tag.name))
|
||||
const tagPosts = allPages.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post?.tags && post?.tags.includes(tag.name))
|
||||
// 处理文章页数
|
||||
const postCount = tagPosts.length
|
||||
const totalPages = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import React from 'react'
|
||||
import { getGlobalData } from '@/lib/notion/getNotionData'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import * as ThemeMap from '@/themes'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import { getLayoutByTheme } from '@/themes/theme'
|
||||
|
||||
/**
|
||||
* 标签首页
|
||||
@@ -10,10 +10,12 @@ import BLOG from '@/blog.config'
|
||||
* @returns
|
||||
*/
|
||||
const TagIndex = props => {
|
||||
const { theme } = useGlobal()
|
||||
const ThemeComponents = ThemeMap[theme]
|
||||
const { locale } = useGlobal()
|
||||
const { siteInfo } = props
|
||||
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
|
||||
const meta = {
|
||||
title: `${locale.COMMON.TAGS} | ${siteInfo?.title}`,
|
||||
description: siteInfo?.description,
|
||||
@@ -21,12 +23,14 @@ const TagIndex = props => {
|
||||
slug: 'tag',
|
||||
type: 'website'
|
||||
}
|
||||
return <ThemeComponents.LayoutTagIndex {...props} meta={meta} />
|
||||
props = { ...props, meta }
|
||||
|
||||
return <Layout {...props} />
|
||||
}
|
||||
|
||||
export async function getStaticProps() {
|
||||
const from = 'tag-index-props'
|
||||
const props = await getGlobalNotionData({ from })
|
||||
const props = await getGlobalData({ from })
|
||||
delete props.allPages
|
||||
return {
|
||||
props,
|
||||
|
||||
BIN
public/bg_image.jpg
Executable file → Normal file
BIN
public/bg_image.jpg
Executable file → Normal file
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 65 KiB |
@@ -1,2 +1,8 @@
|
||||
/* 静态文件导入 自定义样式*/
|
||||
|
||||
#theme-fukasawa .sideLeft hr{
|
||||
opacity: .04;
|
||||
}
|
||||
.fa-info:before {
|
||||
content: "\f05a";
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.collapse-wrapper .code-toolbar {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.toolbar-item{
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -21,7 +25,7 @@
|
||||
|
||||
pre[class*='language-'] {
|
||||
margin-top: 0rem !important;
|
||||
margin-bottom: 0rem !important;
|
||||
// margin-bottom: 0rem !important;
|
||||
padding-top: 1.5rem !important;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
#theme-fukasawa .grid-item {
|
||||
height: auto;
|
||||
break-inside: avoid-column;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
/* 大屏幕(宽度≥1024px)下显示3列 */
|
||||
@media (min-width: 1024px) {
|
||||
#theme-fukasawa .grid-container {
|
||||
column-count: 3;
|
||||
column-gap: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 小屏幕(宽度≥640px)下显示2列 */
|
||||
@media (min-width: 640px) and (max-width: 1023px) {
|
||||
#theme-fukasawa .grid-container {
|
||||
column-count: 2;
|
||||
column-gap: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端(宽度<640px)下显示1列 */
|
||||
@media (max-width: 639px) {
|
||||
#theme-fukasawa .grid-container {
|
||||
column-count: 1;
|
||||
column-gap: .5rem;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/* 菜单下划线动画 */
|
||||
#theme-hexo .menu-link {
|
||||
text-decoration: none;
|
||||
background-image: linear-gradient(#928CEE, #928CEE);
|
||||
background-repeat: no-repeat;
|
||||
background-position: bottom center;
|
||||
background-size: 0 2px;
|
||||
transition: background-size 100ms ease-in-out;
|
||||
}
|
||||
|
||||
#theme-hexo .menu-link:hover {
|
||||
background-size: 100% 2px;
|
||||
color: #928CEE;
|
||||
}
|
||||
|
||||
/* 设置了从上到下的渐变黑色 */
|
||||
#theme-hexo .header-cover::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(to bottom, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0.2) 10%, rgba(0,0,0,0) 25%, rgba(0,0,0,0.2) 75%, rgba(0,0,0,0.5) 100%);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
|
||||
/* 设置了从上到下的渐变黑色 */
|
||||
#theme-matery .header-cover::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(to bottom, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0.2) 10%, rgba(0,0,0,0) 25%, rgba(0,0,0,0.2) 75%, rgba(0,0,0,0.5) 100%);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
#theme-simple #announcement-content {
|
||||
/* background-color: #f6f6f6; */
|
||||
}
|
||||
|
||||
#theme-simple #blog-item-title {
|
||||
color: #276077;
|
||||
}
|
||||
|
||||
.dark #theme-simple #blog-item-title {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.notion {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
/* 菜单下划线动画 */
|
||||
#theme-simple .menu-link {
|
||||
text-decoration: none;
|
||||
background-image: linear-gradient(#dd3333, #dd3333);
|
||||
background-repeat: no-repeat;
|
||||
background-position: bottom center;
|
||||
background-size: 0 2px;
|
||||
transition: background-size 100ms ease-in-out;
|
||||
}
|
||||
|
||||
#theme-simple .menu-link:hover {
|
||||
background-size: 100% 2px;
|
||||
color: #dd3333;
|
||||
}
|
||||
|
||||
BIN
public/images/feature-1.webp
Normal file
BIN
public/images/feature-1.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user