Search
Product search results for the keyword in the `q` query parameter.
Framework: TanStack Start
This page is written for TanStack Start, the default and currently supported framework. See what depends on the framework.
The customer sees the products that match their keyword, in the same list layout as a category. Customise this route to show a loading state when the keyword changes, or change the head tags.
URL
| Pattern | Example |
|---|---|
/{-$locale}/search | /search?q=shirt · /ar/search?q=shirt |
Optional param: locale (locale prefix).
Search params
| Name | Type | Default | Description |
|---|---|---|---|
q | string | '' | The keyword. A missing or non-string value becomes ''. |
sort | string | none | The sort order chosen in the sort menu. |
page | number | 1 | Left out of the URL on page 1. The API pages only by its own cursor, so the loader ignores a number and the list starts from the first batch. |
Try it
Change the keyword and the language, or switch to a phone width. This is the real search route running against the demo store.
/ar/search?q=lightOpen/{-$locale}/search- locale = ar
- path = /search
- search.q = light
Route file
The engine writes this file into its own .twilight/ folder, not into app/routes/. Search has its own template: it reads q, sort and page from the URL.
// @auto-generated
import { createFileRoute } from '@tanstack/react-router';
import { ProductListing } from '@salla.sa/twilight-theme-engine/routes/product-listing';
import type { ProductListLoaderData } from '@salla.sa/twilight-theme-engine/routes/product-listing';
import { withHead } from '@salla.sa/twilight-theme-engine/tanstack';
export const Route = createFileRoute('/{-$locale}/search')({
validateSearch: (search: Record<string, unknown>) => {
const page = Number(search.page) || 1;
const sort = (search.sort as string) || undefined;
return {
q: (search.q as string) || '',
...(page > 1 ? { page } : {}),
...(sort ? { sort } : {}),
};
},
loaderDeps: ({ search }) => ({ q: search.q, page: search.page, sort: search.sort }),
loader: ({ deps, params }): Promise<ProductListLoaderData> =>
ProductListing.loader({
params: { source: 'search' },
search: { q: deps.q, page: deps.page, sort: deps.sort },
locale: params.locale,
}),
head: withHead(ProductListing),
pendingComponent: () => <ProductListing.Pending />,
component: ProductListingComponent,
});
function ProductListingComponent() {
const data: ProductListLoaderData = Route.useLoaderData();
return <ProductListing.Component {...data} />;
}ProductListing is shared with category, brand, tag and products. To replace only this route, add app/routes/search.tsx without the // @auto-generated line, and the build uses yours. To restyle every product list at once, register a page as product:list-page instead; see Replace the product list page.
A customised version
This keeps the same search params, shows a product-grid skeleton while the loader re-runs, and asks search engines not to index result pages.
import { createFileRoute } from '@tanstack/react-router';
import { ProductListing } from '@salla.sa/twilight-theme-engine/routes/product-listing';
import type { ProductListLoaderData } from '@salla.sa/twilight-theme-engine/routes/product-listing';
import { ProductGridSkeleton } from '@salla.sa/twilight-theme-engine/skeleton';
import { withHead } from '@salla.sa/twilight-theme-engine/tanstack';
export const Route = createFileRoute('/{-$locale}/search')({
validateSearch: (search: Record<string, unknown>) => {
const page = Number(search.page) || 1;
const sort = (search.sort as string) || undefined;
return { q: (search.q as string) || '', ...(page > 1 ? { page } : {}), ...(sort ? { sort } : {}) };
},
loaderDeps: ({ search }) => ({ q: search.q, page: search.page, sort: search.sort }),
loader: ({ deps, params }): Promise<ProductListLoaderData> =>
ProductListing.loader({
params: { source: 'search' },
search: { q: deps.q, page: deps.page, sort: deps.sort },
locale: params.locale,
}),
head: withHead(ProductListing, (result) => ({
...result,
robots: 'noindex, follow',
})),
pendingComponent: () => <ProductGridSkeleton />,
component: ListComponent,
});
function ListComponent() {
const data: ProductListLoaderData = Route.useLoaderData();
return <ProductListing.Component {...data} />;
}What it loads
ProductListLoaderData, shared by every product list:
| Field | Description |
|---|---|
page | title (the keyword), slug and breadcrumbs. |
source | { type: 'search', value: keyword }. |
query | sort (default ourSuggest) and filters: true. |
products | The first batch of products. |
pagination | next: the cursor for the next batch, or null. |
filters | The filters for the filter panel. |
The loader trims q and requests the matching products on the server. More products load in the browser as the customer scrolls. See Loading data.
Head and SEO
Title is the keyword and description is Browse <keyword>, with matching Open Graph and Twitter tags and hreflang links. There is no canonical tag.
Edge cases
- Empty
q. No request is made. The page shows the empty state with the translated "Search Results" title. - No matches. The same empty state, titled with the keyword.
- Sort and page. The route keeps
q,sortandpagein the URL, so the sort menu works. A reload starts the list from the first batch again, becausepageis not a cursor. - Request failure. There is no 404 for search: a failed request shows the error page.
- Locale. See locale prefix.