394 lines
14 KiB
Plaintext
394 lines
14 KiB
Plaintext
---
|
||
import { render } from "astro:content";
|
||
import * as path from "node:path";
|
||
import Comment from "@components/comment/index.astro";
|
||
import License from "@components/misc/License.astro";
|
||
import Markdown from "@components/misc/Markdown.astro";
|
||
import I18nKey from "@i18n/i18nKey";
|
||
import { i18n } from "@i18n/translation";
|
||
import MainGridLayout from "@layouts/MainGridLayout.astro";
|
||
import { getSortedPosts } from "@utils/content-utils";
|
||
import {
|
||
getFileDirFromPath,
|
||
getPostUrlBySlug,
|
||
removeFileExtension,
|
||
} from "@utils/url-utils";
|
||
import { Icon } from "astro-icon/components";
|
||
import dayjs from "dayjs";
|
||
import utc from "dayjs/plugin/utc";
|
||
import PostMetadata from "@/components/content/PostMeta.astro";
|
||
import RandomCoverImage from "@/components/misc/RandomCoverImage.astro";
|
||
import SharePoster from "@/components/misc/SharePoster.svelte";
|
||
import { coverImageConfig } from "@/config/coverImageConfig";
|
||
import { licenseConfig } from "@/config/licenseConfig";
|
||
import { profileConfig } from "@/config/profileConfig";
|
||
import { siteConfig } from "@/config/siteConfig";
|
||
import { sponsorConfig } from "@/config/sponsorConfig";
|
||
import { formatDateToYYYYMMDD } from "@/utils/date-utils";
|
||
import { processCoverImageSync } from "@/utils/image-utils";
|
||
import { url } from "@/utils/url-utils";
|
||
|
||
export async function getStaticPaths() {
|
||
const blogEntries = await getSortedPosts();
|
||
return blogEntries.map((entry) => {
|
||
// 将 id 转换为 slug(移除扩展名)以匹配路由参数
|
||
const slug = removeFileExtension(entry.id);
|
||
return {
|
||
params: { slug },
|
||
props: { entry },
|
||
};
|
||
});
|
||
}
|
||
|
||
const { entry } = Astro.props;
|
||
const { Content, headings } = await render(entry);
|
||
|
||
const { remarkPluginFrontmatter } = await render(entry);
|
||
|
||
// 处理随机图:如果image为"api",则从配置的API获取随机图
|
||
const processedImage = processCoverImageSync(entry.data.image, entry.id);
|
||
|
||
let posterCoverUrl = processedImage;
|
||
if (processedImage) {
|
||
const isLocal = !(
|
||
processedImage.startsWith("/") ||
|
||
processedImage.startsWith("http") ||
|
||
processedImage.startsWith("https") ||
|
||
processedImage.startsWith("data:")
|
||
);
|
||
if (isLocal) {
|
||
const basePath = getFileDirFromPath(entry.filePath || "");
|
||
const files = import.meta.glob<ImageMetadata>("../../**", {
|
||
import: "default",
|
||
});
|
||
let normalizedPath = path
|
||
.normalize(path.join("../../", basePath, processedImage))
|
||
.replace(/\\/g, "/");
|
||
const file = files[normalizedPath];
|
||
if (file) {
|
||
const img = await file();
|
||
posterCoverUrl = img.src;
|
||
}
|
||
}
|
||
}
|
||
|
||
dayjs.extend(utc);
|
||
|
||
const jsonLd = {
|
||
"@context": "https://schema.org",
|
||
"@type": "BlogPosting",
|
||
headline: entry.data.title,
|
||
description: entry.data.description || entry.data.title,
|
||
keywords: entry.data.tags,
|
||
author: {
|
||
"@type": "Person",
|
||
name: profileConfig.name,
|
||
url: Astro.site,
|
||
},
|
||
datePublished: formatDateToYYYYMMDD(entry.data.published),
|
||
inLanguage: entry.data.lang
|
||
? entry.data.lang.replace("_", "-")
|
||
: siteConfig.lang.replace("_", "-"),
|
||
// TODO include cover image here
|
||
};
|
||
---
|
||
|
||
<MainGridLayout
|
||
banner={processedImage}
|
||
title={entry.data.title}
|
||
description={entry.data.description}
|
||
lang={entry.data.lang}
|
||
setOGTypeArticle={true}
|
||
postSlug={entry.id}
|
||
headings={headings}
|
||
>
|
||
<script
|
||
is:inline
|
||
slot="head"
|
||
type="application/ld+json"
|
||
set:html={JSON.stringify(jsonLd)}
|
||
/>
|
||
<div
|
||
class="flex w-full rounded-[var(--radius-large)] overflow-hidden relative mb-4"
|
||
>
|
||
<div
|
||
id="post-container"
|
||
class:list={[
|
||
"card-base z-10 px-6 md:px-9 pt-6 pb-4 relative w-full ",
|
||
{},
|
||
]}
|
||
>
|
||
<!-- word count and reading time -->
|
||
<div
|
||
class="flex flex-row text-black/30 dark:text-white/30 gap-5 mb-3 transition onload-animation"
|
||
>
|
||
<div class="flex flex-row items-center">
|
||
<div
|
||
class="transition h-6 w-6 rounded-md bg-black/5 dark:bg-white/10 text-black/50 dark:text-white/50 flex items-center justify-center mr-2"
|
||
>
|
||
<Icon name="material-symbols:notes-rounded" />
|
||
</div>
|
||
<div class="text-sm">
|
||
{remarkPluginFrontmatter.words}
|
||
{" " + i18n(I18nKey.wordsCount)}
|
||
</div>
|
||
</div>
|
||
<div class="flex flex-row items-center">
|
||
<div
|
||
class="transition h-6 w-6 rounded-md bg-black/5 dark:bg-white/10 text-black/50 dark:text-white/50 flex items-center justify-center mr-2"
|
||
>
|
||
<Icon name="material-symbols:schedule-outline-rounded" />
|
||
</div>
|
||
<div class="text-sm">
|
||
{remarkPluginFrontmatter.minutes}
|
||
{
|
||
" " +
|
||
i18n(
|
||
remarkPluginFrontmatter.minutes === 1
|
||
? I18nKey.minuteCount
|
||
: I18nKey.minutesCount
|
||
)
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- title -->
|
||
<div class="relative onload-animation">
|
||
<h1
|
||
data-pagefind-body
|
||
data-pagefind-weight="10"
|
||
data-pagefind-meta="title"
|
||
class="transition w-full block font-bold mb-3
|
||
text-3xl md:text-[2.25rem]/[2.75rem]
|
||
text-black/90 dark:text-white/90
|
||
md:before:w-1 before:h-5 before:rounded-md before:bg-[var(--primary)]
|
||
before:absolute before:top-[0.75rem] before:left-[-1.125rem]"
|
||
>
|
||
{entry.data.title}
|
||
</h1>
|
||
</div>
|
||
|
||
<!-- metadata -->
|
||
<div class="onload-animation">
|
||
<PostMetadata
|
||
className="mb-5"
|
||
published={entry.data.published}
|
||
updated={entry.data.updated}
|
||
tags={entry.data.tags}
|
||
category={entry.data.category || undefined}
|
||
id={entry.id}
|
||
/>
|
||
{
|
||
!processedImage && (
|
||
<div class="border-[var(--line-divider)] border-dashed border-b-[1px] mt-3 mb-5" />
|
||
)
|
||
}
|
||
</div>
|
||
|
||
<!-- always show cover as long as it has one -->
|
||
|
||
{
|
||
processedImage && coverImageConfig.enableInPost && (
|
||
<div style="margin-top:1rem;">
|
||
<RandomCoverImage
|
||
id="post-cover"
|
||
src={processedImage}
|
||
basePath={getFileDirFromPath(entry.filePath || '')}
|
||
class="mb-8 rounded-xl banner-container onload-animation"
|
||
seed={entry.id}
|
||
preview={false}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
<Markdown class="mb-6 markdown-content onload-animation">
|
||
<Content />
|
||
</Markdown>
|
||
|
||
{/* 赞助按钮 & 分享按钮 */}
|
||
{
|
||
(siteConfig.sharePoster || (sponsorConfig.showButtonInPost && siteConfig.pages.sponsor)) && (
|
||
<div class="mb-6 rounded-xl onload-animation">
|
||
<div class="p-6 bg-[var(--license-block-bg)] rounded-xl">
|
||
<div class="flex flex-col sm:flex-row items-center justify-between gap-4">
|
||
<div class="flex items-center gap-3 flex-1">
|
||
<div class="h-12 w-12 rounded-lg bg-[var(--primary)] flex items-center justify-center text-white dark:text-black/70 flex-shrink-0">
|
||
<Icon
|
||
name={sponsorConfig.showButtonInPost &&
|
||
siteConfig.pages.sponsor
|
||
? "material-symbols:favorite"
|
||
: "material-symbols:share"}
|
||
class="text-2xl"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<h3 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-1">
|
||
{
|
||
sponsorConfig.showButtonInPost && siteConfig.pages.sponsor
|
||
? i18n(I18nKey.sponsorButton)
|
||
: i18n(I18nKey.shareOnSocial)
|
||
}
|
||
</h3>
|
||
<p class="text-sm text-neutral-600 dark:text-neutral-400">
|
||
{
|
||
sponsorConfig.showButtonInPost && siteConfig.pages.sponsor
|
||
? i18n(I18nKey.sponsorButtonText)
|
||
: i18n(I18nKey.shareOnSocialDescription)
|
||
}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div class="flex items-center gap-3">
|
||
{
|
||
siteConfig.sharePoster && (
|
||
<SharePoster
|
||
client:load
|
||
title={entry.data.title}
|
||
author={profileConfig.name}
|
||
description={entry.data.description || entry.data.title}
|
||
pubDate={formatDateToYYYYMMDD(entry.data.published)}
|
||
coverImage={posterCoverUrl}
|
||
url={Astro.url.href}
|
||
siteTitle={siteConfig.title}
|
||
avatar={profileConfig.avatar}
|
||
/>
|
||
)
|
||
}
|
||
{
|
||
sponsorConfig.showButtonInPost && siteConfig.pages.sponsor && (
|
||
<a
|
||
href={url("/sponsor/")}
|
||
class="inline-flex items-center gap-2 px-6 py-3 bg-[var(--primary)] text-white dark:text-black/70 rounded-lg font-medium hover:bg-[var(--primary)]/80 hover:scale-105 active:scale-95 transition-all whitespace-nowrap"
|
||
>
|
||
<span>{i18n(I18nKey.sponsor)}</span>
|
||
<Icon name="fa6-solid:arrow-right" class="text-sm" />
|
||
</a>
|
||
)
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
{
|
||
licenseConfig.enable && (
|
||
<License
|
||
title={entry.data.title}
|
||
id={entry.id}
|
||
pubDate={entry.data.published}
|
||
author={entry.data.author}
|
||
sourceLink={entry.data.sourceLink}
|
||
licenseName={entry.data.licenseName}
|
||
licenseUrl={entry.data.licenseUrl}
|
||
class="mb-6 rounded-xl license-container onload-animation"
|
||
/>
|
||
)
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 上次编辑时间 -->
|
||
{
|
||
siteConfig.showLastModified && (() => {
|
||
const lastModified = dayjs(
|
||
entry.data.updated || entry.data.published
|
||
);
|
||
const now = dayjs();
|
||
const daysDiff = now.diff(lastModified, "day");
|
||
// 使用用户定义的阈值,如果没有定义则默认为1天
|
||
const outdatedThreshold = siteConfig.outdatedThreshold ?? 1;
|
||
const shouldShowOutdatedCard = daysDiff >= outdatedThreshold;
|
||
|
||
return shouldShowOutdatedCard ? (
|
||
<div class="card-base p-6 mb-4">
|
||
<div class="flex items-center gap-2">
|
||
<div class="transition h-9 w-9 rounded-lg overflow-hidden relative flex items-center justify-center mr-0">
|
||
<Icon
|
||
name="material-symbols:history-rounded"
|
||
class="text-4xl text-[var(--primary)] transition-transform group-hover:translate-x-0.5 bg-[var(--enter-btn-bg)] p-2 rounded-md"
|
||
/>
|
||
</div>
|
||
|
||
{(() => {
|
||
const dateStr = lastModified.format("YYYY-MM-DD");
|
||
const isOutdated = daysDiff >= 1;
|
||
|
||
return (
|
||
<div class="flex flex-col gap-0.1">
|
||
<div class="text-[1.0rem] leading-tight text-black/75 dark:text-white/75">
|
||
{`${i18n(I18nKey.lastModifiedPrefix)}${dateStr}${
|
||
isOutdated
|
||
? `,${i18n(I18nKey.lastModifiedDaysAgo).replace("{days}", daysDiff.toString())}`
|
||
: ""
|
||
}`}
|
||
</div>
|
||
{isOutdated && (
|
||
<p class="text-[0.8rem] leading-tight text-black/75 dark:text-white/75">
|
||
{i18n(I18nKey.lastModifiedOutdated)}
|
||
</p>
|
||
)}
|
||
</div>
|
||
);
|
||
})()}
|
||
</div>
|
||
</div>
|
||
) : null;
|
||
})()
|
||
}
|
||
|
||
<div
|
||
class="flex flex-col md:flex-row justify-between mb-4 gap-4 overflow-hidden w-full"
|
||
>
|
||
<a
|
||
href={entry.data.nextSlug ? getPostUrlBySlug(entry.data.nextSlug) : "#"}
|
||
class:list={[
|
||
"w-full font-bold overflow-hidden active:scale-95",
|
||
{ "pointer-events-none": !entry.data.nextSlug },
|
||
]}
|
||
>
|
||
{
|
||
entry.data.nextSlug && (
|
||
<div class="btn-card rounded-2xl w-full h-[3.75rem] max-w-full px-4 flex items-center !justify-start gap-4">
|
||
<Icon
|
||
name="material-symbols:chevron-left-rounded"
|
||
class="text-[2rem] text-[var(--primary)]"
|
||
/>
|
||
<div class="overflow-hidden transition overflow-ellipsis whitespace-nowrap max-w-[calc(100%_-_3rem)] text-black/75 dark:text-white/75">
|
||
{entry.data.nextTitle}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
</a>
|
||
|
||
<a
|
||
href={entry.data.prevSlug ? getPostUrlBySlug(entry.data.prevSlug) : "#"}
|
||
class:list={[
|
||
"w-full font-bold overflow-hidden active:scale-95",
|
||
{ "pointer-events-none": !entry.data.prevSlug },
|
||
]}
|
||
>
|
||
{
|
||
entry.data.prevSlug && (
|
||
<div class="btn-card rounded-2xl w-full h-[3.75rem] max-w-full px-4 flex items-center !justify-end gap-4">
|
||
<div class="overflow-hidden transition overflow-ellipsis whitespace-nowrap max-w-[calc(100%_-_3rem)] text-black/75 dark:text-white/75">
|
||
{entry.data.prevTitle}
|
||
</div>
|
||
<Icon
|
||
name="material-symbols:chevron-right-rounded"
|
||
class="text-[2rem] text-[var(--primary)]"
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
</a>
|
||
</div>
|
||
|
||
<!-- 评论 -->
|
||
{entry.data.comment && <Comment post={entry} />}
|
||
</MainGridLayout>
|