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

URLExample
/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
Route
/{locale}/blog
This address becomes
  • locale = ar
  • path = /blog

Route file

The engine writes this file into app/routes/blog.tsx.

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.

app/routes/blog.tsx
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:

FieldTypeWhat it is
pagePagetitle (the translated "Blog") and breadcrumbs (Home, Blog)
articlesArticleSummary[]The latest articles
categoriesBlogCategory[]Sidebar categories
slidesArticleSummary[]Featured articles for the slider
popularArticleSummary[]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 === 0 in 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 locale you pass to the loader is not used; the content language follows the request, so /ar/blog returns Arabic content.

See also

On this page