PageSingle
A merchant's content page, such as About us or Terms: loads it by id and shows its HTML and comments.
import { PageSingle, PageSingleLazy, pageSingleLoader, PageSingleProps, StaticPage } from '@salla.sa/twilight-theme-engine/routes/page';In plain words
Merchants write simple pages in the Salla dashboard: About us, Shipping policy, Terms. Their addresses end in page-<id>. PageSingle.loader fetches one by id, and the component shows its title and the HTML the merchant wrote.
Naming trap: PageSingle is the route module. The component on its own is exported as PageSingleLazy.
The store's footer links to these pages: follow one from the home page on Built-in pages.
Signature
const PageSingle: {
readonly id: 'page-single';
readonly loader: (
ctx: { params: { id: string }; locale?: string },
extend?: (data: PageSingleProps, ctx: { params: { id: string } }) => Record<string, unknown> | Promise<Record<string, unknown>>
) => Promise<PageSingleProps>;
readonly head: (ctx: TwilightContext, data: PageSingleProps) => HeadDescriptor;
readonly Component: React.LazyExoticComponent<(props: PageSingleProps) => JSX.Element>;
};
const PageSingleLazy: typeof PageSingle.Component;
function pageSingleLoader({ params }: { params: { id: string } }): Promise<PageSingleProps>;
interface PageSingleProps { page: StaticPage }
interface StaticPage extends Page {
id?: number;
content?: string; // the merchant's HTML
url?: string;
metadata?: { title?: string; description?: string };
created_at?: { published_time?: string; modified_time?: string };
}Try it live
import { createFileRoute } from '@tanstack/react-router';
import { PageSingle } from '@salla.sa/twilight-theme-engine/routes/page';
import type { PageSingleProps } from '@salla.sa/twilight-theme-engine/routes/page';
import { withHead } from '@salla.sa/twilight-theme-engine/tanstack';
// app/routes/$slug.page-$id.tsx, as the plugin generates it (without its first line)
export const Route = createFileRoute('/{-$locale}/$slug/page-{$id}')({
// 1. loader: fetch the page, return the Component's props
loader: ({ params }): Promise<PageSingleProps> =>
PageSingle.loader({ params: { id: params.id }, locale: params.locale }),
// 2. head: title, description, canonical and Open Graph tags from the same data
head: withHead(PageSingle),
// 3. component: draw the page from the loader data
component: PageSingleComponent,
});
function PageSingleComponent() {
const data: PageSingleProps = Route.useLoaderData();
return <PageSingle.Component {...data} />;
}
Example
import { createFileRoute } from '@tanstack/react-router';
import { PageSingle } from '@salla.sa/twilight-theme-engine/routes/page';
import type { PageSingleProps } from '@salla.sa/twilight-theme-engine/routes/page';
import { withHead } from '@salla.sa/twilight-theme-engine/tanstack';
export const Route = createFileRoute('/{-$locale}/$slug/page-{$id}')({
loader: ({ params }): Promise<PageSingleProps> =>
PageSingle.loader({ params: { id: params.id }, locale: params.locale }),
head: withHead(PageSingle),
component: PageSingleComponent,
});
function PageSingleComponent() {
const data: PageSingleProps = Route.useLoaderData();
return <PageSingle.Component {...data} />;
}
How it behaves
URL:
/{-$locale}/$slug/page-{$id}; only the id is read. The generated route sets nopendingComponent, so client navigations show the router's defaultPageSkeleton.pageSingleLoadercallspage.findOrThrow(id): any failure shows the 404 page. It maps the API'snametopage.titleand copiescontent,url,metadataandcreated_at.head: the title, a description made from the content with HTML tags removed,canonicalfrompage.url, Open Graph typearticlewith the published and modified times, and hreflang alternates.The component replaces
with spaces and renderscontentas HTML. Whenpage.idis set it adds Salla's comments web component for the page.Lazy: outside a route, wrap
PageSingleLazyin<Suspense>.
Gotchas
headputs the whole content into the description: there is no length cap (Product's is 160 characters) and entities stay in it. Trim it withwithHead(PageSingle, (result) => ({ ...result, description: result.description?.slice(0, 160) })).metadata.titleandmetadata.description, the SEO fields a merchant can fill for the page, are loaded but not used byhead.The content is rendered with
dangerouslySetInnerHTMLand the engine does not sanitise it: it is trusted as the merchant's own HTML. Scripts inside it do not run.
Related
Source and docs
- Engine source:
packages/theme-engine/src/routes/page/index.tsx