Routing
Every directory under app/ is a URL segment; every page.tsx is a renderable page.
Pages
app/page.tsx → /
import { Typography } from "antd";
export default function HomePage() {
return <Typography.Title>Welcome to Manh</Typography.Title>;
}app/orders/page.tsx → /orders
import { ProTable } from "@ant-design/pro-components";
import { useApiClient } from "app/api";
export default function OrdersPage() {
const api = useApiClient();
return (
<ProTable
rowKey="id"
request={async (p) => {
const { data, total } = await api.orders.list({ page: p.current, pageSize: p.pageSize });
return { data, total, success: true };
}}
columns={[
{ title: "ID", dataIndex: "id" },
{ title: "Customer", dataIndex: "customerName" },
{ title: "Total", dataIndex: "total", valueType: "money" },
]}
/>
);
}Layouts
Layouts wrap all pages in their directory scope and nest automatically: parent layout → child layout → page.
app/layout.tsx — root layout
import { Outlet } from "react-router-dom";
import { ProLayout } from "@ant-design/pro-components";
export default function RootLayout() {
return (
<ProLayout title="Manh Admin">
<Outlet />
</ProLayout>
);
}app/orders/layout.tsx — nested layout
import { Outlet } from "react-router-dom";
import { PageContainer } from "@ant-design/pro-components";
export default function OrdersLayout() {
return (
<PageContainer title="Orders">
<Outlet />
</PageContainer>
);
}Every
layout.tsxmust render<Outlet />, otherwise child pages will not appear.
Dynamic Routes
Wrap a segment in brackets: app/orders/[id]/page.tsx → /orders/:id. Append :type for auto-validation ([id:uuid], [slug:slug], [n:number]) — invalid URLs render 404 before the page mounts.
import { useParams } from "react-router-dom";
import { Descriptions } from "antd";
import { useRequest } from "app/hooks";
import { useApiClient } from "app/api";
export default function OrderDetailPage() {
const { id } = useParams();
const api = useApiClient();
const { data } = useRequest(() => api.orders.detail(id!), { refreshDeps: [id] });
return (
<Descriptions title={`Order ${id}`} bordered>
<Descriptions.Item label="Customer">{data?.customerName}</Descriptions.Item>
<Descriptions.Item label="Total">{data?.total}</Descriptions.Item>
</Descriptions>
);
}Route Groups
Parentheses around a folder name create a route group — a segment that organizes files without appearing in the URL.
app/(admin)/users/page.tsx → /users (NOT /admin/users)Use route groups to share a layout or middleware among unrelated routes.
Page meta (SEO + link preview)
Export a static meta object from a page or the root layout. The deploy writes it into that route’s HTML <head>, so search engines and link previews see the title, description and image without running the app.
// app/layout.tsx
export const meta = { siteName: "Spa Hoa Sen", description: "Spa thảo dược ở Huế", lang: "vi" };
// app/pricing/page.tsx
export const meta = {
title: "Bảng giá", // → "Bảng giá · Spa Hoa Sen"
description: "Giá gội, massage, liệu trình",
image: "https://storage.manh.run/…/public/pricing.jpg",
};
// app/admin/page.tsx
export const meta = { noindex: true };Values must be plain strings or true/false — the deploy reads the source, it never runs it. image must be an absolute https:// URL (upload under public/ with app/std/blob). Pages with URL params use the home page’s head for now.