Skip to content
Twilight React Playground
ثيم رائدaren

BlogAuthorRoute, BlogCategoryRoute, BlogTagRoute

route-moduleBeginnerserverbrowserlive demo

Blog article lists filtered by one author, one category or one tag, each with the category sidebar and more articles as the reader scrolls.

import { BlogAuthorRoute, BlogCategoryRoute, BlogTagRoute, BlogAuthorPage, BlogCategoryPage, BlogTagPage, blogAuthorLoader, blogCategoryLoader, blogTagLoader, BlogAuthorLoaderData, BlogCategoryLoaderData, BlogTagLoaderData } from '@salla.sa/twilight-theme-engine/routes/blog';

In plain words

Clicking an author's name, a blog category or a tag opens a list of the matching articles. The three pages work the same way: the loader fetches the first articles for that author, category or tag, plus the blog's categories for the sidebar, and the page loads more articles as the reader scrolls.

Reach them from the blog on Built-in pages.

Signature

const BlogAuthorRoute: {
  readonly id: 'blog.index.author';
  readonly loader: (ctx: { params: { id: string }; locale?: string }, extend?) => Promise<BlogAuthorLoaderData>;
  readonly head: (ctx: TwilightContext, data: BlogAuthorLoaderData) => HeadDescriptor;
  readonly Component: React.LazyExoticComponent<(props: BlogAuthorLoaderData) => JSX.Element>;
};
const BlogCategoryRoute: { id: 'blog.index.category'; loader(ctx: { params: { slug; id }; locale? }, extend?); head; Component };
const BlogTagRoute: { id: 'blog.index.tag'; loader(ctx: { params: { slug; id }; locale? }, extend?); head; Component };

const BlogAuthorPage / BlogCategoryPage / BlogTagPage;   // each module's Component
function blogAuthorLoader({ params }: { params: { id: string } }): Promise<BlogAuthorLoaderData>;
function blogCategoryLoader({ params }: { params: { slug: string; id: string } }): Promise<BlogCategoryLoaderData>;
function blogTagLoader({ params }: { params: { slug: string; id: string } }): Promise<BlogTagLoaderData>;

interface BlogAuthorLoaderData {
  page: Page;
  author: BlogAuthorType;          // BlogCategoryLoaderData: blogCategory; BlogTagLoaderData: blogTag
  categories: BlogCategoryType[];
  articles: ArticleSummary[];
  cursor: Pagination;              // cursor.next loads the next articles
}

Try it live

The first blog category of this store, drawn by BlogCategoryRoute.Component, and the head descriptor built for it.Try this: switch the view to head: with blank SEO fields the title and description are empty strings, so the "Articles in" fallback never shows.
Storefront canvas · en · LTR
Runs in the browser…
Controls
What a theme writes
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';

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} />;
}

Example

app/routes/blog_.author.$id.tsx (generated)
import { createFileRoute } from '@tanstack/react-router';
import { BlogAuthorRoute } from '@salla.sa/twilight-theme-engine/routes/blog';
import type { BlogAuthorLoaderData } 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';

export const Route = createFileRoute('/{-$locale}/blog/author/$id')({
  loader: ({ params }): Promise<BlogAuthorLoaderData> =>
    BlogAuthorRoute.loader({ params: { id: params.id }, locale: params.locale }),
  head: withHead(BlogAuthorRoute),
  pendingComponent: () => <BlogSkeleton />,
  component: BlogAuthorRouteComponent,
});

function BlogAuthorRouteComponent() {
  const data: BlogAuthorLoaderData = Route.useLoaderData();
  return <BlogAuthorRoute.Component {...data} />;
}

How it behaves

  • URLs: /{-$locale}/blog/author/$id, /{-$locale}/blog/$slug/c-{$id} and /{-$locale}/blog/$slug/tag-{$id}, each with BlogSkeleton as the pending component.

  • Each loader requests the articles for the author, category or tag and the blog overview in parallel; the overview is used only for the sidebar's categories. Errors are not caught.

  • head: the author page uses the author name as title; the category page uses meta.title ?? name, meta.description and meta.canonical ?? url; the tag page uses the tag name and its URL as canonical. All add hreflang alternates.

  • The pages load further articles in the browser from cursor.next as the reader scrolls. Slots: blog:author.start and .end, blog:category.start and .end, blog:tag.start and .end.

  • All three components are lazy: outside a route, wrap them in <Suspense>.

Gotchas

  • The author and tag descriptions are English literals whatever the page language: Articles by <name> and Articles tagged with <name>. On an Arabic store, replace them with withHead(Module, extend).

  • The category head falls back with ??, but the API sends a blank SEO field as an empty string (meta: { title: '', … }). A category whose SEO fields were left blank therefore gets no title, description or canonical of its own (the head adapter drops empty values), and the Articles in <name> fallback never appears. Fall back with || in withHead's extend.

  • On the author page, page.id is the author's name, and loading more articles asks the API by author.name while the first page was asked by the id in the URL (packages/theme-engine/src/routes/blog/BlogAuthorPage.tsx).

Related

Source and docs