Commit 00dc1c9

HPCesia <me@hpcesia.com>
2025-02-06 14:26:29
refactor: reading time
1 parent 79a220d
src/components/misc/PostInfo.astro
@@ -3,22 +3,26 @@ import { articleConfig } from '@/config';
 import MetaIcon from '@components/widgets/MetaIcon.astro';
 import I18nKey from '@i18n/I18nKey';
 import { i18n } from '@i18n/translation';
-import { countWords } from '@utils/content-utils';
 
 interface Props {
   title: string;
   publishedAt: Date;
   category: string;
   tags: string[];
-  wordCount: ReturnType<typeof countWords>;
+  wordsCount: number;
+  readingTime: number;
   class?: string;
 }
 
-const { title, publishedAt, category, tags, wordCount, class: className } = Astro.props;
-
-const readTime =
-  Math.ceil(wordCount.cjk / articleConfig.readingTime.wordsPerMinute.cjk) +
-  Math.ceil(wordCount.nonCjk / articleConfig.readingTime.wordsPerMinute.nonCjk);
+const {
+  title,
+  publishedAt,
+  category,
+  tags,
+  wordsCount: wordCount,
+  readingTime,
+  class: className,
+} = Astro.props;
 
 const metas: ({ icon: string; text: string; link?: string } | undefined)[] = [
   {
@@ -28,13 +32,13 @@ const metas: ({ icon: string; text: string; link?: string } | undefined)[] = [
   articleConfig.wordCount
     ? {
         icon: 'material-symbols:docs-rounded',
-        text: `${wordCount.total} ${wordCount.total === 1 ? i18n(I18nKey.wordCount) : i18n(I18nKey.wordsCount)}`,
+        text: `${wordCount} ${wordCount === 1 ? i18n(I18nKey.wordCount) : i18n(I18nKey.wordsCount)}`,
       }
     : undefined,
   articleConfig.readingTime
     ? {
         icon: 'material-symbols:nest-clock-farsight-analog-rounded',
-        text: `${readTime} ${readTime === 1 ? i18n(I18nKey.minuteCount) : i18n(I18nKey.minutesCount)}`,
+        text: `${readingTime} ${readingTime === 1 ? i18n(I18nKey.minuteCount) : i18n(I18nKey.minutesCount)}`,
       }
     : undefined,
   category
src/pages/posts/[article].astro
@@ -7,7 +7,6 @@ import Markdown from '@components/utils/Markdown.astro';
 import ProfileCard from '@components/widgets/ProfileCard.astro';
 import TOC from '@components/widgets/TOC.astro';
 import GridLayout from '@layouts/GridLayout.astro';
-import { countWords } from '@utils/content-utils';
 import { getCollection, render } from 'astro:content';
 
 export async function getStaticPaths() {
@@ -19,9 +18,7 @@ export async function getStaticPaths() {
 }
 
 const { article } = Astro.props;
-const { Content, headings } = await render(article);
-
-const wordCount = countWords(article.body || '');
+const { Content, headings, remarkPluginFrontmatter } = await render(article);
 ---
 
 <GridLayout title={article.data.title} description={article.data.description}>
@@ -31,7 +28,8 @@ const wordCount = countWords(article.body || '');
       publishedAt={article.data.published}
       category={article.data.category}
       tags={article.data.tags}
-      wordCount={wordCount}
+      wordsCount={remarkPluginFrontmatter.words}
+      readingTime={remarkPluginFrontmatter.minutes}
       class="mx-2 mt-4"
     />
   </Fragment>
src/plugins/remark-reading-time.mjs
@@ -0,0 +1,11 @@
+import { toString } from 'mdast-util-to-string';
+import getReadingTime from 'reading-time';
+
+export function remarkReadingTime() {
+  return (tree, { data }) => {
+    const textOnPage = toString(tree);
+    const readingTime = getReadingTime(textOnPage);
+    data.astro.frontmatter.minutes = Math.max(1, Math.round(readingTime.minutes));
+    data.astro.frontmatter.words = readingTime.words;
+  };
+}
src/utils/content-utils.ts
@@ -64,19 +64,3 @@ export async function getTimeArchives() {
     }))
     .sort((a, b) => b.year - a.year);
 }
-
-export function countWords(text: string): { cjk: number; nonCjk: number; total: number } {
-  const cjkRegex =
-    /[\u4E00-\u9FFF\u3400-\u4DBF\u20000-\u2A6DF\u2A700-\u2B73F\u2B740-\u2B81F\u2B820-\u2CEAF\uF900-\uFAFF\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF]/g;
-  const cjkCount = (text.match(cjkRegex) || []).length;
-  const nonCjkText = text.replace(cjkRegex, '');
-  const wordCount = nonCjkText
-    .trim()
-    .split(/\s+/)
-    .filter((word) => word.length > 0).length;
-  return {
-    cjk: cjkCount,
-    nonCjk: wordCount,
-    total: cjkCount + wordCount,
-  };
-}
astro.config.mjs
@@ -1,4 +1,5 @@
 // @ts-check
+import { remarkReadingTime } from './src/plugins/remark-reading-time.mjs';
 import sitemap from '@astrojs/sitemap';
 import tailwind from '@astrojs/tailwind';
 import icon from 'astro-icon';
@@ -17,4 +18,7 @@ export default defineConfig({
     sitemap({ filter: (page) => !page.includes('/archives/') && !page.includes('/about/') }),
     pagefind(),
   ],
+  markdown: {
+    remarkPlugins: [remarkReadingTime],
+  },
 });
package.json
@@ -24,7 +24,9 @@
     "astro-pagefind": "^1.8.0",
     "autoprefixer": "^10.4.20",
     "daisyui": "^4.12.23",
+    "mdast-util-to-string": "^4.0.0",
     "postcss-load-config": "^6.0.1",
+    "reading-time": "^1.5.0",
     "sass": "^1.84.0",
     "sharp": "^0.33.5",
     "tailwindcss": "^3.4.17",