跳到正文

主题自定义配置笔记

Updated:
编辑此页

本文档持续记录 AstroPaper 主题的各种自定义配置位置和修改方法。

⚡ 通用修改步骤

  1. 定位对应的配置文件
  2. 找到需要修改的代码或配置项
  3. 进行修改并保存文件
  4. 开发服务器会自动热重载,实时预览效果

提示:修改后记得在明暗两种主题下都进行测试,确保视觉效果一致。

🎨 背景颜色修改

📁 主要文件位置

src/styles/global.css

颜色变量定义

浅色主题(默认)

:root,
html[data-theme="light"] {
  --background: #fdfdfd; /* ← 修改此处 */
}

深色主题

html[data-theme="dark"] {
  --background: #212737; /* ← 修改此处 */
}

🔗 导航菜单激活状态样式

📁 样式定义位置

src/styles/global.css

推荐样式配置

.active-nav {
  @apply text-accent underline decoration-solid decoration-1 underline-offset-8;
}

样式参数说明

  • text-accent:文字变为强调色(自动适配明暗主题)
    • 浅色主题:蓝色 #006cac
    • 深色主题:橙色 #ff6b01
  • decoration-solid:直线样式(替代原来的波浪线)
  • decoration-1:1px 粗细的细线
  • underline-offset-8:8px 间距,提供良好的视觉呼吸感

应用方式

当导航菜单项处于激活状态时,会自动应用 .active-nav 类,显示文字颜色变化 + 细直线下划线效果。

自定义调整建议

如果需要微调下划线间距,可以修改 underline-offset 值:

  • underline-offset-3:紧凑型间距
  • underline-offset-5:中等间距
  • underline-offset-8:宽松间距(当前推荐)

📁 配置文件位置

src/constants.ts

配置对象

export const SOCIALS: Social[] = [
  {
    name: "GitHub",
    href: "https://github.com/MaizeDev/my-blog-maize",
    linkTitle: `${SITE.title} on GitHub`,
    icon: IconGitHub,
  },
  {
    name: "X", 
    href: "https://x.com/username",
    linkTitle: `${SITE.title} on X`,
    icon: IconBrandX,
  },
  // ... 其他社交链接
] as const;

可用图标

  • IconMailIconGitHubIconBrandXIconLinkedin
  • IconWhatsappIconFacebookIconTelegramIconPinterest

修改方法

  • 添加链接:在 SOCIALS 数组中添加新对象
  • 删除链接:从数组中移除对应对象或注释掉
  • 修改链接:更改 hreflinkTitle 的值
  • 更换图标:使用不同的图标组件(需确保已导入)

显示逻辑

首页会自动检测 SOCIALS.length > 0,如果数组为空则不显示 “Social Links:” 区域。

🌐 分享链接新窗口打开

📁 问题描述

默认情况下,文章页面的分享链接会在当前窗口跳转到分享网站,影响用户浏览体验。

📁 配置文件位置

src/components/ShareLinks.astro

🔧 解决方案

修改 LinkButton 组件,添加 target="_blank"rel="noopener noreferrer" 属性:

<LinkButton
  href={`${social.href + URL}`}
  target="_blank"
  rel="noopener noreferrer"
  class="scale-90 p-2 hover:rotate-6 sm:p-1"
  title={social.linkTitle}
>
  <social.icon class="inline-block size-6 scale-125 fill-transparent stroke-current stroke-2 opacity-90 group-hover:fill-transparent sm:scale-110" />
  <span class="sr-only">{social.linkTitle}</span>
</LinkButton>

📝 属性说明

  • target="_blank":在新窗口/标签页中打开链接
  • rel="noopener":防止新页面通过 window.opener 控制原页面,提升安全性
  • rel="noreferrer":防止发送 Referer 头,保护用户隐私

💡 效果

修改后,所有分享链接(Facebook、X/Twitter、WhatsApp、Telegram、Pinterest、邮件等)都会在新窗口中打开,不会中断用户当前的博客浏览体验。

☁️ Cloudflare Pages 构建问题排查与解决

📁 问题背景

在使用 Obsidian + Templater 插件创建博客文章模板后,提交到仓库并通过 Cloudflare Pages 构建时遇到两个关键错误,导致构建失败。

❌ 错误一:InvalidContentEntryDataError(日期格式错误)

🔍 错误日志

[InvalidContentEntryDataError] blog → slug data does not match collection schema.
pubDatetime: Expected type "date", received "string"
modDatetime: Expected type "date", received "string"
Location: /opt/buildhome/repo/src/data/blog/_templates/blog-post-template.md:0:0

🧠 根本原因

  1. 模板文件被误处理:Astro 内容集合系统尝试解析 _templates/ 目录下的模板文件
  2. 占位符格式问题:模板中的 {{date}}T{{time}}Z 是字符串占位符,不是有效的日期对象
  3. Schema 验证失败:内容集合 schema 要求 pubDatetimemodDatetime 必须是 z.date() 类型

🔧 解决方案

更新内容集合配置,明确排除模板目录:

📁 文件位置:src/content.config.ts

const blog = defineCollection({
  loader: glob({ 
    pattern: ["**/[^_]*.md", "!_templates/**"], // ← 添加排除规则
    base: `./${BLOG_PATH}` 
  }),
  // ... 其他配置
});

💡 最佳实践建议

  • 模板目录命名:始终使用 _ 开头的目录名(如 _templates)存放模板文件
  • 显式排除:在 glob 模式中明确添加排除规则 !_templates/**
  • Obsidian Templater 配置:确保日期输出为 ISO 8601 格式:
    pubDatetime: <%* tp.date.now("YYYY-MM-DDTHH:mm:ss") + "Z" %>
    ```

❌ 错误二:ImageNotFound(图片路径错误)

🔍 错误日志

[ImageNotFound] Could not find requested image `../../assets/images/forrest-gump-quote.png`
Location: /opt/buildhome/repo/src/data/blog/_releases/how-to-update-dependencies.md

🧠 根本原因

相对路径层级计算错误

  • 文件位置:src/data/blog/_releases/how-to-update-dependencies.md
  • 错误路径:../../assets/images/forrest-gump-quote.png(只回退了2级)
  • 正确路径:../../../assets/images/forrest-gump-quote.png(需要回退3级)

🔧 解决方案

修正相对路径层级:

📁 文件位置:src/data/blog/_releases/how-to-update-dependencies.md

# 修正前(错误)
ogImage: ../../assets/images/forrest-gump-quote.png

# 修正后(正确)
ogImage: ../../../assets/images/forrest-gump-quote.png

💡 路径计算规则

文件所在目录到项目根目录的层级ogImage 路径前缀
src/data/blog/3级../../../
src/data/blog/_releases/4级../../../../
src/data/blog/_examples/4级../../../../

🚀 更优解决方案建议

考虑使用 Astro 的 @/ 别名路径(如果配置支持):

ogImage: @/assets/images/forrest-gump-quote.png

这样可以避免手动计算相对路径层级的复杂性。

🧹 构建产物清理规范

🔧 清理命令

# 删除构建产物
rm -rf dist

# 删除搜索索引
rm -rf public/pagefind

📋 清理时机

  • 提交代码前
  • 重新构建前
  • 部署失败后排查问题时

📁 版本控制

确保 .gitignore 中包含以下条目:

dist/
public/pagefind/
.astro/

✅ 验证步骤

  1. 本地构建测试:运行 pnpm run build 确保无错误
  2. 清理构建产物:删除 dist/public/pagefind/
  3. 提交代码:推送修复后的代码到仓库
  4. 云端验证:观察 Cloudflare Pages 构建日志确认成功

📝 Notes 微博客系统

这次已经在 AstroPaper 里接入了独立的 Notes 系统,用来承载短文本、碎碎念和带图片的轻内容,不再和长文章混在一起。

📁 关键文件

  • src/content.config.ts
  • src/data/notes/
  • src/pages/notes/index.astro
  • src/components/NoteItem.astro
  • src/utils/getSortedNotes.ts
  • src/utils/noteFilter.ts

✅ 当前能力

  • Notes 内容单独放在 src/data/notes/
  • 使用独立 content collection,不需要 titledescription
  • 完整支持 Markdown 和图片渲染
  • pubDatetime 倒序排列
  • 沿用发布过滤逻辑,支持 draft
  • 页面路由为 /notes

Content Schema

const notes = defineCollection({
  loader: glob({
    pattern: ["**/[^_]*.md", "!_templates/**"],
    base: "./src/data/notes",
  }),
  schema: () =>
    z.object({
      pubDatetime: z.date(),
      modDatetime: z.date().optional().nullable(),
      draft: z.boolean().optional(),
      timezone: z.string().optional(),
    }),
});

推荐 Frontmatter

---
pubDatetime: 2026-03-30T21:10:00Z
---
天气不错,适合散步。今天折腾了 AstroPaper,感觉很棒。

页面表现

  • /notes 页面保留了两种展示方式:CardsLines
  • 每条 Note 的时间固定显示在右下角
  • 首屏先显示一部分内容,继续滚动时逐步加载
  • 没有分页,使用 IntersectionObserver 做渐进加载
  • 卡片和列表都保留了轻量动画,并兼容 prefers-reduced-motion

排序与过滤逻辑

const getSortedNotes = (notes: CollectionEntry<"notes">[]) => {
  return notes
    .filter(noteFilter)
    .sort(
      (a, b) =>
        Math.floor(new Date(b.data.pubDatetime).getTime() / 1000) -
        Math.floor(new Date(a.data.pubDatetime).getTime() / 1000)
    );
};

🔎 Search 页面重构

搜索页已经从”只有壳子”改成了真正可用的本地全文搜索,并且样式收敛到更接近 AstroPaper 的简洁风格。

📁 关键文件

  • src/pages/search.astro
  • src/env.d.ts
  • public/pagefind/

✅ 当前能力

  • 搜索路由为 /search
  • 搜索框支持即时搜索和回车搜索
  • 查询词会同步到 URL 参数 ?q=
  • 搜索结果会区分 PostsNotesPage
  • 搜索结果展示路径、标题、摘要和命中片段
  • 空状态、加载状态、错误状态都已处理

实现方式

搜索页直接调用 Pagefind 浏览器端索引:

const importPagefind = new Function(
  "return import('/pagefind/pagefind.js');"
) as () => Promise<PagefindModule>;

然后在前端执行:

  • pagefind.search(term):回车立即搜索
  • pagefind.debouncedSearch(term, {}, 180):输入时防抖搜索

开发时注意

Pagefind 依赖构建后的索引,所以本地如果第一次测试搜索,需要先运行一次构建命令:

pnpm run build

🧱 全局布局与显示逻辑整理

这次还顺手把页面的公共结构做了统一,不再由每个页面单独插入 HeaderFooter

📁 关键文件

  • src/layouts/Layout.astro
  • src/layouts/Main.astro
  • src/layouts/AboutLayout.astro
  • src/layouts/PostDetails.astro
  • src/components/Header.astro
  • src/components/Footer.astro

✅ 当前策略

  • Layout.astro 统一负责全局 Header、内容区和 Footer
  • 主页面默认使用固定 Header + 固定 Footer
  • 文章详情页显式关闭固定 Footer,保留自然阅读流
  • Main.astro 统一了标题、描述和正文之间的间距
  • 已移除页面顶部的面包屑导航,例如 Home » Search

Header 调整

  • Header 变为吸顶导航
  • 移动端菜单使用统一的展开/关闭逻辑
  • 当前导航项会自动高亮
  • 搜索和明暗模式按钮继续保留在导航区
  • 主页面 Footer 固定在视口底部
  • 为避免遮挡内容,布局层会自动给内容区预留底部空间
  • 文章详情页通过 fixedFooter: false 关闭固定 Footer

布局开关示例

const layoutProps = {
  title: `${title} | ${SITE.title}`,
  author,
  description,
  pubDatetime,
  modDatetime,
  canonicalURL,
  ogImage,
  scrollSmooth: true,
  fixedFooter: false,
};

📚 当前页面规则总结

  • 首页、Posts、Tags、Archives、Notes、Search、About、404:统一使用全局 Header 和 Footer
  • Posts 列表分页保留不变
  • Tags 分页保留不变
  • Notes 不分页,改为滚动渐进加载
  • 文章详情页保持阅读优先,不启用固定 Footer

✍️ 文章页排版修正

最近对文章详情页又做了一轮排版回收,重点是把阅读体验调顺,而不是单纯把字放大。

📁 关键文件

  • src/layouts/PostDetails.astro
  • src/styles/typography.css

这次修复了什么

  • 文章页重新对齐到主布局宽度,不再出现比 Header 更宽导致的视觉偏移
  • 文章标题去掉了会干扰混合中英文标题的平衡换行策略
  • 标题下方的日期和编辑链接缩小了一档,并收紧了上边距
  • 正文与 h2 / h3 / h4 的字号、行高、间距重新做了比例调整
  • 去掉了技术文章里不太合适的 h3 斜体效果

长标题换行 bug 的原因

长标题显示异常,主要不是单纯”字体太大”,而是两个因素叠加:

  1. 标题使用了 text-balance,浏览器会尝试把多行标题分配得尽量平均。
  2. 标题里像 WD-403-bit 这样的连字符词,浏览器会把 - 当作可断行点。

这两个规则叠加后,就会出现把一个词拆成两半的情况,比如:

WD-
40

解决方案

现在文章页会在渲染标题时,把字母或数字之间的普通连字符 - 转成不可断行连字符 ,避免这类词被拆开:

const displayTitle = title.replace(
  /([A-Za-z0-9])-([A-Za-z0-9])/g,
  "$1‑$2"
);

这样像 WD-403-bit 会整体换行,不会再被硬拆开。

📝 经验总结

  1. 内容集合隔离:模板文件、归档文件必须与实际博客文章物理隔离
  2. 路径精确计算:相对路径必须严格计算目录层级深度
  3. 本地先行验证:重要修改先在本地构建验证,再提交到云端
  4. 错误日志分析:仔细阅读构建日志,定位具体文件和行号

📁 内容集合配置优化(排除主题自带文档)

📌 问题背景

_examples_releases 目录包含 AstroPaper 主题自带的示例文章和发布说明文档。这些文件:

  • 不需要在博客中展示
  • 可能包含不符合当前 schema 的字段
  • 会增加不必要的构建处理时间

🔧 解决方案

更新内容集合 glob 模式,明确排除所有主题自带目录:

📁 文件位置:src/content.config.ts

const blog = defineCollection({
  loader: glob({ 
    pattern: [
      "**/[^_]*.md",     // 匹配所有不以 _ 开头的 Markdown 文件
      "!_templates/**",   // 排除模板目录
      "!_examples/**",    // 排除示例文章目录  
      "!_releases/**"     // 排除发布说明目录
    ],
    base: `./${BLOG_PATH}` 
  }),
  // ... 其他配置
});

💡 优势说明

  • 构建性能提升:减少不必要的文件处理
  • 错误预防:避免主题自带文档的 schema 不兼容问题
  • 内容纯净:确保博客只显示用户原创内容
  • 维护简便:无需手动删除或隐藏主题自带文件

📋 目录用途说明

目录用途是否排除原因
_templatesObsidian 博客模板✅ 是防止模板占位符导致构建错误
_examples主题示例文章✅ 是主题自带,无需展示
_releases主题发布说明✅ 是主题自带文档,无需展示
(根目录)用户原创文章❌ 否实际博客内容

🚀 最佳实践建议

  • 保留但排除:不要删除主题自带文件,便于参考和更新
  • 统一管理:所有非用户内容都使用 _ 前缀并统一排除
  • 定期检查:主题更新后检查是否有新的归档目录需要排除

💬 评论系统迁移(Giscus)

📁 关键文件

  • src/constants.ts
  • src/components/Comments.tsx
  • src/config.ts

迁移时需要注意的点

Giscus 不是只改仓库地址就能恢复。它依赖四个关键配置:

  1. repo
  2. repoId
  3. category
  4. categoryId

如果你删除旧仓库、重建新仓库,那么旧的 repoIdcategoryId 会直接失效。

Giscus 正常工作的前提

  • 仓库必须是 公开仓库
  • 仓库必须启用 GitHub Discussions
  • 必须安装 Giscus App

当前代码里的处理方式

为了避免配置未完成时前端直接显示报错,评论组件现在会先检查配置是否完整:

export const GISCUS_IS_CONFIGURED = Boolean(
  GISCUS.repo && GISCUS.repoId && GISCUS.category && GISCUS.categoryId
);

如果配置不完整,就不渲染 Giscus,而是显示一张提示卡片,引导去重新生成配置。

新仓库迁移后要改的地方

  • src/constants.ts 里的 repo
  • src/constants.ts 里的 repoId
  • src/constants.ts 里的 categoryId
  • src/config.ts 里的 SITE.editPost.url

可替代的评论系统

  • Giscus:最适合 GitHub 驱动博客,支持明暗主题,和 AstroPaper 兼容度高
  • Utterances:更轻,基于 GitHub Issues,接入简单,但表现比 Giscus 更朴素
  • Cusdis:界面干净,支持暗色模式和自托管,不依赖 GitHub Discussions
  • Waline:功能最丰富,可玩性高,但需要服务端和数据库,维护成本更高

编辑此页