vite-spa-seo
v2.1.0
Published
Ultra-lightweight Build-Time Prerendering & Runtime SEO Meta Injector for Vite SPAs (React, Vue, Svelte). Zero-config Puppeteer snapshots + zero-overhead Express middleware.
Maintainers
Readme
vite-spa-seo
High-performance, hybrid SEO and social metadata injection engine for Single Page Applications (Vite, React, Vue, Svelte) on Node.js / Express.
Delivers complete server-side metadata, OpenGraph cards, Twitter cards, Google Shopping Schema.org JSON-LD, and pre-rendered semantic HTML in ~2 milliseconds — eliminating runtime Chromium dependencies and server memory overhead without requiring a full rewrite to Next.js or Nuxt.
Overview
Single Page Applications (SPAs) built with modern bundlers like Vite output a minimal index.html file containing an empty root mounting element:
<div id="root"></div> <!-- or <div id="app"></div> -->While modern browsers execute JavaScript to render the full user interface, major web crawlers (Googlebot, Bingbot), search engine indexers, and social media scrapers (WhatsApp, Facebook, Twitter, LinkedIn, Slack) frequently evaluate only the initial server-delivered HTML payload. As a result, SPAs often suffer from:
- Incomplete Search Indexation: Missing unique page titles, meta descriptions, and product schema.
- Blank Social Share Previews: Inability to display rich thumbnail cards on chat and social platforms.
- Server Resource Exhaustion: Traditional headless browser renderers (Puppeteer, Rendertron) consume 500 MB–1 GB+ of RAM per instance, causing out-of-memory (OOM) crashes on standard cloud VPS instances.
vite-spa-seo resolves this through a two-tier hybrid architecture:
- Build-Time Prerendering (CLI): Generates static, fully rendered HTML snapshots of primary routes during the build process on your local or CI machine.
- Zero-Overhead Runtime Middleware (Express): Automatically serves pre-rendered snapshots when available, and dynamically injects metadata and structured JSON-LD for thousands of database-driven dynamic routes in under 2ms.
Key Features
- Zero Runtime Dependencies: Core middleware is written in pure Node.js with no external dependencies.
- Zero RAM Footprint on Production: No Chromium or browser instances running on your live web server.
- Automatic Framework & Container Detection: Seamlessly supports React (
#root), Vue (#app), Svelte, and custom root containers. - Smart Directory Resolution: Automatically discovers
dist,build, or custom frontend paths without mandatory manual configuration. - Rich Snippet Ready: Built-in helpers for Google Shopping, BreadcrumbList, Articles, and Schema.org JSON-LD.
- Sub-2ms In-Memory Caching: High-throughput memory cache with configurable TTL and instantaneous programmatic invalidation.
- Seamless Client Hydration: React and Vue mount smoothly over injected content without hydration mismatch warnings.
Installation
# In your backend project (Express / Node.js)
npm install vite-spa-seo
# Optional: In your frontend project for build-time static snapshots
npm install -D vite-spa-seo puppeteerQuick Start
Basic Implementation in Express (server.js)
Mount the middleware in your Express application before your static file handlers:
const express = require('express');
const { createSpaSeoMiddleware } = require('vite-spa-seo');
const app = express();
app.use(
createSpaSeoMiddleware({
siteUrl: 'https://example.com',
siteName: 'Example Store',
// Static Routes
staticPages: {
'/': {
title: 'Home | Example Store',
description: 'Explore our catalog of premium curated products with fast shipping.',
},
'/about-us': {
title: 'About Us | Example Store',
description: 'Learn more about our mission, journey, and our global team.',
},
},
// Dynamic Parameterized Routes
routes: [
{
path: '/product/:sku',
handler: async ({ params }) => {
const product = await database.findProductBySku(params.sku);
if (!product) return null; // Fallback to standard client-side 404 handling
return {
title: `${product.name} | Example Store`,
description: product.description,
image: product.imageUrl,
schema: {
'@type': 'Product',
name: product.name,
sku: product.sku,
offers: {
price: product.price,
priceCurrency: 'USD',
availability: product.inStock
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock',
},
},
};
},
},
],
})
);
app.listen(3000, () => {
console.log('Application running on port 3000');
});Build-Time Prerendering (Optional)
To produce static HTML snapshots containing the complete, rendered DOM for static pages and high-priority dynamic routes, execute the prerender CLI during build time.
1. Configure Build Script in package.json
{
"scripts": {
"build": "vite build && vite-spa-seo prerender --routes /,/about-us,/contact,/pricing"
}
}2. Configuration File (vite-spa-seo.config.js)
For larger projects, create a configuration file in the project root:
module.exports = {
distDir: './dist',
routes: [
'/',
'/about-us',
'/contact',
'/privacy-policy',
'/terms-conditions',
'/product/featured-item-1',
'/product/featured-item-2',
],
readySelector: '#root > *, #app > *', // Awaits framework mounting
concurrency: 2,
};When deployed to production, the Express middleware automatically detects any pre-rendered files and serves them directly with zero injection latency.
Configuration Reference
createSpaSeoMiddleware(options)
| Option | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| distDir | string | (Auto-detected) | Path to the directory containing index.html. Searches ./dist, ../dist, ./build, ../frontend/dist. |
| indexHtmlPath | string | distDir/index.html | Explicit path to the HTML template file. |
| siteUrl | string | '' | Base canonical domain (e.g., https://example.com). |
| siteName | string | '' | Brand name populated in og:site_name. |
| rootSelector | string | <div id="root"></div> | Target container for semantic fallback content. Auto-matches #root or #app. |
| cacheTTL | number | 3600000 (1h) | Duration in milliseconds to retain rendered HTML strings in memory. Set 0 to disable. |
| routes | Array<DynamicRoute> | [] | Array of dynamic route definitions and asynchronous handlers. |
| staticPages | StaticPagesMap | {} | Key-value mapping of exact paths to static metadata objects or provider functions. |
| ignorePaths | Array<string \| RegExp> | ['/api', '/admin'] | Request prefixes or patterns excluded from SEO handling. |
| serveAssets | boolean | true | Automatically serves files from dist/assets with immutable caching headers. |
Route Handler Return Object (SeoData)
interface SeoData {
title?: string;
description?: string;
canonical?: string;
image?: string;
type?: string; // e.g., 'website', 'product', 'article'
twitterCard?: 'summary' | 'summary_large_image';
openGraph?: Record<string, string | number | boolean>;
schema?: Record<string, any> | Array<Record<string, any>>;
breadcrumbs?: Array<{ name: string; url: string }>;
bodyContent?: string; // Custom semantic HTML injected inside the root element
}Examples
Dynamic E-Commerce Product Route
{
path: '/product/:sku',
cacheTTL: 30 * 60 * 1000, // 30-minute cache per product
handler: async ({ params }) => {
const item = await catalog.getBySku(params.sku);
if (!item) return null;
return {
title: `${item.title} | Store`,
description: item.summary,
canonical: `/product/${item.sku}`,
image: item.thumbnailUrl,
schema: {
'@context': 'https://schema.org',
'@type': 'Product',
name: item.title,
sku: item.sku,
image: [item.thumbnailUrl],
offers: {
'@type': 'Offer',
price: item.price,
priceCurrency: 'USD',
availability: item.quantity > 0
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock',
},
},
breadcrumbs: [
{ name: 'Home', url: '/' },
{ name: item.categoryName, url: `/category/${item.categorySlug}` },
{ name: item.title, url: `/product/${item.sku}` },
],
};
},
}Blog / Editorial Article
{
path: '/articles/:slug',
handler: async ({ params }) => {
const post = await posts.findBySlug(params.slug);
if (!post) return null;
return {
title: `${post.headline} | News`,
description: post.excerpt,
image: post.coverImage,
type: 'article',
openGraph: {
'article:published_time': post.publishedDate,
'article:author': post.author,
},
};
},
}Cache Management
The middleware exposes programmatic cache invalidation through the .purgeCache() method:
const seoMiddleware = createSpaSeoMiddleware({ ... });
app.use(seoMiddleware);
// Invalidate specific route when a record is updated
app.post('/api/webhook/product-updated', (req, res) => {
const { sku } = req.body;
seoMiddleware.purgeCache(`/product/${sku}`);
res.json({ success: true, purged: `/product/${sku}` });
});
// Purge all stored HTML representations
app.post('/api/admin/clear-cache', (req, res) => {
seoMiddleware.purgeCache();
res.json({ success: true, message: 'All SEO caches cleared' });
});Performance & Benchmarks
| Metric | Traditional SSR / Puppeteer | vite-spa-seo | Improvement |
| :--- | :--- | :--- | :--- |
| Server RAM Usage | 500 MB – 1.2 GB | < 15 MB | 98% reduction |
| Response Latency | 800ms – 2,500ms | ~2ms | 99% faster |
| Server Crash Risk (OOM) | High on low-tier VPS | Zero | Enterprise-grade stability |
| Setup Time | Days (Full refactoring) | < 5 minutes | Instant integration |
Testing
Execute the comprehensive unit and integration test suite:
npm testLicense
Distributed under the MIT License. See LICENSE for details.
Developed and maintained by Devendra.
