# The config in the URL

Everything a viewer can change is one `WrappedConfig`, and the whole of it fits in a query string. Put it there and the link is the video: whoever opens it sees the same palette, format, order, edits and transitions as the person who shared it. Three functions do the work, and none of them needs a router.

> Live preview: try it at https://bettertyped.github.io/react-wrapped/docs/guides/config-in-the-url/
>
> Change a setting and read the query. Or type a query and open it.

## Three functions

| Function | What it does |
| --- | --- |
| `parseWrappedSearch(input)` | Reads whatever a router or `URLSearchParams` hands over into a `WrappedSearch`. Anything unexpected becomes "not set". It never throws. |
| `configFromSearch(search, defaults?)` | Turns a `WrappedSearch` into a full `WrappedConfig`, with your defaults for everything the link leaves out. |
| `searchFromConfig(config, defaults?)` | The way back: a `WrappedSearch` with every key that is at its default left out. |

`link-is-the-video.tsx`

```tsx
import { type CSSProperties, useState } from "react";

import {
  configFromSearch,
  DEFAULT_WRAPPED_CONFIG,
  parseWrappedSearch,
  searchFromConfig,
  type WrappedConfig,
} from "@react-wrapped/core";
import { WrappedConfigurator } from "@react-wrapped/player";

/** The config as a query string. A setting at its default is left out, so most links are short. */
export function toQuery(config: WrappedConfig): string {
  const params = new URLSearchParams();
  for (const [key, value] of Object.entries(searchFromConfig(config))) {
    if (value !== undefined) params.set(key, String(value));
  }
  return params.toString();
}

/** A query string back into a config. Anything in it that makes no sense falls back to the default. */
export function fromQuery(query: string): WrappedConfig {
  return configFromSearch(parseWrappedSearch(Object.fromEntries(new URLSearchParams(query))));
}

const PANEL: CSSProperties = { maxWidth: 420 };

export function LinkIsTheVideo() {
  const [config, setConfig] = useState<WrappedConfig>(DEFAULT_WRAPPED_CONFIG);
  const [pasted, setPasted] = useState("p=grape&m=light&f=square&s=1.5");
  const query = toQuery(config);
  return (
    <div>
      <div style={PANEL}>
        <WrappedConfigurator config={config} onChange={setConfig} />
      </div>
      <output>{query ? `?${query}` : "Nothing to add: every setting is at its default."}</output>
      <div role="group" aria-label="Open a link">
        <input
          type="text"
          aria-label="A query string to open"
          value={pasted}
          onChange={(event) => setPasted(event.target.value)}
        />
        <button type="button" onClick={() => setConfig(fromQuery(pasted))}>
          Open this link
        </button>
      </div>
    </div>
  );
}
```

## The short keys

A `WrappedSearch` uses one letter per setting, so a link stays readable when someone pastes it into a chat.

| Key | Type | What it carries |
| --- | --- | --- |
| `p?` | `string` | `palette`: the palette's id. |
| `a?` | `string` | `accent`: six hex digits without the hash. Only with the custom palette. |
| `m?` | `"dark" \| "light"` | `mode`. |
| `f?` | `"story" \| "square" \| "landscape"` | `format`. |
| `c?` | `"round" \| "square" \| "circle"` | `shape`, the calendar's cells. |
| `s?` | `"0.75" \| "1" \| "1.5"` | `speed`. |
| `t?` | `string` | `cuts`: picked transitions as `scene:transition` pairs with commas between them. |
| `h?` | `string` | `hidden`: scene keys with commas between them. |
| `o?` | `string` | `order`: scene keys with commas between them. |
| `e?` | `string` | `edits`: the rewritten texts as one JSON object. |
| `g?` | `0` | `grain`: there only when the grain is off. |

```bash
?p=grape&m=light&f=square&t=total:drip,week:iris&h=languages
```

## What is left out

- A key at its default is not written. A video nobody changed has no query at all.
- `a` is written only with the custom palette. `g` is written only as `0`, when the grain is off.
- The scene editor stores only texts that differ from the original and only a transition that differs from the seed's pick, so `e` and `t` stay short too.
- Pass the same `defaults` to `configFromSearch` and `searchFromConfig`. Then "left out" means the same thing both ways.

## A mangled link still plays

- Every value is checked: the format against the three formats, the accent against six hex digits, each text against a length limit.
- A transition the page does not know is dropped and that scene plays the seed's pick. Scene keys that no longer exist are ignored.
- Edits that are not a flat object of short strings are dropped as a whole.
- Palette ids that were renamed still open as what they were.

## In the address bar

With no router, a small hook reads the link once and writes it back on every change. `WrappedView` and every other part take the pair it returns as `config` and `onConfigChange`.

`use-url-config.ts`

```ts
import { useCallback, useEffect, useState } from "react";

import {
  configFromSearch,
  DEFAULT_WRAPPED_CONFIG,
  parseWrappedSearch,
  searchFromConfig,
  type WrappedConfig,
} from "@react-wrapped/core";

/** The config, kept in the address bar: read from the link when the page opens, written back on every change. */
export function useUrlConfig(defaults: WrappedConfig = DEFAULT_WRAPPED_CONFIG) {
  const [config, setConfig] = useState(defaults);

  // After the first paint: the server does not know the query string of a static page, so both render the defaults.
  useEffect(() => {
    const query = Object.fromEntries(new URLSearchParams(window.location.search));
    setConfig(configFromSearch(parseWrappedSearch(query), defaults));
  }, [defaults]);

  const change = useCallback(
    (next: WrappedConfig) => {
      setConfig(next);
      const params = new URLSearchParams();
      // The same defaults both ways, so "left out" means the same thing when the link is read again.
      for (const [key, value] of Object.entries(searchFromConfig(next, defaults))) {
        if (value !== undefined) params.set(key, String(value));
      }
      const query = params.toString();
      // Replaced, not pushed: trying six palettes should not take six presses of the back button to undo.
      window.history.replaceState(null, "", `${window.location.pathname}${query ? `?${query}` : ""}`);
    },
    [defaults],
  );

  return [config, change] as const;
}
```

With a router, `parseWrappedSearch` is shaped to be a route's search validator: it takes the raw search object and returns a typed one. Read the route's search, pass it through `configFromSearch`, and on change navigate with `searchFromConfig(next)` and `replace`.

> A config in state is fine too. The docs examples keep it there. Put it in the URL when the page is someone's own and they will share it.

## Related

- [WrappedConfigurator](https://bettertyped.github.io/react-wrapped/docs/player/configurator/)
- [Timeline](https://bettertyped.github.io/react-wrapped/docs/player/timeline/)
- [Server rendering](https://bettertyped.github.io/react-wrapped/docs/guides/server-rendering/)
- [Custom transition](https://bettertyped.github.io/react-wrapped/docs/guides/custom-transition/)
- [Your first Wrapped](https://bettertyped.github.io/react-wrapped/docs/first-wrapped/)

Source: https://bettertyped.github.io/react-wrapped/docs/guides/config-in-the-url/
