Next.js中Notion API数据获取与渲染信息处理架构问询
解决方案:分层职责+结构化转换
针对你的场景,最规范简洁的方式是把数据获取、格式转换、UI渲染拆成三层独立职责,既避免在API层生成HTML的耦合,也不让组件处理原始Notion数据的繁琐。
1. 数据获取层:只做数据拉取(/lib/notion.js)
这一层只负责调用Notion SDK获取原始数据,不处理任何格式或结构转换,保持职责单一:
import { Client } from '@notionhq/client'; const notion = new Client({ auth: process.env.NOTION_API_KEY }); // 获取单个地点+关联城市的原始数据 export async function getPlaceWithCities(placeId) { // 拉取地点详情 const place = await notion.pages.retrieve({ page_id: placeId }); // 拉取关联的城市数据(假设城市是relation类型属性) const cityIds = place.properties.Cities.relation.map(item => item.id); const cities = await Promise.all( cityIds.map(id => notion.pages.retrieve({ page_id: id })) ); return { place, cities }; }
2. 格式转换层:结构化处理Notion文本(/lib/formatters.js)
专门写工具函数,把Notion的富文本结构转换成组件易读的结构化数据(不是HTML字符串,而是保留样式标记的对象数组):
// 转换单块富文本 function formatTextBlock(block) { const { plain_text, annotations, href } = block; return { content: plain_text, styles: { bold: annotations.bold, italic: annotations.italic, strikethrough: annotations.strikethrough, underline: annotations.underline, code: annotations.code, color: annotations.color }, link: href }; } // 转换整个富文本数组 export function formatRichText(richTextArray) { return richTextArray.map(formatTextBlock); } // 封装地点数据的完整转换 export function formatPlace(rawPlace) { return { id: rawPlace.id, name: rawPlace.properties.Name.title[0].plain_text, description: formatRichText(rawPlace.properties.Description.rich_text) }; } // 封装城市数据的完整转换 export function formatCity(rawCity) { return { id: rawCity.id, name: rawCity.properties.Name.title[0].plain_text, intro: formatRichText(rawCity.properties.Intro.rich_text) }; }
3. 页面与组件层:专注渲染逻辑
页面文件(/pages/[placeId].js)
在getStaticProps里完成数据拉取+格式转换,给组件传递干净的数据:
import { getPlaceWithCities } from '../lib/notion'; import { formatPlace, formatCity } from '../lib/formatters'; import PlaceCard from '../components/PlaceCard'; export async function getStaticProps({ params }) { const { place, cities } = await getPlaceWithCities(params.placeId); return { props: { place: formatPlace(place), cities: cities.map(formatCity) }, revalidate: 86400 // 每天重新同步一次数据 }; } export default function PlacePage({ place, cities }) { return <PlaceCard place={place} cities={cities} />; }
通用富文本组件(/components/RichText.js)
专门处理结构化文本的渲染,用JSX映射样式:
export default function RichText({ textBlocks }) { return ( <span> {textBlocks.map((block, idx) => { let Element = 'span'; const classNames = []; // 根据样式选择元素或添加类名 if (block.styles.code) Element = 'code'; if (block.styles.bold) classNames.push('font-bold'); if (block.styles.italic) classNames.push('italic'); if (block.styles.strikethrough) classNames.push('line-through'); if (block.styles.underline) classNames.push('underline'); if (block.styles.color !== 'default') classNames.push(`text-${block.styles.color}`); const props = { className: classNames.join(' '), ...(block.link && { href: block.link, target: '_blank', rel: 'noopener noreferrer' }) }; return <Element key={idx} {...props}>{block.content}</Element>; })} </span> ); }
地点详情组件(/components/PlaceCard.js)
组合其他组件完成最终渲染:
import RichText from './RichText'; export default function PlaceCard({ place, cities }) { return ( <div className="place-card"> <h1>{place.name}</h1> <div className="place-description"> <RichText textBlocks={place.description} /> </div> <h2>关联城市</h2> <div className="cities-list"> {cities.map(city => ( <div key={city.id} className="city-item"> <h3>{city.name}</h3> <RichText textBlocks={city.intro} /> </div> ))} </div> </div> ); }
为什么这么做?
- 职责清晰:每一层只做一件事,新手容易理解和维护,不会把数据逻辑和UI逻辑混在一起。
- 灵活性高:结构化的文本数据可以在任何组件里复用,还能结合Tailwind、CSS Modules等任意样式方案,比直接生成HTML更灵活。
- 可扩展性强:以后要加新的文本样式或数据类型,只需要修改转换层或组件层,不用动数据获取逻辑。
内容的提问来源于stack exchange,提问作者linkyndy
相关产品推荐
相关产品推荐

