Commit 7507609

HPCesia <me@hpcesia.com>
2025-03-07 16:12:45
feat: draft preview in dev mode
1 parent 70842f9
src/components/aside/siteinfo/Stats.astro
@@ -1,5 +1,5 @@
 ---
-import { asideConfig, siteConfig } from '@/config';
+import { asideConfig, buildConfig, siteConfig } from '@/config';
 import I18nKey from '@i18n/I18nKey';
 import { i18n } from '@i18n/translation';
 import { getPostsCount } from '@utils/content-utils';
@@ -46,7 +46,11 @@ import { getCollection, render } from 'astro:content';
               </div>
               <div class="stat-value text-base">
                 {(async () => {
-                  const entries = await getCollection('posts');
+                  const posts = await getCollection('posts');
+                  const drafts = await getCollection('drafts');
+                  const onDev = import.meta.env.DEV;
+                  const entries =
+                    onDev && buildConfig.showDraftsOnDev ? [...posts, ...drafts] : posts;
                   const words = await Promise.all(
                     entries.map(async (entry) => {
                       const { remarkPluginFrontmatter } = await render(entry);
src/i18n/langs/en.ts
@@ -46,4 +46,7 @@ export const en: Translation = {
   [Key.author]: 'Author',
   [Key.publishedAt]: 'Published at',
   [Key.license]: 'License',
+
+  [Key.draftDevNote]:
+    'This is a draft and will only be displayed in `DEV` mode. To disable draft preview, please modify `buildConfig.showDraftsOnDev` to `false` in `src/config.ts`.',
 };
src/i18n/langs/zh_CN.ts
@@ -46,4 +46,7 @@ export const zh_CN: Translation = {
   [Key.author]: '作者',
   [Key.publishedAt]: '发布于',
   [Key.license]: '许可协议',
+
+  [Key.draftDevNote]:
+    '这是一篇草稿,只会在 `DEV` 模式下显示。关闭草稿预览,请修改 `src/config.ts` 中的 `buildConfig.showDraftsOnDev` 为 `false`。',
 };
src/i18n/langs/zh_TW.ts
@@ -46,4 +46,7 @@ export const zh_TW: Translation = {
   [Key.author]: '作者',
   [Key.publishedAt]: '發佈於',
   [Key.license]: '許可協議',
+
+  [Key.draftDevNote]:
+    '這是一篇草稿,只會在 `DEV` 模式下顯示。關閉草稿預覽,請修改 `src/config.ts` 中的 `buildConfig.showDraftsOnDev` 為 `false`。',
 };
src/i18n/I18nKey.ts
@@ -43,6 +43,9 @@ enum I18nKey {
   author = 'author',
   publishedAt = 'publishedAt',
   license = 'license',
+
+  /** Note in the top of drafts content in dev mode. This key supports markdown syntax, using `markdown-it`. */
+  draftDevNote = 'draftDevNote',
 }
 
 export default I18nKey;
src/pages/posts/[article].astro
@@ -1,22 +1,36 @@
 ---
-import { siteConfig } from '@/config';
+import { buildConfig, siteConfig } from '@/config';
 import License from '@components/misc/License.astro';
 import PostInfo from '@components/misc/PostInfo.astro';
 import ImageWrapper from '@components/utils/ImageWrapper.astro';
 import Markdown from '@components/utils/Markdown.astro';
+import I18nKey from '@i18n/I18nKey';
+import { i18n } from '@i18n/translation';
 import PostPageLayout from '@layouts/PostPageLayout.astro';
+import { Icon } from 'astro-icon/components';
 import { getCollection, render } from 'astro:content';
+import dayjs from 'dayjs';
+import MarkdownIt from 'markdown-it';
 import path from 'path';
 
 export async function getStaticPaths() {
-  const articles = await getCollection('posts');
+  const posts = (await getCollection('posts')).map((post) => ({
+    article: post,
+    isDraft: false,
+  }));
+  const drafts = (await getCollection('drafts')).map((post) => ({
+    article: post,
+    isDraft: true,
+  }));
+  const onDev = import.meta.env.DEV;
+  const articles = onDev && buildConfig.showDraftsOnDev ? [...posts, ...drafts] : posts;
   return articles.map((article) => ({
-    params: { article: article.data.slug },
-    props: { article },
+    params: { article: article.article.data.slug },
+    props: { article: article.article, isDraft: article.isDraft },
   }));
 }
 
-const { article } = Astro.props;
+const { article, isDraft } = Astro.props;
 const { Content, headings, remarkPluginFrontmatter } = await render(article);
 const hasCover =
   article.data.cover !== '' && article.data.cover !== undefined && article.data.cover !== null;
@@ -26,6 +40,10 @@ const coverSrc = hasCover
     : article.data.cover
   : undefined;
 const description = article.data.description || remarkPluginFrontmatter.excerpt;
+const publishTime =
+  'published' in article.data
+    ? article.data.published
+    : dayjs(remarkPluginFrontmatter.createAt).toDate();
 ---
 
 <PostPageLayout
@@ -39,7 +57,7 @@ const description = article.data.description || remarkPluginFrontmatter.excerpt;
   <Fragment slot="header-content">
     <PostInfo
       title={article.data.title}
-      publishedAt={article.data.published}
+      publishedAt={publishTime}
       category={article.data.category}
       tags={article.data.tags}
       wordCount={remarkPluginFrontmatter.words}
@@ -54,7 +72,18 @@ const description = article.data.description || remarkPluginFrontmatter.excerpt;
     )
   }
   <Markdown>
+    {
+      isDraft && (
+        <div class="admonition admonition-note">
+          <p class="admonition-title">
+            <Icon name="material-symbols:info-outline-rounded" />
+            NOTE
+          </p>
+          <Fragment set:html={new MarkdownIt().render(i18n(I18nKey.draftDevNote)!)} />
+        </div>
+      )
+    }
     <Content />
   </Markdown>
-  <License time={article.data.published} lang={article.data.lang} />
+  <License time={publishTime} lang={article.data.lang} />
 </PostPageLayout>
src/types/config.ts
@@ -179,6 +179,15 @@ export type SiteConfig = {
       };
 };
 
+export type BuildConfig = {
+  /**
+   * Whether to show drafts on development mode.
+   *
+   * 是否在开发模式下显示草稿。
+   */
+  showDraftsOnDev: boolean;
+};
+
 export type ProfileConfig = {
   /**
    * The avatar of the profile.
src/utils/content-utils.ts
@@ -1,12 +1,31 @@
+import { buildConfig } from '@/config';
 import type { BlogPostData } from '@/types/data';
 import type { BlogPost } from '@/types/data';
 import I18nKey from '@i18n/I18nKey';
 import { i18n } from '@i18n/translation';
-import { getCollection } from 'astro:content';
+import { getCollection, render } from 'astro:content';
+import dayjs from 'dayjs';
 import path from 'path';
 
 export async function getSortedPosts(): Promise<BlogPost[]> {
-  const allBlogPosts = (await getCollection('posts')) as unknown as BlogPost[];
+  let allBlogPosts;
+  const posts = (await getCollection('posts')) as unknown as BlogPost[];
+  const onDev = import.meta.env.DEV;
+
+  if (onDev && buildConfig.showDraftsOnDev) {
+    const draftEntries = await getCollection('drafts');
+    const drafts = await Promise.all(
+      draftEntries.map(async (draft) => {
+        const { remarkPluginFrontmatter } = await render(draft);
+        const published = dayjs(remarkPluginFrontmatter.createAt as string).toDate();
+        const data = Object.assign(draft.data, { published });
+        return Object.assign(draft, { data }) as unknown as BlogPost;
+      })
+    );
+    allBlogPosts = [...posts, ...drafts];
+  } else {
+    allBlogPosts = posts;
+  }
   const sortedBlogPosts = allBlogPosts.sort(
     (a: { data: BlogPostData }, b: { data: BlogPostData }) => {
       const dateA = new Date(a.data.published);
src/config.ts
@@ -1,6 +1,7 @@
 import type {
   ArticleConfig,
   AsideConfig,
+  BuildConfig,
   CommentConfig,
   FooterConfig,
   LicenseConfig,
@@ -38,6 +39,10 @@ export const siteConfig: SiteConfig = {
   },
 };
 
+export const buildConfig: BuildConfig = {
+  showDraftsOnDev: true,
+};
+
 export const profileConfig: ProfileConfig = {
   avatar: 'assets/img/avatar.jpg',
   name: 'Lorem Ipsum',
src/content.config.ts
@@ -10,7 +10,23 @@ const postsCollection = defineCollection({
     title: z.string(),
     slug: z.string(),
     published: z.date(),
-    draft: z.boolean().optional().default(false),
+    description: z.string().optional().default(''),
+    cover: z.string().optional().default(''),
+    tags: z.array(z.string()).optional().default([]),
+    category: z.string().optional().default(''),
+    lang: z.string().optional().default(''),
+    comment: z.boolean().optional().default(true),
+  }),
+});
+
+const draftsCollection = defineCollection({
+  loader: glob({
+    pattern: '**/*.{md,mdx}',
+    base: 'src/content/drafts',
+  }),
+  schema: z.object({
+    title: z.string(),
+    slug: z.string(),
     description: z.string().optional().default(''),
     cover: z.string().optional().default(''),
     tags: z.array(z.string()).optional().default([]),
@@ -34,5 +50,6 @@ const specCollection = defineCollection({
 
 export const collections = {
   posts: postsCollection,
+  drafts: draftsCollection,
   spec: specCollection,
 };