generate-blog-data.js 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. #!/usr/bin/env node
  2. import { readdirSync, readFileSync, writeFileSync } from 'fs'
  3. import { join } from 'path'
  4. import matter from 'gray-matter'
  5. import MarkdownIt from 'markdown-it'
  6. const md = new MarkdownIt()
  7. // Read all markdown files in the posts directory
  8. function getPosts(postsDir) {
  9. try {
  10. const files = readdirSync(postsDir)
  11. const posts = []
  12. for (const file of files) {
  13. if (file.endsWith('.md')) {
  14. const filePath = join(postsDir, file)
  15. const content = readFileSync(filePath, 'utf-8')
  16. const { data: frontmatter } = matter(content)
  17. if (frontmatter.title && frontmatter.date) {
  18. posts.push({
  19. title: frontmatter.title,
  20. url: `/posts/${file.replace('.md', '')}`,
  21. date: formatDate(frontmatter.date),
  22. author: frontmatter.author,
  23. tags: frontmatter.tags,
  24. excerpt: frontmatter.excerpt
  25. ? md.render(frontmatter.excerpt)
  26. : generateExcerpt(content)
  27. })
  28. }
  29. }
  30. }
  31. // Sort by date, newest first
  32. return posts.sort((a, b) => new Date(b.date.time) - new Date(a.date.time))
  33. } catch (error) {
  34. console.error('Error reading posts:', error)
  35. return []
  36. }
  37. }
  38. function formatDate(raw) {
  39. const date = new Date(raw)
  40. date.setUTCHours(12)
  41. return {
  42. time: +date,
  43. string: date.toLocaleDateString('en-US', {
  44. year: 'numeric',
  45. month: 'long',
  46. day: 'numeric'
  47. })
  48. }
  49. }
  50. function generateExcerpt(content) {
  51. // Remove frontmatter
  52. const contentWithoutFrontmatter = content.replace(/---[\s\S]*?---/, '')
  53. // Split by paragraphs (separated by blank lines)
  54. const paragraphs = contentWithoutFrontmatter
  55. .split(/\n\s*\n/)
  56. .map(p => p.trim())
  57. .filter(p => p.length > 0)
  58. // Take the first three complete paragraphs
  59. let excerpt = ''
  60. let paragraphCount = 0
  61. const maxParagraphs = 3
  62. for (const paragraph of paragraphs) {
  63. // Skip heading lines (starting with #)
  64. if (paragraph.startsWith('#')) {
  65. continue
  66. }
  67. // If the paragraph is too long (>300 chars), truncate to 300 chars
  68. const truncatedParagraph = paragraph.length > 300
  69. ? paragraph.slice(0, 300).trim() + '...'
  70. : paragraph
  71. excerpt += truncatedParagraph + '\n\n'
  72. paragraphCount++
  73. if (paragraphCount >= maxParagraphs) {
  74. break
  75. }
  76. }
  77. // If no suitable paragraph found, use the original method
  78. if (!excerpt.trim()) {
  79. const rawExcerpt = contentWithoutFrontmatter.slice(0, 200).trim() + '...'
  80. return md.render(rawExcerpt)
  81. }
  82. // Convert to HTML
  83. return md.render(excerpt.trim())
  84. }
  85. // Generate English blog data
  86. const enPosts = getPosts(join(process.cwd(), 'docs/posts'))
  87. const enData = `export const posts = ${JSON.stringify(enPosts, null, 2)}`
  88. // Generate Chinese blog data
  89. const zhPosts = getPosts(join(process.cwd(), 'docs/zh/posts'))
  90. // Add /zh/ prefix for Chinese blog post URLs
  91. const zhPostsWithPrefix = zhPosts.map(post => ({
  92. ...post,
  93. url: `/zh${post.url}`
  94. }))
  95. const zhData = `export const posts = ${JSON.stringify(zhPostsWithPrefix, null, 2)}`
  96. // Write data files
  97. writeFileSync(join(process.cwd(), 'docs/.vitepress/data/blog-data.js'), enData)
  98. writeFileSync(join(process.cwd(), 'docs/.vitepress/data/zh-blog-data.js'), zhData)
  99. console.log(`Generated blog data: ${enPosts.length} English posts, ${zhPosts.length} Chinese posts`)