Skip to content
Merged
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
96 changes: 65 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,65 +1,98 @@
# Individual Web Index(网页索引)
# Individual Web Index (个人网页索引)

[![GitHub stars][stars-image]][stars-url]
[![GitHub forks][forks-image]][forks-url]
[![license GPL-3.0][license-image]][license-url]
[![commit-count-permonth][commit-image]][commit-url]
[![contributors-count][contributors-image]][contributors-url]

> “在算法编织的迷雾中,找回数字生活里的那一枚‘锚点’。”

## 1.如何部署
---

在2024年5月21日的更新之后,部署这个项目变得更加简单。您只需要复制这个[Notion页面][notion-url]作为您自己的数据库,根据需要添加网站数据,然后fork这个仓库
在信息爆炸的今天,寻找特定、高质量的网站往往如同大海捞针。我们并不缺更多的搜索结果,缺的是一份属于自己的、精准的、可控的“数字指南”

接下来,进入Vercel部署,添加两个环境变量NOTION_TOKEN和DATABASE_ID。这些分别是从[我的集成][access-url]网站获得的密钥,以及您的Notion数据库的ID,当然也不要忘记给你的数据库连接集成
**Individual Web Index** 并不是一个复杂的工具。它更像是一份数字时代的“私人黄页”。它将 Notion 的灵活性与 Next.js 的高性能结合,为你打造一个极致、纯净的书签导航系统

这样就完成了部署,可以尽情在notion数据库中添加你自己需要的网页。
---

## 2.创建的目的
## 💡 为什么在 AI 时代,我们依然需要一份“黄页”?

很久以前,互联网初期有一个叫做黄页的网站,上面索引了许多其他网站。随着时间的推移,它逐渐演变成了搜索引擎
很久以前,互联网初期有一种叫“黄页”的东西。它不依赖算法,只负责收录。随着搜索引擎的崛起,黄页逐渐淡出视野

然而,在搜索引擎中,当涉及到寻找特定网站时,结果往往缺乏准确性。因此,我仍然认为每个人都需要自己的黄页
然而,在当下日益复杂的搜索环境下,结果的准确性往往被各种权重与广告消解。当你想要寻找那个曾经惊艳过你的小众工具,或者那个常读常新的技术博客时,搜索引擎并不总是那么可靠

这个个人黄页将包含一系列经常使用的网站,以及添加标签和简短描述的功能,方便搜索和浏览。
我仍然固执地认为,**每个人都需要一份属于自己的黄页**。
- **它是私人定制的**:只收录你真正认可的、高频使用的网站。
- **它是直观高效的**:包含标签分类与简短描述,让浏览与搜索不再是负担。
- **它是毫秒级触达的**:支持全拼与简拼搜索,让你在指尖跳跃间,找到那个久违的页面。

最终,我决定将这个个人黄页命名为“个人网站索引”。
## ✨ 进化:从手动录入到云端数据库

## 3.网站迭代
这个项目经历了漫长的迭代。

从我有这个想法到现在,这个网站经历了多次迭代,URL也变化了几次。当前并且是最终的网站是[nowscott.top](https://nowscott.top)
起初,所有的网站数据都是我一行行手动输入到 JSON 文件里的。虽然那种“纯手工”的纯粹感让人着迷,但随着收藏的增加,维护成本也变得日益臃肿。每添加一个新站点,都意味着要打开编辑器、修改代码、重新部署……这种繁琐逐渐背离了“工具应服务于人”的初衷

在最新的 **v3.0.0** 版本中,我们进行了一次彻底的现代化重构。我们引入了 **Tailwind CSS v4**,重新设计了全站 UI,打造了极具现代感的“深海蓝”深色模式与高对比度的胶囊式布局,让信息的呈现更加高效且美观
后来,我偶然在 B 站看到了一位 Up 主的分享:[用 Notion 当数据库写一个简单的 API][bilibili-url]。那一刻,我意识到 Notion 才是这个项目最完美的“后台”——它既有数据库的结构化,又有笔记软件的易用性

起初,数据文件是直接手动输入的。而在最新版本中,我们利用 Notion API 将数据存储在 Notion 的数据库中,并配合全新的前端架构实现了极致的访问速度
目前的 **v3.0.0** 版本,正式实现了这一构想。我们利用 Notion API 将数据彻底云端化,并配合全新的前端架构,实现了极致的访问速度。这意味着,我只需在手机或电脑的 Notion App 里随手一记,几秒钟后,我的个人黄页就会自动更新

## 4.如何使用Notion API
(从Notion获取数据的代码未包含在此项目中。)
在追求效率的同时,我们也没有忽略视觉的表达。我们引入了 **Tailwind CSS v4**,重新设计了全站 UI。深色模式下,那抹“深海蓝”不仅是为了视觉的舒适,更是为了在喧嚣的互联网中提供一丝静谧感。高对比度的胶囊式布局,让信息的呈现更加高效且美观。

这个想法起源于Bilibili上一个上传者的视频,下面提供链接:
当前的官方站点也终于落定在:[0211120.xyz](https://0211120.xyz)。

[【S1E3】用Notion当数据库写一个简单的API](https://www.bilibili.com/video/BV1gF411E7pV/?share_source=copy_web&vd_source=98c7014c35363c157a4fba4929dbda77)
## 📓 关于 Notion API 的集成

在这个视频中,我学习到了如何使用Notion API从Notion获取数据并在网页上显示。如果你有任何问题,可以观看视频了解更多信息
正如前面提到的,这个想法起源于 B 站 Up 主的启发。虽然项目本身专注于前端展示,但其核心驱动力来自于 Notion 的开放接口

总而言之,我利用 Notion API 从 Notion 获取数据并在前端展示。虽然 Notion API 本身响应时间可能较长,但我通过下文提到的优化手段彻底解决了这个问题。
> **参考资源**:[【S1E3】用 Notion 当数据库写一个简单的 API][bilibili-url]

## 5.一些优化
在这个视频中,我系统地学习了如何将 Notion 作为一个“无头数据库”(Headless CMS)来驱动网页内容。虽然从 Notion 获取数据的底层代码属于项目的“心脏”,但我更愿意分享这种将“笔记软件”转化为“数据引擎”的思维方式。

我无法改变Notion API的访问速度,但我可以改变获取数据的方式。在 **v3.0.0** 中,我们实现了 **“双层缓存机制”**:
尽管 Notion API 的原生响应速度有时并不理想,但我通过下文提到的缓存机制彻底解决了这一瓶颈。

- **服务器端内存缓存**:通过在服务器端实现持久化的内存缓存,将原本 7-8 秒的 Notion API 延迟降低到了毫秒级(在开发模式和 Vercel ISR 中均生效)。
## ⚡️ 速度与激情的“双层缓存”

虽然 Notion API 让数据管理变得优雅,但它的原生响应速度一直是一个让人头疼的短板——有时甚至需要等待数秒才能返回结果。

**我无法改变 Notion API 的访问速度,但我可以改变获取数据的方式。**

为了不让等待消磨掉寻找灵感的兴致,在 **v3.0.0** 中,我们实施了深度的技术优化,通过所谓的“双层缓存机制”彻底解决了这一瓶颈:

- **服务器端内存缓存**:通过在服务器端实现持久化的内存缓存,我们将原本长达 7-8 秒的 Notion API 延迟降低到了**毫秒级**。这一优化在开发模式和 Vercel ISR 中均已生效,确保了数据的即时性与访问的高性能。
- **客户端状态管理 (StatsContext)**:利用 React Context 在页面间共享数据,实现主页与关于页之间的“无缝、无刷新”即时导航。
- **🔍 智能搜索**:由 `tiny-pinyin` 驱动的拼音搜索功能。它支持中文名称与标签的全拼/简拼检索,让你能通过拼音快速定位到目标站点。
- **🎨 自适应图标**:全新的动态 SVG Favicon。它能随系统主题颜色实时变换,在细节处保持视觉的绝对统一。

## 🚀 如何搭建你的数字指南针?

自 2024 年 5 月 21 日起,部署这个项目变得像呼吸一样自然:

1. **准备数据库**:复制这个 [Notion 页面模板][notion-url] 作为你自己的数据库,按需添加网站数据。
2. **复刻仓库**:Fork 本仓库到你的 GitHub 账号。
3. **Vercel 部署**:
- 添加环境变量 `NOTION_TOKEN`(来自 [我的集成][access-url])和 `DATABASE_ID`。
- **别忘了**:在 Notion 页面中给你的数据库连接该集成,确保 API 能够正常调用。

此外,我们还引入了:
- **智能搜索**:由 `tiny-pinyin` 驱动的拼音搜索功能,让你能通过拼音快速找到中文标签和网站。
- **自适应图标**:全新的动态 SVG Favicon,能随主题颜色实时变换,保持视觉统一。
### 📊 访问统计 (可选)
如果你还希望开启**全站访问量统计**,只需在 [Turso][turso-url] 创建一个免费数据库,并配置 `TURSO_DATABASE_URL` 与 `TURSO_AUTH_TOKEN`。若未配置,该功能将静默隐藏,绝不打扰。

## 6.最后的说明
---

总之,这个项目是一个基于网络的个人网站索引,它通过深度优化解决了访问速度慢的问题,并确保了数据及时更新。这些优化使修改变得更加容易,并提供了更高效、无缝的用户体验。感谢您对这个项目的兴趣。
## 📖 写在最后

如果您对这个项目有任何问题或询问,可以通过电子邮件联系我:[nowscott@qq.com](mailto:nowscott@qq.com)
这个项目是我对“个人数字空间”的一次小小探索。在今天这个信息过载的时代,拥有一个能够完全掌控、响应迅速且美感在线的个人书签系统,其实是一件挺奢侈的事情。

**Individual Web Index** 并不是为了取代任何强大的工具,它只是为了在浩瀚的互联网海洋中,为你留下一处能够随时停靠的避风港。

如果你也曾为找不到那个收藏已久的网址而焦虑,或者厌倦了被搜索引擎牵着鼻子走,或许你可以试试搭建这份属于你自己的“网页索引”。愿这个小工具能像一阵清风,为你繁忙的数字生活带来一丝清爽。

感谢每一位关注这个项目的你。如果你有任何问题、想法,或者只是单纯地想聊聊关于数字生活的心得,欢迎通过以下方式找到我:

- **Email**: [nowscott@qq.com](mailto:nowscott@qq.com)
- **示例站点**: [0211120.xyz](https://0211120.xyz)

---

[![Star History Chart][stars-history-image]][stars-url]

Expand All @@ -76,10 +109,11 @@
[contributors-url]:https://github.com/NowScott/IndWebIndex/graphs/contributors "贡献者"
[contributors-image]:https://img.shields.io/github/contributors/NowScott/IndWebIndex

[web-url]:https://www.nowscott.top "中文网页"
[web-url]:https://0211120.xyz "官方示例"
[web-image]:https://img.shields.io/badge/%E7%BD%91%E9%A1%B5%E9%A2%84%E8%A7%88-%E4%B8%AD%E6%96%87-blue
[stars-history-image]:https://api.star-history.com/svg?repos=NowScott/IndWebIndex&type=Date

[notion-url]:https://nowscott.notion.site/aef09fbce63649cba8b9269374dbb641?v=36b6f2625b8e4e5faf9f68e1284bd2bc&pvs=74

[access-url]:https://www.notion.so/my-integrations
[turso-url]:https://turso.tech "Turso Database"
[bilibili-url]:https://www.bilibili.com/video/BV1gF411E7pV/?share_source=copy_web&vd_source=98c7014c35363c157a4fba4929dbda77
18 changes: 10 additions & 8 deletions components/Footer.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,17 @@ const Footer = () => {
<span>投稿网页</span>
</a>

<div className='flex items-center gap-1.5 text-sky-500 dark:text-zinc-300'>
<HiOutlineCursorClick className='w-4 h-4' />
<span className="whitespace-nowrap">访问量:</span>
<span className='inline-flex items-center justify-start min-w-[3.8rem]'>
<span className='text-sky-600 dark:text-zinc-100 font-extrabold'>
<CountUp end={stats.visitCount} duration={1200} isPending={stats.visitCount === null} />
{stats.visitCount !== undefined && (
<div className='flex items-center gap-1.5 text-sky-500 dark:text-zinc-300'>
<HiOutlineCursorClick className='w-4 h-4' />
<span className="whitespace-nowrap">访问量:</span>
<span className='inline-flex items-center justify-start min-w-[3.8rem]'>
<span className='text-sky-600 dark:text-zinc-100 font-extrabold'>
<CountUp end={stats.visitCount} duration={1200} isPending={stats.visitCount === null} />
</span>
</span>
</span>
</div>
</div>
)}

<div className='flex items-center gap-1.5 text-sky-500 dark:text-zinc-300'>
<HiOutlineCode className='w-4 h-4' />
Expand Down
19 changes: 16 additions & 3 deletions contexts/StatsContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,18 @@ export const StatsProvider = ({ children }) => {
try {
// 每次刷新页面(即 StatsProvider 挂载时)都发送 POST 请求以增加访问量
const response = await fetch('/api/visit-count', { method: 'POST' });
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);

if (!response.ok) {
console.warn(`[StatsContext] Visit count API returned status: ${response.status}`);
updateStats({ visitCount: undefined });
return;
}

const data = await response.json();
if (data && data.enabled === false) {
updateStats({ visitCount: undefined });
return;
}
if (data && typeof data.count !== 'undefined') {
updateStats({ visitCount: data.count });
}
Expand All @@ -38,9 +47,13 @@ export const StatsProvider = ({ children }) => {
try {
const getRes = await fetch('/api/visit-count', { method: 'GET' });
const getData = await getRes.json();
updateStats({ visitCount: getData.count });
if (getData && getData.enabled === false) {
updateStats({ visitCount: undefined });
} else {
updateStats({ visitCount: getData.count });
}
} catch (e) {
updateStats({ visitCount: 0 });
updateStats({ visitCount: undefined });
}
}
};
Expand Down
5 changes: 4 additions & 1 deletion lib/turso.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ const url = process.env.TURSO_DATABASE_URL || process.env.indwebindex_TURSO_DATA
const authToken = process.env.TURSO_AUTH_TOKEN || process.env.indwebindex_TURSO_AUTH_TOKEN;

if (!url || !authToken) {
console.error('Turso configuration missing. Please check TURSO_DATABASE_URL and TURSO_AUTH_TOKEN in environment variables.');
// 访问量功能是可选的,若缺失环境变量则静默退出
if (process.env.NODE_ENV === 'development') {
console.warn('Turso configuration missing. Visit count feature will be disabled.');
}
}

export const turso = createClient({
Expand Down
16 changes: 7 additions & 9 deletions pages/api/visit-count.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@ export default async function handler(req, res) {

// 检查 Turso 客户端是否已配置
if (!process.env.TURSO_DATABASE_URL && !process.env.indwebindex_TURSO_DATABASE_URL) {
console.error('[API] Turso database URL missing in environment');
return res.status(500).json({ error: 'Database configuration missing (URL)' });
return res.status(200).json({ enabled: false, message: 'Turso database URL missing' });
}
if (!process.env.TURSO_AUTH_TOKEN && !process.env.indwebindex_TURSO_AUTH_TOKEN) {
console.error('[API] Turso auth token missing in environment');
return res.status(500).json({ error: 'Database configuration missing (Token)' });
return res.status(200).json({ enabled: false, message: 'Turso auth token missing' });
}

try {
Expand Down Expand Up @@ -57,12 +55,12 @@ export default async function handler(req, res) {
return res.status(405).end(`Method ${req.method} Not Allowed`);
} catch (error) {
console.error('[API] Error in visit-count (Turso):', error);
// 在生产环境提供更详细的错误原因
const errorDetail = error.message || String(error);
return res.status(500).json({
// 如果数据库执行报错,也将功能标记为禁用,避免前端报错
return res.status(200).json({
enabled: false,
count: 0,
error: 'Internal Server Error',
detail: process.env.NODE_ENV === 'development' ? errorDetail : undefined
error: 'Database operation failed',
detail: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
}
Loading