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.
Files changed (218) hide show
  1. package/.dockerignore +1 -0
  2. package/.env.local +2 -0
  3. package/.eslintrc.json +15 -0
  4. package/.github/stale.yml +16 -0
  5. package/.github/workflows/codeql-analysis.yml +73 -0
  6. package/.github/workflows/docker-ghcr.yaml +59 -0
  7. package/.prettierrc +9 -0
  8. package/.vscode/launch.json +28 -0
  9. package/.vscode/settings.json +6 -0
  10. package/Dockerfile +38 -0
  11. package/LICENSE +21 -0
  12. package/README.md +16 -0
  13. package/blog.config.js +454 -0
  14. package/components/Ackee.js +83 -0
  15. package/components/AdBlockDetect.js +40 -0
  16. package/components/AlgoliaSearchModal.js +250 -0
  17. package/components/Artalk.js +30 -0
  18. package/components/Busuanzi.js +26 -0
  19. package/components/ChatBase.js +19 -0
  20. package/components/Collapse.tsx +134 -0
  21. package/components/Comment.js +161 -0
  22. package/components/CommonHead.tsx +101 -0
  23. package/components/CommonScript.js +125 -0
  24. package/components/CusdisComponent.js +35 -0
  25. package/components/CustomContextMenu.js +221 -0
  26. package/components/DebugPanel.js +134 -0
  27. package/components/DisableCopy.js +21 -0
  28. package/components/Draggable.js +167 -0
  29. package/components/Equation.js +31 -0
  30. package/components/ExternalPlugins.js +75 -0
  31. package/components/ExternalScript.js +29 -0
  32. package/components/FacebookMessenger.js +255 -0
  33. package/components/FacebookPage.js +34 -0
  34. package/components/Fireworks.js +210 -0
  35. package/components/FlipCard.js +56 -0
  36. package/components/FlutteringRibbon.js +322 -0
  37. package/components/FullScreenButton.js +48 -0
  38. package/components/Giscus.js +33 -0
  39. package/components/Gitalk.js +42 -0
  40. package/components/GoogleAdsense.js +111 -0
  41. package/components/Gtag.js +18 -0
  42. package/components/HeroIcons.tsx +321 -0
  43. package/components/KatexReact.js +53 -0
  44. package/components/LazyImage.js +95 -0
  45. package/components/Live2D.js +52 -0
  46. package/components/Loading.js +20 -0
  47. package/components/Mark.js +28 -0
  48. package/components/NProgress.ts +8 -0
  49. package/components/Nest.js +124 -0
  50. package/components/NotionIcon.js +20 -0
  51. package/components/NotionPage.tsx +206 -0
  52. package/components/Player.js +54 -0
  53. package/components/PrismMac.js +236 -0
  54. package/components/QrCode.js +34 -0
  55. package/components/Ribbon.js +98 -0
  56. package/components/Sakura.js +192 -0
  57. package/components/Select.js +40 -0
  58. package/components/ShareBar.js +29 -0
  59. package/components/ShareButtons.js +403 -0
  60. package/components/SideBarDrawer.js +50 -0
  61. package/components/StarrySky.js +130 -0
  62. package/components/Tabs.js +64 -0
  63. package/components/ThemeSwitch.js +67 -0
  64. package/components/Twikoo.js +27 -0
  65. package/components/TwikooCommentCount.js +22 -0
  66. package/components/TwikooCommentCounter.js +78 -0
  67. package/components/TwikooRecentComments.js +11 -0
  68. package/components/Utterances.js +35 -0
  69. package/components/VConsole.js +76 -0
  70. package/components/ValineComponent.js +59 -0
  71. package/components/ValineCount.js +6 -0
  72. package/components/ValinePanel.js +3 -0
  73. package/components/Vercel.tsx +54 -0
  74. package/components/WWAds.js +18 -0
  75. package/components/WalineComponent.js +83 -0
  76. package/components/WebMention.js +173 -0
  77. package/components/Webwhiz.js +17 -0
  78. package/components/WordCount.js +73 -0
  79. package/hooks/useToggleClickOutSide.ts +32 -0
  80. package/hooks/useWindowSize.ts +30 -0
  81. package/lib/algolia.js +108 -0
  82. package/lib/busuanzi.js +99 -0
  83. package/lib/cache/cacheManager.ts +49 -0
  84. package/lib/cache/localFileCache.ts +56 -0
  85. package/lib/cache/memoryMache.ts +20 -0
  86. package/lib/cache/mongoDbCache.ts +70 -0
  87. package/lib/cache/types.ts +5 -0
  88. package/lib/font.js +46 -0
  89. package/lib/global.tsx +129 -0
  90. package/lib/gtag.js +17 -0
  91. package/lib/mailchimp.js +49 -0
  92. package/lib/memorize.js +0 -0
  93. package/lib/mhchem.js +1696 -0
  94. package/lib/notion/getAllCategories.ts +51 -0
  95. package/lib/notion/getAllPageIds.ts +51 -0
  96. package/lib/notion/getAllPosts.js +68 -0
  97. package/lib/notion/getAllTags.ts +43 -0
  98. package/lib/notion/getNotionData.ts +340 -0
  99. package/lib/notion/getPageInfoOfPostPage.ts +58 -0
  100. package/lib/notion/getPageProperties.ts +203 -0
  101. package/lib/notion/getPageTableOfContents.ts +107 -0
  102. package/lib/notion/getPostBlocks.ts +147 -0
  103. package/lib/notion/mapImage.ts +130 -0
  104. package/lib/notion/types.ts +125 -0
  105. package/lib/notion.js +2 -0
  106. package/lib/robots.txt.js +25 -0
  107. package/lib/rss.js +63 -0
  108. package/lib/sitemap.xml.js +67 -0
  109. package/lib/utils.js +212 -0
  110. package/next-env.d.ts +5 -0
  111. package/next-i18next.config.js +7 -0
  112. package/next-sitemap.config.js +11 -0
  113. package/next.config.js +124 -0
  114. package/package.json +92 -0
  115. package/pages/404.tsx +40 -0
  116. package/pages/[prefix]/[slug].tsx +123 -0
  117. package/pages/[prefix]/index.tsx +223 -0
  118. package/pages/_app.js +59 -0
  119. package/pages/_document.js +42 -0
  120. package/pages/api/subscribe.js +22 -0
  121. package/pages/archive/index.tsx +79 -0
  122. package/pages/category/[category]/index.tsx +87 -0
  123. package/pages/category/[category]/page/[page].tsx +103 -0
  124. package/pages/category/index.tsx +43 -0
  125. package/pages/index.tsx +88 -0
  126. package/pages/page/[page].tsx +93 -0
  127. package/pages/search/[keyword]/index.tsx +162 -0
  128. package/pages/search/[keyword]/page/[page].tsx +166 -0
  129. package/pages/search/index.tsx +69 -0
  130. package/pages/sitemap.xml.js +70 -0
  131. package/pages/tag/[tag]/index.tsx +73 -0
  132. package/pages/tag/[tag]/page/[page].tsx +87 -0
  133. package/pages/tag/index.tsx +42 -0
  134. package/postcss.config.js +6 -0
  135. package/public/ads.txt +1 -0
  136. package/public/avatar.png +0 -0
  137. package/public/avatar.svg +11 -0
  138. package/public/bg_image.jpg +0 -0
  139. package/public/css/all.min.css +9 -0
  140. package/public/css/custom.css +8 -0
  141. package/public/css/img-shadow.css +5 -0
  142. package/public/css/prism-mac-style.css +58 -0
  143. package/public/favicon.ico +0 -0
  144. package/public/favicon.svg +9 -0
  145. package/public/js/cusdis.es.js +107 -0
  146. package/public/js/custom.js +1 -0
  147. package/public/locales/en/common.json +44 -0
  148. package/public/locales/en/menu.json +9 -0
  149. package/public/locales/en/nav.json +11 -0
  150. package/public/locales/zh-CN/common.json +44 -0
  151. package/public/locales/zh-CN/menu.json +9 -0
  152. package/public/locales/zh-CN/nav.json +9 -0
  153. package/public/webfonts/fa-brands-400.ttf +0 -0
  154. package/public/webfonts/fa-brands-400.woff2 +0 -0
  155. package/public/webfonts/fa-regular-400.ttf +0 -0
  156. package/public/webfonts/fa-regular-400.woff2 +0 -0
  157. package/public/webfonts/fa-solid-900.ttf +0 -0
  158. package/public/webfonts/fa-solid-900.woff2 +0 -0
  159. package/public/webfonts/fa-v4compatibility.ttf +0 -0
  160. package/public/webfonts/fa-v4compatibility.woff2 +0 -0
  161. package/styles/animate.css +503 -0
  162. package/styles/globals.css +183 -0
  163. package/styles/notion.css +2064 -0
  164. package/styles/nprogress.css +84 -0
  165. package/styles/prism-theme.css +119 -0
  166. package/styles/utility-patterns.css +79 -0
  167. package/tailwind.config.js +37 -0
  168. package/theme/index.ts +6 -0
  169. package/theme/types/@theme-components.d.ts +29 -0
  170. package/theme/useLayout.ts +41 -0
  171. package/theme/utils.ts +108 -0
  172. package/themes/innocent/package.json +7 -0
  173. package/themes/innocent/theme.config.js +1 -0
  174. package/themes/nobelium/components/Announcement.tsx +27 -0
  175. package/themes/nobelium/components/ArticleFooter.tsx +39 -0
  176. package/themes/nobelium/components/ArticleInfo.tsx +58 -0
  177. package/themes/nobelium/components/ArticleLock.tsx +86 -0
  178. package/themes/nobelium/components/BlogArchiveItem.js +41 -0
  179. package/themes/nobelium/components/BlogListBar.js +39 -0
  180. package/themes/nobelium/components/BlogListPage.tsx +67 -0
  181. package/themes/nobelium/components/BlogListScroll.tsx +96 -0
  182. package/themes/nobelium/components/BlogPost.tsx +33 -0
  183. package/themes/nobelium/components/DarkModeButton.tsx +50 -0
  184. package/themes/nobelium/components/ExampleRecentComments.js +35 -0
  185. package/themes/nobelium/components/Footer.tsx +35 -0
  186. package/themes/nobelium/components/JumpToTopButton.tsx +39 -0
  187. package/themes/nobelium/components/LanguageSwitchButton.tsx +58 -0
  188. package/themes/nobelium/components/MenuItemCollapse.tsx +92 -0
  189. package/themes/nobelium/components/MenuItemDrop.tsx +83 -0
  190. package/themes/nobelium/components/Nav/Nav.module.css +50 -0
  191. package/themes/nobelium/components/Nav/Nav.tsx +187 -0
  192. package/themes/nobelium/components/RandomPostButton.tsx +31 -0
  193. package/themes/nobelium/components/SearchButton.tsx +31 -0
  194. package/themes/nobelium/components/SearchInput.tsx +94 -0
  195. package/themes/nobelium/components/SearchNavBar.js +19 -0
  196. package/themes/nobelium/components/SideBar.js +83 -0
  197. package/themes/nobelium/components/SvgIcon.js +29 -0
  198. package/themes/nobelium/components/TagItem.js +13 -0
  199. package/themes/nobelium/components/Tags.tsx +44 -0
  200. package/themes/nobelium/components/Title.js +19 -0
  201. package/themes/nobelium/index.tsx +28 -0
  202. package/themes/nobelium/layout/LayoutBase.tsx +79 -0
  203. package/themes/nobelium/pages/Archive.tsx +30 -0
  204. package/themes/nobelium/pages/Category.tsx +43 -0
  205. package/themes/nobelium/pages/Home.tsx +22 -0
  206. package/themes/nobelium/pages/PageNotFound.tsx +15 -0
  207. package/themes/nobelium/pages/Post.tsx +34 -0
  208. package/themes/nobelium/pages/PostList.tsx +74 -0
  209. package/themes/nobelium/pages/Search.tsx +65 -0
  210. package/themes/nobelium/pages/Tag.tsx +42 -0
  211. package/themes/nobelium/providers/index.tsx +60 -0
  212. package/themes/nobelium/stores/index.tsx +42 -0
  213. package/themes/nobelium/theme.config.ts +17 -0
  214. package/themes/nobelium/types/index.ts +10 -0
  215. package/tsconfig.json +29 -0
  216. package/types/index.ts +1 -0
  217. package/types/page.ts +102 -0
  218. package/vercel.json +5 -0
@@ -0,0 +1,103 @@
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
+ CategoryPageProps,
11
+ ThemeCategoryPageProps,
12
+ } from '@/types';
13
+ import type { FC } from 'react';
14
+ import type { ParsedUrlQuery } from 'querystring';
15
+
16
+ export interface CategoryPageParams extends ParsedUrlQuery {
17
+ category: string;
18
+ page: string;
19
+ }
20
+
21
+ /**
22
+ * 分类页
23
+ * @param {*} props
24
+ * @returns
25
+ */
26
+
27
+ const CategoryPage: FC<CategoryPageProps> = (props) => {
28
+ const { siteInfo } = props;
29
+ const { t } = useTranslation('common');
30
+ // 根据页面路径加载不同Layout文件
31
+ const Layout = useLayout() as FC<ThemeCategoryPageProps>;
32
+
33
+ const pageMeta: PageMeta = {
34
+ title: `${props.category} | ${t('category')} | ${siteInfo?.title || ''}`,
35
+ description: siteInfo?.description,
36
+ slug: 'category/' + props.category,
37
+ image: siteInfo?.pageCover,
38
+ type: 'website',
39
+ };
40
+
41
+ return <Layout pageMeta={pageMeta} {...props} />;
42
+ };
43
+
44
+ export const getStaticProps: GetStaticProps<
45
+ CategoryPageProps,
46
+ CategoryPageParams
47
+ > = async (context) => {
48
+ const { category, page } = context.params as CategoryPageParams;
49
+ const pageNumber = parseInt(page, 10);
50
+ const { allPages, ...globalProps } = await getGlobalData(
51
+ 'category-page-props',
52
+ );
53
+
54
+ // 过滤状态类型
55
+ const posts = allPages
56
+ ?.filter(
57
+ (post) =>
58
+ post.type === 'Post' &&
59
+ post.status === 'Published' &&
60
+ post.category?.includes(category),
61
+ )
62
+ .slice(
63
+ BLOG.POSTS_PER_PAGE * (pageNumber - 1),
64
+ BLOG.POSTS_PER_PAGE * pageNumber,
65
+ );
66
+
67
+ return {
68
+ props: {
69
+ ...globalProps,
70
+ postCount: posts.length,
71
+ category,
72
+ page: pageNumber,
73
+ },
74
+ revalidate: BLOG.NEXT_REVALIDATE_SECOND,
75
+ };
76
+ };
77
+
78
+ export const getStaticPaths: GetStaticPaths<CategoryPageParams> = async () => {
79
+ const { categoryOptions, allPages } = await getGlobalData('category-paths');
80
+ const paths: { params: CategoryPageParams }[] = [];
81
+
82
+ categoryOptions?.forEach((category) => {
83
+ // 只处理发布状态的文章
84
+ const categoryPosts = allPages?.filter(
85
+ (post) =>
86
+ post.type === 'Post' &&
87
+ post.status === 'Published' &&
88
+ post.category?.includes(category.name),
89
+ );
90
+
91
+ const totalPages = Math.ceil(categoryPosts.length / BLOG.POSTS_PER_PAGE);
92
+ for (let i = 1; i <= totalPages; i++) {
93
+ paths.push({ params: { category: category.name, page: String(i) } });
94
+ }
95
+ });
96
+
97
+ return {
98
+ paths,
99
+ fallback: true,
100
+ };
101
+ };
102
+
103
+ export default CategoryPage;
@@ -0,0 +1,43 @@
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
+ import { omit } from 'lodash';
7
+
8
+ import type { FC } from 'react';
9
+ import type { PageMeta, CategoryIndexProps, ThemeCategoryProps } from '@/types';
10
+ import type { GetStaticProps } from 'next';
11
+
12
+ /**
13
+ * 分类首页
14
+ * @param {*} props
15
+ * @returns
16
+ */
17
+ const Category: FC<CategoryIndexProps> = (props) => {
18
+ const { siteInfo } = props;
19
+ const { t } = useTranslation('common');
20
+
21
+ // 根据页面路径加载不同Layout文件
22
+ const Layout = useLayout() as FC<ThemeCategoryProps>;
23
+
24
+ const pageMeta: PageMeta = {
25
+ title: `${t('category')} | ${siteInfo?.title}`,
26
+ description: siteInfo?.description,
27
+ image: siteInfo?.pageCover,
28
+ slug: 'category',
29
+ type: 'website',
30
+ };
31
+
32
+ return <Layout pageMeta={pageMeta} {...props} />;
33
+ };
34
+
35
+ export const getStaticProps: GetStaticProps<CategoryIndexProps> = async () => {
36
+ const globalData = await getGlobalData('category-index-props');
37
+ return {
38
+ props: omit(globalData, 'allPages'),
39
+ revalidate: BLOG.NEXT_REVALIDATE_SECOND,
40
+ };
41
+ };
42
+
43
+ export default Category;
@@ -0,0 +1,88 @@
1
+ import BLOG from '@/blog.config';
2
+ import { getPostBlocks } from '@/lib/notion';
3
+ import { getGlobalData } from '@/lib/notion/getNotionData';
4
+ import { generateRss } from '@/lib/rss';
5
+ import { generateRobotsTxt } from '@/lib/robots.txt';
6
+ import { useLayout } from '@/theme';
7
+ import { omit } from 'lodash';
8
+
9
+ import type { GetStaticProps } from 'next';
10
+ import type { FC } from 'react';
11
+ import type { HomeIndexProps, ThemeHomeProps } from '@/types';
12
+ import type { PageInfo } from '@/lib/notion/types';
13
+
14
+ /**
15
+ * 首页布局
16
+ * @param {*} props
17
+ * @returns
18
+ */
19
+ const Index: FC<HomeIndexProps> = (props) => {
20
+ // 根据页面路径加载不同Layout文件
21
+ const Layout = useLayout() as FC<ThemeHomeProps>;
22
+ return <Layout {...props} />;
23
+ };
24
+
25
+ /**
26
+ * SSG 获取数据
27
+ * @returns
28
+ */
29
+ export const getStaticProps: GetStaticProps<HomeIndexProps> = async () => {
30
+ const globalData = await getGlobalData('index');
31
+
32
+ const { siteInfo } = globalData;
33
+ let posts: PageInfo[] = globalData.allPages?.filter(
34
+ (page) => page.type === 'Post' && page.status === 'Published',
35
+ );
36
+
37
+ const pageMeta = {
38
+ title: `${siteInfo?.title} | ${siteInfo?.description}`,
39
+ description: siteInfo?.description,
40
+ image: siteInfo?.pageCover,
41
+ slug: '',
42
+ type: 'website',
43
+ };
44
+
45
+ // 处理分页
46
+ if (BLOG.POST_LIST_STYLE === 'scroll') {
47
+ // 滚动列表默认给前端返回所有数据
48
+ }
49
+
50
+ if (BLOG.POST_LIST_STYLE === 'page') {
51
+ posts = posts?.slice(0, BLOG.POSTS_PER_PAGE) || [];
52
+ }
53
+
54
+ // 预览文章内容
55
+ if (BLOG.POST_LIST_PREVIEW === 'true') {
56
+ await Promise.all(
57
+ posts.map(async (post) => {
58
+ if (!post.password) {
59
+ post.blockMap = await getPostBlocks(
60
+ post.id,
61
+ 'slug',
62
+ BLOG.POST_PREVIEW_LINES,
63
+ );
64
+ }
65
+ }),
66
+ );
67
+ }
68
+
69
+ // 生成robotTxt
70
+ generateRobotsTxt();
71
+ // 生成Feed订阅
72
+ if (BLOG.ENABLE_RSS) {
73
+ generateRss(globalData?.latestPosts || []);
74
+ }
75
+
76
+ // 生成全文索引 - 仅在 yarn build 时执行 && process.env.npm_lifecycle_event === 'build'
77
+
78
+ return {
79
+ props: {
80
+ pageMeta,
81
+ posts,
82
+ ...omit(globalData, 'allPages'),
83
+ },
84
+ revalidate: BLOG.NEXT_REVALIDATE_SECOND,
85
+ };
86
+ };
87
+
88
+ export default Index;
@@ -0,0 +1,93 @@
1
+ import BLOG from '@/blog.config';
2
+ import { getPostBlocks } from '@/lib/notion';
3
+ import { getGlobalData } from '@/lib/notion/getNotionData';
4
+ import { useLayout } from '@/theme';
5
+ import { omit } from 'lodash';
6
+
7
+ import type { GetStaticProps, GetStaticPaths } from 'next';
8
+ import type { PageMeta, PageIndexProps, ThemePageProps } from '@/types';
9
+ import type { FC } from 'react';
10
+ import type { ParsedUrlQuery } from 'querystring';
11
+
12
+ export interface PageParams extends ParsedUrlQuery {
13
+ page: string;
14
+ }
15
+
16
+ /**
17
+ * 文章列表分页
18
+ * @param {*} props
19
+ * @returns
20
+ */
21
+ const Page: FC<PageIndexProps> = (props) => {
22
+ const { siteInfo } = props;
23
+
24
+ // 根据页面路径加载不同Layout文件
25
+ const PostList = useLayout() as FC<ThemePageProps>;
26
+ const pageMeta: PageMeta = {
27
+ title: `${props?.page} | Page | ${siteInfo?.title}`,
28
+ description: siteInfo?.description,
29
+ image: siteInfo?.pageCover,
30
+ slug: 'page/' + props.page,
31
+ type: 'website',
32
+ };
33
+
34
+ return <PostList pageMeta={pageMeta} {...props} />;
35
+ };
36
+
37
+ export const getStaticPaths: GetStaticPaths<PageParams> = async () => {
38
+ const from = 'page-paths';
39
+ const { postCount } = await getGlobalData(from);
40
+ const totalPages = Math.ceil(postCount / BLOG.POSTS_PER_PAGE);
41
+ return {
42
+ // remove first page, we 're not gonna handle that.
43
+ paths: Array.from({ length: totalPages - 1 }, (_, i) => ({
44
+ params: { page: '' + (i + 2) },
45
+ })),
46
+ fallback: true,
47
+ };
48
+ };
49
+
50
+ export const getStaticProps: GetStaticProps<
51
+ PageIndexProps,
52
+ PageParams
53
+ > = async (context) => {
54
+ const { page } = context.params as PageParams;
55
+ const pageNumber = parseInt(page, 10);
56
+ const props = await getGlobalData(`page-${pageNumber}`);
57
+ const { allPages } = props;
58
+ const allPosts = allPages?.filter(
59
+ (page) => page.type === 'Post' && page.status === 'Published',
60
+ );
61
+ // 处理分页
62
+ const posts = allPosts.slice(
63
+ BLOG.POSTS_PER_PAGE * (pageNumber - 1),
64
+ BLOG.POSTS_PER_PAGE * pageNumber,
65
+ );
66
+
67
+ // 处理预览
68
+ if (BLOG.POST_LIST_PREVIEW === 'true') {
69
+ await Promise.all(
70
+ posts.map(async (post) => {
71
+ if (!post.password) {
72
+ post.blockMap = await getPostBlocks(
73
+ post.id,
74
+ 'slug',
75
+ BLOG.POST_PREVIEW_LINES,
76
+ );
77
+ }
78
+ }),
79
+ );
80
+ }
81
+
82
+ omit(props, 'allPages');
83
+ return {
84
+ props: {
85
+ ...props,
86
+ posts,
87
+ page: pageNumber,
88
+ },
89
+ revalidate: BLOG.NEXT_REVALIDATE_SECOND,
90
+ };
91
+ };
92
+
93
+ export default Page;
@@ -0,0 +1,162 @@
1
+ import { getGlobalData } from '@/lib/notion/getNotionData';
2
+ import BLOG from '@/blog.config';
3
+ import { useLayout } from '@/theme';
4
+ import { useTranslation } from 'next-i18next';
5
+
6
+ import type { GetStaticProps, GetStaticPaths } from 'next';
7
+ import type {
8
+ PageMeta,
9
+ SearchDetailProps,
10
+ ThemeSearchDetailProps,
11
+ } from '@/types';
12
+ import type { FC } from 'react';
13
+ import type { ParsedUrlQuery } from 'querystring';
14
+ import { DataBaseInfo, PageInfo } from '@/lib/notion/types';
15
+ import { getDataFromCache } from '@/lib/cache/cacheManager';
16
+
17
+ export interface CategoryDetailParams extends ParsedUrlQuery {
18
+ keyword: string;
19
+ }
20
+
21
+ const SearchDetail: FC<SearchDetailProps> = (props) => {
22
+ const { keyword, siteInfo } = props;
23
+ const { t } = useTranslation('nav');
24
+
25
+ // 根据页面路径加载不同Layout文件
26
+ const Layout = useLayout() as FC<ThemeSearchDetailProps>;
27
+
28
+ const pageMeta: PageMeta = {
29
+ title: `${keyword || ''}${keyword ? ' | ' : ''}${t('search')} | ${siteInfo?.title}`,
30
+ description: siteInfo?.title,
31
+ image: siteInfo?.pageCover,
32
+ slug: 'search/' + (keyword || ''),
33
+ type: 'website',
34
+ };
35
+
36
+ return <Layout {...props} pageMeta={pageMeta} />;
37
+ };
38
+
39
+ /**
40
+ * 服务端搜索
41
+ * @param {*} param0
42
+ * @returns
43
+ */
44
+ export const getStaticProps: GetStaticProps<
45
+ SearchDetailProps,
46
+ CategoryDetailParams
47
+ > = async (context) => {
48
+ const { allPages, ...restProps } = await getGlobalData('search-detail-page');
49
+ const { keyword } = context.params as CategoryDetailParams;
50
+ const allPosts = allPages?.filter(
51
+ (page) => page.type === 'Post' && page.status === 'Published',
52
+ );
53
+ const filteredPosts = await filterByMemCache(allPosts, keyword);
54
+ const posts =
55
+ BLOG.POST_LIST_STYLE === 'page'
56
+ ? filteredPosts.slice(0, BLOG.POSTS_PER_PAGE)
57
+ : filteredPosts;
58
+
59
+ return {
60
+ props: {
61
+ ...restProps,
62
+ postCount: posts.length,
63
+ keyword,
64
+ posts,
65
+ },
66
+ revalidate: BLOG.NEXT_REVALIDATE_SECOND,
67
+ };
68
+ };
69
+
70
+ export const getStaticPaths: GetStaticPaths<
71
+ CategoryDetailParams
72
+ > = async () => {
73
+ return {
74
+ paths: [{ params: { keyword: BLOG.TITLE } }],
75
+ fallback: true,
76
+ };
77
+ };
78
+
79
+ function extractTextContent(
80
+ sourceText: string[],
81
+ targetObj: any,
82
+ key: string,
83
+ ): string[] {
84
+ if (!targetObj) return sourceText;
85
+ const textArray = targetObj[key];
86
+ const text =
87
+ typeof textArray === 'object' ? getTextContent(textArray) : textArray;
88
+ return text && text !== 'Untitled' ? sourceText.concat(text) : sourceText;
89
+ }
90
+
91
+ /**
92
+ * 递归获取层层嵌套的数组
93
+ * @param {*} textArray
94
+ * @returns
95
+ */
96
+ function getTextContent(textArray: any): string {
97
+ return Array.isArray(textArray)
98
+ ? textArray.reduce((acc, item) => acc + getTextContent(item), '')
99
+ : typeof textArray === 'string'
100
+ ? textArray
101
+ : '';
102
+ }
103
+
104
+ /**
105
+ * 在内存缓存中进行全文索引
106
+ * @param {*} posts
107
+ * @param keyword 关键词
108
+ * @returns
109
+ */
110
+ async function filterByMemCache(posts: PageInfo[], keyword: string) {
111
+ if (!keyword) return [];
112
+ const lowerKeyword = keyword.toLowerCase().trim();
113
+ const filterPosts: PageInfo[] = [];
114
+
115
+ posts.forEach(async (post) => {
116
+ const page = await getDataFromCache<DataBaseInfo>(
117
+ `page_block_${post.id}`,
118
+ true,
119
+ );
120
+ const tagContent = post?.tags?.join(' ') || '';
121
+ const categoryContent = post?.category || '';
122
+ const articleInfo = (
123
+ post.title +
124
+ post.summary +
125
+ tagContent +
126
+ categoryContent
127
+ ).toLowerCase();
128
+
129
+ post.results = [];
130
+ let hit = articleInfo.includes(lowerKeyword);
131
+ let hitCount = 0;
132
+
133
+ const indexContent = page ? getPageContentText(post, page) : [];
134
+ for (const content of indexContent) {
135
+ if (!content) continue;
136
+ if (content.toLowerCase().includes(lowerKeyword)) {
137
+ hit = true;
138
+ hitCount += 1;
139
+ post.results.push(content);
140
+ } else if ((post.results.length - 1) / (hitCount || 1) < 3) {
141
+ post.results.push(content);
142
+ }
143
+ }
144
+
145
+ if (hit) filterPosts.push(post);
146
+ });
147
+ return filterPosts;
148
+ }
149
+
150
+ export function getPageContentText(post: PageInfo, dataBaseInfo: DataBaseInfo) {
151
+ let indexContent: string[] = [];
152
+ if (dataBaseInfo && dataBaseInfo?.block && !post.password) {
153
+ Object.keys(dataBaseInfo.block).forEach((id) => {
154
+ const properties = dataBaseInfo.block[id]?.value?.properties;
155
+ indexContent = extractTextContent(indexContent, properties, 'title');
156
+ indexContent = extractTextContent(indexContent, properties, 'caption');
157
+ });
158
+ }
159
+ return indexContent;
160
+ }
161
+
162
+ export default SearchDetail;
@@ -0,0 +1,166 @@
1
+ import { getGlobalData } from '@/lib/notion/getNotionData';
2
+ import { getDataFromCache } from '@/lib/cache/cacheManager';
3
+ import BLOG from '@/blog.config';
4
+ import { useLayout } from '@/theme';
5
+ import { useTranslation } from 'next-i18next';
6
+ import { isIterable } from '@/lib/utils';
7
+
8
+ import type { FC } from 'react';
9
+ import type { PageMeta, SearchPageProps, ThemeSearchPageProps } from '@/types';
10
+ import type { ParsedUrlQuery } from 'querystring';
11
+ import type { GetStaticPaths, GetStaticProps } from 'next';
12
+ import type { DataBaseInfo, PageInfo } from '@/lib/notion/types';
13
+
14
+ export interface SearchPageParams extends ParsedUrlQuery {
15
+ keyword: string;
16
+ page: string;
17
+ }
18
+
19
+ const SearchPage: FC<SearchPageProps> = (props) => {
20
+ const { keyword, siteInfo } = props;
21
+ const { t } = useTranslation('nav');
22
+
23
+ // 根据页面路径加载不同Layout文件
24
+ const Layout = useLayout() as FC<ThemeSearchPageProps>;
25
+
26
+ const pageMeta: PageMeta = {
27
+ title: `${keyword || ''}${keyword ? ' | ' : ''}${t('search')} | ${siteInfo?.title}`,
28
+ description: siteInfo?.title,
29
+ image: siteInfo?.pageCover,
30
+ slug: 'search/' + (keyword || ''),
31
+ type: 'website',
32
+ };
33
+
34
+ return <Layout {...props} pageMeta={pageMeta} />;
35
+ };
36
+
37
+ /**
38
+ * 服务端搜索
39
+ * @param {*} param0
40
+ * @returns
41
+ */
42
+ export const getStaticProps: GetStaticProps<
43
+ SearchPageProps,
44
+ SearchPageParams
45
+ > = async (context) => {
46
+ const { keyword, page } = context.params as SearchPageParams;
47
+ const { allPages, ...restProps } = await getGlobalData('search-props');
48
+ const pageNumber = parseInt(page, 10);
49
+ const filteredPosts = allPages?.filter(
50
+ (page) => page.type === 'Post' && page.status === 'Published',
51
+ );
52
+ const posts = (await filterByMemCache(filteredPosts, keyword)).slice(
53
+ BLOG.POSTS_PER_PAGE * (pageNumber - 1),
54
+ BLOG.POSTS_PER_PAGE * pageNumber,
55
+ );
56
+ return {
57
+ props: {
58
+ ...restProps,
59
+ posts,
60
+ postCount: posts.length,
61
+ page: pageNumber,
62
+ keyword,
63
+ },
64
+ revalidate: BLOG.NEXT_REVALIDATE_SECOND,
65
+ };
66
+ };
67
+
68
+ export const getStaticPaths: GetStaticPaths<SearchPageParams> = () => {
69
+ return {
70
+ paths: [{ params: { keyword: BLOG.TITLE, page: '1' } }],
71
+ fallback: true,
72
+ };
73
+ };
74
+
75
+ /**
76
+ * 将对象的指定字段拼接到字符串
77
+ * @param sourceTextArray
78
+ * @param targetObj
79
+ * @param key
80
+ * @returns {*}
81
+ */
82
+ function appendText(sourceTextArray: string[], targetObj: any, key: string) {
83
+ if (!targetObj) return sourceTextArray;
84
+ const textArray = targetObj[key];
85
+ const text = textArray ? getTextContent(textArray) : '';
86
+ if (text && text !== 'Untitled') {
87
+ return sourceTextArray.concat(text);
88
+ }
89
+ return sourceTextArray;
90
+ }
91
+
92
+ /**
93
+ * 递归获取层层嵌套的数组
94
+ * @param {*} textArray
95
+ * @returns
96
+ */
97
+ function getTextContent(textArray: any) {
98
+ if (typeof textArray === 'object' && isIterable(textArray)) {
99
+ let result = '';
100
+ for (const textObj of textArray) {
101
+ result = result + getTextContent(textObj);
102
+ }
103
+ return result;
104
+ } else if (typeof textArray === 'string') {
105
+ return textArray;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * 在内存缓存中进行全文索引
111
+ * @param {*} posts
112
+ * @param keyword 关键词
113
+ * @returns
114
+ */
115
+ async function filterByMemCache(posts: PageInfo[], keyword: string) {
116
+ if (!keyword) return [];
117
+ const lowerKeyword = keyword.toLowerCase().trim();
118
+ const filterPosts: PageInfo[] = [];
119
+
120
+ posts.forEach(async (post) => {
121
+ const page = await getDataFromCache<DataBaseInfo>(
122
+ `page_block_${post.id}`,
123
+ true,
124
+ );
125
+ const tagContent = post?.tags?.join(' ') || '';
126
+ const categoryContent = post?.category || '';
127
+ const articleInfo = (
128
+ post.title +
129
+ post.summary +
130
+ tagContent +
131
+ categoryContent
132
+ ).toLowerCase();
133
+
134
+ post.results = [];
135
+ let hit = articleInfo.includes(lowerKeyword);
136
+ let hitCount = 0;
137
+
138
+ let indexContent = [post.summary];
139
+ if (page && page.block) {
140
+ Object.values(page.block).forEach((block) => {
141
+ const properties = block.value.properties;
142
+ indexContent = appendText(indexContent, properties, 'title');
143
+ indexContent = appendText(indexContent, properties, 'caption');
144
+ });
145
+ }
146
+
147
+ indexContent.forEach((content, index) => {
148
+ if (content && post.results) {
149
+ if (content.toLowerCase().includes(lowerKeyword)) {
150
+ hit = true;
151
+ hitCount += 1;
152
+ post.results.push(content);
153
+ } else if ((post.results.length - 1) / hitCount < 3 || index === 0) {
154
+ post.results.push(content);
155
+ }
156
+ }
157
+ });
158
+
159
+ if (hit) {
160
+ filterPosts.push(post);
161
+ }
162
+ });
163
+ return filterPosts;
164
+ }
165
+
166
+ export default SearchPage;