# Your storefront

Run a shop on the home store in a few minutes, with the search box, listing and product pages of the React components.

From an empty folder to a shop with search, filters and product pages, in six files. You need the stack with the home store from the [Quickstart](/docs/quickstart), and every command runs from the repository root.

The two packages, `@orbsearch/client` and `@orbsearch/react`, are not on npm yet. So the shop lives inside the repository's pnpm workspace, as the demo storefront in `apps/demo` does, and uses React Router.

### Create the app

Make the folder `apps/shop` with its manifest and its Vite config:

```json title="apps/shop/package.json"
{
    "name": "shop",
    "private": true,
    "type": "module",
    "dependencies": {
        "@orbsearch/client": "workspace:*",
        "@orbsearch/react": "workspace:*",
        "@react-router/node": "catalog:",
        "isbot": "catalog:",
        "react": "catalog:",
        "react-dom": "catalog:",
        "react-router": "catalog:"
    },
    "devDependencies": {
        "@react-router/dev": "catalog:",
        "@types/react": "catalog:",
        "@types/react-dom": "catalog:",
        "vite-plus": "catalog:"
    }
}
```

```ts title="apps/shop/vite.config.ts"
import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite-plus";

export default defineConfig({ plugins: [reactRouter()] });
```

`catalog:` takes the versions the whole workspace uses.

### Make a client per page load

Every search of one page load names the same page view, which [Insights](/docs/reference/glossary#insights) counts queries by. So the shop makes a client per request on the server, and one in the browser:

```ts twoslash title="apps/shop/app/search.ts"
import { createSearch } from "@orbsearch/client";

const endpoint = "http://127.0.0.1:7800";

// One client per page load: the server's for each request, the browser's with the page view the server named.
export function searchFor(pageView: string, userAgent?: string | null) {
    return createSearch({ endpoint, pageView, ...(userAgent && { userAgent }) });
}
```

The browser asks the same address for suggestions and "Load more", so it must reach the [Query API](/docs/reference/glossary#query-api) too. It needs no key.

### Answer every path

One route answers every path. It asks `resolve` what the path is, a listing, a product, a redirect or nothing, and renders the page for it:

```ts title="apps/shop/app/routes.ts"
import { route, type RouteConfig } from "@react-router/dev/routes";

export default [route("*", "routes/shop.tsx")] satisfies RouteConfig;
```

```tsx title="apps/shop/app/routes/shop.tsx"
import { isListing, locationOf } from "@orbsearch/client";
import { ListingPage, ProductPage, listingView } from "@orbsearch/react";
import { data, redirect, useLoaderData, type LoaderFunctionArgs } from "react-router";
import { searchFor } from "../search.ts";

export async function loader({ request, url }: LoaderFunctionArgs) {
    const pageView = crypto.randomUUID();
    const search = searchFor(pageView, request.headers.get("user-agent"));
    const page = await search.resolve(url.pathname + url.search, { locale: "de" }, { signal: request.signal });
    const status = { status: page.status };

    if (isListing(page)) {
        const listing = listingView(page, { path: url.pathname, locale: "de" });
        return data({ kind: "listing" as const, listing, pageView }, status);
    }
    if (page.product) {
        return data({ kind: "product" as const, product: page.product, path: url.pathname, pageView }, status);
    }
    const location = locationOf(page);
    if (location !== null) throw redirect(location, page.status);
    throw data(null, status);
}

export default function Shop() {
    const shown = useLoaderData<typeof loader>();
    if (shown.kind === "product") {
        return <ProductPage product={shown.product} trail={[{ title: shown.product.title, href: shown.path }]} />;
    }
    return <ListingPage listing={shown.listing} />;
}
```

The loader keeps the status `resolve` gives: a moved URL answers 301, a URL removed on purpose 410, an unknown one 404. A shop's own pages, such as its cart, come before the catch-all in `routes.ts`.

### Lay out the root

The root holds the provider and the search box on every page, and loads the components' styles:

```tsx title="apps/shop/app/root.tsx"
import { SearchBox, StorefrontProvider } from "@orbsearch/react";
import "@orbsearch/react/orbsearch.css";
import { useMemo, type ReactNode } from "react";
import { Links, Meta, Outlet, Scripts, useLocation, useNavigate, useRouteLoaderData } from "react-router";
import type { loader } from "./routes/shop.tsx";
import { searchFor } from "./search.ts";

export function Layout({ children }: { children: ReactNode }) {
    const navigate = useNavigate();
    const location = useLocation();
    const shown = useRouteLoaderData<typeof loader>("routes/shop");
    const pageView = shown?.pageView;
    // The browser's searches count as the page load the server answered.
    const search = useMemo(() => searchFor(pageView ?? crypto.randomUUID()), [pageView]);
    const listing = shown?.kind === "listing" ? shown.listing : undefined;

    return (
        <html lang="de">
            <head>
                <meta charSet="utf-8" />
                <meta name="viewport" content="width=device-width, initial-scale=1" />
                <Meta />
                <Links />
            </head>
            <body>
                <StorefrontProvider
                    search={search}
                    locale="de"
                    navigate={(href, { replace, keepScroll } = {}) =>
                        void navigate(href, {
                            ...(replace && { replace }),
                            ...(keepScroll && { preventScrollReset: true }),
                        })
                    }
                >
                    <header>
                        <SearchBox action="/suche" query={listing?.from ?? listing?.query ?? ""} at={location.key} />
                    </header>
                    <main>{children}</main>
                </StorefrontProvider>
                <Scripts />
            </body>
        </html>
    );
}

export default function App() {
    return <Outlet />;
}
```

`/suche` is the home store's search page (suche: search). `navigate` hands every filter, chip and sort to React Router, so a choice is a navigation to the next URL.

### Start the shop

Install the workspace's new member and start it:

```sh title="Terminal"
vp install
vp -C apps/shop dev
```

Open [localhost:5173/suche?q=graues sofa](http://localhost:5173/suche?q=graues%20sofa). The search for "graues Sofa" (grey sofa) lands on the sofa page `/wohnen/sofas` with grey chosen, two sofas, the banner the shop's rule places and a filter bar. The home store's photos point at a placeholder host, so their frames stay empty.

## What you have

- **Every page renders on the server,** so it arrives with its results and works before the script loads. Search, filters, sort and paging are links and GET forms underneath.
- **Every URL is the merchant's.** A category, a product and the search page sit at the paths the catalog and the release give them ([URLs and redirects](/docs/urls)).
- **Clicks and views are reported** for Insights by the components themselves ([Clicks and views](/docs/clicks-and-views)).

`listingView` reads `resolve`'s answer into the view `ListingPage` draws. What only your shop knows, such as your home in the trail and your category navigation, is empty in it and yours to set ([React components](/docs/react#the-two-pages)).

## Next

- [Customize](/docs/customize): give the shop your look and your own parts.
- [React components](/docs/react): every part and what it takes.
- [Recipes](/docs/recipes): one facet in a layout of your own, filters in a sheet on a phone.
