npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vite-static-renderer

v1.0.3

Published

A performant static site generator with CLI and Vite integration

Downloads

230

Readme

vite-static-renderer

A high-performance static site generator with CLI and Vite integration. Transform your SPA into lightning-fast, SEO-ready static pages with modern browser rendering, intelligent asset handling, and automatic sitemap.xml and robots.txt generation.

✨ Features

  • 🚀 High Performance: Built with Playwright and Fastify for maximum speed
  • ⚡ Vite Integration: Seamless plugin integration with your Vite workflow
  • 🔧 CLI Tool: Standalone command-line interface for any project
  • 🌐 Universal: Works with React, Vue, Angular, Svelte, and any SPA framework
  • 📱 Modern Rendering: Uses real browsers for accurate rendering
  • 🎯 Smart Configuration: Multiple config formats with intelligent defaults
  • ⚙️ Parallel Processing: Configurable concurrent rendering for speed
  • 🔄 Dynamic Routes: Generate routes from APIs, files, or databases
  • 🤖 SEO-Ready: Automatic sitemap.xml and robots.txt generation with Canonical URL rewrite support
  • 📦 Asset Optimization: Intelligent asset copying and optimization
  • 🎨 Flexible Output: Multiple naming strategies and structure options

📦 Installation

As a development dependency:

npm install --save-dev vite-static-renderer
# or
yarn add -D vite-static-renderer
# or
pnpm add -D vite-static-renderer

Global installation (for CLI usage):

npm install -g vite-static-renderer

🚀 Quick Start

1. CLI Usage

Create a configuration file:

// vite-static.config.js
export default {
  canonicalUrl: 'https://www.my-awesome-site.com',
  routes: ['/', '/about', '/contact'],
  inputDir: 'dist',
  outputDir: 'static'
};

Generate static pages:

# Generate with existing build
npx vite-static generate

# Build and generate in one command
npx vite-static generate --build

# With custom config
npx vite-static generate -c my-config.js

2. Vite Plugin Usage

// vite.config.js
import { defineConfig } from 'vite';
import viteStatic from 'vite-static-renderer/vite';

export default defineConfig({
  plugins: [
    viteStatic({
      canonicalUrl: 'https://www.my-awesome-site.com',
      routes: ['/', '/about', '/contact'],
      enabled: true // Set to false to disable in development
    })
  ]
});

Now your static pages will be generated automatically after each build!

⚙️ Configuration

Basic Configuration

// vite-static.config.js
export default {
  // Input/Output
  inputDir: 'dist',           // Built app directory
  outputDir: 'static',        // Static files output
  publicDir: 'public',        // Public assets directory

  // Production Base URL (fixes port/localhost URLs in meta-tags & scripts)
  canonicalUrl: 'https://www.my-awesome-site.com',

  // Routes to render
  routes: ['/', '/about', '/contact'],

  // Browser settings
  browser: {
    headless: true,
    timeout: 30000,
    viewport: { width: 1280, height: 720 }
  },

  // Performance
  parallel: 4,                // Concurrent renders
  retries: 2,                 // Retry failed renders

  // Output structure
  fileNaming: 'nested'        // 'nested' | 'flat' | 'custom'
};

Advanced Configuration

// vite-static.config.js
export default {
  // Dynamic route generation
  routes: async () => {
    const posts = await fetch('/api/posts').then(r => r.json());
    return ['/', ...posts.map(p => `/blog/${p.slug}`)];
  },

  // Multiple dynamic route patterns
  dynamicRoutes: [
    {
      pattern: '/blog/*',
      generator: async () => {
        const posts = await getBlogPosts();
        return posts.map(post => `/blog/${post.slug}`);
      }
    },
    {
      pattern: '/products/*',
      generator: async () => {
        const products = await getProducts();
        return products.map(product => `/products/${product.id}`);
      }
    }
  ],

  // Wait conditions
  waitFor: {
    networkIdle: true,
    selector: '#app',           // Wait for specific element
    timeout: 5000
  },

  // HTML processing
  minifyHtml: true,
  injectMeta: {
    'generator': 'vite-static-renderer',
    'viewport': 'width=device-width, initial-scale=1'
  },

  // Custom file naming
  customNaming: (route) => {
    if (route === '/') return 'home.html';
    return `${route.replace(/^\//, '').replace(/\//g, '-')}.html`;
  },

  // Lifecycle hooks
  beforeRender: async (route) => {
    console.log(`Rendering ${route}...`);
  },

  afterRender: async (route, html) => {
    // Modify HTML before writing
    return html.replace('{{ROUTE}}', route);
  },

  onError: async (route, error) => {
    console.error(`Failed to render ${route}:`, error.message);
  }
};

🔍 SEO Features

Automate the creation of canonical settings, sitemap.xml, and robots.txt for improved search engine indexing.

Canonical URL & Local Port Cleansing

Providing a canonicalUrl at the top level is highly recommended. Because pages are rendered headlessly via a local temporary server, any absolute paths or meta tags generated by your frontend framework (e.g., <link rel="canonical">, <meta property="og:url">, absolute asset URLs) would normally contain localhost port addresses.

Setting canonicalUrl does two things automatically:

  1. Global Port Replacement: Scans the compiled HTML and replaces all instances of the temporary local server (e.g., http://localhost:9099) with your production base URL.
  2. Canonical Tag Auto-Injection: Injects a clean <link rel="canonical" href="https://yourdomain.com/route"> tag in the <head> of each rendered page, or dynamically overwrites existing ones so they are pristine.

Sitemap Generation

Enable sitemap generation by providing a sitemap configuration object. If canonicalUrl is configured, hostname is optional and will fall back to your canonical URL.

// vite-static.config.js
export default {
  // ... other configs
  canonicalUrl: 'https://www.my-awesome-site.com',
  sitemap: {
    // hostname: 'https://www.my-awesome-site.com', //Optional Now
    exclude: ['/404', '/admin/*'] // Optional: Exclude specific routes using glob patterns
  }
};

This will generate a sitemap.xml file in your outputDir with all successfully rendered routes.

Robots.txt Generation

Create a robots.txt file by defining a robots configuration object with a policy array.

// vite-static.config.js
export default {
  // ... other configs
  robots: {
    policy: [
      {
        userAgent: '*',
        disallow: '/admin',
        allow: '/public/'
      },
      {
        userAgent: 'Googlebot',
        disallow: '/private/'
      }
    ]
  }
};

Note: If sitemap or canonicalUrl is configured, a Sitemap: directive pointing to the correct absolute path will be automatically added to your robots.txt file, complying with search engine crawlers' best practices.

🌐 Dynamic Routes

From API/CMS

// vite-static.config.js
export default async function() {
  // Fetch from headless CMS
  const posts = await fetch('https://api.contentful.com/spaces/YOUR_SPACE/entries', {
    headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
  }).then(r => r.json());

  const blogRoutes = posts.items.map(post => `/blog/${post.fields.slug}`);

  return {
    routes: ['/', '/blog', ...blogRoutes],
    inputDir: 'dist',
    outputDir: 'static'
  };
}

From Local Files

// vite-static.config.js
import { readdir, readFile } from 'fs/promises';
import { join } from 'path';

export default async function() {
  // Read markdown files
  const postsDir = join(process.cwd(), 'content/posts');
  const files = await readdir(postsDir);
  
  const blogRoutes = files
    .filter(file => file.endsWith('.md'))
    .map(file => `/blog/${file.replace('.md', '')}`);

  return {
    routes: ['/', '/blog', ...blogRoutes],
    inputDir: 'dist',
    outputDir: 'static'
  };
}

From JSON Data

// vite-static.config.js
import { readFile } from 'fs/promises';

export default async function() {
  const data = JSON.parse(await readFile('data/products.json', 'utf-8'));
  const productRoutes = data.products.map(p => `/products/${p.slug}`);

  return {
    routes: ['/', '/products', ...productRoutes],
    inputDir: 'dist',
    outputDir: 'static'
  };
}

🔧 CLI Reference

Commands

# Generate static pages
vite-static generate [options]
vite-static gen [options]        # Alias

# Initialize configuration
vite-static init [options]

Options

# Configuration
-c, --config <path>           # Custom config file path
-i, --input <dir>             # Input directory (default: dist)
-o, --output <dir>            # Output directory (default: static)

# Build integration
--build                       # Run build command before generating
--no-cache                    # Disable caching

# Performance
-p, --parallel <num>          # Parallel renders (default: 4)

# Debugging
--verbose                     # Verbose logging

Examples

# Basic usage
npx vite-static generate

# With build step
npx vite-static generate --build

# Custom configuration
npx vite-static generate -c custom.config.js

# Override directories
npx vite-static generate -i build -o public

# High performance
npx vite-static generate --parallel 8

# Initialize new project
npx vite-static init --template blog

📁 Output Structures

Nested Structure (default)

static/
├── index.html              # /
├── about/
│   └── index.html         # /about
├── blog/
│   └── index.html         # /blog
│   ├── post-1/
│   │   └── index.html     # /blog/post-1
│   └── post-2/
│       └── index.html     # /blog/post-2
└── assets/
    ├── style.css
    └── app.js

Flat Structure

// Config
export default {
  fileNaming: 'flat'
};
static/
├── index.html              # /
├── about.html             # /about
├── blog.html              # /blog
├── blog-post-1.html       # /blog/post-1
├── blog-post-2.html       # /blog/post-2
└── assets/
    ├── style.css
    └── app.js

Custom Naming

// Config
export default {
  fileNaming: 'custom',
  customNaming: (route) => {
    if (route === '/') return 'home.html';
    if (route.startsWith('/blog/')) {
      return `articles/${route.replace('/blog/', '')}.html`;
    }
    return `${route.replace(/^\//, '')}.html`;
  }
};

🎯 Framework Examples

React

// vite-static.config.js
export default {
  routes: ['/', '/about', '/products'],
  waitFor: {
    selector: '#root',        // Wait for React root
    networkIdle: true
  }
};

Vue

// vite-static.config.js
export default {
  routes: ['/', '/about', '/products'],
  waitFor: {
    selector: '#app',         // Wait for Vue app
    networkIdle: true
  }
};

Next.js

// vite-static.config.js
export default {
  inputDir: 'out',            // Next.js static export
  routes: ['/', '/about'],
  buildCommand: 'npm run build && npm run export'
};

Nuxt

// vite-static.config.js
export default {
  inputDir: 'dist',
  routes: ['/', '/about'],
  buildCommand: 'npm run generate'
};

📊 Performance Tips

Optimize Rendering Speed

export default {
  // Increase parallel processing
  parallel: 8,

  // Optimize wait conditions
  waitFor: {
    networkIdle: false,       // Disable if not needed
    load: true,               // Wait for page load only
    timeout: 3000             // Reduce timeout
  },

  // Enable caching
  cache: true,
  cacheDir: '.cache/static',

  // Skip unnecessary assets
  browser: {
    headless: true,           // Always use headless in production
    timeout: 15000            // Reduce browser timeout
  }
};

Memory Management

export default {
  // Process routes in smaller batches
  parallel: 4,               // Don't overload memory

  // Enable retries for stability
  retries: 3,

  // Clean up between renders
  beforeRender: async (route) => {
    if (global.gc) global.gc();
  }
};

🔍 Troubleshooting

Common Issues

Build directory not found

# Make sure your app is built first
npm run build
npx vite-static generate

# Or use --build flag
npx vite-static generate --build

Rendering timeouts

export default {
  waitFor: {
    timeout: 60000,           // Increase timeout
    networkIdle: false        // Disable network wait
  },
  browser: {
    timeout: 60000            // Increase browser timeout
  }
};

Memory issues

export default {
  parallel: 2,                // Reduce concurrent renders
  browser: {
    headless: true            // Always use headless
  }
};

Route not rendering correctly

export default {
  waitFor: {
    selector: '.content',     // Wait for specific content
    timeout: 10000
  },
  beforeRender: async (route) => {
    console.log(`Rendering: ${route}`);
  }
};

🤝 Integration Examples

GitHub Actions

# .github/workflows/deploy.yml
name: Build and Deploy
on:
  push:
    branches: [ main ]

jobs:
  build-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      
      - run: npm install
      - run: npx vite-static generate --build
      
      - uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./static

Docker

FROM node:18-alpine

WORKDIR /app
COPY package*.json ./
RUN npm install

COPY . .
RUN npm run build
RUN npx vite-static generate

FROM nginx:alpine
COPY --from=0 /app/static /usr/share/nginx/html

Netlify

# netlify.toml
[build]
  command = "npm run build && npx vite-static generate"
  publish = "static"

[[plugins]]
  package = "@netlify/plugin-lighthouse"

📚 API Reference

Configuration Types

interface RenderConfig {
  // I/O Configuration
  inputDir?: string;
  outputDir?: string;
  publicDir?: string;
  
  // Routes
  routes?: string[] | (() => Promise<string[]>);
  dynamicRoutes?: DynamicRoute[];
  
  // Browser Options
  browser?: BrowserConfig;
  waitFor?: WaitConfig;
  
  // Output Options
  fileNaming?: 'nested' | 'flat' | 'custom';
  customNaming?: (route: string) => string;
  minifyHtml?: boolean;
  
  // Performance
  parallel?: number;
  retries?: number;
  cache?: boolean;
  
  // SEO
  canonicalUrl?: string;
  sitemap?: SitemapConfig;
  robots?: RobotsTxtConfig;

  // Hooks
  beforeRender?: (route: string) => Promise<void>;
  afterRender?: (route: string, html: string) => Promise<string>;
  onError?: (route: string, error: Error) => Promise<void>;
}

🎉 Migration Guide

From other static generators

From prerender-spa-plugin

// Old webpack config
new PrerenderSPAPlugin({
  staticDir: path.join(__dirname, 'dist'),
  routes: ['/', '/about'],
  outputDir: path.join(__dirname, 'prerendered')
});

// New Vite config
export default {
  routes: ['/', '/about'],
  inputDir: 'dist',
  outputDir: 'prerendered'
};

🆘 Support


Made with ❤️ for the modern web