A lightweight CLI tool that converts your React SPA into static HTML files, each acting as a standalone entry point.
- Prerender specified React app routes into static HTML files
- Dynamic route generation from files, APIs, or databases
- Flexible output structure (flat files or nested directories)
- Outputs static pages in a configurable directory
- Supports custom route lists via
prerender.config.js
- Copies static assets excluding HTML files
- Easy-to-use CLI with debug support
- Cross-platform compatibility
Install as a development dependency:
npm install --save-dev react-static-prerender
-
Create a
prerender.config.js
file in your project root to specify routes, input build directory, and output directory.Static Routes Example
If your project has
"type": "module"
in package.json:export default { routes: ["/", "/about", "/contact"], outDir: "static-pages", serveDir: "build", flatOutput: false, // Optional: true for about.html, false for about/index.html buildCommand: "npm run build" // Default, can be omitted };
For Vite/Yarn/PNPM projects:
export default { routes: ["/", "/about", "/contact"], outDir: "static-pages", serveDir: "dist", // Vite uses 'dist' buildCommand: "vite/yarn/pnpm build" };
If your project uses CommonJS (no
"type": "module"
):module.exports = { routes: ["/", "/about", "/contact"], outDir: "static-pages", serveDir: "build", flatOutput: false, // Optional: true for about.html, false for about/index.html };
For dynamic content like blog posts, product pages, or any data-driven routes, use a function-based configuration:
// prerender.config.js
import fs from 'fs/promises';
import path from 'path';
export default async function() {
// Read blog posts from markdown files
const blogPosts = await getBlogPostsFromFiles();
const blogRoutes = blogPosts.map(post => `/blog/${post.slug}`);
return {
routes: [
"/",
"/about",
"/blog",
...blogRoutes // Dynamic blog routes
],
outDir: "static-pages",
serveDir: "build"
};
}
async function getBlogPostsFromFiles() {
try {
const postsDir = path.join(process.cwd(), 'content/blog');
const files = await fs.readdir(postsDir);
return files
.filter(file => file.endsWith('.md'))
.map(file => ({
slug: file.replace(/\.md$/, ''),
filename: file
}));
} catch (error) {
console.warn('⚠️ Could not read blog posts:', error.message);
return [];
}
}
// prerender.config.js
import fs from 'fs/promises';
export default async function() {
let blogRoutes = [];
try {
const postsData = await fs.readFile('./src/data/posts.json', 'utf-8');
const posts = JSON.parse(postsData);
blogRoutes = posts.map(post => `/blog/${post.slug}`);
} catch (error) {
console.warn('⚠️ Could not load blog posts:', error.message);
}
return {
routes: ["/", "/blog", ...blogRoutes],
outDir: "static-pages",
serveDir: "build"
};
}
// prerender.config.js
export default async function() {
const blogRoutes = await getBlogRoutesFromAPI();
return {
routes: ["/", "/blog", ...blogRoutes],
outDir: "static-pages",
serveDir: "build"
};
}
async function getBlogRoutesFromAPI() {
try {
// Example: Contentful, Strapi, Ghost, etc.
const response = await fetch('https://your-cms.com/api/posts?fields=slug');
const data = await response.json();
return data.posts.map(post => `/blog/${post.slug}`);
} catch (error) {
console.warn('⚠️ Could not fetch from API:', error.message);
return [];
}
}
// prerender.config.js
export default async function() {
const [blogRoutes, productRoutes, categoryRoutes] = await Promise.all([
getBlogRoutes(),
getProductRoutes(),
getCategoryRoutes()
]);
return {
routes: [
// Static routes
"/",
"/about",
"/contact",
// Dynamic routes
...blogRoutes,
...productRoutes,
...categoryRoutes
],
outDir: "static-pages",
serveDir: "build"
};
}
async function getBlogRoutes() {
// Your blog post logic here
return ["/blog/getting-started", "/blog/advanced-tips"];
}
async function getProductRoutes() {
// Your product logic here
return ["/products/widget-1", "/products/gadget-2"];
}
async function getCategoryRoutes() {
// Your category logic here
return ["/category/tech", "/category/design"];
}
// prerender.config.js
const fs = require('fs/promises');
module.exports = async function() {
const blogRoutes = await getBlogRoutes();
return {
routes: ["/", "/blog", ...blogRoutes],
outDir: "static-pages",
serveDir: "build"
};
}
async function getBlogRoutes() {
try {
const postsData = await fs.readFile('./src/data/posts.json', 'utf-8');
const posts = JSON.parse(postsData);
return posts.map(post => `/blog/${post.slug}`);
} catch (error) {
console.warn('⚠️ Could not load blog posts:', error.message);
return [];
}
}
- Make sure your React app is built and ready to be prerendered or run the command with --with-build flag.
- Run the prerender command to generate static HTML pages.
npx react-static-prerender
If you want to automatically build before prerendering:
npx react-static-prerender --with-build
For debugging server issues:
npx react-static-prerender --debug
(Optional) Add an npm script to simplify future runs:
"scripts": {
"prerender": "react-static-prerender --with-build"
}
Then run with:
npm run prerender
Option | Type | Default | Description |
---|---|---|---|
routes |
string[] |
[] |
Array of routes to prerender (e.g., ["/", "/about"] ) |
outDir |
string |
"static-pages" |
Output directory for generated static files |
serveDir |
string |
"build" |
Directory containing your built React app |
buildCommand |
string |
"npm run build" |
Command to build your app when using --with-build
|
flatOutput |
boolean |
false |
Output structure: true = about.html , false = about/index.html
|
Flag | Description |
---|---|
--with-build |
Runs npm run build before prerendering |
--debug |
Shows detailed server logs for troubleshooting |
static-pages/
├── index.html # / route
├── about/
│ └── index.html # /about route
├── blog/
│ └── index.html # /blog route
├── blog/
│ ├── getting-started/
│ │ └── index.html # /blog/getting-started route
│ └── advanced-tips/
│ └── index.html # /blog/advanced-tips route
└── contact/
└── index.html # /contact route
static-pages/
├── index.html # / route
├── about.html # /about route
├── blog.html # /blog route
├── blog-getting-started.html # /blog/getting-started route
├── blog-advanced-tips.html # /blog/advanced-tips route
└── contact.html # /contact route
- Blog sites with dynamic post generation
- E-commerce with product pages
- Documentation sites with dynamic content
- Portfolio sites with project pages
- News sites with article pages
- Any SPA with data-driven routes
- SEO Friendly: Pre-generated HTML improves search engine crawling
- Fast Loading: Eliminates client-side rendering delay for initial page load
- Static Hosting: Perfect for CDNs, GitHub Pages, Netlify, Vercel
- Dynamic Content: Generate routes from any data source
- Minimal Setup: Simple configuration with sensible defaults
- Flexible Output: Choose between flat files or nested directory structure
- Node.js 18 or higher
- React app build ready for prerendering or run the command with --with-build flag
Make sure your React app is built before running prerender, or use the --with-build
flag.
Use the --debug
flag to see detailed server logs:
npx react-static-prerender --debug
The tool automatically finds available ports starting from 5050, so port conflicts should be rare.
If you're having issues with dynamic routes:
- Make sure your config file exports a function that returns a promise
- Check that all imported modules are available
- Use
--debug
to see detailed error messages
Contributions are welcome. Please keep code clean and follow best practices.
MIT © Janko Stanic