Skip to content

如何在 AstroPaper 中添加預估閱讀時間

Updated: at 02:53 PM

根據 Astro 文檔,我們可以使用 remark 插件在前置資料中添加閱讀時間屬性。然而,由於某些原因,我們無法按照 Astro 文檔中的說明添加此功能。因此,為了實現這一點,我們需要稍微調整一下。本文將演示如何做到這一點。

目錄

在 PostDetails 中添加閱讀時間

步驟 (1) 安裝所需的依賴項。

npm install reading-time mdast-util-to-string

步驟 (2) 在 utils 目錄下創建 remark-reading-time.mjs 文件

import getReadingTime from "reading-time";
import { toString } from "mdast-util-to-string";

export function remarkReadingTime() {
  return function (tree, { data }) {
    const textOnPage = toString(tree);
    const readingTime = getReadingTime(textOnPage);
    // readingTime.text 會給我們一個友好的字符串表示的閱讀時間,
    // 即 "3 min read"
    data.astro.frontmatter.minutesRead = readingTime.text;
  };
}

步驟 (3) 將插件添加到 astro.config.ts

import { remarkReadingTime } from "./src/utils/remark-reading-time.mjs"; // 確保你的相對路徑是正確的

// https://astro.build/config
export default defineConfig({
  site: SITE.website,
  integrations: [
    // 其他集成
  ],
  markdown: {
    remarkPlugins: [
      remarkToc,
      remarkReadingTime, // 👈🏻 我們的插件
      [
        remarkCollapse,
        {
          test: "目錄",
        },
      ],
    ],
    // 其他配置
  },
  // 其他配置
});

步驟 (4) 將 readingTime 添加到博客模式 (src/content/config.ts)

import { SITE } from "@config";
import { defineCollection, z } from "astro:content";

const blog = defineCollection({
  type: "content_layer",
  loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }),
  schema: ({ image }) =>
    z.object({
      // 其他...
      canonicalURL: z.string().optional(),
      readingTime: z.string().optional(), // 👈🏻 readingTime 前置資料
      // 其他...
    }),
});

export const collections = { blog };

步驟 (5) 在 src/utils 目錄下創建一個名為 getPostsWithRT.ts 的新文件。

import type { CollectionEntry } from "astro:content";
import { slugifyStr } from "./slugify";

interface Frontmatter {
  frontmatter: {
    title: string;
    minutesRead: string;
  };
}

export const getReadingTime = async () => {
  // 使用 glob 獲取所有文章。這是為了獲取更新的前置資料
  const globPosts = import.meta.glob<Frontmatter>("../content/blog/*.md");

  // 然後,將這些前置資料值設置在一個 JS Map 中,使用鍵值對
  const mapFrontmatter = new Map();
  const globPostsValues = Object.values(globPosts);
  await Promise.all(
    globPostsValues.map(async globPost => {
      const { frontmatter } = await globPost();
      mapFrontmatter.set(
        slugifyStr(frontmatter.title),
        frontmatter.minutesRead
      );
    })
  );

  return mapFrontmatter;
};

const getPostsWithRT = async (posts: CollectionEntry<"blog">[]) => {
  const mapFrontmatter = await getReadingTime();
  return posts.map(post => {
    post.data.readingTime = mapFrontmatter.get(slugifyStr(post.data.title));
    return post;
  });
};

export default getPostsWithRT;

步驟 (6) 將 src/pages/posts/[slug]/index.astrogetStaticPaths 重構如下

---
// 其他導入
import getPostsWithRT from "@utils/getPostsWithRT";

export interface Props {
  post: CollectionEntry<"blog">;
}

export async function getStaticPaths() {
  const posts = await getCollection("blog", ({ data }) => !data.draft);

  const postsWithRT = await getPostsWithRT(posts); // 使用此函數替換閱讀時間邏輯

   const postResult = postsWithRT.map(post => ({ // 確保用 postsWithRT 替換 posts
    params: { slug: post.slug },
    props: { post },
  }));

// 其他代碼

步驟 (7) 將 PostDetails.astro 重構如下。現在你可以在 PostDetails.astro 中訪問並顯示 readingTime

---
// 導入

export interface Props {
  post: CollectionEntry<"blog">;
}

const { post } = Astro.props;

const {
  title,
  author,
  description,
  ogImage,
  readingTime, // 我們現在可以直接從前置資料中訪問 readingTime
  pubDatetime,
  modDatetime,
  tags } = post.data;

// 其他代碼
---

在 PostDetails 之外訪問閱讀時間(可選)

通過遵循前面的步驟,你現在可以在文章詳情頁中訪問 readingTime 前置資料屬性。有時,這正是你想要的。如果是這樣,你可以跳到下一節。然而,如果你想在索引、文章和技術上任何地方顯示「預估閱讀時間」,你需要執行以下額外步驟。

步驟 (1) 更新 utils/getSortedPosts.ts 如下

import type { CollectionEntry } from "astro:content";
import getPostsWithRT from "./getPostsWithRT";

const getSortedPosts = async (posts: CollectionEntry<"blog">[]) => {
  // 確保此函數是異步的
  const postsWithRT = await getPostsWithRT(posts); // 添加閱讀時間
  return postsWithRT
    .filter(({ data }) => !data.draft)
    .sort(
      (a, b) =>
        Math.floor(
          new Date(b.data.modDatetime ?? b.data.pubDatetime).getTime() / 1000
        ) -
        Math.floor(
          new Date(a.data.modDatetime ?? a.data.pubDatetime).getTime() / 1000
        )
    );
};

export default getSortedPosts;

步驟 (2) 確保重構每個使用 getSortedPosts 函數的文件。你只需在 getSortedPosts 函數前添加 await 關鍵字。

使用 getSortedPosts 函數的文件如下

你所要做的就是這樣

const sortedPosts = getSortedPosts(posts); // 舊代碼 ❌
const sortedPosts = await getSortedPosts(posts); // 新代碼 ✅

現在,getPostsByTag 函數變成了一個異步函數。因此,我們也需要 await getPostsByTag 函數。

const postsByTag = getPostsByTag(posts, tag); // 舊代碼 ❌
const postsByTag = await getPostsByTag(posts, tag); // 新代碼 ✅

此外,更新 src/pages/tags/[tag]/[page].astrogetStaticPaths 如下:

export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
  const posts = await getCollection("blog");
  const tags = getUniqueTags(posts);

  // 確保等待承諾
  const paths = await Promise.all(
    tags.map(async ({ tag, tagName }) => {
      const tagPosts = await getPostsByTag(posts, tag);

      return paginate(tagPosts, {
        params: { tag },
        props: { tagName },
        pageSize: SITE.postPerPage,
      });
    })
  );

  return paths.flat(); // 展平數組
}

現在你可以在 PostDetails 之外的其他地方訪問 readingTime

顯示閱讀時間(可選)

既然你現在可以在文章詳情(或如果你完成了上述部分,則在任何地方)訪問 readingTime,那麼是否顯示 readingTime 完全取決於你。

但在本節中,我將向你展示如何在我的組件中顯示 readingTime。這是可選的。如果你願意,可以忽略本節。

步驟 (1) 更新 Datetime 組件以顯示 readingTime

// 其他代碼

interface Props extends DatetimesProps, EditPostProps {
  size?: "sm" | "lg";
  className?: string;
  readingTime: string | undefined; // 新類型
}

export default function Datetime({
  pubDatetime,
  modDatetime,
  size = "sm",
  className = "",
  editPost,
  postId,
  readingTime, // 新屬性
}: Props) {
  return (
    // 其他代碼
    <span className={`italic ${size === "sm" ? "text-sm" : "text-base"}`}>
      <FormattedDatetime pubDatetime={pubDatetime} modDatetime={modDatetime} />
      <span> ({readingTime})</span> {/* 顯示閱讀時間 */}
      {size === "lg" && <EditPost editPost={editPost} postId={postId} />}
    </span>
    // 其他代碼
  );
}

步驟 (2) 然後,從其父組件傳遞 readingTime 屬性。

文件:Card.tsx

export default function Card({ href, frontmatter, secHeading = true }: Props) {
  const { title, pubDatetime, modDatetime description, readingTime } = frontmatter; // 不要忘記在這裡添加 readingTime
  return (
    ...
    <Datetime
      pubDatetime={pubDatetime}
      modDatetime={modDatetime}
      readingTime={readingTime}
    />
    ...
  );
}

文件:PostDetails.astro

// 其他代碼
<main id="main-content">
  <h1 class="post-title">{title}</h1>
  <Datetime
    pubDatetime={pubDatetime}
    modDatetime={modDatetime}
    size="lg"
    className="my-2"
    readingTime={readingTime}
  />
  {/* 其他代碼 */}
</main>
// 其他代碼

結論

通過遵循提供的步驟和調整,你現在可以將這個有用的功能集成到你的內容中。我希望這篇文章能幫助你在博客中添加 readingTime。AstroPaper 可能會在未來的版本中默認包含閱讀時間。🤷🏻‍♂️

感謝閱讀 🙏🏻


Previous Post
Predefined color schemes
Next Post
在 AstroPaper 主題中新增文章