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,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;
|
package/pages/index.tsx
ADDED
@@ -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;
|