File-Based Routing

File-Based Routing#

Overview#

Duxt uses file-based routing where the file structure in your pages/ directories automatically defines your application routes.

No manual route configuration required - just create a file and the route exists.

Basic Routes#

Files map directly to URL paths:

FileRoute
lib/posts/pages/index.dart/posts
lib/posts/pages/create.dart/posts/create
lib/users/pages/index.dart/users
lib/users/pages/settings.dart/users/settings
// lib/posts/pages/index.dart → /posts
import 'package:jaspr/jaspr.dart';
import 'package:jaspr/dom.dart';

class PostsPage extends StatelessComponent {
  const PostsPage({super.key});

  @override
  Component build(BuildContext context) {
    return div([
      h1([text('All Posts')]),
    ]);
  }
}

Dynamic Routes#

Use _param_ syntax for dynamic segments (Dart-compatible alternative to Next.js [param]):

FileRoute
lib/posts/pages/_id_.dart/posts/:id
lib/posts/pages/_id_/edit.dart/posts/:id/edit
lib/users/pages/_userId_/posts/_postId_.dart /users/:userId/posts/:postId
// lib/posts/pages/_id_.dart → /posts/:id
import 'package:jaspr/jaspr.dart';

class PostDetailPage extends StatelessComponent {
  final String id;

  const PostDetailPage({required this.id, super.key});

  @override
  Component build(BuildContext context) {
    return div([
      h1([text('Post $id')]),
    ]);
  }
}

// The route builder passes params automatically:
// Route(
//   path: '/posts/:id',
//   builder: (context, state) => PostDetailPage(id: state.params['id']!),
// )

Why _param_ instead of [param]?#

Dart doesn't allow [ or ] in filenames. The _param_ convention is Dart-compatible while providing the same functionality:

FrameworkDynamic Route Syntax
Next.js[slug].tsx
Nuxt[slug].vue
SvelteKit[slug]/+page.svelte
Duxt_slug_.dart

Nested Routes#

Create nested directories for nested routes. You can nest as deep as you want:

lib/company/pages/
├── index.dart              → /company
├── about.dart              → /company/about
└── team/
    ├── index.dart          → /company/team
    └── engineering.dart    → /company/team/engineering

Deep Nesting Example#

FileRoute
lib/company/pages/index.dart/company
lib/company/pages/about.dart/company/about
lib/company/pages/team/index.dart/company/team
lib/company/pages/team/engineering.dart /company/team/engineering
lib/company/pages/team/design.dart/company/team/design

With Dynamic Parameters#

lib/blog/pages/
├── index.dart              → /blog
├── create.dart             → /blog/create
├── _slug_.dart             → /blog/:slug
└── _slug_/
    ├── edit.dart           → /blog/:slug/edit
    ├── comments.dart       → /blog/:slug/comments
    └── comments/
        └── _commentId_.dart → /blog/:slug/comments/:commentId

Route Parameters#

Route parameters are passed to your component via the route builder. Access them through RouteState:

// In app.dart or routes configuration
Route(
  path: '/posts/:id',
  builder: (context, state) {
    // Access path params
    final id = state.params['id']!;
    // Access query params (?search=hello)
    final search = state.queryParams['search'];

    return PostDetailPage(id: id, search: search);
  },
)

Your component receives typed parameters:

class PostDetailPage extends StatelessComponent {
  final String id;
  final String? search;

  const PostDetailPage({required this.id, this.search, super.key});

  @override
  Component build(BuildContext context) {
    return div([
      text('Viewing post $id'),
      if (search != null) text('Searching: $search'),
    ]);
  }
}

Auto-detection from Constructor#

Duxt automatically detects required parameters from your component constructor and adds them as dynamic route segments:

// lib/blog/pages/post.dart
class BlogPostPage extends StatelessComponent {
  final String slug;  // Required param detected!

  const BlogPostPage({required this.slug, super.key});
  // ...
}
// Generates route: /blog/post/:slug

This means you can also just name your file normally and let Duxt infer the dynamic segment from your constructor.

Navigate between routes programmatically using context extensions:

import 'package:jaspr/jaspr.dart';
import 'package:duxt/duxt.dart';

class MyPage extends StatelessComponent {
  const MyPage({super.key});

  @override
  Component build(BuildContext context) {
    return div([
      button(
        events: {'click': (_) => context.push('/posts')},
        [text('View Posts')],
      ),
      button(
        events: {'click': (_) => context.push('/posts/123')},
        [text('View Post 123')],
      ),
      button(
        events: {'click': (_) => context.back()},
        [text('Go Back')],
      ),
      button(
        events: {'click': (_) => context.replace('/login')},
        [text('Replace (no history)')],
      ),
    ]);
  }
}

Available Navigation Methods#

// Navigate to a path (adds to history)
context.push('/posts');

// Navigate with extra data
context.push('/posts/1', extra: {'from': 'list'});

// Replace current route (no history entry)
context.replace('/login');

// Go back in browser history
context.back();

// Navigate by named route
context.pushNamed('post-detail', params: {'id': '123'});

// Preload a route for faster navigation
context.preload('/posts');

Using useRouter#

You can also get the router directly:

final router = useRouter(context);
router.push('/posts');
router.back();

Use jaspr_router's Link component or standard anchor tags:

import 'package:jaspr_router/jaspr_router.dart';

// Jaspr Router Link (client-side navigation)
Link(to: '/posts', child: text('View Posts'))

// Standard HTML anchor
a(href: '/posts', [text('View Posts')])

Route Generation#

Routes are automatically generated when you run:

duxt dev
duxt build

Namespace Routes#

Modules inside namespaces are routed with the namespace as a URL prefix:

FileRoute
lib/admin/posts/pages/index.dart/admin/posts
lib/admin/posts/pages/_id_.dart/admin/posts/:id
lib/admin/users/pages/index.dart/admin/users

Theme Namespace#

The theme/ namespace is special - it strips the prefix, routing directly to root paths:

FileRoute
lib/theme/home/pages/index.dart/
lib/theme/blog/pages/index.dart/blog
lib/theme/blog/pages/_slug_.dart/blog/:slug
lib/theme/about/pages/index.dart/about

Namespace Layouts#

If a namespace has a layouts/default.dart file, all routes in that namespace are automatically wrapped with the layout:

lib/admin/
  layouts/default.dart        AdminLayout
  posts/pages/index.dart      /admin/posts (wrapped in AdminLayout)
  users/pages/index.dart      /admin/users (wrapped in AdminLayout)

See Namespaces for full documentation.

Content Routes#

In addition to Dart pages, Duxt also routes markdown files from content/ directories:

SourceRoute
lib/docs/pages/index.dart/docs (Dart)
lib/docs/content/cli.md/docs/cli (markdown)
lib/docs/content/guides/index.md/docs/guides (markdown)

Content routes are generated alongside page routes:

lib/docs/
├── pages/
│   └── index.dart          → /docs
└── content/
    ├── cli.md              → /docs/cli
    └── guides/
        └── index.md        → /docs/guides

This enables a unified routing system where both Dart components and markdown content coexist in the same module. See Content System for details on layouts and configuration.