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

@liquefy/cascade.prerender

v2.1.0

Published

Cascade prerendered: every address of a built app rendered in a real browser and written down as a page of its own - for search engines, link previews and AI - with head tags, structured data and a sitemap. A Vite plugin and a CLI.

Readme

cascade.prerender

Every page of a built Cascade app, prerendered: each address opened in a real browser (headless Chromium, through Playwright), left to settle, and written down as a page of its own. A search engine, a link preview or an AI fetching /shop/product/12 then gets the product page itself, not an empty shell waiting for JavaScript. When the app's code loads, it takes over in one frame.

Why a real browser: Cascade lays out from real measurements (an element's size, the width of text). Without a layout engine there is nothing accurate to write down.

npm install @liquefy/cascade.prerender
npx playwright install chromium

In the app: the root target

The app renders onto a PrerenderedElementTarget instead of a plain DOMElementTarget. It is the same target, except that its static forElement() first removes the snapshot a prerendered page came with:

import { PrerenderedElementTarget } from "@liquefy/cascade.prerender/client";

const target = PrerenderedElementTarget.forElement(document.getElementById("application"));
app.renderOnto(target, context);

Use it only for the root: everything rendered inside it gets plain DOMElementTargets, as always. A page that wasn't prerendered (while developing, say) is left as it is. @liquefy/cascade.prerender/client is all that goes into the app's bundle. The rest of the package (Node, Playwright) runs at build time only.

With Vite

Once vite build has built the app, its pages are prerendered into the same folder:

// vite.config.js
import { defineConfig } from "vite";
import { cascadePrerender } from "@liquefy/cascade.prerender";

export default defineConfig({
  plugins: [
    cascadePrerender({
      site: "https://shop.example",
      // The pages to start from. Every page they link to is found too.
      routes: async () => ["", ...(await fetchProductIds()).map((id) => `product/${id}`)],
      exclude: [/^checkout/, /^account/],
    }),
  ],
});

CASCADE_PRERENDER=false vite build builds without prerendering.

From the command line

Any built app, with any build tool:

npx cascade-prerender dist --base /shop/ --site https://shop.example --routes routes.txt

In the app: the head

What a search engine shows, and what a shared link previews, comes from the head. documentHead() (in cascade.dom) builds the head from the app's data and follows it as the user navigates. A prerendered page is written with that head too:

import { browserLocation, documentHead } from "@liquefy/cascade.dom";

const location = browserLocation({ base: import.meta.env.BASE_URL });

documentHead(() => {
  const product = catalogue.productAt(location.path);
  if (!product) return { title: "Shop" };
  return {
    title: `${product.name} - Shop`,
    description: product.summary,
    canonical: location.href(location.path),
    image: product.photo,
    type: "product",
    // schema.org data - prices and availability shown right in search results.
    structuredData: {
      "@context": "https://schema.org",
      "@type": "Product",
      name: product.name,
      image: product.photo,
      offers: { "@type": "Offer", price: product.price, priceCurrency: "EUR", availability: "https://schema.org/InStock" },
    },
  };
});

What a prerendered page is

The app's own index.html, with:

  • the snapshot: what the app rendered into its root element, which is marked data-prerendered
  • the title and the documentHead() tags
  • the style sheets the app added as it rendered, so the snapshot looks right without JavaScript

Its scripts are unchanged. When the app starts, PrerenderedElementTarget removes the snapshot and its style sheets, and the app renders in the same frame.

Any address in a page that points at the local copy rendered from is changed to point at site (canonical links, og:url), and a sitemap.xml is written. Debug ids (the id cascade gives every element it creates) are left out.

While prerendering, globalThis.cascadePrerendering is set. Use it to leave out anything that doesn't belong in a snapshot: analytics, a cookie banner, a notice meant for one visitor.

Options

| Option | | |---|---| | dist | The built app's folder (the plugin uses Vite's outDir) | | base | Where the app is served from (the plugin uses Vite's base) | | routes | Addresses to render, below the base: a list, or a function (async if it needs to be). Default [""] | | crawl | Also render every address within the app that a page links to (default true) | | exclude | Addresses not to render: a RegExp, a function of the route, or a list of either | | root | The id of the element the app renders into (default "application") | | site | Where the app will be served, e.g. "https://shop.example". Used to make addresses absolute and to write sitemap.xml | | files | "html" writes product/12.html, served at /product/12 by GitHub Pages, Netlify, Cloudflare Pages and others (default). "directory" writes product/12/index.html | | viewport | The browser's size (default { width: 1280, height: 800 }) | | quiet, timeout | A page counts as settled after quiet ms with no changes (default 300). After timeout ms (default 15000) it is captured as it is, with a warning | | concurrency | How many pages render at once (default 4) | | keepDebugIds | Keep cascade's debug ids (default false) | | transform(html, route) | A last change to each page before it is written | | launch | Options for Playwright's chromium.launch() |

Limits

  • Pages are rendered at one viewport size. On a phone, the snapshot shows the desktop layout until the app takes over, which happens as soon as its code loads.
  • Shadow DOM (web components such as mdui's) is not part of the snapshot: their light DOM children are, but not what they draw themselves.
  • The server must serve the app's index.html for addresses that have no page of their own, as single-page app hosting already does.