Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat UUID redirect #3129

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 3 additions & 2 deletions blog.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ 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'
}

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
Loading