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