Blog tag
The articles carrying one blog tag, with a sidebar of 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 that carry one tag, under the tag's name, and more load as they scroll. Tag links appear on articles and article cards. Customise this route to change the list layout or the title. This is the blog tag; products have their own tag page.
URL
| URL | Example |
|---|---|
/blog/$slug/tag-{$id} | /blog/launch/tag-7 |
/{locale}/blog/$slug/tag-{$id} | /ar/blog/launch/tag-7 |
Params
| Name | Type | Example | Description |
|---|---|---|---|
slug | string | launch | For readable URLs. The loader never reads it. |
id | string | 7 | The tag id. The tag- is part of the URL. |
locale | string | undefined | ar | Optional (locale prefix). |
Product tags live at /$slug/tag-{$id}, without the /blog prefix.
Search params
None. Paging is not in the URL.
Route file
The engine writes this into app/routes/blog_.$slug.tag-$id.tsx.
// @auto-generated
import { createFileRoute } from '@tanstack/react-router';
import { BlogTagRoute } from '@salla.sa/twilight-theme-engine/routes/blog';
import type { BlogTagLoaderData } 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';
/**
* BlogTagRoute route configuration.
* Loads page data via loader and renders the BlogTagRoute component.
*/
export const Route = createFileRoute('/{-$locale}/blog/$slug/tag-{$id}')({
loader: ({ params }): Promise<BlogTagLoaderData> =>
BlogTagRoute.loader({ params: { slug: params.slug, id: params.id }, locale: params.locale }),
head: withHead(BlogTagRoute),
pendingComponent: () => <BlogSkeleton />,
component: BlogTagRouteComponent,
});
function BlogTagRouteComponent() {
const data: BlogTagLoaderData = Route.useLoaderData();
return <BlogTagRoute.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 of the first batch.
import { createFileRoute } from '@tanstack/react-router';
import { BlogTagRoute } from '@salla.sa/twilight-theme-engine/routes/blog';
import type { BlogTagLoaderData } 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/tag-{$id}')({
loader: ({ params }): Promise<BlogTagLoaderData> =>
BlogTagRoute.loader({ params: { slug: params.slug, id: params.id }, locale: params.locale }),
head: withHead(BlogTagRoute),
pendingComponent: () => <BlogSkeleton />,
component: TagPage,
});
function TagPage() {
const { page, blogTag, articles }: BlogTagLoaderData = Route.useLoaderData();
return (
<div className="container">
<Breadcrumb page={page} />
<HookSlot name="blog:tag.start" />
<h1>#{blogTag.name}</h1>
{articles.length === 0 ? (
<NoContent icon="sicon-inbox" message="No articles with this tag" />
) : (
<ul>
{articles.map((a) => (
<li key={a.id}>
<Link to={a.url}>{a.name}</Link>
</li>
))}
</ul>
)}
<HookSlot name="blog:tag.end" />
</div>
);
}What it loads
BlogTagRoute.loader returns BlogTagLoaderData:
| Field | Type | What it is |
|---|---|---|
page | Page | title and id of the tag; breadcrumbs Home, Blog, tag |
blogTag | BlogTag | id (a number), name, url |
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 tag'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:tag.start, blog:tag.end.
Head and SEO
BlogTagRoute.head uses blogTag.name as the title, the English text Articles tagged with {name} as the description, blogTag.url as the canonical link, and hreflang links. The description is not translated; override it with withHead(BlogTagRoute, (result) => ({ ...result, description: '...' })). See SEO and head.
Edge cases
- More articles than one batch. When
cursor.nextis set, the stock page loads the next batches as the customer scrolls, in the browser only. A custom page that shows onlyarticlesshows only the first batch. - Empty tag. The page shows the "no articles" state and no heading.
- Unknown tag 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. - Not the product tag.
/blog/x/tag-7is a blog tag;/x/tag-7and/tags/7are product tags. - Locale. The
localeyou pass to the loader is not used; the language follows the request.