mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-05-14 07:26:52 +00:00
Merge branch 'main' into pr/emengweb/1575
This commit is contained in:
174
.env.local
174
.env.local
@@ -1,2 +1,174 @@
|
||||
# 环境变量 @see https://www.nextjs.cn/docs/basic-features/environment-variables
|
||||
NEXT_PUBLIC_VERSION=4.0.18
|
||||
NEXT_PUBLIC_VERSION=4.2.0
|
||||
|
||||
|
||||
# 可在此添加环境变量,去掉最左边的(# )注释即可
|
||||
# Notion页面ID,必须
|
||||
# NOTION_PAGE_ID=
|
||||
|
||||
# 非必须
|
||||
# NEXT_PUBLIC_PSEUDO_STATIC=
|
||||
# NEXT_PUBLIC_REVALIDATE_SECOND=
|
||||
# NEXT_PUBLIC_THEME=
|
||||
# NEXT_PUBLIC_THEME_SWITCH=
|
||||
# NEXT_PUBLIC_LANG=
|
||||
# NEXT_PUBLIC_APPEARANCE=
|
||||
# NEXT_PUBLIC_APPEARANCE_DARK_TIME=
|
||||
# NEXT_PUBLIC_GREETING_WORDS=
|
||||
# NEXT_PUBLIC_CUSTOM_MENU=
|
||||
# NEXT_PUBLIC_AUTHOR=
|
||||
# NEXT_PUBLIC_BIO=
|
||||
# NEXT_PUBLIC_LINK=
|
||||
# NEXT_PUBLIC_KEYWORD=
|
||||
# NEXT_PUBLIC_CONTACT_EMAIL=
|
||||
# NEXT_PUBLIC_CONTACT_WEIBO=
|
||||
# NEXT_PUBLIC_CONTACT_TWITTER=
|
||||
# NEXT_PUBLIC_CONTACT_GITHUB=
|
||||
# NEXT_PUBLIC_CONTACT_TELEGRAM=
|
||||
# NEXT_PUBLIC_CONTACT_LINKEDIN=
|
||||
# NEXT_PUBLIC_CONTACT_INSTAGRAM=
|
||||
# NEXT_PUBLIC_CONTACT_BILIBILI=
|
||||
# NEXT_PUBLIC_CONTACT_YOUTUBE=
|
||||
# NEXT_PUBLIC_FAVICON=
|
||||
# NEXT_PUBLIC_FONT_STYLE=
|
||||
# NEXT_PUBLIC_FONT_URL=
|
||||
# NEXT_PUBLIC_FONT_SANS=
|
||||
# NEXT_PUBLIC_FONT_SERIF=
|
||||
# NEXT_PUBLIC_FONT_AWESOME_PATH=
|
||||
# NEXT_PUBLIC_PRISM_THEME_PREFIX_PATH=
|
||||
# NEXT_PUBLIC_PRISM_THEME_SWITCH=
|
||||
# NEXT_PUBLIC_PRISM_THEME_LIGHT_PATH=
|
||||
# NEXT_PUBLIC_PRISM_THEME_DARK_PATH=
|
||||
# NEXT_PUBLIC_CODE_MAC_BAR=
|
||||
# NEXT_PUBLIC_CODE_LINE_NUMBERS=
|
||||
# NEXT_PUBLIC_CODE_COLLAPSE=
|
||||
# NEXT_PUBLIC_CODE_COLLAPSE_EXPAND_DEFAULT=
|
||||
# NEXT_PUBLIC_MERMAID_CDN=
|
||||
# NEXT_PUBLIC_QR_CODE_CDN=
|
||||
# NEXT_PUBLIC_BACKGROUND_LIGHT=
|
||||
# NEXT_PUBLIC_BACKGROUND_DARK=
|
||||
# NEXT_PUBLIC_SUB_PATH=
|
||||
# NEXT_PUBLIC_POST_SHARE_BAR=
|
||||
# NEXT_PUBLIC_POST_SHARE_SERVICES=
|
||||
# NEXT_PUBLIC_POST_URL_PREFIX=
|
||||
# NEXT_PUBLIC_POST_LIST_STYLE=
|
||||
# NEXT_PUBLIC_POST_PREVIEW=
|
||||
# NEXT_PUBLIC_POST_RECOMMEND_COUNT=
|
||||
# NEXT_PUBLIC_POSTS_PER_PAGE=
|
||||
# NEXT_PUBLIC_POST_SORT_BY=
|
||||
# NEXT_PUBLIC_ALGOLIA_APP_ID=
|
||||
# ALGOLIA_ADMIN_APP_KEY=
|
||||
# NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_APP_KEY=
|
||||
# NEXT_PUBLIC_ALGOLIA_INDEX=
|
||||
# NEXT_PUBLIC_PREVIEW_CATEGORY_COUNT=
|
||||
# NEXT_PUBLIC_PREVIEW_TAG_COUNT=
|
||||
# NEXT_PUBLIC_POST_DISABLE_GALLERY_CLICK=
|
||||
# NEXT_PUBLIC_FIREWORKS=
|
||||
# NEXT_PUBLIC_FIREWORKS_COLOR=
|
||||
# NEXT_PUBLIC_SAKURA=
|
||||
# NEXT_PUBLIC_NEST=
|
||||
# NEXT_PUBLIC_FLUTTERINGRIBBON=
|
||||
# NEXT_PUBLIC_RIBBON=
|
||||
# NEXT_PUBLIC_STARRY_SKY=
|
||||
# NEXT_PUBLIC_CHATBASE_ID=
|
||||
# NEXT_PUBLIC_WEB_WHIZ_ENABLED=
|
||||
# NEXT_PUBLIC_WEB_WHIZ_BASE_URL=
|
||||
# NEXT_PUBLIC_WEB_WHIZ_CHAT_BOT_ID=
|
||||
# NEXT_PUBLIC_WIDGET_PET=
|
||||
# NEXT_PUBLIC_WIDGET_PET_LINK=
|
||||
# NEXT_PUBLIC_WIDGET_PET_SWITCH_THEME=
|
||||
# NEXT_PUBLIC_MUSIC_PLAYER=
|
||||
# NEXT_PUBLIC_MUSIC_PLAYER_VISIBLE=
|
||||
# NEXT_PUBLIC_MUSIC_PLAYER_AUTO_PLAY=
|
||||
# NEXT_PUBLIC_MUSIC_PLAYER_LRC_TYPE=
|
||||
# NEXT_PUBLIC_MUSIC_PLAYER_CDN_URL=
|
||||
# NEXT_PUBLIC_MUSIC_PLAYER_ORDER=
|
||||
# NEXT_PUBLIC_MUSIC_PLAYER_AUDIO_LIST=
|
||||
# NEXT_PUBLIC_MUSIC_PLAYER_METING=
|
||||
# NEXT_PUBLIC_MUSIC_PLAYER_METING_SERVER=
|
||||
# NEXT_PUBLIC_MUSIC_PLAYER_METING_ID=
|
||||
# NEXT_PUBLIC_MUSIC_PLAYER_METING_LRC_TYPE=
|
||||
# NEXT_PUBLIC_COMMENT_ARTALK_SERVER=
|
||||
# NEXT_PUBLIC_COMMENT_ARTALK_JS=
|
||||
# NEXT_PUBLIC_COMMENT_ARTALK_CSS=
|
||||
# NEXT_PUBLIC_COMMENT_ENV_ID=
|
||||
# NEXT_PUBLIC_COMMENT_TWIKOO_COUNT_ENABLE=
|
||||
# NEXT_PUBLIC_COMMENT_TWIKOO_CDN_URL=
|
||||
# NEXT_PUBLIC_COMMENT_UTTERRANCES_REPO=
|
||||
# NEXT_PUBLIC_COMMENT_GISCUS_REPO=
|
||||
# NEXT_PUBLIC_COMMENT_GISCUS_REPO_ID=
|
||||
# NEXT_PUBLIC_COMMENT_GISCUS_CATEGORY_ID=
|
||||
# NEXT_PUBLIC_COMMENT_GISCUS_MAPPING=
|
||||
# NEXT_PUBLIC_COMMENT_GISCUS_REACTIONS_ENABLED=
|
||||
# NEXT_PUBLIC_COMMENT_GISCUS_EMIT_METADATA=
|
||||
# NEXT_PUBLIC_COMMENT_GISCUS_INPUT_POSITION=
|
||||
# NEXT_PUBLIC_COMMENT_GISCUS_LANG=
|
||||
# NEXT_PUBLIC_COMMENT_GISCUS_LOADING=
|
||||
# NEXT_PUBLIC_COMMENT_GISCUS_CROSSORIGIN=
|
||||
# NEXT_PUBLIC_COMMENT_CUSDIS_APP_ID=
|
||||
# NEXT_PUBLIC_COMMENT_CUSDIS_HOST=
|
||||
# NEXT_PUBLIC_COMMENT_CUSDIS_SCRIPT_SRC=
|
||||
# NEXT_PUBLIC_COMMENT_GITALK_REPO=
|
||||
# NEXT_PUBLIC_COMMENT_GITALK_OWNER=
|
||||
# NEXT_PUBLIC_COMMENT_GITALK_ADMIN=
|
||||
# NEXT_PUBLIC_COMMENT_GITALK_CLIENT_ID=
|
||||
# NEXT_PUBLIC_COMMENT_GITALK_CLIENT_SECRET=
|
||||
# NEXT_PUBLIC_COMMENT_GITALK_JS_CDN_URL=
|
||||
# NEXT_PUBLIC_COMMENT_GITALK_CSS_CDN_URL=
|
||||
# NEXT_PUBLIC_COMMENT_GITTER_ROOM=
|
||||
# NEXT_PUBLIC_COMMENT_DAO_VOICE_ID=
|
||||
# NEXT_PUBLIC_COMMENT_TIDIO_ID=
|
||||
# NEXT_PUBLIC_VALINE_CDN=
|
||||
# NEXT_PUBLIC_VALINE_ID=
|
||||
# NEXT_PUBLIC_VALINE_KEY=
|
||||
# NEXT_PUBLIC_VALINE_SERVER_URLS=
|
||||
# NEXT_PUBLIC_VALINE_PLACEHOLDER=
|
||||
# NEXT_PUBLIC_WALINE_SERVER_URL=
|
||||
# NEXT_PUBLIC_WALINE_RECENT=
|
||||
# NEXT_PUBLIC_WEBMENTION_ENABLE=
|
||||
# NEXT_PUBLIC_WEBMENTION_AUTH=
|
||||
# NEXT_PUBLIC_WEBMENTION_HOSTNAME=
|
||||
# NEXT_PUBLIC_TWITTER_USERNAME=
|
||||
# NEXT_PUBLIC_WEBMENTION_TOKEN=
|
||||
# NEXT_PUBLIC_ANALYTICS_VERCEL=
|
||||
# NEXT_PUBLIC_ANALYTICS_BUSUANZI_ENABLE=
|
||||
# NEXT_PUBLIC_ANALYTICS_BAIDU_ID=
|
||||
# NEXT_PUBLIC_ANALYTICS_CNZZ_ID=
|
||||
# NEXT_PUBLIC_ANALYTICS_GOOGLE_ID=
|
||||
# NEXT_PUBLIC_ANALYTICS_ACKEE_TRACKER=
|
||||
# NEXT_PUBLIC_ANALYTICS_ACKEE_DATA_SERVER=
|
||||
# NEXT_PUBLIC_ANALYTICS_ACKEE_DOMAIN_ID=
|
||||
# NEXT_PUBLIC_SEO_GOOGLE_SITE_VERIFICATION=
|
||||
# NEXT_PUBLIC_SEO_BAIDU_SITE_VERIFICATION=
|
||||
# NEXT_PUBLIC_ADSENSE_GOOGLE_ID=
|
||||
# NEXT_PUBLIC_ADSENSE_GOOGLE_TEST=
|
||||
# NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_IN_ARTICLE=
|
||||
# NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_FLOW=
|
||||
# NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_NATIVE=
|
||||
# NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_AUTO=
|
||||
# NEXT_PUBLIC_WWAD_ID=
|
||||
# NEXT_PUBLIC_WWADS_AD_BLOCK_DETECT=
|
||||
# NEXT_PUBLIC_NOTION_PROPERTY_PASSWORD=
|
||||
# NEXT_PUBLIC_NOTION_PROPERTY_TYPE=
|
||||
# NEXT_PUBLIC_NOTION_PROPERTY_TYPE_POST=
|
||||
# NEXT_PUBLIC_NOTION_PROPERTY_TYPE_PAGE=
|
||||
# NEXT_PUBLIC_NOTION_PROPERTY_TYPE_NOTICE=
|
||||
# NEXT_PUBLIC_NOTION_PROPERTY_TYPE_MENU=
|
||||
# NEXT_PUBLIC_NOTION_PROPERTY_TYPE_SUB_MENU=
|
||||
# NEXT_PUBLIC_NOTION_PROPERTY_TITLE=
|
||||
# NEXT_PUBLIC_NOTION_PROPERTY_STATUS=
|
||||
# NEXT_PUBLIC_NOTION_PROPERTY_STATUS_PUBLISH=
|
||||
# NEXT_PUBLIC_NOTION_PROPERTY_STATUS_INVISIBLE=
|
||||
# NEXT_PUBLIC_NOTION_PROPERTY_SUMMARY=
|
||||
# NEXT_PUBLIC_NOTION_PROPERTY_SLUG=
|
||||
# NEXT_PUBLIC_NOTION_PROPERTY_CATEGORY=
|
||||
# NEXT_PUBLIC_NOTION_PROPERTY_DATE=
|
||||
# NEXT_PUBLIC_NOTION_PROPERTY_TAGS=
|
||||
# NEXT_PUBLIC_NOTION_PROPERTY_ICON=
|
||||
# NEXT_PUBLIC_ENABLE_RSS=
|
||||
# MAILCHIMP_LIST_ID=
|
||||
# MAILCHIMP_API_KEY=
|
||||
# NEXT_PUBLIC_DEBUG=
|
||||
# ENABLE_CACHE=
|
||||
# VERCEL_ENV=
|
||||
# NEXT_PUBLIC_VERSION=
|
||||
|
||||
18
.github/ISSUE_TEMPLATE/bug_report.md
vendored
18
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -12,23 +12,23 @@ assignees: tangly1024
|
||||
-->
|
||||
|
||||
**描述bug**
|
||||
简单说明bug的现象、相关的错误提示、日志等
|
||||
【此项必填】简单说明bug的现象、相关的错误提示、日志等
|
||||
|
||||
**复现步骤**
|
||||
出现这个bug的操作步骤
|
||||
【此项必填】出现这个bug的操作步骤
|
||||
|
||||
**期望的正常结果**
|
||||
希望按这个步骤,正常操作结果是什么
|
||||
【此项必填】希望按这个步骤,正常操作结果是什么
|
||||
|
||||
**截图**
|
||||
相关的页面,应该的结果
|
||||
【可选】相关的页面,应该的结果
|
||||
|
||||
**环境**
|
||||
|
||||
- 操作系统: [例如. iOS, Android, macOS, windows]
|
||||
- 浏览器 [例如. chrome, safari, firefox]
|
||||
- NotionNext版本 [e.g. 3.13.6]
|
||||
- 主题 [例如. hexo]
|
||||
- 【必填】NotionNext版本 [例如. 4.0.18]
|
||||
- 【必填】主题 [例如. hexo]
|
||||
- 【可选】操作系统: [例如. iOS, Android, macOS, windows]
|
||||
- 【可选】浏览器 [例如. chrome, safari, firefox]
|
||||
|
||||
**补充说明**
|
||||
与问题相关的其它说明
|
||||
【可选】与问题相关的其它说明
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: Feature request (新特性建议)
|
||||
about: Suggest an idea for Nobelium.
|
||||
about: Suggest an idea for Notion Next.
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: tangly1024
|
||||
|
||||
@@ -201,6 +201,9 @@
|
||||
- **图标**: [Fontawesome](https://fontawesome.com/v6/icons/)
|
||||
|
||||
|
||||
## 🔗 友情链接
|
||||
- [Elog](https://github.com/LetTTGACO/elog) Markdown 批量导出工具、开放式跨平台博客解决方案,随意组合写作平台(语雀/Notion/FlowUs/飞书)和博客平台(Hexo/Vitepress/Halo/Confluence/WordPress等)
|
||||
|
||||
## License
|
||||
|
||||
The MIT License.
|
||||
|
||||
@@ -5,10 +5,10 @@ const BLOG = {
|
||||
process.env.NOTION_PAGE_ID || '02ab3b8678004aa69e9e415905ef32a5',
|
||||
PSEUDO_STATIC: process.env.NEXT_PUBLIC_PSEUDO_STATIC || false, // 伪静态路径,开启后所有文章URL都以 .html 结尾。
|
||||
NEXT_REVALIDATE_SECOND: process.env.NEXT_PUBLIC_REVALIDATE_SECOND || 5, // 更新内容缓存间隔 单位(秒);即每个页面有5秒的纯静态期、此期间无论多少次访问都不会抓取notion数据;调大该值有助于节省Vercel资源、同时提升访问速率,但也会使文章更新有延迟。
|
||||
THEME: process.env.NEXT_PUBLIC_THEME || 'hexo', // 当前主题,在themes文件夹下可找到所有支持的主题;主题名称就是文件夹名,例如 example,fukasawa,gitbook,heo,hexo,landing,matery,medium,next,nobelium,plog,simple
|
||||
THEME: process.env.NEXT_PUBLIC_THEME || 'simple', // 当前主题,在themes文件夹下可找到所有支持的主题;主题名称就是文件夹名,例如 example,fukasawa,gitbook,heo,hexo,landing,matery,medium,next,nobelium,plog,simple
|
||||
THEME_SWITCH: process.env.NEXT_PUBLIC_THEME_SWITCH || false, // 是否显示切换主题按钮
|
||||
LANG: process.env.NEXT_PUBLIC_LANG || 'zh-CN', // e.g 'zh-CN','en-US' see /lib/lang.js for more.
|
||||
SINCE: 2021, // e.g if leave this empty, current year will be used.
|
||||
SINCE: process.env.NEXT_SINCE || 2021, // e.g if leave this empty, current year will be used.
|
||||
APPEARANCE: process.env.NEXT_PUBLIC_APPEARANCE || 'light', // ['light', 'dark', 'auto'], // light 日间模式 , dark夜间模式, auto根据时间和主题自动夜间模式
|
||||
APPEARANCE_DARK_TIME: process.env.NEXT_PUBLIC_APPEARANCE_DARK_TIME || [18, 6], // 夜间模式起至时间,false时关闭根据时间自动切换夜间模式
|
||||
|
||||
@@ -37,6 +37,11 @@ const BLOG = {
|
||||
|
||||
BLOG_FAVICON: process.env.NEXT_PUBLIC_FAVICON || '/favicon.ico', // blog favicon 配置, 默认使用 /public/favicon.ico,支持在线图片,如 https://img.imesong.com/favicon.png
|
||||
|
||||
RANDOM_IMAGE_URL: process.env.NEXT_PUBLIC_RANDOM_IMAGE_URL || '', // 随机图片API,如果未配置下面的关键字,主页封面,头像,文章封面图都会被替换为随机图片
|
||||
RANDOM_IMAGE_REPLACE_TEXT: process.env.NEXT_PUBLIC_RANDOM_IMAGE_NOT_REPLACE_TEXT || 'images.unsplash.com', // 触发替换图片的 url 关键字(多个支持用英文逗号分开),只有图片地址中包含此关键字才会替换为上方随机图片url
|
||||
// eg: images.unsplash.com(notion图床的所有图片都会替换),如果你在 notion 里已经添加了一个随机图片 url,恰巧那个服务跑路或者挂掉,想一键切换所有配图可以将该 url 配置在这里
|
||||
// 默认下会将你上传到 notion的主页封面图和头像也给替换,建议将主页封面图和头像放在其他图床,在 notion 里配置 link 即可。
|
||||
|
||||
// START ************网站字体*****************
|
||||
|
||||
FONT_STYLE: process.env.NEXT_PUBLIC_FONT_STYLE || 'font-sans', // ['font-serif','font-sans'] 两种可选,分别是衬线和无衬线: 参考 https://www.jianshu.com/p/55e410bd2115
|
||||
@@ -85,13 +90,14 @@ const BLOG = {
|
||||
// END ************网站字体*****************
|
||||
CAN_COPY: process.env.NEXT_PUBLIC_CAN_COPY || true, // 是否允许复制页面内容 默认允许,如果设置为false、则全栈禁止复制内容。
|
||||
CUSTOM_RIGHT_CLICK_CONTEXT_MENU: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU || true, // 自定义右键菜单,覆盖系统菜单
|
||||
CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH || true,
|
||||
|
||||
// 自定义外部脚本,外部样式
|
||||
CUSTOM_EXTERNAL_JS: [''], // e.g. ['http://xx.com/script.js','http://xx.com/script.js']
|
||||
CUSTOM_EXTERNAL_CSS: [''], // e.g. ['http://xx.com/style.css','http://xx.com/style.css']
|
||||
|
||||
// 侧栏布局 是否反转(左变右,右变左) 已支持主题: hexo next medium fukasawa example
|
||||
LAYOUT_SIDEBAR_REVERSE: false,
|
||||
LAYOUT_SIDEBAR_REVERSE: process.env.NEXT_PUBLIC_LAYOUT_SIDEBAR_REVERSE || false,
|
||||
|
||||
// 一个小插件展示你的facebook fan page~ @see https://tw.andys.pro/article/add-facebook-fanpage-notionnext
|
||||
FACEBOOK_PAGE_TITLE: process.env.NEXT_PUBLIC_FACEBOOK_PAGE_TITLE || null, // 邊欄 Facebook Page widget 的標題欄,填''則無標題欄 e.g FACEBOOK 粉絲團'
|
||||
@@ -179,13 +185,20 @@ const BLOG = {
|
||||
STARRY_SKY: process.env.NEXT_PUBLIC_STARRY_SKY || false, // 开关
|
||||
|
||||
// ********挂件组件相关********
|
||||
// AI 文章摘要生成 @see https://docs_s.tianli0.top/
|
||||
TianliGPT_CSS: process.env.NEXT_PUBLIC_TIANLI_GPT_CSS || 'https://cdn1.tianli0.top/gh/zhheo/Post-Abstract-AI@0.15.2/tianli_gpt.css',
|
||||
TianliGPT_JS: process.env.NEXT_PUBLIC_TIANLI_GPT_JS || 'https://cdn1.tianli0.top/gh/zhheo/Post-Abstract-AI@0.15.2/tianli_gpt.js',
|
||||
TianliGPT_KEY: process.env.NEXT_PUBLIC_TIANLI_GPT_KEY || '',
|
||||
|
||||
// Chatbase 是否显示chatbase机器人 https://www.chatbase.co/
|
||||
CHATBASE_ID: process.env.NEXT_PUBLIC_CHATBASE_ID || null,
|
||||
// WebwhizAI 机器人 @see https://github.com/webwhiz-ai/webwhiz
|
||||
WEB_WHIZ_ENABLED: process.env.NEXT_PUBLIC_WEB_WHIZ_ENABLED || false, // 是否显示
|
||||
WEB_WHIZ_BASE_URL: process.env.NEXT_PUBLIC_WEB_WHIZ_BASE_URL || 'https://api.webwhiz.ai', // 可以自建服务器
|
||||
WEB_WHIZ_CHAT_BOT_ID: process.env.NEXT_PUBLIC_WEB_WHIZ_CHAT_BOT_ID || null, // 在后台获取ID
|
||||
|
||||
DIFY_CHATBOT_ENABLED: process.env.NEXT_PUBLIC_DIFY_CHATBOT_ENABLED || false,
|
||||
DIFY_CHATBOT_BASE_URL: process.env.NEXT_PUBLIC_DIFY_CHATBOT_BASE_URL || '',
|
||||
DIFY_CHATBOT_TOKEN: process.env.NEXT_PUBLIC_DIFY_CHATBOT_TOKEN || '',
|
||||
// 悬浮挂件
|
||||
WIDGET_PET: process.env.NEXT_PUBLIC_WIDGET_PET || true, // 是否显示宠物挂件
|
||||
WIDGET_PET_LINK:
|
||||
@@ -231,6 +244,8 @@ const BLOG = {
|
||||
// ********挂件组件相关********
|
||||
// ----> 评论互动 可同时开启多个支持 WALINE VALINE GISCUS CUSDIS UTTERRANCES GITALK
|
||||
|
||||
COMMENT_HIDE_SINGLE_TAB: process.env.NEXT_PUBLIC_COMMENT_HIDE_SINGLE_TAB || false, //Whether hide the tab when there's no tabs. 只有一个评论组件时是否隐藏切换组件的标签页
|
||||
|
||||
// artalk 评论插件
|
||||
COMMENT_ARTALK_SERVER: process.env.NEXT_PUBLIC_COMMENT_ARTALK_SERVER || '', // ArtalkServert后端地址 https://artalk.js.org/guide/deploy.html
|
||||
COMMENT_ARTALK_JS: process.env.NEXT_PUBLIC_COMMENT_ARTALK_JS || 'https://cdnjs.cloudflare.com/ajax/libs/artalk/2.5.5/Artalk.js', // ArtalkServert js cdn
|
||||
@@ -239,7 +254,7 @@ const BLOG = {
|
||||
// twikoo
|
||||
COMMENT_TWIKOO_ENV_ID: process.env.NEXT_PUBLIC_COMMENT_ENV_ID || '', // TWIKOO后端地址 腾讯云环境填envId;Vercel环境填域名,教程:https://tangly1024.com/article/notionnext-twikoo
|
||||
COMMENT_TWIKOO_COUNT_ENABLE: process.env.NEXT_PUBLIC_COMMENT_TWIKOO_COUNT_ENABLE || false, // 博客列表是否显示评论数
|
||||
COMMENT_TWIKOO_CDN_URL: process.env.NEXT_PUBLIC_COMMENT_TWIKOO_CDN_URL || 'https://cdn.staticfile.org/twikoo/1.6.16/twikoo.min.js', // twikoo客户端cdn
|
||||
COMMENT_TWIKOO_CDN_URL: process.env.NEXT_PUBLIC_COMMENT_TWIKOO_CDN_URL || 'https://cdn.staticfile.org/twikoo/1.6.17/twikoo.min.js', // twikoo客户端cdn
|
||||
|
||||
// utterance
|
||||
COMMENT_UTTERRANCES_REPO:
|
||||
@@ -304,13 +319,11 @@ const BLOG = {
|
||||
// HOSTNAME: Webmention绑定之网域,通常即为本站网址
|
||||
// TWITTER_USERNAME: 评论显示区域需要的资讯
|
||||
// TOKEN: Webmention的API token
|
||||
COMMENT_WEBMENTION: {
|
||||
ENABLE: process.env.NEXT_PUBLIC_WEBMENTION_ENABLE || false,
|
||||
AUTH: process.env.NEXT_PUBLIC_WEBMENTION_AUTH || '',
|
||||
HOSTNAME: process.env.NEXT_PUBLIC_WEBMENTION_HOSTNAME || '',
|
||||
TWITTER_USERNAME: process.env.NEXT_PUBLIC_TWITTER_USERNAME || '',
|
||||
TOKEN: process.env.NEXT_PUBLIC_WEBMENTION_TOKEN || ''
|
||||
},
|
||||
COMMENT_WEBMENTION_ENABLE: process.env.NEXT_PUBLIC_WEBMENTION_ENABLE || false,
|
||||
COMMENT_WEBMENTION_AUTH: process.env.NEXT_PUBLIC_WEBMENTION_AUTH || '',
|
||||
COMMENT_WEBMENTION_HOSTNAME: process.env.NEXT_PUBLIC_WEBMENTION_HOSTNAME || '',
|
||||
COMMENT_WEBMENTION_TWITTER_USERNAME: process.env.NEXT_PUBLIC_TWITTER_USERNAME || '',
|
||||
COMMENT_WEBMENTION_TOKEN: process.env.NEXT_PUBLIC_WEBMENTION_TOKEN || '',
|
||||
|
||||
// <---- 评论插件
|
||||
|
||||
@@ -321,6 +334,13 @@ const BLOG = {
|
||||
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
|
||||
|
||||
// 51la 站点统计 https://www.51.la/
|
||||
ANALYTICS_51LA_ID: process.env.NEXT_PUBLIC_ANALYTICS_51LA_ID || '', // id,在51la后台获取 参阅 https://docs.tangly1024.com/article/notion-next-51-la
|
||||
ANALYTICS_51LA_CK: process.env.NEXT_PUBLIC_ANALYTICS_51LA_CK || '', // ck,在51la后台获取
|
||||
|
||||
// Matomo 网站统计
|
||||
MATOMO_HOST_URL: process.env.NEXT_PUBLIC_MATOMO_HOST_URL || '', // Matomo服务器地址,不带斜杠
|
||||
MATOMO_SITE_ID: process.env.NEXT_PUBLIC_MATOMO_SITE_ID || '', // Matomo网站ID
|
||||
// ACKEE网站访客统计工具
|
||||
ANALYTICS_ACKEE_TRACKER: process.env.NEXT_PUBLIC_ANALYTICS_ACKEE_TRACKER || '', // e.g 'https://ackee.tangly1024.com/tracker.js'
|
||||
ANALYTICS_ACKEE_DATA_SERVER: process.env.NEXT_PUBLIC_ANALYTICS_ACKEE_DATA_SERVER || '', // e.g https://ackee.tangly1024.com , don't end with a slash
|
||||
@@ -332,6 +352,9 @@ const BLOG = {
|
||||
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
|
||||
|
||||
// 微软 Clarity 站点分析
|
||||
CLARITY_ID: process.env.NEXT_PUBLIC_CLARITY_ID || null , // 只需要复制Clarity脚本中的ID部分,ID是一个十位的英文数字组合
|
||||
|
||||
// <---- 站点统计
|
||||
|
||||
// START---->营收相关
|
||||
@@ -392,12 +415,14 @@ const BLOG = {
|
||||
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, // 文章图片是否自动添加阴影
|
||||
IMG_COMPRESS_WIDTH: process.env.NEXT_PUBLIC_IMG_COMPRESS_WIDTH || 800, // Notion图片压缩宽度
|
||||
|
||||
// 开发相关
|
||||
NOTION_ACCESS_TOKEN: process.env.NOTION_ACCESS_TOKEN || '', // Useful if you prefer not to make your database public
|
||||
DEBUG: process.env.NEXT_PUBLIC_DEBUG || false, // 是否显示调试按钮
|
||||
ENABLE_CACHE: process.env.ENABLE_CACHE || process.env.npm_lifecycle_event === 'build', // 缓存在开发调试和打包过程中选择性开启,正式部署开启此功能意义不大。
|
||||
isProd: process.env.VERCEL_ENV === 'production', // distinguish between development and production environment (ref: https://vercel.com/docs/environment-variables#system-environment-variables) isProd: process.env.VERCEL_ENV === 'production' // distinguish between development and production environment (ref: https://vercel.com/docs/environment-variables#system-environment-variables)
|
||||
BUNDLE_ANALYZER: process.env.ANALYZE === 'true' || false, // 是否展示编译依赖内容与大小
|
||||
VERSION: process.env.NEXT_PUBLIC_VERSION // 版本号
|
||||
}
|
||||
|
||||
|
||||
12
components/AOSAnimation.js
Normal file
12
components/AOSAnimation.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import AOS from 'aos'
|
||||
import { isBrowser } from 'react-notion-x'
|
||||
|
||||
/**
|
||||
* 加载滚动动画
|
||||
* https://michalsnik.github.io/aos/
|
||||
*/
|
||||
export default function AOSAnimation() {
|
||||
if (isBrowser) {
|
||||
AOS.init()
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,47 @@
|
||||
'use strict'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import BLOG from '@/blog.config'
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
import { useRouter } from 'next/router'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
const Ackee = () => {
|
||||
const router = useRouter()
|
||||
const server = siteConfig('ANALYTICS_ACKEE_DATA_SERVER')
|
||||
const domainId = siteConfig('ANALYTICS_ACKEE_DOMAIN_ID')
|
||||
|
||||
// 或者使用其他依赖数组,根据需要执行 handleAckee
|
||||
useEffect(() => {
|
||||
handleAckeeCallback()
|
||||
}, [router])
|
||||
|
||||
// handleAckee 函数
|
||||
const handleAckeeCallback = () => {
|
||||
handleAckee(
|
||||
router.asPath,
|
||||
{
|
||||
server: BLOG.ANALYTICS_ACKEE_DATA_SERVER,
|
||||
domainId: BLOG.ANALYTICS_ACKEE_DOMAIN_ID
|
||||
server: server,
|
||||
domainId: domainId
|
||||
},
|
||||
{
|
||||
/*
|
||||
* Enable or disable tracking of personal data.
|
||||
* We recommend to ask the user for permission before turning this option on.
|
||||
*/
|
||||
* Enable or disable tracking of personal data.
|
||||
* We recommend to ask the user for permission before turning this option on.
|
||||
*/
|
||||
detailed: true,
|
||||
/*
|
||||
* Enable or disable tracking when on localhost.
|
||||
*/
|
||||
* Enable or disable tracking when on localhost.
|
||||
*/
|
||||
ignoreLocalhost: false,
|
||||
/*
|
||||
* Enable or disable the tracking of your own visits.
|
||||
* This is enabled by default, but should be turned off when using a wildcard Access-Control-Allow-Origin header.
|
||||
* Some browsers strictly block third-party cookies. The option won't have an impact when this is the case.
|
||||
*/
|
||||
* Enable or disable the tracking of your own visits.
|
||||
* This is enabled by default, but should be turned off when using a wildcard Access-Control-Allow-Origin header.
|
||||
* Some browsers strictly block third-party cookies. The option won't have an impact when this is the case.
|
||||
*/
|
||||
ignoreOwnVisits: false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 或者使用其他依赖数组,根据需要执行 handleAckee
|
||||
useEffect(() => {
|
||||
handleAckeeCallback()
|
||||
}, [router])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -53,8 +55,8 @@ export default Ackee
|
||||
* @param {Object} environment - Object containing the URL of the Ackee server and the domain id.
|
||||
* @param {?Object} options - Ackee options.
|
||||
*/
|
||||
const handleAckee = async function(pathname, environment, options = {}) {
|
||||
await loadExternalResource(BLOG.ANALYTICS_ACKEE_TRACKER, 'js')
|
||||
const handleAckee = async function (pathname, environment, options = {}) {
|
||||
await loadExternalResource(siteConfig('ANALYTICS_ACKEE_TRACKER'), 'js')
|
||||
const ackeeTracker = window.ackeeTracker
|
||||
|
||||
const instance = ackeeTracker?.create(environment.server, options)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useState, useImperativeHandle, useRef } from 'react'
|
||||
import BLOG from '@/blog.config'
|
||||
import algoliasearch from 'algoliasearch'
|
||||
import replaceSearchResult from '@/components/Mark'
|
||||
import Link from 'next/link'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import throttle from 'lodash/throttle'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
/**
|
||||
* 结合 Algolia 实现的弹出式搜索框
|
||||
@@ -31,8 +31,8 @@ export default function AlgoliaSearchModal({ cRef }) {
|
||||
}
|
||||
})
|
||||
|
||||
const client = algoliasearch(BLOG.ALGOLIA_APP_ID, BLOG.ALGOLIA_SEARCH_ONLY_APP_KEY)
|
||||
const index = client.initIndex(BLOG.ALGOLIA_INDEX)
|
||||
const client = algoliasearch(siteConfig('ALGOLIA_APP_ID'), siteConfig('ALGOLIA_SEARCH_ONLY_APP_KEY'))
|
||||
const index = client.initIndex(siteConfig('ALGOLIA_INDEX'))
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
@@ -97,48 +97,79 @@ export default function AlgoliaSearchModal({ cRef }) {
|
||||
setIsModalOpen(false)
|
||||
}
|
||||
|
||||
if (!BLOG.ALGOLIA_APP_ID) {
|
||||
if (!siteConfig('ALGOLIA_APP_ID')) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<div id='search-wrapper' className={`${isModalOpen ? 'opacity-100' : 'invisible opacity-0 pointer-events-none'} z-30 fixed h-screen w-screen left-0 top-0 mt-12 flex items-start 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 className="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) => handleInputChange(e)}
|
||||
className="text-black dark:text-gray-200 bg-gray-50 dark:bg-gray-600 outline-blue-500 w-full px-4 my-2 py-1 mb-4 border rounded-md" />
|
||||
|
||||
{/* 标签组 */}
|
||||
<div className='mb-4'>
|
||||
<TagGroups/>
|
||||
</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 text-black dark:text-gray-200">
|
||||
{result.title}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<Pagination totalPage={totalPage} page={page} switchPage={switchPage}/>
|
||||
<div>{totalHit > 0 && <div>共搜索到 {totalHit} 条结果,用时 {useTime} 毫秒</div> }</div>
|
||||
<div className='text-gray-600 mt-2'><span><i className="fa-brands fa-algolia"></i> Algolia 提供搜索服务</span> </div>
|
||||
</div>
|
||||
|
||||
{/* 遮罩 */}
|
||||
<div onClick={closeModal} className="z-30 fixed top-0 left-0 w-full h-full flex items-center justify-center glassmorphism" />
|
||||
|
||||
<div
|
||||
id="search-wrapper"
|
||||
className={`${
|
||||
isModalOpen ? 'opacity-100' : 'invisible opacity-0 pointer-events-none'
|
||||
} z-30 fixed h-screen w-screen left-0 top-0 mt-12 flex items-start 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
|
||||
className="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 => handleInputChange(e)}
|
||||
className="text-black dark:text-gray-200 bg-gray-50 dark:bg-gray-600 outline-blue-500 w-full px-4 my-2 py-1 mb-4 border rounded-md"
|
||||
/>
|
||||
|
||||
{/* 标签组 */}
|
||||
<div className="mb-4">
|
||||
<TagGroups />
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
{searchResults.map(result => (
|
||||
<li key={result.objectID} className="replace my-2">
|
||||
<a
|
||||
href={`${siteConfig('SUB_PATH', '')}/${result.slug}`}
|
||||
className="font-bold hover:text-blue-600 text-black dark:text-gray-200"
|
||||
>
|
||||
{result.title}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<Pagination totalPage={totalPage} page={page} switchPage={switchPage} />
|
||||
<div>
|
||||
{totalHit > 0 && (
|
||||
<div>
|
||||
共搜索到 {totalHit} 条结果,用时 {useTime} 毫秒
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-gray-600 mt-2">
|
||||
<span>
|
||||
<i className="fa-brands fa-algolia"></i> Algolia 提供搜索服务
|
||||
</span>{' '}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 遮罩 */}
|
||||
<div
|
||||
onClick={closeModal}
|
||||
className="z-30 fixed top-0 left-0 w-full h-full flex items-center justify-center glassmorphism"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
// import { loadExternalResource } from '@/lib/utils'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
/**
|
||||
* Giscus评论 @see https://giscus.app/zh-CN
|
||||
* Contribute by @txs https://github.com/txs/NotionNext/commit/1bf7179d0af21fb433e4c7773504f244998678cb
|
||||
* Artalk 自托管评论系统 @see https://artalk.js.org/
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
|
||||
const Artalk = ({ siteInfo }) => {
|
||||
const artalkCss = siteConfig('COMMENT_ARTALK_CSS')
|
||||
const artalkServer = siteConfig('COMMENT_ARTALK_SERVER')
|
||||
const artalkLocale = siteConfig('LANG')
|
||||
const site = siteConfig('TITLE')
|
||||
|
||||
useEffect(() => {
|
||||
loadExternalResource(BLOG.COMMENT_ARTALK_CSS, 'css')
|
||||
initArtalk()
|
||||
}, [])
|
||||
|
||||
const initArtalk = async () => {
|
||||
await loadExternalResource(artalkCss, 'css')
|
||||
window?.Artalk?.init({
|
||||
server: BLOG.COMMENT_ARTALK_SERVER, // 后端地址
|
||||
server: artalkServer, // 后端地址
|
||||
el: '#artalk', // 容器元素
|
||||
locale: BLOG.LANG,
|
||||
locale: artalkLocale,
|
||||
// pageKey: '/post/1', // 固定链接 (留空自动获取)
|
||||
// pageTitle: '关于引入 Artalk 的这档子事', // 页面标题 (留空自动获取)
|
||||
site: siteInfo?.title // 你的站点名
|
||||
site: site // 你的站点名
|
||||
})
|
||||
}, [])
|
||||
}
|
||||
return (
|
||||
<div id="artalk"></div>
|
||||
)
|
||||
|
||||
@@ -2,14 +2,14 @@ import busuanzi from '@/lib/busuanzi'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
// import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
let path = ''
|
||||
|
||||
export default function Busuanzi () {
|
||||
const { theme } = useGlobal()
|
||||
const Router = useRouter()
|
||||
Router.events.on('routeChangeComplete', (url, option) => {
|
||||
const router = useRouter()
|
||||
router.events.on('routeChangeComplete', (url, option) => {
|
||||
if (url !== path) {
|
||||
path = url
|
||||
busuanzi.fetch()
|
||||
@@ -17,7 +17,7 @@ export default function Busuanzi () {
|
||||
})
|
||||
|
||||
// 更换主题时更新
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (theme) {
|
||||
busuanzi.fetch()
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
/**
|
||||
* 这是一个嵌入组件,可以在任意位置全屏显示您的chat-base对话框
|
||||
* 暂时没有页面引用
|
||||
* 因为您可以直接用内嵌网页的方式放入您的notion中 https://www.chatbase.co/chatbot-iframe/${BLOG.CHATBASE_ID}
|
||||
* 因为您可以直接用内嵌网页的方式放入您的notion中 https://www.chatbase.co/chatbot-iframe/${siteConfig('CHATBASE_ID')}
|
||||
*/
|
||||
export default function ChatBase() {
|
||||
if (!BLOG.CHATBASE_ID) {
|
||||
if (!siteConfig('CHATBASE_ID')) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return <iframe
|
||||
src={`https://www.chatbase.co/chatbot-iframe/${BLOG.CHATBASE_ID}`}
|
||||
src={`https://www.chatbase.co/chatbot-iframe/${siteConfig('CHATBASE_ID')}`}
|
||||
width="100%"
|
||||
style={{ height: '100%', minHeight: '700px' }}
|
||||
frameborder="0"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import dynamic from 'next/dynamic'
|
||||
import Tabs from '@/components/Tabs'
|
||||
import { isBrowser } from '@/lib/utils'
|
||||
import { isBrowser, isSearchEngineBot } from '@/lib/utils'
|
||||
import { useRouter } from 'next/router'
|
||||
import Artalk from './Artalk'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
const WalineComponent = dynamic(
|
||||
() => {
|
||||
@@ -55,13 +55,6 @@ const ValineComponent = dynamic(() => import('@/components/ValineComponent'), {
|
||||
ssr: false
|
||||
})
|
||||
|
||||
/**
|
||||
* 是否有评论
|
||||
*/
|
||||
export const commentEnable = BLOG.COMMENT_TWIKOO_ENV_ID || BLOG.COMMENT_WALINE_SERVER_URL || BLOG.COMMENT_VALINE_APP_ID ||
|
||||
BLOG.COMMENT_GISCUS_REPO || BLOG.COMMENT_CUSDIS_APP_ID || BLOG.COMMENT_UTTERRANCES_REPO ||
|
||||
BLOG.COMMENT_GITALK_CLIENT_ID || BLOG.COMMENT_WEBMENTION.ENABLE
|
||||
|
||||
/**
|
||||
* 评论组件
|
||||
* @param {*} param0
|
||||
@@ -70,6 +63,21 @@ BLOG.COMMENT_GISCUS_REPO || BLOG.COMMENT_CUSDIS_APP_ID || BLOG.COMMENT_UTTERRANC
|
||||
const Comment = ({ siteInfo, frontMatter, className }) => {
|
||||
const router = useRouter()
|
||||
|
||||
const COMMENT_ARTALK_SERVER = siteConfig('COMMENT_ARTALK_SERVER')
|
||||
const COMMENT_TWIKOO_ENV_ID = siteConfig('COMMENT_TWIKOO_ENV_ID')
|
||||
const COMMENT_WALINE_SERVER_URL = siteConfig('COMMENT_WALINE_SERVER_URL')
|
||||
const COMMENT_VALINE_APP_ID = siteConfig('COMMENT_VALINE_APP_ID')
|
||||
const COMMENT_GISCUS_REPO = siteConfig('COMMENT_GISCUS_REPO')
|
||||
const COMMENT_CUSDIS_APP_ID = siteConfig('COMMENT_CUSDIS_APP_ID')
|
||||
const COMMENT_UTTERRANCES_REPO = siteConfig('COMMENT_UTTERRANCES_REPO')
|
||||
const COMMENT_GITALK_CLIENT_ID = siteConfig('COMMENT_GITALK_CLIENT_ID')
|
||||
const COMMENT_WEBMENTION_ENABLE = siteConfig('COMMENT_WEBMENTION_ENABLE')
|
||||
|
||||
if (isSearchEngineBot()) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 当连接中有特殊参数时跳转到评论区
|
||||
if (isBrowser && ('giscus' in router.query || router.query.target === 'comment')) {
|
||||
setTimeout(() => {
|
||||
const url = router.asPath.replace('?target=comment', '')
|
||||
@@ -83,47 +91,47 @@ const Comment = ({ siteInfo, frontMatter, className }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={frontMatter?.id} id='comment' className={`comment mt-5 text-gray-800 dark:text-gray-300 ${className || ''}`}>
|
||||
<Tabs>
|
||||
{BLOG.COMMENT_ARTALK_SERVER && (<div key='Artalk'>
|
||||
<Artalk siteInfo={siteInfo} />
|
||||
</div>)}
|
||||
<div key={frontMatter?.id} id='comment' className={`comment mt-5 text-gray-800 dark:text-gray-300 ${className || ''}`}>
|
||||
<Tabs>
|
||||
{COMMENT_ARTALK_SERVER && (<div key='Artalk'>
|
||||
<Artalk />
|
||||
</div>)}
|
||||
|
||||
{BLOG.COMMENT_TWIKOO_ENV_ID && (<div key='Twikoo'>
|
||||
<TwikooCompenent />
|
||||
</div>)}
|
||||
{COMMENT_TWIKOO_ENV_ID && (<div key='Twikoo'>
|
||||
<TwikooCompenent />
|
||||
</div>)}
|
||||
|
||||
{BLOG.COMMENT_WALINE_SERVER_URL && (<div key='Waline'>
|
||||
<WalineComponent />
|
||||
</div>)}
|
||||
{COMMENT_WALINE_SERVER_URL && (<div key='Waline'>
|
||||
<WalineComponent />
|
||||
</div>)}
|
||||
|
||||
{BLOG.COMMENT_VALINE_APP_ID && (<div key='Valine' name='reply'>
|
||||
<ValineComponent path={frontMatter.id} />
|
||||
</div>)}
|
||||
{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>
|
||||
)}
|
||||
{COMMENT_GISCUS_REPO && (
|
||||
<div key="Giscus">
|
||||
<GiscusComponent className="px-2" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{BLOG.COMMENT_CUSDIS_APP_ID && (<div key='Cusdis'>
|
||||
<CusdisComponent frontMatter={frontMatter} />
|
||||
</div>)}
|
||||
{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>)}
|
||||
{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>)}
|
||||
{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>
|
||||
</div>
|
||||
{COMMENT_WEBMENTION_ENABLE && (<div key='WebMention'>
|
||||
<WebMentionComponent frontMatter={frontMatter} className="px-2" />
|
||||
</div>)}
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import Head from 'next/head'
|
||||
|
||||
const CommonHead = ({ meta, children }) => {
|
||||
let url = BLOG?.PATH?.length ? `${BLOG.LINK}/${BLOG.SUB_PATH}` : BLOG.LINK
|
||||
let url = siteConfig('PATH')?.length ? `${siteConfig('LINK')}/${siteConfig('SUB_PATH', '')}` : siteConfig('LINK')
|
||||
let image
|
||||
if (meta) {
|
||||
url = `${url}/${meta.slug}`
|
||||
image = meta.image || '/bg_image.jpg'
|
||||
}
|
||||
const title = meta?.title || BLOG.TITLE
|
||||
const description = meta?.description || BLOG.DESCRIPTION
|
||||
const title = meta?.title || siteConfig('TITLE')
|
||||
const description = meta?.description || siteConfig('DESCRIPTION')
|
||||
const type = meta?.type || 'website'
|
||||
const keywords = meta?.tags || BLOG.KEYWORDS
|
||||
const lang = BLOG.LANG.replace('-', '_') // Facebook OpenGraph 要 zh_CN 這樣的格式才抓得到語言
|
||||
const category = meta?.category || BLOG.KEYWORDS || '軟體科技' // section 主要是像是 category 這樣的分類,Facebook 用這個來抓連結的分類
|
||||
const keywords = meta?.tags || siteConfig('KEYWORDS')
|
||||
const lang = siteConfig('LANG').replace('-', '_') // Facebook OpenGraph 要 zh_CN 這樣的格式才抓得到語言
|
||||
const category = meta?.category || siteConfig('KEYWORDS') // section 主要是像是 category 這樣的分類,Facebook 用這個來抓連結的分類
|
||||
|
||||
return (
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
<meta name="theme-color" content={BLOG.BACKGROUND_DARK} />
|
||||
<meta name="theme-color" content={siteConfig('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 && (
|
||||
{siteConfig('SEO_GOOGLE_SITE_VERIFICATION') && (
|
||||
<meta
|
||||
name="google-site-verification"
|
||||
content={BLOG.SEO_GOOGLE_SITE_VERIFICATION}
|
||||
content={siteConfig('SEO_GOOGLE_SITE_VERIFICATION')}
|
||||
/>
|
||||
)}
|
||||
{BLOG.SEO_BAIDU_SITE_VERIFICATION && (<meta name="baidu-site-verification" content={BLOG.SEO_BAIDU_SITE_VERIFICATION} />)}
|
||||
{siteConfig('SEO_BAIDU_SITE_VERIFICATION') && (<meta name="baidu-site-verification" content={siteConfig('SEO_BAIDU_SITE_VERIFICATION')} />)}
|
||||
<meta name="keywords" content={keywords} />
|
||||
<meta name="description" content={description} />
|
||||
<meta property="og:locale" content={lang} />
|
||||
@@ -36,33 +36,33 @@ const CommonHead = ({ meta, children }) => {
|
||||
<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:site_name" content={siteConfig('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 && (
|
||||
{siteConfig('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`} />
|
||||
<link rel="webmention" href={`https://webmention.io/${siteConfig('COMMENT_WEBMENTION_HOSTNAME')}/webmention`} />
|
||||
<link rel="pingback" href={`https://webmention.io/${siteConfig('COMMENT_WEBMENTION_HOSTNAME')}/xmlrpc`} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{BLOG.COMMENT_WEBMENTION.ENABLE && BLOG.COMMENT_WEBMENTION.AUTH !== '' && (
|
||||
<link href={BLOG.COMMENT_WEBMENTION.AUTH} rel="me" />
|
||||
{siteConfig('COMMENT_WEBMENTION_ENABLE') && siteConfig('COMMENT_WEBMENTION_AUTH') !== '' && (
|
||||
<link href={siteConfig('COMMENT_WEBMENTION_AUTH')} rel="me" />
|
||||
)}
|
||||
|
||||
{JSON.parse(BLOG.ANALYTICS_BUSUANZI_ENABLE) && <meta name="referrer" content="no-referrer-when-downgrade" />}
|
||||
{JSON.parse(siteConfig('ANALYTICS_BUSUANZI_ENABLE')) && <meta name="referrer" content="no-referrer-when-downgrade" />}
|
||||
{meta?.type === 'Post' && (
|
||||
<>
|
||||
<meta
|
||||
property="article:published_time"
|
||||
content={meta.publishDay}
|
||||
/>
|
||||
<meta property="article:author" content={BLOG.AUTHOR} />
|
||||
<meta property="article:author" content={siteConfig('AUTHOR')} />
|
||||
<meta property="article:section" content={category} />
|
||||
<meta property="article:publisher" content={BLOG.FACEBOOK_PAGE} />
|
||||
<meta property="article:publisher" content={siteConfig('FACEBOOK_PAGE')} />
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
import BLOG from '@/blog.config'
|
||||
|
||||
/**
|
||||
* 第三方代码 统计脚本
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const CommonScript = () => {
|
||||
return (<>
|
||||
|
||||
{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 反馈 */}
|
||||
<script async dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function(i,s,o,g,r,a,m){i["DaoVoiceObject"]=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;a.charset="utf-8";m.parentNode.insertBefore(a,m)})(window,document,"script",('https:' == document.location.protocol ? 'https:' : 'http:') + "//widget.daovoice.io/widget/daf1a94b.js","daovoice")
|
||||
`
|
||||
}}
|
||||
/>
|
||||
<script async dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
daovoice('init', {
|
||||
app_id: "${BLOG.COMMENT_DAO_VOICE_ID}"
|
||||
});
|
||||
daovoice('update');
|
||||
`
|
||||
}}
|
||||
/>
|
||||
</>)}
|
||||
|
||||
{BLOG.AD_WWADS_ID && <script type="text/javascript" charSet="UTF-8" src="https://cdn.wwads.cn/js/makemoney.js" async></script>}
|
||||
|
||||
{BLOG.COMMENT_CUSDIS_APP_ID && <script defer src={`https://cusdis.com/js/widget/lang/${BLOG.LANG.toLowerCase()}.js`} />}
|
||||
|
||||
{BLOG.COMMENT_TWIKOO_ENV_ID && <script defer src={BLOG.COMMENT_TWIKOO_CDN_URL}/> }
|
||||
|
||||
{BLOG.COMMENT_ARTALK_SERVER && <script defer src={BLOG.COMMENT_ARTALK_JS}/> }
|
||||
|
||||
{BLOG.COMMENT_TIDIO_ID && <script async src={`//code.tidio.co/${BLOG.COMMENT_TIDIO_ID}.js`} />}
|
||||
|
||||
{/* gitter聊天室 */}
|
||||
{BLOG.COMMENT_GITTER_ROOM && (<>
|
||||
<script src="https://sidecar.gitter.im/dist/sidecar.v1.js" async defer/>
|
||||
<script async dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
((window.gitter = {}).chat = {}).options = {
|
||||
room: '${BLOG.COMMENT_GITTER_ROOM}'
|
||||
};
|
||||
`
|
||||
}}/>
|
||||
</>)}
|
||||
|
||||
{/* 代码统计 */}
|
||||
{/* ackee统计脚本 */}
|
||||
{/* {BLOG.ANALYTICS_ACKEE_TRACKER && (
|
||||
<script async src={BLOG.ANALYTICS_ACKEE_TRACKER}
|
||||
data-ackee-server={BLOG.ANALYTICS_ACKEE_DATA_SERVER}
|
||||
data-ackee-domain-id={BLOG.ANALYTICS_ACKEE_DOMAIN_ID}
|
||||
/>
|
||||
)} */}
|
||||
|
||||
{/* 百度统计 */}
|
||||
{BLOG.ANALYTICS_BAIDU_ID && (
|
||||
<script async
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
var _hmt = _hmt || [];
|
||||
(function() {
|
||||
var hm = document.createElement("script");
|
||||
hm.src = "https://hm.baidu.com/hm.js?${BLOG.ANALYTICS_BAIDU_ID}";
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(hm, s);
|
||||
})();
|
||||
`
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 站长统计 */}
|
||||
{BLOG.ANALYTICS_CNZZ_ID && (
|
||||
<script async
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
document.write(unescape("%3Cspan style='display:none' id='cnzz_stat_icon_${BLOG.ANALYTICS_CNZZ_ID}'%3E%3C/span%3E%3Cscript src='https://s9.cnzz.com/z_stat.php%3Fid%3D${BLOG.ANALYTICS_CNZZ_ID}' type='text/javascript'%3E%3C/script%3E"));
|
||||
`
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 谷歌统计 */}
|
||||
{BLOG.ANALYTICS_GOOGLE_ID && (<>
|
||||
<script async
|
||||
src={`https://www.googletagmanager.com/gtag/js?id=${BLOG.ANALYTICS_GOOGLE_ID}`}
|
||||
/>
|
||||
<script async
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '${BLOG.ANALYTICS_GOOGLE_ID}', {
|
||||
page_path: window.location.pathname,
|
||||
});
|
||||
`
|
||||
}}
|
||||
/>
|
||||
</>)}
|
||||
|
||||
{/* 引入音乐播放 */}
|
||||
{JSON.parse(BLOG.MUSIC_PLAYER) && <script async src={BLOG.MUSIC_PLAYER_CDN_URL} />}
|
||||
{JSON.parse(BLOG.MUSIC_PLAYER) && JSON.parse(BLOG.MUSIC_PLAYER_METING) && <script async src="https://cdnjs.cloudflare.com/ajax/libs/meting/2.0.1/Meting.min.js" />}
|
||||
</>)
|
||||
}
|
||||
|
||||
export default CommonScript
|
||||
@@ -1,28 +1,34 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
const CusdisComponent = ({ frontMatter }) => {
|
||||
const { locale } = useGlobal()
|
||||
const router = useRouter()
|
||||
const { isDarkMode } = useGlobal()
|
||||
const { isDarkMode, lang } = useGlobal()
|
||||
const src = siteConfig('COMMENT_CUSDIS_SCRIPT_SRC')
|
||||
const i18nForCusdis = siteConfig('LANG').toLowerCase().indexOf('zh') === 0 ? siteConfig('LANG').toLowerCase() : siteConfig('LANG').toLowerCase().substring(0, 2)
|
||||
const langCDN = siteConfig('COMMENT_CUSDIS_LANG_SRC', `https://cusdis.com/js/widget/lang/${i18nForCusdis}.js`)
|
||||
|
||||
// 处理cusdis主题
|
||||
useEffect(() => {
|
||||
loadExternalResource(BLOG.COMMENT_CUSDIS_SCRIPT_SRC, 'js').then(url => {
|
||||
const CUSDIS = window.CUSDIS
|
||||
CUSDIS?.initial()
|
||||
})
|
||||
}, [isDarkMode])
|
||||
loadCusdis()
|
||||
}, [isDarkMode, lang])
|
||||
|
||||
const loadCusdis = async () => {
|
||||
await loadExternalResource(langCDN, 'js')
|
||||
await loadExternalResource(src, 'js')
|
||||
|
||||
window?.CUSDIS?.initial()
|
||||
}
|
||||
|
||||
return <div id="cusdis_thread"
|
||||
lang={locale.LOCALE.toLowerCase()}
|
||||
data-host={BLOG.COMMENT_CUSDIS_HOST}
|
||||
data-app-id={BLOG.COMMENT_CUSDIS_APP_ID}
|
||||
lang={lang.toLowerCase()}
|
||||
data-host={siteConfig('COMMENT_CUSDIS_HOST')}
|
||||
data-app-id={siteConfig('COMMENT_CUSDIS_APP_ID')}
|
||||
data-page-id={frontMatter.id}
|
||||
data-page-url={BLOG.LINK + router.asPath}
|
||||
data-page-url={siteConfig('LINK') + router.asPath}
|
||||
data-page-title={frontMatter.title}
|
||||
data-theme={isDarkMode ? 'dark' : 'light'}
|
||||
></div>
|
||||
|
||||
@@ -3,8 +3,8 @@ import { useRouter } from 'next/router'
|
||||
import { useEffect, useState, useRef, useLayoutEffect } from 'react'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { saveDarkModeToCookies, THEMES } from '@/themes/theme'
|
||||
import BLOG from '@/blog.config'
|
||||
import useWindowSize from '@/hooks/useWindowSize'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
/**
|
||||
* 自定义右键菜单
|
||||
@@ -28,7 +28,7 @@ export default function CustomContextMenu(props) {
|
||||
function handleJumpToRandomPost() {
|
||||
const randomIndex = Math.floor(Math.random() * latestPosts.length)
|
||||
const randomPost = latestPosts[randomIndex]
|
||||
router.push(`${BLOG.SUB_PATH}/${randomPost?.slug}`)
|
||||
router.push(`${siteConfig('SUB_PATH', '')}/${randomPost?.slug}`)
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@@ -162,10 +162,12 @@ export default function CustomContextMenu(props) {
|
||||
{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>
|
||||
{siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH') && (
|
||||
<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>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { saveDarkModeToCookies } from '@/themes/theme'
|
||||
import { Moon, Sun } from './HeroIcons'
|
||||
import { useImperativeHandle } from 'react'
|
||||
|
||||
@@ -8,7 +7,7 @@ import { useImperativeHandle } from 'react'
|
||||
*/
|
||||
const DarkModeButton = (props) => {
|
||||
const { cRef, className } = props
|
||||
const { isDarkMode, updateDarkMode } = useGlobal()
|
||||
const { isDarkMode, toggleDarkMode } = useGlobal()
|
||||
|
||||
/**
|
||||
* 对外暴露方法
|
||||
@@ -16,22 +15,12 @@ const DarkModeButton = (props) => {
|
||||
useImperativeHandle(cRef, () => {
|
||||
return {
|
||||
handleChangeDarkMode: () => {
|
||||
handleChangeDarkMode()
|
||||
toggleDarkMode()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 用户手动设置主题
|
||||
const 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 onClick={handleChangeDarkMode} className={`${className || ''} flex justify-center dark:text-gray-200 text-gray-800`}>
|
||||
return <div onClick={toggleDarkMode} 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>
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Select from './Select'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { THEMES } from '@/themes/theme'
|
||||
import { useRouter } from 'next/router'
|
||||
import { siteConfigMap } from '@/lib/config'
|
||||
import { getQueryParam } from '@/lib/utils'
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -13,14 +14,14 @@ const DebugPanel = () => {
|
||||
const [show, setShow] = useState(false)
|
||||
const { theme, switchTheme, locale } = useGlobal()
|
||||
const router = useRouter()
|
||||
const currentTheme = getQueryParam(router.asPath, 'theme') || theme
|
||||
const [siteConfig, updateSiteConfig] = useState({})
|
||||
|
||||
// 主题下拉框
|
||||
const themeOptions = THEMES?.map(t => ({ value: t, text: t }))
|
||||
|
||||
useEffect(() => {
|
||||
updateSiteConfig(Object.assign({}, BLOG))
|
||||
// updateThemeConfig(Object.assign({}, ThemeMap[BLOG.THEME].THEME_CONFIG))
|
||||
updateSiteConfig(Object.assign({}, siteConfigMap()))
|
||||
}, [])
|
||||
|
||||
function toggleShow() {
|
||||
@@ -71,7 +72,7 @@ const DebugPanel = () => {
|
||||
<div className='flex'>
|
||||
<Select
|
||||
label={locale.COMMON.THEME_SWITCH}
|
||||
value={theme}
|
||||
value={currentTheme}
|
||||
options={themeOptions}
|
||||
onChange={handleUpdateDebugTheme}
|
||||
/>
|
||||
|
||||
32
components/DifyChatbot.js
Normal file
32
components/DifyChatbot.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useEffect } from 'react';
|
||||
import { siteConfig } from '@/lib/config';
|
||||
|
||||
export default function DifyChatbot() {
|
||||
useEffect(() => {
|
||||
// 这里使用 siteConfig() 函数调用来获取配置值
|
||||
if (!siteConfig('DIFY_CHATBOT_ENABLED')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 配置 DifyChatbot,同样需要调用 siteConfig() 获取相应的配置值
|
||||
window.difyChatbotConfig = {
|
||||
token: siteConfig('DIFY_CHATBOT_TOKEN'),
|
||||
baseUrl: siteConfig('DIFY_CHATBOT_BASE_URL')
|
||||
};
|
||||
|
||||
// 加载 DifyChatbot 脚本
|
||||
const script = document.createElement('script');
|
||||
script.src = `${siteConfig('DIFY_CHATBOT_BASE_URL')}/embed.min.js`; // 注意调用 siteConfig()
|
||||
script.id = siteConfig('DIFY_CHATBOT_TOKEN'); // 注意调用 siteConfig()
|
||||
script.defer = true;
|
||||
document.body.appendChild(script);
|
||||
|
||||
return () => {
|
||||
// 在组件卸载时清理 script 标签
|
||||
const existingScript = document.getElementById(siteConfig('DIFY_CHATBOT_TOKEN')); // 注意调用 siteConfig()
|
||||
if (existingScript) document.body.removeChild(existingScript);
|
||||
};
|
||||
}, []); // 注意依赖数组为空,意味着脚本将仅在加载页面时执行一次
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
/**
|
||||
@@ -6,7 +6,7 @@ import { useEffect } from 'react'
|
||||
*/
|
||||
export default function DisableCopy() {
|
||||
useEffect(() => {
|
||||
if (!JSON.parse(BLOG.CAN_COPY)) {
|
||||
if (!JSON.parse(siteConfig('CAN_COPY'))) {
|
||||
// 全栈添加禁止复制的样式
|
||||
document.getElementsByTagName('html')[0].classList.add('forbid-copy')
|
||||
// 监听复制事件
|
||||
|
||||
@@ -8,8 +8,12 @@ const katexSettings = {
|
||||
strict: false
|
||||
}
|
||||
|
||||
/**
|
||||
* 数学公式
|
||||
* @param {} param0
|
||||
* @returns
|
||||
*/
|
||||
export const Equation = ({ block, math, inline = false, className, ...rest }) => {
|
||||
// const { recordMap } = useNotionContext()
|
||||
math = math || getBlockTitle(block, null)
|
||||
if (!math) return null
|
||||
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import BLOG from 'blog.config'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import dynamic from 'next/dynamic'
|
||||
import LA51 from './LA51'
|
||||
import WebWhiz from './Webwhiz'
|
||||
import TianLiGPT from './TianliGPT'
|
||||
import { GlobalStyle } from './GlobalStyle'
|
||||
|
||||
// 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'
|
||||
import { CUSTOM_EXTERNAL_CSS, CUSTOM_EXTERNAL_JS, IMG_SHADOW } from '@/blog.config'
|
||||
import { isBrowser, loadExternalResource } from '@/lib/utils'
|
||||
|
||||
const TwikooCommentCounter = dynamic(() => import('@/components/TwikooCommentCounter'), { ssr: false })
|
||||
const DebugPanel = dynamic(() => import('@/components/DebugPanel'), { ssr: false })
|
||||
@@ -22,6 +17,7 @@ const FlutteringRibbon = dynamic(() => import('@/components/FlutteringRibbon'),
|
||||
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 DifyChatbot = dynamic(() => import('@/components/DifyChatbot'), { 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 })
|
||||
@@ -33,35 +29,262 @@ const VConsole = dynamic(() => import('@/components/VConsole'), { ssr: false })
|
||||
const CustomContextMenu = dynamic(() => import('@/components/CustomContextMenu'), { ssr: false })
|
||||
const DisableCopy = dynamic(() => import('@/components/DisableCopy'), { ssr: false })
|
||||
const AdBlockDetect = dynamic(() => import('@/components/AdBlockDetect'), { ssr: false })
|
||||
const LoadingProgress = dynamic(() => import('@/components/LoadingProgress'), { ssr: false })
|
||||
const AosAnimation = dynamic(() => import('@/components/AOSAnimation'), { ssr: false })
|
||||
|
||||
/**
|
||||
* 各种第三方组件
|
||||
* 各种插件脚本
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const ExternalPlugin = (props) => {
|
||||
const DISABLE_PLUGIN = siteConfig('DISABLE_PLUGIN')
|
||||
const THEME_SWITCH = siteConfig('THEME_SWITCH')
|
||||
const DEBUG = siteConfig('DEBUG')
|
||||
const ANALYTICS_ACKEE_TRACKER = siteConfig('ANALYTICS_ACKEE_TRACKER')
|
||||
const ANALYTICS_VERCEL = siteConfig('ANALYTICS_VERCEL')
|
||||
const ANALYTICS_BUSUANZI_ENABLE = siteConfig('ANALYTICS_BUSUANZI_ENABLE')
|
||||
const ADSENSE_GOOGLE_ID = siteConfig('ADSENSE_GOOGLE_ID')
|
||||
const FACEBOOK_APP_ID = siteConfig('FACEBOOK_APP_ID')
|
||||
const FACEBOOK_PAGE_ID = siteConfig('FACEBOOK_PAGE_ID')
|
||||
const FIREWORKS = siteConfig('FIREWORKS')
|
||||
const SAKURA = siteConfig('SAKURA')
|
||||
const STARRY_SKY = siteConfig('STARRY_SKY')
|
||||
const MUSIC_PLAYER = siteConfig('MUSIC_PLAYER')
|
||||
const NEST = siteConfig('NEST')
|
||||
const FLUTTERINGRIBBON = siteConfig('FLUTTERINGRIBBON')
|
||||
const COMMENT_TWIKOO_COUNT_ENABLE = siteConfig('COMMENT_TWIKOO_COUNT_ENABLE')
|
||||
const RIBBON = siteConfig('RIBBON')
|
||||
const CUSTOM_RIGHT_CLICK_CONTEXT_MENU = siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU')
|
||||
const CAN_COPY = siteConfig('CAN_COPY')
|
||||
const WEB_WHIZ_ENABLED = siteConfig('WEB_WHIZ_ENABLED')
|
||||
const AD_WWADS_BLOCK_DETECT = siteConfig('AD_WWADS_BLOCK_DETECT')
|
||||
const CHATBASE_ID = siteConfig('CHATBASE_ID')
|
||||
const COMMENT_DAO_VOICE_ID = siteConfig('COMMENT_DAO_VOICE_ID')
|
||||
const AD_WWADS_ID = siteConfig('AD_WWADS_ID')
|
||||
const COMMENT_TWIKOO_ENV_ID = siteConfig('COMMENT_TWIKOO_ENV_ID')
|
||||
const COMMENT_TWIKOO_CDN_URL = siteConfig('COMMENT_TWIKOO_CDN_URL')
|
||||
const COMMENT_ARTALK_SERVER = siteConfig('COMMENT_ARTALK_SERVER')
|
||||
const COMMENT_ARTALK_JS = siteConfig('COMMENT_ARTALK_JS')
|
||||
const COMMENT_TIDIO_ID = siteConfig('COMMENT_TIDIO_ID')
|
||||
const COMMENT_GITTER_ROOM = siteConfig('COMMENT_GITTER_ROOM')
|
||||
const ANALYTICS_BAIDU_ID = siteConfig('ANALYTICS_BAIDU_ID')
|
||||
const ANALYTICS_CNZZ_ID = siteConfig('ANALYTICS_CNZZ_ID')
|
||||
const ANALYTICS_GOOGLE_ID = siteConfig('ANALYTICS_GOOGLE_ID')
|
||||
const MATOMO_HOST_URL = siteConfig('MATOMO_HOST_URL')
|
||||
const MATOMO_SITE_ID = siteConfig('MATOMO_SITE_ID')
|
||||
const ANALYTICS_51LA_ID = siteConfig('ANALYTICS_51LA_ID')
|
||||
const ANALYTICS_51LA_CK = siteConfig('ANALYTICS_51LA_CK')
|
||||
const DIFY_CHATBOT_ENABLED = siteConfig('DIFY_CHATBOT_ENABLED')
|
||||
const TIANLI_KEY = siteConfig('TianliGPT_KEY')
|
||||
const GLOBAL_JS = siteConfig('GLOBAL_JS')
|
||||
const CLARITY_ID = siteConfig('CLARITY_ID')
|
||||
|
||||
// 自定义样式css和js引入
|
||||
if (isBrowser) {
|
||||
// 初始化AOS动画
|
||||
// 静态导入本地自定义样式
|
||||
loadExternalResource('/css/custom.css', 'css')
|
||||
loadExternalResource('/js/custom.js', 'js')
|
||||
|
||||
// 自动添加图片阴影
|
||||
if (IMG_SHADOW) {
|
||||
loadExternalResource('/css/img-shadow.css', 'css')
|
||||
}
|
||||
|
||||
// 导入外部自定义脚本
|
||||
if (CUSTOM_EXTERNAL_JS && CUSTOM_EXTERNAL_JS.length > 0) {
|
||||
for (const url of CUSTOM_EXTERNAL_JS) {
|
||||
loadExternalResource(url, 'js')
|
||||
}
|
||||
}
|
||||
|
||||
// 导入外部自定义样式
|
||||
if (CUSTOM_EXTERNAL_CSS && CUSTOM_EXTERNAL_CSS.length > 0) {
|
||||
for (const url of CUSTOM_EXTERNAL_CSS) {
|
||||
loadExternalResource(url, 'css')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (DISABLE_PLUGIN) {
|
||||
return null
|
||||
}
|
||||
|
||||
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} />}
|
||||
{!JSON.parse(BLOG.CAN_COPY) && <DisableCopy/>}
|
||||
{JSON.parse(BLOG.WEB_WHIZ_ENABLED) && <WebWhiz/>}
|
||||
{JSON.parse(BLOG.AD_WWADS_BLOCK_DETECT) && <AdBlockDetect/>}
|
||||
<VConsole/>
|
||||
</>
|
||||
|
||||
{/* 全局样式嵌入 */}
|
||||
<GlobalStyle/>
|
||||
|
||||
{THEME_SWITCH && <ThemeSwitch />}
|
||||
{DEBUG && <DebugPanel />}
|
||||
{ANALYTICS_ACKEE_TRACKER && <Ackee />}
|
||||
{ANALYTICS_GOOGLE_ID && <Gtag />}
|
||||
{ANALYTICS_VERCEL && <Analytics />}
|
||||
{ANALYTICS_BUSUANZI_ENABLE && <Busuanzi />}
|
||||
{ADSENSE_GOOGLE_ID && <GoogleAdsense />}
|
||||
{FACEBOOK_APP_ID && FACEBOOK_PAGE_ID && <Messenger />}
|
||||
{FIREWORKS && <Fireworks />}
|
||||
{SAKURA && <Sakura />}
|
||||
{STARRY_SKY && <StarrySky />}
|
||||
{MUSIC_PLAYER && <MusicPlayer />}
|
||||
{NEST && <Nest />}
|
||||
{FLUTTERINGRIBBON && <FlutteringRibbon />}
|
||||
{COMMENT_TWIKOO_COUNT_ENABLE && <TwikooCommentCounter {...props} />}
|
||||
{RIBBON && <Ribbon />}
|
||||
{DIFY_CHATBOT_ENABLED && <DifyChatbot />}
|
||||
{CUSTOM_RIGHT_CLICK_CONTEXT_MENU && <CustomContextMenu {...props} />}
|
||||
{!CAN_COPY && <DisableCopy />}
|
||||
{WEB_WHIZ_ENABLED && <WebWhiz />}
|
||||
{AD_WWADS_BLOCK_DETECT && <AdBlockDetect />}
|
||||
{TIANLI_KEY && <TianLiGPT/>}
|
||||
<VConsole />
|
||||
<LoadingProgress />
|
||||
<AosAnimation />
|
||||
{ANALYTICS_51LA_ID && ANALYTICS_51LA_CK && <LA51/>}
|
||||
|
||||
{ANALYTICS_51LA_ID && ANALYTICS_51LA_CK && (<>
|
||||
<script id="LA_COLLECT" src="//sdk.51.la/js-sdk-pro.min.js" defer/>
|
||||
{/* <script async dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
LA.init({id:"${ANALYTICS_51LA_ID}",ck:"${ANALYTICS_51LA_CK}",hashMode:true,autoTrack:true})
|
||||
`
|
||||
}} /> */}
|
||||
</>)}
|
||||
|
||||
{/* 注入JS脚本 */}
|
||||
{GLOBAL_JS && <script async dangerouslySetInnerHTML={{
|
||||
__html: GLOBAL_JS
|
||||
}} />}
|
||||
|
||||
{CHATBASE_ID && (<>
|
||||
<script id={CHATBASE_ID} src="https://www.chatbase.co/embed.min.js" defer />
|
||||
<script async dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window.chatbaseConfig = {
|
||||
chatbotId: "${CHATBASE_ID}",
|
||||
}
|
||||
`
|
||||
}} />
|
||||
</>)}
|
||||
|
||||
{CLARITY_ID && (<>
|
||||
<script async dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function(c,l,a,r,i,t,y){
|
||||
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
|
||||
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
||||
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
||||
})(window, document, "clarity", "script", "${CLARITY_ID}");
|
||||
`
|
||||
}} />
|
||||
</>)}
|
||||
|
||||
{COMMENT_DAO_VOICE_ID && (<>
|
||||
{/* DaoVoice 反馈 */}
|
||||
<script async dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function(i,s,o,g,r,a,m){i["DaoVoiceObject"]=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;a.charset="utf-8";m.parentNode.insertBefore(a,m)})(window,document,"script",('https:' == document.location.protocol ? 'https:' : 'http:') + "//widget.daovoice.io/widget/daf1a94b.js","daovoice")
|
||||
`
|
||||
}}
|
||||
/>
|
||||
<script async dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
daovoice('init', {
|
||||
app_id: "${COMMENT_DAO_VOICE_ID}"
|
||||
});
|
||||
daovoice('update');
|
||||
`
|
||||
}}
|
||||
/>
|
||||
</>)}
|
||||
|
||||
{AD_WWADS_ID && <script type="text/javascript" src="https://cdn.wwads.cn/js/makemoney.js" async></script>}
|
||||
|
||||
{COMMENT_TWIKOO_ENV_ID && <script defer src={COMMENT_TWIKOO_CDN_URL} />}
|
||||
|
||||
{COMMENT_ARTALK_SERVER && <script defer src={COMMENT_ARTALK_JS} />}
|
||||
|
||||
{COMMENT_TIDIO_ID && <script async src={`//code.tidio.co/${COMMENT_TIDIO_ID}.js`} />}
|
||||
|
||||
{/* gitter聊天室 */}
|
||||
{COMMENT_GITTER_ROOM && (<>
|
||||
<script src="https://sidecar.gitter.im/dist/sidecar.v1.js" async defer />
|
||||
<script async dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
((window.gitter = {}).chat = {}).options = {
|
||||
room: '${COMMENT_GITTER_ROOM}'
|
||||
};
|
||||
`
|
||||
}} />
|
||||
</>)}
|
||||
|
||||
{/* 百度统计 */}
|
||||
{ANALYTICS_BAIDU_ID && (
|
||||
<script async
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
var _hmt = _hmt || [];
|
||||
(function() {
|
||||
var hm = document.createElement("script");
|
||||
hm.src = "https://hm.baidu.com/hm.js?${ANALYTICS_BAIDU_ID}";
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(hm, s);
|
||||
})();
|
||||
`
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 站长统计 */}
|
||||
{ANALYTICS_CNZZ_ID && (
|
||||
<script async
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
document.write(unescape("%3Cspan style='display:none' id='cnzz_stat_icon_${ANALYTICS_CNZZ_ID}'%3E%3C/span%3E%3Cscript src='https://s9.cnzz.com/z_stat.php%3Fid%3D${ANALYTICS_CNZZ_ID}' type='text/javascript'%3E%3C/script%3E"));
|
||||
`
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 谷歌统计 */}
|
||||
{ANALYTICS_GOOGLE_ID && (<>
|
||||
<script async
|
||||
src={`https://www.googletagmanager.com/gtag/js?id=${ANALYTICS_GOOGLE_ID}`}
|
||||
/>
|
||||
<script async
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '${ANALYTICS_GOOGLE_ID}', {
|
||||
page_path: window.location.pathname,
|
||||
});
|
||||
`
|
||||
}}
|
||||
/>
|
||||
</>)}
|
||||
|
||||
{/* Matomo 统计 */}
|
||||
{MATOMO_HOST_URL && MATOMO_SITE_ID && (
|
||||
<script async dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
var _paq = window._paq = window._paq || [];
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
(function() {
|
||||
var u="//${MATOMO_HOST_URL}/";
|
||||
_paq.push(['setTrackerUrl', u+'matomo.php']);
|
||||
_paq.push(['setSiteId', '${MATOMO_SITE_ID}']);
|
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
`
|
||||
}} />
|
||||
)}
|
||||
|
||||
</>
|
||||
}
|
||||
|
||||
export default ExternalPlugin
|
||||
|
||||
@@ -1,14 +1,41 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { Component } from 'react'
|
||||
import { Component, useEffect, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
export default function Messenger() {
|
||||
return <MessengerCustomerChat
|
||||
pageId={BLOG.FACEBOOK_PAGE_ID}
|
||||
appId={BLOG.FACEBOOK_APP_ID}
|
||||
language={BLOG.LANG.replace('-', '_')}
|
||||
shouldShowDialog={true}
|
||||
/>
|
||||
const pageId = siteConfig('FACEBOOK_PAGE_ID')
|
||||
const appId = siteConfig('FACEBOOK_APP_ID')
|
||||
const language = siteConfig('LANG').replace('-', '_')
|
||||
|
||||
// 新增一个状态变量用于追踪是否已经滚动过
|
||||
const [showMessenger, setShowMessenger] = useState(false);
|
||||
|
||||
const showTheComponent = () => {
|
||||
window.removeEventListener('scroll', showTheComponent);
|
||||
if (!showMessenger) {
|
||||
setShowMessenger(true);
|
||||
}
|
||||
};
|
||||
|
||||
// 延时7秒,或页面滚动时加载该组件
|
||||
useEffect(() => {
|
||||
window.addEventListener('scroll', showTheComponent);
|
||||
setTimeout(() => {
|
||||
showTheComponent()
|
||||
}, 7000);
|
||||
return () => {
|
||||
window.removeEventListener('scroll', showTheComponent);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <>
|
||||
{showMessenger && <MessengerCustomerChat
|
||||
pageId={pageId}
|
||||
appId={appId}
|
||||
language={language}
|
||||
shouldShowDialog={true}
|
||||
/>}
|
||||
</>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,8 +51,8 @@ class MessengerCustomerChat extends Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
* 初始化
|
||||
*/
|
||||
componentDidMount() {
|
||||
this.setFbAsyncInit()
|
||||
this.reloadSDKAsynchronously()
|
||||
@@ -34,19 +61,19 @@ class MessengerCustomerChat extends Component {
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
prevProps.pageId !== this.props.pageId ||
|
||||
prevProps.appId !== this.props.appId ||
|
||||
prevProps.shouldShowDialog !== this.props.shouldShowDialog ||
|
||||
prevProps.htmlRef !== this.props.htmlRef ||
|
||||
prevProps.minimized !== this.props.minimized ||
|
||||
prevProps.themeColor !== this.props.themeColor ||
|
||||
prevProps.loggedInGreeting !== this.props.loggedInGreeting ||
|
||||
prevProps.loggedOutGreeting !== this.props.loggedOutGreeting ||
|
||||
prevProps.greetingDialogDisplay !== this.props.greetingDialogDisplay ||
|
||||
prevProps.greetingDialogDelay !== this.props.greetingDialogDelay ||
|
||||
prevProps.autoLogAppEvents !== this.props.autoLogAppEvents ||
|
||||
prevProps.xfbml !== this.props.xfbml ||
|
||||
prevProps.version !== this.props.version ||
|
||||
prevProps.language !== this.props.language
|
||||
prevProps.appId !== this.props.appId ||
|
||||
prevProps.shouldShowDialog !== this.props.shouldShowDialog ||
|
||||
prevProps.htmlRef !== this.props.htmlRef ||
|
||||
prevProps.minimized !== this.props.minimized ||
|
||||
prevProps.themeColor !== this.props.themeColor ||
|
||||
prevProps.loggedInGreeting !== this.props.loggedInGreeting ||
|
||||
prevProps.loggedOutGreeting !== this.props.loggedOutGreeting ||
|
||||
prevProps.greetingDialogDisplay !== this.props.greetingDialogDisplay ||
|
||||
prevProps.greetingDialogDelay !== this.props.greetingDialogDelay ||
|
||||
prevProps.autoLogAppEvents !== this.props.autoLogAppEvents ||
|
||||
prevProps.xfbml !== this.props.xfbml ||
|
||||
prevProps.version !== this.props.version ||
|
||||
prevProps.language !== this.props.language
|
||||
) {
|
||||
this.setFbAsyncInit()
|
||||
this.reloadSDKAsynchronously()
|
||||
@@ -60,8 +87,8 @@ class MessengerCustomerChat extends Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
* 初始化
|
||||
*/
|
||||
setFbAsyncInit() {
|
||||
const { appId, autoLogAppEvents, xfbml, version } = this.props
|
||||
|
||||
@@ -80,18 +107,18 @@ class MessengerCustomerChat extends Component {
|
||||
loadSDKAsynchronously() {
|
||||
const { language } = this.props;
|
||||
/* eslint-disable */
|
||||
(function (d, s, id) {
|
||||
var js,
|
||||
fjs = d.getElementsByTagName(s)[0];
|
||||
if (d.getElementById(id)) {
|
||||
return;
|
||||
}
|
||||
js = d.createElement(s);
|
||||
js.id = id;
|
||||
js.src = `https://connect.facebook.net/${language}/sdk/xfbml.customerchat.js`;
|
||||
fjs.parentNode.insertBefore(js, fjs);
|
||||
})(document, 'script', 'facebook-jssdk');
|
||||
/* eslint-enable */
|
||||
(function (d, s, id) {
|
||||
var js,
|
||||
fjs = d.getElementsByTagName(s)[0];
|
||||
if (d.getElementById(id)) {
|
||||
return;
|
||||
}
|
||||
js = d.createElement(s);
|
||||
js.id = id;
|
||||
js.src = `https://connect.facebook.net/${language}/sdk/xfbml.customerchat.js`;
|
||||
fjs.parentNode.insertBefore(js, fjs);
|
||||
})(document, 'script', 'facebook-jssdk');
|
||||
/* eslint-enable */
|
||||
}
|
||||
|
||||
removeFacebookSDK() {
|
||||
@@ -147,25 +174,25 @@ class MessengerCustomerChat extends Component {
|
||||
|
||||
const refAttribute = htmlRef !== undefined ? `ref="${htmlRef}"` : ''
|
||||
const minimizedAttribute =
|
||||
minimized !== undefined ? `minimized="${minimized}"` : ''
|
||||
minimized !== undefined ? `minimized="${minimized}"` : ''
|
||||
const themeColorAttribute =
|
||||
themeColor !== undefined ? `theme_color="${themeColor}"` : ''
|
||||
themeColor !== undefined ? `theme_color="${themeColor}"` : ''
|
||||
const loggedInGreetingAttribute =
|
||||
loggedInGreeting !== undefined
|
||||
? `logged_in_greeting="${loggedInGreeting}"`
|
||||
: ''
|
||||
loggedInGreeting !== undefined
|
||||
? `logged_in_greeting="${loggedInGreeting}"`
|
||||
: ''
|
||||
const loggedOutGreetingAttribute =
|
||||
loggedOutGreeting !== undefined
|
||||
? `logged_out_greeting="${loggedOutGreeting}"`
|
||||
: ''
|
||||
loggedOutGreeting !== undefined
|
||||
? `logged_out_greeting="${loggedOutGreeting}"`
|
||||
: ''
|
||||
const greetingDialogDisplayAttribute =
|
||||
greetingDialogDisplay !== undefined
|
||||
? `greeting_dialog_display="${greetingDialogDisplay}"`
|
||||
: ''
|
||||
greetingDialogDisplay !== undefined
|
||||
? `greeting_dialog_display="${greetingDialogDisplay}"`
|
||||
: ''
|
||||
const greetingDialogDelayAttribute =
|
||||
greetingDialogDelay !== undefined
|
||||
? `greeting_dialog_delay="${greetingDialogDelay}"`
|
||||
: ''
|
||||
greetingDialogDelay !== undefined
|
||||
? `greeting_dialog_delay="${greetingDialogDelay}"`
|
||||
: ''
|
||||
|
||||
return {
|
||||
__html: `<div
|
||||
@@ -192,8 +219,8 @@ class MessengerCustomerChat extends Component {
|
||||
const element = event.target
|
||||
if (
|
||||
element.className &&
|
||||
typeof element.className === 'string' &&
|
||||
element.className.includes('fb_dialog')
|
||||
typeof element.className === 'string' &&
|
||||
element.className.includes('fb_dialog')
|
||||
) {
|
||||
this.controlPlugin()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { FacebookProvider, Page } from 'react-facebook'
|
||||
import { FacebookIcon } from 'react-share'
|
||||
|
||||
@@ -7,27 +7,27 @@ import { FacebookIcon } from 'react-share'
|
||||
* @returns
|
||||
*/
|
||||
const FacebookPage = () => {
|
||||
if (!BLOG.FACEBOOK_APP_ID || !BLOG.FACEBOOK_PAGE) {
|
||||
if (!siteConfig('FACEBOOK_APP_ID') || !siteConfig('FACEBOOK_PAGE')) {
|
||||
return <></>
|
||||
}
|
||||
return <div className="shadow-md hover:shadow-xl dark:text-gray-300 border dark:border-black rounded-xl px-2 py-4 bg-white dark:bg-hexo-black-gray lg:duration-100 justify-center">
|
||||
{BLOG.FACEBOOK_PAGE && (
|
||||
{siteConfig('FACEBOOK_PAGE') && (
|
||||
<div className="flex items-center pb-2">
|
||||
<a
|
||||
href={BLOG.FACEBOOK_PAGE}
|
||||
href={siteConfig('FACEBOOK_PAGE')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-1 pr-2 pt-0"
|
||||
>
|
||||
<FacebookIcon size={28} round />
|
||||
</a>
|
||||
<a href={BLOG.FACEBOOK_PAGE} rel="noopener noreferrer" target="_blank">
|
||||
{BLOG.FACEBOOK_PAGE_TITLE}
|
||||
<a href={siteConfig('FACEBOOK_PAGE')} rel="noopener noreferrer" target="_blank">
|
||||
{siteConfig('FACEBOOK_PAGE_TITLE')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{BLOG.FACEBOOK_APP_ID && <FacebookProvider appId={BLOG.FACEBOOK_APP_ID}>
|
||||
<Page href={BLOG.FACEBOOK_PAGE} tabs="timeline" />
|
||||
{siteConfig('FACEBOOK_APP_ID') && <FacebookProvider appId={siteConfig('FACEBOOK_APP_ID')}>
|
||||
<Page href={siteConfig('FACEBOOK_PAGE')} tabs="timeline" />
|
||||
</FacebookProvider>}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -4,11 +4,17 @@
|
||||
*/
|
||||
import { useEffect } from 'react'
|
||||
import anime from 'animejs'
|
||||
import BLOG from 'blog.config'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
/**
|
||||
* 鼠标点击烟花特效
|
||||
* @returns
|
||||
*/
|
||||
const Fireworks = () => {
|
||||
const fireworksColor = siteConfig('FIREWORKS_COLOR')
|
||||
|
||||
useEffect(() => {
|
||||
createFireworks({})
|
||||
createFireworks({ colors: fireworksColor })
|
||||
}, [])
|
||||
return <canvas id='fireworks' className='fireworks'></canvas>
|
||||
}
|
||||
@@ -20,7 +26,7 @@ export default Fireworks
|
||||
*/
|
||||
function createFireworks(config) {
|
||||
const defaultConfig = {
|
||||
colors: BLOG.FIREWORKS_COLOR,
|
||||
colors: config?.colors,
|
||||
numberOfParticules: 20,
|
||||
orbitRadius: {
|
||||
min: 50,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Giscus from '@giscus/react'
|
||||
|
||||
@@ -15,17 +15,17 @@ const GiscusComponent = () => {
|
||||
|
||||
return (
|
||||
<Giscus
|
||||
repo={BLOG.COMMENT_GISCUS_REPO}
|
||||
repoId={BLOG.COMMENT_GISCUS_REPO_ID}
|
||||
categoryId={BLOG.COMMENT_GISCUS_CATEGORY_ID}
|
||||
mapping={BLOG.COMMENT_GISCUS_MAPPING}
|
||||
reactionsEnabled={BLOG.COMMENT_GISCUS_REACTIONS_ENABLED}
|
||||
emitMetadata={BLOG.COMMENT_GISCUS_EMIT_METADATA}
|
||||
repo={siteConfig('COMMENT_GISCUS_REPO')}
|
||||
repoId={siteConfig('COMMENT_GISCUS_REPO_ID')}
|
||||
categoryId={siteConfig('COMMENT_GISCUS_CATEGORY_ID')}
|
||||
mapping={siteConfig('COMMENT_GISCUS_MAPPING')}
|
||||
reactionsEnabled={siteConfig('COMMENT_GISCUS_REACTIONS_ENABLED')}
|
||||
emitMetadata={siteConfig('COMMENT_GISCUS_EMIT_METADATA')}
|
||||
theme={theme}
|
||||
inputPosition={BLOG.COMMENT_GISCUS_INPUT_POSITION}
|
||||
lang={BLOG.COMMENT_GISCUS_LANG}
|
||||
loading={BLOG.COMMENT_GISCUS_LOADING}
|
||||
crossorigin={BLOG.COMMENT_GISCUS_CROSSORIGIN}
|
||||
inputPosition={siteConfig('COMMENT_GISCUS_INPUT_POSITION')}
|
||||
lang={siteConfig('COMMENT_GISCUS_LANG')}
|
||||
loading={siteConfig('COMMENT_GISCUS_LOADING')}
|
||||
crossorigin={siteConfig('COMMENT_GISCUS_CROSSORIGIN')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,37 +1,39 @@
|
||||
// import 'gitalk/dist/gitalk.css'
|
||||
import BLOG from '@/blog.config'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
import { useEffect } from 'react'
|
||||
// import GitalkComponent from 'gitalk/dist/gitalk-component'
|
||||
|
||||
/**
|
||||
* gitalk评论插件
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
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)
|
||||
// }} />
|
||||
const loadGitalk = async() => {
|
||||
await loadExternalResource(BLOG.COMMENT_GITALK_CSS_CDN_URL, 'css')
|
||||
await loadExternalResource(BLOG.COMMENT_GITALK_JS_CDN_URL, 'js')
|
||||
const Gitalk = window.Gitalk
|
||||
const gitalkCSSCDN = siteConfig('COMMENT_GITALK_CSS_CDN_URL')
|
||||
const gitalkJSCDN = siteConfig('COMMENT_GITALK_JS_CDN_URL')
|
||||
const clientId = siteConfig('COMMENT_GITALK_CLIENT_ID')
|
||||
const clientSecret = siteConfig('COMMENT_GITALK_CLIENT_SECRET')
|
||||
const repo = siteConfig('COMMENT_GITALK_REPO')
|
||||
const owner = siteConfig('COMMENT_GITALK_OWNER')
|
||||
const admin = siteConfig('COMMENT_GITALK_ADMIN').split(',')
|
||||
const distractionFreeMode = siteConfig('COMMENT_GITALK_DISTRACTION_FREE_MODE')
|
||||
|
||||
const loadGitalk = async() => {
|
||||
await loadExternalResource(gitalkCSSCDN, 'css')
|
||||
await loadExternalResource(gitalkJSCDN, '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(','),
|
||||
clientID: clientId,
|
||||
clientSecret: clientSecret,
|
||||
repo: repo,
|
||||
owner: owner,
|
||||
admin: admin,
|
||||
id: frontMatter.id, // Ensure uniqueness and length less than 50
|
||||
distractionFreeMode: JSON.parse(BLOG.COMMENT_GITALK_DISTRACTION_FREE_MODE) // Facebook-like distraction free mode
|
||||
distractionFreeMode: distractionFreeMode // Facebook-like distraction free mode
|
||||
})
|
||||
|
||||
gitalk.render('gitalk-container')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadGitalk()
|
||||
}, [])
|
||||
|
||||
20
components/GlobalStyle.js
Normal file
20
components/GlobalStyle.js
Normal file
@@ -0,0 +1,20 @@
|
||||
/* eslint-disable react/no-unknown-property */
|
||||
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
/**
|
||||
* 这里的css样式对全局生效
|
||||
* 主题客制化css
|
||||
* @returns
|
||||
*/
|
||||
const GlobalStyle = () => {
|
||||
// 从NotionConfig中读取样式
|
||||
const GLOBAL_CSS = siteConfig('GLOBAL_CSS')
|
||||
return (<style jsx global>{`
|
||||
|
||||
${GLOBAL_CSS}
|
||||
|
||||
`}</style>)
|
||||
}
|
||||
|
||||
export { GlobalStyle }
|
||||
@@ -1,4 +1,4 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
@@ -9,7 +9,7 @@ import { useEffect } from 'react'
|
||||
*/
|
||||
export default function GoogleAdsense() {
|
||||
const initGoogleAdsense = () => {
|
||||
loadExternalResource(`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${BLOG.ADSENSE_GOOGLE_ID}`, 'js').then(url => {
|
||||
loadExternalResource(`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${siteConfig('ADSENSE_GOOGLE_ID')}`, 'js').then(url => {
|
||||
setTimeout(() => {
|
||||
const ads = document.getElementsByClassName('adsbygoogle')
|
||||
const adsbygoogle = window.adsbygoogle
|
||||
@@ -44,7 +44,7 @@ export default function GoogleAdsense() {
|
||||
* 添加 可以在本地调试
|
||||
*/
|
||||
const AdSlot = ({ type = 'show' }) => {
|
||||
if (!BLOG.ADSENSE_GOOGLE_ID) {
|
||||
if (!siteConfig('ADSENSE_GOOGLE_ID')) {
|
||||
return null
|
||||
}
|
||||
// 文章内嵌广告
|
||||
@@ -53,9 +53,9 @@ const AdSlot = ({ type = 'show' }) => {
|
||||
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>
|
||||
data-adtest={siteConfig('ADSENSE_GOOGLE_TEST') ? 'on' : 'off'}
|
||||
data-ad-client={siteConfig('ADSENSE_GOOGLE_ID')}
|
||||
data-ad-slot={siteConfig('ADSENSE_GOOGLE_SLOT_IN_ARTICLE')}></ins>
|
||||
}
|
||||
|
||||
// 信息流广告
|
||||
@@ -64,9 +64,9 @@ const AdSlot = ({ type = 'show' }) => {
|
||||
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>
|
||||
data-adtest={siteConfig('ADSENSE_GOOGLE_TEST') ? 'on' : 'off'}
|
||||
data-ad-client={siteConfig('ADSENSE_GOOGLE_ID')}
|
||||
data-ad-slot={siteConfig('ADSENSE_GOOGLE_SLOT_FLOW')}></ins>
|
||||
}
|
||||
|
||||
// 原生广告
|
||||
@@ -74,17 +74,17 @@ const AdSlot = ({ type = 'show' }) => {
|
||||
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>
|
||||
data-adtest={siteConfig('ADSENSE_GOOGLE_TEST') ? 'on' : 'off'}
|
||||
data-ad-client={siteConfig('ADSENSE_GOOGLE_ID')}
|
||||
data-ad-slot={siteConfig('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-client={siteConfig('ADSENSE_GOOGLE_ID')}
|
||||
data-adtest={siteConfig('ADSENSE_GOOGLE_TEST') ? 'on' : 'off'}
|
||||
data-ad-slot={siteConfig('ADSENSE_GOOGLE_SLOT_AUTO')}
|
||||
data-ad-format="auto"
|
||||
data-full-width-responsive="true"></ins>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import KaTeX from 'katex'
|
||||
import React from 'react'
|
||||
import { memo, useEffect, useState } from 'react'
|
||||
|
||||
/**
|
||||
* 数学公式
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const TeX = ({
|
||||
children,
|
||||
math,
|
||||
@@ -13,9 +18,9 @@ const TeX = ({
|
||||
}) => {
|
||||
const Component = asComponent || (block ? 'div' : 'span')
|
||||
const content = (children ?? math)
|
||||
const [state, setState] = React.useState({ innerHtml: '' })
|
||||
const [state, setState] = useState({ innerHtml: '' })
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
try {
|
||||
const innerHtml = KaTeX.renderToString(content, {
|
||||
displayMode: true,
|
||||
@@ -50,4 +55,4 @@ const TeX = ({
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(TeX)
|
||||
export default memo(TeX)
|
||||
|
||||
18
components/LA51.js
Normal file
18
components/LA51.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
/**
|
||||
* 51LA统计
|
||||
*/
|
||||
export default function LA51() {
|
||||
const ANALYTICS_51LA_ID = siteConfig('ANALYTICS_51LA_ID')
|
||||
const ANALYTICS_51LA_CK = siteConfig('ANALYTICS_51LA_CK')
|
||||
useEffect(() => {
|
||||
const LA = window.LA
|
||||
if (LA) {
|
||||
LA.init({ id: `${ANALYTICS_51LA_ID}`, ck: `${ANALYTICS_51LA_CK}`, hashMode: true, autoTrack: true })
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <></>
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import Head from 'next/head'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function LazyImage({
|
||||
id,
|
||||
src,
|
||||
alt,
|
||||
placeholderSrc = BLOG.IMG_LAZY_LOAD_PLACEHOLDER,
|
||||
placeholderSrc,
|
||||
className,
|
||||
width,
|
||||
height,
|
||||
@@ -22,6 +22,9 @@ export default function LazyImage({
|
||||
}) {
|
||||
const imageRef = useRef(null)
|
||||
const [imageLoaded, setImageLoaded] = useState(false)
|
||||
if (!placeholderSrc) {
|
||||
placeholderSrc = siteConfig('IMG_LAZY_LOAD_PLACEHOLDER')
|
||||
}
|
||||
|
||||
const handleImageLoad = () => {
|
||||
setImageLoaded(true)
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
/* eslint-disable no-undef */
|
||||
import BLOG from '@/blog.config'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
/**
|
||||
* 网页动画
|
||||
* @returns
|
||||
*/
|
||||
export default function Live2D() {
|
||||
const { theme, switchTheme } = useGlobal()
|
||||
const showPet = JSON.parse(BLOG.WIDGET_PET)
|
||||
const showPet = JSON.parse(siteConfig('WIDGET_PET'))
|
||||
const petLink = siteConfig('WIDGET_PET_LINK')
|
||||
|
||||
useEffect(() => {
|
||||
if (showPet) {
|
||||
@@ -16,7 +21,7 @@ export default function Live2D() {
|
||||
if (typeof window?.loadlive2d !== 'undefined') {
|
||||
// https://github.com/xiazeyu/live2d-widget-models
|
||||
try {
|
||||
loadlive2d('live2d', BLOG.WIDGET_PET_LINK)
|
||||
loadlive2d('live2d', petLink)
|
||||
} catch (error) {
|
||||
console.error('读取PET模型', error)
|
||||
}
|
||||
@@ -26,7 +31,7 @@ export default function Live2D() {
|
||||
}, [theme])
|
||||
|
||||
function handleClick() {
|
||||
if (JSON.parse(BLOG.WIDGET_PET_SWITCH_THEME)) {
|
||||
if (JSON.parse(siteConfig('WIDGET_PET_SWITCH_THEME'))) {
|
||||
switchTheme()
|
||||
}
|
||||
}
|
||||
|
||||
29
components/LoadingProgress.js
Normal file
29
components/LoadingProgress.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import NProgress from 'nprogress'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
/**
|
||||
* 出现页面加载进度条
|
||||
*/
|
||||
export default function LoadingProgress() {
|
||||
const router = useRouter()
|
||||
// 加载进度条
|
||||
useEffect(() => {
|
||||
const handleStart = (url) => {
|
||||
NProgress.start()
|
||||
}
|
||||
|
||||
const handleStop = () => {
|
||||
NProgress.done()
|
||||
}
|
||||
|
||||
router.events.on('routeChangeStart', handleStart)
|
||||
router.events.on('routeChangeError', handleStop)
|
||||
router.events.on('routeChangeComplete', handleStop)
|
||||
return () => {
|
||||
router.events.off('routeChangeStart', handleStart)
|
||||
router.events.off('routeChangeComplete', handleStop)
|
||||
router.events.off('routeChangeError', handleStop)
|
||||
}
|
||||
}, [router])
|
||||
}
|
||||
@@ -1,14 +1,18 @@
|
||||
import { NotionRenderer } from 'react-notion-x'
|
||||
import dynamic from 'next/dynamic'
|
||||
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 { useEffect, useRef } from 'react'
|
||||
import 'katex/dist/katex.min.css'
|
||||
import { mapImgUrl } from '@/lib/notion/mapImage'
|
||||
import { isBrowser } from '@/lib/utils'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { NotionRenderer } from 'react-notion-x'
|
||||
|
||||
// Notion渲染
|
||||
// const NotionRenderer = dynamic(() => import('react-notion-x').then(async (m) => {
|
||||
// return m.NotionRenderer
|
||||
// }), {
|
||||
// ssr: false
|
||||
// })
|
||||
|
||||
const Code = dynamic(() =>
|
||||
import('react-notion-x/build/third-party/code').then(async (m) => {
|
||||
@@ -16,6 +20,7 @@ const Code = dynamic(() =>
|
||||
}), { ssr: false }
|
||||
)
|
||||
|
||||
// 公式
|
||||
const Equation = dynamic(() =>
|
||||
import('@/components/Equation').then(async (m) => {
|
||||
// 化学方程式
|
||||
@@ -37,6 +42,13 @@ const PrismMac = dynamic(() => import('@/components/PrismMac'), {
|
||||
ssr: false
|
||||
})
|
||||
|
||||
/**
|
||||
* tweet嵌入
|
||||
*/
|
||||
const TweetEmbed = dynamic(() => import('react-tweet-embed'), {
|
||||
ssr: false
|
||||
})
|
||||
|
||||
const Collection = dynamic(() =>
|
||||
import('react-notion-x/build/third-party/collection').then((m) => m.Collection), { ssr: true }
|
||||
)
|
||||
@@ -63,7 +75,7 @@ const NotionPage = ({ post, className }) => {
|
||||
|
||||
useEffect(() => {
|
||||
// 将相册gallery下的图片加入放大功能
|
||||
if (JSON.parse(BLOG.POST_DISABLE_GALLERY_CLICK)) {
|
||||
if (siteConfig('POST_DISABLE_GALLERY_CLICK')) {
|
||||
setTimeout(() => {
|
||||
if (isBrowser) {
|
||||
const imgList = document?.querySelectorAll('.notion-collection-card-cover img')
|
||||
|
||||
@@ -1,27 +1,54 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
/**
|
||||
* 音乐播放器
|
||||
* @returns
|
||||
*/
|
||||
const Player = () => {
|
||||
const [player, setPlayer] = useState()
|
||||
const ref = useRef(null)
|
||||
const lrcType = JSON.parse(siteConfig('MUSIC_PLAYER_LRC_TYPE'))
|
||||
const playerVisible = JSON.parse(siteConfig('MUSIC_PLAYER_VISIBLE'))
|
||||
const autoPlay = JSON.parse(siteConfig('MUSIC_PLAYER_AUTO_PLAY'))
|
||||
const meting = JSON.parse(siteConfig('MUSIC_PLAYER_METING'))
|
||||
const order = siteConfig('MUSIC_PLAYER_ORDER')
|
||||
const audio = siteConfig('MUSIC_PLAYER_AUDIO_LIST')
|
||||
|
||||
const lrcType = JSON.parse(BLOG.MUSIC_PLAYER_LRC_TYPE)
|
||||
const playerVisible = JSON.parse(BLOG.MUSIC_PLAYER_VISIBLE)
|
||||
const autoPlay = JSON.parse(BLOG.MUSIC_PLAYER_AUTO_PLAY)
|
||||
const musicPlayerEnable = siteConfig('MUSIC_PLAYER')
|
||||
const musicPlayerCDN = siteConfig('MUSIC_PLAYER_CDN_URL')
|
||||
const musicMetingEnable = siteConfig('MUSIC_PLAYER_METING')
|
||||
const musicMetingCDNUrl = siteConfig('MUSIC_PLAYER_METING_CDN_URL', 'https://cdnjs.cloudflare.com/ajax/libs/meting/2.0.1/Meting.min.js')
|
||||
|
||||
const meting = JSON.parse(BLOG.MUSIC_PLAYER_METING)
|
||||
const initMusicPlayer = async () => {
|
||||
if (!musicPlayerEnable) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await loadExternalResource(musicPlayerCDN, 'js')
|
||||
} catch (error) {
|
||||
console.error('音乐组件异常', error)
|
||||
}
|
||||
|
||||
if (musicMetingEnable) {
|
||||
await loadExternalResource(musicMetingCDNUrl, 'js')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!meting && window.APlayer) {
|
||||
setPlayer(new window.APlayer({
|
||||
container: ref.current,
|
||||
fixed: true,
|
||||
lrcType: lrcType,
|
||||
autoplay: autoPlay,
|
||||
order: BLOG.MUSIC_PLAYER_ORDER,
|
||||
audio: BLOG.MUSIC_PLAYER_AUDIO_LIST
|
||||
order: order,
|
||||
audio: audio
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
initMusicPlayer()
|
||||
return () => {
|
||||
setPlayer(undefined)
|
||||
}
|
||||
@@ -39,11 +66,11 @@ const Player = () => {
|
||||
fixed="true"
|
||||
type="playlist"
|
||||
preload="auto"
|
||||
lrc-type={BLOG.MUSIC_PLAYER_METING_LRC_TYPE}
|
||||
lrc-type={siteConfig('MUSIC_PLAYER_METING_LRC_TYPE')}
|
||||
autoplay={autoPlay}
|
||||
order={BLOG.MUSIC_PLAYER_ORDER}
|
||||
server={BLOG.MUSIC_PLAYER_METING_SERVER}
|
||||
id={BLOG.MUSIC_PLAYER_METING_ID}
|
||||
order={siteConfig('MUSIC_PLAYER_ORDER')}
|
||||
server={siteConfig('MUSIC_PLAYER_METING_SERVER')}
|
||||
id={siteConfig('MUSIC_PLAYER_METING_ID')}
|
||||
/>
|
||||
: <div ref={ref} data-player={player} />
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@ import 'prismjs/plugins/line-numbers/prism-line-numbers'
|
||||
import 'prismjs/plugins/line-numbers/prism-line-numbers.css'
|
||||
|
||||
// mermaid图
|
||||
import BLOG from '@/blog.config'
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
/**
|
||||
* 代码美化相关
|
||||
@@ -23,22 +23,36 @@ import { useGlobal } from '@/lib/global'
|
||||
const PrismMac = () => {
|
||||
const router = useRouter()
|
||||
const { isDarkMode } = useGlobal()
|
||||
const codeMacBar = siteConfig('CODE_MAC_BAR')
|
||||
const prismjsAutoLoader = siteConfig('PRISM_JS_AUTO_LOADER')
|
||||
const prismjsPath = siteConfig('PRISM_JS_PATH')
|
||||
|
||||
const prismThemeSwitch = siteConfig('PRISM_THEME_SWITCH')
|
||||
const prismThemeDarkPath = siteConfig('PRISM_THEME_DARK_PATH')
|
||||
const prismThemeLightPath = siteConfig('PRISM_THEME_LIGHT_PATH')
|
||||
const prismThemePrefixPath = siteConfig('PRISM_THEME_PREFIX_PATH')
|
||||
|
||||
const mermaidCDN = siteConfig('MERMAID_CDN')
|
||||
const codeLineNumbers = siteConfig('CODE_LINE_NUMBERS')
|
||||
|
||||
const codeCollapse = siteConfig('CODE_COLLAPSE')
|
||||
const codeCollapseExpandDefault = siteConfig('CODE_COLLAPSE_EXPAND_DEFAULT')
|
||||
|
||||
useEffect(() => {
|
||||
if (JSON.parse(BLOG.CODE_MAC_BAR)) {
|
||||
if (codeMacBar) {
|
||||
loadExternalResource('/css/prism-mac-style.css', 'css')
|
||||
}
|
||||
// 加载prism样式
|
||||
loadPrismThemeCSS(isDarkMode)
|
||||
loadPrismThemeCSS(isDarkMode, prismThemeSwitch, prismThemeDarkPath, prismThemeLightPath, prismThemePrefixPath)
|
||||
// 折叠代码
|
||||
loadExternalResource(BLOG.PRISM_JS_AUTO_LOADER, 'js').then((url) => {
|
||||
loadExternalResource(prismjsAutoLoader, 'js').then((url) => {
|
||||
if (window?.Prism?.plugins?.autoloader) {
|
||||
window.Prism.plugins.autoloader.languages_path = BLOG.PRISM_JS_PATH
|
||||
window.Prism.plugins.autoloader.languages_path = prismjsPath
|
||||
}
|
||||
|
||||
renderPrismMac()
|
||||
renderMermaid()
|
||||
renderCollapseCode()
|
||||
renderPrismMac(codeLineNumbers)
|
||||
renderMermaid(mermaidCDN)
|
||||
renderCollapseCode(codeCollapse, codeCollapseExpandDefault)
|
||||
})
|
||||
}, [router, isDarkMode])
|
||||
|
||||
@@ -46,18 +60,18 @@ const PrismMac = () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载样式
|
||||
* 加载Prism主题样式
|
||||
*/
|
||||
const loadPrismThemeCSS = (isDarkMode) => {
|
||||
const loadPrismThemeCSS = (isDarkMode, prismThemeSwitch, prismThemeDarkPath, prismThemeLightPath, prismThemePrefixPath) => {
|
||||
let PRISM_THEME
|
||||
let PRISM_PREVIOUS
|
||||
if (JSON.parse(BLOG.PRISM_THEME_SWITCH)) {
|
||||
if (prismThemeSwitch) {
|
||||
if (isDarkMode) {
|
||||
PRISM_THEME = BLOG.PRISM_THEME_DARK_PATH
|
||||
PRISM_PREVIOUS = BLOG.PRISM_THEME_LIGHT_PATH
|
||||
PRISM_THEME = prismThemeDarkPath
|
||||
PRISM_PREVIOUS = prismThemeLightPath
|
||||
} else {
|
||||
PRISM_THEME = BLOG.PRISM_THEME_LIGHT_PATH
|
||||
PRISM_PREVIOUS = BLOG.PRISM_THEME_DARK_PATH
|
||||
PRISM_THEME = prismThemeLightPath
|
||||
PRISM_PREVIOUS = prismThemeDarkPath
|
||||
}
|
||||
const previousTheme = document.querySelector(`link[href="${PRISM_PREVIOUS}"]`)
|
||||
if (previousTheme) {
|
||||
@@ -65,15 +79,15 @@ const loadPrismThemeCSS = (isDarkMode) => {
|
||||
}
|
||||
loadExternalResource(PRISM_THEME, 'css')
|
||||
} else {
|
||||
loadExternalResource(BLOG.PRISM_THEME_PREFIX_PATH, 'css')
|
||||
loadExternalResource(prismThemePrefixPath, 'css')
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 将代码块转为可折叠对象
|
||||
*/
|
||||
const renderCollapseCode = () => {
|
||||
if (!JSON.parse(BLOG.CODE_COLLAPSE)) {
|
||||
const renderCollapseCode = (codeCollapse, codeCollapseExpandDefault) => {
|
||||
if (!codeCollapse) {
|
||||
return
|
||||
}
|
||||
const codeBlocks = document.querySelectorAll('.code-toolbar')
|
||||
@@ -116,7 +130,7 @@ const renderCollapseCode = () => {
|
||||
// 点击后折叠展开代码
|
||||
header.addEventListener('click', collapseCode)
|
||||
// 是否自动展开
|
||||
if (JSON.parse(BLOG.CODE_COLLAPSE_EXPAND_DEFAULT)) {
|
||||
if (codeCollapseExpandDefault) {
|
||||
header.click()
|
||||
}
|
||||
}
|
||||
@@ -125,7 +139,7 @@ const renderCollapseCode = () => {
|
||||
/**
|
||||
* 将mermaid语言 渲染成图片
|
||||
*/
|
||||
const renderMermaid = async() => {
|
||||
const renderMermaid = async(mermaidCDN) => {
|
||||
const observer = new MutationObserver(async mutationsList => {
|
||||
for (const m of mutationsList) {
|
||||
if (m.target.className === 'notion-code language-mermaid') {
|
||||
@@ -146,7 +160,7 @@ const renderMermaid = async() => {
|
||||
}
|
||||
}
|
||||
if (needLoad) {
|
||||
loadExternalResource(BLOG.MERMAID_CDN, 'js').then(url => {
|
||||
loadExternalResource(mermaidCDN, 'js').then(url => {
|
||||
setTimeout(() => {
|
||||
const mermaid = window.mermaid
|
||||
mermaid?.contentLoaded()
|
||||
@@ -162,11 +176,11 @@ const renderMermaid = async() => {
|
||||
}
|
||||
}
|
||||
|
||||
function renderPrismMac() {
|
||||
function renderPrismMac(codeLineNumbers) {
|
||||
const container = document?.getElementById('notion-article')
|
||||
|
||||
// Add line numbers
|
||||
if (JSON.parse(BLOG.CODE_LINE_NUMBERS)) {
|
||||
if (codeLineNumbers) {
|
||||
const codeBlocks = container?.getElementsByTagName('pre')
|
||||
if (codeBlocks) {
|
||||
Array.from(codeBlocks).forEach(item => {
|
||||
@@ -200,7 +214,7 @@ function renderPrismMac() {
|
||||
}
|
||||
|
||||
// 折叠代码行号bug
|
||||
if (JSON.parse(BLOG.CODE_LINE_NUMBERS)) {
|
||||
if (codeLineNumbers) {
|
||||
fixCodeLineStyle()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
@@ -6,22 +6,26 @@ import { useEffect } from 'react'
|
||||
* 二维码生成
|
||||
*/
|
||||
export default function QrCode({ value }) {
|
||||
const qrCodeCDN = siteConfig('QR_CODE_CDN')
|
||||
|
||||
useEffect(() => {
|
||||
let qrcode
|
||||
if (!value) {
|
||||
return
|
||||
}
|
||||
loadExternalResource(BLOG.QR_CODE_CDN, 'js').then(url => {
|
||||
const QRCode = window.QRCode
|
||||
qrcode = new QRCode(document.getElementById('qrcode'), {
|
||||
text: value,
|
||||
width: 256,
|
||||
height: 256,
|
||||
colorDark: '#000000',
|
||||
colorLight: '#ffffff',
|
||||
correctLevel: QRCode.CorrectLevel.H
|
||||
})
|
||||
// console.log('二维码', qrcode, value)
|
||||
loadExternalResource(qrCodeCDN, 'js').then(url => {
|
||||
const QRCode = window?.QRCode
|
||||
if (typeof QRCode !== 'undefined') {
|
||||
qrcode = new QRCode(document.getElementById('qrcode'), {
|
||||
text: value,
|
||||
width: 256,
|
||||
height: 256,
|
||||
colorDark: '#000000',
|
||||
colorLight: '#ffffff',
|
||||
correctLevel: QRCode.CorrectLevel.H
|
||||
})
|
||||
// console.log('二维码', qrcode, value)
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
if (qrcode) {
|
||||
|
||||
@@ -1,28 +1,21 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import ShareButtons from './ShareButtons'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const ShareButtons = dynamic(() => import('@/components/ShareButtons'), { ssr: false })
|
||||
|
||||
/**
|
||||
* 分享栏
|
||||
* @param {} param0
|
||||
* @returns
|
||||
*/
|
||||
const ShareBar = ({ post }) => {
|
||||
const router = useRouter()
|
||||
|
||||
if (!JSON.parse(BLOG.POST_SHARE_BAR_ENABLE) || !post || post?.type !== 'Post') {
|
||||
if (!JSON.parse(siteConfig('POST_SHARE_BAR_ENABLE')) || !post || post?.type !== 'Post') {
|
||||
return <></>
|
||||
}
|
||||
|
||||
const shareUrl = BLOG.LINK + router.asPath
|
||||
|
||||
return <div className='m-1 overflow-x-auto'>
|
||||
<div className='flex w-full md:justify-end'>
|
||||
<ShareButtons shareUrl={shareUrl} title={post.title} image={post.pageCover} body={
|
||||
post?.title +
|
||||
' | ' +
|
||||
BLOG.TITLE +
|
||||
' ' +
|
||||
shareUrl +
|
||||
' ' +
|
||||
post?.summary
|
||||
} />
|
||||
<ShareButtons post={post} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState } from 'react'
|
||||
|
||||
import {
|
||||
@@ -56,9 +57,15 @@ const QrCode = dynamic(() => import('@/components/QrCode'), { ssr: false })
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const ShareButtons = ({ shareUrl, title, body, image }) => {
|
||||
const services = BLOG.POSTS_SHARE_SERVICES.split(',')
|
||||
const titleWithSiteInfo = title + ' | ' + BLOG.TITLE
|
||||
const ShareButtons = ({ post }) => {
|
||||
const router = useRouter()
|
||||
const shareUrl = siteConfig('LINK') + router.asPath
|
||||
const title = post.title || siteConfig('TITLE')
|
||||
const image = post.pageCover
|
||||
const body = post?.title + ' | ' + title + ' ' + shareUrl + ' ' + post?.summary
|
||||
|
||||
const services = siteConfig('POSTS_SHARE_SERVICES').split(',')
|
||||
const titleWithSiteInfo = title + ' | ' + siteConfig('TITLE')
|
||||
const { locale } = useGlobal()
|
||||
const [qrCodeShow, setQrCodeShow] = useState(false)
|
||||
|
||||
@@ -93,7 +100,7 @@ const ShareButtons = ({ shareUrl, title, body, image }) => {
|
||||
<FacebookMessengerShareButton
|
||||
key={singleService}
|
||||
url={shareUrl}
|
||||
appId={BLOG.FACEBOOK_APP_ID}
|
||||
appId={siteConfig('FACEBOOK_APP_ID')}
|
||||
className="mx-1"
|
||||
>
|
||||
<FacebookMessengerIcon size={32} round />
|
||||
@@ -336,7 +343,7 @@ 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'>
|
||||
<a 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' aria-label="Share by QQ" href={`http://connect.qq.com/widget/shareqq/index.html?url=${shareUrl}&sharesource=qzone&title=${title}&desc=${body}`} >
|
||||
<i className='fab fa-qq w-8' />
|
||||
</a>
|
||||
</button>
|
||||
@@ -349,7 +356,7 @@ const ShareButtons = ({ shareUrl, title, body, image }) => {
|
||||
<div className='absolute'>
|
||||
<div id='pop' className={(qrCodeShow ? 'opacity-100 ' : ' invisible opacity-0') + ' z-40 absolute bottom-10 -left-10 bg-white shadow-xl transition-all duration-200 text-center'}>
|
||||
<div className='p-2 mt-1 w-28 h-28'>
|
||||
<QrCode value={shareUrl}/>
|
||||
{ qrCodeShow && <QrCode value={shareUrl}/> }
|
||||
</div>
|
||||
<span className='text-black font-semibold p-1 rounded-t-lg text-sm mx-auto mb-1'>
|
||||
{locale.COMMON.SCAN_QR_CODE}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useState } from 'react';
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
/**
|
||||
* Tabs切换标签
|
||||
@@ -6,59 +7,39 @@ import React, { useState } from 'react'
|
||||
* @returns
|
||||
*/
|
||||
const Tabs = ({ className, children }) => {
|
||||
const [currentTab, setCurrentTab] = useState(0)
|
||||
const [currentTab, setCurrentTab] = useState(0);
|
||||
|
||||
if (!children) {
|
||||
return <></>
|
||||
const validChildren = children.filter(c => c);
|
||||
|
||||
if (validChildren.length === 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
children = children.filter(c => c !== '')
|
||||
|
||||
let count = 0
|
||||
children.forEach(e => {
|
||||
if (e) {
|
||||
count++
|
||||
}
|
||||
})
|
||||
|
||||
if (count === 0) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
if (count === 1) {
|
||||
return <section className={'duration-200 ' + className}>
|
||||
{children}
|
||||
</section>
|
||||
}
|
||||
|
||||
function tabClickHandle(i) {
|
||||
setCurrentTab(i)
|
||||
}
|
||||
|
||||
return <div className={'mb-5 duration-200 ' + className}>
|
||||
<ul className='flex justify-center space-x-5 pb-4 dark:text-gray-400 text-gray-600 overflow-auto'>
|
||||
{children.map((item, index) => {
|
||||
return <li key={index}
|
||||
className={(currentTab === index ? 'font-black border-b-2 border-red-400 text-red-400 animate__animated animate__jello ' : 'font-extralight cursor-pointer') + ' text-sm font-sans '}
|
||||
onClick={() => {
|
||||
tabClickHandle(index)
|
||||
}}>
|
||||
{item?.key}
|
||||
</li>
|
||||
})}
|
||||
return (
|
||||
<div className={`mb-5 duration-200 ${className}`}>
|
||||
{!(validChildren.length === 1 && siteConfig('COMMENT_HIDE_SINGLE_TAB')) && (
|
||||
<ul className="flex justify-center space-x-5 pb-4 dark:text-gray-400 text-gray-600 overflow-auto">
|
||||
{validChildren.map((item, index) => (
|
||||
<li key={index}
|
||||
className={`${currentTab === index ? 'font-black border-b-2 border-red-600 text-red-600 animate__animated animate__jello' : 'font-extralight cursor-pointer'} text-sm font-sans`}
|
||||
onClick={() => setCurrentTab(index)}>
|
||||
{item.key}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div>
|
||||
{children.map((item, index) => {
|
||||
return <section key={index}
|
||||
data-aos="fade-up"
|
||||
data-aos-duration="300"
|
||||
data-aos-once="true"
|
||||
data-aos-anchor-placement="top-bottom">
|
||||
{currentTab === index && item}
|
||||
</section>
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{/* 标签切换的时候不销毁 DOM 元素,使用 CSS 样式进行隐藏 */}
|
||||
<div>
|
||||
{validChildren.map((item, index) => (
|
||||
<section
|
||||
key={index}
|
||||
className={`${currentTab === index ? 'opacity-100 static h-auto' : 'opacity-0 absolute h-0 pointer-events-none overflow-hidden'}`}>
|
||||
{item}
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Tabs
|
||||
export default Tabs;
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import React, { useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { Draggable } from './Draggable'
|
||||
import { THEMES } from '@/themes/theme'
|
||||
import { useRouter } from 'next/router'
|
||||
import DarkModeButton from './DarkModeButton'
|
||||
import { getQueryParam } from '@/lib/utils'
|
||||
import LANGS from '@/lib/lang'
|
||||
/**
|
||||
*
|
||||
* @returns 主题切换
|
||||
*/
|
||||
const ThemeSwitch = () => {
|
||||
const { theme } = useGlobal()
|
||||
const { theme, lang, changeLang, locale, isDarkMode, toggleDarkMode } = useGlobal()
|
||||
const router = useRouter()
|
||||
const currentTheme = getQueryParam(router.asPath, 'theme') || theme
|
||||
// const currentLang = getQueryParam(router.asPath, 'lang') || lang
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
// 修改当前路径url中的 theme 参数
|
||||
// 例如 http://localhost?theme=hexo 跳转到 http://localhost?theme=newTheme
|
||||
const onSelectChange = (e) => {
|
||||
const onThemeSelectChange = (e) => {
|
||||
setIsLoading(true)
|
||||
const newTheme = e.target.value
|
||||
const query = router.query
|
||||
@@ -25,27 +29,52 @@ const ThemeSwitch = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const onLangSelectChange = (e) => {
|
||||
const newLang = e.target.value
|
||||
changeLang(newLang)
|
||||
}
|
||||
|
||||
return (<>
|
||||
<Draggable>
|
||||
<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'>
|
||||
<div id="draggableBox" style={{ left: '0px', top: '80vh' }} className="fixed group space-y-2 overflow-hidden z-50 p-3 flex flex-col items-start dark:text-white bg-gray-50 dark:bg-black rounded-xl shadow-lg border dark:border-gray-800">
|
||||
{/* 深色按钮 */}
|
||||
<div className="text-sm flex items-center w-0 group-hover:w-32 transition-all duration-200">
|
||||
<DarkModeButton />
|
||||
<div onClick={toggleDarkMode} className='cursor-pointer w-0 group-hover:w-24 transition-all duration-200 overflow-hidden whitespace-nowrap pl-1 h-auto'>{isDarkMode ? locale.MENU.DARK_MODE : locale.MENU.LIGHT_MODE}</div>
|
||||
</div>
|
||||
|
||||
{/* 翻译按钮 */}
|
||||
<div className="text-sm flex items-center group-hover:w-32 transition-all duration-200">
|
||||
<i className="fa-solid fa-language w-5" />
|
||||
<div className='w-0 group-hover:w-24 transition-all duration-200 overflow-hidden'>
|
||||
<select value={lang} onChange={onLangSelectChange} name="themes" className='pl-1 bg-gray-50 dark:bg-black appearance-none outline-none dark:text-white uppercase cursor-pointer'>
|
||||
{Object.keys(LANGS)?.map(t => {
|
||||
return <option key={t} value={t}>{LANGS[t].LOCALE}</option>
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 主题切换按钮 */}
|
||||
<div className="text-sm flex items-center group-hover:w-32 transition-all duration-200">
|
||||
<i className="fa-solid fa-palette w-5" />
|
||||
<div className='w-0 group-hover:w-24 transition-all duration-200 overflow-hidden'>
|
||||
<select value={currentTheme} onChange={onThemeSelectChange} name="themes" className='pl-1 bg-gray-50 dark:bg-black appearance-none outline-none dark:text-white 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>
|
||||
|
||||
{/* 切换主题加载时的全屏遮罩 */}
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
41
components/TianliGPT.js
Normal file
41
components/TianliGPT.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable camelcase */
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
import { useEffect } from 'react'
|
||||
/**
|
||||
* TianliGpt AI文章摘要生成工具 @see https://docs_s.tianli0.top/
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
|
||||
const TianLiGPT = () => {
|
||||
const tianliKey = siteConfig('TianliGPT_KEY')
|
||||
const tianliCss = siteConfig('TianliGPT_CSS')
|
||||
const tianliJs = siteConfig('TianliGPT_JS')
|
||||
|
||||
useEffect(() => {
|
||||
initArtalk()
|
||||
}, [])
|
||||
|
||||
if (!tianliKey) {
|
||||
return null
|
||||
}
|
||||
|
||||
const initArtalk = async () => {
|
||||
console.log('loading tianliGPT', tianliKey, tianliCss, tianliJs)
|
||||
|
||||
if (!tianliKey) {
|
||||
return
|
||||
}
|
||||
await loadExternalResource(tianliCss, 'css')
|
||||
|
||||
window.tianliGPT_postSelector = '#notion-article';
|
||||
window.tianliGPT_key = tianliKey;
|
||||
|
||||
await loadExternalResource(tianliJs, 'js')
|
||||
}
|
||||
return <></>
|
||||
}
|
||||
|
||||
export default TianLiGPT
|
||||
@@ -1,6 +1,6 @@
|
||||
import BLOG from '@/blog.config'
|
||||
// import { loadExternalResource } from '@/lib/utils'
|
||||
import { useEffect } from 'react'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Giscus评论 @see https://giscus.app/zh-CN
|
||||
@@ -10,18 +10,48 @@ import { useEffect } from 'react'
|
||||
*/
|
||||
|
||||
const Twikoo = ({ isDarkMode }) => {
|
||||
const envId = siteConfig('COMMENT_TWIKOO_ENV_ID')
|
||||
const el = siteConfig('COMMENT_TWIKOO_ELEMENT_ID', '#twikoo')
|
||||
const twikooCDNURL = siteConfig('COMMENT_TWIKOO_CDN_URL')
|
||||
const lang = siteConfig('LANG')
|
||||
const [isInit] = useState(useRef(false))
|
||||
|
||||
const loadTwikoo = async () => {
|
||||
try {
|
||||
await loadExternalResource(twikooCDNURL, 'js')
|
||||
const twikoo = window?.twikoo
|
||||
if (
|
||||
typeof twikoo !== 'undefined' &&
|
||||
twikoo &&
|
||||
typeof twikoo.init === 'function'
|
||||
) {
|
||||
twikoo.init({
|
||||
envId: envId, // 腾讯云环境填 envId;Vercel 环境填地址(https://xxx.vercel.app)
|
||||
el: el, // 容器元素
|
||||
lang: lang // 用于手动设定评论区语言,支持的语言列表 https://github.com/imaegoo/twikoo/blob/main/src/client/utils/i18n/index.js
|
||||
// region: 'ap-guangzhou', // 环境地域,默认为 ap-shanghai,腾讯云环境填 ap-shanghai 或 ap-guangzhou;Vercel 环境不填
|
||||
// path: location.pathname, // 用于区分不同文章的自定义 js 路径,如果您的文章路径不是 location.pathname,需传此参数
|
||||
})
|
||||
console.log('twikoo init', twikoo)
|
||||
isInit.current = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('twikoo 加载失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
window?.twikoo?.init({
|
||||
envId: BLOG.COMMENT_TWIKOO_ENV_ID, // 腾讯云环境填 envId;Vercel 环境填地址(https://xxx.vercel.app)
|
||||
el: '#twikoo', // 容器元素
|
||||
lang: BLOG.LANG // 用于手动设定评论区语言,支持的语言列表 https://github.com/imaegoo/twikoo/blob/main/src/client/utils/i18n/index.js
|
||||
// region: 'ap-guangzhou', // 环境地域,默认为 ap-shanghai,腾讯云环境填 ap-shanghai 或 ap-guangzhou;Vercel 环境不填
|
||||
// path: location.pathname, // 用于区分不同文章的自定义 js 路径,如果您的文章路径不是 location.pathname,需传此参数
|
||||
})
|
||||
const interval = setInterval(() => {
|
||||
if (isInit.current) {
|
||||
console.log('twioo init! clear interval')
|
||||
clearInterval(interval)
|
||||
} else {
|
||||
loadTwikoo()
|
||||
}
|
||||
}, 1000)
|
||||
return () => clearInterval(interval)
|
||||
}, [isDarkMode])
|
||||
return (
|
||||
<div id="twikoo"></div>
|
||||
)
|
||||
return <div id="twikoo"></div>
|
||||
}
|
||||
|
||||
export default Twikoo
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
// import twikoo from 'twikoo'
|
||||
|
||||
/**
|
||||
@@ -8,7 +8,7 @@ import BLOG from '@/blog.config'
|
||||
*/
|
||||
|
||||
const TwikooCommentCount = ({ post, className }) => {
|
||||
if (!JSON.parse(BLOG.COMMENT_TWIKOO_COUNT_ENABLE)) {
|
||||
if (!JSON.parse(siteConfig('COMMENT_TWIKOO_COUNT_ENABLE'))) {
|
||||
return null
|
||||
}
|
||||
return <a href={`${post.slug}?target=comment`} className={`mx-1 hidden comment-count-wrapper-${post.id} ${className || ''}`}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
import { useRouter } from 'next/router'
|
||||
@@ -13,16 +13,37 @@ import { useEffect } from 'react'
|
||||
const TwikooCommentCounter = (props) => {
|
||||
let commentsData = []
|
||||
const { theme } = useGlobal()
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
// console.log('路由触发评论计数')
|
||||
if (props?.posts && props?.posts?.length > 0) {
|
||||
fetchTwikooData(props.posts)
|
||||
}
|
||||
}, [router.events])
|
||||
|
||||
// 监控主题变化时的的评论数
|
||||
useEffect(() => {
|
||||
// console.log('主题触发评论计数', commentsData)
|
||||
updateCommentCount()
|
||||
}, [theme])
|
||||
|
||||
const twikooCDNURL = siteConfig('COMMENT_TWIKOO_CDN_URL')
|
||||
const twikooENVID = siteConfig('COMMENT_TWIKOO_ENV_ID')
|
||||
|
||||
/**
|
||||
* 加载外部twikoojs
|
||||
* @param {*} posts
|
||||
*/
|
||||
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')
|
||||
await loadExternalResource(twikooCDNURL, 'js')
|
||||
const twikoo = window.twikoo
|
||||
twikoo.getCommentsCount({
|
||||
envId: BLOG.COMMENT_TWIKOO_ENV_ID, // 环境 ID
|
||||
envId: twikooENVID, // 环境 ID
|
||||
// region: 'ap-guangzhou', // 环境地域,默认为 ap-shanghai,如果您的环境地域不是上海,需传此参数
|
||||
urls: posts?.map(post => post.slug), // 不包含协议、域名、参数的文章路径列表,必传参数
|
||||
includeReply: true // 评论数是否包括回复,默认:false
|
||||
@@ -58,20 +79,7 @@ const TwikooCommentCounter = (props) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
|
||||
/**
|
||||
* 评论插件
|
||||
@@ -9,28 +10,48 @@ import { useEffect } from 'react'
|
||||
* @constructor
|
||||
*/
|
||||
const Utterances = ({ issueTerm, layout }) => {
|
||||
const { isDarkMode } = useGlobal()
|
||||
|
||||
const [isLoading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const theme =
|
||||
BLOG.APPEARANCE === 'auto'
|
||||
? 'preferred-color-scheme'
|
||||
: BLOG.APPEARANCE === 'light'
|
||||
? 'github-light'
|
||||
: 'github-dark'
|
||||
const script = document.createElement('script')
|
||||
const anchor = document.getElementById('comments')
|
||||
script.setAttribute('src', 'https://utteranc.es/client.js')
|
||||
script.setAttribute('crossorigin', 'anonymous')
|
||||
script.setAttribute('async', true)
|
||||
script.setAttribute('repo', BLOG.COMMENT_UTTERRANCES_REPO)
|
||||
script.setAttribute('issue-term', 'title')
|
||||
script.setAttribute('theme', theme)
|
||||
anchor.appendChild(script)
|
||||
const script = document.createElement('script');
|
||||
const anchor = document.getElementById('comments');
|
||||
script.onload = () => setLoading(false);
|
||||
script.setAttribute('src', 'https://utteranc.es/client.js');
|
||||
script.setAttribute('crossorigin', 'anonymous');
|
||||
script.setAttribute('async', true);
|
||||
script.setAttribute('repo', siteConfig('COMMENT_UTTERRANCES_REPO'));
|
||||
script.setAttribute('issue-term', 'title');
|
||||
// 初始主题
|
||||
script.setAttribute('theme', isDarkMode ? 'github-dark' : 'github-light');
|
||||
anchor.appendChild(script);
|
||||
|
||||
return () => {
|
||||
anchor.innerHTML = ''
|
||||
// anchor.innerHTML = ''
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// 直接设置 iframe 的类来改变主题,不重新加载脚本
|
||||
const iframe = document.querySelector('iframe.utterances-frame');
|
||||
if (iframe) {
|
||||
iframe.contentWindow.postMessage({
|
||||
type: 'set-theme',
|
||||
theme: isDarkMode ? 'github-dark' : 'github-light'
|
||||
}, 'https://utteranc.es');
|
||||
}
|
||||
})
|
||||
return <div id="comments" className='utterances' >
|
||||
</div>
|
||||
}, [isDarkMode]);
|
||||
|
||||
return (
|
||||
<div id="comments" className='utterances'>
|
||||
{isLoading && (
|
||||
<div className="flex justify-center items-center m-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-2 border-indigo-400 border-t-transparent"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Utterances
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
const ValineComponent = ({ path }) => {
|
||||
const loadValine = async () => {
|
||||
try {
|
||||
await loadExternalResource(BLOG.COMMENT_VALINE_CDN, 'js')
|
||||
await loadExternalResource(siteConfig('COMMENT_VALINE_CDN'), 'js')
|
||||
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,
|
||||
lang: siteConfig('LANG'), // 用于手动设定评论区语言,支持的语言列表 https://github.com/imaegoo/twikoo/blob/main/src/client/utils/i18n/index.js
|
||||
appId: siteConfig('COMMENT_VALINE_APP_ID'),
|
||||
appKey: siteConfig('COMMENT_VALINE_APP_KEY'),
|
||||
avatar: '',
|
||||
path,
|
||||
recordIP: true,
|
||||
placeholder: BLOG.COMMENT_VALINE_PLACEHOLDER,
|
||||
serverURLs: BLOG.COMMENT_VALINE_SERVER_URLS,
|
||||
placeholder: siteConfig('COMMENT_VALINE_PLACEHOLDER'),
|
||||
serverURLs: siteConfig('COMMENT_VALINE_SERVER_URLS'),
|
||||
visitor: true
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react'
|
||||
import BLOG from '@/blog.config'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
/**
|
||||
* 万维广告插件
|
||||
@@ -8,11 +7,13 @@ import BLOG from '@/blog.config'
|
||||
* @returns {JSX.Element | null} - 返回渲染的 JSX 元素或 null
|
||||
*/
|
||||
export default function WWAds({ orientation = 'vertical', sticky = false, className }) {
|
||||
if (!JSON.parse(BLOG.AD_WWADS_ID)) {
|
||||
const adWWADSId = siteConfig('AD_WWADS_ID')
|
||||
|
||||
if (!adWWADSId) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`wwads-cn ${orientation === 'vertical' ? 'wwads-vertical' : 'wwads-horizontal'} ${sticky ? 'wwads-sticky' : ''} z-10 ${className || ''}`} data-id={BLOG.AD_WWADS_ID}></div>
|
||||
)
|
||||
return <div data-id={adWWADSId} className={`wwads-cn
|
||||
${orientation === 'vertical' ? 'wwads-vertical' : 'wwads-horizontal'}
|
||||
${sticky ? 'wwads-sticky' : ''} z-10 ${className || ''}`} />
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react'
|
||||
import React, { createRef } from 'react'
|
||||
import { init } from '@waline/client'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import '@waline/client/dist/waline.css'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
const path = ''
|
||||
let waline = null
|
||||
@@ -12,7 +12,7 @@ let waline = null
|
||||
* @returns
|
||||
*/
|
||||
const WalineComponent = (props) => {
|
||||
const containerRef = React.createRef()
|
||||
const containerRef = createRef()
|
||||
const router = useRouter()
|
||||
|
||||
const updateWaline = url => {
|
||||
@@ -26,8 +26,8 @@ const WalineComponent = (props) => {
|
||||
waline = init({
|
||||
...props,
|
||||
el: containerRef.current,
|
||||
serverURL: BLOG.COMMENT_WALINE_SERVER_URL,
|
||||
lang: BLOG.lang,
|
||||
serverURL: siteConfig('COMMENT_WALINE_SERVER_URL'),
|
||||
lang: siteConfig('LANG'),
|
||||
reaction: true,
|
||||
dark: 'html.dark',
|
||||
emoji: [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import Image from 'next/image'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
/**
|
||||
* 评论插件
|
||||
@@ -78,7 +78,7 @@ const WebmentionReplies = ({ target }) => {
|
||||
const [mentions, setMentions] = useState([])
|
||||
const fetchMentions = async (target) =>
|
||||
fetch(
|
||||
`https://webmention.io/api/mentions.jf2?per-page=500&target=${encodeURIComponent(target)}&token=${BLOG.COMMENT_WEBMENTION.TOKEN}`
|
||||
`https://webmention.io/api/mentions.jf2?per-page=500&target=${encodeURIComponent(target)}&token=${siteConfig('COMMENT_WEBMENTION_TOKEN')}`
|
||||
).then((response) => (response.json ? response.json() : response))
|
||||
useEffect(() => {
|
||||
async function getMentions() {
|
||||
@@ -137,8 +137,8 @@ const WebmentionReplies = ({ target }) => {
|
||||
|
||||
const WebMentionBlock = ({ frontMatter }) => {
|
||||
const router = useRouter()
|
||||
const url = `https://${BLOG.COMMENT_WEBMENTION.HOSTNAME}${router.asPath}`
|
||||
const tweet = `${frontMatter.title} by @${BLOG.COMMENT_WEBMENTION.TWITTER_USERNAME} ${url}`
|
||||
const url = `https://${siteConfig('COMMENT_WEBMENTION_HOSTNAME')}${router.asPath}`
|
||||
const tweet = `${frontMatter.title} by @${siteConfig('COMMENT_WEBMENTION_TWITTER_USERNAME')} ${url}`
|
||||
|
||||
return (
|
||||
<div className='webmention-block'>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import ExternalScript from './ExternalScript'
|
||||
|
||||
/**
|
||||
@@ -10,8 +10,8 @@ export default function WebWhiz() {
|
||||
const props = {
|
||||
id: '__webwhizSdk__',
|
||||
src: 'https://www.unpkg.com/webwhiz@1.0.0/dist/sdk.js',
|
||||
baseUrl: BLOG.WEB_WHIZ_BASE_URL,
|
||||
chatbotId: BLOG.WEB_WHIZ_CHAT_BOT_ID
|
||||
baseUrl: siteConfig('WEB_WHIZ_BASE_URL'),
|
||||
chatbotId: siteConfig('WEB_WHIZ_CHAT_BOT_ID')
|
||||
}
|
||||
return <ExternalScript {...props}/>
|
||||
}
|
||||
|
||||
33
hooks/useAdjustStyle.js
Normal file
33
hooks/useAdjustStyle.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { isBrowser } from '@/lib/utils';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const useAdjustStyle = () => {
|
||||
/**
|
||||
* 避免 callout 含有图片时溢出撑开父容器
|
||||
*/
|
||||
const adjustCalloutImg = () => {
|
||||
const callOuts = document.querySelectorAll('.notion-callout-text');
|
||||
callOuts.forEach((callout) => {
|
||||
const images = callout.querySelectorAll('figure.notion-asset-wrapper.notion-asset-wrapper-image > div');
|
||||
const calloutWidth = callout.offsetWidth;
|
||||
images.forEach((container) => {
|
||||
const imageWidth = container.offsetWidth;
|
||||
if (imageWidth + 50 > calloutWidth) {
|
||||
container.style.setProperty('width', '100%');
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isBrowser) {
|
||||
adjustCalloutImg();
|
||||
window.addEventListener('resize', adjustCalloutImg);
|
||||
return () => {
|
||||
window.removeEventListener('resize', adjustCalloutImg);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
};
|
||||
|
||||
export default useAdjustStyle;
|
||||
@@ -42,7 +42,7 @@ const uploadDataToAlgolia = async(post) => {
|
||||
if (!existed || !existed?.lastEditedDate || !existed?.lastIndexDate) {
|
||||
needUpdateIndex = true
|
||||
} else {
|
||||
const lastEditedDate = new Date(existed.lastEditedDate)
|
||||
const lastEditedDate = new Date(post.lastEditedDate)
|
||||
const lastIndexDate = new Date(existed.lastIndexDate)
|
||||
if (lastEditedDate.getTime() > lastIndexDate.getTime()) {
|
||||
needUpdateIndex = true
|
||||
|
||||
35
lib/cache/cache_manager.js
vendored
35
lib/cache/cache_manager.js
vendored
@@ -3,27 +3,18 @@ import FileCache from './local_file_cache'
|
||||
import MongoCache from './mongo_db_cache'
|
||||
import BLOG from '@/blog.config'
|
||||
|
||||
let api
|
||||
if (process.env.MONGO_DB_URL && process.env.MONGO_DB_NAME) {
|
||||
api = MongoCache
|
||||
} else if (process.env.ENABLE_FILE_CACHE) {
|
||||
api = FileCache
|
||||
} else {
|
||||
api = MemoryCache
|
||||
}
|
||||
|
||||
/**
|
||||
* 为减少频繁接口请求,notion数据将被缓存
|
||||
* @param {*} key
|
||||
* @returns
|
||||
*/
|
||||
export async function getDataFromCache(key, force) {
|
||||
if (BLOG.ENABLE_CACHE || force) {
|
||||
const dataFromCache = await api.getCache(key)
|
||||
if (JSON.parse(BLOG.ENABLE_CACHE) || force) {
|
||||
const dataFromCache = await getApi().getCache(key)
|
||||
if (JSON.stringify(dataFromCache) === '[]') {
|
||||
return null
|
||||
}
|
||||
return api.getCache(key)
|
||||
return getApi().getCache(key)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
@@ -33,12 +24,26 @@ export async function setDataToCache(key, data) {
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
await api.setCache(key, data)
|
||||
await getApi().setCache(key, data)
|
||||
}
|
||||
|
||||
export async function delCacheData(key) {
|
||||
if (!BLOG.ENABLE_CACHE) {
|
||||
if (!JSON.parse(BLOG.ENABLE_CACHE)) {
|
||||
return
|
||||
}
|
||||
await api.delCache(key)
|
||||
await getApi().delCache(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存实现类
|
||||
* @returns
|
||||
*/
|
||||
function getApi() {
|
||||
if (process.env.MONGO_DB_URL && process.env.MONGO_DB_NAME) {
|
||||
return MongoCache
|
||||
} else if (process.env.ENABLE_FILE_CACHE) {
|
||||
return FileCache
|
||||
} else {
|
||||
return MemoryCache
|
||||
}
|
||||
}
|
||||
|
||||
10
lib/cache/local_file_cache.js
vendored
10
lib/cache/local_file_cache.js
vendored
@@ -41,7 +41,7 @@ export async function setCache (key, data) {
|
||||
fs.writeFileSync(jsonFile, JSON.stringify(json))
|
||||
}
|
||||
|
||||
export async function delCache (key, data) {
|
||||
export async function delCache (key) {
|
||||
const exist = await fs.existsSync(jsonFile)
|
||||
const json = exist ? JSON.parse(await fs.readFileSync(jsonFile)) : {}
|
||||
delete json.key
|
||||
@@ -49,4 +49,12 @@ export async function delCache (key, data) {
|
||||
fs.writeFileSync(jsonFile, JSON.stringify(json))
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理缓存
|
||||
*/
|
||||
export async function cleanCache() {
|
||||
const json = {}
|
||||
fs.writeFileSync(jsonFile, JSON.stringify(json))
|
||||
}
|
||||
|
||||
export default { getCache, setCache, delCache }
|
||||
|
||||
96
lib/config.js
Normal file
96
lib/config.js
Normal file
@@ -0,0 +1,96 @@
|
||||
'use client'
|
||||
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from './global'
|
||||
import { deepClone } from './utils'
|
||||
|
||||
/**
|
||||
* 读取配置顺序
|
||||
* 1. 优先读取NotionConfig表
|
||||
* 2. 其次读取环境变量
|
||||
* 3. 再读取blog.config.js / 或各个主题的CONFIG文件
|
||||
* @param {*} key ; 参数名
|
||||
* @param {*} defaultVal ; 参数不存在默认返回值
|
||||
* @param {*} extendConfig ; 参考配置对象{key:val},如果notion中找不到优先尝试在这里面查找
|
||||
* @returns
|
||||
*/
|
||||
export const siteConfig = (key, defaultVal = null, extendConfig = null) => {
|
||||
let global = null
|
||||
try {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
global = useGlobal()
|
||||
} catch (error) {}
|
||||
|
||||
// 首先 配置最优先读取NOTION中的表格配置
|
||||
let val = null
|
||||
let siteInfo = null
|
||||
|
||||
if (global) {
|
||||
val = global.NOTION_CONFIG?.[key]
|
||||
siteInfo = global.siteInfo
|
||||
// console.log('当前变量', key, val)
|
||||
}
|
||||
|
||||
if (!val) {
|
||||
// 这里针对部分key做一些兼容处理
|
||||
switch (key) {
|
||||
case 'HOME_BANNER_IMAGE':
|
||||
val = siteInfo?.pageCover // 封面图取Notion的封面
|
||||
break
|
||||
case 'AVATAR':
|
||||
val = siteInfo?.icon // 封面图取Notion的头像
|
||||
break
|
||||
case 'TITLE':
|
||||
val = siteInfo?.title // 标题取Notion中的标题
|
||||
break
|
||||
case 'DESCRIPTION':
|
||||
val = siteInfo?.description // 标题取Notion中的标题
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 其次 有传入的配置参考,则尝试读取
|
||||
if (!val && extendConfig) {
|
||||
val = extendConfig[key]
|
||||
}
|
||||
|
||||
// 其次 NOTION没有找到配置,则会读取blog.config.js文件
|
||||
if (!val) {
|
||||
val = BLOG[key]
|
||||
}
|
||||
|
||||
if (!val) {
|
||||
return defaultVal
|
||||
} else {
|
||||
if (typeof val === 'string') {
|
||||
if (val === 'true' || val === 'false') {
|
||||
return JSON.parse(val);
|
||||
}
|
||||
return val;
|
||||
} else {
|
||||
try {
|
||||
return JSON.parse(val);
|
||||
} catch (error) {
|
||||
// 如果值是一个字符串但不是有效的 JSON 格式,直接返回字符串
|
||||
return val;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取所有配置
|
||||
* 1. 优先读取NotionConfig表
|
||||
* 2. 其次读取环境变量
|
||||
* 3. 再读取blog.config.js文件
|
||||
* @param {*} key
|
||||
* @returns
|
||||
*/
|
||||
export const siteConfigMap = () => {
|
||||
const val = deepClone(BLOG)
|
||||
for (const key in val) {
|
||||
val[key] = siteConfig(key)
|
||||
// console.log('site', key, val[key], siteConfig(key))
|
||||
}
|
||||
return val
|
||||
}
|
||||
128
lib/global.js
128
lib/global.js
@@ -1,59 +1,27 @@
|
||||
import { generateLocaleDict, initLocale } from './lang'
|
||||
import { generateLocaleDict, initLocale, saveLangToCookies } from './lang'
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import BLOG from '@/blog.config'
|
||||
import { THEMES, initDarkMode } from '@/themes/theme'
|
||||
import NProgress from 'nprogress'
|
||||
import { getQueryVariable, isBrowser } from './utils'
|
||||
|
||||
import { THEMES, initDarkMode, saveDarkModeToCookies } from '@/themes/theme'
|
||||
import { APPEARANCE, LANG, THEME } from 'blog.config'
|
||||
const GlobalContext = createContext()
|
||||
|
||||
/**
|
||||
* 全局变量Provider,包括语言本地化、样式主题、搜索词
|
||||
* 定义全局变量,包括语言、主题、深色模式、加载状态
|
||||
* @param children
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
export function GlobalContextProvider(props) {
|
||||
const { children, siteInfo, categoryOptions, tagOptions } = props
|
||||
const { post, children, siteInfo, categoryOptions, tagOptions, NOTION_CONFIG } = props
|
||||
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 [lang, updateLang] = useState(NOTION_CONFIG?.LANG || LANG) // 默认语言
|
||||
const [locale, updateLocale] = useState(generateLocaleDict(NOTION_CONFIG?.LANG || LANG)) // 默认语言
|
||||
const [theme, setTheme] = useState(NOTION_CONFIG?.THEME || THEME) // 默认博客主题
|
||||
const [isDarkMode, updateDarkMode] = useState(NOTION_CONFIG?.APPEARANCE || APPEARANCE === 'dark') // 默认深色模式
|
||||
const [onLoading, setOnLoading] = useState(false) // 抓取文章数据
|
||||
|
||||
useEffect(() => {
|
||||
initLocale(lang, locale, updateLang, updateLocale)
|
||||
initDarkMode(updateDarkMode)
|
||||
initTheme()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const handleStart = (url) => {
|
||||
NProgress.start()
|
||||
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()
|
||||
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)
|
||||
return () => {
|
||||
router.events.off('routeChangeStart', handleStart)
|
||||
router.events.off('routeChangeComplete', handleStop)
|
||||
router.events.off('routeChangeError', handleStop)
|
||||
}
|
||||
}, [router])
|
||||
// 是否全屏
|
||||
const fullWidth = post?.fullWidth ?? false
|
||||
|
||||
// 切换主题
|
||||
function switchTheme() {
|
||||
@@ -66,10 +34,65 @@ export function GlobalContextProvider(props) {
|
||||
return newTheme
|
||||
}
|
||||
|
||||
// 切换深色模式
|
||||
const toggleDarkMode = () => {
|
||||
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')
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新语言
|
||||
*/
|
||||
function changeLang(lang) {
|
||||
if (lang) {
|
||||
saveLangToCookies(lang)
|
||||
updateLang(lang)
|
||||
updateLocale(generateLocaleDict(lang))
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
initDarkMode(updateDarkMode)
|
||||
initLocale(lang, locale, updateLang, updateLocale)
|
||||
}, [])
|
||||
|
||||
// 加载进度条
|
||||
useEffect(() => {
|
||||
const handleStart = (url) => {
|
||||
const { theme } = router.query
|
||||
if (theme && !url.includes(`theme=${theme}`)) {
|
||||
const newUrl = `${url}${url.includes('?') ? '&' : '?'}theme=${theme}`
|
||||
router.push(newUrl)
|
||||
}
|
||||
setOnLoading(true)
|
||||
}
|
||||
const handleStop = () => {
|
||||
setOnLoading(false)
|
||||
}
|
||||
|
||||
router.events.on('routeChangeStart', handleStart)
|
||||
router.events.on('routeChangeError', handleStop)
|
||||
router.events.on('routeChangeComplete', handleStop)
|
||||
return () => {
|
||||
router.events.off('routeChangeStart', handleStart)
|
||||
router.events.off('routeChangeComplete', handleStop)
|
||||
router.events.off('routeChangeError', handleStop)
|
||||
}
|
||||
}, [router])
|
||||
|
||||
return (
|
||||
<GlobalContext.Provider value={{
|
||||
fullWidth,
|
||||
NOTION_CONFIG,
|
||||
toggleDarkMode,
|
||||
onLoading,
|
||||
setOnLoading,
|
||||
lang,
|
||||
changeLang,
|
||||
locale,
|
||||
updateLocale,
|
||||
isDarkMode,
|
||||
@@ -86,23 +109,4 @@ export function GlobalContextProvider(props) {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换主题时的特殊处理
|
||||
* @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)
|
||||
|
||||
27
lib/lang.js
27
lib/lang.js
@@ -12,7 +12,7 @@ import { getQueryVariable, isBrowser, mergeDeep } from './utils'
|
||||
* 在这里配置所有支持的语言
|
||||
* 国家-地区
|
||||
*/
|
||||
const lang = {
|
||||
const LANGS = {
|
||||
'en-US': enUS,
|
||||
'zh-CN': zhCN,
|
||||
'zh-HK': zhHK,
|
||||
@@ -22,7 +22,7 @@ const lang = {
|
||||
'ja-JP': jaJP
|
||||
}
|
||||
|
||||
export default lang
|
||||
export default LANGS
|
||||
|
||||
/**
|
||||
* 获取当前语言字典
|
||||
@@ -30,33 +30,33 @@ export default lang
|
||||
* @returns 不同语言对应字典
|
||||
*/
|
||||
export function generateLocaleDict(langString) {
|
||||
const supportedLocales = Object.keys(lang)
|
||||
const supportedLocales = Object.keys(LANGS)
|
||||
let userLocale
|
||||
|
||||
// 将语言字符串拆分为语言和地区代码,例如将 "zh-CN" 拆分为 "zh" 和 "CN"
|
||||
const [language, region] = langString.split(/[-_]/)
|
||||
const [language, region] = langString?.split(/[-_]/)
|
||||
|
||||
// 优先匹配语言和地区都匹配的情况
|
||||
const specificLocale = `${language}-${region}`
|
||||
if (supportedLocales.includes(specificLocale)) {
|
||||
userLocale = lang[specificLocale]
|
||||
userLocale = LANGS[specificLocale]
|
||||
}
|
||||
|
||||
// 然后尝试匹配只有语言匹配的情况
|
||||
if (!userLocale) {
|
||||
const languageOnlyLocales = supportedLocales.filter(locale => locale.startsWith(language))
|
||||
if (languageOnlyLocales.length > 0) {
|
||||
userLocale = lang[languageOnlyLocales[0]]
|
||||
userLocale = LANGS[languageOnlyLocales[0]]
|
||||
}
|
||||
}
|
||||
|
||||
// 如果还没匹配到,则返回最接近的语言包
|
||||
if (!userLocale) {
|
||||
const fallbackLocale = supportedLocales.find(locale => locale.startsWith('en'))
|
||||
userLocale = lang[fallbackLocale]
|
||||
userLocale = LANGS[fallbackLocale]
|
||||
}
|
||||
|
||||
return mergeDeep({}, lang['en-US'], userLocale)
|
||||
return mergeDeep({}, LANGS['en-US'], userLocale)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,11 +65,12 @@ export function generateLocaleDict(langString) {
|
||||
*/
|
||||
export function initLocale(lang, locale, changeLang, changeLocale) {
|
||||
if (isBrowser) {
|
||||
const queryLang = getQueryVariable('lang') || loadLangFromCookies() || window.navigator.language
|
||||
const queryLang = getQueryVariable('lang') || loadLangFromCookies()
|
||||
let currentLang = lang
|
||||
if (queryLang !== lang) {
|
||||
if (queryLang && queryLang !== 'undefined' && queryLang !== lang) {
|
||||
currentLang = queryLang
|
||||
}
|
||||
|
||||
changeLang(currentLang)
|
||||
saveLangToCookies(currentLang)
|
||||
|
||||
@@ -88,9 +89,9 @@ export const loadLangFromCookies = () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存语言
|
||||
* @param newTheme
|
||||
*/
|
||||
* 保存语言
|
||||
* @param newTheme
|
||||
*/
|
||||
export const saveLangToCookies = (lang) => {
|
||||
cookie.save('lang', lang, { path: '/' })
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default {
|
||||
LOCALE: 'en-US',
|
||||
LOCALE: 'English',
|
||||
MENU: {
|
||||
WALK_AROUND: 'Walk Around',
|
||||
CATEGORY: 'Category',
|
||||
@@ -41,6 +41,7 @@ export default {
|
||||
ARTICLE_DETAIL: 'Article Details',
|
||||
PASSWORD_ERROR: 'Password Error!',
|
||||
ARTICLE_LOCK_TIPS: 'Please Enter the password:',
|
||||
NO_RESULTS_FOUND: 'No results found.',
|
||||
SUBMIT: 'Submit',
|
||||
POST_TIME: 'Post on',
|
||||
LAST_EDITED_TIME: 'Last edited',
|
||||
@@ -52,8 +53,8 @@ export default {
|
||||
ANNOUNCEMENT: 'Announcement',
|
||||
START_READING: 'Start Reading',
|
||||
MINUTE: 'min',
|
||||
WORD_COUNT: 'W.C.'
|
||||
|
||||
WORD_COUNT: 'Words',
|
||||
READ_TIME: 'Read Time'
|
||||
},
|
||||
PAGINATION: {
|
||||
PREV: 'Prev',
|
||||
@@ -66,5 +67,10 @@ export default {
|
||||
POST: {
|
||||
BACK: 'Back',
|
||||
TOP: 'Top'
|
||||
},
|
||||
MAILCHIMP: {
|
||||
SUBSCRIBE: 'Subscribe',
|
||||
MSG: 'Get the latest news and articles to your inbox every month.',
|
||||
EMAIL: 'Email'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default {
|
||||
LOCALE: 'fr-FR',
|
||||
LOCALE: 'français',
|
||||
NAV: {
|
||||
INDEX: 'Accueil',
|
||||
RSS: 'RSS',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default {
|
||||
LOCALE: 'ja-JP',
|
||||
LOCALE: '日本語',
|
||||
NAV: {
|
||||
INDEX: 'ホーム',
|
||||
RSS: '購読',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default {
|
||||
LOCALE: 'tr-TR',
|
||||
LOCALE: 'Türkçe',
|
||||
NAV: {
|
||||
INDEX: 'Blog',
|
||||
RSS: 'RSS',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default {
|
||||
LOCALE: 'zh-CN',
|
||||
LOCALE: '中文(简体)',
|
||||
MENU: {
|
||||
WALK_AROUND: '随便逛逛',
|
||||
CATEGORY: '博客分类',
|
||||
@@ -40,6 +40,7 @@ export default {
|
||||
VIEWS: '次查看',
|
||||
COPYRIGHT_NOTICE: '本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。',
|
||||
RESULT_OF_SEARCH: '篇搜索到的结果',
|
||||
NO_RESULTS_FOUND: '没有找到文章',
|
||||
ARTICLE_DETAIL: '文章详情',
|
||||
PASSWORD_ERROR: '密码错误!',
|
||||
ARTICLE_LOCK_TIPS: '文章已上锁,请输入访问密码',
|
||||
@@ -54,7 +55,8 @@ export default {
|
||||
ANNOUNCEMENT: '公告',
|
||||
START_READING: '开始阅读',
|
||||
MINUTE: '分钟',
|
||||
WORD_COUNT: '字数'
|
||||
WORD_COUNT: '字数',
|
||||
READ_TIME: '阅读时长'
|
||||
},
|
||||
PAGINATION: {
|
||||
PREV: '上页',
|
||||
@@ -67,5 +69,10 @@ export default {
|
||||
POST: {
|
||||
BACK: '返回上页',
|
||||
TOP: '回到顶部'
|
||||
},
|
||||
MAILCHIMP: {
|
||||
SUBSCRIBE: '邮件订阅',
|
||||
MSG: '订阅以获取每月更新的新闻和文章,直接发送至您的邮箱。',
|
||||
EMAIL: '邮箱'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export default {
|
||||
LOCALE: '中文(繁体香港)',
|
||||
NAV: {
|
||||
INDEX: '網誌',
|
||||
RSS: '訂閱',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default {
|
||||
LOCALE: 'zh-TW',
|
||||
LOCALE: '中文(繁体台湾)',
|
||||
NAV: {
|
||||
INDEX: '部落格',
|
||||
RSS: '訂閱',
|
||||
|
||||
@@ -16,7 +16,6 @@ export async function getNotion(pageId) {
|
||||
}
|
||||
|
||||
const postInfo = blockMap?.block?.[idToUuid(pageId)].value
|
||||
|
||||
return {
|
||||
id: pageId,
|
||||
type: postInfo,
|
||||
@@ -26,7 +25,7 @@ export async function getNotion(pageId) {
|
||||
status: 'Published',
|
||||
createdTime: formatDate(new Date(postInfo.created_time).toString(), BLOG.LANG),
|
||||
lastEditedDay: formatDate(new Date(postInfo?.last_edited_time).toString(), BLOG.LANG),
|
||||
fullWidth: false,
|
||||
fullWidth: postInfo?.fullWidth,
|
||||
page_cover: getPageCover(postInfo),
|
||||
date: { start_date: formatDate(new Date(postInfo?.last_edited_time).toString(), BLOG.LANG) },
|
||||
blockMap
|
||||
|
||||
140
lib/notion/getNotionConfig.js
Normal file
140
lib/notion/getNotionConfig.js
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* 从Notion中读取站点配置;
|
||||
* 在Notion模板中创建一个类型为CONFIG的页面,再添加一个数据库表格,即可用于填写配置
|
||||
* Notion数据库配置优先级最高,将覆盖vercel环境变量以及blog.config.js中的配置
|
||||
* --注意--
|
||||
* 数据库请从模板复制 https://www.notion.so/tanghh/287869a92e3d4d598cf366bd6994755e
|
||||
*
|
||||
*/
|
||||
import { getDateValue, getTextContent } from 'notion-utils'
|
||||
import { getPostBlocks } from './getPostBlocks'
|
||||
import getAllPageIds from './getAllPageIds'
|
||||
|
||||
/**
|
||||
* 从Notion中读取Config配置表
|
||||
* @param {*} allPages
|
||||
* @returns
|
||||
*/
|
||||
export async function getConfigMapFromConfigPage(allPages) {
|
||||
// 默认返回配置文件
|
||||
const notionConfig = {}
|
||||
|
||||
if (!allPages || !Array.isArray(allPages) || allPages.length === 0) {
|
||||
console.warn('[Notion配置] 忽略的配置')
|
||||
return null
|
||||
}
|
||||
const configPage = allPages?.find(post => {
|
||||
return post && post?.type && (post?.type === 'CONFIG' || post?.type === 'config' || post?.type === 'Config')
|
||||
})
|
||||
|
||||
if (!configPage) {
|
||||
console.warn('[Notion配置] 未找到配置页面')
|
||||
return null
|
||||
}
|
||||
const configPageId = configPage.id
|
||||
// console.log('[Notion配置]请求配置数据 ', configPage.id)
|
||||
let pageRecordMap = await getPostBlocks(configPageId, 'config-table')
|
||||
// console.log('配置中心Page', configPageId, pageRecordMap)
|
||||
let content = pageRecordMap.block[configPageId].value.content
|
||||
for (const table of ['Config-Table', 'CONFIG-TABLE']) {
|
||||
if (content) break
|
||||
pageRecordMap = await getPostBlocks(configPageId, table)
|
||||
content = pageRecordMap.block[configPageId].value.content
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
console.warn('[Notion配置] 未找到配置表格', pageRecordMap.block[configPageId], pageRecordMap.block[configPageId].value)
|
||||
return null
|
||||
}
|
||||
|
||||
// 找到配置文件中的database
|
||||
// for (const contentId of content) {
|
||||
// console.log('内容', contentId, configPageRecordMap.block[contentId].value.type === 'collection_view')
|
||||
// }
|
||||
const configTableId = content?.find(contentId => {
|
||||
return pageRecordMap.block[contentId].value.type === 'collection_view'
|
||||
})
|
||||
|
||||
// eslint-disable-next-line no-constant-condition, no-self-compare
|
||||
if (!configTableId) {
|
||||
console.warn('[Notion配置]未找到配置表格数据', pageRecordMap.block[configPageId], pageRecordMap.block[configPageId].value)
|
||||
return null
|
||||
}
|
||||
|
||||
// 页面查找
|
||||
const databaseRecordMap = pageRecordMap.block[configTableId]
|
||||
const block = pageRecordMap.block || {}
|
||||
const rawMetadata = databaseRecordMap.value
|
||||
// Check Type Page-Database和Inline-Database
|
||||
if (
|
||||
rawMetadata?.type !== 'collection_view_page' && rawMetadata?.type !== 'collection_view'
|
||||
) {
|
||||
console.error(`pageId "${configTableId}" is not a database`)
|
||||
return null
|
||||
}
|
||||
// console.log('表格', databaseRecordMap, block, rawMetadata)
|
||||
const collectionId = rawMetadata?.collection_id
|
||||
const collection = pageRecordMap.collection[collectionId].value
|
||||
const collectionQuery = pageRecordMap.collection_query
|
||||
const collectionView = pageRecordMap.collection_view
|
||||
const schema = collection?.schema
|
||||
const viewIds = rawMetadata?.view_ids
|
||||
const pageIds = getAllPageIds(collectionQuery, collectionId, collectionView, viewIds)
|
||||
if (pageIds?.length === 0) {
|
||||
console.error('[Notion配置]获取到的文章列表为空,请检查notion模板', collectionQuery, collection, collectionView, viewIds, databaseRecordMap)
|
||||
}
|
||||
// 遍历用户的表格
|
||||
for (let i = 0; i < pageIds.length; i++) {
|
||||
const id = pageIds[i]
|
||||
const value = block[id]?.value
|
||||
if (!value) {
|
||||
continue
|
||||
}
|
||||
const rawProperties = Object.entries(block?.[id]?.value?.properties || [])
|
||||
const excludeProperties = ['date', 'select', 'multi_select', 'person']
|
||||
const properties = {}
|
||||
for (let i = 0; i < rawProperties.length; i++) {
|
||||
const [key, val] = rawProperties[i]
|
||||
properties.id = id
|
||||
if (schema[key]?.type && !excludeProperties.includes(schema[key].type)) {
|
||||
properties[schema[key].name] = getTextContent(val)
|
||||
} else {
|
||||
switch (schema[key]?.type) {
|
||||
case 'date': {
|
||||
const dateProperty = getDateValue(val)
|
||||
delete dateProperty.type
|
||||
properties[schema[key].name] = dateProperty
|
||||
break
|
||||
}
|
||||
case 'select':
|
||||
case 'multi_select': {
|
||||
const selects = getTextContent(val)
|
||||
if (selects[0]?.length) {
|
||||
properties[schema[key].name] = selects.split(',')
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (properties) {
|
||||
// 将表格中的字段映射成 英文
|
||||
const config = {
|
||||
enable: (properties['启用'] || properties.Enable) === 'Yes',
|
||||
key: properties['配置名'] || properties.Name,
|
||||
value: properties['配置值'] || properties.Value
|
||||
}
|
||||
|
||||
// 只导入生效的配置
|
||||
if (config.enable) {
|
||||
// console.log('[Notion配置]', config.key, config.value)
|
||||
notionConfig[config.key] = config.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return notionConfig
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import getAllPageIds from './getAllPageIds'
|
||||
import { getAllTags } from './getAllTags'
|
||||
import getPageProperties from './getPageProperties'
|
||||
import { mapImgUrl, compressImage } from './mapImage'
|
||||
import { getConfigMapFromConfigPage } from './getNotionConfig'
|
||||
|
||||
/**
|
||||
* 获取博客数据
|
||||
@@ -272,6 +273,7 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
|
||||
|
||||
// 文章计数
|
||||
let postCount = 0
|
||||
|
||||
// 查找所有的Post和Page
|
||||
const allPages = collectionData.filter(post => {
|
||||
if (post?.type === 'Post' && post.status === 'Published') {
|
||||
@@ -282,6 +284,9 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
|
||||
(post?.status === 'Invisible' || post?.status === 'Published')
|
||||
})
|
||||
|
||||
// 站点配置优先读取配置表格,否则读取blog.config.js 文件
|
||||
const NOTION_CONFIG = await getConfigMapFromConfigPage(collectionData) || {}
|
||||
|
||||
// Sort by date
|
||||
if (BLOG.POSTS_SORT_BY === 'date') {
|
||||
allPages.sort((a, b) => {
|
||||
@@ -300,6 +305,7 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
|
||||
const allNavPages = getNavPages({ allPages })
|
||||
|
||||
return {
|
||||
NOTION_CONFIG,
|
||||
notice,
|
||||
siteInfo,
|
||||
allPages,
|
||||
|
||||
@@ -6,6 +6,15 @@ import formatDate from '../formatDate'
|
||||
import md5 from 'js-md5'
|
||||
import { mapImgUrl } from './mapImage'
|
||||
|
||||
/**
|
||||
* 获取页面元素成员属性
|
||||
* @param {*} id
|
||||
* @param {*} block
|
||||
* @param {*} schema
|
||||
* @param {*} authToken
|
||||
* @param {*} tagOptions
|
||||
* @returns
|
||||
*/
|
||||
export default async function getPageProperties(id, block, schema, authToken, tagOptions) {
|
||||
const rawProperties = Object.entries(block?.[id]?.value?.properties || [])
|
||||
const excludeProperties = ['date', 'select', 'multi_select', 'person']
|
||||
@@ -92,11 +101,11 @@ export default async function getPageProperties(id, block, schema, authToken, ta
|
||||
delete properties.content
|
||||
|
||||
// 处理URL
|
||||
if (properties.type === BLOG.NOTION_PROPERTY_NAME.type_post) {
|
||||
if (properties.type === 'Post') {
|
||||
properties.slug = (BLOG.POST_URL_PREFIX) ? generateCustomizeUrl(properties) : (properties.slug ?? properties.id)
|
||||
} else if (properties.type === BLOG.NOTION_PROPERTY_NAME.type_page) {
|
||||
} else if (properties.type === 'Page') {
|
||||
properties.slug = properties.slug ?? properties.id
|
||||
} else if (properties.type === BLOG.NOTION_PROPERTY_NAME.type_menu || properties.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu) {
|
||||
} else if (properties.type === 'Menu' || properties.type === 'SubMenu') {
|
||||
// 菜单路径为空、作为可展开菜单使用
|
||||
properties.to = properties.slug ?? '#'
|
||||
properties.name = properties.title ?? ''
|
||||
@@ -108,6 +117,7 @@ export default async function getPageProperties(id, block, schema, authToken, ta
|
||||
properties.slug += '.html'
|
||||
}
|
||||
}
|
||||
// 密码字段md5
|
||||
properties.password = properties.password ? md5(properties.slug + properties.password) : ''
|
||||
return properties
|
||||
}
|
||||
|
||||
@@ -62,8 +62,10 @@ export async function getPageWithRetry(id, from, retryAttempts = 3) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取到的blockMap删除不需要的字段
|
||||
* 并且对于页面内容进行特殊处理,比如文件url格式化
|
||||
* 获取到的页面BLOCK特殊处理
|
||||
* 1.删除冗余字段
|
||||
* 2.比如文件、视频、音频、url格式化
|
||||
* 3.代码块等元素兼容
|
||||
* @param {*} id 页面ID
|
||||
* @param {*} pageBlock 页面元素
|
||||
* @param {*} slice 截取数量
|
||||
@@ -102,7 +104,10 @@ function filterPostBlocks(id, pageBlock, slice) {
|
||||
}
|
||||
|
||||
// 如果是文件,或嵌入式PDF,需要重新加密签名
|
||||
if ((b?.value?.type === 'file' || b?.value?.type === 'pdf' || b?.value?.type === 'video') && b?.value?.properties?.source?.[0][0]) {
|
||||
if ((b?.value?.type === 'file' || b?.value?.type === 'pdf' || b?.value?.type === 'video' || b?.value?.type === 'audio') &&
|
||||
b?.value?.properties?.source?.[0][0] &&
|
||||
b?.value?.properties?.source?.[0][0].indexOf('amazonaws.com') > 0
|
||||
) {
|
||||
const oldUrl = b?.value?.properties?.source?.[0][0]
|
||||
const newUrl = `https://notion.so/signed/${encodeURIComponent(oldUrl)}?table=block&id=${b?.value?.id}`
|
||||
b.value.properties.source[0][0] = newUrl
|
||||
|
||||
@@ -1,5 +1,81 @@
|
||||
import BLOG from '@/blog.config'
|
||||
|
||||
/**
|
||||
* 图片映射
|
||||
* 1. 如果是 /xx.xx 相对路径格式,则转化为 完整notion域名图片
|
||||
* 2. 如果是 bookmark类型的block 图片封面无需处理
|
||||
* @param {*} img
|
||||
* @param {*} value
|
||||
* @returns
|
||||
*/
|
||||
const mapImgUrl = (img, block, type = 'block', from) => {
|
||||
if (!img) {
|
||||
return null
|
||||
}
|
||||
let ret = null
|
||||
// 相对目录,则视为notion的自带图片
|
||||
if (img.startsWith('/')) {
|
||||
ret = BLOG.NOTION_HOST + img
|
||||
} else {
|
||||
ret = img
|
||||
}
|
||||
|
||||
// Notion 图床转换为永久地址
|
||||
const isNotionSignImg = ret.indexOf('https://www.notion.so/image') !== 0 && (ret.indexOf('secure.notion-static.com') > 0 || ret.indexOf('prod-files-secure') > 0)
|
||||
const isImgBlock = BLOG.IMG_URL_TYPE === 'Notion' || type !== 'block'
|
||||
if (isNotionSignImg && isImgBlock) {
|
||||
ret = BLOG.NOTION_HOST + '/image/' + encodeURIComponent(ret) + '?table=' + type + '&id=' + block.id
|
||||
}
|
||||
|
||||
if (!isEmoji(ret) && ret.indexOf('notion.so/images/page-cover') < 0) {
|
||||
if (BLOG.RANDOM_IMAGE_URL) {
|
||||
// 只有配置了随机图片接口,才会替换图片
|
||||
const texts = BLOG.RANDOM_IMAGE_REPLACE_TEXT
|
||||
let isReplace = false;
|
||||
if (texts) {
|
||||
const textArr = texts.split(',')
|
||||
// 判断是否包含替换的文本
|
||||
textArr.forEach(text => {
|
||||
if (ret.indexOf(text) > -1) {
|
||||
isReplace = true
|
||||
}
|
||||
})
|
||||
} else {
|
||||
isReplace = true
|
||||
}
|
||||
if (isReplace) {
|
||||
ret = BLOG.RANDOM_IMAGE_URL
|
||||
}
|
||||
}
|
||||
|
||||
// 图片url优化,确保每一篇文章的图片url唯一
|
||||
if (ret && ret.length > 4) {
|
||||
// 图片接口拼接唯一识别参数,防止请求的图片被缓,而导致随机结果相同
|
||||
const separator = ret.includes('?') ? '&' : '?'
|
||||
ret = `${ret.trim()}${separator}t=${block.id}`
|
||||
}
|
||||
}
|
||||
|
||||
// 文章封面压缩
|
||||
if (from === 'pageCoverThumbnail' || block.type === 'image') {
|
||||
// 统一压缩图片
|
||||
const width = block?.format?.block_width
|
||||
ret = compressImage(ret, width)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是emoji图标
|
||||
* @param {*} str
|
||||
* @returns
|
||||
*/
|
||||
function isEmoji(str) {
|
||||
const emojiRegex = /[\u{1F300}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F900}-\u{1F9FF}\u{1F018}-\u{1F270}\u{238C}\u{2B06}\u{2B07}\u{2B05}\u{27A1}\u{2194}-\u{2199}\u{2194}\u{21A9}\u{21AA}\u{2934}\u{2935}\u{25AA}\u{25AB}\u{25FE}\u{25FD}\u{25FB}\u{25FC}\u{25B6}\u{25C0}\u{1F200}-\u{1F251}]/u;
|
||||
return emojiRegex.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩图片
|
||||
* 1. Notion图床可以通过指定url-query参数来压缩裁剪图片 例如 ?xx=xx&width=400
|
||||
@@ -11,7 +87,7 @@ const compressImage = (image, width = 800, quality = 50, fmt = 'webp') => {
|
||||
return null
|
||||
}
|
||||
if (image.indexOf(BLOG.NOTION_HOST) === 0 && image.indexOf('amazonaws.com') > 0) {
|
||||
return `${image}&width=${width}`
|
||||
return `${image}&width=${width}&cache=v2`
|
||||
}
|
||||
// 压缩unsplash图片
|
||||
if (image.indexOf('https://images.unsplash.com/') === 0) {
|
||||
@@ -40,51 +116,4 @@ const compressImage = (image, width = 800, quality = 50, fmt = 'webp') => {
|
||||
return image
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片映射
|
||||
* 1. 如果是 /xx.xx 相对路径格式,则转化为 完整notion域名图片
|
||||
* 2. 如果是 bookmark类型的block 图片封面无需处理
|
||||
* @param {*} img
|
||||
* @param {*} value
|
||||
* @returns
|
||||
*/
|
||||
const mapImgUrl = (img, block, type = 'block', from) => {
|
||||
if (!img) {
|
||||
return null
|
||||
}
|
||||
let ret = null
|
||||
// 相对目录,则视为notion的自带图片
|
||||
if (img.startsWith('/')) {
|
||||
ret = BLOG.NOTION_HOST + img
|
||||
} else {
|
||||
ret = img
|
||||
}
|
||||
|
||||
// Notion 图床转换为永久地址
|
||||
const isNotionImg = ret.indexOf('secure.notion-static.com') > 0 || ret.indexOf('prod-files-secure') > 0
|
||||
const isImgBlock = BLOG.IMG_URL_TYPE === 'Notion' || type !== 'block'
|
||||
if (isNotionImg && isImgBlock) {
|
||||
ret = BLOG.NOTION_HOST + '/image/' + encodeURIComponent(ret) + '?table=' + type + '&id=' + block.id
|
||||
}
|
||||
|
||||
if (!isEmoji(ret) && ret.indexOf('notion.so/images/page-cover') < 0) {
|
||||
// 随机图片接口优化 防止因url一致而随机结果相同
|
||||
const separator = ret.includes('?') ? '&' : '?'
|
||||
// 拼接唯一识别参数,防止请求的图片被缓存
|
||||
ret = `${ret.trim()}${separator}t=${block.id}`
|
||||
}
|
||||
|
||||
// 文章封面
|
||||
if (from === 'pageCoverThumbnail') {
|
||||
ret = compressImage(ret)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
function isEmoji(str) {
|
||||
const emojiRegex = /[\u{1F300}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F900}-\u{1F9FF}\u{1F018}-\u{1F270}]/u;
|
||||
return emojiRegex.test(str);
|
||||
}
|
||||
|
||||
export { mapImgUrl, compressImage }
|
||||
|
||||
64
lib/utils.js
64
lib/utils.js
@@ -7,6 +7,20 @@ import { memo } from 'react'
|
||||
*/
|
||||
export const isBrowser = typeof window !== 'undefined'
|
||||
|
||||
/**
|
||||
* google机器人
|
||||
* @returns
|
||||
*/
|
||||
export const isSearchEngineBot = () => {
|
||||
if (typeof navigator === 'undefined') {
|
||||
return false
|
||||
}
|
||||
// 获取用户代理字符串
|
||||
const userAgent = navigator.userAgent;
|
||||
// 使用正则表达式检测是否包含搜索引擎爬虫关键字
|
||||
return /Googlebot|bingbot|Baidu/.test(userAgent)
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件持久化
|
||||
*/
|
||||
@@ -75,7 +89,6 @@ export function getQueryVariable(key) {
|
||||
}
|
||||
return (false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 URL 中指定参数的值
|
||||
* @param {string} url
|
||||
@@ -83,8 +96,10 @@ export function getQueryVariable(key) {
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function getQueryParam(url, param) {
|
||||
const searchParams = new URLSearchParams(url.split('?')[1])
|
||||
return searchParams.get(param)
|
||||
// 移除哈希部分
|
||||
const urlWithoutHash = url.split('#')[0];
|
||||
const searchParams = new URLSearchParams(urlWithoutHash.split('?')[1]);
|
||||
return searchParams.get(param);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,3 +217,46 @@ export const isMobile = () => {
|
||||
|
||||
return isMobile
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描页面上的所有文本节点,将url格式的文本转为可点击链接
|
||||
* @param {*} node
|
||||
*/
|
||||
export const scanAndConvertToLinks = (node) => {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
const text = node.textContent;
|
||||
const urlRegex = /https?:\/\/[^\s]+/g;
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
|
||||
const newNode = document.createElement('span');
|
||||
|
||||
while ((match = urlRegex.exec(text)) !== null) {
|
||||
const beforeText = text.substring(lastIndex, match.index);
|
||||
const url = match[0];
|
||||
|
||||
if (beforeText) {
|
||||
newNode.appendChild(document.createTextNode(beforeText));
|
||||
}
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.target = '_blank'
|
||||
link.textContent = url;
|
||||
|
||||
newNode.appendChild(link);
|
||||
|
||||
lastIndex = urlRegex.lastIndex;
|
||||
}
|
||||
|
||||
if (lastIndex < text.length) {
|
||||
newNode.appendChild(document.createTextNode(text.substring(lastIndex)));
|
||||
}
|
||||
|
||||
node.parentNode.replaceChild(newNode, node);
|
||||
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
for (const childNode of node.childNodes) {
|
||||
scanAndConvertToLinks(childNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
const BLOG = require('./blog.config')
|
||||
|
||||
/**
|
||||
* 通常没啥用,sitemap交给 /pages/sitemap.xml.js 动态生成
|
||||
*/
|
||||
module.exports = {
|
||||
siteUrl: BLOG.LINK,
|
||||
changefreq: 'daily',
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
enabled: process.env.ANALYZE === 'true'
|
||||
})
|
||||
|
||||
const { THEME } = require('./blog.config')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const BLOG = require('./blog.config')
|
||||
|
||||
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
enabled: BLOG.BUNDLE_ANALYZER
|
||||
})
|
||||
|
||||
/**
|
||||
* 扫描指定目录下的文件夹名,用于获取当前有几个主题
|
||||
@@ -91,6 +92,7 @@ module.exports = withBundleAnalyzer({
|
||||
// })
|
||||
// }
|
||||
// 动态主题:添加 resolve.alias 配置,将动态路径映射到实际路径
|
||||
console.log('加载默认主题', path.resolve(__dirname, 'themes', THEME))
|
||||
config.resolve.alias['@theme-components'] = path.resolve(__dirname, 'themes', THEME)
|
||||
return config
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "notion-next",
|
||||
"version": "4.0.18",
|
||||
"version": "4.2.0",
|
||||
"homepage": "https://github.com/tangly1024/NotionNext.git",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getGlobalData } from '@/lib/notion/getNotionData'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useRouter } from 'next/router'
|
||||
import { getLayoutByTheme } from '@/themes/theme'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
/**
|
||||
* 404
|
||||
@@ -9,13 +9,12 @@ import { getLayoutByTheme } from '@/themes/theme'
|
||||
* @returns
|
||||
*/
|
||||
const NoFound = props => {
|
||||
const { siteInfo } = useGlobal()
|
||||
const meta = { title: `${props?.siteInfo?.title} | 页面找不到啦`, image: siteInfo?.pageCover }
|
||||
const meta = { title: `${siteConfig('TITLE')} | 页面找不到啦`, image: siteConfig('HOME_BANNER_IMAGE') }
|
||||
|
||||
props = { ...props, meta }
|
||||
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
|
||||
|
||||
return <Layout {...props} />
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getLayoutByTheme } from '@/themes/theme'
|
||||
import md5 from 'js-md5'
|
||||
import { isBrowser } from '@/lib/utils'
|
||||
import { uploadDataToAlgolia } from '@/lib/algolia'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
/**
|
||||
* 根据notion的slug访问页面
|
||||
@@ -66,7 +67,7 @@ const Slug = props => {
|
||||
}, [post])
|
||||
|
||||
const meta = {
|
||||
title: post ? `${post?.title} | ${siteInfo?.title}` : `${props?.siteInfo?.title || BLOG.TITLE} | loading`,
|
||||
title: post ? `${post?.title} | ${siteConfig('TITLE')}` : `${siteConfig('TITLE')} | loading`,
|
||||
description: post?.summary,
|
||||
type: post?.type,
|
||||
slug: post?.slug,
|
||||
@@ -76,7 +77,7 @@ const Slug = props => {
|
||||
}
|
||||
props = { ...props, lock, meta, setLock, validPassword }
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
|
||||
return <Layout {...props} />
|
||||
}
|
||||
|
||||
|
||||
@@ -6,52 +6,44 @@ 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 'aos/dist/aos.css' // You can also use <link> for styles
|
||||
|
||||
import { GlobalContextProvider } from '@/lib/global'
|
||||
import { getGlobalLayoutByTheme } from '@/themes/theme'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { getQueryParam } from '../lib/utils'
|
||||
import useAdjustStyle from '@/hooks/useAdjustStyle'
|
||||
|
||||
import AOS from 'aos'
|
||||
import 'aos/dist/aos.css' // You can also use <link> for styles
|
||||
import dynamic from 'next/dynamic'
|
||||
import { isBrowser, loadExternalResource } from '@/lib/utils'
|
||||
import BLOG from '@/blog.config'
|
||||
|
||||
// 各种扩展插件 动画等
|
||||
const ExternalPlugins = dynamic(() => import('@/components/ExternalPlugins'))
|
||||
// 各种扩展插件 这个要阻塞引入
|
||||
import ExternalPlugins from '@/components/ExternalPlugins'
|
||||
import { THEME } from '@/blog.config'
|
||||
|
||||
const MyApp = ({ Component, pageProps }) => {
|
||||
// 自定义样式css和js引入
|
||||
if (isBrowser) {
|
||||
// 初始化AOS动画
|
||||
AOS.init()
|
||||
// 静态导入本地自定义样式
|
||||
loadExternalResource('/css/custom.css', 'css')
|
||||
loadExternalResource('/js/custom.js', 'js')
|
||||
// 一些可能出现 bug 的样式,可以统一放入该钩子进行调整
|
||||
useAdjustStyle();
|
||||
|
||||
// 自动添加图片阴影
|
||||
if (BLOG.IMG_SHADOW) {
|
||||
loadExternalResource('/css/img-shadow.css', 'css')
|
||||
}
|
||||
const route = useRouter()
|
||||
const queryParam = useMemo(() => {
|
||||
return getQueryParam(route.asPath, 'theme') || THEME
|
||||
}, [route])
|
||||
|
||||
// 导入外部自定义脚本
|
||||
if (BLOG.CUSTOM_EXTERNAL_JS && BLOG.CUSTOM_EXTERNAL_JS.length > 0) {
|
||||
for (const url of BLOG.CUSTOM_EXTERNAL_JS) {
|
||||
loadExternalResource(url, 'js')
|
||||
}
|
||||
}
|
||||
|
||||
// 导入外部自定义样式
|
||||
if (BLOG.CUSTOM_EXTERNAL_CSS && BLOG.CUSTOM_EXTERNAL_CSS.length > 0) {
|
||||
for (const url of BLOG.CUSTOM_EXTERNAL_CSS) {
|
||||
loadExternalResource(url, 'css')
|
||||
}
|
||||
}
|
||||
}
|
||||
const GLayout = useCallback(
|
||||
props => {
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getGlobalLayoutByTheme(queryParam)
|
||||
return <Layout {...props} />
|
||||
},
|
||||
[queryParam]
|
||||
)
|
||||
|
||||
return (
|
||||
<GlobalContextProvider {...pageProps}>
|
||||
<Component {...pageProps} />
|
||||
<ExternalPlugins {...pageProps} />
|
||||
</GlobalContextProvider>
|
||||
<GlobalContextProvider {...pageProps}>
|
||||
<GLayout {...pageProps}>
|
||||
<Component {...pageProps} />
|
||||
</GLayout>
|
||||
<ExternalPlugins {...pageProps} />
|
||||
</GlobalContextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// eslint-disable-next-line @next/next/no-document-import-in-page
|
||||
import Document, { Html, Head, Main, NextScript } from 'next/document'
|
||||
import BLOG from '@/blog.config'
|
||||
import CommonScript from '@/components/CommonScript'
|
||||
|
||||
class MyDocument extends Document {
|
||||
static async getInitialProps(ctx) {
|
||||
@@ -14,7 +13,6 @@ class MyDocument extends Document {
|
||||
<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" />
|
||||
@@ -22,6 +20,7 @@ class MyDocument extends Document {
|
||||
</>}
|
||||
|
||||
{BLOG.FONT_URL?.map((fontUrl, index) => {
|
||||
console.log(fontUrl)
|
||||
if (fontUrl.endsWith('.css')) {
|
||||
return <link key={index} rel="stylesheet" href={fontUrl} />
|
||||
} else {
|
||||
|
||||
15
pages/api/cache.js
Normal file
15
pages/api/cache.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { cleanCache } from '@/lib/cache/local_file_cache'
|
||||
|
||||
/**
|
||||
* 清理缓存
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
export default async function handler(req, res) {
|
||||
try {
|
||||
await cleanCache()
|
||||
res.status(200).json({ status: 'success', message: 'Clean cache successful!' })
|
||||
} catch (error) {
|
||||
res.status(400).json({ status: 'error', message: 'Clean cache failed!', error })
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,14 @@ import { useRouter } from 'next/router'
|
||||
import { getLayoutByTheme } from '@/themes/theme'
|
||||
import { isBrowser } from '@/lib/utils'
|
||||
import { formatDateFmt } from '@/lib/formatDate'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
const ArchiveIndex = props => {
|
||||
const { siteInfo } = props
|
||||
const { locale } = useGlobal()
|
||||
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
|
||||
|
||||
useEffect(() => {
|
||||
if (isBrowser) {
|
||||
@@ -29,8 +30,8 @@ const ArchiveIndex = props => {
|
||||
}, [])
|
||||
|
||||
const meta = {
|
||||
title: `${locale.NAV.ARCHIVE} | ${siteInfo?.title}`,
|
||||
description: siteInfo?.description,
|
||||
title: `${locale.NAV.ARCHIVE} | ${siteConfig('TITLE')}`,
|
||||
description: siteConfig('DESCRIPTION'),
|
||||
image: siteInfo?.pageCover,
|
||||
slug: 'archive',
|
||||
type: 'website'
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useGlobal } from '@/lib/global'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import { getLayoutByTheme } from '@/themes/theme'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
/**
|
||||
* 分类页
|
||||
@@ -15,13 +16,13 @@ export default function Category(props) {
|
||||
const { locale } = useGlobal()
|
||||
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
|
||||
|
||||
const meta = {
|
||||
title: `${props.category} | ${locale.COMMON.CATEGORY} | ${
|
||||
siteInfo?.title || ''
|
||||
siteConfig('TITLE') || ''
|
||||
}`,
|
||||
description: siteInfo?.description,
|
||||
description: siteConfig('DESCRIPTION'),
|
||||
slug: 'category/' + props.category,
|
||||
image: siteInfo?.pageCover,
|
||||
type: 'website'
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useGlobal } from '@/lib/global'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import { getLayoutByTheme } from '@/themes/theme'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
/**
|
||||
* 分类页
|
||||
@@ -15,13 +16,13 @@ export default function Category(props) {
|
||||
const { siteInfo } = props
|
||||
const { locale } = useGlobal()
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
|
||||
|
||||
const meta = {
|
||||
title: `${props.category} | ${locale.COMMON.CATEGORY} | ${
|
||||
siteInfo?.title || ''
|
||||
siteConfig('TITLE') || ''
|
||||
}`,
|
||||
description: siteInfo?.description,
|
||||
description: siteConfig('DESCRIPTION'),
|
||||
slug: 'category/' + props.category,
|
||||
image: siteInfo?.pageCover,
|
||||
type: 'website'
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useGlobal } from '@/lib/global'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import { getLayoutByTheme } from '@/themes/theme'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
/**
|
||||
* 分类首页
|
||||
@@ -15,11 +16,11 @@ export default function Category(props) {
|
||||
const { siteInfo } = props
|
||||
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
|
||||
|
||||
const meta = {
|
||||
title: `${locale.COMMON.CATEGORY} | ${siteInfo?.title}`,
|
||||
description: siteInfo?.description,
|
||||
title: `${locale.COMMON.CATEGORY} | ${siteConfig('TITLE')}`,
|
||||
description: siteConfig('DESCRIPTION'),
|
||||
image: siteInfo?.pageCover,
|
||||
slug: 'category',
|
||||
type: 'website'
|
||||
|
||||
@@ -3,8 +3,9 @@ import { getPostBlocks } from '@/lib/notion'
|
||||
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 { siteConfig } from '@/lib/config'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
/**
|
||||
* 首页布局
|
||||
@@ -13,8 +14,16 @@ import { getLayoutByTheme } from '@/themes/theme'
|
||||
*/
|
||||
const Index = props => {
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
return <Layout {...props} />
|
||||
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
|
||||
|
||||
const meta = {
|
||||
title: `${siteConfig('TITLE')} | ${siteConfig('DESCRIPTION')}`,
|
||||
description: siteConfig('DESCRIPTION'),
|
||||
image: siteConfig('HOME_BANNER_IMAGE'),
|
||||
slug: '',
|
||||
type: 'website'
|
||||
}
|
||||
return <Layout meta={meta} {...props} />
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,16 +34,8 @@ export async function getStaticProps() {
|
||||
const from = 'index'
|
||||
const props = await getGlobalData({ from })
|
||||
|
||||
const { siteInfo } = props
|
||||
props.posts = props.allPages?.filter(page => page.type === 'Post' && page.status === 'Published')
|
||||
|
||||
const meta = {
|
||||
title: `${siteInfo?.title} | ${siteInfo?.description}`,
|
||||
description: siteInfo?.description,
|
||||
image: siteInfo?.pageCover,
|
||||
slug: '',
|
||||
type: 'website'
|
||||
}
|
||||
// 处理分页
|
||||
if (BLOG.POST_LIST_STYLE === 'scroll') {
|
||||
// 滚动列表默认给前端返回所有数据
|
||||
@@ -65,10 +66,7 @@ export async function getStaticProps() {
|
||||
delete props.allPages
|
||||
|
||||
return {
|
||||
props: {
|
||||
meta,
|
||||
...props
|
||||
},
|
||||
props,
|
||||
revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getPostBlocks } from '@/lib/notion'
|
||||
import { getGlobalData } from '@/lib/notion/getNotionData'
|
||||
import { useRouter } from 'next/router'
|
||||
import { getLayoutByTheme } from '@/themes/theme'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
/**
|
||||
* 文章列表分页
|
||||
@@ -13,11 +14,11 @@ const Page = props => {
|
||||
const { siteInfo } = props
|
||||
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
|
||||
|
||||
const meta = {
|
||||
title: `${props?.page} | Page | ${siteInfo?.title}`,
|
||||
description: siteInfo?.description,
|
||||
title: `${props?.page} | Page | ${siteConfig('TITLE')}`,
|
||||
description: siteConfig('DESCRIPTION'),
|
||||
image: siteInfo?.pageCover,
|
||||
slug: 'page/' + props.page,
|
||||
type: 'website'
|
||||
|
||||
@@ -4,17 +4,18 @@ import { getDataFromCache } from '@/lib/cache/cache_manager'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import { getLayoutByTheme } from '@/themes/theme'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
const Index = props => {
|
||||
const { keyword, siteInfo } = props
|
||||
const { locale } = useGlobal()
|
||||
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
|
||||
|
||||
const meta = {
|
||||
title: `${keyword || ''}${keyword ? ' | ' : ''}${locale.NAV.SEARCH} | ${siteInfo?.title}`,
|
||||
description: siteInfo?.title,
|
||||
title: `${keyword || ''}${keyword ? ' | ' : ''}${locale.NAV.SEARCH} | ${siteConfig('TITLE')}`,
|
||||
description: siteConfig('TITLE'),
|
||||
image: siteInfo?.pageCover,
|
||||
slug: 'search/' + (keyword || ''),
|
||||
type: 'website'
|
||||
@@ -112,7 +113,7 @@ const isIterable = obj =>
|
||||
async function filterByMemCache(allPosts, keyword) {
|
||||
const filterPosts = []
|
||||
if (keyword) {
|
||||
keyword = keyword.trim()
|
||||
keyword = keyword.trim().toLowerCase()
|
||||
}
|
||||
for (const post of allPosts) {
|
||||
const cacheKey = 'page_block_' + post.id
|
||||
@@ -130,7 +131,7 @@ async function filterByMemCache(allPosts, keyword) {
|
||||
if (!c) {
|
||||
continue
|
||||
}
|
||||
const index = c.toLowerCase().indexOf(keyword.toLowerCase())
|
||||
const index = c.toLowerCase().indexOf(keyword)
|
||||
if (index > -1) {
|
||||
hit = true
|
||||
hitCount += 1
|
||||
|
||||
@@ -4,17 +4,18 @@ import { getDataFromCache } from '@/lib/cache/cache_manager'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import { getLayoutByTheme } from '@/themes/theme'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
const Index = props => {
|
||||
const { keyword, siteInfo } = props
|
||||
const { locale } = useGlobal()
|
||||
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
|
||||
|
||||
const meta = {
|
||||
title: `${keyword || ''}${keyword ? ' | ' : ''}${locale.NAV.SEARCH} | ${siteInfo?.title}`,
|
||||
description: siteInfo?.title,
|
||||
title: `${keyword || ''}${keyword ? ' | ' : ''}${locale.NAV.SEARCH} | ${siteConfig('TITLE')}`,
|
||||
description: siteConfig('TITLE'),
|
||||
image: siteInfo?.pageCover,
|
||||
slug: 'search/' + (keyword || ''),
|
||||
type: 'website'
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useGlobal } from '@/lib/global'
|
||||
import { useRouter } from 'next/router'
|
||||
import BLOG from '@/blog.config'
|
||||
import { getLayoutByTheme } from '@/themes/theme'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
/**
|
||||
* 搜索路由
|
||||
@@ -14,7 +15,7 @@ const Search = props => {
|
||||
const { locale } = useGlobal()
|
||||
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
|
||||
|
||||
const router = useRouter()
|
||||
const keyword = getSearchKey(router)
|
||||
@@ -34,8 +35,8 @@ const Search = props => {
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: `${keyword || ''}${keyword ? ' | ' : ''}${locale.NAV.SEARCH} | ${siteInfo?.title}`,
|
||||
description: siteInfo?.description,
|
||||
title: `${keyword || ''}${keyword ? ' | ' : ''}${locale.NAV.SEARCH} | ${siteConfig('TITLE')}`,
|
||||
description: siteConfig('DESCRIPTION'),
|
||||
image: siteInfo?.pageCover,
|
||||
slug: 'search',
|
||||
type: 'website'
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getGlobalData } from '@/lib/notion/getNotionData'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import { getLayoutByTheme } from '@/themes/theme'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
/**
|
||||
* 标签下的文章列表
|
||||
@@ -14,11 +15,11 @@ const Tag = props => {
|
||||
const { tag, siteInfo } = props
|
||||
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
|
||||
|
||||
const meta = {
|
||||
title: `${tag} | ${locale.COMMON.TAGS} | ${siteInfo?.title}`,
|
||||
description: siteInfo?.description,
|
||||
title: `${tag} | ${locale.COMMON.TAGS} | ${siteConfig('TITLE')}`,
|
||||
description: siteConfig('DESCRIPTION'),
|
||||
image: siteInfo?.pageCover,
|
||||
slug: 'tag/' + tag,
|
||||
type: 'website'
|
||||
|
||||
@@ -3,17 +3,18 @@ import { getGlobalData } from '@/lib/notion/getNotionData'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import { getLayoutByTheme } from '@/themes/theme'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
const Tag = props => {
|
||||
const { locale } = useGlobal()
|
||||
const { tag, siteInfo } = props
|
||||
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
|
||||
|
||||
const meta = {
|
||||
title: `${tag} | ${locale.COMMON.TAGS} | ${siteInfo?.title}`,
|
||||
description: siteInfo?.description,
|
||||
title: `${tag} | ${locale.COMMON.TAGS} | ${siteConfig('TITLE')}`,
|
||||
description: siteConfig('DESCRIPTION'),
|
||||
image: siteInfo?.pageCover,
|
||||
slug: 'tag/' + tag,
|
||||
type: 'website'
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useGlobal } from '@/lib/global'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import { getLayoutByTheme } from '@/themes/theme'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
/**
|
||||
* 标签首页
|
||||
@@ -14,11 +15,11 @@ const TagIndex = props => {
|
||||
const { siteInfo } = props
|
||||
|
||||
// 根据页面路径加载不同Layout文件
|
||||
const Layout = getLayoutByTheme(useRouter())
|
||||
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
|
||||
|
||||
const meta = {
|
||||
title: `${locale.COMMON.TAGS} | ${siteInfo?.title}`,
|
||||
description: siteInfo?.description,
|
||||
title: `${locale.COMMON.TAGS} | ${siteConfig('TITLE')}`,
|
||||
description: siteConfig('DESCRIPTION'),
|
||||
image: siteInfo?.pageCover,
|
||||
slug: 'tag',
|
||||
type: 'website'
|
||||
|
||||
@@ -160,6 +160,11 @@ nav {
|
||||
@apply text-blue-700
|
||||
}
|
||||
|
||||
/* twikoo 内置的 element-ui 加载样式 */
|
||||
.el-loading-spinner {
|
||||
@apply flex justify-center items-center;
|
||||
}
|
||||
|
||||
/* Webmention style */
|
||||
.webmention-block {
|
||||
background: rgba(0, 116, 222, .2);
|
||||
|
||||
@@ -724,7 +724,6 @@ svg.notion-page-icon {
|
||||
.notion-text {
|
||||
width: 100%;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.5rem !important;
|
||||
word-break: break-word;
|
||||
padding: 3px 2px !important;
|
||||
margin: 1px 0 !important;
|
||||
@@ -1131,9 +1130,10 @@ code[class*='language-'] {
|
||||
transition: background 20ms ease-in 0s;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
opacity: 0.9;
|
||||
|
||||
padding: 6px 2px;
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
line-height: 1.2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import Link from 'next/link'
|
||||
|
||||
/**
|
||||
@@ -24,7 +24,7 @@ export default function BlogListGroupByDate({ archiveTitle, archivePosts }) {
|
||||
{post?.publishDay}
|
||||
</span>{' '}
|
||||
|
||||
<Link href={`${BLOG.SUB_PATH}/${post.slug}`} className="dark:text-gray-400 dark:hover:text-gray-300 overflow-x-hidden hover:underline cursor-pointer text-gray-600">
|
||||
<Link href={`${siteConfig('SUB_PATH', '')}/${post.slug}`} className="dark:text-gray-400 dark:hover:text-gray-300 overflow-x-hidden hover:underline cursor-pointer text-gray-600">
|
||||
{post.title}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import BLOG from '@/blog.config'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useRouter } from 'next/router'
|
||||
import Link from 'next/link'
|
||||
@@ -10,14 +10,14 @@ export const BlogListPage = props => {
|
||||
const { page = 1, posts, postCount } = props
|
||||
const { locale } = useGlobal()
|
||||
const router = useRouter()
|
||||
const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
|
||||
const totalPage = Math.ceil(postCount / parseInt(siteConfig('POSTS_PER_PAGE')))
|
||||
const currentPage = +page
|
||||
|
||||
const showPrev = currentPage > 1
|
||||
const showNext = page < totalPage
|
||||
const pagePrefix = router.asPath.split('?')[0].replace(/\/page\/[1-9]\d*/, '').replace(/\/$/, '')
|
||||
|
||||
const showPageCover = CONFIG.POST_LIST_COVER
|
||||
const showPageCover = siteConfig('EXAMPLE_POST_LIST_COVER', null, CONFIG)
|
||||
|
||||
return (
|
||||
<div className={`w-full ${showPageCover ? 'md:pr-2' : 'md:pr-12'} mb-12`}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import throttle from 'lodash.throttle'
|
||||
import BlogPostCard from './BlogPostCard'
|
||||
import CONFIG from '../config'
|
||||
@@ -9,33 +9,33 @@ export const BlogListScroll = props => {
|
||||
const { posts } = props
|
||||
const { locale } = useGlobal()
|
||||
|
||||
const [page, updatePage] = React.useState(1)
|
||||
const [page, updatePage] = useState(1)
|
||||
|
||||
let hasMore = false
|
||||
const postsToShow = posts
|
||||
? Object.assign(posts).slice(0, BLOG.POSTS_PER_PAGE * page)
|
||||
? Object.assign(posts).slice(0, parseInt(siteConfig('POSTS_PER_PAGE')) * page)
|
||||
: []
|
||||
|
||||
if (posts) {
|
||||
const totalCount = posts.length
|
||||
hasMore = page * BLOG.POSTS_PER_PAGE < totalCount
|
||||
hasMore = page * parseInt(siteConfig('POSTS_PER_PAGE')) < totalCount
|
||||
}
|
||||
const handleGetMore = () => {
|
||||
if (!hasMore) return
|
||||
updatePage(page + 1)
|
||||
}
|
||||
|
||||
const targetRef = React.useRef(null)
|
||||
const targetRef = useRef(null)
|
||||
|
||||
// 监听滚动自动分页加载
|
||||
const scrollTrigger = React.useCallback(throttle(() => {
|
||||
const scrollTrigger = useCallback(throttle(() => {
|
||||
const scrollS = window.scrollY + window.outerHeight
|
||||
const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0
|
||||
if (scrollS > clientHeight + 100) {
|
||||
handleGetMore()
|
||||
}
|
||||
}, 500))
|
||||
const showPageCover = CONFIG.POST_LIST_COVER
|
||||
const showPageCover = siteConfig('EXAMPLE_POST_LIST_COVER', null, CONFIG)
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('scroll', scrollTrigger)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import CONFIG from '../config'
|
||||
import Link from 'next/link'
|
||||
import TwikooCommentCount from '@/components/TwikooCommentCount'
|
||||
import LazyImage from '@/components/LazyImage'
|
||||
|
||||
const BlogPostCard = ({ post }) => {
|
||||
const showPageCover = CONFIG.POST_LIST_COVER && post?.pageCoverThumbnail
|
||||
const showPageCover = siteConfig('EXAMPLE_POST_LIST_COVER', null, CONFIG) && post?.pageCoverThumbnail
|
||||
|
||||
return <article className={`${showPageCover ? 'flex md:flex-row flex-col-reverse' : ''} replace mb-12 `}>
|
||||
<div className={`${showPageCover ? 'md:w-7/12' : ''}`}>
|
||||
@@ -18,10 +18,10 @@ const BlogPostCard = ({ post }) => {
|
||||
</h2>
|
||||
|
||||
<div className="mb-4 text-sm text-gray-700 dark:text-gray-300">
|
||||
by <a href="#" className="text-gray-700 dark:text-gray-300">{BLOG.AUTHOR}</a> on {post.date?.start_date || post.createdTime}
|
||||
by <a href="#" className="text-gray-700 dark:text-gray-300">{siteConfig('AUTHOR')}</a> on {post.date?.start_date || post.createdTime}
|
||||
<TwikooCommentCount post={post} className='pl-1'/>
|
||||
<span className="font-bold mx-1"> | </span>
|
||||
<a href={`/category${post.category}`} className="text-gray-700 dark:text-gray-300 hover:underline">{post.category}</a>
|
||||
<Link href={`/category/${post.category}`} className="text-gray-700 dark:text-gray-300 hover:underline">{post.category}</Link>
|
||||
{/* <span className="font-bold mx-1"> | </span> */}
|
||||
{/* <a href="#" className="text-gray-700">2 Comments</a> */}
|
||||
</div>
|
||||
@@ -40,9 +40,9 @@ const BlogPostCard = ({ post }) => {
|
||||
</div>
|
||||
{/* 图片封面 */}
|
||||
{showPageCover && (
|
||||
<div className="md:w-5/12 w-full overflow-hidden p-1">
|
||||
<Link href={`${BLOG.SUB_PATH}/${post.slug}`} passHref legacyBehavior>
|
||||
<LazyImage src={post?.pageCoverThumbnail} className='h-44 bg-center bg-cover hover:scale-110 duration-200' />
|
||||
<div className="md:w-5/12 w-full h-44 overflow-hidden p-1">
|
||||
<Link href={`${siteConfig('SUB_PATH', '')}/${post.slug}`} passHref legacyBehavior>
|
||||
<LazyImage src={post?.pageCoverThumbnail} className='w-full bg-cover hover:scale-110 duration-200' />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import Link from 'next/link'
|
||||
import { RecentComments } from '@waline/client'
|
||||
|
||||
@@ -9,11 +9,11 @@ import { RecentComments } from '@waline/client'
|
||||
* @returns
|
||||
*/
|
||||
const ExampleRecentComments = (props) => {
|
||||
const [comments, updateComments] = React.useState([])
|
||||
const [onLoading, changeLoading] = React.useState(true)
|
||||
React.useEffect(() => {
|
||||
const [comments, updateComments] = useState([])
|
||||
const [onLoading, changeLoading] = useState(true)
|
||||
useEffect(() => {
|
||||
RecentComments({
|
||||
serverURL: BLOG.COMMENT_WALINE_SERVER_URL,
|
||||
serverURL: siteConfig('COMMENT_WALINE_SERVER_URL'),
|
||||
count: 5
|
||||
}).then(({ comments }) => {
|
||||
changeLoading(false)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user