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