Overall AI readiness score
A combined result across all audit signals.
Overall AI readiness score
A combined result across all audit signals.
Overall AI readiness assessment
state-in-url was independently audited by llmsmap. state-in-url.dev currently scores 71/100 for AI readiness. The result combines AI-specific files, crawler policy, sitemap discovery, structured homepage data, and—when available—the mobile Google Lighthouse technical profile.
llms.txt is accessible and contains 2,702 tokens. No accessible llms-full.txt was detected, so deeper context still has to be assembled from regular pages. A separate ai.txt policy is published.
robots.txt is available. 11 of 11 tracked AI bots are not blocked. Declared sitemaps: 1. No Schema.org type was detected on the homepage. 0 OpenGraph tags were found and markup completeness is 0%, leaving more entity interpretation to crawlers.
The mobile Lighthouse profile adds Performance 83/100, Accessibility 100/100, Best Practices 100/100, SEO 100/100, and experimental Agentic Browsing 100/100. These signals have a limited weight: they complement rather than replace llms.txt, robots.txt, and structured-data checks.
A mobile Lighthouse measurement. Google’s experimental Agentic Browsing category is explained separately and does not replace the broader llmsmap AI-readiness score.
Performance
Accessibility
Best Practices
Technical SEO
Agentic Browsing
Mobile performance is 83/100, with the largest visible content block appearing in 4.1 s and the browser main thread blocked for 220 ms. Layout shift was 0. The main thread is where the browser runs JavaScript, calculates layout, and paints the page; long work there delays both user input and browser-agent actions.
Accessibility scored 100/100, Best Practices 100/100, and technical SEO 100/100. The experimental Agentic Browsing category scored 100/100. It measures signals Google currently tests for software agents and is shown separately from the llmsmap AI-readiness score.
Split long JavaScript tasks, defer non-critical scripts and styles, and shorten blocking request chains. This helps the primary content appear sooner and makes controls usable earlier.
Remove unused CSS and JavaScript, load heavy widgets on demand, and limit third-party scripts. Less code means less parsing and background work on the device.
First content
Main content
Layout stability
Blocking time
Visual speed
Machine-readable files, crawler policy, discovery, and homepage markup.
Full version was not found
1 sitemap found
Schema.org markup was not found on the homepage
OpenGraph tags were not found on the homepage
Based on robots.txt analysis
Declared discovery routes for crawlers and agents.
# state-in-url
> A React hook library for storing typed, JSON-serializable state in URL query parameters. ~2 KB, zero runtime deps. Supports the Next.js App Router (14/15/16), React Router 6/7, Remix 2, Astro (React/Preact islands), and plain React. MIT licensed.
Last updated: 2026-08-31. Canonical: <https://state-in-url.dev/llms.txt>.
## For AI coding agents — preferred path
This package ships task-focused SKILL.md files via [@tanstack/intent](https://tanstack.com/intent/latest/docs/overview). If your agent supports Intent, that is the right surface to load instead of this file:
```bash
# in the user's project, once
npx @tanstack/intent@latest install
# list skills available for installed libraries
npx @tanstack/intent@latest list
# load a specific skill into the current context
npx @tanstack/intent@latest load state-in-url#feature-state-hook
```
Available skills (under `node_modules/state-in-url/skills/`):
| Skill | Type | When to load |
|---|---|---|
| `feature-state-hook` | core | Defining state and wrapping `useUrlState` in a feature-scoped custom hook |
| `input-handling` | core | Text inputs / sliders / fast-changing controls |
| `nextjs-ssr` | framework | Next.js App Router: `searchParams` forwarding, Proxy for layouts |
| `react-router-remix-setup` | framework | React Router v6/v7 or Remix v2 setup |
| `astro-setup` | framework | Astro: React/Preact islands with the `searchParams` prop, or no client framework via `encodeState`/`decodeState` |
| `form-library-integration` | composition | Pairing with `react-hook-form` (or formik) |
| `shared-state-no-url` | core | `useSharedState` — cross-component state without URL sync |
The rest of this file is a condensed reference for agents that cannot load Intent skills.
## Comparison with alternatives
Full page: <https://state-in-url.dev/vs/nuqs> (Markdown: /vs/nuqs.md). Sizes: whole-library import, esbuild minify + gzip, measured August 2026 (nuqs 2.10.1).
| Library | Setup | Nested objects and dates | Size | Pick it when |
|---|---|---|---|---|
| state-in-url | None - import the hook | Preserved automatically, types included | ~2.9 KB gzip, zero deps | One typed object, zero setup, on Next.js / React Router / Remix / Astro |
| nuqs | Adapter component, parser per key | JSON parser plus your own validator | ~6.7 KB gzip, one dep | Each value as its own readable query param |
| TanStack Router | validateSearch on each route | JSON-first; dates need custom serialization | Built into the router | You are on TanStack Router |
| use-query-params | Provider plus router adapter, param config per key | Via a JSON param type, loosely typed | ~4.4 KB gzip + serialize-query-params | A codebase already built on it |
| useSearchParams | None - built into the router | Strings only, everything by hand | 0 KB | One or two flat string params |
## Package info
- npm: https://www.npmjs.com/package/state-in-url
- repo: https://github.com/asmyshlyaev177/state-in-url
- website: https://state-in-url.dev
- full README: https://raw.githubusercontent.com/asmyshlyaev177/state-in-url/refs/heads/master/README.md
## Supported frameworks
| Framework | Versions | Import path |
|---|---|---|
| Next.js (App Router only) | 14 / 15 / 16 | `state-in-url/next` |
| React Router | v7 | `state-in-url/react-router` |
| React Router | v6 | `state-in-url/react-router6` |
| Remix | v2 | `state-in-url/remix` |
| Astro | React or Preact (compat) islands, on-demand rendered pages | `state-in-url/astro` |
| Framework-agnostic | — | `state-in-url` (`useSharedState`), `state-in-url/encodeState` |
Pages Router is **not** supported.
## Install
```bash
npm install state-in-url
```
In `tsconfig.json`, ensure `"moduleResolution": "Bundler"` (or `"Node16"` / `"NodeNext"`).
## Core rules — read first
1. **Type, never interface.** The hook's generic constraint `JSONCompatible<T>` rejects `interface`. Always declare a `type` AND annotate the const: `const FOO_STATE: FooState = { ... }`.
2. **Default-state object must be a module-scoped `const`.** Never built from props, hook returns, or destructuring inside a component. Sharing is keyed by object identity.
3. **One feature → one shared default-state const → one custom hook.** Components import the hook, never `useUrlState` directly with their own defaults object.
4. **No secrets in URL.** Entity IDs (`jobId`, `memberId`) are fine; tokens, API keys, PII are not.
5. **JSON-serializable only.** Functions, BigInt, Symbol, Map, Set, class instances will not round-trip. Dates are supported.
6. **URL size**: keep total query-string under ~12 KB to stay safe across CDNs (Vercel header limit is 14 KB).
## API: `useUrlState`
```typescript
const { urlState, setState, setUrl, reset } = useUrlState(DEFAULT_STATE, options?);
```
- `urlState` — current state, typed identically to `DEFAULT_STATE`.
- `setState(value)` — updates internal state synchronously; **does not touch URL**.
- `setUrl(value, opts?)` — updates state + URL. URL write is throttled (next tick).
- `reset(opts?)` — sets state and URL back to `DEFAULT_STATE`.
Setter call signatures (both `setState` and `setUrl`):
```typescript
setUrl({ field: 'newValue' }); // partial patch
setUrl(curr => ({ ...curr, field: 'newValue' })); // functional with current
setUrl((curr, initial) => initial); // reset via callback
setUrl(); // flush current state to URL
```
`options` (per-call or as hook-level defaults):
- `replace?: boolean` — `router.replace` (default `true`) vs `router.push`.
- `scroll?: boolean` — Next.js scroll behavior (default `false`).
- React Router / Remix: any `NavigateOptions` (e.g. `preventScrollReset`).
Hook-level options (second arg to `useUrlState`):
- `searchParams` — pass `searchParams` from a Next.js server component. **Required for SSR correctness in Next.js App Router.** `useSearchParams()` is a fallback for components the prop cannot reach, and costs prerendering; see below.
- `useHistory?: boolean` — Next.js only. Default `true`. Uses `window.history.pushState` to avoid `_rsc` server round-trips on URL changes. Flip to `false` only when server data must refetch on URL change.
## Recommended pattern: feature-scoped hook
```typescript
// features/jobs/jobsState.ts
export type JobsState = {
status: '' | 'active' | 'closed';
tab: 'details' | 'qa' | 'applicants';
jobId: string;
};
export const JOBS_STATE: JobsState = {
status: '',
tab: 'details',
jobId: '',
};
```
```typescript
// features/jobs/useJobsState.ts
'use client';
import { useUrlState } from 'state-in-url/next';
import { JOBS_STATE } from './jobsState';
// `searchParams` comes from the server component at the top of the feature.
// Nested callers pass nothing and share the same store.
export function useJobsState(searchParams?: object) {
return useUrlState(JOBS_STATE, { searchParams });
}
```
Any component calling `useJobsState()` shares the same URL-synced store. No Context, no Provider.
Do not call `useSearchParams()` to feed the hook. It needs a `<Suspense>` boundary, opts the page out of prerendering, and buys nothing the server prop does not — on the client the hook already reads `window.location.search`. Thread `searchParams` from the server component. Reach for `useSearchParams()` only when a component genuinely cannot receive it, and accept the bailout.
## Next.js App Router — server page forwarding `searchParams`
In **Next.js 15+**, `searchParams` is a `Promise`:
```typescript
// app/jobs/page.tsx (Server Component)
import { JobsList } from './JobsList';
export default async function Page({
searchParams,
}: {
searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
const sp = await searchParams;
return <JobsList searchParams={sp} />;
}
// app/jobs/JobsList.tsx
'use client';
import { useUrlState } from 'state-in-url/next';
import { JOBS_STATE } from 'features/jobs/jobsState';
export function JobsList({ searchParams }: { searchParams: object }) {
const { urlState, setUrl } = useUrlState(JOBS_STATE, { searchParams });
// ...
}
```
Without `searchParams`, the first render uses defaults, then a client effect re-syncs from the URL on the next tick — visible flash plus hydration warning.
The hook does not call `useSearchParams` itself, so a component using it needs no `<Suspense>` boundary and does not opt its page out of prerendering — PPR and `cacheComponents: true` included. Initial state comes from `searchParams` on the server and `window.location.search` on the client; later changes are tracked through the History API, which also catches a bare `history.pushState` from unrelated code. A prerendered page still renders defaults, since there is no query string at build time — render dynamically when a shared stateful link must be correct on first paint.
## Next.js App Router — server layout (Proxy workaround)
Server layouts don't receive `searchParams`. Expose the query string via a Proxy header (Next.js 16+; `middleware.ts` still works as a deprecated alias):
```typescript
// proxy.ts
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
export function proxy(request: NextRequest) {
const sp = (request.url.includes('_next') ? '' : request.url).split('?')[1] ?? '';
const headers = new Headers(request.headers);
headers.set('searchParams', sp);
return NextResponse.next({ request: { headers } });
}
```
```typescript
// app/jobs/layout.tsx
import { headers } from 'next/headers';
import { decodeState } from 'state-in-url/encodeState';
import { JOBS_STATE } from 'features/jobs/jobsState';
export default async function Layout({ children }: { children: React.ReactNode }) {
const sp = (await headers()).get('searchParams') ?? '';
const initial = decodeState(sp, JOBS_STATE);
return <>{/* use `initial` */}{children}</>;
}
```
## Input handling — instant feedback, deferred URL write
For text inputs and other fast-changing controls, split `setState` (instant render) and `setUrl` (URL flush):
```typescript
<input
value={urlState.q}
onChange={(e) => setState({ q: e.target.value })}
onBlur={() => setUrl()}
/>
```
`setUrl()` with no args flushes the current state to the URL with content-based diffing — safe to call repeatedly.
## React Router and Remix
API is identical to the Next.js variant; only the import path and `NavigateOptions` differ.
```typescript
import { useUrlState } from 'state-in-url/react-router'; // RR v7
import { useUrlState } from 'state-in-url/react-router6'; // RR v6 (since 6.0.0)
import { useUrlState } from 'state-in-url/remix'; // Remix v2
setUrl({ tab: 'b' }, { replace: false, preventScrollReset: true });
```
## Astro
Same API inside a React (or Preact `compat: true`) island. Pass `Object.fromEntries(Astro.url.searchParams)` as the island's `searchParams` prop so the server render matches the URL — a plain object, `URLSearchParams` serializes to `{}`. The page must render on demand (`output: 'server'` or `export const prerender = false`, with an adapter). Islands sharing one default-state object share state; there is no client router, so `replace` defaults to `true`.
```astro
---
import { Filters } from '../components/Filters';
const searchParams = Object.fromEntries(Astro.url.searchParams);
---
<Filters client:load searchParams={searchParams} />
```
```typescript
import { useUrlState } from 'state-in-url/astro';
export function Filters({ searchParams }: { searchParams?: Record<string, string> }) {
const { urlState, setUrl } = useUrlState(FILTERS_STATE, { searchParams });
...
}
```
No client framework at all: `decodeState(Astro.url.searchParams, FILTERS_STATE)` and `encodeState(next, FILTERS_STATE, Astro.url.searchParams)` from `state-in-url/encodeState` do the same job in the frontmatter, for links.
## react-hook-form integration
Share one defaults object, hydrate `useForm` from `urlState`, push form changes back via RHF `subscribe()` (not `watch()`):
```typescript
const { urlState, setUrl } = useUrlState(FILTERS_STATE, { searchParams });
const form = useForm<FiltersState>({ defaultValues: urlState });
React.useEffect(() => {
const sub = form.subscribe({
formState: { values: true },
callback: ({ values }) => setUrl(values),
});
return () => sub();
}, [form, setUrl]);
```
## `useSharedState` — cross-component state without URL
```typescript
import { useSharedState } from 'state-in-url';
const CART_STATE: CartState = { items: [], isOpen: false };
const { state, setState } = useSharedState(CART_STATE);
```
Same module-scoped default-state-identity rule. Use only when URL sync is explicitly NOT wanted.
## Common mistakes (CRITICAL — fix any agent generating these)
| Mistake | Fix |
|---|---|
| `defaultState` defined inside a React component | Move to a module-scoped `const`, annotated with a `type` |
| `interface FeatureState { ... }` for state shape | Use `type FeatureState = { ... }` |
| `setUrl` inside `useEffect` with `urlState` in deps | Causes infinite loop; gate on the actual change or derive on read |
| Two components each declaring `const DEFAULTS = {...}` | Export one const from a shared file; components import the hook |
| `setUrl`/`setState` called during render | Move into a handler or effect |
| `interface` instead of `type` | Same as above; `type` is required |
| Reading `searchParams` in a Next.js server layout | Use the Proxy + `headers()` + `decodeState` pattern |
| Using `state-in-url/next` in Pages Router | Not supported; use App Router or build a custom hook with `useUrlStateBase` |
| Importing `state-in-url/react-router` in a RR v6 project | Use `state-in-url/react-router6` (moved in 6.0.0) |
| `setUrl({...})` for fast typing inputs | Use `setState` on change, `setUrl()` on blur |
| Mutating `urlState` directly | Always go through `setState`/`setUrl` |
| Storing functions / `BigInt` / `Symbol` / `Map` / `Set` | Not serializable; use plain JSON-compatible values (Date is OK) |
| Storing tokens / API keys / passwords / PII in URL | Use auth storage; URL state is fully public |
## Documentation resources
- Next.js App Router guide (searchParams forwarding, layouts, history, FAQ): https://state-in-url.dev/nextjs — Markdown at https://state-in-url.dev/nextjs.md
- Comparison with nuqs, TanStack Router, use-query-params: https://state-in-url.dev/vs/nuqs — Markdown at https://state-in-url.dev/vs/nuqs.md
- Full README (humans): https://github.com/asmyshlyaev177/state-in-url/blob/master/README.md
- URL size limits: https://github.com/asmyshlyaev177/state-in-url/blob/master/Limits.md
- Working examples per framework: `packages/example-nextjs{14,15,16}`, `packages/example-react-router{6,7}`, `packages/example-remix2`, `packages/example-astro`
- JSDoc comments are available in IDE for all exported functions
## Contact
- Issues and bug reports: https://github.com/asmyshlyaev177/state-in-url/issues
- Discussions: https://github.com/asmyshlyaev177/state-in-url/discussions
- Maintainer: Aleksandr Smyshliaev (asmyshlyaev177) — https://asmyshlyaev177.dev
## AI Discovery Files
- ai.txt (what AI systems may and may not do with this content): https://state-in-url.dev/ai.txt
- ai.json (the same policy, machine-parseable): https://state-in-url.dev/ai.json
- identity.json (canonical project identity): https://state-in-url.dev/identity.json
- brand.txt (correct naming and terminology): https://state-in-url.dev/brand.txt
- faq-ai.txt (direct answers to common questions): https://state-in-url.dev/faq-ai.txt
- developer-ai.txt (exports, API surface, constraints): https://state-in-url.dev/developer-ai.txt
- robots-ai.txt (AI crawler directives): https://state-in-url.dev/robots-ai.txt
Sites with a close AI-readiness score — compare how they set up llms.txt and markup.