chore: 升级 Astro 5.16.5 → 7.3.3

依赖
- astro 5 → 7.3.3,@astrojs/{mdx,react,rss,sitemap,check}、astro-expressive-code
  与 @swup/astro 同步升到最新
- 新增 @astrojs/markdown-remark(Astro 7 默认改用 Sätteri 处理器,
  本项目依赖大量 remark/rehype 插件,故显式固定回 unified 管线)

内容集合迁移到 Content Layer API(Astro 6 已移除旧 API)
- src/content/config.ts → src/content.config.ts,三个集合改用 glob() loader
- z 改从 astro/zod 导入
- 所有 entry.slug → entry.id;entry.render() → render(entry)

Astro 7 兼容
- 修复 bookshelf-content.astro 里多行 {/* */} 注释导致的 Rust 编译器
  "Unterminated regular expression"
- 删除该文件里一处多余的 </div>(旧 Go 编译器会静默纠正,新编译器不会)
- global.css: @import "tailwindcss" → "tailwindcss/index.css"
  (Vite 8 不再解析裸包名的 style 条件)
- astro.config.mjs 显式设置 compressHTML: true,保持 v6 的空白处理行为

验证:构建 41 页通过;与线上(Astro 5 构建)对比 39 个 URL 无一丢失,
仅新增新文章 DIY-computer 的 2 个 URL;逐页渲染文本与线上一致;
pnpm build 与 dev server 均正常。
This commit is contained in:
2026-09-17 17:14:08 +08:00
parent fe9adb246f
commit 738fe3175e
15 changed files with 3826 additions and 3492 deletions
+50 -46
View File
@@ -21,6 +21,7 @@ import { remarkExcerpt } from "./src/plugins/remark-excerpt.js"
import { remarkReadingTime } from "./src/plugins/remark-reading-time.mjs"
import { pluginLanguageBadge } from "./src/plugins/expressive-code/language-badge"
import { pluginCustomCopyButton } from "./src/plugins/expressive-code/custom-copy-button"
import { unified } from "@astrojs/markdown-remark"
import { site } from "./src/data/site"
const createAdmonitionComponent = (type) => (properties = {}, children = []) => {
@@ -85,57 +86,60 @@ export default defineConfig({
sitemap(),
mdx(),
],
compressHTML: true,
markdown: {
remarkPlugins: [
remarkMath,
remarkReadingTime,
remarkExcerpt,
remarkShiftHeadings,
remarkGithubAdmonitionsToDirectives,
remarkDirective,
remarkSectionize,
parseDirectiveNode,
],
rehypePlugins: [
rehypeKatex,
rehypeSlug,
[
rehypeComponents,
{
components: {
github: GithubCardComponent,
note: createAdmonitionComponent("note"),
tip: createAdmonitionComponent("tip"),
important: createAdmonitionComponent("important"),
caution: createAdmonitionComponent("caution"),
warning: createAdmonitionComponent("warning"),
},
},
processor: unified({
remarkPlugins: [
remarkMath,
remarkReadingTime,
remarkExcerpt,
remarkShiftHeadings,
remarkGithubAdmonitionsToDirectives,
remarkDirective,
remarkSectionize,
parseDirectiveNode,
],
[
rehypeAutolinkHeadings,
{
behavior: "append",
properties: {
className: ["anchor"],
},
content: {
type: "element",
tagName: "span",
properties: {
className: ["anchor-icon"],
"data-pagefind-ignore": true,
rehypePlugins: [
rehypeKatex,
rehypeSlug,
[
rehypeComponents,
{
components: {
github: GithubCardComponent,
note: createAdmonitionComponent("note"),
tip: createAdmonitionComponent("tip"),
important: createAdmonitionComponent("important"),
caution: createAdmonitionComponent("caution"),
warning: createAdmonitionComponent("warning"),
},
children: [
{
type: "text",
value: "#",
},
],
},
},
],
[
rehypeAutolinkHeadings,
{
behavior: "append",
properties: {
className: ["anchor"],
},
content: {
type: "element",
tagName: "span",
properties: {
className: ["anchor-icon"],
"data-pagefind-ignore": true,
},
children: [
{
type: "text",
value: "#",
},
],
},
},
],
],
],
}),
},
vite: {
server: {
+11 -10
View File
@@ -14,14 +14,15 @@
"reading": "node scripts/fetch-reading.mjs"
},
"dependencies": {
"@astrojs/mdx": "^5.0.6",
"@astrojs/react": "^4.4.2",
"@astrojs/rss": "^4.0.12",
"@astrojs/sitemap": "^3.5.1",
"@astrojs/markdown-remark": "^7.3.1",
"@astrojs/mdx": "^8.0.1",
"@astrojs/react": "^6.0.6",
"@astrojs/rss": "^4.0.19",
"@astrojs/sitemap": "^3.7.4",
"@aws-sdk/client-s3": "^3.1037.0",
"@expressive-code/core": "^0.41.5",
"@expressive-code/plugin-collapsible-sections": "^0.41.5",
"@expressive-code/plugin-line-numbers": "^0.41.5",
"@expressive-code/core": "^0.44.2",
"@expressive-code/plugin-collapsible-sections": "^0.44.2",
"@expressive-code/plugin-line-numbers": "^0.44.2",
"@hookform/resolvers": "^3.10.0",
"@radix-ui/react-accordion": "1.2.2",
"@radix-ui/react-alert-dialog": "1.1.4",
@@ -50,9 +51,9 @@
"@radix-ui/react-toggle": "1.1.1",
"@radix-ui/react-toggle-group": "1.1.1",
"@radix-ui/react-tooltip": "1.1.6",
"@swup/astro": "^1.7.0",
"@swup/astro": "^1.8.0",
"@tabler/icons-react": "^3.40.0",
"astro-expressive-code": "^0.41.5",
"astro-expressive-code": "^0.44.2",
"autoprefixer": "^10.4.20",
"bcryptjs": "^3.0.2",
"class-variance-authority": "^0.7.1",
@@ -102,7 +103,7 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/sanitize-html": "^2.16.0",
"astro": "^5.16.5",
"astro": "^7.3.3",
"postcss": "^8.5",
"tailwindcss": "^4.1.9",
"tw-animate-css": "1.3.3",
+3733 -3404
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -5,3 +5,7 @@ allowBuilds:
esbuild: true
sharp: true
swup: true
minimumReleaseAgeExclude:
- '@astrojs/react@6.0.6'
- astro@7.3.3
+3 -10
View File
@@ -163,9 +163,7 @@ const recommendBadge = (status?: string) => {
{/* ========================= Main ========================= */}
<section class="flex-1 min-w-0" data-bookshelf-main>
{/* ========================= 最近在读(微信读书) =========================
数据在构建期由 scripts/fetch-reading.mjs 抓到 src/data/reading.json,
这里只负责渲染,页面本身不发任何请求。 */}
{/* ========================= 最近在读(微信读书) ========================= 数据在构建期由 scripts/fetch-reading.mjs 抓到 src/data/reading.json, 这里只负责渲染,页面本身不发任何请求。 */}
{isReadingView && (
<div id="reading" class="mb-16 scroll-mt-24">
<div class="flex flex-wrap items-end justify-between gap-x-6 gap-y-2 mb-6">
@@ -205,9 +203,7 @@ const recommendBadge = (status?: string) => {
<div class="text-sm italic font-serif text-muted-foreground">博学而笃志,切问而近思</div>
</div>
{/* ---------- Stats + Search + filters ----------
只有存在手工书单时才显示。books.json 为空时这三行都是「0 本已读 /
0 本推荐 / 0 个领域」加一排没东西可筛的按钮,纯占地方。 */}
{/* ---------- Stats + Search + filters ---------- 只有存在手工书单时才显示。books.json 为空时这三行都是「0 本已读 / 0 本推荐 / 0 个领域」加一排没东西可筛的按钮,纯占地方。 */}
{hasManualBooks && (
<>
<div class="flex flex-wrap gap-x-7 gap-y-2 text-sm text-muted-foreground mb-7">
@@ -340,9 +336,7 @@ const recommendBadge = (status?: string) => {
</section>
))}
{/* 微信读书里已读完的书,并入「书籍」区。
卡片用和「最近在读」同一个 ReadingCard 组件,只是底部换成「开始/读完」两行、没有进度条。
data-weread-card 让搜索框和领域/评价筛选同样作用于这一组。 */}
{/* 微信读书里已读完的书,并入「书籍」区。 卡片用和「最近在读」同一个 ReadingCard 组件,只是底部换成「开始/读完」两行、没有进度条。 data-weread-card 让搜索框和领域/评价筛选同样作用于这一组。 */}
{finishedReading.length > 0 && (
<section data-books-section={WEREAD_CATEGORY}>
<div class="flex items-center gap-3 mb-5">
@@ -363,7 +357,6 @@ const recommendBadge = (status?: string) => {
/>
))}
</div>
</div>
</section>
)}
</div>
@@ -1,7 +1,9 @@
import { defineCollection, z } from "astro:content"
import { defineCollection } from "astro:content"
import { glob } from "astro/loaders"
import { z } from "astro/zod"
const posts = defineCollection({
type: "content",
loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/posts" }),
schema: z.object({
title: z.string(),
published: z.date(),
@@ -26,7 +28,7 @@ const posts = defineCollection({
})
const thoughts = defineCollection({
type: "content",
loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/thoughts" }),
schema: z.object({
title: z.string().optional(),
published: z.date(),
@@ -43,9 +45,9 @@ const thoughts = defineCollection({
})
const studies = defineCollection({
type: "content",
// 注意:frontmatter 里的 `slug` 字段被 Astro 当 reserved key 处理(用作 URL slug),
// 不会出现在 schema 校验的 data 里。所以这里不声明 slug,直接读 entry.slug。
loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/studies" }),
// 注意:`id` 由 glob loader 从文件路径生成(Content Layer API),
// 所以这里不声明 slug,直接读 entry.id。
schema: z.object({
title: z.string(),
status: z.enum(["在读", "沉淀中", "暂搁", "已结"]),
+3 -3
View File
@@ -41,11 +41,11 @@ export async function getSortedPosts(ignorePinned = false): Promise<PostEntry[]>
export async function getPostsWithNav(): Promise<PostEntry[]> {
const sorted = await getSortedPosts()
for (let i = 1; i < sorted.length; i += 1) {
sorted[i].data.nextSlug = sorted[i - 1].slug
sorted[i].data.nextSlug = sorted[i - 1].id
sorted[i].data.nextTitle = sorted[i - 1].data.title
}
for (let i = 0; i < sorted.length - 1; i += 1) {
sorted[i].data.prevSlug = sorted[i + 1].slug
sorted[i].data.prevSlug = sorted[i + 1].id
sorted[i].data.prevTitle = sorted[i + 1].data.title
}
return sorted
@@ -63,7 +63,7 @@ export const toPostMeta = (post: PostEntry) => {
const category = (post.data.category ?? "").toString().trim() || "未分类"
return {
slug: post.slug,
slug: post.id,
title: post.data.title,
excerpt,
category,
+1 -1
View File
@@ -62,7 +62,7 @@ export async function getSiteStats(latestCount = 4): Promise<SiteStats> {
.slice(0, latestCount)
.map((post) => ({
title: post.data.title,
href: `/posts/${post.slug}/`,
href: `/posts/${post.id}/`,
date: new Date(post.data.published).toISOString().slice(0, 10),
}))
+2 -2
View File
@@ -307,8 +307,8 @@ function shape(entry: StudyEntry, idx: number): StudyData {
const no = String(idx + 1).padStart(2, "0")
return {
id: entry.slug,
slug: entry.slug,
id: entry.id,
slug: entry.id,
no,
title: entry.data.title,
subtitle: entry.data.subtitle,
+2 -2
View File
@@ -57,7 +57,7 @@ export const toThoughtMeta = (thought: ThoughtEntry) => {
const content = md.render(body)
return {
slug: thought.slug,
slug: thought.id,
title: thought.data.title,
content,
date: formatDate(thought.data.published),
@@ -75,7 +75,7 @@ export const toThoughtPreview = (thought: ThoughtEntry, maxLength = 96) => {
const text = stripHtml(content)
return {
slug: thought.slug,
slug: thought.id,
title: thought.data.title,
date: formatDate(thought.data.published),
excerpt: truncateText(text, maxLength),
+1 -1
View File
@@ -4,7 +4,7 @@ import { getSortedPosts } from "@/lib/posts"
export async function getStaticPaths() {
const posts = await getSortedPosts(true)
return posts.map((post) => ({
params: { slug: post.slug },
params: { slug: post.id },
}))
}
+5 -4
View File
@@ -4,6 +4,7 @@ import ArticleContent from "@/components/ArticleContent.astro"
import SiteHeader from "@/components/site-header.astro"
import Footer from "@/components/footer.astro"
import { getPostsWithNav, type PostEntry } from "@/lib/posts"
import { render } from "astro:content"
import Disclaimer from "@/components/Disclaimer.astro"
import PasswordProtection from "@/components/PasswordProtection.astro"
import Comments from "@/components/Comments.astro"
@@ -15,7 +16,7 @@ import { site, siteTitleSuffix } from "@/data/site"
export async function getStaticPaths() {
const posts = await getPostsWithNav()
return posts.map((post) => ({
params: { slug: post.slug },
params: { slug: post.id },
props: { post },
}))
}
@@ -26,7 +27,7 @@ if (!post) {
return Astro.redirect("/")
}
const { Content, remarkPluginFrontmatter } = await post.render()
const { Content, remarkPluginFrontmatter } = await render(post)
const words = remarkPluginFrontmatter?.words
const minutes = remarkPluginFrontmatter?.minutes
@@ -113,8 +114,8 @@ if (post.data.encrypted && post.data.password) {
</div>
{encryptedData ? (
<div class="mb-6">
<PasswordProtection encryptedData={encryptedData} slug={post.slug} class="mb-6 rounded-xl" />
<div id={`article-content-${post.slug}`} class="mb-6 hidden encrypted-content">
<PasswordProtection encryptedData={encryptedData} slug={post.id} class="mb-6 rounded-xl" />
<div id={`article-content-${post.id}`} class="mb-6 hidden encrypted-content">
<!-- 内容将在验证密码后动态插入 -->
</div>
</div>
+1 -1
View File
@@ -30,7 +30,7 @@ export async function GET(context: APIContext) {
title: post.data.title,
pubDate: post.data.published,
description: post.data.description || "",
link: `/posts/${post.slug}/`,
link: `/posts/${post.id}/`,
}
if (index >= FULL_CONTENT_LIMIT) return item
+1 -1
View File
@@ -12,7 +12,7 @@ const posts = await getSortedPosts(true)
// 转换为时间线所需的简化数据
const timelineItems = posts.map((post) => ({
slug: post.slug,
slug: post.id,
title: post.data.title,
date: post.data.published.toISOString().slice(0, 10),
}))
+1 -1
View File
@@ -1,5 +1,5 @@
@import "./transition.css";
@import "tailwindcss";
@import "tailwindcss/index.css";
@import "tw-animate-css";
@plugin "@tailwindcss/typography";