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

@putnami/web

v0.3.0

Published

React integration with SSR support, client-side hydration, and routing utilities

Readme

@putnami/web

React integration with SSR support, client-side hydration, and routing utilities built on React Router.

Installation

putnami deps add @putnami/web

Features

  • Server-Side Rendering (SSR) - React 19 HTML rendering with a streamed response body
  • File-based Routing - Automatic route generation from file structure (convention over configuration)
  • Type-safe Data Loading - Loaders and actions with full TypeScript support
  • Client-side Hydration - Seamless hydration with React Router v7
  • Conditional Exports - Automatic server/browser code splitting
  • Document Management - Built-in helpers for SEO, meta tags, and document head
  • Error Boundaries - Route-level error handling with error boundaries
  • Form Handling - Progressive enhancement with type-safe form actions
  • Prefetching - Intelligent link prefetching for better performance

Table of Contents

Quick Start

Using the React Plugin (Recommended)

The easiest way to get started is using the file-based routing plugin:

// src/main.ts
import { application } from '@putnami/application';
import { react } from '@putnami/web';

export const app = () => application().use(react());

Then create your pages using the file-based routing convention:

// src/app/page.tsx
import { useLoaderData } from '@putnami/web';

interface PageData {
  user: { name: string };
}

export default function HomePage() {
  const { user } = useLoaderData<PageData>();
  return <h1>Welcome, {user.name}!</h1>;
}

// src/app/loader.ts
import { loader } from '@putnami/web';

export default loader(async (ctx) => {
  const user = await fetchUser(ctx);
  return { user };
});

Manual Route Configuration

For more control, you can manually configure routes:

import { ReactApplication } from '@putnami/web';
import { application, http } from '@putnami/application';

const reactApp = new ReactApplication();

// Add a page with loader
reactApp.reactPage('/', {
  page: HomePage,
  loader: homeLoader,
});

// Add layouts for nested routes
reactApp.reactLayout('/dashboard', DashboardLayout);

reactApp.reactPage('/dashboard/settings', {
  page: SettingsPage,
  loader: settingsLoader,
  action: settingsAction,
});

// Register hydration script
reactApp.reactScript('/hydrate.main.js', 'hydrate.main.js');

// Integrate with main application
const app = application()
  .use(http({ port: 3000 }))
  .use(reactApp.getHttpPlugin());
app.start();

📚 Learn More: See Getting Started Guide for a complete walkthrough.

Architecture

flowchart TD
    A[HTTP Request] --> B[React Router Static Handler]
    B --> C[Execute Loaders]
    C --> D[Render React Components]
    D --> E[Stream HTML Response]
    E --> F[Client Receives HTML]
    F --> G[Hydrate React App]
    G --> H[Interactive Client App]

    I[File Structure] --> J[React Plugin Scanner]
    J --> K[Generate Routes]
    K --> L[Build Client Bundle]
    L --> M[Hydration Script]

Request Flow

  1. Server-Side Rendering: When a request arrives, the server executes loaders, renders React components, and streams HTML
  2. Hydration: The client receives the HTML and hydrates it with React, making it interactive
  3. Client Navigation: Subsequent navigations happen client-side with data prefetching

Key Concepts

  • Loaders: Server-side data fetching functions that run before rendering
  • Actions: Server-side form handlers for mutations
  • Layouts: Wrapper components that nest and share UI across routes
  • Error Boundaries: Route-level error handling components

📚 Learn More: See Architecture Deep Dive for detailed information.

Documentation

Comprehensive guides to help you build great React applications:

API Reference

ReactApplication

Main class for configuring React SSR routes.

| Method | Description | |--------|-------------| | reactPage(route, options) | Add a page with optional loader/action | | reactLayout(route, layout, loader?) | Add a layout component | | reactError(route, error) | Add an error boundary | | reactScript(route, path) | Register a static script | | getHttpPlugin() | Get the internal HttpPlugin |

Hooks

Type-safe wrappers around React Router hooks with improved generics.

useLoaderData<T>()

Returns typed loader data for the current route.

interface UserData {
  name: string;
  email: string;
}

function Profile() {
  const data = useLoaderData<UserData>();
  return <span>{data.name}</span>;
}

useActionData<T>()

Returns action data with ok and status fields, or undefined if no action has been performed.

interface FormResult {
  message: string;
}

function Form() {
  const result = useActionData<FormResult>();
  if (result?.ok) {
    return <div>{result.message}</div>;
  }
  return <form>...</form>;
}

useRouteLoaderData<T>(id)

Returns loader data for a specific route by ID.

function ChildComponent() {
  const rootData = useRouteLoaderData<RootData>('root');
  return <span>{rootData.user.name}</span>;
}

Other Re-exported Hooks

All standard React Router hooks are re-exported:

  • useNavigation - Navigation state
  • useNavigate - Programmatic navigation
  • useParams - Route parameters
  • useLocation - Current location
  • useSearchParams - Query parameters
  • useFetcher - Data fetching without navigation
  • useSubmit - Form submission
  • And more...

Dependencies

This package relies on the following key dependencies:

  • React (^19.2.3) - React rendering and component model
  • React Router (^7.14.2) - Client-side routing and SSR support
  • React DOM (^19.2.3) - DOM rendering and hydration

Conditional Exports

The package uses conditional exports for automatic code splitting:

{
  "exports": {
    ".": {
      "browser": "./src/index.browser.ts",
      "default": "./src/index.ts"
    }
  }
}
  • Default/server graph: SSR, generation, loaders, actions, and client-compatible re-exports
  • Browser graph: Client components, hydration, hooks, and lightweight route builders

The two entries are published from separate build graphs. Browser code cannot reach SSR implementations or server-only runtime imports through a shared chunk. Loaders, actions, and authorization always execute on the server; the browser receives only serialized loader/action results and bounded authentication/role/scope hints for conditional UI.

Examples

Simple Blog

// src/app/posts/[slug]/page.tsx
import { useLoaderData } from '@putnami/web';

export default function PostPage() {
  const { post } = useLoaderData<{ post: Post }>();
  return <article>{post.content}</article>;
}

// src/app/posts/[slug]/loader.ts
import { loader } from '@putnami/web';

export default loader(async (ctx) => {
  const slug = ctx.params.slug;
  const post = await getPost(slug);
  return { post };
});

Form with Action

// src/app/contact/page.tsx
import { Form, useActionData } from '@putnami/web';

export default function ContactPage() {
  const result = useActionData<{ message: string }>();

  return (
    <Form method="post">
      <input name="email" type="email" />
      <button type="submit">Send</button>
      {result?.ok && <p>{result.message}</p>}
    </Form>
  );
}

// src/app/contact/action.ts
import { action } from '@putnami/web';

export default action(async (ctx) => {
  const formData = await ctx.req.formData();
  await sendEmail(formData.get('email'));
  return { message: 'Email sent!' };
});

📚 More Examples: See Advanced Patterns for real-world examples.

Troubleshooting

Common issues and quick fixes:

  • Hydration mismatch: Ensure server and client render the same HTML
  • Route not found: Check file naming conventions match expected patterns
  • Loader errors: Verify the loader resolves serializable data or an explicit response
  • Action not working: Ensure form has method="post" and action is defined

📚 Full Guide: See Troubleshooting Guide for detailed solutions.

Support and contract

@putnami/web is a public, documented, maintained package classified stable. The web-application-delivery specification and the accepted decisions for separate render graphs and server-authoritative data/security define the promise.

The contract includes explicit SSR/SSG/ISR/island modes, request-isolated document state, script-safe hydration data, validated server loaders/actions, layout/page security propagation, default CSP/CSRF protection, cache isolation, and a deterministic render manifest. OAuth identity, API streaming, UI styling, and persistence remain owned by their framework packages. No default-framework or cross-language parity claim is made. Before v1.0.0, minor 0.x releases may contain documented breaking changes.

License

FSL-1.1-MIT