diff --git a/blog.config.js b/blog.config.js index 4906d18a..97b66b9e 100644 --- a/blog.config.js +++ b/blog.config.js @@ -37,6 +37,7 @@ const BLOG = { ...require('./conf/animation.config'), // 动效美化效果 ...require('./conf/widget.config'), // 悬浮在网页上的挂件,聊天客服、宠物挂件、音乐播放器等 ...require('./conf/ad.config'), // 广告营收插件 + ...require('./conf/plugin.config'), // 其他第三方插件 algolia全文索引 // 高级用法 ...require('./conf/layout-map.config'), // 路由与布局映射自定义,例如自定义特定路由的页面布局 @@ -50,13 +51,6 @@ const BLOG = { // 自定义菜单 CUSTOM_MENU: process.env.NEXT_PUBLIC_CUSTOM_MENU || true, // 支持Menu类型的菜单,替代了3.12版本前的Page类型 - // 网站全文搜索 - ALGOLIA_APP_ID: process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || null, // 在这里查看 https://dashboard.algolia.com/account/api-keys/ - ALGOLIA_ADMIN_APP_KEY: process.env.ALGOLIA_ADMIN_APP_KEY || null, // 管理后台的KEY,不要暴露在代码中,在这里查看 https://dashboard.algolia.com/account/api-keys/ - ALGOLIA_SEARCH_ONLY_APP_KEY: - process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_APP_KEY || null, // 客户端搜索用的KEY - ALGOLIA_INDEX: process.env.NEXT_PUBLIC_ALGOLIA_INDEX || null, // 在Algolia中创建一个index用作数据库 - // 文章列表相关设置 CAN_COPY: process.env.NEXT_PUBLIC_CAN_COPY || true, // 是否允许复制页面内容 默认允许,如果设置为false、则全栈禁止复制内容。 @@ -67,7 +61,10 @@ const BLOG = { // 欢迎语打字效果,Hexo,Matery主题支持, 英文逗号隔开多个欢迎语。 GREETING_WORDS: process.env.NEXT_PUBLIC_GREETING_WORDS || - 'Hi,我是一个程序员, Hi,我是一个打工人,Hi,我是一个干饭人,欢迎来到我的博客🎉' + 'Hi,我是一个程序员, Hi,我是一个打工人,Hi,我是一个干饭人,欢迎来到我的博客🎉', + + // uuid重定向至 slug + UUID_REDIRECT: process.env.UUID_REDIRECT || false } module.exports = BLOG diff --git a/components/ShareButtons.js b/components/ShareButtons.js index 5ea54055..29fd37b4 100644 --- a/components/ShareButtons.js +++ b/components/ShareButtons.js @@ -5,48 +5,48 @@ import { useRouter } from 'next/router' import { useEffect, useState } from 'react' import { - EmailIcon, - EmailShareButton, - FacebookIcon, - FacebookMessengerIcon, - FacebookMessengerShareButton, - FacebookShareButton, - HatenaIcon, - HatenaShareButton, - InstapaperIcon, - InstapaperShareButton, - LineIcon, - LineShareButton, - LinkedinIcon, - LinkedinShareButton, - LivejournalIcon, - LivejournalShareButton, - MailruIcon, - MailruShareButton, - OKIcon, - OKShareButton, - PinterestIcon, - PinterestShareButton, - PocketIcon, - PocketShareButton, - RedditIcon, - RedditShareButton, - TelegramIcon, - TelegramShareButton, - TumblrIcon, - TumblrShareButton, - TwitterIcon, - TwitterShareButton, - VKIcon, - VKShareButton, - ViberIcon, - ViberShareButton, - WeiboIcon, - WeiboShareButton, - WhatsappIcon, - WhatsappShareButton, - WorkplaceIcon, - WorkplaceShareButton + EmailIcon, + EmailShareButton, + FacebookIcon, + FacebookMessengerIcon, + FacebookMessengerShareButton, + FacebookShareButton, + HatenaIcon, + HatenaShareButton, + InstapaperIcon, + InstapaperShareButton, + LineIcon, + LineShareButton, + LinkedinIcon, + LinkedinShareButton, + LivejournalIcon, + LivejournalShareButton, + MailruIcon, + MailruShareButton, + OKIcon, + OKShareButton, + PinterestIcon, + PinterestShareButton, + PocketIcon, + PocketShareButton, + RedditIcon, + RedditShareButton, + TelegramIcon, + TelegramShareButton, + TumblrIcon, + TumblrShareButton, + TwitterIcon, + TwitterShareButton, + ViberIcon, + ViberShareButton, + VKIcon, + VKShareButton, + WeiboIcon, + WeiboShareButton, + WhatsappIcon, + WhatsappShareButton, + WorkplaceIcon, + WorkplaceShareButton } from 'react-share' const QrCode = dynamic(() => import('@/components/QrCode'), { ssr: false }) @@ -61,6 +61,8 @@ const ShareButtons = ({ post }) => { const [shareUrl, setShareUrl] = useState(siteConfig('LINK') + router.asPath) const title = post?.title || siteConfig('TITLE') const image = post?.pageCover + const tags = post.tags || [] + const hashTags = tags.map(tag => `#${tag}`).join(',') const body = post?.title + ' | ' + title + ' ' + shareUrl + ' ' + post?.summary @@ -90,299 +92,282 @@ const ShareButtons = ({ post }) => { return ( <> {services.map(singleService => { - if (singleService === 'facebook') { - return ( - - - - ) - } - if (singleService === 'messenger') { - return ( - - - - ) - } - if (singleService === 'line') { - return ( - - - - ) - } - if (singleService === 'reddit') { - return ( - - - - ) - } - if (singleService === 'email') { - return ( - - - - ) - } - if (singleService === 'twitter') { - return ( - - - - ) - } - if (singleService === 'telegram') { - return ( - - - - ) - } - if (singleService === 'whatsapp') { - return ( - - - - ) - } - if (singleService === 'linkedin') { - return ( - - - - ) - } - if (singleService === 'pinterest') { - return ( - - - - ) - } - if (singleService === 'vkshare') { - return ( - - - - ) - } - if (singleService === 'okshare') { - return ( - - - - ) - } - if (singleService === 'tumblr') { - return ( - - - - ) - } - if (singleService === 'livejournal') { - return ( - - - - ) - } - if (singleService === 'mailru') { - return ( - - - - ) - } - if (singleService === 'viber') { - return ( - - - - ) - } - if (singleService === 'workplace') { - return ( - - - - ) - } - if (singleService === 'weibo') { - return ( - - - - ) - } - if (singleService === 'pocket') { - return ( - - - - ) - } - if (singleService === 'instapaper') { - return ( - - - - ) - } - if (singleService === 'hatena') { - return ( - - - - ) - } - if (singleService === 'qq') { - return ( - - ) - } - if (singleService === 'wechat') { - return ( - + ) + case 'wechat': + return ( + - ) +
+
+
+ {qrCodeShow && } +
+ + {locale.COMMON.SCAN_QR_CODE} + +
+
+ + ) + case 'link': + return ( + + ) + default: + return <> } - if (singleService === 'link') { - return ( - - ) - } - return <> })} ) diff --git a/conf/dev.config.js b/conf/dev.config.js index 2cbdfa93..c34a8c64 100644 --- a/conf/dev.config.js +++ b/conf/dev.config.js @@ -7,6 +7,10 @@ module.exports = { // TAILWINDCSS 配置的自定义颜色,作废 BACKGROUND_LIGHT: '#eeeeee', // use hex value, don't forget '#' e.g #fffefc BACKGROUND_DARK: '#000000', // use hex value, don't forget '#' + + // Redis 缓存数据库地址 + REDIS_URL: process.env.REDIS_URL || '', + ENABLE_CACHE: process.env.ENABLE_CACHE || process.env.npm_lifecycle_event === 'build' || diff --git a/conf/plugin.config.js b/conf/plugin.config.js index 371e75d6..aa9e0016 100644 --- a/conf/plugin.config.js +++ b/conf/plugin.config.js @@ -2,6 +2,13 @@ * 一些插件 */ module.exports = { + // 网站全文搜索 + ALGOLIA_APP_ID: process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || null, // 在这里查看 https://dashboard.algolia.com/account/api-keys/ + ALGOLIA_ADMIN_APP_KEY: process.env.ALGOLIA_ADMIN_APP_KEY || null, // 管理后台的KEY,不要暴露在代码中,在这里查看 https://dashboard.algolia.com/account/api-keys/ + ALGOLIA_SEARCH_ONLY_APP_KEY: + process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_APP_KEY || null, // 客户端搜索用的KEY + ALGOLIA_INDEX: process.env.NEXT_PUBLIC_ALGOLIA_INDEX || null, // 在Algolia中创建一个index用作数据库 + // AI 文章摘要生成 AI_SUMMARY_API: process.env.AI_SUMMARY_API || '', diff --git a/lib/cache/cache_manager.js b/lib/cache/cache_manager.js index f35a1fe1..f0c412ab 100644 --- a/lib/cache/cache_manager.js +++ b/lib/cache/cache_manager.js @@ -1,6 +1,55 @@ import BLOG from '@/blog.config' import FileCache from './local_file_cache' import MemoryCache from './memory_cache' +import RedisCache from './redis_cache' + +// 配置是否开启Vercel环境中的缓存,因为Vercel中现有两种缓存方式在无服务环境下基本都是无意义的,纯粹的浪费资源 +const enableCacheInVercel = + process.env.npm_lifecycle_event === 'build' || + process.env.npm_lifecycle_event === 'export' || + !BLOG['isProd'] + +/** + * 尝试从缓存中获取数据,如果没有则尝试获取数据并写入缓存,最终返回所需数据 + * @param key + * @param getDataFunction + * @param getDataArgs + * @returns {Promise<*|null>} + */ +export async function getOrSetDataWithCache( + key, + getDataFunction, + ...getDataArgs +) { + return getOrSetDataWithCustomCache(key, null, getDataFunction, ...getDataArgs) +} + +/** + * 尝试从缓存中获取数据,如果没有则尝试获取数据并自定义写入缓存,最终返回所需数据 + * @param key + * @param customCacheTime + * @param getDataFunction + * @param getDataArgs + * @returns {Promise<*|null>} + */ +export async function getOrSetDataWithCustomCache( + key, + customCacheTime, + getDataFunction, + ...getDataArgs +) { + const dataFromCache = await getDataFromCache(key) + if (dataFromCache) { + console.log('[缓存-->>API]:', key) + return dataFromCache + } + const data = await getDataFunction(...getDataArgs) + if (data) { + console.log('[API-->>缓存]:', key) + await setDataToCache(key, data, customCacheTime) + } + return data || null +} /** * 为减少频繁接口请求,notion数据将被缓存 @@ -20,8 +69,15 @@ export async function getDataFromCache(key, force) { } } +/** + * 写入缓存 + * @param {*} key + * @param {*} data + * @param {*} customCacheTime + * @returns + */ export async function setDataToCache(key, data, customCacheTime) { - if (!data) { + if (!enableCacheInVercel || !data) { return } // console.trace('[API-->>缓存写入]:', key) @@ -39,8 +95,10 @@ export async function delCacheData(key) { * 缓存实现类 * @returns */ -function getApi() { - if (process.env.ENABLE_FILE_CACHE) { +export function getApi() { + if (BLOG.REDIS_URL) { + return RedisCache + } else if (process.env.ENABLE_FILE_CACHE) { return FileCache } else { return MemoryCache diff --git a/lib/cache/redis_cache.js b/lib/cache/redis_cache.js new file mode 100644 index 00000000..b35f472a --- /dev/null +++ b/lib/cache/redis_cache.js @@ -0,0 +1,41 @@ +import BLOG from '@/blog.config' +import { siteConfig } from '@/lib/config' +import Redis from 'ioredis' + +export const redisClient = BLOG.REDIS_URL ? new Redis(BLOG.REDIS_URL) : {} + +const cacheTime = Math.trunc( + siteConfig('NEXT_REVALIDATE_SECOND', BLOG.NEXT_REVALIDATE_SECOND) * 1.5 +) + +export async function getCache(key) { + try { + const data = await redisClient.get(key) + return data ? JSON.parse(data) : null + } catch (e) { + console.error('redisClient读取失败 ' + e) + } +} + +export async function setCache(key, data, customCacheTime) { + try { + await redisClient.set( + key, + JSON.stringify(data), + 'EX', + customCacheTime || cacheTime + ) + } catch (e) { + console.error('redisClient写入失败 ' + e) + } +} + +export async function delCache(key) { + try { + await redisClient.del(key) + } catch (e) { + console.error('redisClient删除失败 ' + e) + } +} + +export default { getCache, setCache, delCache } diff --git a/lib/config.js b/lib/config.js index 4244a04e..54b043a7 100644 --- a/lib/config.js +++ b/lib/config.js @@ -43,6 +43,7 @@ export const siteConfig = (key, defaultVal = null, extendConfig = {}) => { case 'AI_SUMMARY_KEY': case 'AI_SUMMARY_CACHE_TIME': case 'AI_SUMMARY_WORD_LIMIT': + case 'UUID_REDIRECT': // LINK比较特殊, if (key === 'LINK') { if (!extendConfig || Object.keys(extendConfig).length === 0) { diff --git a/lib/db/getSiteData.js b/lib/db/getSiteData.js index 21a0d4a4..a40334b5 100755 --- a/lib/db/getSiteData.js +++ b/lib/db/getSiteData.js @@ -12,6 +12,7 @@ import { deepClone } from '@/lib/utils' import { idToUuid } from 'notion-utils' import { siteConfig } from '../config' import { extractLangId, extractLangPrefix, getShortId } from '../utils/pageId' +import { getOrSetDataWithCache } from '@/lib/cache/cache_manager' export { getAllTags } from '../notion/getAllTags' export { getPost } from '../notion/getNotionPost' @@ -65,14 +66,15 @@ export async function getGlobalData({ */ export async function getSiteDataByPageId({ pageId, from }) { // 获取NOTION原始数据,此接支持mem缓存。 - const pageRecordMap = await getPage(pageId, from) - // 将Notion数据按规则转成站点数据 - const data = await converNotionToSiteDate( + return await getOrSetDataWithCache( + `site_data_${pageId}`, + async (pageId, from) => { + const pageRecordMap = await getPage(pageId, from) + return convertNotionToSiteDate(pageId, from, deepClone(pageRecordMap)) + }, pageId, - from, - deepClone(pageRecordMap) + from ) - return data } /** @@ -139,7 +141,7 @@ const EmptyData = pageId => { * 这里统一对数据格式化 * @returns {Promise} */ -async function converNotionToSiteDate(pageId, from, pageRecordMap) { +async function convertNotionToSiteDate(pageId, from, pageRecordMap) { if (!pageRecordMap) { console.error('can`t get Notion Data ; Which id is: ', pageId) return {} @@ -273,11 +275,12 @@ async function converNotionToSiteDate(pageId, from, pageRecordMap) { categoryOptions: getCategoryOptions(schema) }) // 所有标签 - const tagOptions = getAllTags({ - allPages, - tagOptions: getTagOptions(schema), - NOTION_CONFIG - }) + const tagOptions = + getAllTags({ + allPages, + tagOptions: getTagOptions(schema), + NOTION_CONFIG + }) || null // 旧的菜单 const customNav = getCustomNav({ allPages: collectionData.filter( diff --git a/lib/notion/getNotionAPI.js b/lib/notion/getNotionAPI.js index c2cbb6a6..757534a3 100644 --- a/lib/notion/getNotionAPI.js +++ b/lib/notion/getNotionAPI.js @@ -1,10 +1,14 @@ -import { NotionAPI } from 'notion-client' +import { NotionAPI as NotionLibrary } from 'notion-client' import BLOG from '@/blog.config' -export default function getNotionAPI() { - return new NotionAPI({ +const notionAPI = getNotionAPI() + +function getNotionAPI() { + return new NotionLibrary({ activeUser: BLOG.NOTION_ACTIVE_USER || null, authToken: BLOG.NOTION_TOKEN_V2 || null, userTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone }) } + +export default notionAPI diff --git a/lib/notion/getPageProperties.js b/lib/notion/getPageProperties.js index 5d5e7ff6..571c1b8c 100644 --- a/lib/notion/getPageProperties.js +++ b/lib/notion/getPageProperties.js @@ -1,5 +1,4 @@ import BLOG from '@/blog.config' -import { NotionAPI } from 'notion-client' import { getDateValue, getTextContent } from 'notion-utils' import formatDate from '../utils/formatDate' // import { createHash } from 'crypto' @@ -12,7 +11,7 @@ import { } from '../utils' import { extractLangPrefix } from '../utils/pageId' import { mapImgUrl } from './mapImage' -import getNotionAPI from '@/lib/notion/getNotionAPI' +import notionAPI from '@/lib/notion/getNotionAPI' /** * 获取页面元素成员属性 @@ -57,12 +56,11 @@ export default async function getPageProperties( case 'person': { const rawUsers = val.flat() const users = [] - const api = getNotionAPI() for (let i = 0; i < rawUsers.length; i++) { if (rawUsers[i][0][1]) { const userId = rawUsers[i][0] - const res = await api.getUsers(userId) + const res = await notionAPI.getUsers(userId) const resValue = res?.recordMapWithRoles?.notion_user?.[userId[1]]?.value const user = { diff --git a/lib/notion/getPostBlocks.js b/lib/notion/getPostBlocks.js index d5896ddc..3c08f0bd 100644 --- a/lib/notion/getPostBlocks.js +++ b/lib/notion/getPostBlocks.js @@ -1,8 +1,7 @@ import BLOG from '@/blog.config' -import { getDataFromCache, setDataToCache } from '@/lib/cache/cache_manager' -import { NotionAPI } from 'notion-client' +import { getDataFromCache, getOrSetDataWithCache, setDataToCache } from '@/lib/cache/cache_manager' import { deepClone, delay } from '../utils' -import getNotionAPI from '@/lib/notion/getNotionAPI' +import notionAPI from '@/lib/notion/getNotionAPI' /** * 获取文章内容 @@ -12,21 +11,28 @@ import getNotionAPI from '@/lib/notion/getNotionAPI' * @returns */ export async function getPage(id, from = null, slice) { - const cacheKey = `page_block_${id}` - let pageBlock = await getDataFromCache(cacheKey) - if (pageBlock) { - // console.debug('[API<<--缓存]', `from:${from}`, cacheKey) - return convertNotionBlocksToPost(id, pageBlock, slice) - } + return await getOrSetDataWithCache( + `page_content_${id}_${slice}`, + async (id, slice) => { + const cacheKey = `page_block_${id}` + let pageBlock = await getDataFromCache(cacheKey) + if (pageBlock) { + // console.debug('[API<<--缓存]', `from:${from}`, cacheKey) + return convertNotionBlocksToPost(id, pageBlock, slice) + } - // 抓取最新数据 - pageBlock = await getPageWithRetry(id, from) + // 抓取最新数据 + pageBlock = await getPageWithRetry(id, from) - if (pageBlock) { - await setDataToCache(cacheKey, pageBlock) - return convertNotionBlocksToPost(id, pageBlock, slice) - } - return pageBlock + if (pageBlock) { + await setDataToCache(cacheKey, pageBlock) + return convertNotionBlocksToPost(id, pageBlock, slice) + } + return pageBlock + }, + id, + slice + ) } /** @@ -43,9 +49,8 @@ export async function getPageWithRetry(id, from, retryAttempts = 3) { retryAttempts < 3 ? `剩余重试次数:${retryAttempts}` : '' ) try { - const api = getNotionAPI() const start = new Date().getTime() - const pageData = await api.getPage(id) + const pageData = await notionAPI.getPage(id) const end = new Date().getTime() console.log('[API<<--响应]', `耗时:${end - start}ms - from:${from}`) return pageData @@ -163,14 +168,12 @@ export const fetchInBatches = async (ids, batchSize = 100) => { ids = [ids] } - const api = getNotionAPI() - let fetchedBlocks = {} for (let i = 0; i < ids.length; i += batchSize) { const batch = ids.slice(i, i + batchSize) console.log('[API-->>请求] Fetching missing blocks', batch, ids.length) const start = new Date().getTime() - const pageChunk = await api.getBlocks(batch) + const pageChunk = await notionAPI.getBlocks(batch) const end = new Date().getTime() console.log( `[API<<--响应] 耗时:${end - start}ms Fetching missing blocks count:${ids.length} ` diff --git a/lib/redirect.js b/lib/redirect.js new file mode 100644 index 00000000..7a05dfee --- /dev/null +++ b/lib/redirect.js @@ -0,0 +1,15 @@ +import fs from 'fs' + +export function generateRedirectJson({ allPages }) { + let uuidSlugMap = {} + allPages.forEach(page => { + if (page.type === 'Post' && page.status === 'Published') { + uuidSlugMap[page.id] = page.slug + } + }) + try { + fs.writeFileSync('./public/redirect.json', JSON.stringify(uuidSlugMap)) + } catch (error) { + console.warn('无法写入文件', error) + } +} diff --git a/middleware.ts b/middleware.ts index dc623d08..550c6f8b 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,5 +1,8 @@ import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server' -import { NextResponse } from 'next/server' +import { NextRequest, NextResponse } from 'next/server' +import { checkStrIsNotionId, getLastPartOfUrl } from '@/lib/utils' +import { idToUuid } from 'notion-utils' +import BLOG from './blog.config' /** * Clerk 身份验证中间件 @@ -30,8 +33,31 @@ const isTenantAdminRoute = createRouteMatcher([ * @returns */ // eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars -const noAuthMiddleware = async (req: any, ev: any) => { +const noAuthMiddleware = async (req: NextRequest, ev: any) => { // 如果没有配置 Clerk 相关环境变量,返回一个默认响应或者继续处理请求 + if (BLOG['UUID_REDIRECT']) { + let redirectJson: Record = {} + try { + const response = await fetch(`${req.nextUrl.origin}/redirect.json`) + if (response.ok) { + redirectJson = (await response.json()) as Record + } + } catch (err) { + console.error('Error fetching static file:', err) + } + let lastPart = getLastPartOfUrl(req.nextUrl.pathname) as string + if (checkStrIsNotionId(lastPart)) { + lastPart = idToUuid(lastPart) + } + if (lastPart && redirectJson[lastPart]) { + const redirectToUrl = req.nextUrl.clone() + redirectToUrl.pathname = '/' + redirectJson[lastPart] + console.log( + `redirect from ${req.nextUrl.pathname} to ${redirectToUrl.pathname}` + ) + return NextResponse.redirect(redirectToUrl, 308) + } + } return NextResponse.next() } /** diff --git a/package.json b/package.json index 85dff78c..f9d7ec20 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "notion-next", - "version": "4.8.0", + "version": "4.8.1", "homepage": "https://github.com/tangly1024/NotionNext.git", "license": "MIT", "repository": { @@ -31,6 +31,7 @@ "algoliasearch": "^4.18.0", "axios": "^1.7.2", "feed": "^4.2.2", + "ioredis": "^5.4.2", "js-md5": "^0.7.3", "lodash.throttle": "^4.1.1", "memory-cache": "^0.2.0", diff --git a/pages/index.js b/pages/index.js index 31e2610b..b66b5610 100644 --- a/pages/index.js +++ b/pages/index.js @@ -5,6 +5,7 @@ import { generateRobotsTxt } from '@/lib/robots.txt' import { generateRss } from '@/lib/rss' import { generateSitemapXml } from '@/lib/sitemap.xml' import { DynamicLayout } from '@/themes/theme' +import { generateRedirectJson } from '@/lib/redirect' /** * 首页布局 @@ -60,6 +61,10 @@ export async function getStaticProps(req) { generateRss(props) // 生成 generateSitemapXml(props) + if (siteConfig('UUID_REDIRECT', false, props?.NOTION_CONFIG)) { + // 生成重定向 JSON + generateRedirectJson(props) + } // 生成全文索引 - 仅在 yarn build 时执行 && process.env.npm_lifecycle_event === 'build' diff --git a/public/css/spoiler-text.css b/public/css/spoiler-text.css index 1dcd045a..ba68870b 100644 --- a/public/css/spoiler-text.css +++ b/public/css/spoiler-text.css @@ -1,15 +1,19 @@ -/* Spoiler text styles */ +/* Spoiler text styles with sharp edges */ .spoiler-text { color: transparent; /* 文字透明 */ - background-color: #808080; /* 背景为黑色 */ - border-color: #808080; - text-decoration-color: #808080; - text-emphasis-color: #808080; - border-radius: 8px; - filter: blur(1px); /* 初始模糊 */ + background-color: #000000; /* 背景为黑色 */ + border-color: #000000; /* 边框颜色 */ + text-decoration-color: #000000; /* 删除线颜色 */ + text-emphasis-color: #000000; /* 强调文字颜色 */ + filter: none; /* 移除模糊效果 */ --hide-transition: 0.3s ease-out; - transition: opacity var(--hide-transition), - filter var(--hide-transition); + transition: + color var(--hide-transition), + background-color var(--hide-transition), + border-color var(--hide-transition), + text-decoration-color var(--hide-transition), + text-emphasis-color var(--hide-transition), + opacity 0.35s cubic-bezier(.25,.46,.45,.94); /* 平滑过渡 */ } .spoiler-text:hover { @@ -19,5 +23,26 @@ text-decoration-color: inherit; text-emphasis-color: inherit; opacity: 1; /* 鼠标悬停时恢复不透明度 */ - filter: blur(0); /* 鼠标悬停时解除模糊 */ -} \ No newline at end of file +} + +/* Spoiler child elements with transition */ +.spoiler-text * { + transition: opacity 0.35s cubic-bezier(.25,.46,.45,.94); /* 子元素透明度平滑过渡 */ +} + +/* Spoiler when not hovered */ +.spoiler-text:not(:hover) { + color: transparent!important; /* 非悬停时文字透明 */ + background-color: #000000!important; /* 非悬停时背景为黑色 */ + border-color: #000000!important; /* 非悬停时边框为黑色 */ +} + +.spoiler-text:not(:hover) * { + opacity: 0!important; /* 非悬停时子元素透明 */ +} + +/* Remove border in non-hover states */ +.spoiler-text:not(:hover), +.spoiler-text:not(:hover) * { + border: none!important; +} diff --git a/themes/starter/components/CTA.js b/themes/starter/components/CTA.js index 643a0d48..2bf97e76 100644 --- a/themes/starter/components/CTA.js +++ b/themes/starter/components/CTA.js @@ -26,7 +26,7 @@ export const CTA = () => {

- {siteConfig('STARTER_CTA_DESCRIOTN')} + {siteConfig('STARTER_CTA_DESCRIPTION')}

{siteConfig('STARTER_CTA_BUTTON') && ( <> diff --git a/themes/starter/config.js b/themes/starter/config.js index 39ea4773..520e4d92 100644 --- a/themes/starter/config.js +++ b/themes/starter/config.js @@ -364,7 +364,7 @@ const CONFIG = { STARTER_CTA_ENABLE: true, STARTER_CTA_TITLE: '你还在等待什么呢?', STARTER_CTA_TITLE_2: '现在开始吧', - STARTER_CTA_DESCRIOTN: + STARTER_CTA_DESCRIPTION: '访问NotionNext的操作文档,我们提供了详细的教程,帮助你即刻搭建站点', STARTER_CTA_BUTTON: true, // 是否显示按钮 STARTER_CTA_BUTTON_URL: diff --git a/themes/theme.js b/themes/theme.js index 97fcd64a..3d89f878 100644 --- a/themes/theme.js +++ b/themes/theme.js @@ -6,7 +6,7 @@ import { useRouter } from 'next/router' import { getQueryParam, getQueryVariable, isBrowser } from '../lib/utils' // 在next.config.js中扫描所有主题 -export const { THEMES = [] } = getConfig().publicRuntimeConfig +export const { THEMES = [] } = getConfig()?.publicRuntimeConfig || {} /** * 获取主题配置 diff --git a/yarn.lock b/yarn.lock index 9ee525b4..74fd3138 100644 --- a/yarn.lock +++ b/yarn.lock @@ -299,6 +299,11 @@ resolved "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== +"@ioredis/commands@^1.1.1": + version "1.2.0" + resolved "https://mirrors.cloud.tencent.com/npm/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11" + integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg== + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -1238,6 +1243,11 @@ clone-response@^1.0.2: dependencies: mimic-response "^1.0.0" +cluster-key-slot@^1.1.0: + version "1.1.2" + resolved "https://mirrors.cloud.tencent.com/npm/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" + integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -1443,6 +1453,11 @@ delayed-stream@~1.0.0: resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== +denque@^2.1.0: + version "2.1.0" + resolved "https://mirrors.cloud.tencent.com/npm/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" + integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== + didyoumean@^1.2.2: version "1.2.2" resolved "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" @@ -2404,6 +2419,21 @@ invariant@^2.2.4: dependencies: loose-envify "^1.0.0" +ioredis@^5.4.2: + version "5.4.2" + resolved "https://mirrors.cloud.tencent.com/npm/ioredis/-/ioredis-5.4.2.tgz#ebb6f1a10b825b2c0fb114763d7e82114a0bee6c" + integrity sha512-0SZXGNGZ+WzISQ67QDyZ2x0+wVxjjUndtD8oSeik/4ajifeiRufed8fCb8QW8VMyi4MXcS+UO1k/0NGhvq1PAg== + dependencies: + "@ioredis/commands" "^1.1.1" + cluster-key-slot "^1.1.0" + debug "^4.3.4" + denque "^2.1.0" + lodash.defaults "^4.2.0" + lodash.isarguments "^3.1.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.1.0" + is-array-buffer@^3.0.4: version "3.0.4" resolved "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" @@ -2770,6 +2800,16 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://mirrors.cloud.tencent.com/npm/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== + +lodash.isarguments@^3.1.0: + version "3.1.0" + resolved "https://mirrors.cloud.tencent.com/npm/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -3604,6 +3644,18 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://mirrors.cloud.tencent.com/npm/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" + integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w== + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://mirrors.cloud.tencent.com/npm/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" + integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A== + dependencies: + redis-errors "^1.0.0" + reflect.getprototypeof@^1.0.4, reflect.getprototypeof@^1.0.6: version "1.0.8" resolved "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.8.tgz#c58afb17a4007b4d1118c07b92c23fca422c5d82" @@ -3917,6 +3969,11 @@ stacktrace-js@^2.0.2: stack-generator "^2.0.5" stacktrace-gps "^3.0.4" +standard-as-callback@^2.1.0: + version "2.1.0" + resolved "https://mirrors.cloud.tencent.com/npm/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" + integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== + std-env@^3.7.0: version "3.8.0" resolved "https://registry.npmmirror.com/std-env/-/std-env-3.8.0.tgz#b56ffc1baf1a29dcc80a3bdf11d7fca7c315e7d5" @@ -3927,16 +3984,7 @@ streamsearch@^1.1.0: resolved "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: version "4.2.3" resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -4017,14 +4065,7 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==