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

bini-export

v1.0.4

Published

Static export for Bini.js with true SSG — pre-renders routes to static HTML with full page content via headless browser

Readme

bini-export

npm version license vite bini-router typescript PRs Welcome

Static Site Generator for Bini.js projects, with optional true SSG via headless-browser pre-rendering. Discovers routes from src/app/, pre-renders them to static HTML with a pool of parallel headless Chrome tabs, generates 404.html, and leaves dist/ ready for GitHub Pages, S3, Firebase, Surge, and any other static host.


📦 Install

npm install -D bini-export

🚀 Quick Start

1. Add to vite.config.ts

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { biniroute } from 'bini-router';
import { biniExport } from 'bini-export';

export default defineConfig({
  plugins: [
    react(),
    biniroute(),
    biniExport(), // SSG enabled by default (puppeteer installs automatically)
  ],
});

2. Add export script to package.json

{
  "scripts": {
    "export": "vite build --mode export"
  }
}

3. Run export

npm run export

Your static site is now in dist/.


✨ Features

| Feature | Description | |---------|-------------| | True SSG | Pre-renders every discovered route to fully static HTML via a headless-Chrome pool, not just an SPA shell | | Parallel rendering | Routes render concurrently across multiple tabs sharing one browser instance (tunable via concurrency) | | SEO Ready | Rendered content is present in the HTML for search engines and AI crawlers | | File-based Routing | Automatically discovers routes from src/app/ (page.tsx / page.jsx) | | Route Groups | Ignores (folder) segments in the URL path | | Private Folders | Skips any _folder (and files starting with _) during route collection | | MDX/Markdown pages | Treats .mdx/.md files under src/app/ as routes (rendering itself is handled by your MDX loader, not this plugin) | | Runtime CSS capture | Captures <style>/<link rel="stylesheet"> tags injected into <head> at runtime (e.g. CSS-in-JS) so pages are styled on first paint, not just after hydration | | Asset path normalization | Rewrites relative href/src paths to absolute so nested route pages (e.g. /about/index.html) don't lose CSS/JS | | 404 Handling | Generates 404.html — either a copy of your custom not-found page, or a redirect-and-restore script for SPA routing on static hosts | | Graceful fallback | Since puppeteer ships as a dependency, this mainly guards against edge cases (corrupted install, unsupported platform, a route erroring out mid-render) — any route that fails to pre-render falls back to your built SPA shell instead of failing the whole build |

Not currently supported (despite sometimes being assumed for SSG tools like this): dynamic routes with bracket segments ([slug]) are skipped automatically during route collection — there's no getStaticPaths mechanism. If you need a dynamic route pre-rendered, pass its concrete paths explicitly via the routes option.


📁 How It Works

  1. Build — Vite builds your app normally for the export mode
  2. Collect Routes — Discovers static routes from src/app/ (or uses routes if you passed it explicitly)
  3. Normalize the template — Reads the built dist/index.html and rewrites relative asset paths to absolute
  4. Launch Browser — Starts a local Vite preview server and one headless Chrome instance
  5. Pre-render in parallel — A pool of tabs (default up to min(8, cpus × 2)) pulls routes from a shared queue; each tab navigates, waits for your content and any runtime-injected styles, then extracts the rendered HTML
  6. Save — Writes each route's HTML to its matching output path (/aboutdist/about/index.html)
  7. Shell fallback — Any route that wasn't successfully pre-rendered (puppeteer missing, or that route errored) gets the plain built shell instead
  8. 404.html — Written last, either from your custom not-found page or as a redirect-and-restore script

⚙️ Options

biniExport({
  // Vite mode that activates this plugin
  mode?: string; // @default 'export'

  // Write dist/404.html
  copy404?: boolean; // @default true

  // Enable true SSG via headless-browser prerendering.
  // If false (or puppeteer is missing), routes get the plain SPA shell.
  ssg?: boolean; // @default true

  // Routes to pre-render. Auto-detected from src/app/ if not specified.
  // Required if you need dynamic ([slug]) routes rendered, since those
  // are skipped by auto-detection.
  routes?: string[];

  // Selector that must exist in the DOM before a route is considered rendered
  waitForSelector?: string; // @default '#root'

  // Max time (ms) to wait for a single route to finish rendering
  renderTimeoutMs?: number; // @default 15000

  // Number of routes rendered in parallel (separate tabs, one shared browser)
  concurrency?: number; // @default min(8, cpus() * 2)

  // How long to wait for a 'bini-render-ready' custom event before giving up
  // and using whatever's currently in the DOM
  readyEventTimeoutMs?: number; // @default 300

  // Puppeteer's page.goto waitUntil condition.
  // 'load'/'domcontentloaded' are fast; 'networkidle0'/'networkidle2' wait
  // longer but are safer if your content depends on requests firing after load.
  navigationWaitUntil?: 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'; // @default 'load'

  // Custom Puppeteer launch options (merged over internal defaults tuned
  // for headless batch rendering — background throttling, extensions, etc.
  // are disabled by default)
  puppeteerOptions?: {
    headless?: boolean;
    args?: string[];
    executablePath?: string;
    timeout?: number;
  };
})

A note on navigationWaitUntil and dynamic content

If your page fetches data after the load event fires (e.g. inside a useEffect), the default 'load' may snapshot before that data resolves. Either:

  • make sure your app dispatches a bini-render-ready CustomEvent on document once it's actually done rendering, or
  • set navigationWaitUntil: 'networkidle0' for slower-but-safer behavior.

A note on blocked resources during rendering

To speed up pre-rendering, image/font/media requests are blocked at the network layer (via CDP Network.setBlockedURLs) while Chrome renders each route. This only affects what Puppeteer fetches during the render step — it has no effect on your shipped dist/ output or what real visitors' browsers load. If your app does font-based layout measurement (e.g. canvas text-fitting) during initial render, be aware fallback fonts will be in effect at snapshot time.


🗂️ Output Structure

Actual structure depends on your Vite build config, but a typical output looks like:

dist/
├── index.html          ✅ Pre-rendered home page
├── 404.html            ✅ Custom not-found copy, or redirect handler
├── about/
│   └── index.html      ✅ Pre-rendered about page
├── docs/
│   ├── index.html      ✅ Pre-rendered docs landing
│   └── api-cors/
│       └── index.html  ✅ Pre-rendered nested page
└── assets/              ✅ Hashed JS/CSS/image bundles (Vite's default output)

Any route that failed to pre-render (or if puppeteer/ssg isn't enabled) still gets an index.html at the correct path — it's just your built SPA shell rather than pre-rendered content.


🛠️ 404 Handling

| Situation | What gets written to 404.html | |-----------|----------------------------------| | src/app/not-found.tsx or not-found.jsx exists | A copy of the built index.html template — your client-side router renders the custom not-found UI | | No custom not-found file | A small redirect script: saves the requested path to sessionStorage, redirects to the site root, and a receiver script on every page restores the URL via history.replaceState |


🌐 Works on Any Static Host

Pre-rendered static routes work anywhere. For paths not known at build time (e.g. you rely on client-side routing for something not in your route list), point your host's error/fallback page at 404.html:

| Host | Static Routes | Client-side Fallback Routes | |------|---------------|------------------------------| | GitHub Pages | ✅ | ✅ via 404.html | | AWS S3 + CloudFront | ✅ | ✅ set the error document to 404.html | | Firebase Hosting | ✅ | ✅ via 404.html rewrite | | Surge.sh | ✅ | ✅ via 404.html | | Netlify (static) | ✅ | ✅ via 404.html | | Vercel (static) | ✅ | ✅ via 404.html |


📚 Related Packages


🤝 Contributing

Contributions are welcome! Please read our Contributing Guide first.

  1. Fork the repository
  2. Create a feature branch
  3. Commit your changes
  4. Push to the branch
  5. Open a Pull Request

📄 License

MIT © Binidu Ranasinghe