first commit

This commit is contained in:
2026-01-07 16:24:34 +00:00
commit 5c1399bb96
362 changed files with 59794 additions and 0 deletions

View File

@@ -0,0 +1,393 @@
---
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>