diff --git a/.cursor/rules/contributing.mdc b/.cursor/rules/contributing.mdc
deleted file mode 100644
index 5c637e65..00000000
--- a/.cursor/rules/contributing.mdc
+++ /dev/null
@@ -1,242 +0,0 @@
----
-description:
-globs:
-alwaysApply: true
----
-
-# Wouter - Minimalist React Router
-
-## Project Overview
-
-Wouter is a **minimalist-friendly ~2.1KB React router** that focuses on performance, small bundle size, and simplicity. It provides hook-based routing for React and Preact applications.
-
-### Key Features
-
-- Tiny bundle size (2.1KB gzipped vs 18.7KB React Router)
-- Hook-based API (`useLocation`, `useRoute`, `useParams`, etc.)
-- Zero dependencies
-- Supports React and Preact
-- Optional top-level `` component
-- Nested routing support
-- SSR support
-- Memory location for testing
-
-## Project Structure
-
-```
-wouter/
-├── packages/
-│ ├── wouter/ # Main React package
-│ │ ├── src/
-│ │ │ ├── index.js # Main router implementation
-│ │ │ ├── use-browser-location.js
-│ │ │ ├── use-hash-location.js
-│ │ │ ├── memory-location.js
-│ │ │ ├── react-deps.js # React imports
-│ │ │ └── paths.js # Path utilities
-│ │ ├── test/ # Test files
-│ │ ├── types/ # TypeScript definitions
-│ │ └── esm/ # Built ES modules
-│ └── wouter-preact/ # Preact package
-├── assets/ # Documentation assets
-└── .github/ # GitHub workflows
-```
-
-## Development Workflow
-
-### Prerequisites
-
-- Node.js v20.17.0+ or v22.9.0+
-- npm
-
-### Commands
-
-```bash
-# Install dependencies
-npm install
-
-# Run tests (watch mode)
-npm test
-
-# Run tests (single run)
-npm test -- --run
-
-# Run specific test file
-npm test -- packages/wouter/test/router.test.tsx --run
-
-# Run tests with coverage
-npm test -- --coverage
-
-# Build packages
-npm run build
-
-# Type checking
-npm run typecheck
-
-# Lint code
-npm run lint
-```
-
-### Test Files Organization
-
-- `test/router.test.tsx` - Router component and memoization tests
-- `test/use-location.test.tsx` - Location hook tests
-- `test/use-route.test.tsx` - Route matching tests
-- `test/link.test.tsx` - Link component tests
-- `test/memory-location.test.ts` - Memory location for testing
-- `test/nested-route.test.tsx` - Nested routing tests
-- `test/ssr.test.tsx` - Server-side rendering tests
-
-## Key Development Principles
-
-### Performance First
-
-- **Bundle size matters**: Every byte counts. Use size-limit CI checks
-- **Stable object references**: Prevent unnecessary re-renders with proper memoization
-- **Minimize dependencies**: Zero external dependencies policy
-
-### Code Style
-
-- **Functional programming**: Prefer hooks over class components
-- **Compact code**: Optimize for bundle size (may sacrifice readability)
-- **React patterns**: Use `React.memo`, `useCallback`, `useMemo` appropriately
-
-### Testing Guidelines
-
-- **Comprehensive coverage**: Test all public APIs
-- **Real-world scenarios**: Include complex use cases (like language switching)
-- **Memory location**: Use for isolated testing
-- **Memoization tests**: Verify stable object references
-
-## Core APIs
-
-### Hooks
-
-- `useLocation()` - Get/set current location
-- `useRoute(pattern)` - Match current location against pattern
-- `useParams()` - Extract route parameters
-- `useSearch()` - Get search/query string
-- `useSearchParams()` - Get/set URLSearchParams
-- `useRouter()` - Access router configuration
-
-### Components
-
-- `` - Optional configuration wrapper
-- `` - Conditional rendering based on path
-- `` - Navigation component
-- `` - Exclusive routing
-- `` - Programmatic navigation
-
-### Location Hooks
-
-- `useBrowserLocation` - Browser history API
-- `useHashLocation` - Hash-based routing
-- `memoryLocation` - In-memory for testing
-
-## Performance Considerations
-
-### Router Memoization
-
-The Router component uses sophisticated memoization to prevent unnecessary re-renders:
-
-- Only creates new router objects when props actually change
-- Preserves stable references for `React.memo` components
-- Critical for performance in complex applications
-
-### Bundle Optimization
-
-- Use `// ... existing code ...` pattern in edits to minimize diffs
-- Prefer shorter variable names in production builds
-- Optimize for gzip/brotli compression
-
-## Common Patterns
-
-### Testing Router Components
-
-```javascript
-import { memoryLocation } from "wouter/memory-location";
-
-const { hook } = memoryLocation({ path: "/test/path" });
-render(
-
-
-
-);
-```
-
-### Custom Location Hooks
-
-```javascript
-const useCustomLocation = () => {
- const [location, setLocation] = useBrowserLocation();
- // Add custom logic here
- return [location, setLocation];
-};
-```
-
-### Nested Routing
-
-```javascript
-
-
-
-
-
-```
-
-## Debugging Tips
-
-### Common Issues
-
-1. **Router memoization**: Check if unnecessary re-renders occur
-2. **Base path conflicts**: Verify base path inheritance in nested routers
-3. **Pattern matching**: Use regexparam syntax for route patterns
-4. **SSR hydration**: Ensure client/server router configs match
-
-### Debugging Tools
-
-- React DevTools Profiler for render tracking
-- Network tab for bundle size verification
-- Console warnings for deprecated patterns
-
-## Contributing Guidelines
-
-### Before Making Changes
-
-1. Run full test suite: `npm test -- --run`
-2. Check bundle size impact
-3. Verify TypeScript definitions
-4. Test with both React and Preact
-
-### Adding New Features
-
-1. Consider bundle size impact
-2. Add comprehensive tests
-3. Update TypeScript definitions
-4. Document in README if public API
-5. Maintain backward compatibility
-
-### Performance Changes
-
-1. Add before/after performance tests
-2. Verify memoization behavior
-3. Test with complex nested scenarios
-4. Check for memory leaks in long-running apps
-
-## Architecture Notes
-
-### Router Context System
-
-- Default router created on-demand
-- Context inheritance with override capabilities
-- Memoization prevents cascade re-renders
-- Base path accumulation in nested routers
-
-### Pattern Matching
-
-- Uses regexparam library internally
-- Supports named parameters, wildcards, optional segments
-- Regex patterns supported for complex matching
-- Loose mode for nested routing
-
-This project prioritizes performance and minimalism while maintaining full feature parity with larger routing solutions.
diff --git a/.cursor/rules/publishing.mdc b/.cursor/rules/publishing.mdc
deleted file mode 100644
index dc89c544..00000000
--- a/.cursor/rules/publishing.mdc
+++ /dev/null
@@ -1,109 +0,0 @@
----
-description:
-globs:
-alwaysApply: true
----
-# Publishing Wouter Packages
-
-This document outlines the process for publishing new versions of the wouter packages to npm.
-
-## Prerequisites
-
-- Ensure you have npm publish permissions for both `wouter` and `wouter-preact` packages
-- Have npm authentication set up (you'll need OTP access)
-- All tests should be passing
-- All changes should be committed to the repository
-
-## Publishing Process
-
-### 1. Version Bump
-
-Update the version in both package.json files:
-- `packages/wouter/package.json`
-- `packages/wouter-preact/package.json`
-
-For semantic versioning:
-- **Patch** (x.x.X): Bug fixes, small improvements
-- **Minor** (x.X.x): New features, backward compatible
-- **Major** (X.x.x): Breaking changes
-
-### 2. Dry Run Validation
-
-Run dry publish commands to validate the packages before actual publishing:
-
-```bash
-npm publish --dry-run -w wouter
-npm publish --dry-run -w wouter-preact
-```
-
-This will show you:
-- Package contents and file list
-- Bundle size information
-- Version confirmation
-- Any potential issues
-
-### 3. Build Verification (Optional)
-
-Check the build artifacts in the `/esm/` folders to ensure the latest updates are properly built:
-- `packages/wouter/esm/`
-- `packages/wouter-preact/esm/`
-
-The `prepublishOnly` script automatically builds the packages, but it's good to verify.
-
-## ⚠️ Confirmation Required
-
-**ALWAYS ask for user confirmation before proceeding with the following steps:**
-
-### 4. Publish to npm
-
-Publish both packages to the npm registry:
-
-```bash
-npm publish -w wouter
-npm publish -w wouter-preact
-```
-
-**Note:** npm might fail with "EOTP" (Error One-Time Password) even when the OTP was entered correctly in the browser. This is a known npm issue - the packages are usually still published successfully despite the error message.
-
-### 5. Git Commit and Tag
-
-After successful publishing:
-
-1. **Commit the version changes:**
- ```bash
- git add packages/wouter/package.json packages/wouter-preact/package.json
- git commit -m "Bump version to X.X.X"
- ```
-
-2. **Create a version tag:**
- ```bash
- git tag vX.X.X
- ```
-
-3. **Push to remote (optional):**
- ```bash
- git push origin main
- git push origin vX.X.X
- ```
-
-## Troubleshooting
-
-### Common Issues
-
-- **Authentication errors**: Run `npm login` or use the browser authentication flow
-- **OTP timeout**: Generate a fresh OTP code from your authenticator app
-- **Permission denied**: Ensure you have publish permissions for both packages
-- **Version conflicts**: Check if the version already exists on npm
-
-### Verification
-
-After publishing, verify the packages are available:
-- Check [wouter on npm](mdc:https:/www.npmjs.com/package/wouter)
-- Check [wouter-preact on npm](mdc:https:/www.npmjs.com/package/wouter-preact)
-- Test installation: `npm install wouter@latest`
-
-## Package Information
-
-- **wouter**: Main React package (~22KB package size)
-- **wouter-preact**: Preact-specific package (~22KB package size)
-- Both packages maintain version parity and should be published together
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..8d23e85a
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,117 @@
+name: Publish packages
+
+on:
+ push:
+ tags: ["v*"]
+ workflow_dispatch:
+ inputs:
+ tag:
+ description: Existing release tag (for example v3.12.0 or v3.12.0-next.0)
+ required: true
+ type: string
+ dry_run:
+ description: Validate and pack without publishing
+ type: boolean
+ default: false
+
+permissions:
+ contents: read
+
+concurrency:
+ group: npm-publish
+ cancel-in-progress: false
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ id-token: write
+ env:
+ RELEASE_TAG: ${{ inputs.tag || github.ref_name }}
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ ref: refs/tags/${{ inputs.tag || github.ref_name }}
+ fetch-depth: 0
+ persist-credentials: false
+
+ - uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: 1.4.2
+
+ # npm >= 11.5.1 supports OIDC. Bun handles installs and checks.
+ - uses: actions/setup-node@v6
+ with:
+ node-version: 24
+ registry-url: https://registry.npmjs.org
+ package-manager-cache: false
+ - run: npm install --global npm@11.17.0
+
+ - name: Check release tag and package versions
+ id: release
+ run: |
+ git merge-base --is-ancestor HEAD origin/main
+ bun - <<'JS'
+ import { appendFileSync } from "node:fs";
+ const tag = process.env.RELEASE_TAG;
+ const number = "(?:0|[1-9]\\d*)";
+ const identifier = "(?:0|[1-9]\\d*|\\d*[A-Za-z-][\\dA-Za-z-]*)";
+ const pattern = new RegExp(`^v${number}\\.${number}\\.${number}(?:-${identifier}(?:\\.${identifier})*)?$`);
+ if (!pattern.test(tag)) throw new Error(`Invalid release tag: ${tag}`);
+ const version = tag.slice(1);
+ for (const name of ["wouter", "wouter-preact"]) {
+ const pkg = await Bun.file(`packages/${name}/package.json`).json();
+ if (pkg.name !== name || pkg.version !== version || pkg.private) {
+ throw new Error(`${name} must be public and have version ${version}`);
+ }
+ }
+ appendFileSync(process.env.GITHUB_OUTPUT, `version=${version}\ndist_tag=${version.includes("-") ? "next" : "latest"}\n`);
+ JS
+
+ - run: bun install --frozen-lockfile
+ - run: bun test --coverage
+
+ # Tests remove copied Preact sources, so prepare packages after testing.
+ - name: Prepare packages
+ run: |
+ bun run --cwd packages/wouter prepublishOnly
+ bun run --cwd packages/wouter-preact prepublishOnly
+ - run: bun run test-types
+ - run: bun run lint
+ - run: bun run size
+
+ - name: Pack and inspect both packages
+ run: |
+ mkdir -p "$RUNNER_TEMP/packages"
+ npm pack --workspace wouter --workspace wouter-preact --ignore-scripts --json --pack-destination "$RUNNER_TEMP/packages" > "$RUNNER_TEMP/packages.json"
+ bun - <<'JS'
+ const packed = await Bun.file(`${process.env.RUNNER_TEMP}/packages.json`).json();
+ if (packed.length !== 2) throw new Error("Expected both packages");
+ for (const pkg of packed) {
+ const manifest = await Bun.file(`packages/${pkg.name}/package.json`).json();
+ const files = new Set(pkg.files.map(file => file.path));
+ const required = ["README.md", "src/react-deps.js", "src/paths.js", ...Object.values(manifest.exports).flatMap(entry => Object.values(entry))];
+ for (const path of required) {
+ if (!files.has(path.replace(/^\.\//, ""))) throw new Error(`${pkg.name} is missing ${path}`);
+ }
+ }
+ JS
+
+ - name: Publish missing versions to npm
+ if: ${{ !inputs.dry_run }}
+ env:
+ VERSION: ${{ steps.release.outputs.version }}
+ DIST_TAG: ${{ steps.release.outputs.dist_tag }}
+ run: |
+ for package in wouter wouter-preact; do
+ # Allow recovery when only one package was published on a previous run.
+ if npm view "$package@$VERSION" version --json > "$RUNNER_TEMP/version.json"; then
+ echo "$package@$VERSION already exists; skipping"
+ continue
+ elif ! jq -e '.error.code == "E404"' "$RUNNER_TEMP/version.json" > /dev/null; then
+ echo "::error::Could not check $package@$VERSION on npm"
+ exit 1
+ fi
+ npm publish "$RUNNER_TEMP/packages/$package-$VERSION.tgz" --ignore-scripts --access public --tag "$DIST_TAG"
+ done
diff --git a/AGENTS.md b/AGENTS.md
index b8100b77..c9a7f579 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,111 +1,43 @@
----
-description: Use Bun instead of Node.js, npm, pnpm, or vite.
-globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
-alwaysApply: false
----
-
-Default to using Bun instead of Node.js.
-
-- Use `bun ` instead of `node ` or `ts-node `
-- Use `bun test` instead of `jest` or `vitest`
-- Use `bun build ` instead of `webpack` or `esbuild`
-- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
-- Use `bun run
-