nnbb 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +1 -0
- package/.env.local +2 -0
- package/.eslintrc.json +15 -0
- package/.github/stale.yml +16 -0
- package/.github/workflows/codeql-analysis.yml +73 -0
- package/.github/workflows/docker-ghcr.yaml +59 -0
- package/.prettierrc +9 -0
- package/.vscode/launch.json +28 -0
- package/.vscode/settings.json +6 -0
- package/Dockerfile +38 -0
- package/LICENSE +21 -0
- package/README.md +16 -0
- package/blog.config.js +454 -0
- package/components/Ackee.js +83 -0
- package/components/AdBlockDetect.js +40 -0
- package/components/AlgoliaSearchModal.js +250 -0
- package/components/Artalk.js +30 -0
- package/components/Busuanzi.js +26 -0
- package/components/ChatBase.js +19 -0
- package/components/Collapse.tsx +134 -0
- package/components/Comment.js +161 -0
- package/components/CommonHead.tsx +101 -0
- package/components/CommonScript.js +125 -0
- package/components/CusdisComponent.js +35 -0
- package/components/CustomContextMenu.js +221 -0
- package/components/DebugPanel.js +134 -0
- package/components/DisableCopy.js +21 -0
- package/components/Draggable.js +167 -0
- package/components/Equation.js +31 -0
- package/components/ExternalPlugins.js +75 -0
- package/components/ExternalScript.js +29 -0
- package/components/FacebookMessenger.js +255 -0
- package/components/FacebookPage.js +34 -0
- package/components/Fireworks.js +210 -0
- package/components/FlipCard.js +56 -0
- package/components/FlutteringRibbon.js +322 -0
- package/components/FullScreenButton.js +48 -0
- package/components/Giscus.js +33 -0
- package/components/Gitalk.js +42 -0
- package/components/GoogleAdsense.js +111 -0
- package/components/Gtag.js +18 -0
- package/components/HeroIcons.tsx +321 -0
- package/components/KatexReact.js +53 -0
- package/components/LazyImage.js +95 -0
- package/components/Live2D.js +52 -0
- package/components/Loading.js +20 -0
- package/components/Mark.js +28 -0
- package/components/NProgress.ts +8 -0
- package/components/Nest.js +124 -0
- package/components/NotionIcon.js +20 -0
- package/components/NotionPage.tsx +206 -0
- package/components/Player.js +54 -0
- package/components/PrismMac.js +236 -0
- package/components/QrCode.js +34 -0
- package/components/Ribbon.js +98 -0
- package/components/Sakura.js +192 -0
- package/components/Select.js +40 -0
- package/components/ShareBar.js +29 -0
- package/components/ShareButtons.js +403 -0
- package/components/SideBarDrawer.js +50 -0
- package/components/StarrySky.js +130 -0
- package/components/Tabs.js +64 -0
- package/components/ThemeSwitch.js +67 -0
- package/components/Twikoo.js +27 -0
- package/components/TwikooCommentCount.js +22 -0
- package/components/TwikooCommentCounter.js +78 -0
- package/components/TwikooRecentComments.js +11 -0
- package/components/Utterances.js +35 -0
- package/components/VConsole.js +76 -0
- package/components/ValineComponent.js +59 -0
- package/components/ValineCount.js +6 -0
- package/components/ValinePanel.js +3 -0
- package/components/Vercel.tsx +54 -0
- package/components/WWAds.js +18 -0
- package/components/WalineComponent.js +83 -0
- package/components/WebMention.js +173 -0
- package/components/Webwhiz.js +17 -0
- package/components/WordCount.js +73 -0
- package/hooks/useToggleClickOutSide.ts +32 -0
- package/hooks/useWindowSize.ts +30 -0
- package/lib/algolia.js +108 -0
- package/lib/busuanzi.js +99 -0
- package/lib/cache/cacheManager.ts +49 -0
- package/lib/cache/localFileCache.ts +56 -0
- package/lib/cache/memoryMache.ts +20 -0
- package/lib/cache/mongoDbCache.ts +70 -0
- package/lib/cache/types.ts +5 -0
- package/lib/font.js +46 -0
- package/lib/global.tsx +129 -0
- package/lib/gtag.js +17 -0
- package/lib/mailchimp.js +49 -0
- package/lib/memorize.js +0 -0
- package/lib/mhchem.js +1696 -0
- package/lib/notion/getAllCategories.ts +51 -0
- package/lib/notion/getAllPageIds.ts +51 -0
- package/lib/notion/getAllPosts.js +68 -0
- package/lib/notion/getAllTags.ts +43 -0
- package/lib/notion/getNotionData.ts +340 -0
- package/lib/notion/getPageInfoOfPostPage.ts +58 -0
- package/lib/notion/getPageProperties.ts +203 -0
- package/lib/notion/getPageTableOfContents.ts +107 -0
- package/lib/notion/getPostBlocks.ts +147 -0
- package/lib/notion/mapImage.ts +130 -0
- package/lib/notion/types.ts +125 -0
- package/lib/notion.js +2 -0
- package/lib/robots.txt.js +25 -0
- package/lib/rss.js +63 -0
- package/lib/sitemap.xml.js +67 -0
- package/lib/utils.js +212 -0
- package/next-env.d.ts +5 -0
- package/next-i18next.config.js +7 -0
- package/next-sitemap.config.js +11 -0
- package/next.config.js +124 -0
- package/package.json +92 -0
- package/pages/404.tsx +40 -0
- package/pages/[prefix]/[slug].tsx +123 -0
- package/pages/[prefix]/index.tsx +223 -0
- package/pages/_app.js +59 -0
- package/pages/_document.js +42 -0
- package/pages/api/subscribe.js +22 -0
- package/pages/archive/index.tsx +79 -0
- package/pages/category/[category]/index.tsx +87 -0
- package/pages/category/[category]/page/[page].tsx +103 -0
- package/pages/category/index.tsx +43 -0
- package/pages/index.tsx +88 -0
- package/pages/page/[page].tsx +93 -0
- package/pages/search/[keyword]/index.tsx +162 -0
- package/pages/search/[keyword]/page/[page].tsx +166 -0
- package/pages/search/index.tsx +69 -0
- package/pages/sitemap.xml.js +70 -0
- package/pages/tag/[tag]/index.tsx +73 -0
- package/pages/tag/[tag]/page/[page].tsx +87 -0
- package/pages/tag/index.tsx +42 -0
- package/postcss.config.js +6 -0
- package/public/ads.txt +1 -0
- package/public/avatar.png +0 -0
- package/public/avatar.svg +11 -0
- package/public/bg_image.jpg +0 -0
- package/public/css/all.min.css +9 -0
- package/public/css/custom.css +8 -0
- package/public/css/img-shadow.css +5 -0
- package/public/css/prism-mac-style.css +58 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +9 -0
- package/public/js/cusdis.es.js +107 -0
- package/public/js/custom.js +1 -0
- package/public/locales/en/common.json +44 -0
- package/public/locales/en/menu.json +9 -0
- package/public/locales/en/nav.json +11 -0
- package/public/locales/zh-CN/common.json +44 -0
- package/public/locales/zh-CN/menu.json +9 -0
- package/public/locales/zh-CN/nav.json +9 -0
- package/public/webfonts/fa-brands-400.ttf +0 -0
- package/public/webfonts/fa-brands-400.woff2 +0 -0
- package/public/webfonts/fa-regular-400.ttf +0 -0
- package/public/webfonts/fa-regular-400.woff2 +0 -0
- package/public/webfonts/fa-solid-900.ttf +0 -0
- package/public/webfonts/fa-solid-900.woff2 +0 -0
- package/public/webfonts/fa-v4compatibility.ttf +0 -0
- package/public/webfonts/fa-v4compatibility.woff2 +0 -0
- package/styles/animate.css +503 -0
- package/styles/globals.css +183 -0
- package/styles/notion.css +2064 -0
- package/styles/nprogress.css +84 -0
- package/styles/prism-theme.css +119 -0
- package/styles/utility-patterns.css +79 -0
- package/tailwind.config.js +37 -0
- package/theme/index.ts +6 -0
- package/theme/types/@theme-components.d.ts +29 -0
- package/theme/useLayout.ts +41 -0
- package/theme/utils.ts +108 -0
- package/themes/innocent/package.json +7 -0
- package/themes/innocent/theme.config.js +1 -0
- package/themes/nobelium/components/Announcement.tsx +27 -0
- package/themes/nobelium/components/ArticleFooter.tsx +39 -0
- package/themes/nobelium/components/ArticleInfo.tsx +58 -0
- package/themes/nobelium/components/ArticleLock.tsx +86 -0
- package/themes/nobelium/components/BlogArchiveItem.js +41 -0
- package/themes/nobelium/components/BlogListBar.js +39 -0
- package/themes/nobelium/components/BlogListPage.tsx +67 -0
- package/themes/nobelium/components/BlogListScroll.tsx +96 -0
- package/themes/nobelium/components/BlogPost.tsx +33 -0
- package/themes/nobelium/components/DarkModeButton.tsx +50 -0
- package/themes/nobelium/components/ExampleRecentComments.js +35 -0
- package/themes/nobelium/components/Footer.tsx +35 -0
- package/themes/nobelium/components/JumpToTopButton.tsx +39 -0
- package/themes/nobelium/components/LanguageSwitchButton.tsx +58 -0
- package/themes/nobelium/components/MenuItemCollapse.tsx +92 -0
- package/themes/nobelium/components/MenuItemDrop.tsx +83 -0
- package/themes/nobelium/components/Nav/Nav.module.css +50 -0
- package/themes/nobelium/components/Nav/Nav.tsx +187 -0
- package/themes/nobelium/components/RandomPostButton.tsx +31 -0
- package/themes/nobelium/components/SearchButton.tsx +31 -0
- package/themes/nobelium/components/SearchInput.tsx +94 -0
- package/themes/nobelium/components/SearchNavBar.js +19 -0
- package/themes/nobelium/components/SideBar.js +83 -0
- package/themes/nobelium/components/SvgIcon.js +29 -0
- package/themes/nobelium/components/TagItem.js +13 -0
- package/themes/nobelium/components/Tags.tsx +44 -0
- package/themes/nobelium/components/Title.js +19 -0
- package/themes/nobelium/index.tsx +28 -0
- package/themes/nobelium/layout/LayoutBase.tsx +79 -0
- package/themes/nobelium/pages/Archive.tsx +30 -0
- package/themes/nobelium/pages/Category.tsx +43 -0
- package/themes/nobelium/pages/Home.tsx +22 -0
- package/themes/nobelium/pages/PageNotFound.tsx +15 -0
- package/themes/nobelium/pages/Post.tsx +34 -0
- package/themes/nobelium/pages/PostList.tsx +74 -0
- package/themes/nobelium/pages/Search.tsx +65 -0
- package/themes/nobelium/pages/Tag.tsx +42 -0
- package/themes/nobelium/providers/index.tsx +60 -0
- package/themes/nobelium/stores/index.tsx +42 -0
- package/themes/nobelium/theme.config.ts +17 -0
- package/themes/nobelium/types/index.ts +10 -0
- package/tsconfig.json +29 -0
- package/types/index.ts +1 -0
- package/types/page.ts +102 -0
- package/vercel.json +5 -0
@@ -0,0 +1,123 @@
|
|
1
|
+
import BLOG from '@/blog.config';
|
2
|
+
import { getPostBlocks } from '@/lib/notion';
|
3
|
+
import { getGlobalData } from '@/lib/notion/getNotionData';
|
4
|
+
import { idToUuid } from 'notion-utils';
|
5
|
+
|
6
|
+
import { getPageInfoOfPostPage } from '@/lib/notion/getPageInfoOfPostPage';
|
7
|
+
import Slug, { findRelatedPosts } from '.';
|
8
|
+
import { uploadDataToAlgolia } from '@/lib/algolia';
|
9
|
+
|
10
|
+
import type { FC } from 'react';
|
11
|
+
import type { ParsedUrlQuery } from 'querystring';
|
12
|
+
import type { PrefixSlugProps } from '@/types';
|
13
|
+
import type { GetStaticProps, GetStaticPaths } from 'next';
|
14
|
+
|
15
|
+
export interface PrefixSlugParams extends ParsedUrlQuery {
|
16
|
+
prefix: string;
|
17
|
+
slug: string;
|
18
|
+
}
|
19
|
+
|
20
|
+
/**
|
21
|
+
* 根据notion的slug访问页面
|
22
|
+
* @param {*} props
|
23
|
+
* @returns
|
24
|
+
*/
|
25
|
+
const PrefixSlug: FC<PrefixSlugProps> = (props) => {
|
26
|
+
return <Slug {...props} />;
|
27
|
+
};
|
28
|
+
|
29
|
+
export const getStaticPaths: GetStaticPaths<PrefixSlugParams> = async () => {
|
30
|
+
if (!BLOG.isProd) {
|
31
|
+
return {
|
32
|
+
paths: [],
|
33
|
+
fallback: true,
|
34
|
+
};
|
35
|
+
}
|
36
|
+
|
37
|
+
const { allPages } = await getGlobalData('slug-detail');
|
38
|
+
return {
|
39
|
+
paths: allPages
|
40
|
+
.filter(
|
41
|
+
(row) =>
|
42
|
+
row.slug.indexOf('/') > 0 && row.type && row.type.indexOf('Menu') < 0,
|
43
|
+
)
|
44
|
+
.map((row) => ({
|
45
|
+
params: {
|
46
|
+
prefix: row.slug.split('/')[0],
|
47
|
+
slug: row.slug.split('/')[1],
|
48
|
+
},
|
49
|
+
})),
|
50
|
+
fallback: true,
|
51
|
+
};
|
52
|
+
};
|
53
|
+
|
54
|
+
export const getStaticProps: GetStaticProps<
|
55
|
+
PrefixSlugProps,
|
56
|
+
PrefixSlugParams
|
57
|
+
> = async (context) => {
|
58
|
+
const { prefix, slug } = context.params as PrefixSlugParams;
|
59
|
+
let fullSlug = `${prefix}/${slug}`;
|
60
|
+
|
61
|
+
if (BLOG.PSEUDO_STATIC && !fullSlug.endsWith('.html')) {
|
62
|
+
fullSlug += '.html';
|
63
|
+
}
|
64
|
+
|
65
|
+
const from = `slug-detail-${fullSlug}`;
|
66
|
+
const { allPages, ...restProps } = await getGlobalData(from);
|
67
|
+
|
68
|
+
// 在列表内查找文章
|
69
|
+
let post = allPages.find((p) => {
|
70
|
+
return p.slug === fullSlug || p.id === idToUuid(fullSlug);
|
71
|
+
});
|
72
|
+
|
73
|
+
// 处理非列表内文章的内信息
|
74
|
+
if (!post) {
|
75
|
+
const pageId = slug.slice(-1)[0];
|
76
|
+
if (pageId.length >= 32) {
|
77
|
+
post = await getPageInfoOfPostPage(pageId, fullSlug);
|
78
|
+
}
|
79
|
+
}
|
80
|
+
|
81
|
+
// 无法获取文章
|
82
|
+
if (!post) {
|
83
|
+
return {
|
84
|
+
props: {
|
85
|
+
...restProps,
|
86
|
+
post: null,
|
87
|
+
prev: null,
|
88
|
+
next: null,
|
89
|
+
recommendPosts: [],
|
90
|
+
},
|
91
|
+
revalidate: BLOG.NEXT_REVALIDATE_SECOND,
|
92
|
+
};
|
93
|
+
}
|
94
|
+
|
95
|
+
// 文章内容加载
|
96
|
+
if (!post.blockMap) {
|
97
|
+
post.blockMap = await getPostBlocks(post.id, from);
|
98
|
+
}
|
99
|
+
|
100
|
+
// 生成全文索引 && process.env.npm_lifecycle_event === 'build' && JSON.parse(BLOG.ALGOLIA_RECREATE_DATA)
|
101
|
+
if (BLOG.ALGOLIA_APP_ID) {
|
102
|
+
uploadDataToAlgolia(post);
|
103
|
+
}
|
104
|
+
|
105
|
+
// 推荐关联文章处理
|
106
|
+
const allPosts = allPages.filter(
|
107
|
+
(page) => page.type === 'Post' && page.status === 'Published',
|
108
|
+
);
|
109
|
+
const { prev, next, recommendPosts } = findRelatedPosts(post, allPosts);
|
110
|
+
|
111
|
+
return {
|
112
|
+
props: {
|
113
|
+
...restProps,
|
114
|
+
post,
|
115
|
+
prev,
|
116
|
+
next,
|
117
|
+
recommendPosts,
|
118
|
+
},
|
119
|
+
revalidate: BLOG.NEXT_REVALIDATE_SECOND,
|
120
|
+
};
|
121
|
+
};
|
122
|
+
|
123
|
+
export default PrefixSlug;
|
@@ -0,0 +1,223 @@
|
|
1
|
+
import BLOG from '@/blog.config';
|
2
|
+
import { getPostBlocks } from '@/lib/notion';
|
3
|
+
import { getGlobalData } from '@/lib/notion/getNotionData';
|
4
|
+
import { useCallback, useEffect, useState } from 'react';
|
5
|
+
import { idToUuid } from 'notion-utils';
|
6
|
+
import { useRouter } from 'next/router';
|
7
|
+
import { getPageInfoOfPostPage } from '@/lib/notion/getPageInfoOfPostPage';
|
8
|
+
import { getPageTableOfContents } from '@/lib/notion/getPageTableOfContents';
|
9
|
+
import { useLayout } from '@/theme';
|
10
|
+
import md5 from 'js-md5';
|
11
|
+
import { isBrowser } from '@/lib/utils';
|
12
|
+
import { uploadDataToAlgolia } from '@/lib/algolia';
|
13
|
+
|
14
|
+
import type { FC } from 'react';
|
15
|
+
import type { ParsedUrlQuery } from 'querystring';
|
16
|
+
import type { PageMeta, SlugIndexProps, ThemePrefixProps } from '@/types';
|
17
|
+
import type { GetStaticProps, GetStaticPaths } from 'next';
|
18
|
+
import { PageInfo, PagePropertiesType } from '@/lib/notion/types';
|
19
|
+
|
20
|
+
export interface SlugIndexParams extends ParsedUrlQuery {
|
21
|
+
prefix: string;
|
22
|
+
}
|
23
|
+
|
24
|
+
/**
|
25
|
+
* 根据notion的slug访问页面
|
26
|
+
* @param {*} props
|
27
|
+
* @returns
|
28
|
+
*/
|
29
|
+
const Slug: FC<SlugIndexProps> = (props) => {
|
30
|
+
const { post, siteInfo } = props;
|
31
|
+
const router = useRouter();
|
32
|
+
|
33
|
+
// 文章锁🔐
|
34
|
+
const [isLock, setIsLock] = useState<boolean>(!!(post && post.password));
|
35
|
+
|
36
|
+
/**
|
37
|
+
* 验证文章密码
|
38
|
+
* @param {*} result
|
39
|
+
*/
|
40
|
+
const validPassword = useCallback(
|
41
|
+
(passInput: string) => {
|
42
|
+
if (!post) {
|
43
|
+
return false;
|
44
|
+
}
|
45
|
+
const encrypt = md5(post.slug + passInput);
|
46
|
+
if (passInput && encrypt === post.password) {
|
47
|
+
setIsLock(false);
|
48
|
+
return true;
|
49
|
+
}
|
50
|
+
return false;
|
51
|
+
},
|
52
|
+
[post],
|
53
|
+
);
|
54
|
+
|
55
|
+
// 文章加载
|
56
|
+
useEffect(() => {
|
57
|
+
// 404
|
58
|
+
if (!post) {
|
59
|
+
setTimeout(() => {
|
60
|
+
if (isBrowser) {
|
61
|
+
const article = document.getElementById('notion-article');
|
62
|
+
if (!article) {
|
63
|
+
router.push('/404').then(() => {
|
64
|
+
console.warn('找不到页面', router.asPath);
|
65
|
+
});
|
66
|
+
}
|
67
|
+
}
|
68
|
+
}, 8 * 1000); // 404时长 8秒
|
69
|
+
}
|
70
|
+
|
71
|
+
// 文章加密
|
72
|
+
if (post?.password && post?.password !== '') {
|
73
|
+
setIsLock(true);
|
74
|
+
} else {
|
75
|
+
setIsLock(false);
|
76
|
+
if (!isLock && post?.blockMap?.block) {
|
77
|
+
post.content = Object.keys(post.blockMap.block).filter(
|
78
|
+
(key) => post.blockMap?.block[key]?.value?.parent_id === post.id,
|
79
|
+
);
|
80
|
+
post.toc = getPageTableOfContents(post, post.blockMap);
|
81
|
+
}
|
82
|
+
}
|
83
|
+
}, [post, isLock]);
|
84
|
+
|
85
|
+
const pageMeta: PageMeta = {
|
86
|
+
title: post
|
87
|
+
? `${post?.title} | ${siteInfo?.title}`
|
88
|
+
: `${props?.siteInfo?.title || BLOG.TITLE} | loading`,
|
89
|
+
description: post?.summary || '',
|
90
|
+
type: post?.type || '',
|
91
|
+
slug: post?.slug || '',
|
92
|
+
image:
|
93
|
+
post?.pageCoverThumbnail || siteInfo?.pageCover || BLOG.HOME_BANNER_IMAGE,
|
94
|
+
category: post?.category?.[0],
|
95
|
+
tags: post?.tags,
|
96
|
+
};
|
97
|
+
|
98
|
+
// 根据页面路径加载不同Layout文件
|
99
|
+
const Layout = useLayout() as FC<ThemePrefixProps>;
|
100
|
+
|
101
|
+
return (
|
102
|
+
<Layout
|
103
|
+
{...props}
|
104
|
+
pageMeta={pageMeta}
|
105
|
+
isLock={isLock}
|
106
|
+
validPassword={validPassword}
|
107
|
+
/>
|
108
|
+
);
|
109
|
+
};
|
110
|
+
|
111
|
+
export const getStaticPaths: GetStaticPaths<SlugIndexParams> = async () => {
|
112
|
+
if (!BLOG.isProd) {
|
113
|
+
return {
|
114
|
+
paths: [],
|
115
|
+
fallback: true,
|
116
|
+
};
|
117
|
+
}
|
118
|
+
|
119
|
+
const { allPages } = await getGlobalData('slug-index');
|
120
|
+
return {
|
121
|
+
paths: allPages
|
122
|
+
.filter(
|
123
|
+
(row) =>
|
124
|
+
row.slug.indexOf('/') < 0 && row.type && row.type.indexOf('Menu') < 0,
|
125
|
+
)
|
126
|
+
.map((row) => ({ params: { prefix: row.slug } })),
|
127
|
+
fallback: true,
|
128
|
+
};
|
129
|
+
};
|
130
|
+
|
131
|
+
export const getStaticProps: GetStaticProps<
|
132
|
+
SlugIndexProps,
|
133
|
+
SlugIndexParams
|
134
|
+
> = async (context) => {
|
135
|
+
const { prefix } = context.params as SlugIndexParams;
|
136
|
+
const fullSlug =
|
137
|
+
BLOG.PSEUDO_STATIC && !prefix.endsWith('.html') ? `${prefix}.html` : prefix;
|
138
|
+
const from = `slug-index-${fullSlug}`;
|
139
|
+
|
140
|
+
const { allPages, ...restProps } = await getGlobalData(from);
|
141
|
+
|
142
|
+
// 在列表内查找文章
|
143
|
+
let post = allPages.find((p) => {
|
144
|
+
return p.slug === fullSlug || p.id === idToUuid(fullSlug);
|
145
|
+
});
|
146
|
+
|
147
|
+
// 处理非列表内文章的内信息
|
148
|
+
if (!post) {
|
149
|
+
const pageId = prefix;
|
150
|
+
if (pageId.length >= 32) {
|
151
|
+
post = await getPageInfoOfPostPage(pageId, from);
|
152
|
+
}
|
153
|
+
}
|
154
|
+
|
155
|
+
// 无法获取文章
|
156
|
+
if (!post) {
|
157
|
+
return {
|
158
|
+
props: {
|
159
|
+
...restProps,
|
160
|
+
post: null,
|
161
|
+
prev: null,
|
162
|
+
next: null,
|
163
|
+
recommendPosts: [],
|
164
|
+
},
|
165
|
+
revalidate: BLOG.NEXT_REVALIDATE_SECOND,
|
166
|
+
};
|
167
|
+
}
|
168
|
+
|
169
|
+
// 文章内容加载
|
170
|
+
if (!post.blockMap) {
|
171
|
+
post.blockMap = await getPostBlocks(post.id, from);
|
172
|
+
}
|
173
|
+
|
174
|
+
// 生成全文索引 && process.env.npm_lifecycle_event === 'build' && JSON.parse(BLOG.ALGOLIA_RECREATE_DATA)
|
175
|
+
if (BLOG.ALGOLIA_APP_ID) {
|
176
|
+
uploadDataToAlgolia(post);
|
177
|
+
}
|
178
|
+
|
179
|
+
// 推荐关联文章处理
|
180
|
+
const allPosts = allPages.filter(
|
181
|
+
(page) => page.type === 'Post' && page.status === 'Published',
|
182
|
+
);
|
183
|
+
const { prev, next, recommendPosts } = findRelatedPosts(post, allPosts);
|
184
|
+
|
185
|
+
return {
|
186
|
+
props: {
|
187
|
+
...restProps,
|
188
|
+
post,
|
189
|
+
prev,
|
190
|
+
next,
|
191
|
+
recommendPosts,
|
192
|
+
},
|
193
|
+
revalidate: BLOG.NEXT_REVALIDATE_SECOND,
|
194
|
+
};
|
195
|
+
};
|
196
|
+
|
197
|
+
export const findRelatedPosts: (
|
198
|
+
post: PageInfo,
|
199
|
+
allPosts: PageInfo[],
|
200
|
+
count?: number,
|
201
|
+
) => {
|
202
|
+
prev: PageInfo | null;
|
203
|
+
next: PageInfo | null;
|
204
|
+
recommendPosts: PageInfo[];
|
205
|
+
} = (post, allPosts, count = 6) => {
|
206
|
+
if (!allPosts.length || !post) {
|
207
|
+
return { prev: null, next: null, recommendPosts: [] };
|
208
|
+
}
|
209
|
+
const relatedPosts = allPosts.filter(
|
210
|
+
(p) =>
|
211
|
+
p.id !== post.id &&
|
212
|
+
p.type === PagePropertiesType.Post &&
|
213
|
+
post.tags.some((tag) => p.tags.includes(tag)),
|
214
|
+
);
|
215
|
+
|
216
|
+
return {
|
217
|
+
prev: allPosts[allPosts.indexOf(post) - 1] || allPosts[allPosts.length - 1],
|
218
|
+
next: allPosts[allPosts.indexOf(post) + 1] || allPosts[0],
|
219
|
+
recommendPosts: relatedPosts.slice(0, count),
|
220
|
+
};
|
221
|
+
};
|
222
|
+
|
223
|
+
export default Slug;
|
package/pages/_app.js
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
import '@/styles/animate.css'; // @see https://animate.style/
|
2
|
+
import '@/styles/globals.css';
|
3
|
+
import '@/styles/utility-patterns.css';
|
4
|
+
|
5
|
+
// core styles shared by all of react-notion-x (required)
|
6
|
+
import 'react-notion-x/src/styles.css';
|
7
|
+
import '@/styles/notion.css'; // 重写部分样式
|
8
|
+
|
9
|
+
import { GlobalContextProvider } from '@/lib/global';
|
10
|
+
import { appWithTranslation } from 'next-i18next';
|
11
|
+
|
12
|
+
import dynamic from 'next/dynamic';
|
13
|
+
import { isBrowser, loadExternalResource } from '@/lib/utils';
|
14
|
+
import BLOG from '@/blog.config';
|
15
|
+
import AOS from 'aos';
|
16
|
+
import 'aos/dist/aos.css'; // You can also use <link> for styles
|
17
|
+
import { initNProgress } from '@/components/NProgress';
|
18
|
+
import '@/styles/nprogress.css';
|
19
|
+
|
20
|
+
// 各种扩展插件 动画等
|
21
|
+
const ExternalPlugins = dynamic(() => import('@/components/ExternalPlugins'));
|
22
|
+
|
23
|
+
const MyApp = ({ Component, pageProps }) => {
|
24
|
+
if (isBrowser) {
|
25
|
+
AOS.init();
|
26
|
+
initNProgress();
|
27
|
+
// 静态导入本地自定义样式
|
28
|
+
loadExternalResource('/css/custom.css', 'css');
|
29
|
+
loadExternalResource('/js/custom.js', 'js');
|
30
|
+
|
31
|
+
// 自动添加图片阴影
|
32
|
+
if (BLOG.IMG_SHADOW) {
|
33
|
+
loadExternalResource('/css/img-shadow.css', 'css');
|
34
|
+
}
|
35
|
+
|
36
|
+
// 导入外部自定义脚本
|
37
|
+
if (BLOG.CUSTOM_EXTERNAL_JS && BLOG.CUSTOM_EXTERNAL_JS.length > 0) {
|
38
|
+
for (const url of BLOG.CUSTOM_EXTERNAL_JS) {
|
39
|
+
loadExternalResource(url, 'js');
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
// 导入外部自定义样式
|
44
|
+
if (BLOG.CUSTOM_EXTERNAL_CSS && BLOG.CUSTOM_EXTERNAL_CSS.length > 0) {
|
45
|
+
for (const url of BLOG.CUSTOM_EXTERNAL_CSS) {
|
46
|
+
loadExternalResource(url, 'css');
|
47
|
+
}
|
48
|
+
}
|
49
|
+
}
|
50
|
+
|
51
|
+
return (
|
52
|
+
<GlobalContextProvider {...pageProps}>
|
53
|
+
<Component {...pageProps} />
|
54
|
+
<ExternalPlugins {...pageProps} />
|
55
|
+
</GlobalContextProvider>
|
56
|
+
);
|
57
|
+
};
|
58
|
+
|
59
|
+
export default appWithTranslation(MyApp);
|
@@ -0,0 +1,42 @@
|
|
1
|
+
// eslint-disable-next-line @next/next/no-document-import-in-page
|
2
|
+
import Document, { Html, Head, Main, NextScript } from 'next/document'
|
3
|
+
import BLOG from '@/blog.config'
|
4
|
+
import CommonScript from '@/components/CommonScript'
|
5
|
+
|
6
|
+
class MyDocument extends Document {
|
7
|
+
static async getInitialProps(ctx) {
|
8
|
+
const initialProps = await Document.getInitialProps(ctx)
|
9
|
+
return { ...initialProps }
|
10
|
+
}
|
11
|
+
|
12
|
+
render() {
|
13
|
+
return (
|
14
|
+
<Html lang={BLOG.LANG}>
|
15
|
+
<Head>
|
16
|
+
<link rel='icon' href= {`${BLOG.BLOG_FAVICON}`} />
|
17
|
+
<CommonScript />
|
18
|
+
{/* 预加载字体 */}
|
19
|
+
{BLOG.FONT_AWESOME && <>
|
20
|
+
<link rel='preload' href={BLOG.FONT_AWESOME} as="style" crossOrigin="anonymous" />
|
21
|
+
<link rel="stylesheet" href={BLOG.FONT_AWESOME} crossOrigin="anonymous" referrerPolicy="no-referrer" />
|
22
|
+
</>}
|
23
|
+
|
24
|
+
{BLOG.FONT_URL?.map((fontUrl, index) => {
|
25
|
+
if (fontUrl.endsWith('.css')) {
|
26
|
+
return <link key={index} rel="stylesheet" href={fontUrl} />
|
27
|
+
} else {
|
28
|
+
return <link key={index} rel="preload" href={fontUrl} as="font" type="font/woff2" />
|
29
|
+
}
|
30
|
+
})}
|
31
|
+
</Head>
|
32
|
+
|
33
|
+
<body className={`${BLOG.FONT_STYLE} font-light scroll-smooth`}>
|
34
|
+
<Main />
|
35
|
+
<NextScript />
|
36
|
+
</body>
|
37
|
+
</Html>
|
38
|
+
)
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
export default MyDocument
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import subscribeToMailchimpApi from '@/lib/mailchimp'
|
2
|
+
|
3
|
+
/**
|
4
|
+
* 接受邮件订阅
|
5
|
+
* @param {*} req
|
6
|
+
* @param {*} res
|
7
|
+
*/
|
8
|
+
export default async function handler(req, res) {
|
9
|
+
if (req.method === 'POST') {
|
10
|
+
const { email, firstName, lastName } = req.body
|
11
|
+
try {
|
12
|
+
const response = await subscribeToMailchimpApi({ email, first_name: firstName, last_name: lastName })
|
13
|
+
const data = await response.json()
|
14
|
+
console.log('data', data)
|
15
|
+
res.status(200).json({ status: 'success', message: 'Subscription successful!' })
|
16
|
+
} catch (error) {
|
17
|
+
res.status(400).json({ status: 'error', message: 'Subscription failed!', error })
|
18
|
+
}
|
19
|
+
} else {
|
20
|
+
res.status(405).json({ status: 'error', message: 'Method not allowed' })
|
21
|
+
}
|
22
|
+
}
|
@@ -0,0 +1,79 @@
|
|
1
|
+
import { getGlobalData } from '@/lib/notion/getNotionData';
|
2
|
+
import { useEffect, type FC } from 'react';
|
3
|
+
import BLOG from '@/blog.config';
|
4
|
+
import { useLayout } from '@/theme';
|
5
|
+
import { isBrowser } from '@/lib/utils';
|
6
|
+
import { useTranslation } from 'next-i18next';
|
7
|
+
import dayjs from 'dayjs';
|
8
|
+
import { omit } from 'lodash';
|
9
|
+
|
10
|
+
import type { GetStaticProps } from 'next';
|
11
|
+
import type {
|
12
|
+
PageMeta,
|
13
|
+
ArchiveIndexProps,
|
14
|
+
ThemeArchiveProps,
|
15
|
+
} from '../../types/page';
|
16
|
+
import type { PageInfo } from '@/lib/notion/types';
|
17
|
+
|
18
|
+
const ArchiveIndex: FC<ArchiveIndexProps> = (props) => {
|
19
|
+
const { siteInfo } = props;
|
20
|
+
const { t } = useTranslation('nav');
|
21
|
+
|
22
|
+
// 根据页面路径加载不同Layout文件
|
23
|
+
const Archive = useLayout() as FC<ThemeArchiveProps>;
|
24
|
+
|
25
|
+
useEffect(() => {
|
26
|
+
if (isBrowser) {
|
27
|
+
const anchor = window.location.hash;
|
28
|
+
if (anchor) {
|
29
|
+
setTimeout(() => {
|
30
|
+
const anchorElement = document.getElementById(anchor.substring(1));
|
31
|
+
if (anchorElement) {
|
32
|
+
anchorElement.scrollIntoView({
|
33
|
+
block: 'start',
|
34
|
+
behavior: 'smooth',
|
35
|
+
});
|
36
|
+
}
|
37
|
+
}, 300);
|
38
|
+
}
|
39
|
+
}
|
40
|
+
}, []);
|
41
|
+
|
42
|
+
const pageMeta: PageMeta = {
|
43
|
+
title: `${t('archive')} | ${siteInfo?.title}`,
|
44
|
+
description: siteInfo?.description,
|
45
|
+
image: siteInfo?.pageCover,
|
46
|
+
slug: 'archive',
|
47
|
+
type: 'website',
|
48
|
+
};
|
49
|
+
|
50
|
+
return <Archive pageMeta={pageMeta} {...props} />;
|
51
|
+
};
|
52
|
+
|
53
|
+
export const getStaticProps: GetStaticProps<ArchiveIndexProps> = async () => {
|
54
|
+
const globalData = await getGlobalData('archive-index');
|
55
|
+
|
56
|
+
const posts = globalData.allPages?.filter(
|
57
|
+
(page) => page.type === 'Post' && page.status === 'Published',
|
58
|
+
) as PageInfo[];
|
59
|
+
|
60
|
+
const archivePosts: Record<string, PageInfo[]> = {};
|
61
|
+
posts
|
62
|
+
.sort((a, b) => (dayjs(b.publishDate).isAfter(a.publishDate) ? 1 : -1))
|
63
|
+
.forEach((post) => {
|
64
|
+
const date = dayjs(post.publishDate).format('yyyy-MM');
|
65
|
+
if (!archivePosts[date]) archivePosts[date] = [];
|
66
|
+
archivePosts[date].push(post);
|
67
|
+
});
|
68
|
+
|
69
|
+
return {
|
70
|
+
props: {
|
71
|
+
...omit(globalData, 'allPages'),
|
72
|
+
posts,
|
73
|
+
archivePosts,
|
74
|
+
},
|
75
|
+
revalidate: BLOG.NEXT_REVALIDATE_SECOND,
|
76
|
+
};
|
77
|
+
};
|
78
|
+
|
79
|
+
export default ArchiveIndex;
|
@@ -0,0 +1,87 @@
|
|
1
|
+
import { getGlobalData } from '@/lib/notion/getNotionData';
|
2
|
+
import React from 'react';
|
3
|
+
import BLOG from '@/blog.config';
|
4
|
+
import { useLayout } from '@/theme';
|
5
|
+
import { useTranslation } from 'next-i18next';
|
6
|
+
|
7
|
+
import type { GetStaticProps, GetStaticPaths } from 'next';
|
8
|
+
import type {
|
9
|
+
PageMeta,
|
10
|
+
CategoryDetailProps,
|
11
|
+
ThemeCategoryDetailProps,
|
12
|
+
} from '@/types';
|
13
|
+
import type { FC } from 'react';
|
14
|
+
import type { ParsedUrlQuery } from 'querystring';
|
15
|
+
|
16
|
+
export interface CategoryDetailParams extends ParsedUrlQuery {
|
17
|
+
category: string;
|
18
|
+
}
|
19
|
+
|
20
|
+
/**
|
21
|
+
* 分类页
|
22
|
+
* @param {*} props
|
23
|
+
* @returns
|
24
|
+
*/
|
25
|
+
const CategoryDetail: FC<CategoryDetailProps> = (props) => {
|
26
|
+
const { siteInfo } = props;
|
27
|
+
const { t } = useTranslation('common');
|
28
|
+
|
29
|
+
// 根据页面路径加载不同Layout文件
|
30
|
+
const Layout = useLayout() as FC<ThemeCategoryDetailProps>;
|
31
|
+
|
32
|
+
const pageMeta: PageMeta = {
|
33
|
+
title: `${props.category} | ${t('category')} | ${siteInfo?.title || ''}`,
|
34
|
+
description: siteInfo?.description,
|
35
|
+
slug: 'category/' + props.category,
|
36
|
+
image: siteInfo?.pageCover,
|
37
|
+
type: 'website',
|
38
|
+
};
|
39
|
+
|
40
|
+
return <Layout pageMeta={pageMeta} {...props} />;
|
41
|
+
};
|
42
|
+
|
43
|
+
export const getStaticProps: GetStaticProps<
|
44
|
+
CategoryDetailProps,
|
45
|
+
CategoryDetailParams
|
46
|
+
> = async (context) => {
|
47
|
+
const { category } = context.params as CategoryDetailParams;
|
48
|
+
const { allPages, ...globalProps } = await getGlobalData(
|
49
|
+
'category-detail-props',
|
50
|
+
);
|
51
|
+
|
52
|
+
const filteredPosts = allPages.filter(
|
53
|
+
(page) =>
|
54
|
+
page.type === 'Post' &&
|
55
|
+
page.status === 'Published' &&
|
56
|
+
page.category?.includes(category),
|
57
|
+
);
|
58
|
+
const posts =
|
59
|
+
BLOG.POST_LIST_STYLE === 'page'
|
60
|
+
? filteredPosts.slice(0, BLOG.POSTS_PER_PAGE)
|
61
|
+
: filteredPosts;
|
62
|
+
|
63
|
+
return {
|
64
|
+
props: {
|
65
|
+
...globalProps,
|
66
|
+
category,
|
67
|
+
posts,
|
68
|
+
postCount: posts.length,
|
69
|
+
},
|
70
|
+
revalidate: BLOG.NEXT_REVALIDATE_SECOND,
|
71
|
+
};
|
72
|
+
};
|
73
|
+
|
74
|
+
export const getStaticPaths: GetStaticPaths<
|
75
|
+
CategoryDetailParams
|
76
|
+
> = async () => {
|
77
|
+
const from = 'category-paths';
|
78
|
+
const { categoryOptions } = await getGlobalData(from);
|
79
|
+
return {
|
80
|
+
paths: categoryOptions.map((category) => ({
|
81
|
+
params: { category: category.name },
|
82
|
+
})),
|
83
|
+
fallback: true,
|
84
|
+
};
|
85
|
+
};
|
86
|
+
|
87
|
+
export default CategoryDetail;
|