feat(upgrade): 迁移到 Astro V6

This commit is contained in:
Ad-closeNN
2026-04-19 22:12:00 +08:00
parent a96c270bfe
commit 27a6f94f64
11 changed files with 2940 additions and 3437 deletions
+3 -1
View File
@@ -26,4 +26,6 @@ package-lock.json
bun.lockb bun.lockb
yarn.lock yarn.lock
src/content/.obsidian src/content/.obsidian
.playwright-mcp
+26 -26
View File
@@ -16,32 +16,32 @@
"preinstall": "npx only-allow pnpm" "preinstall": "npx only-allow pnpm"
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.9.4", "@astrojs/check": "^0.9.8",
"@astrojs/rss": "^4.0.12", "@astrojs/rss": "^4.0.18",
"@astrojs/sitemap": "^3.4.2", "@astrojs/sitemap": "^3.7.2",
"@astrojs/svelte": "7.1.0", "@astrojs/svelte": "8.0.5",
"@astrojs/tailwind": "^6.0.2", "@astrojs/tailwind": "^6.0.2",
"@expressive-code/core": "^0.41.3", "@expressive-code/core": "^0.41.7",
"@expressive-code/plugin-collapsible-sections": "^0.41.3", "@expressive-code/plugin-collapsible-sections": "^0.41.7",
"@expressive-code/plugin-line-numbers": "^0.41.3", "@expressive-code/plugin-line-numbers": "^0.41.7",
"@iconify-json/fa6-brands": "^1.2.6", "@iconify-json/fa6-brands": "^1.2.6",
"@iconify-json/fa6-regular": "^1.2.4", "@iconify-json/fa6-regular": "^1.2.4",
"@iconify-json/fa6-solid": "^1.2.4", "@iconify-json/fa6-solid": "^1.2.4",
"@iconify-json/ic": "^1.2.2", "@iconify-json/ic": "^1.2.4",
"@iconify-json/material-symbols": "^1.2.30", "@iconify-json/material-symbols": "^1.2.67",
"@iconify/svelte": "^4.2.0", "@iconify/svelte": "^4.2.0",
"@swup/astro": "^1.7.0", "@swup/astro": "^1.8.0",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.19",
"astro": "5.12.8", "astro": "6.1.8",
"astro-expressive-code": "^0.41.3", "astro-expressive-code": "^0.41.7",
"astro-icon": "^1.1.5", "astro-icon": "^1.1.5",
"hastscript": "^9.0.1", "hastscript": "^9.0.1",
"katex": "^0.16.22", "katex": "^0.16.45",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.1",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
"node-html-parser": "^7.0.1", "node-html-parser": "^7.1.0",
"overlayscrollbars": "^2.11.4", "overlayscrollbars": "^2.15.1",
"pagefind": "^1.3.0", "pagefind": "^1.5.2",
"photoswipe": "^5.4.4", "photoswipe": "^5.4.4",
"reading-time": "^1.5.0", "reading-time": "^1.5.0",
"rehype-autolink-headings": "^7.1.0", "rehype-autolink-headings": "^7.1.0",
@@ -54,22 +54,22 @@
"remark-github-admonitions-to-directives": "^1.0.5", "remark-github-admonitions-to-directives": "^1.0.5",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"remark-sectionize": "^2.1.0", "remark-sectionize": "^2.1.0",
"sanitize-html": "^2.17.0", "sanitize-html": "^2.17.3",
"sharp": "^0.34.3", "sharp": "^0.34.5",
"stylus": "^0.64.0", "stylus": "^0.64.0",
"svelte": "^5.37.3", "svelte": "^5.55.4",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.19",
"typescript": "^5.9.2", "typescript": "^5.9.3",
"unist-util-visit": "^5.0.0" "unist-util-visit": "^5.1.0"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/ts-plugin": "^1.10.4", "@astrojs/ts-plugin": "^1.10.7",
"@biomejs/biome": "2.1.3", "@biomejs/biome": "2.1.3",
"@rollup/plugin-yaml": "^4.1.2", "@rollup/plugin-yaml": "^4.1.2",
"@types/hast": "^3.0.4", "@types/hast": "^3.0.4",
"@types/markdown-it": "^14.1.2", "@types/markdown-it": "^14.1.2",
"@types/mdast": "^4.0.4", "@types/mdast": "^4.0.4",
"@types/sanitize-html": "^2.16.0", "@types/sanitize-html": "^2.16.1",
"postcss-import": "^16.1.1", "postcss-import": "^16.1.1",
"postcss-nesting": "^13.0.2" "postcss-nesting": "^13.0.2"
}, },
+2851 -3355
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -19,7 +19,7 @@ interface Post {
data: { data: {
title: string; title: string;
tags: string[]; tags: string[];
category?: string; category?: string | null;
published: Date; published: Date;
}; };
} }
+6 -5
View File
@@ -7,6 +7,7 @@ import { i18n } from "../i18n/translation";
import { getDir } from "../utils/url-utils"; import { getDir } from "../utils/url-utils";
import ImageWrapper from "./misc/ImageWrapper.astro"; import ImageWrapper from "./misc/ImageWrapper.astro";
import PostMetadata from "./PostMeta.astro"; import PostMetadata from "./PostMeta.astro";
import { render } from 'astro:content';
interface Props { interface Props {
class?: string; class?: string;
@@ -20,7 +21,7 @@ interface Props {
image: string; image: string;
description: string; description: string;
draft: boolean; draft: boolean;
style: string; style?: string;
} }
const { const {
entry, entry,
@@ -32,7 +33,7 @@ const {
category, category,
image, image,
description, description,
style, style = "",
} = Astro.props; } = Astro.props;
const className = Astro.props.class; const className = Astro.props.class;
@@ -40,7 +41,7 @@ const hasCover = image !== undefined && image !== null && image !== "";
const coverWidth = "28%"; const coverWidth = "28%";
const { remarkPluginFrontmatter } = await entry.render(); const { remarkPluginFrontmatter } = await render(entry);
--- ---
<div class:list={["card-base flex flex-col-reverse md:flex-col w-full rounded-[var(--radius-large)] overflow-hidden relative border border-black/20 dark:border-white/20 hover:scale-[1.02] hover:shadow-xl transition-all duration-[300ms]", className]} style={style}> <div class:list={["card-base flex flex-col-reverse md:flex-col w-full rounded-[var(--radius-large)] overflow-hidden relative border border-black/20 dark:border-white/20 hover:scale-[1.02] hover:shadow-xl transition-all duration-[300ms]", className]} style={style}>
<div class:list={["pl-6 md:pl-9 pr-6 md:pr-2 pt-6 md:pt-7 pb-6 relative", {"w-full md:w-[calc(100%_-_52px_-_12px)]": !hasCover, "w-full md:w-[calc(100%_-_var(--coverWidth)_-_12px)]": hasCover}]}> <div class:list={["pl-6 md:pl-9 pr-6 md:pr-2 pt-6 md:pt-7 pb-6 relative", {"w-full md:w-[calc(100%_-_52px_-_12px)]": !hasCover, "w-full md:w-[calc(100%_-_var(--coverWidth)_-_12px)]": hasCover}]}>
@@ -72,7 +73,7 @@ const { remarkPluginFrontmatter } = await entry.render();
<div>{remarkPluginFrontmatter.minutes} {" " + i18n(I18nKey.minutesCount)}</div> <div>{remarkPluginFrontmatter.minutes} {" " + i18n(I18nKey.minutesCount)}</div>
<div>|</div> <div>|</div>
<div> <div>
<span class="text-50 text-sm font-medium" id={`page-views-${entry.slug}`}>加载中...</span> <span class="text-50 text-sm font-medium" id={`page-views-${entry.id}`}>加载中...</span>
</div> </div>
</div> </div>
@@ -110,7 +111,7 @@ const { remarkPluginFrontmatter } = await entry.render();
<!-- https://github.com/afoim/fuwari/blob/81f22decb17ff7ee1dd480c10773f7ba8f4df296/src/components/PostCard.astro --> <!-- https://github.com/afoim/fuwari/blob/81f22decb17ff7ee1dd480c10773f7ba8f4df296/src/components/PostCard.astro -->
<script define:vars={{ slug: entry.slug }}> <script define:vars={{ slug: entry.id }}>
// 获取文章浏览量统计 // 获取文章浏览量统计
async function fetchPostCardViews(slug) { async function fetchPostCardViews(slug) {
const displayElement = document.getElementById(`page-views-${slug}`); const displayElement = document.getElementById(`page-views-${slug}`);
+1 -1
View File
@@ -17,7 +17,7 @@ const interval = 50;
category={entry.data.category} category={entry.data.category}
published={entry.data.published} published={entry.data.published}
updated={entry.data.updated} updated={entry.data.updated}
url={getPostUrlBySlug(entry.slug)} url={getPostUrlBySlug(entry.id)}
image={entry.data.image} image={entry.data.image}
description={entry.data.description} description={entry.data.description}
draft={entry.data.draft} draft={entry.data.draft}
+39
View File
@@ -0,0 +1,39 @@
import { defineCollection } from "astro:content";
import { z } from "astro/zod";
import { glob } from "astro/loaders";
const postsCollection = defineCollection({
loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/posts" }),
schema: z.object({
title: z.string(),
published: z.date(),
updated: z.date().optional(),
draft: z.boolean().optional().default(false),
description: z.string().optional().default(""),
image: z.string().optional().default(""),
tags: z.array(z.string()).optional().default([]),
category: z.string().optional().nullable().default(""),
showcover: z.boolean().optional().default(true),
customcover: z.string().optional().default(""),
lang: z.string().optional().default(""),
prevTitle: z.string().default(""),
prevSlug: z.string().default(""),
nextTitle: z.string().default(""),
nextSlug: z.string().default(""),
}),
});
const specCollection = defineCollection({
loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/spec" }),
schema: z.object({
title: z.string().optional(),
published: z.date().optional(),
updated: z.date().optional(),
draft: z.boolean().optional().default(false),
}),
});
export const collections = {
posts: postsCollection,
spec: specCollection,
};
-37
View File
@@ -1,37 +0,0 @@
import { defineCollection, z } from "astro:content";
const postsCollection = defineCollection({
schema: z.object({
title: z.string(),
published: z.date(),
updated: z.date().optional(),
draft: z.boolean().optional().default(false),
description: z.string().optional().default(""),
image: z.string().optional().default(""),
tags: z.array(z.string()).optional().default([]),
category: z.string().optional().nullable().default(""),
showcover: z.boolean().optional().default(true),
customcover: z.string().optional().default(""),
lang: z.string().optional().default(""),
/* For internal use */
prevTitle: z.string().default(""),
prevSlug: z.string().default(""),
nextTitle: z.string().default(""),
nextSlug: z.string().default(""),
}),
});
/* https://github.com/afoim/fuwari/commit/8b651d5d4e666c520d8fc95e89bf9d0ecf307644#diff-544dcd1cb4d05890db2dcf497052df475216a57683c346216e43133407b7ea58 */
const specCollection = defineCollection({
schema: z.object({
title: z.string().optional(),
published: z.date().optional(),
updated: z.date().optional(),
draft: z.boolean().optional().default(false),
}),
});
export const collections = {
posts: postsCollection,
spec: specCollection,
};
+8 -5
View File
@@ -1,5 +1,6 @@
--- ---
import path from "node:path"; import path from "node:path";
import { render, type CollectionEntry } from "astro:content";
import License from "@components/misc/License.astro"; import License from "@components/misc/License.astro";
import Markdown from "@components/misc/Markdown.astro"; import Markdown from "@components/misc/Markdown.astro";
import I18nKey from "@i18n/i18nKey"; import I18nKey from "@i18n/i18nKey";
@@ -17,15 +18,17 @@ import { formatDateToYYYYMMDD } from "../../utils/date-utils";
export async function getStaticPaths() { export async function getStaticPaths() {
const blogEntries = await getSortedPosts(); const blogEntries = await getSortedPosts();
return blogEntries.map((entry) => ({ return blogEntries.map((entry) => ({
params: { slug: entry.slug }, params: { slug: entry.id },
props: { entry }, props: { entry },
})); }));
} }
const { entry } = Astro.props; interface Props {
const { Content, headings } = await entry.render(); entry: CollectionEntry<"posts">;
}
const { remarkPluginFrontmatter } = await entry.render(); const { entry }: Props = Astro.props;
const { Content, headings, remarkPluginFrontmatter } = await render(entry);
const jsonLd = { const jsonLd = {
"@context": "https://schema.org", "@context": "https://schema.org",
@@ -134,7 +137,7 @@ const customcover = entry.data.customcover;
<Content /> <Content />
</Markdown> </Markdown>
{licenseConfig.enable && <License title={entry.data.title} slug={entry.slug} pubDate={entry.data.published} class="mb-6 rounded-xl license-container onload-animation"></License>} {licenseConfig.enable && <License title={entry.data.title} slug={entry.id} pubDate={entry.data.published} class="mb-6 rounded-xl license-container onload-animation"></License>}
</div> </div>
</div> </div>
+2 -3
View File
@@ -1,7 +1,6 @@
import rss from '@astrojs/rss'; import rss from '@astrojs/rss';
import sanitizeHtml from 'sanitize-html'; import sanitizeHtml from 'sanitize-html';
import MarkdownIt from 'markdown-it'; import MarkdownIt from 'markdown-it';
import { getCollection } from 'astro:content';
import { siteConfig } from '@/config'; import { siteConfig } from '@/config';
import { parse as htmlParser } from 'node-html-parser'; import { parse as htmlParser } from 'node-html-parser';
import { getImage } from 'astro:assets'; import { getImage } from 'astro:assets';
@@ -27,7 +26,7 @@ export async function GET(context: APIContext) {
for (const post of posts) { for (const post of posts) {
// convert markdown to html string // convert markdown to html string
const body = markdownParser.render(post.body); const body = markdownParser.render(post.body ?? '');
// convert html string to DOM-like structure // convert html string to DOM-like structure
const html = htmlParser.parse(body); const html = htmlParser.parse(body);
// hold all img tags in variable images // hold all img tags in variable images
@@ -66,7 +65,7 @@ export async function GET(context: APIContext) {
title: post.data.title, title: post.data.title,
description: post.data.description, description: post.data.description,
pubDate: post.data.published, pubDate: post.data.published,
link: `/posts/${post.slug}/`, link: `/posts/${post.id}/`,
// sanitize the new html string with corrected image paths // sanitize the new html string with corrected image paths
content: sanitizeHtml(html.toString(), { content: sanitizeHtml(html.toString(), {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']), allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
+3 -3
View File
@@ -21,11 +21,11 @@ export async function getSortedPosts() {
const sorted = await getRawSortedPosts(); const sorted = await getRawSortedPosts();
for (let i = 1; i < sorted.length; i++) { for (let i = 1; i < sorted.length; i++) {
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++) { for (let i = 0; i < sorted.length - 1; i++) {
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;
} }
@@ -40,7 +40,7 @@ export async function getSortedPostsList(): Promise<PostForList[]> {
// delete post.body // delete post.body
const sortedPostsList = sortedFullPosts.map((post) => ({ const sortedPostsList = sortedFullPosts.map((post) => ({
slug: post.slug, slug: post.id,
data: post.data, data: post.data,
})); }));