mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-05-18 07:26:48 +00:00
Merge branch 'main' into original-fix
This commit is contained in:
12
lib/cache/cache_manager.js
vendored
12
lib/cache/cache_manager.js
vendored
@@ -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) {
|
||||
|
||||
4
lib/cache/memory_cache.js
vendored
4
lib/cache/memory_cache.js
vendored
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
43
lib/lang.js
43
lib/lang.js
@@ -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 {*}
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 d’Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International (CC BY-NC-SA 4.0)',
|
||||
COPYRIGHT_NOTICE:
|
||||
'Attribution - Pas d’Utilisation 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",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,5 +58,8 @@ export default {
|
||||
POST: {
|
||||
BACK: '前のページに戻る',
|
||||
TOP: '上に戻る'
|
||||
},
|
||||
AI_SUMMARY: {
|
||||
NAME: 'AIインテリジェントサマリー',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,5 +53,8 @@ export default {
|
||||
POST: {
|
||||
BACK: 'Geri',
|
||||
TOP: 'Yukarı'
|
||||
},
|
||||
AI_SUMMARY: {
|
||||
NAME: 'Yapay Zeka Akıllı Özet',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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智能摘要',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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智能摘要',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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智能摘要',
|
||||
}
|
||||
}
|
||||
|
||||
131
lib/notion/CustomNotionApi.ts
Normal file
131
lib/notion/CustomNotionApi.ts
Normal 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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
lib/notion/getNotionAPI.js
Normal file
10
lib/notion/getNotionAPI.js
Normal 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
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
32
lib/plugins/aiSummary.js
Normal 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
27
lib/plugins/wordCount.js
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user