├── .prettierignore
├── .dockerignore
├── public
├── css
│ ├── custom.css
│ ├── img-shadow.css
│ ├── theme-simple.css
│ └── prism-mac-style.css
├── js
│ └── custom.js
├── favicon.ico
├── webfonts
│ ├── fa-solid-900.ttf
│ ├── fa-brands-400.ttf
│ ├── fa-brands-400.woff2
│ ├── fa-regular-400.ttf
│ ├── fa-solid-900.woff2
│ ├── fa-regular-400.woff2
│ ├── fa-v4compatibility.ttf
│ └── fa-v4compatibility.woff2
└── favicon.svg
├── lib
├── notion.js
├── datasource
│ ├── flowus
│ │ ├── index.js
│ │ ├── utils.js
│ │ ├── getPageProperties.js
│ │ └── getPageData.js
│ ├── notion
│ │ ├── index.js
│ │ ├── utils.js
│ │ ├── getPageData.js
│ │ └── getPageProperties.js
│ ├── utils.js
│ ├── index.js
│ └── website.js
├── lang
│ ├── zh-HK.js
│ ├── zh-TW.js
│ ├── zh-CN.js
│ ├── en-US.js
│ └── fr-FR.js
├── notion
│ ├── getMetadata.js
│ ├── mapImage.js
│ ├── getAllPageIds.js
│ ├── getAllTags.js
│ ├── getAllLinks.js
│ ├── getPageTableOfContents.js
│ └── getPageProperties.js
├── cache
│ ├── memory_cache.js
│ ├── cache_manager.js
│ ├── local_file_cache.js
│ └── mongo_db_cache.js
├── gtag.js
├── robots.txt.js
├── font.js
├── formatDate.js
├── sitemap.xml.js
├── lang.js
├── theme.js
├── global.js
├── busuanzi.js
└── utils.js
├── themes
├── plain
│ ├── config.js
│ ├── Layout404.js
│ ├── LayoutIndex.js
│ ├── LayoutPage.js
│ ├── index.js
│ ├── components
│ │ ├── ArticlePage.js
│ │ ├── ListEmpty.js
│ │ ├── LinkMenuList.js
│ │ ├── LinkTag.js
│ │ ├── AsideLeft.js
│ │ ├── Logo.js
│ │ ├── SiteFooter.js
│ │ ├── ListPage.js
│ │ ├── TopNav.js
│ │ ├── LinkMenuItemDrop.js
│ │ ├── MenuItemDrop.js
│ │ ├── LinkCard.js
│ │ ├── MenuItemCollapse.js
│ │ └── SearchInput.js
│ ├── icons
│ │ └── logo.js
│ └── LayoutBase.js
└── index.js
├── vercel.json
├── components
├── comment
│ ├── Valine.js
│ ├── ValinePanel.js
│ ├── ValineCount.js
│ ├── Twikoo.js
│ ├── Giscus.js
│ ├── Utterances.js
│ ├── ValineComponent.js
│ ├── WalineComponent.js
│ └── WebMention.js
├── Gtag.js
├── NotionIcon.js
├── Busuanzi.js
├── GoogleAdsense.js
├── DarkModeButton.js
├── Select.js
├── ExternalScript.js
├── ThemeSwitch.js
├── KatexReact.js
├── Tabs.js
├── SideBarDrawer.js
├── Collapse.js
├── CommonHead.js
├── NotionPage.js
├── CommonScript.js
├── PrismMac.js
├── Draggable.js
├── DebugPanel.js
└── Vercel.js
├── postcss.config.js
├── .prettierrc.json
├── .env.example
├── .github
├── ISSUE_TEMPLATE
│ ├── config.yml
│ ├── feature_request.md
│ ├── deployment-error.md
│ └── bug_report.md
├── stale.yml
└── workflows
│ ├── docker-ghcr.yaml
│ └── codeql-analysis.yml
├── jsconfig.json
├── next-sitemap.config.js
├── pages
├── api
│ └── revalidate.js
├── 404.js
├── sitemap.xml.js
├── _document.js
├── index.js
├── page
│ └── [page].js
├── [prefix]
│ └── [page].js
└── _app.js
├── .gitignore
├── .eslintrc.js
├── Dockerfile
├── README.md
├── tailwind.config.js
├── LICENSE
├── core
├── settings.js
└── notion.js
├── .prettierrc.js
├── next.config.js
├── styles
├── nprogress.css
├── prism-theme.css
└── globals.css
├── package.json
└── config.js
/.prettierignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .next*
--------------------------------------------------------------------------------
/public/css/custom.css:
--------------------------------------------------------------------------------
1 | /* 静态文件导入 自定义样式*/
2 |
3 |
--------------------------------------------------------------------------------
/public/js/custom.js:
--------------------------------------------------------------------------------
1 | // 这里编写自定义js脚本;将被静态引入到页面中
2 |
--------------------------------------------------------------------------------
/lib/notion.js:
--------------------------------------------------------------------------------
1 | export { getAllTags } from './notion/getAllTags'
2 |
--------------------------------------------------------------------------------
/themes/plain/config.js:
--------------------------------------------------------------------------------
1 | const CONFIG = {}
2 | export default CONFIG
3 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liudding/notion-portal/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "cleanUrls": true,
3 | "trailingSlash": false,
4 | "headers": []
5 | }
6 |
--------------------------------------------------------------------------------
/components/comment/Valine.js:
--------------------------------------------------------------------------------
1 | import { Valine } from 'react-valine'
2 |
3 | export default Valine
4 |
--------------------------------------------------------------------------------
/components/comment/ValinePanel.js:
--------------------------------------------------------------------------------
1 | import { ValinePanel } from 'react-valine'
2 |
3 | export default ValinePanel
4 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {}
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/public/webfonts/fa-solid-900.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liudding/notion-portal/HEAD/public/webfonts/fa-solid-900.ttf
--------------------------------------------------------------------------------
/public/webfonts/fa-brands-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liudding/notion-portal/HEAD/public/webfonts/fa-brands-400.ttf
--------------------------------------------------------------------------------
/public/webfonts/fa-brands-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liudding/notion-portal/HEAD/public/webfonts/fa-brands-400.woff2
--------------------------------------------------------------------------------
/public/webfonts/fa-regular-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liudding/notion-portal/HEAD/public/webfonts/fa-regular-400.ttf
--------------------------------------------------------------------------------
/public/webfonts/fa-solid-900.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liudding/notion-portal/HEAD/public/webfonts/fa-solid-900.woff2
--------------------------------------------------------------------------------
/public/webfonts/fa-regular-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liudding/notion-portal/HEAD/public/webfonts/fa-regular-400.woff2
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": false,
4 | "trailingComma": "none",
5 | "arrowParens": "avoid"
6 | }
7 |
--------------------------------------------------------------------------------
/public/webfonts/fa-v4compatibility.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liudding/notion-portal/HEAD/public/webfonts/fa-v4compatibility.ttf
--------------------------------------------------------------------------------
/public/webfonts/fa-v4compatibility.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liudding/notion-portal/HEAD/public/webfonts/fa-v4compatibility.woff2
--------------------------------------------------------------------------------
/components/comment/ValineCount.js:
--------------------------------------------------------------------------------
1 | import { ValineCount } from 'react-valine'
2 |
3 | /**
4 | * 显示评论数
5 | */
6 | export default ValineCount
7 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # 环境变量 @see https://www.nextjs.cn/docs/basic-features/environment-variables
2 | NEXT_PUBLIC_VERSION=3.13.3
3 | NEXT_PUBLIC_REVALIDATE_SECOND=600000
--------------------------------------------------------------------------------
/lib/datasource/flowus/index.js:
--------------------------------------------------------------------------------
1 | import { getPageData } from './getPageData'
2 | import getPageProperties from './getPageProperties'
3 |
4 | export {
5 | getPageData,
6 | getPageProperties
7 | }
8 |
--------------------------------------------------------------------------------
/lib/datasource/notion/index.js:
--------------------------------------------------------------------------------
1 | import { getPageData } from './getPageData'
2 | import getPageProperties from './getPageProperties'
3 |
4 | export {
5 | getPageData,
6 | getPageProperties
7 | }
8 |
--------------------------------------------------------------------------------
/public/css/img-shadow.css:
--------------------------------------------------------------------------------
1 | /* 图片阴影 */
2 | #notion-article img{
3 | box-shadow: rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px;
4 | border-radius: 0.5rem;
5 | }
6 |
--------------------------------------------------------------------------------
/themes/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 切换主题请修改 config.js 中的 THEME 字段
3 | */
4 | import * as plain from './plain'
5 |
6 | export const ALL_THEME = [
7 | 'plain'
8 | ]
9 | export {
10 | plain
11 | }
12 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: Join Notion CN Community
4 | url: https://t.me/Notionso
5 | about: Ask and discuss Notion and Nobelium with other community members.
6 |
--------------------------------------------------------------------------------
/themes/plain/Layout404.js:
--------------------------------------------------------------------------------
1 | import LayoutBase from './LayoutBase'
2 |
3 | export const Layout404 = props => {
4 | return
5 | 404
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/themes/plain/LayoutIndex.js:
--------------------------------------------------------------------------------
1 | import ListPage from './components/ListPage'
2 | import LayoutBase from './LayoutBase'
3 |
4 | export const LayoutIndex = (props) => {
5 | return
6 |
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/themes/plain/LayoutPage.js:
--------------------------------------------------------------------------------
1 | import ListPage from './components/ListPage'
2 | import LayoutBase from './LayoutBase'
3 |
4 | export const LayoutPage = (props) => {
5 | return
6 |
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@/*": ["./*"],
6 | "@/components/*": ["components/*"],
7 | "@/data/*": ["data/*"],
8 | "@/lib/*": ["lib/*"],
9 | "@/styles/*": ["styles/*"]
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/themes/plain/index.js:
--------------------------------------------------------------------------------
1 | import CONFIG from './config'
2 | import { LayoutIndex } from './LayoutIndex'
3 | import { Layout404 } from './Layout404'
4 | import { LayoutPage } from './LayoutPage'
5 |
6 | export {
7 | CONFIG as THEME_CONFIG,
8 | LayoutIndex,
9 | Layout404,
10 | LayoutPage
11 | }
12 |
--------------------------------------------------------------------------------
/next-sitemap.config.js:
--------------------------------------------------------------------------------
1 | const CONFIG = require('./config')
2 |
3 | module.exports = {
4 | siteUrl: CONFIG.LINK,
5 | changefreq: 'daily',
6 | priority: 0.7,
7 | generateRobotsTxt: true,
8 | sitemapSize: 7000
9 | // ...other options
10 | // https://github.com/iamvishnusankar/next-sitemap#configuration-options
11 | }
12 |
--------------------------------------------------------------------------------
/lib/lang/zh-HK.js:
--------------------------------------------------------------------------------
1 | export default {
2 | NAV: {
3 | INDEX: '網誌',
4 | RSS: '訂閱',
5 | SEARCH: '搜尋',
6 | ABOUT: '關於',
7 | MAIL: '電郵'
8 | },
9 | PAGINATION: {
10 | PREV: '上一頁',
11 | NEXT: '下一頁'
12 | },
13 | SEARCH: {
14 | ARTICLES: '搜尋文章',
15 | TAGS: '搜尋標簽'
16 | },
17 | POST: {
18 | BACK: '返回',
19 | TOP: '回到頂端'
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/pages/api/revalidate.js:
--------------------------------------------------------------------------------
1 | export default async function handler(req, res) {
2 | if (req.query.secret !== process.env.REVALIDATE_SECRET) {
3 | return res.status(401).json({ message: 'Invalid secret' })
4 | }
5 |
6 | try {
7 | await res.revalidate('/')
8 | return res.json({ revalidated: true })
9 | } catch (err) {
10 | return res.status(500).send('Error revalidating')
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/themes/plain/components/ArticlePage.js:
--------------------------------------------------------------------------------
1 | import NotionPage from '@/components/NotionPage'
2 |
3 | /**
4 | * 详情文章页
5 | * @returns {JSX.Element}
6 | * @constructor
7 | */
8 | const ArticlePage = ({
9 | page,
10 | configs
11 | }) => {
12 | return (
13 |
14 |
15 |
16 | )
17 | }
18 |
19 | export default ArticlePage
20 |
--------------------------------------------------------------------------------
/themes/plain/components/ListEmpty.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * 空白博客 列表
4 | * @returns {JSX.Element}
5 | * @constructor
6 | */
7 | const ListEmpty = ({ currentSearch }) => {
8 | return
9 |
没有找到 {(currentSearch &&
{currentSearch}
)}
10 |
11 | }
12 | export default ListEmpty
13 |
--------------------------------------------------------------------------------
/lib/notion/getMetadata.js:
--------------------------------------------------------------------------------
1 | export default function getMetadata (rawMetadata) {
2 | const metadata = {
3 | locked: rawMetadata?.format?.block_locked,
4 | page_full_width: rawMetadata?.format?.page_full_width,
5 | page_font: rawMetadata?.format?.page_font,
6 | page_small_text: rawMetadata?.format?.page_small_text,
7 | created_time: rawMetadata.created_time,
8 | last_edited_time: rawMetadata.last_edited_time
9 | }
10 | return metadata
11 | }
12 |
--------------------------------------------------------------------------------
/lib/datasource/utils.js:
--------------------------------------------------------------------------------
1 | import CONFIG from '@/config'
2 | import { idToUuid } from 'notion-utils'
3 |
4 | export function getWebsiteConfigPageId() {
5 | if (CONFIG.DATASOURCE === 'notion') {
6 | return idToUuid(CONFIG.WEBSITE_NOTION_PAGE_ID)
7 | }
8 |
9 | return CONFIG.WEBSITE_NOTION_PAGE_ID
10 | }
11 |
12 | export function getLinksPageId() {
13 | if (CONFIG.DATASOURCE === 'notion') {
14 | return idToUuid(CONFIG.LINKS_NOTION_PAGE_ID)
15 | }
16 |
17 | return CONFIG.LINKS_NOTION_PAGE_ID
18 | }
19 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request (新特性建议)
3 | about: Suggest an idea for Nobelium.
4 | title: ''
5 | labels: enhancement
6 | assignees: tangly1024
7 | ---
8 |
9 |
13 |
14 | **为什么提出这个新的特性改动**
15 | 简要说明此特性解决的问题,例如,『博客站点的读者互动性不够强,和读者无法建立紧密的联系...』
16 |
17 | **描述一下你推荐的解决方案**
18 | 简要说明你的解决方案建议,例如,『Giscus评论插件功能更加强大,用户只需留言既可在你的邮箱收到通知。。。』
19 |
20 | **描述一下你考虑过的其它替代解决方案**
21 | 简要说明你所有想过的有可能解决此问题的方案。
22 |
23 | **补充说明**
24 | 补充与此特性相关的内容
25 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/deployment-error.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Deployment error (部署错误)
3 | about: 在安装部署NotionNext时需要什么帮助吗
4 | title: ''
5 | labels: deployment
6 | assignees: tangly1024
7 | ---
8 |
9 |
10 |
14 |
15 | **描述遇到的问题**
16 | 简单说明你遇到的问题,相关的日志、错误信息
17 |
18 | **相应配置**
19 | 相关的配置,例如notion_page_id;你的网站地址
20 |
21 | **截图**
22 | 相关的页面,应该用结果
23 |
24 | **环境**
25 |
26 | - 操作系统: [例如. iOS, Android, macOS, windows]
27 | - 浏览器 [例如. chrome, safari, firefox]
28 | - 版本 [e.g. 22]
29 |
--------------------------------------------------------------------------------
/lib/cache/memory_cache.js:
--------------------------------------------------------------------------------
1 | import cache from 'memory-cache'
2 | import CONFIG from '@/config'
3 |
4 | const cacheTime = CONFIG.isProd ? 10 * 60 : 120 * 60 // 120 minutes for dev,10 minutes for prod
5 |
6 | export async function getCache(key, options) {
7 | return await cache.get(key)
8 | }
9 |
10 | export async function setCache(key, data) {
11 | await cache.put(key, data, cacheTime * 1000)
12 | }
13 |
14 | export async function delCache(key) {
15 | await cache.del(key)
16 | }
17 |
18 | export default { getCache, setCache, delCache }
19 |
--------------------------------------------------------------------------------
/components/Gtag.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import { useRouter } from 'next/router'
3 | import * as gtag from '@/lib/gtag'
4 |
5 | const Gtag = () => {
6 | const router = useRouter()
7 | useEffect(() => {
8 | const gtagRouteChange = url => {
9 | gtag.pageview(url)
10 | }
11 | router.events.on('routeChangeComplete', gtagRouteChange)
12 | return () => {
13 | router.events.off('routeChangeComplete', gtagRouteChange)
14 | }
15 | }, [router.events])
16 | return null
17 | }
18 | export default Gtag
19 |
--------------------------------------------------------------------------------
/lib/gtag.js:
--------------------------------------------------------------------------------
1 | import CONFIG from '@/config'
2 |
3 | // https://developers.google.com/analytics/devguides/collection/gtagjs/pages
4 | export const pageview = url => {
5 | window.gtag('config', CONFIG.ANALYTICS_GOOGLE_ID, {
6 | page_path: url
7 | })
8 | }
9 |
10 | // https://developers.google.com/analytics/devguides/collection/gtagjs/events
11 | export const event = ({ action, category, label, value }) => {
12 | window.gtag('event', action, {
13 | event_category: category,
14 | event_label: label,
15 | value: value
16 | })
17 | }
18 |
--------------------------------------------------------------------------------
/components/NotionIcon.js:
--------------------------------------------------------------------------------
1 | /**
2 | * notion的图标icon
3 | * 可能是emoji 可能是 svg 也可能是 图片
4 | * @returns
5 | */
6 | const NotionIcon = ({ icon }) => {
7 | if (!icon) {
8 | return <>>
9 | }
10 |
11 | if (icon.startsWith('http') || icon.startsWith('data:')) {
12 | // return
13 | // eslint-disable-next-line @next/next/no-img-element
14 | return
15 | }
16 |
17 | return {icon}
18 | }
19 |
20 | export default NotionIcon
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report (Bug反馈)
3 | about: 报告一个软件的BUG来让NotionNext变得更好
4 | title: ''
5 | labels: bug
6 | assignees: tangly1024
7 | ---
8 |
9 |
13 |
14 | **描述bug**
15 | 简单说明bug的现象、相关的错误提示、日志等
16 |
17 | **复现步骤**
18 | 出现这个bug的操作步骤
19 |
20 | **期望的正常结果**
21 | 希望按这个步骤,正常操作结果是什么
22 |
23 | **截图**
24 | 相关的页面,应该用结果
25 |
26 | **环境**
27 |
28 | - 操作系统: [例如. iOS, Android, macOS, windows]
29 | - 浏览器 [例如. chrome, safari, firefox]
30 | - 版本 [e.g. 22]
31 |
32 | **补充说明**
33 | 与问题相关的其它说明
--------------------------------------------------------------------------------
/themes/plain/components/LinkMenuList.js:
--------------------------------------------------------------------------------
1 | import { LinkMenuItemDrop } from './LinkMenuItemDrop'
2 |
3 | export const LinkMenuList = (props) => {
4 | const { categories } = props
5 |
6 | return (<>
7 |
8 | {categories?.map(link => )}
9 |
10 |
11 | {categories?.map(link => )}
12 |
13 | >
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/lib/datasource/flowus/utils.js:
--------------------------------------------------------------------------------
1 |
2 | export function getTextContent(nodes) {
3 | return (nodes || []).reduce((carry, item) => {
4 | return carry + (item.text || '')
5 | }, '')
6 | }
7 |
8 | export function getImageUrl(path) {
9 | if (!path) {
10 | return null
11 | }
12 |
13 | if (path.startsWith('http')) {
14 | return path
15 | }
16 |
17 | if (path.startsWith('/')) {
18 | return 'https://cdn.flowus.cn' + path
19 | }
20 |
21 | return 'https://cdn.flowus.cn/' + path
22 | }
23 |
24 | export function getIconUrl(path) {
25 | return 'https://baiyunshan.flowus.cn/assets/byte-icon/dark' + path
26 | }
27 |
--------------------------------------------------------------------------------
/lib/robots.txt.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import { getGlobalSettings } from '@/core/settings'
3 |
4 | export async function generateRobotsTxt() {
5 | const settings = await getGlobalSettings()
6 | const content = `
7 | # *
8 | User-agent: *
9 | Allow: /
10 |
11 | # Host
12 | Host: ${settings.WEBSITE_URL}
13 |
14 | # Sitemaps
15 | Sitemap: ${settings.WEBSITE_URL}/sitemap.xml
16 |
17 | `
18 | try {
19 | fs.mkdirSync('./public', { recursive: true })
20 | fs.writeFileSync('./public/robots.txt', content)
21 | } catch (error) {
22 | // 在vercel运行环境是只读的,这里会报错;
23 | // 但在vercel编译阶段、或VPS等其他平台这行代码会成功执行
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/components/Busuanzi.js:
--------------------------------------------------------------------------------
1 | import busuanzi from '@/lib/busuanzi'
2 | import { useRouter } from 'next/router'
3 | import { useGlobal } from '@/lib/global'
4 | // import { useRouter } from 'next/router'
5 | import React from 'react'
6 |
7 | let path = ''
8 |
9 | export default function Busuanzi () {
10 | const { theme } = useGlobal()
11 | const Router = useRouter()
12 | Router.events.on('routeChangeComplete', (url, option) => {
13 | if (url !== path) {
14 | path = url
15 | busuanzi.fetch()
16 | }
17 | })
18 |
19 | // 更换主题时更新
20 | React.useEffect(() => {
21 | if (theme) {
22 | busuanzi.fetch()
23 | }
24 | }, [theme])
25 | return null
26 | }
27 |
--------------------------------------------------------------------------------
/pages/404.js:
--------------------------------------------------------------------------------
1 | import * as ThemeMap from '@/themes'
2 | import { useGlobal } from '@/lib/global'
3 | import { getWebsiteConfigs } from '@/lib/datasource/website'
4 |
5 | /**
6 | * 404
7 | * @param {*} props
8 | * @returns
9 | */
10 | const NoFound = props => {
11 | const { theme, siteInfo } = useGlobal()
12 | const ThemeComponents = ThemeMap[theme]
13 | const meta = { title: `${props?.siteInfo?.title} | 页面找不到啦`, image: siteInfo?.pageCover }
14 | return
15 | }
16 |
17 | export async function getStaticProps () {
18 | const props = (await getWebsiteConfigs()) || {}
19 | return { props }
20 | }
21 |
22 | export default NoFound
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
36 | # dev
37 | /data.json
38 | /pnpm-lock.yaml
39 | .idea
40 | .vscode
41 |
42 |
43 | # sitemap
44 | /public/robots.txt
45 | /public/sitemap.xml
46 | /public/rss/*
--------------------------------------------------------------------------------
/pages/sitemap.xml.js:
--------------------------------------------------------------------------------
1 | // pages/sitemap.xml.js
2 | import { getServerSideSitemap } from 'next-sitemap'
3 | import { getGlobalSettings } from '@/core/settings'
4 |
5 | export const getServerSideProps = async (ctx) => {
6 | const settings = await getGlobalSettings()
7 | const fields = [
8 | {
9 | loc: `${settings.WEBSITE_URL}`,
10 | lastmod: new Date().toISOString().split('T')[0],
11 | changefreq: 'daily',
12 | priority: '0.7'
13 | }
14 | ]
15 |
16 | // 缓存
17 | ctx.res.setHeader(
18 | 'Cache-Control',
19 | 'public, max-age=3600, stale-while-revalidate=59'
20 | )
21 |
22 | return getServerSideSitemap(ctx, fields)
23 | }
24 |
25 | export default () => {
26 | }
27 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Number of days of inactivity before an issue becomes stale
2 | daysUntilStale: 7
3 | # Number of days of inactivity before a stale issue is closed
4 | daysUntilClose: 3
5 | # Issues with these labels will never be considered stale
6 | exemptLabels:
7 | - pinned
8 | - security
9 | # Label to use when marking an issue as stale
10 | staleLabel: wontfix
11 | # Comment to post when marking an issue as stale. Set to `false` to disable
12 | markComment: >
13 | This issue has been automatically marked as stale because it has not had
14 | recent activity. It will be closed if no further activity occurs.
15 | # Comment to post when closing a stale issue. Set to `false` to disable
16 | closeComment: false
17 |
--------------------------------------------------------------------------------
/themes/plain/components/LinkTag.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const tagColors = {
4 | gray: 'rgba(120, 119, 116, 1)',
5 | brown: 'rgba(159, 107, 83, 1)',
6 | orange: 'rgba(217, 115, 13, 1)',
7 | yellow: 'rgba(203, 145, 47, 1)',
8 | green: 'rgba(68, 131, 97, 1)',
9 | purple: 'rgba(144, 101, 176, 1)',
10 | pink: 'rgba(193, 76, 138, 1)',
11 | red: 'rgba(212, 76, 71, 1)'
12 | }
13 |
14 | const LinkTag = ({ tag }) => {
15 | const color = tagColors[tag.color]
16 |
17 | return (
18 |
19 | {tag.name}
20 |
21 | )
22 | }
23 |
24 | export default LinkTag
25 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2021: true,
5 | node: true
6 | },
7 | extends: [
8 | 'plugin:react/recommended',
9 | 'plugin:@next/next/recommended',
10 | 'standard'
11 | ],
12 | parserOptions: {
13 | ecmaFeatures: {
14 | jsx: true
15 | },
16 | ecmaVersion: 12,
17 | sourceType: 'module'
18 | },
19 | plugins: [
20 | 'react',
21 | 'react-hooks'
22 | ],
23 | settings: {
24 | react: {
25 | version: 'detect'
26 | }
27 | },
28 | rules: {
29 | 'react/prop-types': 'off',
30 | 'space-before-function-paren': 0,
31 | 'react-hooks/rules-of-hooks': 'error' // Checks rules of Hooks
32 | },
33 | globals: {
34 | React: true
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/themes/plain/components/AsideLeft.js:
--------------------------------------------------------------------------------
1 | import Logo from './Logo'
2 | import { LinkMenuList } from './LinkMenuList'
3 |
4 | function AsideLeft(props) {
5 | return
6 |
7 |
11 |
12 |
15 |
16 |
17 | }
18 |
19 | export default AsideLeft
20 |
--------------------------------------------------------------------------------
/public/css/theme-simple.css:
--------------------------------------------------------------------------------
1 | #theme-simple #announcement-content {
2 | /* background-color: #f6f6f6; */
3 | }
4 |
5 | #theme-simple #blog-item-title {
6 | color: #276077;
7 | }
8 |
9 | .dark #theme-simple #blog-item-title {
10 | color: #d1d5db;
11 | }
12 |
13 | .notion {
14 | margin-top: 0 !important;
15 | margin-bottom: 0 !important;
16 | }
17 |
18 |
19 | /* 菜单下划线动画 */
20 | #theme-simple .menu-link {
21 | text-decoration: none;
22 | background-image: linear-gradient(#dd3333, #dd3333);
23 | background-repeat: no-repeat;
24 | background-position: bottom center;
25 | background-size: 0 2px;
26 | transition: background-size 100ms ease-in-out;
27 | }
28 |
29 | #theme-simple .menu-link:hover {
30 | background-size: 100% 2px;
31 | color: #dd3333;
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/lib/datasource/notion/utils.js:
--------------------------------------------------------------------------------
1 | // import { defaultMapImageUrl } from 'react-notion-x'
2 |
3 | export function getImageUrl(imgObj, block) {
4 | if (!imgObj) {
5 | return null
6 | }
7 | if (imgObj.startsWith('/')) {
8 | return 'https://www.notion.so' + imgObj // notion内部图片转相对路径为绝对路径
9 | }
10 |
11 | if (block?.type === 'bookmark') {
12 | return imgObj
13 | }
14 |
15 | if (imgObj.startsWith('http')) {
16 | if (imgObj.indexOf('secure.notion-static.com') > 0 || imgObj.indexOf('prod-files-secure') > 0) {
17 | let table = 'block'
18 | if (!block.type && block.schema) {
19 | table = 'collection'
20 | }
21 |
22 | return 'https://www.notion.so/image/' + encodeURIComponent(imgObj) + '?table=' + table + '&id=' + block.id
23 | }
24 | }
25 |
26 | // 其他图片链接 或 emoji
27 | return imgObj
28 | }
29 |
--------------------------------------------------------------------------------
/themes/plain/components/Logo.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import LogoIcon from '../icons/logo'
3 |
4 | const Logo = props => {
5 | const { siteInfo } = props
6 | return (
7 |
8 |
11 |
12 | {siteInfo.icon
13 | // eslint-disable-next-line @next/next/no-img-element
14 | ?
16 | : }
17 | {siteInfo?.title}
18 |
19 |
20 | )
21 | }
22 |
23 | export default Logo
24 |
--------------------------------------------------------------------------------
/pages/_document.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @next/next/no-document-import-in-page
2 | import Document, { Html, Head, Main, NextScript } from 'next/document'
3 | import CONFIG from '@/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 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | )
26 | }
27 | }
28 |
29 | export default MyDocument
30 |
--------------------------------------------------------------------------------
/themes/plain/components/SiteFooter.js:
--------------------------------------------------------------------------------
1 | function SiteFooter({ configs }) {
2 | const d = new Date()
3 | const currentYear = d.getFullYear()
4 | const copyrightDate = (function () {
5 | if (configs.COPYRIGHT_SINCE && configs.COPYRIGHT_SINCE !== currentYear) {
6 | return configs.COPYRIGHT_SINCE + '-' + currentYear
7 | }
8 | return currentYear
9 | })()
10 |
11 | return (
12 |
20 | )
21 | }
22 | export default SiteFooter
23 |
--------------------------------------------------------------------------------
/lib/notion/mapImage.js:
--------------------------------------------------------------------------------
1 | import CONFIG from '@/config'
2 |
3 | /**
4 | * Notion图片映射处理有emoji的图标
5 | * @param {*} img
6 | * @param {*} value
7 | * @returns
8 | */
9 | const mapImgUrl = (img, block, type = 'block') => {
10 | let ret = null
11 | if (!img) {
12 | return ret
13 | }
14 | // 相对目录,则视为notion的自带图片
15 | if (img.startsWith('/')) ret = 'https://www.notion.so' + img
16 |
17 | // 书签的地址本身就是永久链接,无需处理
18 | if (!ret && block?.type === 'bookmark') {
19 | ret = img
20 | }
21 |
22 | // notion永久图床地址
23 | if (!ret && img.indexOf('secure.notion-static.com') > 0 && (CONFIG.IMG_URL_TYPE === 'Notion' || type !== 'block')) {
24 | ret = 'https://www.notion.so/image/' + encodeURIComponent(img) + '?table=' + type + '&id=' + block.id
25 | }
26 |
27 | // 剩余的是第三方图片url或emoji
28 | if (!ret) {
29 | ret = img
30 | }
31 |
32 | return ret
33 | }
34 |
35 | export { mapImgUrl }
36 |
--------------------------------------------------------------------------------
/components/GoogleAdsense.js:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router'
2 | import { useEffect } from 'react'
3 |
4 | export default function GoogleAdsense () {
5 | const initGoogleAdsense = () => {
6 | const ads = document.getElementsByClassName('adsbygoogle').length
7 | const newAdsCount = ads
8 | if (newAdsCount > 0) {
9 | for (let i = 0; i <= newAdsCount; i++) {
10 | try {
11 | // eslint-disable-next-line no-undef
12 | (adsbygoogle = window.adsbygoogle || []).push({})
13 | } catch (e) {
14 |
15 | }
16 | }
17 | }
18 | }
19 |
20 | const router = useRouter()
21 | useEffect(() => {
22 | initGoogleAdsense()
23 | router.events.on('routeChangeComplete', initGoogleAdsense)
24 | return () => {
25 | router.events.off('routeChangeComplete', initGoogleAdsense)
26 | }
27 | }, [router.events])
28 | return null
29 | }
30 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG WEBSITE_NOTION_PAGE_ID
2 | # Install dependencies only when needed
3 | FROM node:14-alpine AS deps
4 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
5 | RUN apk add --no-cache libc6-compat
6 | WORKDIR /app
7 | COPY package.json ./
8 | RUN yarn install --frozen-lockfile
9 |
10 | # Rebuild the source code only when needed
11 | FROM node:14-alpine AS builder
12 | ARG WEBSITE_NOTION_PAGE_ID
13 | WORKDIR /app
14 | COPY --from=deps /app/node_modules ./node_modules
15 | COPY . .
16 | RUN yarn build
17 |
18 | ENV NODE_ENV production
19 |
20 | EXPOSE 3000
21 |
22 | # Next.js collects completely anonymous telemetry data about general usage.
23 | # Learn more here: https://nextjs.org/telemetry
24 | # Uncomment the following line in case you want to disable telemetry.
25 | # ENV NEXT_TELEMETRY_DISABLED 1
26 |
27 | CMD ["yarn", "start"]
28 |
--------------------------------------------------------------------------------
/components/DarkModeButton.js:
--------------------------------------------------------------------------------
1 | import { useGlobal } from '@/lib/global'
2 | import { saveDarkModeToCookies } from '@/lib/theme'
3 |
4 | const DarkModeButton = (props) => {
5 | const { isDarkMode, updateDarkMode } = useGlobal()
6 | // 用户手动设置主题
7 | const handleChangeDarkMode = () => {
8 | const newStatus = !isDarkMode
9 | saveDarkModeToCookies(newStatus)
10 | updateDarkMode(newStatus)
11 | const htmlElement = document.getElementsByTagName('html')[0]
12 | htmlElement.classList?.remove(newStatus ? 'light' : 'dark')
13 | htmlElement.classList?.add(newStatus ? 'dark' : 'light')
14 | }
15 |
16 | return
17 |
19 |
20 | }
21 | export default DarkModeButton
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Notion Portal
2 |
3 | ## 部署
4 |
5 | ### 部署到 Vercel
6 |
7 | 1. 点击按钮 [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fliudding%2Fnotion-portal), 开始部署
8 | 1. 如果没有 Vercel 账户需要先注册,然后授权访问 Github
9 | 2. 跟随 Vercel 引导,fork 项目,并等待最终部署成功
10 | 2. 准备网站数据模版
11 | 1. 点击打开 Notion 页面:[模版页面](https://dingliu.notion.site/NotionPortal-Template-354428ac80374a9d84a1890225578a0b)
12 | 2. 点击右上角的 "Duplicate" 将模版复制到自己的 Notion 空间
13 | 3. 在 “**NotionPortal**” 页面中设置网站的名称等网站信息
14 | 4. 在 “**我的链接**” 页面中存放你收集的网站
15 | 5. 点击右上角 Share 将页面 Publish to Web
16 | 3. 配置网站
17 | 1. 回到你 fork 的 Github 项目
18 | 2. 根据你的需要修改 config.js 中的配置
19 | 1. 网站信息页面的 ID 配置为 `WEBSITE_NOTION_PAGE_ID`
20 | 2. 链接页面的 ID 配置为 `LINKS_NOTION_PAGE_ID`
21 | 4. 也可以在 Vercel 环境变量中配置站点
22 | 1. 回到 Vercel,进入【Settings --> Environment Variables】配置环境变量
23 | 2. 将第 2 步中的两个页面 ID 配置到环境变量
24 | 5. 完成,可以访问你的导航网站了
25 |
--------------------------------------------------------------------------------
/lib/notion/getAllPageIds.js:
--------------------------------------------------------------------------------
1 |
2 | export default function getAllPageIds (collectionQuery, collectionId, collectionView, viewIds) {
3 | if (!collectionQuery && !collectionView) {
4 | return []
5 | }
6 | let pageIds = []
7 | if (collectionQuery && Object.values(collectionQuery).length > 0) {
8 | const pageSet = new Set()
9 | Object.values(collectionQuery[collectionId]).forEach(view => {
10 | view?.blockIds?.forEach(id => pageSet.add(id)) // group视图
11 | view?.collection_group_results?.blockIds?.forEach(id => pageSet.add(id)) // table视图
12 | })
13 | pageIds = [...pageSet]
14 | // console.log('PageIds: 从collectionQuery获取', collectionQuery, pageIds.length)
15 | } else if (viewIds && viewIds.length > 0) {
16 | const ids = collectionView[viewIds[0]].value.page_sort
17 | // console.log('PageIds: 从viewId获取', viewIds)
18 | for (const id of ids) {
19 | pageIds.push(id)
20 | }
21 | }
22 | return pageIds
23 | }
24 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const CONFIG = require('./config')
2 | const { fontFamilies } = require('./lib/font')
3 |
4 | module.exports = {
5 | content: ['./pages/**/*.js', './components/**/*.js', './layouts/**/*.js', './themes/**/*.js'],
6 | darkMode: CONFIG.APPEARANCE === 'class' ? 'media' : 'class', // or 'media' or 'class'
7 | theme: {
8 | fontFamily: fontFamilies,
9 | extend: {
10 | colors: {
11 | day: {
12 | DEFAULT: CONFIG.BACKGROUND_LIGHT || '#ffffff'
13 | },
14 | night: {
15 | DEFAULT: CONFIG.BACKGROUND_DARK || '#111827'
16 | },
17 | hexo: {
18 | 'background-gray': '#f5f5f5',
19 | 'black-gray': '#101414',
20 | 'light-gray': '#e5e5e5'
21 | }
22 | },
23 | maxWidth: {
24 | side: '14rem',
25 | '9/10': '90%',
26 | '8xl': '90rem'
27 | }
28 | }
29 | },
30 | variants: {
31 | extend: {}
32 | },
33 | plugins: []
34 | }
35 |
--------------------------------------------------------------------------------
/components/comment/Twikoo.js:
--------------------------------------------------------------------------------
1 | import CONFIG from '@/config'
2 | import React from 'react'
3 | import twikoo from 'twikoo'
4 |
5 | /**
6 | * Giscus评论 @see https://giscus.app/zh-CN
7 | * Contribute by @txs https://github.com/txs/NotionNext/commit/1bf7179d0af21fb433e4c7773504f244998678cb
8 | * @returns {JSX.Element}
9 | * @constructor
10 | */
11 |
12 | const Twikoo = ({ isDarkMode }) => {
13 | React.useEffect(() => {
14 | twikoo({
15 | envId: CONFIG.COMMENT_TWIKOO_ENV_ID, // 腾讯云环境填 envId;Vercel 环境填地址(https://xxx.vercel.app)
16 | el: '#twikoo', // 容器元素
17 | lang: CONFIG.LANG // 用于手动设定评论区语言,支持的语言列表 https://github.com/imaegoo/twikoo/blob/main/src/client/utils/i18n/index.js
18 | // region: 'ap-guangzhou', // 环境地域,默认为 ap-shanghai,腾讯云环境填 ap-shanghai 或 ap-guangzhou;Vercel 环境不填
19 | // path: location.pathname, // 用于区分不同文章的自定义 js 路径,如果您的文章路径不是 location.pathname,需传此参数
20 | })
21 | })
22 | return (
23 |
24 | )
25 | }
26 |
27 | export default Twikoo
28 |
--------------------------------------------------------------------------------
/components/Select.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | /**
4 | * 下拉单选框
5 | */
6 | class Select extends React.Component {
7 | constructor (props) {
8 | super(props)
9 | this.handleChange = this.handleChange.bind(this)
10 | }
11 |
12 | handleChange (event) {
13 | const { onChange } = this.props
14 | onChange(event.target.value)
15 | }
16 |
17 | render () {
18 | return (
19 |
20 | {this.props.label}
21 |
22 | {this.props.options?.map(o => (
23 |
24 | {o.text}
25 |
26 | ))}
27 |
28 |
29 | )
30 | }
31 | }
32 | Select.defaultProps = {
33 | label: '',
34 | value: '1',
35 | options: [
36 | { value: '1', text: '选项1' },
37 | { value: '2', text: '选项2' }
38 | ]
39 | }
40 | export default Select
41 |
--------------------------------------------------------------------------------
/lib/font.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 在此处配置字体
3 | */
4 | const CONFIG = require('../config')
5 |
6 | // const { fontFamily } = require('tailwindcss/defaultTheme')
7 |
8 | function CJK() {
9 | switch (CONFIG.LANG.toLowerCase()) {
10 | case 'zh-cn':
11 | case 'zh-sg':
12 | return 'SC'
13 | case 'zh':
14 | case 'zh-hk':
15 | case 'zh-tw':
16 | return 'TC'
17 | case 'ja':
18 | case 'ja-jp':
19 | return 'JP'
20 | case 'ko':
21 | case 'ko-kr':
22 | return 'KR'
23 | default:
24 | return null
25 | }
26 | }
27 |
28 | const fontSansCJK = !CJK()
29 | ? []
30 | : [`"Noto Sans CJK ${CJK()}"`, `"Noto Sans ${CJK()}"`]
31 | const fontSerifCJK = !CJK()
32 | ? []
33 | : [`"Noto Serif CJK ${CJK()}"`, `"Noto Serif ${CJK()}"`]
34 |
35 | const fontFamilies = {
36 | sans: [...CONFIG.FONT_SANS, ...fontSansCJK],
37 | serif: [...CONFIG.FONT_SERIF, ...fontSerifCJK],
38 | noEmoji: [
39 | 'ui-sans-serif',
40 | 'system-ui',
41 | '-apple-system',
42 | 'BlinkMacSystemFont',
43 | 'sans-serif'
44 | ]
45 | }
46 | module.exports = { fontFamilies }
47 |
--------------------------------------------------------------------------------
/public/css/prism-mac-style.css:
--------------------------------------------------------------------------------
1 | /**
2 | * @author https://github.com/txs
3 | * 当配置文件 CODE_MAC_BAR 开启时,此样式会被动态引入,将开启代码组件左上角的mac图标
4 | **/
5 | .code-toolbar {
6 | position: relative;
7 | padding-top: 0 !important;
8 | padding-bottom: 0 !important;
9 | width: 100%;
10 | border-radius: 0.5rem;
11 | margin-bottom: 0.5rem;
12 | }
13 |
14 | .toolbar-item{
15 | white-space: nowrap;
16 | }
17 |
18 | .toolbar-item > button {
19 | margin-top: -0.1rem;
20 | }
21 |
22 | pre[class*='language-'] {
23 | margin-top: 0rem !important;
24 | margin-bottom: 0rem !important;
25 | padding-top: 1.5rem !important;
26 |
27 | }
28 |
29 | .pre-mac {
30 | position: absolute;
31 | left: 0.9rem;
32 | top: 0.5rem;
33 | z-index: 10;
34 | }
35 |
36 | .pre-mac > span {
37 | width: 10px;
38 | height: 10px;
39 | border-radius: 50%;
40 | margin-right: 5px;
41 | float: left;
42 | }
43 |
44 | .pre-mac > span:nth-child(1) {
45 | background: red;
46 | }
47 |
48 | .pre-mac > span:nth-child(2) {
49 | background: sandybrown;
50 | }
51 |
52 | .pre-mac > span:nth-child(3) {
53 | background: limegreen;
54 | }
55 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021-present, tangly1024
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/components/comment/Giscus.js:
--------------------------------------------------------------------------------
1 | import CONFIG from '@/config'
2 | import { useGlobal } from '@/lib/global'
3 | import Giscus from '@giscus/react'
4 |
5 | /**
6 | * Giscus评论 @see https://giscus.app/zh-CN
7 | * Contribute by @txs https://github.com/txs/NotionNext/commit/1bf7179d0af21fb433e4c7773504f244998678cb
8 | * @returns {JSX.Element}
9 | * @constructor
10 | */
11 |
12 | const GiscusComponent = () => {
13 | const { isDarkMode } = useGlobal()
14 | const theme = isDarkMode ? 'dark' : 'light'
15 |
16 | return (
17 |
30 | )
31 | }
32 |
33 | export default GiscusComponent
34 |
--------------------------------------------------------------------------------
/components/comment/Utterances.js:
--------------------------------------------------------------------------------
1 | import CONFIG from '@/config'
2 | import { useEffect } from 'react'
3 |
4 | /**
5 | * 评论插件
6 | * @param issueTerm
7 | * @param layout
8 | * @returns {JSX.Element}
9 | * @constructor
10 | */
11 | const Utterances = ({ issueTerm, layout }) => {
12 | useEffect(() => {
13 | const theme =
14 | CONFIG.APPEARANCE === 'auto'
15 | ? 'preferred-color-scheme'
16 | : CONFIG.APPEARANCE === 'light'
17 | ? 'github-light'
18 | : 'github-dark'
19 | const script = document.createElement('script')
20 | const anchor = document.getElementById('comments')
21 | script.setAttribute('src', 'https://utteranc.es/client.js')
22 | script.setAttribute('crossorigin', 'anonymous')
23 | script.setAttribute('async', true)
24 | script.setAttribute('repo', CONFIG.COMMENT_UTTERRANCES_REPO)
25 | script.setAttribute('issue-term', 'title')
26 | script.setAttribute('theme', theme)
27 | anchor.appendChild(script)
28 | return () => {
29 | anchor.innerHTML = ''
30 | }
31 | })
32 | return
34 | }
35 |
36 | export default Utterances
37 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import CONFIG from '@/config'
2 |
3 | import * as ThemeMap from '@/themes'
4 | import { useGlobal } from '@/lib/global'
5 | import { generateRobotsTxt } from '@/lib/robots.txt'
6 | import { getLinks, getWebsiteConfigs } from '@/lib/datasource/website'
7 | const Index = props => {
8 | const { theme } = useGlobal()
9 | const ThemeComponents = ThemeMap[theme]
10 | return
11 | }
12 |
13 | export async function getStaticProps() {
14 | const {
15 | siteInfo,
16 | meta,
17 | configs
18 | } = await getWebsiteConfigs()
19 |
20 | meta.title = `${siteInfo?.title} | ${siteInfo?.description}`
21 | meta.description = meta.description || siteInfo?.description || ''
22 | meta.image = siteInfo?.pageCover || null
23 | meta.type = 'website'
24 |
25 | const links = await getLinks()
26 |
27 | // 生成robotTxt
28 | generateRobotsTxt()
29 |
30 | return {
31 | props: {
32 | configs,
33 | meta,
34 |
35 | siteInfo,
36 | categories: links.categories,
37 | links: links.links
38 | },
39 | revalidate: parseInt(CONFIG.NEXT_REVALIDATE_SECOND)
40 | }
41 | }
42 |
43 | export default Index
44 |
--------------------------------------------------------------------------------
/components/ExternalScript.js:
--------------------------------------------------------------------------------
1 | import CONFIG from '@/config'
2 | import { loadExternalResource } from '@/lib/utils'
3 | import { useEffect } from 'react'
4 |
5 | /**
6 | * 自定义引入外部JS 和 CSS
7 | * @returns
8 | */
9 | const ExternalScript = () => {
10 | useEffect(() => {
11 | // 静态导入本地自定义样式
12 | loadExternalResource(CONFIG.FONT_AWESOME, 'css')
13 | loadExternalResource('/css/custom.css', 'css')
14 | loadExternalResource('/js/custom.js', 'js')
15 |
16 | // 自动添加图片阴影
17 | if (CONFIG.IMG_SHADOW) {
18 | loadExternalResource('/css/img-shadow.css', 'css')
19 | }
20 |
21 | if (CONFIG.CUSTOM_EXTERNAL_JS && CONFIG.CUSTOM_EXTERNAL_JS.length > 0) {
22 | for (const url of CONFIG.CUSTOM_EXTERNAL_JS) {
23 | loadExternalResource(url, 'js')
24 | }
25 | }
26 | if (CONFIG.CUSTOM_EXTERNAL_CSS && CONFIG.CUSTOM_EXTERNAL_CSS.length > 0) {
27 | for (const url of CONFIG.CUSTOM_EXTERNAL_CSS) {
28 | loadExternalResource(url, 'css')
29 | }
30 | }
31 | // 渲染所有字体
32 | CONFIG.FONT_URL?.forEach(e => {
33 | loadExternalResource(e, 'css')
34 | })
35 | }, [])
36 |
37 | return null
38 | }
39 |
40 | export default ExternalScript
41 |
--------------------------------------------------------------------------------
/lib/notion/getAllTags.js:
--------------------------------------------------------------------------------
1 | import { isIterable } from '../utils'
2 |
3 | /**
4 | * 获取所有文章的标签
5 | * @param allPosts
6 | * @param sliceCount 默认截取数量为12,若为0则返回全部
7 | * @param tagOptions tags的下拉选项
8 | * @returns {Promise<{}|*[]>}
9 | */
10 | export function getAllTags({ allPages, sliceCount = 0, tagOptions }) {
11 | const allPosts = allPages.filter(page => page.type === 'Post' && page.status === 'Published')
12 |
13 | if (!allPosts || !tagOptions) {
14 | return []
15 | }
16 | // 计数
17 | let tags = allPosts.map(p => p.tags)
18 | tags = [...tags.flat()]
19 | const tagObj = {}
20 | tags.forEach(tag => {
21 | if (tag in tagObj) {
22 | tagObj[tag]++
23 | } else {
24 | tagObj[tag] = 1
25 | }
26 | })
27 | const list = []
28 | if (isIterable(tagOptions)) {
29 | tagOptions.forEach(c => {
30 | const count = tagObj[c.value]
31 | if (count) {
32 | list.push({ id: c.id, name: c.value, color: c.color, count })
33 | }
34 | })
35 | }
36 |
37 | // 按照数量排序
38 | // list.sort((a, b) => b.count - a.count)
39 | if (sliceCount && sliceCount > 0) {
40 | return list.slice(0, sliceCount)
41 | } else {
42 | return list
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/themes/plain/components/ListPage.js:
--------------------------------------------------------------------------------
1 | import * as Scroll from 'react-scroll'
2 | import ListEmpty from './ListEmpty'
3 | import LinkCard from './LinkCard'
4 |
5 | /**
6 | * 列表页
7 | * @returns {JSX.Element}
8 | * @constructor
9 | */
10 | const ListPage = ({
11 | links = [],
12 | categories,
13 | configs
14 | }) => {
15 | if (!links || links.length === 0) {
16 | return
17 | }
18 |
19 | return (
20 |
21 | {categories?.map(category => {
22 | return (
23 |
24 | {category.children.map((sub) => (
25 | {sub.title}
26 |
27 | {sub.children.map(item => (
28 |
29 | ))}
30 |
31 | ))}
32 |
33 | )
34 | })}
35 |
36 | )
37 | }
38 |
39 | export default ListPage
40 |
--------------------------------------------------------------------------------
/lib/lang/zh-TW.js:
--------------------------------------------------------------------------------
1 | export default {
2 | LOCALE: 'zh-TW',
3 | NAV: {
4 | INDEX: '部落格',
5 | RSS: '訂閱',
6 | SEARCH: '搜尋',
7 | ABOUT: '關於',
8 | NAVIGATOR: '導航',
9 | MAIL: '電郵',
10 | ARCHIVE: '封存'
11 | },
12 | COMMON: {
13 | MORE: '更多',
14 | NO_MORE: '沒有更多了',
15 | LATEST_POSTS: '最新文章',
16 | TAGS: '標籤',
17 | NO_TAG: '無標籤',
18 | CATEGORY: '分類',
19 | SHARE: '分享',
20 | SCAN_QR_CODE: 'QRCode',
21 | URL_COPIED: '連結已複製!',
22 | TABLE_OF_CONTENTS: '目錄',
23 | RELATE_POSTS: '相關文章',
24 | COPYRIGHT: '著作權',
25 | AUTHOR: '作者',
26 | URL: '連結',
27 | ANALYTICS: '分析',
28 | POSTS: '篇文章',
29 | ARTICLE: '文章',
30 | VISITORS: '位訪客',
31 | VIEWS: '次查看',
32 | COPYRIGHT_NOTICE: '本文採用 CC BY-NC-SA 4.0 許可協議,轉載請註明出處。',
33 | RESULT_OF_SEARCH: '篇搜尋到的结果',
34 | ARTICLE_DETAIL: '完整文章',
35 | PASSWORD_ERROR: '密碼錯誤!',
36 | ARTICLE_LOCK_TIPS: '文章已上鎖,請輸入訪問密碼',
37 | SUBMIT: '提交',
38 | POST_TIME: '发布于',
39 | LAST_EDITED_TIME: '最后更新'
40 | },
41 | PAGINATION: {
42 | PREV: '上一頁',
43 | NEXT: '下一頁'
44 | },
45 | SEARCH: {
46 | ARTICLES: '搜尋文章',
47 | TAGS: '搜尋標籤'
48 | },
49 | POST: {
50 | BACK: '返回',
51 | TOP: '回到頂端'
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/lib/formatDate.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 格式化日期
3 | * @param date
4 | * @param local
5 | * @returns {string}
6 | */
7 | export default function formatDate (date, local) {
8 | if (!date) return ''
9 | const d = new Date(date)
10 | const options = { year: 'numeric', month: 'short', day: 'numeric' }
11 | const res = d.toLocaleDateString(local, options)
12 | return local.slice(0, 2).toLowerCase() === 'zh'
13 | ? res.replace('年', '-').replace('月', '-').replace('日', '')
14 | : res
15 | }
16 |
17 | export function formatDateFmt (timestamp, fmt) {
18 | const date = new Date(timestamp)
19 | const o = {
20 | 'M+': date.getMonth() + 1, // 月份
21 | 'd+': date.getDate(), // 日
22 | 'h+': date.getHours(), // 小时
23 | 'm+': date.getMinutes(), // 分
24 | 's+': date.getSeconds(), // 秒
25 | 'q+': Math.floor((date.getMonth() + 3) / 3), // 季度
26 | S: date.getMilliseconds() // 毫秒
27 | }
28 | if (/(y+)/.test(fmt)) {
29 | fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
30 | }
31 | for (const k in o) {
32 | if (new RegExp('(' + k + ')').test(fmt)) {
33 | fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (('00' + o[k]).substr(('' + o[k]).length)))
34 | }
35 | }
36 | return fmt.trim()
37 | }
38 |
--------------------------------------------------------------------------------
/components/ThemeSwitch.js:
--------------------------------------------------------------------------------
1 | import { useGlobal } from '@/lib/global'
2 | import { ALL_THEME } from '@/themes'
3 | import React from 'react'
4 | import { Draggable } from './Draggable'
5 | /**
6 | *
7 | * @returns 主题切换
8 | */
9 | export function ThemeSwitch() {
10 | const { theme, changeTheme } = useGlobal()
11 |
12 | const onSelectChange = (e) => {
13 | changeTheme(e.target.value)
14 | }
15 |
16 | return (<>
17 |
18 |
19 |
20 |
21 | {/*
{theme}
*/}
22 |
23 | {ALL_THEME.map(t => {
24 | return {t}
25 | })}
26 |
27 |
28 |
29 |
30 | >
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/pages/page/[page].js:
--------------------------------------------------------------------------------
1 | import { useGlobal } from '@/lib/global'
2 | import * as ThemeMap from '@/themes'
3 | import { getGlobalSettings } from '@/core/settings'
4 | import { getWebsiteConfigs } from '@/lib/datasource/website'
5 |
6 | const Page = props => {
7 | const { theme } = useGlobal()
8 | const { siteInfo } = props
9 |
10 | const ThemeComponents = ThemeMap[theme]
11 | if (!siteInfo) {
12 | return <>>
13 | }
14 |
15 | const meta = {
16 | title: `${props.page} | Page | ${siteInfo?.title}`,
17 | description: siteInfo?.description,
18 | image: siteInfo?.pageCover,
19 | slug: 'page/' + props.page,
20 | type: 'website'
21 | }
22 | return
23 | }
24 |
25 | export async function getStaticPaths() {
26 | return {
27 | // remove first page, we 're not gonna handle that.
28 | paths: Array.from({ length: 1 }, (_, i) => ({
29 | params: { page: '' + (i + 2) }
30 | })),
31 | fallback: true
32 | }
33 | }
34 |
35 | export async function getStaticProps({ params: { page } }) {
36 | const props = await getWebsiteConfigs()
37 | const settings = await getGlobalSettings()
38 |
39 | props.page = page
40 |
41 | return {
42 | props,
43 | revalidate: parseInt(settings.REVALIDATE)
44 | }
45 | }
46 |
47 | export default Page
48 |
--------------------------------------------------------------------------------
/core/settings.js:
--------------------------------------------------------------------------------
1 | import { getGlobalNotionCollectionData } from '@/lib/notion/getNotionData'
2 | import CONFIG from '@/config'
3 |
4 | let SETTINGS = null
5 |
6 | export function initGlobalSettings(blocks) {
7 | SETTINGS = formatSettings(blocks)
8 | return SETTINGS
9 | }
10 |
11 | export async function getGlobalSettings() {
12 | if (SETTINGS) {
13 | return SETTINGS
14 | }
15 |
16 | const websiteSettings = await getGlobalNotionCollectionData({
17 | pageId: CONFIG.WEBSITE_NOTION_PAGE_ID
18 | })
19 |
20 | return initGlobalSettings(websiteSettings.collectionRows)
21 | }
22 |
23 | export function formatSettings(blocks) {
24 | const settings = {}
25 | for (const item of blocks) {
26 | if (!(item.Name in SETTING_KEYS)) {
27 | continue
28 | }
29 |
30 | const key = SETTING_KEYS[item.Name]
31 | if (typeof key !== 'object' || key === null) {
32 | settings[item.Name] = item.Value ?? key
33 | continue
34 | }
35 |
36 | if (key.type === 'array') {
37 | settings[item.Name] = item.Value.split(key.delimiter || ',')
38 | }
39 | }
40 |
41 | return settings
42 | }
43 |
44 | const SETTING_KEYS = {
45 | WEBSITE_URL: {},
46 | KEYWORDS: '',
47 | CATEGORY_LEVELS: {
48 | type: 'array',
49 | delimiter: ','
50 | },
51 | COPYRIGHT_SINCE: '',
52 | APPEARANCE: 'light',
53 | REVALIDATE: 120
54 | }
55 |
--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/lib/cache/cache_manager.js:
--------------------------------------------------------------------------------
1 | import MemoryCache from './memory_cache'
2 | import FileCache from './local_file_cache'
3 | // import MongoCache from './mongo_db_cache'
4 |
5 | let api
6 | if (process.env.MONGO_DB_URL && process.env.MONGO_DB_NAME) {
7 | // api = MongoCache
8 | } else if (process.env.ENABLE_FILE_CACHE) {
9 | api = FileCache
10 | } else {
11 | api = MemoryCache
12 | }
13 |
14 | /**
15 | * 为减少频繁接口请求,notion数据将被缓存
16 | * @param {*} key
17 | * @param force
18 | * @returns
19 | */
20 | export async function getDataFromCache(key, force) {
21 | if (process.env.ENABLE_CACHE || force) {
22 | const dataFromCache = await api.getCache(key)
23 | if (JSON.stringify(dataFromCache) === '[]') {
24 | return null
25 | }
26 | return api.getCache(key)
27 | } else {
28 | return null
29 | }
30 | }
31 |
32 | export async function setDataToCache(key, data) {
33 | if (!data) {
34 | return
35 | }
36 | await api.setCache(key, data)
37 | }
38 |
39 | export async function getOrSetFromCache(key, func, force) {
40 | let cacheData = await getDataFromCache(key, force)
41 | if (!cacheData && func) {
42 | cacheData = await func()
43 | await setDataToCache(key, cacheData)
44 | }
45 | return cacheData
46 | }
47 |
48 | export async function delCacheData(key) {
49 | if (!process.env.ENABLE_CACHE) {
50 | return
51 | }
52 | await api.delCache(key)
53 | }
54 |
--------------------------------------------------------------------------------
/lib/lang/zh-CN.js:
--------------------------------------------------------------------------------
1 | export default {
2 | LOCALE: 'zh-CN',
3 | NAV: {
4 | INDEX: '首页',
5 | RSS: '订阅',
6 | SEARCH: '搜索',
7 | ABOUT: '关于',
8 | NAVIGATOR: '导航',
9 | MAIL: '邮箱',
10 | ARCHIVE: '归档'
11 | },
12 | COMMON: {
13 | MORE: '更多',
14 | NO_MORE: '没有更多了',
15 | LATEST_POSTS: '最新文章',
16 | TAGS: '标签',
17 | NO_TAG: 'NoTag',
18 | CATEGORY: '分类',
19 | SHARE: '分享',
20 | SCAN_QR_CODE: '扫一扫二维码',
21 | URL_COPIED: '链接已复制!',
22 | TABLE_OF_CONTENTS: '目录',
23 | RELATE_POSTS: '相关文章',
24 | COPYRIGHT: '声明',
25 | AUTHOR: '作者',
26 | URL: '链接',
27 | ANALYTICS: '统计',
28 | POSTS: '篇文章',
29 | ARTICLE: '文章',
30 | VISITORS: '位访客',
31 | VIEWS: '次查看',
32 | COPYRIGHT_NOTICE: '本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。',
33 | RESULT_OF_SEARCH: '篇搜索到的结果',
34 | ARTICLE_DETAIL: '文章详情',
35 | PASSWORD_ERROR: '密码错误!',
36 | ARTICLE_LOCK_TIPS: '文章已上锁,请输入访问密码',
37 | SUBMIT: '提交',
38 | POST_TIME: '发布于',
39 | LAST_EDITED_TIME: '最后更新',
40 | RECENT_COMMENTS: '最新评论',
41 | DEBUG_OPEN: '开启调试',
42 | DEBUG_CLOSE: '关闭调试',
43 | THEME_SWITCH: '切换主题',
44 | ANNOUNCEMENT: '公告'
45 | },
46 | PAGINATION: {
47 | PREV: '上一页',
48 | NEXT: '下一页'
49 | },
50 | SEARCH: {
51 | ARTICLES: '搜索文章',
52 | TAGS: '搜索标签'
53 | },
54 | POST: {
55 | BACK: '返回上页',
56 | TOP: '回到顶部'
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/components/KatexReact.js:
--------------------------------------------------------------------------------
1 | import KaTeX from 'katex'
2 | import React from 'react'
3 |
4 | const TeX = ({
5 | children,
6 | math,
7 | block,
8 | errorColor,
9 | renderError,
10 | settings,
11 | as: asComponent,
12 | ...props
13 | }) => {
14 | const Component = asComponent || (block ? 'div' : 'span')
15 | const content = (children ?? math)
16 | const [state, setState] = React.useState({ innerHtml: '' })
17 |
18 | React.useEffect(() => {
19 | try {
20 | const innerHtml = KaTeX.renderToString(content, {
21 | displayMode: true,
22 | errorColor,
23 | throwOnError: !!renderError,
24 | ...settings
25 | })
26 |
27 | setState({ innerHtml })
28 | } catch (error) {
29 | if (error instanceof KaTeX.ParseError || error instanceof TypeError) {
30 | if (renderError) {
31 | setState({ errorElement: renderError(error) })
32 | } else {
33 | setState({ innerHtml: error.message })
34 | }
35 | } else {
36 | throw error
37 | }
38 | }
39 | }, [block, content, errorColor, renderError, settings])
40 |
41 | if ('errorElement' in state) {
42 | return state.errorElement
43 | }
44 |
45 | return (
46 |
50 | )
51 | }
52 |
53 | export default React.memo(TeX)
54 |
--------------------------------------------------------------------------------
/lib/sitemap.xml.js:
--------------------------------------------------------------------------------
1 |
2 | import fs from 'fs'
3 | import CONFIG from '@/config'
4 |
5 | export async function generateSitemapXml({ allPages }) {
6 | const urls = [{
7 | loc: `${CONFIG.LINK}`,
8 | lastmod: new Date().toISOString().split('T')[0],
9 | changefreq: 'daily'
10 | }]
11 |
12 | allPages?.forEach(post => {
13 | urls.push({
14 | loc: `${CONFIG.LINK}/${post.slug}`,
15 | lastmod: new Date(post?.date?.start_date || post?.createdTime).toISOString().split('T')[0],
16 | changefreq: 'daily'
17 | })
18 | })
19 | const xml = createSitemapXml(urls)
20 | try {
21 | fs.writeFileSync('sitemap.xml', xml)
22 | fs.writeFileSync('./public/sitemap.xml', xml)
23 | } catch (error) {
24 | console.warn('无法写入文件', error)
25 | }
26 | }
27 | function createSitemapXml(urls) {
28 | let urlsXml = ''
29 | urls.forEach(u => {
30 | urlsXml += `
31 | ${u.loc}
32 | ${u.lastmod}
33 | ${u.changefreq}
34 |
35 | `
36 | })
37 |
38 | return `
39 |
44 | ${urlsXml}
45 |
46 | `
47 | }
48 |
--------------------------------------------------------------------------------
/pages/[prefix]/[page].js:
--------------------------------------------------------------------------------
1 | import { useGlobal } from '@/lib/global'
2 | import * as ThemeMap from '@/themes'
3 | import { getLinks, getWebsiteConfigs } from '@/lib/datasource/website'
4 | import { getPageData } from '@/lib/datasource'
5 |
6 | const Page = props => {
7 | const { theme } = useGlobal()
8 | const { siteInfo, meta } = props
9 | const ThemeComponents = ThemeMap[theme]
10 | if (!siteInfo) {
11 | return <>>
12 | }
13 |
14 | const metadata = {
15 | ...meta,
16 | title: `${props.page} | ${siteInfo?.title}`,
17 | description: meta?.description || siteInfo?.description,
18 | image: siteInfo?.pageCover,
19 | slug: 'page/' + props.page,
20 | type: 'website'
21 | }
22 | return
23 | }
24 |
25 | export async function getStaticPaths(props) {
26 | const { configs } = await getWebsiteConfigs()
27 | const { links } = await getLinks()
28 |
29 | return {
30 | paths: links.map(i => ({
31 | params: { page: i.id, prefix: configs.DETAIL_PAGE_URL_PREFIX || 'p' }
32 | })),
33 | fallback: true
34 | }
35 | }
36 |
37 | export async function getStaticProps({ params }) {
38 | const { siteInfo, configs } = await getWebsiteConfigs()
39 | const page = await getPageData(params.page)
40 |
41 | return {
42 | props: { siteInfo, configs, params, page: page },
43 | revalidate: parseInt(configs.REVALIDATE)
44 | }
45 | }
46 |
47 | export default Page
48 |
--------------------------------------------------------------------------------
/themes/plain/icons/logo.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | const LogoIcon = () => (
4 |
5 |
6 |
7 | )
8 |
9 | export default LogoIcon
10 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | /**
2 | * .prettierrc.js
3 | * 在VSCode中安装prettier插件 打开插件配置填写`.prettierrc.js` 将本文件作为其代码格式化规范
4 | * 在本文件中修改格式化规则,不会同时触发改变ESLint代码检查,所以每次修改本文件需要重启VSCode,ESLint检查才能同步代码格式化
5 | * 需要相应的代码格式化规范请自行查阅配置,下面为默认项目配置
6 | */
7 | module.exports = {
8 | // 一行最多多少个字符
9 | printWidth: 150,
10 | // 指定每个缩进级别的空格数
11 | tabWidth: 2,
12 | // 使用制表符而不是空格缩进行
13 | useTabs: true,
14 | // 在语句末尾是否需要分号
15 | semi: true,
16 | // 是否使用单引号
17 | singleQuote: true,
18 | // 更改引用对象属性的时间 可选值""
19 | quoteProps: 'as-needed',
20 | // 多行时尽可能打印尾随逗号。(例如,单行数组永远不会出现逗号结尾。) 可选值"",默认none
21 | trailingComma: 'all',
22 | // 在对象文字中的括号之间打印空格
23 | bracketSpacing: true,
24 | // jsx 中是否使用单引号
25 | jsxSingleQuote: false,
26 | // jsx 标签的反尖括号需要换行
27 | jsxBracketSameLine: false,
28 | // 在单独的箭头函数参数周围包括括号 always:(x) => x \ avoid:x => x
29 | arrowParens: 'always',
30 | // 这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码
31 | rangeStart: 0,
32 | rangeEnd: Infinity,
33 | // 指定要使用的解析器,不需要写文件开头的 @prettier
34 | requirePragma: false,
35 | // 不需要自动在文件开头插入 @prettier
36 | insertPragma: false,
37 | // 使用默认的折行标准 always\never\preserve
38 | proseWrap: 'preserve',
39 | // 指定HTML文件的全局空格敏感度 css\strict\ignore
40 | htmlWhitespaceSensitivity: 'css',
41 | // 在 windows 操作系统中换行符通常是回车 (CR) 加换行分隔符 (LF),也就是回车换行(CRLF),
42 | // 然而在 Linux 和 Unix 中只使用简单的换行分隔符 (LF)。
43 | // 对应的控制字符为 "\n" (LF) 和 "\r\n"(CRLF)。auto意为保持现有的行尾
44 | // 换行符使用 lf 结尾是 可选值""
45 | endOfLine: 'auto',
46 | //保存时自动格式化
47 | formatOnSave: true,
48 | //粘贴时自动格式化
49 | formatOnPaste: true,
50 | };
51 |
--------------------------------------------------------------------------------
/components/comment/ValineComponent.js:
--------------------------------------------------------------------------------
1 | import CONFIG from '@/config'
2 | import { useRouter } from 'next/router'
3 | import React from 'react'
4 | import Valine from 'valine'
5 |
6 | const ValineComponent = (props) => {
7 | const router = useRouter()
8 | const initValine = (url) => {
9 | const valine = new Valine({
10 | el: '#v-comments',
11 | appId: CONFIG.COMMENT_VALINE_APP_ID,
12 | appKey: CONFIG.COMMENT_VALINE_APP_KEY,
13 | avatar: '',
14 | path: url || router.asPath,
15 | recordIP: true,
16 | placeholder: CONFIG.COMMENT_VALINE_PLACEHOLDER,
17 | serverURLs: CONFIG.COMMENT_VALINE_SERVER_URLS,
18 | visitor: true
19 | })
20 | if (!valine) {
21 | console.error('valine错误')
22 | }
23 | }
24 |
25 | const updateValine = url => {
26 | // 移除旧的评论区,否则会重复渲染。
27 | const wrapper = document.getElementById('v-wrapper')
28 | const comments = document.getElementById('v-comments')
29 | wrapper.removeChild(comments)
30 | const newComments = document.createElement('div')
31 | newComments.id = 'v-comments'
32 | newComments.name = new Date()
33 | wrapper.appendChild(newComments)
34 | initValine(url)
35 | }
36 |
37 | React.useEffect(() => {
38 | initValine()
39 | router.events.on('routeChangeComplete', updateValine)
40 | return () => {
41 | router.events.off('routeChangeComplete', updateValine)
42 | }
43 | }, [])
44 | return
45 |
46 |
47 | }
48 |
49 | export default ValineComponent
50 |
--------------------------------------------------------------------------------
/lib/lang/en-US.js:
--------------------------------------------------------------------------------
1 | export default {
2 | LOCALE: 'en-US',
3 | NAV: {
4 | INDEX: 'Blog',
5 | RSS: 'RSS',
6 | SEARCH: 'Search',
7 | ABOUT: 'About',
8 | MAIL: 'E-Mail',
9 | ARCHIVE: 'Archive'
10 | },
11 | COMMON: {
12 | MORE: 'More',
13 | NO_MORE: 'No More',
14 | LATEST_POSTS: 'Latest posts',
15 | TAGS: 'Tags',
16 | NO_TAG: 'NoTag',
17 | CATEGORY: 'Category',
18 | SHARE: 'Share',
19 | SCAN_QR_CODE: 'Scan QRCode',
20 | URL_COPIED: 'URL has copied!',
21 | TABLE_OF_CONTENTS: 'Catalog',
22 | RELATE_POSTS: 'Relate Posts',
23 | COPYRIGHT: 'Copyright',
24 | AUTHOR: 'Author',
25 | URL: 'URL',
26 | POSTS: 'Posts',
27 | ARTICLE: 'Article',
28 | VISITORS: 'Visitors',
29 | VIEWS: 'Views',
30 | COPYRIGHT_NOTICE: 'All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!',
31 | RESULT_OF_SEARCH: 'Results Found',
32 | ARTICLE_DETAIL: 'Article Details',
33 | PASSWORD_ERROR: 'Password Error!',
34 | ARTICLE_LOCK_TIPS: 'Please Enter the password:',
35 | SUBMIT: 'Submit',
36 | POST_TIME: 'Post on',
37 | LAST_EDITED_TIME: 'Last edited',
38 | RECENT_COMMENTS: 'Recent Comments',
39 | DEBUG_OPEN: 'Debug',
40 | DEBUG_CLOSE: 'Close',
41 | THEME_SWITCH: 'Theme Switch',
42 | ANNOUNCEMENT: 'Announcement'
43 | },
44 | PAGINATION: {
45 | PREV: 'Prev',
46 | NEXT: 'Next'
47 | },
48 | SEARCH: {
49 | ARTICLES: 'Search Articles',
50 | TAGS: 'Search in'
51 | },
52 | POST: {
53 | BACK: 'Back',
54 | TOP: 'Top'
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/themes/plain/components/TopNav.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import SideBarDrawer from '@/components/SideBarDrawer'
3 | import Logo from './Logo'
4 | import { LinkMenuList } from './LinkMenuList'
5 |
6 | /**
7 | * 顶部导航
8 | * @param {*} param0
9 | * @returns
10 | */
11 | const TopNav = props => {
12 | const [isOpen, changeShow] = useState(false)
13 |
14 | const toggleMenuOpen = () => {
15 | changeShow(!isOpen)
16 | }
17 |
18 | return (
19 |
20 | {/* 导航栏 */}
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | {isOpen ? : }
32 |
33 |
34 |
35 |
36 |
37 |
{ changeShow(false) }}>
38 |
39 |
42 |
43 |
44 |
45 |
)
46 | }
47 |
48 | export default TopNav
49 |
--------------------------------------------------------------------------------
/lib/cache/local_file_cache.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 |
3 | const path = require('path')
4 | // 文件缓存持续10秒
5 | const cacheInvalidSeconds = 1000000000 * 1000
6 | // 文件名
7 | const jsonFile = path.resolve('./data.json')
8 |
9 | export async function getCache (key) {
10 | const exist = await fs.existsSync(jsonFile)
11 | if (!exist) return null
12 | const data = await fs.readFileSync(jsonFile)
13 | let json = null
14 | if (!data) return null
15 | try {
16 | json = JSON.parse(data)
17 | } catch (error) {
18 | console.error('读取JSON缓存文件失败', data)
19 | return null
20 | }
21 | // 缓存超过有效期就作废
22 | const cacheValidTime = new Date(parseInt(json[key + '_expire_time']) + cacheInvalidSeconds)
23 | const currentTime = new Date()
24 | if (!cacheValidTime || cacheValidTime < currentTime) {
25 | return null
26 | }
27 | return json[key]
28 | }
29 |
30 | /**
31 | * 并发请求写文件异常; Vercel生产环境不支持写文件。
32 | * @param key
33 | * @param data
34 | * @returns {Promise}
35 | */
36 | export async function setCache (key, data) {
37 | const exist = await fs.existsSync(jsonFile)
38 | const json = exist ? JSON.parse(await fs.readFileSync(jsonFile)) : {}
39 | json[key] = data
40 | json[key + '_expire_time'] = new Date().getTime()
41 | fs.writeFileSync(jsonFile, JSON.stringify(json))
42 | }
43 |
44 | export async function delCache (key, data) {
45 | const exist = await fs.existsSync(jsonFile)
46 | const json = exist ? JSON.parse(await fs.readFileSync(jsonFile)) : {}
47 | delete json.key
48 | json[key + '_expire_time'] = new Date().getTime()
49 | fs.writeFileSync(jsonFile, JSON.stringify(json))
50 | }
51 |
52 | export default { getCache, setCache, delCache }
53 |
--------------------------------------------------------------------------------
/lib/lang/fr-FR.js:
--------------------------------------------------------------------------------
1 | export default {
2 | LOCALE: 'fr-FR',
3 | NAV: {
4 | INDEX: 'Accueil',
5 | RSS: 'RSS',
6 | SEARCH: 'Recherche',
7 | ABOUT: 'À propos',
8 | NAVIGATOR: 'Navigation',
9 | MAIL: 'E-mail',
10 | ARCHIVE: 'Archive'
11 | },
12 | COMMON: {
13 | MORE: 'Plus',
14 | NO_MORE: 'Aucune donnée',
15 | LATEST_POSTS: 'Nouveaux articles',
16 | TAGS: 'Tags',
17 | NO_TAG: 'Non tag',
18 | CATEGORY: 'Catégorie(s)',
19 | SHARE: 'Partager',
20 | SCAN_QR_CODE: 'Scan QRCode',
21 | URL_COPIED: "L'URL est copé!",
22 | TABLE_OF_CONTENTS: 'Sommaire',
23 | RELATE_POSTS: 'Article similaire',
24 | COPYRIGHT: 'Droit d\'auteur',
25 | AUTHOR: 'Auteur',
26 | URL: 'Link',
27 | ANALYTICS: 'Analytique',
28 | POSTS: 'Articles',
29 | ARTICLE: 'Article(s)',
30 | VISITORS: 'Visiteurs',
31 | VIEWS: 'Views',
32 | COPYRIGHT_NOTICE: 'Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International (CC BY-NC-SA 4.0)',
33 | RESULT_OF_SEARCH: 'Résultats',
34 | ARTICLE_DETAIL: 'Plus de détails',
35 | PASSWORD_ERROR: 'Mot de passe est incorrect!',
36 | ARTICLE_LOCK_TIPS: 'Saisir le mot de passe pour accéder au contenu',
37 | SUBMIT: 'Valider',
38 | POST_TIME: 'Date de publication',
39 | LAST_EDITED_TIME: 'Date de modification',
40 | RECENT_COMMENTS: 'Nouveau commentaire'
41 | },
42 | PAGINATION: {
43 | PREV: 'PREV',
44 | NEXT: 'NEXT'
45 | },
46 | SEARCH: {
47 | ARTICLES: 'Recherche les articles',
48 | TAGS: 'Recherche les tags'
49 | },
50 | POST: {
51 | BACK: 'Page precedente',
52 | TOP: 'Haut'
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const withBundleAnalyzer = require('@next/bundle-analyzer')({
2 | enabled: process.env.ANALYZE === 'true'
3 | })
4 |
5 | module.exports = withBundleAnalyzer({
6 | images: {
7 | // 图片压缩
8 | dangerouslyAllowSVG: true,
9 | formats: ['image/avif', 'image/webp'],
10 | // 允许next/image加载的图片 域名
11 | remotePatterns: [
12 | {
13 | protocol: 'https',
14 | hostname: '**'
15 | }
16 | ]
17 | },
18 |
19 | async rewrites() {
20 | return [
21 | {
22 | source: '/:path*.html',
23 | destination: '/:path*'
24 | }
25 | ]
26 | },
27 | async headers() {
28 | return [
29 | {
30 | source: '/:path*{/}?',
31 | headers: [
32 | { key: 'Access-Control-Allow-Credentials', value: 'true' },
33 | { key: 'Access-Control-Allow-Origin', value: '*' },
34 | {
35 | key: 'Access-Control-Allow-Methods',
36 | value: 'GET,OPTIONS,PATCH,DELETE,POST,PUT'
37 | },
38 | {
39 | key: 'Access-Control-Allow-Headers',
40 | value:
41 | 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version'
42 | }
43 | ]
44 | }
45 | ]
46 | },
47 | webpack: (config, { dev, isServer }) => {
48 | // Replace React with Preact only in client production build
49 | // if (!dev && !isServer) {
50 | // Object.assign(config.resolve.alias, {
51 | // react: 'preact/compat',
52 | // 'react-dom/test-utils': 'preact/test-utils',
53 | // 'react-dom': 'preact/compat'
54 | // })
55 | // }
56 | return config
57 | }
58 | })
59 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import CONFIG from '@/config'
2 | import React, { useEffect } from 'react'
3 | import dynamic from 'next/dynamic'
4 |
5 | import 'animate.css'
6 | import '@/styles/globals.css'
7 | import '@/styles/nprogress.css'
8 |
9 | // core styles shared by all of react-notion-x (required)
10 | import 'react-notion-x/src/styles.css'
11 | import '@/styles/notion.css' // 重写部分样式
12 |
13 | import { GlobalContextProvider } from '@/lib/global'
14 | import { DebugPanel } from '@/components/DebugPanel'
15 | import { ThemeSwitch } from '@/components/ThemeSwitch'
16 | import ExternalScript from '@/components/ExternalScript'
17 | import smoothscroll from 'smoothscroll-polyfill'
18 |
19 | import AOS from 'aos'
20 | import 'aos/dist/aos.css' // You can also use for styles
21 | import { isMobile } from '@/lib/utils'
22 |
23 | const Gtag = dynamic(() => import('@/components/Gtag'), { ssr: false })
24 | const Busuanzi = dynamic(() => import('@/components/Busuanzi'), { ssr: false })
25 | const GoogleAdsense = dynamic(() => import('@/components/GoogleAdsense'), {
26 | ssr: false
27 | })
28 |
29 | const MyApp = ({ Component, pageProps }) => {
30 | // 外部插件
31 | const externalPlugins = <>
32 | {JSON.parse(CONFIG.THEME_SWITCH) && }
33 | {JSON.parse(CONFIG.DEBUG) && }
34 | {CONFIG.ANALYTICS_GOOGLE_ID && }
35 | {JSON.parse(CONFIG.ANALYTICS_BUSUANZI_ENABLE) && }
36 | {CONFIG.ADSENSE_GOOGLE_ID && }
37 |
38 | >
39 |
40 | useEffect(() => {
41 | AOS.init()
42 | if (isMobile()) {
43 | smoothscroll.polyfill()
44 | }
45 | }, [])
46 |
47 | return (
48 |
49 |
50 | {externalPlugins}
51 |
52 | )
53 | }
54 |
55 | export default MyApp
56 |
--------------------------------------------------------------------------------
/styles/nprogress.css:
--------------------------------------------------------------------------------
1 | /* Make clicks pass-through */
2 | #nprogress {
3 | pointer-events: none;
4 | }
5 |
6 | #nprogress .bar {
7 | background: #29d;
8 |
9 | position: fixed;
10 | z-index: 1031;
11 | top: 0;
12 | left: 0;
13 |
14 | width: 100%;
15 | height: 2px;
16 | }
17 |
18 | /* Fancy blur effect */
19 | #nprogress .peg {
20 | display: block;
21 | position: absolute;
22 | right: 0px;
23 | width: 100px;
24 | height: 100%;
25 | box-shadow: 0 0 10px #29d, 0 0 5px #29d;
26 | opacity: 1;
27 |
28 | -webkit-transform: rotate(3deg) translate(0px, -4px);
29 | -ms-transform: rotate(3deg) translate(0px, -4px);
30 | transform: rotate(3deg) translate(0px, -4px);
31 | }
32 |
33 | /* Remove these to get rid of the spinner */
34 | #nprogress .spinner {
35 | display: block;
36 | position: fixed;
37 | z-index: 1031;
38 | top: 15px;
39 | right: 15px;
40 | }
41 |
42 | #nprogress .spinner-icon {
43 | width: 18px;
44 | height: 18px;
45 | box-sizing: border-box;
46 |
47 | border: solid 2px transparent;
48 | border-top-color: #29d;
49 | border-left-color: #29d;
50 | border-radius: 50%;
51 |
52 | -webkit-animation: nprogress-spinner 400ms linear infinite;
53 | animation: nprogress-spinner 400ms linear infinite;
54 | }
55 |
56 | .nprogress-custom-parent {
57 | overflow: hidden;
58 | position: relative;
59 | }
60 |
61 | .nprogress-custom-parent #nprogress .spinner,
62 | .nprogress-custom-parent #nprogress .bar {
63 | position: absolute;
64 | }
65 |
66 | @-webkit-keyframes nprogress-spinner {
67 | 0% {
68 | -webkit-transform: rotate(0deg);
69 | }
70 |
71 | 100% {
72 | -webkit-transform: rotate(360deg);
73 | }
74 | }
75 |
76 | @keyframes nprogress-spinner {
77 | 0% {
78 | transform: rotate(0deg);
79 | }
80 |
81 | 100% {
82 | transform: rotate(360deg);
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/lib/datasource/notion/getPageData.js:
--------------------------------------------------------------------------------
1 | import CONFIG from '@/config'
2 | import { NotionAPI } from 'notion-client'
3 |
4 | export async function getPageData(pageId, slice) {
5 | const authToken = CONFIG.NOTION_ACCESS_TOKEN || null
6 | const api = new NotionAPI({
7 | authToken,
8 | userTimeZone: 'Asia/ShangHai'
9 | })
10 | const pageData = await api.getPage(pageId)
11 | console.info('[响应成功]:')
12 |
13 | pageData.block = formatBlocks(pageData.block)
14 | pageData.collection = formatCollections(pageData.collection)
15 | pageData.collection_view = formatCollectionViews(pageData.collection_view)
16 |
17 | return pageData
18 | }
19 |
20 | function formatBlocks(blocks) {
21 | const m = {}
22 | for (const i in blocks) {
23 | const block = blocks[i].value
24 |
25 | delete block.version
26 | delete block.created_by_table
27 | delete block.created_by_id
28 | delete block.last_edited_by_table
29 | delete block.last_edited_by_id
30 | delete block.space_id
31 |
32 | m[block.id] = block
33 | }
34 |
35 | return m
36 | }
37 |
38 | function formatCollections(collections) {
39 | const m = {}
40 | for (const i in collections) {
41 | const c = collections[i].value
42 |
43 | delete c.version
44 | delete c.created_by_table
45 | delete c.created_by_id
46 | delete c.last_edited_by_table
47 | delete c.last_edited_by_id
48 | delete c.space_id
49 |
50 | m[c.id] = c
51 | }
52 |
53 | return m
54 | }
55 |
56 | function formatCollectionViews(views) {
57 | const m = {}
58 | for (const i in views) {
59 | const c = views[i].value
60 |
61 | delete c.version
62 | delete c.created_by_table
63 | delete c.created_by_id
64 | delete c.last_edited_by_table
65 | delete c.last_edited_by_id
66 | delete c.space_id
67 |
68 | m[c.id] = c
69 | }
70 |
71 | return m
72 | }
73 |
--------------------------------------------------------------------------------
/components/Tabs.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 |
3 | /**
4 | * Tabs切换标签
5 | * @param {*} param0
6 | * @returns
7 | */
8 | const Tabs = ({ className, children }) => {
9 | const [currentTab, setCurrentTab] = useState(0)
10 |
11 | if (!children) {
12 | return <>>
13 | }
14 |
15 | children = children.filter(c => c !== '')
16 |
17 | let count = 0
18 | children.forEach(e => {
19 | if (e) {
20 | count++
21 | }
22 | })
23 |
24 | if (count === 0) {
25 | return <>>
26 | }
27 |
28 | if (count === 1) {
29 | return
32 | }
33 |
34 | function tabClickHandle(i) {
35 | setCurrentTab(i)
36 | }
37 |
38 | return
39 |
40 | {children.map((item, index) => {
41 | return {
44 | tabClickHandle(index)
45 | }}>
46 | {item?.key}
47 |
48 | })}
49 |
50 |
51 | {children.map((item, index) => {
52 | return
57 | {currentTab === index && item}
58 |
59 | })}
60 |
61 |
62 | }
63 |
64 | export default Tabs
65 |
--------------------------------------------------------------------------------
/lib/notion/getAllLinks.js:
--------------------------------------------------------------------------------
1 | import CONFIG from '@/config'
2 | import getAllPageIds from './getAllPageIds'
3 | import getPageProperties from './getPageProperties'
4 | import { getNotionPageData } from '@/lib/notion/getNotionData'
5 | import { delCacheData } from '@/lib/cache/cache_manager'
6 |
7 | /**
8 | * 获取所有链接列表
9 | * @param notionPageData
10 | * @param from
11 | * @param pageType 页面类型数组 ['Post','Page']
12 | * @returns {Promise<*[]>}
13 | */
14 | export async function getAllLinks({ notionPageData, from, pageType }) {
15 | if (!notionPageData) {
16 | notionPageData = await getNotionPageData({ from })
17 | }
18 | if (!notionPageData) {
19 | return []
20 | }
21 |
22 | const { block, schema, tagOptions, collectionQuery, collectionId, collectionView, viewIds } = notionPageData
23 | const data = []
24 | const pageIds = getAllPageIds(collectionQuery, collectionId, collectionView, viewIds)
25 | for (let i = 0; i < pageIds.length; i++) {
26 | const id = pageIds[i]
27 | const value = block[id]?.value
28 | if (!value) {
29 | continue
30 | }
31 | const properties = (await getPageProperties(id, block, schema, null, tagOptions, notionPageData.siteInfo)) || null
32 | data.push(properties)
33 | }
34 |
35 | // remove all the the items doesn't meet requirements
36 | const posts = data.filter(post => {
37 | return post.title && post?.status?.[0] === 'Published' && pageType.indexOf(post?.type?.[0]) > -1
38 | })
39 |
40 | if (!posts || posts.length === 0) {
41 | const cacheKey = 'page_block_' + CONFIG.WEBSITE_NOTION_PAGE_ID
42 | await delCacheData(cacheKey)
43 | }
44 |
45 | // Sort by date
46 | if (CONFIG.POSTS_SORT_BY === 'date') {
47 | posts.sort((a, b) => {
48 | const dateA = new Date(a?.date?.start_date || a.createdTime)
49 | const dateB = new Date(b?.date?.start_date || b.createdTime)
50 | return dateB - dateA
51 | })
52 | }
53 | return posts
54 | }
55 |
--------------------------------------------------------------------------------
/.github/workflows/docker-ghcr.yaml:
--------------------------------------------------------------------------------
1 | name: Docker ghcr.io
2 |
3 | # This workflow uses actions that are not certified by GitHub.
4 | # They are provided by a third-party and are governed by
5 | # separate terms of service, privacy policy, and support
6 | # documentation.
7 |
8 | on:
9 | push:
10 | branches: [main]
11 | # Publish semver tags as releases.
12 | tags: ["v*.*.*"]
13 | pull_request:
14 | branches: [main]
15 |
16 | env:
17 | # Use docker.io for Docker Hub if empty
18 | REGISTRY: ghcr.io
19 | # github.repository as /
20 | IMAGE_NAME: ${{ github.repository }}
21 |
22 | jobs:
23 | build:
24 | runs-on: ubuntu-latest
25 | permissions:
26 | contents: read
27 | packages: write
28 |
29 | steps:
30 | - name: Checkout repository
31 | uses: actions/checkout@v2
32 |
33 | # Login against a Docker registry except on PR
34 | # https://github.com/docker/login-action
35 | - name: Log into registry ${{ env.REGISTRY }}
36 | if: github.event_name != 'pull_request'
37 | uses: docker/login-action@v1
38 | with:
39 | registry: ${{ env.REGISTRY }}
40 | username: ${{ github.actor }}
41 | password: ${{ secrets.GITHUB_TOKEN }}
42 |
43 | # Extract metadata (tags, labels) for Docker
44 | # https://github.com/docker/metadata-action
45 | - name: Extract Docker metadata
46 | id: meta
47 | uses: docker/metadata-action@v3
48 | with:
49 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
50 |
51 | # Build and push Docker image with Buildx (don't push on PR)
52 | # https://github.com/docker/build-push-action
53 | - name: Build and push Docker image
54 | uses: docker/build-push-action@v2
55 | with:
56 | context: .
57 | push: ${{ github.event_name != 'pull_request' }}
58 | tags: ${{ steps.meta.outputs.tags }}
59 | labels: ${{ steps.meta.outputs.labels }}
60 |
--------------------------------------------------------------------------------
/lib/cache/mongo_db_cache.js:
--------------------------------------------------------------------------------
1 | const MongoClient = require('mongodb').MongoClient
2 |
3 | const DB_URL = process.env.MONGO_DB_URL // e.g. mongodb+srv://mongo_user:[password]@xxx.mongodb.net//?retryWrites=true&w=majority
4 | const DB_NAME = process.env.MONGO_DB_NAME // e.g. tangly1024
5 | const DB_COLLECTION = 'posts'
6 |
7 | export async function getCache (key) {
8 | const client = await MongoClient.connect(DB_URL).catch(err => { console.error(err) })
9 | const dbo = client.db(DB_NAME)
10 | const query = { block_id: key }
11 | const res = await dbo.collection('posts').findOne(query).catch(err => { console.error(err) })
12 | await client.close()
13 | return res
14 | }
15 |
16 | /**
17 | * 并发请求写文件异常; Vercel生产环境不支持写文件。
18 | * @param key
19 | * @param data
20 | * @returns {Promise}
21 | */
22 | export async function setCache (key, data) {
23 | const client = await MongoClient.connect(DB_URL).catch(err => { console.error(err) })
24 | const dbo = client.db(DB_NAME)
25 | data.block_id = key
26 | const query = { block_id: key }
27 | const jsonObj = JSON.parse(JSON.stringify(data))
28 |
29 | const updRes = await dbo.collection(DB_COLLECTION).updateOne(query, { $set: jsonObj }).catch(err => { console.error(err) })
30 | console.log('更新结果', key, updRes)
31 | if (updRes.matchedCount === 0) {
32 | const insertRes = await dbo.collection(DB_COLLECTION).insertOne(jsonObj).catch(err => { console.error(err) })
33 | console.log('插入结果', key, insertRes)
34 | }
35 | await client.close()
36 | return data
37 | }
38 |
39 | export async function delCache (key, data) {
40 | const client = await MongoClient.connect(DB_URL).catch(err => { console.error(err) })
41 | const dbo = client.db(DB_NAME)
42 | const query = { block_id: key }
43 | const res = await dbo.collection('posts').deleteOne(query).catch(err => { console.error(err) })
44 | console.log('删除结果', key, res)
45 | await client.close()
46 | return null
47 | }
48 |
49 | export default { getCache, setCache, delCache }
50 |
--------------------------------------------------------------------------------
/themes/plain/LayoutBase.js:
--------------------------------------------------------------------------------
1 | import CommonHead from '@/components/CommonHead'
2 | import TopNav from './components/TopNav'
3 | import AsideLeft from './components/AsideLeft'
4 | import { useGlobal } from '@/lib/global'
5 | import SiteFooter from './components/SiteFooter'
6 |
7 | /**
8 | * 基础布局 采用左右两侧布局,移动端使用顶部导航栏
9 | * @returns {JSX.Element}
10 | * @constructor
11 | * @param props
12 | */
13 | const LayoutBase = (props) => {
14 | const {
15 | children,
16 | headerSlot,
17 | meta,
18 | siteInfo,
19 | configs
20 | } = props
21 | const { onLoading } = useGlobal()
22 |
23 | const LoadingCover =
29 |
30 | return (
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | {siteInfo?.pageCover &&
39 | {/* eslint-disable-next-line @next/next/no-img-element */}
40 |
44 |
}
45 |
46 |
47 | {headerSlot &&
{headerSlot}
}
48 |
{onLoading ? LoadingCover : children}
49 |
50 |
51 |
52 |
53 |
54 |
)
55 | }
56 |
57 | export default LayoutBase
58 |
--------------------------------------------------------------------------------
/themes/plain/components/LinkMenuItemDrop.js:
--------------------------------------------------------------------------------
1 | import * as Scroll from 'react-scroll'
2 | import { useState } from 'react'
3 |
4 | export const LinkMenuItemDrop = ({ link }) => {
5 | const [show, changeShow] = useState(false)
6 | link.subMenus = link.subMenus || link.children
7 | const hasSubMenu = link?.subMenus?.length > 0
8 |
9 | return
10 |
11 | changeShow(!show)} to={link?.id} spy={true} smooth={true} offset={-25} duration={500} className='w-full my-auto items-center rounded p-2 px-4 hover:bg-gray-100 justify-between flex select-none' >
12 |
13 | {link.slot}
14 | {hasSubMenu &&
15 |
16 |
17 |
18 | }
19 |
20 |
21 |
22 | {/* 子菜单 */}
23 | {hasSubMenu &&
24 |
25 | {link?.subMenus?.map(sLink => {
26 | return
27 |
28 | {sLink.icon && }
29 | {sLink.title || sLink.title}
30 | {sLink.slot}
31 |
32 |
33 | })}
34 |
35 | }
36 |
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/lib/lang.js:
--------------------------------------------------------------------------------
1 | import zhCN from './lang/zh-CN'
2 | import enUS from './lang/en-US'
3 | import zhHK from './lang/zh-HK'
4 | import zhTW from './lang/zh-TW'
5 | import frFR from '@/lib/lang/fr-FR'
6 | import cookie from 'react-cookies'
7 | import { getQueryVariable, isBrowser, mergeDeep } from './utils'
8 |
9 | const lang = {
10 | 'en-US': enUS,
11 | 'zh-CN': zhCN,
12 | 'zh-HK': zhHK,
13 | 'zh-TW': zhTW,
14 | 'fr-FR': frFR
15 | }
16 |
17 | export default lang
18 |
19 | /**
20 | * 获取当前语言字典
21 | * @returns 不同语言对应字典
22 | */
23 | export function generateLocaleDict(langString) {
24 | let userLocale = lang['en-US']
25 |
26 | switch (langString.toLowerCase()) {
27 | case 'zh-cn':
28 | case 'zh-sg':
29 | userLocale = lang['zh-CN']
30 | break
31 | case 'zh-hk':
32 | userLocale = lang['zh-HK']
33 | break
34 | case 'zh-tw':
35 | userLocale = lang['zh-TW']
36 | break
37 | case 'fr-fr':
38 | userLocale = lang['fr-FR']
39 | break
40 | default:
41 | userLocale = lang['en-US']
42 | }
43 | return mergeDeep({}, lang['en-US'], userLocale)
44 | }
45 |
46 | /**
47 | * 初始化站点翻译
48 | * 根据用户当前浏览器语言进行切换
49 | */
50 | export function initLocale(lang, locale, changeLang, changeLocale) {
51 | if (isBrowser()) {
52 | const queryLang = getQueryVariable('lang') || loadLangFromCookies() || window.navigator.language
53 | let currentLang = lang
54 | if (queryLang !== lang) {
55 | currentLang = queryLang
56 | }
57 | changeLang(currentLang)
58 | saveLangToCookies(currentLang)
59 |
60 | const targetLocale = generateLocaleDict(currentLang)
61 | if (JSON.stringify(locale) !== JSON.stringify(currentLang)) {
62 | changeLocale(targetLocale)
63 | }
64 | }
65 | }
66 | /**
67 | * 读取语言
68 | * @returns {*}
69 | */
70 | export const loadLangFromCookies = () => {
71 | return cookie.load('lang')
72 | }
73 |
74 | /**
75 | * 保存语言
76 | * @param newTheme
77 | */
78 | export const saveLangToCookies = (lang) => {
79 | cookie.save('lang', lang, { path: '/' })
80 | }
81 |
--------------------------------------------------------------------------------
/components/SideBarDrawer.js:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router'
2 | import { useEffect } from 'react'
3 |
4 | /**
5 | * 侧边栏抽屉面板,可以从侧面拉出
6 | * @returns {JSX.Element}
7 | * @constructor
8 | */
9 | const SideBarDrawer = ({ children, isOpen, onOpen, onClose, onClosed, className }) => {
10 | const router = useRouter()
11 | useEffect(() => {
12 | const sideBarDrawerRouteListener = () => {
13 | switchSideDrawerVisible(false)
14 | }
15 | router.events.on('routeChangeComplete', sideBarDrawerRouteListener)
16 | return () => {
17 | router.events.off('routeChangeComplete', sideBarDrawerRouteListener)
18 | }
19 | }, [router.events])
20 |
21 | // 点击按钮更改侧边抽屉状态
22 | const switchSideDrawerVisible = (showStatus) => {
23 | if (showStatus) {
24 | onOpen && onOpen()
25 | } else {
26 | onClose && onClose()
27 | onClosed && setTimeout(() => {
28 | onClosed()
29 | }, 300)
30 | }
31 | const sideBarDrawer = window.document.getElementById('sidebar-drawer')
32 | const sideBarDrawerBackground = window.document.getElementById('sidebar-drawer-background')
33 |
34 | if (showStatus) {
35 | sideBarDrawer.classList.replace('-ml-60', 'ml-0')
36 | sideBarDrawerBackground.classList.replace('hidden', 'block')
37 | } else {
38 | sideBarDrawer.classList.replace('ml-0', '-ml-60')
39 | sideBarDrawerBackground.classList.replace('block', 'hidden')
40 | }
41 | }
42 |
43 | return