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

mates-fullstack-beta

v1.0.0

Published

Full-stack Node.js framework built on the Mates SPA framework. RPC server functions, SSR, WebSockets, file-system routing.

Downloads

143

Readme

Mates SSR

Server-side rendering for Mates. If you know Mates, you know Mates SSR — learned in 5 minutes.


What it is

Mates SSR is a thin shell around Mates. It adds three things:

  1. File-system router — your folder structure is your router
  2. SSR renderer — runs your components on the server, sends real HTML
  3. Vite dev server — TypeScript, HMR, fast DX out of the box

Everything else — components, atom, useState, html, directives, lifecycle hooks — is plain Mates, unchanged.


Installation

bun add mates-ssr mates lit-html
bun add -d vite typescript @types/node

Project structure

my-app/
├── src/
│   ├── pages/              ← file-system router lives here
│   │   ├── index.ts        →  /
│   │   ├── about.ts        →  /about
│   │   ├── blog/
│   │   │   ├── index.ts    →  /blog
│   │   │   └── [slug].ts   →  /blog/:slug
│   │   └── counter.ts      →  /counter
│   ├── components/         ← shared components (plain Mates)
│   ├── App.ts              ← client-side SPA root (for hydration)
│   └── client.ts           ← browser entry point (one line)
├── public/                 ← static assets served as-is
├── mates.config.ts         ← config: port, title, head tags, etc.
├── vite.config.ts
└── package.json

Quick start

1. mates.config.ts

import { defineConfig } from 'mates-ssr';

export default defineConfig({
  port: 3000,
  pagesDir: './src/pages',
  publicDir: './public',
  clientEntry: './src/client.ts',
  head: {
    title: 'My App',
    meta: [
      { name: 'description', content: 'My Mates SSR app' },
    ],
    links: [
      { rel: 'stylesheet', href: '/style.css' },
    ],
  },
});

2. A page file

A page is a standard Mates component — nothing new.

// src/pages/index.ts  →  route: /
import { html, useState } from 'mates';
import type { Props } from 'mates';
import { Layout } from '../components/Layout';

// Optional: per-page <head> tags
export const meta = {
  title: 'Home — My App',
  description: 'Welcome to my app.',
};

// Optional: layout wrapper for this page
export const layout = Layout;

// Required: the page component (default export)
export default (_: Props<{}>) => {
  const [state, update] = useState({ count: 0 });

  return () => html`
    <h1>Hello from SSR</h1>
    <p>Count: ${state.count}</p>
    <button @click=${() => update(() => state.count++)}>+</button>
  `;
};

The server renders this to HTML on first load. After hydration it becomes a fully reactive Mates component — no code changes needed.

3. src/client.ts

import { renderX } from 'mates';
import { App } from './App';

renderX(App, document.getElementById('app')!);

4. src/App.ts (client-side router)

import { html, route, x, location } from 'mates';
import type { Props } from 'mates';
import IndexPage   from './pages/index';
import AboutPage   from './pages/about';
import { Layout }  from './components/Layout';

export const App = (_: Props<{}>) => () => html`
  ${x(Layout, {
    children:
      route('/',      { view: IndexPage }) ??
      route('/about', { view: AboutPage }) ??
      html`<div>404</div>`,
  })}
`;

5. Run

bun run dev     # http://localhost:3000
bun run build   # production build
bun run start   # production server

Routing

Static routes

| File | Route | |-----------------------------|-------------| | src/pages/index.ts | / | | src/pages/about.ts | /about | | src/pages/blog/index.ts | /blog |

Dynamic routes

src/pages/blog/[slug].ts    →  /blog/:slug
src/pages/user/[id].ts      →  /user/:id
src/pages/[...all].ts       →  /* (catch-all)

Dynamic params are passed to your component via props():

// src/pages/blog/[slug].ts
import { html } from 'mates';
import type { Props } from 'mates';

export default (props: Props<{ slug: string }>) => {
  // Read params inside the inner function — always fresh on re-render
  return () => html`
    <h1>Post: ${props().slug}</h1>
  `;
};

Route priority

More specific routes always win:

  1. Static routes beat dynamic routes of the same depth (/about beats /:id)
  2. Deeper routes beat shallower routes
  3. Catch-all routes ([...all]) are always last

Page file exports

Every page file can have up to three exports:

// Required — the page component
export default MyPageComponent;

// Optional — layout that wraps this page
export const layout = MyLayoutComponent;

// Optional — <head> metadata for this page
export const meta = {
  title: 'Page Title',
  description: 'Page description for SEO.',
};

That's the entire API surface of a page file.


Layouts

A layout is a standard Mates component that receives the page content as children.

// src/components/Layout.ts
import { html, x } from 'mates';
import type { Props } from 'mates';
import { Nav } from './Nav';

export const Layout = (props: Props<{ children?: any }>) => {
  return () => html`
    ${x(Nav, {})}
    <main class="main">
      ${props().children ?? ''}
    </main>
  `;
};

Export it from a page file to wrap that page:

// src/pages/about.ts
import { Layout } from '../components/Layout';

export const layout = Layout;

export default () => () => html`<h1>About</h1>`;

How it works on the server: Mates SSR renders the page component first, then passes the resulting HTML string as the children prop to the layout component and renders that too. The final output is the full wrapped HTML.

How it works on the client: Your App.ts wraps each route in the layout directly using x(Layout, { children: ... }), so the shell persists across client-side navigation without re-mounting.


Configuration reference

import { defineConfig } from 'mates-ssr';

export default defineConfig({
  // HTTP port the dev / prod server listens on
  // default: 3000
  port: 3000,

  // Path to the pages directory, relative to project root
  // default: './src/pages'
  pagesDir: './src/pages',

  // Path to the static assets directory
  // default: './public'
  publicDir: './public',

  // Client entry file that calls renderX() for hydration
  // default: './src/client.ts'
  clientEntry: './src/client.ts',

  // CSS selector for the app container element
  // default: '#app'
  appSelector: '#app',

  // Default <head> content for every page
  head: {
    title: 'My App',
    meta: [
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { name: 'description', content: 'My app description' },
      { property: 'og:title', content: 'My App' },
    ],
    links: [
      { rel: 'stylesheet', href: '/style.css' },
      { rel: 'icon', href: '/favicon.ico' },
    ],
    scripts: [
      { src: '/analytics.js', defer: true },
    ],
  },
});

How SSR + hydration works

Browser requests /blog/hello-world
        │
        ▼
  mates-ssr server
        │
        ├── matches route:  /blog/[slug].ts  →  params: { slug: "hello-world" }
        │
        ├── imports the page module
        │
        ├── calls component(propsFn)  →  templateFn        (outer fn — setup)
        │
        ├── calls templateFn()        →  TemplateResult    (inner fn — template)
        │
        ├── render(result, container) in happy-dom
        │
        ├── strips lit-html comment markers
        │
        ├── wraps in layout (if page exports one)
        │
        └── builds full HTML shell  →  sends to browser
                │
                ▼
        Browser paints the page immediately (no JS needed)
                │
                ▼
        client.ts loads, calls renderX(App, container)
                │
                ▼
        lit-html patches the existing DOM in-place
        (attaches event listeners, activates atoms, useState, etc.)
                │
                ▼
        Fully interactive Mates SPA

The component setup runs twice — once on the server (for HTML), once on the client (for reactivity). Because both runs start with the same initial state, the DOM output is identical, and lit-html has nothing to replace — it just adds event bindings.


Rules for components

Components work exactly as in a plain Mates SPA, with one constraint:

Components must be synchronous. The outer function (setup) must return a template function immediately.

// ✅ Correct — synchronous setup
export default (props: Props<{ name: string }>) => {
  const [state, update] = useState({ count: 0 });
  return () => html`<p>Hello, ${props().name}</p>`;
};

// ❌ Wrong — async outer function
export default async (props) => {
  const data = await fetch('/api/data');   // not allowed
  return () => html`...`;
};

If you need data from an API or database, load it before the render starts and pass it to the component as route props. A data-loading middleware or loader pattern (per-route) is planned for v2.


CLI reference

# Start the dev server
# — Vite handles TypeScript, HMR for client bundle
# — File watcher rescans pages/ when files are added or removed
mates-ssr dev

# Build for production
# — Vite bundles src/client.ts → dist/assets/client.js
# — Copies public/ → dist/public/
mates-ssr build

# Start the production server (run after build)
mates-ssr start

Add to your package.json:

{
  "scripts": {
    "dev":   "mates-ssr dev",
    "build": "mates-ssr build",
    "start": "mates-ssr start"
  }
}

Runtime

Mates SSR automatically picks the best available runtime:

  • Bun — used when Bun is available in the environment. Faster startup, native fetch, Bun.serve.
  • Node.js — fallback when Bun is not available. Uses node:http, standard Request/Response globals.

No configuration needed — it just works in both.


What Mates SSR is not

It is intentionally minimal. The following are out of scope:

  • API routes / backend endpoints
  • Server actions / form mutations
  • Streaming HTML
  • Built-in i18n
  • Image optimisation
  • Middleware chains
  • Edge runtime adapters

It is a rendering shell for Mates, not a full-stack framework.


License

MIT