Repository Wiki
LyraVoid/Mizuki

文章创作工作流与 Frontmatter

Mizuki 的文章创作以 src/content/posts/ 目录下的 Markdown/MDX 文件为核心:作者只需书写内容并在文件头部的 YAML frontmatter 中填写元数据,主题会在构建期通过 Astro Content Layer + Zod schema 校验这些字段,并自动完成草稿过滤、排序、置顶、上下篇派生、标签/分类聚合等处理。本文档完整覆盖这条"写文章 → 定义元数据 → 构建期处理"的创作工作流。

Purpose and Scope

本页覆盖以下内容:

  • 文章集合(posts)与规范文档集合(spec)的定义位置与加载方式;
  • Frontmatter 的完整字段参考(类型、默认值、语义),以 src/content.config.ts 中的 Zod schema 为唯一事实来源;
  • 构建期的内容处理管线:草稿过滤、置顶/priority 排序、发布日期排序、prev*/next* 内部字段派生;
  • 标签与分类列表的聚合逻辑,以及相关文章推荐所依赖的分词、Jaccard 相似度与标签 IDF 权重算法。

以下相关主题有意留给兄弟页面,本页仅点到为止:

  • 文章 URL / permalink / alias 的路由解析细节 —— 参见路由与 URL 生成相关页面(实现位于 src/utils/url-utils.ts、src/utils/permalink-utils.ts、src/utils/post-url.ts);
  • 页面加密(encrypted/password 字段的运行时行为)—— 参见页面加密相关页面;
  • RSS/Atom Feed 的内容渲染 —— 参见 Feed 生成相关页面(src/utils/feed-data.ts、src/utils/content-renderer.ts);
  • 主题构建配置(导航、评论系统等)—— 参见配置体系相关页面。

Overview

Mizuki 基于 Astro 的 Content Layer API 组织内容。创作者的全部"输入接口"只有一个:在 src/content/posts/ 下新建 *.md 或 *.mdx 文件并填写 frontmatter。之后的一切(校验、过滤、排序、聚合、推荐)都由构建期代码自动完成:

  1. 加载:src/content.config.ts 定义两个集合——posts(博客文章)与 spec(规范文档)。posts 使用自定义的 postGlob 加载器(src/loaders/post-loader.ts),spec 使用 Astro 内置的 glob 加载器。
  2. 校验:每个文件的 frontmatter 都会被 Zod schema 解析;title 与 published 必填,其余字段均有默认值或可选,保证"最小 frontmatter"也能通过校验。
  3. 派生:src/utils/content-utils.ts 在页面获取数据时执行排序(置顶 → priority → 发布日期倒序),并回写 prevSlug/prevTitle/nextSlug/nextTitle 四个内部字段,供文章详情页渲染上下篇导航。
  4. 聚合:标签与分类列表由全量文章的 tags/category 字段统计而来;空分类会被归入 i18n 的"未分类"。
  5. 推荐:模块后半部分实现了基于标签 IDF 权重 + 标题/描述 Jaccard 相似度的相关文章推荐,支持中英文混合分词(Intl.Segmenter,带正则回退)。

关键设计意图:让"写文章"这一动作保持零配置——frontmatter 的宽松默认值意味着作者可以只写 title + published,其余行为(评论开启、排序、聚合)全部自动获得合理默认;而高级能力(置顶、加密、自定义 permalink、多作者署名、许可证)通过可选字段渐进式启用。

Architecture

Loading diagram...

架构要点说明:

  • src/content.config.ts 是唯一契约:两个集合(posts、spec)及其 schema 全部集中在这 55 行的文件里。posts 走自定义 postGlob 加载器而非 Astro 内置 glob,因为文章需要在加载阶段补充内部字段(schema 中注释为 /* For internal use */ 的 prevTitle/prevSlug/nextTitle/nextSlug 与两个无默认值的必填布尔 _publishedDateOnly、_updatedDateOnly——后者必须由加载器在解析日期时计算并注入,否则 Zod 校验会直接失败)。
  • spec 集合是刻意"无 schema"的对照组:schema: z.object({}) 表示规范文档不受任何 frontmatter 约束,体现了"文章强约束、文档零约束"的分层设计。
  • content-utils.ts 是消费侧唯一入口:所有页面(列表、详情、标签、分类、Feed)都通过该模块读取 posts 集合,保证草稿过滤与排序规则全局一致——任何页面都不会绕过 draft 过滤直接 getCollection("posts")(模块内 4 处 getCollection 调用全部带同一条 PROD 过滤谓词)。

源码级实现:集合定义与加载

集合定义的完整源码如下(全文仅 55 行):

typescript
1import { defineCollection } from "astro:content"; 2import { glob } from "astro/loaders"; 3import { z } from "astro/zod"; 4 5import { postGlob } from "./loaders/post-loader"; 6 7const postsCollection = defineCollection({ 8 loader: postGlob({ pattern: "**/*.{md,mdx}", base: "./src/content/posts" }), 9 schema: z.object({ 10 title: z.string(), 11 published: z.date(), 12 updated: z.date().optional(), 13 draft: z.boolean().optional().default(false), 14 description: z.string().optional().default(""), 15 image: z.string().optional().default(""), 16 tags: z.array(z.string()).optional().default([]), 17 category: z.string().optional().nullable().default(""), 18 lang: z.string().optional().default(""), 19 pinned: z.boolean().optional().default(false), 20 comment: z.boolean().optional().default(true), 21 priority: z.number().optional(), 22 author: z.string().optional().default(""), 23 sourceLink: z.string().optional().default(""), 24 licenseName: z.string().optional().default(""), 25 licenseUrl: z.string().optional().default(""), 26 27 /* Page encryption fields */ 28 encrypted: z.boolean().optional().default(false), 29 password: z.string().optional().default(""), 30 passwordHint: z.string().optional().default(""), 31 hideHomeContent: z.boolean().optional(), 32 33 /* Posts alias */ 34 alias: z.string().optional(), 35 36 /* Custom permalink - 自定义固定链接,优先级高于 alias */ 37 permalink: z.string().optional(), 38 39 /* For internal use */ 40 prevTitle: z.string().default(""), 41 prevSlug: z.string().default(""), 42 nextTitle: z.string().default(""), 43 nextSlug: z.string().default(""), 44 _publishedDateOnly: z.boolean(), 45 _updatedDateOnly: z.boolean(), 46 }), 47}); 48const specCollection = defineCollection({ 49 loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/spec" }), 50 schema: z.object({}), 51}); 52export const collections = { 53 posts: postsCollection, 54 spec: specCollection, 55};

Source: content.config.ts

逐段解读:

  • 加载范围:pattern: "**/*.{md,mdx}" + base: "./src/content/posts" —— 支持任意深度的子目录嵌套,post.id 由加载器基于相对路径生成(排序时的同日期 tie-break 会用到 post.id)。
  • 必填字段只有两个:title: z.string() 与 published: z.date()。缺任一项,Astro 构建会在 content 层校验阶段直接报错——这是"快速失败"设计,避免空标题/空日期的文章静默进入列表。
  • permalink 优先于 alias:源码注释明确写着"自定义固定链接,优先级高于 alias"。两者都是单值 z.string().optional(),URL 解析优先级逻辑位于 src/utils/url-utils.ts(细节见路由相关页面)。
  • _publishedDateOnly / _updatedDateOnly 没有默认值:它们是"必须由加载器供给"的标记字段(发布/更新日期是否只含日期部分),任何绕过 postGlob 直接写入 posts 集合的数据都会因缺少这两个字段而校验失败——这是把"加载器必须运行"固化为类型约束的做法。

关于 postGlob 加载器本身(src/loaders/post-loader.ts):它从 content.config.ts 导入并以上述参数调用,负责扫描目录、解析 frontmatter、注入内部字段。其内部实现不在本页采集范围内,详见源文件 post-loader.ts。

核心处理流程

草稿过滤与排序(getRawSortedPosts / getSortedPosts)

typescript
1// // Retrieve posts and sort them by publication date 2async function getRawSortedPosts() { 3 const allBlogPosts = await getCollection("posts", ({ data }) => { 4 return import.meta.env.PROD ? data.draft !== true : true; 5 }); 6 7 const sorted = allBlogPosts.sort((a, b) => { 8 // 首先按置顶状态排序,置顶文章在前 9 if (a.data.pinned && !b.data.pinned) { 10 return -1; 11 } 12 if (!a.data.pinned && b.data.pinned) { 13 return 1; 14 } 15 16 // 如果置顶状态相同,优先按 Priority 排序(数值越小越靠前) 17 if (a.data.pinned && b.data.pinned) { 18 const priorityA = a.data.priority; 19 const priorityB = b.data.priority; 20 if (priorityA !== undefined && priorityB !== undefined) { 21 if (priorityA !== priorityB) { 22 return priorityA - priorityB; 23 } 24 } else if (priorityA !== undefined) { 25 return -1; 26 } else if (priorityB !== undefined) { 27 return 1; 28 } 29 } 30 31 // 否则按发布日期排序 32 return comparePublishedDatesDescending( 33 a.data.published, 34 b.data.published, 35 a.id, 36 b.id, 37 ); 38 }); 39 return sorted; 40}

Source: content-utils.ts

设计意图:

  • 草稿在开发模式下可见:import.meta.env.PROD ? data.draft !== true : true 意味着 draft: true 的文章仅在 astro dev 中出现,生产构建会将其剔除,作者可以"提交到仓库但不发布"。
  • 排序规则分四层:非置顶在前被置顶覆盖;两篇都置顶时按 priority 升序(数值小者靠前);未设置 priority 的置顶文章排在设置了 priority 的置顶文章之后;其余一律走 comparePublishedDatesDescending(发布日期倒序,并以 post.id 作 tie-break 保证排序稳定、构建可复现)。注意 priority 只对置顶文章生效——这是刻意为之,避免普通文章破坏时间线语义。
  • Array.prototype.sort 原地修改:getCollection 每次返回新数组,因此原地排序安全;但模块内多处函数各自调用 getRawSortedPosts(),每次调用都会重新排序(构建期一次性开销,可接受)。

排序完成后,getSortedPosts 会把上下篇信息回写到文章 data 上:

typescript
1export async function getSortedPosts() { 2 const sorted = await getRawSortedPosts(); 3 4 for (let i = 1; i < sorted.length; i++) { 5 sorted[i].data.nextSlug = sorted[i - 1].id; 6 sorted[i].data.nextTitle = sorted[i - 1].data.title; 7 } 8 for (let i = 0; i < sorted.length - 1; i++) { 9 sorted[i].data.prevSlug = sorted[i + 1].id; 10 sorted[i].data.prevTitle = sorted[i + 1].data.title; 11 } 12 13 return sorted; 14}

Source: content-utils.ts

由于排序为发布日期倒序,数组索引 i-1 是时间上更新的文章,因此被写为 next*("较新一篇"),i+1 被写为 prev*("较旧一篇")。这两个循环直接写入 schema 中标记为 "For internal use" 的字段——这正是 postGlob 加载器需要为这些字段提供空默认值的原因:校验在排序之前发生。

列表页专用数据(getSortedPostsList)

typescript
1export interface PostForList { 2 id: string; 3 data: CollectionEntry<"posts">["data"]; 4 url?: string; // 预计算的文章 URL 5} 6export async function getSortedPostsList(): Promise<PostForList[]> { 7 const sortedFullPosts = await getRawSortedPosts(); 8 9 // 初始化文章 ID 映射(用于 permalink 功能) 10 initPostIdMap(sortedFullPosts); 11 12 // delete post.body,并预计算 URL 13 const sortedPostsList = sortedFullPosts.map((post) => ({ 14 id: post.id, 15 data: post.data, 16 url: getPostUrl(post), 17 })); 18 19 return sortedPostsList; 20}

Source: content-utils.ts

列表页不需要文章正文。该函数通过 map 只保留 id、data、url 三个字段,丢弃体积庞大的 post.body(整个 Markdown 源文本),显著降低传给列表组件的数据量;同时调用 initPostIdMap 建立 permalink 所需的 id 索引(来自 @utils/permalink-utils),再把 URL 预计算一次(getPostUrl,来自 @utils/url-utils),避免列表页 N 篇文章反复调用 URL 生成逻辑。这体现了"数据瘦身 + 预计算"的构建期优化思路。

端到端时序

Loading diagram...

Frontmatter 字段完整参考

以 content.config.ts 的 Zod schema 为唯一事实来源:

字段类型必填默认值语义与用途
titlestring✅—文章标题,列表/详情/SEO 使用
publisheddate✅—发布日期,排序主键(倒序)
updateddate❌—更新日期,详情页展示"最后更新"
draftboolean❌false草稿标记;生产构建剔除,开发模式可见
descriptionstring❌""摘要,用于列表卡片与 meta 描述
imagestring❌""封面图路径
tagsstring[]❌[]标签数组,驱动标签聚合与相关文章推荐
categorystring | null❌""分类;空/null 时归入 i18n "未分类"
langstring❌""文章语言标记
pinnedboolean❌false置顶标记,排序最高优先级
commentboolean❌true是否启用评论区(默认开启)
prioritynumber❌—置顶文章内部排序,数值越小越靠前;仅对 pinned 文章生效
authorstring❌""作者署名(多作者场景)
sourceLinkstring❌""原文/转载来源链接
licenseNamestring❌""许可证名称(如 CC BY 4.0)
licenseUrlstring❌""许可证链接
encryptedboolean❌false页面加密开关(详见加密相关页面)
passwordstring❌""加密口令
passwordHintstring❌""口令提示
hideHomeContentboolean❌—首页隐藏正文内容
aliasstring❌—文章别名,用于兼容旧 URL
permalinkstring❌—自定义固定链接,优先级高于 alias
prevTitle / prevSlugstring❌""内部字段:较旧一篇的标题/id(由排序派生,勿手写)
nextTitle / nextSlugstring❌""内部字段:较新一篇的标题/id(由排序派生,勿手写)
_publishedDateOnlyboolean✅(内部)—加载器注入:发布日期是否仅含日期
_updatedDateOnlyboolean✅(内部)—加载器注入:更新日期是否仅含日期

最小合法 frontmatter 示例(可直接用于 src/content/posts/ 下的新文章):

yaml
1--- 2title: 我的第一篇 Mizuki 文章 3published: 2025-01-01 4---

进阶示例(置顶 + 加密 + 许可证 + 自定义 permalink):

yaml
1--- 2title: 深入理解 Astro Content Layer 3published: 2025-06-01 4updated: 2025-06-10 5description: 一篇关于 Astro 内容层的深度解析 6tags: [Astro, 前端, 内容管理] 7category: 技术笔记 8lang: zh 9pinned: true 10priority: 1 11comment: false 12author: Lyra 13sourceLink: "" 14licenseName: CC BY 4.0 15licenseUrl: https://creativecommons.org/licenses/by/4.0/ 16permalink: posts/deep-dive-astro-content-layer 17---

(以上 YAML 为基于 schema 约束编写的字段用法示例,非仓库内实际文件。)

标签与分类聚合

typescript
1export interface Category { 2 name: string; 3 count: number; 4 url: string; 5} 6 7export async function getCategoryList(): Promise<Category[]> { 8 const allBlogPosts = await getCollection<"posts">("posts", ({ data }) => { 9 return import.meta.env.PROD ? data.draft !== true : true; 10 }); 11 const count: Record<string, number> = {}; 12 allBlogPosts.forEach((post: { data: { category: string | null } }) => { 13 if (!post.data.category) { 14 const ucKey = i18n(I18nKey.uncategorized); 15 count[ucKey] = count[ucKey] ? count[ucKey] + 1 : 1; 16 return; 17 } 18 19 const categoryName = 20 typeof post.data.category === "string" 21 ? post.data.category.trim() 22 : String(post.data.category).trim(); 23 24 count[categoryName] = count[categoryName] ? count[categoryName] + 1 : 1; 25 }); 26 27 const lst = Object.keys(count).sort((a, b) => { 28 return a.toLowerCase().localeCompare(b.toLowerCase()); 29 }); 30 ... 31}

Source: content-utils.ts

要点:

  • 未分类本地化:category 为空("" 或 null,schema 用 .nullable() 允许显式 null)的文章会被计入 i18n(I18nKey.uncategorized),即界面语言下的"未分类",而不是出现空字符串分类。
  • 分类名先 trim() 再计数:防止 技术 与 技术 变成两个分类;随后按 toLowerCase() + localeCompare 做大小写不敏感的字典序排序,保证中英文混合分类名输出稳定。
  • getTagList()(同文件 content-utils.ts L88-L109)逻辑对称:遍历全量文章的 tags 数组建立 countMap,同样按小写 localeCompare 排序后输出 { name, count }[]。区别在于标签没有"未分类"兜底——因为 tags 默认值是 [],空标签不参与聚合。

相关文章推荐算法

content-utils.ts 后半部分实现了一套基于文本统计的相关文章推荐,包含三个协作函数:

分词(tokenize)

typescript
1function tokenize(text: string): Set<string> { 2 const tokens = new Set<string>(); 3 4 const hasSegmenter = 5 typeof Intl !== "undefined" && 6 "Segmenter" in Intl && 7 typeof (Intl as unknown as { Segmenter?: unknown }).Segmenter === 8 "function"; 9 10 if (!hasSegmenter) { 11 const basicTokens = text 12 .toLowerCase() 13 .split(/[\s\p{P}]+/gu) 14 .filter(Boolean); 15 for (const t of basicTokens) { 16 tokens.add(t); 17 } 18 return tokens; 19 } 20 21 const segmenter = new ( 22 Intl as unknown as { 23 Segmenter: new ( 24 locale: string, 25 options: { granularity: string }, 26 ) => { 27 segment: ( 28 text: string, 29 ) => Iterable<{ segment: unknown; isWordLike: boolean | undefined }>; 30 }; 31 } 32 ).Segmenter("zh", { 33 granularity: "word", 34 }); 35 for (const { segment, isWordLike } of segmenter.segment(text)) { 36 if (!isWordLike) { 37 continue; 38 } 39 tokens.add((segment as string).toLowerCase()); 40 } 41 return tokens; 42}

Source: content-utils.ts

Intl.Segmenter 是原生 Intl API,Node 运行时的类型定义可能尚未包含它,因此代码通过 Intl as unknown as {...} 的双重断言访问,并在运行时探测 "Segmenter" in Intl。不支持时回退到 toLowerCase().split(/[\s\p{P}]+/gu) 的朴素分词——源码注释明确说明这是"以避免构建报错"的降级路径。两种路径都过滤标点/空白并统一小写,返回 Set<string> 天然去重。

相似度与标签权重

typescript
1function jaccardSimilarity(a: Set<string>, b: Set<string>): number { 2 if (a.size === 0 && b.size === 0) { 3 return 0; 4 } 5 let intersection = 0; 6 for (const item of a) { 7 if (b.has(item)) { 8 intersection++; 9 } 10 } 11 const union = a.size + b.size - intersection; 12 return union === 0 ? 0 : intersection / union; 13}

Source: content-utils.ts

computeTagIDF(content-utils.ts L225 起)按源码注释给出的公式计算:IDF(tag) = log(N / (1 + df(tag))),其中 N 为总文章数、df 为包含该标签的文章数。稀有标签权重高、常见标签权重低——这与信息检索中经典 IDF 的动机一致:"Astro" 这类专属标签比 "前端" 这类泛标签更能标识文章主题。分母 1 + df 保证 df=0 时不会除零。

推荐流程(模块内推荐函数,基于 content-utils.ts L302 起 的 getCollection 调用):读取全部非草稿文章 → 对每篇候选计算(标题+描述分词集合的 Jaccard 相似度)与(共享标签的 IDF 权重和)→ 综合打分排序 → 取前 N 篇作为相关文章。推荐算法的完整打分细节在本次采集范围之外,见源文件。

API Reference

以下为 src/utils/content-utils.ts 导出的公开函数(类型签名以源码为准):

getSortedPosts(): Promise<CollectionEntry<"posts">[]>

获取排序后的完整文章数组(含 body)。排序规则:置顶在前 → 置顶内部按 priority 升序 → 其余按发布日期倒序。副作用:回写每篇文章的 prevSlug/prevTitle/nextSlug/nextTitle 内部字段。

参数:无 返回:排序后的 posts 集合条目数组 消费方:文章详情页(上下篇导航)、Feed 生成

getSortedPostsList(): Promise<PostForList[]>

获取列表页专用的轻量数据。

返回:PostForList[],其中 PostForList = { id: string; data: ...; url?: string } 副作用:调用 initPostIdMap(sortedFullPosts) 初始化 permalink 的 id 映射;丢弃 post.body;预计算 url: getPostUrl(post)

getTagList(): Promise<Tag[]>

返回:Tag[] = { name: string; count: number }[],按标签名小写 localeCompare 排序

getCategoryList(): Promise<Category[]>

返回:Category[] = { name: string; count: number; url: string }[];空分类归入 i18n "未分类";url 由 getCategoryUrl(c) 生成

模块内部函数(私有)

函数作用
getRawSortedPosts()草稿过滤 + 排序,不派生 prev/next;供 getSortedPosts/getSortedPostsList 复用
tokenize(text)中英文混合分词,Intl.Segmenter 优先、正则回退,返回去重 token 集合
jaccardSimilarity(a, b)计算两个 token 集合的 Jaccard 相似度
computeTagIDF(allPosts)计算每个标签的 IDF 权重:log(N / (1 + df))

失败模式、边界情况与并发

  • Frontmatter 校验失败:title 或 published 缺失、类型不符(如 published: "昨天")时,Astog/Zod 在构建期直接抛错并终止构建——错误会在构建日志中指向具体文件。_publishedDateOnly/_updatedDateOnly 缺失同样会校验失败,但正常路径下由 postGlob 加载器注入,作者无需关心。
  • 草稿泄露边界:draft: true 在 astro dev 下可见(便于预览),但在生产构建中被 4 处 getCollection 谓词一致剔除。若新增页面绕过 content-utils.ts 直接调用 getCollection("posts") 且不携带该谓词,草稿会在生产环境泄露——这是扩展时的强制约定。
  • 排序稳定性:comparePublishedDatesDescending 以 post.id 作 tie-break(见 content-utils.ts L39-L44),同日期文章顺序确定,构建可复现,避免 CI 与本地构建产物 diff 噪音。
  • 空集合/空分类边界:category 为 "" 或 null 均走"未分类"分支;getTagList 中空 tags 数组不产生条目;jaccardSimilarity 对两个空集合显式返回 0;computeTagIDF 用 1 + df 规避除零。
  • 分词降级:在不支持 Intl.Segmenter 的运行时,中文文本退化为整段切分(仅按空白/标点分割),推荐质量下降但构建不失败——这是显式的可用性优先取舍。
  • 数据可变性:getSortedPosts 会原地修改 getCollection 返回对象的 data 字段(写入 prev/next)。Astro 的 Content Layer 每次调用返回独立数据副本,因此该写入不会跨请求/跨页面互相污染;但在同一构建中缓存 CollectionEntry 引用再复用的代码需意识到这些字段可能已被后续调用填充。

性能与运维注意事项

  • post.body 瘦身:列表场景务必使用 getSortedPostsList() 而非 getSortedPosts(),前者把整篇 Markdown 源文本从传给组件的数据中剥离。文章数量增大时,这是列表页内存与序列化开销的主要控制手段。
  • URL 预计算:getPostUrl(post) 在 getSortedPostsList 中每篇只调用一次,列表渲染不再重复计算路由。
  • 重复排序成本:getRawSortedPosts 每次调用都会重新 getCollection + 排序。Astro 在单次构建中会缓存 content 层数据,因此主要成本在比较函数本身(O(n log n)),对数百篇文章量级无压力。
  • IDF 全局依赖:computeTagIDF 需要全量文章统计,文章数增长时推荐计算量为 O(n²)(两两比较);这是构建期一次性开销,不进入运行时。
  • 新增文章的运维流程:仅需 git add src/content/posts/xxx.md 并推送;无数据库、无 CMS、无迁移——内容即文件,历史即 Git 历史。

扩展点

  • 新增 frontmatter 字段:在 content.config.ts 的 postsCollection.schema 中追加 Zod 字段(建议 optional().default() 以保持向后兼容),Astro 会在下次构建自动校验并生成类型;随后可在组件中直接读取 post.data.新字段。
  • 新增集合:仿照 specCollection,用 defineCollection + glob + 空或自定义 schema 定义,并加入 collections 导出对象。
  • 调整排序规则:修改 getRawSortedPosts 的比较器即可全局生效(列表、标签、分类、Feed 同源)。
  • 自定义加载行为:posts 集合不使用 Astro 内置 glob 而使用 postGlob(src/loaders/post-loader.ts),若需在加载期注入/变换字段(如默认分类、slug 规范化),扩展点即在该加载器。
  • 推荐算法调优:调整 computeTagIDF 公式权重或替换 jaccardSimilarity,均不影响数据契约。

相关链接

路由与 permalink 解析、页面加密运行时行为、Feed 生成细节分别属于各自独立的目录页,本页不展开。

Sources

(2 files)