Merge branch 'main' into original-fix

This commit is contained in:
tangly1024
2025-01-01 15:49:15 +08:00
committed by GitHub
355 changed files with 16915 additions and 6775 deletions

View File

@@ -8,22 +8,24 @@ import MemoryCache from './memory_cache'
* @returns
*/
export async function getDataFromCache(key, force) {
if (JSON.parse(BLOG.ENABLE_CACHE) || force) {
if (BLOG.ENABLE_CACHE || force) {
const dataFromCache = await getApi().getCache(key)
if (JSON.stringify(dataFromCache) === '[]') {
if (!dataFromCache || JSON.stringify(dataFromCache) === '[]') {
return null
}
return getApi().getCache(key)
// console.trace('[API-->>缓存]:', key, dataFromCache)
return dataFromCache
} else {
return null
}
}
export async function setDataToCache(key, data) {
export async function setDataToCache(key, data, customCacheTime) {
if (!data) {
return
}
await getApi().setCache(key, data)
// console.trace('[API-->>缓存写入]:', key)
await getApi().setCache(key, data, customCacheTime)
}
export async function delCacheData(key) {

View File

@@ -7,8 +7,8 @@ export async function getCache(key, options) {
return await cache.get(key)
}
export async function setCache(key, data) {
await cache.put(key, data, cacheTime * 1000)
export async function setCache(key, data, customCacheTime) {
await cache.put(key, data, (customCacheTime || cacheTime) * 1000)
}
export async function delCache(key) {

View File

@@ -18,6 +18,8 @@ export const siteConfig = (key, defaultVal = null, extendConfig = {}) => {
if (!key) {
return null
}
const getValue = (value, fallback) => (hasVal(value) ? value : fallback)
const hasVal = value => value !== undefined && value !== null
// 特殊配置处理以下配置只在服务端生效而Global的NOTION_CONFIG仅限前端组件使用因此需要从extendConfig中读取
switch (key) {
@@ -34,7 +36,21 @@ export const siteConfig = (key, defaultVal = null, extendConfig = {}) => {
case 'POST_URL_PREFIX_MAPPING_CATEGORY':
case 'IS_TAG_COLOR_DISTINGUISHED':
case 'TAG_SORT_BY_COUNT':
return convertVal(extendConfig[key] || defaultVal || BLOG[key])
case 'THEME':
case 'LINK':
case 'AI_SUMMARY_API':
case 'AI_SUMMARY_KEY':
case 'AI_SUMMARY_CACHE_TIME':
case 'AI_SUMMARY_WORD_LIMIT':
// LINK比较特殊
if (key === 'LINK') {
if (!extendConfig || Object.keys(extendConfig).length === 0) {
break
}
}
return convertVal(
getValue(extendConfig[key], getValue(defaultVal, BLOG[key]))
)
default:
}
@@ -50,7 +66,7 @@ export const siteConfig = (key, defaultVal = null, extendConfig = {}) => {
// console.warn('SiteConfig警告', key, error)
}
// 首先 配置最优先读取NOTION中的表格配置
// 配置最优先读取NOTION中的表格配置
let val = null
let siteInfo = null
@@ -78,22 +94,27 @@ export const siteConfig = (key, defaultVal = null, extendConfig = {}) => {
}
// 其次 有传入的extendConfig则尝试读取
if (!val && extendConfig) {
if (!hasVal(val) && extendConfig) {
val = extendConfig[key]
}
// 其次 NOTION没有找到配置则会读取blog.config.js文件
if (!val) {
if (!hasVal(val)) {
val = BLOG[key]
}
if (!val) {
if (!hasVal(val)) {
return defaultVal
}
return convertVal(val)
}
export const cleanJsonString = val => {
// 使用正则表达式去掉不必要的空格、换行符和制表符
return val.replace(/\s+/g, ' ').trim()
}
/**
* 从环境变量和NotionConfig读取的配置都是string类型
* 这里识别出配置的字符值若为否 数字、布尔、[]数组,{}对象,若是则转成对应类型
@@ -102,50 +123,50 @@ export const siteConfig = (key, defaultVal = null, extendConfig = {}) => {
* @returns
*/
export const convertVal = val => {
// 如果传入参数本身就是obj、数组、boolean 就无需处理
// 如果传入参数本身就是 obj、数组、boolean就无需处理
if (typeof val !== 'string' || !val) {
return val
}
// 解析数字parseInt将字符串转换为数字
// 检测是否数字并避免数值溢出
if (/^\d+$/.test(val)) {
return parseInt(val)
const parsedNum = Number(val)
// 如果数值大于 JavaScript 最大安全整数,则作为字符串返回
if (parsedNum > Number.MAX_SAFE_INTEGER) {
return val + ''
}
return parsedNum
}
// 检测是否url
if (isUrl(val)) {
return val
}
// 检测是否url
// 检测是否为布尔值
if (val === 'true' || val === 'false') {
return JSON.parse(val)
}
// 配置值前可能有污染的空格
if (val.indexOf('[') < 0 && val.indexOf('{') < 0) {
// 检测是否为 URL
if (isUrl(val)) {
return val
}
// 转换 [] , {} , true/false 这类字符串为对象
// 配置值前可能有污染的空格
// 如果字符串中没有 '[' 或 '{',则直接返回
if (!val.trim().startsWith('{') && !val.trim().startsWith('[')) {
return val
}
// 转换 [] , {} 这类字符串为对象
try {
// 尝试解析json
val = cleanJsonString(val)
const parsedJson = JSON.parse(val)
// 检查解析后的结果是否为对象
if (parsedJson !== null) {
return parsedJson
}
} catch (error) {
// try {
// // 尝试解析对象,对象解析能力不如上一步的json
// const evalObj = eval('(' + val + ')')
// if (evalObj !== null) {
// return evalObj
// }
// } catch (error) {
// // Ojbject 解析失败,返回原始字符串值
// return val
// }
// 解析失败,返回原始字符串
return val
}
return val
}

View File

@@ -1,11 +1,10 @@
import BLOG from '@/blog.config'
import { getDataFromCache, setDataToCache } from '@/lib/cache/cache_manager'
import { getAllCategories } from '@/lib/notion/getAllCategories'
import getAllPageIds from '@/lib/notion/getAllPageIds'
import { getAllTags } from '@/lib/notion/getAllTags'
import { getConfigMapFromConfigPage } from '@/lib/notion/getNotionConfig'
import getPageProperties, {
adjustPageProperties
adjustPageProperties
} from '@/lib/notion/getPageProperties'
import { fetchInBatches, getPage } from '@/lib/notion/getPostBlocks'
import { compressImage, mapImgUrl } from '@/lib/notion/mapImage'
@@ -35,6 +34,10 @@ export async function getGlobalData({
const siteIds = pageId?.split(',') || []
let data = EmptyData(pageId)
if (BLOG.BUNDLE_ANALYZER) {
return data
}
try {
for (let index = 0; index < siteIds.length; index++) {
const siteId = siteIds[index]
@@ -42,7 +45,7 @@ export async function getGlobalData({
const prefix = extractLangPrefix(siteId)
// 第一个id站点默认语言
if (index === 0 || locale === prefix) {
data = await getNotionPageData({
data = await getSiteDataByPageId({
pageId: id,
from
})
@@ -51,7 +54,7 @@ export async function getGlobalData({
} catch (error) {
console.error('异常', error)
}
return data
return handleDataBeforeReturn(deepClone(data))
}
/**
@@ -60,24 +63,245 @@ export async function getGlobalData({
* @param from 请求来源
* @returns {Promise<JSX.Element|*|*[]>}
*/
export async function getNotionPageData({ pageId, from }) {
// 尝试从缓存获取
const cacheKey = 'page_block_' + pageId
let data = await getDataFromCache(cacheKey)
if (data && data.pageIds?.length > 0) {
// console.log('[API<<--缓存]', `from:${from}`, `root-page-id:${pageId}`)
// return data
export async function getSiteDataByPageId({ pageId, from }) {
// 获取NOTION原始数据此接支持mem缓存。
const pageRecordMap = await getPage(pageId, from)
// 将Notion数据按规则转成站点数据
const data = await converNotionToSiteDate(
pageId,
from,
deepClone(pageRecordMap)
)
return data
}
/**
* 获取公告
*/
async function getNotice(post) {
if (!post) {
return null
}
post.blockMap = await getPage(post.id, 'data-notice')
return post
}
/**
* 空的默认数据
* @param {*} pageId
* @returns
*/
const EmptyData = pageId => {
const empty = {
notice: null,
siteInfo: getSiteInfo({}),
allPages: [
{
id: 1,
title: `无法获取Notion数据请检查Notion_ID \n 当前 ${pageId}`,
summary:
'访问文档获取帮助 → https://docs.tangly1024.com/article/vercel-deploy-notion-next',
status: 'Published',
type: 'Post',
slug: 'oops',
publishDay: '2024-11-13',
pageCoverThumbnail: BLOG.HOME_BANNER_IMAGE,
date: {
start_date: '2023-04-24',
lastEditedDay: '2023-04-24',
tagItems: []
}
}
],
allNavPages: [],
collection: [],
collectionQuery: {},
collectionId: null,
collectionView: {},
viewIds: [],
block: {},
schema: {},
tagOptions: [],
categoryOptions: [],
rawMetadata: {},
customNav: [],
customMenu: [],
postCount: 1,
pageIds: [],
latestPosts: []
}
return empty
}
/**
* 将Notion数据转站点数据
* 这里统一对数据格式化
* @returns {Promise<JSX.Element|null|*>}
*/
async function converNotionToSiteDate(pageId, from, pageRecordMap) {
if (!pageRecordMap) {
console.error('can`t get Notion Data ; Which id is: ', pageId)
return {}
}
pageId = idToUuid(pageId)
let block = pageRecordMap.block || {}
const rawMetadata = block[pageId]?.value
// Check Type Page-Database和Inline-Database
if (
rawMetadata?.type !== 'collection_view_page' &&
rawMetadata?.type !== 'collection_view'
) {
console.error(`pageId "${pageId}" is not a database`)
return EmptyData(pageId)
}
const collection = Object.values(pageRecordMap.collection)[0]?.value || {}
const collectionId = rawMetadata?.collection_id
const collectionQuery = pageRecordMap.collection_query
const collectionView = pageRecordMap.collection_view
const schema = collection?.schema
const viewIds = rawMetadata?.view_ids
const collectionData = []
const pageIds = getAllPageIds(
collectionQuery,
collectionId,
collectionView,
viewIds
)
if (pageIds?.length === 0) {
console.error(
'获取到的文章列表为空请检查notion模板',
collectionQuery,
collection,
collectionView,
viewIds,
pageRecordMap
)
} else {
// 从接口读取
data = await getDataBaseInfoByNotionAPI({ pageId, from })
// 存入缓存
if (data) {
await setDataToCache(cacheKey, data)
// console.log('有效Page数量', pageIds?.length)
}
// 抓取主数据库最多抓取1000个blocks溢出的数block这里统一抓取一遍
const blockIdsNeedFetch = []
for (let i = 0; i < pageIds.length; i++) {
const id = pageIds[i]
const value = block[id]?.value
if (!value) {
blockIdsNeedFetch.push(id)
}
}
const fetchedBlocks = await fetchInBatches(blockIdsNeedFetch)
block = Object.assign({}, block, fetchedBlocks)
// 获取每篇文章基础数据
for (let i = 0; i < pageIds.length; i++) {
const id = pageIds[i]
const value = block[id]?.value || fetchedBlocks[id]?.value
const properties =
(await getPageProperties(
id,
value,
schema,
null,
getTagOptions(schema)
)) || null
if (properties) {
collectionData.push(properties)
}
}
// 返回给前端的数据做处理
return handleDataBeforeReturn(deepClone(data))
// 站点配置优先读取配置表格否则读取blog.config.js 文件
const NOTION_CONFIG = (await getConfigMapFromConfigPage(collectionData)) || {}
// 处理每一条数据的字段
collectionData.forEach(function (element) {
adjustPageProperties(element, NOTION_CONFIG)
})
// 站点基础信息
const siteInfo = getSiteInfo({ collection, block, NOTION_CONFIG })
// 文章计数
let postCount = 0
// 查找所有的Post和Page
const allPages = collectionData.filter(post => {
if (post?.type === 'Post' && post.status === 'Published') {
postCount++
}
return (
post &&
post?.slug &&
// !post?.slug?.startsWith('http') &&
(post?.status === 'Invisible' || post?.status === 'Published')
)
})
// Sort by date
if (siteConfig('POSTS_SORT_BY', '', NOTION_CONFIG) === 'date') {
allPages.sort((a, b) => {
return b?.publishDate - a?.publishDate
})
}
const notice = await getNotice(
collectionData.filter(post => {
return (
post &&
post?.type &&
post?.type === 'Notice' &&
post.status === 'Published'
)
})?.[0]
)
// 所有分类
const categoryOptions = getAllCategories({
allPages,
categoryOptions: getCategoryOptions(schema)
})
// 所有标签
const tagOptions = getAllTags({
allPages,
tagOptions: getTagOptions(schema),
NOTION_CONFIG
})
// 旧的菜单
const customNav = getCustomNav({
allPages: collectionData.filter(
post => post?.type === 'Page' && post.status === 'Published'
)
})
// 新的菜单
const customMenu = await getCustomMenu({ collectionData, NOTION_CONFIG })
const latestPosts = getLatestPosts({ allPages, from, latestPostCount: 6 })
const allNavPages = getNavPages({ allPages })
return {
NOTION_CONFIG,
notice,
siteInfo,
allPages,
allNavPages,
collection,
collectionQuery,
collectionId,
collectionView,
viewIds,
block,
schema,
tagOptions,
categoryOptions,
rawMetadata,
customNav,
customMenu,
postCount,
pageIds,
latestPosts
}
}
/**
@@ -113,9 +337,47 @@ function handleDataBeforeReturn(db) {
db.allNavPages = shortenIds(db?.allNavPages)
// db.allPages = cleanBlocks(db?.allPages)
db.allNavPages = cleanPages(db?.allNavPages, db.tagOptions)
db.allPages = cleanPages(db.allPages, db.tagOptions)
db.latestPosts = cleanPages(db.latestPosts, db.tagOptions)
return db
}
/**
* 处理文章列表中的异常数据
* @param {Array} allPages - 所有页面数据
* @param {Array} tagOptions - 标签选项
* @returns {Array} 处理后的 allPages
*/
function cleanPages(allPages, tagOptions) {
// 校验参数是否为数组
if (!Array.isArray(allPages) || !Array.isArray(tagOptions)) {
console.warn('Invalid input: allPages and tagOptions should be arrays.')
return allPages || [] // 返回空数组或原始值
}
// 提取 tagOptions 中所有合法的标签名
const validTags = new Set(
tagOptions
.map(tag => (typeof tag.name === 'string' ? tag.name : null))
.filter(Boolean) // 只保留合法的字符串
)
// 遍历所有的 pages
allPages.forEach(page => {
// 确保 tagItems 是数组
if (Array.isArray(page.tagItems)) {
// 对每个 page 的 tagItems 进行过滤
page.tagItems = page.tagItems.filter(
tagItem =>
validTags.has(tagItem?.name) && typeof tagItem.name === 'string' // 校验 tagItem.name 是否是字符串
)
}
})
return allPages
}
/**
* 清理一组数据的id
* @param {*} items
@@ -218,7 +480,7 @@ function getCustomNav({ allPages }) {
p.to = p.slug
customNav.push({
icon: p.icon || null,
name: p.title,
name: p.title || p.name || '',
href: p.href,
target: p.target,
show: true
@@ -237,16 +499,15 @@ function getCustomMenu({ collectionData, NOTION_CONFIG }) {
const menuPages = collectionData.filter(
post =>
post.status === 'Published' &&
(post?.type === BLOG.NOTION_PROPERTY_NAME.type_menu ||
post?.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu)
(post?.type === 'Menu' || post?.type === 'SubMenu')
)
const menus = []
if (menuPages && menuPages.length > 0) {
menuPages.forEach(e => {
e.show = true
if (e.type === BLOG.NOTION_PROPERTY_NAME.type_menu) {
if (e.type === 'Menu') {
menus.push(e)
} else if (e.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu) {
} else if (e.type === 'SubMenu') {
const parentMenu = menus[menus.length - 1]
if (parentMenu) {
if (parentMenu.subMenus) {
@@ -367,228 +628,3 @@ export function getNavPages({ allPages }) {
ext: item.ext || {}
}))
}
/**
* 获取公告
*/
async function getNotice(post) {
if (!post) {
return null
}
post.blockMap = await getPage(post.id, 'data-notice')
return post
}
// 没有数据时返回
const EmptyData = pageId => {
const empty = {
notice: null,
siteInfo: getSiteInfo({}),
allPages: [
{
id: 1,
title: `无法获取Notion数据请检查Notion_ID \n 当前 ${pageId}`,
summary:
'访问文档获取帮助→ https://tangly1024.com/article/vercel-deploy-notion-next',
status: 'Published',
type: 'Post',
slug: '13a171332816461db29d50e9f575b00d',
pageCoverThumbnail: BLOG.HOME_BANNER_IMAGE,
date: {
start_date: '2023-04-24',
lastEditedDay: '2023-04-24',
tagItems: []
}
}
],
allNavPages: [],
collection: [],
collectionQuery: {},
collectionId: null,
collectionView: {},
viewIds: [],
block: {},
schema: {},
tagOptions: [],
categoryOptions: [],
rawMetadata: {},
customNav: [],
customMenu: [],
postCount: 1,
pageIds: [],
latestPosts: []
}
return empty
}
/**
* 调用NotionAPI获取Page数据
* @returns {Promise<JSX.Element|null|*>}
*/
async function getDataBaseInfoByNotionAPI({ pageId, from }) {
console.log('[Fetching Data]', pageId, from)
const pageRecordMap = await getPage(pageId, from)
if (!pageRecordMap) {
console.error('can`t get Notion Data ; Which id is: ', pageId)
return {}
}
pageId = idToUuid(pageId)
let block = pageRecordMap.block || {}
const rawMetadata = block[pageId]?.value
// Check Type Page-Database和Inline-Database
if (
rawMetadata?.type !== 'collection_view_page' &&
rawMetadata?.type !== 'collection_view'
) {
console.error(`pageId "${pageId}" is not a database`)
return EmptyData(pageId)
}
const collection = Object.values(pageRecordMap.collection)[0]?.value || {}
const collectionId = rawMetadata?.collection_id
const collectionQuery = pageRecordMap.collection_query
const collectionView = pageRecordMap.collection_view
const schema = collection?.schema
const viewIds = rawMetadata?.view_ids
const collectionData = []
const pageIds = getAllPageIds(
collectionQuery,
collectionId,
collectionView,
viewIds
)
if (pageIds?.length === 0) {
console.error(
'获取到的文章列表为空请检查notion模板',
collectionQuery,
collection,
collectionView,
viewIds,
pageRecordMap
)
} else {
// console.log('有效Page数量', pageIds?.length)
}
// 抓取主数据库最多抓取1000个blocks溢出的数block这里统一抓取一遍
const blockIdsNeedFetch = []
for (let i = 0; i < pageIds.length; i++) {
const id = pageIds[i]
const value = block[id]?.value
if (!value) {
blockIdsNeedFetch.push(id)
}
}
const fetchedBlocks = await fetchInBatches(blockIdsNeedFetch)
block = Object.assign({}, block, fetchedBlocks)
// 获取每篇文章基础数据
for (let i = 0; i < pageIds.length; i++) {
const id = pageIds[i]
const value = block[id]?.value || fetchedBlocks[id]?.value
const properties =
(await getPageProperties(
id,
value,
schema,
null,
getTagOptions(schema)
)) || null
if (properties) {
collectionData.push(properties)
}
}
// 站点配置优先读取配置表格否则读取blog.config.js 文件
const NOTION_CONFIG = (await getConfigMapFromConfigPage(collectionData)) || {}
// 处理每一条数据的字段
collectionData.forEach(function (element) {
adjustPageProperties(element, NOTION_CONFIG)
})
// 站点基础信息
const siteInfo = getSiteInfo({ collection, block, pageId })
// 文章计数
let postCount = 0
// 查找所有的Post和Page
const allPages = collectionData.filter(post => {
if (post?.type === 'Post' && post.status === 'Published') {
postCount++
}
return (
post &&
post?.slug &&
// !post?.slug?.startsWith('http') &&
(post?.status === 'Invisible' || post?.status === 'Published')
)
})
// Sort by date
if (siteConfig('POSTS_SORT_BY', '', NOTION_CONFIG) === 'date') {
allPages.sort((a, b) => {
return b?.publishDate - a?.publishDate
})
}
const notice = await getNotice(
collectionData.filter(post => {
return (
post &&
post?.type &&
post?.type === 'Notice' &&
post.status === 'Published'
)
})?.[0]
)
// 所有分类
const categoryOptions = getAllCategories({
allPages,
categoryOptions: getCategoryOptions(schema)
})
// 所有标签
const tagOptions = getAllTags({
allPages,
tagOptions: getTagOptions(schema),
NOTION_CONFIG
})
// 旧的菜单
const customNav = getCustomNav({
allPages: collectionData.filter(
post => post?.type === 'Page' && post.status === 'Published'
)
})
// 新的菜单
const customMenu = await getCustomMenu({ collectionData, NOTION_CONFIG })
const latestPosts = getLatestPosts({ allPages, from, latestPostCount: 6 })
const allNavPages = getNavPages({ allPages })
return {
NOTION_CONFIG,
notice,
siteInfo,
allPages,
allNavPages,
collection,
collectionQuery,
collectionId,
collectionView,
viewIds,
block,
schema,
tagOptions,
categoryOptions,
rawMetadata,
customNav,
customMenu,
postCount,
pageIds,
latestPosts
}
}

View File

@@ -1,10 +1,11 @@
import { APPEARANCE, LANG, NOTION_PAGE_ID, THEME } from '@/blog.config'
import {
THEMES,
getThemeConfig,
initDarkMode,
saveDarkModeToLocalStorage
} from '@/themes/theme'
import { APPEARANCE, LANG, NOTION_PAGE_ID, THEME } from 'blog.config'
import { useUser } from '@clerk/nextjs'
import { useRouter } from 'next/router'
import { createContext, useContext, useEffect, useState } from 'react'
import {
@@ -13,14 +14,12 @@ import {
redirectUserLang,
saveLangToLocalStorage
} from './lang'
const GlobalContext = createContext()
/**
* 定义全局变量,包括语言、主题、深色模式、加载状态
* @param children
* @returns {JSX.Element}
* @constructor
* 全局上下文
*/
const GlobalContext = createContext()
export function GlobalContextProvider(props) {
const {
post,
@@ -43,6 +42,12 @@ export function GlobalContextProvider(props) {
const [onLoading, setOnLoading] = useState(false) // 抓取文章数据
const router = useRouter()
// 登录验证相关
const enableClerk = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
const { isLoaded, isSignedIn, user } = enableClerk
? useUser()
: { isLoaded: true, isSignedIn: false, user: false }
// 是否全屏
const fullWidth = post?.fullWidth ?? false
@@ -74,10 +79,6 @@ export function GlobalContextProvider(props) {
htmlElement.classList?.add(newStatus ? 'dark' : 'light')
}
/**
* 更新语言
* 这里是代码级别的多语言,整个站点和文章内容的多语言不在此处理
*/
function changeLang(lang) {
if (lang) {
saveLangToLocalStorage(lang)
@@ -86,16 +87,22 @@ export function GlobalContextProvider(props) {
}
}
// 添加路由变化时的语言处理
useEffect(() => {
initLocale(router.locale, changeLang, updateLocale)
}, [router])
useEffect(() => {
initDarkMode(updateDarkMode, defaultDarkMode)
initLocale(lang, locale, updateLang, updateLocale)
// 可以
if (NOTION_CONFIG?.REDIRECT_LANG) {
if (
NOTION_CONFIG?.REDIRECT_LANG &&
JSON.parse(NOTION_CONFIG?.REDIRECT_LANG)
) {
redirectUserLang(NOTION_PAGE_ID)
}
setOnLoading(false)
}, [])
// 加载进度条
useEffect(() => {
const handleStart = url => {
const { theme } = router.query
@@ -103,10 +110,15 @@ export function GlobalContextProvider(props) {
const newUrl = `${url}${url.includes('?') ? '&' : '?'}theme=${theme}`
router.push(newUrl)
}
setOnLoading(true)
if (!onLoading) {
setOnLoading(true)
}
}
const handleStop = () => {
setOnLoading(false)
if (onLoading) {
setOnLoading(false)
}
}
const currentTheme = router?.query?.theme || theme
@@ -120,11 +132,14 @@ export function GlobalContextProvider(props) {
router.events.off('routeChangeComplete', handleStop)
router.events.off('routeChangeError', handleStop)
}
}, [router])
}, [router, onLoading])
return (
<GlobalContext.Provider
value={{
isLoaded,
isSignedIn,
user,
fullWidth,
NOTION_CONFIG,
THEME_CONFIG,

View File

@@ -65,39 +65,32 @@ export function generateLocaleDict(langString) {
}
/**
* 初始化站点翻译
* 根据用户当前浏览器语言进行切换
* 站点翻译
* 借助router中的locale机制根据locale自动切换对应的语言
*/
export function initLocale(lang, locale, changeLang, changeLocale) {
export function initLocale(locale, changeLang, updateLocale) {
if (isBrowser) {
// 用户请求的语言
let queryLang =
getQueryVariable('locale') ||
getQueryVariable('lang') ||
loadLangFromLocalStorage()
// 根据router中的locale对象判断当前语言表现为前缀中包含 zh、en 等。
let pathLocaleLang = null
if (locale === 'en' || locale === 'zh') {
pathLocaleLang = locale === 'en' ? 'en-US' : 'zh-CN'
}
// 如果有query参数切换语言则优先
const queryLang =
getQueryVariable('locale') || getQueryVariable('lang') || pathLocaleLang
if (queryLang) {
// 用正则表达式匹配有效的语言标识符例如zh-CN可选的 -CN 部分)
queryLang = queryLang.match(/[a-zA-Z]{2}(?:-[a-zA-Z]{2})?/)
if (queryLang) {
queryLang = queryLang[0]
const match = queryLang.match(/[a-zA-Z]{2}(?:-[a-zA-Z]{2})?/)
if (match) {
const targetLang = match[0]
changeLang(targetLang)
const targetLocale = generateLocaleDict(targetLang)
updateLocale(targetLocale)
}
}
let currentLang = lang
if (queryLang && queryLang !== lang) {
currentLang = queryLang
}
changeLang(currentLang)
saveLangToLocalStorage(currentLang)
const targetLocale = generateLocaleDict(currentLang)
if (JSON.stringify(locale) !== JSON.stringify(currentLang)) {
changeLocale(targetLocale)
}
}
}
/**
* 读取语言
* @returns {*}

View File

@@ -21,6 +21,8 @@ export default {
},
COMMON: {
THEME: 'Theme',
SIGN_IN: 'Sign In',
SIGN_OUT: 'Sign Out',
ARTICLE_LIST: 'Article List',
RECOMMEND_POSTS: 'Recommend Posts',
MORE: 'More',
@@ -66,7 +68,8 @@ export default {
MINUTE: 'min',
WORD_COUNT: 'Words',
READ_TIME: 'Read Time',
NEXT_POST: '下一篇'
NEXT_POST: 'Next',
PREV_POST: 'Prev'
},
PAGINATION: {
PREV: 'Prev',
@@ -84,5 +87,8 @@ export default {
SUBSCRIBE: 'Subscribe',
MSG: 'Get the latest news and articles to your inbox every month.',
EMAIL: 'Email'
},
AI_SUMMARY: {
NAME: 'AI intelligent summary',
}
}

View File

@@ -21,7 +21,7 @@ export default {
URL_COPIED: "L'URL est copé!",
TABLE_OF_CONTENTS: 'Sommaire',
RELATE_POSTS: 'Article similaire',
COPYRIGHT: 'Droit d\'auteur',
COPYRIGHT: "Droit d'auteur",
AUTHOR: 'Auteur',
URL: 'Link',
ANALYTICS: 'Analytique',
@@ -29,7 +29,8 @@ export default {
ARTICLE: 'Article(s)',
VISITORS: 'Visiteurs',
VIEWS: 'Views',
COPYRIGHT_NOTICE: 'Attribution - Pas dUtilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International (CC BY-NC-SA 4.0)',
COPYRIGHT_NOTICE:
'Attribution - Pas dUtilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International (CC BY-NC-SA 4.0)',
RESULT_OF_SEARCH: 'Résultats',
ARTICLE_DETAIL: 'Plus de détails',
PASSWORD_ERROR: 'Mot de passe est incorrect!',
@@ -50,5 +51,8 @@ export default {
POST: {
BACK: 'Page precedente',
TOP: 'Haut'
},
AI_SUMMARY: {
NAME: "Résumé intelligent par l'IA",
}
}

View File

@@ -58,5 +58,8 @@ export default {
POST: {
BACK: '前のページに戻る',
TOP: '上に戻る'
},
AI_SUMMARY: {
NAME: 'AIインテリジェントサマリー',
}
}

View File

@@ -53,5 +53,8 @@ export default {
POST: {
BACK: 'Geri',
TOP: 'Yukarı'
},
AI_SUMMARY: {
NAME: 'Yapay Zeka Akıllı Özet',
}
}

View File

@@ -21,6 +21,8 @@ export default {
},
COMMON: {
THEME: 'Theme',
SIGN_IN: '登录',
SIGN_OUT: '登出',
ARTICLE_LIST: '文章列表',
RECOMMEND_POSTS: '推荐文章',
MORE: '更多',
@@ -66,7 +68,8 @@ export default {
MINUTE: '分钟',
WORD_COUNT: '字数',
READ_TIME: '阅读时长',
NEXT_POST: '下一篇'
NEXT_POST: '下一篇',
PREV_POST: '上一篇'
},
PAGINATION: {
PREV: '上页',
@@ -84,5 +87,8 @@ export default {
SUBSCRIBE: '邮件订阅',
MSG: '订阅以获取每月更新的新闻和文章,直接发送至您的邮箱。',
EMAIL: '邮箱'
},
AI_SUMMARY: {
NAME: 'AI智能摘要',
}
}

View File

@@ -37,7 +37,9 @@ export default {
ARTICLE_LOCK_TIPS: '文章已上鎖,請輸入訪問密碼',
SUBMIT: '提交',
POST_TIME: '发布于',
LAST_EDITED_TIME: '最后更新'
LAST_EDITED_TIME: '最后更新',
NEXT_POST: '下一篇',
PREV_POST: '上一篇'
},
PAGINATION: {
PREV: '上一頁',
@@ -50,5 +52,8 @@ export default {
POST: {
BACK: '返回',
TOP: '回到頂端'
},
AI_SUMMARY: {
NAME: 'AI智能摘要',
}
}

View File

@@ -37,7 +37,9 @@ export default {
ARTICLE_LOCK_TIPS: '文章已上鎖,請輸入訪問密碼',
SUBMIT: '提交',
POST_TIME: '发布于',
LAST_EDITED_TIME: '最后更新'
LAST_EDITED_TIME: '最后更新',
NEXT_POST: '下一篇',
PREV_POST: '上一篇'
},
PAGINATION: {
PREV: '上一頁',
@@ -50,5 +52,8 @@ export default {
POST: {
BACK: '返回',
TOP: '回到頂端'
},
AI_SUMMARY: {
NAME: 'AI智能摘要',
}
}

View File

@@ -0,0 +1,131 @@
const axios = require('axios')
// 发送 Notion API 请求
async function postNotion(
properties: any,
databaseId: string,
listContentMain: any[],
token: string
) {
const url = 'https://api.notion.com/v1/pages'
const children = listContentMain
.map(contentMain => {
if (contentMain.type === 'paragraph') {
return {
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: [
{ type: 'text', text: { content: contentMain.content } }
]
}
}
} else if (['file', 'image'].includes(contentMain.type)) {
return {
object: 'block',
type: contentMain.type,
[contentMain.type]: {
type: 'external',
external: { url: contentMain.content }
}
}
}
return null
})
.filter(Boolean)
const payload = {
parent: { database_id: databaseId },
properties,
children
}
const headers = {
accept: 'application/json',
'Notion-Version': '2022-06-28',
'content-type': 'application/json',
Authorization: `Bearer ${token}`
}
try {
const response = await axios.post(url, payload, { headers })
return response
} catch (error: any) {
console.error('写入Notion异常', error)
throw new Error(`Error posting to Notion: ${error.message}`)
}
}
// 处理响应结果
function responseResult(response: { status: number; data: any }) {
if (response.status === 200) {
console.log('成功...')
console.log(response.data)
} else {
console.log('失败...')
console.log(response.data)
}
}
// 准备属性字段
function notionProperty(
id: any,
avatar: any,
name: any,
mail: any,
lastLoginTime: any,
token: any
) {
return {
id: {
rich_text: [
{
type: 'text',
text: {
content: id,
link: null
}
}
]
},
avatar: {
files: [
{
name: 'Project Alpha blueprint',
external: {
url: avatar
}
}
]
},
name: {
title: [
{
text: {
content: name
}
}
]
},
mail: {
email: mail
},
last_login_time: {
date: {
start: lastLoginTime
}
},
token: {
rich_text: [
{
type: 'text',
text: {
content: token,
link: null
}
}
]
}
}
}

View File

@@ -1,5 +1,6 @@
import { idToUuid } from 'notion-utils'
import { checkStrIsNotionId, getLastPartOfUrl, isBrowser } from '../utils'
import { loadLangFromLocalStorage } from '@/lib/lang'
/**
* 处理页面内连接跳转:
@@ -7,45 +8,46 @@ import { checkStrIsNotionId, getLastPartOfUrl, isBrowser } from '../utils'
* 2.url是notion-id转成站内文章链接
*/
export const convertInnerUrl = allPages => {
if (isBrowser) {
const allAnchorTags = document
?.getElementById('notion-article')
?.getElementsByTagName('a')
if (!isBrowser) {
return
}
const allAnchorTags = document
?.getElementById('notion-article')
?.querySelectorAll('a.notion-link')
if (!allAnchorTags) {
return
}
const currentURL = window.location.origin + window.location.pathname
if (!allAnchorTags) {
return
}
const { origin, pathname } = window.location;
const currentURL = origin + pathname
const currentPathLang = pathname.split('/').filter(Boolean)[0]
const lang = loadLangFromLocalStorage().split(/[-_]/)[0]
const langPrefix = lang === currentPathLang ? '/' + lang : ''
for (const anchorTag of allAnchorTags) {
// url替换成slug
for (const anchorTag of allAnchorTags) {
// 检查url
if (anchorTag?.href) {
// 如果url是一个Notion_id尝试匹配成博客的文章内链
const slug = getLastPartOfUrl(anchorTag.href)
if (checkStrIsNotionId(slug)) {
const slugPage = allPages?.find(page => {
return idToUuid(slug).indexOf(page.short_id) === 0
})
if (slugPage) {
anchorTag.href = slugPage?.href
}
if (anchorTag?.href) {
// 如果url是一个Notion_id尝试匹配成博客的文章内链
const slug = getLastPartOfUrl(anchorTag.href)
if (checkStrIsNotionId(slug)) {
const slugPage = allPages?.find(page => {
return idToUuid(slug).indexOf(page.short_id) === 14
})
if (slugPage) {
anchorTag.href = langPrefix + slugPage?.href
}
}
}
// 链接在当前页面打开
for (const anchorTag of allAnchorTags) {
if (anchorTag?.target === '_blank') {
const hrefWithoutQueryHash = anchorTag.href.split('?')[0].split('#')[0]
const hrefWithRelativeHash =
currentURL.split('#')[0] || '' + anchorTag.href.split('#')[1] || ''
if (
currentURL === hrefWithoutQueryHash ||
currentURL === hrefWithRelativeHash
) {
anchorTag.target = '_self'
}
if (anchorTag?.target === '_blank') {
const hrefWithoutQueryHash = anchorTag.href.split('?')[0].split('#')[0]
const hrefWithRelativeHash =
currentURL.split('#')[0] || '' + anchorTag.href.split('#')[1] || ''
if (
currentURL === hrefWithoutQueryHash ||
currentURL === hrefWithRelativeHash
) {
anchorTag.target = '_self'
}
}
}
}
}

View File

@@ -0,0 +1,10 @@
import { NotionAPI } from 'notion-client'
import BLOG from '@/blog.config'
export default function getNotionAPI() {
return new NotionAPI({
activeUser: BLOG.NOTION_ACTIVE_USER || null,
authToken: BLOG.NOTION_TOKEN_V2 || null,
userTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
})
}

View File

@@ -36,7 +36,7 @@ export async function getConfigMapFromConfigPage(allPages) {
})
if (!configPage) {
console.warn('[Notion配置] 未找到配置页面')
// console.warn('[Notion配置] 未找到配置页面')
return null
}
const configPageId = configPage.id
@@ -51,11 +51,11 @@ export async function getConfigMapFromConfigPage(allPages) {
}
if (!content) {
console.warn(
'[Notion配置] 未找到配置表格',
pageRecordMap.block[configPageId],
pageRecordMap.block[configPageId].value
)
// console.warn(
// '[Notion配置] 未找到配置表格',
// pageRecordMap.block[configPageId],
// pageRecordMap.block[configPageId].value
// )
return null
}
@@ -66,11 +66,11 @@ export async function getConfigMapFromConfigPage(allPages) {
// eslint-disable-next-line no-constant-condition, no-self-compare
if (!configTableId) {
console.warn(
'[Notion配置]未找到配置表格数据',
pageRecordMap.block[configPageId],
pageRecordMap.block[configPageId].value
)
// console.warn(
// '[Notion配置]未找到配置表格数据',
// pageRecordMap.block[configPageId],
// pageRecordMap.block[configPageId].value
// )
return null
}
@@ -157,17 +157,24 @@ export async function getConfigMapFromConfigPage(allPages) {
// 只导入生效的配置
if (config.enable) {
// console.log('[Notion配置]', config.key, config.value)
notionConfig[config.key] = config.value
notionConfig[config.key] =
parseTextToJson(config.value) || config.value || null
// 配置不能是undefined至少是null
}
}
}
// 最后检查Notion_Config页面的INLINE_CONFIG是否是一个js对象
const combine = Object.assign(
{},
deepClone(notionConfig),
parseConfig(notionConfig?.INLINE_CONFIG)
)
let combine = notionConfig
try {
// 将INLINE_CONFIG合并@see https://docs.tangly1024.com/article/notion-next-inline-config
combine = Object.assign(
{},
deepClone(notionConfig),
notionConfig?.INLINE_CONFIG
)
} catch (err) {
console.warn('解析 INLINE_CONFIG 配置时出错,请检查JSON格式', err)
}
return combine
}
@@ -186,7 +193,23 @@ export function parseConfig(configString) {
const config = eval('(' + configString + ')')
return config
} catch (evalError) {
console.error('解析 eval(INLINE_CONFIG) 配置时出错:', evalError)
console.warn(
'解析 eval(INLINE_CONFIG) 配置时出错,请检查JSON格式',
evalError
)
return {}
}
}
/**
* 解析文本为JSON
* @param text
* @returns {any|null}
*/
export function parseTextToJson(text) {
try {
return JSON.parse(text)
} catch (error) {
return null
}
}

View File

@@ -6,12 +6,13 @@ import formatDate from '../utils/formatDate'
import md5 from 'js-md5'
import { siteConfig } from '../config'
import {
checkStartWithHttp,
convertUrlStartWithOneSlash,
getLastSegmentFromUrl
checkStartWithHttp,
convertUrlStartWithOneSlash,
getLastSegmentFromUrl
} from '../utils'
import { extractLangPrefix } from '../utils/pageId'
import { mapImgUrl } from './mapImage'
import getNotionAPI from '@/lib/notion/getNotionAPI'
/**
* 获取页面元素成员属性
@@ -56,7 +57,7 @@ export default async function getPageProperties(
case 'person': {
const rawUsers = val.flat()
const users = []
const api = new NotionAPI({ authToken })
const api = getNotionAPI()
for (let i = 0; i < rawUsers.length; i++) {
if (rawUsers[i][0][1]) {
@@ -149,20 +150,25 @@ function convertToJSON(str) {
* 映射用户自定义表头
*/
function mapProperties(properties) {
if (properties?.type === BLOG.NOTION_PROPERTY_NAME.type_post) {
properties.type = 'Post'
const typeMap = {
[BLOG.NOTION_PROPERTY_NAME.type_post]: 'Post',
[BLOG.NOTION_PROPERTY_NAME.type_page]: 'Page',
[BLOG.NOTION_PROPERTY_NAME.type_notice]: 'Notice',
[BLOG.NOTION_PROPERTY_NAME.type_menu]: 'Menu',
[BLOG.NOTION_PROPERTY_NAME.type_sub_menu]: 'SubMenu'
}
if (properties?.type === BLOG.NOTION_PROPERTY_NAME.type_page) {
properties.type = 'Page'
const statusMap = {
[BLOG.NOTION_PROPERTY_NAME.status_publish]: 'Published',
[BLOG.NOTION_PROPERTY_NAME.status_invisible]: 'Invisible'
}
if (properties?.type === BLOG.NOTION_PROPERTY_NAME.type_notice) {
properties.type = 'Notice'
if (properties?.type && typeMap[properties.type]) {
properties.type = typeMap[properties.type]
}
if (properties?.status === BLOG.NOTION_PROPERTY_NAME.status_publish) {
properties.status = 'Published'
}
if (properties?.status === BLOG.NOTION_PROPERTY_NAME.status_invisible) {
properties.status = 'Invisible'
if (properties?.status && statusMap[properties.status]) {
properties.status = statusMap[properties.status]
}
}
@@ -175,9 +181,7 @@ export function adjustPageProperties(properties, NOTION_CONFIG) {
// 1.按照用户配置的URL_PREFIX 转换一下slug
// 2.为文章添加一个href字段存储最终调整的路径
if (properties.type === 'Post') {
if (siteConfig('POST_URL_PREFIX', '', NOTION_CONFIG)) {
properties.slug = generateCustomizeSlug(properties, NOTION_CONFIG)
}
properties.slug = generateCustomizeSlug(properties, NOTION_CONFIG)
properties.href = properties.slug ?? properties.id
} else if (properties.type === 'Page') {
properties.href = properties.slug ?? properties.id
@@ -240,11 +244,16 @@ function generateCustomizeSlug(postProperties, NOTION_CONFIG) {
return postProperties.slug
}
let fullPrefix = ''
const allSlugPatterns = siteConfig(
'POST_URL_PREFIX',
'',
NOTION_CONFIG
).split('/')
let allSlugPatterns = NOTION_CONFIG?.POST_URL_PREFIX
if (allSlugPatterns === undefined || allSlugPatterns === null) {
allSlugPatterns = siteConfig(
'POST_URL_PREFIX',
BLOG.POST_URL_PREFIX,
NOTION_CONFIG
).split('/')
} else {
allSlugPatterns = allSlugPatterns.split('/')
}
const POST_URL_PREFIX_MAPPING_CATEGORY = siteConfig(
'POST_URL_PREFIX_MAPPING_CATEGORY',
@@ -291,9 +300,9 @@ function generateCustomizeSlug(postProperties, NOTION_CONFIG) {
fullPrefix = fullPrefix.substring(0, fullPrefix.length - 1) // 去掉尾部部的"/"
}
if(fullPrefix){
if (fullPrefix) {
return `${fullPrefix}/${postProperties.slug ?? postProperties.id}`
}else{
} else {
return `${postProperties.slug ?? postProperties.id}`
}
}

View File

@@ -12,7 +12,7 @@ const indentLevels = {
* H1, H2, and H3 elements.
*/
export const getPageTableOfContents = (page, recordMap) => {
const contents = (page.content ?? [])
const contents = page.content ?? []
const toc = getBlockHeader(contents, recordMap)
const indentLevelStack = [
{
@@ -69,20 +69,28 @@ function getBlockHeader(contents, recordMap, toc) {
continue
}
const { type } = block
if (type.indexOf('header') >= 0) {
const existed = toc.find(e => e.id === blockId)
if (!existed) {
toc.push({
id: blockId,
type,
text: getTextContent(block.properties?.title),
indentLevel: indentLevels[type]
})
}
}
if (block.content?.length > 0) {
getBlockHeader(block.content, recordMap, toc)
} else {
if (type.indexOf('header') >= 0) {
const existed = toc.find(e => e.id === blockId)
if (!existed) {
toc.push({
id: blockId,
type,
text: getTextContent(block.properties?.title),
indentLevel: indentLevels[type]
})
}
} else if (type === 'transclusion_reference') {
getBlockHeader(
[block.format.transclusion_reference_pointer.id],
recordMap,
toc
)
} else if (type === 'transclusion_container') {
getBlockHeader(block.content, recordMap, toc)
}
}
}

View File

@@ -2,6 +2,7 @@ import BLOG from '@/blog.config'
import { getDataFromCache, setDataToCache } from '@/lib/cache/cache_manager'
import { NotionAPI } from 'notion-client'
import { deepClone, delay } from '../utils'
import getNotionAPI from '@/lib/notion/getNotionAPI'
/**
* 获取文章内容
@@ -10,19 +11,20 @@ import { deepClone, delay } from '../utils'
* @param {*} slice
* @returns
*/
export async function getPage(id, from, slice) {
const cacheKey = 'page_block_' + id
export async function getPage(id, from = null, slice) {
const cacheKey = `page_block_${id}`
let pageBlock = await getDataFromCache(cacheKey)
if (pageBlock) {
// console.log('[API<<--缓存]', `from:${from}`, cacheKey)
return filterPostBlocks(id, pageBlock, slice)
// console.debug('[API<<--缓存]', `from:${from}`, cacheKey)
return convertNotionBlocksToPost(id, pageBlock, slice)
}
// 抓取最新数据
pageBlock = await getPageWithRetry(id, from)
if (pageBlock) {
await setDataToCache(cacheKey, pageBlock)
return filterPostBlocks(id, pageBlock, slice)
return convertNotionBlocksToPost(id, pageBlock, slice)
}
return pageBlock
}
@@ -41,11 +43,7 @@ export async function getPageWithRetry(id, from, retryAttempts = 3) {
retryAttempts < 3 ? `剩余重试次数:${retryAttempts}` : ''
)
try {
const authToken = BLOG.NOTION_ACCESS_TOKEN || null
const api = new NotionAPI({
authToken,
userTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
})
const api = getNotionAPI()
const start = new Date().getTime()
const pageData = await api.getPage(id)
const end = new Date().getTime()
@@ -69,7 +67,7 @@ export async function getPageWithRetry(id, from, retryAttempts = 3) {
}
/**
* 获取到的页面BLOCK特殊处理
* Notion页面BLOCK格式化处理
* 1.删除冗余字段
* 2.比如文件、视频、音频、url格式化
* 3.代码块等元素兼容
@@ -78,72 +76,72 @@ export async function getPageWithRetry(id, from, retryAttempts = 3) {
* @param {*} slice 截取数量
* @returns
*/
function filterPostBlocks(id, blockMap, slice) {
function convertNotionBlocksToPost(id, blockMap, slice) {
const clonePageBlock = deepClone(blockMap)
let count = 0
const blocksToProcess = Object.keys(clonePageBlock?.block || {})
// 循环遍历文档的每个block
for (let i = 0; i < blocksToProcess.length; i++) {
const blockId = blocksToProcess[i]
const b = clonePageBlock?.block[blockId]
if (slice && slice > 0 && count > slice) {
delete clonePageBlock?.block[blockId]
continue
}
// 当BlockId等于PageId时移除
if (b?.value?.id === id) {
// 此block含有敏感信息
delete b?.value?.properties
continue
}
count++
if (b?.value?.type === 'sync_block' && b?.value?.children) {
const childBlocks = b.value.children
// 移除同步块
delete clonePageBlock.block[blockId]
// 用子块替代同步块
childBlocks.forEach((childBlock, index) => {
const newBlockId = `${blockId}_child_${index}`
clonePageBlock.block[newBlockId] = childBlock
blocksToProcess.splice(i + index + 1, 0, newBlockId)
})
// 重新处理新加入的子块
i--
continue
}
// 处理 c++、c#、汇编等语言名字映射
if (b?.value?.type === 'code') {
if (b?.value?.properties?.language?.[0][0] === 'C++') {
b.value.properties.language[0][0] = 'cpp'
const blockId = blocksToProcess[i]
const b = clonePageBlock?.block[blockId]
if (slice && slice > 0 && count > slice) {
delete clonePageBlock?.block[blockId]
continue
}
if (b?.value?.properties?.language?.[0][0] === 'C#') {
b.value.properties.language[0][0] = 'csharp'
// 当BlockId等于PageId时移除
if (b?.value?.id === id) {
// 此block含有敏感信息
delete b?.value?.properties
continue
}
if (b?.value?.properties?.language?.[0][0] === 'Assembly') {
b.value.properties.language[0][0] = 'asm6502'
count++
if (b?.value?.type === 'sync_block' && b?.value?.children) {
const childBlocks = b.value.children
// 移除同步块
delete clonePageBlock.block[blockId]
// 用子块替代同步块
childBlocks.forEach((childBlock, index) => {
const newBlockId = `${blockId}_child_${index}`
clonePageBlock.block[newBlockId] = childBlock
blocksToProcess.splice(i + index + 1, 0, newBlockId)
})
// 重新处理新加入的子块
i--
continue
}
// 处理 c++、c#、汇编等语言名字映射
if (b?.value?.type === 'code') {
if (b?.value?.properties?.language?.[0][0] === 'C++') {
b.value.properties.language[0][0] = 'cpp'
}
if (b?.value?.properties?.language?.[0][0] === 'C#') {
b.value.properties.language[0][0] = 'csharp'
}
if (b?.value?.properties?.language?.[0][0] === 'Assembly') {
b.value.properties.language[0][0] = 'asm6502'
}
}
// 如果是文件或嵌入式PDF需要重新加密签名
if (
(b?.value?.type === 'file' ||
b?.value?.type === 'pdf' ||
b?.value?.type === 'video' ||
b?.value?.type === 'audio') &&
b?.value?.properties?.source?.[0][0] &&
b?.value?.properties?.source?.[0][0].indexOf('amazonaws.com') > 0
) {
const oldUrl = b?.value?.properties?.source?.[0][0]
const newUrl = `https://notion.so/signed/${encodeURIComponent(oldUrl)}?table=block&id=${b?.value?.id}`
b.value.properties.source[0][0] = newUrl
}
}
// 如果是文件或嵌入式PDF需要重新加密签名
if (
(b?.value?.type === 'file' ||
b?.value?.type === 'pdf' ||
b?.value?.type === 'video' ||
b?.value?.type === 'audio') &&
b?.value?.properties?.source?.[0][0] &&
b?.value?.properties?.source?.[0][0].indexOf('amazonaws.com') > 0
) {
const oldUrl = b?.value?.properties?.source?.[0][0]
const newUrl = `https://notion.so/signed/${encodeURIComponent(oldUrl)}?table=block&id=${b?.value?.id}`
b.value.properties.source[0][0] = newUrl
}
}
// 去掉不用的字段
if (id === BLOG.NOTION_PAGE_ID) {
@@ -165,11 +163,7 @@ export const fetchInBatches = async (ids, batchSize = 100) => {
ids = [ids]
}
const authToken = BLOG.NOTION_ACCESS_TOKEN || null
const api = new NotionAPI({
authToken,
userTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
})
const api = getNotionAPI()
let fetchedBlocks = {}
for (let i = 0; i < ids.length; i += batchSize) {

32
lib/plugins/aiSummary.js Normal file
View File

@@ -0,0 +1,32 @@
/**
* get Ai summary
* @returns {Promise<string>}
* @param aiSummaryAPI
* @param aiSummaryKey
* @param truncatedText
*/
export async function getAiSummary(aiSummaryAPI, aiSummaryKey, truncatedText) {
try {
console.log('请求文章摘要', truncatedText.slice(0, 100))
const response = await fetch(aiSummaryAPI, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
token: aiSummaryKey,
content: truncatedText
})
})
if (response.ok) {
const data = await response.json()
return data.summary
} else {
throw new Error('Response not ok')
}
} catch (error) {
console.error('ChucklePostAI请求失败', error)
return '获取文章摘要失败,请稍后再试。'
}
}

27
lib/plugins/wordCount.js Normal file
View File

@@ -0,0 +1,27 @@
/**
* 更新字数统计和阅读时间
*/
export function countWords(pageContentText) {
const wordCount = fnGetCpmisWords(pageContentText)
// 阅读速度 300-500每分钟
const readTime = Math.floor(wordCount / 400) + 1
return { wordCount, readTime }
}
// 用word方式计算正文字数
function fnGetCpmisWords(str) {
if (!str) {
return 0
}
let sLen = 0
try {
// eslint-disable-next-line no-irregular-whitespace
str = str.replace(/(\r\n+|\s+| +)/g, '龘')
// eslint-disable-next-line no-control-regex
str = str.replace(/[\x00-\xff]/g, 'm')
str = str.replace(/m+/g, '*')
str = str.replace(/龘+/g, '')
sLen = str.length
} catch (e) {}
return sLen
}

View File

@@ -39,8 +39,8 @@ export async function generateRss(props) {
const SUB_PATH = NOTION_CONFIG?.SUB_PATH || BLOG.SUB_PATH
const CONTACT_EMAIL = NOTION_CONFIG?.CONTACT_EMAIL || BLOG.CONTACT_EMAIL
// 检查 feed 文件是否在30分钟内更新过
if (isFeedRecentlyUpdated('./public/rss/feed.xml', 60)) {
// 检查 feed 文件是否在10分钟内更新过
if (isFeedRecentlyUpdated('./public/rss/feed.xml', 10)) {
return
}

View File

@@ -1,31 +1,43 @@
import fs from 'fs'
import BLOG from '@/blog.config'
export async function generateSitemapXml({ allPages }) {
const urls = [{
loc: `${BLOG.LINK}`,
lastmod: new Date().toISOString().split('T')[0],
changefreq: 'daily'
}, {
loc: `${BLOG.LINK}/archive`,
lastmod: new Date().toISOString().split('T')[0],
changefreq: 'daily'
}, {
loc: `${BLOG.LINK}/category`,
lastmod: new Date().toISOString().split('T')[0],
changefreq: 'daily'
}, {
loc: `${BLOG.LINK}/tag`,
lastmod: new Date().toISOString().split('T')[0],
changefreq: 'daily'
}]
import fs from 'fs'
import { siteConfig } from './config'
/**
* 生成站点地图
* @param {*} param0
*/
export async function generateSitemapXml({ allPages, NOTION_CONFIG }) {
const link = siteConfig('LINK', BLOG.LINK, NOTION_CONFIG)
const urls = [
{
loc: `${link}`,
lastmod: new Date().toISOString().split('T')[0],
changefreq: 'daily',
priority: 1.0
},
{
loc: `${link}/archive`,
lastmod: new Date().toISOString().split('T')[0],
changefreq: 'daily',
priority: 1.0
},
{
loc: `${link}/category`,
lastmod: new Date().toISOString().split('T')[0],
changefreq: 'daily'
},
{
loc: `${link}/tag`,
lastmod: new Date().toISOString().split('T')[0],
changefreq: 'daily'
}
]
// 循环页面生成
allPages?.forEach(post => {
const slugWithoutLeadingSlash = post?.slug?.startsWith('/') ? post?.slug?.slice(1) : post.slug
const slugWithoutLeadingSlash = post?.slug?.startsWith('/')
? post?.slug?.slice(1)
: post.slug
urls.push({
loc: `${BLOG.LINK}/${slugWithoutLeadingSlash}`,
loc: `${link}/${slugWithoutLeadingSlash}`,
lastmod: new Date(post?.publishDay).toISOString().split('T')[0],
changefreq: 'daily'
})

View File

@@ -1,22 +1,31 @@
import BLOG from '@/blog.config'
/**
* 格式化日期
* @param date
* @param local
* @returns {string}
*/
export default function formatDate (date, local) {
export default function formatDate(date, local = BLOG.LANG) {
if (!date || !local) return date || ''
const d = new Date(date)
const options = { year: 'numeric', month: 'short', day: 'numeric' }
const res = d.toLocaleDateString(local, options)
// 如果格式是中文日期,则转为横杆
const format = local.slice(0, 2).toLowerCase() === 'zh'
? res.replace('年', '-').replace('月', '-').replace('日', '')
: res
const format =
local.slice(0, 2).toLowerCase() === 'zh'
? res.replace('年', '-').replace('月', '-').replace('日', '')
: res
return format
}
export function formatDateFmt (timestamp, fmt) {
/**
* 时间戳格式化
* @param {*} timestamp
* @param {*} fmt
* @returns
*/
export function formatDateFmt(timestamp, fmt) {
const date = new Date(timestamp)
const o = {
'M+': date.getMonth() + 1, // 月份
@@ -28,11 +37,17 @@ export function formatDateFmt (timestamp, fmt) {
S: date.getMilliseconds() // 毫秒
}
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
fmt = fmt.replace(
RegExp.$1,
(date.getFullYear() + '').substr(4 - RegExp.$1.length)
)
}
for (const k in o) {
if (new RegExp('(' + k + ')').test(fmt)) {
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (('00' + o[k]).substr(('' + o[k]).length)))
fmt = fmt.replace(
RegExp.$1,
RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length)
)
}
}
return fmt.trim()

View File

@@ -27,16 +27,9 @@ export const shuffleArray = array => {
* google机器人
* @returns
*/
export const isSearchEngineBot = () => {
if (typeof navigator === 'undefined') {
return false
}
// 获取用户代理字符串
const userAgent = navigator.userAgent
// 使用正则表达式检测是否包含搜索引擎爬虫关键字
return /Googlebot|bingbot|Baidu/.test(userAgent)
}
export const isSearchEngineBot =
typeof navigator !== 'undefined' &&
/Googlebot|bingbot|Baidu/.test(navigator.userAgent)
/**
* 组件持久化
*/
@@ -146,7 +139,7 @@ export function getLastPartOfUrl(url) {
* @param type js 或 css
* @returns {Promise<unknown>}
*/
export function loadExternalResource(url, type) {
export function loadExternalResource(url, type = 'js') {
// 检查是否已存在
const elements =
type === 'js'
@@ -176,11 +169,11 @@ export function loadExternalResource(url, type) {
}
if (tag) {
tag.onload = () => {
console.log('Load Success', url)
// console.log('Load Success', url)
resolve(url)
}
tag.onerror = () => {
console.log('Load Error', url)
console.warn('Load Error', url)
reject(url)
}
document.head.appendChild(tag)
@@ -211,6 +204,9 @@ export function getQueryVariable(key) {
* @returns {string|null}
*/
export function getQueryParam(url, param) {
if (!url) {
return ''
}
// 移除哈希部分
const urlWithoutHash = url.split('#')[0]
const searchParams = new URLSearchParams(urlWithoutHash.split('?')[1])

View File

@@ -38,10 +38,7 @@ function getShortId(uuid) {
if (!uuid || uuid.indexOf('-') < 0) {
return uuid
}
// 找到第一个 '-' 的位置
const index = uuid.indexOf('-')
// 截取从开始到第一个 '-' 之前的部分
return uuid.substring(0, index)
return uuid.substring(14)
}
module.exports = { extractLangPrefix, extractLangId, getShortId }

View File

@@ -2,6 +2,15 @@
* 文章相关工具
*/
import { checkStartWithHttp } from '.'
import { getPostBlocks } from '@/lib/db/getSiteData'
import { getPageTableOfContents } from '@/lib/notion/getPageTableOfContents'
import { siteConfig } from '@/lib/config'
import { getDataFromCache, setDataToCache } from '@/lib/cache/cache_manager'
import { getPageContentText } from '@/pages/search/[keyword]'
import { getAiSummary } from '@/lib/plugins/aiSummary'
import BLOG from '@/blog.config'
import { uploadDataToAlgolia } from '@/lib/plugins/algolia'
import { countWords } from '@/lib/plugins/wordCount'
/**
* 获取文章的关联推荐文章列表,目前根据标签关联性筛选
@@ -88,3 +97,87 @@ export function checkSlugHasMorThanTwoSlash(row) {
!checkStartWithHttp(slug)
)
}
/**
* 获取文章摘要
* @param props
* @param pageContentText
* @returns {Promise<void>}
*/
async function getPageAISummary(props, pageContentText) {
const aiSummaryAPI = siteConfig('AI_SUMMARY_API')
if (aiSummaryAPI) {
const post = props.post
const cacheKey = `ai_summary_${post.id}`
let aiSummary = await getDataFromCache(cacheKey)
if (aiSummary) {
props.post.aiSummary = aiSummary
} else {
const aiSummaryKey = siteConfig('AI_SUMMARY_KEY')
const aiSummaryCacheTime = siteConfig('AI_SUMMARY_CACHE_TIME')
const wordLimit = siteConfig('AI_SUMMARY_WORD_LIMIT', '1000')
let content = ''
for (let heading of post.toc) {
content += heading.text + ' '
}
content += pageContentText
const combinedText = post.title + ' ' + content
const truncatedText = combinedText.slice(0, wordLimit)
aiSummary = await getAiSummary(aiSummaryAPI, aiSummaryKey, truncatedText)
await setDataToCache(cacheKey, aiSummary, aiSummaryCacheTime)
props.post.aiSummary = aiSummary
}
}
}
/**
* 处理文章数据
* @param props
* @param from
* @returns {Promise<void>}
*/
export async function processPostData(props, from) {
// 文章内容加载
if (!props?.post?.blockMap) {
props.post.blockMap = await getPostBlocks(props.post.id, from)
}
if (props.post?.blockMap?.block) {
// 目录默认加载
props.post.content = Object.keys(props.post.blockMap.block).filter(
key => props.post.blockMap.block[key]?.value?.parent_id === props.post.id
)
props.post.toc = getPageTableOfContents(props.post, props.post.blockMap)
const pageContentText = getPageContentText(props.post, props.post.blockMap)
const { wordCount, readTime } = countWords(pageContentText)
props.post.wordCount = wordCount
props.post.readTime = readTime
await getPageAISummary(props, pageContentText)
}
// 生成全文索引 && JSON.parse(BLOG.ALGOLIA_RECREATE_DATA)
if (BLOG.ALGOLIA_APP_ID) {
uploadDataToAlgolia(props?.post)
}
// 推荐关联文章处理
const allPosts = props.allPages?.filter(
page => page.type === 'Post' && page.status === 'Published'
)
if (allPosts && allPosts.length > 0) {
const index = allPosts.indexOf(props.post)
props.prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0]
props.next = allPosts.slice(index + 1, index + 2)[0] ?? allPosts[0]
props.recommendPosts = getRecommendPost(
props.post,
allPosts,
siteConfig('POST_RECOMMEND_COUNT')
)
} else {
props.prev = null
props.next = null
props.recommendPosts = []
}
delete props.allPages
}