解决冲突

This commit is contained in:
Vixcity
2023-07-28 14:54:30 +08:00
567 changed files with 17001 additions and 5969 deletions

View File

@@ -1,2 +1,2 @@
# 环境变量 @see https://www.nextjs.cn/docs/basic-features/environment-variables
NEXT_PUBLIC_VERSION=3.13.5
NEXT_PUBLIC_VERSION=4.0.6

View File

@@ -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

View File

@@ -21,13 +21,14 @@ assignees: tangly1024
希望按这个步骤,正常操作结果是什么
**截图**
相关的页面,应该结果
相关的页面,应该结果
**环境**
- 操作系统: [例如. iOS, Android, macOS, windows]
- 浏览器 [例如. chrome, safari, firefox]
- 版本 [e.g. 22]
- NotionNext版本 [e.g. 3.13.6]
- 主题 [例如. hexo]
**补充说明**
与问题相关的其它说明

View File

@@ -29,21 +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://tangly1024.com/article/how-to-update-notionnext)
- [更新指南 - (升级您的代码)](https://docs.tangly1024.com/article/how-to-update-notionnext)
- [历史版本汇总 - 查询版本功能特性](https://tangly1024.com/article/notion-next-changelogs)
- [版本汇总 - (查询变动功能)](https://docs.tangly1024.com/article/notion-next-changelogs)
## 致谢
感谢Craig Hart发起的Nobelium项目
@@ -168,6 +168,26 @@
<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>

View File

@@ -1,16 +1,19 @@
// 注: process.env.XX是Vercel的环境变量配置方式见https://docs.tangly1024.com/zh/features/personality
const BLOG = {
// Important page_idDuplicate Template from https://www.notion.so/tanghh/02ab3b8678004aa69e9e415905ef32a5
NOTION_PAGE_ID:
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时关闭根据时间自动切换夜间模式
// 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 || 'Vixcity', // 您的昵称 例如 tangly1024
@@ -24,28 +27,37 @@ const BLOG = {
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 首页
CONTACT_INSTAGRAM: process.env.NEXT_PUBLIC_CONTACT_INSTAGRAM || '', // 您的instagram地址
CONTACT_BILIBILI: process.env.NEXT_PUBLIC_CONTACT_BILIBILI || 'https://space.bilibili.com/260909298', // B站主页
CONTACT_YOUTUBE: process.env.NEXT_PUBLIC_CONTACT_YOUTUBE || '', // Youtube主页
// 网站字体
FONT_STYLE: process.env.NEXT_PUBLIC_FONT_STYLE || 'font-serif', // ['font-serif','font-sans'] 两种可选,分别是衬线和无衬线: 参考 https://www.jianshu.com/p/55e410bd2115
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
FONT_URL: [
// 字体CSS 例如 https://npm.elemecdn.com/lxgw-wenkai-webfont@1.6.0/style.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'
],
// 无衬线字体 例如'"LXGW WenKai"'
FONT_SANS: [
// 无衬线字体 例如'LXGW WenKai'
'Bitter',
// '"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',
'"Microsoft YaHei"',
'"Helvetica Neue"',
'Helvetica',
'"Source Han Sans SC"',
@@ -53,8 +65,9 @@ const BLOG = {
'sans-serif',
'"Apple Color Emoji"'
],
// 衬线字体 例如'"LXGW WenKai"'
FONT_SERIF: [
// 衬线字体 例如'LXGW WenKai'
// '"LXGW WenKai"',
'Bitter',
'"Noto Serif SC"',
'SimSun',
@@ -65,7 +78,11 @@ const BLOG = {
'"Segoe UI Symbol"',
'"Apple Color Emoji"'
],
FONT_AWESOME: process.env.NEXT_PUBLIC_FONT_AWESOME_PATH || '/css/all.min.css', // font-awesome 字体图标地址、默认读取本地; 可选 https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/font-awesome/6.0.0/css/all.min.css
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']
@@ -82,14 +99,24 @@ const BLOG = {
BEI_AN: process.env.NEXT_PUBLIC_BEI_AN || '', // 备案号 闽ICP备XXXXXXX
// START********代码相关********
// 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', // 是否显示行号
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 '#'
@@ -105,16 +132,25 @@ const BLOG = {
// 支援類似 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_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: 10, // post counts per page
POSTS_SORT_BY: process.env.NEXT_PUBLIC_POST_SORT_BY || 'date', // 排序方式 '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 || true, // 开关
// 烟花色彩,感谢 https://github.com/Vixcity 提交的色彩
@@ -127,24 +163,24 @@ const BLOG = {
// 樱花飘落特效
SAKURA: process.env.NEXT_PUBLIC_SAKURA || false, // 开关
// 漂浮线段特效
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, // 开关
// ********挂件组件相关********
// 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-unitychan@1.0.5/assets/unitychan.model.json', // 挂件模型地址 @see https://github.com/xiazeyu/live2d-widget-models
WIDGET_PET_SWITCH_THEME: false, // 点击宠物挂件切换博客主题
WIDGET_PET_SWITCH_THEME: true, // 点击宠物挂件切换博客主题
// 好看的主题
// 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
@@ -189,10 +225,13 @@ const BLOG = {
MUSIC_PLAYER_METING_LRC_TYPE:
process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_LRC_TYPE || '1', // 可选值: 3 | 1 | 00禁用 lrc 歌词1lrc 格式的字符串3lrc 文件 url
// ********挂件组件相关********
// ----> 评论互动 可同时开启多个支持 WALINE VALINE GISCUS CUSDIS UTTERRANCES GITALK
// twikoo
COMMENT_TWIKOO_ENV_ID: process.env.NEXT_PUBLIC_COMMENT_ENV_ID || '', // TWIKOO地址 腾讯云环境填 envIdVercel 环境域名地址(https://xxx.vercel.app)
COMMENT_TWIKOO_ENV_ID: process.env.NEXT_PUBLIC_COMMENT_ENV_ID || '', // TWIKOO后端地址 腾讯云环境填envIdVercel环境域名,教程: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:
@@ -233,11 +272,14 @@ const BLOG = {
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
@@ -265,8 +307,8 @@ const BLOG = {
// <---- 评论插件
// ----> 站点统计
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_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
@@ -281,10 +323,18 @@ const BLOG = {
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: {
@@ -311,18 +361,21 @@ const BLOG = {
icon: process.env.NEXT_PUBLIC_NOTION_PROPERTY_ICON || 'icon'
},
// RSS
// 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 || 'VIXCITY BLOG', // 站点标题 被notion中的页面标题覆盖
HOME_BANNER_IMAGE:
HOME_BANNER_IMAGE:
process.env.NEXT_PUBLIC_HOME_BANNER_IMAGE || './bg_image.jpg', // 首页背景大图, 会被notion中的封面图覆盖若无封面图则会使用代码中的 /public/bg_image.jpg 文件
DESCRIPTION:
DESCRIPTION:
process.env.NEXT_PUBLIC_DESCRIPTION || '我的小破博客', // 站点描述被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, // 文章图片是否自动添加阴影

View 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
View 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>
}

View File

@@ -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>
)

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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`} />}

View 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 >
)
}

View File

@@ -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

View File

@@ -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

View File

@@ -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>
}

View 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

View File

@@ -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
}

View File

@@ -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
View 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>
)
}

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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
View 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
View 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>}
</>)
}

View File

@@ -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
View 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
View 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)
}
}

View File

@@ -1,7 +0,0 @@
import dynamic from 'next/dynamic'
const MusicPlayer = dynamic(
() => import('@/components/Player'),
{ ssr: false }
)
export default MusicPlayer

View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

@@ -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,

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -14,15 +14,15 @@ const ShareBar = ({ post }) => {
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
} />
<ShareButtons shareUrl={shareUrl} title={post.title} image={post.pageCover} body={
post?.title +
' | ' +
BLOG.TITLE +
' ' +
shareUrl +
' ' +
post?.summary
} />
</div>
</div>
}

View File

@@ -1,7 +1,7 @@
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
import copy from 'copy-to-clipboard'
import QRCode from 'qrcode.react'
import dynamic from 'next/dynamic'
import { useState } from 'react'
import {
@@ -49,6 +49,13 @@ import {
HatenaIcon
} from 'react-share'
const QRCode = dynamic(
() => {
return import('qrcode.react')
},
{ ssr: false }
)
/**
* @author https://github.com/txs
* @param {*} param0
@@ -334,9 +341,9 @@ const ShareButtons = ({ shareUrl, title, body, image }) => {
}
if (singleService === 'qq') {
return <button key={singleService} className='cursor-pointer bg-blue-600 text-white rounded-full mx-1'>
<div target='_blank' rel='noreferrer' href={`http://connect.qq.com/widget/shareqq/index.html?url=${shareUrl}&sharesource=qzone&title=${title}&desc=${body}`} >
<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' />
</div>
</a>
</button>
}
if (singleService === 'wechat') {

View File

@@ -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')
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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, // 腾讯云环境填 envIdVercel 环境填地址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-guangzhouVercel 环境不填
// 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, // 腾讯云环境填 envIdVercel 环境填地址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-guangzhouVercel 环境不填
// path: location.pathname, // 用于区分不同文章的自定义 js 路径,如果您的文章路径不是 location.pathname需传此参数
})
} catch (error) {
console.error('twikoo 加载失败', error)
}
}
useEffect(() => {
loadTwikoo()
}, [])
return (
<div id="twikoo"></div>
<div id="twikoo"></div>
)
}

View 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

View 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

View File

@@ -0,0 +1,12 @@
/**
* 显示最近评论 TODO
* @returns {JSX.Element}
* @constructor
*/
const TwikooRecentComments = (props) => {
return null
}
export default TwikooRecentComments

66
components/VConsole.js Normal file
View 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

View File

@@ -1,3 +0,0 @@
import { Valine } from 'react-valine'
export default Valine

View File

@@ -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
View 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>&nbsp;{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标签和&nbsp;之类的特殊符合
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
}

View File

@@ -4,6 +4,7 @@
"paths": {
"@/*": ["./*"],
"@/components/*": ["components/*"],
"@/theme/*": ["theme/*"],
"@/data/*": ["data/*"],
"@/lib/*": ["lib/*"],
"@/styles/*": ["styles/*"]

42
lib/algolia.js Normal file
View 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 }

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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
View 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: '上に戻る'
}
}

View File

@@ -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,7 +21,7 @@ export default {
COMMON: {
MORE: '更多',
NO_MORE: '没有更多了',
LATEST_POSTS: '最新文章',
LATEST_POSTS: '最新发布',
TAGS: '标签',
NO_TAG: 'NoTag',
CATEGORY: '分类',
@@ -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
View 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
View File

View 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 => {

View File

@@ -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

View File

@@ -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 => {

View File

@@ -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)
}
}

View File

@@ -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)
@@ -68,12 +69,12 @@ export async function getNotionPageData({ pageId, from }) {
console.log('[缓存]:', `from:${from}`, `root-page-id:${pageId}`)
return data
}
const pageRecordMap = await getDataBaseInfoByNotionAPI({ 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,6 +212,32 @@ 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|*>}
@@ -191,30 +256,10 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
rawMetadata?.type !== 'collection_view_page' && rawMetadata?.type !== 'collection_view'
) {
console.error(`pageId "${pageId}" is not a database`)
return {
notice: null,
siteInfo: getBlogInfo({}),
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: [] } }],
collection: [],
collectionQuery: {},
collectionId: null,
collectionView: {},
viewIds: [],
block: {},
schema: {},
tagOptions: [],
categoryOptions: [],
rawMetadata: {},
customNav: [],
customMenu: [],
postCount: 1,
pageIds: [],
latestPosts: []
}
return EmptyData(pageId)
}
const collection = Object.values(pageRecordMap.collection)[0]?.value || {}
const siteInfo = getBlogInfo({ collection, block })
const siteInfo = getSiteInfo({ collection, block })
const collectionId = rawMetadata?.collection_id
const collectionQuery = pageRecordMap.collection_query
const collectionView = pageRecordMap.collection_view
@@ -242,7 +287,7 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
let postCount = 0
// 查找所有的Post和Page
const allPages = collectionData.filter(post => {
if (post.type === 'Post' && post.status === 'Published') {
if (post?.type === 'Post' && post.status === 'Published') {
postCount++
}
return post && post?.slug &&
@@ -253,24 +298,25 @@ async function getDataBaseInfoByNotionAPI({ 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 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,

View File

@@ -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)}`
}

View File

@@ -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

View File

@@ -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 }

View File

@@ -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)
})
}

View File

@@ -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 => {

View File

@@ -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

View File

@@ -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
}
})

View File

@@ -1,6 +1,6 @@
{
"name": "notion-next",
"version": "3.13.5",
"version": "4.0.6",
"homepage": "https://github.com/tangly1024/NotionNext.git",
"license": "MIT",
"repository": {
@@ -17,26 +17,25 @@
"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",
"@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.3.1",
"notion-client": "6.15.6",
@@ -54,11 +53,8 @@
"react-notion-x": "6.16.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",
@@ -72,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": {

View File

@@ -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
View 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

View File

@@ -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).filter(key => post.blockMap.block[key]?.value.parent_id === post.id)
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 || []

View File

@@ -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>
)
}

View File

@@ -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
View 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' })
}
}

View File

@@ -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 {

View File

@@ -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 }

View File

@@ -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 => {

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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')
// 处理分页

View File

@@ -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

View File

@@ -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

View File

@@ -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']
})

View File

@@ -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'
}

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View File

@@ -1,2 +1,8 @@
/* 静态文件导入 自定义样式*/
#theme-fukasawa .sideLeft hr{
opacity: .04;
}
.fa-info:before {
content: "\f05a";
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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%);
}

View File

@@ -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%);
}

View File

@@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Some files were not shown because too many files have changed in this diff Show More