Blog category
The articles of one blog category, with a sidebar of all categories and load-more on scroll.
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 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 | Example |
|---|---|
/blog/$slug/c-{$id} | /blog/news/c-12 |
/{locale}/blog/$slug/c-{$id} | /ar/blog/news/c-12 |
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). |
Search params
None. Paging is not in the URL.
Route file
The engine writes this into 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
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).
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
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. The first request is rendered on the server. Further batches are fetched only in the browser.
Hook slots: blog:category.start, blog:category.end.
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.
Edge cases
- More articles than one batch. When
cursor.nextis 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 instead of a server error.
- Wrong
$slug. Ignored, no redirect. The canonical link points atblogCategory.url. - Locale. The
localeyou pass to the loader is not used; the language follows the request.