@page-speed/router
Lightweight, SSR-compatible routing for the OpenSite website platform. Native browser routing with zero dependencies, tree-shakable exports, and < 3KB gzipped.


Features
- 🚀 Ultra-lightweight: < 3KB minified + gzipped
- 🔥 SSR-safe: Built for server-side rendering with proper hydration
- 🌲 Tree-shakable: Import only what you need
- 🎯 Native browser APIs: Uses History API and window.location
- 📦 Zero dependencies: Only React as peer dependency
- ⚡ Performance-first: Optimized for Core Web Vitals
- 🎨 TypeScript: Full type safety
- 🧩 Modular: Hooks work independently, provider optional
Installation
npm install @page-speed/router
pnpm add @page-speed/router
yarn add @page-speed/router
Quick Start
Basic Navigation (No Provider Required)
Many hooks work without any provider setup:
import { useNavigation, useUrl } from '@page-speed/router';
function App() {
const { navigateTo } = useNavigation();
const { pathname } = useUrl();
return (
<div>
<p>Current path: {pathname}</p>
<button onClick={() => navigateTo('/about')}>
Go to About
</button>
</div>
);
}
With RouterProvider (For Dynamic Params)
import { RouterProvider, useParams } from '@page-speed/router';
const routes = [
{ path: '/' },
{ path: '/blog/:slug' },
{ path: '/products/:category/:id' }
];
function App() {
return (
<RouterProvider routes={routes}>
<BlogPost />
</RouterProvider>
);
}
function BlogPost() {
const { slug } = useParams();
return <div>Blog post: {slug}</div>;
}
Core Hooks
useUrl()
Get current URL information. Works without RouterProvider.
const {
href,
pathname,
search,
hash,
origin,
host,
hostname,
port,
protocol
} = useUrl();
useNavigation()
Programmatic navigation. Works without RouterProvider.
const { navigateTo, replace, reload } = useNavigation();
navigateTo('/about');
navigateTo({
path: '/blog',
anchor: 'comments',
replace: false,
smooth: true,
state: { from: 'home' }
});
replace('/new-path');
reload();
useGoBack()
Safe back navigation with fallback.
const { goBack, canGoBack } = useGoBack({
fallback: '/home'
});
goBack();
goBack(-2);
<button disabled={!canGoBack}>Back</button>
useParams()
Extract dynamic route parameters. Requires RouterProvider with routes.
const params = useParams();
const allParams = useParams(true);
useRouteMatch()
Check if current path matches a pattern.
const match = useRouteMatch('/blog/:slug');
if (match.isMatch) {
console.log(match.params);
}
Utility Hooks
usePathname()
const pathname = usePathname();
useSearchParams()
const params = useSearchParams();
const repeated = useSearchParams();
useHash()
const hash = useHash();
useUpdateSearchParams()
const updateParams = useUpdateSearchParams();
updateParams({ page: '2', sort: 'date' });
updateParams({ tag: ['news', 'events'] });
updateParams({ category: null });
updateParams({ page: '3' }, true);
SSR Setup
Rails Integration
<!-- customer_websites/chai_index.html.erb -->
<script>
window.__ROUTER_INITIAL_STATE__ = {
path: '<%= @initial_path %>',
params: <%= @initial_params.to_json %>
};
</script>
<div id="root"></div>
import { RouterProvider } from '@page-speed/router';
const initialState = window.__ROUTER_INITIAL_STATE__;
ReactDOM.hydrateRoot(
document.getElementById('root'),
<RouterProvider
initialPath={initialState.path}
routes={routes}
>
<App />
</RouterProvider>
);
Next.js Integration
import { RouterProvider } from '@page-speed/router';
export default function App({ Component, pageProps, router }) {
return (
<RouterProvider initialPath={router.pathname}>
<Component {...pageProps} />
</RouterProvider>
);
}
Anchor Navigation
Automatic smooth scrolling to anchors:
const { navigateTo } = useNavigation();
navigateTo({
path: '/docs',
anchor: 'installation'
});
navigateTo({ anchor: 'features' });
navigateTo({
path: '/about',
anchor: 'team',
smooth: false
});
Advanced Usage
Custom Route Matching
import { matchPath, buildPath } from '@page-speed/router';
const match = matchPath('/blog/hello', '/blog/:slug');
const path = buildPath('/blog/:slug', { slug: 'world' });
Multiple Route Patterns
import { useMultiMatch } from '@page-speed/router';
const match = useMultiMatch([
{ pattern: '/blog/:slug' },
{ pattern: '/products/:id' },
{ pattern: '/', exact: true }
]);
if (match?.path.startsWith('/blog')) {
}
Navigation Events
<RouterProvider
onNavigate={(path) => {
analytics.track('page_view', { path });
}}
>
<App />
</RouterProvider>
Note: onNavigate only fires for the initial load when initialPath matches
the current location. For reliable page-view tracking (initial load + every
navigation), use the analytics hooks below.
Page View Analytics (Opt-In)
Fully optional — if you never import these, they are tree-shaken away and the
router behaves exactly as before. Apps that use the router without analytics
(showcases, internal tools) need no changes.
usePageViews() — bring your own backend
Fires once after mount and once per client-side navigation (routechange and
browser back/forward). Consecutive duplicate paths are deduped, so the double
event dispatched by navigateTo and hash-only changes never double-count.
import { usePageViews } from '@page-speed/router';
usePageViews(({ path, previousPath, isInitial }) => {
myAnalytics.track('page_view', { path, isInitial });
});
usePageViewAnalytics() / <PageViewAnalytics /> — DashTrack analytics engine
Reports page views to POST /website_page_views on the DashTrack API.
import { RouterProvider, PageViewAnalytics } from '@page-speed/router';
<RouterProvider>
<PageViewAnalytics websiteToken={website.token} />
<App />
</RouterProvider>
websiteToken | string | null | undefined | - | The website's UUID token (not the numeric website id). Tracking no-ops when absent. |
apiBaseUrl | string | https://api.dashtrack.com | Analytics API origin |
category | string | 'webpage' | Page view category |
enabled | boolean | true | Set false to suspend tracking (e.g. in development) |
transformPayload | (payload, view) => payload | null | - | Inspect/extend the payload, or return null to skip a view |
Delivery details: payload is nested under the website_page_view wrapper key,
sent with keepalive: true and no credentials; failures are swallowed so
analytics can never break the host site. The visitor IP is derived server-side
from the request — no client-side IP lookup is performed.
API Reference
RouterProvider Props
children | ReactNode | - | Child components |
initialPath | string | - | Initial path for SSR |
routes | Route[] | [] | Route patterns for param extraction |
scrollBehavior | 'smooth' | 'auto' | 'smooth' | Default scroll behavior |
onNavigate | (path: string) => void | - | Navigation callback |
Route Object
interface Route {
path: string;
exact?: boolean;
}
Performance
- Bundle Size: < 3KB minified + gzipped
- Tree-shaking: Import individual hooks for smaller bundles
- No Re-renders: Optimized context updates
- Lazy Loading: Dynamic imports for code splitting
- 60fps Scrolling: Smooth anchor navigation
Browser Support
- Chrome/Edge 88+
- Firefox 78+
- Safari 14+
- Chrome Android 88+
- Safari iOS 14+
Requires History API and IntersectionObserver support.
Migration Guide
From React Router
import { useNavigate, useParams } from 'react-router-dom';
const navigate = useNavigate();
navigate('/about');
import { useNavigation, useParams } from '@page-speed/router';
const { navigateTo } = useNavigation();
navigateTo('/about');
From Next.js Router
import { useRouter } from 'next/router';
const router = useRouter();
router.push('/about');
import { useNavigation } from '@page-speed/router';
const { navigateTo } = useNavigation();
navigateTo('/about');
TypeScript
Full TypeScript support with exported types:
import type {
UrlState,
NavigateOptions,
RouteParams,
Route
} from '@page-speed/router';
Contributing
See CONTRIBUTING.md for development setup and guidelines.
License
BSD-3-Clause
Support
Built with ❤️ for the OpenSite ecosystem