Blog
The blog home page, with a slider of featured articles, a category sidebar and the latest articles.
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 featured articles in a slider, the categories in a sidebar and the latest articles in a grid. It leads to the other four blog pages: article, category, tag and author. Customise this route to change the layout of the blog home or its title.
URL
| URL | Example |
|---|---|
/blog | /blog |
/{locale}/blog | /ar/blog |
The only param is the optional locale (locale prefix). There are no search params.
Try it
This is the blog as the engine renders it, running against the demo store. Switch the language or the width.
/ar/blogOpen/{locale}/blog- locale = ar
- path = /blog
Route file
The engine writes this file into app/routes/blog.tsx.
// @auto-generated
import { createFileRoute } from '@tanstack/react-router';
import { Blog } from '@salla.sa/twilight-theme-engine/routes/blog';
import type { BlogPageProps } 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';
/**
* Blog route configuration.
* Loads page data via loader and renders the Blog component.
*/
export const Route = createFileRoute('/{-$locale}/blog')({
loader: ({ params }): Promise<BlogPageProps> => Blog.loader({ locale: params.locale }),
head: withHead(Blog),
pendingComponent: () => <BlogSkeleton />,
component: BlogComponent,
});
function BlogComponent() {
const data: BlogPageProps = Route.useLoaderData();
return <Blog.Component {...data} />;
}To take the page over, delete the // @auto-generated line. Keep the createFileRoute path as it is. Blog gives you the loader, head and stock Component, so you can change any one of them.
A customised version
This keeps the engine's loader and skeleton, adds a suffix to the title and draws a plain list. The stock page's hook slots are kept so merchant apps still work.
import { createFileRoute } from '@tanstack/react-router';
import { Blog } from '@salla.sa/twilight-theme-engine/routes/blog';
import type { BlogPageProps } 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')({
loader: ({ params }): Promise<BlogPageProps> => Blog.loader({ locale: params.locale }),
head: withHead(Blog, (result) => ({ ...result, title: `${result.title} | Journal` })),
pendingComponent: () => <BlogSkeleton />,
component: JournalPage,
});
function JournalPage() {
const { page, articles }: BlogPageProps = Route.useLoaderData();
return (
<div className="container">
<Breadcrumb page={page} />
<HookSlot name="blog:start" />
<h1>{page.title}</h1>
{articles.length === 0 ? (
<NoContent icon="sicon-inbox" message="No articles yet" />
) : (
<ul>
{articles.map((a) => (
<li key={a.id}>
<Link to={a.url}>{a.name}</Link>
</li>
))}
</ul>
)}
<HookSlot name="blog:end" />
</div>
);
}What it loads
Blog.loader returns BlogPageProps:
| Field | Type | What it is |
|---|---|---|
page | Page | title (the translated "Blog") and breadcrumbs (Home, Blog) |
articles | ArticleSummary[] | The latest articles |
categories | BlogCategory[] | Sidebar categories |
slides | ArticleSummary[] | Featured articles for the slider |
popular | ArticleSummary[] | Popular articles |
All four lists come from one request in the blog API module. The first request is rendered on the server, so the HTML already has the articles. The loader accepts an optional second argument, extend(data, ctx), to merge your own fields into the data.
Hook slots: blog:start, blog:items.start, blog:items.end, blog:end.
Head and SEO
Blog.head sets the title and adds hreflang links for every store language. No description is set; add one with withHead(Blog, (result) => ({ ...result, description: '...' })). See SEO and head.
Edge cases
- The blog API fails. The loader returns empty lists, so the page shows its "no articles" state instead of an error page. Handle
articles.length === 0in a replacement component. - No featured slides. The slider is not rendered and a plain heading is shown. With neither slides nor articles, the page shows the empty state.
- One batch only. Loading more articles happens on the category, tag and author pages.
- Locale. The
localeyou pass to the loader is not used; the content language follows the request, so/ar/blogreturns Arabic content.