Maroon 架构文档

项目概述

Maroon 是一个基于 Astro 5 的静态个人博客,支持多主题切换(奶油/星空)、Markdown 内容管理、ViewTransitions 页面过渡动画。包含博客文档两大内容板块,以及标签筛选、全文搜索等功能。

  • 技术栈:Astro 5 + TypeScript + CSS Custom Properties
  • Monorepo:npm workspaces — 主应用 + astro-maroon 主题包
  • 构建输出:纯静态 HTML(output: 'static'

项目结构

maroon/
├── packages/
│ └── astro-maroon/ # 独立主题包(可发布到 npm)
│ ├── src/
│ │ ├── components/ # UI 组件
│ │ │ ├── blog/ # PostCard
│ │ │ ├── docs/ # DocsSidebar
│ │ │ ├── home/ # Hero, SeriesCard, SeriesSection
│ │ │ ├── shared/ # Sidebar, TOC, PageNav
│ │ │ ├── Header.astro / Footer.astro / Search.astro
│ │ ├── layouts/
│ │ │ ├── BaseLayout.astro # 根布局
│ │ │ ├── PostLayout.astro # 博客文章
│ │ │ └── DocsLayout.astro # 文档
│ │ ├── styles/ # CSS 变量、排版、布局
│ │ ├── types/ # SiteConfig, RoutesConfig, PostCardProps...
│ │ └── utils/ # formatDate, generatePath, themes...
│ └── package.json
├── src/ # 主应用 — 胶水层
│ ├── config/
│ │ └── maroon.ts # ⭐ 唯一配置入口(站点 + 内容类型 + 路由)
│ ├── content/
│ │ ├── blog/ / docs/ / pages/ # Markdown 内容
│ │ └── utils.ts # 查询工具
│ ├── content.config.ts # Zod Schema(Astro 强制路径)
│ ├── middleware.ts # 配置注入 Astro.locals.site
│ └── pages/ # 路由(极薄胶水层)
├── tsconfig.json # 路径别名 → 主题包
├── astro.config.mjs
└── package.json # workspaces: ["packages/*"]

分层架构

架构分为两层:App 层 负责配置和路由,主题包 负责渲染。

App 层 (src/)
├── config/maroon.ts ⭐ ──→ 页面读取
├── content/blog/ docs/ ──→ pages/ 路由生成
├── middleware.ts ──→ Astro.locals.site(运行时注入)
└── pages/ ──→ 调用主题包布局渲染
↓ Astro.locals.site + props
astro-maroon 主题包
├── layouts/
│ ├── BaseLayout.astro ← 根布局(Header + Footer + Search)
│ ├── PostLayout.astro ← 博客文章
│ └── DocsLayout.astro ← 文档页面
├── components/
│ ├── shared/PageNav ← 上下篇导航
│ ├── shared/TOC ← 目录
│ ├── shared/Sidebar ← 标签云 + 个人资料
│ ├── blog/PostCard ← 文章卡片
│ └── docs/DocsSidebar ← 文档分类侧边栏
├── styles/
│ ├── base.css ← CSS 变量 + 主题色
│ ├── prose.css ← 排版 + 代码块 + 表格
│ └── layout.css ← 网格 + 响应式断点
└── utils/
├── content.ts ← buildSidebarData, getTagStats
├── generate-path.ts ← URL 路径生成
├── themes.ts ← 主题列表
└── toc-overlay.ts ← TOC 浮层切换

唯一配置入口:src/config/maroon.ts

新人搭站只需改这一个文件。 站点信息 + 内容类型 + 路由全部在此配置。

// === 内容类型注册 ===
export const contentRegistry: ContentTypeConfig[] = [
{ id: 'blog', label: '博客', route: {...}, layout: 'post', ... },
{ id: 'docs', label: '文档', route: {...}, layout: 'doc', ... },
];
// === 站点信息 ===
export const siteConfig: SiteConfig = {
title: '栗かな', author: '栗かな',
avatar: '/icon.png', bio: '日语专业 / 技术探索中',
social: { github: 'https://github.com/Mepuru' },
};

ContentTypeConfig 字段说明

字段类型说明
idstring对应 src/content/{id}/ 目录名
labelstring显示名称(导航栏、标题)
route{ prefix, pattern }URL 路径模板
layout'post' | 'doc'使用哪个 Layout
sidebarIncludedboolean详情页是否显示侧边栏
showInNavboolean?是否出现在导航栏
hasTagsboolean?是否参与标签聚合
seriesobject?首页系列卡片配置

自动推导函数

函数生成内容消费方
generateRoutes()Astro.locals.site.routes所有组件的路径读取
generateNavItems()siteConfig.navHeader 导航栏
generateSeriesConfigs()系列配置首页 SeriesSection
generateTaggableCollections()带 tags 的 collection ID 列表标签页

数据流

Middleware 注入链路

maroon.ts ──→ middleware.ts ──→ Astro.locals.site ──→ 所有布局/组件自动读取

src/middleware.ts 在每次请求时合并配置注入:

context.locals.site = {
...siteConfig, // 站点信息 + 导航
themes, // 主题列表
defaultTheme, // 默认主题
routes: generateRoutes(), // 路由表
};

Layout 读取优先级

props → Astro.locals.site → 硬编码兜底

Astro.locals.site 可用字段

{
title, description, author, avatar, icon, bio, // 站点信息
nav: [{ href, label }], // 导航栏
social: { github }, // 社交链接
footer: { icp, icpUrl }, // 备案号
docs: { emptyTexts }, // 文档空状态
themes: [{ id, name }], // 主题列表
defaultTheme, // 默认主题
routes: { blog, docs, tags, about, home, icon }, // 路由表
}

布局系统

普通页面(about / 404 / 列表页)

.layout-wrapper.wide {
display: block;
/* max-width/padding 继承自 .layout-wrapper 基础规则 */
}

博客列表页(有侧边栏)

两栏网格布局——左侧文章列表,右侧侧边栏。

.layout-wrapper {
display: grid;
grid-template-columns: 1fr 260px;
gap: 2rem;
/* ↓ 博客页额外传给 BaseLayout: sidebar={true} */
}

文章详情页(fullWidth 模式)

解除外层宽度限制,TOC 固定左侧,文章内容居中,侧边栏在右侧。

.layout-wrapper.full-width {
max-width: none;
margin: 0;
padding: 0 1.5rem;
}
.post-page {
padding-left: 220px; /* 给左侧 TOC 让位 */
}
.post-article {
max-width: 800px;
margin: 0 auto;
padding: 2rem 1.5rem;
}

文档页(fullWidth + 固定侧边栏/TOC)

全宽布局,侧边栏和 TOC 固定定位,文档内容区域用 margin 避开两侧固定栏。

.docs-sidebar {
position: fixed;
left: 0;
width: 260px;
}
.docs-main {
margin-left: 260px; /* 避开左侧侧边栏 */
margin-right: 200px; /* 避开右侧 TOC */
}
.doc-article {
max-width: 800px;
margin: 0 auto;
}
.toc-wrapper {
position: fixed;
right: 0.25rem;
width: 200px;
}

响应式断点

宽度博客文档
>1200px(桌面默认)TOC fixed 左 + Sidebar 右Sidebar fixed 左 + TOC fixed 右
≤1200pxTOC→悬浮按钮TOC→悬浮按钮
≤768px单列 + TOC 按钮侧边栏→抽屉 + TOC 按钮

共享组件

组件位置用途用法
TOCshared/TOC.astro文章目录,监听滚动高亮<TOC headings={headings} />
PageNavshared/PageNav.astro上下篇导航<PageNav prev next pattern="/blog/[slug]" />
Sidebarshared/Sidebar.astro个人资料 + 标签云由 BaseLayout 按需渲染
PostCardblog/PostCard.astro文章卡片<PostCard title slug pubDate ... />
DocsSidebardocs/DocsSidebar.astro文档分类导航由 DocsLayout 自动渲染

主题系统

两套预设主题:奶油(cream) / 星空(starry)

通过 CSS 自定义属性 + data-theme 属性切换。选择写入 localStorage

增加新主题

改主题包的两个文件:

  1. packages/astro-maroon/src/utils/themes.ts{ id, name }
  2. packages/astro-maroon/src/styles/base.css[data-theme="xxx"] 变量块

必须覆盖的 CSS 变量: --bg, --fg, --accent, --accent-light, --border, --muted, --card-bg, --code-bg, --header-bg, --search-bg, --shadow-*, --gradient-*, --theme-label, --radius-*


开发规范

命名约定

类别规则示例
组件文件PascalCaseHeader.astro, PostCard.astro
页面路由kebab-case + [][...slug].astro, [tag].astro
工具函数camelCaseformatDate(), getPublishedPosts()
类型/接口PascalCaseSiteConfig, PostCardProps
CSS 类名kebab-case.post-card, .nav-links
CSS 变量kebab-case, -- 前缀--header-height, --font-size-hero-title
目录名kebab-caseshared/, home/, blog/

导入顺序

---
// 1. Astro 内置
import { getCollection } from 'astro:content';
// 2. 主题包布局/组件
import BaseLayout from 'astro-maroon/layouts/BaseLayout.astro';
// 3. 本地工具
import { buildBlogSidebar } from '../../content/utils';
// 4. 类型
import type { PostCardProps } from 'astro-maroon/types';
// 5. 样式
import 'astro-maroon/styles/layout.css';
---

CSS 规范

  • 变量驱动:禁止在组件样式中硬编码颜色/尺寸,所有主题色在 base.css[data-theme] 中定义
  • 响应式断点≤768px 手机 / ≤1200px 平板 / ≥769px 桌面
  • 过渡动画:主题切换 0.5s cubic-bezier(0.4, 0, 0.2, 1),hover 0.2s ease
  • 区块注释:用 /* ==== 区域名 ==== */ 分隔

TypeScript 规范

  • 严格模式astro/tsconfigs/strict
  • Props 接口 — 优先 interface,联合/交叉用 type
  • 避免 any — 优先 unknown + 类型收窄
  • 公共类型 — 放在 packages/astro-maroon/src/types/

新增内容类型完整流程

以新增”笔记”为例:

1. 配置入口

src/config/maroon.tscontentRegistry 加一条:

{
id: 'notes',
label: '笔记',
route: { prefix: '/notes', pattern: '/notes/[slug]' },
layout: 'post',
sidebarIncluded: false,
showInNav: true,
hasTags: true,
series: {
description: '学习笔记',
countLabel: '篇笔记',
sortField: 'pubDate',
sortOrder: 'desc',
},
}

2. Zod Schema

src/content.config.tsdefineCollection(Astro 5 写法):

import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const notes = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/notes' }),
schema: z.object({
title: z.string(),
pubDate: z.coerce.date(),
draft: z.boolean().default(false),
}),
});
export const collections = { blog, docs, pages, notes };

3. 内容目录

src/content/notes/ 下放 .md 文件。

4. 路由文件

src/pages/notes/index.astro(列表页)+ src/pages/notes/[...slug].astro(详情页),参照 src/pages/blog/ 下的文件。

完成后自动生效:导航栏出现”笔记”入口、首页出现系列卡片、URL /notes/xxx 自动可用。


工作流

本地开发

Terminal window
npm run dev # 启动开发服务器(热更新)
npm run build # 生产构建 + Pagefind 搜索索引
npm run preview # 预览构建产物

Git 提交

commit message 用中文,前缀标识改动类型:

feat: 新功能
fix: 修复问题
refactor: 重构(不改功能)
docs: 文档更新
style: 代码格式清理(不影响逻辑)
chore: 构建配置/工具更新

常见陷阱

Astro 模板变量

<!-- ✅ 正确 -->
<a href={blogPrefix}>返回列表</a>
<!-- ❌ 错误 -->
<a href="{blogPrefix}">返回列表</a>

表格溢出

已通过 prose.css 全局修复:

.prose table { display: block; max-width: 100%; overflow-x: auto; }

flex 项溢出

white-space: nowrap + flex: 1 的容器必须加 min-width: 0


给后续开发者的规则

  1. 每完成一个逻辑阶段后 git commit
  2. 不在组件中硬编码路径/文案 — 从 Astro.locals.site.routes 读取
  3. 新增内容查询用 src/content/utils.ts 的工具函数
  4. 所有 getCollection 必须包裹 try/catch
  5. CSS 关键尺寸用变量--sidebar-width--header-height
  6. 发布前先 npm run build 验证零报错
  7. 改代码后同步更新本文档
Maroon 架构文档
https://kurikana.cn/docs/architecture/
作者 栗かな
发布 2026/05/16
协议 CC BY 4.0