The Node.js® Website
1'use strict';
2
3import { createReadStream } from 'node:fs';
4import { basename, extname, join } from 'node:path';
5import readline from 'node:readline';
6
7import graymatter from 'gray-matter';
8
9import { getMarkdownFiles } from '../../next.helpers.mjs';
10
11// gets the current blog path based on local module path
12const blogPath = join(process.cwd(), 'pages/en/blog');
13
14/**
15 * This contains the metadata of all available blog categories
16 */
17const blogCategories = new Set(['all']);
18
19/**
20 * This method parses the source (raw) Markdown content into Frontmatter
21 * and returns basic information for blog posts
22 *
23 * @param {string} filename the filename related to the blogpost
24 * @param {string} source the source markdown content of the blog post
25 */
26const getFrontMatter = (filename, source) => {
27 const {
28 title = 'Untitled',
29 author = 'The Node.js Project',
30 date = new Date(),
31 category = 'uncategorized',
32 } = graymatter(source).data;
33
34 // We also use publishing years as categories for the blog
35 const publishYear = new Date(date).getUTCFullYear();
36
37 // Provides a full list of categories for the Blog Post which consists of
38 // all = (all blog posts), publish year and the actual blog category
39 const categories = [category, `year-${publishYear}`, 'all'];
40
41 // we add the year to the categories set
42 blogCategories.add(`year-${publishYear}`);
43
44 // we add the category to the categories set
45 blogCategories.add(category);
46
47 // this is the url used for the blog post it based on the category and filename
48 const slug = `/blog/${category}/${basename(filename, extname(filename))}`;
49
50 return { title, author, date: new Date(date), categories, slug };
51};
52
53/**
54 * This method is used to generate the Node.js Website Blog Data
55 * for self-consumption during RSC and Static Builds
56 *
57 * @return {Promise<import('../../types').BlogData>}
58 */
59const generateBlogData = async () => {
60 // We retrieve the full pathnames of all Blog Posts to read each file individually
61 const filenames = await getMarkdownFiles(process.cwd(), 'pages/en/blog', [
62 '**/index.md',
63 ]);
64
65 return new Promise(resolve => {
66 const posts = [];
67 const rawFrontmatter = [];
68
69 filenames.forEach(filename => {
70 // We create a stream for reading a file instead of reading the files
71 const _stream = createReadStream(join(blogPath, filename));
72
73 // We create a readline interface to read the file line-by-line
74 const _readLine = readline.createInterface({ input: _stream });
75
76 // Creates an array of the metadata based on the filename
77 // This prevents concurrency issues since the for-loop is synchronous
78 // and these event listeners are not
79 rawFrontmatter[filename] = [0, ''];
80
81 // We read line by line
82 _readLine.on('line', line => {
83 rawFrontmatter[filename][1] += `${line}\n`;
84
85 // We observe the frontmatter separators
86 if (line === '---') {
87 rawFrontmatter[filename][0] += 1;
88 }
89
90 // Once we have two separators we close the readLine and the stream
91 if (rawFrontmatter[filename][0] === 2) {
92 _readLine.close();
93 _stream.close();
94 }
95 });
96
97 // Then we parse gray-matter on the frontmatter
98 // This allows us to only read the frontmatter part of each file
99 // and optimise the read-process as we have thousands of markdown files
100 _readLine.on('close', () => {
101 posts.push(getFrontMatter(filename, rawFrontmatter[filename][1]));
102
103 if (posts.length === filenames.length) {
104 resolve({ categories: [...blogCategories], posts });
105 }
106 });
107 });
108 });
109};
110
111export default generateBlogData;