How to Display Blog and Documentation Counts in Docusaurus
Adding a metric counter that displays the total number of blog articles and documentation pages is a great way to make a Docusaurus site feel active.
However, since Docusaurus compiles your pages statically, you cannot call internal hooks like useBlogPosts() inside static landing pages (such as src/pages/index.js). Doing so causes server-side rendering (SSR) failures during build execution on platforms like Vercel.
This guide provides a robust, build-time solution that counts flat files, subdirectories, and nested folders, saving the results to a static JSON file that can be safely imported anywhere in your project.
The Directory Counting Challengeโ
A typical Docusaurus repository organizes content in two distinct formats:
- Flat Files: Individual markdown files sitting directly in the parent directory (e.g.
blog/my-first-post.md). - Folder-based Structures: Nested subdirectories that contain an entry index file (e.g.
blog/nested-post/index.mdx).
A naive file counter that only checks file extensions in the root directory will skip folder-based articles entirely. To count all items correctly, the script must parse flat files, check subdirectories for markdown content, and recursively traverse documentation paths.
Implementing the Build-Time Scriptโ
Create a Node.js script in your project root at scripts/blogStats.js. This script scans your blog/ and docs/ paths, calculates their sizes, and saves the output to a JSON file:
// scripts/blogStats.js
const fs = require('fs');
const path = require('path');
const blogDir = path.join(__dirname, '..', 'blog');
const docsDir = path.join(__dirname, '..', 'docs');
/**
* Counts blog articles.
* Checks for individual markdown files and folders containing a markdown file.
*/
function countBlogEntries(dir) {
if (!fs.existsSync(dir)) return 0;
let count = 0;
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isFile() && (entry.name.endsWith('.md') || entry.name.endsWith('.mdx'))) {
count += 1;
} else if (entry.isDirectory()) {
const innerFiles = fs.readdirSync(fullPath);
const hasMarkdown = innerFiles.some(f => f.endsWith('.md') || f.endsWith('.mdx'));
if (hasMarkdown) {
count += 1;
}
}
}
return count;
}
/**
* Recursively counts all markdown files in the docs directory.
*/
function countMarkdownFilesRecursive(dir) {
if (!fs.existsSync(dir)) return 0;
let count = 0;
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isFile() && (entry.name.endsWith('.md') || entry.name.endsWith('.mdx'))) {
count += 1;
} else if (entry.isDirectory()) {
count += countMarkdownFilesRecursive(fullPath);
}
}
return count;
}
const blogCount = countBlogEntries(blogDir);
const docsCount = countMarkdownFilesRecursive(docsDir);
const totalCount = blogCount + docsCount;
// Ensure output directory exists
const outputPath = path.join(__dirname, '..', 'src', 'data');
if (!fs.existsSync(outputPath)) {
fs.mkdirSync(outputPath, { recursive: true });
}
// Write counts to JSON
fs.writeFileSync(
path.join(outputPath, 'blogStats.json'),
JSON.stringify({
blog: blogCount,
docs: docsCount,
total: totalCount
}, null, 2)
);
console.log(`โ
Blog count: ${blogCount}`);
console.log(`โ
Docs count: ${docsCount}`);
console.log(`โ
Total count: ${totalCount}`);
Hooking the Script into Docusaurus Build Lifecycleโ
Configure your package.json to execute this counter before building the static site:
{
"scripts": {
"prebuild": "node scripts/blogStats.js",
"prestart": "node scripts/blogStats.js",
"build": "docusaurus build",
"start": "docusaurus start"
}
}
By leveraging prebuild and prestart, Node.js runs the count calculations automatically before Docusaurus compiles the source files.
Creating the Counter Componentโ
Now, create a React component at src/components/BlogPostCount.js to import and display this statistics data:
// src/components/BlogPostCount.js
import React from 'react';
import stats from '../data/blogStats.json';
export default function BlogPostCount() {
return (
<div className="stats-container">
<p>๐ Total Blog Articles: <strong>{stats.blog}</strong></p>
<p>๐ Documentation Pages: <strong>{stats.docs}</strong></p>
<p>โจ Combined Total: <strong>{stats.total}</strong></p>
</div>
);
}
Import this component into your home page (src/pages/index.js) to display the live count:
// src/pages/index.js
import React from 'react';
import Layout from '@theme/Layout';
import BlogPostCount from '../components/BlogPostCount';
export default function Home() {
return (
<Layout title="Home">
<main className="container margin-vert--xl">
<h1>Welcome to My Site</h1>
<BlogPostCount />
</main>
</Layout>
);
}
Sourcesโ
- [1] Docusaurus Documentation: Static Site Generation (SSG) Architecture
- [2] Node.js Documentation: fs Module Synchronous File APIs
- [3] Vercel Documentation: Build Step Configurations and Lifecycle Hooks
