Lightweight, SSR-compatible routing for the OpenSite website platform. Native browser routing with zero dependencies, tree-shakable exports, and < 3KB gzipped.
- 🚀 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
npm install @page-speed/router
# or
pnpm add @page-speed/router
# or
yarn add @page-speed/routerMany 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>
);
}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>;
}Get current URL information. Works without RouterProvider.
const {
href, // Full URL
pathname, // Path without query/hash
search, // Query string
hash, // Hash fragment
origin, // Protocol + host
host, // Hostname + port
hostname, // Just hostname
port, // Port number
protocol // Protocol (http:, https:)
} = useUrl();Programmatic navigation. Works without RouterProvider.
const { navigateTo, replace, reload } = useNavigation();
// Navigate to path
navigateTo('/about');
// Navigate with options
navigateTo({
path: '/blog',
anchor: 'comments', // Smooth scroll to anchor
replace: false, // Use pushState (default)
smooth: true, // Smooth scrolling (default)
state: { from: 'home' } // History state
});
// Replace current entry
replace('/new-path');
// Reload page
reload();Safe back navigation with fallback.
const { goBack, canGoBack } = useGoBack({
fallback: '/home' // Where to go if no history
});
// Go back or fallback
goBack();
// Go back multiple steps
goBack(-2);
// Conditional UI
<button disabled={!canGoBack}>Back</button>Extract dynamic route parameters. Requires RouterProvider with routes.
// Route: /blog/:slug
// URL: /blog/hello-world
const params = useParams();
// { slug: 'hello-world' }
// Include query params
const allParams = useParams(true);
// { slug: 'hello-world', page: '2' }Check if current path matches a pattern.
const match = useRouteMatch('/blog/:slug');
if (match.isMatch) {
console.log(match.params); // { slug: 'current-slug' }
}const pathname = usePathname(); // '/blog/post-1'const params = useSearchParams();
// URL: /products?category=electronics&sort=price
// Returns: { category: 'electronics', sort: 'price' }
const repeated = useSearchParams();
// URL: /products?tag=news&tag=events&page=2
// Returns: { tag: ['news', 'events'], page: '2' }const hash = useHash();
// URL: /blog#comments
// Returns: 'comments' (without #)const updateParams = useUpdateSearchParams();
// Add/update params
updateParams({ page: '2', sort: 'date' });
// Replace one key with repeated values (unrelated params are retained)
updateParams({ tag: ['news', 'events'] });
// Remove param
updateParams({ category: null });
// Replace history
updateParams({ page: '3' }, true);<!-- customer_websites/chai_index.html.erb -->
<script>
window.__ROUTER_INITIAL_STATE__ = {
path: '<%= @initial_path %>',
params: <%= @initial_params.to_json %>
};
</script>
<div id="root"></div>// Client entry point
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>
);// _app.tsx
import { RouterProvider } from '@page-speed/router';
export default function App({ Component, pageProps, router }) {
return (
<RouterProvider initialPath={router.pathname}>
<Component {...pageProps} />
</RouterProvider>
);
}Automatic smooth scrolling to anchors:
const { navigateTo } = useNavigation();
// Navigate to page and scroll to anchor
navigateTo({
path: '/docs',
anchor: 'installation'
});
// Scroll to anchor on current page
navigateTo({ anchor: 'features' });
// Control scroll behavior
navigateTo({
path: '/about',
anchor: 'team',
smooth: false // Instant scroll
});import { matchPath, buildPath } from '@page-speed/router';
// Match a path
const match = matchPath('/blog/hello', '/blog/:slug');
// { isMatch: true, params: { slug: 'hello' }, path: '/blog/hello' }
// Build a path from pattern
const path = buildPath('/blog/:slug', { slug: 'world' });
// '/blog/world'import { useMultiMatch } from '@page-speed/router';
const match = useMultiMatch([
{ pattern: '/blog/:slug' },
{ pattern: '/products/:id' },
{ pattern: '/', exact: true }
]);
if (match?.path.startsWith('/blog')) {
// Handle blog routes
}<RouterProvider
onNavigate={(path) => {
// Track page views
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.
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.
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 });
});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>| Option | Type | Default | Description |
|---|---|---|---|
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.
| Prop | Type | Default | Description |
|---|---|---|---|
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 |
interface Route {
path: string; // Pattern like '/blog/:slug'
exact?: boolean; // Exact match required
}- 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
- Chrome/Edge 88+
- Firefox 78+
- Safari 14+
- Chrome Android 88+
- Safari iOS 14+
Requires History API and IntersectionObserver support.
// Before (React Router)
import { useNavigate, useParams } from 'react-router-dom';
const navigate = useNavigate();
navigate('/about');
// After (@page-speed/router)
import { useNavigation, useParams } from '@page-speed/router';
const { navigateTo } = useNavigation();
navigateTo('/about');// Before (Next.js)
import { useRouter } from 'next/router';
const router = useRouter();
router.push('/about');
// After (@page-speed/router)
import { useNavigation } from '@page-speed/router';
const { navigateTo } = useNavigation();
navigateTo('/about');Full TypeScript support with exported types:
import type {
UrlState,
NavigateOptions,
RouteParams,
Route
} from '@page-speed/router';See CONTRIBUTING.md for development setup and guidelines.
BSD-3-Clause
Built with ❤️ for the OpenSite ecosystem
