<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: DedSec</title>
    <description>The latest articles on DEV Community by DedSec (@brighteyekid).</description>
    <link>https://dev.to/brighteyekid</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3956022%2F921ee256-0ada-44bc-ba7e-bc23062dad92.jpeg</url>
      <title>DEV Community: DedSec</title>
      <link>https://dev.to/brighteyekid</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/brighteyekid"/>
    <language>en</language>
    <item>
      <title>rendermw — Zero-Dependency Dynamic Rendering Middleware for Express SPAs</title>
      <dc:creator>DedSec</dc:creator>
      <pubDate>Fri, 29 May 2026 05:30:00 +0000</pubDate>
      <link>https://dev.to/brighteyekid/rendermw-zero-dependency-dynamic-rendering-middleware-for-express-spas-5bh5</link>
      <guid>https://dev.to/brighteyekid/rendermw-zero-dependency-dynamic-rendering-middleware-for-express-spas-5bh5</guid>
      <description>&lt;h2&gt;
  
  
  rendermw — Zero-Dependency Dynamic Rendering Middleware for Express SPAs
&lt;/h2&gt;

&lt;p&gt;Single-page applications solved frontend UX years ago.&lt;/p&gt;

&lt;p&gt;SEO is still a mess.&lt;/p&gt;

&lt;p&gt;Most React/Vue/Angular SPAs ship an almost empty HTML document to crawlers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"root"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Humans eventually see content after hydration.&lt;/p&gt;

&lt;p&gt;Bots often don't.&lt;/p&gt;

&lt;p&gt;That creates problems with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;search indexing,&lt;/li&gt;
&lt;li&gt;Open Graph previews,&lt;/li&gt;
&lt;li&gt;structured data,&lt;/li&gt;
&lt;li&gt;link unfurling,&lt;/li&gt;
&lt;li&gt;and crawl reliability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most existing solutions are operationally expensive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;headless Chrome clusters,&lt;/li&gt;
&lt;li&gt;Puppeteer rendering,&lt;/li&gt;
&lt;li&gt;external prerender APIs,&lt;/li&gt;
&lt;li&gt;or full SSR rewrites.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted something much smaller and much more deterministic.&lt;/p&gt;

&lt;p&gt;So I built &lt;code&gt;rendermw&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is rendermw?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;rendermw&lt;/code&gt; is a zero-dependency Express middleware that dynamically serves semantic HTML to bots while real users continue receiving the normal SPA.&lt;/p&gt;

&lt;p&gt;No Puppeteer.&lt;br&gt;
No Chromium.&lt;br&gt;
No external rendering services.&lt;br&gt;
No framework lock-in.&lt;/p&gt;

&lt;p&gt;Just route-driven semantic HTML generated from your existing backend data.&lt;/p&gt;


&lt;h2&gt;
  
  
  The core idea
&lt;/h2&gt;

&lt;p&gt;Most SPAs already know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what data belongs on the page,&lt;/li&gt;
&lt;li&gt;what metadata should exist,&lt;/li&gt;
&lt;li&gt;what schema should be emitted,&lt;/li&gt;
&lt;li&gt;and what the semantic HTML structure should look like.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You already have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;database queries,&lt;/li&gt;
&lt;li&gt;APIs,&lt;/li&gt;
&lt;li&gt;route params,&lt;/li&gt;
&lt;li&gt;product/article metadata,&lt;/li&gt;
&lt;li&gt;pricing,&lt;/li&gt;
&lt;li&gt;breadcrumbs,&lt;/li&gt;
&lt;li&gt;and canonical URLs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So instead of trying to server-render the entire React application, &lt;code&gt;rendermw&lt;/code&gt; focuses on only what bots actually need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;semantic HTML,&lt;/li&gt;
&lt;li&gt;metadata,&lt;/li&gt;
&lt;li&gt;JSON-LD,&lt;/li&gt;
&lt;li&gt;Open Graph tags,&lt;/li&gt;
&lt;li&gt;Twitter cards,&lt;/li&gt;
&lt;li&gt;crawlable content.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it.&lt;/p&gt;


&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Incoming Request
      │
      ▼
 ┌────────────────────────────────────────────┐
 │               rendermw                     │
 │                                            │
 │  Detect bot user-agent                     │
 │       │                                    │
 │       ├── Real User ───────► next()        │
 │       │                                    │
 │       └── Bot                              │
 │              │                             │
 │              ▼                             │
 │       Match route pattern                  │
 │              │                             │
 │              ▼                             │
 │       Execute render()                     │
 │              │                             │
 │              ▼                             │
 │       Build HTML shell                     │
 │              │                             │
 │              ▼                             │
 │       Return semantic HTML                 │
 └────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Real users bypass everything immediately.&lt;/p&gt;

&lt;p&gt;The first operation is bot detection.&lt;/p&gt;

&lt;p&gt;If the request is not from a crawler:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;zero rendering work,&lt;/li&gt;
&lt;li&gt;zero route matching,&lt;/li&gt;
&lt;li&gt;zero HTML generation,&lt;/li&gt;
&lt;li&gt;zero cache access.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just &lt;code&gt;next()&lt;/code&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why not Puppeteer?
&lt;/h2&gt;

&lt;p&gt;Puppeteer solves rendering by launching Chrome.&lt;/p&gt;

&lt;p&gt;That creates multiple problems at scale:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chrome instances&lt;/td&gt;
&lt;td&gt;High memory usage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold starts&lt;/td&gt;
&lt;td&gt;Slow response times&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rendering overhead&lt;/td&gt;
&lt;td&gt;CPU spikes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrastructure complexity&lt;/td&gt;
&lt;td&gt;Hard deployments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network waterfalls&lt;/td&gt;
&lt;td&gt;Slower crawls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Headless instability&lt;/td&gt;
&lt;td&gt;Random failures&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most SEO crawlers don't actually need a fully hydrated React tree.&lt;/p&gt;

&lt;p&gt;They need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;metadata,&lt;/li&gt;
&lt;li&gt;content,&lt;/li&gt;
&lt;li&gt;structure,&lt;/li&gt;
&lt;li&gt;and schema.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So &lt;code&gt;rendermw&lt;/code&gt; skips browser rendering entirely.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why not SSR?
&lt;/h2&gt;

&lt;p&gt;SSR frameworks are good if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you're starting greenfield,&lt;/li&gt;
&lt;li&gt;or already deeply integrated into SSR architecture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But many teams already have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;mature SPAs,&lt;/li&gt;
&lt;li&gt;large frontend codebases,&lt;/li&gt;
&lt;li&gt;custom Vite/Webpack builds,&lt;/li&gt;
&lt;li&gt;or legacy React architectures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Migrating a production SPA to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js,&lt;/li&gt;
&lt;li&gt;Nuxt,&lt;/li&gt;
&lt;li&gt;Remix,&lt;/li&gt;
&lt;li&gt;Astro,&lt;/li&gt;
&lt;li&gt;or full SSR&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…can become a multi-month infrastructure rewrite.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rendermw&lt;/code&gt; works without touching the frontend architecture.&lt;/p&gt;

&lt;p&gt;Your SPA remains unchanged.&lt;/p&gt;


&lt;h2&gt;
  
  
  Example
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rendermw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rendermw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rendermw&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;siteUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://mystore.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/products/:slug&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

      &lt;span class="na"&gt;render&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findBySlug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; — My Store`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://mystore.com/products/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

          &lt;span class="na"&gt;ogImage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

          &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@context&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://schema.org&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Product&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;

          &lt;span class="na"&gt;breadcrumbs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Home&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://mystore.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://mystore.com/products/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;],&lt;/span&gt;

          &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
            &amp;lt;main&amp;gt;
              &amp;lt;h1&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/h1&amp;gt;
              &amp;lt;p&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/p&amp;gt;
            &amp;lt;/main&amp;gt;
          `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Generated HTML
&lt;/h2&gt;

&lt;p&gt;Bots receive a fully structured document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Nike Air Max&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"description"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"canonical"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:title"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"Nike Air Max"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:description"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/ld+json"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Product&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;main&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Nike Air Max&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Built-in features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Bot detection
&lt;/h3&gt;

&lt;p&gt;Includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Googlebot&lt;/li&gt;
&lt;li&gt;Bingbot&lt;/li&gt;
&lt;li&gt;Twitterbot&lt;/li&gt;
&lt;li&gt;LinkedInBot&lt;/li&gt;
&lt;li&gt;Discordbot&lt;/li&gt;
&lt;li&gt;TelegramBot&lt;/li&gt;
&lt;li&gt;Slackbot&lt;/li&gt;
&lt;li&gt;Facebook crawlers&lt;/li&gt;
&lt;li&gt;Ahrefsbot&lt;/li&gt;
&lt;li&gt;Semrushbot&lt;/li&gt;
&lt;li&gt;Lighthouse&lt;/li&gt;
&lt;li&gt;and more&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Custom bots can also be added.&lt;/p&gt;




&lt;h3&gt;
  
  
  Route matching
&lt;/h3&gt;

&lt;p&gt;Supports Express-style params:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/products/:slug
/blog/:slug
/shop/:category/:id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Params are extracted automatically.&lt;/p&gt;




&lt;h3&gt;
  
  
  JSON-LD support
&lt;/h3&gt;

&lt;p&gt;Supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product schema&lt;/li&gt;
&lt;li&gt;Article schema&lt;/li&gt;
&lt;li&gt;Organization schema&lt;/li&gt;
&lt;li&gt;FAQ schema&lt;/li&gt;
&lt;li&gt;BreadcrumbList&lt;/li&gt;
&lt;li&gt;arbitrary custom schema&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;rendermw&lt;/code&gt; does not enforce schema structure.&lt;/p&gt;

&lt;p&gt;You provide raw JSON-LD directly.&lt;/p&gt;




&lt;h3&gt;
  
  
  Open Graph + Twitter cards
&lt;/h3&gt;

&lt;p&gt;Automatically emits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;og:title&lt;/li&gt;
&lt;li&gt;og:description&lt;/li&gt;
&lt;li&gt;og:image&lt;/li&gt;
&lt;li&gt;twitter:card&lt;/li&gt;
&lt;li&gt;twitter:image&lt;/li&gt;
&lt;li&gt;canonical URLs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Relative image paths are converted into absolute URLs automatically.&lt;/p&gt;




&lt;h3&gt;
  
  
  Built-in cache
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;rendermw&lt;/code&gt; ships with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;in-memory TTL caching,&lt;/li&gt;
&lt;li&gt;lazy expiration,&lt;/li&gt;
&lt;li&gt;zero dependencies.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;rendermw&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;cacheTTL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Headers expose cache status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;X-Render-MW: fresh
X-Render-MW: cache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Internal design decisions
&lt;/h2&gt;

&lt;p&gt;A few implementation constraints shaped the package heavily.&lt;/p&gt;




&lt;h3&gt;
  
  
  Zero runtime dependencies
&lt;/h3&gt;

&lt;p&gt;No runtime dependencies besides Express as a peer dependency.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no Redis requirement,&lt;/li&gt;
&lt;li&gt;no headless browser,&lt;/li&gt;
&lt;li&gt;no external services,&lt;/li&gt;
&lt;li&gt;no filesystem cache,&lt;/li&gt;
&lt;li&gt;no heavyweight abstractions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The middleware is intentionally small.&lt;/p&gt;




&lt;h3&gt;
  
  
  Zero overhead for real users
&lt;/h3&gt;

&lt;p&gt;The first operation is always:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isBot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No route parsing.&lt;br&gt;
No rendering.&lt;br&gt;
No cache work.&lt;/p&gt;

&lt;p&gt;Real traffic remains untouched.&lt;/p&gt;




&lt;h3&gt;
  
  
  Data-first rendering
&lt;/h3&gt;

&lt;p&gt;The middleware does not attempt to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;interpret React,&lt;/li&gt;
&lt;li&gt;parse component trees,&lt;/li&gt;
&lt;li&gt;execute frontend bundles,&lt;/li&gt;
&lt;li&gt;or emulate a browser.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It simply asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What should bots see for this route?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then returns that HTML.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;The package currently includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unit tests,&lt;/li&gt;
&lt;li&gt;integration tests,&lt;/li&gt;
&lt;li&gt;cache tests,&lt;/li&gt;
&lt;li&gt;route matching tests,&lt;/li&gt;
&lt;li&gt;schema tests,&lt;/li&gt;
&lt;li&gt;middleware tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Jest&lt;/li&gt;
&lt;li&gt;Supertest&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Current test count:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;100+ passing tests&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Real-world applicability
&lt;/h2&gt;

&lt;p&gt;This architecture works particularly well for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;e-commerce platforms,&lt;/li&gt;
&lt;li&gt;marketplaces,&lt;/li&gt;
&lt;li&gt;CMS-backed SPAs,&lt;/li&gt;
&lt;li&gt;content-heavy frontend apps,&lt;/li&gt;
&lt;li&gt;React dashboards with public pages,&lt;/li&gt;
&lt;li&gt;Vite applications,&lt;/li&gt;
&lt;li&gt;legacy CRA apps,&lt;/li&gt;
&lt;li&gt;Express APIs serving frontend bundles.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Especially when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SEO matters,&lt;/li&gt;
&lt;li&gt;but SSR migration cost is too high.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What this is NOT
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;rendermw&lt;/code&gt; is not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a React renderer,&lt;/li&gt;
&lt;li&gt;an SSR framework,&lt;/li&gt;
&lt;li&gt;a hydration system,&lt;/li&gt;
&lt;li&gt;a frontend runtime.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dynamic rendering middleware,&lt;/li&gt;
&lt;li&gt;for bots,&lt;/li&gt;
&lt;li&gt;using semantic HTML,&lt;/li&gt;
&lt;li&gt;generated from backend data.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Example use cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  E-commerce
&lt;/h3&gt;

&lt;p&gt;Generate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product schema&lt;/li&gt;
&lt;li&gt;Offer schema&lt;/li&gt;
&lt;li&gt;breadcrumbs&lt;/li&gt;
&lt;li&gt;pricing metadata&lt;/li&gt;
&lt;li&gt;semantic product pages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;without rendering the entire storefront server-side.&lt;/p&gt;




&lt;h3&gt;
  
  
  Blogs
&lt;/h3&gt;

&lt;p&gt;Generate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Article schema&lt;/li&gt;
&lt;li&gt;Open Graph metadata&lt;/li&gt;
&lt;li&gt;author metadata&lt;/li&gt;
&lt;li&gt;canonical URLs&lt;/li&gt;
&lt;li&gt;semantic article content&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;for crawlers and social previews.&lt;/p&gt;




&lt;h3&gt;
  
  
  SaaS marketing pages
&lt;/h3&gt;

&lt;p&gt;Generate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;landing page metadata,&lt;/li&gt;
&lt;li&gt;feature descriptions,&lt;/li&gt;
&lt;li&gt;FAQ schema,&lt;/li&gt;
&lt;li&gt;semantic content blocks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;without introducing SSR complexity into the app itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I open sourced it
&lt;/h2&gt;

&lt;p&gt;Most SEO tooling around SPAs still assumes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSR everywhere,&lt;/li&gt;
&lt;li&gt;or browser rendering everywhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a large middle ground where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;backend data already exists,&lt;/li&gt;
&lt;li&gt;semantic output is deterministic,&lt;/li&gt;
&lt;li&gt;and full browser rendering is unnecessary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;rendermw&lt;/code&gt; is aimed at that layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Current status
&lt;/h2&gt;

&lt;p&gt;Current package status:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript support&lt;/li&gt;
&lt;li&gt;npm package&lt;/li&gt;
&lt;li&gt;strict-mode TS&lt;/li&gt;
&lt;li&gt;tested middleware&lt;/li&gt;
&lt;li&gt;MIT licensed&lt;/li&gt;
&lt;li&gt;open source&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GitHub:&lt;br&gt;
&lt;a href="https://github.com/brighteyekid/rendermw" rel="noopener noreferrer"&gt;https://github.com/brighteyekid/rendermw&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;npm:&lt;br&gt;
&lt;a href="https://www.npmjs.com/package/rendermw" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/rendermw&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;Modern SPAs solved frontend interactivity.&lt;/p&gt;

&lt;p&gt;SEO infrastructure around SPAs is still disproportionately heavy.&lt;/p&gt;

&lt;p&gt;In many cases you don't actually need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSR,&lt;/li&gt;
&lt;li&gt;hydration on the server,&lt;/li&gt;
&lt;li&gt;or browser rendering.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You just need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;crawlable HTML,&lt;/li&gt;
&lt;li&gt;metadata,&lt;/li&gt;
&lt;li&gt;schema,&lt;/li&gt;
&lt;li&gt;and deterministic semantic output.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the entire idea behind &lt;code&gt;rendermw&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>node</category>
      <category>express</category>
      <category>seo</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
