| ---
|
| import { render } from "astro:content";
|
| import * as path from "node:path";
|
| import Comment from "@components/comment/index.astro";
|
| import Markdown from "@components/common/Markdown.astro";
|
| import KatexManager from "@components/features/KatexManager.astro";
|
| import License from "@components/misc/License.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 CoverImage from "@/components/common/CoverImage.astro";
|
| import PostMetadata from "@/components/layout/PostMeta.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) => {
|
|
|
| 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);
|
|
|
|
|
| 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("_", "-"),
|
|
|
| };
|
| ---
|
|
|
| <MainGridLayout
|
| banner={processedImage}
|
| title={entry.data.title}
|
| description={entry.data.description}
|
| lang={entry.data.lang}
|
| setOGTypeArticle={true}
|
| postSlug={entry.id}
|
| headings={headings}
|
| >
|
|
|
| <KatexManager slot="head" />
|
|
|
| <script
|
| is:inline
|
| slot="head"
|
| type="application/ld+json"
|
| set:html={JSON.stringify(jsonLd)}
|
| />
|
| <div
|
| class="flex w-full rounded-(--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 ",
|
| {},
|
| ]}
|
| >
|
|
|
| <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>
|
|
|
|
|
| <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-(--primary)
|
| before:absolute before:top-3 before:-left-4.5"
|
| >
|
| {entry.data.title}
|
| </h1>
|
| </div>
|
|
|
|
|
| <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-(--line-divider) border-dashed border-b mt-3 mb-5" />
|
| )
|
| }
|
| </div>
|
|
|
|
|
|
|
| {
|
| processedImage && coverImageConfig.enableInPost && (
|
| <div style="margin-top:1rem;">
|
| <CoverImage
|
| id="post-cover"
|
| src={processedImage}
|
| basePath={getFileDirFromPath(entry.filePath || '')}
|
| class="mb-8 rounded-xl banner-container onload-animation"
|
| 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-(--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-(--primary) flex items-center justify-center text-white dark:text-black/70 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-(--primary) text-white dark:text-black/70 rounded-lg font-medium hover:bg-(--primary)/80 hover:scale-105 active:scale-95 transition-all whitespace-nowrap"
|
| >
|
| <span>{i18n(I18nKey.sponsor)}</span>
|
| <Icon name="fa7-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-(--primary) transition-transform group-hover:translate-x-0.5 bg-(--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-15 max-w-full px-4 flex items-center justify-start! gap-4">
|
| <Icon
|
| name="material-symbols:chevron-left-rounded"
|
| class="text-[2rem] text-(--primary)"
|
| />
|
| <div class="overflow-hidden transition text-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-15 max-w-full px-4 flex items-center justify-end! gap-4">
|
| <div class="overflow-hidden transition text-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-(--primary)"
|
| />
|
| </div>
|
| )
|
| }
|
| </a>
|
| </div>
|
|
|
|
|
| {entry.data.comment && <Comment post={entry} />}
|
| </MainGridLayout>
|
|
|