Skip to content

Latest commit

 

History

History
338 lines (261 loc) · 10.7 KB

File metadata and controls

338 lines (261 loc) · 10.7 KB
title File-Based Routing
description Define pages via directory structure using the Vite plugin instead of explicit module configuration

File-Based Routing

File-based routing allows you to define pages by simply placing components in a directory structure, eliminating the need for explicit defineModule() and defineResource() calls.

Overview

Instead of manually assembling module/resource hierarchies, you define pages as files in a pages/ directory. The path is automatically derived from the directory structure.

src/pages/
├── page.tsx                  # / (root path)
├── purchasing/
│   ├── page.tsx              # /purchasing
│   └── orders/
│       ├── page.tsx          # /purchasing/orders
│       └── [id]/
│           └── page.tsx      # /purchasing/orders/:id
└── (admin)/                  # Grouping (not included in path)
    └── settings/
        └── page.tsx          # /settings

Directories without a page.tsx still contribute a path segment — navigating to that path returns a 404 while their children remain accessible:

src/pages/
├── admin/
│   ├── users/
│   │   └── page.tsx   # /admin/users → renders page
│   └── roles/
│       └── page.tsx   # /admin/roles → renders page
│   # no page.tsx for /admin → 404

Guards defined on a page.tsx apply to that exact path. Directories without page.tsx do not support guards — add guards to the individual child pages instead.

Setup

1. Configure Vite Plugin

// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { appShellRoutes } from "@tailor-platform/app-shell-vite-plugin";

export default defineConfig({
  plugins: [
    react(),
    appShellRoutes({
      entrypoint: "src/App.tsx", // recommended
    }),
  ],
});

Plugin Options

Option Type Default Description
pagesDir string "src/pages" Directory containing page files
logLevel "info" | "debug" | "off" "info" Log level for plugin output
entrypoint string undefined File that renders the AppShell component (relative to project root). See note below.
generateTypedRoutes boolean | { output: string } false Generate a TypeScript file with type-safe route definitions

entrypoint (recommended): When specified, only imports from this file are intercepted and replaced with the pages-injected AppShell. All other files — including page components — import directly from the real package. Without this option, all user-code imports of @tailor-platform/app-shell are intercepted, which can cause TDZ (Temporal Dead Zone) errors in page components that also import from the package.

2. Use AppShell (No Configuration Needed)

// App.tsx
import { AppShell, SidebarLayout, DefaultSidebar } from "@tailor-platform/app-shell";

const App = () => {
  return (
    <AppShell title="My App">
      <SidebarLayout sidebar={<DefaultSidebar />} />
    </AppShell>
  );
};

Pages are automatically discovered and injected—no modules prop required!

Page Components

Minimal Example

The simplest page is just a default-exported component:

// src/pages/about/page.tsx
export default () => <div>About</div>;

Full Example

Use appShellPageProps static field to configure page metadata and guards:

// src/pages/dashboard/page.tsx
import type { AppShellPageProps } from "@tailor-platform/app-shell";
import { authGuard } from "../guards";
import { DashboardIcon } from "../icons";

const DashboardPage = () => {
  return <div>Dashboard Content</div>;
};

DashboardPage.appShellPageProps = {
  meta: {
    title: "Dashboard",
    icon: <DashboardIcon />,
  },
  guards: [authGuard],
} satisfies AppShellPageProps;

export default DashboardPage;

AppShellPageProps Type

type AppShellPageProps = {
  meta?: {
    title?: LocalizedString;
    icon?: ReactNode;
    breadcrumbTitle?: string | ((segment: string) => string);
  };
  guards?: Guard[];
  loader?: LoaderHandler;
};

Path Conventions

Directory Name Converts To Description
orders orders Static segment
[id] :id Dynamic parameter
[...slug] *slug Catch-all — matches zero or more path segments
(group) (excluded) Grouping only (not in path)
_lib (ignored) Not routed (for shared logic)

Examples

src/pages/
├── users/
│   ├── page.tsx              # /users
│   └── [userId]/
│       └── page.tsx          # /users/:userId
├── (marketing)/
│   ├── campaigns/
│   │   └── page.tsx          # /campaigns (not /marketing/campaigns)
│   └── analytics/
│       └── page.tsx          # /analytics
└── _utils/
    └── helpers.ts            # Not routed (shared utilities)

Guards

Guards are not automatically inherited from parent pages. Each page must explicitly define its own guards:

// /dashboard/page.tsx
DashboardPage.appShellPageProps = {
  guards: [authGuard],
} satisfies AppShellPageProps;

// /dashboard/admin/page.tsx — must include authGuard explicitly
AdminPage.appShellPageProps = {
  guards: [authGuard, adminGuard],
} satisfies AppShellPageProps;

To share common guards across pages, compose them from a shared module:

// src/guards.ts
export const requireAuth = [authGuard];
export const requireAdmin = [authGuard, adminGuard];

// src/pages/dashboard/orders/page.tsx
import { requireAuth } from "@/guards";

OrdersPage.appShellPageProps = {
  guards: [...requireAuth],
} satisfies AppShellPageProps;

Typed Routes

Enable generateTypedRoutes in the Vite plugin to generate type-safe route helpers:

// vite.config.ts
appShellRoutes({
  generateTypedRoutes: true,
});

This generates src/routes.generated.ts with a paths helper:

import { paths } from './routes.generated';
import { Link } from '@tailor-platform/app-shell';

// Static routes
<Link to={paths.for("/dashboard")}>Dashboard</Link>

// Dynamic routes - params are type-checked
<Link to={paths.for("/dashboard/orders/:id", { id: orderId })}>Order</Link>

// TypeScript catches errors:
paths.for("/dashboard/orders/:id"); // Error: missing 'id'
paths.for("/invalid");              // Error: route doesn't exist

Comparison with Legacy API

Before: Explicit Hierarchy Assembly

const orderDetailResource = defineResource({ path: ":id", component: OrderDetail });
const ordersResource = defineResource({
  path: "orders",
  component: OrdersList,
  subResources: [orderDetailResource],
});
const purchasingModule = defineModule({
  path: "purchasing",
  resources: [ordersResource],
});

<AppShell modules={[purchasingModule]} />;

After: File-Based Pages

// src/pages/purchasing/orders/[id]/page.tsx
const OrderDetailPage = () => <div>Order Detail</div>;
OrderDetailPage.appShellPageProps = {
  meta: { title: "Order Detail" },
} satisfies AppShellPageProps;
export default OrderDetailPage;
// App.tsx - No configuration needed
<AppShell title="My App">
  <SidebarLayout sidebar={<DefaultSidebar />} />
</AppShell>

Concept Mapping

Legacy API File-Based Notes
Module First-level directory
Resource Directory structure
defineModule() Not needed
defineResource() Not needed
path property Directory name Auto-derived
component property page.tsx default export File convention
meta property Page.appShellPageProps.meta Static field
guards property Page.appShellPageProps.guards Static field (no inheritance)
subResources property Subdirectories Auto-derived

Compatibility

File-based pages and explicit modules prop are mutually exclusive.

Usage Patterns

// ✅ Pattern 1: File-based pages with plugin (recommended)
// vite.config.ts has plugin configured
<AppShell title="My App">
  <SidebarLayout sidebar={<DefaultSidebar />} />
</AppShell>

// ✅ Pattern 2: Explicit modules without plugin (legacy)
<AppShell modules={[myModule]} title="My App">
  <SidebarLayout sidebar={<DefaultSidebar />} />
</AppShell>

// ✅ Pattern 3: Plugin enabled + modules prop (modules takes precedence)
// Even with plugin enabled, modules prop is used when provided
<AppShell modules={[myModule]} title="My App">
  <SidebarLayout sidebar={<DefaultSidebar />} />
</AppShell>

// ❌ Pattern 4: No plugin + no modules (runtime error)
<AppShell title="My App">
  {/* → Runtime error: No routes configured */}
</AppShell>

Migration Guide

To migrate from defineModule/defineResource to file-based routing:

  1. Add Vite Plugin

    // vite.config.ts
    import { appShellRoutes } from "@tailor-platform/app-shell-vite-plugin";
    
    export default defineConfig({
      plugins: [react(), appShellRoutes({ entrypoint: "src/App.tsx" })],
    });
  2. Create pages directory structure

    • Map each module to a top-level directory
    • Map each resource to a subdirectory with page.tsx
    • Use [param] for dynamic segments
  3. Move component and metadata

    // Before: defineResource({ path: "orders", component: Orders, meta: {...} })
    
    // After: src/pages/orders/page.tsx
    const OrdersPage = () => <Orders />;
    OrdersPage.appShellPageProps = { meta: {...} };
    export default OrdersPage;
  4. Remove modules prop from <AppShell> once all pages are migrated