Skip to content

Commit d1d59fc

Browse files
committed
Feat: add reading time and word count display
- Added readingTime utility to calculate word count and reading duration - Display word count and estimated reading time on article pages - Added unit tests for reading time calculation - Based on ~200 characters/words per minute reading speed
1 parent 5c8bec2 commit d1d59fc

3 files changed

Lines changed: 74 additions & 0 deletions

File tree

src/pages/writing/[slug].astro

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import BaseLayout from '../../layouts/BaseLayout.astro';
33
import { getCollection, render } from 'astro:content';
44
import { formatShanghai } from '../../utils/dateFormat';
5+
import { calculateReadingTime } from '../../utils/readingTime';
56
67
export async function getStaticPaths() {
78
// Include drafts so they can be previewed via direct link.
@@ -18,6 +19,10 @@ const { Content, headings } = await render(post);
1819
1920
const toc = (headings ?? []).filter((h) => h.depth >= 2 && h.depth <= 3);
2021
const isDraft = Boolean(post.data.draft);
22+
23+
// Calculate reading time from post body content
24+
const postBody = post.body || '';
25+
const { words, minutes } = calculateReadingTime(postBody);
2126
---
2227

2328
<BaseLayout
@@ -61,6 +66,7 @@ const isDraft = Boolean(post.data.draft);
6166
</>
6267
))}</>
6368
) : null}
69+
<> · {words} 字 · 约 {minutes} 分钟阅读</>
6470
</p>
6571

6672
<div class="post-content">

src/utils/readingTime.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { calculateReadingTime } from './readingTime';
3+
4+
describe('calculateReadingTime', () => {
5+
it('should count Chinese characters correctly', () => {
6+
const result = calculateReadingTime('你好世界');
7+
expect(result.words).toBe(4);
8+
expect(result.minutes).toBe(1);
9+
});
10+
11+
it('should count English words correctly', () => {
12+
const result = calculateReadingTime('Hello world test');
13+
expect(result.words).toBe(3);
14+
expect(result.minutes).toBe(1);
15+
});
16+
17+
it('should handle mixed content', () => {
18+
const result = calculateReadingTime('你好 Hello 世界 world');
19+
expect(result.words).toBe(6);
20+
});
21+
22+
it('should calculate reading time for longer content', () => {
23+
// 400 Chinese characters should be ~2 minutes
24+
const longText = '你'.repeat(400);
25+
const result = calculateReadingTime(longText);
26+
expect(result.words).toBe(400);
27+
expect(result.minutes).toBe(2);
28+
});
29+
30+
it('should return at least 1 minute', () => {
31+
const result = calculateReadingTime('');
32+
expect(result.words).toBe(0);
33+
expect(result.minutes).toBe(1);
34+
});
35+
36+
it('should ignore HTML tags', () => {
37+
const result = calculateReadingTime('<p>Hello</p> <div>World</div>');
38+
expect(result.words).toBe(2);
39+
});
40+
});

src/utils/readingTime.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Calculate reading time for content
3+
* Based on average reading speed: ~200 Chinese characters per minute
4+
*/
5+
6+
export function calculateReadingTime(content: string): { words: number; minutes: number } {
7+
// Remove HTML tags and extra whitespace
8+
const text = content
9+
.replace(/<[^>]*>/g, '')
10+
.replace(/\s+/g, ' ')
11+
.trim();
12+
13+
// Count Chinese characters and words
14+
// Chinese characters count as 1 word each
15+
// English words are counted by splitting on whitespace
16+
const chineseChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length;
17+
const englishWords = (text.match(/[a-zA-Z0-9]+/g) || []).length;
18+
19+
const totalWords = chineseChars + englishWords;
20+
21+
// Average reading speed: 200 characters/words per minute
22+
const minutes = Math.ceil(totalWords / 200);
23+
24+
return {
25+
words: totalWords,
26+
minutes: Math.max(1, minutes), // At least 1 minute
27+
};
28+
}

0 commit comments

Comments
 (0)