Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions blog.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,11 @@ const BLOG = {
process.env.NEXT_PUBLIC_GREETING_WORDS ||
'Hi,我是一个程序员, Hi,我是一个打工人,Hi,我是一个干饭人,欢迎来到我的博客🎉',

// uuid重定向至 slug
UUID_REDIRECT: process.env.UUID_REDIRECT || false
// uuid重定向至 slug(不支持Notion配置!)
UUID_REDIRECT: process.env.UUID_REDIRECT || false,
REDIRECT_CACHE_KEY:
process.env.REDIRECT_CACHE_KEY ||
`uuid_slug_map_${process.env.NOTION_PAGE_ID}`
}

module.exports = BLOG
6 changes: 5 additions & 1 deletion conf/dev.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ module.exports = {
BACKGROUND_LIGHT: '#eeeeee', // use hex value, don't forget '#' e.g #fffefc
BACKGROUND_DARK: '#000000', // use hex value, don't forget '#'

// Redis 缓存数据库地址
// Redis 缓存数据库地址 (警告:缓存时间使用了NEXT_REVALIDATE_SECOND,且无法从Notion获取)
REDIS_URL: process.env.REDIS_URL || '',

// UpStash Redis 缓存数据库地址(支持RESTful API调用,中间件可用)
UPSTASH_REDIS_URL: process.env.UPSTASH_REDIS_URL || '',
UPSTASH_REDIS_TOKEN: process.env.UPSTASH_REDIS_TOKEN || '',

ENABLE_CACHE:
process.env.ENABLE_CACHE ||
process.env.npm_lifecycle_event === 'build' ||
Expand Down
4 changes: 2 additions & 2 deletions lib/cache/cache_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import MemoryCache from './memory_cache'
import RedisCache from './redis_cache'

// 配置是否开启Vercel环境中的缓存,因为Vercel中现有两种缓存方式在无服务环境下基本都是无意义的,纯粹的浪费资源
const enableCacheInVercel =
export const isNotVercelProduction =
process.env.npm_lifecycle_event === 'build' ||
process.env.npm_lifecycle_event === 'export' ||
!BLOG['isProd']
Expand Down Expand Up @@ -77,7 +77,7 @@ export async function getDataFromCache(key, force) {
* @returns
*/
export async function setDataToCache(key, data, customCacheTime) {
if (!enableCacheInVercel || !data) {
if (!data) {
return
}
// console.trace('[API-->>缓存写入]:', key)
Expand Down
23 changes: 19 additions & 4 deletions lib/cache/local_file_cache.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import fs from 'fs'
import { isNotVercelProduction } from '@/lib/cache/cache_manager'

const path = require('path')
// 文件缓存持续10秒
const cacheInvalidSeconds = 1000000000 * 1000
// 文件名
const jsonFile = path.resolve('./data.json')

export async function getCache (key) {
export async function getCache(key) {
if (!isNotVercelProduction) {
return
}
const exist = await fs.existsSync(jsonFile)
if (!exist) return null
const data = await fs.readFileSync(jsonFile)
Expand All @@ -19,7 +23,9 @@ export async function getCache (key) {
return null
}
// 缓存超过有效期就作废
const cacheValidTime = new Date(parseInt(json[key + '_expire_time']) + cacheInvalidSeconds)
const cacheValidTime = new Date(
parseInt(json[key + '_expire_time']) + cacheInvalidSeconds
)
const currentTime = new Date()
if (!cacheValidTime || cacheValidTime < currentTime) {
return null
Expand All @@ -33,15 +39,21 @@ export async function getCache (key) {
* @param data
* @returns {Promise<null>}
*/
export async function setCache (key, data) {
export async function setCache(key, data) {
if (!isNotVercelProduction) {
return
}
const exist = await fs.existsSync(jsonFile)
const json = exist ? JSON.parse(await fs.readFileSync(jsonFile)) : {}
json[key] = data
json[key + '_expire_time'] = new Date().getTime()
fs.writeFileSync(jsonFile, JSON.stringify(json))
}

export async function delCache (key) {
export async function delCache(key) {
if (!isNotVercelProduction) {
return
}
const exist = await fs.existsSync(jsonFile)
const json = exist ? JSON.parse(await fs.readFileSync(jsonFile)) : {}
delete json.key
Expand All @@ -53,6 +65,9 @@ export async function delCache (key) {
* 清理缓存
*/
export async function cleanCache() {
if (!isNotVercelProduction) {
return
}
const json = {}
fs.writeFileSync(jsonFile, JSON.stringify(json))
}
Expand Down
10 changes: 10 additions & 0 deletions lib/cache/memory_cache.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
import cache from 'memory-cache'
import BLOG from 'blog.config'
import { isNotVercelProduction } from '@/lib/cache/cache_manager'

const cacheTime = BLOG.isProd ? 10 * 60 : 120 * 60 // 120 minutes for dev,10 minutes for prod

export async function getCache(key, options) {
if (!isNotVercelProduction) {
return
}
return await cache.get(key)
}

export async function setCache(key, data, customCacheTime) {
if (!isNotVercelProduction) {
return
}
await cache.put(key, data, (customCacheTime || cacheTime) * 1000)
}

export async function delCache(key) {
if (!isNotVercelProduction) {
return
}
await cache.del(key)
}

Expand Down
6 changes: 3 additions & 3 deletions lib/cache/redis_cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ 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) : {}
export const redisClient = BLOG.REDIS_URL ? new Redis(BLOG.REDIS_URL) : null

const cacheTime = Math.trunc(
export const redisCacheTime = Math.trunc(
siteConfig('NEXT_REVALIDATE_SECOND', BLOG.NEXT_REVALIDATE_SECOND) * 1.5
)

Expand All @@ -23,7 +23,7 @@ export async function setCache(key, data, customCacheTime) {
key,
JSON.stringify(data),
'EX',
customCacheTime || cacheTime
customCacheTime || redisCacheTime
)
} catch (e) {
console.error('redisClient写入失败 ' + e)
Expand Down
46 changes: 46 additions & 0 deletions lib/cache/upstash_redis_cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import BLOG from '@/blog.config'
import { Redis } from '@upstash/redis'

export const upstashRedisClient =
BLOG.UPSTASH_REDIS_URL && BLOG.UPSTASH_REDIS_TOKEN
? new Redis({
url: BLOG.UPSTASH_REDIS_URL,
token: BLOG.UPSTASH_REDIS_TOKEN
})
: null

export const upstashRedisCacheTime = Math.trunc(
BLOG.NEXT_REVALIDATE_SECOND * 1.5
)

export async function getCache(key) {
try {
const data = await upstashRedisClient.get(key)
return data ? JSON.parse(data) : null
} catch (e) {
console.error('upstash 读取失败 ' + e)
}
}

export async function setCache(key, data, customCacheTime) {
try {
await upstashRedisClient.set(
key,
JSON.stringify(data),
'EX',
customCacheTime || upstashRedisCacheTime
)
} catch (e) {
console.error('upstash 写入失败 ' + e)
}
}

export async function delCache(key) {
try {
await upstashRedisClient.del(key)
} catch (e) {
console.error('upstash 删除失败 ' + e)
}
}

export default { getCache, setCache, delCache }
1 change: 0 additions & 1 deletion lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ 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) {
Expand Down
27 changes: 22 additions & 5 deletions lib/redirect.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
import fs from 'fs'
import { redisClient } from '@/lib/cache/redis_cache'
import { upstashRedisClient } from '@/lib/cache/upstash_redis_cache'
import BLOG from '@/blog.config'

export function generateRedirectJson({ allPages }) {
export async 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)
if (upstashRedisClient) {
try {
await upstashRedisClient.hset(BLOG.REDIRECT_CACHE_KEY, uuidSlugMap)
} catch (e) {
console.warn('写入 upstashRedis 失败', e)
}
} else if (redisClient) {
try {
await redisClient.hset(BLOG.REDIRECT_CACHE_KEY, uuidSlugMap)
} catch (e) {
console.warn('写入Redis失败', e)
}
} else {
try {
fs.writeFileSync('./public/redirect.json', JSON.stringify(uuidSlugMap))
} catch (error) {
console.warn('无法写入文件', error)
}
}
}
11 changes: 11 additions & 0 deletions lib/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,17 @@ export function checkStartWithHttp(str) {
}
}

// 检查一个字符串是否UUID https://ihateregex.io/expr/uuid/
export function checkStrIsUuid(str) {
if (!str) {
return false
}
// 使用正则表达式进行匹配
const regex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/
return regex.test(str)
}


// 检查一个字符串是否notionid : 32位,仅由数字英文构成
export function checkStrIsNotionId(str) {
if (!str) {
Expand Down
72 changes: 55 additions & 17 deletions middleware.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
import { NextRequest, NextResponse } from 'next/server'
import { checkStrIsNotionId, getLastPartOfUrl } from '@/lib/utils'
import {
checkStrIsNotionId,
checkStrIsUuid,
getLastPartOfUrl
} from '@/lib/utils'
import { idToUuid } from 'notion-utils'
import BLOG from './blog.config'
import { upstashRedisClient } from '@/lib/cache/upstash_redis_cache'

/**
* Clerk 身份验证中间件
Expand Down Expand Up @@ -36,26 +41,59 @@ const isTenantAdminRoute = createRouteMatcher([
const noAuthMiddleware = async (req: NextRequest, ev: any) => {
// 如果没有配置 Clerk 相关环境变量,返回一个默认响应或者继续处理请求
if (BLOG['UUID_REDIRECT']) {
let redirectJson: Record<string, string> = {}
try {
const response = await fetch(`${req.nextUrl.origin}/redirect.json`)
if (response.ok) {
redirectJson = (await response.json()) as Record<string, string>
}
} 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)
if (checkStrIsUuid(lastPart)) {
let redirectJson: Record<string, string | null> = {}
if (upstashRedisClient) {
const redisResult = (await upstashRedisClient.hget(
BLOG.REDIRECT_CACHE_KEY,
lastPart
)) as string
redirectJson = {
[lastPart]: redisResult
}
} else if (BLOG.REDIS_URL) {
try {
const redisResponse = await fetch(
`${req.nextUrl.origin}/api/redirect`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
lastPart: lastPart
})
}
)
const redisResult = await redisResponse?.json()
redirectJson = {
[lastPart]: redisResult?.data
}
} catch (e) {
console.warn('读取Redis失败', e)
}
} else {
try {
const response = await fetch(`${req.nextUrl.origin}/redirect.json`)
if (response.ok) {
redirectJson = (await response.json()) as Record<string, string>
}
} catch (err) {
console.error('Error fetching static file:', err)
}
}
if (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()
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@clerk/nextjs": "^5.1.5",
"@headlessui/react": "^1.7.15",
"@next/bundle-analyzer": "^12.1.1",
"@upstash/redis": "^1.34.3",
"@vercel/analytics": "^1.0.0",
"algoliasearch": "^4.18.0",
"axios": "^1.7.2",
Expand Down
17 changes: 17 additions & 0 deletions pages/api/redirect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { redisCacheTime, redisClient } from '@/lib/cache/redis_cache'
import BLOG from '@/blog.config'

export default async function handler(req, res) {
const { lastPart } = req.body
try {
const result =
(await redisClient.hget(BLOG.REDIRECT_CACHE_KEY, lastPart)) || null
res.setHeader(
'Cache-Control',
`public, max-age=${redisCacheTime}, stale-while-revalidate=${redisCacheTime / 6}`
)
res.status(200).json({ status: 'success', data: result })
} catch (error) {
res.status(400).json({ status: 'error', message: 'failed!', error })
}
}
2 changes: 1 addition & 1 deletion pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export async function getStaticProps(req) {
generateRss(props)
// 生成
generateSitemapXml(props)
if (siteConfig('UUID_REDIRECT', false, props?.NOTION_CONFIG)) {
if (BLOG['UUID_REDIRECT']) {
// 生成重定向 JSON
generateRedirectJson(props)
}
Expand Down
Loading