# Blog category (https://docs.salla.dev/twilight/react/routes/blog-category)



<Callout title="Framework: TanStack Start">
  This page is written for TanStack Start, the default and currently supported framework. [See what depends on the framework](https://docs.salla.dev/twilight/react/start/frameworks).
</Callout>

The customer sees the articles of one blog category under its name, with the sidebar of all categories beside them. More articles load as they scroll. Customise this route to change the list layout or the title.

## URL [#url]

| URL                            | Example              |
| ------------------------------ | -------------------- |
| `/blog/$slug/c-{$id}`          | `/blog/news/c-12`    |
| `/{locale}/blog/$slug/c-{$id}` | `/ar/blog/news/c-12` |

### Params [#params]

| Name     | Type                  | Example | Description                                             |
| -------- | --------------------- | ------- | ------------------------------------------------------- |
| `slug`   | `string`              | `news`  | For readable URLs. The loader never reads it.           |
| `id`     | `string`              | `12`    | The category id. The `c-` is part of the URL.           |
| `locale` | `string \| undefined` | `ar`    | Optional ([locale prefix](https://docs.salla.dev/twilight/react/routes/home#locale-prefix)). |

### Search params [#search-params]

None. Paging is not in the URL.

## Route file [#route-file]

The engine writes this into `app/routes/blog_.$slug.c-$id.tsx`.

```tsx title="app/routes/blog_.$slug.c-$id.tsx"
// @auto-generated
import { createFileRoute } from '@tanstack/react-router';
import { BlogCategoryRoute } from '@salla.sa/twilight-theme-engine/routes/blog';
import type { BlogCategoryLoaderData } from '@salla.sa/twilight-theme-engine/routes/blog';
import { BlogSkeleton } from '@salla.sa/twilight-theme-engine/skeleton';
import { withHead } from '@salla.sa/twilight-theme-engine/tanstack';

/**
 * BlogCategoryRoute route configuration.
 * Loads page data via loader and renders the BlogCategoryRoute component.
 */
export const Route = createFileRoute('/{-$locale}/blog/$slug/c-{$id}')({
  loader: ({ params }): Promise<BlogCategoryLoaderData> =>
    BlogCategoryRoute.loader({ params: { slug: params.slug, id: params.id }, locale: params.locale }),
  head: withHead(BlogCategoryRoute),
  pendingComponent: () => <BlogSkeleton />,
  component: BlogCategoryRouteComponent,
});

function BlogCategoryRouteComponent() {
  const data: BlogCategoryLoaderData = Route.useLoaderData();
  return <BlogCategoryRoute.Component {...data} />;
}
```

Delete the `// @auto-generated` line to own the file. Keep the path in `createFileRoute`.

### A customised version [#a-customised-version]

This keeps the engine's loader, head and skeleton and draws a plain list. A list that renders only `articles` shows only the first batch (see the edge cases).

```tsx title="app/routes/blog_.$slug.c-$id.tsx"
import { createFileRoute } from '@tanstack/react-router';
import { BlogCategoryRoute } from '@salla.sa/twilight-theme-engine/routes/blog';
import type { BlogCategoryLoaderData } from '@salla.sa/twilight-theme-engine/routes/blog';
import { BlogSkeleton } from '@salla.sa/twilight-theme-engine/skeleton';
import { withHead } from '@salla.sa/twilight-theme-engine/tanstack';
import { HookSlot } from '@salla.sa/twilight-theme-engine/hooks';
import { Breadcrumb, Link, NoContent } from '@salla.sa/twilight-theme-engine/components/common';

export const Route = createFileRoute('/{-$locale}/blog/$slug/c-{$id}')({
  loader: ({ params }): Promise<BlogCategoryLoaderData> =>
    BlogCategoryRoute.loader({ params: { slug: params.slug, id: params.id }, locale: params.locale }),
  head: withHead(BlogCategoryRoute),
  pendingComponent: () => <BlogSkeleton />,
  component: CategoryPage,
});

function CategoryPage() {
  const { page, blogCategory, articles }: BlogCategoryLoaderData = Route.useLoaderData();
  return (
    <div className="container">
      <Breadcrumb page={page} />
      <HookSlot name="blog:category.start" />
      <h1>{blogCategory.name}</h1>
      {articles.length === 0 ? (
        <NoContent icon="sicon-inbox" message="No articles in this category" />
      ) : (
        <ul>
          {articles.map((a) => (
            <li key={a.id}>
              <Link to={a.url}>{a.name}</Link>
            </li>
          ))}
        </ul>
      )}
      <HookSlot name="blog:category.end" />
    </div>
  );
}
```

## What it loads [#what-it-loads]

`BlogCategoryRoute.loader` returns `BlogCategoryLoaderData`:

| Field          | Type               | What it is                                                           |
| -------------- | ------------------ | -------------------------------------------------------------------- |
| `page`         | `Page`             | `title` and `id` of the category; breadcrumbs Home, Blog, category   |
| `blogCategory` | `BlogCategory`     | `id`, `name`, `url`, optional `meta` (title, description, canonical) |
| `categories`   | `BlogCategory[]`   | All categories, for the sidebar                                      |
| `articles`     | `ArticleSummary[]` | The first batch of articles                                          |
| `cursor`       | `Pagination`       | `next`, `previous`, `current`                                        |

It makes two requests at once: this category's articles and the blog overview (for the sidebar), through the [blog API module](https://docs.salla.dev/twilight/react/api/modules/blog). The first request is rendered on the server. Further batches are fetched only in the browser.

[Hook slots](https://docs.salla.dev/twilight/react/guide/hooks-and-slots): `blog:category.start`, `blog:category.end`.

## Head and SEO [#head-and-seo]

`BlogCategoryRoute.head` uses `blogCategory.meta.title` (else the name), `meta.description` (else the English text `Articles in {name}`), `meta.canonical` (else `blogCategory.url`) and hreflang links. See [SEO and head](https://docs.salla.dev/twilight/react/guide/seo-and-head).

## Edge cases [#edge-cases]

* **More articles than one batch.** When `cursor.next` is set, the stock page loads the next batch as the customer scrolls, in the browser only, so the server HTML has the first batch. A custom page must use the same cursor, or the extra articles never show.
* **Empty category.** The page shows the "no articles" state and no heading.
* **Unknown category id.** The loader throws a not-found error, so the customer sees the [not-found page](https://docs.salla.dev/twilight/react/routes/not-found) instead of a server error.
* **Wrong `$slug`.** Ignored, no redirect. The canonical link points at `blogCategory.url`.
* **Locale.** The `locale` you pass to the loader is not used; the language follows the request.

## See also [#see-also]

* [Blog](https://docs.salla.dev/twilight/react/routes/blog), [Blog tag](https://docs.salla.dev/twilight/react/routes/blog-tag), [Blog author](https://docs.salla.dev/twilight/react/routes/blog-author)
* [Fork a built-in route](https://docs.salla.dev/twilight/react/recipes/fork-a-built-in-route)
* [Routes overview](https://docs.salla.dev/twilight/react/routes)
