BlogAuthorRoute, BlogCategoryRoute, BlogTagRoute
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
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
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 withBlogSkeletonas 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 usesmeta.title ?? name,meta.descriptionandmeta.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.nextas the reader scrolls. Slots:blog:author.startand.end,blog:category.startand.end,blog:tag.startand.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>andArticles tagged with <name>. On an Arabic store, replace them withwithHead(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 theArticles in <name>fallback never appears. Fall back with||inwithHead's extend.On the author page,
page.idis the author's name, and loading more articles asks the API byauthor.namewhile the first page was asked by the id in the URL (packages/theme-engine/src/routes/blog/BlogAuthorPage.tsx).
Related
The blog home page with featured slides, categories and latest articles, rendering empty lists rather than an error when the blog fails to load.
BlogSingleOne blog article with its related articles, likes and comments, using the article's own SEO title, description and image when set.
Blog data typesThe shapes of blog articles, authors, categories and tags that the blog loaders return, for typing your own blog components.
Source and docs
- Engine source:
packages/theme-engine/src/routes/blog/index.tsx