前言
之前一直用的是Notionnext搭建的博客,部署在免费的vercel,notion更新内容,博客自动更新,确实挺方便的,但是发现问题:它消耗vercel相关额度太高了,特别是ISR writes,都给我用超了!再加上前几天notion api啥的更新,导致博客首页打不开,突然感觉太麻烦,动不动就加载失败了,于是寻找其他方案。
所以就找到了:nextjs-notion-blog-starter-kit,样式极简,也没啥多余的功能,总之不错!
❓起因
随后用vercel部署了这个项目几天,但是发现问题:
- 第一次部署直接报错了:
Error: The Edge Function "api/social-image" size is 1.04 MB and your plan size limit is 1 MB.
- 页面链接后缀是页面标题🤡打开直接404
- 发现其
ISR writes消耗几乎赶上了Notionnext,一天消耗三四千次🤡
😠这怎么能行?于是就问了豆包得到解决方案😋
✅解决和优化
🐣解决第一次部署报错
找到
social-image.tsx文件,然后注释掉这一句即可 export const runtime = 'edge'位置可以在下面代码里看到,有注释
点击展开查看代码
import ky from 'ky' import { type NextApiRequest, type NextApiResponse } from 'next' import { ImageResponse } from 'next/og' import { type PageBlock } from 'notion-types' import { getBlockIcon, getBlockTitle, getBlockValue, getPageProperty, isUrl, parsePageId } from 'notion-utils' import * as libConfig from '@/lib/config' import interSemiBoldFont from '@/lib/fonts/inter-semibold' import { mapImageUrl } from '@/lib/map-image-url' import { notion } from '@/lib/notion-api' import { type NotionPageInfo, type PageError } from '@/lib/types' // 👉 就删掉这一行!解决1MB体积报错 // export const runtime = 'edge' export default async function OGImage( req: NextApiRequest, res: NextApiResponse ) { const { searchParams } = new URL(req.url!) const pageId = parsePageId( searchParams.get('id') || libConfig.rootNotionPageId ) if (!pageId) { return new Response('Invalid notion page id', { status: 400 }) } const pageInfoOrError = await getNotionPageInfo({ pageId }) if (pageInfoOrError.type === 'error') { return res.status(pageInfoOrError.error.statusCode).send({ error: pageInfoOrError.error.message }) } const pageInfo = pageInfoOrError.data return new ImageResponse( <div style={{ position: 'relative', width: '100%', height: '100%', display: 'flex', flexDirection: 'column', backgroundColor: '#1F2027', alignItems: 'center', justifyContent: 'center', color: 'black' }} > {pageInfo.image && ( <img src={pageInfo.image} style={{ position: 'absolute', width: '100%', height: '100%', objectFit: 'cover' }} /> )} <div style={{ position: 'relative', width: 900, height: 465, display: 'flex', flexDirection: 'column', border: '16px solid rgba(0,0,0,0.3)', borderRadius: 8, zIndex: '1' }} > <div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'space-around', backgroundColor: '#fff', padding: 24, alignItems: 'center', textAlign: 'center' }} > {pageInfo.detail && ( <div style={{ fontSize: 32, opacity: 0 }}>{pageInfo.detail}</div> )} <div style={{ fontSize: 70, fontWeight: 700, fontFamily: 'Inter' }} > {pageInfo.title} </div> {pageInfo.detail && ( <div style={{ fontSize: 32, opacity: 0.6 }}>{pageInfo.detail}</div> )} </div> </div> {pageInfo.authorImage && ( <div style={{ position: 'absolute', top: 47, left: 104, height: 128, width: 128, display: 'flex', borderRadius: '50%', border: '4px solid #fff', zIndex: '5' }} > <img src={pageInfo.authorImage} style={{ width: '100%', height: '100%' }} /> </div> )} </div>, { width: 1200, height: 630, fonts: [ { name: 'Inter', data: interSemiBoldFont, style: 'normal', weight: 700 } ] } ) } export async function getNotionPageInfo({ pageId }: { pageId: string }): Promise< | { type: 'success'; data: NotionPageInfo } | { type: 'error'; error: PageError } > { const recordMap = await notion.getPage(pageId) const keys = Object.keys(recordMap?.block || {}) const block = getBlockValue(recordMap?.block?.[keys[0]!]) if (!block) { throw new Error('Invalid recordMap for page') } const blockSpaceId = block.space_id if ( blockSpaceId && libConfig.rootNotionSpaceId && blockSpaceId !== libConfig.rootNotionSpaceId ) { return { type: 'error', error: { statusCode: 400, message: `Notion page "${pageId}" belongs to a different workspace.` } } } const isBlogPost = block.type === 'page' && block.parent_table === 'collection' const title = getBlockTitle(block, recordMap) || libConfig.name const imageCoverPosition = (block as PageBlock).format?.page_cover_position ?? libConfig.defaultPageCoverPosition const imageObjectPosition = imageCoverPosition ? `center ${(1 - imageCoverPosition) * 100}%` : undefined const imageBlockUrl = mapImageUrl( getPageProperty<string>('Social Image', block, recordMap) || (block as PageBlock).format?.page_cover, block ) const imageFallbackUrl = mapImageUrl(libConfig.defaultPageCover, block) const blockIcon = getBlockIcon(block, recordMap) const authorImageBlockUrl = mapImageUrl( blockIcon && isUrl(blockIcon) ? blockIcon : undefined, block ) const authorImageFallbackUrl = mapImageUrl(libConfig.defaultPageIcon, block) const [authorImage, image] = await Promise.all([ getCompatibleImageUrl(authorImageBlockUrl, authorImageFallbackUrl), getCompatibleImageUrl(imageBlockUrl, imageFallbackUrl) ]) const author = getPageProperty<string>('Author', block, recordMap) || libConfig.author const publishedTime = getPageProperty<number>('Published', block, recordMap) const datePublished = publishedTime ? new Date(publishedTime) : undefined const date = isBlogPost && datePublished ? `${datePublished.toLocaleString('en-US', { month: 'long' })} ${datePublished.getFullYear()` : undefined const detail = date || author || libConfig.domain const pageInfo: NotionPageInfo = { pageId, title, image, imageObjectPosition, author, authorImage, detail } return { type: 'success', data: pageInfo } } async function isUrlReachable( url: string | undefined | null ): Promise<boolean> { if (!url) { return false } try { await ky.head(url) return true } catch { return false } } async function getCompatibleImageUrl( url: string | undefined | null, fallbackUrl: string | undefined | null ): Promise<string | undefined> { const image = (await isUrlReachable(url)) ? url : fallbackUrl if (image) { const imageUrl = new URL(image) if (imageUrl.host === 'images.unsplash.com') { if (!imageUrl.searchParams.has('w')) { imageUrl.searchParams.set('w', '1200') imageUrl.searchParams.set('fit', 'max') return imageUrl.toString() } } } return image ?? undefined }
改完之后, 重新部署一次,成功了😋🎉
🐣修改页面链接后缀格式
☯︎首先找到
site.config.ts 文件,你就只需要加这一行:includeNotionIdInUrls: true,
我放在大概这个位置了(供参考):
import { siteConfig } from './lib/site-config' export default siteConfig({ // the site's root Notion page (required) rootNotionPageId: '#', // if you want to restrict pages to a single notion workspace (optional) // (this should be a Notion ID; see the docs for how to extract this) rootNotionSpaceId: null, // basic site info (required) name: '酷小呵笔记', domain: 'notes.kuhehe.top', author: '酷小呵', // 👉 开启页面ID路由(只有子页面带ID,首页不带) includeNotionIdInUrls: true, // open graph metadata (optional) description: '酷小呵的资源笔记', // social usernames (optional) twitter: '', github: '', linkedin: '', // default notion icon and cover images for site-wide consistency (optional) defaultPageIcon: null, defaultPageCover: null, defaultPageCoverPosition: 0.5, // 你之前已经关掉了社交图片,避免1MB报错,这个很对 isPreviewImageSupportEnabled: false, isRedisEnabled: false, // map of notion page IDs to URL paths pageUrlOverrides: null, navigationStyle: 'default' })
然后找到
lib/get-canonical-page-id.ts 文件直接复制下面代码直接覆盖粘贴😋就行了
import { type ExtendedRecordMap } from 'notion-types' import { parsePageId } from 'notion-utils' import { inversePageUrlOverrides } from './config' export function getCanonicalPageId( pageId: string, recordMap: ExtendedRecordMap, { uuid = true }: { uuid?: boolean } = {} ): string | undefined { const cleanPageId = parsePageId(pageId, { uuid: false }) if (!cleanPageId) { return } const override = inversePageUrlOverrides[cleanPageId] if (override) { return override } else { // 👇 关键:这里加 { uuid: false } 就不会有横杠了 return parsePageId(pageId, { uuid: false }) ?? undefined } }
至此,链接后缀问题圆满解决!!🎉🎉🎉现在的链接后缀,就是notion页面id了!!
🐣解决ISR writes额度消耗过多
修改页面缓存过期时间
revalidate,默认过期时间是10秒,这样过了10秒有人访问页面,就要消耗ISR writes,太浪费了!!😠😋怎么改呢?👇👇
找到
pages/index.tsx 和 pages/[pageId].tsx 两个文件,进去会看(找)到这个单词:revalidate它后面的数字,就是页面缓存过期时间,默认10秒,你可以按照你的需求修改
所以我改成了:1296000 ,直接半个月!😋
这样的话,每次部署完之后,你的博客在这半个月内,不管你notion页面内容如何更新,博客上始终是不会更新的😋【这样也就完全不再消耗ISR writes额度了】
那么你可能会想:那这样的话,我还怎么实时更新网站内容??😠本来用这个就是因为实时更新才选择的
😇别急,我又问了豆包解决方案:
找到
pages/api 文件夹,在里面上传一个文件命名为:revalidate.ts内容如下,直接复制进去:
import type { NextApiRequest, NextApiResponse } from 'next' export default async function handler( req: NextApiRequest, res: NextApiResponse ) { const REVALIDATE_SECRET = '123456' // 记得改成你自己的密码 if (req.query.secret !== REVALIDATE_SECRET) { return res.status(401).json({ message: '密钥错误' }) } try { const path = req.query.path as string | undefined if (path) { // 刷新单个指定页面 await res.revalidate(path) return res.json({ status: 'success', msg: `已刷新页面: ${path}`, path }) } else { // 👉 这里是修复后的正确代码!! await res.revalidate('/') // 刷新首页 await res.revalidate('/[pageId]') // 刷新所有文章页(适配你的项目) return res.json({ status: 'success', msg: '已刷新首页 + 全部文章页面缓存' }) } } catch (err) { return res.status(500).json({ message: '刷新失败' }) } }
里面的密码,可以不用修改(比较懒),也可以改成你自己的(请随意)
该文件功能:
👉手动:刷新首页 / 刷新指定文章 / 刷新整站
这样是不是就方便多了?修改了哪个页面,就更新哪个页面的缓存,不用重新部署!!😋
刷新方法:【示例】
将你要更新的链接,编辑好输入到浏览器打开即可刷新!
你只要记住3条链接就够了,xxx是密码
1. 刷首页:
https://你的域名/api/revalidate?secret=xxx&path=/ 2. 刷单页面:
https://你的域名/api/revalidate?secret=xxx&path=/页面id 3. 刷全站(首页+所有文章):
https://你的域名/api/revalidate?secret=xxx🍎比如你的文章地址是:(xxx密码是123456)
https://xxx.vercel.app/12345678123456781234567812345678
只刷新这一个页面的链接是:
https://xxx.vercel.app/api/revalidate?secret=123456&path=/12345678123456781234567812345678
只刷新首页:
https://xxx.vercel.app/api/revalidate?secret=123456&path=/
刷新首页 + 所有文章:(整站)【没必要】
https://xxx.vercel.app/api/revalidate?secret=123456
这样,页面的更新,就变成了手动的,非常节省ISR writes额度的😋😋😋不信的可以跟着我这试一下哦😠😠
为了方便更新😋
我让豆包给我写了一个静态html放本地,功能是:输入页面id点击按钮,就直接触发页面更新😋省的亲手去编辑链接粘贴到浏览器了😇很方便

点击查看代码
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>博客缓存刷新工具</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; } body { min-height: 100vh; background: linear-gradient(135deg, #0f0f23, #1a1a2e); display: flex; align-items: center; justify-content: center; padding: 15px; } .glass-card { width: 100%; max-width: 400px; background: rgba(255, 255, 255, 0.05); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 22px; padding: 28px 22px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); } .card-title { color: #fff; font-size: 21px; font-weight: 600; text-align: center; margin-bottom: 26px; letter-spacing: 0.8px; } .input-box { display: flex; width: 100%; gap: 8px; margin-bottom: 18px; flex-wrap: nowrap; } #pageId { flex: 1; min-width: 0; padding: 12px 14px; background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 14px; color: #fff; font-size: 14px; outline: none; transition: all 0.3s ease; } #pageId:focus { border-color: rgba(130, 210, 255, 0.5); background: rgba(255, 255, 255, 0.12); } #pageId::placeholder { color: rgba(255, 255, 255, 0.4); } .btn { flex-shrink: 0; padding: 12px 12px; border: none; border-radius: 14px; background: rgba(130, 210, 255, 0.15); color: #82d2ff; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.3s ease; border: 1px solid rgba(130, 210, 255, 0.3); white-space: nowrap; } .btn:hover { background: rgba(130, 210, 255, 0.25); box-shadow: 0 0 12px rgba(130, 210, 255, 0.3); } .btn-group { display: flex; gap: 8px; margin-bottom: 22px; } .btn-home { background: rgba(166, 122, 255, 0.15); color: #a67aff; border-color: rgba(166, 122, 255, 0.3); } .btn-home:hover { background: rgba(166, 122, 255, 0.25); box-shadow: 0 0 12px rgba(166, 122, 255, 0.3); } .btn-all { background: rgba(98, 255, 174, 0.15); color: #62ffae; border-color: rgba(98, 255, 174, 0.3); } .btn-all:hover { background: rgba(98, 255, 174, 0.25); box-shadow: 0 0 12px rgba(98, 255, 174, 0.3); } .tip { text-align: center; color: rgba(255,255,255,0.5); font-size: 12px; } </style> </head> <body> <div class="glass-card"> <h3 class="card-title">博客缓存刷新工具</h3> <div class="input-box"> <input type="text" id="pageId" placeholder="输入文章页面ID"> <button class="btn" onclick="refreshSingle()">刷新单篇</button> </div> <div class="btn-group"> <button class="btn btn-home" onclick="refreshHome()">首页刷新</button> <button class="btn btn-all" onclick="refreshAll()">整站刷新</button> </div> <p class="tip">点击即后台刷新,无任何弹窗提示</p> </div> <script> const CONFIG = { domain: "https://你的域名", //改成你的域名 secret: "123456" // 👈 改成你真实的密钥 } // 静默发送请求,完全不处理返回值,不报错、不显示 function sendRequest(url) { // 用隐形图片方式GET请求,最稳、不跨域、不抛错 const img = new Image() img.src = url // 只发请求,完全不监听成功失败 setTimeout(() => { img.remove() }, 1000) } // 刷新单篇 function refreshSingle() { const pageId = document.getElementById('pageId').value.trim() if (!pageId) return const url = `${CONFIG.domain}/api/revalidate?secret=${CONFIG.secret}&path=/${encodeURIComponent(pageId)}` sendRequest(url) } // 刷新首页 function refreshHome() { const url = `${CONFIG.domain}/api/revalidate?secret=${CONFIG.secret}&path=/` sendRequest(url) } // 整站刷新 function refreshAll() { const url = `${CONFIG.domain}/api/revalidate?secret=${CONFIG.secret}` sendRequest(url) } </script> </body> </html>
😋结束
至此,我对这个博客的所有优化,全部完成了,比较满意!😋😋
我觉得这样是最节省ISR writes额度的办法了,仅供参考哈!😋
