# Blog article (https://docs.salla.dev/twilight/react/routes/blog-article)



<Callout title="Framework: TanStack Start">
  This page is written for TanStack Start, the default and currently supported framework. [See what depends on the framework](https://docs.salla.dev/twilight/react/start/frameworks).
</Callout>

The customer reads one post: title, date, author, cover image, body, tags, likes, comments and a sidebar of related articles. Customise this route to change the article layout, keep the page's hook slots, or fix the 404 behaviour for unknown articles.

## URL [#url]

| URL                            | Example                     |
| ------------------------------ | --------------------------- |
| `/blog/$slug/a-{$id}`          | `/blog/launch-day/a-482`    |
| `/{locale}/blog/$slug/a-{$id}` | `/ar/blog/launch-day/a-482` |

### Params [#params]

| Name     | Type                  | Example      | Description                                                                                 |
| -------- | --------------------- | ------------ | ------------------------------------------------------------------------------------------- |
| `slug`   | `string`              | `launch-day` | For readable URLs. The loader never reads it.                                               |
| `id`     | `string`              | `482`        | The article id. The `a-` in front is part of the URL and is removed before the loader runs. |
| `locale` | `string \| undefined` | `ar`         | Optional ([locale prefix](https://docs.salla.dev/twilight/react/routes/home#locale-prefix)).                                     |

There are no search params.

### Try it [#try-it]

This is a blog article as the engine renders it, running against the demo store. Switch the language or the width.

<RoutePreview path="/blog/-/a-1673272609" pattern="/{locale}/blog/$slug/a-{$id}" />

## Route file [#route-file]

The engine writes this into `app/routes/blog_.$slug.a-$id.tsx`. The `blog_` prefix makes the page a sibling of `/blog`, not a child of it.

```tsx title="app/routes/blog_.$slug.a-$id.tsx"
// @auto-generated
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';

/**
 * BlogSingle route configuration.
 * Loads page data via loader and renders the BlogSingle component.
 */
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} />;
}
```

Delete the `// @auto-generated` line to own the file. Keep the path in `createFileRoute`.

### A customised version [#a-customised-version]

This replaces the component with a minimal article layout. It keeps the engine's loader and head, keeps two hook slots, and sets a `key` so state resets when you move from one article to the next.

```tsx title="app/routes/blog_.$slug.a-$id.tsx"
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';
import { HookSlot } from '@salla.sa/twilight-theme-engine/hooks';
import { Breadcrumb, Link } from '@salla.sa/twilight-theme-engine/components/common';

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: ArticlePage,
});

function ArticlePage() {
  const { page, article, related = [] }: BlogSinglePageProps = Route.useLoaderData();
  return (
    <article key={article.id} className="container">
      <Breadcrumb page={page} />
      <HookSlot name="blog:single.start" />
      <h1>{article.name}</h1>
      <p>
        By <Link to={article.author.url}>{article.author.name}</Link>
      </p>
      <div dangerouslySetInnerHTML={{ __html: article.description ?? '' }} />
      {article.tags?.map((tag) => (
        <Link key={tag.id} to={tag.url}>
          #{tag.name}
        </Link>
      ))}
      <HookSlot name="blog:single.end" />
      <ul>
        {related.map((r) => (
          <li key={r.id}>
            <Link to={r.url}>{r.name}</Link>
          </li>
        ))}
      </ul>
    </article>
  );
}
```

## What it loads [#what-it-loads]

`BlogSingle.loader` returns `BlogSinglePageProps`:

| Field     | Type               | What it is                                                                                                                 |
| --------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------- |
| `page`    | `Page`             | `title` and `id` of the article, breadcrumbs Home, Blog, article                                                           |
| `article` | `ArticleDetail`    | `id`, `name`, `description` (the HTML body), `image`, `url`, `author`, `created_at`, `stats`, `tags`, `categories`, `meta` |
| `related` | `ArticleSummary[]` | Copy of `article.related`                                                                                                  |

It makes one request through the [blog API module](https://docs.salla.dev/twilight/react/api/modules/blog), on the server for the first request and in the browser for later navigations.

Likes and comments are not part of the loader data. They load in the browser and appear when the store's `blog.allow_likes_and_comments` setting is on. `article.description` is HTML and the stock page renders it as such.

[Hook slots](https://docs.salla.dev/twilight/react/guide/hooks-and-slots): `blog:single.start`, `blog:single.end`, `blog:single.related.start`, `blog:single.related.end`.

## Head and SEO [#head-and-seo]

`BlogSingle.head` uses `article.meta.title` (else `article.name`), `meta.description` (else the article text) and `meta.canonical` (else `article.url`). It also sets an Open Graph title and image (`meta.og_image`, else `article.image`) and hreflang links. See [SEO and head](https://docs.salla.dev/twilight/react/guide/seo-and-head).

## Edge cases [#edge-cases]

* **Unknown or deleted article id.** The loader throws a not-found error, so the customer sees the [not-found page](https://docs.salla.dev/twilight/react/routes/not-found) instead of a server error.
* **Wrong `$slug`.** It is not checked and there is no redirect: `/blog/anything/a-482` shows article 482. The canonical link points at `article.url`.
* **Non-numeric id.** `a-abc` is sent to the API as is, so it fails like an unknown id.
* **Slow API.** Requests time out after 8 seconds and the error page shows.
* **No related articles.** The sidebar block is hidden.
* **Locale.** The `locale` you pass to the loader is not used; the article language follows the request.

## See also [#see-also]

* [Blog](https://docs.salla.dev/twilight/react/routes/blog), [Blog tag](https://docs.salla.dev/twilight/react/routes/blog-tag), [Blog author](https://docs.salla.dev/twilight/react/routes/blog-author)
* [Fork a built-in route](https://docs.salla.dev/twilight/react/recipes/fork-a-built-in-route)
* [Routes overview](https://docs.salla.dev/twilight/react/routes)
