Commit 478254c

HPCesia <me@hpcesia.com>
2025-02-13 16:18:18
feat: post cover as banner
- Fix the path wrong when use relative path as post cover. - Use swup parallal plugin to switch banner. - Delete unnecessary render cache, because astro already have it.
1 parent a16a9cf
src/assets/img/demo_banner.jpg
Binary file
src/components/aside/siteinfo/Stats.astro
@@ -2,9 +2,9 @@
 import { asideConfig, siteConfig } from '@/config';
 import I18nKey from '@i18n/I18nKey';
 import { i18n } from '@i18n/translation';
-import { getOrCreateRenderResult, getPostsCount } from '@utils/content-utils';
+import { getPostsCount } from '@utils/content-utils';
 import { Icon } from 'astro-icon/components';
-import { getCollection } from 'astro:content';
+import { getCollection, render } from 'astro:content';
 ---
 
 <div class="stats stats-vertical w-full">
@@ -49,7 +49,7 @@ import { getCollection } from 'astro:content';
                   const entries = await getCollection('posts');
                   const words = await Promise.all(
                     entries.map(async (entry) => {
-                      const { remarkPluginFrontmatter } = await getOrCreateRenderResult(entry);
+                      const { remarkPluginFrontmatter } = await render(entry);
                       return remarkPluginFrontmatter.words as number;
                     })
                   );
src/components/widgets/PostCard.astro
@@ -15,9 +15,10 @@ interface Props {
   category?: string;
   cover?: string;
   description: string;
+  basePath: string;
 }
 
-const { title, url, published, updated, tags, category, cover } = Astro.props;
+const { title, url, published, updated, tags, category, cover, basePath } = Astro.props;
 const className = Astro.props.class;
 
 const hasCover = cover !== '' && cover !== undefined && cover !== null;
@@ -88,7 +89,7 @@ const metas: ({ icon: string; text: string; link?: string; time?: Date } | undef
   {
     hasCover ? (
       <figure class="md:w-3/4 md:max-w-96">
-        <PostCardCover url={url} title={title} cover={cover} />
+        <PostCardCover url={url} title={title} cover={cover} basePath={basePath} />
       </figure>
     ) : (
       <figure>
src/components/widgets/PostCardCover.astro
@@ -6,9 +6,10 @@ interface Props {
   url: string;
   title: string;
   cover: string;
+  basePath?: string;
 }
 
-const { url, title, cover } = Astro.props;
+const { url, title, cover, basePath } = Astro.props;
 ---
 
 <a
@@ -21,5 +22,5 @@ const { url, title, cover } = Astro.props;
   >
     <Icon name="material-symbols:chevron-right-rounded" class="h-24 w-24 text-white" />
   </div>
-  <ImageWrapper src={cover} alt={title} />
+  <ImageWrapper src={cover} alt={title} basePath={basePath} />
 </a>
src/components/Banner.astro
@@ -5,21 +5,24 @@ import ImageWrapper from './utils/ImageWrapper.astro';
 
 interface Props {
   src?: string;
+  basePath?: string;
 }
 
 const siteBanner = siteConfig.banner;
 if (siteBanner === false) throw Error('Should not show this error');
 
 const src = Astro.props.src || (siteBanner.src as string);
-const customBanner = Boolean(Astro.props.src !== undefined);
 ---
 
-<div
-  id="banner"
-  class="relative max-h-screen scale-105 overflow-hidden opacity-0 duration-1000"
-  data-custom-banner={customBanner}
->
-  <ImageWrapper id="banner-img" src={src} class="h-full object-cover" />
+<div id="banner" class="relative max-h-screen scale-105 opacity-0 duration-1000">
+  <div class="h-full w-full">
+    <ImageWrapper
+      id="banner-img"
+      src={src}
+      class="swup-transition-parallel absolute! h-full w-full overflow-hidden"
+      basePath={Astro.props.basePath}
+    />
+  </div>
   <div
     id="banner-mask"
     class="from-base-100 to-base-100/0 absolute bottom-0 grid h-1/2 min-h-48 w-full bg-linear-0"
@@ -103,6 +106,21 @@ const customBanner = Boolean(Astro.props.src !== undefined);
       const heightExtend = document.getElementById('page-height-extend');
       heightExtend?.classList.add('hidden');
     });
+
+    // 处理 Banner 图片切换
+    window.swup?.hooks.before('content:insert', (_, { containers }) => {
+      for (const container of containers) {
+        if (container.selector !== '#banner-img') continue;
+        const prevWrapper = container.previous;
+        const nextWrapper = container.next;
+        const prevImg = prevWrapper.querySelector('img') as HTMLImageElement;
+        const nextImg = nextWrapper.querySelector('img') as HTMLImageElement;
+        if (prevImg.src !== nextImg.src) continue;
+        prevWrapper.classList.add('hidden');
+        prevWrapper.classList.remove('swup-transition-parallel');
+        nextWrapper.classList.remove('swup-transition-parallel');
+      }
+    });
   }
 
   if (window.swup) {
src/components/PostsPage.astro
@@ -1,14 +1,12 @@
 ---
 import { siteConfig } from '@/config';
-import type { BlogPostData } from '@/types/data';
+import type { BlogPost } from '@/types/data';
+import path from 'path';
 import Pagination from './widgets/Pagination.astro';
 import PostCard from './widgets/PostCard.astro';
 
 interface Props {
-  posts: {
-    body: string;
-    data: BlogPostData;
-  }[];
+  posts: BlogPost[];
   currentPage: number;
   postsPerPage?: number;
   baseUrl: string;
@@ -36,6 +34,7 @@ posts = posts.slice((currentPage - 1) * postsPerPage, currentPage * postsPerPage
           category={data.category}
           cover={data.cover}
           description={data.description}
+          basePath={path.join('content/posts/', post.id)}
         />
       );
     })
src/layouts/GlobalLayout.astro
@@ -14,10 +14,14 @@ interface Props {
   title?: string;
   description?: string;
   lang?: string;
+  banner?: {
+    src: string;
+    basePath?: string;
+  };
 }
 
 let { title, lang } = Astro.props;
-const { description } = Astro.props;
+const { description, banner } = Astro.props;
 
 let pageTitle: string;
 if (title) pageTitle = `${title} - ${siteConfig.title}`;
@@ -81,7 +85,7 @@ const favicons: Favicon[] =
     <SideToolBar />
     <Navbar>
       <slot name="header" />
-      {siteConfig.banner !== false && <Banner />}
+      {siteConfig.banner !== false && <Banner src={banner?.src} basePath={banner?.basePath} />}
       <div id="body-wrap" class="w-full items-center md:px-4">
         <slot />
       </div>
src/layouts/MainLayout.astro
@@ -6,11 +6,15 @@ interface Props {
   title?: string;
   description?: string;
   lang?: string;
+  banner?: {
+    src: string;
+    basePath?: string;
+  };
 }
-const { title, description, lang } = Astro.props;
+const { title, description, lang, banner } = Astro.props;
 ---
 
-<GlobalLayout title={title} description={description} lang={lang}>
+<GlobalLayout title={title} description={description} lang={lang} banner={banner}>
   <slot slot="head" name="head" />
   <slot slot="header" name="header" />
   <!-- Main content -->
src/layouts/PostPageLayout.astro
@@ -12,12 +12,16 @@ interface Props {
   lang?: string;
   headings?: MarkdownHeading[];
   comment?: boolean;
+  banner?: {
+    src: string;
+    basePath?: string;
+  };
 }
 
-const { title, description, lang, headings, comment } = Astro.props;
+const { title, description, lang, headings, comment, banner } = Astro.props;
 ---
 
-<MainLayout title={title} description={description} lang={lang}>
+<MainLayout title={title} description={description} lang={lang} banner={banner}>
   <slot slot="head" name="head" />
   <slot slot="header-content" name="header-content" />
   <div class="card border-base-300 swup-transition-fade border-2 px-6 py-4">
src/pages/posts/[article].astro
@@ -5,8 +5,8 @@ import PostInfo from '@components/misc/PostInfo.astro';
 import ImageWrapper from '@components/utils/ImageWrapper.astro';
 import Markdown from '@components/utils/Markdown.astro';
 import PostPageLayout from '@layouts/PostPageLayout.astro';
-import { getOrCreateRenderResult } from '@utils/content-utils';
-import { getCollection } from 'astro:content';
+import { getCollection, render } from 'astro:content';
+import path from 'path';
 
 export async function getStaticPaths() {
   const articles = await getCollection('posts');
@@ -17,11 +17,12 @@ export async function getStaticPaths() {
 }
 
 const { article } = Astro.props;
-const { Content, headings, remarkPluginFrontmatter } = await getOrCreateRenderResult(article);
+const { Content, headings, remarkPluginFrontmatter } = await render(article);
 const hasCover =
   article.data.cover !== '' && article.data.cover !== undefined && article.data.cover !== null;
 
 const description = article.data.description || remarkPluginFrontmatter.excerpt;
+const basePath = path.join('content/posts/', article.id);
 ---
 
 <PostPageLayout
@@ -30,6 +31,7 @@ const description = article.data.description || remarkPluginFrontmatter.excerpt;
   headings={headings}
   comment={article.data.comment}
   lang={article.data.lang}
+  banner={hasCover ? { src: article.data.cover, basePath: basePath } : undefined}
 >
   <Fragment slot="header-content">
     <PostInfo
@@ -49,6 +51,7 @@ const description = article.data.description || remarkPluginFrontmatter.excerpt;
         src={article.data.cover}
         class="mb-6 rounded-xl shadow"
         alt={article.data.title}
+        basePath={basePath}
       />
     )
   }
src/styles/transition.css
@@ -14,6 +14,16 @@ html.is-animating .swup-transition-slide {
   @apply -translate-x-20 opacity-0;
 }
 
+html.is-changing .swup-transition-parallel {
+  @apply transition-all duration-500 ease-linear;
+}
+html.is-changing .swup-transition-parallel.is-previous-container {
+  @apply -translate-x-full;
+}
+html.is-changing .swup-transition-parallel.is-next-container {
+  @apply translate-x-full;
+}
+
 .onload-animation {
   opacity: 0;
   animation: 300ms fade-in-up;
src/types/data.ts
@@ -1,3 +1,5 @@
+import type { RenderedContent } from 'astro:content';
+
 export type BlogPostData = {
   body: string;
   title: string;
@@ -10,3 +12,10 @@ export type BlogPostData = {
   category?: string;
   comment?: boolean;
 };
+
+export type BlogPost = {
+  id: string;
+  rendered: RenderedContent;
+  body: string;
+  data: BlogPostData;
+};
src/utils/content-utils.ts
@@ -1,38 +1,11 @@
 import type { BlogPostData } from '@/types/data';
+import type { BlogPost } from '@/types/data';
 import I18nKey from '@i18n/I18nKey';
 import { i18n } from '@i18n/translation';
-import type { MarkdownHeading } from 'astro';
-import type { AstroComponentFactory } from 'astro/runtime/server/index.d.ts';
 import { getCollection } from 'astro:content';
-import { type CollectionEntry, render } from 'astro:content';
 
-interface RenderResult {
-  Content: AstroComponentFactory;
-  headings: MarkdownHeading[];
-  remarkPluginFrontmatter: Record<string, unknown>;
-}
-
-const renderCache = new Map<string, RenderResult>();
-
-export async function getOrCreateRenderResult(article: CollectionEntry<'posts'>) {
-  const cacheKey = article.id;
-
-  if (renderCache.has(cacheKey)) {
-    return renderCache.get(cacheKey)!;
-  }
-
-  const { Content, headings, remarkPluginFrontmatter } = await render(article);
-  const result = { Content, headings, remarkPluginFrontmatter };
-
-  renderCache.set(cacheKey, result);
-  return result;
-}
-
-export async function getSortedPosts(): Promise<{ body: string; data: BlogPostData }[]> {
-  const allBlogPosts = (await getCollection('posts')) as unknown as {
-    body: string;
-    data: BlogPostData;
-  }[];
+export async function getSortedPosts(): Promise<BlogPost[]> {
+  const allBlogPosts = (await getCollection('posts')) as unknown as BlogPost[];
   const sortedBlogPosts = allBlogPosts.sort(
     (a: { data: BlogPostData }, b: { data: BlogPostData }) => {
       const dateA = new Date(a.data.published);
astro.config.mjs
@@ -35,11 +35,12 @@ export default defineConfig({
     mdx(),
     swup({
       theme: false,
-      containers: ['main'],
+      containers: ['main', '#banner-img'],
       animationClass: 'swup-transition-',
       globalInstance: true,
       smoothScrolling: true,
       progress: true,
+      parallel: ['#banner-img'],
     }),
   ],
   markdown: {