An open API service indexing awesome lists of open source software.

https://github.com/brighteyekid/rendermw

Zero-dependency dynamic rendering middleware for Express. No Puppeteer. No external services. No cost. Bots get semantic HTML. Users get your SPA.
https://github.com/brighteyekid/rendermw

angular bots crawler dynamic-rendering express expressjs indexing middleware nodejs open-graph prerender react seo spa typescript vue

Last synced: about 7 hours ago
JSON representation

Zero-dependency dynamic rendering middleware for Express. No Puppeteer. No external services. No cost. Bots get semantic HTML. Users get your SPA.

Awesome Lists containing this project

README

          


rendermw



npm version
tests
zero deps
MIT



### *Bots get semantic HTML. Users get your SPA. You get rankings.*

**No Puppeteer  ·  No paid services  ·  No architecture rewrites  ·  No overhead**


```bash
npm install rendermw
```


[Documentation](#the-problem)  ·  [Quick Start](#quick-start)  ·  [API Reference](#api-reference)  ·  [Code of Conduct](./CODE_OF_CONDUCT.md)  ·  [Security](./SECURITY.md)


---

## The problem

When Googlebot crawls your React, Vue, or Angular app, it sees this:

```html




```

Your products, articles, and pages are **completely invisible** to every search engine and social crawler.
Zero indexing. Zero rich results. Zero rankings.

**Every existing fix has a painful cost:**

| Solution | The catch |
|:---|:---|
| **Puppeteer / Rendertron** | Spawns a Chrome instance per request. Slow, expensive, crashes under load |
| **Prerender.io** | $99–$449/month. All bot traffic routes through a third-party service |
| **Migrate to Next.js / Nuxt** | Rewrite your entire frontend. Weeks of work. Massive risk |

**rendermw solves it in an afternoon.** Describe each route's data as a plain async function. rendermw builds the complete HTML document — title, description, canonical, Open Graph, Twitter Card, JSON-LD schema, BreadcrumbList — and serves it to bots. Real users pass through untouched.

---

## How it works

![rendermw request flow diagram](./diagram.png)

---

## Installation

```bash
npm install rendermw
```

> **Peer dependency:** Express ≥ 4.0.0
> ```bash
> npm install express
> ```

TypeScript types are included — no `@types/rendermw` needed.

---

## Quick start

```js
const express = require('express');
const rendermw = require('rendermw');

const app = express();

app.use(rendermw({
siteUrl: 'https://mystore.com',
routes: [
{
path: '/products/:slug',
render: async ({ slug }) => {
const product = await db.products.findBySlug(slug);

return {
title: `${product.name} — My Store`,
description: product.description,
canonical: `https://mystore.com/products/${slug}`,
ogImage: product.imageUrl,
ogType: 'product',
schema: {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
offers: {
'@type': 'Offer',
price: product.price,
priceCurrency: 'USD',
availability: 'https://schema.org/InStock',
},
},
breadcrumbs: [
{ name: 'Home', url: 'https://mystore.com' },
{ name: 'Products', url: 'https://mystore.com/products' },
{ name: product.name, url: `https://mystore.com/products/${slug}` },
],
html: `

${product.name}


${product.description}


$${product.price}



`,
};
},
},
],
}));

// SPA fallback — real users always land here
app.get('*', (_req, res) => res.sendFile('index.html', { root: './dist' }));

app.listen(3000);
```

---

## TypeScript

```ts
import express from 'express';
import rendermw from 'rendermw';
import type { RenderPayload, RenderOptions } from 'rendermw';

const app = express();

const options: RenderOptions = {
siteUrl: 'https://mystore.com',
routes: [
{
path: '/products/:slug',
render: async ({ slug }): Promise => ({
title: `${slug} — My Store`,
description: 'Premium products.',
canonical: `https://mystore.com/products/${slug}`,
html: `

${slug}

`,
}),
},
],
};

app.use(rendermw(options));
app.get('*', (_req, res) => res.sendFile('index.html', { root: './dist' }));
app.listen(3000);
```

---

## API Reference

### `rendermw(options)` → `RequestHandler`

#### `RenderOptions`

| Option | Type | Default | Description |
|:---|:---|:---:|:---|
| `siteUrl` | `string` | **required** | Base URL e.g. `"https://example.com"`. Resolves relative OG image paths. |
| `routes` | `RenderRoute[]` | **required** | Routes rendermw intercepts for bots. |
| `cache` | `boolean` | `true` | Enable in-memory HTML cache. |
| `cacheTTL` | `number` | `86400` | Cache TTL in seconds. Default: 24 hours. |
| `bots` | `string[]` | `[]` | Extra bot UA substrings beyond the built-in list. |
| `debug` | `boolean` | `false` | Log every bot hit to console. |

---

#### `RenderRoute`

| Field | Type | Description |
|:---|:---|:---|
| `path` | `string` | Express-style pattern e.g. `"/products/:slug"` |
| `render` | `(params, query) => Promise` | Called when a bot matches this route. |

`render()` receives:
- `params` — `Record` — URL path params e.g. `{ slug: "nike-air-max" }`
- `query` — `Record` — query string e.g. `{ page: "2", sort: "price" }`

---

#### `RenderPayload`

| Field | Type | Req | Description |
|:---|:---|:---:|:---|
| `title` | `string` | ✅ | `` · `og:title` · `twitter:title` |
| `description` | `string` | ✅ | `` · OG · Twitter |
| `canonical` | `string` | ✅ | `` · `og:url` |
| `html` | `string` | ✅ | Semantic body HTML served to bots |
| `ogImage` | `string` | — | OG / Twitter image. Relative paths resolved against `siteUrl` |
| `ogType` | `string` | — | Defaults to `"website"`. Use `"article"` or `"product"` |
| `schema` | `object \| object[]` | — | Raw JSON-LD. Arrays emit one `` tag per item |
| `breadcrumbs` | `Breadcrumb[]` | — | Auto-converted to `BreadcrumbList` JSON-LD |
| `lang` | `string` | — | `<html lang="">`. Defaults to `"en"` |

---

### HTML output

Every bot response is a complete, valid HTML document:

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nike Air Max 270 — My Store</title>
<meta name="description" content="Experience the biggest Air unit yet.">
<link rel="canonical" href="https://mystore.com/products/nike-air-max">
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">

<!-- Open Graph -->
<meta property="og:title" content="Nike Air Max 270 — My Store">
<meta property="og:description" content="Experience the biggest Air unit yet.">
<meta property="og:url" content="https://mystore.com/products/nike-air-max">
<meta property="og:type" content="product">
<meta property="og:image" content="https://mystore.com/images/nike-air-max.jpg">

<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Nike Air Max 270 — My Store">
<meta name="twitter:description" content="Experience the biggest Air unit yet.">
<meta name="twitter:image" content="https://mystore.com/images/nike-air-max.jpg">

<!-- JSON-LD: Product -->
<script type="application/ld+json">
{ "@context": "https://schema.org", "@type": "Product", "name": "Nike Air Max 270", ... }



{ "@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [ ... ] }


Nike Air Max 270


Experience the biggest Air unit yet.