添加修订功能
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
---
|
||||
import type { PostChangelogEntry } from '@/lib/posts';
|
||||
|
||||
interface Props {
|
||||
entries: PostChangelogEntry[];
|
||||
}
|
||||
|
||||
const { entries } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="post-changelog" id="article-changelog" aria-labelledby="article-changelog-title">
|
||||
<header class="post-changelog-head">
|
||||
<div class="post-changelog-icon" aria-hidden="true"><i class="fa-solid fa-clock-rotate-left"></i></div>
|
||||
<div>
|
||||
<span>REVISION HISTORY</span>
|
||||
<h2 id="article-changelog-title">文章修订记录</h2>
|
||||
</div>
|
||||
<strong>{entries.length} 次</strong>
|
||||
</header>
|
||||
<ol class="post-changelog-list">
|
||||
{entries.map((entry) => (
|
||||
<li>
|
||||
<span class="post-changelog-marker" aria-hidden="true"></span>
|
||||
<div class="post-changelog-entry">
|
||||
<div>
|
||||
<h3>{entry.title}</h3>
|
||||
{entry.date && <time datetime={entry.date.toISOString()}>{entry.dateText}</time>}
|
||||
</div>
|
||||
{entry.description && <p>{entry.description}</p>}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
|
||||
@@ -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无报错即可。
|
||||
运行一下这个Action无报错即可。
|
||||
|
||||
+68
-1
@@ -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<string, unknown>;
|
||||
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<string, unknown>): { 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<string, unknown>;
|
||||
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<BlogPost[]> {
|
||||
|
||||
return entries
|
||||
.map((entry) => {
|
||||
const data = entry.data as Record<string, unknown>;
|
||||
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<BlogPost[]> {
|
||||
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,
|
||||
|
||||
@@ -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 = {
|
||||
<header class={post.cover ? 'article-header article-header-cover' : 'article-header'}>
|
||||
{post.cover && <img class="article-header-cover-img" src={post.cover} alt="" loading="eager" decoding="async" />}
|
||||
<div class="article-header-content">
|
||||
<time datetime={post.date.toISOString()}>{post.dateText}</time>
|
||||
<div class="article-date-line">
|
||||
<time datetime={post.date.toISOString()}>发布于 {post.dateText}</time>
|
||||
{post.updated && <time datetime={post.updated.toISOString()}>最后更新于 {post.updatedText}</time>}
|
||||
</div>
|
||||
<h1>{post.title}</h1>
|
||||
{post.cover ? (
|
||||
<div class="article-hero-meta" aria-label="文章信息">
|
||||
@@ -181,19 +197,33 @@ const articleJsonLd = {
|
||||
<aside
|
||||
class="butterfly-note butterfly-note--warning article-outdated-note"
|
||||
aria-label="文章时效性提示"
|
||||
data-published-at={post.date.toISOString()}
|
||||
data-freshness-at={(post.updated ?? post.date).toISOString()}
|
||||
data-has-new-version={newVersionHref ? 'true' : 'false'}
|
||||
hidden
|
||||
>
|
||||
<i class="fa-solid fa-triangle-exclamation" aria-hidden="true"></i>
|
||||
<div class="butterfly-note__content">
|
||||
<p>
|
||||
注意:本文发布于 {post.dateText},距今已超过一年,部分内容可能已经过时,请结合最新资料判断。
|
||||
</p>
|
||||
{newVersionHref ? (
|
||||
<p>
|
||||
本文已有更新版本:
|
||||
<a href={newVersionHref} target={newVersionExternal ? '_blank' : undefined} rel={newVersionExternal ? 'noreferrer' : undefined}>
|
||||
{newVersionTitle}<i class="fa-solid fa-arrow-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
。旧版内容保留作为历史参考。
|
||||
{post.changelog.length > 0 && <a href="#article-changelog">查看修订记录</a>}
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
注意:本文最后维护于 {post.updatedText ?? post.dateText},距今已超过一年,部分内容可能已经过时,请结合最新资料判断。
|
||||
{post.changelog.length > 0 && <a href="#article-changelog">查看修订记录</a>}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
<div class="prose" data-toc={JSON.stringify(toc)}>
|
||||
<Content />
|
||||
</div>
|
||||
{post.changelog.length > 0 && <PostChangelog entries={post.changelog} />}
|
||||
<PostCopyrightCard post={post} />
|
||||
{post.comments && <TwikooComments envId={siteConfig.comments.envId} region={siteConfig.comments.region} path={post.href} title={post.title} />}
|
||||
</article>
|
||||
@@ -288,13 +318,14 @@ const articleJsonLd = {
|
||||
|
||||
<script is:inline>
|
||||
(() => {
|
||||
document.querySelectorAll('.article-outdated-note[data-published-at]').forEach((note) => {
|
||||
const publishedAt = new Date(note.dataset.publishedAt || '');
|
||||
if (Number.isNaN(publishedAt.valueOf())) return;
|
||||
document.querySelectorAll('.article-outdated-note[data-freshness-at]').forEach((note) => {
|
||||
const freshnessAt = new Date(note.dataset.freshnessAt || '');
|
||||
const hasNewVersion = note.dataset.hasNewVersion === 'true';
|
||||
if (Number.isNaN(freshnessAt.valueOf())) return;
|
||||
|
||||
const threshold = new Date();
|
||||
threshold.setFullYear(threshold.getFullYear() - 1);
|
||||
note.hidden = publishedAt >= threshold;
|
||||
note.hidden = !hasNewVersion && freshnessAt >= threshold;
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
Reference in New Issue
Block a user