The component model is a way of assembling an interface from reusable parts: one button, one heading, one card block are described once and dropped into hundreds of places. This trait cuts both ways. A rule set in a shared component instantly spreads across the whole product - but so does a mistake. A single inaccessible Button, or a single Link that is not really a link, breaks not one page but the entire store.
Hence the first principle: accessibility is built into the primitive, not bolted on at each call site. A primitive is the base component everything else is built on top of. An icon button with no text is invisible to a screen reader, so it is wrapped so that a name is mandatory: the aria-label attribute carries the accessible name, and the icon itself is marked aria-hidden so the reader does not announce it twice. Now no developer can physically insert a nameless icon - the types demand a label.
The same single-source principle governs metadata and routes. A component must not guess its own URL or decide on its own which language versions a page has. Otherwise one template sets canonical with a trailing slash, another without, a third forgets hreflang, and the search engine sees a mess of contradictory signals. The description of a route must be single and server-side, and every link is derived from it.
In Next.js this is the job of generateMetadata - it runs on the server and returns the page metadata before the HTML is sent. Inside the alternates object the canonical field points at the page itself (a self-canonical), and languages lists the alternate language versions with their hreflang codes, including x-default for a reader whose language matched none of them. Reciprocity is mandatory: if the Arabic version links to the English one, the English one must link back to the Arabic - otherwise the cluster falls apart. In the current App Router params arrives as a promise, so it is awaited.
Routing is built around the locale too. A [locale] segment at the start of the path gives every language its own prefix - /en, /ru, /ar - and its own indexable page. The next-intl library centralizes the configuration through defineRouting (the list of locales and the default one) and createNavigation (type-safe Link and navigation), while a setRequestLocale call in every layout and page lets the page render statically instead of falling back to dynamic. The folders echo the same idea: translation messages, the i18n layer, the metadata layer and the accessibility layer live apart, so each rule lives in one place.
app/
[locale]/
layout.tsx # lang, dir, providers
page.tsx
products/
[slug]/page.tsx
sitemap.ts
robots.ts
messages/
en.json
ru.json
lib/
i18n/
metadata/
accessibility/Structured data belongs in the same server layer. schema.org markup in JSON-LD - an object with the Product type, a name, a price and availability - is best rendered right in a server component as a script tag with type application/ld+json, using the same product data that the title and the price on the page are built from. That way the label a human sees and the data a robot reads come from one source and never drift apart.
A failure in a component architecture is always mass-produced. A client component assembled a link to another locale on its own, got the prefix wrong - and broke hreflang across every product card at once. Or one IconButton was left without a label, and every Buy button in the whole store stayed just button to the reader. The cure is not walking the pages before release but the fact that the name, the URL and the metadata are set once in the primitive and in the server route description - then the fix, too, spreads across the whole product at once.
export function IconButton({
label,
children,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & {
label: string;
}) {
return (
<button type="button" aria-label={label} {...props}>
<span aria-hidden="true">{children}</span>
</button>
);
}export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string; slug: string }>;
}): Promise<Metadata> {
const { locale, slug } = await params;
const path = `/products/${slug}`;
return {
alternates: {
canonical: `https://shop.example/${locale}${path}`,
languages: {
en: `https://shop.example/en${path}`,
ru: `https://shop.example/ru${path}`,
ar: `https://shop.example/ar${path}`,
"x-default": `https://shop.example/en${path}`,
},
},
};
}