站点配置与扩展

FolioConfig、Vite plugin options 与 React slots 的公共契约。

Tomz Dang约 5 分钟

FolioConfig#

type FolioConfig = {
  title: string
  description: string
  author?: FolioAuthor | FolioAuthor[]
  logo?: string
  siteUrl?: string
  base?: string
  navigation?: {
    label: string
    href: string
    /** 额外的路由前缀,命中即视为当前页;只影响高亮,不改变渲染形态。 */
    activeMatch?: string[]
    children?: {
      label: string
      href: string
      icon?: string
      description?: string
      /** 阅读任务分组;省略时面板保持旧版的扁平结构。 */
      group?: string
    }[]
    panel?: {
      eyebrow?: string
      title?: string
      description?: string
      overviewLabel?: string
    }
  }[]
  collections?: {
    path: string
    title?: string
    description?: string
    emptyMessage?: string
  }[]
  home?: FolioHomeConfig
  footer?: string | false | FolioFooterConfig
  github?: string
}

title 与 description 是必填项。author 是生成 Article/TechArticle JSON-LD 时的默认作者;文档 Frontmatter 的 author 优先,两者都缺失时不生成作者字段。siteUrl 也是 canonical、sitemap、robots 与 feed URL 的前置条件。

本页描述当前 dev 源码。collections、home 与 author 属于尚未包含在已发布 0.1.1 中的候选契约;已发布包的能力边界以消费者契约为准。

Footer 已抽成公开的 FolioFooter 组件(包内 src/ui/footer/)。原有字符串配置保持不变;不配置时仍显示默认 Built with Folio.,设置为 false 可以隐藏页脚。需要标准门户信息层级时使用结构化配置:

footer: {
  brand: {
    name: "Folio",
    description: "Git-native 内容与门户发布引擎。",
    logo: { src: "/folio-logo.svg", darkSrc: "/folio-logo-inverse.svg", alt: "Folio" },
  },
  groups: [
    {
      title: "使用指南",
      links: [
        { label: "快速开始", href: "/guide/getting-started" },
        { label: "GitHub", href: "https://github.com/uichat-mira/folio", newTab: true },
      ],
    },
  ],
  legal: {
    copyright: "© 2026 Mira Organization",
    // 按站点真实资质填写;没有备案信息就不要添加。
    registrations: [],
    links: [],
  },
},

内部链接通过 React Router 跳转;以 / 开头的 Logo 资源路径会根据站点部署子路径解析。也可通过 FolioApp 的 slots.footer 完全替换默认页脚(自行提供 footer 语义元素)。

使用 Folio 阅读壳(docsShell)时,<FolioApp ui={{ footerOnDocs: false }} /> 会同时隐藏全站 Footer 和正文 articleFooter 插槽。它既适用于 type: doc,也适用于使用同一阅读壳的 type: project。首页、文章和集合页不受影响。默认值为 true,保留已有消费者行为;slots.footer 也服从阅读壳的显示策略。此开关由布局决定,而非按内容类型分别设置;若关闭 docsShell,普通页面布局独立决定 Footer。静态路由正文的预渲染不会额外注入 React 应用 Footer。

内置首页配置#

FolioConfig.home 只配置内置首页,不需要复制静态路由或默认组件:

home: {
  meta: { left: "FIELD NOTES", right: "ARCHIVE" },
  sections: [
    { type: "intro", title: "写在前面" },
    { type: "featured", collection: "/notes" },
    { type: "recent", collection: "/notes", limit: 3 },
    { type: "collections", paths: ["/projects"], limit: 2 },
    { type: "about", path: "/info/about", linkLabel: "继续阅读" },
  ],
}

sections 的数组顺序就是首页区块顺序;省略时保留默认顺序,[] 只保留可选的 meta 条。精选与近期内容复用 config.collections 的路由集合,不写死 /blogs 或 /projects。resolveFolioHome(config, docs) 会返回 React 与默认静态首页共用的结构化模型。

三种首页路径#

  1. 直接使用默认首页:不设置 home,也不设置 slots.home。
  2. 通过配置自定义:设置 config.home;React 与默认静态首页使用同一份选择与排序。
  3. 高级完整覆写:slots.home 仍然完整替换 React 首页;staticRoutes.home 只替换静态 /,其他路由、404、sitemap 与 robots 不变。完整自定义静态 routes 时,该路由数组仍然是全站真相源;需要时可在其中调用 renderFolioStaticHome(context)。

静态 staticRoutes.home 接收显式 HTML,不能把任意 ReactNode 自动变成静态 HTML;SSR 与 React 服务端渲染由 consumer 自行负责。以上 home 配置与 staticRoutes.home 是 dev 候选契约,未包含在已发布的 0.1.1 中。

Vite plugin#

type FolioPluginOptions = {
  contentDir?: string
  config: FolioConfig
  staticRoutes?: boolean | FolioStaticBuildOptions
  exclude?: (sourcePath: string) => boolean
  route?: (sourcePath: string, doc: FolioDoc) => string
}

contentDir 默认是 content。exclude 在解析后过滤文件,route 用于 consumer 级 URL 兼容。staticRoutes 的完整字段、SEO、索引策略与 feed 配置见 静态构建与 SEO。

React slots#

type FolioSlots = {
  home?: ReactNode
  headerActions?: ReactNode
  articleFooter?: ReactNode
  navigation?: (props: FolioTreeNavProps) => ReactNode
  docToc?: (props: FolioTocProps) => ReactNode
  footer?: ReactNode
}

可复用文档导航与页内目录#

左右栏是 Folio 的独立公开组件,均从 @uichat-mira/folio 导出,沿用原有单入口 CSS。消费者可独立使用组件,也可组合到文档阅读壳里:

  • FolioTreeNav:唯一左侧导航渲染器,接收 tree 和 currentPath;普通技术文档与项目文章都使用它,支持路由、分组标题、层级与展开状态。
  • buildFolioNavigationTree(docs, current, { scopePath? }):Core 纯函数,生成统一导航树;可通过 scopePath 显式声明范围。默认以文档集合与项目路由推导范围,不维护第二份导航清单。
  • FolioToc:传入 headings,按标题生成锚点与滚动高亮,支持 className、onNavigate,空列表不渲染。

导航的 collection / section 分类始终显示为不可点击、不可折叠的标签;只有有子页面的内容父节点才出现 + / −。标题链接负责导航,右侧按钮独立控制展开/收起;整行共享背景与圆角,选中态高于悬停态。FolioApp 将展开状态保存在阅读路由上层,换文章时不会丢失。当前文章的祖先初始展开,其他有概览的分支初始收起;点击箭头不会切换路由。展开区域按实际内容高度平滑变化,折叠时子链接不可获取焦点,并尊重操作系统的减少动态效果设置。公共 FolioTreeExpansionProps 继续支持 defaultExpanded、expandedPaths、onExpandChange;collapsible: false 可显式切回路由驱动的无按钮模式。没有 JavaScript 时静态页面保留完整链接,不渲染无效按钮。

默认无需配置。想覆盖内置文档页的左右栏,可以使用动态渲染插槽:

import { FolioApp, FolioTreeNav, FolioToc } from "@uichat-mira/folio";

<FolioApp
  config={config}
  docs={docs}
  slots={{
    navigation: (props) => <FolioTreeNav {...props} className="my-tree-nav" />,
    docToc: (props) => <FolioToc {...props} className="my-doc-toc" />,
  }}
/>

插槽会随着当前文档自动收到新数据,桌面和打开的移动端菜单使用同一渲染方式。移动端 props.onNavigate 用于跳转后关闭抽屉,覆写时应继续传递。若完全替换导航 DOM,消费者应保持相应语义、布局类名和无障碍行为;独立使用 FolioTreeNav 时需位于 React Router 内。

静态 HTML 仍使用默认导航和 TOC,保证无 JS 时能浏览。React 的插槽不自动改变静态构建输出,消费者若需要自定义静态布局,应通过站点的 staticRoutes 管理。

约定式项目目录与父级推导#

项目内容不需要手工维护目录清单,也不需要在每篇文章上填写父级 ID。按以下结构放置 Markdown,Folio 会从路由层级构建一棵项目树:

projects/
  com-design/
    index.md                     # 项目概览 -> /projects/com-design
    repeated-operations/
      index.md                   # 子文章 -> /projects/com-design/repeated-operations
    notes/
      design-principles/
        index.md                 # 深层文章,继续嵌套

Frontmatter 示例:

---
type: project
title: "ComDesign:AI 时代的设计系统实验"
order: 2
description: "关于设计系统的长期探索"
---
  • 默认情况下,Markdown 路径自动推导 URL,目录中的 index.md 对应概览父节点,子目录文章成为它的后代。
  • 侧栏的子文章依 order 排序;当前页会高亮,上一篇/下一篇仅在同一顶层项目内移动。
  • 如果父目录暂时没有 index.md,导航会显示不可点击的目录分组,不会创建假的概览链接。
  • 显式设置 path 会覆写默认路由,因此也会改变导航归属;不同项目之间只按完整路径段匹配,避免相似前缀误归属。

项目文章和普通技术文档只通过 FolioTreeNav 渲染,不再保留第二套左侧导航组件。核心的 buildFolioNavigationTree(docs, current, { scopePath? }) 统一组合分类与嵌套项目节点;buildFolioContentTree 保留供纯目录树使用,resolveFolioProjectTree 仍负责项目内的上一篇/下一篇。需要独立排版时也可以自行组合:

import {
  buildFolioContentTree,
  FolioTreeNav,
} from "@uichat-mira/folio";

const tree = buildFolioContentTree(docs, { rootPath: "/projects" });
<FolioTreeNav tree={tree} currentPath={currentDoc.path} />;

消费者使用唯一的 slots.navigation(props) 自定义侧栏,回调接收 tree、currentPath 和 onNavigate,对普通文档与项目文章完全一致。移动端应继续传递 onNavigate,以便跳转后关闭抽屉。

Folio 的静态构建与 React 使用同一份 Core 树形模型;如需在自定义 staticRoutes 中保持约定式项目阅读结构,可使用构建入口 @uichat-mira/folio/vite 提供的 renderFolioStaticProjectShell(context, doc, markdownHtml?)。

官方 Folio 首页就是 slots.home 的真实 consumer。slots.home 的优先级高于 config.home:只要提供 React 首页节点,就完全使用该节点。

品牌、复杂 landing page 和额外交互属于 consumer;Folio runtime 不需要为官网增加隐藏接口。完整的首页定制边界见消费者契约。