From ed5c0c343ac884c492ad4b9672652625fe8f7930 Mon Sep 17 00:00:00 2001 From: biss Date: Fri, 17 Jul 2026 16:56:32 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BF=AE=E8=AE=A2=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/PostChangelog.astro | 34 ++++ src/content.config.ts | 5 + .../2025.08/use-github-to-deploy-on-server.md | 9 +- src/lib/posts.ts | 69 +++++++- src/pages/posts/[slug].astro | 49 ++++-- src/styles/site.css | 1 + src/styles/site/17-revisions.css | 150 ++++++++++++++++++ 7 files changed, 306 insertions(+), 11 deletions(-) create mode 100644 src/components/PostChangelog.astro create mode 100644 src/styles/site/17-revisions.css diff --git a/src/components/PostChangelog.astro b/src/components/PostChangelog.astro new file mode 100644 index 0000000..db7e510 --- /dev/null +++ b/src/components/PostChangelog.astro @@ -0,0 +1,34 @@ +--- +import type { PostChangelogEntry } from '@/lib/posts'; + +interface Props { + entries: PostChangelogEntry[]; +} + +const { entries } = Astro.props; +--- + +
+
+ +
+ REVISION HISTORY +

文章修订记录

+
+ {entries.length} 次 +
+
    + {entries.map((entry) => ( +
  1. + +
    +
    +

    {entry.title}

    + {entry.date && } +
    + {entry.description &&

    {entry.description}

    } +
    +
  2. + ))} +
+
diff --git a/src/content.config.ts b/src/content.config.ts index c6be834..6492845 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -17,6 +17,11 @@ const posts = defineCollection({ cover: z.any().optional(), series: z.any().optional(), seriesOrder: z.any().optional(), + changelog: z.any().optional(), + newVersion: z.any().optional(), + newVersionTitle: z.any().optional(), + supersededBy: z.any().optional(), + replacement: z.any().optional(), }).passthrough(), }); diff --git a/src/content/posts/2025/2025.08/use-github-to-deploy-on-server.md b/src/content/posts/2025/2025.08/use-github-to-deploy-on-server.md index e9379f5..fa39050 100644 --- a/src/content/posts/2025/2025.08/use-github-to-deploy-on-server.md +++ b/src/content/posts/2025/2025.08/use-github-to-deploy-on-server.md @@ -3,6 +3,13 @@ title: 使用GitHub推送Hexo到服务器 categories: 建站手札 cover: https://pic.biss.click/image/e0a08509-dea4-4af7-bafa-c81ca9d1cf8d.webp series: webcustom +updated: 2026-07-17 13:20:00 +newVersion: 8f3b9d21 +newVersionTitle: 网站迁移日志:从 Hexo 到 Astro +changelog: + - date: 2026-07-17 13:20:00 + title: 增加新版入口 + description: 标记本文为早期 Hexo 部署记录,并补充当前 Astro 版本的网站迁移日志入口。 tags: 网站 abbrlink: ce1ec3fe summary: >- @@ -135,4 +142,4 @@ jobs: `KEY`:密钥(私钥) `PORT`:SSH登录端口,一般为22 # 测试 -运行一下这个Action无报错即可。 \ No newline at end of file +运行一下这个Action无报错即可。 diff --git a/src/lib/posts.ts b/src/lib/posts.ts index 4838c96..51ec4f4 100644 --- a/src/lib/posts.ts +++ b/src/lib/posts.ts @@ -1,6 +1,13 @@ import type { CollectionEntry } from 'astro:content'; import { getCollection } from 'astro:content'; +export type PostChangelogEntry = { + date?: Date; + dateText?: string; + title: string; + description?: string; +}; + export type BlogPost = { entry: CollectionEntry<'posts'>; title: string; @@ -13,6 +20,9 @@ export type BlogPost = { categories: string[]; series?: string; seriesOrder?: number; + changelog: PostChangelogEntry[]; + newVersion?: string; + newVersionTitle?: string; summary: string; comments: boolean; cover?: string; @@ -62,6 +72,54 @@ function formatDate(date: Date): string { }).format(date); } +function makeChangelog(value: unknown): PostChangelogEntry[] { + if (!Array.isArray(value)) return []; + + return value + .flatMap((item) => { + if (typeof item === 'string' && item.trim()) { + return [{ title: '内容更新', description: item.trim() }]; + } + + if (!item || typeof item !== 'object') return []; + const data = item as Record; + const date = toOptionalDate(data.date ?? data.updated ?? data.at); + const titleValue = data.title ?? data.version ?? '内容更新'; + const descriptionValue = data.description ?? data.note ?? data.content ?? data.changes; + const description = Array.isArray(descriptionValue) + ? descriptionValue.map(String).filter(Boolean).join(';') + : typeof descriptionValue === 'string' && descriptionValue.trim() + ? descriptionValue.trim() + : undefined; + const title = String(titleValue).trim() || '内容更新'; + + if (!date && !description && title === '内容更新') return []; + return [{ date, dateText: date ? formatDate(date) : undefined, title, description }]; + }) + .sort((a, b) => (b.date?.valueOf() ?? 0) - (a.date?.valueOf() ?? 0)); +} + +function getNewVersion(data: Record): { href?: string; title?: string } { + const value = data.newVersion ?? data.supersededBy ?? data.replacement; + if (typeof value === 'string' && value.trim()) { + return { + href: value.trim(), + title: typeof data.newVersionTitle === 'string' ? data.newVersionTitle.trim() || undefined : undefined, + }; + } + + if (value && typeof value === 'object') { + const item = value as Record; + const hrefValue = item.href ?? item.url ?? item.slug; + return { + href: typeof hrefValue === 'string' ? hrefValue.trim() || undefined : undefined, + title: typeof item.title === 'string' ? item.title.trim() || undefined : undefined, + }; + } + + return {}; +} + function fallbackSlug(id: string): string { return id .split('/') @@ -88,8 +146,14 @@ export async function getAllPosts(): Promise { return entries .map((entry) => { + const data = entry.data as Record; const date = toDate(entry.data.date); - const updated = toOptionalDate(entry.data.updated); + const changelog = makeChangelog(data.changelog); + const explicitUpdated = toOptionalDate(entry.data.updated); + const latestChangelogDate = changelog.find((item) => item.date)?.date; + const updatedCandidates = [explicitUpdated, latestChangelogDate].filter((item): item is Date => Boolean(item)); + const updated = updatedCandidates.sort((a, b) => b.valueOf() - a.valueOf())[0]; + const newVersion = getNewVersion(data); const slug = String(entry.data.abbrlink ?? fallbackSlug(entry.id)); const title = String(entry.data.title ?? slug); const series = typeof entry.data.series === 'string' && entry.data.series.trim() @@ -109,6 +173,9 @@ export async function getAllPosts(): Promise { categories: getCategories(entry), series, seriesOrder: Number.isFinite(rawSeriesOrder) ? rawSeriesOrder : undefined, + changelog, + newVersion: newVersion.href, + newVersionTitle: newVersion.title, summary: makeSummary(entry), comments: entry.data.comments !== false, cover: typeof entry.data.cover === 'string' ? entry.data.cover : undefined, diff --git a/src/pages/posts/[slug].astro b/src/pages/posts/[slug].astro index 4076845..85cdeef 100644 --- a/src/pages/posts/[slug].astro +++ b/src/pages/posts/[slug].astro @@ -1,6 +1,7 @@ --- import ArticleSidebar from '@/components/ArticleSidebar.astro'; import PostCopyrightCard from '@/components/PostCopyrightCard.astro'; +import PostChangelog from '@/components/PostChangelog.astro'; import PostSeries from '@/components/PostSeries.astro'; import TwikooComments from '@/components/TwikooComments.astro'; import BaseLayout from '@/layouts/BaseLayout.astro'; @@ -89,6 +90,18 @@ const seriesPosts = post.series : []; const canonicalUrl = new URL(post.href, siteConfig.site.url).href; const articleImage = new URL(post.cover ?? siteConfig.author.avatar, siteConfig.site.url).href; +function getNewVersionHref(value?: string): string | undefined { + if (!value) return undefined; + if (/^https?:\/\//i.test(value) || value.startsWith('/')) return value; + return `/posts/${encodeURIComponent(value.replace(/^posts\//, '').replace(/\/$/, ''))}/`; +} + +const newVersionHref = getNewVersionHref(post.newVersion); +const newVersionPost = newVersionHref + ? allPosts.find((item) => item.href === newVersionHref || item.slug === post.newVersion) + : undefined; +const newVersionTitle = post.newVersionTitle ?? newVersionPost?.title ?? '查看新版文章'; +const newVersionExternal = Boolean(newVersionHref && /^https?:\/\//i.test(newVersionHref)); const articleJsonLd = { '@context': 'https://schema.org', '@type': 'BlogPosting', @@ -144,7 +157,10 @@ const articleJsonLd = {
{post.cover && }
- +

{post.title}

{post.cover ? (
@@ -181,19 +197,33 @@ const articleJsonLd = {
+ {post.changelog.length > 0 && } {post.comments && } @@ -288,13 +318,14 @@ const articleJsonLd = { diff --git a/src/styles/site.css b/src/styles/site.css index 8fb6d7e..e3a7300 100644 --- a/src/styles/site.css +++ b/src/styles/site.css @@ -14,3 +14,4 @@ @import "./site/14-animations.css"; @import "./site/15-reading-series.css"; @import "./site/16-pwa.css"; +@import "./site/17-revisions.css"; diff --git a/src/styles/site/17-revisions.css b/src/styles/site/17-revisions.css new file mode 100644 index 0000000..cb53e31 --- /dev/null +++ b/src/styles/site/17-revisions.css @@ -0,0 +1,150 @@ +.article-date-line { + display: flex; + flex-wrap: wrap; + gap: 4px 14px; + color: var(--muted); + font-size: 0.92rem; +} + +.article-date-line time + time::before { + content: "·"; + margin-right: 14px; +} + +.article-outdated-note a { + display: inline-flex; + align-items: center; + gap: 6px; + margin: 0 3px; + color: inherit; + font-weight: 900; + text-decoration: underline; + text-underline-offset: 3px; +} + +.post-changelog { + margin-top: 38px; + overflow: hidden; + border: 1px solid rgba(15, 118, 110, 0.18); + border-radius: 12px; + background: + radial-gradient(circle at 10% 0%, rgba(255, 255, 255, 0.78), transparent 30%), + rgba(236, 253, 245, 0.46); +} + +.post-changelog-head { + display: grid; + grid-template-columns: 44px minmax(0, 1fr) auto; + align-items: center; + gap: 13px; + border-bottom: 1px solid rgba(15, 118, 110, 0.14); + padding: 17px 20px; +} + +.post-changelog-icon { + display: grid; + width: 44px; + height: 44px; + place-items: center; + border-radius: 12px; + color: #fff; + background: #3eb8be; +} + +.post-changelog-head span { + color: var(--accent); + font-size: 0.7rem; + font-weight: 900; + letter-spacing: 0.1em; +} + +.post-changelog-head h2 { + margin: 1px 0 0; + font-size: 1.2rem; +} + +.post-changelog-head > strong { + color: var(--muted); + font-size: 0.84rem; +} + +.post-changelog-list { + margin: 0; + padding: 16px 20px 18px 34px; + list-style: none; +} + +.post-changelog-list li { + position: relative; + border-left: 1px solid rgba(15, 118, 110, 0.22); + padding: 0 0 20px 22px; +} + +.post-changelog-list li:last-child { + padding-bottom: 0; +} + +.post-changelog-marker { + position: absolute; + top: 8px; + left: -5px; + width: 9px; + height: 9px; + border: 2px solid var(--surface); + border-radius: 999px; + background: #3eb8be; + box-shadow: 0 0 0 3px rgba(62, 184, 190, 0.16); +} + +.post-changelog-entry > div { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 16px; +} + +.post-changelog-entry h3, +.post-changelog-entry p { + margin: 0; +} + +.post-changelog-entry h3 { + font-size: 1rem; +} + +.post-changelog-entry time { + flex: 0 0 auto; + color: var(--muted); + font-size: 0.82rem; +} + +.post-changelog-entry p { + margin-top: 4px; + color: var(--muted); + font-size: 0.92rem; + line-height: 1.7; +} + +:root[data-theme='dark'] .post-changelog { + border-color: rgba(139, 224, 213, 0.22); + background: + radial-gradient(circle at 10% 0%, rgba(139, 224, 213, 0.1), transparent 32%), + rgba(13, 23, 29, 0.88); +} + +:root[data-theme='dark'] .post-changelog-list li { + border-left-color: rgba(139, 224, 213, 0.24); +} + +:root[data-theme='dark'] .post-changelog-marker { + border-color: #132028; +} + +@media (max-width: 720px) { + .article-date-line { display: grid; gap: 1px; } + .article-date-line time + time::before { content: none; } + .post-changelog-head { grid-template-columns: 40px minmax(0, 1fr) auto; padding: 15px; } + .post-changelog-icon { width: 40px; height: 40px; } + .post-changelog-list { padding-right: 15px; padding-left: 27px; } + .post-changelog-entry > div { align-items: flex-start; flex-direction: column; gap: 1px; } +}