03 · 导航页与客户端渲染
书签导航页 /bookmarks/nav/ 目标是:加载快、可搜索、分区清晰、与 Starlight 主题一致,但不依赖 docs 布局。
---// src/bookmarks/nav/entry.astro(integrations/bookmarks-admin injectRoute)import { NavBookmarksPage } from '@/bookmarks/nav/NavBookmarksPage.tsx'import { serializeBookmarkSectionsForPage } from '@/bookmarks/shared/data/page-data'import { getBookmarkSections } from '@/bookmarks/shared/data/queries'import themeInitScript from '@/theme/scripts/init.inline.js?raw'import '@/bookmarks/nav/styles/bookmarks-page.css'
const sections = await getBookmarkSections()const sectionsJson = serializeBookmarkSectionsForPage(sections)---
<!doctype html><html lang="zh-CN"> <head> <meta charset="UTF-8" /> <title>书签导航 · wwlight</title> <link rel="icon" href="/favicon.svg" type="image/svg+xml" data-wwlight-favicon /> <script is:inline set:html={themeInitScript} /> </head> <body> <script type="application/json" id="bookmarks-sections-data" set:html={sectionsJson} /> <NavBookmarksPage client:only="react" /> </body></html>与 Starlight 页面的差异:
- 独立
<html>,无 sidebar / TOC - 构建时在服务端查 DB,数据通过
<script type="application/json">注入 - React 用
client:only完全客户端挂载(无需 SSR hydration 一致性)
JSON 注水与 XSS 防护
Section titled “JSON 注水与 XSS 防护”export function serializeBookmarkSectionsForPage(sections): string { return JSON.stringify(sections) .replace(/<\//g, "\\u003c/") .replace(/<!--/g, "\\u003c!--");}
export function readBookmarkSectionsFromPage(): BookmarkSectionData[] { const el = document.getElementById("bookmarks-sections-data"); return JSON.parse(el.textContent);}转义 < 防止书签标题/描述里出现 </script> 打断 HTML 解析。React 端用 useState(readBookmarkSectionsFromPage) 首屏读取,避免二次请求。
NavBookmarksPage└── ThemeProvider(@/theme,与文档站同一 storage 键) └── NavBookmarks ├── BookmarkPageHeader + NavPageActions │ └── ColorThemePicker / 返回博客 / 管理端(BookmarkSettingsIcon) ├── BookmarkStatsCards + 搜索框 └── Tabs(按 Section 切换) └── NavSectionPanel └── NavBookmarkCardGroup └── NavBookmarkCard(.bookmark-card) └── BackToTopfilterBookmarkSections() 对 title、description、url、card 名、section 名做大小写不敏感匹配;无结果的 section 整组隐藏。搜索时 clampSelectedSection 防止 Tab 索引越界。
head 内联 @/theme/scripts/init.inline.js?raw,在 CSS 前写入 data-theme / data-color-*,避免 FOUC。ThemeProvider(@/theme/components/color-mode/Provider)mount 时 syncSiteThemeFromStorage(),并订阅 subscribeSiteThemeStorage——与 Starlight 顶栏定制器读写同一套 wwlight:color-mode 等键(见 主题系列)。
顶栏 NavPageActions 挂 ColorThemePicker(variant=bookmarks),管理端入口用 BookmarkSettingsIcon 而非通用 Cog 图标。
样式链:
bookmarks-page.css → bookmarks-nav-app.css → bookmarks-theme-shared.css(Tailwind + shadcn-theme + @/theme/styles/index.css) → bookmarks-card.css(.bookmark-card 读 --card / --border)卡片勿用 border-border/50 等 opacity 叠层,暗色下边框会几乎不可见。
搭一个最小导航页
Section titled “搭一个最小导航页”-
新建 demo 页或复制
src/bookmarks/nav/entry.astro内容 -
新建极简 React 组件,只渲染 section 数量:
import { readBookmarkSectionsFromPage } from "@/bookmarks/shared/data/page-data";export function Demo() {const sections = readBookmarkSectionsFromPage();return <p>{sections.length} 个分区</p>;} -
client:only="react"挂载,访问/demo-bookmarks/验证注水 -
确认 view-source 里能看到 JSON script 标签
与管理端 UI 的复用
Section titled “与管理端 UI 的复用”导航页与管理端共享:
BookmarkSectionData类型filterBookmarkSections/clampSelectedSection等工具.bookmark-card(shared/styles/bookmarks-card.css)- shadcn 风格
Input、Tabs、Card
管理端在此基础上叠加编辑、拖拽、对话框——见 05 · 管理端 UI。