BlogSingle
One blog article with its related articles, likes and comments, using the article's own SEO title, description and image when set.
import { BlogSingle, BlogSinglePage, blogSingleLoader, BlogSinglePageProps } from '@salla.sa/twilight-theme-engine/routes/blog';In plain words
An article address looks like /blog/<slug>/a-<id>. BlogSingle.loader fetches that article, and the page shows it with a like button, comments and related articles.
When the merchant filled in the article's SEO fields, the page title, description and share image come from those fields instead of the article's name and text. Open an article from the blog on Built-in pages.
Signature
const BlogSingle: {
readonly id: 'blog.single';
readonly loader: (
ctx: { params: { slug: string; id: string }; locale?: string },
extend?: (data: BlogSinglePageProps, ctx: { params: { slug: string; id: string } }) => Record<string, unknown> | Promise<Record<string, unknown>>
) => Promise<BlogSinglePageProps>;
readonly head: (ctx: TwilightContext, data: BlogSinglePageProps) => HeadDescriptor;
readonly Component: React.LazyExoticComponent<(props: BlogSinglePageProps) => JSX.Element>;
};
const BlogSinglePage: typeof BlogSingle.Component;
function blogSingleLoader({ params }: { params: { slug: string; id: string } }): Promise<BlogSinglePageProps>;
interface BlogSinglePageProps {
page: Page;
article: ArticleDetail;
related?: ArticleSummary[]; // article.related
}Try it live
import { createFileRoute } from '@tanstack/react-router';
import { BlogSingle } from '@salla.sa/twilight-theme-engine/routes/blog';
import type { BlogSinglePageProps } 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/a-{$id}')({
loader: ({ params }): Promise<BlogSinglePageProps> =>
BlogSingle.loader({ params: { slug: params.slug, id: params.id }, locale: params.locale }),
// An empty SEO field would leave the page without its own title: fall back with ||.
head: withHead(BlogSingle, (result, _ctx, data) => ({
...result,
title: data.article.meta?.title || data.article.name,
description: data.article.meta?.description || data.article.description?.replace(/<[^>]+>/g, '').slice(0, 160),
canonical: data.article.meta?.canonical || data.article.url,
})),
pendingComponent: () => <BlogSkeleton />,
component: BlogSingleComponent,
});
function BlogSingleComponent() {
const data: BlogSinglePageProps = Route.useLoaderData();
return <BlogSingle.Component {...data} />;
}
Example
import { createFileRoute } from '@tanstack/react-router';
import { BlogSingle } from '@salla.sa/twilight-theme-engine/routes/blog';
import type { BlogSinglePageProps } 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/a-{$id}')({
loader: ({ params }): Promise<BlogSinglePageProps> =>
BlogSingle.loader({ params: { slug: params.slug, id: params.id }, locale: params.locale }),
head: withHead(BlogSingle),
pendingComponent: () => <BlogSkeleton />,
component: BlogSingleComponent,
});
function BlogSingleComponent() {
const data: BlogSinglePageProps = Route.useLoaderData();
return <BlogSingle.Component {...data} />;
}
How it behaves
URL:
/{-$locale}/blog/$slug/a-{$id}. The slug is passed on but only the id is used.head: titlearticle.meta.title ?? article.name, descriptionarticle.meta.description ?? article.description, canonicalarticle.meta.canonical ?? article.url, Open Graph title and image (meta.og_image ?? image), hreflang alternates. No Twitter tags.BlogSinglePageshows the like button and Salla's comments web component only when the store enablessettings.blog.allow_likes_and_comments. It usesuseBlogLikefor the button anduseCommentsto refresh the comments. Slots:blog:single.start,blog:single.end,blog:single.related.startandblog:single.related.end.In the article detail,
descriptionis the article's whole HTML body. It is rendered withdangerouslySetInnerHTML( replaced by spaces), and it is also whatheadfalls back to for the meta description.Lazy: outside a route, wrap
BlogSinglePagein<Suspense>.
Gotchas
A missing article does not show the 404 page.
blogSingleLoadercallsblog.articles.findwithoutorThrow, so the API's HTTP error reaches the router's default error component, which shows a 400 page (or a 500 page when the API answers with no article).SEO fields the merchant left blank arrive as empty strings (
meta: { title: '', description: '', canonical: '', og_image: '' }on the demo store), andheadfalls back with??, which keeps an empty string. The head adapter then drops every empty value, so such an article gets no title, description, canonical or share image of its own and keeps the store's title. Fall back with||inwithHead's extend, as the demo's code does.
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.
BlogAuthorRoute, BlogCategoryRoute, BlogTagRouteBlog article lists filtered by one author, one category or one tag, each with the category sidebar and more articles as the reader scrolls.
useBlogLikeLikes or unlikes a blog article through the Salla API, with a local like count and a per-browser memory of liked articles.
useCommentsA React key that goes up whenever a shopper posts a comment, so Salla's comments component reloads without a page refresh.
Source and docs
- Engine source:
packages/theme-engine/src/routes/blog/index.tsx